BRouter: analysis + embed the offline routing engine (Stage 1)

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

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

Segments (.rd5, ~13-23 MB per 5x5 tile) are delivered later (Stage 2, wired
download inside BalDav). IMPLEMENTATION_PLAN.md tracks the 5 stages.
This commit is contained in:
4aiman 2026-09-09 00:56:13 +03:00
commit c3188911c3
15 changed files with 2338 additions and 1 deletions

79
IMPLEMENTATION_PLAN.md Normal file
View file

@ -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/<tile>.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-флаг).

30
LICENSE-brouter Normal file
View file

@ -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.

View file

@ -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 миграции)

191
docs/brouter-findings.md Normal file
View file

@ -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 <segmentdir> <profiledir> <customprofiledir> <port> <maxthreads> [bindaddress]
```
Запрос (тот же API, что у brouter.de/brouter-web):
```
GET http://127.0.0.1:<port>/brouter?lonlats=39.7,47.2|40.1,47.4&profile=shortest&format=geojson&timode=0&alternativeidx=0
```
Параметры (из байткода `RouteServer`): `lonlats` (пары lon,lat через `|`), `profile`, `format`
(gpx/geojson/kml), `timode` (turnInstructionMode), `alternativeidx`, `nogos`. Нужен JRE + каталог
сегментов `.rd5` + каталог профилей. Годится для инструментов `tools/track/` (проложить маршрут по
`lonlats`, что уже выдаёт `tools/steep_segments.js`).
**(б) Приложение BRouter на телефоне — Server-Mode.** Приложение поднимает локальный HTTP-сервер
(порт 17777 по умолчанию); любой клиент на устройстве, включая WebView BalDav, шлёт тот же
`GET /brouter?lonlats=...`. Требует установленного приложения BRouter и скачанных сегментов.
**(в) Приложение BRouter — Android-intent.** С v1.6.2 у приложения есть «интерфейс без файлового
доступа»: BalDav (Kotlin) шлёт intent к `btools.routingapp` с опорными точками и профилем, получает
GPX/GeoJSON обратно. Данные приложения с Android 11 лежат в
`Android/media/btools.routingapp/brouter/segments4`.
**(г) Встраивание движка в BalDav.** `brouter-1.6.3-all.jar` — чистая Java (356 КБ), пакеты
`router`/`mapaccess`/`expressions`/`codec`/`util`. Классы движка можно добавить в Android-модуль
BalDav (`src-tauri/gen/android`) и звать из Kotlin, а из JS — через существующий Rust↔Kotlin-мост
(как GPS-сервис). Полностью самодостаточно и оффлайн, второе приложение не нужно.
## 6. Идея 1 — заполнение GPS-пропусков по дороге
- **Как:** при разрыве взять последнюю точку до пропуска и первую после, проложить между ними
маршрут профилем `shortest.brf`, вставить дорожную полилинию как заполнение гэпа.
- **Стык с `computeStats` (frontend/js/gps.js, ветка `isGap`):** сейчас гэп заполняется хордой или
`скорость×время`. Дорожный путь точнее (по реальным дорогам). Точки-заполнители помечать
`estimated` (как dead-reckoning), чтобы они не влияли на глюк-фильтры высоты и были видимо
«достроенными». **Решить:** идёт ли дорожная дистанция в зачёт трека или только рисуется (сейчас
гэп считается по скорости; дорожная длина может быть и точнее, и длиннее реального пути).
- **Риски/границы:** без сегментов на регион заполнить нечем; дорога-кратчайшая может отличаться от
фактически проеханной; на бездорожье (поля, где пользователь реально ездит) кратчайший путь по
графу дорог может увести. Нужен разумный лимит длины гэпа, при котором достраиваем.
## 7. Идея 2 — оффлайн-планирование маршрута по точкам
- **Что уже есть в BalDav** (frontend/js/main.js): `showRouteEditor` (тап по карте = точка,
тап по линии = вставка, тащить/удалять), `showRouteDetail` (предпросмотр + профиль высоты
`routeElevStats` + фуллскрин), `drawRoutePolyline`, «Сохранённые маршруты», топо/уклон-обводка.
Парсер `parseRouteGPX` уже читает GPX/`<ele>`.
- **Чего не хватает:** оффлайн-роутера между опорными точками. Сейчас линия между тапами — прямая
(ручное рисование). BRouter вернёт дорожный GPX/GeoJSON, который **уже парсится** `parseRouteGPX`,
и дальше работает существующий предпросмотр/профиль высоты.
- То есть UI-обвязка в основном готова; предметная работа — вызов BRouter и подмена прямых отрезков
дорожным маршрутом (+ выбор профиля: trekking с `uphillcost` для «поменьше в горку»).
## 8. Варианты интеграции и рекомендация
| Вариант | Плюсы | Минусы |
|---|---|---|
| **A. Зависимость от приложения BRouter** (intent или localhost HTTP) | Быстро; роутинга в BalDav нет; BRouter сам качает/обновляет сегменты | Второе приложение ставить и настраивать; управление сегментами вне BalDav; связанность |
| **B. Встраивание движка** (jar в Android-модуль + Rust/JS-команда) | Самодостаточно, полностью оффлайн, один apk; сегменты кладём куда хотим | Бандлинг Java-движка; доставка `.rd5` в приложение; **проверить лицензию** (GitHub abrensch/brouter); управление хранилищем сегментов |
| **C. Порт на Rust/JS** | Нет внешних зависимостей | Огромный объём (движок + формат `.rd5` + интерпретатор профилей) — **отклонить** |
**Решение — B (встраивание)** (подтверждено пользователем). Встраиваемость проверена по байткоду:
движок (150 классов) использует только `java.io/net/util/lang/text/security/zip` — ни AWT/Swing/
RMI/ImageIO/servlet/sun.*, всё есть на Android ART. `RouteServer` — на голых `ServerSocket`/`Socket`
(контейнер не нужен). Значит в процессе можно звать напрямую `btools.router.RoutingEngine`
(без HTTP-сервера): дать каталог сегментов + профиль + опорные точки, получить GPX/GeoJSON.
Для инструментов `tools/track/` (анализ, не телефон) сразу годится **вариант (а)** — jar на компе.
## 9. Решения и открытые вопросы
Решено (2026-09-07):
1. **Лицензия — MIT** («Copyright (c) 2019 BRouter contributors», github.com/abrensch/brouter).
Встраивание разрешено без ограничений; при распространении сохранить копирайт-нотис MIT
(положить `LICENSE-brouter` в репозиторий рядом с движком).
2. **Интеграция — вариант B (встраивание движка)**. Код BalDav открыт, MIT это позволяет.
3. **Профиль — trekking** (для обеих идей). Идея 1 всё же считается `shortest` для чистого
кратчайшего пути (см. §6); trekking — для планирования (идея 2).
4. **Идея 1, порог хорды:** прокладывать по графу, НО если дорожная длина в разы больше хорды —
рисовать хорду (граф увёл не туда, напр. вокруг поля). Коэффициент подобрать на практике
(старт ~3×, вынести в конфиг/константу).
5. **«Крутилка» `uphillcost`** (см. §4) — пользователь про неё не знал. План: начать со
стандартного trekking; если маршруты слишком в горку — добавить настройку «избегать подъёмов»
(поднимать `uphillcost`). Пока не крутим.
6. **Доставка сегментов — загрузка по проводу внутри BalDav** (решено). Экран в BalDav: дома на
Wi-Fi/проводе скачать готовые тайлы региона с brouter.de/segments4 в папку приложения
(app data dir, аналог seed-БД); в поле сеть не нужна. Тот же паттерн, что задумывался для DEM/
прогноза. Свою сборку сегментов из OSM+SRTM не делаем.
7. **Встраиваемость подтверждена** (см. §8): движок без десктоп-зависимостей, зовём
`btools.router.RoutingEngine` в процессе из Kotlin.
## 10. Проверка (PoC, отдельным шагом, по согласию)
Требует JRE и загрузки сегментов региона (сеть) — вне текущей задачи. Когда разрешишь:
запустить `RouteServer` на компе с каталогом `.rd5` и `profiles2/`, дёрнуть
`GET /brouter?lonlats=...&profile=shortest&format=geojson` по паре точек из
`tools/steep_segments.js`, убедиться, что возвращается дорожный маршрут с `track-length`/`ascend`.

353
docs/gps-model-findings.md Normal file
View file

@ -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`), выше описан
алгоритмически.

