aerocontext-planning 0.4.2

Flight-route planning over aerocontext: Garmin .fpl import/export, route expansion, corridor generation, and the vertical cross-section
Documentation
//! Flight-plan route strings: tokenize, then expand against a
//! [`NavDataSnapshot`](aerocontext_core::NavDataSnapshot) into an ordered
//! point list.
//!
//! v0.1 scope: idents (airports/navaids/fixes), airways (`V`/`J`/`T`/`Q`
//! and the other published designator families), `DCT`, lat/lon waypoints
//! (`45N073W`, `4530N07315W`), and SID/STAR procedure tokens — the latter
//! are recognized and reported but contribute no geometry (no procedure
//! engine). Anything else fails loudly with a typed error: a corridor
//! must never silently omit legs.

mod expand;
mod token;

use aerocontext_core::GeoPoint;

pub use expand::{expand, expand_str};
pub use token::parse;

/// One token of a route string.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum RouteToken {
    /// A point identifier: airport, navaid, or fix.
    Ident(String),
    /// An airway designator, e.g. `"V25"`, `"J94"`, `"Q120"`.
    Airway(String),
    /// The literal `DCT` separator.
    Dct,
    /// An inline lat/lon waypoint.
    LatLon(GeoPoint),
    /// A SID/STAR procedure token, e.g. `"TRUKN2"` or `"BLUES2.IIU"`.
    Procedure {
        /// Procedure computer code.
        name: String,
        /// Transition, when dot-notation was used.
        transition: Option<String>,
    },
}

/// A route point produced by expansion.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct RoutePoint {
    /// Published identifier, `None` for inline lat/lon waypoints.
    pub ident: Option<String>,
    /// Resolved position.
    pub position: GeoPoint,
    /// The airway this point was emitted from, when it came from an
    /// airway traversal rather than an explicit token.
    pub via_airway: Option<String>,
}

impl RoutePoint {
    /// An anonymous point.
    pub fn new(position: GeoPoint) -> Self {
        Self {
            ident: None,
            position,
            via_airway: None,
        }
    }

    /// Set the identifier.
    #[must_use]
    pub fn with_ident(mut self, ident: Option<String>) -> Self {
        self.ident = ident;
        self
    }

    /// Set the originating airway.
    #[must_use]
    pub fn with_via_airway(mut self, via_airway: Option<String>) -> Self {
        self.via_airway = via_airway;
        self
    }
}

/// An expanded route: geometry plus the procedures awaiting a future
/// procedure engine.
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct ExpandedRoute {
    /// Ordered route geometry.
    pub points: Vec<RoutePoint>,
    /// SID/STAR tokens recognized but not expanded (no geometry).
    pub procedures: Vec<RouteToken>,
}

/// What an unsupported token is.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnsupportedKind {
    /// Fix-radial-distance waypoint, e.g. `OAK090025`.
    FixRadialDistance,
    /// Inline speed/altitude change, e.g. `LEXOR/N0467F380`.
    SpeedAltitudeChange,
    /// Any other slash-bearing token (delays, legacy forms).
    SlashToken,
}

/// Failure parsing or expanding a route string.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RouteError {
    /// The route string is empty.
    #[error("empty route string")]
    Empty,
    /// A token form v0.1 deliberately does not expand.
    #[error("unsupported token {token:?} ({kind:?})")]
    UnsupportedToken {
        /// What kind of unsupported form.
        kind: UnsupportedKind,
        /// The verbatim token.
        token: String,
    },
    /// A route may not start or end with an airway designator.
    #[error("airway {airway} at the route edge needs an entry/exit fix")]
    AirwayAtRouteEdge {
        /// The offending designator.
        airway: String,
    },
    /// Two airway designators need a shared transition fix between them.
    #[error("airways {first} and {second} are adjacent; name the transition fix between them")]
    AdjacentAirways {
        /// First designator.
        first: String,
        /// Second designator.
        second: String,
    },
    /// An identifier is not in the snapshot.
    #[error("{ident} is not in the navigation snapshot (cycle effective {cycle})")]
    UnresolvedIdent {
        /// The identifier.
        ident: String,
        /// The snapshot's effective date, for the operator.
        cycle: chrono::NaiveDate,
    },
    /// No airway with this designator exists in the snapshot.
    #[error("unknown airway {airway}")]
    UnknownAirway {
        /// The designator.
        airway: String,
    },
    /// The entry fix is not a published point of the airway.
    #[error("{fix} is not on airway {airway} (entry)")]
    EntryFixNotOnAirway {
        /// The fix named before the airway.
        fix: String,
        /// The designator.
        airway: String,
    },
    /// The exit fix is not a published point of the airway.
    #[error("{fix} is not on airway {airway} (exit)")]
    ExitFixNotOnAirway {
        /// The fix named after the airway.
        fix: String,
        /// The designator.
        airway: String,
    },
    /// Both fixes are on more than one airway instance (locations).
    #[error("airway {airway} is ambiguous between locations for {entry}..{exit}")]
    AmbiguousAirway {
        /// The designator.
        airway: String,
        /// Entry fix.
        entry: String,
        /// Exit fix.
        exit: String,
    },
    /// The traversal crosses a published gap.
    #[error("airway {airway} is discontinued between {from} and {to}")]
    DiscontinuedSegment {
        /// The designator.
        airway: String,
        /// Last point before the gap.
        from: String,
        /// First point after the gap.
        to: String,
    },
}

#[cfg(test)]
mod tests;