From 3ba7ac20aa9eb13d69d042214bf363c9de17412c Mon Sep 17 00:00:00 2001 From: Stream Date: Tue, 5 May 2026 17:10:02 +0300 Subject: [PATCH] auto-sync: 2026-05-05 17:10:02 --- tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md | 166 ++++++++++++++++++ tasks/enduro-trails/prototype/static/app.js | 68 ++++++- .../enduro-trails/prototype/static/index.html | 3 + 3 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md diff --git a/tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md b/tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md new file mode 100644 index 0000000..1214b7c --- /dev/null +++ b/tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md @@ -0,0 +1,166 @@ +# Dev Task: Enduro Trails — UX фиксы #1, #3, #5 + +**Файлы:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/` +**Деплой:** `node /tmp/deploy_static.js` +**Бэкенд не трогать.** + +--- + +## Фикс 1: Кнопка «+» на мини-баре + +Сейчас чтобы добавить промежуточную точку нужно открыть полную панель. +Нужно: добавить кнопку «+» прямо на мини-баре. + +### HTML — в `#sheet-route-mini` добавить кнопку рядом со стрелками: +```html + +``` + +Вставить в `.mini-route-info` после `.mini-route-arrows`. + +### CSS: +```css +.mini-add-btn { + width: 32px; height: 32px; + display: flex; align-items: center; justify-content: center; + background: var(--accent); border: none; + border-radius: 10px; color: #fff; + cursor: pointer; flex-shrink: 0; + -webkit-tap-highlight-color: transparent; +} +.mini-add-btn:active { opacity: 0.8; transform: scale(0.94); } +``` + +### JS — новая функция: +```js +function miniAddWaypoint() { + // Enter waypoint-adding mode without opening full sheet + if (!routeMode) { + // Re-enter route mode silently (no sheet open) + routeMode = true; + document.getElementById('tb-route').classList.add('active'); + updateMapModeClass(); + } + addWaypointMode(); + // Show hint on mini-bar + document.getElementById('mini-stats').textContent = 'Тапни на карте для добавления точки'; +} +``` + +После добавления точки (в обработчике клика карты где `addingWaypoint = false`) — восстановить нормальный текст мини-бара: +```js +// После rebuildWaypointMarkers(); renderWaypointsList(); при addingWaypoint +updateMiniRouteCard(); // обновить мини-бар +``` + +--- + +## Фикс 3: После добавления точки — свернуть панель в мини-бар + +Сейчас после добавления точки полная панель остаётся открытой. +Нужно: после успешного построения маршрута (когда routeResults обновились) — если панель открыта, свернуть её в мини-бар. + +### В функции `drawRouteResults()` — в конце добавить: +```js +// Auto-minimize sheet after route is built +const sheet = document.getElementById('sheet-route'); +if (sheet && sheet.classList.contains('open')) { + minimizeSheet('sheet-route'); +} +``` + +**Важно:** это должно срабатывать только когда маршрут уже был (перестройка), а не при первом построении. Проверить: если `routeResults.length > 0` до вызова — значит это перестройка, сворачивать. Если первый раз — не сворачивать (пусть пользователь видит варианты). + +Логика: +```js +const wasBuilt = routeResults.length > 0; // до обновления routeResults +// ... существующий код drawRouteResults ... +// В конце: +if (wasBuilt) { + const sheet = document.getElementById('sheet-route'); + if (sheet && sheet.classList.contains('open')) { + minimizeSheet('sheet-route'); + } +} +``` + +--- + +## Фикс 5: Reverse geocoding — названия мест вместо координат + +Сейчас в списке точек показываются координаты: `55.7234, 37.6123`. +Нужно: показывать название места через Nominatim reverse geocoding. + +### Добавить функцию reverse geocoding: +```js +const geocodeCache = {}; + +async function reverseGeocode(lat, lon) { + const key = `${lat.toFixed(4)},${lon.toFixed(4)}`; + if (geocodeCache[key]) return geocodeCache[key]; + try { + const resp = await fetch( + `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=ru`, + { headers: { 'Accept-Language': 'ru' } } + ); + const data = await resp.json(); + // Приоритет: village/town/city > road > county + const a = data.address || {}; + const name = a.village || a.town || a.city || a.suburb || a.road || a.county || a.state || `${lat.toFixed(3)}, ${lon.toFixed(3)}`; + geocodeCache[key] = name; + return name; + } catch(e) { + return `${lat.toFixed(3)}, ${lon.toFixed(3)}`; + } +} +``` + +### Изменить `renderWaypointsList()`: + +Сделать функцию async и использовать geocoding: +```js +async function renderWaypointsList() { + const list = document.getElementById('waypoints-list'); + if (!routeWaypoints.length) { list.innerHTML = ''; return; } + + // Render immediately with coords, then update with names + list.innerHTML = routeWaypoints.map((wp, i) => { + const labelClass = i === 0 ? 'start' : i === routeWaypoints.length - 1 ? 'end' : 'mid'; + const color = labelClass === 'start' ? 'var(--success)' : labelClass === 'end' ? 'var(--red)' : '#0066ff'; + const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`; + return `
+
+ ${coordText} + +
`; + }).join(''); + + // Async update with place names + routeWaypoints.forEach(async (wp, i) => { + const name = await reverseGeocode(wp.lat, wp.lon); + const el = document.getElementById(`wl-label-${i}`); + if (el) el.textContent = name; + }); +} +``` + +### Также обновить мини-бар stats при первом построении маршрута: +В `updateMiniRouteCard()` — stats показывает км и % грунт, это ок. Не трогать. + +--- + +## Порядок реализации +1. Фикс 5 (geocoding) — добавить функцию, изменить renderWaypointsList +2. Фикс 3 (auto-minimize) — изменить drawRouteResults +3. Фикс 1 (кнопка + на мини-баре) — HTML + CSS + JS +4. Деплой + проверка + +## Проверка +1. Построить маршрут A→B → панель показывает варианты (не сворачивается при первом построении) +2. Добавить промежуточную точку через кнопку «+» в мини-баре → маршрут перестраивается → панель сворачивается +3. Открыть полную панель → в списке точек видны названия мест (не координаты) +4. Добавить точку через кнопку «+ Точка» в полной панели → маршрут перестраивается → панель сворачивается diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js index af2f6a2..fe3236b 100644 --- a/tasks/enduro-trails/prototype/static/app.js +++ b/tasks/enduro-trails/prototype/static/app.js @@ -497,22 +497,51 @@ function rebuildWaypointMarkers() { }); } -function renderWaypointsList() { +// ─── Reverse Geocoding ─────────────────────────────────────────── +const geocodeCache = {}; + +async function reverseGeocode(lat, lon) { + const key = `${lat.toFixed(4)},${lon.toFixed(4)}`; + if (geocodeCache[key]) return geocodeCache[key]; + try { + const resp = await fetch( + `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=ru`, + { headers: { 'Accept-Language': 'ru' } } + ); + const data = await resp.json(); + const a = data.address || {}; + const name = a.village || a.town || a.city || a.suburb || a.road || a.county || a.state || `${lat.toFixed(3)}, ${lon.toFixed(3)}`; + geocodeCache[key] = name; + return name; + } catch(e) { + return `${lat.toFixed(3)}, ${lon.toFixed(3)}`; + } +} + +async function renderWaypointsList() { const list = document.getElementById('waypoints-list'); if (!routeWaypoints.length) { list.innerHTML = ''; return; } + + // Render immediately with coords, then update with place names list.innerHTML = routeWaypoints.map((wp, i) => { - let labelClass, labelText; - if (i === 0) { labelClass = 'start'; labelText = 'A'; } - else if (i === routeWaypoints.length - 1) { labelClass = 'end'; labelText = 'B'; } - else { labelClass = 'mid'; labelText = String(i); } - return `
-
- ${wp.lat.toFixed(4)}, ${wp.lon.toFixed(4)} + const labelClass = i === 0 ? 'start' : i === routeWaypoints.length - 1 ? 'end' : 'mid'; + const color = labelClass === 'start' ? 'var(--success)' : labelClass === 'end' ? 'var(--red)' : '#0066ff'; + const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`; + return `
+
+ ${coordText}
`; }).join(''); + + // Async update labels with place names + routeWaypoints.forEach(async (wp, i) => { + const name = await reverseGeocode(wp.lat, wp.lon); + const el = document.getElementById(`wl-label-${i}`); + if (el) el.textContent = name; + }); } function removeWaypoint(idx) { @@ -576,6 +605,7 @@ async function buildRoute() { function drawRouteResults(routes, activeIdx) { const map = window._map; activeRouteIdx = activeIdx; + const wasBuilt = routeResults.length > 0; // track rebuild vs first build routeResults = routes; // Clear old layers @@ -630,6 +660,14 @@ function drawRouteResults(routes, activeIdx) { // Update mini sheet if visible const miniEl = document.getElementById('sheet-route-mini'); if (miniEl && miniEl.classList.contains('visible')) showMiniRouteSheet(); + + // Auto-minimize sheet on rebuild (not on first build) + if (wasBuilt) { + const sheet = document.getElementById('sheet-route'); + if (sheet && sheet.classList.contains('open')) { + minimizeSheet('sheet-route'); + } + } } function selectRoute(idx) { @@ -1034,6 +1072,7 @@ function initRouteClicks(map) { } rebuildWaypointMarkers(); renderWaypointsList(); if (routeWaypoints.length >= 2) debounceBuildRoute(); + updateMiniRouteCard(); return; } @@ -1618,6 +1657,19 @@ document.addEventListener('DOMContentLoaded', () => { // ─── Mini Route Bar ────────────────────────────────────────────────── +function miniAddWaypoint() { + // Enter waypoint-adding mode without opening full sheet + if (!routeMode) { + routeMode = true; + document.getElementById('tb-route').classList.add('active'); + updateMapModeClass(); + } + addWaypointMode(); + // Show hint on mini-bar + const statsEl = document.getElementById('mini-stats'); + if (statsEl) statsEl.textContent = 'Тапни на карте для добавления точки'; +} + function showMiniRouteSheet() { if (!routeResults || routeResults.length === 0) return; updateMiniRouteCard(); diff --git a/tasks/enduro-trails/prototype/static/index.html b/tasks/enduro-trails/prototype/static/index.html index f8c6502..4424d93 100644 --- a/tasks/enduro-trails/prototype/static/index.html +++ b/tasks/enduro-trails/prototype/static/index.html @@ -242,6 +242,9 @@
+