Place transit and shop markers from the vector tile, not Overpass #31

Open
opened 2026-08-10 07:08:20 +00:00 by jeroen · 0 comments
Owner

Problem

The transit and shop marker layers were streamed entirely from the Overpass API.
LAYER_CELL_RADIUS = 3 (crates/viberfox/src/systems/gui/data_layers.rs:103)
means a 7×7 ring of z14 cells per layer per anchor — 98 live bbox queries to
establish a location, against a service that rate-limits, answers a timeout with
HTTP 200, and needs the 15 s → 30 s → 1 m → 2 m → 4 m backoff ladder in
CellStream to survive (data_layers.rs:678). Until Overpass answered, nothing
stood on the map.

Meanwhile map_geometry has already fetched, cached and decoded the vector tile
those markers stand on, and Shortbread puts the same features in it. Measured on
the Groningen z14 fixture (crates/viberfox/tests/fixtures/groningen_z14.mvt,
tile z14/8490/5319): public_transport holds 25 stops and pois holds 467
features, 55 of them carrying a shop. docs/notes/vector-tile-unused-layers.md:23
records public_transport as used for "nothing — transit stops come from
Overpass instead".

Approach

Two commits, already on this branch.

crates/geo/src/pois.rsextract_pois (pois.rs:91) reads a tile point
layer into classified TilePois, following the two rules furniture already
uses for the same layer: classify on key and value (pois carries no kind),
and own exactly [0, extent) so a feature buffered into two neighbouring tiles
lands in one cell with no tolerance to tune. single_point and tag_str move
here from furniture so the two readers of the layer share one implementation.
Bevy-free, per the crate's rule.

