auto-sync: 2026-05-06 01:00:01
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# BRD: Enduro Trails — Поиск точек маршрута
|
||||
|
||||
**Версия:** 1.0
|
||||
**Версия:** 1.1
|
||||
**Дата:** 2026-05-05
|
||||
**Автор:** Стрим 🌊
|
||||
**Статус:** 🔄 В разработке
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
## 2. Цель
|
||||
|
||||
Убрать верхний search bar. Добавить поиск прямо в список точек маршрута — inline, для каждой точки отдельно.
|
||||
Убрать верхний search bar. Добавить поиск прямо в список точек маршрута — inline, для каждой точки отдельно. При первом открытии режима маршрута — онбординг с полями ввода старта и финиша.
|
||||
|
||||
---
|
||||
|
||||
@@ -30,9 +30,36 @@
|
||||
- Удалить вызов `initSearch()` из JS
|
||||
- Кнопку темы (`#btn-theme`) перенести в `#map-controls-r` (рядом с компасом и геолокацией)
|
||||
|
||||
**Почему:** bar занимал ~52px сверху, перекрывал карту, дублировал функционал который теперь встроен в waypoints.
|
||||
### 3.2 Онбординг при активации режима маршрута
|
||||
|
||||
### 3.2 Inline поиск в каждой точке маршрута
|
||||
Когда пользователь тапает «Маршрут» в toolbar и точек ещё нет — показывать вместо пустого списка:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ 🔍 Откуда? │ ← поле поиска старта
|
||||
│ или тапни на карте │ ← подсказка
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
После выбора старта автоматически появляется:
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ S Название старта │ ← добавленная точка
|
||||
├─────────────────────────────────┤
|
||||
│ 🔍 Куда? │ ← поле поиска финиша
|
||||
│ или тапни на карте │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
После выбора финиша — поля исчезают, маршрут строится, показывается обычный `wl-list`.
|
||||
|
||||
**Детали:**
|
||||
- Поля поиска используют тот же Nominatim что и inline поиск в wl-item
|
||||
- Подсказка «или тапни на карте» — мелкий текст `var(--text3)` под полем
|
||||
- Тап на карте тоже работает — добавляет точку и переходит к следующему полю
|
||||
- Если точки уже есть — онбординг не показывается, сразу wl-list
|
||||
|
||||
### 3.3 Inline поиск в каждой точке маршрута
|
||||
|
||||
В каждом `wl-item` добавить кнопку-лупу. Тап → разворачивается inline панель поиска прямо под точкой.
|
||||
|
||||
@@ -77,7 +104,7 @@
|
||||
| Аспект | Решение |
|
||||
|--------|---------|
|
||||
| Поиск API | Nominatim `/search` (уже используется в проекте) |
|
||||
| Debounce | 400ms (как в старом search bar) |
|
||||
| Debounce | 400ms |
|
||||
| Состояние | `wpSearchTimeout` — глобальная переменная для debounce |
|
||||
| Закрытие панели | Тап на лупу повторно — закрывает; открытие другой — закрывает текущую |
|
||||
| После выбора | `routeWaypoints[idx]` = `{lat, lon}`, rebuild + route |
|
||||
@@ -85,12 +112,29 @@
|
||||
|
||||
---
|
||||
|
||||
## 6. Definition of Done
|
||||
## 6. Баги после редизайна (требуют исправления)
|
||||
|
||||
| Баг | Описание | Вероятная причина |
|
||||
|-----|---------|------------------|
|
||||
| Метки в верхнем левом углу | Попап метки отображается не на позиции маркера, а в углу экрана | CSS `position` маркера сбивает позиционирование MapLibre |
|
||||
| Метки линейки в верхнем углу | Попап точки линейки отображается не на треке, а в углу | Та же причина — CSS позиционирование маркера |
|
||||
| Подсказка «Тапни на карте» пропала | `#route-status` не виден когда точек нет | Скрыт или перекрыт после редизайна |
|
||||
|
||||
**Вероятная причина багов с маркерами:** в редизайне изменились глобальные CSS стили для `position`, `transform` или `z-index` — это сбивает позиционирование MapLibre маркеров. Маркер MapLibre должен иметь `position: absolute` без переопределения в глобальных стилях.
|
||||
|
||||
**Фикс:** проверить CSS для `.maplibregl-marker`, `.marker-flag`, `.ruler-marker` — убрать любой `position: fixed/relative/static` который может переопределять поведение маркера.
|
||||
|
||||
---
|
||||
|
||||
## 7. Definition of Done
|
||||
|
||||
| Критерий | Статус |
|
||||
|----------|--------|
|
||||
| Верхний search bar отсутствует | ⬜ |
|
||||
| Кнопка темы работает в map-controls-r | ⬜ |
|
||||
| При активации маршрута без точек — поле поиска старта | ⬜ |
|
||||
| После старта — поле поиска финиша | ⬜ |
|
||||
| Подсказка «или тапни на карте» видна | ⬜ |
|
||||
| Иконка лупы в каждом wl-item | ⬜ |
|
||||
| Тап на лупу → inline поиск открывается | ⬜ |
|
||||
| Поиск находит места через Nominatim | ⬜ |
|
||||
@@ -98,37 +142,56 @@
|
||||
| Маршрут перестраивается после выбора | ⬜ |
|
||||
| Карта летит к выбранному месту | ⬜ |
|
||||
| Одновременно открыта только одна панель | ⬜ |
|
||||
| Метки отображаются на позиции маркера (не в углу) | ⬜ |
|
||||
| Метки линейки отображаются на треке | ⬜ |
|
||||
| Деплой + health check OK | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 7. Файлы
|
||||
## 8. Файлы
|
||||
|
||||
| Файл | Изменения |
|
||||
|------|-----------|
|
||||
| `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()` |
|
||||
| `prototype/static/app.css` | Удалить стили search bar; добавить `.wl-search-*`; исправить позиционирование маркеров |
|
||||
| `prototype/static/app.js` | Удалить `initSearch()`; добавить онбординг, `openWaypointSearch()`, `doWaypointSearch()`, `selectWaypointSearchResult()` |
|
||||
|
||||
---
|
||||
|
||||
## 8. Дополнительные требования (05.05.2026)
|
||||
---
|
||||
|
||||
### 8.1 Онбординг при активации режима маршрута
|
||||
## 9. Дополнительные замечания (05.05.2026 — пакет 2)
|
||||
|
||||
Когда пользователь тапает кнопку «Маршрут» в toolbar и точек ещё нет — показывать:
|
||||
| # | Замечание | Детали |
|
||||
|---|-----------|--------|
|
||||
| 1 | Иконка колеса | Заменить на нормальное мотокросс колесо |
|
||||
| 2 | Флажок финиша | Чёрно-белая шахматная раскраска как финишный флаг |
|
||||
| 3 | Спиннер в основном листе | Вращающееся колесо пока строится маршрут (как в мини-баре) |
|
||||
| 4 | Тёмная карта | При тёмной теме — тёмный стиль карты, при светлой — светлый |
|
||||
| 5 | Автозум на маршрут | Плавный `fitBounds` после построения маршрута, маршрут по центру без выхода за границы |
|
||||
| 6 | Мини-бар перекрывает кнопки справа | Исправить z-index/позиционирование `#sheet-route-mini` |
|
||||
|
||||
1. **Поле ввода/поиска для старта** с placeholder «Откуда? Введи название или тапни на карте»
|
||||
2. После выбора старта — **поле ввода/поиска для финиша** с placeholder «Куда? Введи название или тапни на карте»
|
||||
3. Подсказка под полем: «или тапни на карте» (мелкий текст, `var(--text3)`)
|
||||
### Детали по пунктам
|
||||
|
||||
**UX flow:**
|
||||
- Открылся sheet маршрута, точек нет → показать поле старта
|
||||
- Пользователь вводит → Nominatim поиск → выбирает → точка S добавлена
|
||||
- Автоматически появляется поле финиша
|
||||
- Пользователь вводит или тапает на карте → точка F добавлена → маршрут строится
|
||||
- После добавления обеих точек — поля исчезают, показывается обычный `wl-list`
|
||||
**П.1 Иконка колеса:**
|
||||
Заменить текущую SVG на нормальное мотокросс колесо — спицы, кноблинг, центральная втулка. Использовать Lucide `bike` или нарисовать SVG колеса с спицами.
|
||||
|
||||
### 8.2 Подсказка «Тапни на карте»
|
||||
**П.2 Флажок финиша:**
|
||||
В `waypointPinSvg('F', ...)` для финишной точки использовать шахматный узор (SVG `pattern` чёрные/белые клетки) вместо красного цвета.
|
||||
|
||||
`#route-status` с текстом «Тапни точку старта на карте» пропал после редизайна — восстановить видимость. Должен показываться когда точек нет или когда активен режим добавления точки (`addingWaypoint = true`).
|
||||
**П.3 Спиннер в основном листе:**
|
||||
В `#route-cards` показывать skeleton/спиннер пока идёт запрос к OSRM. Тот же спиннер что в мини-баре.
|
||||
|
||||
**П.4 Тёмная карта:**
|
||||
`map.setStyle()` при смене темы. Тёмный стиль: `/style-dark.json` (или текущий тёмный), светлый: `/style-light.json` (или текущий светлый). После `setStyle` пересоздать слои маршрутов через `map.on('style.load', onMapStyleLoad)`.
|
||||
|
||||
**П.5 Автозум:**
|
||||
После `drawRouteResults()` вызывать:
|
||||
```js
|
||||
const coords = routeResults[0].geometry.coordinates;
|
||||
const bounds = coords.reduce((b, c) => b.extend(c), new maplibregl.LngLatBounds(coords[0], coords[0]));
|
||||
map.fitBounds(bounds, { padding: 60, duration: 1000, maxZoom: 14 });
|
||||
```
|
||||
|
||||
**П.6 Мини-бар:**
|
||||
`#sheet-route-mini` перекрывает `#map-controls-r`. Исправить `right` или `width` мини-бара чтобы не залезал на кнопки справа.
|
||||
|
||||
@@ -305,7 +305,7 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
.wl-add .wl-label { color: var(--text3); }
|
||||
|
||||
/* ── Route Status ─────────────────────────────── */
|
||||
#route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; }
|
||||
#route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; min-height: 20px; }
|
||||
|
||||
/* ── Route Cards ──────────────────────────────── */
|
||||
#route-cards, #link-cards, #scenic-cards { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
|
||||
@@ -451,6 +451,11 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
}
|
||||
#ruler-info.visible { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* ── Fix: MapLibre markers must stay absolute ────── */
|
||||
.maplibregl-marker {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
/* ── Waypoint Markers ─────────────────────────── */
|
||||
.route-waypoint-marker { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4)); width: 28px; height: 36px; cursor: grab; display: block; }
|
||||
.route-waypoint-marker:active { cursor: grabbing; }
|
||||
@@ -509,6 +514,35 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
}
|
||||
.marker-anim { animation: markerPopIn 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28) both; }
|
||||
|
||||
/* ── Onboarding (empty waypoints state) ─────────── */
|
||||
.wl-onboarding {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.wl-onboard-field {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px 12px 8px 0;
|
||||
}
|
||||
.wl-onboard-input {
|
||||
flex: 1;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.wl-onboard-input:focus { border-color: var(--accent); }
|
||||
.wl-onboard-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text3);
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
/* ── Misc ────────────────────────────────────── */
|
||||
.text-accent { color: var(--accent); }
|
||||
.text-gold { color: var(--gold); }
|
||||
@@ -595,7 +629,7 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
/* ── Mini Route Bar ───────────────────────── */
|
||||
#sheet-route-mini {
|
||||
position: fixed;
|
||||
bottom: 72px; left: 0; right: 0;
|
||||
bottom: 72px; left: 0; right: 56px;
|
||||
height: 64px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
@@ -646,6 +680,23 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
#sheet-route-mini { left: 72px; width: 380px; right: auto; border-radius: 0 14px 0 0; }
|
||||
}
|
||||
|
||||
/* ── Route Loading Spinner ───────────────────── */
|
||||
.route-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
.route-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
/* ── Moto Wheel Loading Indicator ────────────── */
|
||||
.moto-wheel {
|
||||
width: 32px; height: 32px;
|
||||
|
||||
151
tasks/enduro-trails/prototype/static/app.js
vendored
151
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -528,6 +528,27 @@ async function reverseGeocode(lat, lon) {
|
||||
|
||||
function waypointPinSvg(label, color) {
|
||||
const fs = label.length > 1 ? '7' : '9';
|
||||
|
||||
// Finish flag — checkered pattern
|
||||
if (label === 'F') {
|
||||
const uid = Math.random().toString(36).slice(2);
|
||||
return `<svg width="20" height="26" viewBox="0 0 28 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="checker-${uid}" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
|
||||
<rect width="4" height="4" fill="black"/>
|
||||
<rect x="4" y="0" width="4" height="4" fill="white"/>
|
||||
<rect x="0" y="4" width="4" height="4" fill="white"/>
|
||||
<rect x="4" y="4" width="4" height="4" fill="black"/>
|
||||
</pattern>
|
||||
<clipPath id="pin-clip-${uid}">
|
||||
<path d="M14 1C6.82 1 1 6.82 1 14C1 24 14 35 14 35C14 35 27 24 27 14C27 6.82 21.18 1 14 1Z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path d="M14 1C6.82 1 1 6.82 1 14C1 24 14 35 14 35C14 35 27 24 27 14C27 6.82 21.18 1 14 1Z" fill="url(#checker-${uid})" stroke="white" stroke-width="1.5"/>
|
||||
<text x="14" y="19" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="9" font-weight="700" fill="white" stroke="black" stroke-width="0.5">${label}</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
return `<svg width="20" height="26" viewBox="0 0 28 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 1C6.82 1 1 6.82 1 14C1 24 14 35 14 35C14 35 27 24 27 14C27 6.82 21.18 1 14 1Z" fill="${color}" stroke="white" stroke-width="1.5"/>
|
||||
<text x="14" y="19" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="${fs}" font-weight="700" fill="white">${label}</text>
|
||||
@@ -605,7 +626,29 @@ function getRouteSegmentDistances() {
|
||||
|
||||
async function renderWaypointsList() {
|
||||
const list = document.getElementById('waypoints-list');
|
||||
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
|
||||
|
||||
// ── Onboarding: no waypoints yet ──────────────────────────────
|
||||
if (!routeWaypoints.length) {
|
||||
list.innerHTML = `
|
||||
<div class="wl-onboarding">
|
||||
<div class="wl-onboard-field" id="wl-onboard-start">
|
||||
<div class="wl-pin">${waypointPinSvg('S', '#2EA043')}</div>
|
||||
<div style="flex:1">
|
||||
<input class="wl-onboard-input" id="wl-onboard-input-start"
|
||||
type="text" placeholder="Откуда? Введи название..."
|
||||
autocomplete="off" autocorrect="off">
|
||||
<div class="wl-search-results" id="wl-onboard-results-start"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-onboard-hint">или тапни на карте</div>
|
||||
</div>`;
|
||||
_initOnboardSearch('start');
|
||||
_initWaypointDragHandles(list);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Onboarding: only start added, need finish ──────────────────
|
||||
// (handled below after normal list render)
|
||||
|
||||
const gripSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>`;
|
||||
|
||||
@@ -649,8 +692,28 @@ async function renderWaypointsList() {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Onboarding finish field: only start added, no finish yet
|
||||
if (routeWaypoints.length === 1) {
|
||||
html += `
|
||||
<div class="wl-onboard-field" id="wl-onboard-finish">
|
||||
<div class="wl-pin">${waypointPinSvg('F', '#FF3B1F')}</div>
|
||||
<div style="flex:1">
|
||||
<input class="wl-onboard-input" id="wl-onboard-input-finish"
|
||||
type="text" placeholder="Куда? Введи название..."
|
||||
autocomplete="off" autocorrect="off">
|
||||
<div class="wl-search-results" id="wl-onboard-results-finish"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-onboard-hint">или тапни на карте</div>`;
|
||||
}
|
||||
|
||||
list.innerHTML = html;
|
||||
|
||||
// Init finish onboard search if only 1 waypoint
|
||||
if (routeWaypoints.length === 1) {
|
||||
_initOnboardSearch('finish');
|
||||
}
|
||||
|
||||
// Async geocode
|
||||
routeWaypoints.forEach(async (wp, i) => {
|
||||
const name = await reverseGeocode(wp.lat, wp.lon);
|
||||
@@ -662,6 +725,64 @@ async function renderWaypointsList() {
|
||||
_initWaypointDragHandles(list);
|
||||
}
|
||||
|
||||
// ─── Onboard search helpers ────────────────────────────────────────
|
||||
function _initOnboardSearch(type) {
|
||||
const input = document.getElementById(`wl-onboard-input-${type}`);
|
||||
const resultsEl = document.getElementById(`wl-onboard-results-${type}`);
|
||||
if (!input) return;
|
||||
|
||||
let timeout = null;
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(timeout);
|
||||
const q = input.value.trim();
|
||||
if (q.length < 2) { resultsEl.innerHTML = ''; return; }
|
||||
timeout = setTimeout(() => _doOnboardSearch(type, q, resultsEl), 400);
|
||||
});
|
||||
|
||||
// Autofocus only for start field (finish field appears inline)
|
||||
if (type === 'start') {
|
||||
setTimeout(() => input.focus(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
async function _doOnboardSearch(type, query, resultsEl) {
|
||||
resultsEl.innerHTML = '<div class="wl-search-result-item"><span 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 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="_selectOnboardResult('${type}', ${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 style="color:var(--red)">Ошибка</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function _selectOnboardResult(type, lat, lon, name) {
|
||||
const wp = { lat: parseFloat(lat), lon: parseFloat(lon) };
|
||||
if (type === 'start') {
|
||||
routeWaypoints.unshift(wp);
|
||||
} else {
|
||||
routeWaypoints.push(wp);
|
||||
}
|
||||
rebuildWaypointMarkers();
|
||||
renderWaypointsList();
|
||||
window._map.flyTo({ center: [parseFloat(lon), parseFloat(lat)], zoom: 12, duration: 600 });
|
||||
if (routeWaypoints.length >= 2) debounceBuildRoute();
|
||||
updateMiniRouteCard();
|
||||
}
|
||||
|
||||
function _initWaypointDragHandles(list) {
|
||||
let dragIdx = -1;
|
||||
let startY = 0;
|
||||
@@ -817,6 +938,7 @@ async function buildRoute() {
|
||||
|
||||
// Show mini-bar with spinning wheel
|
||||
showMiniRouteLoading();
|
||||
showRouteLoading();
|
||||
// Close main sheet if open
|
||||
closeSheet('sheet-route');
|
||||
|
||||
@@ -905,6 +1027,23 @@ function drawRouteResults(routes, activeIdx) {
|
||||
|
||||
renderRouteCards(routes);
|
||||
|
||||
// Auto-zoom to active route after drawing
|
||||
const activeRoute = routes[activeIdx] || routes[0];
|
||||
if (activeRoute && activeRoute.geometry && activeRoute.geometry.coordinates) {
|
||||
const coords = activeRoute.geometry.coordinates;
|
||||
if (coords.length > 1) {
|
||||
const bounds = coords.reduce(
|
||||
(b, c) => b.extend(c),
|
||||
new maplibregl.LngLatBounds(coords[0], coords[0])
|
||||
);
|
||||
map.fitBounds(bounds, {
|
||||
padding: { top: 80, bottom: 160, left: 20, right: 20 },
|
||||
duration: 1200,
|
||||
maxZoom: 14
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update mini sheet if visible
|
||||
const miniEl = document.getElementById('sheet-route-mini');
|
||||
if (miniEl && miniEl.classList.contains('visible')) showMiniRouteSheet();
|
||||
@@ -2022,7 +2161,15 @@ function selectMiniRoute(idx) {
|
||||
renderWaypointsList();
|
||||
}
|
||||
|
||||
// ─── Mini Route Loading Indicator ─────────────────────────────────
|
||||
// ─── Route Loading Indicators ────────────────────────────────────
|
||||
function showRouteLoading() {
|
||||
const el = document.getElementById('route-cards');
|
||||
if (el) el.innerHTML = `<div class="route-loading">
|
||||
<div class="route-spinner"></div>
|
||||
<span style="color:var(--text3);font-size:13px">Строю маршрут...</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function showMiniRouteLoading() {
|
||||
const wheel = document.getElementById('mini-wheel');
|
||||
const statsEl = document.getElementById('mini-stats');
|
||||
|
||||
@@ -228,22 +228,20 @@
|
||||
<div class="mini-handle" id="mini-route-handle"></div>
|
||||
<div class="mini-route-info">
|
||||
<!-- Moto wheel loading indicator -->
|
||||
<svg id="mini-wheel" class="moto-wheel" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Rim -->
|
||||
<circle cx="20" cy="20" r="17" fill="none" stroke="var(--accent)" stroke-width="2.5"/>
|
||||
<!-- Tyre (outer with knobby tread) -->
|
||||
<circle cx="20" cy="20" r="19" fill="none" stroke="var(--text2)" stroke-width="1.5" stroke-dasharray="3 2"/>
|
||||
<!-- Hub -->
|
||||
<circle cx="20" cy="20" r="3" fill="var(--accent)"/>
|
||||
<!-- Spokes (8) -->
|
||||
<line x1="20" y1="3" x2="20" y2="17" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="20" y1="23" x2="20" y2="37" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="20" x2="17" y2="20" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="23" y1="20" x2="37" y2="20" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="7.9" y1="7.9" x2="17.5" y2="17.5" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="22.5" y1="22.5" x2="32.1" y2="32.1" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="32.1" y1="7.9" x2="22.5" y2="17.5" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<line x1="17.5" y1="22.5" x2="7.9" y2="32.1" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<svg id="mini-wheel" class="moto-wheel" width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Обод -->
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<!-- Втулка -->
|
||||
<circle cx="12" cy="12" r="2"/>
|
||||
<!-- Спицы (8 штук) -->
|
||||
<line x1="12" y1="2" x2="12" y2="10"/>
|
||||
<line x1="12" y1="14" x2="12" y2="22"/>
|
||||
<line x1="2" y1="12" x2="10" y2="12"/>
|
||||
<line x1="14" y1="12" x2="22" y2="12"/>
|
||||
<line x1="4.93" y1="4.93" x2="10.59" y2="10.59"/>
|
||||
<line x1="13.41" y1="13.41" x2="19.07" y2="19.07"/>
|
||||
<line x1="19.07" y1="4.93" x2="13.41" y2="10.59"/>
|
||||
<line x1="10.59" y1="13.41" x2="4.93" y2="19.07"/>
|
||||
</svg>
|
||||
<div class="mini-route-dot" id="mini-dot"></div>
|
||||
<div class="mini-route-text">
|
||||
|
||||
Reference in New Issue
Block a user