//! The Map fidelity cascade (SPEC §1.2). //! //! A render request for a [`CellKey`] declares a *requested fidelity* — a //! ceiling. The resolver returns the **highest tier at or below that ceiling //! that holds data for the cell**, short-circuiting on a cache hit (F0) and //! falling back toward the F1 base floor, which the standard requires to be //! defined everywhere on the datum. The shared family invariant holds: stop at //! the first tier that closes the request, and record it. //! //! The cascade is pure: it depends only on [`map_schema`] and a [`TileSource`] //! the caller supplies. Building the energy receipt for the closing tier is the //! caller's job (see `map-receipt`), keeping metering and resolution separate. use map_schema::{CellKey, ContentId, Tier}; /// What a resolver can ask of a tile store. Implementations back this with a /// tileset directory, an OPFS cache, a live entity feed, etc. pub trait TileSource { /// Is the cell already resolved in the local store (the F0 short-circuit)? /// Returns the cached tile's content id. fn cached(&self, cell: &CellKey) -> Option; /// Does `tier` hold data for `cell`? Returns the tile's content id if so. /// /// A conforming source MUST return `Some` for [`Tier::Base`] for any cell on /// the datum — the floor that makes graceful fallback a property of the /// standard (SPEC §1.2). fn tile(&self, cell: &CellKey, tier: Tier) -> Option; } /// The outcome of resolving a render request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Resolution { /// The tier that closed the request. pub tier: Tier, /// The content id of the tile that closed it. pub content_id: ContentId, /// Whether the resolver fell back below the requested fidelity (true when a /// higher tier was requested but absent for this cell). pub fell_back: bool, } #[derive(Debug, thiserror::Error)] pub enum CascadeError { /// The source violated SPEC §1.2: not even the F1 base floor was present for /// a cell the source was asked to cover. #[error("uncovered region: no F1 base tile for the requested cell")] UncoveredRegion, } /// The fidelity ladder, descending (excludes the F0 cache short-circuit, which /// is handled separately). A request walks this filtered to tiers at or below /// the ceiling, taking the first that holds data. const LADDER_DESC: [Tier; 4] = [Tier::Photoreal, Tier::Feature, Tier::Field, Tier::Base]; /// Resolve a render request (SPEC §1.2). /// /// `requested` is the fidelity ceiling. A request below [`Tier::Base`] (i.e. /// `Cache`) is clamped to `Base`, since `Cache` is a short-circuit, not a /// fidelity a caller targets. pub fn resolve( source: &dyn TileSource, cell: &CellKey, requested: Tier, ) -> Result { // F0 — cache short-circuit. Cheapest tier; closes any previously-resolved cell. if let Some(content_id) = source.cached(cell) { return Ok(Resolution { tier: Tier::Cache, content_id, fell_back: false }); } let ceiling = requested.rank().max(Tier::Base.rank()); // Walk the ladder from the ceiling downward; first tier with data wins. for tier in LADDER_DESC { if tier.rank() > ceiling { continue; // MUST NOT return a tier above the request. } if let Some(content_id) = source.tile(cell, tier) { return Ok(Resolution { tier, content_id, fell_back: tier.rank() < ceiling, }); } } // The floor is required to be defined everywhere; absence is a source bug. Err(CascadeError::UncoveredRegion) } #[cfg(test)] mod tests { use super::*; use map_schema::{AltBand, Lod, S2CellId, TimeBucket}; use std::collections::HashMap; fn cell(s2: u64) -> CellKey { CellKey { s2: S2CellId(s2), alt: AltBand(0), time: TimeBucket::ATEMPORAL, lod: Lod(0) } } /// A source described by which tiers each cell holds, plus a cache set. #[derive(Default)] struct Mock { present: HashMap>, cache: Vec, } impl TileSource for Mock { fn cached(&self, c: &CellKey) -> Option { self.cache.contains(&c.s2.0).then(|| ContentId::of_bytes(b"cached")) } fn tile(&self, c: &CellKey, tier: Tier) -> Option { self.present .get(&c.s2.0) .filter(|tiers| tiers.contains(&tier)) .map(|_| ContentId::of_bytes(&[tier.rank()])) } } #[test] fn returns_highest_available_at_or_below_ceiling() { let mut m = Mock::default(); m.present.insert(1, vec![Tier::Base, Tier::Field, Tier::Photoreal]); // ceiling Photoreal -> Photoreal closes, no fallback let r = resolve(&m, &cell(1), Tier::Photoreal).unwrap(); assert_eq!(r.tier, Tier::Photoreal); assert!(!r.fell_back); // ceiling Field -> must NOT return Photoreal; Field closes let r = resolve(&m, &cell(1), Tier::Field).unwrap(); assert_eq!(r.tier, Tier::Field); assert!(!r.fell_back); } #[test] fn falls_back_to_floor_when_higher_tiers_absent() { let mut m = Mock::default(); m.present.insert(2, vec![Tier::Base]); // only the floor let r = resolve(&m, &cell(2), Tier::Photoreal).unwrap(); assert_eq!(r.tier, Tier::Base); assert!(r.fell_back); // requested Photoreal, got Base } #[test] fn cache_short_circuits() { let mut m = Mock::default(); m.present.insert(3, vec![Tier::Base, Tier::Photoreal]); m.cache.push(3); let r = resolve(&m, &cell(3), Tier::Photoreal).unwrap(); assert_eq!(r.tier, Tier::Cache); assert!(!r.fell_back); } #[test] fn uncovered_region_is_an_error() { let m = Mock::default(); // nothing present, including the floor assert!(matches!(resolve(&m, &cell(9), Tier::Base), Err(CascadeError::UncoveredRegion))); } }