Compare commits

...

2 commits

Author SHA1 Message Date
7d85773ae3 BRouter: fix routed-line re-route loop; verify planning + gap-fill on desktop
doBrouter() ended with changed(), which nulled routeGeom and re-queued
doBrouter — an endless re-route loop where the road line was never drawn
(editor kept the straight line, distance unchanged). Draw directly instead
(updatePolyline/updateStats).

Verified end-to-end in the UI on a virtual display (Xvfb) against the
RouteServer: editor 'По дорогам' turns a 1.69km straight line into a 2.25km
road-following route; the gap-fill '🧩' overlay draws a shortest-road cyan
dashed line across a track's GPS dropouts. Docs (README/PROJECT_MAP/plan/log)
updated to match; router.js/poi.js now listed in PROJECT_MAP.
2026-09-09 18:18:35 +03:00
b8fed04f95 BRouter: gap-fill along roads + release hardening
Stage 4 — gap fill: GPS.fillGaps(points) routes each GPS dropout (dt>gap
threshold, chord>60m) with the shortest road path and splices the routed points
in (flagged estimated), falling back to the chord when the road is >3x the chord
(graph detoured). A 🧩 button on the track detail draws the road-filled path as
a cyan dashed overlay — non-destructive (stats recompute over filled points is a
follow-up). Also fixes doBrouter: parseRouteGPX returns {name,points}, not an
array (planning would have kept the straight line).

Release hardening: network_security_config.xml permits cleartext only to the
mirror/RouteServer hosts (base off, debug-overrides on) — wired via
android:networkSecurityConfig; proguard keeps btools.** so release minification
doesn't strip the embedded engine.
2026-09-09 17:43:56 +03:00
8 changed files with 120 additions and 9 deletions

View file

@ -76,13 +76,16 @@ app data dir; профиль trekking (идея 2) и shortest (идея 1); л
- Фолбэк на прямую линию, если движок/сегмент недоступны.
**Success:** тап точек даёт маршрут по дорогам с профилем высоты; сохраняется как маршрут.
**Tests:** спланировать знакомый маршрут офлайн, сверить с онлайн-BRouter (та же геометрия).
**Status:** СДЕЛАНО (компилируется; backend роутинга проверен). В `showRouteEditor` кнопка-тумблер
**Status:** СДЕЛАНО и ПРОВЕРЕНО В UI (десктоп, Xvfb, 2026-09-09). В `showRouteEditor` кнопка-тумблер
«🚲 По дорогам»: waypoints остаются редактируемыми маркерами, линия между ними прокладывается
BRouter'ом (`brouterMode`/`routeGeom`, debounce 450мс), сохраняется дорожная геометрия. Роутинг —
`BRouter.route` (router.js): Android → встроенный движок, иначе (десктоп/дом) → PC-RouteServer по
HTTP (`http_request`, мимо CSP; хосты LAN 192.168.1.18:17777 / WAN 77.66.192.45:17777). Проверено:
RouteServer на 3 точки trekking отдал 83 trkpt (3594м); `parseRouteGPX` читает trkpt. ОСТАЛОСЬ:
сквозной прогон в UI (тап точек → дорожная линия) — на десктопе/телефоне.
HTTP (`http_request`, мимо CSP; хосты LAN 192.168.1.18:17777 / WAN 77.66.192.45:17777).
Сквозной прогон в редакторе: 3 точки по Новочеркасску → прямая 1.69км сменилась дорожной линией
2.25км по улицам (Буденновская → кольцо → Ермака). БАГ, найденный и исправленный при этой проверке:
`doBrouter` в конце звал `changed()`, который обнулял `routeGeom` и заново ставил `doBrouter` в
очередь — бесконечный цикл, дорожная линия никогда не рисовалась. Заменено на прямой
`updatePolyline(); updateStats();`. ОСТАЛОСЬ: прогон на телефоне (встроенный движок).
## Stage 4 — Идея 1: заполнение GPS-пропусков по дороге
**Goal:** GPS-разрыв заполняется дорожным путём (shortest), с фолбэком на хорду.
@ -94,7 +97,21 @@ app data dir; профиль trekking (идея 2) и shortest (идея 1); л
**Success:** записанный трек с дропаутом GPS показывает дорожный фолловинг вместо хорды, кроме
случая «дорога в разы длиннее».
**Tests:** трек с известным дропаутом на дороге → заполнение по дороге; дропаут на поле → хорда.
**Status:** not started
**Status:** СДЕЛАНО (v1, non-destructive) и ПРОВЕРЕНО В UI (десктоп, Xvfb, 2026-09-09).
`GPS.fillGaps(points)` (gps.js): для каждого гэпа (dt>gapThreshold и хорда>60м) прокладывает
`shortest`, вставляет дорожные точки (флаг `estimated`); фолбэк на хорду если дорога > 3× хорды.
Кнопка «🧩» на детали трека — рисует дорожно-заполненный путь голубым пунктиром оверлеем (данные
не меняет). Прогон: трек 2026-06-22 (session 1782115144011, 16.24км) с двумя дропаутами GPS
(~2км хорды) — «🧩» проложил голубой пунктир по улицам между красно-зелёными концами гэпов.
Трек без гэпов (21.81км 2026-07-07) — тумблер включается, оверлей пуст (нечего заполнять), без
ошибок. Пересчёт статистики по заполненным точкам — следующий шаг (сейчас только визуализация).
## Release-хардненинг (item 4, сделано)
- `network_security_config.xml` (res/xml): cleartext OFF глобально в release, разрешён только хостам
зеркала/RouteServer (192.168.1.1 / 192.168.1.18 / 77.66.192.45); `debug-overrides` — всё в debug.
Подключён в AndroidManifest (`android:networkSecurityConfig`).
- `proguard-rules.pro`: `-keep class btools.** { *; }` + `-dontwarn btools.**` — минификация release
не вырежет встроенный движок.
## Stage 5 — Полировка (опционально)
**Goal:** удобство и корректность.

