auto-sync: 2026-05-05 18:10:01
This commit is contained in:
289
tasks/enduro-trails/DEV_TASK_PHASE5_MARKERS.md
Normal file
289
tasks/enduro-trails/DEV_TASK_PHASE5_MARKERS.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# Dev Task: Enduro Trails — Маркеры и редизайн панели маршрута
|
||||
|
||||
**Файлы:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/`
|
||||
**Деплой:** `node /tmp/deploy_static.js`
|
||||
**Бэкенд не трогать.**
|
||||
|
||||
---
|
||||
|
||||
## Часть 1: Маркеры точек на карте
|
||||
|
||||
### Текущее состояние
|
||||
- Старт: зелёный круг «A»
|
||||
- Финиш: красный круг «B»
|
||||
- Промежуточные: белый круг с синей рамкой, цифра
|
||||
|
||||
### Новый дизайн маркеров
|
||||
|
||||
**Старт (index === 0):** флаг-пин зелёный с буквой «S»
|
||||
**Финиш (index === total-1):** флаг-пин красный с буквой «F»
|
||||
**Промежуточные:** синий пин с цифрой 1, 2, 3...
|
||||
|
||||
Форма пина (капля/слеза, как у Google Maps):
|
||||
```
|
||||
╭───╮
|
||||
│ S │
|
||||
╰─┬─╯
|
||||
│
|
||||
```
|
||||
|
||||
Реализация через SVG в innerHTML маркера:
|
||||
|
||||
```js
|
||||
function createWaypointMarkerEl(index, total) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'route-waypoint-marker marker-anim';
|
||||
el.style.cssText = 'cursor: grab; width: 28px; height: 36px; position: relative;';
|
||||
|
||||
let bg, label;
|
||||
if (index === 0) {
|
||||
bg = '#2EA043'; label = 'S';
|
||||
} else if (index === total - 1) {
|
||||
bg = '#FF3B1F'; label = 'F';
|
||||
} else {
|
||||
bg = '#0066ff'; label = String(index);
|
||||
}
|
||||
|
||||
el.innerHTML = `<svg width="28" height="36" viewBox="0 0 28 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 0C6.268 0 0 6.268 0 14C0 24.5 14 36 14 36C14 36 28 24.5 28 14C28 6.268 21.732 0 14 0Z" fill="${bg}"/>
|
||||
<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="${bg}" stroke="white" stroke-width="1.5"/>
|
||||
<text x="14" y="19" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="${label.length > 1 ? '9' : '11'}" font-weight="700" fill="white">${label}</text>
|
||||
</svg>`;
|
||||
|
||||
return el;
|
||||
}
|
||||
```
|
||||
|
||||
CSS для маркера (добавить в app.css):
|
||||
```css
|
||||
.route-waypoint-marker { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4)); }
|
||||
.route-waypoint-marker:active { cursor: grabbing; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Часть 2: Список точек в панели — редизайн
|
||||
|
||||
### Текущее состояние
|
||||
```
|
||||
● 55.723, 37.612 [×]
|
||||
● 56.123, 38.234 [×]
|
||||
```
|
||||
|
||||
### Новый дизайн
|
||||
```
|
||||
[S] Москва, Тверская ул. [×]
|
||||
[1] Клин [×]
|
||||
[F] Тверь, центр [×]
|
||||
```
|
||||
|
||||
Иконка слева — мини-версия пина (SVG, 20×26px), цвет соответствует маркеру на карте.
|
||||
|
||||
```js
|
||||
function waypointPinSvg(label, color) {
|
||||
const fs = label.length > 1 ? '7' : '9';
|
||||
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>
|
||||
</svg>`;
|
||||
}
|
||||
```
|
||||
|
||||
Изменить `renderWaypointsList()`:
|
||||
```js
|
||||
async function renderWaypointsList() {
|
||||
const list = document.getElementById('waypoints-list');
|
||||
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
|
||||
|
||||
list.innerHTML = routeWaypoints.map((wp, i) => {
|
||||
const isStart = i === 0;
|
||||
const isEnd = i === routeWaypoints.length - 1;
|
||||
const label = isStart ? 'S' : isEnd ? 'F' : String(i);
|
||||
const color = isStart ? '#2EA043' : isEnd ? '#FF3B1F' : '#0066ff';
|
||||
const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`;
|
||||
return `<div class="wl-item" id="wl-item-${i}">
|
||||
<div class="wl-pin">${waypointPinSvg(label, color)}</div>
|
||||
<span class="wl-label" id="wl-label-${i}">${coordText}</span>
|
||||
<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>`;
|
||||
}).join('');
|
||||
|
||||
// Async geocode
|
||||
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;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
CSS для списка точек (обновить):
|
||||
```css
|
||||
.wl-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.wl-item:last-child { border-bottom: none; }
|
||||
.wl-pin { flex-shrink: 0; display: flex; align-items: center; }
|
||||
.wl-label {
|
||||
flex: 1; font-size: 13px; color: var(--text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
.wl-remove {
|
||||
width: 28px; height: 28px; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: none; border: none; color: var(--text3);
|
||||
cursor: pointer; border-radius: 6px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.wl-remove:active { background: var(--red-bg); color: var(--red); }
|
||||
.wl-remove svg { width: 14px; height: 14px; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Часть 3: Карточки маршрутов — редизайн
|
||||
|
||||
### Новый дизайн карточки
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ● Вариант 1 1013 км 14ч │ ← header
|
||||
│ ████████████████░░░░ 82% грунт │ ← полоска + подпись
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Полоска грунт/асфальт — высота 6px, скруглённая, без отдельных pill-бейджей
|
||||
- Под полоской: «82% грунт · 18% асфальт» одной строкой мелким текстом
|
||||
- Активная карточка: оранжевый левый бордер (4px) вместо полного бордера
|
||||
- Компактнее: padding 10px 12px вместо 12px 14px
|
||||
|
||||
```js
|
||||
function renderRouteCards(routes) {
|
||||
const container = document.getElementById('route-cards');
|
||||
container.innerHTML = routes.map((route, i) => {
|
||||
const color = ROUTE_COLORS[i] || '#888888';
|
||||
const distKm = (route.distance_m / 1000).toFixed(0);
|
||||
const timeStr = formatDuration(route.duration_s);
|
||||
const isActive = i === activeRouteIdx;
|
||||
const s = route.stats || {};
|
||||
const dirtPct = s.dirt_total_pct || 0;
|
||||
const asphPct = s.asphalt_pct || 0;
|
||||
|
||||
return `<div class="route-card${isActive ? ' active' : ''}" onclick="selectRoute(${i})">
|
||||
<div class="rc-header">
|
||||
<span class="rc-dot" style="background:${color}"></span>
|
||||
<span class="rc-title">Вариант ${i + 1}</span>
|
||||
<span class="rc-meta">${distKm} км · ${timeStr}</span>
|
||||
</div>
|
||||
<div class="rc-bar-wrap">
|
||||
<div class="rc-bar">
|
||||
<div class="rc-bar-dirt" style="width:${dirtPct}%"></div>
|
||||
<div class="rc-bar-asphalt" style="width:${asphPct}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rc-bar-label">${dirtPct}% грунт${asphPct ? ` · ${asphPct}% асфальт` : ''}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
```
|
||||
|
||||
CSS карточек (обновить/добавить):
|
||||
```css
|
||||
.route-card {
|
||||
background: var(--surface2);
|
||||
border: 1.5px solid var(--border);
|
||||
border-left: 4px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.route-card:active { background: var(--surface3, var(--surface2)); }
|
||||
.route-card.active {
|
||||
border-color: var(--border);
|
||||
border-left-color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
}
|
||||
.rc-header {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.rc-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.rc-title { font-size: 13px; font-weight: 700; color: var(--text); flex: 1; }
|
||||
.rc-meta { font-size: 12px; color: var(--text2); white-space: nowrap; }
|
||||
.rc-bar-wrap { margin-bottom: 4px; }
|
||||
.rc-bar {
|
||||
height: 6px; border-radius: 3px;
|
||||
background: var(--border);
|
||||
display: flex; overflow: hidden;
|
||||
}
|
||||
.rc-bar-dirt { background: var(--gold); border-radius: 3px 0 0 3px; }
|
||||
.rc-bar-asphalt { background: var(--text3); }
|
||||
.rc-bar-label { font-size: 11px; color: var(--text2); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Часть 4: Кнопки действий — компактнее
|
||||
|
||||
Текущие кнопки «+ Точка», «GPX», «Сброс» — сделать компактнее, иконки без текста на мобиле (или иконка + короткий текст).
|
||||
|
||||
```html
|
||||
<div class="route-actions" id="route-actions" style="display:none">
|
||||
<button class="btn-action" onclick="addWaypointMode()" title="Добавить точку">
|
||||
<svg><!-- plus --></svg>
|
||||
<span>Точка</span>
|
||||
</button>
|
||||
<button class="btn-action primary" onclick="downloadGPX()" title="Скачать GPX">
|
||||
<svg><!-- download --></svg>
|
||||
<span>GPX</span>
|
||||
</button>
|
||||
<button class="btn-action danger" onclick="clearRoute()" title="Сбросить маршрут">
|
||||
<svg><!-- trash --></svg>
|
||||
<span>Сброс</span>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
CSS:
|
||||
```css
|
||||
.route-actions {
|
||||
display: flex; gap: 6px; margin: 8px 0;
|
||||
}
|
||||
.btn-action {
|
||||
flex: 1; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center; gap: 5px;
|
||||
background: var(--surface2); border: 1px solid var(--border);
|
||||
border-radius: 10px; color: var(--text2);
|
||||
font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-action svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
.btn-action:active { background: var(--surface3, var(--border)); }
|
||||
.btn-action.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.btn-action.primary:active { opacity: 0.85; }
|
||||
.btn-action.danger:active { background: var(--red-bg); color: var(--red); border-color: var(--red); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Порядок реализации
|
||||
1. Часть 1: новые SVG-маркеры (createWaypointMarkerEl)
|
||||
2. Часть 2: waypointPinSvg + renderWaypointsList + CSS
|
||||
3. Часть 3: renderRouteCards + CSS карточек
|
||||
4. Часть 4: CSS кнопок действий
|
||||
5. Деплой + проверка
|
||||
|
||||
## Проверка
|
||||
1. Поставить старт → зелёный пин «S» на карте
|
||||
2. Поставить финиш → красный пин «F»
|
||||
3. Добавить промежуточную → синий пин «1»
|
||||
4. В списке точек: иконки пинов + названия мест
|
||||
5. Карточки маршрутов: компактные, полоска, левый оранжевый бордер у активной
|
||||
@@ -261,22 +261,47 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
.wp-add:active { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* ── Waypoints List ───────────────────────────── */
|
||||
#waypoints-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
|
||||
.wl-item { display: flex; align-items: center; gap: 8px; background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; }
|
||||
.wl-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.wl-label { flex: 1; font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wl-remove { width: 28px; height: 28px; border: none; background: none; color: var(--text3); cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 6px; flex-shrink: 0; }
|
||||
#waypoints-list { display: flex; flex-direction: column; margin-bottom: 10px; }
|
||||
.wl-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.wl-item:last-child { border-bottom: none; }
|
||||
.wl-pin { flex-shrink: 0; display: flex; align-items: center; }
|
||||
.wl-label {
|
||||
flex: 1; font-size: 13px; color: var(--text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
.wl-remove {
|
||||
width: 28px; height: 28px; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: none; border: none; color: var(--text3);
|
||||
cursor: pointer; border-radius: 6px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.wl-remove:active { background: var(--red-bg); color: var(--red); }
|
||||
.wl-remove svg { width: 14px; height: 14px; }
|
||||
|
||||
/* Route actions */
|
||||
.route-actions { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.btn-action { flex: 1; height: 38px; background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; color: var(--text2); font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 5px; cursor: pointer; transition: all 0.15s; }
|
||||
.btn-action svg { width: 14px; height: 14px; }
|
||||
.btn-action:active { background: var(--surface3); transform: scale(0.94); }
|
||||
.route-actions {
|
||||
display: flex; gap: 6px; margin: 8px 0;
|
||||
}
|
||||
.btn-action {
|
||||
flex: 1; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center; gap: 5px;
|
||||
background: var(--surface2); border: 1px solid var(--border);
|
||||
border-radius: 10px; color: var(--text2);
|
||||
font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-action svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
.btn-action:active { background: var(--surface3, var(--border)); }
|
||||
.btn-action.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.btn-action.primary:active { opacity: 0.85; }
|
||||
.btn-action.danger:active { background: var(--red-bg); color: var(--red); border-color: var(--red); }
|
||||
.btn-action.primary { border-color: var(--accent); color: var(--accent); }
|
||||
.btn-action.primary:active { background: var(--accent-bg); }
|
||||
|
||||
/* ── Route Status ─────────────────────────────── */
|
||||
#route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; }
|
||||
@@ -284,21 +309,36 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
/* ── Route Cards ──────────────────────────────── */
|
||||
#route-cards, #link-cards, #scenic-cards { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
|
||||
.route-card {
|
||||
background: var(--surface2); border: 1.5px solid var(--border);
|
||||
border-radius: 14px; padding: 12px 14px; cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s;
|
||||
background: var(--surface2);
|
||||
border: 1.5px solid var(--border);
|
||||
border-left: 4px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
animation: cardFadeIn 0.2s ease-out both;
|
||||
}
|
||||
.route-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
||||
.route-card:active { background: var(--surface3, var(--surface2)); }
|
||||
.route-card.active {
|
||||
border-color: var(--border);
|
||||
border-left-color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
}
|
||||
.rc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.rc-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.rc-title { flex: 1; font-size: 13px; font-weight: 700; color: var(--text); }
|
||||
.rc-km { font-size: 14px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||
.rc-time { font-size: 12px; color: var(--text2); font-variant-numeric: tabular-nums; }
|
||||
.rc-bar { height: 5px; border-radius: 3px; background: var(--surface3); overflow: hidden; margin-bottom: 8px; display: flex; }
|
||||
.rc-bar-dirt { background: var(--gold); height: 100%; transition: width 0.4s; }
|
||||
.rc-bar-asphalt { background: var(--text3); height: 100%; flex: 1; }
|
||||
.rc-title { font-size: 13px; font-weight: 700; color: var(--text); flex: 1; }
|
||||
.rc-meta { font-size: 12px; color: var(--text2); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.rc-bar-wrap { margin-bottom: 4px; }
|
||||
.rc-bar {
|
||||
height: 6px; border-radius: 3px;
|
||||
background: var(--border);
|
||||
display: flex; overflow: hidden;
|
||||
}
|
||||
.rc-bar-dirt { background: var(--gold); border-radius: 3px 0 0 3px; transition: width 0.4s; }
|
||||
.rc-bar-asphalt { background: var(--text3); }
|
||||
.rc-bar-label { font-size: 11px; color: var(--text2); }
|
||||
.rc-stats { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
|
||||
/* Stat pills */
|
||||
@@ -411,7 +451,8 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
#ruler-info.visible { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* ── Waypoint Markers ─────────────────────────── */
|
||||
.route-waypoint-marker { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; color: #fff; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.5); border: 2px solid rgba(255,255,255,0.8); }
|
||||
.route-waypoint-marker { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4)); }
|
||||
.route-waypoint-marker:active { cursor: grabbing; }
|
||||
.named-marker-el { font-size: 22px; cursor: pointer; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); user-select: none; line-height: 1; }
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
|
||||
74
tasks/enduro-trails/prototype/static/app.js
vendored
74
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -460,19 +460,22 @@ function addWaypointMode() {
|
||||
function createWaypointMarkerEl(index, total) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'route-waypoint-marker marker-anim';
|
||||
let bg, text, color = '#fff';
|
||||
el.style.cssText = 'cursor: grab; width: 28px; height: 36px; position: relative;';
|
||||
|
||||
let bg, label;
|
||||
if (index === 0) {
|
||||
bg = '#2EA043'; text = 'A';
|
||||
bg = '#2EA043'; label = 'S';
|
||||
} else if (index === total - 1) {
|
||||
bg = '#FF3B1F'; text = 'B';
|
||||
bg = '#FF3B1F'; label = 'F';
|
||||
} else {
|
||||
bg = '#fff'; text = String(index); color = '#0066ff';
|
||||
el.style.cssText = `width:18px;height:18px;background:${bg};border:2px solid #0066ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:${color};box-shadow:0 1px 4px rgba(0,0,0,0.3);cursor:grab;`;
|
||||
el.textContent = text;
|
||||
return el;
|
||||
bg = '#0066ff'; label = String(index);
|
||||
}
|
||||
el.style.cssText = `width:22px;height:22px;background:${bg};border:2px solid #fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:${color};box-shadow:0 1px 4px rgba(0,0,0,0.3);cursor:grab;`;
|
||||
el.textContent = text;
|
||||
|
||||
el.innerHTML = `<svg width="28" height="36" 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="${bg}" stroke="white" stroke-width="1.5"/>
|
||||
<text x="14" y="19" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="${label.length > 1 ? '9' : '11'}" font-weight="700" fill="white">${label}</text>
|
||||
</svg>`;
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -518,25 +521,34 @@ async function reverseGeocode(lat, lon) {
|
||||
}
|
||||
}
|
||||
|
||||
function waypointPinSvg(label, color) {
|
||||
const fs = label.length > 1 ? '7' : '9';
|
||||
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>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
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) => {
|
||||
const labelClass = i === 0 ? 'start' : i === routeWaypoints.length - 1 ? 'end' : 'mid';
|
||||
const color = labelClass === 'start' ? 'var(--success)' : labelClass === 'end' ? 'var(--red)' : '#0066ff';
|
||||
const isStart = i === 0;
|
||||
const isEnd = i === routeWaypoints.length - 1;
|
||||
const label = isStart ? 'S' : isEnd ? 'F' : String(i);
|
||||
const color = isStart ? '#2EA043' : isEnd ? '#FF3B1F' : '#0066ff';
|
||||
const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`;
|
||||
return `<div class="wl-item" id="wl-item-${i}">
|
||||
<div class="wl-dot" style="background:${color}"></div>
|
||||
<div class="wl-pin">${waypointPinSvg(label, color)}</div>
|
||||
<span class="wl-label" id="wl-label-${i}">${coordText}</span>
|
||||
<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" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||||
<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>`;
|
||||
}).join('');
|
||||
|
||||
// Async update labels with place names
|
||||
// Async geocode
|
||||
routeWaypoints.forEach(async (wp, i) => {
|
||||
const name = await reverseGeocode(wp.lat, wp.lon);
|
||||
const el = document.getElementById(`wl-label-${i}`);
|
||||
@@ -696,32 +708,26 @@ function renderRouteCards(routes) {
|
||||
const container = document.getElementById('route-cards');
|
||||
container.innerHTML = routes.map((route, i) => {
|
||||
const color = ROUTE_COLORS[i] || '#888888';
|
||||
const distKm = (route.distance_m / 1000).toFixed(1);
|
||||
const distKm = (route.distance_m / 1000).toFixed(0);
|
||||
const timeStr = formatDuration(route.duration_s);
|
||||
const isActive = i === activeRouteIdx;
|
||||
|
||||
let statsHtml = '';
|
||||
if (route.stats) {
|
||||
const s = route.stats;
|
||||
statsHtml = `
|
||||
<div class="rc-bar">
|
||||
<div class="rc-bar-dirt" style="width:${s.dirt_total_pct || 0}%"></div>
|
||||
<div class="rc-bar-asphalt"></div>
|
||||
</div>
|
||||
<div class="rc-stats">
|
||||
<span class="stat-pill dirt">🟡 ${s.dirt_total_pct || 0}% грунт</span>
|
||||
${s.asphalt_pct ? `<span class="stat-pill asphalt">⬜ ${s.asphalt_pct}% асфальт</span>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const s = route.stats || {};
|
||||
const dirtPct = s.dirt_total_pct || 0;
|
||||
const asphPct = s.asphalt_pct || 0;
|
||||
|
||||
return `<div class="route-card${isActive ? ' active' : ''}" onclick="selectRoute(${i})">
|
||||
<div class="rc-header">
|
||||
<span class="rc-dot" style="background:${color}"></span>
|
||||
<span class="rc-title">Вариант ${i + 1}</span>
|
||||
<span class="rc-km">${distKm} км</span>
|
||||
<span class="rc-time">${timeStr}</span>
|
||||
<span class="rc-meta">${distKm} км · ${timeStr}</span>
|
||||
</div>
|
||||
${statsHtml}
|
||||
<div class="rc-bar-wrap">
|
||||
<div class="rc-bar">
|
||||
<div class="rc-bar-dirt" style="width:${dirtPct}%"></div>
|
||||
<div class="rc-bar-asphalt" style="width:${asphPct}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rc-bar-label">${dirtPct}% грунт${asphPct ? ` · ${asphPct}% асфальт` : ''}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user