From f47d36ad6041decdcaa0ee6af685f867eef256e6 Mon Sep 17 00:00:00 2001 From: Stream Date: Tue, 5 May 2026 00:20:01 +0300 Subject: [PATCH] auto-sync: 2026-05-05 00:20:01 --- tasks/enduro-trails/prototype/static/app.js | 320 ++++++++++++++++++++ 1 file changed, 320 insertions(+) diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js index f57ed70..41c4e99 100644 --- a/tasks/enduro-trails/prototype/static/app.js +++ b/tasks/enduro-trails/prototype/static/app.js @@ -1142,4 +1142,324 @@ function initRulerClicks(map) { }); } +// ─── Фаза 4: Разведка ─────────────────────────────────────────────────────── +let reconMode = false; +let reconCenter = null; +let reconRadius = 20; + +function toggleReconMode() { + reconMode = !reconMode; + const btn = document.getElementById('btn-recon'); + if (reconMode) { + deactivateAllModes(); + reconMode = true; + btn.classList.add('active'); + window._map.getCanvas().style.cursor = 'crosshair'; + } else { + btn.classList.remove('active'); + window._map.getCanvas().style.cursor = ''; + clearRecon(); + } +} + +function makeCircleGeoJSON(lon, lat, radiusKm) { + const coords = []; + for (let i = 0; i <= 64; i++) { + const a = (2 * Math.PI * i) / 64; + const dlat = (radiusKm / 111) * Math.cos(a); + const dlon = (radiusKm / (111 * Math.cos(lat * Math.PI / 180))) * Math.sin(a); + coords.push([lon + dlon, lat + dlat]); + } + return { type: 'Feature', geometry: { type: 'Polygon', coordinates: [coords] }, properties: {} }; +} + +async function doRecon(lon, lat) { + reconCenter = [lon, lat]; + const map = window._map; + const panel = document.getElementById('recon-panel'); + panel.style.display = 'block'; + document.getElementById('recon-stats').innerHTML = 'Загружаю...'; + + // Draw circle + const circle = makeCircleGeoJSON(lon, lat, reconRadius); + if (map.getSource('recon-circle')) { + map.getSource('recon-circle').setData(circle); + } else { + map.addSource('recon-circle', { type: 'geojson', data: circle }); + map.addLayer({ + id: 'recon-circle-fill', type: 'fill', source: 'recon-circle', + paint: { 'fill-color': '#ff6600', 'fill-opacity': 0.08 } + }); + map.addLayer({ + id: 'recon-circle-stroke', type: 'line', source: 'recon-circle', + paint: { 'line-color': '#ff6600', 'line-width': 2, 'line-opacity': 0.5 } + }); + } + + // API call + const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || ''; + try { + const resp = await fetch(`${basePath}/api/recon`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ lon, lat, radius_km: reconRadius }) + }); + const data = await resp.json(); + const t = data.trails || {}; + const p = data.poi || {}; + document.getElementById('recon-stats').innerHTML = ` +
🛤 ${t.total_count || 0} грунтовок · ${t.total_km || 0} км
+
🟡 Lev1-2: ${t.lev12_count || 0} шт · ${t.lev12_km || 0} км
+
🔴 Lev3-5: ${t.lev345_count || 0} шт · ${t.lev345_km || 0} км
+
🔴 Тропы: ${t.path_count || 0} шт · ${t.path_km || 0} км
+
+ 💧 Озёра: ${p['natural=water'] || 0} · 👁 Виды: ${p['tourism=viewpoint'] || 0}
+ 🌊 Броды: ${p['ford=yes'] || 0} · 🏚 Руины: ${p['historic=ruins'] || 0} +
+ `; + } catch(e) { + document.getElementById('recon-stats').innerHTML = 'Ошибка загрузки'; + } +} + +function setReconRadius(km) { + reconRadius = km; + document.querySelectorAll('.recon-radius-btn').forEach(b => { + b.classList.toggle('active', +b.dataset.km === km); + }); + if (reconCenter) doRecon(reconCenter[0], reconCenter[1]); +} + +function clearRecon() { + const map = window._map; + if (map.getLayer('recon-circle-fill')) map.removeLayer('recon-circle-fill'); + if (map.getLayer('recon-circle-stroke')) map.removeLayer('recon-circle-stroke'); + if (map.getSource('recon-circle')) map.removeSource('recon-circle'); + document.getElementById('recon-panel').style.display = 'none'; + reconCenter = null; +} + +// ─── Фаза 4: Связка ─────────────────────────────────────────────────────────── +let linkMode = false; +let linkPoints = []; +let linkMarkers = []; + +function toggleLinkMode() { + linkMode = !linkMode; + const btn = document.getElementById('btn-link'); + if (linkMode) { + deactivateAllModes(); + linkMode = true; + btn.classList.add('active'); + window._map.getCanvas().style.cursor = 'crosshair'; + document.getElementById('route-panel').style.display = 'block'; + document.querySelector('#route-panel > div:first-child').textContent = '🔗 Связка'; + document.getElementById('route-status').textContent = 'Кликни первую точку'; + document.getElementById('route-info').style.display = 'none'; + document.getElementById('route-actions').style.display = 'none'; + } else { + btn.classList.remove('active'); + window._map.getCanvas().style.cursor = ''; + clearLink(); + } +} + +function addLinkPoint(lng, lat) { + const map = window._map; + linkPoints.push({ lon: lng, lat: lat }); + const idx = linkPoints.length; + const el = document.createElement('div'); + el.style.cssText = 'width:16px;height:16px;background:#ff6600;border:2px solid #fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:#fff;'; + el.textContent = idx; + const marker = new maplibregl.Marker({ element: el, anchor: 'center', draggable: true }).setLngLat([lng, lat]).addTo(map); + linkMarkers.push(marker); + + if (idx === 1) { + document.getElementById('route-status').textContent = 'Кликни вторую точку'; + } else if (idx >= 2) { + buildLinkRoute(); + } +} + +async function buildLinkRoute() { + const map = window._map; + document.getElementById('route-status').textContent = '⏳ Ищу связку...'; + const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || ''; + try { + const resp = await fetch(`${basePath}/api/route`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ waypoints: linkPoints, alternatives: 3 }) + }); + if (!resp.ok) throw new Error('Не найдена'); + const data = await resp.json(); + if (data.routes && data.routes.length > 0) { + renderRouteCards(data.routes, 'link'); + document.getElementById('route-status').textContent = '✅ Связка найдена'; + } else { + document.getElementById('route-status').textContent = '❌ Грунтовая связка не найдена'; + } + } catch(e) { + document.getElementById('route-status').textContent = '❌ ' + e.message; + } +} + +function clearLink() { + linkPoints = []; + linkMarkers.forEach(m => m.remove()); + linkMarkers = []; + const map = window._map; + // Remove route layers + for (let i = 0; i < 5; i++) { + const lid = `link-line-${i}`; + if (map.getLayer(lid)) map.removeLayer(lid); + const sid = `link-src-${i}`; + if (map.getSource(sid)) map.removeSource(sid); + } + document.getElementById('route-panel').style.display = 'none'; + document.getElementById('route-cards').innerHTML = ''; +} + +// ─── Фаза 4: Красивый маршрут ──────────────────────────────────────────────── +let scenicMode = false; +let scenicStart = null; +let scenicStartMarker = null; +let scenicTargetKm = 100; +let scenicRoutes = []; +let activeScenicIdx = 0; + +function toggleScenicMode() { + scenicMode = !scenicMode; + const btn = document.getElementById('btn-scenic'); + if (scenicMode) { + deactivateAllModes(); + scenicMode = true; + btn.classList.add('active'); + window._map.getCanvas().style.cursor = 'crosshair'; + document.getElementById('scenic-panel').style.display = 'block'; + document.getElementById('scenic-status').textContent = 'Кликни точку старта на карте'; + } else { + btn.classList.remove('active'); + window._map.getCanvas().style.cursor = ''; + clearScenic(); + } +} + +function setScenicDist(km) { + scenicTargetKm = km; + document.querySelectorAll('.scenic-dist-btn').forEach(b => { + b.classList.toggle('active', +b.textContent === km); + }); + const inp = document.getElementById('scenic-dist-input'); + if (inp) inp.value = km; +} + +async function buildScenicRoute() { + if (!scenicStart) return; + const map = window._map; + document.getElementById('scenic-status').textContent = '⏳ Строю красивый маршрут...'; + document.getElementById('btn-build-scenic').textContent = '⏳ Строю...'; + document.getElementById('btn-build-scenic').disabled = true; + + const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || ''; + try { + const resp = await fetch(`${basePath}/api/scenic`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ lon: scenicStart.lon, lat: scenicStart.lat, target_km: scenicTargetKm }) + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + throw new Error(err.detail || 'Ошибка'); + } + const data = await resp.json(); + scenicRoutes = data.routes || []; + + if (scenicRoutes.length === 0) throw new Error('Маршрут не найден'); + + // Draw routes on map + const colors = ['#0066ff', '#00aa44', '#9933cc']; + scenicRoutes.forEach((r, i) => { + const geojson = { type: 'Feature', geometry: r.geometry, properties: {} }; + const sid = `scenic-src-${i}`; + const lid = `scenic-line-${i}`; + if (map.getSource(sid)) map.removeSource(sid); + if (map.getLayer(lid)) map.removeLayer(lid); + map.addSource(sid, { type: 'geojson', data: geojson }); + map.addLayer({ + id: lid, type: 'line', source: sid, + paint: { + 'line-color': colors[i % colors.length], + 'line-width': i === activeScenicIdx ? 5 : 3, + 'line-opacity': i === activeScenicIdx ? 0.9 : 0.5, + }, + layout: { 'line-cap': 'round', 'line-join': 'round' } + }); + }); + + // Show cards + const cardsEl = document.getElementById('scenic-cards'); + if (cardsEl) { + cardsEl.innerHTML = scenicRoutes.map((r, i) => { + const col = colors[i % colors.length]; + const km = (r.distance_m / 1000).toFixed(0); + const time = formatDuration(r.duration_s); + const dirt = r.stats?.dirt_total_pct || '?'; + const pois = (r.scenic_pois || []).map(p => { + const icon = {'natural=water':'💧','tourism=viewpoint':'👁','historic=ruins':'🏚','natural=peak':'🔺','natural=cave_entrance':'🕳','ford=yes':'🌊'}[p.type] || '📍'; + return `
${icon} ${p.name || p.type}
`; + }).join(''); + return `
+
+ + ${r.name || 'Вариант '+(i+1)} + ${km} км + ${time} +
+
${dirt}% грунт · score=${r.scenic_score||0}
+ ${pois ? '
'+pois+'
' : ''} +
`; + }).join(''); + } + + document.getElementById('scenic-status').textContent = `✅ ${scenicRoutes.length} маршрут(ов)`; + } catch(e) { + document.getElementById('scenic-status').textContent = '❌ ' + e.message; + } + document.getElementById('btn-build-scenic').textContent = '🎨 Построить маршрут'; + document.getElementById('btn-build-scenic').disabled = false; +} + +function selectScenicRoute(idx) { + activeScenicIdx = idx; + const map = window._map; + scenicRoutes.forEach((_, i) => { + const lid = `scenic-line-${i}`; + if (map.getLayer(lid)) { + map.setLayoutProperty(lid, 'visibility', 'visible'); + map.setPaintProperty(lid, 'line-width', i === idx ? 5 : 3); + map.setPaintProperty(lid, 'line-opacity', i === idx ? 0.9 : 0.5); + } + }); + document.querySelectorAll('#scenic-cards .route-card').forEach((c, i) => { + c.classList.toggle('active', i === idx); + }); +} + +function clearScenic() { + const map = window._map; + scenicRoutes.forEach((_, i) => { + const lid = `scenic-line-${i}`; + const sid = `scenic-src-${i}`; + if (map.getLayer(lid)) map.removeLayer(lid); + if (map.getSource(sid)) map.removeSource(sid); + }); + if (scenicStartMarker) { scenicStartMarker.remove(); scenicStartMarker = null; } + scenicStart = null; + scenicRoutes = []; + document.getElementById('scenic-panel').style.display = 'none'; + const cardsEl = document.getElementById('scenic-cards'); + if (cardsEl) cardsEl.innerHTML = ''; +} + initMap();