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;
|
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' });
|
||||||
|
|||||||
Reference in New Issue
Block a user