1pub mod airspace;
4pub mod airway;
5pub mod runway;
6
7use chrono::{Days, NaiveDate};
8use serde::{Deserialize, Serialize};
9
10pub use airspace::{
11 Airspace, AirspaceKind, AltitudeDatum, AltitudeLimit, ControlledClass, RestrictiveKind,
12};
13pub use airway::{Airway, AirwayLocation, AirwayPoint};
14pub use runway::{Runway, RunwayEnd};
15
16use crate::model::GeoPoint;
17
18pub const AIRAC_CYCLE_DAYS: u64 = 28;
21
22pub const FAA_NASR_CYCLE_DAYS: u64 = AIRAC_CYCLE_DAYS;
24
25pub const FAA_NASR_SUBSCRIPTION_URL: &str =
28 "https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/";
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[non_exhaustive]
33pub enum NavDataAuthority {
34 FaaNasr,
36 FaaCifp,
38}
39
40impl NavDataAuthority {
41 pub fn slug(self) -> &'static str {
44 match self {
45 Self::FaaNasr => "faa-nasr",
46 Self::FaaCifp => "faa-cifp",
47 }
48 }
49}
50
51#[derive(Debug, thiserror::Error)]
53#[non_exhaustive]
54pub enum NavDataError {
55 #[error("could not compute {cycle_days}-day navigation-data cycle after {effective_on}")]
57 CycleEndOutOfRange {
58 effective_on: NaiveDate,
60 cycle_days: u64,
62 },
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[non_exhaustive]
68pub struct NavDataCycle {
69 pub authority: NavDataAuthority,
71 pub effective_on: NaiveDate,
73 pub next_effective_on: NaiveDate,
75 pub source: String,
77}
78
79impl NavDataCycle {
80 pub fn faa_nasr(effective_on: NaiveDate) -> Result<Self, NavDataError> {
82 Self::new(
83 NavDataAuthority::FaaNasr,
84 effective_on,
85 FAA_NASR_CYCLE_DAYS,
86 FAA_NASR_SUBSCRIPTION_URL,
87 )
88 }
89
90 pub fn new(
93 authority: NavDataAuthority,
94 effective_on: NaiveDate,
95 cycle_days: u64,
96 source: impl Into<String>,
97 ) -> Result<Self, NavDataError> {
98 let next_effective_on = effective_on.checked_add_days(Days::new(cycle_days)).ok_or(
99 NavDataError::CycleEndOutOfRange {
100 effective_on,
101 cycle_days,
102 },
103 )?;
104 Ok(Self {
105 authority,
106 effective_on,
107 next_effective_on,
108 source: source.into(),
109 })
110 }
111
112 pub fn contains(&self, date: NaiveDate) -> bool {
114 self.effective_on <= date && date < self.next_effective_on
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[non_exhaustive]
121pub enum NavPointKind {
122 Airport,
124 Waypoint,
126 Navaid,
128 Other(String),
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134#[non_exhaustive]
135pub struct NavPoint {
136 pub ident: String,
138 pub kind: NavPointKind,
140 pub position: GeoPoint,
142 pub name: Option<String>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub region: Option<String>,
148}
149
150impl NavPoint {
151 pub fn new(ident: impl Into<String>, kind: NavPointKind, position: GeoPoint) -> Self {
153 Self {
154 ident: ident.into(),
155 kind,
156 position,
157 name: None,
158 region: None,
159 }
160 }
161
162 #[must_use]
164 pub fn with_name(mut self, name: Option<String>) -> Self {
165 self.name = name;
166 self
167 }
168
169 #[must_use]
171 pub fn with_region(mut self, region: Option<String>) -> Self {
172 self.region = region;
173 self
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[non_exhaustive]
180pub struct NavDataSnapshot {
181 pub cycle: NavDataCycle,
183 pub points: Vec<NavPoint>,
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
187 pub airways: Vec<Airway>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 pub runways: Vec<Runway>,
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
193 pub airspaces: Vec<Airspace>,
194}
195
196impl NavDataSnapshot {
197 pub fn new(cycle: NavDataCycle, points: Vec<NavPoint>) -> Self {
199 Self {
200 cycle,
201 points,
202 airways: Vec::new(),
203 runways: Vec::new(),
204 airspaces: Vec::new(),
205 }
206 }
207
208 #[must_use]
210 pub fn with_airways(mut self, airways: Vec<Airway>) -> Self {
211 self.airways = airways;
212 self
213 }
214
215 #[must_use]
217 pub fn with_runways(mut self, runways: Vec<Runway>) -> Self {
218 self.runways = runways;
219 self
220 }
221
222 #[must_use]
224 pub fn with_airspaces(mut self, airspaces: Vec<Airspace>) -> Self {
225 self.airspaces = airspaces;
226 self
227 }
228
229 pub fn runways_at<'a>(&'a self, airport_ident: &'a str) -> impl Iterator<Item = &'a Runway> {
231 let wanted = airport_ident.trim();
232 self.runways
233 .iter()
234 .filter(move |rwy| rwy.airport_ident.eq_ignore_ascii_case(wanted))
235 }
236
237 pub fn airspaces_at<'a>(&'a self, center_ident: &'a str) -> impl Iterator<Item = &'a Airspace> {
240 let wanted = center_ident.trim();
241 self.airspaces.iter().filter(move |airspace| {
242 airspace
243 .center_ident
244 .as_deref()
245 .is_some_and(|center| center.eq_ignore_ascii_case(wanted))
246 })
247 }
248
249 pub fn resolve(&self, ident: &str) -> Option<&NavPoint> {
251 let wanted = ident.trim();
252 self.points
253 .iter()
254 .find(|point| point.ident.eq_ignore_ascii_case(wanted))
255 }
256
257 pub fn resolve_preferring_region(
261 &self,
262 ident: &str,
263 region: Option<&str>,
264 ) -> Option<&NavPoint> {
265 let wanted = ident.trim();
266 if let Some(region) = region
267 && let Some(exact) = self.points.iter().find(|point| {
268 point.ident.eq_ignore_ascii_case(wanted)
269 && point.region.as_deref().is_some_and(|r| r == region)
270 })
271 {
272 return Some(exact);
273 }
274 self.resolve(wanted)
275 }
276
277 pub fn airways_named<'a>(&'a self, designator: &'a str) -> impl Iterator<Item = &'a Airway> {
280 let wanted = designator.trim();
281 self.airways
282 .iter()
283 .filter(move |airway| airway.ident.eq_ignore_ascii_case(wanted))
284 }
285
286 pub fn contains(&self, date: NaiveDate) -> bool {
288 self.cycle.contains(date)
289 }
290}
291
292#[cfg(test)]
293mod tests;