Compare commits
5 commits
86a9a1846b
...
c3188911c3
| Author | SHA1 | Date | |
|---|---|---|---|
| c3188911c3 | |||
| 5b345a1df4 | |||
| 2d23f3a475 | |||
| 9f8ab0eeb1 | |||
| e9efb4fc6c |
25 changed files with 2753 additions and 28 deletions
79
IMPLEMENTATION_PLAN.md
Normal file
79
IMPLEMENTATION_PLAN.md
Normal 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
30
LICENSE-brouter
Normal 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.
|
||||
|
|
@ -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 миграции)
|
||||
|
|
@ -103,7 +110,7 @@
|
|||
| `activity_log` | Лог активности | date, type, minutes, kcal, activity_type (squats/jumprope/stretch/swim/manual), sets, reps, distance_m |
|
||||
| `supplements` | Добавки (БАД) | name, daily_dose_g, витамины/минералы |
|
||||
| `supplement_log` | Приём добавок | date, supplement_id, dose_g |
|
||||
| `gps_sessions` | GPS тренировки | date, activity_type, distance, kcal, speed, elevation, bike_id, smoothed, smooth_window, smooth_iters (де-джиттер) |
|
||||
| `gps_sessions` | GPS тренировки | date, activity_type, distance, kcal, speed, elevation, bike_id, smoothed, smooth_window, smooth_iters (де-джиттер), pauses (JSON ручных пауз), wind_marks (JSON меток «ветер в лицо») |
|
||||
| `gps_points` | Точки трека | session_id, ts, lat, lon, altitude_gps_m, altitude_srtm_m, speed_ms, seq, bearing_deg, accuracy_m, lat_smoothed, lon_smoothed (обратимый де-джиттер), heart_rate, spo2, bp_sys, bp_dia, accel_rms/max, gyro_pitch/yaw |
|
||||
| `routes` | Сохранённые маршруты (оверлей навигации) | name, color, points_json (`{lat,lon,ele?}`), created_at |
|
||||
| `bikes` | Велосипеды | name, weight_kg, wheel, звёзды, surface_type, is_active |
|
||||
|
|
@ -177,17 +184,29 @@
|
|||
```
|
||||
Power = v * (m*g*sin(gradient) + Crr*m*g*cos(gradient) + 0.5*CdA*rho*v^2)
|
||||
m = rider_kg + bike_kg; gradient из СГЛАЖЕННОЙ высоты
|
||||
Crr = 0.004 (асфальт) / 0.006 (смешанный) / 0.008 (грунт)
|
||||
Crr (ручной surface_type): 0.004 асфальт / 0.006 смешанный / 0.008 грунт
|
||||
Crr по акселерометру (crrFromAccel, если есть accel_rms >10 точек): <3=0.004, 3-7=0.006,
|
||||
7-11=0.008, 11-15=0.012 (плотный грунт), >15=0.020 (трава/колея/щебень). Прежний потолок 0.008
|
||||
занижал грунты ~3x (замер: трава требует 0.020-0.031). Мапится и медиана сессии, и каждая точка (ptCrr).
|
||||
CdA = 0.6, rho = 1.225, efficiency = 0.25
|
||||
|
||||
Педалирование (power>0): kcal = max(физика, MET-нетто), физика = power*dt/(0.25*4184)
|
||||
MET по скорости: <16км/ч=6.0, <19=6.8, <22=8.0, <25=10.0, <28=12.0, иначе 14.0
|
||||
MET-нетто = (dt/60)*((MET-1)*3.5*weight)/200 (−1 MET покоя, он уже в BMR)
|
||||
Floor только при v≥5км/ч (ниже — дрожание GPS, не езда)
|
||||
ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ (по решению не правим, нужна калибровка): max() не СКЛАДЫВАЕТ —
|
||||
на подъёмах физика выигрывает, а накладные (баланс/стабилизация корпуса, без внешней работы,
|
||||
их ловит MET-этаж) теряются. Направление ошибки известно: подъёмы недосчитаны (по поездке ~1.34x
|
||||
MET/физика, но равнина 1.85x, подъём 0.41x). Честный фикс = физика/КПД + накладные, отложен.
|
||||
Накат / freewheel (power≤0, «велик катился бы сам»): kcal = базальный метаболизм
|
||||
(детектор кручу/накат по IMU-каденсу проверен на 50Hz — сигнал слишком слаб, не используется; хук cadence_rpm — под BLE-датчик шатуна)
|
||||
|
||||
Набор высоты: кумулятивный гистерезис (elevRef, порог 1м) — на плотных треках per-point разница <1м даёт 0
|
||||
ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ (по решению не правим): абсолютный набор по сырому GPS-альтиметру расходится
|
||||
с независимым пересчётом в ОБЕ стороны (медиана разброса высоты между проездами одной точки ~10м).
|
||||
Без модели рельефа (DEM) не разрешается. Реальный фикс — предзагрузка DEM по району дома по проводу
|
||||
(кнопка «уточнить высоты» → GPXZ работает, но требует сети, а РЭБ/РКН глушат в поле). Пока: набор
|
||||
имеет точность порядка десятков процентов, на расчёт мощности влияет слабо (там уклон по окну, ограничен сверху).
|
||||
```
|
||||
|
||||
### Дистанция по сегментам (computeStats, gps.js ~189-235)
|
||||
|
|
@ -198,6 +217,23 @@ segMaxMs = maxSpeedKmh/3.6 (вело) или walkMaxSpeedKmh/3.6 (пеш); gapTh
|
|||
норма (иначе) → segDist = хорда (haversine)
|
||||
distance += segDist; высота/maxSpeed — только на «норме»; ккал в разрыве/estimated = walk-MET по времени
|
||||
Зависит только от ts+speed (не от флага estimated) → live == «Стоп» == пересчёт. gapThreshold настраивается, применяется при пересчёте.
|
||||
|
||||
Ручная пауза (opts.pauses = [{startTs,endTs}] мс): сегмент, пересекающий паузу (prevMs<endTs И pMs>startTs),
|
||||
исключается ЦЕЛИКОМ — ни дистанции, ни высоты, ни ккал, ни времени (pausedSec вычитается из duration), elevRef=null
|
||||
(перепад, набранный пешком за паузу, не банкуется на первом сегменте после резюма). Авто-GPS-гэп (без паузы) — считается как раньше.
|
||||
Глюк-гард высоты: соседний сглаженный перепад с грейдом > maxRealGradePct (1.0 = 100% = 45°) не банкуется и не двигает elevRef
|
||||
(скачок/ступенька, в т.ч. Android no-fix 0.0). Плавный приход к любому значению (реальный 0 у реки) проходит.
|
||||
Kotlin пишет "alt":null без фикса высоты (hasAltitude()), честный 0 с фиксом сохраняется. Паузы: колонка gps_sessions.pauses (JSON).
|
||||
Пеший пуш велосипеда (только односкоростной, gear_count 1 или пусто + есть звезда/трещотка/колесо):
|
||||
на фиксированной передаче каденс <30 об/мин = велик не едет, его ведут. Порог скорости walkBikeMs = 30*wheelCirc*gearRatio/60.
|
||||
Сегмент медленнее порога (нормальный фикс, не гэп) исключается из ездовой дистанции/скорости/высоты (elevRef=null — пеший
|
||||
подъём не склеивается с ездовым), усилие считается как ходьба (walk-MET нетто), не по физике. #7: исключается КАЖДАЯ такая точка
|
||||
без оглядки на длительность (краткие торможения тоже, безвредно ~0м); в отчёт как «пешком» идут только сплошные пробеги ≥30с.
|
||||
Возврат computeStats: walkBikeSec (время пуша ≥30с-пробегами), walkBikeMeters (пешая дистанция). opts.gearCount прокинут во все вызовы.
|
||||
Метки ветра (разметка, НЕ расчёт): кнопка «Ветер в лицо» на экране записи — тап открывает интервал встречного ветра,
|
||||
тап закрывает. GPS.toggleWindMark()/_closeOpenWindMark(); GPS.recording.windMarks=[{startTs,endTs}] мс, персист в localStorage,
|
||||
запись в БД (DB.setSessionWindMarks) на финише + восстановление в recovery. Колонка gps_sessions.wind_marks (JSON). Это ground
|
||||
truth для проверки оценки ветра по треку (tools/track/track.py, wind_by_passes) — на distance/kcal/power НЕ влияет. Голос — потом.
|
||||
```
|
||||
|
||||
### Ходьба/бег — MET-таблица
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
- **Дневник питания** — ~13000 продуктов с меткой источника (Магнит, 5ка, ИОН, KDV, Гриднев, свои). Поиск порядко-независимый (слова в любом порядке). Каталог чистится от неиспользованного импорта без метки (health-diet и пр.) кнопкой в настройках — съеденные и в рецептах сохраняются. 37 нутриентов (витамины, минералы, omega-3/6, вода, клетчатка). Рецепты с термообработкой. Формулы порций (4*30, 300+50).
|
||||
- **GPS трекинг** — запись маршрутов на велосипеде/пешком в фоне (foreground service, точки пишутся при выключенном экране; один файл на сессию с id сессии в имени `gps_points_<ms>_s<id>.jsonl` для однозначной привязки, переживает START_STICKY-перезапуск ОС; полный до-импорт нативных точек по timestamp при финализации — без потерь, даже если экран был выключен всю запись, а при пробуждении экрана точки видимо нарастают на карте по мере импорта). При живой записи нативный файл — источник истины для БД, watchPosition только рисует маркер (без дублей одного фикса с разными ts). Восстановление точек из файлов на устройстве — матч файла к сессии по id/дате, дозаполнение разрывов с дедупом по близости ±2.5с, безопасный пересчёт без удаления точек, чистка уже импортированных файлов. Оффлайн карты (Leaflet + IndexedDB кэш). Импорт GPX/FTB (FitoTrack). Экспорт GPX. Фильтр altitude outlier'ов и GPS-залипаний. Отбор точек по точности (жёсткий порог по настройке «Мин. точность» — рубит грубые сотовые фиксы ~800м, оставляет слабый но точный GPS; где сигнала нет — честный разрыв). Направление (bearing) каждой точки пишется в БД. Вращение карты по компасу (leaflet-rotate, EMA-сглаживание, gimbal lock компенсация) и course-up по GPS-курсу (в ландшафте карта доворачивается вместе с экраном — курс к физическому верху). Ландшафтный режим (CSS rotation + панель справа, 3 режима: off/auto/lock). Dead reckoning при потере GPS (гироскоп + акселерометр). Placeholder до GPS (пиксельарт). Карта проезженных дорог (дедупликация 2м, вело/пешие). Настраиваемый цвет линии трека (чтобы отличать от цветных дорог). Навигационный маркер позиции с треугольником-направленцем (по компасу или GPS-курсу); порядок слоёв тайлы<дороги<маршрут<трек<маркер. Импорт тайлов по регионам (SQLite, роутер LAN).
|
||||
- **BLE пульсометр** — подключение к FitPro браслетам и стандартным HR датчикам (0x180D). ЧСС, SpO2, давление в реальном времени при записи трека.
|
||||
- **Расчёт ккал** — велосипед: НЕТТО-модель (суточный баланс уже учитывает BMR, поэтому активность считается сверх покоя). Кручение = max(физика мощности из gradient+Crr+CdA / КПД, эмпирический MET-нетто по скорости); накат (freewheel, `power≤0` — «велик катился бы сам») = только базовый обмен. MET-таблица для ходьбы/бега и тренировок (приседания 6.0, скакалка 12.3, растяжки 2.3, бассейн 8.0). Набор высоты — кумулятивный гистерезис (устойчив на плотных треках, где per-point разница <1м). Пункт меню «Пересчитать высоту/калории» пересчитывает статистику всех треков без изменения точек. Профили велосипедов. BMR по Mifflin-St Jeor с коэффициентом активности.
|
||||
- **Расчёт ккал** — велосипед: НЕТТО-модель (суточный баланс уже учитывает BMR, поэтому активность считается сверх покоя). Кручение = max(физика мощности из gradient+Crr+CdA / КПД, эмпирический MET-нетто по скорости); накат (freewheel, `power≤0` — «велик катился бы сам») = только базовый обмен. Crr на грунтах уточняется по вибрации акселерометра (до 0.020 на траве/колее). Ходьба с велосипедом в горку (только односкоростной) отделяется автоматически по скорости ниже каденса 30 об/мин и не засчитывается как езда. MET-таблица для ходьбы/бега и тренировок (приседания 6.0, скакалка 12.3, растяжки 2.3, бассейн 8.0). Набор высоты — кумулятивный гистерезис (устойчив на плотных треках, где per-point разница <1м); абсолютная точность набора ограничена сырым GPS-альтиметром (порядка десятков процентов, без модели рельефа не разрешается). Известное ограничение: `max(физика, MET)` не складывает накладные расходы, поэтому подъёмы по ккал слегка недосчитываются. Пункт меню «Пересчитать высоту/калории» пересчитывает статистику всех треков без изменения точек. Профили велосипедов. BMR по Mifflin-St Jeor с коэффициентом активности.
|
||||
- **Дистанция на разрывах GPS** — на «слепых» участках (интервал между фиксами > `gapThresholdSec`) путь считается по скорости (speed×dt, с ограничением по макс. скорости), а не прямой хордой — иначе разрывы занижают/искажают длину. Телепорт-скачки без временного разрыва отбрасываются. Расчёт зависит только от времени и скорости, поэтому live-счётчик, «Стоп» и пересчёт дают одинаковую длину. Порог `gapThresholdSec` настраивается; при изменении и пересчёте применяется сразу.
|
||||
- **Тренировки** — приседания (подходы x повторения), скакалка, растяжки, бассейн (дистанция). Автоматический расчёт ккал по MET и весу.
|
||||
- **Нутриенты** — 37 микронутриентов (витамины, минералы, жирные кислоты, женьшень, ресвератрол, CoQ10). Нормы РСП/АУП (РФ/ЕС/США). Фармакокинетика — запасы в организме по полураспаду.
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
- **Высоты маршрута (рельеф до поездки)** — экран предпросмотра маршрута (тап по названию): профиль высоты (крутые участки красным), длина/набор/сброс/макс. уклон, тап по точке — высота и уклон, разворот карты на весь экран. Топографическая обводка линии с тогглом **уклон/высота**: уклон дискретными полосами по 1% (зелёный→жёлтый→красный→коричневый→чёрный, ≥10% чёрный) или гипсометрия по высоте (зелёный→жёлтый→коричневый). Обводка и тоггл доступны и в оверлее на карте записанного трека и во время записи; маршрут рисуется ПОД треком. Высоты (`<ele>` из GPX или онлайн) тянутся по цепочке: **GPXZ** (Copernicus GLO-30, точнее для уклонов; ключ в GPS-настройках, хранится только на устройстве) → open-elevation → open-meteo → OpenTopoData (без ключа). Калории пересчитываются по сглаженным точкам; для уклона высоты сглаживаются (гасит вертикальный шум SRTM).
|
||||
- **Редактор маршрутов** — создание («Новый маршрут») и правка сохранённого маршрута прямо в приложении: тап по карте — добавить точку в конец, тап по линии — вставить точку в это место, тащить точку — двигать, тап по точке — удалить. Высоты подтягиваются онлайн после отрисовки (из предпросмотра).
|
||||
- **Де-джиттер трека** — сглаживание дрожащего GPS-трека методом Савицкого-Голея (настраиваемое окно/число итераций, живое превью на карте детали трека). Хранится обратимо: оригинал и сглаженный вариант лежат раздельно, калории пересчитываются по сглаженным точкам, откат к оригиналу в один тап. Точность каждого фикса (`accuracy_m`) пишется в БД: при пересчёте трека точки хуже порога «Макс. погрешность GPS» исключаются из расчёта и линии (обратимо — точки в БД остаются; поднять порог = вернуть). Работает во всех пересчётах (кнопка «Пересчитать», «Пересчитать всё», после де-джиттера); импорт/старьё без точности и dead-reckoning заполнения не трогает.
|
||||
- **Метки ветра** — кнопка «Ветер в лицо» на экране записи: тап открывает интервал встречного ветра, тап закрывает. Метки хранятся с треком (`wind_marks`) как эталон для проверки оценки ветра по треку; на расчёт калорий/мощности не влияют. Голосовые метки — на будущее.
|
||||
- **Экспорт/импорт** — свои продукты, рецепты и добавки одним файлом (JSON).
|
||||
- **Вес, сон, добавки** — логирование с историей.
|
||||
|
||||
|
|
|
|||
191
docs/brouter-findings.md
Normal file
191
docs/brouter-findings.md
Normal 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
353
docs/gps-model-findings.md
Normal 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`), выше описан
|
||||
алгоритмически.
|
||||
56
docs/plans/2026-09-07-manual-pauses-altitude-glitches.md
Normal file
56
docs/plans/2026-09-07-manual-pauses-altitude-glitches.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# План: ручные паузы и глюки высоты в расчёте статистики трека
|
||||
|
||||
## Context (зачем)
|
||||
Две ошибки в `GPS.computeStats` (frontend/js/gps.js):
|
||||
1. **Ручная пауза считается как езда.** Пользователь внизу подъёма ставит паузу руками,
|
||||
идёт пешком, наверху снимает. `computeStats` через гэп всё равно считает дистанцию
|
||||
по позиции, а `elevRef` переживает гэп → первый сегмент после резюма сравнивается с
|
||||
высотой ДО паузы → перепад подъёма утекает в elevGain. Нужно: **ручную паузу
|
||||
исключать** (ни дистанции, ни перепада, ни времени, ни ккал), а **автоматический
|
||||
GPS-гэп в движении — считать как сейчас** (интерполяция).
|
||||
2. **Глюки высоты, включая честный 0.** Android при отсутствии фикса высоты пишет
|
||||
`location.altitude=0.0` → в данных 146 точек `altitude_gps_m=0`, которые как «реальная
|
||||
высота» дают ложные перепады. НО плавный приход к 0 у реки — реальная высота
|
||||
(пользователь часто у рек). Правило: отбрасывать **нереальный уклон** (скачок /
|
||||
грейд >45° = >100%), а плавные значения (в т.ч. 0) — оставлять.
|
||||
|
||||
Решения (по ответам): **только для НОВЫХ треков** (ручного «разрезания» старых не делаем).
|
||||
Паузы отслеживаются в `GPS.recording.pauses` (`[{startTs,endTs,startLat,...}]`, мс epoch),
|
||||
но в БД НЕ сохранялись — начали сохранять. Данные разбираются офлайн через `tools/track/`.
|
||||
|
||||
## Изменения (финал, как реализовано)
|
||||
1. **Persist пауз (БД).** Колонка `pauses TEXT` в `gps_sessions` тем же JS-путём ADD COLUMN
|
||||
под `PRAGMA table_info(gps_sessions)`-гардом (db.js ~1540). `finishGpsSession(...pauses)`
|
||||
получил аргумент `pauses`: `hasPauses = pauses !== undefined` → пишет `JSON.stringify(pauses||[])`,
|
||||
`undefined` оставляет колонку как есть. `getGpsSession` парсит `pauses` из JSON.
|
||||
2. **`computeStats(points, wKg, opts)` — исключение ручных пауз.** `opts.pauses` (мс epoch) →
|
||||
`inManualPause(aMs,bMs) = pauses.some(z => aMs<z.e && bMs>z.s)`. В сегментном цикле:
|
||||
`if (inManualPause(prevMs,pMs)) { pausedSec += dt; elevRef = null; continue; }` — целиком
|
||||
исключает сегмент (дистанция/скорость/высота/ккал), сбрасывает elevRef. `duration -= pausedSec`.
|
||||
3. **Фильтр высоты по уклону.** В банковании перепада: `glitch = prevAlt!=null && segDist>0 &&
|
||||
|smoothedAlt[i]-prevAlt|/segDist > MAX_GRADE` (`MAX_GRADE = config.maxRealGradePct ?? 1.0`
|
||||
= 100% = 45°). При glitch — не банковать и не двигать elevRef. Плавные изменения (0 у реки) проходят.
|
||||
Существующие фильтры (выброс >4м, «застревание», сглаживание) оставлены.
|
||||
4. **Запись высоты (Kotlin `GpsRecorderService.kt` ~465).** `altStr = if (location.hasAltitude())
|
||||
location.altitude.toString() else "null"` → `"alt":${altStr}`. Нет фикса → null (не 0). Идёт вперёд.
|
||||
5. **Проброс пауз через вызовы `computeStats`/`finishGpsSession`:** live-стоп (recPauses),
|
||||
`recalculateSession` recorded-ветка (sessPauses), recovery (`s.pauses||[]`),
|
||||
`recomputeAllStats` bulk (bulkPauses из session.pauses), `showSessionDetail` (main.js:
|
||||
`{pauses: session.pauses||[]}` + вычет pauseMs из duration).
|
||||
Ручная-маршрут ветка recalculateSession паузы не передаёт (у нарисованных маршрутов пауз нет).
|
||||
|
||||
## Файлы
|
||||
- `frontend/js/gps.js` — computeStats (паузы + фильтр уклона), проброс в finish/recalculate/recovery/stop/bulk.
|
||||
- `frontend/js/db.js` — миграция `pauses`, finishGpsSession, getGpsSession.
|
||||
- `frontend/js/main.js` — showSessionDetail: передать `session.pauses`, вычесть pauseMs.
|
||||
- `src-tauri/gen/android/app/src/main/java/dev/baldav/app/GpsRecorderService.kt` — alt=null без фикса.
|
||||
|
||||
## Верификация
|
||||
1. `node --check` на трёх JS — чисто.
|
||||
2. `./build.sh -d -i android` → короткая поездка с ручной паузой на склоне → в деталях трека
|
||||
участок паузы не учтён (нет дистанции/перепада), в БД `pauses` = JSON; треки с 0-глюками не раздуты,
|
||||
плавный 0 у реки — профиль цел.
|
||||
|
||||
## Не трогаем
|
||||
- Ручной инструмент «выделить/исключить участок» — отложено.
|
||||
- Старые треки ретроактивно не правим (паузы не были сохранены). Seed/продукты не трогаем.
|
||||
39
docs/session-logs/2026-09-07_brouter-findings.md
Normal file
39
docs/session-logs/2026-09-07_brouter-findings.md
Normal 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 + загрузка, по согласию).
|
||||
47
docs/session-logs/2026-09-07_gps-model-findings-impl.md
Normal file
47
docs/session-logs/2026-09-07_gps-model-findings-impl.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Сессия 2026-09-07: реализация находок по расчётной модели GPS
|
||||
|
||||
## Контекст
|
||||
Разобрали `docs/gps-model-findings.md` (вывод независимого инструмента `tools/track/track.py`
|
||||
по 156 поездкам). Пользователь принял решения по каждой находке. Плана в plan-mode не было —
|
||||
работали по прямым решениям пользователя, зафиксированным ниже.
|
||||
|
||||
## Решения пользователя и что сделано
|
||||
- **#1 Crr-потолок 0.008 занижен → внедряем.** `crrFromAccel` в gps.js расширен: 7-11=0.008,
|
||||
11-15=0.012 (плотный грунт), >15=0.020 (трава/колея). Мапится и медиана сессии, и per-point ptCrr.
|
||||
- **#2 max(физика,MET) не складывает, подъёмы недосчитаны → только пометить.** Не меняем формулу:
|
||||
добавлен комментарий у `Math.max(physicsKcal, metNetKcal)` (ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ, нужна калибровка),
|
||||
записано в README и PROJECT_MAP.
|
||||
- **#3+#7 ходьба с велосипедом как езда → внедряем.** computeStats: для односкоростного
|
||||
(`gearCount` 1/пусто + звезда/трещотка/колесо) порог walkBikeMs = 30 об/мин; сегмент медленнее
|
||||
порога (нормальный фикс) исключается из ездовой дистанции/скорости/высоты (elevRef=null),
|
||||
усилие = walk-MET нетто. #7: исключается каждая точка без гейта по длительности, в отчёт «пешком»
|
||||
только пробеги ≥30с. Возврат: walkBikeSec/walkBikeMeters. `gearCount` прокинут во все вызовы
|
||||
(live-stop, recalculateSession, recovery, bulk, main.js старт записи + detail). Время пуша ≥30с
|
||||
вычитается из duration (средняя скорость остаётся чисто ездовой).
|
||||
- **#4 набор высоты расходится в обе стороны → только пометить.** Не код: записано в README и
|
||||
PROJECT_MAP (предел сырого GPS-альтиметра, реальный фикс — предзагрузка DEM по проводу).
|
||||
- **#5/#6** — к сведению (пульс потерян браслет; ветер в track.py, не в приложении).
|
||||
|
||||
## Ветер (часть 2 доки)
|
||||
Пользователь: калории от неточного ветра не считать. Хочет простые отслеживаемые вещи — метки
|
||||
«ветер в лицо», писать данные и делать выводы после набора километража. Это вариант 5 в роли
|
||||
РАЗМЕТКИ, не измерения. Трубка Пито (вариант 3) — точно нет. Вариант 2 (предзагрузка прогноза) —
|
||||
объяснил (скачать прогноз дома по проводу как подсказку-затравку для оценки по треку; изъян тот же,
|
||||
что у «уточнить высоты» — надо не забыть нажать перед выездом).
|
||||
|
||||
Решения по UX (AskUserQuestion): кнопка-тоггл, голос потом; только «в лицо» (бинарно). Реализовано:
|
||||
- кнопка «Ветер в лицо» на экране записи (тап открыть интервал / тап закрыть, активная подсвечена);
|
||||
- GPS.toggleWindMark()/_closeOpenWindMark(); windMarks=[{startTs,endTs}] мс, персист в localStorage,
|
||||
запись DB.setSessionWindMarks на финише, восстановление в recovery (недозакрытый интервал закрывается);
|
||||
- колонка gps_sessions.wind_marks (JSON), парсинг в getGpsSession. На distance/kcal/power НЕ влияет.
|
||||
- Анализ (сравнение меток с оценкой ветра по треку) — позже в tools/track/track.py, после километража.
|
||||
|
||||
## Результат
|
||||
Модельные правки #1/#3/#7 внедрены, #2/#4 задокументированы. `node --check` чист по всем файлам.
|
||||
Собрано и поставлено ранее build 436 (только правки пауз/высоты); эти правки требуют новой сборки.
|
||||
|
||||
## Следующие шаги
|
||||
- Собрать/поставить новую сборку, проверить: на грунтах ккал выше; пеший подъём на односкоростном
|
||||
не в ездовой статистике и не склеивается с ездовым подъёмом; кнопка «Ветер в лицо» пишет
|
||||
интервалы в gps_sessions.wind_marks.
|
||||
- Ветер: добавить голосовые метки (отдельным шагом); анализ меток vs оценка по треку в track.py.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Сессия 2026-09-07: ручные паузы и глюки высоты в статистике трека
|
||||
|
||||
## Контекст
|
||||
На последнем ~56км-треке подъём пройден пешком: внизу пауза руками, наверху снята. BalDav
|
||||
мостил GPS-гэп (дистанция + перепад высоты через elevRef, переживший гэп) как езду. Плюс
|
||||
Android пишет `altitude=0.0` без фикса высоты (146 точек в данных), что как «реальная высота»
|
||||
раздувает набор высоты. Пользователь часто у рек, где 0 может быть честным — нельзя тупо
|
||||
выкидывать 0. Правило: исключать нереальный уклон (>45°/>100%), плавные значения (в т.ч. 0) хранить.
|
||||
|
||||
## Действия (по плану docs/plans/2026-09-07-manual-pauses-altitude-glitches.md)
|
||||
Работали по утверждённому плану. Реализовано:
|
||||
- db.js: колонка `gps_sessions.pauses TEXT` (JS ADD COLUMN миграция), `finishGpsSession(...,pauses)`
|
||||
с `hasPauses`-гардом (undefined = не трогать колонку), `getGpsSession` парсит JSON.
|
||||
- gps.js `computeStats`: `opts.pauses` → `inManualPause`; сегмент, пересекающий паузу,
|
||||
исключается целиком (дистанция/скорость/высота/ккал), `elevRef=null`, `pausedSec` вычитается
|
||||
из duration. Глюк-гард высоты: соседний перепад с грейдом > `maxRealGradePct` (1.0=45°) не
|
||||
банкуется и не двигает elevRef. Проброс пауз во все вызовы: live-стоп, recalculateSession
|
||||
(recorded), recovery, recomputeAllStats (bulk), showSessionDetail.
|
||||
- main.js showSessionDetail: передаёт `session.pauses`, вычитает pauseMs из длительности.
|
||||
- Kotlin GpsRecorderService: `alt=null` без `hasAltitude()`, честный 0 с фиксом сохраняется.
|
||||
|
||||
## Результат
|
||||
Только для НОВЫХ треков (старые паузы не сохранялись — ретро-фикса нет; UI ручного разрезания
|
||||
отложен). Все 3 JS проходят `node --check`. PROJECT_MAP обновлён (computeStats-раздел + колонка pauses).
|
||||
|
||||
## Следующие шаги
|
||||
- `./build.sh -d -i android`, записать короткую поездку с ручной паузой на склоне, проверить:
|
||||
участок паузы не в статистике, `pauses` в БД = JSON, треки с 0-глюками не раздуты.
|
||||
24
docs/session-logs/2026-09-07_rec-statusbar-backdrop.md
Normal file
24
docs/session-logs/2026-09-07_rec-statusbar-backdrop.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Сессия 2026-09-07: статус-бар на экране записи
|
||||
|
||||
## Проблема
|
||||
При включённом трекинге не видно системный статус-бар. Пользователь: перекрывают его только
|
||||
карты, остальное приложение ведёт себя нормально. Просьба — сделать бар видимым (не рисовать свой
|
||||
индикатор батареи).
|
||||
|
||||
## Причина
|
||||
Карта на экране записи (`.rec-map-wrap`/`.rec-map`) рисуется edge-to-edge и заходит под системный
|
||||
статус-бар. Контролы карты (компас, зум-кнопки leaflet) уже сдвинуты вниз на `--safe-area-top`
|
||||
(строки 1155, 1161 styles.css) — то есть дизайн задумывался с видимым статус-баром, но у самого бара
|
||||
нет подложки: иконки теряются на тайлах карты. На остальных экранах подложку даёт «шапка»
|
||||
(`padding-top: calc(... + var(--safe-area-top)); background: var(--surface)`).
|
||||
|
||||
## Решение (только CSS, без сдвига координат карты)
|
||||
Псевдоэлемент `.rec-map-wrap::before` — сплошная полоса высотой `var(--safe-area-top)` цвета
|
||||
`var(--surface)`, `z-index:1100`, `pointer-events:none`. Даёт статус-бару подложку как «шапка».
|
||||
Контролы карты уже начинаются ниже полосы, наложения нет. В ландшафте (rot-90/270) полоса отключена
|
||||
(`display:none`) — там карта повёрнута и инсет физически в другом месте.
|
||||
|
||||
`--safe-area-top` ставится из нативных инсетов (main.js:189, get_safe_insets), на устройстве ненулевой.
|
||||
|
||||
## Файлы
|
||||
- frontend/css/styles.css — `.rec-map-wrap::before` + отключение в rot-90/270.
|
||||
|
|
@ -1159,6 +1159,21 @@ select {
|
|||
}
|
||||
/* Keep Leaflet zoom controls below the status bar on the recording map. */
|
||||
.rec-map-wrap .leaflet-top { margin-top: var(--safe-area-top, 0px); }
|
||||
/* Status-bar backdrop. The recording map is edge-to-edge (draws behind the system
|
||||
status bar), so the bar had no backing and its icons were unreadable over the map
|
||||
tiles. Paint a solid strip the height of the top inset — like the header on every
|
||||
other screen — so the status bar stays visible. Controls already start below it. */
|
||||
.rec-map-wrap::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: var(--safe-area-top, 0px);
|
||||
background: var(--surface);
|
||||
z-index: 1100;
|
||||
pointer-events: none;
|
||||
}
|
||||
.rec-screen.rot-90 .rec-map-wrap::before,
|
||||
.rec-screen.rot-270 .rec-map-wrap::before { display: none; }
|
||||
.rec-compass-arrow { position: absolute; inset: 0; transition: transform 0.12s linear; }
|
||||
.rec-compass-arrow::before {
|
||||
content: "N"; position: absolute; top: 1px; left: 50%; transform: translateX(-50%);
|
||||
|
|
@ -1262,6 +1277,7 @@ select {
|
|||
.rec-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
padding-bottom: calc(8px + var(--safe-area-bottom));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -779,14 +779,19 @@ async function addGpsPointsBulk(sessionId, points, startSeq) {
|
|||
return inserted;
|
||||
}
|
||||
|
||||
async function finishGpsSession(id, endTime, distM, kcal, avgSpd, maxSpd, elevGain, elevLoss) {
|
||||
async function finishGpsSession(id, endTime, distM, kcal, avgSpd, maxSpd, elevGain, elevLoss, pauses) {
|
||||
// Guard against NaN — SQLite may reject or corrupt NaN values
|
||||
const safe = v => (v != null && !isNaN(v)) ? v : null;
|
||||
// pauses: array of manual-pause intervals; store as JSON. undefined = leave column as-is.
|
||||
const hasPauses = pauses !== undefined;
|
||||
const pausesJson = hasPauses ? JSON.stringify(pauses || []) : null;
|
||||
await execute(
|
||||
`UPDATE gps_sessions SET end_time=?1, total_distance_m=?2, total_kcal=?3,
|
||||
avg_speed_kmh=?4, max_speed_kmh=?5, elevation_gain_m=?6, elevation_loss_m=?7
|
||||
avg_speed_kmh=?4, max_speed_kmh=?5, elevation_gain_m=?6, elevation_loss_m=?7${hasPauses ? ", pauses=?9" : ""}
|
||||
WHERE id=?8`,
|
||||
[endTime, safe(distM), safe(kcal), safe(avgSpd), safe(maxSpd), safe(elevGain), safe(elevLoss), id]
|
||||
hasPauses
|
||||
? [endTime, safe(distM), safe(kcal), safe(avgSpd), safe(maxSpd), safe(elevGain), safe(elevLoss), id, pausesJson]
|
||||
: [endTime, safe(distM), safe(kcal), safe(avgSpd), safe(maxSpd), safe(elevGain), safe(elevLoss), id]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -843,7 +848,18 @@ async function getGpsSessionsForDate(date) {
|
|||
|
||||
async function getGpsSession(id) {
|
||||
const rows = await select("SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions WHERE id = ?1", [id]);
|
||||
return rows.length ? rows[0] : null;
|
||||
if (!rows.length) return null;
|
||||
const s = rows[0];
|
||||
if (typeof s.pauses === "string") { try { s.pauses = JSON.parse(s.pauses); } catch { s.pauses = []; } }
|
||||
if (typeof s.wind_marks === "string") { try { s.wind_marks = JSON.parse(s.wind_marks); } catch { s.wind_marks = []; } }
|
||||
return s;
|
||||
}
|
||||
|
||||
// Save headwind labels for a session (JSON). Kept separate from finishGpsSession so
|
||||
// recompute paths preserve marks (they never call this). Marks are ground truth for
|
||||
// the wind estimate — they do not affect distance/kcal/power.
|
||||
async function setSessionWindMarks(id, marks) {
|
||||
await execute("UPDATE gps_sessions SET wind_marks=?1 WHERE id=?2", [JSON.stringify(marks || []), id]);
|
||||
}
|
||||
|
||||
async function getGpsPoints(sessionId) {
|
||||
|
|
@ -1188,6 +1204,7 @@ window.DB.createGpsSession = createGpsSession;
|
|||
window.DB.addGpsPoint = addGpsPoint;
|
||||
window.DB.addGpsPointsBulk = addGpsPointsBulk;
|
||||
window.DB.finishGpsSession = finishGpsSession;
|
||||
window.DB.setSessionWindMarks = setSessionWindMarks;
|
||||
window.DB.saveSmoothedTrack = saveSmoothedTrack;
|
||||
window.DB.setSessionSmoothed = setSessionSmoothed;
|
||||
window.DB.getGpsSessions = getGpsSessions;
|
||||
|
|
@ -1529,6 +1546,8 @@ async function runMigrations() {
|
|||
if (!gsCols.includes("smoothed")) await execute("ALTER TABLE gps_sessions ADD COLUMN smoothed INTEGER DEFAULT 0");
|
||||
if (!gsCols.includes("smooth_window")) await execute("ALTER TABLE gps_sessions ADD COLUMN smooth_window INTEGER");
|
||||
if (!gsCols.includes("smooth_iters")) await execute("ALTER TABLE gps_sessions ADD COLUMN smooth_iters INTEGER");
|
||||
if (!gsCols.includes("pauses")) await execute("ALTER TABLE gps_sessions ADD COLUMN pauses TEXT"); // JSON [{startTs,endTs,...}] manual pauses
|
||||
if (!gsCols.includes("wind_marks")) await execute("ALTER TABLE gps_sessions ADD COLUMN wind_marks TEXT"); // JSON [{startTs,endTs}] headwind labels (ground truth for wind estimate)
|
||||
|
||||
// Add tire_pressure_bar to bikes (for Crr correction by tire pressure)
|
||||
const bikeCols = (await select("PRAGMA table_info(bikes)")).map(r => r.name);
|
||||
|
|
|
|||
|
|
@ -111,6 +111,16 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
let elevRef = null; // running reference for cumulative elevation hysteresis
|
||||
const speeds = []; // m/s per segment
|
||||
|
||||
// Manual pauses (ms epoch): the rider hand-paused, so that stretch must NOT count
|
||||
// (walked, not ridden) — unlike an automatic GPS gap in motion, which we keep.
|
||||
// A segment that touches a pause interval is dropped and elevRef is reset (so the
|
||||
// elevation delta across the pause is not banked as a climb).
|
||||
const manualPauses = (opts && opts.pauses ? opts.pauses : [])
|
||||
.map(z => ({ s: z.startTs, e: z.endTs })).filter(z => z.s != null && z.e != null);
|
||||
const inManualPause = (aMs, bMs) => manualPauses.some(z => aMs < z.e && bMs > z.s);
|
||||
const MAX_GRADE = GPS.config.maxRealGradePct ?? 1.0; // 1.0 = 100% = 45°; steeper adjacent alt step = glitch
|
||||
let pausedSec = 0;
|
||||
|
||||
// 5-point moving average on altitude to kill GPS jitter (typically 5-15m noise)
|
||||
// Step 1: Extract raw altitudes and filter outliers.
|
||||
// An outlier is a point that jumps far from its neighbors and returns back —
|
||||
|
|
@ -171,15 +181,21 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
const rho = GPS.config.airDensity ?? 1.225;
|
||||
const efficiency = (GPS.config.pedalEfficiency ?? 25) / 100;
|
||||
// Rolling resistance: base from surface type, corrected by tire pressure.
|
||||
// If accelerometer data available, estimate Crr from vibration (accel RMS):
|
||||
// RMS < 3 → smooth asphalt (0.004), 3-7 → mixed (0.006), > 7 → gravel (0.008)
|
||||
// If accelerometer data available, estimate Crr from vibration (accel RMS).
|
||||
// Scale (lower bound of a back-solve on measured tracks — better to undercount):
|
||||
// RMS < 3 → 0.004 smooth asphalt | 11-15 → 0.012 packed dirt
|
||||
// 3-7 → 0.006 mixed | > 15 → 0.020 grass / rut / gravel
|
||||
// 7-11 → 0.008 broken asphalt
|
||||
// The old ceiling of 0.008 (gravel) undercounted rough surfaces ~3x: measured
|
||||
// grass+tractor-rut needs 0.020-0.031, packed dirt 0.012. crrFromAccel maps both
|
||||
// the session median (default Crr) and each point (ptCrr below) through this scale.
|
||||
const crrFromAccel = (rms) => rms < 3 ? 0.004 : rms < 7 ? 0.006 : rms < 11 ? 0.008 : rms < 15 ? 0.012 : 0.020;
|
||||
let baseCrr = opts?.surfaceCrr ?? 0.006;
|
||||
const accelVals = sorted.map(p => p.accel_rms).filter(v => v != null && v > 0);
|
||||
if (accelVals.length > 10) {
|
||||
accelVals.sort((a, b) => a - b);
|
||||
const medianAccel = accelVals[Math.floor(accelVals.length / 2)];
|
||||
const accelCrr = medianAccel < 3 ? 0.004 : medianAccel < 7 ? 0.006 : 0.008;
|
||||
baseCrr = accelCrr; // override with sensor-based estimate
|
||||
baseCrr = crrFromAccel(medianAccel); // override with sensor-based estimate
|
||||
}
|
||||
const tirePressure = opts?.tirePressureBar ?? 3.0;
|
||||
const Crr = baseCrr * Math.sqrt(3.0 / tirePressure);
|
||||
|
|
@ -197,12 +213,31 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
const wheelCirc = hasGearData ? Math.PI * opts.wheelDiameterInch * 0.0254 : 0; // meters
|
||||
const gearRatio = hasGearData ? opts.chainringTeeth / opts.cogTeeth : 0;
|
||||
|
||||
// Walking the bike (single-speed only). On a fixed ratio, below ~30 rpm cadence
|
||||
// the bike can't stay upright — so a sustained sub-30-rpm speed means the rider
|
||||
// is pushing it, not riding. speed = cadence * distance-per-crank-rev.
|
||||
// distance-per-rev = wheelCirc * gearRatio; at 30 rpm → walkBikeMs m/s.
|
||||
// Only for single-speed (gear_count 1 or unset): on a geared bike low speed proves
|
||||
// nothing (accelerometer can't tell pushing from riding — measured). Sub-threshold
|
||||
// segments are excluded from ride distance/speed/elevation so a pushed climb doesn't
|
||||
// merge with the ridden one; the effort is counted as walking, not bike physics.
|
||||
const gearCount = opts?.gearCount;
|
||||
const singleSpeed = isBike && hasGearData && (gearCount == null || gearCount <= 1);
|
||||
const walkBikeMs = singleSpeed ? (30 * wheelCirc * gearRatio) / 60 : 0;
|
||||
let walkBikeSec = 0, walkBikeDist = 0; // reported total (only runs ≥30s, see #7)
|
||||
let wbRunSec = 0, wbRunDist = 0; // current contiguous push run
|
||||
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
let p = sorted[i], prev = sorted[i - 1];
|
||||
let d = GPS.distance(prev.lat, prev.lon, p.lat, p.lon);
|
||||
let dt = (new Date(p.ts) - new Date(prev.ts)) / 1000;
|
||||
let v = dt > 0 ? d / dt : 0; // m/s
|
||||
|
||||
// Manual pause: exclude entirely (walked, not ridden). Reset elevRef so the
|
||||
// elevation gained during the pause isn't banked on the next real segment.
|
||||
let prevMs = new Date(prev.ts).getTime(), pMs = new Date(p.ts).getTime();
|
||||
if (inManualPause(prevMs, pMs)) { pausedSec += Math.max(0, dt); elevRef = null; continue; }
|
||||
|
||||
let isEstimated = (sorted[i].accuracy >= 999) || sorted[i].estimated;
|
||||
let segMaxMs = isBike ? (GPS.config.maxSpeedKmh / 3.6) : (GPS.config.walkMaxSpeedKmh / 3.6);
|
||||
let gapThr = GPS.config.gapThresholdSec ?? 30;
|
||||
|
|
@ -230,6 +265,24 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
// altitude and max-speed stats; gap and spike segments do not.
|
||||
let normalMove = !isGap && !isSpike && dt > 0;
|
||||
|
||||
// Walking the bike (single-speed): a normal-fix segment slower than the 30-rpm
|
||||
// speed. Exclude it from ride distance/speed/elevation (elevRef reset breaks the
|
||||
// pushed-vs-ridden climb merge) and count the effort as walking, not bike physics.
|
||||
// Per #7: exclude EVERY such point regardless of duration (brief brake-to-stops
|
||||
// land here too — harmless, ~0 m and low cost); accumulate contiguous runs and
|
||||
// only report those ≥30s as "walked" (a stop before a pothole isn't walking).
|
||||
if (walkBikeMs > 0 && normalMove && v > 0 && v < walkBikeMs) {
|
||||
wbRunSec += dt; wbRunDist += segDist;
|
||||
elevRef = null;
|
||||
const wKmh = v * 3.6;
|
||||
const wmet = wKmh < 4 ? 2.5 : wKmh < 5.5 ? 3.5 : wKmh < 7 ? 5.0 : 8.0;
|
||||
totalKcal += (dt / 60) * ((wmet - 1) * 3.5 * wKg) / 200; // NET (BMR booked in daily balance)
|
||||
continue;
|
||||
}
|
||||
// Not (or no longer) pushing → flush a completed push run into the reported total.
|
||||
if (wbRunSec >= 30) { walkBikeSec += wbRunSec; walkBikeDist += wbRunDist; }
|
||||
wbRunSec = 0; wbRunDist = 0;
|
||||
|
||||
if (segDist > 0) distance += segDist;
|
||||
if (normalMove) {
|
||||
speeds.push(v);
|
||||
|
|
@ -239,10 +292,17 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
// climbs. Accumulate the rise/fall from the reference and only bank it (and
|
||||
// advance the reference) once it clears the 1 m dead zone.
|
||||
if (smoothedAlt[i] != null) {
|
||||
if (elevRef == null) elevRef = smoothedAlt[i];
|
||||
let rise = smoothedAlt[i] - elevRef;
|
||||
if (rise > 1) { elevGain += rise; elevRef = smoothedAlt[i]; }
|
||||
else if (rise < -1) { elevLoss += -rise; elevRef = smoothedAlt[i]; }
|
||||
// Reject a physically impossible adjacent altitude step (glitch — incl. an
|
||||
// Android no-fix 0.0): grade over the segment steeper than MAX_GRADE (45°).
|
||||
// A smooth change to any value (e.g. a real 0 near a river) passes.
|
||||
let prevAlt = smoothedAlt[i - 1];
|
||||
let glitch = prevAlt != null && segDist > 0 && Math.abs(smoothedAlt[i] - prevAlt) / segDist > MAX_GRADE;
|
||||
if (!glitch) {
|
||||
if (elevRef == null) elevRef = smoothedAlt[i];
|
||||
let rise = smoothedAlt[i] - elevRef;
|
||||
if (rise > 1) { elevGain += rise; elevRef = smoothedAlt[i]; }
|
||||
else if (rise < -1) { elevLoss += -rise; elevRef = smoothedAlt[i]; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,7 +335,7 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
// Per-point Crr from accelerometer (if available), else session-wide Crr
|
||||
const ptAccel = sorted[i].accel_rms;
|
||||
const ptCrr = (ptAccel != null && ptAccel > 0)
|
||||
? (ptAccel < 3 ? 0.004 : ptAccel < 7 ? 0.006 : 0.008) * Math.sqrt(3.0 / tirePressure)
|
||||
? crrFromAccel(ptAccel) * Math.sqrt(3.0 / tirePressure)
|
||||
: Crr;
|
||||
|
||||
// Power required (watts): gravity + rolling resistance + aerodynamic drag
|
||||
|
|
@ -310,6 +370,12 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
if (vKmh >= 5) {
|
||||
const met = vKmh < 16 ? 6.0 : vKmh < 19 ? 6.8 : vKmh < 22 ? 8.0 : vKmh < 25 ? 10.0 : vKmh < 28 ? 12.0 : 14.0;
|
||||
const metNetKcal = (dt / 60) * ((met - 1) * 3.5 * wKg) / 200;
|
||||
// KNOWN LIMITATION (measured, left as-is by decision): max() never SUMS the
|
||||
// two, so on climbs — where physics wins big — the biological overhead the
|
||||
// MET floor captures (balancing, core stabilisation, no external work) is
|
||||
// dropped. Direction of the error is known: climbs are undercounted (whole-
|
||||
// ride ~1.34x MET/physics, but flat 1.85x and climb 0.41x). A principled fix
|
||||
// (physics/eff + overhead) needs calibration, so it is deferred, not applied.
|
||||
totalKcal += Math.max(physicsKcal, metNetKcal);
|
||||
} else {
|
||||
totalKcal += physicsKcal; // creeping < 5 km/h: negligible
|
||||
|
|
@ -340,6 +406,9 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
}
|
||||
}
|
||||
|
||||
// Flush a push run still open at the track's end.
|
||||
if (wbRunSec >= 30) { walkBikeSec += wbRunSec; walkBikeDist += wbRunDist; }
|
||||
|
||||
// 9-point smoothed speeds for maxSpeed (kills spikes from GPS noise)
|
||||
let maxSpeed = 0;
|
||||
for (let i = 0; i < speeds.length; i++) {
|
||||
|
|
@ -353,7 +422,9 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
|
||||
const first = new Date(sorted[0].ts);
|
||||
const last = new Date(sorted[sorted.length - 1].ts);
|
||||
const duration = (last - first) / 1000;
|
||||
// Exclude manual-pause time and sustained bike-push time — both keep avgSpeed
|
||||
// (distance/duration) a pure riding figure, consistent with the excluded distance.
|
||||
const duration = Math.max(0, (last - first) / 1000 - pausedSec - walkBikeSec);
|
||||
const avgSpeed = duration > 0 ? (distance / duration) : 0;
|
||||
const avgSpeedKmh = avgSpeed * 3.6;
|
||||
const maxSpeedKmh = maxSpeed * 3.6;
|
||||
|
|
@ -459,6 +530,8 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
duration: Math.round(duration),
|
||||
avgCadence,
|
||||
segments,
|
||||
walkBikeSec: Math.round(walkBikeSec), // time spent pushing the bike (single-speed, ≥30s runs)
|
||||
walkBikeMeters: Math.round(walkBikeDist), // distance walked pushing it (excluded from ride distance)
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -889,6 +962,32 @@ GPS._processIncomingPoint = async function(p, fromNative) {
|
|||
if (GPS.recording.onPoint) GPS.recording.onPoint(p);
|
||||
};
|
||||
|
||||
// Headwind labelling (variant 5 as VALIDATION, not measurement): the rider taps
|
||||
// to open a "wind in the face" interval and taps again to close it. Marks are
|
||||
// stored per session ([{startTs, endTs}] ms epoch) and later compared against the
|
||||
// from-track wind estimate — they are ground truth, they do NOT feed kcal/power.
|
||||
// Returns true if a headwind interval is now open (button should show active).
|
||||
GPS.toggleWindMark = function() {
|
||||
if (!GPS.recording.windMarks) GPS.recording.windMarks = [];
|
||||
if (GPS.recording._currentWind) {
|
||||
GPS.recording._currentWind.endTs = Date.now();
|
||||
GPS.recording._currentWind = null;
|
||||
} else {
|
||||
GPS.recording._currentWind = { startTs: Date.now(), endTs: null };
|
||||
GPS.recording.windMarks.push(GPS.recording._currentWind);
|
||||
}
|
||||
GPS._persistState();
|
||||
return GPS.recording._currentWind != null;
|
||||
};
|
||||
|
||||
// Close an interval left open (rider forgot to tap off before Stop / recovery).
|
||||
GPS._closeOpenWindMark = function() {
|
||||
if (GPS.recording._currentWind && GPS.recording._currentWind.endTs == null) {
|
||||
GPS.recording._currentWind.endTs = Date.now();
|
||||
}
|
||||
GPS.recording._currentWind = null;
|
||||
};
|
||||
|
||||
// Persist recording state to localStorage for crash recovery
|
||||
GPS._persistState = function() {
|
||||
try {
|
||||
|
|
@ -900,6 +999,7 @@ GPS._persistState = function() {
|
|||
totalPauseMs: GPS.recording.totalPauseMs,
|
||||
manualPaused: GPS.recording.manualPaused,
|
||||
pauses: GPS.recording.pauses || [],
|
||||
windMarks: GPS.recording.windMarks || [],
|
||||
pointCount: GPS.recording.points.length,
|
||||
bikeId: GPS.recording.bikeId ?? null,
|
||||
bikeWeightKg: GPS.recording.bikeWeightKg ?? null,
|
||||
|
|
@ -908,6 +1008,7 @@ GPS._persistState = function() {
|
|||
chainringTeeth: GPS.recording.chainringTeeth ?? null,
|
||||
cogTeeth: GPS.recording.cogTeeth ?? null,
|
||||
wheelDiameterInch: GPS.recording.wheelDiameterInch ?? null,
|
||||
gearCount: GPS.recording.gearCount ?? null,
|
||||
}));
|
||||
} catch {}
|
||||
};
|
||||
|
|
@ -991,10 +1092,17 @@ GPS.recoverSession = async function() {
|
|||
chainringTeeth: s.chainringTeeth,
|
||||
cogTeeth: s.cogTeeth,
|
||||
wheelDiameterInch: s.wheelDiameterInch,
|
||||
gearCount: s.gearCount,
|
||||
pauses: s.pauses || [],
|
||||
};
|
||||
const stats = GPS.computeStats(pts, s.weightKg || 80, recoverOpts);
|
||||
await DB.finishGpsSession(s.sessionId, new Date().toTimeString().slice(0, 5),
|
||||
stats.distance, stats.kcal, stats.avgSpeed, stats.maxSpeed, stats.elevGain, stats.elevLoss);
|
||||
stats.distance, stats.kcal, stats.avgSpeed, stats.maxSpeed, stats.elevGain, stats.elevLoss, s.pauses || []);
|
||||
// Restore headwind marks captured before the crash (close a dangling open one)
|
||||
if (Array.isArray(s.windMarks) && s.windMarks.length) {
|
||||
for (const w of s.windMarks) if (w && w.endTs == null) w.endTs = new Date(pts[pts.length - 1].ts).getTime();
|
||||
try { await DB.setSessionWindMarks(s.sessionId, s.windMarks); } catch (e) { console.error("[GPS] wind marks restore failed:", e); }
|
||||
}
|
||||
// Store bike_id if present
|
||||
if (s.bikeId) await DB.updateSessionBike(s.sessionId, s.bikeId);
|
||||
console.log("[GPS] Recovered session", s.sessionId, "with", pts.length, "points");
|
||||
|
|
@ -1028,6 +1136,8 @@ GPS.startRecording = async function(activityType, weightKg) {
|
|||
GPS.recording.manualPaused = false;
|
||||
GPS.recording.pauses = [];
|
||||
GPS.recording._currentPause = null;
|
||||
GPS.recording.windMarks = []; // headwind labels: [{startTs, endTs}] ms epoch
|
||||
GPS.recording._currentWind = null; // open headwind interval, if any
|
||||
GPS.recording.nativeService = false;
|
||||
GPS.recording._pollTimer = null;
|
||||
GPS.recording._lastProcessedTs = 0;
|
||||
|
|
@ -1170,16 +1280,24 @@ GPS.stopRecording = async function() {
|
|||
chainringTeeth: GPS.recording.chainringTeeth,
|
||||
cogTeeth: GPS.recording.cogTeeth,
|
||||
wheelDiameterInch: GPS.recording.wheelDiameterInch,
|
||||
gearCount: GPS.recording.gearCount,
|
||||
};
|
||||
const recPauses = GPS.recording.pauses || [];
|
||||
recOpts.pauses = recPauses;
|
||||
const stats = GPS.computeStats(GPS.recording.points, GPS.recording.weightKg, recOpts);
|
||||
const endTime = new Date().toTimeString().slice(0, 5);
|
||||
|
||||
// Close an open headwind interval and persist all marks with the session.
|
||||
GPS._closeOpenWindMark();
|
||||
const recWindMarks = GPS.recording.windMarks || [];
|
||||
|
||||
try {
|
||||
await DB.finishGpsSession(
|
||||
GPS.recording.sessionId, endTime,
|
||||
stats.distance, stats.kcal, stats.avgSpeed, stats.maxSpeed,
|
||||
stats.elevGain, stats.elevLoss
|
||||
stats.elevGain, stats.elevLoss, recPauses
|
||||
);
|
||||
try { await DB.setSessionWindMarks(GPS.recording.sessionId, recWindMarks); } catch (e) { console.error("[GPS] wind marks save failed:", e); }
|
||||
// DB save confirmed — now safe to delete the points file
|
||||
if (nativePointsFile) {
|
||||
try { await window.__TAURI__.core.invoke("delete_points_file", { filename: nativePointsFile }); } catch {}
|
||||
|
|
@ -1207,6 +1325,9 @@ GPS.stopRecording = async function() {
|
|||
GPS.recording.chainringTeeth = null;
|
||||
GPS.recording.cogTeeth = null;
|
||||
GPS.recording.wheelDiameterInch = null;
|
||||
GPS.recording.gearCount = null;
|
||||
GPS.recording.windMarks = [];
|
||||
GPS.recording._currentWind = null;
|
||||
GPS._clearPersistedState();
|
||||
return { sessionId: sid, stats, pauses };
|
||||
};
|
||||
|
|
@ -1710,6 +1831,8 @@ GPS.recalculateSession = async function(sessionId, opts) {
|
|||
const CRR_MAP = { asphalt: 0.004, mixed: 0.006, gravel: 0.008 };
|
||||
|
||||
// Use cleaned sorted array, not original points
|
||||
const sessPauses = Array.isArray(session.pauses) ? session.pauses
|
||||
: (typeof session.pauses === "string" ? (() => { try { return JSON.parse(session.pauses); } catch { return []; } })() : []);
|
||||
const stats = GPS.computeStats(sorted, weightKg, {
|
||||
bikeWeightKg: bike?.weight_kg ?? 0,
|
||||
activityType: session.activity_type ?? "bike",
|
||||
|
|
@ -1718,13 +1841,15 @@ GPS.recalculateSession = async function(sessionId, opts) {
|
|||
chainringTeeth: bike?.chainring_teeth,
|
||||
cogTeeth: bike?.cog_teeth,
|
||||
wheelDiameterInch: bike?.wheel_diameter_inch,
|
||||
gearCount: bike?.gear_count,
|
||||
pauses: sessPauses,
|
||||
});
|
||||
|
||||
// Update session in DB
|
||||
// Update session in DB (preserve pauses)
|
||||
await DB.finishGpsSession(sessionId,
|
||||
session.end_time ?? session.start_time,
|
||||
stats.distance, stats.kcal, stats.avgSpeed, stats.maxSpeed,
|
||||
stats.elevGain, stats.elevLoss
|
||||
stats.elevGain, stats.elevLoss, sessPauses
|
||||
);
|
||||
|
||||
return stats;
|
||||
|
|
@ -1760,6 +1885,8 @@ GPS.recomputeAllStats = async function(onProgress) {
|
|||
const points = await DB.getGpsPoints(sid);
|
||||
if (points.length < 2) { skipped++; continue; }
|
||||
const bike = session.bike_id ? await DB.getBike(session.bike_id) : await DB.getActiveBike();
|
||||
const bulkPauses = Array.isArray(session.pauses) ? session.pauses
|
||||
: (typeof session.pauses === "string" ? (() => { try { return JSON.parse(session.pauses); } catch { return []; } })() : []);
|
||||
const stats = GPS.computeStats(points, weightKg, {
|
||||
bikeWeightKg: bike?.weight_kg ?? 0,
|
||||
activityType: session.activity_type ?? "bike",
|
||||
|
|
@ -1768,9 +1895,11 @@ GPS.recomputeAllStats = async function(onProgress) {
|
|||
chainringTeeth: bike?.chainring_teeth,
|
||||
cogTeeth: bike?.cog_teeth,
|
||||
wheelDiameterInch: bike?.wheel_diameter_inch,
|
||||
gearCount: bike?.gear_count,
|
||||
pauses: bulkPauses,
|
||||
});
|
||||
await DB.finishGpsSession(sid, session.end_time ?? session.start_time,
|
||||
stats.distance, stats.kcal, stats.avgSpeed, stats.maxSpeed, stats.elevGain, stats.elevLoss);
|
||||
stats.distance, stats.kcal, stats.avgSpeed, stats.maxSpeed, stats.elevGain, stats.elevLoss, bulkPauses);
|
||||
updated++;
|
||||
} catch (e) { console.error(`[recalc-all] ${sid} failed`, e); skipped++; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2774,6 +2774,7 @@ async function renderGpsScreen(container) {
|
|||
GPS.recording.chainringTeeth = bike.chainring_teeth;
|
||||
GPS.recording.cogTeeth = bike.cog_teeth;
|
||||
GPS.recording.wheelDiameterInch = bike.wheel_diameter_inch;
|
||||
GPS.recording.gearCount = bike.gear_count;
|
||||
}
|
||||
}
|
||||
await GPS.startRecording(type, wkg);
|
||||
|
|
@ -3840,6 +3841,7 @@ async function renderRecordingInline(container) {
|
|||
</div>
|
||||
<div class="rec-bottom">
|
||||
<div class="rec-status" id="rec-status">${GPS.recording.state === "waiting_gps" ? "Поиск GPS..." : GPS.recording.state === "ready" ? "Начните движение..." : ""}</div>
|
||||
<button id="rec-wind" class="btn-secondary"${GPS.recording._currentWind ? ' style="background:#ffb74d;color:#1a1a1a"' : ""}>Ветер в лицо</button>
|
||||
<button id="rec-pause" class="btn-secondary">${GPS.recording.manualPaused ? "Продолжить" : "Пауза"}</button>
|
||||
<button id="rec-stop" class="btn-primary gps-rec recording">Стоп</button>
|
||||
</div>
|
||||
|
|
@ -4288,6 +4290,15 @@ async function renderRecordingInline(container) {
|
|||
fetchTemperature();
|
||||
let weatherTimer = setInterval(fetchTemperature, 15 * 60 * 1000);
|
||||
|
||||
// Headwind label toggle: tap to open a "wind in the face" interval, tap to close.
|
||||
// Marks are ground truth for validating the from-track wind estimate — not kcal.
|
||||
let windBtn = container.querySelector("#rec-wind");
|
||||
if (windBtn) windBtn.onclick = () => {
|
||||
let active = GPS.toggleWindMark();
|
||||
windBtn.style.background = active ? "#ffb74d" : "";
|
||||
windBtn.style.color = active ? "#1a1a1a" : "";
|
||||
};
|
||||
|
||||
// Pause button
|
||||
container.querySelector("#rec-pause").onclick = () => {
|
||||
if (GPS.recording.manualPaused) {
|
||||
|
|
@ -4541,14 +4552,16 @@ async function showSessionDetail(sessionId) {
|
|||
elevGain: Math.round(session.elevation_gain_m || 0),
|
||||
elevLoss: Math.round(session.elevation_loss_m || 0),
|
||||
duration: 0,
|
||||
} : GPS.computeStats(points, state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80);
|
||||
// Compute duration from points regardless
|
||||
} : GPS.computeStats(points, state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80, { pauses: session.pauses || [] });
|
||||
// Manual-pause total (ms) to exclude from displayed duration
|
||||
let pauseMs = (session.pauses || []).reduce((a, z) => a + Math.max(0, (z.endTs || 0) - (z.startTs || 0)), 0);
|
||||
// Compute duration from points regardless (minus manual pauses)
|
||||
if (points.length >= 2) {
|
||||
stats.duration = Math.round((new Date(points[points.length-1].ts) - new Date(points[0].ts)) / 1000);
|
||||
stats.duration = Math.max(0, Math.round((new Date(points[points.length-1].ts) - new Date(points[0].ts)) / 1000 - pauseMs / 1000));
|
||||
}
|
||||
// Always compute segments from points (needed for the Segments section)
|
||||
if (!stats.segments && points.length >= 3) {
|
||||
let fullStats = GPS.computeStats(points, state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80);
|
||||
let fullStats = GPS.computeStats(points, state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80, { pauses: session.pauses || [] });
|
||||
stats.segments = fullStats.segments;
|
||||
}
|
||||
|
||||
|
|
@ -4600,6 +4613,7 @@ async function showSessionDetail(sessionId) {
|
|||
|
||||
showModal(`
|
||||
<h3>${actName}</h3>
|
||||
<div id="sd-track-id" style="font-size:11px;color:var(--muted);margin:-6px 0 8px;cursor:pointer;user-select:all">ID: ${session.id}</div>
|
||||
|
||||
<div class="sd-section">
|
||||
<div class="sd-title">Время</div>
|
||||
|
|
@ -4773,6 +4787,7 @@ async function showSessionDetail(sessionId) {
|
|||
bikeWeightKg: bike?.weight_kg ?? 0, activityType: session.activity_type ?? "bike",
|
||||
surfaceCrr: CRR[bike?.surface_type] ?? 0.006, tirePressureBar: bike?.tire_pressure_bar ?? 3.0,
|
||||
chainringTeeth: bike?.chainring_teeth, cogTeeth: bike?.cog_teeth, wheelDiameterInch: bike?.wheel_diameter_inch,
|
||||
gearCount: bike?.gear_count,
|
||||
});
|
||||
await DB.finishGpsSession(sessionId, session.end_time ?? session.start_time,
|
||||
st.distance, st.kcal, st.avgSpeed, st.maxSpeed, st.elevGain, st.elevLoss);
|
||||
|
|
@ -5158,6 +5173,10 @@ async function showSessionDetail(sessionId) {
|
|||
let exp = document.querySelector("#sd-export");
|
||||
if (exp) exp.onclick = () => GPS.exportGPX(sessionId);
|
||||
|
||||
// Track ID — click to copy to clipboard
|
||||
let idEl = document.querySelector("#sd-track-id");
|
||||
if (idEl) idEl.onclick = () => copyToClipboard(String(sessionId), "ID скопирован: " + sessionId);
|
||||
|
||||
// Chart fullscreen in modal — push to winStack, back collapses
|
||||
document.querySelectorAll(".chart-wrap").forEach(wrap => {
|
||||
wrap.onclick = () => {
|
||||
|
|
@ -7561,6 +7580,23 @@ function showToast(msg, duration) {
|
|||
setTimeout(hide, duration || 6000);
|
||||
}
|
||||
|
||||
// Copy text to clipboard (navigator.clipboard with a hidden-textarea fallback for
|
||||
// WebViews where the async API is unavailable). Optional toast on success.
|
||||
function copyToClipboard(text, toastMsg) {
|
||||
let ok = () => { if (toastMsg) showToast(toastMsg); };
|
||||
let fallback = () => {
|
||||
try {
|
||||
let ta = document.createElement("textarea");
|
||||
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
|
||||
document.body.appendChild(ta); ta.focus(); ta.select();
|
||||
document.execCommand("copy"); ta.remove(); ok();
|
||||
} catch (e) { showToast("Не удалось скопировать"); }
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(ok).catch(fallback);
|
||||
} else fallback();
|
||||
}
|
||||
|
||||
function esc(str) {
|
||||
return String(str ?? "")
|
||||
.replace(/&/g, "&").replace(/</g, "<")
|
||||
|
|
|
|||
20
frontend/js/router.js
Normal file
20
frontend/js/router.js
Normal 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;
|
||||
},
|
||||
};
|
||||
77
src-tauri/gen/android/app/build.gradle.kts
Normal file
77
src-tauri/gen/android/app/build.gradle.kts
Normal 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")
|
||||
BIN
src-tauri/gen/android/app/libs/brouter-1.6.3-all.jar
Normal file
BIN
src-tauri/gen/android/app/libs/brouter-1.6.3-all.jar
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 )
|
||||
|
|
@ -460,7 +460,10 @@ class GpsRecorderService : Service(), LocationListener, SensorEventListener {
|
|||
|
||||
pointCount++
|
||||
val bearing = if (location.hasBearing()) location.bearing else -1f
|
||||
val line = """{"lat":${location.latitude},"lon":${location.longitude},"alt":${location.altitude},"speed":${location.speed},"acc":${location.accuracy},"bearing":${bearing},"ts":${now}${sensorData}}"""
|
||||
// No altitude fix -> write null, not 0.0: Android returns 0.0 when hasAltitude()
|
||||
// is false, and a fake 0 corrupts elevation stats. A real fixed 0 is kept.
|
||||
val altStr = if (location.hasAltitude()) location.altitude.toString() else "null"
|
||||
val line = """{"lat":${location.latitude},"lon":${location.longitude},"alt":${altStr},"speed":${location.speed},"acc":${location.accuracy},"bearing":${bearing},"ts":${now}${sensorData}}"""
|
||||
synchronized(bufferLock) {
|
||||
pointBuffer.add(line)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue