Skip to main content

aerocontext_core/
navdata.rs

1//! Cycle-aware navigation-point data.
2
3pub mod airway;
4
5use chrono::{Days, NaiveDate};
6use serde::{Deserialize, Serialize};
7
8pub use airway::{Airway, AirwayLocation, AirwayPoint};
9
10use crate::model::GeoPoint;
11
12/// FAA NASR subscriber files advance on a 28-day effective cycle.
13/// The 28-day AIRAC grid every FAA aeronautical product follows.
14pub const AIRAC_CYCLE_DAYS: u64 = 28;
15
16/// NASR subscriber cadence (the AIRAC grid).
17pub const FAA_NASR_CYCLE_DAYS: u64 = AIRAC_CYCLE_DAYS;
18
19/// FAA page that publishes current, preview, and archived NASR subscriber
20/// files.
21pub const FAA_NASR_SUBSCRIPTION_URL: &str =
22    "https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/";
23
24/// Authority and product family for a navigation-data snapshot.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[non_exhaustive]
27pub enum NavDataAuthority {
28    /// FAA 28 Day NASR Subscription.
29    FaaNasr,
30    /// FAA Coded Instrument Flight Procedures (ARINC 424).
31    FaaCifp,
32}
33
34impl NavDataAuthority {
35    /// Stable lowercase slug used in manifests, store paths, and release
36    /// names, e.g. `"faa-nasr"`. Every variant must map to a unique slug.
37    pub fn slug(self) -> &'static str {
38        match self {
39            Self::FaaNasr => "faa-nasr",
40            Self::FaaCifp => "faa-cifp",
41        }
42    }
43}
44
45/// Failure constructing or using navigation data.
46#[derive(Debug, thiserror::Error)]
47#[non_exhaustive]
48pub enum NavDataError {
49    /// The next effective date cannot be represented.
50    #[error("could not compute {cycle_days}-day navigation-data cycle after {effective_on}")]
51    CycleEndOutOfRange {
52        /// Cycle effective date.
53        effective_on: NaiveDate,
54        /// Cycle length in days.
55        cycle_days: u64,
56    },
57}
58
59/// Publication-cycle metadata for a navigation-data snapshot.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct NavDataCycle {
63    /// Authority that published the data.
64    pub authority: NavDataAuthority,
65    /// First date this snapshot is effective.
66    pub effective_on: NaiveDate,
67    /// First date a successor cycle is effective.
68    pub next_effective_on: NaiveDate,
69    /// URL or local product identifier used to create the snapshot.
70    pub source: String,
71}
72
73impl NavDataCycle {
74    /// FAA NASR cycle metadata from an effective date.
75    pub fn faa_nasr(effective_on: NaiveDate) -> Result<Self, NavDataError> {
76        Self::new(
77            NavDataAuthority::FaaNasr,
78            effective_on,
79            FAA_NASR_CYCLE_DAYS,
80            FAA_NASR_SUBSCRIPTION_URL,
81        )
82    }
83
84    /// Cycle metadata from an authority, effective date, cycle length, and
85    /// source identifier.
86    pub fn new(
87        authority: NavDataAuthority,
88        effective_on: NaiveDate,
89        cycle_days: u64,
90        source: impl Into<String>,
91    ) -> Result<Self, NavDataError> {
92        let next_effective_on = effective_on.checked_add_days(Days::new(cycle_days)).ok_or(
93            NavDataError::CycleEndOutOfRange {
94                effective_on,
95                cycle_days,
96            },
97        )?;
98        Ok(Self {
99            authority,
100            effective_on,
101            next_effective_on,
102            source: source.into(),
103        })
104    }
105
106    /// Whether `date` falls inside this effective cycle.
107    pub fn contains(&self, date: NaiveDate) -> bool {
108        self.effective_on <= date && date < self.next_effective_on
109    }
110}
111
112/// Kind of navigation point carried by a snapshot.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[non_exhaustive]
115pub enum NavPointKind {
116    /// Airport, heliport, seaplane base, or similar landing facility.
117    Airport,
118    /// Published waypoint/fix.
119    Waypoint,
120    /// Ground-based navigation aid.
121    Navaid,
122    /// A point kind this crate does not model yet.
123    Other(String),
124}
125
126/// One published navigation point with WGS84 coordinates.
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128#[non_exhaustive]
129pub struct NavPoint {
130    /// Published identifier.
131    pub ident: String,
132    /// Point family.
133    pub kind: NavPointKind,
134    /// WGS84 coordinates.
135    pub position: GeoPoint,
136    /// Human-readable published name when present.
137    pub name: Option<String>,
138    /// ICAO region of the point (e.g. `"K2"`), when the source provides
139    /// one — disambiguates idents that recur across regions.
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub region: Option<String>,
142}
143
144impl NavPoint {
145    /// A navigation point with no display name or region.
146    pub fn new(ident: impl Into<String>, kind: NavPointKind, position: GeoPoint) -> Self {
147        Self {
148            ident: ident.into(),
149            kind,
150            position,
151            name: None,
152            region: None,
153        }
154    }
155
156    /// Set the published display name.
157    #[must_use]
158    pub fn with_name(mut self, name: Option<String>) -> Self {
159        self.name = name;
160        self
161    }
162
163    /// Set the ICAO region.
164    #[must_use]
165    pub fn with_region(mut self, region: Option<String>) -> Self {
166        self.region = region;
167        self
168    }
169}
170
171/// A cycle-tagged set of published navigation points and airways.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173#[non_exhaustive]
174pub struct NavDataSnapshot {
175    /// Publication-cycle metadata.
176    pub cycle: NavDataCycle,
177    /// Airports, waypoints, and navaids in this snapshot.
178    pub points: Vec<NavPoint>,
179    /// Published airways, when the source carries them.
180    #[serde(default, skip_serializing_if = "Vec::is_empty")]
181    pub airways: Vec<Airway>,
182}
183
184impl NavDataSnapshot {
185    /// A snapshot for one publication cycle, without airways.
186    pub fn new(cycle: NavDataCycle, points: Vec<NavPoint>) -> Self {
187        Self {
188            cycle,
189            points,
190            airways: Vec::new(),
191        }
192    }
193
194    /// Set the airways.
195    #[must_use]
196    pub fn with_airways(mut self, airways: Vec<Airway>) -> Self {
197        self.airways = airways;
198        self
199    }
200
201    /// Find a navigation point by identifier, case-insensitively.
202    pub fn resolve(&self, ident: &str) -> Option<&NavPoint> {
203        let wanted = ident.trim();
204        self.points
205            .iter()
206            .find(|point| point.ident.eq_ignore_ascii_case(wanted))
207    }
208
209    /// [`Self::resolve`], preferring an exact `(ident, region)` match when
210    /// a region hint is given (airway points carry one), falling back to
211    /// the first ident match.
212    pub fn resolve_preferring_region(
213        &self,
214        ident: &str,
215        region: Option<&str>,
216    ) -> Option<&NavPoint> {
217        let wanted = ident.trim();
218        if let Some(region) = region
219            && let Some(exact) = self.points.iter().find(|point| {
220                point.ident.eq_ignore_ascii_case(wanted)
221                    && point.region.as_deref().is_some_and(|r| r == region)
222            })
223        {
224            return Some(exact);
225        }
226        self.resolve(wanted)
227    }
228
229    /// Airways matching a designator, case-insensitively (the same ident
230    /// can exist per [`AirwayLocation`]).
231    pub fn airways_named<'a>(&'a self, designator: &'a str) -> impl Iterator<Item = &'a Airway> {
232        let wanted = designator.trim();
233        self.airways
234            .iter()
235            .filter(move |airway| airway.ident.eq_ignore_ascii_case(wanted))
236    }
237
238    /// Whether `date` falls inside this snapshot's effective cycle.
239    pub fn contains(&self, date: NaiveDate) -> bool {
240        self.cycle.contains(date)
241    }
242}
243
244#[cfg(test)]
245mod tests;