View file

@ -27,6 +27,8 @@
| `js/sync.js` | ~300 | Синхронизация между устройствами (LAN, PHP сервер на роутере) |
| `js/chzn.js` | ~330 | Честный знак (CRPT). Скан DataMatrix → карточка Нацкаталога → префилл КБЖУ. `ChZ`: сессия в localStorage (`chzn_session`), `importFromText`, `refresh`/`ensureSid` (sid 48ч, refreshToken ~год), `authedPost/Get` (userId+sid в query, авто-refresh на 401), `check`, `nutritionInfo` (витамины, часто 204), `normalizeCode` (реконструкция GS 0x1D перед AI 93), `parseCard` (группа «Энергетическая ценность…», имена атрибутов разнятся у производителей), `resolve`. `/authorize/anonymous` закрыт Play Integrity — свою сессию не завести, только импорт из приложения (`tools/chzn_export.sh`) |
| `js/tilecache.js` | 178 | Оффлайн кэш тайлов карты (IndexedDB) |
| `js/router.js` | ~80 | Оффлайн-роутинг BRouter. `BRouter.route(points,[profile])`: Android → встроенный движок (`invoke("brouter_route")`), иначе/при ошибке → PC-RouteServer по HTTP (`http_request`, мимо CSP; LAN 192.168.1.18:17777 / WAN 77.66.192.45:17777). Профили `trekking` (планирование) / `shortest` (гэпы). Докачка сегментов `.rd5`: `downloadTile/downloadStatus/listTiles/downloadTileAndWait` |
| `js/poi.js` | ~200 | POI-оверлей на картах: круглые контрастные бейджи (эмодзи в круге, не пины), фильтр категорий (`POI.attach(map)`, данные `data/pois.json`, localStorage `baldav_poi_cats/shown`) |
| `js/bmr.js` | ~70 | BMR (Mifflin-St Jeor), РСП из БД (33 нутриента), полураспад |
| `css/styles.css` | ~2100 | Темная тема, мобильный дизайн, HR зоны, ландшафтный режим |
| `index.html` | 28 | Точка входа, порядок загрузки скриптов |

View file

