baldav/IMPLEMENTATION_PLAN.md
4aiman 99cd4c8d30 Scrub location/identity: home city name and external mirror IP → placeholders
Remove the home-city name from docs/comments/UI strings and the external
mirror IP (WAN) from code, docs and the Android cleartext allowlist, replacing
the IP with mirror.invalid (set the real host locally, uncommitted). LAN mirror
and routing keep working; the WAN fallback needs the real host set. Generic
big-city map presets are kept (not identifying).
2026-09-10 00:55:45 +03:00

170 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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_<tile>.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-флаг).