auto-sync: 2026-05-04 11:10:01
This commit is contained in:
803
tasks/enduro-trails/prototype/static/app.js
vendored
803
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -1,3 +1,25 @@
|
|||||||
|
// ─── Утилиты ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
const totalMin = Math.round(seconds / 60);
|
||||||
|
if (totalMin < 60) return totalMin + ' мин';
|
||||||
|
const days = Math.floor(totalMin / 1440);
|
||||||
|
const hours = Math.floor((totalMin % 1440) / 60);
|
||||||
|
const mins = totalMin % 60;
|
||||||
|
if (days > 0) {
|
||||||
|
if (mins === 0) return `${days} дн ${hours} ч`;
|
||||||
|
return `${days} дн ${hours} ч ${mins} мин`;
|
||||||
|
}
|
||||||
|
if (mins === 0) return `${hours} ч`;
|
||||||
|
return `${hours} ч ${mins} мин`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDist(m) {
|
||||||
|
if (!m) return '—';
|
||||||
|
if (m >= 1000) return (m / 1000).toFixed(1) + ' км';
|
||||||
|
return Math.round(m) + ' м';
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Компас ───────────────────────────────────────────────────────────────────
|
// ─── Компас ───────────────────────────────────────────────────────────────────
|
||||||
let compassLocked = false;
|
let compassLocked = false;
|
||||||
|
|
||||||
@@ -84,6 +106,648 @@ function toggleLayer(group) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─── Роутинг — состояние ──────────────────────────────────────────────────────
|
||||||
|
const ROUTE_COLORS = ['#0066ff', '#00aa44', '#9933cc', '#ff8800', '#888888'];
|
||||||
|
|
||||||
|
let routeMode = false;
|
||||||
|
let routeWaypoints = []; // [{lon, lat}, ...]
|
||||||
|
let routeResults = []; // массив маршрутов из API
|
||||||
|
let activeRouteIdx = 0;
|
||||||
|
let waypointMarkers = []; // MapLibre маркеры точек
|
||||||
|
let addingWaypoint = false; // режим добавления промежуточной точки
|
||||||
|
let buildDebounceTimer = null;
|
||||||
|
|
||||||
|
function getBasePath() {
|
||||||
|
return window.location.pathname.replace(/\/[^/]*$/, '') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Режим маршрута ───────────────────────────────────────────────────────────
|
||||||
|
function toggleRouteMode() {
|
||||||
|
routeMode = !routeMode;
|
||||||
|
const btn = document.getElementById('btn-route');
|
||||||
|
const panel = document.getElementById('route-panel');
|
||||||
|
if (routeMode) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
panel.style.display = 'block';
|
||||||
|
clearRoute();
|
||||||
|
window._map.getCanvas().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
panel.style.display = 'none';
|
||||||
|
clearRoute();
|
||||||
|
window._map.getCanvas().style.cursor = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearRoute() {
|
||||||
|
// Убираем маркеры точек
|
||||||
|
waypointMarkers.forEach(m => m.remove());
|
||||||
|
waypointMarkers = [];
|
||||||
|
routeWaypoints = [];
|
||||||
|
routeResults = [];
|
||||||
|
activeRouteIdx = 0;
|
||||||
|
addingWaypoint = false;
|
||||||
|
|
||||||
|
// Убираем слои маршрутов
|
||||||
|
const map = window._map;
|
||||||
|
if (map) {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (map.getLayer('route-line-' + i)) map.removeLayer('route-line-' + i);
|
||||||
|
if (map.getLayer('route-line-' + i + '-outline')) map.removeLayer('route-line-' + i + '-outline');
|
||||||
|
}
|
||||||
|
if (map.getSource('route')) map.removeSource('route');
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (map.getSource('route-' + i)) map.removeSource('route-' + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('route-status').textContent = 'Кликни точку старта';
|
||||||
|
document.getElementById('route-actions').style.display = 'none';
|
||||||
|
document.getElementById('route-cards').innerHTML = '';
|
||||||
|
document.getElementById('waypoints-list').innerHTML = '';
|
||||||
|
document.getElementById('btn-add-waypoint').style.display = '';
|
||||||
|
|
||||||
|
if (routeMode && map) map.getCanvas().style.cursor = 'crosshair';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Добавление промежуточной точки ──────────────────────────────────────────
|
||||||
|
function startAddWaypoint() {
|
||||||
|
if (routeWaypoints.length >= 10) return;
|
||||||
|
addingWaypoint = true;
|
||||||
|
window._map.getCanvas().style.cursor = 'crosshair';
|
||||||
|
document.getElementById('route-status').textContent = 'Кликни на карте для добавления точки';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Маркеры точек ────────────────────────────────────────────────────────────
|
||||||
|
function createWaypointMarkerEl(index, total) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'route-waypoint-marker';
|
||||||
|
let bg, text, color = '#fff';
|
||||||
|
if (index === 0) {
|
||||||
|
bg = '#00aa44'; text = 'A';
|
||||||
|
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;`;
|
||||||
|
} else if (index === total - 1) {
|
||||||
|
bg = '#cc0000'; text = 'B';
|
||||||
|
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;`;
|
||||||
|
} else {
|
||||||
|
text = String(index);
|
||||||
|
el.style.cssText = `width:18px;height:18px;background:#fff;border:2px solid #0066ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:#0066ff;box-shadow:0 1px 4px rgba(0,0,0,0.3);cursor:grab;`;
|
||||||
|
}
|
||||||
|
el.textContent = text;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildWaypointMarkers() {
|
||||||
|
waypointMarkers.forEach(m => m.remove());
|
||||||
|
waypointMarkers = [];
|
||||||
|
const map = window._map;
|
||||||
|
routeWaypoints.forEach((wp, i) => {
|
||||||
|
const el = createWaypointMarkerEl(i, routeWaypoints.length);
|
||||||
|
const marker = new maplibregl.Marker({ element: el, anchor: 'center', draggable: true })
|
||||||
|
.setLngLat([wp.lon, wp.lat])
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
// Захватываем индекс в замыкании
|
||||||
|
(function(idx) {
|
||||||
|
marker.on('dragend', () => {
|
||||||
|
const lngLat = marker.getLngLat();
|
||||||
|
routeWaypoints[idx] = { lon: lngLat.lng, lat: lngLat.lat };
|
||||||
|
renderWaypointsList();
|
||||||
|
debounceBuildRoute();
|
||||||
|
});
|
||||||
|
})(i);
|
||||||
|
|
||||||
|
waypointMarkers.push(marker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWaypointsList() {
|
||||||
|
const list = document.getElementById('waypoints-list');
|
||||||
|
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
|
||||||
|
|
||||||
|
list.innerHTML = routeWaypoints.map((wp, i) => {
|
||||||
|
let labelClass, labelText;
|
||||||
|
if (i === 0) { labelClass = 'start'; labelText = 'A'; }
|
||||||
|
else if (i === routeWaypoints.length - 1) { labelClass = 'end'; labelText = 'B'; }
|
||||||
|
else { labelClass = 'mid'; labelText = String(i); }
|
||||||
|
|
||||||
|
return `<div class="waypoint-row" draggable="true"
|
||||||
|
ondragstart="onWpDragStart(event,${i})"
|
||||||
|
ondragover="onWpDragOver(event,${i})"
|
||||||
|
ondrop="onWpDrop(event,${i})"
|
||||||
|
ondragleave="onWpDragLeave(event)">
|
||||||
|
<span class="waypoint-label ${labelClass}">${labelText}</span>
|
||||||
|
<span class="waypoint-coords">${wp.lat.toFixed(4)}, ${wp.lon.toFixed(4)}</span>
|
||||||
|
<button class="waypoint-remove" onclick="removeWaypoint(${i})" title="Удалить точку">✕</button>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Показываем/скрываем кнопку добавления
|
||||||
|
document.getElementById('btn-add-waypoint').style.display =
|
||||||
|
routeWaypoints.length >= 10 ? 'none' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Drag-and-drop порядка точек ──────────────────────────────────────────────
|
||||||
|
let dragWpIdx = null;
|
||||||
|
|
||||||
|
function onWpDragStart(e, idx) {
|
||||||
|
dragWpIdx = idx;
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWpDragOver(e, idx) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
document.querySelectorAll('.waypoint-row').forEach((r, i) => {
|
||||||
|
r.classList.toggle('drag-over', i === idx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWpDragLeave(e) {
|
||||||
|
e.currentTarget.classList.remove('drag-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWpDrop(e, idx) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelectorAll('.waypoint-row').forEach(r => r.classList.remove('drag-over'));
|
||||||
|
if (dragWpIdx === null || dragWpIdx === idx) return;
|
||||||
|
const moved = routeWaypoints.splice(dragWpIdx, 1)[0];
|
||||||
|
routeWaypoints.splice(idx, 0, moved);
|
||||||
|
dragWpIdx = null;
|
||||||
|
rebuildWaypointMarkers();
|
||||||
|
renderWaypointsList();
|
||||||
|
if (routeWaypoints.length >= 2) debounceBuildRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWaypoint(idx) {
|
||||||
|
routeWaypoints.splice(idx, 1);
|
||||||
|
rebuildWaypointMarkers();
|
||||||
|
renderWaypointsList();
|
||||||
|
if (routeWaypoints.length >= 2) {
|
||||||
|
debounceBuildRoute();
|
||||||
|
} else {
|
||||||
|
// Убираем маршруты с карты
|
||||||
|
const map = window._map;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (map.getLayer('route-line-' + i)) map.removeLayer('route-line-' + i);
|
||||||
|
if (map.getLayer('route-line-' + i + '-outline')) map.removeLayer('route-line-' + i + '-outline');
|
||||||
|
if (map.getSource('route-' + i)) map.removeSource('route-' + i);
|
||||||
|
}
|
||||||
|
routeResults = [];
|
||||||
|
document.getElementById('route-cards').innerHTML = '';
|
||||||
|
document.getElementById('route-actions').style.display =
|
||||||
|
routeWaypoints.length >= 2 ? 'block' : 'none';
|
||||||
|
document.getElementById('route-status').textContent =
|
||||||
|
routeWaypoints.length === 0 ? 'Кликни точку старта' :
|
||||||
|
routeWaypoints.length === 1 ? 'Кликни точку финиша' : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─── Построение маршрута ──────────────────────────────────────────────────────
|
||||||
|
function debounceBuildRoute() {
|
||||||
|
clearTimeout(buildDebounceTimer);
|
||||||
|
buildDebounceTimer = setTimeout(buildRoute, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildRoute() {
|
||||||
|
if (routeWaypoints.length < 2) return;
|
||||||
|
const map = window._map;
|
||||||
|
const basePath = getBasePath();
|
||||||
|
|
||||||
|
document.getElementById('route-status').textContent = '⏳ Строю маршрут...';
|
||||||
|
const btn = document.getElementById('btn-build-route');
|
||||||
|
if (btn) btn.textContent = '⏳ Строю...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(basePath + '/api/route', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ waypoints: routeWaypoints, alternatives: 5 }),
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error('Маршрут не найден');
|
||||||
|
const data = await resp.json();
|
||||||
|
routeResults = data.routes || [];
|
||||||
|
if (!routeResults.length) throw new Error('Маршрут не найден');
|
||||||
|
|
||||||
|
// Убираем старые слои
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (map.getLayer('route-line-' + i)) map.removeLayer('route-line-' + i);
|
||||||
|
if (map.getLayer('route-line-' + i + '-outline')) map.removeLayer('route-line-' + i + '-outline');
|
||||||
|
if (map.getSource('route-' + i)) map.removeSource('route-' + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рисуем все маршруты
|
||||||
|
routeResults.forEach((route, i) => {
|
||||||
|
const color = ROUTE_COLORS[i] || '#888888';
|
||||||
|
const isActive = i === activeRouteIdx;
|
||||||
|
map.addSource('route-' + i, {
|
||||||
|
type: 'geojson',
|
||||||
|
data: { type: 'Feature', geometry: route.geometry, properties: {} }
|
||||||
|
});
|
||||||
|
// Обводка (белая) для активного
|
||||||
|
map.addLayer({
|
||||||
|
id: 'route-line-' + i + '-outline',
|
||||||
|
type: 'line',
|
||||||
|
source: 'route-' + i,
|
||||||
|
paint: {
|
||||||
|
'line-color': '#ffffff',
|
||||||
|
'line-width': isActive ? 7 : 4,
|
||||||
|
'line-opacity': isActive ? 0.6 : 0,
|
||||||
|
},
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' }
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: 'route-line-' + i,
|
||||||
|
type: 'line',
|
||||||
|
source: 'route-' + i,
|
||||||
|
paint: {
|
||||||
|
'line-color': color,
|
||||||
|
'line-width': isActive ? 5 : 3,
|
||||||
|
'line-opacity': isActive ? 0.95 : 0.5,
|
||||||
|
},
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Клик на линию маршрута
|
||||||
|
map.on('click', 'route-line-' + i, (e) => {
|
||||||
|
e.stopPropagation ? e.stopPropagation() : null;
|
||||||
|
selectRoute(i);
|
||||||
|
});
|
||||||
|
map.on('mouseenter', 'route-line-' + i, () => {
|
||||||
|
map.getCanvas().style.cursor = 'pointer';
|
||||||
|
highlightRoute(i);
|
||||||
|
});
|
||||||
|
map.on('mouseleave', 'route-line-' + i, () => {
|
||||||
|
map.getCanvas().style.cursor = routeMode ? 'crosshair' : '';
|
||||||
|
unhighlightRoute(i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
activeRouteIdx = 0;
|
||||||
|
renderRouteCards(routeResults);
|
||||||
|
document.getElementById('route-status').textContent = `✅ ${routeResults.length} маршрут(ов)`;
|
||||||
|
document.getElementById('route-actions').style.display = 'block';
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('route-status').textContent = '❌ ' + e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btn) btn.textContent = '🗺️ Построить маршрут';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Выбор и подсветка маршрутов ─────────────────────────────────────────────
|
||||||
|
function selectRoute(idx) {
|
||||||
|
activeRouteIdx = idx;
|
||||||
|
const map = window._map;
|
||||||
|
routeResults.forEach((_, i) => {
|
||||||
|
const isActive = i === idx;
|
||||||
|
if (map.getLayer('route-line-' + i)) {
|
||||||
|
map.setPaintProperty('route-line-' + i, 'line-width', isActive ? 5 : 3);
|
||||||
|
map.setPaintProperty('route-line-' + i, 'line-opacity', isActive ? 0.95 : 0.5);
|
||||||
|
}
|
||||||
|
if (map.getLayer('route-line-' + i + '-outline')) {
|
||||||
|
map.setPaintProperty('route-line-' + i + '-outline', 'line-width', isActive ? 7 : 4);
|
||||||
|
map.setPaintProperty('route-line-' + i + '-outline', 'line-opacity', isActive ? 0.6 : 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Обновляем CSS карточек
|
||||||
|
document.querySelectorAll('.route-card').forEach((card, i) => {
|
||||||
|
card.classList.toggle('active', i === idx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightRoute(idx) {
|
||||||
|
const map = window._map;
|
||||||
|
if (map.getLayer('route-line-' + idx)) {
|
||||||
|
map.setPaintProperty('route-line-' + idx, 'line-width', 7);
|
||||||
|
map.setPaintProperty('route-line-' + idx, 'line-opacity', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhighlightRoute(idx) {
|
||||||
|
const isActive = idx === activeRouteIdx;
|
||||||
|
const map = window._map;
|
||||||
|
if (map.getLayer('route-line-' + idx)) {
|
||||||
|
map.setPaintProperty('route-line-' + idx, 'line-width', isActive ? 5 : 3);
|
||||||
|
map.setPaintProperty('route-line-' + idx, 'line-opacity', isActive ? 0.95 : 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Карточки маршрутов ───────────────────────────────────────────────────────
|
||||||
|
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 timeStr = formatDuration(route.duration_s);
|
||||||
|
const isActive = i === activeRouteIdx;
|
||||||
|
|
||||||
|
let barHtml = '';
|
||||||
|
let summaryHtml = '';
|
||||||
|
let detailsHtml = '';
|
||||||
|
|
||||||
|
if (route.stats) {
|
||||||
|
const s = route.stats;
|
||||||
|
barHtml = `
|
||||||
|
<div class="route-coverage-bar">
|
||||||
|
<div style="width:${s.track_lev12_pct}%;background:#FFD700" title="Lev1-2: ${(s.track_lev12_m/1000).toFixed(1)} км"></div>
|
||||||
|
<div style="width:${s.track_lev345_pct}%;background:#FF4400" title="Lev3-5: ${(s.track_lev345_m/1000).toFixed(1)} км"></div>
|
||||||
|
<div style="width:${s.path_pct}%;background:#cc0000" title="Тропы: ${(s.path_m/1000).toFixed(1)} км"></div>
|
||||||
|
<div style="width:${s.asphalt_pct}%;background:#aaaaaa" title="Асфальт: ${(s.asphalt_m/1000).toFixed(1)} км"></div>
|
||||||
|
</div>`;
|
||||||
|
summaryHtml = `<div class="route-card-summary">${s.dirt_total_pct}% грунт · ${s.asphalt_pct}% асфальт</div>`;
|
||||||
|
detailsHtml = `
|
||||||
|
<div class="route-card-details" id="route-details-${i}" style="display:none;">
|
||||||
|
<div class="route-stat-row">🟡 Lev1-2 ${(s.track_lev12_m/1000).toFixed(1)} км ${s.track_lev12_pct}%</div>
|
||||||
|
<div class="route-stat-row">🔴 Lev3-5 ${(s.track_lev345_m/1000).toFixed(1)} км ${s.track_lev345_pct}%</div>
|
||||||
|
<div class="route-stat-row">🔴 Тропы ${(s.path_m/1000).toFixed(1)} км ${s.path_pct}%</div>
|
||||||
|
<div class="route-stat-row">⬜ Асфальт ${(s.asphalt_m/1000).toFixed(1)} км ${s.asphalt_pct}%</div>
|
||||||
|
<div style="margin-top:6px;display:flex;gap:6px;">
|
||||||
|
<button onclick="event.stopPropagation();downloadGPX()"
|
||||||
|
style="flex:1;padding:4px;background:#f0f0f0;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:11px;">
|
||||||
|
📥 GPX
|
||||||
|
</button>
|
||||||
|
<button onclick="event.stopPropagation();selectRoute(${i})"
|
||||||
|
style="flex:1;padding:4px;background:#ff6600;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">
|
||||||
|
Выбрать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="route-details-toggle" onclick="event.stopPropagation();toggleRouteDetails(${i})">Подробнее ▼</button>`;
|
||||||
|
} else {
|
||||||
|
detailsHtml = `
|
||||||
|
<div class="route-card-details" id="route-details-${i}" style="display:none;">
|
||||||
|
<div style="margin-top:6px;display:flex;gap:6px;">
|
||||||
|
<button onclick="event.stopPropagation();downloadGPX()"
|
||||||
|
style="flex:1;padding:4px;background:#f0f0f0;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:11px;">
|
||||||
|
📥 GPX
|
||||||
|
</button>
|
||||||
|
<button onclick="event.stopPropagation();selectRoute(${i})"
|
||||||
|
style="flex:1;padding:4px;background:#ff6600;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">
|
||||||
|
Выбрать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="route-details-toggle" onclick="event.stopPropagation();toggleRouteDetails(${i})">Подробнее ▼</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="route-card${isActive ? ' active' : ''}" id="route-card-${i}"
|
||||||
|
onclick="selectRoute(${i})"
|
||||||
|
onmouseenter="highlightRoute(${i})"
|
||||||
|
onmouseleave="unhighlightRoute(${i})">
|
||||||
|
<div class="route-card-header">
|
||||||
|
<span class="route-color-dot" style="background:${color}"></span>
|
||||||
|
<span class="route-card-title">Вариант ${i + 1}</span>
|
||||||
|
<span class="route-card-dist">${distKm} км</span>
|
||||||
|
<span class="route-card-time">${timeStr}</span>
|
||||||
|
</div>
|
||||||
|
${barHtml}
|
||||||
|
${summaryHtml}
|
||||||
|
${detailsHtml}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRouteDetails(idx) {
|
||||||
|
const details = document.getElementById('route-details-' + idx);
|
||||||
|
const btn = details ? details.nextElementSibling : null;
|
||||||
|
if (!details) return;
|
||||||
|
const isOpen = details.style.display !== 'none';
|
||||||
|
details.style.display = isOpen ? 'none' : 'block';
|
||||||
|
if (btn) btn.textContent = isOpen ? 'Подробнее ▼' : 'Свернуть ▲';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─── GPX экспорт ─────────────────────────────────────────────────────────────
|
||||||
|
function escapeXml(str) {
|
||||||
|
return (str || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadGPX() {
|
||||||
|
const route = routeResults[activeRouteIdx];
|
||||||
|
if (!route) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const dateStr = now.toISOString().slice(0, 10);
|
||||||
|
const timeStr = now.toISOString().replace(/[-:]/g, '').slice(0, 15);
|
||||||
|
const filename = `enduro-${timeStr}.gpx`;
|
||||||
|
|
||||||
|
const distKm = (route.distance_m / 1000).toFixed(1);
|
||||||
|
const dirtPct = route.stats ? route.stats.dirt_total_pct : '?';
|
||||||
|
|
||||||
|
// Waypoints: точки маршрута
|
||||||
|
const wpts = routeWaypoints.map((wp, i) => {
|
||||||
|
const name = i === 0 ? 'Старт' : i === routeWaypoints.length - 1 ? 'Финиш' : `Точка ${i}`;
|
||||||
|
return ` <wpt lat="${wp.lat}" lon="${wp.lon}"><name>${escapeXml(name)}</name></wpt>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавить флажки из localStorage
|
||||||
|
const markers = loadMarkers();
|
||||||
|
markers.forEach(m => {
|
||||||
|
wpts.push(` <wpt lat="${m.lat}" lon="${m.lon}"><name>${escapeXml(m.name)}</name><sym>${escapeXml(m.icon)}</sym></wpt>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Трек
|
||||||
|
const trkpts = route.geometry.coordinates.map(([lon, lat]) =>
|
||||||
|
` <trkpt lat="${lat}" lon="${lon}"/>`
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
const gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gpx version="1.1" creator="Enduro Trails" xmlns="http://www.topografix.com/GPX/1/1">
|
||||||
|
<metadata>
|
||||||
|
<name>Enduro route ${dateStr}</name>
|
||||||
|
<desc>${distKm} км · ${dirtPct}% грунт</desc>
|
||||||
|
<time>${now.toISOString()}</time>
|
||||||
|
</metadata>
|
||||||
|
${wpts.join('\n')}
|
||||||
|
<trk>
|
||||||
|
<name>Enduro route ${dateStr}</name>
|
||||||
|
<trkseg>
|
||||||
|
${trkpts}
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>`;
|
||||||
|
|
||||||
|
const blob = new Blob([gpx], { type: 'application/gpx+xml' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Флажки / именованные метки ──────────────────────────────────────────────
|
||||||
|
const MARKER_ICONS = ['🚩', '⛺', '🔧', '⛽', '💧', '📍'];
|
||||||
|
const MARKERS_KEY = 'enduro_markers';
|
||||||
|
let markerMode = false;
|
||||||
|
let namedMarkerObjects = {}; // id -> MapLibre Marker
|
||||||
|
|
||||||
|
function loadMarkers() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(MARKERS_KEY) || '[]');
|
||||||
|
} catch(e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMarkers(markers) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(MARKERS_KEY, JSON.stringify(markers));
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('localStorage недоступен');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMarkerMode() {
|
||||||
|
markerMode = !markerMode;
|
||||||
|
const btn = document.getElementById('btn-markers');
|
||||||
|
if (markerMode) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
window._map.getCanvas().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
window._map.getCanvas().style.cursor = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMarker(lngLat) {
|
||||||
|
const markers = loadMarkers();
|
||||||
|
if (markers.length >= 50) {
|
||||||
|
alert('Достигнут лимит 50 меток');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Простой диалог выбора иконки и имени
|
||||||
|
const iconChoice = promptIconChoice();
|
||||||
|
if (iconChoice === null) return; // отмена
|
||||||
|
|
||||||
|
const rawName = prompt('Название метки (Enter = автоимя):');
|
||||||
|
if (rawName === null) return; // отмена
|
||||||
|
const autoName = rawName.trim() || `Метка ${markers.length + 1}`;
|
||||||
|
|
||||||
|
const marker = {
|
||||||
|
id: Date.now(),
|
||||||
|
name: autoName,
|
||||||
|
icon: iconChoice,
|
||||||
|
lat: lngLat.lat,
|
||||||
|
lon: lngLat.lng
|
||||||
|
};
|
||||||
|
markers.push(marker);
|
||||||
|
saveMarkers(markers);
|
||||||
|
drawNamedMarker(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptIconChoice() {
|
||||||
|
const msg = 'Выберите иконку:\n' + MARKER_ICONS.map((ic, i) => `${i+1}. ${ic}`).join('\n') + '\n\nВведите номер (1-6) или Enter для 🚩:';
|
||||||
|
const input = prompt(msg);
|
||||||
|
if (input === null) return null;
|
||||||
|
const idx = parseInt(input, 10) - 1;
|
||||||
|
if (idx >= 0 && idx < MARKER_ICONS.length) return MARKER_ICONS[idx];
|
||||||
|
return MARKER_ICONS[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawNamedMarker(markerData) {
|
||||||
|
const map = window._map;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'named-marker-el';
|
||||||
|
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>
|
||||||
|
<div class="popup-row"><span class="popup-key">Координаты</span><span class="popup-val">${markerData.lat.toFixed(5)}, ${markerData.lon.toFixed(5)}</span></div>
|
||||||
|
<div style="margin-top:8px;display:flex;gap:6px;flex-wrap:wrap;">
|
||||||
|
<button onclick="useMarkerAsA(${markerData.id})" style="flex:1;padding:3px 6px;background:#00aa44;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">→ Точка A</button>
|
||||||
|
<button onclick="useMarkerAsB(${markerData.id})" style="flex:1;padding:3px 6px;background:#cc0000;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">→ Точка B</button>
|
||||||
|
<button onclick="removeMarker(${markerData.id})" style="flex:1;padding:3px 6px;background:#f0f0f0;border:1px solid #ccc;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);
|
||||||
|
|
||||||
|
el.addEventListener('click', () => mlMarker.togglePopup());
|
||||||
|
namedMarkerObjects[markerData.id] = mlMarker;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkers() {
|
||||||
|
const markers = loadMarkers();
|
||||||
|
markers.forEach(m => drawNamedMarker(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMarker(id) {
|
||||||
|
if (namedMarkerObjects[id]) {
|
||||||
|
namedMarkerObjects[id].remove();
|
||||||
|
delete namedMarkerObjects[id];
|
||||||
|
}
|
||||||
|
const markers = loadMarkers().filter(m => m.id !== id);
|
||||||
|
saveMarkers(markers);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMarkerAsA(id) {
|
||||||
|
const markers = loadMarkers();
|
||||||
|
const m = markers.find(x => x.id === id);
|
||||||
|
if (!m) return;
|
||||||
|
if (!routeMode) {
|
||||||
|
toggleRouteMode();
|
||||||
|
}
|
||||||
|
if (routeWaypoints.length === 0) {
|
||||||
|
routeWaypoints.push({ lon: m.lon, lat: m.lat });
|
||||||
|
} else {
|
||||||
|
routeWaypoints[0] = { lon: m.lon, lat: m.lat };
|
||||||
|
}
|
||||||
|
rebuildWaypointMarkers();
|
||||||
|
renderWaypointsList();
|
||||||
|
updateRouteActionsVisibility();
|
||||||
|
if (routeWaypoints.length >= 2) debounceBuildRoute();
|
||||||
|
// Закрыть попап
|
||||||
|
if (namedMarkerObjects[id]) namedMarkerObjects[id].getPopup().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMarkerAsB(id) {
|
||||||
|
const markers = loadMarkers();
|
||||||
|
const m = markers.find(x => x.id === id);
|
||||||
|
if (!m) return;
|
||||||
|
if (!routeMode) {
|
||||||
|
toggleRouteMode();
|
||||||
|
}
|
||||||
|
if (routeWaypoints.length === 0) {
|
||||||
|
routeWaypoints.push({ lon: m.lon, lat: m.lat });
|
||||||
|
routeWaypoints.push({ lon: m.lon, lat: m.lat });
|
||||||
|
} else if (routeWaypoints.length === 1) {
|
||||||
|
routeWaypoints.push({ lon: m.lon, lat: m.lat });
|
||||||
|
} else {
|
||||||
|
routeWaypoints[routeWaypoints.length - 1] = { lon: m.lon, lat: m.lat };
|
||||||
|
}
|
||||||
|
rebuildWaypointMarkers();
|
||||||
|
renderWaypointsList();
|
||||||
|
updateRouteActionsVisibility();
|
||||||
|
if (routeWaypoints.length >= 2) debounceBuildRoute();
|
||||||
|
if (namedMarkerObjects[id]) namedMarkerObjects[id].getPopup().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllMarkers() {
|
||||||
|
if (!confirm('Удалить все метки?')) return;
|
||||||
|
Object.values(namedMarkerObjects).forEach(m => m.remove());
|
||||||
|
namedMarkerObjects = {};
|
||||||
|
saveMarkers([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRouteActionsVisibility() {
|
||||||
|
document.getElementById('route-actions').style.display =
|
||||||
|
routeWaypoints.length >= 2 ? 'block' : 'none';
|
||||||
|
document.getElementById('route-status').textContent =
|
||||||
|
routeWaypoints.length === 0 ? 'Кликни точку старта' :
|
||||||
|
routeWaypoints.length === 1 ? 'Кликни точку финиша' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ─── Map init ─────────────────────────────────────────────────────────────────
|
// ─── Map init ─────────────────────────────────────────────────────────────────
|
||||||
async function initMap() {
|
async function initMap() {
|
||||||
const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || '';
|
const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || '';
|
||||||
@@ -112,6 +776,7 @@ async function initMap() {
|
|||||||
initRouteClicks(map);
|
initRouteClicks(map);
|
||||||
initRulerClicks(map);
|
initRulerClicks(map);
|
||||||
initSearch();
|
initSearch();
|
||||||
|
renderMarkers();
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on('error', (e) => {
|
map.on('error', (e) => {
|
||||||
@@ -159,6 +824,8 @@ async function initMap() {
|
|||||||
|
|
||||||
['trails-track', 'trails-path-bridleway', 'trails-asphalt'].forEach(layerId => {
|
['trails-track', 'trails-path-bridleway', 'trails-asphalt'].forEach(layerId => {
|
||||||
map.on('click', layerId, (e) => {
|
map.on('click', layerId, (e) => {
|
||||||
|
// Не показываем попап трека если активен режим маршрута или линейки
|
||||||
|
if (routeMode || rulerMode || markerMode) return;
|
||||||
const props = e.features[0].properties;
|
const props = e.features[0].properties;
|
||||||
const html = `
|
const html = `
|
||||||
<div class="popup-title">${props.name || 'Без названия'}</div>
|
<div class="popup-title">${props.name || 'Без названия'}</div>
|
||||||
@@ -170,11 +837,16 @@ async function initMap() {
|
|||||||
`;
|
`;
|
||||||
popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
||||||
});
|
});
|
||||||
map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer'; });
|
map.on('mouseenter', layerId, () => {
|
||||||
map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; });
|
if (!routeMode && !rulerMode && !markerMode) map.getCanvas().style.cursor = 'pointer';
|
||||||
|
});
|
||||||
|
map.on('mouseleave', layerId, () => {
|
||||||
|
if (!routeMode && !rulerMode && !markerMode) map.getCanvas().style.cursor = '';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on('click', 'poi-circles', (e) => {
|
map.on('click', 'poi-circles', (e) => {
|
||||||
|
if (routeMode || rulerMode || markerMode) return;
|
||||||
const props = e.features[0].properties;
|
const props = e.features[0].properties;
|
||||||
const html = `
|
const html = `
|
||||||
<div class="popup-title">${props.name || 'Без названия'}</div>
|
<div class="popup-title">${props.name || 'Без названия'}</div>
|
||||||
@@ -182,10 +854,15 @@ async function initMap() {
|
|||||||
`;
|
`;
|
||||||
popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
||||||
});
|
});
|
||||||
map.on('mouseenter', 'poi-circles', () => { map.getCanvas().style.cursor = 'pointer'; });
|
map.on('mouseenter', 'poi-circles', () => {
|
||||||
map.on('mouseleave', 'poi-circles', () => { map.getCanvas().style.cursor = ''; });
|
if (!routeMode && !rulerMode && !markerMode) map.getCanvas().style.cursor = 'pointer';
|
||||||
|
});
|
||||||
|
map.on('mouseleave', 'poi-circles', () => {
|
||||||
|
if (!routeMode && !rulerMode && !markerMode) map.getCanvas().style.cursor = '';
|
||||||
|
});
|
||||||
|
|
||||||
map.on('click', (e) => {
|
map.on('click', (e) => {
|
||||||
|
if (routeMode || rulerMode || markerMode) return;
|
||||||
const features = map.queryRenderedFeatures(e.point, {
|
const features = map.queryRenderedFeatures(e.point, {
|
||||||
layers: ['trails-track', 'trails-path-bridleway', 'trails-asphalt', 'poi-circles'],
|
layers: ['trails-track', 'trails-path-bridleway', 'trails-asphalt', 'poi-circles'],
|
||||||
});
|
});
|
||||||
@@ -206,91 +883,51 @@ async function checkDataAvailability() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Роутинг ──────────────────────────────────────────────────────────────────
|
// ─── Клики на карте (маршрут + метки) ────────────────────────────────────────
|
||||||
let routeMode = false;
|
|
||||||
let routeStart = null;
|
|
||||||
let routeEnd = null;
|
|
||||||
let routeMarkers = [];
|
|
||||||
|
|
||||||
function toggleRouteMode() {
|
|
||||||
routeMode = !routeMode;
|
|
||||||
const btn = document.getElementById('btn-route');
|
|
||||||
const panel = document.getElementById('route-panel');
|
|
||||||
if (routeMode) {
|
|
||||||
btn.classList.add('active');
|
|
||||||
panel.style.display = 'block';
|
|
||||||
clearRoute();
|
|
||||||
window._map.getCanvas().style.cursor = 'crosshair';
|
|
||||||
} else {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
panel.style.display = 'none';
|
|
||||||
clearRoute();
|
|
||||||
window._map.getCanvas().style.cursor = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearRoute() {
|
|
||||||
routeStart = null;
|
|
||||||
routeEnd = null;
|
|
||||||
routeMarkers.forEach(m => m.remove());
|
|
||||||
routeMarkers = [];
|
|
||||||
const map = window._map;
|
|
||||||
if (map.getLayer('route-line')) map.removeLayer('route-line');
|
|
||||||
if (map.getSource('route')) map.removeSource('route');
|
|
||||||
document.getElementById('route-status').textContent = 'Кликни точку старта';
|
|
||||||
document.getElementById('route-info').style.display = 'none';
|
|
||||||
if (routeMode) map.getCanvas().style.cursor = 'crosshair';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildRoute() {
|
|
||||||
const map = window._map;
|
|
||||||
document.getElementById('route-status').textContent = '⏳ Строю маршрут...';
|
|
||||||
const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || '';
|
|
||||||
try {
|
|
||||||
const resp = await fetch(
|
|
||||||
`${basePath}/api/route?from_lon=${routeStart[0]}&from_lat=${routeStart[1]}&to_lon=${routeEnd[0]}&to_lat=${routeEnd[1]}`
|
|
||||||
);
|
|
||||||
if (!resp.ok) throw new Error('Маршрут не найден');
|
|
||||||
const data = await resp.json();
|
|
||||||
if (map.getSource('route')) {
|
|
||||||
map.getSource('route').setData(data);
|
|
||||||
} else {
|
|
||||||
map.addSource('route', { type: 'geojson', data });
|
|
||||||
map.addLayer({
|
|
||||||
id: 'route-line',
|
|
||||||
type: 'line',
|
|
||||||
source: 'route',
|
|
||||||
paint: { 'line-color': '#0066ff', 'line-width': 4, 'line-opacity': 0.85 },
|
|
||||||
layout: { 'line-cap': 'round', 'line-join': 'round' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const p = data.properties;
|
|
||||||
document.getElementById('route-distance').textContent = `${p.distance_km} км`;
|
|
||||||
document.getElementById('route-duration').textContent = `~${p.duration_min} мин`;
|
|
||||||
document.getElementById('route-status').textContent = '✅ Готово';
|
|
||||||
document.getElementById('route-info').style.display = 'block';
|
|
||||||
} catch(e) {
|
|
||||||
document.getElementById('route-status').textContent = '❌ ' + e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initRouteClicks(map) {
|
function initRouteClicks(map) {
|
||||||
map.on('click', (e) => {
|
map.on('click', (e) => {
|
||||||
|
// Режим добавления метки
|
||||||
|
if (markerMode) {
|
||||||
|
addMarker(e.lngLat);
|
||||||
|
toggleMarkerMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!routeMode) return;
|
if (!routeMode) return;
|
||||||
|
|
||||||
const { lng, lat } = e.lngLat;
|
const { lng, lat } = e.lngLat;
|
||||||
if (!routeStart) {
|
|
||||||
routeStart = [lng, lat];
|
// Режим добавления промежуточной точки
|
||||||
const el = document.createElement('div');
|
if (addingWaypoint) {
|
||||||
el.style.cssText = 'width:14px;height:14px;background:#00aa00;border:2px solid #fff;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,0.3)';
|
addingWaypoint = false;
|
||||||
routeMarkers.push(new maplibregl.Marker({element: el}).setLngLat([lng, lat]).addTo(map));
|
map.getCanvas().style.cursor = 'crosshair';
|
||||||
|
// Вставляем перед последней точкой (B)
|
||||||
|
if (routeWaypoints.length >= 2) {
|
||||||
|
routeWaypoints.splice(routeWaypoints.length - 1, 0, { lon: lng, lat: lat });
|
||||||
|
} else {
|
||||||
|
routeWaypoints.push({ lon: lng, lat: lat });
|
||||||
|
}
|
||||||
|
rebuildWaypointMarkers();
|
||||||
|
renderWaypointsList();
|
||||||
|
updateRouteActionsVisibility();
|
||||||
|
if (routeWaypoints.length >= 2) debounceBuildRoute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обычный режим: A → B
|
||||||
|
if (routeWaypoints.length === 0) {
|
||||||
|
routeWaypoints.push({ lon: lng, lat: lat });
|
||||||
|
rebuildWaypointMarkers();
|
||||||
|
renderWaypointsList();
|
||||||
document.getElementById('route-status').textContent = 'Кликни точку финиша';
|
document.getElementById('route-status').textContent = 'Кликни точку финиша';
|
||||||
} else if (!routeEnd) {
|
} else if (routeWaypoints.length === 1) {
|
||||||
routeEnd = [lng, lat];
|
routeWaypoints.push({ lon: lng, lat: lat });
|
||||||
const el = document.createElement('div');
|
rebuildWaypointMarkers();
|
||||||
el.style.cssText = 'width:14px;height:14px;background:#cc0000;border:2px solid #fff;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,0.3)';
|
renderWaypointsList();
|
||||||
routeMarkers.push(new maplibregl.Marker({element: el}).setLngLat([lng, lat]).addTo(map));
|
updateRouteActionsVisibility();
|
||||||
buildRoute();
|
buildRoute();
|
||||||
}
|
}
|
||||||
|
// Если уже 2+ точек — клик ничего не делает (используй "+ Точка")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,14 +1047,12 @@ function addRulerPoint(lngLat) {
|
|||||||
const label = rulerPoints.length === 1 ? '0 м' :
|
const label = rulerPoints.length === 1 ? '0 м' :
|
||||||
rulerTotal >= 1 ? rulerTotal.toFixed(1) + ' км' : Math.round(rulerTotal * 1000) + ' м';
|
rulerTotal >= 1 ? rulerTotal.toFixed(1) + ' км' : Math.round(rulerTotal * 1000) + ' м';
|
||||||
|
|
||||||
// Кружок — anchor: center, строго 10×10
|
|
||||||
const dot = document.createElement('div');
|
const dot = document.createElement('div');
|
||||||
dot.style.cssText = 'width:10px;height:10px;background:#0088ff;border:2px solid #fff;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,0.3);';
|
dot.style.cssText = 'width:10px;height:10px;background:#0088ff;border:2px solid #fff;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,0.3);';
|
||||||
const dotMarker = new maplibregl.Marker({ element: dot, anchor: 'center' })
|
const dotMarker = new maplibregl.Marker({ element: dot, anchor: 'center' })
|
||||||
.setLngLat([lngLat.lng, lngLat.lat])
|
.setLngLat([lngLat.lng, lngLat.lat])
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
||||||
// Плашка — отдельный маркер, anchor: center, offset вверх на 20px
|
|
||||||
const labelEl = document.createElement('div');
|
const labelEl = document.createElement('div');
|
||||||
labelEl.style.cssText = 'display:inline-flex;align-items:center;gap:3px;background:rgba(0,0,0,0.75);color:#fff;font-size:11px;font-weight:600;padding:2px 6px;border-radius:3px;white-space:nowrap;';
|
labelEl.style.cssText = 'display:inline-flex;align-items:center;gap:3px;background:rgba(0,0,0,0.75);color:#fff;font-size:11px;font-weight:600;padding:2px 6px;border-radius:3px;white-space:nowrap;';
|
||||||
labelEl.innerHTML = `<span>${label}</span><span style="cursor:pointer;opacity:0.7;" onclick="event.stopPropagation();removeRulerPoint(${idx})">✕</span>`;
|
labelEl.innerHTML = `<span>${label}</span><span style="cursor:pointer;opacity:0.7;" onclick="event.stopPropagation();removeRulerPoint(${idx})">✕</span>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user