generated from Grigo/AndroidTemplate
fixed gistogramm
This commit is contained in:
@@ -379,7 +379,7 @@ def health():
|
|||||||
return {
|
return {
|
||||||
"ok": status["db_ok"],
|
"ok": status["db_ok"],
|
||||||
"ts": time.time(),
|
"ts": time.time(),
|
||||||
"api_build": "2026-06-16h",
|
"api_build": "2026-06-16i",
|
||||||
**status,
|
**status,
|
||||||
**elevation_status(),
|
**elevation_status(),
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-26
@@ -48,18 +48,13 @@
|
|||||||
#elevationCanvas.elev-probe { cursor: crosshair; }
|
#elevationCanvas.elev-probe { cursor: crosshair; }
|
||||||
.elev-legend { font-size: 0.7rem; }
|
.elev-legend { font-size: 0.7rem; }
|
||||||
#qualityVizPanel {
|
#qualityVizPanel {
|
||||||
display: none; margin-top: 8px; gap: 8px;
|
display: none; margin-top: 8px;
|
||||||
grid-template-columns: 140px 1fr; align-items: start;
|
|
||||||
}
|
|
||||||
#qualityVizPanel.visible { display: grid; }
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
#qualityVizPanel.visible { grid-template-columns: 1fr; }
|
|
||||||
}
|
}
|
||||||
|
#qualityVizPanel.visible { display: block; }
|
||||||
.quality-viz-box {
|
.quality-viz-box {
|
||||||
background: #0f3460; border: 1px solid #444; border-radius: 6px; padding: 6px;
|
background: #0f3460; border: 1px solid #444; border-radius: 6px; padding: 6px;
|
||||||
}
|
}
|
||||||
.quality-viz-title { font-size: 0.7rem; color: #aaa; margin-bottom: 4px; }
|
.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; }
|
#qualityDistCanvas { width: 100%; height: 140px; display: block; background: #0a0a14; border-radius: 4px; }
|
||||||
#timelineStatsPanel {
|
#timelineStatsPanel {
|
||||||
display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid #333;
|
display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid #333;
|
||||||
@@ -121,8 +116,8 @@
|
|||||||
#pairedStatus { font-size: 0.75rem; color: #aaa; margin-top: 4px; }
|
#pairedStatus { font-size: 0.75rem; color: #aaa; margin-top: 4px; }
|
||||||
.muted { color: #aaa; font-size: 0.75rem; }
|
.muted { color: #aaa; font-size: 0.75rem; }
|
||||||
.legend { font-size: 0.75rem; color: #ccc; }
|
.legend { font-size: 0.75rem; color: #ccc; }
|
||||||
.legend-tx { color: #e94560; }
|
.legend-tx { color: #4fc3f7; }
|
||||||
.legend-rx { color: #4fc3f7; }
|
.legend-rx { color: #e94560; }
|
||||||
#mapModal {
|
#mapModal {
|
||||||
display: none; position: fixed; z-index: 2000;
|
display: none; position: fixed; z-index: 2000;
|
||||||
min-width: 260px; max-width: 360px; max-height: 70vh; overflow: auto;
|
min-width: 260px; max-width: 360px; max-height: 70vh; overflow: auto;
|
||||||
@@ -337,12 +332,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="qualityVizPanel">
|
<div id="qualityVizPanel">
|
||||||
<div class="quality-viz-box">
|
<div class="quality-viz-box">
|
||||||
<div class="quality-viz-title">Качество по направлению TX→RX</div>
|
<div class="quality-viz-title">RX Quality vs расстояние TX↔RX</div>
|
||||||
<canvas id="qualityRoseCanvas" width="140" height="140"></canvas>
|
<canvas id="qualityDistCanvas" width="800" 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>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,7 +378,7 @@
|
|||||||
{ position: 'topright', collapsed: true }
|
{ position: 'topright', collapsed: true }
|
||||||
).addTo(map);
|
).addTo(map);
|
||||||
|
|
||||||
const API_BUILD = '2026-06-16h';
|
const API_BUILD = '2026-06-16i';
|
||||||
|
|
||||||
const markers = {};
|
const markers = {};
|
||||||
let selectedId = null;
|
let selectedId = null;
|
||||||
@@ -470,9 +461,8 @@
|
|||||||
const TRACKS_POLL_MS = 10000;
|
const TRACKS_POLL_MS = 10000;
|
||||||
const TELEMETRY_POLL_MS = 2000;
|
const TELEMETRY_POLL_MS = 2000;
|
||||||
|
|
||||||
const TX_COLOR = '#e94560';
|
const TX_COLOR = '#4fc3f7';
|
||||||
const RX_COLOR = '#4fc3f7';
|
const RX_COLOR = '#e94560';
|
||||||
const GHOST_TX_COLOR = '#ff9800';
|
|
||||||
|
|
||||||
map.on('zoomend moveend', () => {
|
map.on('zoomend moveend', () => {
|
||||||
if (!programmaticMove) userMovedMap = true;
|
if (!programmaticMove) userMovedMap = true;
|
||||||
@@ -1201,9 +1191,7 @@
|
|||||||
const quality = rxQualityFromMeta(rxPos.meta);
|
const quality = rxQualityFromMeta(rxPos.meta);
|
||||||
if (quality == null) continue;
|
if (quality == null) continue;
|
||||||
const distM = haversineM(txPos.lat, txPos.lon, rxPos.lat, rxPos.lon);
|
const distM = haversineM(txPos.lat, txPos.lon, rxPos.lat, rxPos.lon);
|
||||||
const bearing = QualityViz.bearingDeg(
|
samples.push({ distM, quality });
|
||||||
txPos.lat, txPos.lon, rxPos.lat, rxPos.lon);
|
|
||||||
samples.push({ distM, bearing, quality });
|
|
||||||
}
|
}
|
||||||
return samples;
|
return samples;
|
||||||
}
|
}
|
||||||
@@ -1231,9 +1219,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
qualitySamplesCache = buildQualitySamples();
|
qualitySamplesCache = buildQualitySamples();
|
||||||
const roseCanvas = document.getElementById('qualityRoseCanvas');
|
|
||||||
const distCanvas = document.getElementById('qualityDistCanvas');
|
const distCanvas = document.getElementById('qualityDistCanvas');
|
||||||
QualityViz.drawQualityRose(roseCanvas, qualitySamplesCache, qualityColor);
|
|
||||||
QualityViz.drawQualityDistChart(
|
QualityViz.drawQualityDistChart(
|
||||||
distCanvas, qualitySamplesCache, qualityColor, currentTimelineLinkDist());
|
distCanvas, qualitySamplesCache, qualityColor, currentTimelineLinkDist());
|
||||||
panel?.classList.add('visible');
|
panel?.classList.add('visible');
|
||||||
@@ -2404,12 +2390,14 @@
|
|||||||
|
|
||||||
if (txPos) {
|
if (txPos) {
|
||||||
ghostTx = L.circleMarker([txPos.lat, txPos.lon], {
|
ghostTx = L.circleMarker([txPos.lat, txPos.lon], {
|
||||||
radius: 10, color: '#fff', fillColor: GHOST_TX_COLOR, fillOpacity: 0.9, weight: 3
|
radius: 10, color: '#fff', fillColor: TX_COLOR, fillOpacity: 0.95, weight: 3
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
}
|
}
|
||||||
if (rxPos) {
|
if (rxPos) {
|
||||||
|
const q = rxQualityFromMeta(rxPos.meta);
|
||||||
|
const rxGhostColor = q != null ? (qualityColor(q) || RX_COLOR) : RX_COLOR;
|
||||||
ghostRx = L.circleMarker([rxPos.lat, rxPos.lon], {
|
ghostRx = L.circleMarker([rxPos.lat, rxPos.lon], {
|
||||||
radius: 10, color: RX_COLOR, fillColor: RX_COLOR, fillOpacity: 0.9, weight: 3
|
radius: 10, color: '#fff', fillColor: rxGhostColor, fillOpacity: 0.95, weight: 3
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
}
|
}
|
||||||
if (txPos && rxPos) {
|
if (txPos && rxPos) {
|
||||||
|
|||||||
@@ -1,91 +1,7 @@
|
|||||||
/** Polar rose and quality-vs-distance charts for TX/RX track compare. */
|
/** RX quality vs distance chart for TX/RX track compare. */
|
||||||
(function (global) {
|
(function (global) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function bearingDeg(lat1, lon1, lat2, lon2) {
|
|
||||||
const toRad = d => d * Math.PI / 180;
|
|
||||||
const toDeg = r => r * 180 / Math.PI;
|
|
||||||
const dLon = toRad(lon2 - lon1);
|
|
||||||
const y = Math.sin(dLon) * Math.cos(toRad(lat2));
|
|
||||||
const x = Math.cos(toRad(lat1)) * Math.sin(toRad(lat2))
|
|
||||||
- Math.sin(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.cos(dLon);
|
|
||||||
return (toDeg(Math.atan2(y, x)) + 360) % 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawQualityRose(canvas, samples, qualityColor) {
|
|
||||||
if (!canvas) return;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const w = canvas.clientWidth || 140;
|
|
||||||
const h = canvas.clientHeight || 140;
|
|
||||||
if (canvas.width !== w) canvas.width = w;
|
|
||||||
if (canvas.height !== h) canvas.height = h;
|
|
||||||
ctx.fillStyle = '#0a0a14';
|
|
||||||
ctx.fillRect(0, 0, w, h);
|
|
||||||
|
|
||||||
const sectors = 16;
|
|
||||||
const cx = w / 2;
|
|
||||||
const cy = h / 2;
|
|
||||||
const maxR = Math.min(w, h) * 0.38;
|
|
||||||
|
|
||||||
if (!samples?.length) {
|
|
||||||
ctx.fillStyle = '#888';
|
|
||||||
ctx.font = '11px system-ui';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText('нет данных качества', cx, cy);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bins = Array.from({ length: sectors }, () => ({ sum: 0, count: 0 }));
|
|
||||||
const step = 360 / sectors;
|
|
||||||
for (const s of samples) {
|
|
||||||
const idx = Math.floor(((s.bearing % 360) + 360) % 360 / step) % sectors;
|
|
||||||
bins[idx].sum += s.quality;
|
|
||||||
bins[idx].count += 1;
|
|
||||||
}
|
|
||||||
const maxCount = Math.max(1, ...bins.map(b => b.count));
|
|
||||||
|
|
||||||
ctx.strokeStyle = '#333';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(cx, cy, maxR, 0, Math.PI * 2);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
for (let i = 0; i < sectors; i++) {
|
|
||||||
const start = (i * step - 90) * Math.PI / 180;
|
|
||||||
const end = ((i + 1) * step - 90) * Math.PI / 180;
|
|
||||||
const b = bins[i];
|
|
||||||
if (!b.count) continue;
|
|
||||||
const avgQ = b.sum / b.count;
|
|
||||||
const r = maxR * (0.15 + 0.85 * (b.count / maxCount));
|
|
||||||
const col = qualityColor ? qualityColor(avgQ) : '#888';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(cx, cy);
|
|
||||||
ctx.arc(cx, cy, r, start, end);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fillStyle = col;
|
|
||||||
ctx.globalAlpha = 0.85;
|
|
||||||
ctx.fill();
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
ctx.strokeStyle = '#222';
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = '#aaa';
|
|
||||||
ctx.font = '9px system-ui';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText('N', cx, cy - maxR - 4);
|
|
||||||
ctx.fillText('S', cx, cy + maxR + 10);
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.fillText('E', cx + maxR + 4, cy + 3);
|
|
||||||
ctx.textAlign = 'right';
|
|
||||||
ctx.fillText('W', cx - maxR - 4, cy + 3);
|
|
||||||
|
|
||||||
ctx.fillStyle = '#888';
|
|
||||||
ctx.font = '8px system-ui';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText('длина ∝ число точек', cx, h - 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawQualityDistChart(canvas, samples, qualityColor, highlightDist) {
|
function drawQualityDistChart(canvas, samples, qualityColor, highlightDist) {
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
@@ -175,8 +91,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
global.QualityViz = {
|
global.QualityViz = {
|
||||||
bearingDeg,
|
|
||||||
drawQualityRose,
|
|
||||||
drawQualityDistChart,
|
drawQualityDistChart,
|
||||||
};
|
};
|
||||||
})(typeof window !== 'undefined' ? window : globalThis);
|
})(typeof window !== 'undefined' ? window : globalThis);
|
||||||
|
|||||||
Reference in New Issue
Block a user