baldav/IMPLEMENTATION_PLAN.md
4aiman 1546e947e7 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

19 KiB
Raw Blame History

IMPLEMENTATION_PLAN: встраивание BRouter в BalDav

Основано на docs/brouter-findings.md. Цель — оффлайн-роутинг внутри BalDav (движок BRouter встроен в Android-модуль, btools.router.RoutingEngine в процессе, MIT). Две прикладные фичи: идея 1 (заполнение GPS-пропусков по дороге) и идея 2 (оффлайн-планирование маршрута по точкам).

Ключевые решения: встраивание (вариант B); сегменты .rd5 качаются по проводу внутри BalDav в app data dir; профиль trekking (идея 2) и shortest (идея 1); лицензия MIT (положить LICENSE-brouter).

Мост как у GPS-сервиса: JS → Rust invoke → Kotlin (движок) → результат обратно. Данные: профили — assets в apk; сегменты — app data dir (скачиваются). Движок зовём напрямую (без RouteServer).


Stage 1 — Встраивание движка + спайк «проложи 2 точки»

Goal: движок компилируется в составе BalDav и возвращает маршрут по паре точек на телефоне. Changes:

  • Извлечь brouter-1.6.3-all.jar в src-tauri/gen/android/app/libs/, подключить в gradle (implementation files('libs/brouter-1.6.3-all.jar')), включить desugaring при необходимости.
  • Профили в assets: assets/brouter/profiles2/{lookups.dat,trekking.brf,shortest.brf}.
  • Kotlin BRouterService/функция: копирует профили из assets в files, зовёт btools.router.RoutingEngine(segmentsDir, profilesDir, profile, waypoints, outGpx), читает результат.
  • Rust-команда brouter_route(lonlats, profile) -> String(geojson/gpx) в plugin bridge.
  • JS-обёртка (новый frontend/js/router.js или в gps.js): BRouter.route(points, profile).
  • LICENSE-brouter (MIT-нотис) в корень. Success: на телефоне с вручную положенным тайлом региона (adb push для спайка) вызов BRouter.route([[lon,lat],[lon,lat]], "shortest") возвращает дорожный GPX/GeoJSON, а не ошибку. Tests: ручной вызов из DevTools (CDP) + лог движка; сверить, что трек идёт по дорогам. Status: DONE (проверено на устройстве 2026-09-09). shortest Ростов 5.4км за 3.2с; trekking кросс-тайл Ростов→город 44.8км/набор 316м за 1.2с; GPX с track-length/ascend/cost. ВАЖНО: bundled lookups.dat из 1.6.3 — версии 10, а живые сегменты brouter.de/segments4 — v11 («lookup version mismatch»). Ассеты заменены на v11 из репозитория (abrensch/brouter master: misc/profiles2/{lookups.dat,trekking.brf,shortest.brf}); движок 1.6.3 их принимает. Профили/lookups ОБЯЗАНЫ совпадать по версии с сегментами.

Stage 2 — Экран загрузки сегментов по проводу

