baldav/docs/brouter-findings.md
4aiman a52e998fb6 BRouter: analysis + embed the offline routing engine (Stage 1)
Analysis in docs/brouter-findings.md: how BRouter knows roads (.rd5
segments), routes and reaches low elevation gain (profile uphill/downhill
cost + kinematics), the three call surfaces, and an evaluation of two ideas
— fill GPS gaps along roads, and offline waypoint route planning. License is
MIT; the engine has no desktop-only deps, so it embeds on Android ART.

Stage 1 (embed + bridge, compiles into the apk; on-device route not yet run):
- vendored brouter-1.6.3-all.jar into the Android module (dexes cleanly) and
  bundled the trekking/shortest profiles + lookups.dat as assets, copied to
  files/brouter/profiles2 on launch
- MainActivity: a brouter_command.txt handler on the existing FileObserver
  runs btools.router.RoutingEngine off the UI thread (recipe taken via javap
  from ServerHandler) and returns the track as GPX
- Rust brouter_route command: same file bridge as GPS (request + command,
  poll for the result), registered in the invoke handler
- frontend/js/router.js: BRouter.route(points, profile) -> GPX (parseRouteGPX)
- LICENSE-brouter (MIT notice) kept alongside the vendored engine

Segments (.rd5, ~13-23 MB per 5x5 tile) are delivered later (Stage 2, wired
download inside BalDav). IMPLEMENTATION_PLAN.md tracks the 5 stages.
2026-09-09 00:56:13 +03:00

17 KiB
Raw Blame History

Разбор BRouter 1.6.3 под две идеи BalDav (2026-09-07)

Получено разбором brouter-1.6.3.zip (read-only: листинг архива, чтение текстовых доков и профилей через stdout, инспекция brouter-1.6.3-all.jar в памяти — распаковки на диск и запуска Java не было). Цель — понять, как BRouter «знает, где дорога» и строит маршруты с малым набором высоты, и оценить под это две идеи пользователя:

  1. Заполнение GPS-пропусков по дороге — когда GPS пропадает и появляется, пробел заполнять кратчайшим путём по реальным дорогам, а не хордой/скорость×время.
  2. Оффлайн-планирование маршрута по опорным точкам в BalDav — замена онлайн-BRouter.

Обе идеи требуют оффлайн-роутинга; сеть в поле недоступна (РЭБ/РКН — как в обсуждениях ветра и DEM), поэтому годятся только оффлайн-схемы.


1. Что внутри дистрибутива

Часть Что это
BRouterApp.1.6.3-api19-release.apk, ...api30-release.apk Android-приложение btools.routingapp (одно и то же, под разные min-SDK). Считает маршрут, карту НЕ рисует.
brouter-1.6.3-all.jar (356 КБ) Standalone-движок на чистой Java. Main-Class btools.server.BRouter (HTTP-сервер). Тот же движок, что в apk.
profiles2/*.brf Профили маршрутизации (текстовые скрипты стоимости). lookups.dat — таблица тегов/значений.
readmes/ Документация: установка, profile_developers_guide.txt, mapcreation.md, интеграция с OsmAnd, особенности Android 11.

BRouter — маршрутизатор без карты: на вход опорные точки + профиль, на выход трек (GPX/GeoJSON). Отрисовка и навигация — на стороне карт-тула (OsmAnd/Locus/…), либо, в нашем случае, на стороне BalDav.

2. Где дорога: данные .rd5

  • Граф дорог — сегментные файлы .rd5, тайлы 5°×5° по широте/долготе. Внутри — OSM-ways и nodes, закодированные в бинарный битстрим по таблице lookups.dat (для каждого тега — набор значений с частотами и алиасами).
  • Готовые тайлы качаются с brouter.de/brouter/segments4 (пересобираются еженедельно) или строятся самому из OSM-dump + SRTM (mapcreation.md, скрипт process_pbf_planet.sh, требуется Osmosis/pbfparser).
  • Высоты в сегментах берутся из SRTM-90м (CGIAR). Именно они дают BRouter'у профиль рельефа, по которому он считает набор высоты (см. §4). Без SRTM сегменты соберутся, но без высот — маршруты «с малым набором» не получатся.
  • Для нашего региона (Ростовская обл., ~47N/40E) нужны тайлы, чьи SW-углы кратны 5: вероятно E35_N45.rd5 (35–40E, 45–50N) и E40_N45.rd5 (40–45E) — город ~40.1E попадает во второй. Точные имена и размер (обычно десятки МБ на тайл) — проверить при загрузке. Загрузка одноразовая по проводу — тот же паттерн «скачать дома», что для DEM/прогноза.

3. Как строится маршрут (движок)

  • Класс btools.router.RoutingEngine. Ключевые шаги (по строкам байткода): matchWaypointsToNodes — привязка каждой опорной точки к ближайшему дорожному узлу графа (это и есть ответ «где дорога, а где нет»: точка вне дорог подтягивается к дороге), затем поиск пути по графу между привязанными узлами.
  • Стоимость ребра считает интерпретатор профиля (btools.expressions) по тегам ребра из .rd5. Поиск минимизирует суммарную стоимость (по сути A*/Dijkstra по сегментному графу с кэшами профиля).
  • На выход движок отдаёт трек и метрики: track-length, filtered ascend и plain-ascend (набор высоты — сглаженный и сырой), cost, energy, time (по кинематике профиля). Формат — GPX (formatAsGpx/writeGpx) или GeoJSON/KML.