crates/viberfox/src/systems/gui/data_layers.rs — a layer cell is a tile
(LAYER_ZOOM = 14 is the tileset's deepest zoom, data_layers.rs:96), so
placement costs a tile_source cache hit and a marker stands the frame its tile
lands. It inherits that module's single-flight and byte cache rather than keeping
a parallel one.

Overpass is demoted, not deleted, because the tile carries no identity or
contact detail: no OSM id, no website, no addr:street, no ref:IFOPT (the
key transit_live fetches departure boards by). On the same fixture Overpass
answers 65 shops to the tile's 55, 32 of them with a website. Feature::id looks
like an OSM id and is not one — zero of the 55 intersect the node ids Overpass
returns for the same bbox — so it is neither a join key nor a link. The detail
pass therefore runs on its own stream at DETAIL_CELL_RADIUS = 1
(data_layers.rs:117) — 9 cells, not 49 — and merge_details
(data_layers.rs:1799) patches answers onto markers that are already standing,
matching on position (DETAIL_MATCH_M = 10 m, data_layers.rs:127) and
category. It spawns nothing, may arrive in either order, and may never arrive.

Radius 1 is sized to its two readers: a popup needs the POI inside
handle_poi_click's 500 m (data_layers.rs:1139) and a board needs its stop
inside transit_live::FETCH_RANGE's 800 m
(crates/viberfox/src/systems/nav/transit_live.rs:49), both within a 3×3 ring of
~1.4 km cells.

A raster tile source has no point layers to read, so Overpass stays the whole
marker source there — the from_tile gate is per source kind, not per layer
(data_layers.rs:758), and --shot runs with no manifest tile_url land on
that path, which keeps the fallback live rather than dead.

Acceptance criteria

  • Markers on a vector source are placed from the tile, with no Overpass
    request needed to make them appear.
  • The Overpass detail pass covers 9 cells, not 49, and runs on its own
    CellStream (data_layers.rs:488).
  • merge_details matches on position and category, spends each detail
    POI once, and is order-independent — a marker never waits on it and never
    disappears without it.
  • On a raster tile source (including a --shot run with no manifest
    tile_url), Overpass still places the markers.
  • The tile and Overpass classifiers agree, so a stop drawn from the tile and
    described by Overpass cannot land in different categories — covered by
    the_two_transit_classifiers_agree (data_layers.rs:2259) and
    the_two_shop_classifiers_agree (data_layers.rs:2279).
  • docs/notes/vector-tile-unused-layers.md:23 no longer says
    public_transport is used for nothing, and the pois row reflects the
    second reader. Not yet done — this is the one code-side item still open.
  • cargo check --target wasm32-unknown-unknown passes. Not yet run — see
    Verification.
  • Visual check: transit and shop markers stand where they used to, in the
    same colours and pole heights. Not yet run.

Verification

Runnable in the container:

cargo check -p viberfox_geo && cargo check -p viberfox
cargo test -p viberfox_geo          # incl. pois.rs:207, :220, :248
cargo test -p viberfox              # incl. data_layers.rs:2259, :2279, :2313

(Both test runs were still in flight when this issue was filed — whoever picks
it up should re-run them rather than take a green here on trust.)

Two checks that cannot run here and are the reason this ticket stays open:

  • cargo check -p viberfox --target wasm32-unknown-unknown — the container has
    no wasm target installed (rustup target list --installed shows only
    x86_64-unknown-linux-gnu and x86_64-pc-windows-gnu).
  • A visual check on a workstation — no GPU and no Vulkan driver here, so
    cargo shots / --shot cannot run. Compare transit and shop markers against
    main over Groningen at a camera altitude below
    SURFACE_LAYER_MAX_ALT = 20 km (data_layers.rs:101), on a vector
    VIBE_TILE_URL, and again on a raster one to exercise the Overpass fallback.

Out of scope

  • Removing Overpass. It remains the only source of OSM id, website,
    addr:street and ref:IFOPT, and the only marker source on a raster basemap.
  • Joining the two sources by id. Feature::id is not an OSM id; position +
    category is the match, and widening DETAIL_MATCH_M is not a fix for a missed
    one.
  • The other unused tile layers in docs/notes/vector-tile-unused-layers.md
    (addresses, place_labels, street_labels_points) — this ticket covers
    public_transport and the shop share of pois only.
  • Live departure boards (transit_live). They still key on ref:IFOPT from
    the Overpass detail pass; nothing about how they fetch or render changes here.

Branch: feat/31-pois-from-vector-tile

## Problem The transit and shop marker layers were streamed entirely from the Overpass API. `LAYER_CELL_RADIUS = 3` (`crates/viberfox/src/systems/gui/data_layers.rs:103`) means a 7×7 ring of z14 cells per layer per anchor — **98 live bbox queries** to establish a location, against a service that rate-limits, answers a timeout with HTTP 200, and needs the 15 s → 30 s → 1 m → 2 m → 4 m backoff ladder in `CellStream` to survive (`data_layers.rs:678`). Until Overpass answered, nothing stood on the map. Meanwhile `map_geometry` has already fetched, cached and decoded the vector tile those markers stand on, and Shortbread puts the same features in it. Measured on the Groningen z14 fixture (`crates/viberfox/tests/fixtures/groningen_z14.mvt`, tile z14/8490/5319): `public_transport` holds 25 stops and `pois` holds 467 features, 55 of them carrying a `shop`. `docs/notes/vector-tile-unused-layers.md:23` records `public_transport` as used for "**nothing** — transit stops come from Overpass instead". ## Approach Two commits, already on this branch. **`crates/geo/src/pois.rs`** — `extract_pois` (`pois.rs:91`) reads a tile point layer into classified `TilePoi`s, following the two rules `furniture` already uses for the same layer: classify on key *and* value (`pois` carries no `kind`), and own exactly `[0, extent)` so a feature buffered into two neighbouring tiles lands in one cell with no tolerance to tune. `single_point` and `tag_str` move here from `furniture` so the two readers of the layer share one implementation. Bevy-free, per the crate's rule. **`crates/viberfox/src/systems/gui/data_layers.rs`** — a layer cell *is* a tile (`LAYER_ZOOM = 14` is the tileset's deepest zoom, `data_layers.rs:96`), so placement costs a `tile_source` cache hit and a marker stands the frame its tile lands. It inherits that module's single-flight and byte cache rather than keeping a parallel one. Overpass is **demoted, not deleted**, because the tile carries no identity or contact detail: no OSM id, no `website`, no `addr:street`, no `ref:IFOPT` (the key `transit_live` fetches departure boards by). On the same fixture Overpass answers 65 shops to the tile's 55, 32 of them with a website. `Feature::id` looks like an OSM id and is not one — zero of the 55 intersect the node ids Overpass returns for the same bbox — so it is neither a join key nor a link. The detail pass therefore runs on its own stream at `DETAIL_CELL_RADIUS = 1` (`data_layers.rs:117`) — 9 cells, not 49 — and `merge_details` (`data_layers.rs:1799`) patches answers onto markers that are already standing, matching on position (`DETAIL_MATCH_M = 10 m`, `data_layers.rs:127`) and category. It spawns nothing, may arrive in either order, and may never arrive. Radius 1 is sized to its two readers: a popup needs the POI inside `handle_poi_click`'s 500 m (`data_layers.rs:1139`) and a board needs its stop inside `transit_live::FETCH_RANGE`'s 800 m (`crates/viberfox/src/systems/nav/transit_live.rs:49`), both within a 3×3 ring of ~1.4 km cells. A raster tile source has no point layers to read, so Overpass stays the whole marker source there — the `from_tile` gate is per *source kind*, not per layer (`data_layers.rs:758`), and `--shot` runs with no manifest `tile_url` land on that path, which keeps the fallback live rather than dead. ## Acceptance criteria - [ ] Markers on a vector source are placed from the tile, with no Overpass request needed to make them appear. - [ ] The Overpass detail pass covers 9 cells, not 49, and runs on its own `CellStream` (`data_layers.rs:488`). - [ ] `merge_details` matches on position **and** category, spends each detail POI once, and is order-independent — a marker never waits on it and never disappears without it. - [ ] On a raster tile source (including a `--shot` run with no manifest `tile_url`), Overpass still places the markers. - [ ] The tile and Overpass classifiers agree, so a stop drawn from the tile and described by Overpass cannot land in different categories — covered by `the_two_transit_classifiers_agree` (`data_layers.rs:2259`) and `the_two_shop_classifiers_agree` (`data_layers.rs:2279`). - [ ] `docs/notes/vector-tile-unused-layers.md:23` no longer says `public_transport` is used for nothing, and the `pois` row reflects the second reader. **Not yet done — this is the one code-side item still open.** - [ ] `cargo check --target wasm32-unknown-unknown` passes. **Not yet run** — see Verification. - [ ] Visual check: transit and shop markers stand where they used to, in the same colours and pole heights. **Not yet run.** ## Verification Runnable in the container: ``` cargo check -p viberfox_geo && cargo check -p viberfox cargo test -p viberfox_geo # incl. pois.rs:207, :220, :248 cargo test -p viberfox # incl. data_layers.rs:2259, :2279, :2313 ``` (Both test runs were still in flight when this issue was filed — whoever picks it up should re-run them rather than take a green here on trust.) Two checks that **cannot** run here and are the reason this ticket stays open: - `cargo check -p viberfox --target wasm32-unknown-unknown` — the container has no wasm target installed (`rustup target list --installed` shows only `x86_64-unknown-linux-gnu` and `x86_64-pc-windows-gnu`). - A visual check on a workstation — no GPU and no Vulkan driver here, so `cargo shots` / `--shot` cannot run. Compare transit and shop markers against `main` over Groningen at a camera altitude below `SURFACE_LAYER_MAX_ALT = 20 km` (`data_layers.rs:101`), on a vector `VIBE_TILE_URL`, and again on a raster one to exercise the Overpass fallback. ## Out of scope - **Removing Overpass.** It remains the only source of OSM id, `website`, `addr:street` and `ref:IFOPT`, and the only marker source on a raster basemap. - **Joining the two sources by id.** `Feature::id` is not an OSM id; position + category is the match, and widening `DETAIL_MATCH_M` is not a fix for a missed one. - **The other unused tile layers** in `docs/notes/vector-tile-unused-layers.md` (`addresses`, `place_labels`, `street_labels_points`) — this ticket covers `public_transport` and the `shop` share of `pois` only. - **Live departure boards** (`transit_live`). They still key on `ref:IFOPT` from the Overpass detail pass; nothing about how they fetch or render changes here. --- Branch: `feat/31-pois-from-vector-tile`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/cartopolis#31
No description provided.