diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js
index 491a9d4..bc5eda5 100644
--- a/tasks/enduro-trails/prototype/static/app.js
+++ b/tasks/enduro-trails/prototype/static/app.js
@@ -70,7 +70,7 @@ function locateMe() {
(pos) => {
const { longitude, latitude } = pos.coords;
const map = window._map;
- btn.textContent = '📍';
+ btn.textContent = '🎯';
map.flyTo({ center: [longitude, latitude], zoom: 13, duration: 800 });
if (locationMarker) {
locationMarker.setLngLat([longitude, latitude]);
@@ -84,7 +84,7 @@ function locateMe() {
}
},
(err) => {
- btn.textContent = '📍';
+ btn.textContent = '🎯';
alert('Не удалось определить местоположение: ' + err.message);
},
{ enableHighAccuracy: true, timeout: 10000 }
@@ -140,6 +140,8 @@ function toggleRouteMode() {
const btn = document.getElementById('btn-route');
const panel = document.getElementById('route-panel');
if (routeMode) {
+ deactivateAllModes();
+ routeMode = true; // deactivateAllModes выключил, включаем обратно
btn.classList.add('active');
panel.style.display = 'block';
clearRoute();
@@ -417,686 +419,4 @@ function selectRoute(idx) {
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 = `
-
`;
- summaryHtml = `${s.dirt_total_pct}% грунт · ${s.asphalt_pct}% асфальт
`;
- detailsHtml = `
-
-
🟡 Lev1-2 ${(s.track_lev12_m/1000).toFixed(1)} км ${s.track_lev12_pct}%
-
🔴 Lev3-5 ${(s.track_lev345_m/1000).toFixed(1)} км ${s.track_lev345_pct}%
-
🔴 Тропы ${(s.path_m/1000).toFixed(1)} км ${s.path_pct}%
-
⬜ Асфальт ${(s.asphalt_m/1000).toFixed(1)} км ${s.asphalt_pct}%
-
-
-
-
-
- `;
- } else {
- detailsHtml = `
-
-
-
-
-
-
- `;
- }
-
- return `
-
- ${barHtml}
- ${summaryHtml}
- ${detailsHtml}
-
`;
- }).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,'"');
-}
-
-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 ` ${escapeXml(name)}`;
- });
-
- // Добавить флажки из localStorage
- const markers = loadMarkers();
- markers.forEach(m => {
- wpts.push(` ${escapeXml(m.name)}${escapeXml(m.icon)}`);
- });
-
- // Трек
- const trkpts = route.geometry.coordinates.map(([lon, lat]) =>
- ` `
- ).join('\n');
-
- const gpx = `
-
-
- Enduro route ${dateStr}
- ${distKm} км · ${dirtPct}% грунт
-
-
-${wpts.join('\n')}
-
- Enduro route ${dateStr}
-
-${trkpts}
-
-
-`;
-
- 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(`
-
-
-
-
-
-
-
- `);
-
- 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 ─────────────────────────────────────────────────────────────────
-async function initMap() {
- const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || '';
- const tileBase = window.location.origin + basePath;
- const style = await fetch(basePath + '/style.json').then(r => r.json());
- style.sources['trails-tiles'].tiles = [`${tileBase}/api/tiles/{z}/{x}/{y}.mvt`];
-
- const map = new maplibregl.Map({
- container: 'map',
- style: style,
- center: [40.5, 55.5],
- zoom: 7,
- minZoom: 4,
- maxZoom: 18,
- hash: true,
- });
- window._map = map;
-
- map.addControl(new maplibregl.NavigationControl(), 'top-left');
- map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right');
- map.addControl(new maplibregl.FullscreenControl(), 'top-left');
-
- map.on('load', () => {
- document.getElementById('loading').classList.remove('visible');
- checkDataAvailability();
- initRouteClicks(map);
- initRulerClicks(map);
- initSearch();
- renderMarkers();
- });
-
- map.on('error', (e) => {
- console.error('Map error:', e.error?.message || e);
- document.getElementById('loading').classList.remove('visible');
- });
-
- setTimeout(() => {
- document.getElementById('loading').classList.remove('visible');
- }, 15000);
-
- map.on('zoom', () => {
- document.getElementById('zoom-val').textContent = map.getZoom().toFixed(1);
- });
-
- map.on('mousemove', (e) => {
- const { lng, lat } = e.lngLat;
- document.getElementById('coords-val').textContent =
- `${lat.toFixed(4)}, ${lng.toFixed(4)}`;
- });
-
- const popup = new maplibregl.Popup({
- closeButton: true,
- closeOnClick: false,
- maxWidth: '300px',
- });
-
- function formatLength(m) {
- if (!m) return '—';
- if (m >= 1000) return (m / 1000).toFixed(1) + ' км';
- return Math.round(m) + ' м';
- }
-
- function poiTypeLabel(t) {
- const labels = {
- 'natural=peak': '⛰ Вершина',
- 'natural=water': '💧 Вода',
- 'tourism=viewpoint': '👁 Смотровая',
- 'historic=ruins': '🏙 Руины',
- 'natural=cave_entrance': '🕳 Пещера',
- 'ford=yes': '🌊 Брод',
- };
- return labels[t] || t;
- }
-
- ['trails-track', 'trails-path-bridleway', 'trails-asphalt'].forEach(layerId => {
- map.on('click', layerId, (e) => {
- // Не показываем попап трека если активен режим маршрута или линейки
- if (routeMode || rulerMode || markerMode) return;
- const props = e.features[0].properties;
- const html = `
-
-
-
-
-
- ${props.mtb_scale ? `` : ''}
- `;
- popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
- });
- map.on('mouseenter', layerId, () => {
- 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) => {
- if (routeMode || rulerMode || markerMode) return;
- const props = e.features[0].properties;
- const html = `
-
-
- `;
- popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
- });
- map.on('mouseenter', 'poi-circles', () => {
- 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) => {
- if (routeMode || rulerMode || markerMode) return;
- const features = map.queryRenderedFeatures(e.point, {
- layers: ['trails-track', 'trails-path-bridleway', 'trails-asphalt', 'poi-circles'],
- });
- if (!features.length) popup.remove();
- });
-}
-
-async function checkDataAvailability() {
- try {
- const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || '';
- const resp = await fetch(basePath + '/api/health');
- const data = await resp.json();
- if (!data.db_exists) {
- document.getElementById('no-data-warning').classList.add('visible');
- }
- } catch (e) {
- console.warn('Health check failed:', e);
- }
-}
-
-// ─── Клики на карте (маршрут + метки) ────────────────────────────────────────
-function initRouteClicks(map) {
- map.on('click', (e) => {
- // Режим добавления метки
- if (markerMode) {
- addMarker(e.lngLat);
- toggleMarkerMode();
- return;
- }
-
- if (!routeMode) return;
-
- const { lng, lat } = e.lngLat;
-
- // Режим добавления промежуточной точки
- if (addingWaypoint) {
- addingWaypoint = false;
- 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 = 'Кликни точку финиша';
- } else if (routeWaypoints.length === 1) {
- routeWaypoints.push({ lon: lng, lat: lat });
- rebuildWaypointMarkers();
- renderWaypointsList();
- updateRouteActionsVisibility();
- buildRoute();
- }
- // Если уже 2+ точек — клик ничего не делает (используй "+ Точка")
- });
-}
-
-// ─── Поиск (Nominatim) ────────────────────────────────────────────────────────
-let searchTimeout = null;
-
-function initSearch() {
- const input = document.getElementById('search-input');
- const results = document.getElementById('search-results');
- input.addEventListener('input', () => {
- clearTimeout(searchTimeout);
- const q = input.value.trim();
- if (q.length < 2) { results.style.display = 'none'; return; }
- searchTimeout = setTimeout(() => doSearch(q), 400);
- });
- input.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') { results.style.display = 'none'; input.blur(); }
- });
- document.addEventListener('click', (e) => {
- if (!e.target.closest('#search-box')) results.style.display = 'none';
- });
-}
-
-async function doSearch(query) {
- const results = document.getElementById('search-results');
- results.innerHTML = 'Поиск...
';
- results.style.display = 'block';
- try {
- const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=6&countrycodes=ru&accept-language=ru`;
- const resp = await fetch(url, { headers: { 'Accept-Language': 'ru' } });
- const data = await resp.json();
- if (!data.length) {
- results.innerHTML = 'Ничего не найдено
';
- return;
- }
- results.innerHTML = data.map((item) => {
- const name = item.display_name.split(',')[0];
- const detail = item.display_name.split(',').slice(1, 3).join(',').trim();
- return ``;
- }).join('');
- } catch(e) {
- results.innerHTML = 'Ошибка поиска
';
- }
-}
-
-function selectSearchResult(lat, lon, name) {
- window._map.flyTo({ center: [lon, lat], zoom: 13, duration: 800 });
- document.getElementById('search-results').style.display = 'none';
- document.getElementById('search-input').value = name;
-}
-
-// ─── Линейка ──────────────────────────────────────────────────────────────────
-let rulerMode = false;
-let rulerPoints = [];
-let rulerMarkers = [];
-let rulerTotal = 0;
-
-function toggleRuler() {
- rulerMode = !rulerMode;
- const btn = document.getElementById('btn-ruler');
- if (rulerMode) {
- btn.classList.add('active');
- window._map.getCanvas().style.cursor = 'crosshair';
- clearRuler();
- } else {
- btn.classList.remove('active');
- window._map.getCanvas().style.cursor = '';
- clearRuler();
- }
-}
-
-function clearRuler() {
- rulerPoints = [];
- rulerTotal = 0;
- rulerMarkers.forEach(m => m.remove());
- rulerMarkers = [];
- const map = window._map;
- if (map.getLayer('ruler-line')) map.removeLayer('ruler-line');
- if (map.getSource('ruler')) map.removeSource('ruler');
-}
-
-function haversineKm(a, b) {
- const R = 6371;
- const dLat = (b[1] - a[1]) * Math.PI / 180;
- const dLon = (b[0] - a[0]) * Math.PI / 180;
- const s = Math.sin(dLat/2)**2 + Math.cos(a[1]*Math.PI/180) * Math.cos(b[1]*Math.PI/180) * Math.sin(dLon/2)**2;
- return R * 2 * Math.atan2(Math.sqrt(s), Math.sqrt(1-s));
-}
-
-function updateRulerLine() {
- const map = window._map;
- const geojson = { type: 'Feature', geometry: { type: 'LineString', coordinates: rulerPoints } };
- if (map.getSource('ruler')) {
- map.getSource('ruler').setData(geojson);
- } else {
- map.addSource('ruler', { type: 'geojson', data: geojson });
- map.addLayer({
- id: 'ruler-line', type: 'line', source: 'ruler',
- paint: { 'line-color': '#0088ff', 'line-width': 2, 'line-dasharray': [4, 2], 'line-opacity': 0.9 }
- });
- }
-}
-
-function addRulerPoint(lngLat) {
- const map = window._map;
- const pt = [lngLat.lng, lngLat.lat];
- const idx = rulerPoints.length;
- rulerPoints.push(pt);
-
- if (rulerPoints.length > 1) {
- rulerTotal += haversineKm(rulerPoints[rulerPoints.length - 2], pt);
- }
-
- const label = rulerPoints.length === 1 ? '0 м' :
- rulerTotal >= 1 ? rulerTotal.toFixed(1) + ' км' : Math.round(rulerTotal * 1000) + ' м';
-
- 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);';
- const dotMarker = new maplibregl.Marker({ element: dot, anchor: 'center' })
- .setLngLat([lngLat.lng, lngLat.lat])
- .addTo(map);
-
- 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.innerHTML = `${label}✕`;
- const labelMarker = new maplibregl.Marker({ element: labelEl, anchor: 'center', offset: [0, -20] })
- .setLngLat([lngLat.lng, lngLat.lat])
- .addTo(map);
-
- rulerMarkers.push(dotMarker, labelMarker);
- updateRulerLine();
-}
-
-function removeRulerPoint(idx) {
- if (idx < 0 || idx >= rulerPoints.length) return;
- rulerPoints.splice(idx, 1);
- rulerMarkers.forEach(m => m.remove());
- rulerMarkers = [];
- rulerTotal = 0;
- const pts = [...rulerPoints];
- rulerPoints = [];
- pts.forEach(pt => addRulerPoint({ lng: pt[0], lat: pt[1] }));
-}
-
-function initRulerClicks(map) {
- map.on('click', (e) => {
- if (!rulerMode) return;
- addRulerPoint(e.lngLat);
- });
- map.on('dblclick', (e) => {
- if (!rulerMode) return;
- e.preventDefault();
- toggleRuler();
- });
-}
-
-initMap();
+ map.setPaint
\ No newline at end of file