4. Малый набор высоты — как именно (основной режим)

Это чисто вопрос профиля, а не движка. В trekking.brf (context:global):

assign consider_elevation = true
assign downhillcost   = 60     # штраф за спуск
assign downhillcutoff = 1.5    # уклоны меньше 1.5% не штрафуются
assign uphillcost     = 0      # (в trekking по умолчанию 0; крутить вверх — здесь)
assign uphillcutoff   = 1.5

Плюс кинематическая модель времени/энергии — прямой аналог модели мощности BalDav:

assign totalMass  = 90     # масса байк+седок, кг
assign maxSpeed   = 45     # км/ч
assign S_C_x      = 0.225  # CdA (площадь×коэф. лобового сопротивления)
assign C_r        = 0.01   # коэффициент качения
assign bikerPower = 100    # средняя мощность, Вт

Чтобы получить маршрут с малым набором высоты, поднимают uphillcost (и/или downhillcost): движок начинает предпочитать обходные, но более пологие дороги. uphillcutoff/downhillcutoff задают порог уклона, ниже которого рельеф игнорируется. То есть «основной режим» пользователя в онлайн-BRouter — это trekking с ненулевым uphillcost.

Отдельно есть shortest.brf: downhillcost=0, uphillcost=0, turncost=0 — чистейший кратчайший путь по дорогам, без учёта рельефа. Это ровно то, что нужно для идеи 1 (заполнить GPS-пропуск — важно «просто чтобы была дорога», кратчайшая).

5. Как вызвать оффлайн — три поверхности

(а) Standalone-jar на компе (HTTP-сервер). Подтверждённая строка usage:

java -cp brouter-1.6.3-all.jar btools.server.RouteServer <segmentdir> <profiledir> <customprofiledir> <port> <maxthreads> [bindaddress]

Запрос (тот же API, что у brouter.de/brouter-web):

GET http://127.0.0.1:<port>/brouter?lonlats=39.7,47.2|40.1,47.4&profile=shortest&format=geojson&timode=0&alternativeidx=0

Параметры (из байткода RouteServer): lonlats (пары lon,lat через |), profile, format (gpx/geojson/kml), timode (turnInstructionMode), alternativeidx, nogos. Нужен JRE + каталог сегментов .rd5 + каталог профилей. Годится для инструментов tools/track/ (проложить маршрут по lonlats, что уже выдаёт tools/steep_segments.js).

(б) Приложение BRouter на телефоне — Server-Mode. Приложение поднимает локальный HTTP-сервер (порт 17777 по умолчанию); любой клиент на устройстве, включая WebView BalDav, шлёт тот же GET /brouter?lonlats=.... Требует установленного приложения BRouter и скачанных сегментов.

(в) Приложение BRouter — Android-intent. С v1.6.2 у приложения есть «интерфейс без файлового доступа»: BalDav (Kotlin) шлёт intent к btools.routingapp с опорными точками и профилем, получает GPX/GeoJSON обратно. Данные приложения с Android 11 лежат в Android/media/btools.routingapp/brouter/segments4.

(г) Встраивание движка в BalDav. brouter-1.6.3-all.jar — чистая Java (356 КБ), пакеты router/mapaccess/expressions/codec/util. Классы движка можно добавить в Android-модуль BalDav (src-tauri/gen/android) и звать из Kotlin, а из JS — через существующий Rust↔Kotlin-мост (как GPS-сервис). Полностью самодостаточно и оффлайн, второе приложение не нужно.

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

  • Как: при разрыве взять последнюю точку до пропуска и первую после, проложить между ними маршрут профилем shortest.brf, вставить дорожную полилинию как заполнение гэпа.
  • Стык с computeStats (frontend/js/gps.js, ветка isGap): сейчас гэп заполняется хордой или скорость×время. Дорожный путь точнее (по реальным дорогам). Точки-заполнители помечать estimated (как dead-reckoning), чтобы они не влияли на глюк-фильтры высоты и были видимо «достроенными». Решить: идёт ли дорожная дистанция в зачёт трека или только рисуется (сейчас гэп считается по скорости; дорожная длина может быть и точнее, и длиннее реального пути).
  • Риски/границы: без сегментов на регион заполнить нечем; дорога-кратчайшая может отличаться от фактически проеханной; на бездорожье (поля, где пользователь реально ездит) кратчайший путь по графу дорог может увести. Нужен разумный лимит длины гэпа, при котором достраиваем.

