open-standards/map/map-rs/crates/map-route/src/feed.rs
Transaction Science ba1212361f Add aac, map, sandbox standards; catalog the standards family
Commit the previously-untracked standard directories and catalog them in the
root README alongside proof:

- aac/    — communication cargo class: portable boards + a ten-level
            offline-first cascade (Apache-2.0 code / CC-BY-4.0 spec).
- map/    — geospatial cargo class: range-requestable signed tilesets +
            an F0–F4 fidelity cascade (Apache-2.0 code / CC-BY-4.0 spec).
- sandbox/— isolated, metered, attested execution across the heterogeneous
            compute fabric (Apache-2.0 code / CC-BY-4.0 spec).
- jouleclaw-otel-authz, jouleclaw-trust — new jouleclaw reference crates.

Root README now lists aac, map, sandbox, and proof in the catalog, the tree,
and the contribution guide.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-07 17:04:36 -04:00

151 lines
5.5 KiB
Rust

//! Feed-agnostic adapter — turn any live or static source into entities
//! addressed on the standard's 4D grid.
//!
//! The thesis: an operations map is *feeds → an index you own*. The standard
//! only needs to know **where** an item is, optionally **when**, and **how
//! high**. Anything that can answer those three questions — an ADS-B flight, an
//! AIS vessel, a TLE satellite, a CVE with a geolocated asset, an incident, a
//! supply-chain node, an enterprise signal — implements [`Entity`] and resolves
//! to a `map-schema` [`CellKey`]. "Swap the feeds and it visualizes anything
//! with a location or a time" is then a property of the adapter, not a per-feed
//! rewrite.
use chrono::{DateTime, Utc};
use map_index::volume::TimeTier;
use map_schema::{CellKey, Lod, TimeBucket};
use crate::Coordinate;
/// A single observation reduced to the minimum the standard addresses by.
pub trait Entity {
/// Stable identifier within its feed (ICAO hex, MMSI, NORAD id, CVE id, asset tag, …).
fn id(&self) -> &str;
/// Ground position (WGS84).
fn coordinate(&self) -> Coordinate;
/// Altitude in metres above the datum. Surface entities leave this at 0.
fn altitude_m(&self) -> f64 {
self.coordinate().altitude.unwrap_or(0.0)
}
/// Observation time, if temporal. `None` → an atemporal entity (a fixed
/// asset, a static boundary).
fn observed_at(&self) -> Option<DateTime<Utc>> {
None
}
/// Resolve to a [`CellKey`]: S2 cell at `level`, the altitude band for
/// [`Self::altitude_m`], and the time bucket at `tier` (or atemporal).
fn cell_key(&self, level: u8, tier: TimeTier, lod: Lod) -> CellKey {
let c = self.coordinate();
let time = match self.observed_at() {
Some(dt) => map_index::time_bucket(dt.timestamp(), tier),
None => TimeBucket::ATEMPORAL,
};
map_index::cell_key(c.lat, c.lng, level, map_index::alt_band(self.altitude_m()), time, lod)
}
}
/// A source that yields entities. A demonstrator wires one of these per live
/// feed; the standard only needs the trait.
pub trait Feed {
type Item: Entity;
fn name(&self) -> &str;
fn entities(&self) -> Vec<Self::Item>;
}
/// Resolve every entity of a feed to its `(id, CellKey)` — ready to lay out in a
/// tileset directory or to index for a viewport query.
pub fn index_feed<F: Feed>(feed: &F, level: u8, tier: TimeTier, lod: Lod) -> Vec<(String, CellKey)> {
feed.entities()
.into_iter()
.map(|e| (e.id().to_string(), e.cell_key(level, tier, lod)))
.collect()
}
/// A minimal concrete [`Entity`] — a point observation with an optional time and
/// altitude. The reference type a demonstrator maps each feed record onto.
#[derive(Clone, Debug)]
pub struct PointEntity {
pub id: String,
pub coord: Coordinate,
pub altitude_m: f64,
pub observed_at: Option<DateTime<Utc>>,
}
impl PointEntity {
pub fn surface(id: impl Into<String>, lat: f64, lng: f64) -> Self {
Self { id: id.into(), coord: Coordinate::new(lat, lng), altitude_m: 0.0, observed_at: None }
}
pub fn at_altitude(id: impl Into<String>, lat: f64, lng: f64, altitude_m: f64) -> Self {
Self { id: id.into(), coord: Coordinate::new(lat, lng), altitude_m, observed_at: None }
}
pub fn observed(mut self, at: DateTime<Utc>) -> Self {
self.observed_at = Some(at);
self
}
}
impl Entity for PointEntity {
fn id(&self) -> &str {
&self.id
}
fn coordinate(&self) -> Coordinate {
self.coord
}
fn altitude_m(&self) -> f64 {
self.altitude_m
}
fn observed_at(&self) -> Option<DateTime<Utc>> {
self.observed_at
}
}
#[cfg(test)]
mod tests {
use super::*;
/// One heterogeneous feed standing in for "flights, vessels, sats, CVEs,
/// incidents" — the OSIRIS "replace the feeds" generality, expressed once.
struct MixedFeed;
impl Feed for MixedFeed {
type Item = PointEntity;
fn name(&self) -> &str {
"mixed-ops"
}
fn entities(&self) -> Vec<PointEntity> {
vec![
PointEntity::at_altitude("FLT123", 51.47, -0.45, 10_000.0), // a flight, ~high alt
PointEntity::surface("MMSI-001", 1.29, 103.85), // a vessel, surface
PointEntity::at_altitude("NORAD-25544", 0.0, 0.0, 408_000.0), // ISS, LEO
PointEntity::surface("CVE-2026-0001@asset-7", 37.77, -122.42), // a geolocated CVE
]
}
}
#[test]
fn one_trait_addresses_every_feed_kind() {
let indexed = index_feed(&MixedFeed, 12, TimeTier::Realtime, Lod(0));
assert_eq!(indexed.len(), 4);
// Each resolved to a valid level-12 S2 cell.
for (_, key) in &indexed {
assert_eq!(key.s2.level(), Some(12));
}
// Altitude separates them onto distinct bands: surface(0), high-alt, LEO.
let bands: Vec<i16> = indexed.iter().map(|(_, k)| k.alt.0).collect();
assert!(bands.contains(&0), "surface entities → band 0");
assert!(bands.contains(&3), "10 km flight → high-alt band");
assert!(bands.contains(&4), "ISS → LEO band");
}
#[test]
fn temporal_entity_carries_its_bucket() {
use chrono::TimeZone;
let dt = Utc.timestamp_opt(3_600 + 42, 0).unwrap();
let e = PointEntity::surface("x", 0.0, 0.0).observed(dt);
let key = e.cell_key(10, TimeTier::Recent, Lod(0));
assert_eq!(key.time.width_ms, 3_600_000);
assert!(!key.time.is_atemporal());
}
}