auto-sync: 2026-05-05 23:20:01
This commit is contained in:
@@ -13,3 +13,4 @@
|
||||
{"type":"memory.recall.recorded","timestamp":"2026-04-26T10:54:35.677Z","query":"mva154 хост сервер характеристики доступ SSH","resultCount":1,"results":[{"path":"memory/2026-04-11.md","startLine":62,"endLine":71,"score":0.3638544976711273}]}
|
||||
{"type":"memory.recall.recorded","timestamp":"2026-05-04T06:57:05.721Z","query":"enduro trails backlog задачи","resultCount":3,"results":[{"path":"memory/2026-05-02.md","startLine":107,"endLine":142,"score":0.375397714972496},{"path":"memory/2026-05-02.md","startLine":86,"endLine":117,"score":0.3636188447475433},{"path":"memory/2026-05-03.md","startLine":1,"endLine":36,"score":0.3552299261093139}]}
|
||||
{"type":"memory.recall.recorded","timestamp":"2026-05-05T05:03:32.115Z","query":"DEV_TASK_PHASE5 dev agent enduro trails фаза 5","resultCount":1,"results":[{"path":"memory/2026-05-04.md","startLine":1,"endLine":30,"score":0.40014922022819516}]}
|
||||
{"type":"memory.recall.recorded","timestamp":"2026-05-05T20:11:55.846Z","query":"TTS синтез речи модель голос ElevenLabs Yandex SpeechKit","resultCount":2,"results":[{"path":"memory/2026-03-23.md","startLine":64,"endLine":80,"score":0.40174805521965024},{"path":"memory/2026-03-23.md","startLine":48,"endLine":66,"score":0.3850157171487808}]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-05-05T05:03:32.115Z",
|
||||
"updatedAt": "2026-05-05T20:11:55.846Z",
|
||||
"entries": {
|
||||
"memory:memory/2026-04-05.md:29:55": {
|
||||
"key": "memory:memory/2026-04-05.md:29:55",
|
||||
@@ -748,6 +748,68 @@
|
||||
"ui-тестирования",
|
||||
"playwright/puppeteer"
|
||||
]
|
||||
},
|
||||
"memory:memory/2026-03-23.md:64:80": {
|
||||
"key": "memory:memory/2026-03-23.md:64:80",
|
||||
"path": "memory/2026-03-23.md",
|
||||
"startLine": 64,
|
||||
"endLine": 80,
|
||||
"source": "memory",
|
||||
"snippet": "- **Claude Sonnet:** создаёт текст с оптимальной пунктуацией, паузами, плавными формулировками, что лучше для TTS - **DeepSeek v3.2:** генерирует более компактный текст, менее выраженная структура предложений, что может приводить к искажениям при озвучивании - **Ключевой вывод:** Качество голосовых сообщений зависит не от самого синтеза речи, а от текста, который подаётся в TTS-движок - **Рекомендация:** Для задач, где важна качественная озвучка, использовать модели с более структурированным выводом (Claude Sonnet) ### Создание специализированного агента для оптимизации текста под TTS - 23 марта 17:09 - Слава предложил создать первого специализированного агента для формулирования текста, ид",
|
||||
"recallCount": 1,
|
||||
"dailyCount": 0,
|
||||
"groundedCount": 0,
|
||||
"totalScore": 0.40174805521965024,
|
||||
"maxScore": 0.40174805521965024,
|
||||
"firstRecalledAt": "2026-05-05T20:11:55.846Z",
|
||||
"lastRecalledAt": "2026-05-05T20:11:55.846Z",
|
||||
"queryHashes": [
|
||||
"4c737bff00f4"
|
||||
],
|
||||
"recallDays": [
|
||||
"2026-05-05"
|
||||
],
|
||||
"conceptTags": [
|
||||
"v3.2",
|
||||
"tts-движок",
|
||||
"claude",
|
||||
"sonnet",
|
||||
"создаёт",
|
||||
"текст",
|
||||
"оптимальной",
|
||||
"пунктуацией"
|
||||
]
|
||||
},
|
||||
"memory:memory/2026-03-23.md:48:66": {
|
||||
"key": "memory:memory/2026-03-23.md:48:66",
|
||||
"path": "memory/2026-03-23.md",
|
||||
"startLine": 48,
|
||||
"endLine": 66,
|
||||
"source": "memory",
|
||||
"snippet": "### Расчёт экономии от использования специализированных агентов - 23 марта 16:59 - Слава попросил рассчитать экономию от использования группы специализированных агентов вместо одного универсального - Проведён расчёт на основе цен OpenRouter API: - Claude Sonnet 4.6: $3/$15 за 1M токенов (input/output) - Claude Haiku: $1/$5 за 1M токенов - Llama 4 Maverick: $0.15/$0.60 за 1M токенов - Gemini 2.0 Flash: $0.10/$0.40 за 1M токенов - Оценка ежедневного потребления: 105K input, 67K output токенов - Стоимость одной модели Sonnet: ~$1.32 в день - Стоимость команды специалистов: ~$0.69 в день - Экономия: 48% ($0.63 в день, $19 в месяц, $230 в год) - Дополнительные преимущества: повышение каче",
|
||||
"recallCount": 1,
|
||||
"dailyCount": 0,
|
||||
"groundedCount": 0,
|
||||
"totalScore": 0.3850157171487808,
|
||||
"maxScore": 0.3850157171487808,
|
||||
"firstRecalledAt": "2026-05-05T20:11:55.846Z",
|
||||
"lastRecalledAt": "2026-05-05T20:11:55.846Z",
|
||||
"queryHashes": [
|
||||
"4c737bff00f4"
|
||||
],
|
||||
"recallDays": [
|
||||
"2026-05-05"
|
||||
],
|
||||
"conceptTags": [
|
||||
"router",
|
||||
"4.6",
|
||||
"input/output",
|
||||
"0.15",
|
||||
"0.60",
|
||||
"2.0",
|
||||
"0.10",
|
||||
"0.40"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
142
tasks/enduro-trails/prototype/static/app.js
vendored
142
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -1,5 +1,5 @@
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Enduro Trails — Phase 5 Redesign
|
||||
// Enduro Trails - Phase 5 Redesign
|
||||
// Theme system (auto/light/dark + SunCalc), skeleton, swipe, animations
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -54,7 +54,7 @@ function toggleTheme() {
|
||||
if (themeMode === 'auto') themeMode = 'light';
|
||||
else if (themeMode === 'light') themeMode = 'dark';
|
||||
else themeMode = 'auto';
|
||||
|
||||
|
||||
localStorage.setItem('enduro-theme-mode', themeMode);
|
||||
applyTheme();
|
||||
}
|
||||
@@ -64,9 +64,9 @@ function updateThemeButtonIcon() {
|
||||
const moonIcon = document.getElementById('theme-icon-moon');
|
||||
const label = document.getElementById('theme-label');
|
||||
if (!sunIcon || !moonIcon) return;
|
||||
|
||||
|
||||
const dark = isDarkTheme();
|
||||
|
||||
|
||||
if (themeMode === 'auto') {
|
||||
// Dynamic icon based on actual theme
|
||||
sunIcon.style.display = dark ? 'none' : 'block';
|
||||
@@ -89,8 +89,8 @@ function switchMapStyle() {
|
||||
const dark = isDarkTheme();
|
||||
const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || '';
|
||||
const styleUrl = dark ? basePath + '/style.json' : basePath + '/style-light.json';
|
||||
|
||||
// Check if style-light.json exists — if not, keep current
|
||||
|
||||
// Check if style-light.json exists - if not, keep current
|
||||
fetch(styleUrl, { method: 'HEAD' }).then(r => {
|
||||
if (r.ok) {
|
||||
map.setStyle(styleUrl);
|
||||
@@ -170,7 +170,7 @@ function formatDuration(seconds) {
|
||||
}
|
||||
|
||||
function formatDist(m) {
|
||||
if (!m) return '—';
|
||||
if (!m) return '-';
|
||||
if (m >= 1000) return (m / 1000).toFixed(1) + ' км';
|
||||
return Math.round(m) + ' м';
|
||||
}
|
||||
@@ -223,7 +223,7 @@ function initSheetSwipe() {
|
||||
document.querySelectorAll('.bottom-sheet').forEach(sheet => {
|
||||
let startY = 0;
|
||||
let isDragging = false;
|
||||
|
||||
|
||||
sheet.addEventListener('touchstart', (e) => {
|
||||
const rect = sheet.getBoundingClientRect();
|
||||
const touchY = e.touches[0].clientY;
|
||||
@@ -234,7 +234,7 @@ function initSheetSwipe() {
|
||||
sheet.classList.add('swiping');
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
|
||||
sheet.addEventListener('touchmove', (e) => {
|
||||
if (!isDragging) return;
|
||||
const dy = e.touches[0].clientY - startY;
|
||||
@@ -242,7 +242,7 @@ function initSheetSwipe() {
|
||||
sheet.style.transform = `translateY(${dy}px)`;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
|
||||
sheet.addEventListener('touchend', (e) => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
@@ -289,7 +289,7 @@ function showSkeleton(containerId, count) {
|
||||
|
||||
function deactivateAllModes() {
|
||||
// Deactivate all input modes but preserve route/scenic/link data on map
|
||||
if (routeMode) { routeMode = false; document.getElementById('tb-route').classList.remove('active'); closeSheet('sheet-route'); /* NOT clearRoute — keep line on map */ }
|
||||
if (routeMode) { routeMode = false; document.getElementById('tb-route').classList.remove('active'); closeSheet('sheet-route'); /* NOT clearRoute - keep line on map */ }
|
||||
if (rulerMode) toggleRuler();
|
||||
if (markerMode) toggleMarkerMode();
|
||||
if (typeof reconMode !== 'undefined' && reconMode) toggleReconMode();
|
||||
@@ -380,7 +380,7 @@ function toggleLayer(group) {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Роутинг — состояние ───────────────────────────────────────────
|
||||
// ─── Роутинг - состояние ───────────────────────────────────────────
|
||||
const ROUTE_COLORS = ['#0066ff', '#00aa44', '#9933cc', '#ff8800', '#888888'];
|
||||
let routeMode = false;
|
||||
let routeWaypoints = [];
|
||||
@@ -399,25 +399,25 @@ function toggleRouteMode() {
|
||||
const btn = document.getElementById('tb-route');
|
||||
|
||||
if (routeMode) {
|
||||
// If sheet is open — close sheet but stay in mode
|
||||
// If sheet is open - close sheet but stay in mode
|
||||
const sheet = document.getElementById('sheet-route');
|
||||
if (sheet && sheet.classList.contains('open')) {
|
||||
closeSheet('sheet-route');
|
||||
return;
|
||||
}
|
||||
// Sheet is closed — exit mode and clear route
|
||||
// Sheet is closed - exit mode and clear route
|
||||
routeMode = false;
|
||||
btn.classList.remove('active');
|
||||
clearRoute();
|
||||
window._map.getCanvas().style.cursor = '';
|
||||
} else {
|
||||
// Enter route mode — do NOT open sheet
|
||||
// Enter route mode - do NOT open sheet
|
||||
deactivateAllModes();
|
||||
routeMode = true;
|
||||
btn.classList.add('active');
|
||||
clearRoute();
|
||||
window._map.getCanvas().style.cursor = 'crosshair';
|
||||
// sheet is NOT opened — user taps mini-bar to open it
|
||||
// sheet is NOT opened - user taps mini-bar to open it
|
||||
}
|
||||
updateMapModeClass();
|
||||
}
|
||||
@@ -543,8 +543,55 @@ function haversineM(a, b) {
|
||||
}
|
||||
|
||||
function formatSegmentDist(m) {
|
||||
if (m < 1000) return Math.round(m) + ' м';
|
||||
return (m / 1000).toFixed(1).replace('.', ',') + ' км';
|
||||
if (m < 1000) return Math.round(m) + ' м';
|
||||
return (m / 1000).toFixed(1).replace('.', ',') + ' км';
|
||||
}
|
||||
|
||||
// Returns array of route-distance segments (meters) for each waypoint.
|
||||
// segDists[0] = 0, segDists[i] = distance along route geometry from wp[i-1] to wp[i].
|
||||
// Falls back to haversine if route geometry is unavailable or snap fails.
|
||||
function getRouteSegmentDistances() {
|
||||
const route = routeResults[activeRouteIdx];
|
||||
if (!route || !route.geometry || !route.geometry.coordinates) return null;
|
||||
|
||||
const coords = route.geometry.coordinates; // [[lon, lat], ...]
|
||||
const n = coords.length;
|
||||
if (n < 2 || routeWaypoints.length < 2) return null;
|
||||
|
||||
// Convert geometry coords to {lat, lon} for haversineM
|
||||
const geoPts = coords.map(([lon, lat]) => ({ lat, lon }));
|
||||
|
||||
// Snap each waypoint to the nearest geometry point index
|
||||
const snapIdx = routeWaypoints.map(wp => {
|
||||
let bestIdx = 0;
|
||||
let bestDist = Infinity;
|
||||
for (let j = 0; j < n; j++) {
|
||||
const d = haversineM(wp, geoPts[j]);
|
||||
if (d < bestDist) { bestDist = d; bestIdx = j; }
|
||||
}
|
||||
return bestIdx;
|
||||
});
|
||||
|
||||
// For each segment i→i+1, sum haversine along geometry from snapIdx[i] to snapIdx[i+1]
|
||||
const segDists = [0];
|
||||
for (let i = 1; i < routeWaypoints.length; i++) {
|
||||
const from = snapIdx[i - 1];
|
||||
const to = snapIdx[i];
|
||||
if (from === to) {
|
||||
// Same snap point — fallback to straight-line
|
||||
segDists.push(haversineM(routeWaypoints[i - 1], routeWaypoints[i]));
|
||||
continue;
|
||||
}
|
||||
// Walk geometry in the correct direction
|
||||
const step = to > from ? 1 : -1;
|
||||
let dist = 0;
|
||||
for (let j = from; j !== to; j += step) {
|
||||
dist += haversineM(geoPts[j], geoPts[j + step]);
|
||||
}
|
||||
segDists.push(dist);
|
||||
}
|
||||
|
||||
return segDists;
|
||||
}
|
||||
|
||||
async function renderWaypointsList() {
|
||||
@@ -553,13 +600,18 @@ async function renderWaypointsList() {
|
||||
|
||||
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>`;
|
||||
|
||||
const segDists = (routeResults.length > 0 && activeRouteIdx >= 0)
|
||||
? getRouteSegmentDistances()
|
||||
: null;
|
||||
|
||||
let html = 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)}`;
|
||||
const distStr = i > 0 ? formatSegmentDist(haversineM(routeWaypoints[i-1], wp)) : '';
|
||||
const distStr = i > 0 && segDists ? formatSegmentDist(segDists[i]) :
|
||||
i > 0 ? formatSegmentDist(haversineM(routeWaypoints[i-1], wp)) : '';
|
||||
return `<div class="wl-item" id="wl-item-${i}" data-idx="${i}">
|
||||
<div class="wl-pin">${waypointPinSvg(label, color)}</div>
|
||||
<div class="wl-info">
|
||||
@@ -769,7 +821,7 @@ async function buildRoute() {
|
||||
drawRouteResults(routeResults, 0);
|
||||
|
||||
document.getElementById('route-status').textContent = `${routeResults.length} маршрут(ов)`;
|
||||
// Show mini-bar with result — do NOT open main sheet
|
||||
// Show mini-bar with result - do NOT open main sheet
|
||||
hideMiniRouteLoading();
|
||||
showMiniRouteSheet();
|
||||
} catch(e) {
|
||||
@@ -786,14 +838,14 @@ function drawRouteResults(routes, activeIdx) {
|
||||
activeRouteIdx = activeIdx;
|
||||
const wasBuilt = routeResults.length > 0; // track rebuild vs first build
|
||||
routeResults = routes;
|
||||
|
||||
|
||||
// Clear old layers
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try { if (map.getLayer('route-line-' + i)) map.removeLayer('route-line-' + i); } catch(e) {}
|
||||
try { if (map.getLayer('route-line-' + i + '-outline')) map.removeLayer('route-line-' + i + '-outline'); } catch(e) {}
|
||||
try { if (map.getSource('route-' + i)) map.removeSource('route-' + i); } catch(e) {}
|
||||
}
|
||||
|
||||
|
||||
routes.forEach((route, i) => {
|
||||
const color = ROUTE_COLORS[i] || '#888888';
|
||||
const isActive = i === activeIdx;
|
||||
@@ -821,7 +873,7 @@ function drawRouteResults(routes, activeIdx) {
|
||||
},
|
||||
layout: { 'line-cap': 'round', 'line-join': 'round' }
|
||||
});
|
||||
|
||||
|
||||
map.on('click', 'route-line-' + i, (e) => {
|
||||
if (e.stopPropagation) e.stopPropagation();
|
||||
selectRoute(i);
|
||||
@@ -833,7 +885,7 @@ function drawRouteResults(routes, activeIdx) {
|
||||
map.getCanvas().style.cursor = routeMode ? 'crosshair' : '';
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
renderRouteCards(routes);
|
||||
|
||||
// Update mini sheet if visible
|
||||
@@ -988,7 +1040,7 @@ function toggleMarkerMode() {
|
||||
function addMarker(lngLat) {
|
||||
const markers = loadMarkers();
|
||||
if (markers.length >= 50) { alert('Достигнут лимит 50 меток'); return; }
|
||||
|
||||
|
||||
const grid = document.getElementById('marker-type-grid');
|
||||
// Show marker dialog
|
||||
openMarkerDialog(lngLat);
|
||||
@@ -997,7 +1049,7 @@ function addMarker(lngLat) {
|
||||
function openMarkerDialog(lngLat) {
|
||||
const dialog = document.getElementById('marker-dialog');
|
||||
const grid = document.getElementById('marker-type-grid');
|
||||
grid.innerHTML = MARKER_ICONS.map((ic, i) =>
|
||||
grid.innerHTML = MARKER_ICONS.map((ic, i) =>
|
||||
`<button class="marker-type-btn" onclick="selectMarkerType(${i}, ${lngLat.lat}, ${lngLat.lng})">
|
||||
<span class="mt-icon">${ic}</span>
|
||||
<span class="mt-label">${['Флаг','Лагерь','Ремонт','Заправка','Вода','Точка'][i]}</span>
|
||||
@@ -1030,7 +1082,7 @@ function drawNamedMarker(markerData) {
|
||||
el.className = 'named-marker-el marker-anim';
|
||||
el.textContent = markerData.icon;
|
||||
el.title = markerData.name;
|
||||
|
||||
|
||||
const popup = new maplibregl.Popup({ offset: 25, closeButton: true })
|
||||
.setHTML(`
|
||||
<div class="popup-title">${escapeXml(markerData.name)}</div>
|
||||
@@ -1041,12 +1093,12 @@ function drawNamedMarker(markerData) {
|
||||
<button onclick="removeMarker(${markerData.id})" style="flex:1;padding:3px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:11px;">🗑 Удалить</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
||||
const mlMarker = new maplibregl.Marker({ element: el, anchor: 'bottom' })
|
||||
.setLngLat([markerData.lon, markerData.lat])
|
||||
.setPopup(popup)
|
||||
.addTo(map);
|
||||
|
||||
|
||||
namedMarkerObjects[markerData.id] = mlMarker;
|
||||
}
|
||||
|
||||
@@ -1138,7 +1190,7 @@ async function initMap() {
|
||||
});
|
||||
|
||||
function formatLength(m) {
|
||||
if (!m) return '—';
|
||||
if (!m) return '-';
|
||||
if (m >= 1000) return (m / 1000).toFixed(1) + ' км';
|
||||
return Math.round(m) + ' м';
|
||||
}
|
||||
@@ -1161,9 +1213,9 @@ async function initMap() {
|
||||
const props = e.features[0].properties;
|
||||
const html = `
|
||||
<div class="popup-title">${props.name || 'Без названия'}</div>
|
||||
<div class="popup-row"><span class="popup-key">Тип</span><span class="popup-val">${props.highway || '—'}</span></div>
|
||||
<div class="popup-row"><span class="popup-key">Покрытие</span><span class="popup-val">${props.surface || '—'}</span></div>
|
||||
<div class="popup-row"><span class="popup-key">Категория</span><span class="popup-val">${props.tracktype || '—'}</span></div>
|
||||
<div class="popup-row"><span class="popup-key">Тип</span><span class="popup-val">${props.highway || '-'}</span></div>
|
||||
<div class="popup-row"><span class="popup-key">Покрытие</span><span class="popup-val">${props.surface || '-'}</span></div>
|
||||
<div class="popup-row"><span class="popup-key">Категория</span><span class="popup-val">${props.tracktype || '-'}</span></div>
|
||||
<div class="popup-row"><span class="popup-key">Длина</span><span class="popup-val">${formatLength(props.length_m)}</span></div>
|
||||
${props.mtb_scale ? `<div class="popup-row"><span class="popup-key">MTB scale</span><span class="popup-val">${props.mtb_scale}</span></div>` : ''}
|
||||
`;
|
||||
@@ -1421,7 +1473,7 @@ function toggleReconMode() {
|
||||
btn.classList.remove('active');
|
||||
closeSheet('sheet-recon');
|
||||
window._map.getCanvas().style.cursor = '';
|
||||
clearRecon(); // recon data is transient — safe to clear
|
||||
clearRecon(); // recon data is transient - safe to clear
|
||||
} else {
|
||||
deactivateAllModes();
|
||||
reconMode = true;
|
||||
@@ -1446,7 +1498,7 @@ function makeCircleGeoJSON(lon, lat, radiusKm) {
|
||||
async function doRecon(lon, lat) {
|
||||
reconCenter = [lon, lat];
|
||||
const map = window._map;
|
||||
|
||||
|
||||
const circle = makeCircleGeoJSON(lon, lat, reconRadius);
|
||||
if (map.getSource('recon-circle')) {
|
||||
map.getSource('recon-circle').setData(circle);
|
||||
@@ -1465,7 +1517,7 @@ async function doRecon(lon, lat) {
|
||||
const basePath = getBasePath();
|
||||
const resultsDiv = document.getElementById('recon-results');
|
||||
resultsDiv.style.display = 'block';
|
||||
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${basePath}/api/recon`, {
|
||||
method: 'POST',
|
||||
@@ -1475,12 +1527,12 @@ async function doRecon(lon, lat) {
|
||||
const data = await resp.json();
|
||||
const t = data.trails || {};
|
||||
const p = data.poi || {};
|
||||
|
||||
|
||||
document.getElementById('r-total-km').textContent = t.total_km || 0;
|
||||
document.getElementById('r-lev12-km').textContent = t.lev12_km || 0;
|
||||
document.getElementById('r-lev345-km').textContent = t.lev345_km || 0;
|
||||
document.getElementById('r-path-km').textContent = t.path_km || 0;
|
||||
|
||||
|
||||
const poiList = document.getElementById('r-poi-list');
|
||||
const poiTypes = [
|
||||
{ key: 'natural=water', icon: '💧', label: 'Озёра' },
|
||||
@@ -1488,15 +1540,15 @@ async function doRecon(lon, lat) {
|
||||
{ key: 'ford=yes', icon: '🌊', label: 'Броды' },
|
||||
{ key: 'historic=ruins', icon: '🏚', label: 'Руины' },
|
||||
];
|
||||
poiList.innerHTML = poiTypes.map(pt =>
|
||||
poiList.innerHTML = poiTypes.map(pt =>
|
||||
`<div class="poi-row">
|
||||
<span class="poi-row-label"><span class="poi-icon">${pt.icon}</span> ${pt.label}</span>
|
||||
<span class="poi-row-count">${p[pt.key] || 0}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
|
||||
} catch(e) {
|
||||
document.getElementById('r-total-km').textContent = '—';
|
||||
document.getElementById('r-total-km').textContent = '-';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1742,13 +1794,13 @@ function drawScenicRoutes(routes, activeIdx) {
|
||||
const map = window._map;
|
||||
scenicRoutes = routes;
|
||||
activeScenicIdx = activeIdx;
|
||||
|
||||
|
||||
// Clear old
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try { if (map.getLayer(`scenic-line-${i}`)) map.removeLayer(`scenic-line-${i}`); } catch(e) {}
|
||||
try { if (map.getSource(`scenic-src-${i}`)) map.removeSource(`scenic-src-${i}`); } catch(e) {}
|
||||
}
|
||||
|
||||
|
||||
const colors = ['#0066ff', '#00aa44', '#9933cc'];
|
||||
routes.forEach((r, i) => {
|
||||
const geojson = { type: 'Feature', geometry: r.geometry, properties: {} };
|
||||
@@ -1776,7 +1828,7 @@ function drawScenicRoutes(routes, activeIdx) {
|
||||
const pois = (r.scenic_pois || []).map(p => {
|
||||
const SCENIC_LABELS = {'natural=water':'💧 Озёро','tourism=viewpoint':'👁 Смотровая','historic=ruins':'🏚 Руины','natural=peak':'🔺 Вершина','natural=cave_entrance':'🕳 Пещера','ford=yes':'🌊 Брод'};
|
||||
const label = SCENIC_LABELS[p.type] || '📍 ' + p.type;
|
||||
const name = p.name ? ` — ${p.name}` : '';
|
||||
const name = p.name ? ` - ${p.name}` : '';
|
||||
return `<div class="scenic-poi-item">${label}${name}</div>`;
|
||||
}).join('');
|
||||
return `<div class="route-card ${i===activeIdx?'active':''}" onclick="selectScenicRoute(${i})">
|
||||
@@ -1863,7 +1915,7 @@ function updateMiniRouteCard() {
|
||||
const r = routeResults[activeRouteIdx];
|
||||
if (!r) return;
|
||||
const km = (r.distance_m / 1000).toFixed(0);
|
||||
const dirt = r.stats?.dirt_total_pct ?? '—';
|
||||
const dirt = r.stats?.dirt_total_pct ?? '-';
|
||||
document.getElementById('mini-dot').style.background = ROUTE_COLORS[activeRouteIdx % ROUTE_COLORS.length];
|
||||
document.getElementById('mini-label').textContent = `Вариант ${activeRouteIdx + 1} из ${routeResults.length}`;
|
||||
document.getElementById('mini-stats').textContent = `${km} км · ${dirt}% грунт`;
|
||||
|
||||
Reference in New Issue
Block a user