@ -17,7 +17,9 @@
- **Ручные маршруты** — рисование трека на карте тапами, elevation из прошлых треков или open-meteo API.
- **Сохранённые маршруты** — импорт GPX-маршрутов, нарисованных заранее (например, в конструкторе Яндекс.Карт: формат `<rtept>`), выбор активного, наложение цветной линией поверх карты во время записи И на экране просмотра прошлого трека — для визуальной навигации. Управление на отдельной странице (импорт/переименование/цвет/удаление, один активный за раз), переключение линии кнопкой-тогглом. Цвета «проезженных дорог» настраиваются раздельно для вело/пеших/бега.
- **Высоты маршрута (рельеф до поездки)** — экран предпросмотра маршрута (тап по названию): профиль высоты (крутые участки красным), длина/набор/сброс/макс. уклон, тап по точке — высота и уклон, разворот карты на весь экран. Топографическая обводка линии с тогглом **уклон/высота**: уклон дискретными полосами по 1% (зелёный→жёлтый→красный→коричневый→чёрный, ≥10% чёрный) или гипсометрия по высоте (зелёный→жёлтый→коричневый). Обводка и тоггл доступны и в оверлее на карте записанного трека и во время записи; маршрут рисуется ПОД треком. Высоты (`<ele>` из GPX или онлайн) тянутся по цепочке: **GPXZ** (Copernicus GLO-30, точнее для уклонов; ключ в GPS-настройках, хранится только на устройстве) → open-elevation → open-meteo → OpenTopoData (без ключа). Калории пересчитываются по сглаженным точкам; для уклона высоты сглаживаются (гасит вертикальный шум SRTM).
- **Редактор маршрутов** — создание («Новый маршрут») и правка сохранённого маршрута прямо в приложении: тап по карте — добавить точку в конец, тап по линии — вставить точку в это место, тащить точку — двигать, тап по точке — удалить. Высоты подтягиваются онлайн после отрисовки (из предпросмотра).
- **Редактор маршрутов** — создание («Новый маршрут») и правка сохранённого маршрута прямо в приложении: тап по карте — добавить точку в конец, тап по линии — вставить точку в это место, тащить точку — двигать, тап по точке — удалить. Высоты подтягиваются онлайн после отрисовки (из предпросмотра). Тумблер **«🚲 По дорогам»** прокладывает линию между опорными точками по реальным дорогам (BRouter, профиль trekking) и сохраняет дорожную геометрию; выключен — прямые отрезки.
- **Оффлайн-роутинг (BRouter)** — движок BRouter встроен в приложение (Android): маршруты строятся по дорогам без сети, сегменты `.rd5` региона качаются с домашнего зеркала прямо в приложении (экран дорог → «Сегменты»). Используется в редакторе маршрутов («По дорогам») и для заполнения GPS-пропусков. На десктопе/дома движка нет — те же вызовы уходят на домашний BRouter-RouteServer по HTTP.
- **Заполнение GPS-пропусков по дороге** — на детали трека тумблер **«🧩»** прокладывает дорожный путь (BRouter, профиль shortest) через каждый GPS-дропаут (разрыв по времени + реальная хорда) и рисует его голубым пунктиром поверх трека; если дорога в разы длиннее хорды — оставляет хорду. Оверлей, данные трека не меняет.
- **Де-джиттер трека** — сглаживание дрожащего GPS-трека методом Савицкого-Голея (настраиваемое окно/число итераций, живое превью на карте детали трека). Хранится обратимо: оригинал и сглаженный вариант лежат раздельно, калории пересчитываются по сглаженным точкам, откат к оригиналу в один тап. Точность каждого фикса (`accuracy_m`) пишется в БД: при пересчёте трека точки хуже порога «Макс. погрешность GPS» исключаются из расчёта и линии (обратимо — точки в БД остаются; поднять порог = вернуть). Работает во всех пересчётах (кнопка «Пересчитать», «Пересчитать всё», после де-джиттера); импорт/старьё без точности и dead-reckoning заполнения не трогает.
- **POI на карте** — точки интереса (кафе, магазины, вода, туалеты, банки/ATM, остановки, укрытия, смотровые и т.д.) круглыми контрастными значками (эмодзи в кружке, читаемо на светлой и тёмной карте). Кнопка `📍` с панелью-фильтром категорий; рендер по видимой области от зума 13. Данные оффлайн (`data/pois.json`, ~100 км вокруг Новочеркасска, 5662 объекта из OSM/Overpass). На картах записи, деталей трека и дорог.
- **Тёмная карта** — тумблер `☾` на экранах записи, деталей трека и дорог: инвертирует ТОЛЬКО слой тайлов (`invert + hue-rotate 180° + brightness`, трек/маршрут/дороги остаются цветными), выбор запоминается. Работает одинаково на офлайн-тайлах и онлайне, без ключей/квот/перепаковки.

