From e9e7b5d0e715743ce041a6d1cefbe24cfdcdd13c Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Wed, 13 May 2026 00:54:39 +0200 Subject: [PATCH] SegmentCreate: add elevation profile that zooms to selected portion Shows a dim area for the visible range around the selection (4% padding) and a blue overlay for the selected segment, with a light stroke on the top edge. Both the x-domain and y-domain track the selection, so the chart zooms in as the handles narrow. Elevation min/max labels overlaid at top-left and bottom-left. --- site/src/components/SegmentCreate.svelte | 86 +++++++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/site/src/components/SegmentCreate.svelte b/site/src/components/SegmentCreate.svelte index 240d616..8781828 100644 --- a/site/src/components/SegmentCreate.svelte +++ b/site/src/components/SegmentCreate.svelte @@ -18,6 +18,7 @@ // ── Track data ──────────────────────────────────────────────────────────── let gpsPoints: [number, number][] = []; // [lat, lon] + let elevations: (number | null)[] = []; // parallel to gpsPoints let startIdx = 0; let endIdx = 0; let loadingTrack = false; @@ -89,13 +90,19 @@ if (!ts) { trackError = 'Could not load GPS track.'; loadingTrack = false; return; } const pts: [number, number][] = []; - const lats = ts.lat ?? [], lons = ts.lon ?? []; + const elevs: (number | null)[] = []; + const lats = ts.lat ?? [], lons = ts.lon ?? [], tsElevs: (number | null)[] = ts.elevation_m ?? []; for (let i = 0; i < lats.length; i++) { - if (lats[i] != null && lons[i] != null) pts.push([lats[i]!, lons[i]!]); + if (lats[i] != null && lons[i] != null) { + pts.push([lats[i]!, lons[i]!]); + const e = tsElevs[i]; + elevs.push(typeof e === 'number' ? e : null); + } } if (pts.length < 2) { trackError = 'Not enough GPS points.'; loadingTrack = false; return; } gpsPoints = pts; + elevations = elevs; startIdx = 0; endIdx = pts.length - 1; loadingTrack = false; @@ -172,6 +179,62 @@ function onStartInput() { if (startIdx >= endIdx) startIdx = Math.max(0, endIdx - 1); } function onEndInput() { if (endIdx <= startIdx) endIdx = Math.min(maxIdx, startIdx + 1); } + // ── Elevation chart ─────────────────────────────────────────────────────── + const ECHART_W = 600; + const ECHART_H = 80; + const ECHART_PAD = 4; // px margin at top + + $: chartPad = Math.max(1, Math.floor(maxIdx * 0.04)); + $: viewLo = Math.max(0, startIdx - chartPad); + $: viewHi = Math.min(maxIdx, endIdx + chartPad); + + $: elevStats = (() => { + const visible: number[] = []; + for (let i = viewLo; i <= viewHi; i++) { + const e = elevations[i]; + if (e != null) visible.push(e); + } + if (!visible.length) return { min: 0, max: 0, range: 1, hasData: false }; + let min = visible[0], max = visible[0]; + for (const v of visible) { if (v < min) min = v; if (v > max) max = v; } + return { min, max, range: Math.max(1, max - min), hasData: true }; + })(); + + function buildElevPaths( + elevs: (number | null)[], + si: number, ei: number, + vLo: number, vHi: number, + stats: { min: number; max: number; range: number; hasData: boolean }, + ): { dim: string; sel: string; selLine: string } { + if (!stats.hasData || !elevs.length) return { dim: '', sel: '', selLine: '' }; + const span = Math.max(1, vHi - vLo); + const usableH = ECHART_H - ECHART_PAD; + + function area(lo: number, hi: number): { area: string; line: string } { + let firstX = -1, lastX = -1; + const linePts: string[] = []; + for (let i = lo; i <= hi; i++) { + const e = elevs[i]; + if (e == null) continue; + const x = ((i - vLo) / span) * ECHART_W; + const y = ECHART_PAD + usableH * (1 - (e - stats.min) / stats.range); + if (firstX < 0) firstX = x; + lastX = x; + linePts.push(`L${x.toFixed(1)},${y.toFixed(1)}`); + } + if (!linePts.length) return { area: '', line: '' }; + const areaPath = `M${firstX.toFixed(1)},${ECHART_H} ${linePts.join(' ')} L${lastX.toFixed(1)},${ECHART_H} Z`; + const linePath = `M${linePts[0].slice(1)} ${linePts.slice(1).join(' ')}`; + return { area: areaPath, line: linePath }; + } + + const dimResult = area(vLo, vHi); + const selResult = area(Math.max(si, vLo), Math.min(ei, vHi)); + return { dim: dimResult.area, sel: selResult.area, selLine: selResult.line }; + } + + $: elevPaths = buildElevPaths(elevations, startIdx, endIdx, viewLo, viewHi, elevStats); + // ── Save ────────────────────────────────────────────────────────────────── async function save() { if (!canSave) return; @@ -271,7 +334,7 @@ @@ -285,6 +348,23 @@
+ + {#if elevPaths.dim} +
+ + + + {#if elevPaths.selLine} + + {/if} + +
+ {Math.round(elevStats.max)}m + {Math.round(elevStats.min)}m +
+
+ {/if} +

Drag the handles to set the segment start and end