ExplorInk
GitHub

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.

Real X4 framebuffer drawn track-up: streets running diagonally, the north indicator rotated to point left, the position marker's arrow pointing up
device Heading east, so east is up the screen and the north indicator has turned to point left. North-up and frozen-heading are both selectable, from Settings or from the map menu.

Seven rungs, 1 to 45 m/px

The zoom ladder, what each rung shows and what it costs to draw:

Rungm/pxLevel of detailPanel covers
01z130.5 × 0.8 km
13z131.4 × 2.4 km
26z122.9 × 4.8 km
312z115.8 × 9.6 km
420z119.6 × 16 km
532z1115.4 × 25.6 km
645z1124 × 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:

CaseFull redrawsFrame orientations
free ride119
route loaded01

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.

Real X4 framebuffer showing a planned route as a thick black line with an arrowhead at its far end, over roads and a river, with diagonal hatching down the left edge
device The whole route on one panel: 12 m/px, turned north-east so the route runs up the screen, 2,537 ms. The arrowhead marks the far end. The hatching on the left is honest — no map data for that square, and the reader will ask for it.

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.