development · track-up navigation
Track-up navigation on a panel that takes seconds to redraw
A phone map scrolls under a fixed arrow. Here a frame costs 2.5 to 3.6 seconds to build and up to 1,684 ms to put on the glass, so scrolling is not on the table. The map stands still and the marker moves inside it — and the interesting part is deciding when that is no longer good enough.
A fix does not redraw the map
A position packet arrives and is projected through the projection the frame on screen was drawn with — deliberately not a fresh one. The question is not where this fix is on a map; it is where it falls in the picture the panel is already holding. Then one of three things happens: the marker patch is redrawn where it moved, only the rectangles that changed are refreshed, or the viewport is reset and everything is built again.
The reason is arithmetic. The Android bridge sends on distance, 10 or 25 m per packet. At 6 m/px a 10 m step moves the marker under two pixels. Paying a full viewport reset for that — tile reads, a full render pass, a whole-panel waveform — is paying everything for nothing, when the map on the panel is still correct and only a 64×64 patch of it is wrong.
That decision lives in its own file as pure integer arithmetic with no renderer, projection or hardware dependency, which is why it is unit-tested on a laptop instead of on the device.
Verified by replaying a real ride
2026-08-05, on hardware: a recorded ride replayed into the device — 176 packets, zoom rung 2 (6 m/px), regional level of detail. No crash, no leak, heap flat at 58,824 bytes for the whole run.
Track-up: the map is turned, not the arrow
In track-up mode the frame is drawn rotated so the direction of travel runs up the panel, and the north indicator turns instead. The marker’s own arrow points straight up, because the map underneath it was drawn for this heading. There is no per-frame rotation cost: the orientation is chosen when the frame is built, and then it holds still.
Seven rungs, 1 to 45 m/px
The zoom ladder, what each rung shows and what it costs to draw:
| Rung | m/px | Level of detail | Panel covers |
|---|---|---|---|
| 0 | 1 | z13 | 0.5 × 0.8 km |
| 1 | 3 | z13 | 1.4 × 2.4 km |
| 2 | 6 | z12 | 2.9 × 4.8 km |
| 3 | 12 | z11 | 5.8 × 9.6 km |
| 4 | 20 | z11 | 9.6 × 16 km |
| 5 | 32 | z11 | 15.4 × 25.6 km |
| 6 | 45 | z11 | 24 × 40 km |
Rung 4 used to be the top, and it was the top for a reason: a rung’s metres-per-pixel caps the level of detail it can read, and z11 tiles serve about 24.5 m/px before a rotated viewport needs more than a 3×3 tile range. Rungs 5 and 6 go past that and read z11 anyway, which costs tiles — measured on the host renderer over the Malé Karpaty: 6 tiles and 211 KB at rung 4, 9 tiles and 354 KB at rung 5, 12 tiles and 507 KB at rung 6. On the panel that is a 3,646 ms frame, and it buys the full width of a mountain range on one screen.
Two things had to stop being constants
Adding the wide rungs broke two assumptions that had been invisible while the ladder was short:
- The marker size. It is a fixed number of pixels, so the further out you go the more ground it hides — two kilometres of it at 45 m/px. It is drawn at 6/8 and 5/8 scale on the two widest rungs.
- The distance before the marker is redrawn. Also a fixed pixel count: eight metres of ground at the closest rung, but three hundred and sixty at the widest, so the view that needed a steady trickle of updates got almost none. That floor follows the zoom now, down to 2 px at the widest rung.
A loaded route freezes the frame on purpose
The follow policy originally had no idea whether a route was loaded, and on a switchback road that cost a redraw every 500 m or worse — each one a new orientation, so the map kept spinning while the rider went up the same hill.
With a route loaded, the heading-change trigger is switched off entirely: the marker’s arrow already carries the heading relative to the frame, so turning the map buys nothing. Measured on the panel, the same 136 fixes over a switchback pass:
| Case | Full redraws | Frame orientations |
|---|---|---|
| free ride | 11 | 9 |
| route loaded | 0 | 1 |
When the map opens with a route, the device picks the zoom and the orientation that fit the whole route on one screen and draws it once — 2,537 ms for the frame below — and then leaves it alone.
Panning without losing the fix
Observation mode does the opposite: the buttons pan the map instead of following the position, with zoom on a long press. It is for looking ahead at a stop, and leaving it puts you back on the fix. Closing the map menu no longer costs a frame either — the pixels under it are kept and put back, refreshing only that patch of glass, instead of re-reading the tiles to arrive at the picture that was already there.