Skip to main content

aerocontext_core/
navdata.rs

1//! Cycle-aware navigation-point data.
2
3use chrono::{Days, NaiveDate};
4use serde::{Deserialize, Serialize};
5
6use crate::model::GeoPoint;
7
8/// FAA NASR subscriber files advance on a 28-day effective cycle.
9pub const FAA_NASR_CYCLE_DAYS: u64 = 28;
10
11/// FAA page that publishes current, preview, and archived NASR subscriber
12/// files.
13pub const FAA_NASR_SUBSCRIPTION_URL: &str =
14    "https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/";
15
16/// Authority and product family for a navigation-data snapshot.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[non_exhaustive]
19pub enum NavDataAuthority {
20    /// FAA 28 Day NASR Subscription.
21    FaaNasr,
22}
23
24impl NavDataAuthority {
25    /// Stable lowercase slug used in manifests, store paths, and release
26    /// names, e.g. `"faa-nasr"`. Every variant must map to a unique slug.
27    pub fn slug(self) -> &'static str {
28        match self {
29            Self::FaaNasr => "faa-nasr",
30        }
31    }
32}
33
34/// Failure constructing or using navigation data.
35#[derive(Debug, thiserror::Error)]
36#[non_exhaustive]
37pub enum NavDataError {
38    /// The next effective date cannot be represented.
39    #[error("could not compute {cycle_days}-day navigation-data cycle after {effective_on}")]
40    CycleEndOutOfRange {
41        /// Cycle effective date.
42        effective_on: NaiveDate,
43        /// Cycle length in days.
44        cycle_days: u64,
45    },
46}
47
48/// Publication-cycle metadata for a navigation-data snapshot.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[non_exhaustive]
51pub struct NavDataCycle {
52    /// Authority that published the data.
53    pub authority: NavDataAuthority,
54    /// First date this snapshot is effective.
55    pub effective_on: NaiveDate,
56    /// First date a successor cycle is effective.
57    pub next_effective_on: NaiveDate,
58    /// URL or local product identifier used to create the snapshot.
59    pub source: String,
60}
61
62impl NavDataCycle {
63    /// FAA NASR cycle metadata from an effective date.
64    pub fn faa_nasr(effective_on: NaiveDate) -> Result<Self, NavDataError> {
65        Self::new(
66            NavDataAuthority::FaaNasr,
67            effective_on,
68            FAA_NASR_CYCLE_DAYS,
69            FAA_NASR_SUBSCRIPTION_URL,
70        )
71    }
72
73    /// Cycle metadata from an authority, effective date, cycle length, and
74    /// source identifier.
75    pub fn new(
76        authority: NavDataAuthority,
77        effective_on: NaiveDate,
78        cycle_days: u64,
79        source: impl Into<String>,
80    ) -> Result<Self, NavDataError> {
81        let next_effective_on = effective_on.checked_add_days(Days::new(cycle_days)).ok_or(
82            NavDataError::CycleEndOutOfRange {
83                effective_on,
84                cycle_days,
85            },
86        )?;
87        Ok(Self {
88            authority,
89            effective_on,
90            next_effective_on,
91            source: source.into(),
92        })
93    }
94
95    /// Whether `date` falls inside this effective cycle.
96    pub fn contains(&self, date: NaiveDate) -> bool {
97        self.effective_on <= date && date < self.next_effective_on
98    }
99}
100
101/// Kind of navigation point carried by a snapshot.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[non_exhaustive]
104pub enum NavPointKind {
105    /// Airport, heliport, seaplane base, or similar landing facility.
106    Airport,
107    /// Published waypoint/fix.
108    Waypoint,
109    /// Ground-based navigation aid.
110    Navaid,
111    /// A point kind this crate does not model yet.
112    Other(String),
113}
114
115/// One published navigation point with WGS84 coordinates.
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117#[non_exhaustive]
118pub struct NavPoint {
119    /// Published identifier.
120    pub ident: String,
121    /// Point family.
122    pub kind: NavPointKind,
123    /// WGS84 coordinates.
124    pub position: GeoPoint,
125    /// Human-readable published name when present.
126    pub name: Option<String>,
127}
128
129impl NavPoint {
130    /// A navigation point with no display name.
131    pub fn new(ident: impl Into<String>, kind: NavPointKind, position: GeoPoint) -> Self {
132        Self {
133            ident: ident.into(),
134            kind,
135            position,
136            name: None,
137        }
138    }
139
140    /// Set the published display name.
141    #[must_use]
142    pub fn with_name(mut self, name: Option<String>) -> Self {
143        self.name = name;
144        self
145    }
146}
147
148/// A cycle-tagged set of published navigation points.
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150#[non_exhaustive]
151pub struct NavDataSnapshot {
152    /// Publication-cycle metadata.
153    pub cycle: NavDataCycle,
154    /// Airports, waypoints, and navaids in this snapshot.
155    pub points: Vec<NavPoint>,
156}
157
158impl NavDataSnapshot {
159    /// A snapshot for one publication cycle.
160    pub fn new(cycle: NavDataCycle, points: Vec<NavPoint>) -> Self {
161        Self { cycle, points }
162    }
163
164    /// Find a navigation point by identifier, case-insensitively.
165    pub fn resolve(&self, ident: &str) -> Option<&NavPoint> {
166        let wanted = ident.trim();
167        self.points
168            .iter()
169            .find(|point| point.ident.eq_ignore_ascii_case(wanted))
170    }
171
172    /// Whether `date` falls inside this snapshot's effective cycle.
173    pub fn contains(&self, date: NaiveDate) -> bool {
174        self.cycle.contains(date)
175    }
176}
177
178#[cfg(test)]
179mod tests;