use chrono::{Days, NaiveDate};
use serde::{Deserialize, Serialize};
use crate::model::GeoPoint;
pub const FAA_NASR_CYCLE_DAYS: u64 = 28;
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,
}
impl NavDataAuthority {
pub fn slug(self) -> &'static str {
match self {
Self::FaaNasr => "faa-nasr",
}
}
}
#[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>,
}
impl NavPoint {
pub fn new(ident: impl Into<String>, kind: NavPointKind, position: GeoPoint) -> Self {
Self {
ident: ident.into(),
kind,
position,
name: None,
}
}
#[must_use]
pub fn with_name(mut self, name: Option<String>) -> Self {
self.name = name;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NavDataSnapshot {
pub cycle: NavDataCycle,
pub points: Vec<NavPoint>,
}
impl NavDataSnapshot {
pub fn new(cycle: NavDataCycle, points: Vec<NavPoint>) -> Self {
Self { cycle, points }
}
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 contains(&self, date: NaiveDate) -> bool {
self.cycle.contains(date)
}
}
#[cfg(test)]
mod tests;