Two sources, each wrong in its own way
The altimeter records the shape of a ride precisely, every bump and dip, but its level drifts with the weather and with how it was calibrated at the start. The terrain model is a 30 m grid of the whole world. It doesn't drift, but it misses small bumps and measures the top of trees and buildings, so it reads 20 to 30 m high in forests.
So the chart keeps the altimeter's shape and only moves it up or down to the terrain model's level. Where the altimeter can't be trusted, the terrain model takes over for that stretch.
The steps, in the order the code runs them
- Look up the terrain model every 100 m and take the median of each five neighbouring lookups.
- Find where the altimeter can't be trusted: frozen, jumped or dipped.
- Grow those places into whole stretches.
- Correct the drift on the stretches in between, separately on each.
- Blend from one to the other over 100 m at each edge.
Terrain lookups and their median
ANCHOR_SPACING_M 100 mmedian of 5 lookupsThe terrain model is looked up at the first sample with a GPS position after every 100 m of riding. These points are the anchors: all the rules below are checked at anchors. The position is rounded to 5 decimals (about 1 m), the elevation is interpolated between the four nearest 30 m pixels and cached in build/elevation.json.
A single bad lookup would put a spike in the chart wherever the terrain model stands in, so the terrain line uses, at each anchor, the median of that lookup and the two before and after it. One or two stray values out of five never win. The cost is that sharp corners get cut a little, for example at the bottom of a lift where the line turns from flat to steep.
Frozen altimeter
FROZEN_WINDOW_M 1'000 mFROZEN_MINIMUM_CLIMB_M 20 mFROZEN_RATIO 25 %FROZEN_SMOOTHING_M 700 mSome altimeters stop moving, or barely move, for kilometres. This has been common on rides since October 2025. At each anchor the code looks at the 1 km around it (500 m before, 500 m after): if the terrain rises or falls by at least 20 m there, but the altimeter changes by less than a quarter of that, the altimeter is taken as frozen.
For this test the raw lookups are first smoothed with a running median over 700 m, so a tree canopy or a bridge up to about 300 m long doesn't count as a climb the altimeter missed.
Jumps and recalibrations
JUMP_WINDOW_M 50 mJUMP_MINIMUM_M 20 msteeper than 100 %not on a liftSome altimeters also jump while recalibrating, often by hundreds of metres while you stand still. The code compares the altimeter at every sample with the last sample at most 50 m further along. A change of at least 20 m that is also steeper than 100 % (more up or down than along) is a jump. Lifts are left out, since they can be that steep for real.
Every anchor within 100 m of a jump is flagged. That alone covers only a short piece, but the widening in step 3 carries it over the stretch where the altimeter was off before or after the jump. So the level after the jump gets its own correction.
Pressure dips and bumps
DIP_MINIMUM_M 35 mterrain line under 20 m in 1 kmA barometric altimeter also records dips and bumps that aren't there, from gusts, a passing front or a tunnel. Where the terrain line changes by less than 20 m within 1 km but the altimeter changes by at least 35 m, the terrain model stands in.
The test uses the altimeter's own change, not its difference to the terrain model. In a forest the terrain model reads 20 to 30 m high, and that difference alone must not count as a dip. Flatness is judged on the terrain line (the median of 5 from step 1) instead of the 700 m smoothing, so a short real hill keeps its shape.
Of the three rules this one fires most often, usually for short stretches: at least one anchor is flagged as a dip on 382 rides, as frozen on 79 and as a jump on 50.
From flagged anchors to whole stretches
EXCURSION_M 10 mFROZEN_GAP_M 1'000 m+ 500 m on each sideThe three tests only flag anchors where the 1 km window clearly shows the problem. The altimeter is usually off for longer, so the flags grow in three steps:
- Widen. At every anchor, the code knows the altimeter's usual difference to the terrain line, the median over 2 km. Each flagged run spreads over neighbouring anchors where the altimeter is at least 10 m away from that usual difference, up to 1 km on either side. This catches the whole excursion of a dip, or the frozen level before a jump.
- Join. Runs less than 1 km apart become one, since flat bits in between hide a frozen altimeter.
- Extend. Each stretch grows by 500 m on both sides, half the detection window, which misses the edges.
If every anchor ends up flagged, the whole chart is the terrain line. That happened on none of the rides with an altimeter; the most is 96.8 % on shoulder slam (29.04.2026), whose altimeter was frozen almost all day.
Drift correction
CORRECTION_WINDOW_M 2'000 mseparately on each working stretchWhere the altimeter works, the chart keeps its shape and shifts it by its difference to the terrain model. At each anchor the difference is terrain lookup − altimeter; the shift is the median of those differences over 2 km of riding around it (1 km before, 1 km after), then interpolated between anchors.
The 2 km median is what lets the terrain model's own errors pass: a forest that reads 25 m high for a few hundred metres moves the median very little. It still follows real drift, which changes over tens of minutes, not metres.
The shift is computed separately on each working stretch, from that stretch's anchors only. An altimeter usually comes back from a freeze or a jump with a different offset, and the frozen part's numbers would spoil the median.
Blending at the edges
over 100 m, between two anchorsoffset held into the stretchEach anchor gets a terrain weight: 1 on a stretch where the terrain model stands in, 0 where the altimeter works. Between anchors the weight is interpolated, so it ramps from 0 to 1 over the 100 m between the last working anchor and the first flagged one. Each sample's elevation is
(1 − weight) × (altimeter + offset) + weight × terrain line
Over that ramp the offset is held at the value of its working stretch, so each side blends with its own offset. Samples with a weight of at least 0.5 count as terrain samples: they make up elevation_frozen, the share of the ride shown in the terrain colour, and the elevation totals count them from the terrain line. A sample without an altimeter reading gets the terrain line if its weight is at least 0.5, and stays empty otherwise.