Nogo areas: routing avoids impassable spots (planner + engine + RouteServer)
Stage 1 of the nogo/reroute work. Mark impassable circles that BRouter routes around, end to end: - DB nogo_areas table + CRUD; drawNogoAreas() red circles (tap to remove). - BRouter.route(points, profile, nogos): &nogos=lon,lat,r on the RouteServer and nogos in the embedded-engine request; Rust brouter_route passes it through; Kotlin builds OsmNodeNamed(radius,isNogo,nogoWeight=NaN) -> rc.nogopoints + prepareNogoPoints (needs an APK rebuild for offline use). - Planner: ⛔ button toggles nogo-placement (tap map/line), each leg re-routes around nogos. Recording map: ⛔ marks the current spot impassable. Detail map shows the circles. Verified the RouteServer detours around a nogo (2873m -> 3329m).
This commit is contained in:
parent
5a80469bba
commit
1c326ab19c
9 changed files with 310 additions and 16 deletions
|
|
@ -22,12 +22,12 @@
|
|||
| Файл | Строк | Назначение |
|
||||
|------|-------|------------|
|
||||
| `js/main.js` | ~5900 | Экраны, навигация, модалки, графики, нутриенты, HR UI, компас (EMA + gimbal lock), ландшафт (3 режима), дороги. `appConfirm`/`appPrompt` (in-app диалоги вместо нативных, в стеке окон). Страница «Сохранённые маршруты», оверлей маршрута на записи и просмотре (`whenMapReady` — await готовности карты, не таймаут). Панель де-джиттера на детали трека (ползунки окно/итерации, живое превью, сохранить/откат, `reopenDetail` — переоткрытие детали без наложения оверлеев). `showRouteDetail` — предпросмотр маршрута (карта+профиль высоты+тап-высоты+фуллскрин), `showRouteEditor` — создание/правка маршрута (тап карты=точка в конец, тап линии=вставка в сегмент, тащить=двигать, тап точки=удалить; `makeMarker` возвращает маркер для push/splice). `appConfirm`/`appPrompt`/`appAlert` — in-app диалоги (нативных `alert/prompt/confirm` не осталось; `<select>` оставлены). `orient-dbg` (сенсор-оверлей на карте) — только при `window.__BALDAV_DEBUG`. `drawRoutePolyline`/`routeSlopeColor` (полосы уклона по 1%)/`routeElevColor` — топо/уклон-обводка в панели `routePane` (маршрут ПОД треком; на rotate-карте панель вешается внутрь `_rotatePane`, иначе маршрут не виден при записи). `fetchElevations` — цепочка GPXZ→open-elevation→open-meteo→OpenTopoData. Диагностические `console.log("[тег]...")` глушатся в release по `get_app_info().debug` (cfg!(debug_assertions)) |
|
||||
| `js/db.js` | ~1550 | SQLite через Tauri IPC. Миграции, CRUD, 41 поле добавок, water_pct, reserves, sync. Хелперы маршрутов (routes CRUD, `updateRoutePoints`/`addRoute(...,waypoints)` — плотная геометрия + `waypoints_json` {waypoints,legMode} для правки планировщиком, `getRoutes.hasEle/.wpts`). `saveSmoothedTrack` — чанковый `CASE`-апдейт lat/lon_smoothed (18 UPDATE на 4366 точек вместо 4366), `setSessionSmoothed` — откат де-джиттера. `saveGapFill`/`revertGapFill` — обратимый гэп-филл: вставка достроенных точек с `estimated=1` (seq = seq предыдущей реальной точки) / удаление их; колонки `gps_points.estimated`, `gps_sessions.gap_filled` |
|
||||
| `js/db.js` | ~1550 | SQLite через Tauri IPC. Миграции, CRUD, 41 поле добавок, water_pct, reserves, sync. Хелперы маршрутов (routes CRUD, `updateRoutePoints`/`addRoute(...,waypoints)` — плотная геометрия + `waypoints_json` {waypoints,legMode} для правки планировщиком, `getRoutes.hasEle/.wpts`). `saveSmoothedTrack` — чанковый `CASE`-апдейт lat/lon_smoothed (18 UPDATE на 4366 точек вместо 4366), `setSessionSmoothed` — откат де-джиттера. `saveGapFill`/`revertGapFill` — обратимый гэп-филл: вставка достроенных точек с `estimated=1` (seq = seq предыдущей реальной точки) / удаление их; колонки `gps_points.estimated`, `gps_sessions.gap_filled`. Nogo: таблица `nogo_areas` (lat/lon/radius_m) + `getNogoAreas`/`addNogoArea`/`deleteNogoArea` |
|
||||
| `js/gps.js` | ~1650 | GPS запись, ккал (нетто-модель + MET-нетто floor, накат=базовый), altitude filter + гистерезис высоты, BLE HR polling, dead reckoning. Источник истины БД при живой записи — нативный файл (`_processIncomingPoint(p, fromNative)`: watchPosition только рисует, поллинг пишет). `recoverFromFiles` — восстановление точек с устройства (матч по id/дате + `_s<id>` в имени, дедуп ±2.5с, `recalculateSession(noDelete)`, чистка импортированных файлов). `recomputeAllStats` — пересчёт статы всех треков без изменения точек. `filterByAccuracy` — отбор по точности (minAccuracyM) внутри `computeStats`+`filterNoise`: неточные фиксы исключаются из расчёта/линии (обратимо, non-destructive; null/≥999 keep), bearing_deg. `smoothTrackSG(pts,W,it)` — де-джиттер Савицкого-Голея (квадратичный МНК по окну, итеративно, концы закреплены; lat/lon→метры→обратно, все поля точки сохраняются). `parseRouteGPX(xml)` — парсер маршрутов (`rtept`/`trkpt`/`wpt`, читает `<ele>`, DOMParser). `routeElevStats(pts)` — профиль маршрута (накопл. дистанция, грейд к след. точке, набор/сброс с гистерезисом, макс-уклон; высоты сглаживаются). `fillGaps(points)` → `{points, gaps}` — дорожная достройка GPS-дропаутов (BRouter shortest) с интерполяцией ts и флагом `estimated`; `gaps[].status` = filled/skipped-long/skipped-noroute для UI-предпросмотра. `config.gpxzKey` — ключ GPXZ (localStorage) |
|
||||
| `js/sync.js` | ~300 | Синхронизация между устройствами (LAN, PHP сервер на роутере) |
|
||||
| `js/chzn.js` | ~330 | Честный знак (CRPT). Скан DataMatrix → карточка Нацкаталога → префилл КБЖУ. `ChZ`: сессия в localStorage (`chzn_session`), `importFromText`, `refresh`/`ensureSid` (sid 48ч, refreshToken ~год), `authedPost/Get` (userId+sid в query, авто-refresh на 401), `check`, `nutritionInfo` (витамины, часто 204), `normalizeCode` (реконструкция GS 0x1D перед AI 93), `parseCard` (группа «Энергетическая ценность…», имена атрибутов разнятся у производителей), `resolve`. `/authorize/anonymous` закрыт Play Integrity — свою сессию не завести, только импорт из приложения (`tools/chzn_export.sh`) |
|
||||
| `js/tilecache.js` | 178 | Оффлайн кэш тайлов карты (IndexedDB) |
|
||||
| `js/router.js` | ~80 | Оффлайн-роутинг BRouter. `BRouter.route(points,[profile])`: Android → встроенный движок (`invoke("brouter_route")`), иначе/при ошибке → PC-RouteServer по HTTP (`http_request`, мимо CSP; LAN 192.168.1.18:17777 / WAN mirror.invalid:17777). Профили `trekking` (планирование) / `shortest` (гэпы). Докачка сегментов `.rd5`: `downloadTile/downloadStatus/listTiles/downloadTileAndWait` |
|
||||
| `js/router.js` | ~80 | Оффлайн-роутинг BRouter. `BRouter.route(points,[profile])`: Android → встроенный движок (`invoke("brouter_route")`), иначе/при ошибке → PC-RouteServer по HTTP (`http_request`, мимо CSP; LAN 192.168.1.18:17777 / WAN mirror.invalid:17777). Профили `trekking` (планирование) / `shortest` (гэпы). `BRouter.route(points, profile, nogos)` — nogos `[{lat,lon,radius}]` → `&nogos=lon,lat,r` в RouteServer и в запрос встроенного движка (Kotlin `rc.nogopoints`). Докачка сегментов `.rd5`: `downloadTile/downloadStatus/listTiles/downloadTileAndWait` |
|
||||
| `js/poi.js` | ~200 | POI-оверлей на картах: круглые контрастные бейджи (эмодзи в круге, не пины), фильтр категорий (`POI.attach(map)`, данные `data/pois.json`, localStorage `baldav_poi_cats/shown`) |
|
||||
| `js/bmr.js` | ~70 | BMR (Mifflin-St Jeor), РСП из БД (33 нутриента), полураспад |
|
||||
| `css/styles.css` | ~2100 | Темная тема, мобильный дизайн, HR зоны, ландшафтный режим |
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
- **Редактор маршрутов** — создание («Новый маршрут») и правка сохранённого маршрута прямо в приложении: тап по карте — добавить точку в конец, **тап по линии — вставить точку** (широкая зона попадания), долгий тап по линии — переключить отрезок дорога/прямая, тащить точку — двигать, тап по точке — удалить. Роутинг **по-сегментный**: каждый отрезок между опорными точками отдельно либо по дорогам (BRouter, профиль trekking, высоты из BRouter), либо прямой; по дорогам по умолчанию, где дороги/сегментов нет — молча прямая; кнопка **«🚲 По дорогам»** задаёт режим сразу всем. Маршрут в редакторе рисуется **оковкой с раскраской уклон/высота** как в детали; тумблеры день/ночь, уклон/высота и кнопка «моё место» на карте. Сохраняются и **опорные точки** (5-6, для правки — при повторном открытии столько же), и плотная дорожная геометрия с высотами (для показа и профиля уклона).
|
||||
- **Оффлайн-роутинг (BRouter)** — движок BRouter встроен в приложение (Android): маршруты строятся по дорогам без сети, сегменты `.rd5` региона качаются с домашнего зеркала прямо в приложении (экран дорог → «Сегменты»). Используется в редакторе маршрутов («По дорогам») и для заполнения GPS-пропусков. На десктопе/дома движка нет — те же вызовы уходят на домашний BRouter-RouteServer по HTTP.
|
||||
- **Заполнение GPS-пропусков по дороге** — на детали трека кнопка **«🧩»** прокладывает дорожный путь (BRouter, профиль shortest) через каждый GPS-дропаут (разрыв по времени + реальная хорда) и показывает **предпросмотр только достроенных пропусков** (голубым, с бело-чёрной оковкой; недостроенные — красным пунктиром) плюс панель «Достроено N (+X км), M не удалось» с кнопками **Сохранить/Отмена**. Сохранение — **обратимый патч на месте**: достроенные точки помечаются `estimated` и добавляются в трек (оригинал цел), статистика пересчитывается; повторный тап предлагает **«Убрать достройку»** (удаляет только достроенное). Достраиваются только реальные длинные потери сигнала (по умолчанию разрыв > 60с и хорда > 150м); пропуск не достраивается, если дорога заметно длиннее хорды (крюк, > 2×) или почти совпадает с прямой (мостить нечего) — чтобы не перерисовывать нормальные участки объездами.
|
||||
- **Непроезжие места (nogo)** — красные круги, которые роутинг ОБЪЕЗЖАЕТ. Ставятся кнопкой **⛔**: в планировщике — тапом по карте/линии, на карте записи — «здесь нет проезда» (текущее место). Показываются на картах планировщика, записи и детали трека; тап по кругу — убрать. Избегание работает и через домашний RouteServer, и встроенным движком офлайн (BRouter nogopoints). Web-BRouter умеет nogo сам.
|
||||
- **Де-джиттер трека** — сглаживание дрожащего GPS-трека методом Савицкого-Голея (настраиваемое окно/число итераций, живое превью на карте детали трека). Хранится обратимо: оригинал и сглаженный вариант лежат раздельно, калории пересчитываются по сглаженным точкам, откат к оригиналу в один тап. Точность каждого фикса (`accuracy_m`) пишется в БД: при пересчёте трека точки хуже порога «Макс. погрешность GPS» исключаются из расчёта и линии (обратимо — точки в БД остаются; поднять порог = вернуть). Работает во всех пересчётах (кнопка «Пересчитать», «Пересчитать всё», после де-джиттера); импорт/старьё без точности и dead-reckoning заполнения не трогает.
|
||||
- **POI на карте** — точки интереса (кафе, магазины, вода, туалеты, банки/ATM, остановки, укрытия, смотровые и т.д.) круглыми контрастными значками (эмодзи в кружке, читаемо на светлой и тёмной карте). Кнопка `📍` с панелью-фильтром категорий; рендер по видимой области от зума 13. Данные оффлайн (`data/pois.json`, ~100 км вокруг опорной точки, 5662 объекта из OSM/Overpass). На картах записи, деталей трека и дорог.
|
||||
- **Тёмная карта** — тумблер `☾` на экранах записи, деталей трека и дорог: инвертирует ТОЛЬКО слой тайлов (`invert + hue-rotate 180° + brightness`, трек/маршрут/дороги остаются цветными), выбор запоминается. Работает одинаково на офлайн-тайлах и онлайне, без ключей/квот/перепаковки.
|
||||
|
|
|
|||
109
docs/plans/2026-09-12-nogo-reroute.md
Normal file
109
docs/plans/2026-09-12-nogo-reroute.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# План: непроезжие места (nogo) + объезд с возвратом на маршрут на ходу
|
||||
|
||||
## Context (зачем)
|
||||
Пользователь поехал по маршруту, импортированному из web-BRouter, и в одном месте дороги по
|
||||
факту не оказалось. Отсюда две связанные потребности:
|
||||
1. **Помечать непроезжие места** так, чтобы роутинг их ОБЪЕЗЖАЛ. В web-BRouter это уже есть
|
||||
(BR.Nogo, `nogos=`) — там ничего не строим. В BalDav nogo-роутинга НЕТ: `BRouter.route`
|
||||
не принимает nogo ни для RouteServer, ни для встроенного движка. Решение — полное избегание
|
||||
(согласовано).
|
||||
2. **Объезд на ходу с возвратом на маршрут.** Активный маршрут ведёт через непроезжее место.
|
||||
Нужно от точки затыка отступить ~1-2 км ВПЕРЁД по сохранённому маршруту и построить объезд
|
||||
туда от текущей позиции (в обход непроезжего места); не вышло — отступать дальше. Сохранённый
|
||||
маршрут НЕ менять — только показать навигацию, как вернуться на траекторию без больших потерь
|
||||
и разворотов на 180° (если возможно). Результат — навигационный оверлей поверх карты записи.
|
||||
|
||||
Итог решений: nogo — полное избегание (RouteServer + встроенный движок, Kotlin + пересборка);
|
||||
цель объезда — точка НА сохранённом маршруте за местом затыка; результат — навигация, без
|
||||
сохранения и без правки маршрута.
|
||||
|
||||
## Часть A — Nogo (непроезжие места) с полным избеганием
|
||||
|
||||
### A1. БД (`frontend/js/db.js`)
|
||||
- Таблица `CREATE TABLE IF NOT EXISTS nogo_areas (id, lat, lon, radius_m, note, created_at)`
|
||||
(по образцу `goal_markers`, db.js:1628). CRUD: `getNogoAreas`/`addNogoArea(lat,lon,radius,note)`/
|
||||
`deleteNogoArea(id)`; экспорт в `window.DB`.
|
||||
|
||||
### A2. Роутинг с nogo (`frontend/js/router.js`)
|
||||
- `BRouter.route(points, profile, nogos)` — `nogos` = `[{lat,lon,radius}]`.
|
||||
- RouteServer-ветка: добавить `&nogos=lon,lat,radius|...` в URL (`_routeServer(pts, profile, nogos)`).
|
||||
- Встроенная: класть `nogos` в `brouter_request.json` (invoke `brouter_route`).
|
||||
- Все текущие вызовы `route(pts, profile)` продолжают работать (nogos по умолчанию — из
|
||||
`DB.getNogoAreas()`, подтягивать в вызывающих местах: планировщик, достройка, объезд).
|
||||
|
||||
### A3. Встроенный движок (`.../MainActivity.kt`) + пересборка APK
|
||||
- В `routeCommand`/`routeBrouter` прочитать `nogos` из запроса; на каждый — `OsmNodeNamed`
|
||||
(ilon/ilat в микроградусах, `radius` в метрах, `isNogo=true`), собрать в список и передать
|
||||
в `rc.nogopoints` + `rc.prepareNogoPoints(...)` ДО `RoutingEngine`. Зеркалить `btools.server.
|
||||
request.ServerHandler` (парсинг nogos там же в jar). Пересобрать: `./build.sh android -d -i`.
|
||||
|
||||
### A4. UI nogo (`frontend/js/main.js`, `styles.css`)
|
||||
- Показ: красные полупрозрачные круги (`L.circle`) на картах планировщика, записи и детали
|
||||
трека — новый хелпер `drawNogoAreas(map)` (по аналогии с `POI.attach`/`drawActiveRouteOverlay`).
|
||||
- Добавление:
|
||||
- Карта записи: кнопка в стопке (`rec-map-btn`) «нет проезда» — ставит nogo в `lastMovingPoint`
|
||||
(радиус по умолчанию, напр. 25 м; настраиваемый позже).
|
||||
- Планировщик: режим «тап — поставить nogo» (переключатель), тап ставит круг.
|
||||
- Удаление: тап по кругу → подтверждение → `deleteNogoArea`.
|
||||
- Планировщик (`showRouteEditor`, `routeDirty`): передавать nogos в каждый вызов `BRouter.route`,
|
||||
чтобы построенные ноги обходили непроезжее. Показывать круги на карте редактора.
|
||||
|
||||
## Часть B — Объезд с возвратом на маршрут на ходу (карта записи)
|
||||
|
||||
### B1. Алгоритм `rejoinReroute(cur, bearing, routePts, nogos)` (main.js)
|
||||
Возвращает связующий путь от текущей позиции до точки на маршруте за затыком.
|
||||
- Опорная точка затыка `ref`: ближайший к текущей позиции nogo, попавший на маршрут (в пределах
|
||||
~150 м от линии); если nogo нет — проекция текущей позиции на маршрут.
|
||||
- `refIdx` — индекс на `routePts`, ближайший к `ref`; накопленная дистанция `cum[]` вдоль маршрута.
|
||||
- Скип `skip` от 1500 м, шаг +1500 м до конца маршрута (несколько попыток):
|
||||
- `target` = точка маршрута на дистанции `cum[refIdx] + skip` (ВПЕРЁД, к концу маршрута).
|
||||
- вейпоинты: `[cur, target]`, а «без разворота» — при наличии курса добавлять помощник в
|
||||
~30-50 м по `bearing` первым (`[cur, helper, target]`), чтобы старт шёл вперёд.
|
||||
- `gpx = BRouter.route(wps, "trekking", nogos)`; принять, если проложился и длина разумна
|
||||
(< ~3× прямой до target). Иначе увеличить `skip`.
|
||||
- Курс `bearing`: из `GPS.recording` (последние точки / `smoothedHeading` компаса).
|
||||
|
||||
### B2. UI объезда (карта записи, `showRecordScreen` ~3956+)
|
||||
- Кнопка в стопке `rec-map-btn` «объехать» (доступна при активном маршруте). По тапу:
|
||||
запускает `rejoinReroute`, рисует связующий путь навигационным оверлеем (отдельный слой,
|
||||
ярко, поверх; НЕ трогает `baldav_active_route` и запись). Точку возврата пометить.
|
||||
- Если объезд не найден даже у конца маршрута — тост «объезд не найден, отступаю дальше не вышло».
|
||||
- Оверлей держать до ручного сброса/новой перестройки; запись трека не прерывается.
|
||||
|
||||
## Файлы
|
||||
- `frontend/js/db.js` — таблица `nogo_areas` + CRUD.
|
||||
- `frontend/js/router.js` — `route(...,nogos)`, `_routeServer(...,nogos)`.
|
||||
- `src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt` — nogo в движке.
|
||||
- `frontend/js/main.js` — `drawNogoAreas`, кнопки nogo (запись/планировщик), передача nogos в
|
||||
роутинг, `rejoinReroute` + кнопка «объехать» на карте записи.
|
||||
- `frontend/css/styles.css` — стиль круга nogo / панели.
|
||||
|
||||
## Переиспользование (не изобретать)
|
||||
- `goal_markers` CRUD (db.js:1059) — образец для `nogo_areas`.
|
||||
- `drawActiveRouteOverlay`/`drawRoutePolyline` (main.js:3163/3196) — образец слоя-оверлея.
|
||||
- `BRouter.route` (router.js), `GPS.distance`, `GPS.recording.lastMovingPoint`, компас
|
||||
(`smoothedHeading` в `showRecordScreen`).
|
||||
- Kotlin `routeBrouter` (MainActivity.kt:323) — точка вставки nogopoints; зеркалить
|
||||
`btools.server.request.ServerHandler` из jar.
|
||||
- `POI.attach` (poi.js) — образец «слой + добавление/удаление на карте».
|
||||
|
||||
## Верификация
|
||||
Десктоп (Xvfb :99 + домашний RouteServer):
|
||||
1. Nogo: поставить круг на дороге в планировщике → нога маршрута обходит его (сравнить с/без).
|
||||
2. `rejoinReroute` в изоляции: задать активный маршрут, «текущую позицию» рядом с затыком и
|
||||
nogo на маршруте → построенный объезд ведёт к точке на маршруте за nogo, не через nogo;
|
||||
при неудаче отступает дальше. Скрин.
|
||||
Устройство (реальная поездка / симуляция):
|
||||
3. `./build.sh android -d -i` (нужно для nogo во встроенном движке офлайн). На теле: пометить
|
||||
«нет проезда», «объехать» → навигационный оверлей к точке возврата; сохранённый маршрут и
|
||||
запись не меняются.
|
||||
Регрессии: планировщик и достройка (используют `BRouter.route`) работают и без nogo; экран
|
||||
записи и его кнопки целы.
|
||||
|
||||
## Стадии (инкрементально, коммит на стадию)
|
||||
1. A1+A2+A3 (nogo сквозь весь роутинг) + A4-показ/добавление → планировщик обходит nogo.
|
||||
2. B (объезд с возвратом) поверх готового nogo-роутинга.
|
||||
|
||||
## Не трогаем
|
||||
- Web-BRouter (nogo там уже есть).
|
||||
- Сохранённые маршруты не меняем при объезде; запись трека не прерываем.
|
||||
33
docs/session-logs/2026-09-12_nogo-reroute.md
Normal file
33
docs/session-logs/2026-09-12_nogo-reroute.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Сессия 2026-09-12 — непроезжие места (nogo) + объезд на ходу
|
||||
|
||||
## Контекст
|
||||
После реальной поездки по маршруту из web-BRouter в одном месте дороги не оказалось. Две идеи
|
||||
(план `docs/plans/2026-09-12-nogo-reroute.md`, согласовано):
|
||||
1. Помечать непроезжие места так, чтобы роутинг их ОБЪЕЗЖАЛ (полное избегание). В web-BRouter
|
||||
nogo уже есть — там ничего не строим.
|
||||
2. На ходу строить объезд с ВОЗВРАТОМ на сохранённый маршрут за местом затыка (отступ ~1-2 км
|
||||
вперёд, растёт при неудаче), не меняя сохранённый маршрут; показывать навигацией, без
|
||||
разворотов на 180 если можно.
|
||||
|
||||
Разведка: web-бандл уже с nogo (BR.Nogo, `nogos=`). В BalDav nogo-роутинга не было. API
|
||||
встроенного движка (javap): `RoutingContext.nogopoints` + static `prepareNogoPoints(list)`,
|
||||
`OsmNodeNamed.radius/isNogo/nogoWeight`.
|
||||
|
||||
## Сделано — Стадия 1 (nogo сквозь роутинг + показ/добавление)
|
||||
- БД: таблица `nogo_areas` (lat/lon/radius_m/note) + `getNogoAreas`/`addNogoArea`/`deleteNogoArea`.
|
||||
- `router.js`: `BRouter.route(points, profile, nogos)` → `&nogos=lon,lat,r` в RouteServer и
|
||||
`nogos` в запрос встроенного движка.
|
||||
- Rust `brouter_route`: параметр `nogos` (Option<String>) → в `brouter_request.json`.
|
||||
- Kotlin `routeBrouter`: парсит nogos → `OsmNodeNamed`(radius, isNogo, nogoWeight=NaN) →
|
||||
`prepareNogoPoints` + `rc.nogopoints`. Нужна пересборка APK для офлайн-движка.
|
||||
- UI: хелпер `drawNogoAreas` (красные круги, тап — убрать). Планировщик: кнопка ⛔ (режим),
|
||||
тап по карте/линии ставит nogo, каждая нога роутится с nogos; круги показываются. Карта
|
||||
записи: кнопка ⛔ «здесь нет проезда» (ставит в текущей позиции) + показ. Деталь трека: показ.
|
||||
|
||||
## Проверка
|
||||
- Синтаксис db/router/main OK. RouteServer с `&nogos`: маршрут 2873м → 3329м (объезжает круг 300м).
|
||||
- ДЕСКТОП-ТЕСТ планировщика (nogo обходится визуально) — следующий шаг.
|
||||
- Встроенный движок (офлайн) — после `./build.sh android -d -i`, на теле.
|
||||
|
||||
## Дальше
|
||||
- Стадия 2: объезд с возвратом на маршрут на карте записи (`rejoinReroute` + кнопка «объехать»).
|
||||
|
|
@ -1072,6 +1072,27 @@ async function deleteGoalMarker(id) {
|
|||
await execute("DELETE FROM goal_markers WHERE id = ?1", [id]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nogo areas — impassable spots the router must avoid (circle: lat/lon + radius)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getNogoAreas() {
|
||||
return select("SELECT * FROM nogo_areas ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
async function addNogoArea(lat, lon, radiusM, note) {
|
||||
await execute(
|
||||
"INSERT INTO nogo_areas (lat, lon, radius_m, note) VALUES (?1, ?2, ?3, ?4)",
|
||||
[lat, lon, radiusM ?? 25, note ?? null]
|
||||
);
|
||||
let rows = await select("SELECT CAST(id AS TEXT) AS id FROM nogo_areas ORDER BY id DESC LIMIT 1");
|
||||
return rows.length ? rows[0].id : null;
|
||||
}
|
||||
|
||||
async function deleteNogoArea(id) {
|
||||
await execute("DELETE FROM nogo_areas WHERE id = ?1", [id]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Saved routes (hand-drawn reference paths overlaid on the map)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1288,6 +1309,9 @@ window.DB.getBikeMaintenanceDue = getBikeMaintenanceDue;
|
|||
window.DB.getGoalMarkers = getGoalMarkers;
|
||||
window.DB.addGoalMarker = addGoalMarker;
|
||||
window.DB.deleteGoalMarker = deleteGoalMarker;
|
||||
window.DB.getNogoAreas = getNogoAreas;
|
||||
window.DB.addNogoArea = addNogoArea;
|
||||
window.DB.deleteNogoArea = deleteNogoArea;
|
||||
window.DB.getRoutes = getRoutes;
|
||||
window.DB.getRoute = getRoute;
|
||||
window.DB.addRoute = addRoute;
|
||||
|
|
@ -1634,6 +1658,16 @@ async function runMigrations() {
|
|||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)`);
|
||||
|
||||
// --- Nogo areas (impassable spots the router avoids) ---
|
||||
await execute(`CREATE TABLE IF NOT EXISTS nogo_areas (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
lat REAL NOT NULL,
|
||||
lon REAL NOT NULL,
|
||||
radius_m REAL DEFAULT 25,
|
||||
note TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)`);
|
||||
|
||||
// --- Saved routes (hand-drawn reference paths overlaid on the map) ---
|
||||
await execute(`CREATE TABLE IF NOT EXISTS routes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
|
|
|||
|
|
@ -3169,6 +3169,33 @@ async function drawActiveRouteOverlay(map, prev) {
|
|||
return drawRoutePolyline(map, route.points, route.color);
|
||||
}
|
||||
|
||||
// Draw the impassable "nogo" circles (DB.nogo_areas) as red translucent circles. Tapping a
|
||||
// circle offers to remove it; onChange() (if given) fires after a removal so the caller can
|
||||
// re-fetch nogos and re-route. Returns the layer group (removed via the `prev` arg on redraw).
|
||||
async function drawNogoAreas(map, prev, onChange) {
|
||||
if (prev) { try { map.removeLayer(prev); } catch {} }
|
||||
let areas = await DB.getNogoAreas();
|
||||
let group = L.layerGroup();
|
||||
for (let a of areas) {
|
||||
let c = L.circle([a.lat, a.lon], { radius: a.radius_m || 25, color: "#e53935", weight: 2, fillColor: "#e53935", fillOpacity: 0.22 });
|
||||
((area) => c.on("click", async (e) => {
|
||||
L.DomEvent.stop(e);
|
||||
if (await appConfirm("Убрать пометку «нет проезда»?", "Убрать")) {
|
||||
await DB.deleteNogoArea(area.id);
|
||||
if (onChange) onChange();
|
||||
}
|
||||
}))(a);
|
||||
c.addTo(group);
|
||||
}
|
||||
group.addTo(map);
|
||||
return group;
|
||||
}
|
||||
|
||||
// nogos for the router: [{lat,lon,radius}] from the nogo_areas table.
|
||||
async function getNogosForRouter() {
|
||||
return (await DB.getNogoAreas()).map(a => ({ lat: a.lat, lon: a.lon, radius: a.radius_m || 25 }));
|
||||
}
|
||||
|
||||
// Slope tint by absolute grade in %, in 1%-wide discrete bands (hard boundaries
|
||||
// kept — one colour per segment): green -> yellow -> red -> brown -> black.
|
||||
// <1%=0 .. 9–10%=9, >=10% = black. Both up- and downhill coloured by magnitude.
|
||||
|
|
@ -3505,6 +3532,9 @@ async function showRouteEditor(container, routeId) {
|
|||
let defaultMode = "road";
|
||||
let routeTimer = null;
|
||||
let dirty = new Set();
|
||||
let nogos = []; // [{lat,lon,radius}] impassable circles the route must avoid
|
||||
let nogoLayer = null; // red circles overlay
|
||||
let nogoMode = false; // when on, a map tap drops a nogo instead of a waypoint
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="draw-screen">
|
||||
|
|
@ -3582,7 +3612,7 @@ async function showRouteEditor(container, routeId) {
|
|||
let line = L.polyline(legPts(i).map(p => [p.lat, p.lon]),
|
||||
{ color: "#000", weight: 26, opacity: 0, interactive: true, className: "re-hit" }).addTo(map);
|
||||
(function (idx) {
|
||||
line.on("click", (e) => { L.DomEvent.stop(e); insertInLeg(idx, e.latlng); }); // tap → insert
|
||||
line.on("click", (e) => { L.DomEvent.stop(e); if (nogoMode) placeNogo(e.latlng); else insertInLeg(idx, e.latlng); }); // tap → insert (or nogo)
|
||||
line.on("contextmenu", (e) => { L.DomEvent.stop(e); toggleLeg(idx); }); // long-press → road/straight
|
||||
})(i);
|
||||
hitLines.push(line);
|
||||
|
|
@ -3596,7 +3626,7 @@ async function showRouteEditor(container, routeId) {
|
|||
for (let i of idxs) {
|
||||
if (i >= points.length - 1 || legMode[i] !== "road") continue;
|
||||
try {
|
||||
let gpx = await BRouter.route([[points[i].lon, points[i].lat], [points[i+1].lon, points[i+1].lat]], "trekking");
|
||||
let gpx = await BRouter.route([[points[i].lon, points[i].lat], [points[i+1].lon, points[i+1].lat]], "trekking", nogos);
|
||||
let rp = (GPS.parseRouteGPX(gpx) || {}).points || [];
|
||||
legGeom[i] = (rp.length >= 2) ? rp.map(p => ({ lat: p.lat, lon: p.lon, ele: p.ele })) : null; // keep ele for slope
|
||||
} catch (e) { legGeom[i] = null; } // silent fallback to straight (no road / no segments)
|
||||
|
|
@ -3691,6 +3721,37 @@ async function showRouteEditor(container, routeId) {
|
|||
};
|
||||
locCtl.addTo(map);
|
||||
|
||||
// Nogo (impassable) circles: toggle the ⛔ button, then a map/line tap drops a red circle
|
||||
// the routing avoids. Persist in DB; every road leg re-routes to go around them.
|
||||
async function refreshNogos(reroute) {
|
||||
nogos = await getNogosForRouter();
|
||||
nogoLayer = await drawNogoAreas(map, nogoLayer, () => refreshNogos(true));
|
||||
if (reroute) {
|
||||
for (let i = 0; i < legMode.length; i++) if (legMode[i] === "road") { legGeom[i] = null; markDirty(i); }
|
||||
redraw(); updateStats(); scheduleRoute();
|
||||
}
|
||||
}
|
||||
async function placeNogo(latlng) {
|
||||
await DB.addNogoArea(latlng.lat, latlng.lng, 25, null);
|
||||
await refreshNogos(true);
|
||||
}
|
||||
let nogoCtl = L.control({ position: "topleft" });
|
||||
nogoCtl.onAdd = () => {
|
||||
let d = L.DomUtil.create("div", "sd-map-fs-btn");
|
||||
d.innerHTML = "⛔";
|
||||
d.title = "Пометить непроезжее: тап по карте";
|
||||
L.DomEvent.disableClickPropagation(d);
|
||||
d.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
nogoMode = !nogoMode;
|
||||
d.classList.toggle("active", nogoMode);
|
||||
showToast(nogoMode ? "Тап по карте — пометить непроезжее" : "Режим отметки выключен");
|
||||
};
|
||||
return d;
|
||||
};
|
||||
nogoCtl.addTo(map);
|
||||
refreshNogos(false);
|
||||
|
||||
// Existing route: load waypoints. Planner-built → its saved leg modes (re-route road legs
|
||||
// to get fresh geometry + elevations). Imported/legacy dense → all legs straight.
|
||||
if (points.length) {
|
||||
|
|
@ -3708,7 +3769,7 @@ async function showRouteEditor(container, routeId) {
|
|||
() => {}, { enableHighAccuracy: false, timeout: 1000, maximumAge: 5000 });
|
||||
}
|
||||
|
||||
map.on("click", e => addPoint(e.latlng));
|
||||
map.on("click", e => { if (nogoMode) placeNogo(e.latlng); else addPoint(e.latlng); });
|
||||
|
||||
document.querySelector("#re-undo").onclick = () => {
|
||||
if (!points.length) return;
|
||||
|
|
@ -4244,6 +4305,24 @@ async function renderRecordingInline(container) {
|
|||
darkBtn.onclick = () => toggleDarkMap();
|
||||
btnWrap.appendChild(darkBtn);
|
||||
|
||||
// Nogo: mark the current spot impassable while riding (the planner + on-the-fly reroute
|
||||
// will avoid it). Shows existing nogo circles too.
|
||||
let recNogoLayer = null;
|
||||
async function refreshRecNogos() { recNogoLayer = await drawNogoAreas(map, recNogoLayer, refreshRecNogos); }
|
||||
refreshRecNogos();
|
||||
let recNogoBtn = document.createElement("button");
|
||||
recNogoBtn.className = "rec-map-btn";
|
||||
recNogoBtn.textContent = "⛔";
|
||||
recNogoBtn.title = "Здесь нет проезда (отметить текущее место)";
|
||||
recNogoBtn.onclick = async () => {
|
||||
let p = GPS.recording.lastMovingPoint;
|
||||
if (!p) { showToast("Нет позиции"); return; }
|
||||
await DB.addNogoArea(p.lat, p.lon, 25, null);
|
||||
await refreshRecNogos();
|
||||
showToast("Отмечено: нет проезда");
|
||||
};
|
||||
btnWrap.appendChild(recNogoBtn);
|
||||
|
||||
POI.attach(map); // POI overlay + category filter on the recording map
|
||||
// The default-ON draw is deferred to the END of this function (after all the
|
||||
// critical wiring), where it awaits whenMapReady — so it can't delay setup.
|
||||
|
|
@ -4868,6 +4947,7 @@ async function showSessionDetail(sessionId) {
|
|||
new CachedLayer({ maxZoom: 19 }).addTo(map);
|
||||
addDarkMapControl(map); // dark-tiles toggle on the track view
|
||||
POI.attach(map); // POI overlay + category filter
|
||||
{ let nl = null; let rn = async () => { nl = await drawNogoAreas(map, nl, rn); }; rn(); } // impassable-spot circles
|
||||
|
||||
// Gap-fill: route GPS dropouts along roads (BRouter) and PREVIEW only the filled gaps
|
||||
// (not the whole track); then save (reversible, in-place) or discard. If the track is
|
||||
|
|
|
|||
|
|
@ -11,11 +11,20 @@
|
|||
window.BRouter = {
|
||||
_servers: ["http://192.168.1.18:17777", "http://mirror.invalid:17777"],
|
||||
|
||||
async _routeServer(pts, profile) {
|
||||
// nogos: [{lat,lon,radius}] — impassable circles the router must avoid.
|
||||
_nogoParam(nogos) {
|
||||
if (!nogos || !nogos.length) return "";
|
||||
// RouteServer expects nogos=lon,lat,radiusMeters|... (radius in metres).
|
||||
let s = nogos.filter(n => n && n.lat != null && n.lon != null)
|
||||
.map(n => n.lon + "," + n.lat + "," + Math.round(n.radius || 25)).join("|");
|
||||
return s ? "&nogos=" + encodeURIComponent(s) : "";
|
||||
},
|
||||
|
||||
async _routeServer(pts, profile, nogos) {
|
||||
let lonlats = pts.map(p => p[0] + "," + p[1]).join("|");
|
||||
for (let base of this._servers) {
|
||||
try {
|
||||
let url = base + "/brouter?lonlats=" + encodeURIComponent(lonlats) + "&profile=" + profile + "&alternativeidx=0&format=gpx";
|
||||
let url = base + "/brouter?lonlats=" + encodeURIComponent(lonlats) + "&profile=" + profile + "&alternativeidx=0&format=gpx" + this._nogoParam(nogos);
|
||||
let gpx = await window.__TAURI__.core.invoke("http_request", { url, method: "GET" });
|
||||
if (gpx && gpx.indexOf("<gpx") >= 0) return gpx;
|
||||
} catch (e) { /* try next server */ }
|
||||
|
|
@ -23,25 +32,26 @@ window.BRouter = {
|
|||
return null;
|
||||
},
|
||||
|
||||
async route(points, profile) {
|
||||
async route(points, profile, nogos) {
|
||||
let pts = (points || []).map(p => [p[0], p[1]]);
|
||||
profile = profile || "trekking";
|
||||
nogos = (nogos || []).filter(n => n && n.lat != null && n.lon != null);
|
||||
let isAndroid = /Android/i.test(navigator.userAgent || "");
|
||||
// Desktop has no embedded engine (that's Kotlin/Android) — the native command would
|
||||
// just time out; go straight to the PC RouteServer. Android uses the engine, and
|
||||
// falls back to the RouteServer (reachable at home) only if the engine errors.
|
||||
if (!isAndroid) {
|
||||
let gpx = await this._routeServer(pts, profile);
|
||||
let gpx = await this._routeServer(pts, profile, nogos);
|
||||
if (gpx) return gpx;
|
||||
throw new Error("brouter: RouteServer unreachable");
|
||||
}
|
||||
try {
|
||||
let raw = await window.__TAURI__.core.invoke("brouter_route", { lonlats: JSON.stringify(pts), profile });
|
||||
let raw = await window.__TAURI__.core.invoke("brouter_route", { lonlats: JSON.stringify(pts), profile, nogos: JSON.stringify(nogos) });
|
||||
let res = JSON.parse(raw);
|
||||
if (res.ok) return res.gpx;
|
||||
throw new Error(res.error || "engine error");
|
||||
} catch (e) {
|
||||
let gpx = await this._routeServer(pts, profile);
|
||||
let gpx = await this._routeServer(pts, profile, nogos);
|
||||
if (gpx) return gpx;
|
||||
throw new Error("brouter: " + (e.message || e));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,9 +302,16 @@ class MainActivity : TauriActivity() {
|
|||
val pair = arr.getJSONArray(i)
|
||||
pts.add(doubleArrayOf(pair.getDouble(0), pair.getDouble(1)))
|
||||
}
|
||||
// Optional nogos: [{lat,lon,radius}] impassable circles to avoid → [lon,lat,radius].
|
||||
val nogos = ArrayList<DoubleArray>()
|
||||
val na = req.optJSONArray("nogos")
|
||||
if (na != null) for (i in 0 until na.length()) {
|
||||
val o = na.getJSONObject(i)
|
||||
nogos.add(doubleArrayOf(o.getDouble("lon"), o.getDouble("lat"), o.optDouble("radius", 25.0)))
|
||||
}
|
||||
val profilePath = File(filesDir, "brouter/profiles2/$profile.brf").absolutePath
|
||||
val segmentsDir = File(filesDir, "brouter/segments4")
|
||||
val gpx = routeBrouter(segmentsDir, profilePath, pts)
|
||||
val gpx = routeBrouter(segmentsDir, profilePath, pts, nogos)
|
||||
out.put("ok", true)
|
||||
out.put("gpx", gpx)
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -320,11 +327,28 @@ class MainActivity : TauriActivity() {
|
|||
// 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 {
|
||||
private fun routeBrouter(segmentsDir: File, profilePath: String, pts: List<DoubleArray>, nogos: List<DoubleArray> = emptyList()): String {
|
||||
val rc = btools.router.RoutingContext()
|
||||
rc.memoryclass = 128
|
||||
rc.localFunction = profilePath
|
||||
rc.setAlternativeIdx(0)
|
||||
// Nogo circles (impassable): each a hard block (nogoWeight = NaN). prepareNogoPoints
|
||||
// converts the metre radius to the engine's internal units before routing.
|
||||
if (nogos.isNotEmpty()) {
|
||||
val nogoList = ArrayList<btools.router.OsmNodeNamed>()
|
||||
for (n in nogos) {
|
||||
val nn = btools.router.OsmNodeNamed()
|
||||
nn.ilon = ((n[0] + 180.0) * 1000000.0 + 0.5).toInt()
|
||||
nn.ilat = ((n[1] + 90.0) * 1000000.0 + 0.5).toInt()
|
||||
nn.radius = n[2]
|
||||
nn.isNogo = true
|
||||
nn.nogoWeight = Double.NaN
|
||||
nn.name = "nogo"
|
||||
nogoList.add(nn)
|
||||
}
|
||||
btools.router.RoutingContext.prepareNogoPoints(nogoList)
|
||||
rc.nogopoints = nogoList
|
||||
}
|
||||
val wps = ArrayList<btools.router.OsmNodeNamed>()
|
||||
for (p in pts) {
|
||||
val n = btools.router.OsmNodeNamed()
|
||||
|
|
|
|||
|
|
@ -398,16 +398,19 @@ fn gps_service_command(app: tauri::AppHandle, cmd: String) -> Result<String, Str
|
|||
// 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> {
|
||||
fn brouter_route(app: tauri::AppHandle, lonlats: String, profile: String, nogos: Option<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();
|
||||
// nogos is a JSON array string [{lat,lon,radius}] from JS; default to empty array.
|
||||
let nogos_json = nogos.filter(|s| !s.trim().is_empty()).unwrap_or_else(|| "[]".to_string());
|
||||
let req = format!(
|
||||
"{{\"profile\":{},\"lonlats\":{}}}",
|
||||
"{{\"profile\":{},\"lonlats\":{},\"nogos\":{}}}",
|
||||
serde_json::to_string(&profile).map_err(|e| e.to_string())?,
|
||||
lonlats
|
||||
lonlats,
|
||||
nogos_json
|
||||
);
|
||||
let result_name = format!("brouter_result_{}.json", id);
|
||||
// Write request + command to app_data_dir AND app root (where Kotlin watches).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue