Navigation display: route rail with what lies ahead, and a posted speed limit #14

Open
opened 2026-07-30 17:24:26 +00:00 by jeroen · 0 comments
Owner

Problem

Driver mode's chrome (crates/viberfox/src/systems/driver.rs:914) was specified
against two reference navigation views and covered most of their layout: a
manoeuvre card top-left, the trip numbers as a strip down the right edge, a speed
pill at the bottom centre, and the compass cycle. Comparing it element by element
against those references leaves five real gaps, four of them in the right-hand
strip alone.

1. The right strip was a column of numbers, not a route bar. Both references
build their right-hand side around a vertical rail running the vehicle to the
destination, with what you will pass marked along it in proportion. Ours showed
arrival / distance left / time left and nothing else — it answered how far is
left, never what is left.

2. There was no concept of anything along the route. Route
(systems/routing.rs:1246) carries points, distance, duration and turn steps.
No POIs, no services, no waypoints. systems/data_layers.rs streams POIs, but
minimal mode (data_layers.rs:962) disables every layer the moment driving
starts, so during a drive no POI data was even loaded.

3. No speed limit. Neither reference's speedometer stands alone. maxspeed
appears nowhere in the crate; the nearest thing, RoadKind::speed_kmh
(routing.rs:223), is a routing cost per road class and is not retained on the
route.

4. No preview of the manoeuvre after the next one. The card showed one turn.
Two turns in quick succession is exactly the case that gets wrong — you take the
first and are already committed to the second before the card changes.

5. No lane guidance, no traffic, no intermediate waypoints. All three appear
in the references. See Out of scope.

Approach

New crates/viberfox/src/systems/route_ahead.rs — what lies ahead on the route:

  • One Overpass corridor query per route (corridor_query,
    route_ahead.rs:440), around over the route's own polyline thinned to 300 m,
    fetched once when driver mode starts and cached by the query string (so
    narrowing the question is a cache miss by construction, not a stale broad
    answer served from disk).
  • Two output sets in the one request: services (out center) and maxspeed ways
    (out geom). Fuel, charging, parking and rest areas become rail markers;
    maxspeed is sampled onto the route every 25 m, nearest-way-wins
    (stamp_limit, route_ahead.rs:603), so a service road beside a trunk cannot
    claim the sign.
  • The user's saved places are projected locally and appear before the network
    answers — a rail blank for the first seconds of a drive is blank exactly when it
    is looked at.
  • Everything measured along driver::RouteGeom, now shared rather than rebuilt,
    so a rail marker at "9.4 km" and "9.4 km to go" mean the same thing.

driver.rs gains the chrome:

  • route_rail (driver.rs:1131) — the strip becomes a real rail under the trip
    numbers: vehicle at the foot, destination capped at the top, badges spread to a
    minimum gap, overflow counted as "+N", nearest item spelled out underneath,
    hover tooltip per badge.
  • rail_t (driver.rs:1271) — a two-zone scale, not linear. On a 40 km route
    a linear rail puts everything inside the next 2 km into the bottom 2% of the
    track while the empty middle takes nine tenths of the pixels. The near field
    gets RAIL_NEAR_FRACTION of the track (driver.rs:885) and the break is
    drawn as a notch — a scale that changes silently halfway up is a lie about
    distance, and distance is all that is read off it.
  • speed_limit_sign (driver.rs:1287) — the posted limit as a painted road sign
    beside the speedometer; over the limit turns the speed red, never the sign.
  • next_step_gap (driver.rs:1112) — a "then" preview inside the manoeuvre card
    when the following turn is within 300 m.

Acceptance criteria

  • The right strip shows a start-to-destination rail with proportionally placed
    markers for services, saved places and the destination.
  • Markers carry name + distance ahead + distance off the route on hover; the
    nearest one is named under the rail.
  • The rail's near field is not compressed to unreadability on a long route,
    and the scale break is visible rather than implicit.
  • The posted speed limit shows beside the speedometer where OSM has one, and
    is absent where it does not — never inferred from road class.
  • Speed reads red when over the limit by more than a tolerance.
  • The manoeuvre card previews the following turn when it is close.
  • One Overpass request per route, cached; nothing is fetched merely for
    planning a route (the rule driver.rs's module note already sets for
    minimal mode).
  • A dense city centre cannot flood the rail with car parks.