View file

@ -87,3 +87,25 @@ readTimeout 60с, до 30 попыток. Итог на устройстве: E4
нет OpenTopo/Yandex-vec). Транспортную инвертом — плохо в городе. Решение: OSM-стандарт + CSS
`invert(1) hue-rotate(180deg) brightness(1.5)` на .leaflet-tile-pane, тумблер ☾ (запись/детали/дороги),
без перепаковки. Проверено на устройстве.
## Дополнение 2026-09-09 (5): визуальная проверка Stage 3/4 на десктопе (Xvfb) + фикс бага
Задача: доделать Stage 3 (планирование по точкам) и Stage 4 (заполнение гэпов), прогнать тесты
на десктопе со скринами на виртуальном экране (Xvfb :99), не только по API.
Прогон в реальном UI (`npx tauri dev` на Xvfb :99, драйв через xdotool, скрины `import`):
- Stage 3 «🚲 По дорогам»: 3 опорные точки по Новочеркасску. Прямая линия 1.69км сменилась
дорожной 2.25км по улицам (Буденновская → кольцо → просп. Ермака). Роутинг — PC-RouteServer
(десктоп без встроенного движка), профиль trekking.
- Stage 4 «🧩»: трек 2026-06-22 (session 1782115144011, 16.24км, два дропаута ~2км хорды) —
оверлей проложил голубой пунктир по улицам между концами гэпов (профиль shortest). Трек без
фолловинг-гэпов (21.81км 2026-07-07) — тумблер включается, оверлей пуст, без ошибок.
Баг, найденный этой проверкой (был в 8e3c9fd): `doBrouter` в конце звал `changed()`, а тот
обнулял `routeGeom` и снова ставил `doBrouter` в очередь — бесконечный ре-роутинг, дорожная
линия НИКОГДА не рисовалась (в UI линия оставалась прямой, дистанция не менялась). Исправлено:
`doBrouter` рисует напрямую (`updatePolyline(); updateStats();`), `changed()` не трогает. Отдельным
коммитом (цикл виден в истории). Для проверки временно уводил дефолт-центр редактора на
Новочеркасск — откатил.
Проверка на телефоне встроенным движком (Stage 3 планирование, Stage 4 гэпы, докачка сегментов) —
когда тело подключат: `./build.sh install -d`.

View file

