From 17d383ddc621521480d2ad7e820c565cbeecb1dc Mon Sep 17 00:00:00 2001 From: grigo Date: Thu, 11 Jun 2026 08:46:49 +0300 Subject: [PATCH] added bind --- server/static/index.html | 220 +++++++++++++++++++++++++++++++++++---- 1 file changed, 199 insertions(+), 21 deletions(-) diff --git a/server/static/index.html b/server/static/index.html index 1d8b951..66915a8 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -150,7 +150,7 @@ background: none; border: none; color: #eee; font-size: 1.1rem; cursor: pointer; padding: 0 4px; line-height: 1; } - #mapRulerCanvas { width: 100%; height: 120px; display: block; background: #0a0a14; border-radius: 4px; } + #mapRulerCanvas { width: 100%; height: 120px; display: block; background: #0a0a14; border-radius: 4px; cursor: crosshair; } #mapRulerTools { display: flex; gap: 4px; margin-bottom: 6px; flex-wrap: wrap; } #mapRulerTools button { padding: 3px 8px; font-size: 0.7rem; border: 1px solid #444; border-radius: 4px; @@ -359,8 +359,15 @@ let mapRulerPtB = null; let mapRulerLoadState = 'idle'; let mapRulerLineLayer = null; + let mapRulerHitLayer = null; let mapRulerMarkerA = null; let mapRulerMarkerB = null; + let mapRulerCursorDist = null; + let mapRulerCursorMarker = null; + let mapRulerBaseStatus = ''; + let mapRulerChartHover = false; + let mapRulerLineHover = false; + let mapRulerLeaveTimer = null; const DEVICE_POLL_MS = 1000; const CHAT_POLL_MS = 2500; @@ -521,6 +528,57 @@ return { tx, rx }; } + function lineLengthM(a, b) { + return haversineM(a.lat, a.lon, b.lat, b.lon); + } + + function latLonAtLineDist(a, b, distM) { + const total = lineLengthM(a, b); + if (total < 1e-3) return { lat: a.lat, lon: a.lon }; + const f = Math.min(1, Math.max(0, distM / total)); + return { + lat: a.lat + (b.lat - a.lat) * f, + lon: a.lon + (b.lon - a.lon) * f + }; + } + + function projectPointToLineDist(a, b, lat, lon) { + const total = lineLengthM(a, b); + if (total < 1e-3) return 0; + const ax = a.lon; + const ay = a.lat; + const bx = b.lon; + const by = b.lat; + const dx = bx - ax; + const dy = by - ay; + const len2 = dx * dx + dy * dy; + if (len2 < 1e-15) return 0; + let t = ((lon - ax) * dx + (lat - ay) * dy) / len2; + t = Math.max(0, Math.min(1, t)); + const projLat = ay + t * dy; + const projLon = ax + t * dx; + return haversineM(a.lat, a.lon, projLat, projLon); + } + + function elevationAtDist(profile, distM) { + if (!profile?.points?.length || distM == null) return null; + const pts = profile.points.filter(p => p.elevation_m != null); + if (!pts.length) return null; + if (distM <= pts[0].dist_m) return pts[0].elevation_m; + const last = pts[pts.length - 1]; + if (distM >= last.dist_m) return last.elevation_m; + for (let i = 1; i < pts.length; i++) { + const p0 = pts[i - 1]; + const p1 = pts[i]; + if (distM <= p1.dist_m) { + const span = p1.dist_m - p0.dist_m; + const t = span <= 0 ? 0 : (distM - p0.dist_m) / span; + return p0.elevation_m + (p1.elevation_m - p0.elevation_m) * t; + } + } + return null; + } + function buildDirectLinePoints(tx, rx, stepM = 10) { const total = haversineM(tx.lat, tx.lon, rx.lat, rx.lon); if (total < 1) { @@ -556,23 +614,85 @@ if (el) el.textContent = text || ''; } - function updateMapRulerLineLayer(a, b) { - if (mapRulerLineLayer) { - map.removeLayer(mapRulerLineLayer); - mapRulerLineLayer = null; - } - if (!mapRulerOpen || !a || !b) return; - mapRulerLineLayer = L.polyline( - [[a.lat, a.lon], [b.lat, b.lon]], - { color: '#00ff88', weight: 3, dashArray: '8,6', opacity: 0.85 } - ).addTo(map); - } - function clearMapRulerLineLayer() { if (mapRulerLineLayer) { map.removeLayer(mapRulerLineLayer); mapRulerLineLayer = null; } + if (mapRulerHitLayer) { + map.removeLayer(mapRulerHitLayer); + mapRulerHitLayer = null; + } + } + + function clearMapRulerCursorMarker() { + if (mapRulerCursorMarker) { + map.removeLayer(mapRulerCursorMarker); + mapRulerCursorMarker = null; + } + } + + function scheduleClearMapRulerCursor() { + clearTimeout(mapRulerLeaveTimer); + mapRulerLeaveTimer = setTimeout(() => { + if (mapRulerChartHover || mapRulerLineHover) return; + mapRulerCursorDist = null; + clearMapRulerCursorMarker(); + setMapRulerStatus(mapRulerBaseStatus); + drawMapRulerChart(); + }, 60); + } + + function updateMapRulerCursorMarker(pt) { + clearMapRulerCursorMarker(); + mapRulerCursorMarker = L.circleMarker([pt.lat, pt.lon], { + radius: 8, + color: '#fff', + weight: 2, + fillColor: '#00ff88', + fillOpacity: 0.95, + interactive: false + }).addTo(map); + } + + function setMapRulerCursor(distM) { + if (!mapRulerPtA || !mapRulerPtB || elevationPointCount(elevProfileMapLine) === 0) return; + const total = elevProfileMapLine.total_m || lineLengthM(mapRulerPtA, mapRulerPtB); + mapRulerCursorDist = Math.max(0, Math.min(distM, total)); + const pt = latLonAtLineDist(mapRulerPtA, mapRulerPtB, mapRulerCursorDist); + updateMapRulerCursorMarker(pt); + const elev = elevationAtDist(elevProfileMapLine, mapRulerCursorDist); + if (elev != null) { + setMapRulerStatus(`${mapRulerCursorDist.toFixed(0)} m · ${elev.toFixed(1)} m`); + } + drawMapRulerChart(); + } + + function clearMapRulerCursor() { + mapRulerCursorDist = null; + clearMapRulerCursorMarker(); + } + + function updateMapRulerLineLayer(a, b) { + clearMapRulerLineLayer(); + if (!mapRulerOpen || !a || !b) return; + mapRulerLineLayer = L.polyline( + [[a.lat, a.lon], [b.lat, b.lon]], + { color: '#00ff88', weight: 3, dashArray: '8,6', opacity: 0.85, interactive: false } + ).addTo(map); + mapRulerHitLayer = L.polyline( + [[a.lat, a.lon], [b.lat, b.lon]], + { color: '#000', weight: 22, opacity: 0, interactive: true } + ).addTo(map); + mapRulerHitLayer.on('mousemove', e => { + mapRulerLineHover = true; + clearTimeout(mapRulerLeaveTimer); + setMapRulerCursor(projectPointToLineDist(a, b, e.latlng.lat, e.latlng.lng)); + }); + mapRulerHitLayer.on('mouseout', () => { + mapRulerLineHover = false; + scheduleClearMapRulerCursor(); + }); } function clearMapRulerPickPoints() { @@ -587,6 +707,8 @@ mapRulerMarkerB = null; } clearMapRulerLineLayer(); + clearMapRulerCursor(); + mapRulerBaseStatus = ''; elevProfileMapLine = null; } @@ -595,13 +717,17 @@ document.getElementById('btnRulerPick').classList.toggle('active', mode === 'pick'); document.getElementById('btnRulerAutoTxRx').classList.toggle('active', mode === 'auto'); if (mode === 'pick') { - setMapRulerHint(mapRulerPtB - ? 'A и B заданы — клик сбрасывает и задаёт новую A' - : mapRulerPtA - ? 'Клик на карте — точка B' - : 'Клик на карте — точка A, затем точка B'); + setMapRulerHint(mapRulerPtB && elevationPointCount(elevProfileMapLine) > 0 + ? 'Наведите на линию или график — высота и позиция' + : mapRulerPtB + ? 'A и B заданы — клик сбрасывает и задаёт новую A' + : mapRulerPtA + ? 'Клик на карте — точка B' + : 'Клик на карте — точка A, затем точка B'); } else { - setMapRulerHint('Линия между устройствами TX и RX (обновляется при poll)'); + setMapRulerHint(elevationPointCount(elevProfileMapLine) > 0 + ? 'Наведите на линию или график — высота и позиция' + : 'Линия между устройствами TX и RX (обновляется при poll)'); } } @@ -632,6 +758,7 @@ async function loadMapRulerProfileFromPoints(a, b) { if (!a || !b) return; mapRulerLoadState = 'loading'; + clearMapRulerCursor(); setMapRulerStatus('загрузка…'); updateMapRulerLineLayer(a, b); drawMapRulerChart(); @@ -644,8 +771,11 @@ const src = elevProfileMapLine.source === 'elevation' ? 'высоты' : elevProfileMapLine.source === 'server' ? 'сервер' : elevProfileMapLine.source || 'данные'; - setMapRulerStatus(`${dist.toFixed(0)} m · ${src} · ${n} точек`); + mapRulerBaseStatus = `${dist.toFixed(0)} m · ${src} · ${n} точек`; + setMapRulerStatus(mapRulerBaseStatus); + setMapRulerHint('Наведите на линию или график — высота и позиция'); } else { + mapRulerBaseStatus = ''; setMapRulerStatus(`${dist.toFixed(0)} m · ${elevProfileMapLine?.api_error || 'нет данных'}`); } drawMapRulerChart(); @@ -991,6 +1121,7 @@ ctx.stroke(); if (s.cursor != null && maxDist > 0) { const cx = margin.l + (s.cursor / maxDist) * plotW; + const elev = elevationAtDist(s.profile, s.cursor); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.setLineDash([4, 3]); @@ -999,8 +1130,29 @@ ctx.lineTo(cx, margin.t + plotH); ctx.stroke(); ctx.setLineDash([]); + if (elev != null && isFinite(elev)) { + const cy = margin.t + plotH - ((elev - minE) / (maxE - minE)) * plotH; + ctx.fillStyle = '#fff'; + ctx.beginPath(); + ctx.arc(cx, cy, 4.5, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = s.color; + ctx.lineWidth = 2; + ctx.stroke(); + const label = `${Math.round(s.cursor)} m · ${elev.toFixed(1)} m`; + ctx.font = '10px system-ui'; + const tw = ctx.measureText(label).width; + let lx = cx - tw / 2; + lx = Math.max(margin.l, Math.min(lx, margin.l + plotW - tw)); + ctx.fillStyle = 'rgba(10,10,20,0.92)'; + ctx.fillRect(lx - 3, margin.t - 1, tw + 6, 14); + ctx.fillStyle = '#00ff88'; + ctx.fillText(label, lx, margin.t + 10); + } } }); + + canvas._elevLayout = { margin, plotW, plotH, maxDist, minE, maxE }; } function drawElevationChart(cursors) { @@ -1014,7 +1166,11 @@ function drawMapRulerChart() { const series = elevationPointCount(elevProfileMapLine) > 0 - ? [{ color: '#00ff88', profile: elevProfileMapLine, cursor: null }] + ? [{ + color: '#00ff88', + profile: elevProfileMapLine, + cursor: mapRulerCursorDist + }] : []; const idleMsg = elevProfileMapLine?.api_error ? elevProfileMapLine.api_error @@ -1072,6 +1228,8 @@ function resetMapRulerPick() { clearMapRulerPickPoints(); mapRulerLoadState = 'idle'; + mapRulerChartHover = false; + mapRulerLineHover = false; setMapRulerStatus(''); setMapRulerMode('pick'); drawMapRulerChart(); @@ -1766,6 +1924,26 @@ }; document.getElementById('btnRulerClear').onclick = () => resetMapRulerPick(); + (function bindMapRulerChartProbe() { + const canvas = document.getElementById('mapRulerCanvas'); + if (!canvas) return; + canvas.addEventListener('mousemove', e => { + if (!mapRulerOpen || elevationPointCount(elevProfileMapLine) === 0) return; + const layout = canvas._elevLayout; + if (!layout || layout.plotW <= 0) return; + mapRulerChartHover = true; + clearTimeout(mapRulerLeaveTimer); + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const dist = ((x - layout.margin.l) / layout.plotW) * layout.maxDist; + setMapRulerCursor(dist); + }); + canvas.addEventListener('mouseleave', () => { + mapRulerChartHover = false; + scheduleClearMapRulerCursor(); + }); + })(); + async function refreshPairedStatus() { try { const res = await fetch('/api/paired-tracks/active', { cache: 'no-store' });