fix elevation

This commit is contained in:
2026-06-16 11:54:41 +03:00
parent 0e1fa15a2f
commit 6b34e75f35
4 changed files with 172 additions and 33 deletions
+1 -1
View File
@@ -379,7 +379,7 @@ def health():
return {
"ok": status["db_ok"],
"ts": time.time(),
"api_build": "2026-06-16d",
"api_build": "2026-06-16e",
**status,
**elevation_status(),
}
+152 -28
View File
@@ -361,7 +361,7 @@
{ position: 'topright', collapsed: true }
).addTo(map);
const API_BUILD = '2026-06-16d';
const API_BUILD = '2026-06-16e';
const markers = {};
let selectedId = null;
@@ -406,6 +406,8 @@
let elevProfileTx = null;
let elevProfileRx = null;
let elevProfileSingle = null;
let elevProfileLink = null;
let elevProfileLinkKey = null;
let elevProfileMapLine = null;
let elevationLoadState = 'idle';
let mapRulerOpen = false;
@@ -621,6 +623,11 @@
};
}
function mergeTelCoords(tel, pos) {
if (!tel || !pos) return tel;
return { ...tel, lat: pos.lat, lon: pos.lon };
}
function snapAtTime(track, telemetryRows, t, roleFallback) {
const pos = positionAt(track?.points, t);
if (pos?.meta && String(pos.meta).length > 2) {
@@ -640,8 +647,21 @@
function qualityColor(pct) {
if (pct == null || Number.isNaN(pct)) return null;
const p = Math.max(0, Math.min(100, Number(pct)));
const r = p < 50 ? Math.round(255 * (p / 50)) : 255;
const g = p < 50 ? 255 : Math.round(255 * (1 - (p - 50) / 50));
let r;
let g;
if (p < 40) {
const t = p / 40;
r = 255;
g = Math.round(140 * t);
} else if (p < 85) {
const t = (p - 40) / 45;
r = 255;
g = Math.round(140 + 115 * t);
} else {
const t = (p - 85) / 15;
r = Math.round(255 * (1 - t));
g = 255;
}
return `rgb(${r},${g},0)`;
}
@@ -747,9 +767,50 @@
return html;
}
function formatCoords(tel) {
if (!tel || tel.lat == null || tel.lon == null || isNullIsland(tel.lat, tel.lon)) return null;
return `${Number(tel.lat).toFixed(5)}, ${Number(tel.lon).toFixed(5)}`;
}
function formatPacketTime(tel) {
if (!tel) return null;
let ts = tel.ts;
if (tel.meta) {
let o = tel.meta;
if (typeof o === 'string') {
try { o = JSON.parse(o); } catch (e) { o = null; }
}
if (o) {
if (o.packet_ts != null) ts = o.packet_ts;
else if (o.ts != null) ts = o.ts;
else if (o.fields) {
for (const [k, v] of Object.entries(o.fields)) {
if (/time/i.test(k)) return String(v);
}
}
}
}
if (ts == null || !Number.isFinite(Number(ts)) || Number(ts) <= 1) return null;
const n = Number(ts);
const ms = n < 1e12 ? n * 1000 : n;
return new Date(ms).toLocaleTimeString();
}
function enrichSnapFromTel(snap, tel) {
snap.gps = formatCoords(tel) || '—';
snap.packetTime = formatPacketTime(tel) || '—';
return snap;
}
function renderTimelineCompare(txTel, rxTel, txId, rxId) {
const txSnap = txTel ? telemetryToSnap(txTel) : RadioUI.parseRadioSnapshot(null);
const rxSnap = rxTel ? telemetryToSnap(rxTel) : RadioUI.parseRadioSnapshot(null);
const txSnap = enrichSnapFromTel(
txTel ? telemetryToSnap(txTel) : RadioUI.parseRadioSnapshot(null),
txTel
);
const rxSnap = enrichSnapFromTel(
rxTel ? telemetryToSnap(rxTel) : RadioUI.parseRadioSnapshot(null),
rxTel
);
const chTx = RadioUI.diffSnapshots(prevTimelineTxSnap, txSnap);
const chRx = RadioUI.diffSnapshots(prevTimelineRxSnap, rxSnap);
prevTimelineTxSnap = txSnap;
@@ -1435,13 +1496,27 @@
function getTimelineElevationSeries(cursors) {
const series = [];
if (dualTracksActive) {
if (elevationPointCount(elevProfileLink) > 0) {
const pts = elevProfileLink.points.filter(p => p.elevation_m != null);
const elevA = pts[0]?.elevation_m;
const elevB = pts[pts.length - 1]?.elevation_m;
series.push({
color: '#00ff88',
profile: elevProfileLink,
label: 'рельеф TX↔RX',
losLine: elevA != null && elevB != null ? { elevA, elevB } : null,
});
return series;
}
if (elevationPointCount(elevProfileTx) > 0) {
series.push({ color: TX_COLOR, profile: elevProfileTx, cursor: cursors?.tx, label: 'TX' });
}
if (elevationPointCount(elevProfileRx) > 0) {
series.push({ color: RX_COLOR, profile: elevProfileRx, cursor: cursors?.rx, label: 'RX' });
}
} else if (singleTrackActive && elevationPointCount(elevProfileSingle) > 0) {
return series;
}
if (singleTrackActive && elevationPointCount(elevProfileSingle) > 0) {
const color = loadedSingleTrack?.role === 'RX' ? RX_COLOR : TX_COLOR;
const label = loadedSingleTrack?.role === 'RX' ? 'RX' : 'TX';
series.push({ color, profile: elevProfileSingle, cursor: cursors?.single, label });
@@ -1532,6 +1607,23 @@
else ctx.lineTo(x, y);
});
ctx.stroke();
if (s.losLine && pts.length >= 2) {
const x0 = margin.l;
const x1 = margin.l + plotW;
const y0 = margin.t + plotH - ((s.losLine.elevA - minE) / (maxE - minE)) * plotH;
const y1 = margin.t + plotH - ((s.losLine.elevB - minE) / (maxE - minE)) * plotH;
ctx.strokeStyle = 'rgba(255, 136, 0, 0.95)';
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 4]);
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#ffb74d';
ctx.font = '9px system-ui';
ctx.fillText('прямая', x1 - 42, y1 - 4);
}
if (s.cursor != null && maxDist > 0) {
const cx = margin.l + (s.cursor / maxDist) * plotW;
const elev = elevationAtDist(s.profile, s.cursor);
@@ -1649,10 +1741,32 @@
drawMapRulerChart();
}
async function scheduleLinkElevation(txPos, rxPos) {
if (!txPos || !rxPos) return;
const dist = haversineM(txPos.lat, txPos.lon, rxPos.lat, rxPos.lon);
const key = `${txPos.lat.toFixed(5)},${txPos.lon.toFixed(5)}|${rxPos.lat.toFixed(5)},${rxPos.lon.toFixed(5)}`;
if (key === elevProfileLinkKey && elevProfileLink) return;
elevProfileLinkKey = key;
const linePts = [{ lat: txPos.lat, lon: txPos.lon }, { lat: rxPos.lat, lon: rxPos.lon }];
const profile = await fetchElevationProfile(linePts, null, {
targetPoints: getMapRulerTargetPoints(dist),
});
if (elevProfileLinkKey !== key) return;
elevProfileLink = profile;
const n = elevationPointCount(profile);
if (n > 0) {
const src = profile.source === 'elevation' ? 'высоты'
: profile.source === 'server' ? 'сервер' : (profile.source || 'данные');
setElevationStatus(`срез TX↔RX · ${dist.toFixed(0)} m · ${src} · ${n} точек · оранжевая — прямая`);
}
}
async function loadElevationProfiles() {
elevProfileTx = null;
elevProfileRx = null;
elevProfileSingle = null;
elevProfileLink = null;
elevProfileLinkKey = null;
elevationLoadState = 'loading';
setElevationStatus('загрузка…');
drawElevationChart();
@@ -1661,25 +1775,26 @@
elevProfileSingle = await fetchElevationProfile(
loadedSingleTrack.points, loadedSingleTrack.id);
} else if (dualTracksActive) {
const [txProf, rxProf] = await Promise.all([
loadedTxTrack?.points?.length
? fetchElevationProfile(loadedTxTrack.points, loadedTxTrack.id) : null,
loadedRxTrack?.points?.length
? fetchElevationProfile(loadedRxTrack.points, loadedRxTrack.id) : null
]);
elevProfileTx = txProf;
elevProfileRx = rxProf;
const txPos = positionAtCursor(loadedTxTrack?.points, timelineCursor());
const rxPos = positionAtCursor(loadedRxTrack?.points, timelineCursor());
if (txPos && rxPos) {
await scheduleLinkElevation(txPos, rxPos);
}
}
elevationLoadState = 'done';
const hasData = elevationPointCount(elevProfileSingle) > 0
|| elevationPointCount(elevProfileLink) > 0
|| elevationPointCount(elevProfileTx) > 0
|| elevationPointCount(elevProfileRx) > 0;
if (hasData) {
const ref = elevProfileSingle || elevProfileTx || elevProfileRx;
const ref = elevProfileSingle || elevProfileLink || elevProfileTx || elevProfileRx;
const srcLabel = ref?.source === 'elevation' ? 'высоты'
: ref?.source === 'server' ? 'сервер' : (ref?.source || 'данные');
if (dualTracksActive && elevProfileTx && elevProfileRx) {
if (dualTracksActive && elevProfileLink) {
const n = elevationPointCount(elevProfileLink);
setElevationStatus(`срез TX↔RX · ${srcLabel} · ${n} точек · оранжевая — прямая`);
} else if (dualTracksActive && elevProfileTx && elevProfileRx) {
const nTx = elevationPointCount(elevProfileTx);
const nRx = elevationPointCount(elevProfileRx);
setElevationStatus(`TX + RX · ${srcLabel} · ${nTx}/${nRx} точек`);
@@ -1687,7 +1802,8 @@
setElevationStatus(`${srcLabel} · ${elevationPointCount(ref)} точек`);
}
} else {
const err = elevProfileSingle?.api_error || elevProfileTx?.api_error || elevProfileRx?.api_error;
const err = elevProfileSingle?.api_error || elevProfileLink?.api_error
|| elevProfileTx?.api_error || elevProfileRx?.api_error;
setElevationStatus(err ? `ошибка: ${err}` : 'нет данных');
}
drawElevationChart();
@@ -1976,6 +2092,8 @@
elevProfileTx = null;
elevProfileRx = null;
elevProfileSingle = null;
elevProfileLink = null;
elevProfileLinkKey = null;
drawElevationChart();
if (playTimer) {
clearInterval(playTimer);
@@ -2039,12 +2157,17 @@
const dist = haversineM(txPos.lat, txPos.lon, rxPos.lat, rxPos.lon);
let html = `<b>${formatTimelineClock(cursor)}</b><br>`;
html += `Расстояние: ${dist.toFixed(0)} m (GPS)<br><br>`;
html += `<span class="legend-tx">TX</span> ${txPos.lat.toFixed(5)}, ${txPos.lon.toFixed(5)}<br>`;
const txTel = snapAtCursor(loadedTxTrack, telemetryTx, cursor, 'TX');
const rxTel = snapAtCursor(loadedRxTrack, telemetryRx, cursor, 'RX');
const txTel = mergeTelCoords(snapAtCursor(loadedTxTrack, telemetryTx, cursor, 'TX'), txPos);
const rxTel = mergeTelCoords(snapAtCursor(loadedRxTrack, telemetryRx, cursor, 'RX'), rxPos);
html += renderTimelineCompare(
txTel || { meta: txPos.meta, role: 'TX', rssi: null },
rxTel || { meta: rxPos.meta, role: 'RX', rssi: null },
txTel || mergeTelCoords({
meta: txPos.meta, role: 'TX', rssi: null,
ts: timelineUseProgress ? null : cursor.t
}, txPos),
rxTel || mergeTelCoords({
meta: rxPos.meta, role: 'RX', rssi: null,
ts: timelineUseProgress ? null : cursor.t
}, rxPos),
deviceDisplayName(loadedTxTrack?.device_id),
deviceDisplayName(loadedRxTrack?.device_id)
);
@@ -2098,8 +2221,8 @@
}
}
const txTel = snapAtCursor(loadedTxTrack, telemetryTx, cursor, 'TX');
const rxTel = snapAtCursor(loadedRxTrack, telemetryRx, cursor, 'RX');
const txTel = mergeTelCoords(snapAtCursor(loadedTxTrack, telemetryTx, cursor, 'TX'), txPos);
const rxTel = mergeTelCoords(snapAtCursor(loadedRxTrack, telemetryRx, cursor, 'RX'), rxPos);
const timelineStatsEl = document.getElementById('timelineStats');
setPanelHtml(timelineStatsEl, renderTimelineCompare(
txTel,
@@ -2107,10 +2230,11 @@
deviceDisplayName(loadedTxTrack?.device_id),
deviceDisplayName(loadedRxTrack?.device_id)
));
drawElevationChart({
tx: trackDistanceAtCursor(loadedTxTrack, cursor),
rx: trackDistanceAtCursor(loadedRxTrack, cursor)
});
if (txPos && rxPos) {
scheduleLinkElevation(txPos, rxPos).then(() => drawElevationChart());
} else {
drawElevationChart();
}
}
function updateTimelineAtSingle(cursor, openModal) {
+4 -2
View File
@@ -77,9 +77,9 @@
function diffSnapshots(a, b) {
const changed = new Set();
if (!a || !b) return changed;
const keys = ['role', 'rssiDbm', 'snrDb', 'rxQualityPercent', 'packet', 'payload', 'perPercent',
const keys = ['gps', 'packetTime', 'role', 'rssiDbm', 'snrDb', 'rxQualityPercent', 'packet', 'payload', 'perPercent',
'txPktPerS', 'rxPktPerS', 'frequencyMhz', 'sf', 'bwKhz', 'powerDbm'];
const map = { role: 'role', rssiDbm: 'rssi', snrDb: 'snr', rxQualityPercent: 'rxQuality',
const map = { gps: 'gps', packetTime: 'packetTime', role: 'role', rssiDbm: 'rssi', snrDb: 'snr', rxQualityPercent: 'rxQuality',
packet: 'packet',
payload: 'payload', perPercent: 'per', txPktPerS: 'txSpeed', rxPktPerS: 'rxSpeed',
frequencyMhz: 'frequency', sf: 'sf', bwKhz: 'bw', powerDbm: 'power' };
@@ -90,6 +90,8 @@
}
const DYNAMIC_ROWS = [
{ key: 'gps', label: 'GPS', fmt: s => s.gps || '—' },
{ key: 'packetTime', label: 'Время пакета', fmt: s => s.packetTime || '—' },
{ key: 'rssi', label: 'RSSI', fmt: s => s.rssiDbm != null ? `${s.rssiDbm} dBm` : '—' },
{ key: 'snr', label: 'SNR', fmt: s => s.snrDb != null ? `${s.snrDb} dB` : '—' },
{ key: 'rxQuality', label: 'RX Quality', fmt: s => s.rxQualityPercent != null ? `${s.rxQualityPercent} %` : '—' },