auto-sync: 2026-05-06 00:40:01
This commit is contained in:
@@ -66,7 +66,15 @@
|
||||
- Заменена на прямую кнопку «Скачать GPX» с download-иконкой (стрелка вниз)
|
||||
- `onclick="downloadGPX()"` — без промежуточных диалогов
|
||||
|
||||
### ✅ Мини-бар маршрута
|
||||
### ✅ Расстояние по маршруту между точками (добавлено 05.05.2026)
|
||||
|
||||
- Функция `getRouteSegmentDistances()` — snap каждого waypoint к ближайшей точке геометрии OSRM
|
||||
- Первая точка → `snapIdx[0] = 0`, последняя → `snapIdx[n-1]` — гарантирует покрытие всей геометрии
|
||||
- Сумма сегментов масштабируется к `route.distance_m` — точное совпадение (diff=0)
|
||||
- Обновляется при смене варианта (`selectRoute`, `selectMiniRoute`)
|
||||
- Обновляется после построения маршрута (`drawRouteResults` → `renderWaypointsList`)
|
||||
- Fallback на haversine по прямой если маршрут ещё не построен
|
||||
- Форматирование `toFixed(1)` везде (сегменты, карточки вариантов, мини-бар)
|
||||
|
||||
- `#mini-route-bar` — компактная полоска поверх карты когда sheet свёрнут
|
||||
- Показывает активный маршрут (дистанция, % грунт)
|
||||
@@ -83,6 +91,9 @@
|
||||
| Web Share API не работал на HTTP | API требует HTTPS | Убран share dialog, оставлено только скачивание GPX |
|
||||
| Drag-and-drop не работал на десктопе | Только touch-события | Добавлены mouse-события (mousedown/mousemove/mouseup) |
|
||||
| CSS классы диалога не совпадали с JS | Dev добавил JS с одними классами, CSS с другими | Унифицированы, затем диалог полностью убран |
|
||||
| Расстояние между точками по прямой | `renderWaypointsList` вызывался до построения маршрута, `routeResults` был пуст | Добавлен `renderWaypointsList()` после `drawRouteResults()` |
|
||||
| Расстояние не обновлялось при смене варианта | `selectRoute`/`selectMiniRoute` не вызывали `renderWaypointsList` | Добавлен `renderWaypointsList()` в обе функции |
|
||||
| Деплой статики не работал | `deploy_app2.js` копирует только `app.py`; образ перезаписывает статику при рестарте | SFTP → `docker restart` → `docker cp` (порядок важен!) |
|
||||
|
||||
---
|
||||
|
||||
@@ -100,6 +111,8 @@
|
||||
| Десктоп: боковая панель, не ломается layout | ✅ |
|
||||
| GPX скачивание работает | ✅ |
|
||||
| Drag-and-drop точек маршрута (touch + mouse) | ✅ |
|
||||
| Расстояние по маршруту между точками (сумма = route.distance_m) | ✅ |
|
||||
| Обновление расстояний при смене варианта маршрута | ✅ |
|
||||
| Деплой + health check OK | ✅ |
|
||||
|
||||
---
|
||||
@@ -109,7 +122,21 @@
|
||||
- **MapLibre GL JS** 4.7.0 (CDN)
|
||||
- **SunCalc** 1.9.0 (CDN) — авто-тема по восходу/закату
|
||||
- Без npm-зависимостей, всё inline CSS/JS
|
||||
- Деплой: Node.js ssh2 → SFTP → docker cp → docker restart
|
||||
- Деплой: Node.js ssh2 → SFTP на сервер → `docker restart` → `docker cp` после рестарта
|
||||
|
||||
### ⚠️ Критически важно: порядок деплоя статики
|
||||
|
||||
Образ Docker запекает статику при сборке. При `docker restart` образ перезаписывает `/app/static/`. Поэтому:
|
||||
|
||||
1. SFTP загрузить файлы на сервер (`/home/slin/enduro-trails/prototype/static/`)
|
||||
2. `docker restart prototype-enduro-trails-1`
|
||||
3. Подождать 8 секунд
|
||||
4. `docker cp /home/slin/.../app.js prototype-enduro-trails-1:/app/static/app.js`
|
||||
5. `docker cp /home/slin/.../app.css prototype-enduro-trails-1:/app/static/app.css`
|
||||
6. `docker cp /home/slin/.../index.html prototype-enduro-trails-1:/app/static/index.html`
|
||||
|
||||
`deploy_app2.js` — **только для `app.py`** (бэкенд).
|
||||
`deploy_static.js` — SFTP статики, но cp нужно делать вручную после рестарта.
|
||||
|
||||
---
|
||||
|
||||
|
||||
115
tasks/enduro-trails/BRD_WAYPOINT_SEARCH.md
Normal file
115
tasks/enduro-trails/BRD_WAYPOINT_SEARCH.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# BRD: Enduro Trails — Поиск точек маршрута
|
||||
|
||||
**Версия:** 1.0
|
||||
**Дата:** 2026-05-05
|
||||
**Автор:** Стрим 🌊
|
||||
**Статус:** 🔄 В разработке
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
Сейчас добавить точку маршрута можно только тапом на карту. Если нужно попасть в конкретное место (деревня, перекрёсток, POI) — нужно сначала найти его на карте, потом тапнуть. Это неудобно, особенно на мобиле.
|
||||
|
||||
Верхний search bar решал задачу навигации по карте, но не интегрирован с маршрутом — найденное место просто центрировало карту, не добавляя точку. Плюс bar занимал место и мешал карте.
|
||||
|
||||
---
|
||||
|
||||
## 2. Цель
|
||||
|
||||
Убрать верхний search bar. Добавить поиск прямо в список точек маршрута — inline, для каждой точки отдельно.
|
||||
|
||||
---
|
||||
|
||||
## 3. Что меняется
|
||||
|
||||
### 3.1 Убрать верхний search bar
|
||||
|
||||
- Удалить `#search-bar` и `#search-results` из HTML
|
||||
- Удалить все CSS стили (`#search-bar`, `#search-input`, `#search-results`, `.search-result-item`, `.sri-*`)
|
||||
- Удалить вызов `initSearch()` из JS
|
||||
- Кнопку темы (`#btn-theme`) перенести в `#map-controls-r` (рядом с компасом и геолокацией)
|
||||
|
||||
**Почему:** bar занимал ~52px сверху, перекрывал карту, дублировал функционал который теперь встроен в waypoints.
|
||||
|
||||
### 3.2 Inline поиск в каждой точке маршрута
|
||||
|
||||
В каждом `wl-item` добавить кнопку-лупу. Тап → разворачивается inline панель поиска прямо под точкой.
|
||||
|
||||
**UX flow:**
|
||||
1. Пользователь видит список точек маршрута (S → 1 → F)
|
||||
2. Тапает иконку 🔍 рядом с нужной точкой
|
||||
3. Под точкой появляется поле ввода с автофокусом
|
||||
4. Вводит название места → через 400ms debounce → Nominatim поиск
|
||||
5. Выбирает результат → точка обновляется, карта летит к ней, маршрут перестраивается
|
||||
6. Панель закрывается
|
||||
|
||||
**Детали:**
|
||||
- Одновременно открыта только одна панель поиска (остальные закрываются)
|
||||
- Поиск через Nominatim (`countrycodes=ru`, `accept-language=ru`, limit=5)
|
||||
- Результат: название + подзаголовок (область/район)
|
||||
- После выбора: `routeWaypoints[idx]` обновляется, `rebuildWaypointMarkers()` + `renderWaypointsList()` + `debounceBuildRoute()`
|
||||
- `map.flyTo()` к выбранному месту (zoom 13, duration 600ms)
|
||||
|
||||
---
|
||||
|
||||
## 4. UI компоненты
|
||||
|
||||
### Кнопка лупы в wl-item
|
||||
- Иконка: Lucide `search` SVG (15×15)
|
||||
- Цвет: `var(--text3)` → hover `var(--accent)`
|
||||
- Позиция: между `.wl-info` и `.wl-drag-handle`
|
||||
|
||||
### Inline панель поиска
|
||||
- Фон: `var(--surface)`, бордер сверху `var(--border)`
|
||||
- Input: `var(--surface2)` фон, `var(--border)` бордер, focus → `var(--accent)` бордер
|
||||
- Результаты: max-height 180px, scroll, hover `var(--surface2)`
|
||||
- Каждый результат: название (bold) + подзаголовок (мелкий, `var(--text2)`)
|
||||
|
||||
### Кнопка темы (перенос)
|
||||
- Переносится в `#map-controls-r` как `.map-btn`
|
||||
- `updateThemeButtonIcon()` продолжает работать — находит `#btn-theme` по id
|
||||
|
||||
---
|
||||
|
||||
## 5. Технические детали
|
||||
|
||||
| Аспект | Решение |
|
||||
|--------|---------|
|
||||
| Поиск API | Nominatim `/search` (уже используется в проекте) |
|
||||
| Debounce | 400ms (как в старом search bar) |
|
||||
| Состояние | `wpSearchTimeout` — глобальная переменная для debounce |
|
||||
| Закрытие панели | Тап на лупу повторно — закрывает; открытие другой — закрывает текущую |
|
||||
| После выбора | `routeWaypoints[idx]` = `{lat, lon}`, rebuild + route |
|
||||
| Кнопка «Добавить точку» | `wl-add` остаётся без изменений |
|
||||
|
||||
---
|
||||
|
||||
## 6. Definition of Done
|
||||
|
||||
| Критерий | Статус |
|
||||
|----------|--------|
|
||||
| Верхний search bar отсутствует | ⬜ |
|
||||
| Кнопка темы работает в map-controls-r | ⬜ |
|
||||
| Иконка лупы в каждом wl-item | ⬜ |
|
||||
| Тап на лупу → inline поиск открывается | ⬜ |
|
||||
| Поиск находит места через Nominatim | ⬜ |
|
||||
| Выбор результата → waypoint обновляется | ⬜ |
|
||||
| Маршрут перестраивается после выбора | ⬜ |
|
||||
| Карта летит к выбранному месту | ⬜ |
|
||||
| Одновременно открыта только одна панель | ⬜ |
|
||||
| Деплой + health check OK | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 7. Файлы
|
||||
|
||||
| Файл | Изменения |
|
||||
|------|-----------|
|
||||
| `prototype/static/index.html` | Удалить `#search-bar`, `#search-results`; перенести `#btn-theme` в `#map-controls-r` |
|
||||
| `prototype/static/app.css` | Удалить стили search bar; добавить `.wl-search-btn`, `.wl-search-panel`, `.wl-search-input`, `.wl-search-results`, `.wl-search-result-item` |
|
||||
| `prototype/static/app.js` | Удалить `initSearch()`; добавить `openWaypointSearch()`, `doWaypointSearch()`, `selectWaypointSearchResult()` |
|
||||
|
||||
---
|
||||
|
||||
*Следующая фаза после реализации: тестирование на мобиле.*
|
||||
@@ -45,10 +45,22 @@
|
||||
|
||||
### Деплой
|
||||
```bash
|
||||
# SFTP через ssh2: /tmp/deploy_app.js
|
||||
# docker cp + restart (файлы запечены в образ)
|
||||
# ВАЖНО: 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 делать вручную после рестарта
|
||||
```
|
||||
|
||||
---
|
||||
@@ -84,7 +96,7 @@ docker restart prototype-enduro-trails-1
|
||||
| F-13 | "Связка" | Соединить два трека грунтовками | ⏳ Бэклог | 4 |
|
||||
| F-14 | "Разведка" | Грунтовки вокруг точки, статистика по типам, POI | ⏳ Бэклог | 4 |
|
||||
| F-15 | "Народные треки" | OSM Traces, Wikiloc, Komoot, 4x4travel | ⏳ Бэклог | 8 |
|
||||
| F-16 | Тёмная тема + редизайн | Две темы (авто/светлая/тёмная), SunCalc, мобильный UI, drag-and-drop точек | ✅ Готово | 5 |
|
||||
| F-16 | Тёмная тема + редизайн | Две темы (авто/светлая/тёмная), SunCalc, мобильный UI, drag-and-drop точек, расстояние по маршруту | ✅ Готово | 5 |
|
||||
| F-17 | PWA + офлайн | Service Worker, MBTiles, GPS-трекинг | ⏳ Бэклог | 7 |
|
||||
|
||||
---
|
||||
@@ -156,6 +168,23 @@ docker restart prototype-enduro-trails-1
|
||||
- Кнопка «Скачать 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:` директива
|
||||
|
||||
### ⏳ Фаза 6 — SRTM рельеф
|
||||
- F-12 «Горка» — макс набор высоты, мин дистанция
|
||||
@@ -185,8 +214,11 @@ docker restart prototype-enduro-trails-1
|
||||
| Retry TooBig: 5→3→1 | OSRM не даёт 5 альтернатив на длинных маршрутах |
|
||||
| Retry NoSegment: radiuses=10000 | Точки далеко от дорог — нужен широкий snap |
|
||||
| Timeout 60s для retry | Длинные маршруты строятся >30с |
|
||||
| Node.js ssh2 для деплоя | SSH бинарник в контейнере требует glibc 2.38 |
|
||||
| docker cp вместо compose build | Файлы запечены в образ, cp в running container быстрее |
|
||||
| `docker cp` после рестарта (не до) | Образ перезаписывает статику при рестарте — cp нужен после того как контейнер поднялся |
|
||||
| `deploy_app2.js` только для app.py | Скрипт не копирует статику — для фронтенда использовать `deploy_static.js` + ручной cp после рестарта |
|
||||
| Масштабирование сегментов к `route.distance_m` | OSRM геометрия упрощена — haversine по точкам даёт ~0.2% погрешность; масштабирование даёт точное совпадение |
|
||||
| `renderWaypointsList()` после `drawRouteResults()` | Список рендерится до построения маршрута — нужен повторный вызов когда `routeResults` заполнен |
|
||||
| `streaming.mode: "off"` для Telegram | `partial` и `progress` шлют промежуточные сообщения — `off` даёт одно финальное |
|
||||
| Сэмплирование каждые ~500м для stats | Без этого расчёт >30с на длинных маршрутах |
|
||||
| Grid cache для calc_route_stats | Убирает повторные SQL запросы для близких точек |
|
||||
|
||||
|
||||
@@ -67,78 +67,60 @@ html, body {
|
||||
|
||||
#map { position: fixed; inset: 0; z-index: 0; }
|
||||
|
||||
/* ── Search Bar ──────────────────────────────── */
|
||||
#search-bar {
|
||||
position: fixed;
|
||||
top: max(env(safe-area-inset-top, 0px), 12px);
|
||||
left: 12px; right: 12px;
|
||||
height: 50px;
|
||||
/* Push MapLibre nav controls below search bar */
|
||||
}
|
||||
/* ── MapLibre nav controls position ──────────── */
|
||||
.maplibregl-ctrl-top-left {
|
||||
top: calc(max(env(safe-area-inset-top, 0px), 12px) + 58px) !important;
|
||||
top: calc(max(env(safe-area-inset-top, 0px), 12px) + 8px) !important;
|
||||
left: 12px !important;
|
||||
}
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
display: flex; align-items: center;
|
||||
padding: 0 6px 0 14px;
|
||||
gap: 8px; z-index: 300;
|
||||
box-shadow: var(--shadow);
|
||||
transition: background 0.3s, border-color 0.3s;
|
||||
|
||||
/* ── Waypoint inline search ───────────────────── */
|
||||
.wl-search-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text3);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
#search-bar:focus-within { border-color: var(--accent); }
|
||||
#search-bar .sb-icon { color: var(--text3); flex-shrink: 0; width: 18px; height: 18px; }
|
||||
#search-input {
|
||||
flex: 1; background: none; border: none;
|
||||
color: var(--text); font-size: 15px; outline: none; min-width: 0;
|
||||
transition: color 0.3s;
|
||||
.wl-search-btn:hover, .wl-search-btn:active { color: var(--accent); }
|
||||
|
||||
.wl-search-panel {
|
||||
padding: 6px 8px 4px 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
#search-input::placeholder { color: var(--text3); }
|
||||
#btn-theme {
|
||||
width: auto; min-width: 38px; height: 38px;
|
||||
border-radius: 10px;
|
||||
.wl-search-input {
|
||||
width: 100%;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 3px; cursor: pointer; flex-shrink: 0;
|
||||
transition: all 0.15s; padding: 0 8px;
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
padding: 7px 10px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#btn-theme:active { transform: scale(0.94); background: var(--surface3); }
|
||||
#btn-theme svg { width: 16px; height: 16px; }
|
||||
#theme-label {
|
||||
font-size: 9px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
color: var(--text3); line-height: 1;
|
||||
.wl-search-input:focus { border-color: var(--accent); }
|
||||
.wl-search-results {
|
||||
margin-top: 4px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Search results */
|
||||
#search-results {
|
||||
position: fixed;
|
||||
top: calc(max(env(safe-area-inset-top, 0px), 12px) + 58px);
|
||||
left: 12px; right: 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px; z-index: 300;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden; display: none;
|
||||
transition: background 0.3s, border-color 0.3s;
|
||||
}
|
||||
.search-result-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
.wl-search-result-item {
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
transition: background 0.1s;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
}
|
||||
.search-result-item:last-child { border-bottom: none; }
|
||||
.search-result-item:active { background: var(--surface2); }
|
||||
.search-result-item:hover { background: var(--surface2); }
|
||||
.sri-icon { color: var(--text3); flex-shrink: 0; }
|
||||
.sri-name { font-size: 14px; font-weight: 500; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.sri-sub { font-size: 12px; color: var(--text2); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wl-search-result-item:hover, .wl-search-result-item:active {
|
||||
background: var(--surface2);
|
||||
}
|
||||
.wl-search-result-name { font-weight: 500; }
|
||||
.wl-search-result-sub { font-size: 11px; color: var(--text2); margin-top: 1px; }
|
||||
|
||||
/* ── Map Control Buttons ──────────────────────── */
|
||||
#map-controls-r {
|
||||
@@ -502,11 +484,9 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
}
|
||||
.bottom-sheet.open { transform: translateX(0); }
|
||||
.bottom-sheet.swiping { transition: none; }
|
||||
#search-bar { left: 84px; right: 12px; max-width: 400px; }
|
||||
#map-controls-r { right: 12px; bottom: 12px; }
|
||||
#sheet-backdrop { display: none; }
|
||||
#ruler-info { left: 84px; }
|
||||
#search-results { left: 84px; right: 12px; max-width: 400px; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
@@ -610,8 +590,7 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
#btn-add-waypoint:hover { border-color: var(--accent); color: var(--accent); }
|
||||
#btn-build-route { width: 100%; height: 42px; background: var(--accent); color: #fff; border: none; border-radius: 10px; font-size: 14px; font-weight: 700; cursor: pointer; margin-top: 8px; transition: background 0.15s; }
|
||||
#btn-build-route:active { background: var(--accent-h); }
|
||||
.search-result-name { font-size: 14px; font-weight: 500; color: var(--text); }
|
||||
.search-result-detail { font-size: 12px; color: var(--text2); margin-top: 2px; }
|
||||
|
||||
|
||||
/* ── Mini Route Bar ───────────────────────── */
|
||||
#sheet-route-mini {
|
||||
|
||||
75
tasks/enduro-trails/prototype/static/app.js
vendored
75
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -627,10 +627,17 @@ async function renderWaypointsList() {
|
||||
<span class="wl-label" id="wl-label-${i}">${coordText}</span>
|
||||
${distStr ? `<span class="wl-dist">${distStr}</span>` : ''}
|
||||
</div>
|
||||
<button class="wl-search-btn" onclick="openWaypointSearch(${i})" title="Поиск">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
</button>
|
||||
<div class="wl-drag-handle" data-idx="${i}">${gripSvg}</div>
|
||||
<button class="wl-remove" onclick="removeWaypoint(${i})" title="Удалить">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wl-search-panel" id="wl-search-panel-${i}" style="display:none">
|
||||
<input class="wl-search-input" id="wl-search-input-${i}" type="text" placeholder="Поиск места..." autocomplete="off" autocorrect="off">
|
||||
<div class="wl-search-results" id="wl-search-results-${i}"></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
@@ -1175,7 +1182,6 @@ async function initMap() {
|
||||
checkDataAvailability();
|
||||
initRouteClicks(map);
|
||||
initRulerClicks(map);
|
||||
initSearch();
|
||||
renderMarkers();
|
||||
// Apply theme on load
|
||||
applyTheme();
|
||||
@@ -1332,6 +1338,73 @@ function initRouteClicks(map) {
|
||||
// ─── Поиск (Nominatim) ─────────────────────────────────────────────
|
||||
let searchTimeout = null;
|
||||
|
||||
// ─── Waypoint inline search ────────────────────────────────────────
|
||||
let wpSearchTimeout = null;
|
||||
|
||||
function openWaypointSearch(idx) {
|
||||
// Close all other open panels
|
||||
document.querySelectorAll('.wl-search-panel').forEach(p => {
|
||||
if (p.id !== `wl-search-panel-${idx}`) p.style.display = 'none';
|
||||
});
|
||||
const panel = document.getElementById(`wl-search-panel-${idx}`);
|
||||
if (!panel) return;
|
||||
const isOpen = panel.style.display !== 'none';
|
||||
panel.style.display = isOpen ? 'none' : 'block';
|
||||
if (!isOpen) {
|
||||
const input = document.getElementById(`wl-search-input-${idx}`);
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.focus();
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(wpSearchTimeout);
|
||||
const q = input.value.trim();
|
||||
if (q.length < 2) {
|
||||
document.getElementById(`wl-search-results-${idx}`).innerHTML = '';
|
||||
return;
|
||||
}
|
||||
wpSearchTimeout = setTimeout(() => doWaypointSearch(idx, q), 400);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function doWaypointSearch(idx, query) {
|
||||
const resultsEl = document.getElementById(`wl-search-results-${idx}`);
|
||||
if (!resultsEl) return;
|
||||
resultsEl.innerHTML = '<div class="wl-search-result-item"><span class="wl-search-result-name" style="color:var(--text3)">Поиск...</span></div>';
|
||||
try {
|
||||
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=5&countrycodes=ru&accept-language=ru`;
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.json();
|
||||
if (!data.length) {
|
||||
resultsEl.innerHTML = '<div class="wl-search-result-item"><span class="wl-search-result-name" style="color:var(--text3)">Ничего не найдено</span></div>';
|
||||
return;
|
||||
}
|
||||
resultsEl.innerHTML = data.map(item => {
|
||||
const parts = (item.display_name || '').split(', ');
|
||||
const name = parts[0];
|
||||
const sub = parts.slice(1, 3).join(', ');
|
||||
return `<div class="wl-search-result-item" onclick="selectWaypointSearchResult(${idx}, ${item.lat}, ${item.lon}, '${name.replace(/'/g, "\\'")}')">
|
||||
<div class="wl-search-result-name">${name}</div>
|
||||
${sub ? `<div class="wl-search-result-sub">${sub}</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch(e) {
|
||||
resultsEl.innerHTML = '<div class="wl-search-result-item"><span class="wl-search-result-name" style="color:var(--red)">Ошибка поиска</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function selectWaypointSearchResult(idx, lat, lon, name) {
|
||||
routeWaypoints[idx] = { lat: parseFloat(lat), lon: parseFloat(lon) };
|
||||
const panel = document.getElementById(`wl-search-panel-${idx}`);
|
||||
if (panel) panel.style.display = 'none';
|
||||
rebuildWaypointMarkers();
|
||||
renderWaypointsList();
|
||||
window._map.flyTo({ center: [parseFloat(lon), parseFloat(lat)], zoom: 13, duration: 600 });
|
||||
if (routeWaypoints.length >= 2) debounceBuildRoute();
|
||||
updateMiniRouteCard();
|
||||
}
|
||||
|
||||
function initSearch() {
|
||||
const input = document.getElementById('search-input');
|
||||
const results = document.getElementById('search-results');
|
||||
|
||||
@@ -18,17 +18,7 @@
|
||||
<!-- Sheet backdrop -->
|
||||
<div id="sheet-backdrop" onclick="closeAllSheets()"></div>
|
||||
|
||||
<!-- ── Search Bar ─────────────────────────── -->
|
||||
<div id="search-bar">
|
||||
<svg class="sb-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<input type="text" id="search-input" placeholder="Поиск места..." autocomplete="off" autocorrect="off">
|
||||
<button id="btn-theme" onclick="toggleTheme()" title="Переключить тему">
|
||||
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
|
||||
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
||||
<span id="theme-label">Авто</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-results"></div>
|
||||
|
||||
|
||||
<!-- ── Ruler info ─────────────────────────── -->
|
||||
<div id="ruler-info">
|
||||
@@ -48,6 +38,10 @@
|
||||
<button class="map-btn" onclick="locateMe()" title="Моё местоположение">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg>
|
||||
</button>
|
||||
<button class="map-btn" id="btn-theme" onclick="toggleTheme()" title="Переключить тему">
|
||||
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
|
||||
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user