The idea

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

  1. Look up the terrain model every 100 m and take the median of each five neighbouring lookups.
  2. Find where the altimeter can't be trusted: frozen, jumped or dipped.
  3. Grow those places into whole stretches.
  4. Correct the drift on the stretches in between, separately on each.
  5. Blend from one to the other over 100 m at each edge.
Colours used on every chart below:altimeter as recordedterrain lookupsterrain linecorrected elevation (what the chart shows)terrain model stands in
Step 1

Terrain lookups and their median

ANCHOR_SPACING_M 100 mmedian of 5 lookups

The 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.

Schema
the lookup and two on each sidestray lookup (GPS fix after a tunnel, a tree)median of 104, 107, 62, 110, 112 = 107
Example · Whistler, day 2 · 25.06.2025 · km 0 to 0.6
7008009001'0000.00.10.20.30.40.5kmmfirst lookup: 681 mterrain line starts at 1'020 m
altimeterlookupterrain line (median of 5)
The first GPS fix of the ride still placed it in the valley, so the first lookup reads 681 m, 340 m below the next ones. The median of 681, 1'020 and 1'023 m is 1'020 m, so the terrain line ignores it.
Step 2a

Frozen altimeter

FROZEN_WINDOW_M 1'000 mFROZEN_MINIMUM_CLIMB_M 20 mFROZEN_RATIO 25 %FROZEN_SMOOTHING_M 700 m

Some 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.

Schema
1 km windowterrain ≥ 20 maltimeter < 25 % of that
Example · Chur Brambrüesch · 24.09.2026 · km 6.5 to 10.2
1'2001'4001'60078910kmmaltimeter stuck near 1'505 mtrail descends 330 mdrops, then catches uplift
altimeterlookupcorrectedterrain stands inlift
After the lift at km 7.0 the altimeter reads about 1'505 m and stays there for two kilometres while the trail drops 330 m. Then it falls and jumps around before it works again near km 9.7. The corrected elevation follows the terrain line over the whole stretch.
Step 2b

Jumps and recalibrations

JUMP_WINDOW_M 50 mJUMP_MINIMUM_M 20 msteeper than 100 %not on a lift

Some 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.

Schema
50 m along≥ 20 m up or down, steeper than 100 %altimeter recalibrates while you stand or roll slowly
Example · Klosters mit em Thömä · 12.07.2026 · km 1.0 to 2.85
1'2001'3001.01.52.02.5kmmjump: −120 m in 50 maltimeter 30 to 120 m too highafter the jump: right again
altimeterlookupcorrectedterrain stands injump
The altimeter starts this descent about 30 m too high and barely moves while the trail drops, so by km 2.2 it is 120 m off. At km 2.24 it recalibrates and falls 120 m within 50 m, after which it matches the terrain. The terrain model stands in from the start of the frozen part to well past the jump.
Step 2c

Pressure dips and bumps

DIP_MINIMUM_M 35 mterrain line under 20 m in 1 km

A 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.

Schema
1 km windowaltimeter ≥ 35 mterrain line < 20 m
Example · Wissmilen Pass mit em Schmucki · 15.07.2017 · km 50 to 52.7
42545047550050.050.551.051.552.052.5kmm+70 m bump on the flatterrain line: 425 to 428 m
altimeterlookupcorrectedterrain stands in
On the flat valley floor after the pass, the altimeter climbs 70 m and comes back down over two kilometres, while the terrain model stays between 425 and 428 m. The corrected line stays flat.

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.

Step 3

From flagged anchors to whole stretches

EXCURSION_M 10 mFROZEN_GAP_M 1'000 m+ 500 m on each side

The 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:

  1. 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.
  2. Join. Runs less than 1 km apart become one, since flat bits in between hide a frozen altimeter.
  3. Extend. Each stretch grows by 500 m on both sides, half the detection window, which misses the edges.
Schema · 30 anchors, 3 km of riding
Anchors where the altimeter is far (≥ 10 m) from its usual difference to the terrain1 · Detected: frozen, jump or dip2 · Widened over neighbours that are far off too, up to 1 km3 · Joined when less than 1 km apart4 · Extended by 500 m on both sides: the terrain model stands in here1 km = 10 anchors, one every 100 m

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.

Step 4

Drift correction

CORRECTION_WINDOW_M 2'000 mseparately on each working stretch

Where 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.

Schema
+20 m0 m−20 moffset = median over 2 km
altimeterlookupaltimeter + offset
Example · Klöntalersee · 21.09.2023 · the whole ride
-40-20020400255075100125150175kmm
terrain − altimeter at each anchoroffset applied (2 km median)terrain stands in
Over 193 km and a full day, the altimeter's offset wanders between about −20 and +25 m. The dots scatter more than that (forests, single bad lookups), but the median follows only the slow change.
Example · Klöntalersee · km 98 to 118
400600800100105110115kmmaltimeter ~20 m lowaltimeter ~15 m high
altimeterlookupcorrectedterrain stands in
Along the lake the altimeter reads about 20 m low. Down in the valley the offset reverses and it reads up to 15 m high. The corrected line keeps all of the altimeter's small bumps at the terrain's level. Around km 102.5 a pressure bump is replaced by the terrain line.
Step 5

Blending at the edges

over 100 m, between two anchorsoffset held into the stretch

Each 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.

Schema
altimeter + offsetterrain linefrozen stretchterrain weight 01100 m between two anchors
altimeter + offsetterrain lineresult
Example · Chur Brambrüesch · 24.09.2026 · km 9.35 to 10.05
1'5001'5501'6009.49.69.810.0kmmblendterrain linealtimeter + offset
altimeterterrain linecorrectedweight ≥ 0.5
The end of the frozen stretch from step 2a. Between the anchors at km 9.70 and 9.80 the corrected line slides from the terrain line down onto the altimeter, about 20 m lower here, shifted by the small offset of the stretch that follows.
What comes out

On the rides

954rides with an altimeter, all corrected for drift
437with the terrain model standing in somewhere