added gistogramm

This commit is contained in:
2026-06-17 11:12:33 +03:00
parent 40a1ccab1e
commit 920a839197
3 changed files with 390 additions and 7 deletions
+207 -6
View File
@@ -45,7 +45,22 @@
}
#elevationStatus { font-size: 0.7rem; color: #aaa; font-weight: 400; }
#elevationCanvas { width: 100%; height: 130px; display: block; background: #0a0a14; border-radius: 4px; }
#elevationCanvas.elev-probe { cursor: crosshair; }
.elev-legend { font-size: 0.7rem; }
#qualityVizPanel {
display: none; margin-top: 8px; gap: 8px;
grid-template-columns: 140px 1fr; align-items: start;
}
#qualityVizPanel.visible { display: grid; }
@media (max-width: 700px) {
#qualityVizPanel.visible { grid-template-columns: 1fr; }
}
.quality-viz-box {
background: #0f3460; border: 1px solid #444; border-radius: 6px; padding: 6px;
}
.quality-viz-title { font-size: 0.7rem; color: #aaa; margin-bottom: 4px; }
#qualityRoseCanvas { width: 100%; height: 140px; display: block; background: #0a0a14; border-radius: 4px; }
#qualityDistCanvas { width: 100%; height: 140px; display: block; background: #0a0a14; border-radius: 4px; }
#timelineStatsPanel {
display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid #333;
}
@@ -320,6 +335,16 @@
</div>
<canvas id="elevationCanvas" width="800" height="130"></canvas>
</div>
<div id="qualityVizPanel">
<div class="quality-viz-box">
<div class="quality-viz-title">Качество по направлению TX→RX</div>
<canvas id="qualityRoseCanvas" width="140" height="140"></canvas>
</div>
<div class="quality-viz-box">
<div class="quality-viz-title">RX Quality vs расстояние</div>
<canvas id="qualityDistCanvas" width="400" height="140"></canvas>
</div>
</div>
</div>
</div>
</main>
@@ -332,6 +357,7 @@
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="/static/radio-ui.js"></script>
<script src="/static/quality-viz.js"></script>
<script>
if (typeof RadioUI === 'undefined') {
console.error('radio-ui.js not loaded — check /static/radio-ui.js');
@@ -361,7 +387,7 @@
{ position: 'topright', collapsed: true }
).addTo(map);
const API_BUILD = '2026-06-16g';
const API_BUILD = '2026-06-16h';
const markers = {};
let selectedId = null;
@@ -430,6 +456,14 @@
let mapRulerPointsAuto = true;
let mapRulerManualPoints = 100;
let mapRulerReloadTimer = null;
let timelineElevHoverDist = null;
let timelineElevCursorMarker = null;
let timelineElevBaseStatus = '';
let timelineLinkTxPos = null;
let timelineLinkRxPos = null;
let timelineElevLeaveTimer = null;
let timelineElevChartHover = false;
let qualitySamplesCache = [];
const DEVICE_POLL_MS = 1000;
const CHAT_POLL_MS = 2500;
@@ -438,6 +472,7 @@
const TX_COLOR = '#e94560';
const RX_COLOR = '#4fc3f7';
const GHOST_TX_COLOR = '#ff9800';
map.on('zoomend moveend', () => {
if (!programmaticMove) userMovedMap = true;
@@ -1092,6 +1127,118 @@
}
}
function clearTimelineElevCursorMarker() {
if (timelineElevCursorMarker) {
map.removeLayer(timelineElevCursorMarker);
timelineElevCursorMarker = null;
}
}
function updateTimelineElevCursorMarker(pt) {
clearTimelineElevCursorMarker();
timelineElevCursorMarker = L.circleMarker([pt.lat, pt.lon], {
radius: 8,
color: '#fff',
weight: 2,
fillColor: '#00ff88',
fillOpacity: 0.95,
interactive: false
}).addTo(map);
}
function setTimelineElevCursor(distM) {
if (!dualTracksActive || !timelineLinkTxPos || !timelineLinkRxPos
|| elevationPointCount(elevProfileLink) === 0) return;
const total = elevProfileLink.total_m
|| lineLengthM(timelineLinkTxPos, timelineLinkRxPos);
timelineElevHoverDist = Math.max(0, Math.min(distM, total));
const pt = latLonAtLineDist(
timelineLinkTxPos, timelineLinkRxPos, timelineElevHoverDist);
updateTimelineElevCursorMarker(pt);
const elev = elevationAtDist(elevProfileLink, timelineElevHoverDist);
if (elev != null) {
setElevationStatus(`${timelineElevHoverDist.toFixed(0)} m · ${elev.toFixed(1)} m`);
}
drawElevationChart();
}
function scheduleClearTimelineElevCursor() {
clearTimeout(timelineElevLeaveTimer);
timelineElevLeaveTimer = setTimeout(() => {
if (timelineElevChartHover) return;
timelineElevHoverDist = null;
clearTimelineElevCursorMarker();
setElevationStatus(timelineElevBaseStatus);
drawElevationChart();
}, 60);
}
function updateElevationProbeClass() {
const canvas = document.getElementById('elevationCanvas');
if (!canvas) return;
canvas.classList.toggle(
'elev-probe',
dualTracksActive && elevationPointCount(elevProfileLink) > 0
);
}
function buildQualitySamples() {
if (!dualTracksActive || !loadedTxTrack?.points?.length || !loadedRxTrack?.points?.length) {
return [];
}
const samples = [];
const steps = Math.min(
200,
Math.max(loadedTxTrack.points.length, loadedRxTrack.points.length, 2) - 1
);
for (let i = 0; i <= steps; i++) {
const cursor = timelineUseProgress
? { progress: i / steps }
: { t: overlapMin + (i / steps) * Math.max(overlapMax - overlapMin, 1e-6) };
const txPos = positionAtCursor(loadedTxTrack.points, cursor);
const rxPos = positionAtCursor(loadedRxTrack.points, cursor);
if (!txPos || !rxPos) continue;
const quality = rxQualityFromMeta(rxPos.meta);
if (quality == null) continue;
const distM = haversineM(txPos.lat, txPos.lon, rxPos.lat, rxPos.lon);
const bearing = QualityViz.bearingDeg(
txPos.lat, txPos.lon, rxPos.lat, rxPos.lon);
samples.push({ distM, bearing, quality });
}
return samples;
}
function currentTimelineLinkDist() {
if (!timelineLinkTxPos || !timelineLinkRxPos) return null;
return haversineM(
timelineLinkTxPos.lat, timelineLinkTxPos.lon,
timelineLinkRxPos.lat, timelineLinkRxPos.lon);
}
function redrawQualityDistHighlight() {
if (!dualTracksActive) return;
const distCanvas = document.getElementById('qualityDistCanvas');
if (!distCanvas) return;
QualityViz.drawQualityDistChart(
distCanvas, qualitySamplesCache, qualityColor, currentTimelineLinkDist());
}
function rebuildQualityViz() {
const panel = document.getElementById('qualityVizPanel');
if (!dualTracksActive) {
panel?.classList.remove('visible');
qualitySamplesCache = [];
return;
}
qualitySamplesCache = buildQualitySamples();
const roseCanvas = document.getElementById('qualityRoseCanvas');
const distCanvas = document.getElementById('qualityDistCanvas');
QualityViz.drawQualityRose(roseCanvas, qualitySamplesCache, qualityColor);
QualityViz.drawQualityDistChart(
distCanvas, qualitySamplesCache, qualityColor, currentTimelineLinkDist());
panel?.classList.add('visible');
}
function scheduleClearMapRulerCursor() {
clearTimeout(mapRulerLeaveTimer);
mapRulerLeaveTimer = setTimeout(() => {
@@ -1533,6 +1680,7 @@
profile: elevProfileLink,
label: 'рельеф TX↔RX',
losLine: elevA != null && elevB != null ? { elevA, elevB } : null,
cursor: timelineElevHoverDist,
});
return series;
}
@@ -1785,8 +1933,11 @@
if (n > 0) {
const src = profile.source === 'elevation' ? 'высоты'
: profile.source === 'server' ? 'сервер' : (profile.source || 'данные');
setElevationStatus(`срез TX↔RX · ${dist.toFixed(0)} m · ${src} · ${n} точек · оранжевая — прямая`);
timelineElevBaseStatus =
`срез TX↔RX · ${dist.toFixed(0)} m · ${src} · ${n} точек · оранжевая — прямая`;
setElevationStatus(timelineElevBaseStatus);
}
updateElevationProbeClass();
}
async function loadElevationProfiles() {
@@ -1821,7 +1972,14 @@
: ref?.source === 'server' ? 'сервер' : (ref?.source || 'данные');
if (dualTracksActive && elevProfileLink) {
const n = elevationPointCount(elevProfileLink);
setElevationStatus(`срез TX↔RX · ${srcLabel} · ${n} точек · оранжевая — прямая`);
const dist = timelineLinkTxPos && timelineLinkRxPos
? haversineM(
timelineLinkTxPos.lat, timelineLinkTxPos.lon,
timelineLinkRxPos.lat, timelineLinkRxPos.lon)
: (elevProfileLink.total_m || 0);
timelineElevBaseStatus =
`срез TX↔RX · ${dist.toFixed(0)} m · ${srcLabel} · ${n} точек · оранжевая — прямая`;
setElevationStatus(timelineElevBaseStatus);
} else if (dualTracksActive && elevProfileTx && elevProfileRx) {
const nTx = elevationPointCount(elevProfileTx);
const nRx = elevationPointCount(elevProfileRx);
@@ -1835,6 +1993,7 @@
setElevationStatus(err ? `ошибка: ${err}` : 'нет данных');
}
drawElevationChart();
updateElevationProbeClass();
requestAnimationFrame(() => drawElevationChart(
singleTrackActive
? { single: trackDistanceAtCursor(loadedSingleTrack, timelineCursor()) }
@@ -2132,6 +2291,14 @@
elevProfileSingle = null;
elevProfileLink = null;
elevProfileLinkKey = null;
timelineElevHoverDist = null;
timelineLinkTxPos = null;
timelineLinkRxPos = null;
timelineElevBaseStatus = '';
clearTimelineElevCursorMarker();
qualitySamplesCache = [];
document.getElementById('qualityVizPanel')?.classList.remove('visible');
updateElevationProbeClass();
drawElevationChart();
if (playTimer) {
clearInterval(playTimer);
@@ -2237,7 +2404,7 @@
if (txPos) {
ghostTx = L.circleMarker([txPos.lat, txPos.lon], {
radius: 10, color: TX_COLOR, fillColor: TX_COLOR, fillOpacity: 0.9, weight: 3
radius: 10, color: '#fff', fillColor: GHOST_TX_COLOR, fillOpacity: 0.9, weight: 3
}).addTo(map);
}
if (rxPos) {
@@ -2268,11 +2435,18 @@
deviceDisplayName(loadedTxTrack?.device_id),
deviceDisplayName(loadedRxTrack?.device_id)
));
timelineLinkTxPos = txPos || null;
timelineLinkRxPos = rxPos || null;
if (txPos && rxPos) {
scheduleLinkElevation(txPos, rxPos).then(() => drawElevationChart());
scheduleLinkElevation(txPos, rxPos).then(() => {
drawElevationChart();
updateElevationProbeClass();
});
} else {
drawElevationChart();
}
redrawQualityDistHighlight();
}
function updateTimelineAtSingle(cursor, openModal) {
@@ -2316,9 +2490,14 @@
function setTimelineVisible(visible) {
document.getElementById('trackTimeline').classList.toggle('visible', visible);
document.getElementById('timelineStatsPanel').classList.toggle('visible', visible);
document.getElementById('qualityVizPanel')?.classList.toggle(
'visible', visible && dualTracksActive);
setTimeout(() => {
map.invalidateSize();
if (visible) drawElevationChart();
if (visible) {
drawElevationChart();
if (dualTracksActive) rebuildQualityViz();
}
}, 80);
}
@@ -2419,6 +2598,7 @@
setTimelineVisible(true);
updateTimelineAt(timelineCursor());
loadElevationProfiles();
rebuildQualityViz();
}
async function refreshTimelineTelemetry() {
@@ -2595,6 +2775,7 @@
updateTimelineAt(timelineCursor());
}
rebuildQualityViz();
const modeHint = range && range.mode === 'union' ? ' · без пересечения по времени' : '';
document.getElementById('trackInfo').textContent =
`TX #${loadedTxTrack.id} (${loadedTxTrack.points.length}) + RX #${loadedRxTrack.id} (${loadedRxTrack.points.length})${modeHint}`;
@@ -2689,6 +2870,26 @@
});
})();
(function bindTimelineElevationProbe() {
const canvas = document.getElementById('elevationCanvas');
if (!canvas) return;
canvas.addEventListener('mousemove', e => {
if (!dualTracksActive || elevationPointCount(elevProfileLink) === 0) return;
const layout = canvas._elevLayout;
if (!layout || layout.plotW <= 0) return;
timelineElevChartHover = true;
clearTimeout(timelineElevLeaveTimer);
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const dist = ((x - layout.margin.l) / layout.plotW) * layout.maxDist;
setTimelineElevCursor(dist);
});
canvas.addEventListener('mouseleave', () => {
timelineElevChartHover = false;
scheduleClearTimelineElevCursor();
});
})();
(function bindMapRulerChartProbe() {
const canvas = document.getElementById('mapRulerCanvas');
if (!canvas) return;