added bind

This commit is contained in:
2026-06-11 08:46:49 +03:00
parent 8fd7e85c83
commit 17d383ddc6
+195 -17
View File
@@ -150,7 +150,7 @@
background: none; border: none; color: #eee; font-size: 1.1rem; background: none; border: none; color: #eee; font-size: 1.1rem;
cursor: pointer; padding: 0 4px; line-height: 1; 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 { display: flex; gap: 4px; margin-bottom: 6px; flex-wrap: wrap; }
#mapRulerTools button { #mapRulerTools button {
padding: 3px 8px; font-size: 0.7rem; border: 1px solid #444; border-radius: 4px; padding: 3px 8px; font-size: 0.7rem; border: 1px solid #444; border-radius: 4px;
@@ -359,8 +359,15 @@
let mapRulerPtB = null; let mapRulerPtB = null;
let mapRulerLoadState = 'idle'; let mapRulerLoadState = 'idle';
let mapRulerLineLayer = null; let mapRulerLineLayer = null;
let mapRulerHitLayer = null;
let mapRulerMarkerA = null; let mapRulerMarkerA = null;
let mapRulerMarkerB = 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 DEVICE_POLL_MS = 1000;
const CHAT_POLL_MS = 2500; const CHAT_POLL_MS = 2500;
@@ -521,6 +528,57 @@
return { tx, rx }; 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) { function buildDirectLinePoints(tx, rx, stepM = 10) {
const total = haversineM(tx.lat, tx.lon, rx.lat, rx.lon); const total = haversineM(tx.lat, tx.lon, rx.lat, rx.lon);
if (total < 1) { if (total < 1) {
@@ -556,23 +614,85 @@
if (el) el.textContent = text || ''; 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() { function clearMapRulerLineLayer() {
if (mapRulerLineLayer) { if (mapRulerLineLayer) {
map.removeLayer(mapRulerLineLayer); map.removeLayer(mapRulerLineLayer);
mapRulerLineLayer = null; 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() { function clearMapRulerPickPoints() {
@@ -587,6 +707,8 @@
mapRulerMarkerB = null; mapRulerMarkerB = null;
} }
clearMapRulerLineLayer(); clearMapRulerLineLayer();
clearMapRulerCursor();
mapRulerBaseStatus = '';
elevProfileMapLine = null; elevProfileMapLine = null;
} }
@@ -595,13 +717,17 @@
document.getElementById('btnRulerPick').classList.toggle('active', mode === 'pick'); document.getElementById('btnRulerPick').classList.toggle('active', mode === 'pick');
document.getElementById('btnRulerAutoTxRx').classList.toggle('active', mode === 'auto'); document.getElementById('btnRulerAutoTxRx').classList.toggle('active', mode === 'auto');
if (mode === 'pick') { if (mode === 'pick') {
setMapRulerHint(mapRulerPtB setMapRulerHint(mapRulerPtB && elevationPointCount(elevProfileMapLine) > 0
? 'Наведите на линию или график — высота и позиция'
: mapRulerPtB
? 'A и B заданы — клик сбрасывает и задаёт новую A' ? 'A и B заданы — клик сбрасывает и задаёт новую A'
: mapRulerPtA : mapRulerPtA
? 'Клик на карте — точка B' ? 'Клик на карте — точка B'
: 'Клик на карте — точка A, затем точка B'); : 'Клик на карте — точка A, затем точка B');
} else { } else {
setMapRulerHint('Линия между устройствами TX и RX (обновляется при poll)'); setMapRulerHint(elevationPointCount(elevProfileMapLine) > 0
? 'Наведите на линию или график — высота и позиция'
: 'Линия между устройствами TX и RX (обновляется при poll)');
} }
} }
@@ -632,6 +758,7 @@
async function loadMapRulerProfileFromPoints(a, b) { async function loadMapRulerProfileFromPoints(a, b) {
if (!a || !b) return; if (!a || !b) return;
mapRulerLoadState = 'loading'; mapRulerLoadState = 'loading';
clearMapRulerCursor();
setMapRulerStatus('загрузка…'); setMapRulerStatus('загрузка…');
updateMapRulerLineLayer(a, b); updateMapRulerLineLayer(a, b);
drawMapRulerChart(); drawMapRulerChart();
@@ -644,8 +771,11 @@
const src = elevProfileMapLine.source === 'elevation' ? 'высоты' const src = elevProfileMapLine.source === 'elevation' ? 'высоты'
: elevProfileMapLine.source === 'server' ? 'сервер' : elevProfileMapLine.source === 'server' ? 'сервер'
: elevProfileMapLine.source || 'данные'; : elevProfileMapLine.source || 'данные';
setMapRulerStatus(`${dist.toFixed(0)} m · ${src} · ${n} точек`); mapRulerBaseStatus = `${dist.toFixed(0)} m · ${src} · ${n} точек`;
setMapRulerStatus(mapRulerBaseStatus);
setMapRulerHint('Наведите на линию или график — высота и позиция');
} else { } else {
mapRulerBaseStatus = '';
setMapRulerStatus(`${dist.toFixed(0)} m · ${elevProfileMapLine?.api_error || 'нет данных'}`); setMapRulerStatus(`${dist.toFixed(0)} m · ${elevProfileMapLine?.api_error || 'нет данных'}`);
} }
drawMapRulerChart(); drawMapRulerChart();
@@ -991,6 +1121,7 @@
ctx.stroke(); ctx.stroke();
if (s.cursor != null && maxDist > 0) { if (s.cursor != null && maxDist > 0) {
const cx = margin.l + (s.cursor / maxDist) * plotW; const cx = margin.l + (s.cursor / maxDist) * plotW;
const elev = elevationAtDist(s.profile, s.cursor);
ctx.strokeStyle = '#fff'; ctx.strokeStyle = '#fff';
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.setLineDash([4, 3]); ctx.setLineDash([4, 3]);
@@ -999,8 +1130,29 @@
ctx.lineTo(cx, margin.t + plotH); ctx.lineTo(cx, margin.t + plotH);
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]); 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) { function drawElevationChart(cursors) {
@@ -1014,7 +1166,11 @@
function drawMapRulerChart() { function drawMapRulerChart() {
const series = elevationPointCount(elevProfileMapLine) > 0 const series = elevationPointCount(elevProfileMapLine) > 0
? [{ color: '#00ff88', profile: elevProfileMapLine, cursor: null }] ? [{
color: '#00ff88',
profile: elevProfileMapLine,
cursor: mapRulerCursorDist
}]
: []; : [];
const idleMsg = elevProfileMapLine?.api_error const idleMsg = elevProfileMapLine?.api_error
? elevProfileMapLine.api_error ? elevProfileMapLine.api_error
@@ -1072,6 +1228,8 @@
function resetMapRulerPick() { function resetMapRulerPick() {
clearMapRulerPickPoints(); clearMapRulerPickPoints();
mapRulerLoadState = 'idle'; mapRulerLoadState = 'idle';
mapRulerChartHover = false;
mapRulerLineHover = false;
setMapRulerStatus(''); setMapRulerStatus('');
setMapRulerMode('pick'); setMapRulerMode('pick');
drawMapRulerChart(); drawMapRulerChart();
@@ -1766,6 +1924,26 @@
}; };
document.getElementById('btnRulerClear').onclick = () => resetMapRulerPick(); 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() { async function refreshPairedStatus() {
try { try {
const res = await fetch('/api/paired-tracks/active', { cache: 'no-store' }); const res = await fetch('/api/paired-tracks/active', { cache: 'no-store' });