maptrait — the selectable Map trait — guide
Class:
guide. The working recipe. Shape →maptrait-design.md; zero-knowledge →maptrait-for-idiots.md.
Build a map from selectable, stackable sources
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, PointsLayer, WaysLayer};
// 1. A rotatable car-nav camera (heading 35°, pitched 55° back).
let view = MapView::new(LatLon::new(59.33, 18.06), 14.0, (1280.0, 800.0))
.with_bearing(35.0)
.with_tilt(55.0);
// 2. Selectable sources — same WaysLayer type, different ids.
let osm = WaysLayer::new("osm-streets", osm_ways); // pre-decoded Way's
let tv = WaysLayer::new("trafikverket-roads", tv_ways); // second road source
let hits = PointsLayer::new("korp-hits", collector_points, egui::Color32::RED);
// 3. Stack them under one shared renderer.
let mut facet = MapFacet::new("Stockholm", view, MapTheme::day(&theme))
.with_layer(Box::new(osm))
.with_layer(Box::new(tv))
.with_layer(Box::new(hits));
// 4. Paint (native, in an egui frame):
facet.ui(ui);
Reuse the OSM decoder (no re-parse)
// facett-osm reads the ways.parquet through its existing read_ways and wraps it:
let osm = facett_osm::ways_layer("osm-streets", "/data/sweden/ways.parquet", 0, &theme)?;
// A Trafikverket road export saved as GeoParquet plugs in identically:
let tv = facett_osm::ways_layer("trafikverket-roads", "/data/nvdb/roads.parquet", 0, &theme)?;
Select which data draws (runtime)
facet.toggle_layer("trafikverket-roads"); // -> Some(false): hide the overlay
facet.set_layer_enabled("osm-streets", true); // -> true: ensure OSM shown
let n = facet.enabled_features().len(); // features the current frame draws
Rotate / tilt the flat map (car-nav feel)
facet.view_mut().bearing_deg = heading_deg; // turn the map under a fixed screen
facet.view_mut().tilt_deg = 55.0; // pitch back toward the horizon
Day / night
facet.set_theme(MapTheme::night(&theme)); // darker paper, glowing streets
Observe (tests / web hook)
let j = facet.state_json(); // { layers:[{id,kind,enabled}], enabled_layers, features, view, night }
Bench
nornir bench run facett # includes facett.map.layer_frame (ms_per_frame)
MAP_LAYER_BENCH_N=200 cargo run --release --example nornir-bench # heavier grid
Know whether the frame is doing any work (the draw-list memo)
MapFacet builds the CPU draw list once per distinct frame, not once per frame: the walk of every enabled layer plus the PaintKey sort is memoised on (layer_epoch, view, gpu_layers, gpu_owns). A still pane re-derives nothing.
let walks = facet.draw_list_build_count(); // walks + sorts that really ran
let keys = facet.draw_list_feature_count(); // features materialised = PaintKeys computed
facet.ui(ui); // …a still frame leaves both unchanged
The counters are the oracle, not the picture and not a stopwatch. A memoised frame and a rebuilt one paint the same pixels, so the output can never show this; and absolute frame times on oden are contention-dominated (the same binary on the same fixture measured 15.91 ms and 36.78 ms four minutes apart, box load 39–262 on 32 cores), so a millisecond delta would be a measurement of the queue. Use these exactly the way way_lane_scan_count() / gpu_sig_walk_count() are used.
What invalidates it — anything in the key: push_layer / toggle_layer / set_layer_enabled (the epoch), any camera change including a resize and a bearing/tilt change (view), and the GPU lane taking or releasing a layer (gpu_layers / gpu_owns). set_theme does not: no layer's features depend on the theme.
So a pan, a wheel notch and idle auto-rotate all rebuild every frame, by design. The win is the still screen, and it is stated as work removed: over 501 identical asks of a 20 000-way facet, 1 layer walk instead of 501, 1 sort instead of 501, and 20 000 paint keys instead of 10 020 000.
cargo test -p facett-map --release --test draw_list_memo -- --nocapture # prints both numbers