//! Bridge from orbital propagation to the standard's address space. //! //! A live satellite occupies the `F3` (feature / live-entity) tier. Its //! ground-relative address is its **sub-point**: the geodetic position of the //! propagated state, projected to an S2 cell at the requested level, with the //! altitude band for its altitude and the time bucket for the instant. This puts //! a satellite on the same 4D grid (SPEC §2) as every other tile, so an entity //! feed and a basemap share one index. use chrono::{DateTime, Utc}; use map_index::volume::TimeTier; use map_schema::{CellKey, Lod}; use crate::{satellite_position, GeodeticPosition, OrbitalResult, Sgp4State}; /// The [`CellKey`] for a geodetic position at an instant: S2 cell at `level`, /// the altitude band for `alt_km`, and the time bucket at `tier`. pub fn geodetic_cell_key( geo: &GeodeticPosition, level: u8, time_secs: i64, tier: TimeTier, lod: Lod, ) -> CellKey { map_index::cell_key( geo.lat_deg, geo.lon_deg, level, map_index::alt_band(geo.alt_km * 1000.0), map_index::time_bucket(time_secs, tier), lod, ) } /// Propagate `state` to `dt` and return the satellite sub-point's [`CellKey`]. pub fn subpoint_cell_key( state: &Sgp4State, dt: &DateTime, level: u8, tier: TimeTier, lod: Lod, ) -> OrbitalResult { let geo = satellite_position(state, dt)?; Ok(geodetic_cell_key(&geo, level, dt.timestamp(), tier, lod)) } #[cfg(test)] mod tests { use super::*; use crate::tle::parse_tle; const ISS_NAME: &str = "ISS (ZARYA)"; const ISS_L1: &str = "1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9009"; const ISS_L2: &str = "2 25544 51.6416 247.4627 0006703 30.0000 10.0000 15.49560000000007"; #[test] fn iss_subpoint_lands_in_leo_band() { let tle = parse_tle(ISS_NAME, ISS_L1, ISS_L2).unwrap(); let state = Sgp4State::initialize(&tle).unwrap(); // Realtime tier (1 s buckets) is right for a live satellite. let key = subpoint_cell_key(&state, &tle.epoch, 10, TimeTier::Realtime, Lod(0)).unwrap(); // The ISS orbits at ~408 km → the LEO altitude band (schema index 4). assert_eq!(key.alt, map_schema::AltBand(4), "ISS should be in the LEO band"); // Horizontal address is a valid S2 cell at the requested level. assert_eq!(key.s2.level(), Some(10)); // Realtime bucket is 1 s wide. assert_eq!(key.time.width_ms, 1_000); } #[test] fn geodetic_cell_key_maps_surface_to_band_zero() { let geo = GeodeticPosition { lat_deg: 37.77, lon_deg: -122.42, alt_km: 0.0 }; let key = geodetic_cell_key(&geo, 14, 0, TimeTier::Recent, Lod(0)); assert_eq!(key.alt, map_schema::AltBand(0)); assert_eq!(key.s2.level(), Some(14)); } }