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 AIRAC_CYCLE_DAYS: u64 = 28;
15
16pub const FAA_NASR_CYCLE_DAYS: u64 = AIRAC_CYCLE_DAYS;
18
19pub const FAA_NASR_SUBSCRIPTION_URL: &str =
22 "https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[non_exhaustive]
27pub enum NavDataAuthority {
28 FaaNasr,
30 FaaCifp,
32}
33
34impl NavDataAuthority {
35 pub fn slug(self) -> &'static str {
38 match self {
39 Self::FaaNasr => "faa-nasr",
40 Self::FaaCifp => "faa-cifp",
41 }
42 }
43}
44
45#[derive(Debug, thiserror::Error)]
47#[non_exhaustive]
48pub enum NavDataError {
49 #[error("could not compute {cycle_days}-day navigation-data cycle after {effective_on}")]
51 CycleEndOutOfRange {
52 effective_on: NaiveDate,
54 cycle_days: u64,
56 },
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct NavDataCycle {
63 pub authority: NavDataAuthority,
65 pub effective_on: NaiveDate,
67 pub next_effective_on: NaiveDate,
69 pub source: String,
71}
72
73impl NavDataCycle {
74 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 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 pub fn contains(&self, date: NaiveDate) -> bool {
108 self.effective_on <= date && date < self.next_effective_on
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[non_exhaustive]
115pub enum NavPointKind {
116 Airport,
118 Waypoint,
120 Navaid,
122 Other(String),
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128#[non_exhaustive]
129pub struct NavPoint {
130 pub ident: String,
132 pub kind: NavPointKind,
134 pub position: GeoPoint,
136 pub name: Option<String>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub region: Option<String>,
142}
143
144impl NavPoint {
145 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 #[must_use]
158 pub fn with_name(mut self, name: Option<String>) -> Self {
159 self.name = name;
160 self
161 }
162
163 #[must_use]
165 pub fn with_region(mut self, region: Option<String>) -> Self {
166 self.region = region;
167 self
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173#[non_exhaustive]
174pub struct NavDataSnapshot {
175 pub cycle: NavDataCycle,
177 pub points: Vec<NavPoint>,
179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
181 pub airways: Vec<Airway>,
182}
183
184impl NavDataSnapshot {
185 pub fn new(cycle: NavDataCycle, points: Vec<NavPoint>) -> Self {
187 Self {
188 cycle,
189 points,
190 airways: Vec::new(),
191 }
192 }
193
194 #[must_use]
196 pub fn with_airways(mut self, airways: Vec<Airway>) -> Self {
197 self.airways = airways;
198 self
199 }
200
201 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 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 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 pub fn contains(&self, date: NaiveDate) -> bool {
240 self.cycle.contains(date)
241 }
242}
243
244#[cfg(test)]
245mod tests;