1pub mod airway;
4
5use chrono::{Days, NaiveDate};
6use serde::{Deserialize, Serialize};
7
8pub use airway::{Airway, AirwayLocation, AirwayPoint};
9
10use crate::model::GeoPoint;
11
12pub const FAA_NASR_CYCLE_DAYS: u64 = 28;
14
15pub const FAA_NASR_SUBSCRIPTION_URL: &str =
18 "https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[non_exhaustive]
23pub enum NavDataAuthority {
24 FaaNasr,
26 FaaCifp,
28}
29
30impl NavDataAuthority {
31 pub fn slug(self) -> &'static str {
34 match self {
35 Self::FaaNasr => "faa-nasr",
36 Self::FaaCifp => "faa-cifp",
37 }
38 }
39}
40
41#[derive(Debug, thiserror::Error)]
43#[non_exhaustive]
44pub enum NavDataError {
45 #[error("could not compute {cycle_days}-day navigation-data cycle after {effective_on}")]
47 CycleEndOutOfRange {
48 effective_on: NaiveDate,
50 cycle_days: u64,
52 },
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[non_exhaustive]
58pub struct NavDataCycle {
59 pub authority: NavDataAuthority,
61 pub effective_on: NaiveDate,
63 pub next_effective_on: NaiveDate,
65 pub source: String,
67}
68
69impl NavDataCycle {
70 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 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 pub fn contains(&self, date: NaiveDate) -> bool {
104 self.effective_on <= date && date < self.next_effective_on
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[non_exhaustive]
111pub enum NavPointKind {
112 Airport,
114 Waypoint,
116 Navaid,
118 Other(String),
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124#[non_exhaustive]
125pub struct NavPoint {
126 pub ident: String,
128 pub kind: NavPointKind,
130 pub position: GeoPoint,
132 pub name: Option<String>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub region: Option<String>,
138}
139
140impl NavPoint {
141 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 #[must_use]
154 pub fn with_name(mut self, name: Option<String>) -> Self {
155 self.name = name;
156 self
157 }
158
159 #[must_use]
161 pub fn with_region(mut self, region: Option<String>) -> Self {
162 self.region = region;
163 self
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169#[non_exhaustive]
170pub struct NavDataSnapshot {
171 pub cycle: NavDataCycle,
173 pub points: Vec<NavPoint>,
175 #[serde(default, skip_serializing_if = "Vec::is_empty")]
177 pub airways: Vec<Airway>,
178}
179
180impl NavDataSnapshot {
181 pub fn new(cycle: NavDataCycle, points: Vec<NavPoint>) -> Self {
183 Self {
184 cycle,
185 points,
186 airways: Vec::new(),
187 }
188 }
189
190 #[must_use]
192 pub fn with_airways(mut self, airways: Vec<Airway>) -> Self {
193 self.airways = airways;
194 self
195 }
196
197 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 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 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 pub fn contains(&self, date: NaiveDate) -> bool {
236 self.cycle.contains(date)
237 }
238}
239
240#[cfg(test)]
241mod tests;