generated from Grigo/AndroidTemplate
added linear lines
This commit is contained in:
@@ -39,6 +39,10 @@ main { display: grid; grid-template-columns: 1fr 340px; grid-template-rows: 1fr
|
|||||||
#elevationCanvas { width: 100%; height: 130px; display: block; background: #0a0a14; border-radius: 4px; }
|
#elevationCanvas { width: 100%; height: 130px; display: block; background: #0a0a14; border-radius: 4px; }
|
||||||
#elevationCanvas.elev-probe { cursor: crosshair; }
|
#elevationCanvas.elev-probe { cursor: crosshair; }
|
||||||
.elev-legend { font-size: 0.7rem; }
|
.elev-legend { font-size: 0.7rem; }
|
||||||
|
.elevation-los-controls { display: none; align-items: center; gap: 8px; margin: 4px 0; font-size: 0.72rem; color: #ccc; }
|
||||||
|
.elevation-los-controls.visible { display: flex; }
|
||||||
|
.elevation-los-controls label { display: flex; align-items: center; gap: 6px; }
|
||||||
|
#elevationLosStartOffset { width: 170px; accent-color: #ff8800; }
|
||||||
#qualityVizPanel {
|
#qualityVizPanel {
|
||||||
display: none; margin-top: 8px;
|
display: none; margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
+158
-7
@@ -80,6 +80,9 @@ let elevProfileSingle = null;
|
|||||||
let elevProfileLink = null;
|
let elevProfileLink = null;
|
||||||
let elevProfileLinkKey = null;
|
let elevProfileLinkKey = null;
|
||||||
let elevProfileMapLine = null;
|
let elevProfileMapLine = null;
|
||||||
|
const TIMELINE_LOS_START_OFFSET_KEY = 'timelineLosStartOffsetM';
|
||||||
|
let timelineLosStartOffsetM = Number(sessionStorage.getItem(TIMELINE_LOS_START_OFFSET_KEY) || '0') || 0;
|
||||||
|
let timelineLosStartDrag = false;
|
||||||
let elevationLoadState = 'idle';
|
let elevationLoadState = 'idle';
|
||||||
let mapRulerOpen = false;
|
let mapRulerOpen = false;
|
||||||
let apiDebugOpen = false;
|
let apiDebugOpen = false;
|
||||||
@@ -1260,6 +1263,67 @@ function elevationAtDist(profile, distM) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampLosStartOffset(v) {
|
||||||
|
return Math.max(-20, Math.min(100, Number(v) || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTimelineLosStartOffset(v, redraw = true) {
|
||||||
|
timelineLosStartOffsetM = clampLosStartOffset(v);
|
||||||
|
sessionStorage.setItem(TIMELINE_LOS_START_OFFSET_KEY, String(timelineLosStartOffsetM));
|
||||||
|
updateTimelineLosControls();
|
||||||
|
updateTimelineElevationBaseStatus();
|
||||||
|
if (redraw) drawElevationChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtSignedM(v) {
|
||||||
|
if (!Number.isFinite(v)) return '—';
|
||||||
|
return `${v >= 0 ? '+' : ''}${v.toFixed(1)} m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timelineLosSummary(profile = elevProfileLink) {
|
||||||
|
if (elevationPointCount(profile) === 0) return null;
|
||||||
|
const pts = profile.points.filter(p => p.elevation_m != null);
|
||||||
|
const groundA = pts[0]?.elevation_m;
|
||||||
|
const groundB = pts[pts.length - 1]?.elevation_m;
|
||||||
|
const total = profile.total_m || pts[pts.length - 1]?.dist_m || 0;
|
||||||
|
if (!Number.isFinite(groundA) || !Number.isFinite(groundB) || total <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const elevA = groundA + timelineLosStartOffsetM;
|
||||||
|
const delta = groundB - elevA;
|
||||||
|
const angleDeg = Math.atan2(delta, total) * 180 / Math.PI;
|
||||||
|
return {
|
||||||
|
groundA,
|
||||||
|
groundB,
|
||||||
|
elevA,
|
||||||
|
elevB: groundB,
|
||||||
|
total,
|
||||||
|
delta,
|
||||||
|
angleDeg,
|
||||||
|
label: `TX ант. ${fmtSignedM(timelineLosStartOffsetM)} · Δ ${fmtSignedM(delta)} · ${angleDeg.toFixed(2)}°`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimelineLosControls() {
|
||||||
|
const box = document.getElementById('elevationLosControls');
|
||||||
|
const slider = document.getElementById('elevationLosStartOffset');
|
||||||
|
const label = document.getElementById('elevationLosStartOffsetLabel');
|
||||||
|
const visible = dualTracksActive && elevationPointCount(elevProfileLink) > 0;
|
||||||
|
box?.classList.toggle('visible', visible);
|
||||||
|
if (slider) slider.value = String(timelineLosStartOffsetM);
|
||||||
|
if (label) label.textContent = fmtSignedM(timelineLosStartOffsetM);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimelineElevationBaseStatus() {
|
||||||
|
if (!dualTracksActive || elevationPointCount(elevProfileLink) === 0 || !timelineElevBaseStatus) {
|
||||||
|
updateTimelineLosControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const summary = timelineLosSummary(elevProfileLink);
|
||||||
|
setElevationStatus(summary ? `${timelineElevBaseStatus} · ${summary.label}` : timelineElevBaseStatus);
|
||||||
|
updateTimelineLosControls();
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -1385,7 +1449,11 @@ function setTimelineElevCursor(distM) {
|
|||||||
updateTimelineElevCursorMarker(pt);
|
updateTimelineElevCursorMarker(pt);
|
||||||
const elev = elevationAtDist(elevProfileLink, timelineElevHoverDist);
|
const elev = elevationAtDist(elevProfileLink, timelineElevHoverDist);
|
||||||
if (elev != null) {
|
if (elev != null) {
|
||||||
setElevationStatus(`${timelineElevHoverDist.toFixed(0)} m · ${elev.toFixed(1)} m`);
|
const summary = timelineLosSummary(elevProfileLink);
|
||||||
|
setElevationStatus(
|
||||||
|
`${timelineElevHoverDist.toFixed(0)} m · ${elev.toFixed(1)} m`
|
||||||
|
+ (summary ? ` · ${summary.label}` : '')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
drawElevationChart();
|
drawElevationChart();
|
||||||
}
|
}
|
||||||
@@ -1396,7 +1464,7 @@ function scheduleClearTimelineElevCursor() {
|
|||||||
if (timelineElevChartHover) return;
|
if (timelineElevChartHover) return;
|
||||||
timelineElevHoverDist = null;
|
timelineElevHoverDist = null;
|
||||||
clearTimelineElevCursorMarker();
|
clearTimelineElevCursorMarker();
|
||||||
setElevationStatus(timelineElevBaseStatus);
|
updateTimelineElevationBaseStatus();
|
||||||
drawElevationChart();
|
drawElevationChart();
|
||||||
}, 60);
|
}, 60);
|
||||||
}
|
}
|
||||||
@@ -1408,6 +1476,7 @@ function updateElevationProbeClass() {
|
|||||||
'elev-probe',
|
'elev-probe',
|
||||||
dualTracksActive && elevationPointCount(elevProfileLink) > 0
|
dualTracksActive && elevationPointCount(elevProfileLink) > 0
|
||||||
);
|
);
|
||||||
|
updateTimelineLosControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildQualitySamples() {
|
function buildQualitySamples() {
|
||||||
@@ -1974,11 +2043,22 @@ function getTimelineElevationSeries(cursors) {
|
|||||||
const pts = elevProfileLink.points.filter(p => p.elevation_m != null);
|
const pts = elevProfileLink.points.filter(p => p.elevation_m != null);
|
||||||
const elevA = pts[0]?.elevation_m;
|
const elevA = pts[0]?.elevation_m;
|
||||||
const elevB = pts[pts.length - 1]?.elevation_m;
|
const elevB = pts[pts.length - 1]?.elevation_m;
|
||||||
|
const summary = timelineLosSummary(elevProfileLink);
|
||||||
series.push({
|
series.push({
|
||||||
color: '#00ff88',
|
color: '#00ff88',
|
||||||
profile: elevProfileLink,
|
profile: elevProfileLink,
|
||||||
label: 'рельеф TX↔RX',
|
label: 'рельеф TX↔RX',
|
||||||
losLine: elevA != null && elevB != null ? { elevA, elevB } : null,
|
losLine: elevA != null && elevB != null
|
||||||
|
? {
|
||||||
|
elevA: summary?.elevA ?? elevA,
|
||||||
|
elevB,
|
||||||
|
groundA: elevA,
|
||||||
|
offsetA: timelineLosStartOffsetM,
|
||||||
|
delta: summary?.delta,
|
||||||
|
angleDeg: summary?.angleDeg,
|
||||||
|
totalM: summary?.total,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
cursor: timelineElevHoverDist,
|
cursor: timelineElevHoverDist,
|
||||||
});
|
});
|
||||||
return series;
|
return series;
|
||||||
@@ -2104,9 +2184,24 @@ function renderElevationCanvas(canvas, series, loadState, emptyIdleMsg) {
|
|||||||
ctx.lineTo(x1, y1);
|
ctx.lineTo(x1, y1);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
ctx.fillStyle = '#0a0a14';
|
||||||
|
ctx.strokeStyle = '#ffb74d';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x0, y0, 5.5, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fillStyle = '#ffb74d';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x0, y0, 2.5, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
ctx.fillStyle = '#ffb74d';
|
ctx.fillStyle = '#ffb74d';
|
||||||
ctx.font = '9px system-ui';
|
ctx.font = '9px system-ui';
|
||||||
ctx.fillText('прямая', x1 - 42, y1 - 4);
|
const losLabel = Number.isFinite(s.losLine.delta) && Number.isFinite(s.losLine.angleDeg)
|
||||||
|
? `прямая · Δ ${fmtSignedM(s.losLine.delta)} · ${s.losLine.angleDeg.toFixed(2)}°`
|
||||||
|
: 'прямая';
|
||||||
|
const tw = ctx.measureText(losLabel).width;
|
||||||
|
ctx.fillText(losLabel, Math.max(margin.l + 8, x1 - tw - 2), y1 - 4);
|
||||||
}
|
}
|
||||||
if (s.pointsOverlay?.length) {
|
if (s.pointsOverlay?.length) {
|
||||||
ctx.fillStyle = s.pointsOverlayColor || '#ff1744';
|
ctx.fillStyle = s.pointsOverlayColor || '#ff1744';
|
||||||
@@ -2178,7 +2273,7 @@ function renderElevationCanvas(canvas, series, loadState, emptyIdleMsg) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas._elevLayout = { margin, plotW, plotH, maxDist, minE, maxE };
|
canvas._elevLayout = { margin, plotW, plotH, maxDist, minE, maxE, series };
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawElevationChart(cursors) {
|
function drawElevationChart(cursors) {
|
||||||
@@ -3381,7 +3476,7 @@ async function scheduleLinkElevation(txPos, rxPos) {
|
|||||||
: profile.source === 'server' ? 'сервер' : (profile.source || 'данные');
|
: profile.source === 'server' ? 'сервер' : (profile.source || 'данные');
|
||||||
timelineElevBaseStatus =
|
timelineElevBaseStatus =
|
||||||
`срез TX↔RX · ${dist.toFixed(0)} m · ${src} · ${n} точек · оранжевая — прямая`;
|
`срез TX↔RX · ${dist.toFixed(0)} m · ${src} · ${n} точек · оранжевая — прямая`;
|
||||||
setElevationStatus(timelineElevBaseStatus);
|
updateTimelineElevationBaseStatus();
|
||||||
}
|
}
|
||||||
updateElevationProbeClass();
|
updateElevationProbeClass();
|
||||||
}
|
}
|
||||||
@@ -3425,7 +3520,7 @@ async function loadElevationProfiles() {
|
|||||||
: (elevProfileLink.total_m || 0);
|
: (elevProfileLink.total_m || 0);
|
||||||
timelineElevBaseStatus =
|
timelineElevBaseStatus =
|
||||||
`срез TX↔RX · ${dist.toFixed(0)} m · ${srcLabel} · ${n} точек · оранжевая — прямая`;
|
`срез TX↔RX · ${dist.toFixed(0)} m · ${srcLabel} · ${n} точек · оранжевая — прямая`;
|
||||||
setElevationStatus(timelineElevBaseStatus);
|
updateTimelineElevationBaseStatus();
|
||||||
} else if (dualTracksActive && elevProfileTx && elevProfileRx) {
|
} else if (dualTracksActive && elevProfileTx && elevProfileRx) {
|
||||||
const nTx = elevationPointCount(elevProfileTx);
|
const nTx = elevationPointCount(elevProfileTx);
|
||||||
const nRx = elevationPointCount(elevProfileRx);
|
const nRx = elevationPointCount(elevProfileRx);
|
||||||
@@ -4380,6 +4475,37 @@ document.getElementById('btnRulerClear').onclick = () => resetMapRulerPick();
|
|||||||
(function bindTimelineElevationProbe() {
|
(function bindTimelineElevationProbe() {
|
||||||
const canvas = document.getElementById('elevationCanvas');
|
const canvas = document.getElementById('elevationCanvas');
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
const losStartHit = (layout, x, y) => {
|
||||||
|
const line = layout?.series?.find(s => s.losLine)?.losLine;
|
||||||
|
if (!layout || !line || !Number.isFinite(line.elevA)) return false;
|
||||||
|
const handleX = layout.margin.l;
|
||||||
|
const handleY = layout.margin.t + layout.plotH
|
||||||
|
- ((line.elevA - layout.minE) / (layout.maxE - layout.minE)) * layout.plotH;
|
||||||
|
return Math.hypot(x - handleX, y - handleY) <= 14;
|
||||||
|
};
|
||||||
|
const setLosOffsetFromCanvasY = y => {
|
||||||
|
const layout = canvas._elevLayout;
|
||||||
|
const line = layout?.series?.find(s => s.losLine)?.losLine;
|
||||||
|
if (!layout || !line || !Number.isFinite(line.groundA)) return;
|
||||||
|
const yy = Math.max(layout.margin.t, Math.min(layout.margin.t + layout.plotH, y));
|
||||||
|
const elev = layout.minE
|
||||||
|
+ ((layout.margin.t + layout.plotH - yy) / layout.plotH) * (layout.maxE - layout.minE);
|
||||||
|
setTimelineLosStartOffset(elev - line.groundA);
|
||||||
|
};
|
||||||
|
canvas.addEventListener('mousedown', e => {
|
||||||
|
if (!dualTracksActive || elevationPointCount(elevProfileLink) === 0) return;
|
||||||
|
const layout = canvas._elevLayout;
|
||||||
|
if (!layout) return;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
if (!losStartHit(layout, x, y)) return;
|
||||||
|
timelineLosStartDrag = true;
|
||||||
|
timelineElevChartHover = true;
|
||||||
|
clearTimeout(timelineElevLeaveTimer);
|
||||||
|
e.preventDefault();
|
||||||
|
setLosOffsetFromCanvasY(y);
|
||||||
|
});
|
||||||
canvas.addEventListener('mousemove', e => {
|
canvas.addEventListener('mousemove', e => {
|
||||||
if (!dualTracksActive || elevationPointCount(elevProfileLink) === 0) return;
|
if (!dualTracksActive || elevationPointCount(elevProfileLink) === 0) return;
|
||||||
const layout = canvas._elevLayout;
|
const layout = canvas._elevLayout;
|
||||||
@@ -4388,13 +4514,38 @@ document.getElementById('btnRulerClear').onclick = () => resetMapRulerPick();
|
|||||||
clearTimeout(timelineElevLeaveTimer);
|
clearTimeout(timelineElevLeaveTimer);
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
if (timelineLosStartDrag) {
|
||||||
|
setLosOffsetFromCanvasY(y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.style.cursor = losStartHit(layout, x, y) ? 'ns-resize' : 'crosshair';
|
||||||
const dist = ((x - layout.margin.l) / layout.plotW) * layout.maxDist;
|
const dist = ((x - layout.margin.l) / layout.plotW) * layout.maxDist;
|
||||||
setTimelineElevCursor(dist);
|
setTimelineElevCursor(dist);
|
||||||
});
|
});
|
||||||
canvas.addEventListener('mouseleave', () => {
|
canvas.addEventListener('mouseleave', () => {
|
||||||
|
canvas.style.cursor = '';
|
||||||
|
if (timelineLosStartDrag) return;
|
||||||
timelineElevChartHover = false;
|
timelineElevChartHover = false;
|
||||||
scheduleClearTimelineElevCursor();
|
scheduleClearTimelineElevCursor();
|
||||||
});
|
});
|
||||||
|
window.addEventListener('mouseup', () => {
|
||||||
|
if (!timelineLosStartDrag) return;
|
||||||
|
timelineLosStartDrag = false;
|
||||||
|
timelineElevChartHover = false;
|
||||||
|
updateTimelineElevationBaseStatus();
|
||||||
|
scheduleClearTimelineElevCursor();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function bindTimelineLosControls() {
|
||||||
|
const slider = document.getElementById('elevationLosStartOffset');
|
||||||
|
if (!slider) return;
|
||||||
|
slider.value = String(timelineLosStartOffsetM);
|
||||||
|
slider.addEventListener('input', () => {
|
||||||
|
setTimelineLosStartOffset(slider.value);
|
||||||
|
});
|
||||||
|
updateTimelineLosControls();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(function bindMapRulerChartProbe() {
|
(function bindMapRulerChartProbe() {
|
||||||
|
|||||||
@@ -271,6 +271,12 @@
|
|||||||
<span>Линейка высот <span id="elevationStatus" class="muted">—</span></span>
|
<span>Линейка высот <span id="elevationStatus" class="muted">—</span></span>
|
||||||
<span class="elev-legend"><span class="legend-tx">TX</span> <span class="legend-rx">RX</span></span>
|
<span class="elev-legend"><span class="legend-tx">TX</span> <span class="legend-rx">RX</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="elevationLosControls" class="elevation-los-controls">
|
||||||
|
<label>TX ант.:
|
||||||
|
<input type="range" id="elevationLosStartOffset" min="-20" max="100" step="0.5" value="0" />
|
||||||
|
</label>
|
||||||
|
<span id="elevationLosStartOffsetLabel" class="muted">+0.0 m</span>
|
||||||
|
</div>
|
||||||
<canvas id="elevationCanvas" width="800" height="130"></canvas>
|
<canvas id="elevationCanvas" width="800" height="130"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div id="qualityVizPanel">
|
<div id="qualityVizPanel">
|
||||||
|
|||||||
Reference in New Issue
Block a user