@ -1393,6 +1393,36 @@ GPS.parseRouteGPX = function(xmlString) {
return { name, points };
};
// Fill GPS dropouts along roads: for each gap (dt > gapThreshold and a real chord), route
// the SHORTEST road path between the pre/post-gap points and splice the routed points in
// (flagged estimated). Falls back to keeping the chord if the road path is > ~3x the chord
// (the graph detoured, e.g. across a field). Async (BRouter). Returns a NEW points array.
GPS.fillGaps = async function(points, opts) {
opts = opts || {};
const gapThr = opts.gapThresholdSec ?? (GPS.config.gapThresholdSec ?? 30);
const ratio = opts.maxRatio ?? 3;
const sorted = points.slice().sort((a, b) => new Date(a.ts) - new Date(b.ts));
const out = [];
for (let i = 0; i < sorted.length; i++) {
out.push(sorted[i]);
const a = sorted[i], b = sorted[i + 1];
if (!b) continue;
const dt = (new Date(b.ts) - new Date(a.ts)) / 1000;
const chord = GPS.distance(a.lat, a.lon, b.lat, b.lon);
if (dt <= gapThr || chord < 60) continue; // not a gap worth filling
try {
const gpx = await BRouter.route([[a.lon, a.lat], [b.lon, b.lat]], "shortest");
const rp = (GPS.parseRouteGPX(gpx) || {}).points || [];
if (rp.length < 3) continue;
let rlen = 0;
for (let k = 1; k < rp.length; k++) rlen += GPS.distance(rp[k-1].lat, rp[k-1].lon, rp[k].lat, rp[k].lon);
if (rlen > chord * ratio) continue; // road detoured too far — keep the chord
for (let k = 1; k < rp.length - 1; k++) out.push({ lat: rp[k].lat, lon: rp[k].lon, ts: a.ts, estimated: true });
} catch (e) { /* keep the chord for this gap */ }
}
return out;
};
GPS.importGPX = async function(xmlString, weightKg) {
const { points, activityType } = GPS.parseGPX(xmlString);
if (points.length < 2) throw new Error("GPX has fewer than 2 points");

View file

@ -3547,10 +3547,12 @@ async function showRouteEditor(container, routeId) {
if (!brouterMode || points.length < 2) { routeGeom = null; updatePolyline(); updateStats(); return; }
try {
let gpx = await BRouter.route(points.map(p => [p.lon, p.lat]), "trekking");
let rp = GPS.parseRouteGPX(gpx);
routeGeom = (rp && rp.length >= 2) ? rp.map(p => ({ lat: p.lat, lon: p.lon })) : null;
let rp = (GPS.parseRouteGPX(gpx) || {}).points || [];
routeGeom = (rp.length >= 2) ? rp.map(p => ({ lat: p.lat, lon: p.lon })) : null;
} catch (e) { routeGeom = null; showToast("BRouter: " + (e.message || e)); }
changed();
// Draw the routed geometry directly. NOT changed() — that nulls routeGeom and
// re-schedules doBrouter, looping forever and never showing the road line.
updatePolyline(); updateStats();
}
function scheduleRoute() { clearTimeout(routeTimer); routeTimer = setTimeout(doBrouter, 450); }
// A waypoint changed: straight placeholder now, then re-route (if on) shortly after.
@ -4765,6 +4767,29 @@ async function showSessionDetail(sessionId) {
addDarkMapControl(map); // dark-tiles toggle on the track view
POI.attach(map); // POI overlay + category filter
// Gap-fill overlay: route GPS dropouts along roads (BRouter shortest) and draw the
// filled path as a cyan dashed overlay over the track. Non-destructive (no data change).
let gapFillLayer = null;
let GapCtl = L.Control.extend({
options: { position: "topright" },
onAdd: function () {
let b = L.DomUtil.create("button", "rec-map-btn");
b.textContent = "🧩"; b.title = "Достроить пропуски по дорогам"; b.style.position = "static";
L.DomEvent.disableClickPropagation(b);
b.onclick = async () => {
if (gapFillLayer) { map.removeLayer(gapFillLayer); gapFillLayer = null; b.classList.remove("active"); return; }
b.disabled = true; b.classList.add("active");
try {
let filled = await GPS.fillGaps(points);
gapFillLayer = L.polyline(filled.map(p => [p.lat, p.lon]), { color: "#00e5ff", weight: 3, opacity: 0.9, dashArray: "6,6" }).addTo(map);
} catch (e) { showToast("Пропуски: " + (e.message || e)); b.classList.remove("active"); }
b.disabled = false;
};
return b;
},
});
map.addControl(new GapCtl());
// Active saved-route overlay on the detail map + a toggle button (default ON when
// a route is selected). Shares baldav_route_shown with the recording screen.
let sdRouteLayer = null, sdRouteDiv = null;

View file

@ -21,6 +21,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.baldav"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Release keeps cleartext OFF globally, but allows it to the BalDav mirror /
RouteServer hosts (HTTP :88 / :8088 / :17777). Debug allows all cleartext. -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">192.168.1.1</domain>
<domain includeSubdomains="false">192.168.1.18</domain>
<domain includeSubdomains="false">77.66.192.45</domain>
</domain-config>
<debug-overrides cleartextTrafficPermitted="true" />
</network-security-config>