7. Идея 2 — оффлайн-планирование маршрута по точкам

  • Что уже есть в BalDav (frontend/js/main.js): showRouteEditor (тап по карте = точка, тап по линии = вставка, тащить/удалять), showRouteDetail (предпросмотр + профиль высоты routeElevStats + фуллскрин), drawRoutePolyline, «Сохранённые маршруты», топо/уклон-обводка. Парсер parseRouteGPX уже читает GPX/<ele>.
  • Чего не хватает: оффлайн-роутера между опорными точками. Сейчас линия между тапами — прямая (ручное рисование). BRouter вернёт дорожный GPX/GeoJSON, который уже парсится parseRouteGPX, и дальше работает существующий предпросмотр/профиль высоты.
  • То есть UI-обвязка в основном готова; предметная работа — вызов BRouter и подмена прямых отрезков дорожным маршрутом (+ выбор профиля: trekking с uphillcost для «поменьше в горку»).

8. Варианты интеграции и рекомендация

Вариант Плюсы Минусы
A. Зависимость от приложения BRouter (intent или localhost HTTP) Быстро; роутинга в BalDav нет; BRouter сам качает/обновляет сегменты Второе приложение ставить и настраивать; управление сегментами вне BalDav; связанность
B. Встраивание движка (jar в Android-модуль + Rust/JS-команда) Самодостаточно, полностью оффлайн, один apk; сегменты кладём куда хотим Бандлинг Java-движка; доставка .rd5 в приложение; проверить лицензию (GitHub abrensch/brouter); управление хранилищем сегментов
C. Порт на Rust/JS Нет внешних зависимостей Огромный объём (движок + формат .rd5 + интерпретатор профилей) — отклонить

Решение — B (встраивание) (подтверждено пользователем). Встраиваемость проверена по байткоду: движок (150 классов) использует только java.io/net/util/lang/text/security/zip — ни AWT/Swing/ RMI/ImageIO/servlet/sun.*, всё есть на Android ART. RouteServer — на голых ServerSocket/Socket (контейнер не нужен). Значит в процессе можно звать напрямую btools.router.RoutingEngine (без HTTP-сервера): дать каталог сегментов + профиль + опорные точки, получить GPX/GeoJSON. Для инструментов tools/track/ (анализ, не телефон) сразу годится вариант (а) — jar на компе.

9. Решения и открытые вопросы

Решено (2026-09-07):

  1. Лицензия — MIT («Copyright (c) 2019 BRouter contributors», github.com/abrensch/brouter). Встраивание разрешено без ограничений; при распространении сохранить копирайт-нотис MIT (положить LICENSE-brouter в репозиторий рядом с движком).

  2. Интеграция — вариант B (встраивание движка). Код BalDav открыт, MIT это позволяет.

  3. Профиль — trekking (для обеих идей). Идея 1 всё же считается shortest для чистого кратчайшего пути (см. §6); trekking — для планирования (идея 2).

  4. Идея 1, порог хорды: прокладывать по графу, НО если дорожная длина в разы больше хорды — рисовать хорду (граф увёл не туда, напр. вокруг поля). Коэффициент подобрать на практике (старт ~3×, вынести в конфиг/константу).

  5. «Крутилка» uphillcost (см. §4) — пользователь про неё не знал. План: начать со стандартного trekking; если маршруты слишком в горку — добавить настройку «избегать подъёмов» (поднимать uphillcost). Пока не крутим.

  6. Доставка сегментов — загрузка по проводу внутри BalDav (решено). Экран в BalDav: дома на Wi-Fi/проводе скачать готовые тайлы региона с brouter.de/segments4 в папку приложения (app data dir, аналог seed-БД); в поле сеть не нужна. Тот же паттерн, что задумывался для DEM/ прогноза. Свою сборку сегментов из OSM+SRTM не делаем.

  7. Встраиваемость подтверждена (см. §8): движок без десктоп-зависимостей, зовём btools.router.RoutingEngine в процессе из Kotlin.

10. Проверка (PoC, отдельным шагом, по согласию)

Требует JRE и загрузки сегментов региона (сеть) — вне текущей задачи. Когда разрешишь: запустить RouteServer на компе с каталогом .rd5 и profiles2/, дёрнуть GET /brouter?lonlats=...&profile=shortest&format=geojson по паре точек из tools/steep_segments.js, убедиться, что возвращается дорожный маршрут с track-length/ascend.