Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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})");
    }
}