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