auto-sync: 2026-05-05 00:20:01

This commit is contained in:
Stream
2026-05-05 00:20:01 +03:00
parent 4c94462f0e
commit f47d36ad60

View File

@@ -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();