23 KiB
Enduro Trails 🏍️
OSM-карта с фокусом на грунтовые дороги для построения красивых эндуро-маршрутов
Статус: active
Старт: 2026-05-02
Автор: Слава
Концепция
Обычные карты оптимизированы под автомобили — асфальт яркий, грунтовки не видны. Enduro Trails переворачивает эту логику: грунтовки/тропы — главный слой, асфальт — тусклый фон. Плюс фичи для поиска и построения красивых маршрутов (минимум асфальта, максимум красоты).
Регион
- ЦФО + Чувашия (первый регион, прототип)
- Расширение на новые ФО по запросу
Архитектура
Стек
- Pyrosm/Osmium → парсинг PBF
- Spatialite → хранение (прототип), PostGIS → продакшен
- OSRM (кастомный профиль
enduro.lua) → роутинг - FastAPI + uvicorn (4 workers) → бэкенд
- MapLibre GL JS → фронт (веб + PWA)
Инфраструктура
- Сервер:
slin@82.22.50.71, sudomotoZ@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)
Деплой
# ВАЖНО: 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 делать вручную после рестарта
Схема БД
-- 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_msnapIdx[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 тайлы перегенерированы (зумы 5-15, 1.05M тайлов) — убран синий nodata
- ✅ Nginx настроен:
/enduro/terrain/→ alias/home/slin/enduro-trails/data/terrain/ - ✅ Cache-Control:
public, immutable - ✅ Кнопка 🏔️ в toolbar, попап с чекбоксами «Гипсометрия» / «Отмывка»
- ✅ localStorage персистентность состояния
- ✅ Кнопка active при включённом слое
- ✅ Попап закрывается по клику вне / повторному нажатию
- ✅ Hillshade чекбокс disabled на зуме < 10 + hint «Зум 10+»
- ✅
scheme: 'tms'в sources (тайлы в TMS формате) - ✅
bounds: [35, 45, 55, 62]— ограничение запросов регионом данных - ✅ Z-index: рельеф под дорогами (
beforeId= первый road/trail layer)
Известные проблемы:
- ⚠️ Гипсометрия выглядит как однородный зелёный блок на малых зумах — ЦФО равнина, мало перепадов. Нужна корректировка color ramp для лучшей дифференциации низких высот (0-200м)
- ⏳ TC-24/TC-25 (мобильный попап) — не проверены, ждут ручного тестирования
Тест-кейсы (TC-07..TC-25):
- ✅ 19 из 19 автоматизируемых — зелёные
- ⏳ 2 мобильных (TC-24, TC-25) — ожидают ручной проверки
Технические детали:
- Тайлы генерировались
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 запросы для близких точек |
Документы
| Документ | Описание |
|---|---|
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 ⏳) |
reports/ |
Отчёты о тестировании |
Ссылка на онтологию: proj_enduro_trails