generated from Grigo/AndroidTemplate
fix elevation
This commit is contained in:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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} %` : '—' },
|
||||
|
||||
Reference in New Issue
Block a user