aerocontext-core 0.1.0

Provider-neutral aeronautical-context model and the pluggable ContextProvider contract
Documentation
//! Cycle-aware navigation-point data.

use chrono::{Days, NaiveDate};
use serde::{Deserialize, Serialize};

use crate::model::GeoPoint;

/// FAA NASR subscriber files advance on a 28-day effective cycle.
pub const FAA_NASR_CYCLE_DAYS: u64 = 28;

/// FAA page that publishes current, preview, and archived NASR subscriber
/// files.
pub const FAA_NASR_SUBSCRIPTION_URL: &str =
    "https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/";

/// Authority and product family for a navigation-data snapshot.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NavDataAuthority {
    /// FAA 28 Day NASR Subscription.
    FaaNasr,
}

impl NavDataAuthority {
    /// Stable lowercase slug used in manifests, store paths, and release
    /// names, e.g. `"faa-nasr"`. Every variant must map to a unique slug.
    pub fn slug(self) -> &'static str {
        match self {
            Self::FaaNasr => "faa-nasr",
        }
    }
}

/// Failure constructing or using navigation data.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum NavDataError {
    /// The next effective date cannot be represented.
    #[error("could not compute {cycle_days}-day navigation-data cycle after {effective_on}")]
    CycleEndOutOfRange {
        /// Cycle effective date.
        effective_on: NaiveDate,
        /// Cycle length in days.
        cycle_days: u64,
    },
}

/// Publication-cycle metadata for a navigation-data snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NavDataCycle {
    /// Authority that published the data.
    pub authority: NavDataAuthority,
    /// First date this snapshot is effective.
    pub effective_on: NaiveDate,
    /// First date a successor cycle is effective.
    pub next_effective_on: NaiveDate,
    /// URL or local product identifier used to create the snapshot.
    pub source: String,
}

impl NavDataCycle {
    /// FAA NASR cycle metadata from an effective date.
    pub fn faa_nasr(effective_on: NaiveDate) -> Result<Self, NavDataError> {
        Self::new(
            NavDataAuthority::FaaNasr,
            effective_on,
            FAA_NASR_CYCLE_DAYS,
            FAA_NASR_SUBSCRIPTION_URL,
        )
    }

    /// Cycle metadata from an authority, effective date, cycle length, and
    /// source identifier.
    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(),
        })
    }

    /// Whether `date` falls inside this effective cycle.
    pub fn contains(&self, date: NaiveDate) -> bool {
        self.effective_on <= date && date < self.next_effective_on
    }
}

/// Kind of navigation point carried by a snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NavPointKind {
    /// Airport, heliport, seaplane base, or similar landing facility.
    Airport,
    /// Published waypoint/fix.
    Waypoint,
    /// Ground-based navigation aid.
    Navaid,
    /// A point kind this crate does not model yet.
    Other(String),
}

/// One published navigation point with WGS84 coordinates.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NavPoint {
    /// Published identifier.
    pub ident: String,
    /// Point family.
    pub kind: NavPointKind,
    /// WGS84 coordinates.
    pub position: GeoPoint,
    /// Human-readable published name when present.
    pub name: Option<String>,
}

impl NavPoint {
    /// A navigation point with no display name.
    pub fn new(ident: impl Into<String>, kind: NavPointKind, position: GeoPoint) -> Self {
        Self {
            ident: ident.into(),
            kind,
            position,
            name: None,
        }
    }

    /// Set the published display name.
    #[must_use]
    pub fn with_name(mut self, name: Option<String>) -> Self {
        self.name = name;
        self
    }
}

/// A cycle-tagged set of published navigation points.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NavDataSnapshot {
    /// Publication-cycle metadata.
    pub cycle: NavDataCycle,
    /// Airports, waypoints, and navaids in this snapshot.
    pub points: Vec<NavPoint>,
}

impl NavDataSnapshot {
    /// A snapshot for one publication cycle.
    pub fn new(cycle: NavDataCycle, points: Vec<NavPoint>) -> Self {
        Self { cycle, points }
    }

    /// Find a navigation point by identifier, case-insensitively.
    pub fn resolve(&self, ident: &str) -> Option<&NavPoint> {
        let wanted = ident.trim();
        self.points
            .iter()
            .find(|point| point.ident.eq_ignore_ascii_case(wanted))
    }

    /// Whether `date` falls inside this snapshot's effective cycle.
    pub fn contains(&self, date: NaiveDate) -> bool {
        self.cycle.contains(date)
    }
}

#[cfg(test)]
mod tests;