Goal: пользователь скачивает тайлы .rd5 региона внутри BalDav в app data dir. Changes:

  • Экран/настройка «Сегменты дорог»: список нужных тайлов по региону (карта/выбор), кнопка скачать с brouter.de/brouter/segments4/<tile>.rd5, прогресс, размер, дата. Хранить в app data dir (путь, который читает Stage 1).

  • Управление: список скачанного, удалить/обновить. Success: дома по проводу тайлы региона (E35_N45/E40_N45 — подтверждены) скачиваются и лежат так, что Stage 1 их читает; в поле сеть не нужна. Tests: скачать тайл, проложить маршрут офлайн (сеть выключить) — работает. Status: ПРИМИТИВ ГОТОВ (компилируется; тест на железе завтра). Kotlin качает тайл в files/brouter/segments4 (путь детерминирован — качает сам Kotlin, без Rust-догадок): команды dltile:<tile>/listtiles в processBrouterCommand, прогресс/статус в brouter_dl_.json. Rust-реле: brouter_download_tile/brouter_download_status/brouter_list_tiles. JS: BRouter.downloadTile/downloadStatus/listTiles/downloadTileAndWait. ПРОВЕРЕНО НА УСТРОЙСТВЕ 2026-09-09: E40 (14.1 МБ) скачан самим BalDav (done за ~55с), маршрут на скачанном тайле — кросс-тайл trekking 44.8км/316м за 1.66с (метрики = ПК-версии, тайл валиден), listTiles отдаёт оба. Прежний стоп на 7922 байтах был из-за того, что BalDav не входил в сплит-туннель устройства (НЕ сеть/код), — исправлено на устройстве. Загрузчик сделан устойчивым к рывковой связи: HTTP Range-докачка (продолжает с места обрыва), readTimeout 60с, до 30 попыток. UI СДЕЛАН (экран «Сегменты» на карте дорог → модалка showBrouterSegments: тайлы региона E35_N45/E40_N45, статус «есть», кнопка Скачать/Обновить с прогрессом через BRouter.downloadTileAndWait). Компилируется; загрузку с зеркала на устройстве проверить при телефоне.

    ЗЕРКАЛО (по решению пользователя, 2026-09-09): данные качаются НЕ с brouter.de напрямую, а со своего зеркала. Источники в загрузчике по порядку: LAN http://192.168.1.1:88/brouter/segments4 → WAN http://mirror.invalid:8088/brouter/segments4 → brouter.de (запас). Зеркало на роутере (Keenetic uhttpd :88, docroot /opt/share/www → файлы в /opt/share/www/brouter/{segments4,profiles2}, USB, всегда включён). ПРОВЕРЕНО: телефон качает тайл с LAN-зеркала за <2с, маршрут на нём ок. Плюс BRouter RouteServer на ПК (jar, /mnt/02F6E7F6F6E7E7BD/brouter-mirror, systemd brouter.service — автостарт без логина) как удалённый роутинг-эндпоинт пользователя (НЕ для BalDav — тот роутит встроенным движком). WAN-пробросы на Keenetic (PPPoE0): :8088→192.168.1.1:88 (зеркало), :17777→192.168.1.18:17777 (RouteServer) — outward-facing, ставятся с подтверждением.

Stage 3 — Идея 2: оффлайн-планирование по опорным точкам

Goal: тап опорных точек → дорожный маршрут trekking → существующий предпросмотр/профиль. Changes:

  • В showRouteEditor (frontend/js/main.js): между опорными точками звать BRouter.route(..., "trekking") вместо прямых отрезков; результат (GPX/GeoJSON) парсить существующим parseRouteGPX, дальше работает showRouteDetail/routeElevStats/drawRoutePolyline.
  • Фолбэк на прямую линию, если движок/сегмент недоступны. Success: тап точек даёт маршрут по дорогам с профилем высоты; сохраняется как маршрут. Tests: спланировать знакомый маршрут офлайн, сверить с онлайн-BRouter (та же геометрия). 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 mirror.invalid:17777). Сквозной прогон в редакторе: 3 точки по город → прямая 1.69км сменилась дорожной линией 2.25км по улицам (Буденновская → кольцо → Ермака). БАГ, найденный и исправленный при этой проверке: doBrouter в конце звал changed(), который обнулял routeGeom и заново ставил doBrouter в очередь — бесконечный цикл, дорожная линия никогда не рисовалась. Заменено на прямой updatePolyline(); updateStats();. ОСТАЛОСЬ: прогон на телефоне (встроенный движок).

Stage 4 — Идея 1: заполнение GPS-пропусков по дороге

Goal: GPS-разрыв заполняется дорожным путём (shortest), с фолбэком на хорду. Changes:

  • В ветке isGap computeStats/при обработке трека: проложить BRouter.route(предгэп, послегэп, "shortest"); если длина по графу > ~3× хорды (константа, подобрать на практике) — оставить хорду.
  • Точки-заполнители помечать estimated; решить зачёт дорожной дистанции (по умолчанию — считать дорожную длину, но с лимитом гэпа). Success: записанный трек с дропаутом GPS показывает дорожный фолловинг вместо хорды, кроме случая «дорога в разы длиннее». Tests: трек с известным дропаутом на дороге → заполнение по дороге; дропаут на поле → хорда. 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 / mirror.invalid); debug-overrides — всё в debug. Подключён в AndroidManifest (android:networkSecurityConfig).
  • proguard-rules.pro: -keep class btools.** { *; } + -dontwarn btools.** — минификация release не вырежет встроенный движок.

Stage 5 — Полировка (опционально)

Goal: удобство и корректность. Changes: настройка «избегать подъёмов» (крутилка uphillcost в trekking); обновление сегментов; доки (README/PROJECT_MAP), лог сессии; проверка размера apk. Status: not started


Stage 6 — POI-оверлеи на картах BalDav (АНАЛИЗ, вне BRouter, запрошено 2026-09-09)

