facett-geomap — crate card
Reverse-engineered (from source — verify, don't trust the name)
- One-line pitch: A scrollable/zoomable geo map with a live viewport — a Web-Mercator point layer + a sized/labelled cluster-hotspot layer over an optional vector basemap.
- Does: Unlike
facett_map::MapView(static fit-to-rect), carries a pannable/zoomable viewport and draws three stacked layers: (1)(lon,lat)population points, (2)Hotspots drawn as count-sized circles with badge+label, (3) an optional basemap reusingfacett_map::Ways (pre-projected to Mercator).OsmView/MercatorCamera/PointGrid(inosm2d) is the OSM 2D viewer with an optional GPU line-ways fast-path (map-wgpu) through facett-map's kernel. AFacetexposing full viewport + all layers asstate_json;mercator()projection matches facett-map. Graceful: no ways → point+hotspot only. Beautify:OsmView::with_palette/cartographic(day)selects a fixed cartographic preset (facett-mapPaletteMode) alongside the theme-derived look — reskins the built way set + paints the preset land background (state_json["palette"]); in a cartographic preset roads gain a dark casing stroke under the bright fill (CPU lane; theme mode stays byte-identical). Address pins:OsmView::with_pins(Vec<Pin>)overlays labelled markers registered to(lon,lat)on the CPU f64to_pxlane (reuses the GeoMap marker + stable-id + click-hit-test pattern, no twin) — a click resolves to a pin's stable id (Msg::SelectPin/ClearPin,caps().selectable, per-pin AccessKit node),state_jsonreportspins/visible_pins/selected_pin. Clustering still TODO. Graph overlay:OsmView::with_graph(GeoGraph)draws a facett-graphviewGraphModel(reused — nodes/edges/kind colours, no twin) over the basemap, each node placed at a geo(lon,lat)and projected through the MAP camera'sto_px(shared coordinate space, dodging graphview's[0.25,4]zoom clamp);state_jsonreportsgraph_nodes/graph_edges/visible_graph_nodes. Adds afacett-graphviewdep (graphview → facett-core only, no cycle). Optional 2.5D tilt:MercatorCameracarries apitch_deg(OsmView::with_tilt(deg)/Msg::SetTilt, clamped[0, TILT_MAX_DEG=60]) that leans the flat ground plane back toward the horizon — a perspective foreshortening applied inside the singleto_px(exactly inverted into_merc, so zoom-to-cursor + hit-testing stay truthful) that every layer projects through, so ways/points/pins/graph all tilt as one and stay registered; no textures/lighting, never touchesfacett-map3d.pitch_deg == 0is byte-identical to the top-down map; the GPU line lane (flat) falls back to the CPU painter while tilted;viewport_merc_bboxunprojects all four pane corners so the receding trapezoid is fully culled.state_jsonreportspitch_deg/tilted. - Key deps:
facett-core(Facet, theme),facett-map(Way,tessellate, shared projection + GPU kernel),egui,serde_json; optionalegui-wgpu(GPU basemap host). - Camera fit — aspect-correct + resize-aware (the "map frames SMALL in the embedded canvas" fix):
MercatorCamera::fit_merc_bboxfits per axis (min(w/span_x, h/span_y) × FIT_FILL,FIT_FILL0.92), the way a slippy map'sfitBoundsdoes. It previously fitted the data's LARGER span into the pane's SMALLER side (max(span_x, span_y)intomin(w, h)) — a "square" fit that is correct only when the data's long axis happens to align with the pane's short side, and otherwise under-zooms badly (a 0.08 × 0.02 bbox in a 1140 × 620 pane fitted 1.8× too small). Second, the one-shot fit is now resize-aware: an embedded wasm canvas (Streamlit/iframe) lays out progressively, so a pane can report an intermediate size (e.g. 300 × 200 — which clearsMIN_FIT_RECT_PX= 64) and commit the fit there, stranding the camera at a third of the real canvas. A pane that changes size by more thanREFIT_SIZE_TOLERANCE(12%) re-fits, unless the user has taken over navigation (user_navigated, set by pan/zoomMsgs), whose framing is preserved. Native and embedded share the renderer, so one fix serves both (LAW #2). Guards:fit_fills_the_binding_axis_at_every_aspect_ratio(5 data/pane aspect combinations incl. portrait-data-in-landscape-canvas — the Sweden case) anda_pane_that_grows_after_its_first_fit_refits_unless_the_user_navigated. - deck.gl-parity render quality (what we adopted, and why the OSM 2D looked worse): the full deck.gl study lives in
facett-map/.nornir/facett-map.md; two of its lessons land HERE. (a) One width formula — the CPU basemap painter's ad-hocclamp(zoom/50_000, 0.5, 3.0)multiplier is gone; both the fills/outlines and the GPU line lane now resolve widths throughfacett_map::style::line_width_px/line_width_scale(deck.gl'swidthScale+widthMinPixels/widthMaxPixels), passed to the shader as a per-framewidth_scaleuniform onMapPaintCallback. Before this, the CPU land outlines grew with zoom while the GPU roads stayed frozen at a baked constant — the same road 2–3× apart. (b) Label repeat-distance declutter (deck.glCollisionFilterExtension) — OSM splits one street into manyWays and the label pass lettered each one that cleared the box test, so a single street printed its name 5–7× in one view and starved every other street of the 48-label budget. The pass now rejects a name placed withinMIN_REPEAT_PX(380 px) of the same name and pads collision boxes, so the budget goes to distinct streets (measured on the embedded Liechtenstein clip at 16× fit zoom: "Feldkircher Strasse" ×7 → ×2, "Planknerstrasse" ×5 → ×1, and ~25 additional distinct street names appear). Rendered evidence + the contracts:facett-demo/tests/osm2d_deckgl_quality.rs. - One-scale invariant (the "two scales of land and lines" guard): the CPU basemap (land/water fills) projects each vertex through the f64
MercatorCamera::to_px, while the GPU line lane bakes vertices origin-local against the index's cachedMapIndex::originand projects viafacett_map::gpu::MapTransform. Both lanes MUST land at the same screen scale/position or the land and the roads render at two different scales. This is guarded headlessly bygpu_line_lane_and_cpu_basemap_share_one_scale_at_high_zoom(osm2d.rs tests,map-wgpu): it replays the exact live GPU path (real cached origin,cam.zoom as f32) atZOOM_MAXand asserts every GPU-projected vertex is within 1 px of its CPU twin. A reported "two scales when zooming in" reproduces as a large gap here; the origin-local rebase + the cached bbox-min origin keep it sub-pixel (verified: worst gap < 1 px over the sample set at max zoom). - Shape: lib; small (lib.rs + osm2d.rs + osm2d/clip.rs).
- Observed role in the workspace: Re-exported by umbrella
facettasfacett::geomap. Fed by hosts like korp (simulated population lat/lon, ekonomiskt_bistand cluster hotspots, real ways.parquet basemap). Itsmap-wgpupassthrough is toggled by the umbrella'smap-wgpufeature.
Fill in (one line each — only the human knows these)
- Primary consumer(s):
- Why it exists (vs the obvious alternative):
- Hard constraint (airgap / no-C-deps / no-FFI / pure-Rust / etc.):
- Release cadence & independence (rides a parent? own repo someday?):
- Status (production / prototype / experimental / stubbed):
- Must-NOT-change invariant:
- Biggest known gap or TODO:
- Anything a newcomer would get wrong about it: