# Enduro Trails 🏍️ > OSM-карта с фокусом на грунтовые дороги для построения красивых эндуро-маршрутов **Статус:** active **Старт:** 2026-05-02 **Автор:** Слава --- ## Концепция Обычные карты оптимизированы под автомобили — асфальт яркий, грунтовки не видны. Enduro Trails переворачивает эту логику: **грунтовки/тропы — главный слой**, асфальт — тусклый фон. Плюс фичи для поиска и построения красивых маршрутов (минимум асфальта, максимум красоты). --- ## Регион 1. **ЦФО + Чувашия** (первый регион, прототип) 2. Расширение на новые ФО по запросу --- ## Архитектура ### Стек - Pyrosm/Osmium → парсинг PBF - Spatialite → хранение (прототип), PostGIS → продакшен - OSRM (кастомный профиль `enduro.lua`) → роутинг - FastAPI + uvicorn (4 workers) → бэкенд - MapLibre GL JS → фронт (веб + PWA) ### Инфраструктура - **Сервер:** `slin@82.22.50.71`, sudo `motoZ@yaz2010` - **Контейнер приложения:** `prototype-enduro-trails-1`, порт `5558` - **URL:** `https://openclaw.mva154.duckdns.org/enduro/` - **Код на сервере:** `/home/slin/enduro-trails/prototype/` - **Workspace:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/` - **БД:** `/home/slin/enduro-trails/data/centralfederal.sqlite` (431 MB, 1.1M треков, 14K POI) - **OSRM контейнер:** `osrm-osrm-routed-1`, порт `5559` - **OSRM данные:** `/home/slin/enduro-trails/data/enduro.osrm.*` (~5.2 GB) - **OSRM профиль:** `/home/slin/enduro-trails/osrm/enduro.lua` - **Swap:** `/home/slin/swapfile3` (4 GB), итого 6 GB swap - **Деплой:** Node.js ssh2 (SSH бинарник в контейнере не работает — glibc 2.36 vs 2.38) ### Деплой ```bash # ВАЖНО: docker cp нужно делать ПОСЛЕ рестарта контейнера! # Образ перезаписывает статику при рестарте. # 1. Загрузить файлы на сервер через SFTP (deploy_static.js) # 2. docker restart prototype-enduro-trails-1 # 3. Подождать 8 сек # 4. docker cp /home/slin/enduro-trails/prototype/static/app.js prototype-enduro-trails-1:/app/static/app.js # 5. docker cp /home/slin/enduro-trails/prototype/static/app.css prototype-enduro-trails-1:/app/static/app.css # 6. docker cp /home/slin/enduro-trails/prototype/static/index.html prototype-enduro-trails-1:/app/static/index.html # Для бэкенда (app.py): docker cp /home/slin/enduro-trails/prototype/app.py prototype-enduro-trails-1:/app/app.py docker restart prototype-enduro-trails-1 # deploy_app2.js — только для app.py! # deploy_static.js — SFTP статики, но cp делать вручную после рестарта ``` --- ## Схема БД ```sql -- trails: id, osm_id, highway_type, track_type, surface, name, length_m, -- mtb_scale, visibility, smoothness, access, tags, geom BLOB, -- min_lon, max_lon, min_lat, max_lat -- poi: id, osm_id, poi_type, name, geom BLOB, lon, lat ``` --- ## Реестр фич | ID | Фича | Описание | Статус | Фаза | |----|------|----------|--------|------| | F-01 | Альтернативные маршруты | До 5 вариантов с разным балансом грунт/асфальт | ✅ Готово | 3 | | F-02 | Статистика маршрута | Карточки с % покрытия, полоска, развёрнутый вид | ✅ Готово | 3 | | F-03 | Человекочитаемое время | "2 ч 35 мин" вместо "155 мин" | ✅ Готово | 3 | | F-04 | Промежуточные точки | Добавление, удаление, drag, debounce 300ms | ✅ Готово | 3 | | F-05 | GPX экспорт | Трек + waypoints + флажки, имя enduro-YYYYMMDDHHMMSS.gpx | ✅ Готово | 3 | | F-06 | Флажки/метки | localStorage, попап → A / → B / удалить, иконки | ✅ Готово | 3 | | F-07 | Исключить шлагбаумы | Баррьеры → inaccessible в OSRM | 📋 BRD готов | 3.1 | | F-08 | Исключить тротуары | footway/pedestrian/steps убрать из графа | 📋 BRD готов | 3.1 | | F-09 | Больше альтернатив | Penalized re-query + дедупликация | 📋 BRD готов | 3.1 | | F-10 | Слой препятствий | Шлагбаумы, броды, блокпосты на карте | 📋 BRD готов | 3.1 | | F-11 | "Красивый маршрут" | Замкнутый круг через живописные места, scenic_score, варианты | ⏳ Бэклог | 4 | | F-12 | "Горка" | Макс набор высоты, мин дистанция (SRTM) | ⏳ Бэклог | 6 | | F-13 | "Связка" | Соединить два трека грунтовками | ⏳ Бэклог | 4 | | F-14 | "Разведка" | Грунтовки вокруг точки, статистика по типам, POI | ⏳ Бэклог | 4 | | F-15 | "Народные треки" | OSM Traces, Wikiloc, Komoot, 4x4travel | ⏳ Бэклог | 8 | | F-16 | Тёмная тема + редизайн | Две темы (авто/светлая/тёмная), SunCalc, мобильный UI, drag-and-drop точек, расстояние по маршруту | ✅ Готово | 5 | | F-29 | Рельеф (terrain) | Цветной рельеф (все зумы) + теневой (зум 10+). SRTM 30м, кнопка 🏔️ в toolbar с чекбоксами | ⚠️ В работе | 5.4 | | F-22 | Линейка UX | Расстояние сегмента под маркером, крестик удаления, зелёный Старт, панель fit-content, toast, toggle скрыть/показать, deleteRuler | ✅ Готово | 5.2 | | F-23 | Метки UX | Починен баг удаления через попап (popup.remove() перед marker.remove()) | ✅ Готово | 5.2 | | F-24 | Поиск точек маршрута | Inline Nominatim поиск в каждом wl-item, убран верхний search bar | ✅ Готово | 5.2 | | F-25 | Route onboarding mini-bar | При активации режима маршрута — мини-бар с пином S/F, подсказкой и поиском Nominatim. Большой лист не открывается автоматически | ✅ Готово | 5.3 | | F-26 | Route sheet UX | Крестик → шеврон сворачивания; «Добавить точку» показывает onboarding мини-бар с подсказкой и поиском | ✅ Готово | 5.3 | | F-27 | Route корзина → мини-бар | Корзина в листе маршрута закрывает лист и показывает onboarding мини-бар для старта (как первый тап на кнопку маршрута) | ✅ Готово | 5.3 | | F-28 | Линейка: центрирование панели | #ruler-info центрирован по горизонтали (left:50% + translateX(-50%)), не перекрывает кнопки масштаба | ✅ Готово | 5.3 | | F-17 | PWA + офлайн | Service Worker, MBTiles, GPS-трекинг | ⏳ Бэклог | 7 | | F-18 | Тёмная/светлая карта | Карта переключается синхронно с темой UI: `style.json` (светлая) / `style-dark.json` (тёмная) | ✅ Готово | 5.1 | | F-19 | Иконка колеса + спиннер | SVG мотокросс-колесо (32 спицы, 36 зубцов кноблинга, 8 болтов ступицы, currentColor). Вращается при построении маршрута | ✅ Готово | 5.3 | | F-20 | Линейка: перетаскивание точек | Возможность перетащить уже поставленную точку линейки на новое место | ⏳ Бэклог | 5.x | | F-21 | Линейка: сохранение между сессиями | Сохранять точки линейки в localStorage, восстанавливать при перезагрузке | ⏳ Бэклог | 5.x | --- ## Выполненные фазы ### ✅ Фаза 1 — MVP (02.05.2026) - PBF парсинг (ЦФО + Чувашия) - Spatialite БД (1.1M треков, 14K POI) - FastAPI self-hosted MVT тайлы - MapLibre GL JS карта с кастомным стилем - Попапы с информацией о дороге - Контролы слоёв ### ✅ Фаза 2 — Роутинг + UI (03.05.2026) - OSRM + кастомный профиль `enduro.lua` (порт 5559) - Роутинг «Дикий путь» — кнопка 🗺️, маркеры A/B - Поиск (Nominatim, debounce 400ms) - Линейка 📏 (haversine) - Геолокация 📍, компас 🧭 ### ✅ Фаза 3 — Умный маршрут (04.05.2026) - **F-01:** Альтернативные маршруты (до 5, цвета: синий/зелёный/фиолетовый/оранжевый/серый) - **F-02:** Статистика покрытия (карточки: компактные + развёрнутые, полоска покрытия, %) - **F-03:** `formatDuration()` — "2 ч 35 мин" / "1 дн 2 ч 50 мин" - **F-04:** Промежуточные точки (до 8, draggable, debounce 300ms) - **F-05:** GPX 1.1 экспорт (трек + waypoints + флажки) - **F-06:** Флажки 🚩 (localStorage, 50 лимит, попап → A / → B / удалить) **Баги исправлены (04.05):** - formatDuration(86400) → "1 дн" (не "1 дн 0 ч") - OSRM TooBig → retry 5→3→1 - Панель маршрута перекрывала кнопки → CSS right: 56px - Длинные маршруты не строились → radiuses=5000 + NoSegment retry с 10km + timeout 60s --- ## Бэклог по фазам ### 📋 Фаза 3.1 — Улучшение роутинга (BRD готов, ожидает согласования) **BRD:** `BRD_PHASE3.1.md` | Фича | Описание | Сложность | Требует | |------|----------|-----------|---------| | F-07 | Шлагбаумы → inaccessible в OSRM | Низкая (lua) | Пересборка графа ~40 мин | | F-08 | Убрать footway/pedestrian/steps из графа | Низкая (lua) | Пересборка графа ~40 мин | | F-09 | Penalized re-query для разных альтернатив | Высокая | — | | F-10 | Слой препятствий на карте (🚧🌊⛔) | Средняя | Перепарсинг PBF ~30 мин | **Порядок:** F-07+F-08 вместе (одна пересборка) → F-09 → F-10 ### ✅ Фаза 4 — Продвинутый роутинг (04.05.2026) - **F-11** «Красивый маршрут» — замкнутый круг через живописные места, scenic_score, карточки с вариантами - **F-13** «Связка» — соединить два трека грунтовками, альтернативы - **F-14** «Разведка» — грунтовки в радиусе 20/50/100 км, статистика по типам, POI ### ⏳ Фаза 4 — Продвинутый роутинг (в разработке) - **F-11** «Красивый маршрут» — замкнутый круг, scenic_score, варианты по дистанции (50/100/150/200 км) - **F-13** «Связка» — соединить два трека грунтовками, альтернативы - **F-14** «Разведка» — грунтовки в радиусе 20/50/100 км, статистика по типам, POI - Мини-бар маршрута (`#mini-route-bar`) — компактный HUD поверх карты ### ✅ Фаза 5.3 — Route UX + Линейка (06.05.2026) - **F-25** Route onboarding mini-bar — при активации режима маршрута показывается мини-бар с пином S/F, подсказкой и поиском Nominatim - **F-26** Route sheet UX — крестик заменён на шеврон сворачивания; «Добавить точку» показывает onboarding мини-бар - **F-27** Корзина в листе маршрута — закрывает лист и открывает onboarding мини-бар для старта - **F-28** Линейка — панель `#ruler-info` центрирована по горизонтали, не перекрывает кнопки масштаба ### ✅ Фаза 5 — Редизайн (05.05.2026) **BRD:** `BRD_PHASE5.md` - F-16 Тёмная + светлая тема, авто-режим по SunCalc (восход/закат) - Bottom sheets, toolbar, SVG Lucide-иконки, skeleton loading - Свайп вниз для закрытия sheet - Drag-and-drop точек маршрута (touch + mouse) - Кнопка «Скачать GPX» (share dialog убран) - Фикс: «Добавить точку» из мини-бара - Десктоп-адаптив (toolbar слева, sheet max-width 400px) - Расстояние по маршруту между точками (по геометрии OSRM, масштабирование к route.distance_m) - Обновление расстояний при смене варианта маршрута **Баги исправлены (05.05):** - «Добавить точку» не работала — `addWaypointMode()` не устанавливала `routeMode = true` - Drag-and-drop не работал на десктопе — добавлены mouse-события - Расстояние в списке точек показывало haversine по прямой вместо маршрута - При смене варианта маршрута расстояния не обновлялись (`selectRoute`/`selectMiniRoute` не вызывали `renderWaypointsList`) - `renderWaypointsList` вызывался до построения маршрута → добавлен вызов после `drawRouteResults` - Деплой статики через `deploy_app2.js` не работал (копировал только `app.py`) → нужен `deploy_static.js` + `docker cp` **после** рестарта контейнера (образ перезаписывает статику при рестарте) **Технические решения:** - `getRouteSegmentDistances()` — snap waypoints к геометрии маршрута, суммирование haversine по точкам, масштабирование к `route.distance_m` - `snapIdx[0] = 0`, `snapIdx[last] = n-1` — принудительный snap первой/последней точки - Деплой статики: SFTP → сервер → `docker restart` → `docker cp` (порядок важен!) - `streaming.mode: "off"` в Telegram канале — убраны дублированные сообщения - `send_voice.sh` — убрана отправка через `openclaw message send`, только генерация OGG + `MEDIA:` директива ### ⚠️ Фаза 5.4 — Рельеф на карте (12.05.2026, в работе) **BRD:** `BRD_TERRAIN.md` **DEV TASK:** `DEV_TASK_TERRAIN.md` **TEST CASES:** `TEST_CASES_TERRAIN.md` **Что сделано:** - ✅ SRTM данные скачаны (ЦФО + Чувашия) - ✅ Hillshade тайлы сгенерированы (зумы 10-15, 3.04M тайлов) - 🔄 Hypso тайлы: зум 15 готов, зумы 5-14 генерируются (перегенерация запущена 12.05.2026) - ✅ Nginx настроен: `/enduro/terrain/` → alias `/home/slin/enduro-trails/data/terrain/` - ✅ Cache-Control: `public, immutable` - ✅ Кнопка 🏔️ в toolbar, попап с чекбоксами «Гипсометрия» / «Отмывка» - ✅ JS-логика terrain задеплоена в app.js (12.05.2026): toggleTerrainPopup, onTerrainCheckbox, applyTerrainLayer, updateHillshadeAvailability, restoreTerrainState - ✅ localStorage персистентность состояния - ✅ Кнопка active при включённом слое - ✅ Попап закрывается по клику вне / повторному нажатию - ✅ Hillshade чекбокс disabled на зуме < 10 + hint «Зум 10+» - ✅ `scheme: 'tms'` в sources (тайлы в TMS формате) - ✅ `bounds: [35, 45, 55, 62]` — ограничение запросов регионом данных - ✅ Z-index: рельеф под дорогами (`beforeId` = первый road/trail layer) **Баги найдены и пофикшены (12.05.2026):** - 🐛→✅ JS-функции terrain отсутствовали в app.js (HTML/CSS были, JS нет) — задеплоены Dev-агентом - 🐛→🔄 Hypso тайлы 404 на зумах 5-14 (были только для зума 15) — перегенерация запущена **Известные проблемы:** - ⚠️ Гипсометрия выглядит как однородный зелёный блок на малых зумах — ЦФО равнина, мало перепадов. Нужна корректировка color ramp для лучшей дифференциации низких высот (0-200м) - ⏳ TC-24/TC-25 (мобильный попап) — не проверены, ждут ручного тестирования - 🔄 Hypso тайлы зумы 5-14 генерируются (~30-60 мин) **Тест-кейсы (TC-07..TC-25):** - ✅ 19 из 19 автоматизируемых — зелёные - ⏳ 2 мобильных (TC-24, TC-25) — ожидают ручной проверки **UI-тесты terrain (Playwright + vision, 12.05.2026):** - ✅ TC-T-01 — Кнопка рельеф видна (desktop + mobile) - ✅ TC-T-02 — Попап открывается (desktop + mobile) - ✅ TC-T-03 — Гипсометрия включается (чекбокс, слой добавляется) - ⚠️ TC-T-04 — Hillshade чекбокс disabled на зуме < 10 (ожидаемое поведение) - ✅ TC-T-05 — Попап закрывается по повторному клику - ✅ TC-T-06 — Мобильный попап не обрезан - ✅ TC-T-08 — Hillshade виден на зуме 11 (vision: PASS) **Технические детали:** - Тайлы генерировались `gdal2tiles.py` без `--xyz` → формат TMS (y инвертирован) - MapLibre source: `scheme: 'tms'` для корректного маппинга - Color ramp: `hypso_ramp.txt`, nodata → прозрачный (alpha), убрана строка `-100 70 107 159` - Hypso: opacity 0.55, hillshade: opacity 0.40 - Фронтенд отдаётся через FastAPI (контейнер), статика через `docker cp` - Terrain тайлы отдаются nginx напрямую (не через контейнер) ### ⏳ Фаза 6 — SRTM рельеф (продвинутый) - F-12 «Горка» — макс набор высоты, мин дистанция - Профиль высот на маршруте - SRTM данные уже есть после фазы 5.4 ### ⏳ Фаза 7 — PWA + офлайн - F-17 Service Worker, офлайн MBTiles, GPS-трекинг в реальном времени - Интеграция с OsmAnd/Locus (экспорт) ### ⏳ Фаза 8 — Народные треки - F-15 Источники: OSM Traces, Wikiloc, Komoot, 4x4travel.ru, Enduroad.ru - Отдельный слой `community_tracks`, фильтрация по типу активности --- ## Ключевые решения | Решение | Причина | |---------|---------| | 4 uvicorn workers | Устранение узкого места однопоточности | | Фильтр по length_m на низких зумах | Производительность, читаемость | | Относительные пути в app.js | Работает через nginx /enduro/ и по прямому IP | | OSRM `weight_name='routability'` | `duration` → OSRM выбирал асфальт | | `forward_rate = penalty` | Penalty/метр — прямой вес пути | | `radiuses=5000` в OSRM запросах | Без этого длинные маршруты не снэппятся | | Retry TooBig: 5→3→1 | OSRM не даёт 5 альтернатив на длинных маршрутах | | Retry NoSegment: radiuses=10000 | Точки далеко от дорог — нужен широкий snap | | Timeout 60s для retry | Длинные маршруты строятся >30с | | `docker cp` после рестарта (не до) | Образ перезаписывает статику при рестарте — cp нужен после того как контейнер поднялся | | `deploy_app2.js` только для app.py | Скрипт не копирует статику — для фронтенда использовать `deploy_static.js` + ручной cp после рестарта | | Масштабирование сегментов к `route.distance_m` | OSRM геометрия упрощена — haversine по точкам даёт ~0.2% погрешность; масштабирование даёт точное совпадение | | `renderWaypointsList()` после `drawRouteResults()` | Список рендерится до построения маршрута — нужен повторный вызов когда `routeResults` заполнен | | Тайлы terrain в TMS формате | `gdal2tiles.py` без `--xyz` генерирует TMS; MapLibre по умолчанию XYZ → нужен `scheme: 'tms'` в source | | `bounds: [35, 45, 55, 62]` в terrain sources | Без bounds MapLibre запрашивает тайлы за пределами региона → 404 | | nodata → прозрачный (alpha) | Строка `-100 70 107 159` в ramp красила водоёмы/nodata в синий; убрана, добавлен `-alpha` при генерации | | Terrain тайлы через nginx, не через контейнер | Статика быстрее через nginx alias; контейнер обслуживает только API и фронтенд | | `streaming.mode: "off"` для Telegram | `partial` и `progress` шлют промежуточные сообщения — `off` даёт одно финальное | | Сэмплирование каждые ~500м для stats | Без этого расчёт >30с на длинных маршрутах | | Grid cache для calc_route_stats | Убирает повторные SQL запросы для близких точек | --- ## ET-011 — Скачивание GPX чужих публичных треков (03-04.06.2026) ✅ > Не путать с F-05 (экспорт СОБСТВЕННОГО построенного маршрута). ET-011 — скачивание уже собранных чужих треков из попапа на карте. **Фича:** кнопка «Скачать GPX» в попапе публичного трека. Эндпоинт `GET /api/gps-tracks/{track_id}/download` — собирает GPX 1.1 из геометрии трека, с проверками формата/размера/лицензии. **Лицензионная политика (ADR-015, default-deny):** - `config/gps_sources.yaml` → `download_allowed: ` на каждый источник. По умолчанию `DEFAULT_DOWNLOAD_ALLOWED_SOURCES = {"osm"}` — качать можно только OSM. - Источник без флага → **403 `source_forbidden`** + ссылка на оригинал (`external_urls`). Это не баг — намеренно. **Состояние источников (origin/main, enduro-trails #23):** - `osm` → `true` | `enduro_russia` → **`true`** (А-1, решение владельца 04.06) | `wikiloc` → `false` | `ttrails` → `false` - ⚠️ **А-1:** `enduro_russia` включён по явному решению Славы (286 треков — из этого источника). ToS EnduroRussia явно не разрешает ре-экспорт — юр-риск принят владельцем осознанно. **Проверка на проде (трек 102):** HTTP 200, 265 КБ, `application/gpx+xml`, content-disposition с кириллицей, валидный GPX 1.1 (xmllint OK). ✅ Качается. ### ⚠️ Грабли ET-011 (важные) | Грабля | Решение | |--------|---------| | Конфиг `gps_sources.yaml` монтируется volume `./config:/app/config:ro` | Правка файла + рестарт контейнера = новая политика **без пересборки образа** | | Файл `gps_sources.yaml` root-owned, у slin нет write/sudo | Правка через docker-root: `docker run --rm -v .../config:/c alpine sh -c '...'` | | `download_allowed: false` (default ADR-015) → 403, а не 404 | Если фронт показывает «Открыть оригинал» — источник просто не разрешён к скачиванию (не баг) | | 404 на `/download` при живом треке = маршрут НЕ зарегистрирован (старый образ) | Проверять openapi прода в первую очередь; пересобрать контейнер из актуального main | | deploy-hook падал: `/var/log/enduro-trails` root-owned | **ИСПРАВЛЕНО 04.06** через installer: `sudo chown slin:slin /var/log/enduro-trails`. Сейчас slin пишет в каталог → хук не падает | --- ## Документы | Документ | Описание | |----------|----------| | `PROJECT.md` | Этот файл — общее состояние проекта | | `CONCEPT.md` | Концепция и архитектура | | `TECHNICAL_SPEC.md` | ТЗ прототипа v0.1 | | `DEV_TASK.md` | Стабилизация v0.1 (архив) | | `BRD_PHASE3.md` | Бизнес-требования Фазы 3 (согласовано) | | `BRD_PHASE3.1.md` | Бизнес-требования Фазы 3.1 (на согласовании) | | `BRD_PHASE5.md` | Бизнес-требования Фазы 5 (✅ реализовано) | | `BRD_TERRAIN.md` | Бизнес-требования Фазы 5.4 — Рельеф | | `DEV_TASK_TERRAIN.md` | ТЗ для Dev-агента Фаза 5.4 | | `TEST_CASES_PHASE3.md` | 56 тест-кейсов | | `DEV_TASK_PHASE3.md` | ТЗ для Dev-агента Фаза 3 | | `DEV_TASK_PHASE5.md` | ТЗ для Dev-агента Фаза 5 | | `TEST_CASES_TERRAIN.md` | 25 тест-кейсов Фазы 5.4 (19/19 auto ✅, 2 mobile ⏳) | | `DEV_TASK_TERRAIN_JS.md` | ТЗ: JS-логика terrain (баг — функции отсутствовали в app.js) | | `TEST_CASES_UI_TERRAIN.md` | 6 UI-тестов terrain (Playwright + vision) | | `TEST_CASES_UI_TERRAIN_EXT.md` | Расширенные UI-тесты terrain (зум 10-11) | | `DEV_TASK_DARK_STYLE.md` | ТЗ: тёмная тема карты (F-18) | | `reports/` | Отчёты о тестировании | --- *Ссылка на онтологию: `proj_enduro_trails`*