development · place labels
Place names on a one-bit map
A dot tells you something is there. It does not tell you it is Modra. Getting names onto a black-and-white e-ink map means keeping them readable over a road network without blanking out the map to do it — and doing it inside a frame budget already spent on tiles. Twelve names, 163 ms, no label boxes.
The problem with a label box
The usual answer is a white rectangle behind the text. It works, and on this panel it is expensive in the one currency that matters: map. Every label blanks a rectangle of roads, forest and water, and at the widest zoom — where names are most useful — you want a dozen of them on screen. A dozen white boxes is a dozen holes in the map.
So each name is drawn with a white halo around the glyphs, two pixels by default, knocked out letter by letter. The roads and the hatched forest keep running underneath the name. It is weaker separation than a box, and it is compact enough to afford several.
Two tiers, decided when the tiles are built
A place carries a rank in the tile: city 0, town 1, village 2, hamlet and suburb 3, farm 4. The renderer draws rank 0–1 as major — bigger, bold — and rank 2 and up as minor. Which ranks are in the tile at all is a build-time decision per level of detail, so there is no zoom-to-rank table in the firmware to keep in sync.
Names are drawn in their own title case, not in capitals. At a glance, mixed case is the faster read, and a place name is not a heading.
Greedy placement, eight tries per name
Placement cannot be decided while walking the map: where a name can go depends on where the earlier names went, and that is not known until the layer has been walked to the end. So the walk that draws the dots only offers candidates, keeping the best 32 by rank and then by distance to the viewport anchor. Nothing is drawn during it.
Then each candidate, in that order, tries eight positions around its dot — right, left, below, above, then the four diagonals — and takes the first that passes every test:
- the label’s box, inflated by the halo, is fully inside the drawable area. On the device that excludes three bands of furniture drawn after the map: the header strip, the button-hint row along the bottom and the side-hint column. Geometry may run under those and be painted over — a road that continues off the panel still reads as a road. A name with half its letters painted over reads as a different name, so the placer stays out;
- inflated further by a minimum gap, it touches no label already placed, and not the position marker;
- no more than a set percentage of it sits over the route.
A candidate that fails all eight is dropped, and its dot stays. Nothing goes missing; it just goes unnamed. That rule is what makes the whole thing safe to be greedy about.
Two ceilings, not one
The style file carries a maximum label count — an affordability ceiling, a render-cost backstop. The zoom rung carries its own: 3, 4, 6, 8, 10, 12, 14 labels from the closest rung outwards. The smaller of the two wins. A rung knows how much ground is on screen, and that is what decides how many names are useful rather than how many are cheap.
What it cost
Measured on the panel: 163 ms of a 3,646 ms frame at the widest rung, with twelve names placed. The names ride along with the walk that was already drawing place dots, so there is no extra card read for them — the same tile data that puts the dot on the map carries the name. The nearest place shown in the header strip comes from the same source, for the same reason.