facett-demo/src/demo_layers.rs
//! **Google-Maps-style layer toggles demo** — the living proof of the shared
//! [`LayerSet`](facett_map::LayerSet).
//!
//! A [`facett_map::LayerToggles`] checkbox list on the left ("turn on roads, bars,
//! kiosks…") drives a live 2D map on the right: every frame the map's `Way` feed
//! and POI markers are filtered through the *same* `LayerSet` the toggles own
//! ([`LayerSet::filter_ways`]) — the identical seam `facett-map3d`'s
//! `Map3D::from_ways_layerset` consumes, so this demo exercises the engine-agnostic
//! path. Ticking *Buildings* off drops the building fills here exactly as it drops
//! the building prisms in 3D.
//!
//! Observable (`state_json`): the toggle roster + how many ways / POIs the current
//! toggles actually draw — a headless / robot driver flips a layer and reads the
//! drawn count fall.
use facett_core::{Facet, Theme};
use facett_map::{LayerCategory, LayerToggles, MapView, Way};
/// The demo facet: a layer-toggle panel + a live 2D preview that consumes the set.
pub struct LayersDemo {
toggles: LayerToggles,
/// The full base-map feed (roads / water / buildings / landuse / coastline).
ways: Vec<Way>,
/// The full POI feed — `(category, (lon, lat))` markers (bars / kiosks / …).
pois: Vec<(LayerCategory, (f64, f64))>,
/// The persistent 2D map (camera survives across frames; its feed is re-filtered
/// every frame from the shared `LayerSet`).
map: MapView,
/// Last drawn counts (folded into `state_json` for a headless read-back).
drawn_ways: usize,
drawn_pois: usize,
}
impl Default for LayersDemo {
fn default() -> Self {
Self::new()
}
}
impl LayersDemo {
pub fn new() -> Self {
let th = Theme::default();
let ways = demo_ways(&th);
let pois = demo_pois();
// Seed the map with everything; the ui() re-feeds the filtered subset.
let mut map = MapView::new("Layers preview", Vec::new()).with_ways(ways.clone());
map.fit();
Self { toggles: LayerToggles::new("Layers"), ways, pois, map, drawn_ways: 0, drawn_pois: 0 }
}
/// The visible POI markers under the current toggles (category-filtered).
fn visible_pois(&self) -> Vec<(f64, f64)> {
let set = self.toggles.layer_set();
self.pois.iter().filter(|(c, _)| set.is_visible(*c)).map(|(_, p)| *p).collect()
}
}
impl Facet for LayersDemo {
fn title(&self) -> &str {
"Layers"
}
fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal_top(|ui| {
// ── left: the Google-Maps layers checkbox list ──
ui.vertical(|ui| {
ui.set_min_width(170.0);
self.toggles.ui(ui);
});
ui.separator();
// ── right: the live 2D map, fed the filtered subset every frame ──
ui.vertical(|ui| {
let set = self.toggles.layer_set();
let filtered = set.filter_ways(&self.ways);
let pois = self.visible_pois();
self.drawn_ways = filtered.len();
self.drawn_pois = pois.len();
// Re-feed the persistent map (camera preserved) from the shared set.
self.map.ways = filtered;
self.map.points = pois;
self.map.ui(ui);
});
});
}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"toggles": self.toggles.state_json(),
"drawn_ways": self.drawn_ways,
"drawn_pois": self.drawn_pois,
"total_ways": self.ways.len(),
"total_pois": self.pois.len(),
})
}
/// Forward toggle messages to the layer panel (`{"toggle":"bars"}`), so a
/// headless driver can flip a layer and watch `drawn_ways`/`drawn_pois` fall.
fn update_json(&mut self, msg_json: &str) {
self.toggles.update_json(msg_json);
}
}
/// A small hand-built base map around Sigtuna: a couple of roads, a lake, a park
/// (landuse), a shoreline and a few buildings — one of every base category so all
/// the base toggles do something visible.
fn demo_ways(th: &Theme) -> Vec<Way> {
let tag = |k: &str, v: &str| vec![(k.to_string(), v.to_string())];
let mut id = 1i64;
let mut w = |kv: Vec<(String, String)>, lonlat: &[(f64, f64)]| -> Option<Way> {
let way = Way::from_lonlat(id, lonlat, &kv, th);
id += 1;
way
};
let (lon0, lat0) = (17.72_f64, 59.617_f64);
let mut out = Vec::new();
// Roads.
out.extend(w(tag("highway", "primary"), &[(lon0, lat0), (lon0 + 0.02, lat0 + 0.004), (lon0 + 0.03, lat0 + 0.002)]));
out.extend(w(tag("highway", "residential"), &[(lon0 + 0.005, lat0), (lon0 + 0.005, lat0 + 0.01)]));
// Water (a lake ring).
let lake = [(lon0 + 0.012, lat0 + 0.006), (lon0 + 0.022, lat0 + 0.006), (lon0 + 0.022, lat0 + 0.012), (lon0 + 0.012, lat0 + 0.012), (lon0 + 0.012, lat0 + 0.006)];
out.extend(w(tag("natural", "water"), &lake));
// Coastline (the shore line).
out.extend(w(tag("natural", "coastline"), &[(lon0 + 0.012, lat0 + 0.006), (lon0 + 0.012, lat0 + 0.012)]));
// Landuse (a park).
let park = [(lon0, lat0 + 0.008), (lon0 + 0.008, lat0 + 0.008), (lon0 + 0.008, lat0 + 0.013), (lon0, lat0 + 0.013), (lon0, lat0 + 0.008)];
out.extend(w(tag("leisure", "park"), &park));
// Buildings (a little block).
for c in 0..4 {
let bx = lon0 + 0.002 + c as f64 * 0.0015;
let by = lat0 + 0.001;
let b = [(bx, by), (bx + 0.0008, by), (bx + 0.0008, by + 0.0008), (bx, by + 0.0008), (bx, by)];
out.extend(w(tag("building", "yes"), &b));
}
out
}
/// A handful of POI markers — one of each toggleable POI category (a bar, a kiosk,
/// a shop, a restaurant, a generic amenity) — the point feed the POI toggles show.
fn demo_pois() -> Vec<(LayerCategory, (f64, f64))> {
let (lon0, lat0) = (17.73_f64, 59.62_f64);
vec![
(LayerCategory::Bars, (lon0, lat0)),
(LayerCategory::Kiosks, (lon0 + 0.003, lat0 + 0.001)),
(LayerCategory::Shops, (lon0 + 0.006, lat0 + 0.002)),
(LayerCategory::Restaurants, (lon0 + 0.009, lat0 + 0.001)),
(LayerCategory::Amenities, (lon0 + 0.012, lat0)),
]
}
#[cfg(test)]
mod tests {
use super::*;
/// Toggling a layer through the demo facet falls the drawn count — the shared
/// `LayerSet` really drives what the preview draws (headless, no pixels).
#[test]
fn toggling_a_layer_falls_the_drawn_count() {
let mut demo = LayersDemo::new();
// Prime the drawn counts by running one headless ui pass.
let ctx = egui::Context::default();
ctx.run_ui(egui::RawInput::default(), |ui| {
egui::CentralPanel::default().show(ui, |ui| demo.ui(ui));
}).textures_delta.clear();
let before = demo.state_json()["drawn_ways"].as_u64().unwrap();
assert!(before > 0, "the preview drew some ways to start");
// Turn buildings off through the facet's message path, re-run, read the fall.
demo.update_json(r#"{"toggle":"buildings"}"#);
ctx.run_ui(egui::RawInput::default(), |ui| {
egui::CentralPanel::default().show(ui, |ui| demo.ui(ui));
}).textures_delta.clear();
let after = demo.state_json()["drawn_ways"].as_u64().unwrap();
assert!(after < before, "hiding Buildings drew fewer ways ({after} < {before})");
// A POI toggle falls the POI count too.
let pois_before = demo.state_json()["drawn_pois"].as_u64().unwrap();
demo.update_json(r#"{"toggle":"bars"}"#);
ctx.run_ui(egui::RawInput::default(), |ui| {
egui::CentralPanel::default().show(ui, |ui| demo.ui(ui));
}).textures_delta.clear();
let pois_after = demo.state_json()["drawn_pois"].as_u64().unwrap();
assert!(pois_after < pois_before, "hiding Bars drew fewer POIs ({pois_after} < {pois_before})");
}
}