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.elev-probe { cursor: crosshair; }
|
||||
.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 {
|
||||
display: none; margin-top: 8px;
|
||||
}
|
||||
|
||||
+158
-7
@@ -80,6 +80,9 @@ let elevProfileSingle = null;
|
||||
let elevProfileLink = null;
|
||||
let elevProfileLinkKey = 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 mapRulerOpen = false;
|
||||
let apiDebugOpen = false;
|
||||
@@ -1260,6 +1263,67 @@ function elevationAtDist(profile, distM) {
|
||||
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) {
|
||||
const total = haversineM(tx.lat, tx.lon, rx.lat, rx.lon);
|
||||
if (total < 1) {
|
||||
@@ -1385,7 +1449,11 @@ function setTimelineElevCursor(distM) {
|
||||
updateTimelineElevCursorMarker(pt);
|
||||
const elev = elevationAtDist(elevProfileLink, timelineElevHoverDist);
|
||||
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();
|
||||
}
|
||||
@@ -1396,7 +1464,7 @@ function scheduleClearTimelineElevCursor() {
|
||||
if (timelineElevChartHover) return;
|
||||
timelineElevHoverDist = null;
|
||||
clearTimelineElevCursorMarker();
|
||||
setElevationStatus(timelineElevBaseStatus);
|
||||
updateTimelineElevationBaseStatus();
|
||||
drawElevationChart();
|
||||
}, 60);
|
||||
}
|
||||
@@ -1408,6 +1476,7 @@ function updateElevationProbeClass() {
|
||||
'elev-probe',
|
||||
dualTracksActive && elevationPointCount(elevProfileLink) > 0
|
||||
);
|
||||
updateTimelineLosControls();
|
||||
}
|
||||
|
||||
function buildQualitySamples() {
|
||||
@@ -1974,11 +2043,22 @@ function getTimelineElevationSeries(cursors) {
|
||||
const pts = elevProfileLink.points.filter(p => p.elevation_m != null);
|
||||
const elevA = pts[0]?.elevation_m;
|
||||
const elevB = pts[pts.length - 1]?.elevation_m;
|
||||
const summary = timelineLosSummary(elevProfileLink);
|
||||
series.push({
|
||||
color: '#00ff88',
|
||||
profile: elevProfileLink,
|
||||
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,
|
||||
});
|
||||
return series;
|
||||
@@ -2104,9 +2184,24 @@ function renderElevationCanvas(canvas, series, loadState, emptyIdleMsg) {
|
||||
ctx.lineTo(x1, y1);
|
||||
ctx.stroke();
|
||||
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.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) {
|
||||
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) {
|
||||
@@ -3381,7 +3476,7 @@ async function scheduleLinkElevation(txPos, rxPos) {
|
||||
: profile.source === 'server' ? 'сервер' : (profile.source || 'данные');
|
||||
timelineElevBaseStatus =
|
||||
`срез TX↔RX · ${dist.toFixed(0)} m · ${src} · ${n} точек · оранжевая — прямая`;
|
||||
setElevationStatus(timelineElevBaseStatus);
|
||||
updateTimelineElevationBaseStatus();
|
||||
}
|
||||
updateElevationProbeClass();
|
||||
}
|
||||
@@ -3425,7 +3520,7 @@ async function loadElevationProfiles() {
|
||||
: (elevProfileLink.total_m || 0);
|
||||
timelineElevBaseStatus =
|
||||
`срез TX↔RX · ${dist.toFixed(0)} m · ${srcLabel} · ${n} точек · оранжевая — прямая`;
|
||||
setElevationStatus(timelineElevBaseStatus);
|
||||
updateTimelineElevationBaseStatus();
|
||||
} else if (dualTracksActive && elevProfileTx && elevProfileRx) {
|
||||
const nTx = elevationPointCount(elevProfileTx);
|
||||
const nRx = elevationPointCount(elevProfileRx);
|
||||
@@ -4380,6 +4475,37 @@ document.getElementById('btnRulerClear').onclick = () => resetMapRulerPick();
|
||||
(function bindTimelineElevationProbe() {
|
||||
const canvas = document.getElementById('elevationCanvas');
|
||||
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 => {
|
||||
if (!dualTracksActive || elevationPointCount(elevProfileLink) === 0) return;
|
||||
const layout = canvas._elevLayout;
|
||||
@@ -4388,13 +4514,38 @@ document.getElementById('btnRulerClear').onclick = () => resetMapRulerPick();
|
||||
clearTimeout(timelineElevLeaveTimer);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
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;
|
||||
setTimelineElevCursor(dist);
|
||||
});
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
canvas.style.cursor = '';
|
||||
if (timelineLosStartDrag) return;
|
||||
timelineElevChartHover = false;
|
||||
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() {
|
||||
|
||||
@@ -271,6 +271,12 @@
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
<div id="qualityVizPanel">
|
||||
|
||||
Reference in New Issue
Block a user