Идея: показывать OSM-объекты (amenity/shop/tourism) на картах BalDav — при трекинге и просмотре: ATM, банк, скамейка, туалеты, вода, кафе/бар/фастфуд/ресторан/паб, велопарковка/веломастерская, автобусная остановка, пекарня/супермаркет/кофе, смотровая, укрытие, пикник и т.д. (набор — как в brouter-web overlays, см. скрины). С фильтром категорий (тумблеры), иконки скачать и рисовать. Что проанализировать перед реализацией:

  • Источник данных POI ОФФЛАЙН: Overpass-выгрузка по региону (bbox ~100км вокруг город) в локальный стор (SQLite/JSON, как seed/tiles), т.к. сеть в поле недоступна. rd5 не годится как POI-хранилище. Обновление — по проводу с зеркала (как тайлы/сегменты).
  • Иконки: взять набор (напр. используемый brouter-web / maki / OSM-carto sprites), лицензия, положить в assets, рисовать как Leaflet divIcon/marker с кластеризацией на низком зуме (перф).
  • UI: панель категорий с чекбоксами (как в brouter-web), сохранение выбора (localStorage), z-order (POI над тайлами/дорогами, под треком/паком).
  • Перф: сотни-тысячи точек — кластеризация/культинг по зуму и bbox видимой области. Статус: v1 СДЕЛАНО (проверено функционально на устройстве). Данные: Overpass по ~100км вокруг город (адаптивная выгрузка — дробить не понадобилось, весь bbox отдал 5662 POI одним запросом), компактный frontend/data/pois.json [{la,lo,c,n}] (358КБ, бандл, оффлайн). Отрисовка: frontend/js/poi.js — круглые контрастные значки (эмодзи в кружке, .poi-badge, светлый/тёмный под body.dark-map), куллинг по видимым границам + от zoom 13, cap 500/кадр; Leaflet-контрол 📍 с панелью категорий (чекбоксы), выбор в localStorage. Привязан к картам записи/деталей/дорог (POI.attach). Проверено: 124 значка в кадре z16 над город, границы/фильтр корректны. ОСТАЛОСЬ: визуальная проверка на живом экране (WebView в CDP не рисует — скрин чёрный); при желании — расширить область за 100км и добавить недостающие категории (сейчас все 21 категория есть).

Stage 7 — Тёмные тайлы карты в BalDav (запрошено 2026-09-09)

Идея: заменить фон OSM на тёмный (пользователю нравится ÖPNV, но хочет тёмное). ВАЖНО (проверено 2026-09-09): хорошего тёмного растра БЕЗ ключа сейчас нет — CARTO Dark теперь тоже требует ключ (без ключа отдаёт тайлы с ватермаркой "API KEY REQUIRED"). Кандидаты: Thunderforest Transport Dark (ÖPNV-стиль тёмный, ближе всего к желаемому — бесплатный ключ), CARTO Dark (ключ), Stadia (ключ); без ключа и чисто — только светлые (ÖPNVKarte/OpenTopoMap/OSM) или спутник Esri. Источник в frontend/js/tilecache.js хардкодом https://a.tile.openstreetmap.org/{z}/{x}/{y}.png (3 места); оффлайн-тайлы — в tiles_*.db (OSM). РЕШЕНО (2026-09-09): живой CSS-фильтр, без перепаковки. Провайдеров тёмных тайлов (CARTO/ Thunderforest/2GIS/Yandex) отмели — ключи/квоты/ToS/проекция/недоступность в РФ. Транспортная карта инвертом плохо (город). ПОБЕДИЛО: OSM-стандарт (те тайлы, что уже есть) + фильтр invert(1) hue-rotate(180deg) brightness(1.5) на .leaflet-tile-pane — достойно и в городе, и на селе. Реализация: класс body.dark-map + одно CSS-правило (только слой тайлов, оверлеи цветные), тумблер ☾ на экранах записи/деталей трека/дорог (toggleDarkMap/applyDarkMap/addDarkMapControl в main.js), выбор в localStorage baldav_dark_map. Работает одинаково на оффлайн-БД и онлайне, без ключей/квот. Статус: DONE (проверено на устройстве: фильтр на тайлах, overlay none, кнопка активна, персист).

Потенциальные проблемы

  • Desugaring/java.lang.invoke при dex — проверить сборку (AGP обычно тянет).
  • Размер/имена тайлов региона на brouter.de — уточнить при Stage 2.
  • Время роутинга на телефоне на больших .rd5 — замерить (может понадобиться таймаут/поток).
  • Формат вывода: GeoJSON проще парсить, чем GPX; выбрать при Stage 1.
  • Заполнение гэпов не должно ломать существующие фильтры высоты/скорости (estimated-флаг).