generated from Grigo/AndroidTemplate
added bind
This commit is contained in:
+195
-17
@@ -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
|
||||
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' });
|
||||
|
||||
Reference in New Issue
Block a user