Verification

  • cargo check -p viberfox — clean.

  • cargo test -p viberfox --bin viberfox183 passed, including 12 new in
    route_ahead (maxspeed parsing incl. refusal cases, nearest-way-wins, long-way
    stamping, corridor thinning, cache keying, response splitting, remark handling,
    parking cap) and 2 new in driver (rail scale monotonicity/endpoints/clamping,
    "then" preview gating).

  • cargo clippy -p viberfox --bin viberfox — no new warnings.

  • Visual, headless (no window, per the project's screenshot rule):

    VIBE_TILE_URL="https://tiles.garage44.eu/tiles/osm/{z}/{x}/{y}" \
    VIBE_ROUTE_FROM="53.2194,6.5665" VIBE_ROUTE_TO="53.2350,6.5300" \
    cargo run -p viberfox --profile dev-bevy -- --shot /tmp/driver_rail.png \
        --shot-mode avatar --at 53.2194,6.5665,220 --look=-35,0 \
        --ui driver --size 1400x900 --settle 6 --wait 90
    

    Confirmed: rail with badges, destination cap, "+N" overflow, 30 limit sign
    beside the speedometer, "then" chip in the card. Log line
    route corridor ready items=16 limited=191.

Measurement worth keeping (2026-07-30, the route above, 4.8 km): the first
query returned 45 amenities, 43 of them parking — the rail rendered as an
identical column of P badges with "+38" and the single charging point on the
route was pushed off it. Parking is now asked for only when named, a structure or
a park-and-ride, and capped at a minority share whatever the query returns
(MAX_PARKING, route_ahead.rs:113). Same route after: 16 items. Recorded in
docs/notes/navigation-display.md along with the rejected options.

Out of scope

  • Lane guidance. Both references show lane arrows before a junction. There is
    no data: turn:lanes is not in the Shortbread vector tiles the router is built
    from, so it needs a third corridor query, a per-junction lane model, and — for
    the useful version — knowing which lane you are in. Left out rather than
    approximated.
  • Traffic and incidents. One reference colours the route line and the rail by
    congestion and puts a delay chip on it. There is no traffic feed, and adding one
    is a data-source decision, not a UI one.
  • Intermediate waypoints. The rail can draw them the day the planner supports
    more than two endpoints; RouteState (navigation.rs:196) has from and to
    only.
  • Speed-limit alerting beyond colouring the number — no chime, no persistent
    warning.
  • The route planner panel. Unchanged; the rail is driver-mode chrome. Planning
    a route still costs nothing on the network beyond the solve.

Implemented on this branch in 2c22e48. This ticket is the record of the
analysis and the deliberate exclusions.


Branch: feat/14-navigation-display-route-rail

## Problem Driver mode's chrome (`crates/viberfox/src/systems/driver.rs:914`) was specified against two reference navigation views and covered most of their layout: a manoeuvre card top-left, the trip numbers as a strip down the right edge, a speed pill at the bottom centre, and the compass cycle. Comparing it element by element against those references leaves five real gaps, four of them in the right-hand strip alone. **1. The right strip was a column of numbers, not a route bar.** Both references build their right-hand side around a vertical rail running the vehicle to the destination, with what you will pass marked along it in proportion. Ours showed arrival / distance left / time left and nothing else — it answered *how far* is left, never *what* is left. **2. There was no concept of anything along the route.** `Route` (`systems/routing.rs:1246`) carries points, distance, duration and turn steps. No POIs, no services, no waypoints. `systems/data_layers.rs` streams POIs, but minimal mode (`data_layers.rs:962`) disables every layer the moment driving starts, so during a drive no POI data was even loaded. **3. No speed limit.** Neither reference's speedometer stands alone. `maxspeed` appears nowhere in the crate; the nearest thing, `RoadKind::speed_kmh` (`routing.rs:223`), is a routing cost per road class and is not retained on the route. **4. No preview of the manoeuvre after the next one.** The card showed one turn. Two turns in quick succession is exactly the case that gets wrong — you take the first and are already committed to the second before the card changes. **5. No lane guidance, no traffic, no intermediate waypoints.** All three appear in the references. See *Out of scope*. ## Approach New `crates/viberfox/src/systems/route_ahead.rs` — what lies ahead on the route: - One Overpass **corridor** query per route (`corridor_query`, `route_ahead.rs:440`), `around` over the route's own polyline thinned to 300 m, fetched once when driver mode starts and cached by the **query string** (so narrowing the question is a cache miss by construction, not a stale broad answer served from disk). - Two output sets in the one request: services (`out center`) and `maxspeed` ways (`out geom`). Fuel, charging, parking and rest areas become rail markers; `maxspeed` is sampled onto the route every 25 m, nearest-way-wins (`stamp_limit`, `route_ahead.rs:603`), so a service road beside a trunk cannot claim the sign. - The user's saved places are projected locally and appear **before** the network answers — a rail blank for the first seconds of a drive is blank exactly when it is looked at. - Everything measured along `driver::RouteGeom`, now shared rather than rebuilt, so a rail marker at "9.4 km" and "9.4 km to go" mean the same thing. `driver.rs` gains the chrome: - `route_rail` (`driver.rs:1131`) — the strip becomes a real rail under the trip numbers: vehicle at the foot, destination capped at the top, badges spread to a minimum gap, overflow counted as "+N", nearest item spelled out underneath, hover tooltip per badge. - `rail_t` (`driver.rs:1271`) — a **two-zone** scale, not linear. On a 40 km route a linear rail puts everything inside the next 2 km into the bottom 2% of the track while the empty middle takes nine tenths of the pixels. The near field gets `RAIL_NEAR_FRACTION` of the track (`driver.rs:885`) and the break is *drawn* as a notch — a scale that changes silently halfway up is a lie about distance, and distance is all that is read off it. - `speed_limit_sign` (`driver.rs:1287`) — the posted limit as a painted road sign beside the speedometer; over the limit turns the *speed* red, never the sign. - `next_step_gap` (`driver.rs:1112`) — a "then" preview inside the manoeuvre card when the following turn is within 300 m. ## Acceptance criteria - [x] The right strip shows a start-to-destination rail with proportionally placed markers for services, saved places and the destination. - [x] Markers carry name + distance ahead + distance off the route on hover; the nearest one is named under the rail. - [x] The rail's near field is not compressed to unreadability on a long route, and the scale break is visible rather than implicit. - [x] The posted speed limit shows beside the speedometer where OSM has one, and is **absent** where it does not — never inferred from road class. - [x] Speed reads red when over the limit by more than a tolerance. - [x] The manoeuvre card previews the following turn when it is close. - [x] One Overpass request per route, cached; nothing is fetched merely for *planning* a route (the rule `driver.rs`'s module note already sets for minimal mode). - [x] A dense city centre cannot flood the rail with car parks. ## Verification - `cargo check -p viberfox` — clean. - `cargo test -p viberfox --bin viberfox` — **183 passed**, including 12 new in `route_ahead` (maxspeed parsing incl. refusal cases, nearest-way-wins, long-way stamping, corridor thinning, cache keying, response splitting, remark handling, parking cap) and 2 new in `driver` (rail scale monotonicity/endpoints/clamping, "then" preview gating). - `cargo clippy -p viberfox --bin viberfox` — no new warnings. - Visual, headless (no window, per the project's screenshot rule): ``` VIBE_TILE_URL="https://tiles.garage44.eu/tiles/osm/{z}/{x}/{y}" \ VIBE_ROUTE_FROM="53.2194,6.5665" VIBE_ROUTE_TO="53.2350,6.5300" \ cargo run -p viberfox --profile dev-bevy -- --shot /tmp/driver_rail.png \ --shot-mode avatar --at 53.2194,6.5665,220 --look=-35,0 \ --ui driver --size 1400x900 --settle 6 --wait 90 ``` Confirmed: rail with badges, destination cap, "+N" overflow, `30` limit sign beside the speedometer, "then" chip in the card. Log line `route corridor ready items=16 limited=191`. **Measurement worth keeping** (2026-07-30, the route above, 4.8 km): the first query returned **45 amenities, 43 of them parking** — the rail rendered as an identical column of `P` badges with "+38" and the single charging point on the route was pushed off it. Parking is now asked for only when named, a structure or a park-and-ride, and capped at a minority share whatever the query returns (`MAX_PARKING`, `route_ahead.rs:113`). Same route after: 16 items. Recorded in `docs/notes/navigation-display.md` along with the rejected options. ## Out of scope - **Lane guidance.** Both references show lane arrows before a junction. There is no data: `turn:lanes` is not in the Shortbread vector tiles the router is built from, so it needs a third corridor query, a per-junction lane model, and — for the useful version — knowing which lane *you* are in. Left out rather than approximated. - **Traffic and incidents.** One reference colours the route line and the rail by congestion and puts a delay chip on it. There is no traffic feed, and adding one is a data-source decision, not a UI one. - **Intermediate waypoints.** The rail can draw them the day the planner supports more than two endpoints; `RouteState` (`navigation.rs:196`) has `from` and `to` only. - **Speed-limit alerting** beyond colouring the number — no chime, no persistent warning. - **The route planner panel.** Unchanged; the rail is driver-mode chrome. Planning a route still costs nothing on the network beyond the solve. --- Implemented on this branch in `2c22e48`. This ticket is the record of the analysis and the deliberate exclusions. --- Branch: `feat/14-navigation-display-route-rail`
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#14
No description provided.