pub mod airspace;
pub mod airway;
pub mod runway;
use chrono::{Days, NaiveDate};
use serde::{Deserialize, Serialize};
pub use airspace::{
Airspace, AirspaceKind, AltitudeDatum, AltitudeLimit, ControlledClass, RestrictiveKind,
};
pub use airway::{Airway, AirwayLocation, AirwayPoint};
pub use runway::{Runway, RunwayEnd};
use crate::model::GeoPoint;
pub const AIRAC_CYCLE_DAYS: u64 = 28;
pub const FAA_NASR_CYCLE_DAYS: u64 = AIRAC_CYCLE_DAYS;
pub const FAA_NASR_SUBSCRIPTION_URL: &str =
"https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NavDataAuthority {
FaaNasr,
FaaCifp,
}
impl NavDataAuthority {
pub fn slug(self) -> &'static str {
match self {
Self::FaaNasr => "faa-nasr",
Self::FaaCifp => "faa-cifp",
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum NavDataError {
#[error("could not compute {cycle_days}-day navigation-data cycle after {effective_on}")]
CycleEndOutOfRange {
effective_on: NaiveDate,
cycle_days: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NavDataCycle {
pub authority: NavDataAuthority,
pub effective_on: NaiveDate,
pub next_effective_on: NaiveDate,
pub source: String,
}
impl NavDataCycle {
pub fn faa_nasr(effective_on: NaiveDate) -> Result<Self, NavDataError> {
Self::new(
NavDataAuthority::FaaNasr,
effective_on,
FAA_NASR_CYCLE_DAYS,
FAA_NASR_SUBSCRIPTION_URL,
)
}
pub fn new(
authority: NavDataAuthority,
effective_on: NaiveDate,
cycle_days: u64,
source: impl Into<String>,
) -> Result<Self, NavDataError> {
let next_effective_on = effective_on.checked_add_days(Days::new(cycle_days)).ok_or(
NavDataError::CycleEndOutOfRange {
effective_on,
cycle_days,
},
)?;
Ok(Self {
authority,
effective_on,
next_effective_on,
source: source.into(),
})
}
pub fn contains(&self, date: NaiveDate) -> bool {
self.effective_on <= date && date < self.next_effective_on
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NavPointKind {
Airport,
Waypoint,
Navaid,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NavPoint {
pub ident: String,
pub kind: NavPointKind,
pub position: GeoPoint,
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
}
impl NavPoint {
pub fn new(ident: impl Into<String>, kind: NavPointKind, position: GeoPoint) -> Self {
Self {
ident: ident.into(),
kind,
position,
name: None,
region: None,
}
}
#[must_use]
pub fn with_name(mut self, name: Option<String>) -> Self {
self.name = name;
self
}
#[must_use]
pub fn with_region(mut self, region: Option<String>) -> Self {
self.region = region;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NavDataSnapshot {
pub cycle: NavDataCycle,
pub points: Vec<NavPoint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub airways: Vec<Airway>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub runways: Vec<Runway>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub airspaces: Vec<Airspace>,
}
impl NavDataSnapshot {
pub fn new(cycle: NavDataCycle, points: Vec<NavPoint>) -> Self {
Self {
cycle,
points,
airways: Vec::new(),
runways: Vec::new(),
airspaces: Vec::new(),
}
}
#[must_use]
pub fn with_airways(mut self, airways: Vec<Airway>) -> Self {
self.airways = airways;
self
}
#[must_use]
pub fn with_runways(mut self, runways: Vec<Runway>) -> Self {
self.runways = runways;
self
}
#[must_use]
pub fn with_airspaces(mut self, airspaces: Vec<Airspace>) -> Self {
self.airspaces = airspaces;
self
}
pub fn runways_at<'a>(&'a self, airport_ident: &'a str) -> impl Iterator<Item = &'a Runway> {
let wanted = airport_ident.trim();
self.runways
.iter()
.filter(move |rwy| rwy.airport_ident.eq_ignore_ascii_case(wanted))
}
pub fn airspaces_at<'a>(&'a self, center_ident: &'a str) -> impl Iterator<Item = &'a Airspace> {
let wanted = center_ident.trim();
self.airspaces.iter().filter(move |airspace| {
airspace
.center_ident
.as_deref()
.is_some_and(|center| center.eq_ignore_ascii_case(wanted))
})
}
pub fn resolve(&self, ident: &str) -> Option<&NavPoint> {
let wanted = ident.trim();
self.points
.iter()
.find(|point| point.ident.eq_ignore_ascii_case(wanted))
}
pub fn resolve_preferring_region(
&self,
ident: &str,
region: Option<&str>,
) -> Option<&NavPoint> {
let wanted = ident.trim();
if let Some(region) = region
&& let Some(exact) = self.points.iter().find(|point| {
point.ident.eq_ignore_ascii_case(wanted)
&& point.region.as_deref().is_some_and(|r| r == region)
})
{
return Some(exact);
}
self.resolve(wanted)
}
pub fn airways_named<'a>(&'a self, designator: &'a str) -> impl Iterator<Item = &'a Airway> {
let wanted = designator.trim();
self.airways
.iter()
.filter(move |airway| airway.ident.eq_ignore_ascii_case(wanted))
}
pub fn contains(&self, date: NaiveDate) -> bool {
self.cycle.contains(date)
}
}
#[cfg(test)]
mod tests;