diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..488bb11 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,79 @@ +# 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:** not started + +## Stage 2 — Экран загрузки сегментов по проводу +**Goal:** пользователь скачивает тайлы `.rd5` региона внутри BalDav в app data dir. +**Changes:** +- Экран/настройка «Сегменты дорог»: список нужных тайлов по региону (карта/выбор), кнопка скачать + с `brouter.de/brouter/segments4/.rd5`, прогресс, размер, дата. Хранить в app data dir + (путь, который читает Stage 1). +- Управление: список скачанного, удалить/обновить. +**Success:** дома по проводу тайлы региона (E35_N45/E40_N45 — уточнить) скачиваются и лежат так, +что Stage 1 их читает; в поле сеть не нужна. +**Tests:** скачать тайл, проложить маршрут офлайн (сеть выключить) — работает. +**Status:** not started + +## Stage 3 — Идея 2: оффлайн-планирование по опорным точкам +**Goal:** тап опорных точек → дорожный маршрут trekking → существующий предпросмотр/профиль. +**Changes:** +- В `showRouteEditor` (frontend/js/main.js): между опорными точками звать `BRouter.route(..., + "trekking")` вместо прямых отрезков; результат (GPX/GeoJSON) парсить существующим `parseRouteGPX`, + дальше работает `showRouteDetail`/`routeElevStats`/`drawRoutePolyline`. +- Фолбэк на прямую линию, если движок/сегмент недоступны. +**Success:** тап точек даёт маршрут по дорогам с профилем высоты; сохраняется как маршрут. +**Tests:** спланировать знакомый маршрут офлайн, сверить с онлайн-BRouter (та же геометрия). +**Status:** not started + +## Stage 4 — Идея 1: заполнение GPS-пропусков по дороге +**Goal:** GPS-разрыв заполняется дорожным путём (shortest), с фолбэком на хорду. +**Changes:** +- В ветке `isGap` `computeStats`/при обработке трека: проложить `BRouter.route(предгэп, послегэп, + "shortest")`; если длина по графу > ~3× хорды (константа, подобрать на практике) — оставить хорду. +- Точки-заполнители помечать `estimated`; решить зачёт дорожной дистанции (по умолчанию — считать + дорожную длину, но с лимитом гэпа). +**Success:** записанный трек с дропаутом GPS показывает дорожный фолловинг вместо хорды, кроме +случая «дорога в разы длиннее». +**Tests:** трек с известным дропаутом на дороге → заполнение по дороге; дропаут на поле → хорда. +**Status:** not started + +## Stage 5 — Полировка (опционально) +**Goal:** удобство и корректность. +**Changes:** настройка «избегать подъёмов» (крутилка `uphillcost` в trekking); обновление сегментов; +доки (README/PROJECT_MAP), лог сессии; проверка размера apk. +**Status:** not started + +--- + +## Потенциальные проблемы +- Desugaring/`java.lang.invoke` при dex — проверить сборку (AGP обычно тянет). +- Размер/имена тайлов региона на brouter.de — уточнить при Stage 2. +- Время роутинга на телефоне на больших `.rd5` — замерить (может понадобиться таймаут/поток). +- Формат вывода: GeoJSON проще парсить, чем GPX; выбрать при Stage 1. +- Заполнение гэпов не должно ломать существующие фильтры высоты/скорости (estimated-флаг). diff --git a/LICENSE-brouter b/LICENSE-brouter new file mode 100644 index 0000000..e8a0d0e --- /dev/null +++ b/LICENSE-brouter @@ -0,0 +1,30 @@ +BalDav embeds the BRouter routing engine (brouter-1.6.3-all.jar, +src-tauri/gen/android/app/libs/) and its routing profiles +(src-tauri/gen/android/app/src/main/assets/brouter/profiles2/). + +BRouter is a third-party component distributed under the MIT License. +Source: https://github.com/abrensch/brouter + +--- + +MIT License + +Copyright (c) 2019 BRouter contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PROJECT_MAP.md b/PROJECT_MAP.md index 73679fc..752765a 100644 --- a/PROJECT_MAP.md +++ b/PROJECT_MAP.md @@ -84,6 +84,13 @@ | `ion_products.json` | 1111 продуктов ION (сырые данные скрейпера) | | `5ka_products_fresh.json` | 3103 продукта 5ka (сырые данные) | +### Разборы (docs/) + +| Файл | Назначение | +|------|------------| +| `docs/gps-model-findings.md` | Находки по расчётной модели GPS (Crr/ккал/высота/ветер) — часть внедрена, см. лог 2026-09-07 | +| `docs/brouter-findings.md` | Разбор BRouter 1.6.3 (оффлайн-роутинг): данные `.rd5`, профили, малый набор высоты, 3 способа вызова + встраивание; под идеи «заполнение GPS-пропусков» и «оффлайн-планирование по точкам». Рекомендация — встраивание движка (jar 356 КБ) в Android-модуль | + ## База данных ### Таблицы (Rust v1 миграция + JS миграции) diff --git a/docs/brouter-findings.md b/docs/brouter-findings.md new file mode 100644 index 0000000..ad67bd2 --- /dev/null +++ b/docs/brouter-findings.md @@ -0,0 +1,191 @@ +# Разбор 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 [bindaddress] +``` + +Запрос (тот же API, что у brouter.de/brouter-web): + +``` +GET http://127.0.0.1:/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/``. +- **Чего не хватает:** оффлайн-роутера между опорными точками. Сейчас линия между тапами — прямая + (ручное рисование). 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`. diff --git a/docs/gps-model-findings.md b/docs/gps-model-findings.md new file mode 100644 index 0000000..bf9f629 --- /dev/null +++ b/docs/gps-model-findings.md @@ -0,0 +1,353 @@ +# Находки по расчётной модели GPS (2026-09-07) + +Получено при независимом разборе базы `baldav.db` (156 поездок, 366 тыс. точек) +сторонним инструментом `tools/track/`. Константы там взяты из `GPS.computeStats`, +поэтому расхождения означают разницу в модели, а не в настройках. + +Проверка сходимости: дистанция расходится с базой на **0.2%**, калории по формуле +приложения -- на **4%** (трек 4 сентября). То есть база и формула воспроизводятся, +и нижеперечисленное -- не ошибки чтения. + +Пункты про **нулевую отметку высоты** и **паузу на подъёме** сюда не включены: +они уже обсуждены отдельно. + +Файл состоит из двух частей. **Первая -- находки:** измеренные расхождения, +каждое с замером и по возможности с контрольной проверкой. **Вторая -- +предположения:** варианты отслеживания ветра, составленные по просьбе +пользователя, с вопросами к вам в конце. Смешивать их не стоит: первая часть +проверена на данных, вторая -- это перечень возможностей, а не рекомендация. + +--- + +## 1. Потолок качения 0.008 занижен втрое + +`crr_from_accel`: при `accel_rms > 7` возвращается 0.008 и выше не поднимается. +0.008 -- это разбитый асфальт. Реальные грунты дают втрое больше. + +**Замер.** Трек 6 сентября (`id 1788683393199`), километры 36.5-38.5: 1996 м, +ровно (уклон −0.1%), `accel_rms` медиана **15.1**, скорость 13.8 км/ч. +Пользователь описал место как траву и тракторную колею с ямами. +Обратное решение уравнения мощности относительно Crr: + +| если мощность была | требуемый Crr | +|---|---| +| 90 Вт | 0.0198 | +| 110 Вт | 0.0252 | +| 130 Вт | 0.0307 | + +Справочные значения: плотный грунт 0.012, щебень 0.020, трава и песок 0.035. +Замер попадает ровно в диапазон, соответствующий описанию. + +**Контрольная проверка обратным знаком.** Те же вычисления на километрах 9-11 +(25 км/ч, тряска 5.2) дают Crr отрицательным при 90-110 Вт -- значит там +мощность была около 150 Вт. Сплиты на тех километрах показывают 148-172 Вт. +Метод не завышает и не занижает систематически. + +**Предлагаемая шкала** (взята нижняя граница замера -- лучше недосчитать): + + < 3 -> 0.004 гладкий асфальт (как сейчас) + 3-7 -> 0.006 смешанное (как сейчас) + 7-11 -> 0.008 разбитый асфальт (как сейчас) + 11-15 -> 0.012 плотный грунт (новое) + > 15 -> 0.020 трава, колея, щебень (новое) + +**Влияние на трек 6 сентября:** средняя мощность 89 -> 108 Вт, +механическая работа 1233 -> 1585 кДж. То есть на грунтах модель +недосчитывала примерно четверть работы. + +## 2. `max(physics, MET)` никогда не складывает, и подъёмы от этого страдают + +На одних и тех же отрезках трека 6 сентября: + +| участки | физика | MET | MET/физика | +|---|---|---|---| +| вся поездка | 1318 | 1764 | 1.34 | +| равнина, \|уклон\| < 1% | 727 | 1344 | **1.85** | +| в горку круче 2% | 400 | 165 | **0.41** | + +Комментарий в коде говорит про «~2x занижение физики» -- на равнине это верно +(1.85), но по поездке в целом всего 1.34, потому что на подъёмах физика +выигрывает с большим запасом. + +Изъян в том, что оценки не складываются. Биологические накладные расходы +(размахивание ногами, равновесие, гашение тряски корпусом) никуда не деваются +на подъёме, но там берётся физика и табличная оценка отбрасывается целиком. +**Направление ошибки известно: подъёмы недосчитываются.** + +Осмысленная альтернатива, если решите менять: не максимум, а +`физика/КПД + накладные`, где накладные оцениваются табличным минимумом +для медленной езды. Требует калибровки, поэтому просто как направление. + +## 3. Ходьба с велосипедом засчитывается как езда + +На односкоростном передача даёт точный признак: 40/18 на колесе 24" -- +это 4.26 м за оборот шатунов. Скорость ниже **7.7 км/ч** означает каденс +ниже 30 об/мин, а на таком каденсе велосипед не едет, он падает. Значит +человек идёт и ведёт или несёт его. + +На треке 6 сентября таких кусков **0.66 км за 8:44 в движении** -- пользователь +подтвердил, что нёс велосипед руками в горку. Участок: км 56.31, длина 289 м, +набор 47.8 м, **уклон 16.6%**, 4.3 км/ч. + +Побочный эффект для статистики: если этот кусок не отделять, он склеивается +с последующим ездовым подъёмом (км 56.60, 624 м, 3.8%, 12.3 км/ч) в один +«подъём 915 м, 7.8%, 7.7 км/ч», которого в природе не было. + +Признак применим только когда `bikes.gear_count` пусто или 1. На многоскоростном +низкая скорость ничего не доказывает. + +**По акселерометру это не различается, проверено:** нести велосипед в гору 3.24, +ехать по гладкому асфальту 2.88, идти пешком (сессия walk) 3.98. `accel_rms` +показывает дорогу, а не походку. + +## 4. Набор высоты расходится с независимым пересчётом в обе стороны + +| трек | в базе | независимый пересчёт | +|---|---|---| +| 6 сентября | 328 м | 528 м | +| 4 сентября | 314 м | 264 м | + +Знак разный, значит это не простое смещение. Без модели рельефа проверить +нечем: попытка построить карту высот по своим же 156 поездкам провалилась -- +перекрытие маршрутов всего 26% ячеек, а разброс высоты между поездками +в одной и той же точке имеет медиану **10 метров**. + +**Поправка после уточнения у пользователя.** Изначально я счёл пустую +`altitude_srtm_m` (заполнена в одной сессии из 156) недоработкой. Это не так: +поле заполняется вручную кнопкой «уточнить высоты», которая ходит в GPXZ +и подобные сервисы, и пользователь жмёт её редко. Полагаться на сервисы +в поле нельзя -- РЭБ и РКН глушат, устойчиво работает преимущественно +проводной интернет. + +Отсюда следует не «починить SRTM», а вопрос по существу: **расхождение +в наборе высоты остаётся неразрешённым, и разрешить его без внешнего рельефа +нечем.** Если это важно -- имеет смысл думать в сторону предварительной +загрузки рельефа по району дома, по проводу, один раз. Если не важно -- +достаточно знать, что цифра набора имеет точность порядка десятков процентов, +и не строить на ней выводов. На расчёт мощности это влияет слабо: там работает +уклон, а он считается по окну и ограничен сверху. + +## 5. Пульса в точках нет, и это ожидаемо + +`gps_points.heart_rate` равен нулю во всех 156 сессиях. **Это не дефект +записи:** пользователь потерял браслет и пульсометр не носит, писать в поле +попросту нечего. `ble_hr_data.json` и запись устройства в `profile` +остались от прежнего использования. + +Упомянуто только затем, чтобы никто не искал ошибку там, где её нет, +и чтобы разбор треков не обещал анализ по пульсу. + +## 6. Ветер вычисляется из `bearing_deg` + +Побочная находка, в приложении такого нет. Ветер виден по тому, что на ровных +однотипных дорогах скорость зависит от курса. Берутся только точки с ровным +уклоном, невысокой тряской и в седле, считается медианная скорость по восьми +секторам курса, и подбирается ветер, при котором подразумеваемая мощность +во всех секторах одинакова. + +Трек 6 сентября: **6.3 км/ч из направления 345°**. Разброс мощности +по направлениям падает с 33% до 11%. Пользователь независимо подтвердил, +что ветер часто дул в лицо. + +Медианная скорость по курсу на этом треке -- 23.6 км/ч на юго-восток +против 15.7 км/ч на северо-запад, при одинаковых дорогах. + +Второй, независимый метод -- по встречным проездам одного места (758 пар) +-- даёт 3.6 км/ч из 325°. Направление сходится в пределах 20°, сила -- +в пределах множителя 1.8. Это и есть достижимая точность оценки ветра +без датчика. + +Разбор вариантов отслеживания ветра, включая идею пользователя про голосовые +отметки, -- во второй части этого файла. Там же перечислены вопросы, +по которым пользователь просит ваше мнение. + +## 7. Ловушка при отделении ходьбы: порог по длительности не нужен + +Найдено после замечания пользователя, что встречные проезды одного места +случались и пешком. + +Первая реализация признака ходьбы требовала, чтобы медленный кусок длился +дольше 30 секунд подряд. Это оказалось неверно. На треке 6 сентября всего +**14.7 минут** движения медленнее 7.7 км/ч, из них сплошными кусками -- +только 8.7 минуты. Оставшиеся **6 минут** -- короткие эпизоды, и они попадали +в статистику как педалирование. + +Видно это по корзине каденса: «0-40 об/мин» занимала 21 минуту, и **6.4 минуты +из неё приходилось на каденс ниже 30**, которого при езде не бывает вовсе. +После исправления корзина стала 9 минут, а средний каденс поднялся с 64 до 67. + +**Правильное разделение двух вопросов:** + +- *Исключать из статистики* нужно **любую** точку ниже порога, без оглядки + на длительность: на этой передаче ниже 30 об/мин не едут, и точка. Сюда же + попадут обычные торможения до нуля, но они короткие и маломощные, вреда нет. +- *Называть кусок ходьбой в отчёте* стоит только при длительности дольше + 30 секунд подряд -- иначе отчёт будет объявлять ходьбой каждое торможение + перед ямой. + +На оценку ветра эта примесь, к счастью, не повлияла: там отдельный порог +9 км/ч, и пешие точки не проходили. Проверено прямым сравнением -- с ними +3.5 км/ч из 326°, без них 3.6 км/ч из 325°. + + +--- +--- + +# ЧАСТЬ ВТОРАЯ: предложения по отслеживанию ветра + +Ниже -- **не находки, а предположения**, составленные по просьбе пользователя. +Это перечень способов с замерами там, где замеры удалось сделать. +Решение принимает сессия разработки. + +## Зачем + +Ветер -- крупнейший неучтённый источник ошибки в расчёте мощности на равнине. +На треке 6 сентября медианная скорость на одинаковых дорогах составила +**23.6 км/ч на юго-восток против 15.7 км/ч на северо-запад**. Разброс +подразумеваемой мощности по направлениям -- **33%**. Пока ветра в модели нет, +эти 33% списываются на «просел» и «разогнался». + +## Ограничения, названные пользователем + +- **Сетевые сервисы в поле недоступны.** РЭБ и РКН глушат, устойчиво работает + преимущественно проводной интернет. Любая схема, которой нужна сеть во время + поездки, нерабочая. +- **Метеостанция меряет в другой точке.** Стандартное измерение -- на высоте + 10 м в открытом месте, усреднение за 10 минут. Велосипедист едет на высоте + 1.5 м среди лесополос, домов и насыпей, где ветер и слабее, и порывистее. + Среднее по области пользователю не поможет. + +Оба возражения справедливы и отсекают наивное «дёргать погодный API». + +## Вариант 1. Считать ветер из самого трека (без железа и без сети) + +Реализовано и проверено в `tools/track/track.py`. Два независимых метода. + +**1а. Подбор по секторам курса.** Берутся только точки на ровном участке, +с невысокой тряской, в седле. Медианная скорость считается по восьми секторам +направления. Подбирается такой ветер, при котором подразумеваемая мощность +во всех секторах одинакова. + +**1б. Встречные проезды одного места.** Ищутся пары точек в одной ячейке 30 м, +разнесённые по времени, с противоположными курсами, одинаковым покрытием +и зеркальным уклоном. На одной и той же дороге качение и уклон сокращаются, +и разница скоростей при равной мощности задаёт составляющую ветра вдоль дороги. +Составляющие по разным дорогам сводятся в вектор методом наименьших квадратов. + +**Результат на треке 6 сентября:** + +| метод | скорость | направление | +|---|---|---| +| по секторам курса | 6.3 км/ч | 345° | +| по встречным проездам (758 пар) | 3.6 км/ч | 325° | + +Направление сходится в пределах 20°, сила -- в пределах множителя 1.8. + +**Это и есть достижимая точность способа без железа: направление уверенно, +сила -- с точностью до двух раз.** Любой вариант с датчиком должен +эту планку превосходить, иначе он не нужен. + +Ограничения: работает только задним числом, требует разнообразия курсов +(кольцевой маршрут или «туда-обратно» -- идеально, прямая в одну сторону -- +бесполезно), и предполагает, что ветер за поездку не менялся. Последнее +лечится подгонкой в скользящем окне 20-30 минут, но точность падает. + +## Вариант 2. Прогноз, скачанный ЗАРАНЕЕ по проводу + +Обходит запрет на сеть в поле: перед выездом, на домашнем проводном интернете, +скачивается почасовой прогноз ветра по району и кладётся в базу. В поездке сеть +не нужна вообще. + +Прогноз при этом используется **не как истина, а как начальное приближение** +для варианта 1: он задаёт правдоподобный диапазон, а трек его уточняет. +Возражение про «меряют в другой точке» остаётся в силе, но начальное +приближение с известным смещением лучше, чем поиск вслепую. + +Спорный момент: прогноз может оказаться хуже, чем ничего, если приземный ветер +в лесополосах отличается от станционного в разы. Стоит проверить на нескольких +треках, прежде чем закладывать. + +## Вариант 3. Трубка Пито на руле (единственный способ мерить по-настоящему) + +Так устроены настоящие аэродинамические измерители для велосипеда (Notio, +Aeropod). Трубка, направленная вперёд, плюс датчик разности давлений дают +**скорость набегающего воздуха напрямую**, с частотой в десятки герц. + +Разность между воздушной скоростью и путевой скоростью с GPS, вместе с курсом, +даёт вектор ветра непрерывно, включая порывы. Сети не требуется вообще. + +- датчик разности давлений класса MPXV7002DP плюс микроконтроллер с BLE +- порядок цены комплектующих -- полторы-три тысячи рублей +- BLE в приложении уже есть, канал знакомый +- нужна калибровка (сравнением с вариантом 1 на кольцевом маршруте в штиль) + и защита от дождя и грязи +- трубка на руле уязвима при падении и при переноске велосипеда + +**Это единственный из перечисленных способов, который измеряет, а не оценивает.** +И единственный, который видит порывы, а не среднее. + +## Вариант 4. Флюгер на руле + +Дешевле Пито: измеряется только **угол** набегающего потока относительно +велосипеда, обычным поворотным датчиком. Скорости ветра не даёт, но сильно +сужает решение в варианте 1 -- направление перестаёт быть неизвестным, +остаётся подобрать одну величину вместо двух. + +На велосипеде хрупок и чувствителен к завихрениям от самого седока. + +## Вариант 5. Голосовой ввод по словам-триггерам (идея пользователя) + +Седок произносит «в лицо», «в спину», «боковой», «порыв», приложение ставит +отметку времени. Распознавание на Android умеет работать без сети. + +**Замечание, которое считаю важным.** Как измерение это слабо: оценка +субъективна, отметки редкие, а говорить на тяжёлом дыхании и в шуме ветра +неудобно -- причём шум ветра в микрофоне мешает именно тогда, когда ветер +и надо отметить. + +Но у этого есть куда более сильное применение: **не как измерение, +а как разметка для проверки автоматической оценки.** Десяток отметок +«в лицо» и «в спину» за поездку -- это готовый ответ, по которому можно +проверить и откалибровать вариант 1, не покупая железа. Если оценка по треку +совпадает с отметками -- ей можно верить и без отметок. + +При таком применении редкость и субъективность отметок почти не мешают: +проверяется знак и порядок величины, а не точное значение. + +## Вариант 6. Кнопка вместо голоса + +То же самое, но нажатием: выносная кнопка на руле по BLE либо кнопка +на гарнитуре. Надёжнее голоса на тяжёлом дыхании и в шуме, но выражает меньше +-- реально различимы два-три состояния, не больше. + +## Вариант 7. Шум ветра в микрофоне + +Обдув микрофона даёт характерный низкочастотный шум, растущий со скоростью +потока. Теоретически из него извлекается воздушная скорость. + +Практически: в кармане бесполезно, нужен вынесенный микрофон; шум дороги +и шин мешает; калибровка своя для каждого телефона и чехла. Дёшево проверить, +но рассчитывать не стоит. Упомянуто для полноты. + +--- + +## О чём хотелось бы услышать мнение сессии разработки + +1. **Стоит ли вообще ветер в модели**, или проще честно писать «мощность + на равнине оценена с точностью ±30%» и не усложнять. Разброс 33% измерен, + вопрос в том, окупается ли его устранение. +2. **Вариант 5 в роли разметки** (а не измерения) -- насколько это ложится + на текущий UI и на запись событий с отметкой времени. По цене внедрения + это самый дешёвый из осмысленных шагов, и он не требует решать заранее, + покупать ли датчик. +3. **Оправдан ли вариант 3.** Планка задана: без железа выходит направление + с точностью около 20° и сила с точностью до двух раз. Датчик за + несколько тысяч рублей должен дать заметно больше, иначе он не нужен. +4. **Не ломает ли вариант 2 принцип автономности.** Скачивание заранее + по проводу формально сеть в поле не требует, но привязывает точность + расчёта к тому, вспомнил ли пользователь нажать кнопку перед выездом. + Это тот же изъян, что и у кнопки «уточнить высоты». + +Готовый код обоих методов варианта 1 -- в `tools/track/track.py` +(функция `wind_by_passes` и секция «ветер» в `report`), выше описан +алгоритмически. diff --git a/docs/session-logs/2026-09-07_brouter-findings.md b/docs/session-logs/2026-09-07_brouter-findings.md new file mode 100644 index 0000000..bfbeafa --- /dev/null +++ b/docs/session-logs/2026-09-07_brouter-findings.md @@ -0,0 +1,39 @@ +# Сессия 2026-09-07: разбор BRouter под две идеи + +## Контекст +Пользователь спросил, разбирал ли я brouter apk. Не разбирал. В дереве оказался +`brouter-1.6.3.zip` (внутри два apk `btools.routingapp`, движок `brouter-1.6.3-all.jar`, +профили, доки) и `FitPro...apk` (не brouter). Цель разбора (уточнена пользователем): понять, +как BRouter строит маршруты с малым набором высоты, ради двух идей — +1) заполнять пропажу GPS кратчайшим путём по дороге; 2) оффлайн-планирование маршрута по точкам +в BalDav (замена онлайн-BRouter). + +## Действия +Работали по утверждённому плану `~/.claude/plans/merry-discovering-dream.md` (сохранён смысл ниже; +файл планов ротируется). Разбор read-only: листинг архива, чтение доков/профилей через stdout, +инспекция jar в памяти (python zipfile из BytesIO), извлечение строк байткода — распаковки на диск +и запуска Java не было. + +Выяснено: где дорога — сегменты `.rd5` (тайлы 5×5°, OSM в битстрим по lookups.dat, высоты из +SRTM-90м); роутинг — `btools.router.RoutingEngine` (matchWaypointsToNodes → поиск по графу, +стоимость из профиля `btools.expressions`); малый набор высоты — параметры профиля +uphillcost/downhillcost/cutoff + кинематика (totalMass/S_C_x=CdA/C_r/bikerPower, аналог модели +мощности BalDav); `shortest.brf` — чистый кратчайший путь (под идею 1). Вызов оффлайн: jar +`RouteServer ` + HTTP +`/brouter?lonlats=..&profile=..&format=geojson`; приложение BRouter (Server-Mode HTTP или intent); +встраивание jar (356 КБ) в Android-модуль BalDav. + +## Результат +Создан `docs/brouter-findings.md` — полный разбор: состав, данные, алгоритм, малый набор высоты, +3 способа вызова + встраивание, обе идеи со стыком к существующему коду (computeStats ветка isGap; +showRouteEditor/showRouteDetail/parseRouteGPX/routeElevStats в main.js), таблица вариантов +интеграции с рекомендацией (целевой — встраивание движка; сначала проверить ценность на +установленном приложении BRouter), открытые вопросы (лицензия abrensch/brouter, сегменты +качать/строить, хранилище на телефоне, профили). Реализацию идей НЕ делали — отдельные фичи. + +README не трогал: пользовательской фичи пока нет, это анализ. PROJECT_MAP — добавлен раздел +«Разборы (docs/)» с указателями на gps-model-findings и brouter-findings. + +## Следующие шаги +- Ответы пользователя на открытые вопросы §9 разбора → планировать идею 1 и/или идею 2. +- Опционально PoC: RouteServer на компе + сегменты региона (нужен JRE + загрузка, по согласию). diff --git a/frontend/index.html b/frontend/index.html index a9d2326..8170a98 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -23,6 +23,7 @@ + diff --git a/frontend/js/router.js b/frontend/js/router.js new file mode 100644 index 0000000..b18f935 --- /dev/null +++ b/frontend/js/router.js @@ -0,0 +1,20 @@ +// Offline routing via the embedded BRouter engine. +// JS → Rust command `brouter_route` → Kotlin (btools.router) → GPX back. +// BRouter.route(points, profile): +// points — array of [lon, lat] pairs (BRouter order; NOT [lat, lon]) +// profile — "trekking" (planning) | "shortest" (gap fill); default "trekking" +// Returns a GPX string (parse with parseRouteGPX) or throws with the engine's error. +// Needs the region .rd5 segments present on device (files/brouter/segments4). +window.BRouter = { + async route(points, profile) { + let lonlats = JSON.stringify((points || []).map(p => [p[0], p[1]])); + let raw = await window.__TAURI__.core.invoke("brouter_route", { + lonlats, + profile: profile || "trekking", + }); + let res; + try { res = JSON.parse(raw); } catch (e) { throw new Error("brouter: bad result: " + raw); } + if (!res.ok) throw new Error("brouter: " + (res.error || "unknown error")); + return res.gpx; + }, +}; diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts new file mode 100644 index 0000000..d2b3818 --- /dev/null +++ b/src-tauri/gen/android/app/build.gradle.kts @@ -0,0 +1,77 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("rust") +} + +val tauriProperties = Properties().apply { + val propFile = file("tauri.properties") + if (propFile.exists()) { + propFile.inputStream().use { load(it) } + } +} + +android { + compileSdk = 36 + namespace = "dev.baldav.app" + defaultConfig { + manifestPlaceholders["usesCleartextTraffic"] = "false" + applicationId = "dev.baldav.app" + minSdk = 24 + targetSdk = 36 + versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() + versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + } + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + isDebuggable = true + isJniDebuggable = true + isMinifyEnabled = false + packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") + jniLibs.keepDebugSymbols.add("*/x86/*.so") + jniLibs.keepDebugSymbols.add("*/x86_64/*.so") + } + } + getByName("release") { + isMinifyEnabled = true + proguardFiles( + *fileTree(".") { include("**/*.pro") } + .plus(getDefaultProguardFile("proguard-android-optimize.txt")) + .toList().toTypedArray() + ) + } + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + } +} + +rust { + rootDirRel = "../../../" +} + +dependencies { + // BRouter offline routing engine (MIT, github.com/abrensch/brouter) — embedded so + // routes are computed on-device without network. Called in-process from Kotlin + // (btools.router.RoutingEngine). Segments (.rd5) live in app data dir; profiles in assets. + implementation(files("libs/brouter-1.6.3-all.jar")) + implementation("androidx.webkit:webkit:1.14.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("androidx.activity:activity-ktx:1.10.1") + implementation("com.google.android.material:material:1.12.0") + // Optional fused location provider (opt-in via settings). Guarded at runtime so + // the app still works if Google Play services are absent — see GpsRecorderService. + implementation("com.google.android.gms:play-services-location:21.3.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.4") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") +} + +apply(from = "tauri.build.gradle.kts") \ No newline at end of file diff --git a/src-tauri/gen/android/app/libs/brouter-1.6.3-all.jar b/src-tauri/gen/android/app/libs/brouter-1.6.3-all.jar new file mode 100644 index 0000000..4310c1c Binary files /dev/null and b/src-tauri/gen/android/app/libs/brouter-1.6.3-all.jar differ diff --git a/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/lookups.dat b/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/lookups.dat new file mode 100644 index 0000000..5a9599e --- /dev/null +++ b/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/lookups.dat @@ -0,0 +1,938 @@ +---lookupversion:10 +---minorversion:13 + +---context:way + +highway;0029035962 residential +highway;0010319731 service +highway;0007688809 track +highway;0007656124 unclassified +highway;0004141444 footway +highway;0003493551 tertiary +highway;0002852601 path +highway;0002185240 secondary +highway;0001447719 primary +highway;0000699577 cycleway +highway;0000608469 trunk +highway;0000568118 living_street +highway;0000515044 motorway +highway;0000451760 motorway_link +highway;0000442502 steps +highway;0000360177 road +highway;0000318426 pedestrian +highway;0000210535 trunk_link +highway;0000192461 primary_link +highway;0000120758 secondary_link +highway;0000079637 tertiary_link +highway;0000070238 construction +highway;0000058257 bridleway +highway;0000039003 platform +highway;0000037192 proposed planned virtual +highway;0000010307 raceway +highway;0000003152 rest_area +highway;0000002942 abandoned disused razed demolished dismantled +highway;0000002631 services +highway;0000002133 corridor +highway;0000002093 crossing +highway;0000001440 bus_stop +highway;0000001274 yes +highway;0000000679 unsurfaced +highway;0000000108 byway +highway;0000000037 driveway +highway;0000000021 mini_roundabout +highway;0000000020 turning_loop + +tracktype;0000887965 grade2 +tracktype;0000868414 grade3 +tracktype;0000595882 grade1 +tracktype;0000568372 grade4 +tracktype;0000405959 grade5 + +surface;0002497676 asphalt +surface;0001568957 paved +surface;0001562253 unpaved +surface;0000727427 gravel +surface;0000560191 ground +surface;0000350378 dirt +surface;0000237226 grass +surface;0000212587 concrete concrete:plates concrete:lanes +surface;0000188743 paving_stones paving_stones:30 paving_stones:20 +surface;0000113800 cobblestone cobblestone:flattened +surface;0000093164 compacted +surface;0000091171 sand dirt/sand +surface;0000023293 wood +surface;0000019915 pebblestone +surface;0000012866 fine_gravel +surface;0000010681 earth +surface;0000007331 sett +surface;0000005778 mud +surface;0000004549 grass_paver +surface;0000004398 clay +surface;0000003760 metal + +maxspeed;0001058313 50 30_mph 30mph +maxspeed;0000860780 30 20_mph 20mph +maxspeed;0000025232 10 5 7 15 +maxspeed;0000083989 20 10_mph 10mph 15_mph 15mph +maxspeed;0000195097 40 45 25_mph 25mph +maxspeed;0000204646 60 35_mph 35mph 40_mph 40mph +maxspeed;0000130108 70 45_mph 45mph +maxspeed;0000225071 80 50_mph 50mph +maxspeed;0000106719 90 55_mph 55mph +maxspeed;0000134522 100 60_mph 60mph 65_mph 65mph +maxspeed;0000025242 110 70_mph 70mph +maxspeed;0000038763 120 75_mph 75mph +maxspeed;0000026953 130 +maxspeed;0000138654 urban RO:urban RU:urban FR:urban IT:urban AT:urban DE:urban UA:urban +maxspeed;0000138654 rural RO:rural RU:rural FR:rural IT:rural AT:rural DE:rural UA:rural + +service;0001433919 parking_aisle +service;0001305879 driveway +service;0000382788 alley +service;0000018777 drive-through drive_through +service;0000008290 emergency_access +service;0000003138 bus +service;0000001250 parking +service;0000001159 logging + +lit;0000809223 yes + +lanes;0002838405 2 +lanes;0000718138 1 +lanes;0000259502 3 +lanes;0000141651 4 +lanes;0000018473 -1 +lanes;0000017934 5 +lanes;0000008241 6 +lanes;0000003643 1.5 +lanes;0000001087 7 + +access;0002688349 private +access;0000319927 yes +access;0000144799 no +access;0000140215 permissive +access;0000108802 destination +access;0000099899 agricultural forestry +access;0000039934 designated official +access;0000011813 customers +access;0000004007 delivery +access;0000000100 psv +access;0000000100 hov + +foot;0001659694 yes allowed Yes +foot;0000424847 designated official +foot;0000202364 no +foot;0000053031 permissive +foot;0000011661 destination +foot;0000007289 private +foot;0000000167 use_sidepath sidewalk + +bicycle;0001245560 yes allowed +bicycle;0000452059 no +bicycle;0000324902 designated official +bicycle;0000025707 dismount +bicycle;0000020440 permissive +bicycle;0000008286 private +bicycle;0000001553 destination +bicycle;0000000719 use_sidepath use_cycleway +bicycle;0000000385 mtb +bicycle;0000000117 opposite + +motorcar;0000135124 no +motorcar;0000045407 yes +motorcar;0000021494 agricultural forestry +motorcar;0000012090 destination +motorcar;0000008733 private +motorcar;0000005757 designated official +motorcar;0000004116 permissive +motorcar;0000000979 restricted +motorcar;0000000100 psv +motorcar;0000000100 hov + +motor_vehicle;0000212692 no +motor_vehicle;0000184982 yes +motor_vehicle;0000045128 private +motor_vehicle;0000032622 agricultural forestry agricultural;forestry agricultural,forestry +motor_vehicle;0000025396 designated official +motor_vehicle;0000025092 destination +motor_vehicle;0000010895 permissive +motor_vehicle;0000000175 emergency Emergency +motor_vehicle;0000000100 psv +motor_vehicle;0000000100 hov + +motorcycle;0000092079 no +motorcycle;0000027978 yes +motorcycle;0000014652 agricultural forestry +motorcycle;0000008862 destination +motorcycle;0000004877 designated official +motorcycle;0000003936 private +motorcycle;0000002209 permissive +motorcycle;0000000100 psv +motorcycle;0000000100 hov + +vehicle;0000030218 no +vehicle;0000013333 destination +vehicle;0000011692 yes +vehicle;0000007147 agricultural forestry agricultural;forestry +vehicle;0000006305 private +vehicle;0000001294 permissive +vehicle;0000000105 designated +vehicle;0000000100 psv +vehicle;0000000100 hov + +horse;0000227398 no +horse;0000144432 yes +horse;0000014566 designated +horse;0000007223 permissive +horse;0000004755 private +horse;0000000983 official +horse;0000000968 unknown +horse;0000000205 destination + +wheelchair;0000036603 no +wheelchair;0000028451 yes +wheelchair;0000002713 limited +wheelchair;0000001043 unknown +wheelchair;0000000439 designated official +wheelchair;0000000184 destination + +hgv;0000206836 designated +hgv;0000071222 yes +hgv;0000043783 no +hgv;0000019115 destination +hgv;0000005273 delivery +hgv;0000003055 local +hgv;0000001088 agricultural forestry +hgv;0000000461 private +hgv;0000000320 unsuitable +hgv;0000000306 permissive + +cycleway;0000137526 no +cycleway;0000124777 lane +cycleway;0000106948 track +cycleway;0000044652 opposite +cycleway;0000011237 shared +cycleway;0000007312 opposite_lane +cycleway;0000005737 shared_lane +cycleway;0000002533 yes +cycleway;0000002356 opposite_track +cycleway;0000001945 share_busway +cycleway;0000001883 none +cycleway;0000001705 crossing +cycleway;0000001560 unmarked_lane +cycleway;0000001542 right +cycleway;0000001291 segregated +cycleway;0000001065 both +cycleway;0000000892 left +cycleway;0000000399 street +cycleway;0000000344 shoulder +cycleway;0000000326 designated +cycleway;0000000247 proposed planned virtual +cycleway;0000000224 cyclestreet +cycleway;0000000172 path +cycleway;0000000154 sidewalk + +footway;0000104998 sidewalk +footway;0000065943 crossing +footway;0000012342 both +footway;0000008363 none +footway;0000005903 right +footway;0000004159 left +footway;0000003966 no +footway;0000001093 yes +footway;0000000558 separate + +segregated;0000224960 no +segregated;0000051124 yes + +sidewalk;0000194579 none +sidewalk;0000111468 both +sidewalk;0000052950 right +sidewalk;0000024489 left +sidewalk;0000012916 no +sidewalk;0000005725 separate +sidewalk;0000001950 yes + +mtb:scale;0000114272 0 +mtb:scale;0000068284 1 +mtb:scale;0000027311 2 +mtb:scale;0000011529 3 +mtb:scale;0000003666 4 +mtb:scale;0000001957 0+ +mtb:scale;0000001472 5 +mtb:scale;0000000498 1+ +mtb:scale;0000000478 1- +mtb:scale;0000000268 0- +mtb:scale;0000000177 2- +mtb:scale;0000000131 2+ +mtb:scale;0000000115 6 + +sac_scale;0000150704 hiking +sac_scale;0000070463 mountain_hiking +sac_scale;0000010993 demanding_mountain_hiking +sac_scale;0000004549 alpine_hiking +sac_scale;0000001620 demanding_alpine_hiking +sac_scale;0000000831 yes +sac_scale;0000000712 difficult_alpine_hiking +sac_scale;0000000265 T1-hiking + +noexit;0000118665 yes + +motorroad;0000056844 yes + +oneway;0005387257 yes +oneway;0001455407 no +oneway;0000139188 -1 +oneway;0000000892 reversible +oneway;0000000756 1 +oneway;0000000481 true + +junction;0000321066 roundabout +junction;0000002828 spui +junction;0000002134 jughandle +junction;0000001493 approach +junction;0000000100 circular + +bridge;0001842517 yes viaduct true suspension + +tunnel;0000247305 yes +tunnel;0000016890 building_passage +tunnel;0000004237 no +tunnel;0000000265 passage +tunnel;0000000241 culvert +tunnel;0000000122 avalanche_protector +tunnel;0000000114 covered + +lcn;0000073956 yes +lcn;0000002631 proposed + +oneway:bicycle;0000012034 no +oneway:bicycle;0000005217 yes +oneway:bicycle;0000000161 opposite + +cycleway:right;0000012522 lane Lane +cycleway:right;0000006644 track +cycleway:right;0000000971 share_busway +cycleway:right;0000000686 sidepath +cycleway:right;0000000410 shared_lane +cycleway:right;0000000104 opposite_lane +cycleway:right;0000000058 opposite_track +cycleway:right;0000000045 no none +cycleway:right;0000000037 yes +cycleway:right;0000000004 opposite + +cycleway:left;0000005134 lane Lane +cycleway:left;0000003169 track +cycleway:left;0000000656 share_busway +cycleway:left;0000000608 opposite_lane +cycleway:left;0000000475 sidepath +cycleway:left;0000000257 shared_lane +cycleway:left;0000000246 no none +cycleway:left;0000000130 opposite_track +cycleway:left;0000000053 opposite +cycleway:left;0000000014 yes + +incline;0000052784 up +incline;0000035413 down +incline;0000001628 yes +incline;0000000779 steep +incline;0000000861 3% 0 1 2 3 0% 1% 2% 3% +incline;0000000724 5% 4 5 4% +incline;0000000530 8% 6 7 8 6% 7% +incline;0000003109 10% 9 10 9% 10° +incline;0000001297 15% 11 12 13 14 15 11% 12% 13% 14% +incline;0000000997 20% 16 17 18 19 20 16% 17% 18% 19% +incline;0000000409 25% 21 22 23 24 25 21% 22% 23% 24% +incline;0000000263 30% 30 40 50 40% 50% +incline;0000000861 -3% -1 -2 -3 -1% -2% -3% +incline;0000000724 -5% -4 -5 -4% +incline;0000000530 -8% -6 -7 -8 -6% -7% +incline;0000001515 -10% -9 -10 -9% -10° +incline;0000001297 -15% -11 -12 -13 -14 -15 -11% -12% -13% -14% +incline;0000000997 -20% -16 -17 -18 -19 -20 -16% -17% -18% -19% +incline;0000000409 -25% -21 -22 -23 -24 -25 -21% -22% -23% -24% +incline;0000000172 -30% -30 -40 -50 -40% -50% + +toll;0000090536 yes true + +railway;0000157547 rail +railway;0000019316 abandoned +railway;0000016982 tram +railway;0000014387 platform +railway;0000011143 disused +railway;0000004623 light_rail +railway;0000002982 subway +railway;0000002422 narrow_gauge +railway;0000001960 razed +railway;0000001859 preserved + +seamark:type;0001564 recommended_track +seamark:type;0000522 fairway + +waterway;0000016046 river +waterway;0000009496 canal +waterway;0000007876 riverbank +waterway;0000002202 weir +waterway;0000001364 dam +waterway;0000000386 lock +waterway;0000000321 tidal_flat_slough +waterway;0000000179 wadi +waterway;0000000126 dock +waterway;0000000113 fish_pass +waterway;0000000086 boatyard +waterway;0000000071 fairway +waterway;0000000059 lock_gate + +boat;0000019888 no +boat;0000002718 yes +boat;0000000232 private +boat;0000000064 permissive +boat;0000000045 designated + +motorboat;0000001077 yes +motorboat;0000000808 no +motorboat;0000000025 private privat + +route;0000000850 ferry +route;0000000539 hiking +route;0000000505 bicycle +route;0000000454 ski +route;0000000413 mtb +route;0000000194 canoe +route;0000000151 road +route;0000000104 bus + +smoothness;0000068136 good +smoothness;0000042124 bad Bad +smoothness;0000040763 intermediate +smoothness;0000033941 excellent +smoothness;0000012683 very_bad +smoothness;0000009837 horrible terrible +smoothness;0000003515 very_horrible +smoothness;0000000919 impassable +smoothness;0000000251 robust_wheels +smoothness;0000000221 high_clearance +smoothness;0000000132 very_good +smoothness;0000000083 off_road_wheels +smoothness;0000000057 medium Medium average +smoothness;0000000048 poor +smoothness;0000000039 rough + +rcn;0000014518 yes +rcn;0000002862 proposed + +ncn;0000002941 yes +ncn;0000001036 proposed + +ford;0000020552 yes +ford;0000000289 stepping_stones + +trail_visibility;0000067438 good +trail_visibility;0000041280 intermediate +trail_visibility;0000039801 excellent +trail_visibility;0000023482 bad +trail_visibility;0000005853 horrible +trail_visibility;0000002222 no + +class:bicycle:mtb;0000002079 1 +1 +class:bicycle:mtb;0000001191 0 +class:bicycle:mtb;0000001089 2 +2 +class:bicycle:mtb;0000000703 -1 +class:bicycle:mtb;0000000234 -2 +class:bicycle:mtb;0000000140 3 +3 +class:bicycle:mtb;0000000068 -3 + +class:bicycle;0000002842 1 +1 +class:bicycle;0000000595 -1 +class:bicycle;0000000533 2 +2 +class:bicycle;0000000516 -2 +class:bicycle;0000000245 -3 +class:bicycle;0000000170 0 +class:bicycle;0000000108 3 +3 + +route_bicycle_icn;0000088753 yes +route_bicycle_icn;0000000001 proposed +route_bicycle_ncn;0000268180 yes +route_bicycle_ncn;00000000001 proposed +route_bicycle_rcn;0000718163 yes +route_bicycle_rcn;00000000001 proposed +route_bicycle_lcn;0000469215 yes +route_bicycle_lcn;00000000001 proposed + +route_bicycle_;0000024662 yes +route_bicycle_radweit;0000004604 yes + +route_hiking_iwn;0000056005 yes +route_hiking_nwn;0000315813 yes +route_hiking_rwn;0000343219 yes +route_hiking_lwn;0000359332 yes +route_hiking_;0000103733 yes + +route_foot_nwn;0000047923 yes +route_foot_lwn;0000135371 yes +route_foot_rwn;0000115325 yes +route_foot_;0000070583 yes + +route_mtb_;0000066263 yes +route_mtb_lcn;0000023718 yes +route_mtb_ncn;0000004853 yes +route_mtb_rcn;0000013321 yes +route_mtb_mtb;0000006853 yes +route_bicycle_mtb;0000002240 yes + +brouter_route_placeholder_dummy_01;0000000001 dummy +brouter_route_placeholder_dummy_02;0000000001 dummy +brouter_route_placeholder_dummy_03;0000000001 dummy +brouter_route_placeholder_dummy_04;0000000001 dummy +brouter_route_placeholder_dummy_05;0000000001 dummy +brouter_route_placeholder_dummy_06;0000000001 dummy +brouter_route_placeholder_dummy_07;0000000001 dummy +brouter_route_placeholder_dummy_08;0000000001 dummy +brouter_route_placeholder_dummy_09;0000000001 dummy +brouter_route_placeholder_dummy_10;0000000001 dummy +brouter_route_placeholder_dummy_11;0000000001 dummy +brouter_route_placeholder_dummy_12;0000000001 dummy +brouter_route_placeholder_dummy_13;0000000001 dummy +brouter_route_placeholder_dummy_14;0000000001 dummy +brouter_route_placeholder_dummy_15;0000000001 dummy +brouter_route_placeholder_dummy_16;0000000001 dummy +brouter_route_placeholder_dummy_17;0000000001 dummy +brouter_route_placeholder_dummy_18;0000000001 dummy +brouter_route_placeholder_dummy_19;0000000001 dummy +brouter_route_placeholder_dummy_20;0000000001 dummy +brouter_route_placeholder_dummy_21;0000000001 dummy + +ramp:bicycle;0000001305 yes both permissive right left +ramp:bicycle;0000000385 no + +ramp:stroller;0000001099 yes +ramp:stroller;0000000326 no + +ramp:wheelchair;0000000610 yes +ramp:wheelchair;0000000439 no + +ramp:luggage;0000000162 no +ramp:luggage;0000000054 yes automatic manual + +estimated_traffic_class;0000000001 1 +estimated_traffic_class;0000000001 2 +estimated_traffic_class;0000000001 3 +estimated_traffic_class;0000000001 4 +estimated_traffic_class;0000000001 5 +estimated_traffic_class;0000000001 6 +estimated_traffic_class;0000000001 7 + +mtb:scale:uphill;0000018869 0 0+ 0- +mtb:scale:uphill;0000015578 1 1+ 1- +mtb:scale:uphill;0000012338 2 2+ 2- +mtb:scale:uphill;0000009099 3 3+ 3- +mtb:scale:uphill;0000005825 4 4+ 4- +mtb:scale:uphill;0000004628 5 5+ 5- + +crossing;0000101049 zebra +crossing;0000017509 unmarked +crossing;0000013817 traffic_signals +crossing;0000011062 uncontrolled +crossing;0000001722 yes +crossing;0000001678 island +crossing;0000000457 marked +crossing;0000000131 pedestrian_signals +crossing;0000000122 no + +informal;0000002424 yes + +indoor;0000058418 yes +indoor;0000025038 room +indoor;0000005295 wall +indoor;0000004322 corridor +indoor;0000002410 area +indoor;0000000816 column +indoor;0000000568 no +indoor;0000000129 shop +indoor;0000000099 steps + +4wd_only;0000008129 yes Yes +4wd_only;0000000487 recommended +4wd_only;0000000041 no + +concrete;0000000043 plates +concrete;0000000013 lanes + +bus;0001178365 yes +bus;0000006419 designated +bus;0000005602 no +bus;0000001424 urban + +psv;0000072077 yes +psv;0000007456 no +psv;0000007428 designated official + +hov;0000006684 lane +hov;0000003258 designated +hov;0000002162 no +hov;0000001512 yes + +busway;0000000000 opposite opposite_lane opposite_track +busway:left;0000000000 opposite opposite_lane opposite_track +busway:right;0000000000 opposite opposite_lane opposite_track + +cycleway:left:oneway;0000000769 yes +cycleway:left:oneway;0000001595 no +cycleway:left:oneway;0000000927 -1 + +cycleway:right:oneway;0000003084 yes +cycleway:right:oneway;0000002499 no +cycleway:right:oneway;0000000017 -1 + +zone:maxspeed;0000001616 20 DE:20 FR:20 +zone:maxspeed;0000063721 30 DE:30 FR:30 BE:30 HU:30 NO:30 AT:30 ES:30 NL:30 + +cycleway:surface;0000002609 asphalt +cycleway:surface;0000000150 paved +cycleway:surface;0000000012 unpaved +cycleway:surface;0000000010 gravel +cycleway:surface;0000000157 concrete concrete:plates concrete:lanes +cycleway:surface;0000002239 paving_stones paving_stones:30 paving_stones:20 +cycleway:surface;0000000011 cobblestone cobblestone:flattened +cycleway:surface;0000000013 compacted +cycleway:surface;0000000006 fine_gravel +cycleway:surface;0000000011 sett + +maxspeed:backward;0001058313 50 30_mph 30mph +maxspeed:backward;0000860780 30 20_mph 20mph +maxspeed:backward;0000025232 10 5 7 15 +maxspeed:backward;0000083989 20 10_mph 10mph 15_mph 15mph +maxspeed:backward;0000195097 40 45 25_mph 25mph +maxspeed:backward;0000204646 60 35_mph 35mph 40_mph 40mph +maxspeed:backward;0000130108 70 45_mph 45mph +maxspeed:backward;0000225071 80 50_mph 50mph +maxspeed:backward;0000106719 90 55_mph 55mph +maxspeed:backward;0000134522 100 60_mph 60mph 65_mph 65mph +maxspeed:backward;0000025242 110 70_mph 70mph +maxspeed:backward;0000038763 120 75_mph 75mph +maxspeed:backward;0000026953 130 +maxspeed:backward;0000138654 urban RO:urban RU:urban FR:urban IT:urban AT:urban DE:urban UA:urban +maxspeed:backward;0000138654 rural RO:rural RU:rural FR:rural IT:rural AT:rural DE:rural UA:rural + +maxspeed:forward;0001058313 50 30_mph 30mph +maxspeed:forward;0000860780 30 20_mph 20mph +maxspeed:forward;0000025232 10 5 7 15 +maxspeed:forward;0000083989 20 10_mph 10mph 15_mph 15mph +maxspeed:forward;0000195097 40 45 25_mph 25mph +maxspeed:forward;0000204646 60 35_mph 35mph 40_mph 40mph +maxspeed:forward;0000130108 70 45_mph 45mph +maxspeed:forward;0000225071 80 50_mph 50mph +maxspeed:forward;0000106719 90 55_mph 55mph +maxspeed:forward;0000134522 100 60_mph 60mph 65_mph 65mph +maxspeed:forward;0000025242 110 70_mph 70mph +maxspeed:forward;0000038763 120 75_mph 75mph +maxspeed:forward;0000026953 130 +maxspeed:forward;0000138654 urban RO:urban RU:urban FR:urban IT:urban AT:urban DE:urban UA:urban +maxspeed:forward;0000138654 rural RO:rural RU:rural FR:rural IT:rural AT:rural DE:rural UA:rural + +embedded_rails;0000000928 tram +embedded_rails;0000000007 yes +embedded_rails;0000000003 rail + +living_street;0000000404 yes + +sidewalk:bicycle;0000000439 yes designated +sidewalk:left:bicycle;0000001722 yes designated +sidewalk:right:bicycle;0000002667 yes designated + +bicycle_road;0000006521 yes designated + +construction;0000144871 yes +construction;0000008214 minor +construction;0029035962 residential +construction;0010319731 service +construction;0007688809 track +construction;0007656124 unclassified +construction;0004141444 footway +construction;0003493551 tertiary +construction;0002852601 path +construction;0002185240 secondary +construction;0001447719 primary +construction;0000699577 cycleway +construction;0000608469 trunk +construction;0000568118 living_street +construction;0000515044 motorway +construction;0000451760 motorway_link +construction;0000442502 steps +construction;0000360177 road +construction;0000318426 pedestrian +construction;0000210535 trunk_link +construction;0000192461 primary_link +construction;0000120758 secondary_link +construction;0000079637 tertiary_link +construction;0000070238 construction +construction;0000058257 bridleway +construction;0000039003 platform +construction;0000037192 proposed +construction;0000010307 raceway +construction;0000003152 rest_area +construction;0000002942 abandoned +construction;0000002631 services +construction;0000002133 corridor +construction;0000002093 crossing +construction;0000001440 bus_stop +construction;0000001274 yes +construction;0000000679 unsurfaced +construction;0000000108 byway +construction;0000000037 driveway +construction;0000000021 mini_roundabout +construction;0000000020 turning_loop + + ---context:node + +highway;0001314954 bus_stop +highway;0001130090 crossing +highway;0001031274 turning_circle +highway;0000609262 traffic_signals +highway;0000306900 street_lamp +highway;0000136339 stop +highway;0000105097 motorway_junction +highway;0000058076 give_way +highway;0000049111 mini_roundabout +highway;0000030072 milestone +highway;0000017567 speed_camera +highway;0000013806 emergency_access_point +highway;0000009721 platform +highway;0000007369 passing_place +highway;0000005939 ford +highway;0000004831 rest_area +highway;0000003535 elevator +highway;0000002572 turning_loop +highway;0000002540 steps +highway;0000002493 services +highway;0000002133 emergency_bay +highway;0000001372 residential +highway;0000001324 street_light +highway;0000001147 incline_steep +highway;0000001101 stile +highway;0000000904 incline +highway;0000000819 service +highway;0000000817 traffic_calming +highway;0000000662 path +highway;0000000603 footway +highway;0000000438 track +highway;0000000436 no +highway;0000000353 door +highway;0000000283 level_crossing +highway;0000000267 yes +highway;0000000262 road +highway;0000000244 construction +highway;0000000214 unclassified +highway;0000000213 proposed +highway;0000000197 junction +highway;0000000176 distance_marker +highway;0000000158 noexit +highway;0000000155 unknown +highway;0000000134 traffic_sign +highway;0000000123 tertiary +highway;0000000115 trailhead +highway;0000000113 priority_to_right +highway;0000000113 culvert +highway;0000000100 +highway;0000000046 toll_bridge +highway;0000000037 city_entry +highway;0000002967 traffic_mirror +highway;0000001724 priority + +barrier;0000606512 gate +barrier;0000164120 bollard +barrier;0000112184 lift_gate +barrier;0000046779 stile +barrier;0000046255 cycle_barrier +barrier;0000038597 entrance +barrier;0000027579 block +barrier;0000023074 toll_booth +barrier;0000016782 cattle_grid +barrier;0000016154 kissing_gate +barrier;0000003182 turnstile +barrier;0000003160 fence +barrier;0000002701 border_control +barrier;0000002536 sally_port +barrier;0000002504 chain +barrier;0000002470 door +barrier;0000002089 swing_gate +barrier;0000001912 bump_gate +barrier;0000001856 yes +barrier;0000001683 hampshire_gate +barrier;0000000445 wall +barrier;0000000440 bus_trap +barrier;0000000435 ditch +barrier;0000000420 debris +barrier;0000000381 log +barrier;0000000336 chicane +barrier;0000000316 kerb +barrier;0000000270 sump_buster +barrier;0000000268 obstacle +barrier;0000000223 no +barrier;0000000213 horse_stile +barrier;0000000210 full-height_turnstile +barrier;0000000176 windfall +barrier;0000000168 spikes +barrier;0000000168 checkpoint +barrier;0000000166 hedge +barrier;0000000164 footgate +barrier;0000000141 tree +barrier;0000000133 guard_rail +barrier;0000000129 bar +barrier;0000000124 fallen_tree +barrier;0000000118 jersey_barrier +barrier;0000000114 motorcycle_barrier +barrier;0000000114 barrier +barrier;0000000108 rope +barrier;0000000095 stone +barrier;0000000069 traffic_crossing_pole + + +access;0000078544 private +access;0000014933 yes public +access;0000014456 no +access;0000008670 permissive +access;0000006316 destination customers +access;0000000973 agricultural forestry +access;0000000942 designated official + +foot;0000272169 yes +foot;0000036271 no +foot;0000001118 designated official +foot;0000000960 private +foot;0000000833 permissive +foot;0000000127 destination + +bicycle;0000267569 yes +bicycle;0000067796 no +bicycle;0000004075 designated official +bicycle;0000002596 dismount +bicycle;0000000498 permissive +bicycle;0000000359 private +bicycle;0000000075 destination + +motorcar;0000038901 yes +motorcar;0000009323 no +motorcar;0000000895 private +motorcar;0000000216 destination +motorcar;0000000214 permissive +motorcar;0000000093 agricultural forestry +motorcar;0000000084 designated official + +motor_vehicle;0000000066 yes +motor_vehicle;0000000001 permissive +motor_vehicle;0000000000 designated official +motor_vehicle;0000000030 destination +motor_vehicle;0000000073 agricultural forestry +motor_vehicle;0000000136 private +motor_vehicle;0000000469 no + +motorcycle;0000028697 yes +motorcycle;0000007061 no +motorcycle;0000000243 private +motorcycle;0000000237 designated +motorcycle;0000000117 destination +motorcycle;0000000080 permissive +motorcycle;0000000029 agricultural forestry + +vehicle;0000002176 no +vehicle;0000000576 yes +vehicle;0000000556 private +vehicle;0000000262 destination +vehicle;0000000138 agricultural forestry +vehicle;0000000105 permissive +vehicle;0000000003 designated official + +horse;0000021837 yes +horse;0000010811 no +horse;0000000105 private +horse;0000000065 permissive +horse;0000000053 designated official +horse;0000000009 critical +horse;0000000007 destination + +wheelchair;0000203478 yes true +wheelchair;0000100082 no false +wheelchair;0000080430 limited +wheelchair;0000002769 designated official +wheelchair;0000000005 private +wheelchair;0000000005 permissive +wheelchair;0000000139 bad +wheelchair;0000000031 half +wheelchair;0000000005 partial + +hgv;0000001141 no +hgv;0000000359 yes true +hgv;0000000111 destination +hgv;0000000108 designated official +hgv;0000000065 private +hgv;0000000053 delivery +hgv;0000000016 agricultural forestry +hgv;0000000010 permissive + +crossing;0000251032 uncontrolled +crossing;0000200387 traffic_signals +crossing;0000029717 unmarked +crossing;0000022119 island +crossing;0000006981 zebra +crossing;0000003897 no +crossing;0000002166 yes + +railway;0000312710 level_crossing +railway;0000078746 station +railway;0000067876 buffer_stop +railway;0000056184 switch +railway;0000049600 crossing +railway;0000037871 tram_stop +railway;0000024038 halt +railway;0000014285 subway_entrance +railway;0000010890 signal + +waterway;0000004698 weir +waterway;0000001647 lock_gate +waterway;0000000425 waterfall +waterway;0000000337 take_right_side +waterway;0000000332 take_left_side +waterway;0000000219 milestone +waterway;0000000187 depth +waterway;0000000170 lock + +noexit;0000195286 yes + +entrance;0000301732 yes +entrance;0000159853 main +entrance;0000025621 staircase +entrance;0000006666 home +entrance;0000005428 service +entrance;0000001853 emergency +entrance;0000001144 exit +entrance;0000000953 residence +entrance;0000000620 garage +entrance;0000000558 entrance +entrance;0000000504 main_entrance +entrance;0000000439 secondary_entrance +entrance;0000000285 shop +entrance;0000000258 private + +traffic_calming;0000045987 bump bump;choker +traffic_calming;0000040022 hump hump;choker +traffic_calming;0000012499 table table;choker +traffic_calming;0000006808 yes * +traffic_calming;0000005754 cushion cushion;choker +traffic_calming;0000005466 choker choker;cushion choker;hump choker;table choker;bump +traffic_calming;0000005305 island +traffic_calming;0000004686 chicane +traffic_calming;0000004032 rumble_strip +traffic_calming;0000000847 speed_bump +traffic_calming;0000000186 dip + +ford;0000037927 yes +ford;0000000310 stepping_stones + +direction;0000274642 forward +direction;0000249637 backward +direction;0000021634 both + +traffic_signals:direction;0000062645 forward +traffic_signals:direction;0000033961 backward +traffic_signals:direction;0000007309 both diff --git a/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/shortest.brf b/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/shortest.brf new file mode 100644 index 0000000..a4cc7f3 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/shortest.brf @@ -0,0 +1,149 @@ +---context:global # following code refers to global config + +# the elevation parameters + +assign downhillcost 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +assign turnInstructionMode = 1 # 0=none, 1=auto-choose, 2=locus-style, 3=osmand-style, 4=comment-style, 5=gpsies-style, 6=orux-style +assign validForFoot 1 + +---context:way # following code refers to way-tags + +assign any_cycleroute or route_bicycle_icn=yes or route_bicycle_ncn=yes or route_bicycle_rcn=yes route_bicycle_lcn=yes +assign nodeaccessgranted or any_cycleroute lcn=yes + +assign turncost 0 + +assign initialclassifier = + if route=ferry then 1 + else 0 + +assign initialcost switch route=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or any_cycleroute + switch bicycle= + switch bicycle_road=yes + 1 + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +assign accesspenalty switch or bikeaccess footaccess 0 100000 + +assign costfactor + add accesspenalty + + switch and highway= not route=ferry 100000 + + switch route=ferry 5.67 + switch or highway=motorway highway=motorway_link 100000 + switch or highway=proposed highway=abandoned 100000 + 1 + +# way priorities used for voice hint generation + +assign isresidentialorliving = or highway=residential|living_street living_street=yes + +assign priorityclassifier = + + if ( highway=motorway ) then 30 + else if ( highway=motorway_link ) then 29 + else if ( highway=trunk ) then 28 + else if ( highway=trunk_link ) then 27 + else if ( highway=primary ) then 26 + else if ( highway=primary_link ) then 25 + else if ( highway=secondary ) then 24 + else if ( highway=secondary_link ) then 23 + else if ( highway=tertiary ) then 22 + else if ( highway=tertiary_link ) then 21 + else if ( highway=unclassified ) then 20 + else if ( isresidentialorliving ) then 6 + else if ( highway=service ) then 6 + else if ( highway=cycleway ) then 6 + else if ( or bicycle=designated bicycle_road=yes ) then 6 + else if ( highway=track ) then if tracktype=grade1 then 6 else 4 + else if ( highway=bridleway|road|path|footway ) then 4 + else if ( highway=steps ) then 2 + else if ( highway=pedestrian ) then 2 + else 0 + +# some more classifying bits used for voice hint generation... + +assign isroundabout = junction=roundabout +assign islinktype = highway=motorway_link|trunk_link|primary_link|secondary_link|tertiary_link +assign isgoodforcars = if greater priorityclassifier 6 then true + else if ( or isresidentialorliving highway=service ) then true + else if ( and highway=track tracktype=grade1 ) then true + else false + + +# ... encoded into a bitmask + +assign classifiermask add multiply isroundabout 4 + add multiply islinktype 8 + multiply isgoodforcars 16 + +# include `smoothness=` tags in the response's WayTags for track analysis +assign dummyUsage = smoothness= + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or nodeaccessgranted=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost switch or bikeaccess footaccess 0 1000000 diff --git a/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/trekking.brf b/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/trekking.brf new file mode 100644 index 0000000..8a4b94d --- /dev/null +++ b/src-tauri/gen/android/app/src/main/assets/brouter/profiles2/trekking.brf @@ -0,0 +1,330 @@ +# *** The trekking profile is for slow travel +# *** and avoiding car traffic, but still with +# *** a focus on approaching your destination +# *** efficiently. + +---context:global # following code refers to global config + +# Bike profile +assign validForBikes = true + +# Use the following switches to change behaviour +assign allow_steps = true # %allow_steps% | Set to false to disallow steps | boolean +assign allow_ferries = true # %allow_ferries% | set to false to disallow ferries | boolean +assign ignore_cycleroutes = false # %ignore_cycleroutes% | Set to true for better elevation results | boolean +assign stick_to_cycleroutes = false # %stick_to_cycleroutes% | Set to true to just follow cycleroutes | boolean +assign avoid_unsafe = false # %avoid_unsafe% | Set to true to avoid standard highways | boolean +assign considerTurnRestrictions = true # %considerTurnRestrictions% | Set to true to take turn restrictions into account | boolean +assign processUnusedTags = false # %processUnusedTags% | Set to true to output unused tags in data tab | boolean + +# Change elevation parameters +assign consider_elevation = true # %consider_elevation% | Set to false to ignore elevation in routing | boolean +assign downhillcost = 60 # %downhillcost% | Cost for going downhill | number +assign downhillcutoff = 1.5 # %downhillcutoff% | Gradients below this value in percents are not counted. | number +assign uphillcost = 0 # %uphillcost% | Cost for going uphill | number +assign uphillcutoff = 1.5 # %uphillcutoff% | Gradients below this value in percents are not counted. | number + +assign downhillcost = if consider_elevation then downhillcost else 0 +assign uphillcost = if consider_elevation then uphillcost else 0 + +# Kinematic model parameters (travel time computation) +assign totalMass = 90 # %totalMass% | Mass (in kg) of the bike + biker, for travel time computation | number +assign maxSpeed = 45 # %maxSpeed% | Absolute maximum speed (in km/h), for travel time computation | number +assign S_C_x = 0.225 # %S_C_x% | Drag coefficient times the reference area (in m^2), for travel time computation | number +assign C_r = 0.01 # %C_r% | Rolling resistance coefficient (dimensionless), for travel time computation | number +assign bikerPower = 100 # %bikerPower% | Average power (in W) provided by the biker, for travel time computation | number + +# Turn instructions settings +assign turnInstructionMode = 1 # %turnInstructionMode% | Mode for the generated turn instructions | [0=none, 1=auto-choose, 2=locus-style, 3=osmand-style, 4=comment-style, 5=gpsies-style, 6=orux-style] +assign turnInstructionCatchingRange = 40 # %turnInstructionCatchingRange% | Within this distance (in m) several turning instructions are combined into one and the turning angles are better approximated to the general direction | number +assign turnInstructionRoundabouts = true # %turnInstructionRoundabouts% | Set to "false" to avoid generating special turning instructions for roundabouts | boolean + + +---context:way # following code refers to way-tags + +# classifier constants +assign classifier_none = 1 +assign classifier_ferry = 2 + +# +# pre-calculate some logical expressions +# + +assign any_cycleroute = + if route_bicycle_icn=yes then true + else if route_bicycle_ncn=yes then true + else if route_bicycle_rcn=yes then true + else if route_bicycle_lcn=yes then true + else false + +assign nodeaccessgranted = + if any_cycleroute then true + else lcn=yes + +assign is_ldcr = + if ignore_cycleroutes then false + else any_cycleroute + +assign isbike = or bicycle_road=yes or bicycle=yes or or bicycle=permissive bicycle=designated lcn=yes +assign ispaved = surface=paved|asphalt|concrete|paving_stones|sett +assign isunpaved = not or surface= or ispaved surface=fine_gravel|cobblestone +assign probablyGood = or ispaved and ( or isbike highway=footway ) not isunpaved + + +# +# this is the cost (in Meter) for a 90-degree turn +# The actual cost is calculated as turncost*cos(angle) +# (Suppressing turncost while following longdistance-cycleways +# makes them a little bit more magnetic) +# +assign turncost = if is_ldcr then 0 + else if junction=roundabout then 0 + else 90 + + +# +# for any change in initialclassifier, initialcost is added once +# +assign initialclassifier = + if route=ferry then classifier_ferry + else classifier_none + + +# +# calculate the initial cost +# this is added to the total cost each time the costfactor +# changed +# +assign initialcost = + if ( equal initialclassifier classifier_ferry ) then 10000 + else 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess = + if access= then not motorroad=yes + else if access=private|no then false + else true + +# +# calculate logical bike access +# +assign bikeaccess = + if any_cycleroute then true + else if bicycle= then + ( + if bicycle_road=yes then true + else if vehicle= then ( if highway=footway then false else defaultaccess ) + else not vehicle=private|no + ) + else not bicycle=private|no|dismount|use_sidepath + +# +# calculate logical foot access +# +assign footaccess = + if bikeaccess then true + else if bicycle=dismount then true + else if foot= then defaultaccess + else not foot=private|no + +# +# if not bike-, but foot-acess, just a moderate penalty, +# otherwise access is forbidden +# +assign accesspenalty = + if bikeaccess then 0 + else if footaccess then 4 + else 10000 + +# +# handle one-ways. On primary roads, wrong-oneways should +# be close to forbidden, while on other ways we just add +# 4 to the costfactor (making it at least 5 - you are allowed +# to push your bike) +# +assign badoneway = + if reversedirection=yes then + if oneway:bicycle=yes then true + else if oneway= then junction=roundabout + else oneway=yes|true|1 + else oneway=-1 + +assign onewaypenalty = + if ( badoneway ) then + ( + if ( cycleway=opposite|opposite_lane|opposite_track ) then 0 + else if ( oneway:bicycle=no ) then 0 + else if ( highway=primary|primary_link ) then 50 + else if ( highway=secondary|secondary_link ) then 30 + else if ( highway=tertiary|tertiary_link ) then 20 + else 4.0 + ) + else 0.0 + +# +# calculate the cost-factor, which is the factor +# by which the distance of a way-segment is multiplied +# to calculate the cost of that segment. The costfactor +# must be >=1 and it's supposed to be close to 1 for +# the type of way the routing profile is searching for +# +assign isresidentialorliving = or highway=residential|living_street living_street=yes +assign costfactor + + # + # exclude rivers, rails etc. + # + if ( and highway= not route=ferry ) then 10000 + + # + # exclude motorways and proposed roads + # + else if ( highway=motorway|motorway_link ) then 10000 + else if ( highway=proposed|abandoned ) then 10000 + + # + # all other exclusions below (access, steps, ferries,..) + # should not be deleted by the decoder, to be available + # in voice-hint-processing + # + else min 9999 + + # + # apply oneway-and access-penalties + # + add max onewaypenalty accesspenalty + + # + # steps and ferries are special. Note this is handled + # before the cycleroute-switch, to be able + # to really exlude them be setting cost to infinity + # + if ( highway=steps ) then ( if allow_steps then 40 else 10000 ) + else if ( route=ferry ) then ( if allow_ferries then 5.67 else 10000 ) + + # + # handle long-distance cycle-routes. + # + else if ( is_ldcr ) then 1 # always treated as perfect (=1) + else + add ( if stick_to_cycleroutes then 0.5 else 0.05 ) # everything else somewhat up + + # + # some other highway types + # + if ( highway=pedestrian ) then 3 + else if ( highway=bridleway ) then 5 + else if ( highway=cycleway ) then 1 + else if ( isresidentialorliving ) then ( if isunpaved then 1.5 else 1.1 ) + else if ( highway=service ) then ( if isunpaved then 1.6 else 1.3 ) + + # + # tracks and track-like ways are rated mainly be tracktype/grade + # But note that if no tracktype is given (mainly for road/path/footway) + # it can be o.k. if there's any other hint for quality + # + else if ( highway=track|road|path|footway ) then + ( + if ( tracktype=grade1 ) then ( if probablyGood then 1.0 else 1.3 ) + else if ( tracktype=grade2 ) then ( if probablyGood then 1.1 else 2.0 ) + else if ( tracktype=grade3 ) then ( if probablyGood then 1.5 else 3.0 ) + else if ( tracktype=grade4 ) then ( if probablyGood then 2.0 else 5.0 ) + else if ( tracktype=grade5 ) then ( if probablyGood then 3.0 else 5.0 ) + else ( if probablyGood then 1.0 else 5.0 ) + ) + + # + # When avoiding unsafe ways, avoid highways without a bike hint + # + else add ( if ( and avoid_unsafe not isbike ) then 2 else 0 ) + + # + # actuals roads are o.k. if we have a bike hint + # + if ( highway=trunk|trunk_link ) then ( if isbike then 1.5 else 10 ) + else if ( highway=primary|primary_link ) then ( if isbike then 1.2 else 3 ) + else if ( highway=secondary|secondary_link ) then ( if isbike then 1.1 else 1.6 ) + else if ( highway=tertiary|tertiary_link ) then ( if isbike then 1.0 else 1.4 ) + else if ( highway=unclassified ) then ( if isbike then 1.0 else 1.3 ) + + # + # default for any other highway type not handled above + # + else 2.0 + + +# way priorities used for voice hint generation + +assign priorityclassifier = + + if ( highway=motorway ) then 30 + else if ( highway=motorway_link ) then 29 + else if ( highway=trunk ) then 28 + else if ( highway=trunk_link ) then 27 + else if ( highway=primary ) then 26 + else if ( highway=primary_link ) then 25 + else if ( highway=secondary ) then 24 + else if ( highway=secondary_link ) then 23 + else if ( highway=tertiary ) then 22 + else if ( highway=tertiary_link ) then 21 + else if ( highway=unclassified ) then 20 + else if ( isresidentialorliving ) then 6 + else if ( highway=service ) then 6 + else if ( highway=cycleway ) then 6 + else if ( or bicycle=designated bicycle_road=yes ) then 6 + else if ( highway=track ) then if tracktype=grade1 then 6 else 4 + else if ( highway=bridleway|road|path|footway ) then 4 + else if ( highway=steps ) then 2 + else if ( highway=pedestrian ) then 2 + else 0 + +# some more classifying bits used for voice hint generation... + +assign isbadoneway = not equal onewaypenalty 0 +assign isgoodoneway = if reversedirection=yes then oneway=-1 + else if oneway= then junction=roundabout else oneway=yes|true|1 +assign isroundabout = junction=roundabout +assign islinktype = highway=motorway_link|trunk_link|primary_link|secondary_link|tertiary_link +assign isgoodforcars = if greater priorityclassifier 6 then true + else if ( or isresidentialorliving highway=service ) then true + else if ( and highway=track tracktype=grade1 ) then true + else false + +# ... encoded into a bitmask + +assign classifiermask add isbadoneway + add multiply isgoodoneway 2 + add multiply isroundabout 4 + add multiply islinktype 8 + multiply isgoodforcars 16 + +# include `smoothness=` tags in the response's WayTags for track analysis +assign dummyUsage = smoothness= + +---context:node # following code refers to node tags + +assign defaultaccess = + if ( access= ) then true # add default barrier restrictions here! + else if ( access=private|no ) then false + else true + +assign bikeaccess = + if nodeaccessgranted=yes then true + else if bicycle= then + ( + if vehicle= then defaultaccess + else not vehicle=private|no + ) + else not bicycle=private|no|dismount + +assign footaccess = + if bicycle=dismount then true + else if foot= then defaultaccess + else not foot=private|no + +assign initialcost = + if bikeaccess then 0 + else ( if footaccess then 100 else 1000000 ) diff --git a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt index fb0a005..0469e4f 100644 --- a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt @@ -104,10 +104,26 @@ class MainActivity : TauriActivity() { if (!cmdFile.exists()) cmdFile.createNewFile() val bleCmdFile = File(appDataDir, "ble_command.txt") if (!bleCmdFile.exists()) bleCmdFile.createNewFile() + val brouterCmdFile = File(appDataDir, "brouter_command.txt") + if (!brouterCmdFile.exists()) brouterCmdFile.createNewFile() + + // Copy BRouter routing profiles (+ lookups.dat) from assets to files/brouter/profiles2. + // The embedded engine (btools.router) reads them by path alongside the segments. + try { + val profDir = File(filesDir, "brouter/profiles2") + profDir.mkdirs() + for (f in (assets.list("brouter/profiles2") ?: arrayOf())) { + assets.open("brouter/profiles2/$f").use { i -> File(profDir, f).outputStream().use { o -> i.copyTo(o) } } + } + Log.d("BalDav", "BRouter profiles copied to ${profDir.absolutePath}") + } catch (e: Exception) { + Log.e("BalDav", "Failed to copy BRouter profiles", e) + } setupCommandWatcher(appDataDir) processGpsCommand(appDataDir) processBleCommand(appDataDir) + processBrouterCommand(appDataDir) Log.d("BalDav", "Command watcher started on ${appDataDir.absolutePath}") // Diagnostic HTTP endpoint on localhost — GET /gps makes the app request a live @@ -161,6 +177,8 @@ class MainActivity : TauriActivity() { processGpsCommand(appDataDir) processBleCommand(appDataDir) } + } else if (path == "brouter_command.txt") { + runOnUiThread { processBrouterCommand(appDataDir) } } } } @@ -168,6 +186,69 @@ class MainActivity : TauriActivity() { Log.d("BalDav", "FileObserver started on ${appDataDir.absolutePath}") } + // Offline routing via the embedded BRouter engine (btools.router). Request/response + // over files like the GPS bridge: Rust writes brouter_request.json + brouter_command.txt + // ("route:"), we route on a background thread (routing is heavy — never the UI + // thread) and write brouter_result_.json ({ok, gpx} | {ok:false, error}). + private fun processBrouterCommand(appDataDir: File) { + val cmdFile = File(appDataDir, "brouter_command.txt") + if (!cmdFile.exists()) return + val cmd = cmdFile.readText().trim() + if (cmd.isEmpty()) return + cmdFile.writeText("") + if (!cmd.startsWith("route:")) return + val id = cmd.substringAfter("route:") + Thread { + val resultFile = File(appDataDir, "brouter_result_$id.json") + val out = org.json.JSONObject() + try { + val req = org.json.JSONObject(File(appDataDir, "brouter_request.json").readText()) + val profile = req.getString("profile") + val arr = req.getJSONArray("lonlats") + val pts = ArrayList() + for (i in 0 until arr.length()) { + val pair = arr.getJSONArray(i) + pts.add(doubleArrayOf(pair.getDouble(0), pair.getDouble(1))) + } + val profilePath = File(filesDir, "brouter/profiles2/$profile.brf").absolutePath + val segmentsDir = File(filesDir, "brouter/segments4") + val gpx = routeBrouter(segmentsDir, profilePath, pts) + out.put("ok", true) + out.put("gpx", gpx) + } catch (e: Exception) { + Log.e("BalDav", "BRouter route failed", e) + out.put("ok", false) + out.put("error", e.message ?: e.toString()) + } + try { resultFile.writeText(out.toString()) } catch (e: Exception) { Log.e("BalDav", "brouter result write failed", e) } + }.start() + } + + // Minimal embedding recipe (mirrors btools.server.request.ServerHandler): build a + // RoutingContext pointing at the profile (.brf; the engine parses it via ProfileCache + // and loads lookups.dat from the same dir), waypoints as OsmNodeNamed (ilon/ilat are + // micro-degrees), run the engine, return the track as GPX. + private fun routeBrouter(segmentsDir: File, profilePath: String, pts: List): String { + val rc = btools.router.RoutingContext() + rc.memoryclass = 128 + rc.localFunction = profilePath + rc.setAlternativeIdx(0) + val wps = ArrayList() + for (p in pts) { + val n = btools.router.OsmNodeNamed() + n.ilon = ((p[0] + 180.0) * 1000000.0 + 0.5).toInt() + n.ilat = ((p[1] + 90.0) * 1000000.0 + 0.5).toInt() + n.name = "wp" + wps.add(n) + } + val re = btools.router.RoutingEngine(null, null, segmentsDir, wps, rc) + re.doRun(0) + val err = re.getErrorMessage() + if (err != null && err.isNotEmpty()) throw RuntimeException(err) + val track = re.getFoundTrack() ?: throw RuntimeException("no track found") + return track.formatAsGpx() + } + private fun processGpsCommand(appDataDir: File) { val cmdFile = File(appDataDir, "gps_command.txt") if (!cmdFile.exists()) return diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 84a9229..c7a6db0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -393,6 +393,48 @@ fn gps_service_command(app: tauri::AppHandle, cmd: String) -> Result" command where MainActivity's +// FileObserver watches, then poll for brouter_result_.json. `lonlats` is a JSON +// array of [lon,lat] pairs; returns the Kotlin result JSON ({ok,gpx}|{ok:false,error}). +#[tauri::command] +fn brouter_route(app: tauri::AppHandle, lonlats: String, profile: String) -> Result { + let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + std::fs::create_dir_all(&data_dir).ok(); + let id = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH).map_err(|e| e.to_string())? + .as_millis().to_string(); + let req = format!( + "{{\"profile\":{},\"lonlats\":{}}}", + serde_json::to_string(&profile).map_err(|e| e.to_string())?, + lonlats + ); + let result_name = format!("brouter_result_{}.json", id); + // Write request + command to app_data_dir AND app root (where Kotlin watches). + let root = data_dir.parent().and_then(|p| p.parent()).map(|p| p.to_path_buf()); + let write_both = |name: &str, content: &str| { + let _ = std::fs::write(data_dir.join(name), content); + if let Some(r) = &root { let _ = std::fs::write(r.join(name), content); } + }; + write_both("brouter_request.json", &req); + write_both("brouter_command.txt", &format!("route:{}", id)); + // Poll for the result (routing can take a few seconds; cap ~60s). + let mut candidates = vec![data_dir.join(&result_name)]; + if let Some(r) = &root { candidates.push(r.join(&result_name)); } + for _ in 0..600 { + for p in &candidates { + if p.exists() { + if let Ok(s) = std::fs::read_to_string(p) { + let _ = std::fs::remove_file(p); + return Ok(s); + } + } + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + Err("brouter: timeout waiting for result".into()) +} + // Read GPS points from service's JSONL file #[tauri::command] fn gps_service_read_points(app: tauri::AppHandle) -> Result { @@ -904,7 +946,7 @@ pub fn run() { .add_migrations("sqlite:baldav.db", migrations) .build(), ) - .invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_app_info, gps_service_command, gps_service_read_points, save_file, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr]) + .invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_app_info, gps_service_command, gps_service_read_points, brouter_route, save_file, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }