auto-sync: 2026-05-05 00:20:01
This commit is contained in:
320
tasks/enduro-trails/prototype/static/app.js
vendored
320
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -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 = '<span style="color:#888">Загружаю...</span>';
|
||||
|
||||
// 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 = `
|
||||
<div style="font-weight:600;margin-bottom:4px;">🛤 ${t.total_count || 0} грунтовок · ${t.total_km || 0} км</div>
|
||||
<div>🟡 Lev1-2: ${t.lev12_count || 0} шт · ${t.lev12_km || 0} км</div>
|
||||
<div>🔴 Lev3-5: ${t.lev345_count || 0} шт · ${t.lev345_km || 0} км</div>
|
||||
<div>🔴 Тропы: ${t.path_count || 0} шт · ${t.path_km || 0} км</div>
|
||||
<div style="margin-top:6px;border-top:1px solid #eee;padding-top:4px;">
|
||||
💧 Озёра: ${p['natural=water'] || 0} · 👁 Виды: ${p['tourism=viewpoint'] || 0}<br>
|
||||
🌊 Броды: ${p['ford=yes'] || 0} · 🏚 Руины: ${p['historic=ruins'] || 0}
|
||||
</div>
|
||||
`;
|
||||
} catch(e) {
|
||||
document.getElementById('recon-stats').innerHTML = '<span style="color:#c00">Ошибка загрузки</span>';
|
||||
}
|
||||
}
|
||||
|
||||
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 `<div class="scenic-poi-item">${icon} ${p.name || p.type}</div>`;
|
||||
}).join('');
|
||||
return `<div class="route-card ${i===activeScenicIdx?'active':''}" onclick="selectScenicRoute(${i})">
|
||||
<div class="route-card-header">
|
||||
<span class="route-color-dot" style="background:${col}"></span>
|
||||
<span class="route-card-title">${r.name || 'Вариант '+(i+1)}</span>
|
||||
<span class="route-card-dist">${km} км</span>
|
||||
<span class="route-card-time">${time}</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#666">${dirt}% грунт · score=${r.scenic_score||0}</div>
|
||||
${pois ? '<div style="margin-top:4px">'+pois+'</div>' : ''}
|
||||
</div>`;
|
||||
}).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();
|
||||
|
||||
Reference in New Issue
Block a user