View file

@ -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 <segmentdir> <profiledir> <customprofiledir> <port> <maxthreads>` + 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 + загрузка, по согласию).

View file

@ -23,6 +23,7 @@
<script src="js/bmr.js"></script>
<script src="js/tilecache.js"></script>
<script src="js/gps.js"></script>
<script src="js/router.js"></script>
<script src="js/sync.js"></script>
<script src="js/chzn.js"></script>
<script src="js/main.js"></script>

20
frontend/js/router.js Normal file
View file

@ -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;
},
};

View file

@ -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")

Binary file not shown.

View file

@ -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 <residential>
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

View file

@ -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

View file

@ -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 )

View file

@ -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:<id>"), we route on a background thread (routing is heavy — never the UI
// thread) and write brouter_result_<id>.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<DoubleArray>()
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<DoubleArray>): String {
val rc = btools.router.RoutingContext()
rc.memoryclass = 128
rc.localFunction = profilePath
rc.setAlternativeIdx(0)
val wps = ArrayList<btools.router.OsmNodeNamed>()
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

View file

@ -393,6 +393,48 @@ fn gps_service_command(app: tauri::AppHandle, cmd: String) -> Result<String, Str
Ok(format!("sent:{}", cmd))
}
// Offline routing via the embedded BRouter engine (Kotlin runs btools.router). Same
// file bridge as GPS: write the request + a "route:<id>" command where MainActivity's
// FileObserver watches, then poll for brouter_result_<id>.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<String, String> {
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<String, String> {
@ -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");
}