Skip to main content

aerocontext_planning/
route.rs

1//! Flight-plan route strings: tokenize, then expand against a
2//! [`NavDataSnapshot`](aerocontext_core::NavDataSnapshot) into an ordered
3//! point list.
4//!
5//! v0.1 scope: idents (airports/navaids/fixes), airways (`V`/`J`/`T`/`Q`
6//! and the other published designator families), `DCT`, lat/lon waypoints
7//! (`45N073W`, `4530N07315W`), and SID/STAR procedure tokens — the latter
8//! are recognized and reported but contribute no geometry (no procedure
9//! engine). Anything else fails loudly with a typed error: a corridor
10//! must never silently omit legs.
11
12mod expand;
13mod token;
14
15use aerocontext_core::GeoPoint;
16
17pub use expand::{expand, expand_str};
18pub use token::parse;
19
20/// One token of a route string.
21#[derive(Debug, Clone, PartialEq)]
22#[non_exhaustive]
23pub enum RouteToken {
24    /// A point identifier: airport, navaid, or fix.
25    Ident(String),
26    /// An airway designator, e.g. `"V25"`, `"J94"`, `"Q120"`.
27    Airway(String),
28    /// The literal `DCT` separator.
29    Dct,
30    /// An inline lat/lon waypoint.
31    LatLon(GeoPoint),
32    /// A SID/STAR procedure token, e.g. `"TRUKN2"` or `"BLUES2.IIU"`.
33    Procedure {
34        /// Procedure computer code.
35        name: String,
36        /// Transition, when dot-notation was used.
37        transition: Option<String>,
38    },
39}
40
41/// A route point produced by expansion.
42#[derive(Debug, Clone, PartialEq)]
43#[non_exhaustive]
44pub struct RoutePoint {
45    /// Published identifier, `None` for inline lat/lon waypoints.
46    pub ident: Option<String>,
47    /// Resolved position.
48    pub position: GeoPoint,
49    /// The airway this point was emitted from, when it came from an
50    /// airway traversal rather than an explicit token.
51    pub via_airway: Option<String>,
52}
53
54impl RoutePoint {
55    /// An anonymous point.
56    pub fn new(position: GeoPoint) -> Self {
57        Self {
58            ident: None,
59            position,
60            via_airway: None,
61        }
62    }
63
64    /// Set the identifier.
65    #[must_use]
66    pub fn with_ident(mut self, ident: Option<String>) -> Self {
67        self.ident = ident;
68        self
69    }
70
71    /// Set the originating airway.
72    #[must_use]
73    pub fn with_via_airway(mut self, via_airway: Option<String>) -> Self {
74        self.via_airway = via_airway;
75        self
76    }
77}
78
79/// An expanded route: geometry plus the procedures awaiting a future
80/// procedure engine.
81#[derive(Debug, Clone, PartialEq, Default)]
82#[non_exhaustive]
83pub struct ExpandedRoute {
84    /// Ordered route geometry.
85    pub points: Vec<RoutePoint>,
86    /// SID/STAR tokens recognized but not expanded (no geometry).
87    pub procedures: Vec<RouteToken>,
88}
89
90/// What an unsupported token is.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92#[non_exhaustive]
93pub enum UnsupportedKind {
94    /// Fix-radial-distance waypoint, e.g. `OAK090025`.
95    FixRadialDistance,
96    /// Inline speed/altitude change, e.g. `LEXOR/N0467F380`.
97    SpeedAltitudeChange,
98    /// Any other slash-bearing token (delays, legacy forms).
99    SlashToken,
100}
101
102/// Failure parsing or expanding a route string.
103#[derive(Debug, thiserror::Error)]
104#[non_exhaustive]
105pub enum RouteError {
106    /// The route string is empty.
107    #[error("empty route string")]
108    Empty,
109    /// A token form v0.1 deliberately does not expand.
110    #[error("unsupported token {token:?} ({kind:?})")]
111    UnsupportedToken {
112        /// What kind of unsupported form.
113        kind: UnsupportedKind,
114        /// The verbatim token.
115        token: String,
116    },
117    /// A route may not start or end with an airway designator.
118    #[error("airway {airway} at the route edge needs an entry/exit fix")]
119    AirwayAtRouteEdge {
120        /// The offending designator.
121        airway: String,
122    },
123    /// Two airway designators need a shared transition fix between them.
124    #[error("airways {first} and {second} are adjacent; name the transition fix between them")]
125    AdjacentAirways {
126        /// First designator.
127        first: String,
128        /// Second designator.
129        second: String,
130    },
131    /// An identifier is not in the snapshot.
132    #[error("{ident} is not in the navigation snapshot (cycle effective {cycle})")]
133    UnresolvedIdent {
134        /// The identifier.
135        ident: String,
136        /// The snapshot's effective date, for the operator.
137        cycle: chrono::NaiveDate,
138    },
139    /// No airway with this designator exists in the snapshot.
140    #[error("unknown airway {airway}")]
141    UnknownAirway {
142        /// The designator.
143        airway: String,
144    },
145    /// The entry fix is not a published point of the airway.
146    #[error("{fix} is not on airway {airway} (entry)")]
147    EntryFixNotOnAirway {
148        /// The fix named before the airway.
149        fix: String,
150        /// The designator.
151        airway: String,
152    },
153    /// The exit fix is not a published point of the airway.
154    #[error("{fix} is not on airway {airway} (exit)")]
155    ExitFixNotOnAirway {
156        /// The fix named after the airway.
157        fix: String,
158        /// The designator.
159        airway: String,
160    },
161    /// Both fixes are on more than one airway instance (locations).
162    #[error("airway {airway} is ambiguous between locations for {entry}..{exit}")]
163    AmbiguousAirway {
164        /// The designator.
165        airway: String,
166        /// Entry fix.
167        entry: String,
168        /// Exit fix.
169        exit: String,
170    },
171    /// The traversal crosses a published gap.
172    #[error("airway {airway} is discontinued between {from} and {to}")]
173    DiscontinuedSegment {
174        /// The designator.
175        airway: String,
176        /// Last point before the gap.
177        from: String,
178        /// First point after the gap.
179        to: String,
180    },
181}
182
183#[cfg(test)]
184mod tests;