@@ -23,26 +23,44 @@ export interface PathLike {
2323 distance : number
2424}
2525
26- // Distance window (meters) for slope computation. Encoded polylines quantize
27- // elevation to ~0.01m, which makes ~1m bumps over <20m sub-segments read as
28- // 10%+ slopes. Computing slope over a fixed forward distance filters that
29- // noise while preserving real sustained gradients (typical climbs span ≥100m).
30- export const SLOPE_HORIZON_M = 30
31-
32- // Per-segment slope (%) using a forward distance window of SLOPE_HORIZON_M.
33- // Returned array has length elevation.length - 1, indexed by segment start.
34- export function computeWindowedSlopes ( elevation : ElevationPoint [ ] ) : number [ ] {
35- const n = elevation . length
36- if ( n < 2 ) return [ ]
37- const slopes : number [ ] = new Array ( n - 1 )
38- let j = 1
39- for ( let i = 0 ; i < n - 1 ; i ++ ) {
40- if ( j < i + 1 ) j = i + 1
41- while ( j < n - 1 && elevation [ j ] . distance - elevation [ i ] . distance < SLOPE_HORIZON_M ) j ++
42- const dist = elevation [ j ] . distance - elevation [ i ] . distance
43- slopes [ i ] = dist > 0 ? ( 100 * ( elevation [ j ] . elevation - elevation [ i ] . elevation ) ) / dist : 0
26+ export interface ElevationColorRun {
27+ fromIdx : number
28+ toIdx : number
29+ color : string
30+ }
31+
32+ // Quantize an elevation profile into colored runs for the area fill. Points are
33+ // grouped into bins at least minBinDist meters wide and each bin is colored by
34+ // its net (distance-weighted) grade — total elevation change over the bin's
35+ // distance. At normal zoom a bin is a single segment, so the color equals the
36+ // grade the hover popup reports there; when many points share one pixel the net
37+ // grade averages them, so a single noisy sample can't paint the whole pixel as a
38+ // steep climb/descent. Consecutive bins with the same color are merged so the
39+ // renderer draws one polygon per run. Indices refer to the input array.
40+ export function computeElevationColorRuns ( elevation : ElevationPoint [ ] , minBinDist : number ) : ElevationColorRun [ ] {
41+ const bins : ElevationColorRun [ ] = [ ]
42+ let binStart = 0
43+ for ( let i = 0 ; i < elevation . length - 1 ; i ++ ) {
44+ const binSpan = elevation [ i + 1 ] . distance - elevation [ binStart ] . distance
45+ if ( binSpan >= minBinDist || i === elevation . length - 2 ) {
46+ const slope =
47+ binSpan > 0 ? ( 100 * ( elevation [ i + 1 ] . elevation - elevation [ binStart ] . elevation ) ) / binSpan : 0
48+ bins . push ( { fromIdx : binStart , toIdx : i + 1 , color : getSlopeColor ( slope ) } )
49+ binStart = i + 1
50+ }
51+ }
52+
53+ // Merge consecutive bins with the same color.
54+ const runs : ElevationColorRun [ ] = [ ]
55+ for ( const bin of bins ) {
56+ const last = runs [ runs . length - 1 ]
57+ if ( last && last . color === bin . color ) {
58+ last . toIdx = bin . toIdx
59+ } else {
60+ runs . push ( { ...bin } )
61+ }
4462 }
45- return slopes
63+ return runs
4664}
4765
4866export function extractElevationPoints ( coordinates : number [ ] [ ] ) : ElevationPoint [ ] {
@@ -319,13 +337,13 @@ export function buildInclineDetail(elevation: ElevationPoint[]): ChartPathDetail
319337 return { key : '_incline' , label : 'Incline' , type : 'bars' , segments : [ ] , legend }
320338 }
321339
322- // Compute slope between consecutive points and assign incline colors
323- const slopes = computeWindowedSlopes ( elevation )
340+ // Color each segment by its exact grade so the map matches the elevation popup.
324341 const raw : PathDetailSegment [ ] = [ ]
325342 for ( let i = 0 ; i < elevation . length - 1 ; i ++ ) {
326343 const p = elevation [ i ]
327344 const q = elevation [ i + 1 ]
328- const slopePercent = slopes [ i ]
345+ const dist = q . distance - p . distance
346+ const slopePercent = dist > 0 ? ( ( q . elevation - p . elevation ) / dist ) * 100 : 0
329347 const color = getSlopeColor ( slopePercent )
330348 raw . push ( {
331349 fromDistance : p . distance ,
0 commit comments