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]>
151 lines
5.5 KiB
Rust
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());
|
|
}
|
|
}
|