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;