1use std::hash::Hash;
6use std::{collections::HashMap, path};
7
8use geodesy::prelude::*;
9use once_cell::sync::Lazy;
10use serde::Serialize;
11
12use crate::data::field15::{Connector, Field15Element, Modifier, Point};
13use crate::data::{
14 eurocontrol::aixm::{
15 airport_heliport::{parse_airport_heliport_zip_file, AirportHeliport},
16 arrival_leg::{parse_arrival_leg_zip_file, ArrivalLeg},
17 departure_leg::{parse_departure_leg_zip_file, DepartureLeg},
18 designated_point::{parse_designated_point_zip_file, DesignatedPoint},
19 navaid::{parse_navaid_zip_file, Navaid},
20 route::{parse_route_zip_file, Route},
21 route_segment::{parse_route_segment_zip_file, PointReference, RouteSegment},
22 standard_instrument_arrival::{parse_standard_instrument_arrival_zip_file, StandardInstrumentArrival},
23 standard_instrument_departure::{parse_standard_instrument_departure_zip_file, StandardInstrumentDeparture},
24 },
25 field15::{Altitude, Speed},
26};
27use crate::error::ThrustError;
28
29#[derive(Debug)]
48pub struct AirwayDatabase {
49 airports: HashMap<String, AirportHeliport>,
50 navaids: HashMap<String, Navaid>,
51 designated_points: HashMap<String, DesignatedPoint>,
52 route_segments: HashMap<String, RouteSegment>,
53 routes: HashMap<String, Route>,
54 arrival_legs: HashMap<String, ArrivalLeg>,
55 departure_legs: HashMap<String, DepartureLeg>,
56 standard_instrument_arrivals: HashMap<String, StandardInstrumentArrival>,
57 standard_instrument_departures: HashMap<String, StandardInstrumentDeparture>,
58}
59
60impl AirwayDatabase {
61 pub fn new(path: &path::Path) -> Result<Self, ThrustError> {
63 Ok(AirwayDatabase {
64 airports: parse_airport_heliport_zip_file(path.join("AirportHeliport.BASELINE.zip"))?,
65 navaids: parse_navaid_zip_file(path.join("Navaid.BASELINE.zip"))?,
66 designated_points: parse_designated_point_zip_file(path.join("DesignatedPoint.BASELINE.zip"))?,
67 route_segments: parse_route_segment_zip_file(path.join("RouteSegment.BASELINE.zip"))?,
68 routes: parse_route_zip_file(path.join("Route.BASELINE.zip"))?,
69 arrival_legs: if path.join("ArrivalLeg.BASELINE.zip").exists() {
70 parse_arrival_leg_zip_file(path.join("ArrivalLeg.BASELINE.zip"))?
71 } else {
72 HashMap::new()
73 },
74 departure_legs: if path.join("DepartureLeg.BASELINE.zip").exists() {
75 parse_departure_leg_zip_file(path.join("DepartureLeg.BASELINE.zip"))?
76 } else {
77 HashMap::new()
78 },
79 standard_instrument_arrivals: if path.join("StandardInstrumentArrival.BASELINE.zip").exists() {
80 parse_standard_instrument_arrival_zip_file(path.join("StandardInstrumentArrival.BASELINE.zip"))?
81 } else {
82 HashMap::new()
83 },
84 standard_instrument_departures: if path.join("StandardInstrumentDeparture.BASELINE.zip").exists() {
85 parse_standard_instrument_departure_zip_file(path.join("StandardInstrumentDeparture.BASELINE.zip"))?
86 } else {
87 HashMap::new()
88 },
89 })
90 }
91
92 pub fn resolve_sid_points(&self, name: &str) -> Vec<ResolvedPoint> {
94 let sid_ids = self
95 .standard_instrument_departures
96 .values()
97 .filter(|sid| sid.designator.trim().eq_ignore_ascii_case(name))
98 .map(|sid| sid.identifier.clone())
99 .collect::<std::collections::HashSet<_>>();
100
101 let fallback_points = self
102 .standard_instrument_departures
103 .values()
104 .filter(|sid| sid.designator.trim().eq_ignore_ascii_case(name))
105 .flat_map(|sid| sid.connecting_points.iter().cloned())
106 .collect::<Vec<_>>();
107
108 let exit_points = self.procedure_exit_points(&sid_ids, true);
109 let points_to_resolve = if exit_points.is_empty() {
110 fallback_points
111 } else {
112 exit_points
113 };
114
115 let mut points = points_to_resolve
116 .iter()
117 .map(|p| ResolvedPoint::from_db(p, self))
118 .filter(|p| !matches!(p, ResolvedPoint::None))
119 .collect::<Vec<_>>();
120 points.sort_by_key(|p| format!("{p:?}"));
121 points.dedup();
122 points
123 }
124
125 pub fn resolve_star_points(&self, name: &str) -> Vec<ResolvedPoint> {
127 let star_ids = self
128 .standard_instrument_arrivals
129 .values()
130 .filter(|star| star.designator.trim().eq_ignore_ascii_case(name))
131 .map(|star| star.identifier.clone())
132 .collect::<std::collections::HashSet<_>>();
133
134 let fallback_points = self
135 .standard_instrument_arrivals
136 .values()
137 .filter(|star| star.designator.trim().eq_ignore_ascii_case(name))
138 .flat_map(|star| star.connecting_points.iter().cloned())
139 .collect::<Vec<_>>();
140
141 let entry_points = self.procedure_entry_points(&star_ids, false);
142 let points_to_resolve = if entry_points.is_empty() {
143 fallback_points
144 } else {
145 entry_points
146 };
147
148 let mut points = points_to_resolve
149 .iter()
150 .map(|p| ResolvedPoint::from_db(p, self))
151 .filter(|p| !matches!(p, ResolvedPoint::None))
152 .collect::<Vec<_>>();
153 points.sort_by_key(|p| format!("{p:?}"));
154 points.dedup();
155 points
156 }
157
158 pub fn resolve_sid_routes(&self, name: &str) -> Vec<ResolvedRoute> {
160 self.standard_instrument_departures
161 .values()
162 .filter(|sid| sid.designator.trim().eq_ignore_ascii_case(name))
163 .map(|sid| {
164 let segments = self
165 .departure_legs
166 .values()
167 .filter(|leg| leg.departure.as_ref().is_some_and(|id| id == &sid.identifier))
168 .filter_map(|leg| {
169 let start = ResolvedPoint::from_db(&leg.start, self);
170 let end = ResolvedPoint::from_db(&leg.end, self);
171 if matches!(start, ResolvedPoint::None) || matches!(end, ResolvedPoint::None) {
172 None
173 } else {
174 Some(ResolvedRouteSegment {
175 start,
176 end,
177 name: Some(sid.designator.clone()),
178 altitude: None,
179 speed: None,
180 })
181 }
182 })
183 .collect::<Vec<_>>();
184 ResolvedRoute {
185 segments: order_route_segments(segments),
186 name: sid.designator.clone(),
187 }
188 })
189 .collect()
190 }
191
192 pub fn resolve_star_routes(&self, name: &str) -> Vec<ResolvedRoute> {
194 self.standard_instrument_arrivals
195 .values()
196 .filter(|star| star.designator.trim().eq_ignore_ascii_case(name))
197 .map(|star| {
198 let segments = self
199 .arrival_legs
200 .values()
201 .filter(|leg| leg.arrival.as_ref().is_some_and(|id| id == &star.identifier))
202 .filter_map(|leg| {
203 let start = ResolvedPoint::from_db(&leg.start, self);
204 let end = ResolvedPoint::from_db(&leg.end, self);
205 if matches!(start, ResolvedPoint::None) || matches!(end, ResolvedPoint::None) {
206 None
207 } else {
208 Some(ResolvedRouteSegment {
209 start,
210 end,
211 name: Some(star.designator.clone()),
212 altitude: None,
213 speed: None,
214 })
215 }
216 })
217 .collect::<Vec<_>>();
218 ResolvedRoute {
219 segments: order_route_segments(segments),
220 name: star.designator.clone(),
221 }
222 })
223 .collect()
224 }
225
226 fn procedure_exit_points(
227 &self,
228 procedure_ids: &std::collections::HashSet<String>,
229 is_departure: bool,
230 ) -> Vec<PointReference> {
231 let legs: Vec<(PointReference, PointReference)> = if is_departure {
232 self.departure_legs
233 .values()
234 .filter(|leg| leg.departure.as_ref().is_some_and(|id| procedure_ids.contains(id)))
235 .map(|leg| (leg.start.clone(), leg.end.clone()))
236 .collect()
237 } else {
238 self.arrival_legs
239 .values()
240 .filter(|leg| leg.arrival.as_ref().is_some_and(|id| procedure_ids.contains(id)))
241 .map(|leg| (leg.start.clone(), leg.end.clone()))
242 .collect()
243 };
244
245 let mut indegree: HashMap<String, usize> = HashMap::new();
246 let mut outdegree: HashMap<String, usize> = HashMap::new();
247 let mut refs: HashMap<String, PointReference> = HashMap::new();
248
249 for (start, end) in &legs {
250 let s = start.name();
251 let e = end.name();
252
253 if !s.is_empty() {
254 refs.insert(s.clone(), start.clone());
255 outdegree.entry(s).and_modify(|v| *v += 1).or_insert(1);
256 }
257 if !e.is_empty() {
258 refs.insert(e.clone(), end.clone());
259 indegree.entry(e).and_modify(|v| *v += 1).or_insert(1);
260 }
261 }
262
263 refs.into_iter()
264 .filter_map(|(name, point_ref)| {
265 if point_ref.is_airport_heliport() {
266 return None;
267 }
268 let in_d = *indegree.get(&name).unwrap_or(&0);
269 let out_d = *outdegree.get(&name).unwrap_or(&0);
270 if out_d == 0 && in_d > 0 {
271 Some(point_ref)
272 } else {
273 None
274 }
275 })
276 .collect()
277 }
278
279 fn procedure_entry_points(
280 &self,
281 procedure_ids: &std::collections::HashSet<String>,
282 is_departure: bool,
283 ) -> Vec<PointReference> {
284 let legs: Vec<(PointReference, PointReference)> = if is_departure {
285 self.departure_legs
286 .values()
287 .filter(|leg| leg.departure.as_ref().is_some_and(|id| procedure_ids.contains(id)))
288 .map(|leg| (leg.start.clone(), leg.end.clone()))
289 .collect()
290 } else {
291 self.arrival_legs
292 .values()
293 .filter(|leg| leg.arrival.as_ref().is_some_and(|id| procedure_ids.contains(id)))
294 .map(|leg| (leg.start.clone(), leg.end.clone()))
295 .collect()
296 };
297
298 let mut indegree: HashMap<String, usize> = HashMap::new();
299 let mut outdegree: HashMap<String, usize> = HashMap::new();
300 let mut refs: HashMap<String, PointReference> = HashMap::new();
301
302 for (start, end) in &legs {
303 let s = start.name();
304 let e = end.name();
305
306 if !s.is_empty() {
307 refs.insert(s.clone(), start.clone());
308 outdegree.entry(s).and_modify(|v| *v += 1).or_insert(1);
309 }
310 if !e.is_empty() {
311 refs.insert(e.clone(), end.clone());
312 indegree.entry(e).and_modify(|v| *v += 1).or_insert(1);
313 }
314 }
315
316 refs.into_iter()
317 .filter_map(|(name, point_ref)| {
318 if point_ref.is_airport_heliport() {
319 return None;
320 }
321 let in_d = *indegree.get(&name).unwrap_or(&0);
322 let out_d = *outdegree.get(&name).unwrap_or(&0);
323 if in_d == 0 && out_d > 0 {
324 Some(point_ref)
325 } else {
326 None
327 }
328 })
329 .collect()
330 }
331}
332
333fn order_route_segments(segments: Vec<ResolvedRouteSegment>) -> Vec<ResolvedRouteSegment> {
334 let mut out_map: HashMap<ResolvedPoint, Vec<ResolvedRouteSegment>> = HashMap::new();
335 let mut indegree: HashMap<ResolvedPoint, usize> = HashMap::new();
336
337 for segment in segments {
338 out_map.entry(segment.start.clone()).or_default().push(segment.clone());
339 indegree.entry(segment.start.clone()).or_insert(0);
340 indegree.entry(segment.end.clone()).and_modify(|v| *v += 1).or_insert(1);
341 }
342
343 for edges in out_map.values_mut() {
344 edges.sort_by_key(|e| format!("{}", e.end));
345 }
346
347 let mut starts = indegree
348 .iter()
349 .filter_map(|(point, deg)| if *deg == 0 { Some(point.clone()) } else { None })
350 .collect::<Vec<_>>();
351 starts.sort_by_key(|p| format!("{p}"));
352
353 let mut ordered = Vec::new();
354
355 let walk_from = |start: ResolvedPoint,
356 out_map: &mut HashMap<ResolvedPoint, Vec<ResolvedRouteSegment>>,
357 ordered: &mut Vec<ResolvedRouteSegment>| {
358 let mut current = start;
359 loop {
360 let next = out_map
361 .get_mut(¤t)
362 .and_then(|edges| if edges.is_empty() { None } else { Some(edges.remove(0)) });
363 if let Some(segment) = next {
364 current = segment.end.clone();
365 ordered.push(segment);
366 } else {
367 break;
368 }
369 }
370 };
371
372 for start in starts {
373 walk_from(start, &mut out_map, &mut ordered);
374 }
375
376 loop {
377 let mut keys = out_map
378 .iter()
379 .filter_map(|(k, v)| if v.is_empty() { None } else { Some(k.clone()) })
380 .collect::<Vec<_>>();
381 if keys.is_empty() {
382 break;
383 }
384 keys.sort_by_key(|p| format!("{p}"));
385 walk_from(keys.remove(0), &mut out_map, &mut ordered);
386 }
387
388 ordered
389}
390
391const VALID_ROUTE_PREFIXES: [&str; 32] = [
392 "UN", "UM", "UL", "UT", "UZ", "UY", "UP", "UA", "UB", "UG", "UH", "UJ", "UQ", "UR", "UV", "UW", "L", "A", "B", "G",
393 "H", "J", "Q", "R", "T", "V", "W", "Y", "Z", "M", "N", "P",
394];
395
396static WGS84: Lazy<Ellipsoid> = Lazy::new(|| Ellipsoid::named("WGS84").unwrap());
398
399#[derive(Debug, Clone, Serialize)]
409pub struct ResolvedRoute {
410 pub segments: Vec<ResolvedRouteSegment>,
411 pub name: String,
412}
413
414#[derive(Debug, Clone, Serialize)]
426pub struct ResolvedRouteSegment {
427 pub start: ResolvedPoint,
428 pub end: ResolvedPoint,
429 #[serde(skip_serializing_if = "Option::is_none")]
430 pub name: Option<String>,
431 #[serde(skip_serializing_if = "Option::is_none")]
432 pub altitude: Option<Altitude>,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 pub speed: Option<Speed>,
435}
436
437#[derive(Debug, Clone, Serialize)]
450#[serde(untagged)]
451pub enum ResolvedPoint {
452 AirportHeliport(AirportHeliport),
453 Navaid(Navaid),
454 DesignatedPoint(DesignatedPoint),
455 Coordinates { latitude: f64, longitude: f64 },
456 None,
457}
458
459impl std::fmt::Display for ResolvedPoint {
460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461 match self {
462 ResolvedPoint::AirportHeliport(airport) => {
463 write!(
464 f,
465 "AirportHeliport({}: {:.3},{:.3})",
466 airport.icao, airport.latitude, airport.longitude
467 )
468 }
469 ResolvedPoint::Navaid(navaid) => write!(
470 f,
471 "Navaid({}: {:.3},{:.3})",
472 navaid.name.as_ref().unwrap(),
473 navaid.latitude,
474 navaid.longitude
475 ),
476 ResolvedPoint::DesignatedPoint(dp) => write!(
477 f,
478 "DesignatedPoint({}: {:.3}, {:.3})",
479 dp.designator, dp.latitude, dp.longitude
480 ),
481 ResolvedPoint::Coordinates { latitude, longitude } => {
482 write!(f, "Coordinates(lat: {}, lon: {})", latitude, longitude)
483 }
484 ResolvedPoint::None => write!(f, "None"),
485 }
486 }
487}
488
489impl From<&ResolvedPoint> for Coor2D {
490 fn from(val: &ResolvedPoint) -> Self {
491 match val {
492 ResolvedPoint::AirportHeliport(airport) => Coor2D::geo(airport.latitude, airport.longitude),
493 ResolvedPoint::Navaid(navaid) => Coor2D::geo(navaid.latitude, navaid.longitude),
494 ResolvedPoint::DesignatedPoint(dp) => Coor2D::geo(dp.latitude, dp.longitude),
495 ResolvedPoint::Coordinates { latitude, longitude } => Coor2D::geo(*latitude, *longitude),
496 ResolvedPoint::None => Coor2D::default(),
497 }
498 }
499}
500
501impl PartialEq for ResolvedPoint {
502 fn eq(&self, other: &Self) -> bool {
503 match (self, other) {
504 (ResolvedPoint::AirportHeliport(a), ResolvedPoint::AirportHeliport(b)) => a.identifier == b.identifier,
505 (ResolvedPoint::Navaid(a), ResolvedPoint::Navaid(b)) => a.identifier == b.identifier,
506 (ResolvedPoint::DesignatedPoint(a), ResolvedPoint::DesignatedPoint(b)) => a.identifier == b.identifier,
507 (
508 ResolvedPoint::Coordinates {
509 latitude: a_lat,
510 longitude: a_lon,
511 },
512 ResolvedPoint::Coordinates {
513 latitude: b_lat,
514 longitude: b_lon,
515 },
516 ) => (a_lat - b_lat).abs() < f64::EPSILON && (a_lon - b_lon).abs() < f64::EPSILON,
517 (ResolvedPoint::None, ResolvedPoint::None) => true,
518 _ => false,
519 }
520 }
521}
522
523impl Eq for ResolvedPoint {}
524
525impl Hash for ResolvedPoint {
527 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
528 match self {
529 ResolvedPoint::AirportHeliport(airport) => {
530 airport.identifier.hash(state);
531 }
532 ResolvedPoint::Navaid(navaid) => {
533 navaid.identifier.hash(state);
534 }
535 ResolvedPoint::DesignatedPoint(dp) => {
536 dp.identifier.hash(state);
537 }
538 ResolvedPoint::Coordinates { latitude, longitude } => {
539 latitude.to_bits().hash(state);
540 longitude.to_bits().hash(state);
541 }
542 ResolvedPoint::None => {
543 0.hash(state);
544 }
545 }
546 }
547}
548
549impl ResolvedPoint {
550 pub fn from_db(point: &PointReference, db: &AirwayDatabase) -> Self {
552 match point {
553 PointReference::AirportHeliport(id) => {
554 if let Some(airport) = db.airports.get(id) {
555 ResolvedPoint::AirportHeliport(airport.clone())
556 } else {
557 ResolvedPoint::None
558 }
559 }
560 PointReference::Navaid(id) => {
561 if let Some(navaid) = db.navaids.get(id) {
562 ResolvedPoint::Navaid(navaid.clone())
563 } else {
564 ResolvedPoint::None
565 }
566 }
567 PointReference::DesignatedPoint(id) => {
568 if let Some(dp) = db.designated_points.get(id) {
569 ResolvedPoint::DesignatedPoint(dp.clone())
570 } else {
571 ResolvedPoint::None
572 }
573 }
574 PointReference::None => ResolvedPoint::None,
575 }
576 }
577 pub fn lookup(name: &str, db: &AirwayDatabase) -> Vec<Self> {
579 let candidates = db
580 .navaids
581 .values()
582 .filter(|n| {
583 n.name
584 .as_deref()
585 .is_some_and(|n_name| n_name.trim().eq_ignore_ascii_case(name))
586 })
587 .collect::<Vec<_>>();
588 if !candidates.is_empty() {
589 return candidates.iter().map(|n| ResolvedPoint::Navaid((*n).clone())).collect();
590 }
591 let candidates = db
592 .designated_points
593 .values()
594 .filter(|dp| dp.designator.trim().eq_ignore_ascii_case(name))
595 .collect::<Vec<_>>();
596 if !candidates.is_empty() {
597 return candidates
598 .iter()
599 .map(|dp| ResolvedPoint::DesignatedPoint((*dp).clone()))
600 .collect();
601 }
602 vec![]
603 }
604}
605
606impl ResolvedRouteSegment {
607 pub fn from_db(segment: &RouteSegment, db: &AirwayDatabase) -> Self {
609 ResolvedRouteSegment {
610 start: ResolvedPoint::from_db(&segment.start, db),
611 end: ResolvedPoint::from_db(&segment.end, db),
612 name: None,
613 altitude: None,
614 speed: None,
615 }
616 }
617}
618
619impl ResolvedRoute {
620 pub fn from_db(route: &Route, db: &AirwayDatabase) -> Self {
622 let segments = db
623 .route_segments
624 .values()
625 .filter(|segment| segment.route_formed.as_deref() == Some(&route.identifier))
626 .map(|segment| ResolvedRouteSegment::from_db(segment, db))
627 .collect::<Vec<_>>();
628 ResolvedRoute {
629 segments,
630 name: format!(
631 "{}{}{}",
632 route.prefix.as_deref().unwrap_or(""),
633 route.second_letter.as_deref().unwrap_or(""),
634 route.number.as_deref().unwrap_or("")
635 ),
636 }
637 }
638
639 pub fn lookup(name: &str, db: &AirwayDatabase) -> Vec<Self> {
641 if VALID_ROUTE_PREFIXES.iter().any(|prefix| name.starts_with(prefix)) {
642 let last = name.chars().last().unwrap();
646 let (name, multiple) = if last.is_alphabetic() {
647 (&name[..name.len() - 1], Some(last.to_string()))
648 } else {
649 (name, None)
650 };
651 let (prefix, second_letter, number) = if name.starts_with('U') && name.len() >= 3 {
652 (
653 Some("U".to_string()),
654 name.chars().nth(1).unwrap().to_string(),
655 name[2..].to_string(),
656 )
657 } else if name.len() >= 2 {
658 (None, name.chars().next().unwrap().to_string(), name[1..].to_string())
659 } else {
660 (None, "".to_string(), "".to_string())
661 };
662 let candidates = db
663 .routes
664 .values()
665 .filter(|route| {
666 route.prefix.as_deref() == prefix.as_deref()
667 && route.second_letter.as_deref() == Some(&second_letter)
668 && route.number.as_deref() == Some(&number)
669 && route.multiple_identifier.as_deref() == multiple.as_deref()
670 })
671 .collect::<Vec<_>>();
672 return candidates
673 .iter()
674 .map(|route| ResolvedRoute::from_db(route, db))
675 .collect();
676 }
677 vec![]
678 }
679
680 pub fn contains(&self, point: &ResolvedPoint) -> bool {
682 self.segments
683 .iter()
684 .any(|segment| &segment.start == point || &segment.end == point)
685 }
686
687 pub fn between(&self, start: &ResolvedPoint, end: &ResolvedPoint) -> Option<ResolvedRoute> {
691 let mut graph: HashMap<&ResolvedPoint, Vec<(&ResolvedPoint, usize, bool)>> = HashMap::new();
693
694 for (i, segment) in self.segments.iter().enumerate() {
695 graph.entry(&segment.start).or_default().push((&segment.end, i, true));
697
698 graph.entry(&segment.end).or_default().push((&segment.start, i, false));
700 }
701
702 if let Some(path) = Self::dfs_helper(
704 &graph,
705 start,
706 end,
707 &mut Vec::new(),
708 &mut std::collections::HashSet::new(),
709 ) {
710 return Some(self.build_route_from_path(path));
711 }
712
713 None
714 }
715
716 fn dfs_helper<'a>(
717 graph: &HashMap<&'a ResolvedPoint, Vec<(&'a ResolvedPoint, usize, bool)>>,
718 current: &'a ResolvedPoint,
719 target: &'a ResolvedPoint,
720 path: &mut Vec<(usize, bool)>,
721 visited: &mut std::collections::HashSet<usize>,
722 ) -> Option<Vec<(usize, bool)>> {
723 if current == target {
724 return Some(path.clone());
725 }
726
727 if let Some(neighbors) = graph.get(current) {
728 for (next_point, segment_idx, is_forward) in neighbors {
729 if !visited.contains(segment_idx) {
730 visited.insert(*segment_idx);
731 path.push((*segment_idx, *is_forward));
732
733 if let Some(result) = Self::dfs_helper(graph, next_point, target, path, visited) {
734 return Some(result);
735 }
736
737 path.pop();
738 visited.remove(segment_idx);
739 }
740 }
741 }
742
743 None
744 }
745
746 fn build_route_from_path(&self, path: Vec<(usize, bool)>) -> ResolvedRoute {
747 let mut segments = Vec::new();
748
749 for (segment_idx, is_forward) in path {
750 let segment = &self.segments[segment_idx];
751 if is_forward {
752 segments.push(segment.clone());
753 } else {
754 segments.push(ResolvedRouteSegment {
756 start: segment.end.clone(),
757 end: segment.start.clone(),
758 name: Some(self.name.clone()),
759 altitude: segment.altitude.clone(),
760 speed: segment.speed.clone(),
761 });
762 }
763 }
764
765 ResolvedRoute {
766 segments,
767 name: self.name.clone(),
768 }
769 }
770}
771
772#[derive(Debug)]
773enum EnrichedCandidates {
774 Point((Vec<ResolvedPoint>, Option<Altitude>, Option<Speed>)),
775 PointCoords((ResolvedPoint, Option<Altitude>, Option<Speed>)),
776 Airway((Vec<ResolvedRoute>, String, Option<Altitude>, Option<Speed>)),
777 Direct(),
778}
779
780impl AirwayDatabase {
781 pub fn enrich_route(&self, elements: Vec<Field15Element>) -> Vec<ResolvedRouteSegment> {
786 let mut altitude = None;
787 let mut speed = None;
788
789 let mut resolved: Vec<EnrichedCandidates> = Vec::new();
791 for element in &elements {
792 match element {
793 Field15Element::Modifier(m) => {
794 let Modifier {
795 speed: s, altitude: a, ..
796 } = m;
797 altitude = a.clone();
798 speed = s.clone();
799 }
800 Field15Element::Point(Point::Waypoint(name)) => {
801 let lookup = ResolvedPoint::lookup(name, self);
802 if lookup.is_empty() {
803 tracing::warn!("No point found for identifier '{}'", name);
804 }
805 resolved.push(EnrichedCandidates::Point((
806 ResolvedPoint::lookup(name, self),
807 altitude.clone(),
808 speed.clone(),
809 )));
810 }
811 Field15Element::Point(Point::Coordinates((lat, lon))) => {
812 resolved.push(EnrichedCandidates::PointCoords((
813 ResolvedPoint::Coordinates {
814 latitude: *lat,
815 longitude: *lon,
816 },
817 altitude.clone(),
818 speed.clone(),
819 )));
820 }
821 Field15Element::Connector(Connector::Airway(name)) => {
822 let lookup = ResolvedRoute::lookup(name, self);
823 if lookup.is_empty() {
824 tracing::warn!("No airway found for identifier '{}'", name);
825 resolved.push(EnrichedCandidates::Direct());
826 } else {
827 resolved.push(EnrichedCandidates::Airway((
828 lookup,
829 name.to_string(),
830 altitude.clone(),
831 speed.clone(),
832 )));
833 }
834 }
835 Field15Element::Connector(Connector::Sid(name)) => {
836 let lookup = self.resolve_sid_routes(name);
837 if lookup.is_empty() {
838 tracing::warn!("No SID found for identifier '{}'", name);
839 resolved.push(EnrichedCandidates::Direct());
840 } else {
841 resolved.push(EnrichedCandidates::Airway((
842 lookup,
843 name.to_string(),
844 altitude.clone(),
845 speed.clone(),
846 )));
847 }
848 }
849 Field15Element::Connector(Connector::Star(name)) => {
850 let lookup = self.resolve_star_routes(name);
851 if lookup.is_empty() {
852 tracing::warn!("No STAR found for identifier '{}'", name);
853 resolved.push(EnrichedCandidates::Direct());
854 } else {
855 resolved.push(EnrichedCandidates::Airway((
856 lookup,
857 name.to_string(),
858 altitude.clone(),
859 speed.clone(),
860 )));
861 }
862 }
863 Field15Element::Connector(Connector::Direct) => {
864 resolved.push(EnrichedCandidates::Direct());
865 }
866 Field15Element::Connector(Connector::Nat(_)) | Field15Element::Connector(Connector::Pts(_)) => {
867 resolved.push(EnrichedCandidates::Direct());
869 }
870 _ => {}
871 }
872 }
873
874 for i in 1..resolved.len() - 1 {
876 let (before_i, i_and_after) = &mut resolved.split_at_mut(i);
877 if let (EnrichedCandidates::Airway((routes, _, _, _)), after_i) = i_and_after.split_first_mut().unwrap() {
878 tracing::debug!("Filtering airway candidates: {:?}", routes);
879 if let Some(EnrichedCandidates::Point((points, _, _))) = before_i.last() {
880 routes.retain(|r| points.iter().any(|p| r.contains(p)));
881 tracing::debug!("Filtering airway candidates with point {:?}: {:?}", points, routes);
882 }
883 if let Some(EnrichedCandidates::Point((points, _, _))) = after_i.first() {
884 routes.retain(|r| points.iter().any(|p| r.contains(p)));
885 tracing::debug!("Filtering airway candidates with point {:?}: {:?}", points, routes);
886 }
887 }
888 }
889
890 for candidate in resolved.iter_mut() {
892 if let EnrichedCandidates::Airway((routes, name, _, _)) = candidate {
893 if routes.is_empty() {
894 tracing::warn!("No valid airway remaining for '{}'", name);
895 *candidate = EnrichedCandidates::Direct();
896 }
897 }
898 }
899
900 for i in 0..resolved.len() {
902 let (before_i, i_and_after) = &mut resolved.split_at_mut(i);
903 if let (EnrichedCandidates::Point((points, _, _)), after_i) = i_and_after.split_first_mut().unwrap() {
904 tracing::debug!("Filtering point candidates: {:?}", points);
905 if let Some(EnrichedCandidates::Airway((routes, _, _, _))) = before_i.last() {
906 points.retain(|p| routes.iter().any(|r| r.contains(p)));
907 tracing::debug!("Filtering point candidates with airway {:?}: {:?}", routes, points);
908 }
909 if let Some(EnrichedCandidates::Airway((routes, _, _, _))) = after_i.first() {
910 points.retain(|p| routes.iter().any(|r| r.contains(p)));
911 tracing::debug!("Filtering point candidates with airway {:?}: {:?}", routes, points);
912 }
913 }
914 }
915
916 for i in 1..resolved.len() - 1 {
918 let (before_i, i_and_after) = &mut resolved.split_at_mut(i);
919 if let (EnrichedCandidates::Airway((routes, _, _, _)), after_i) = i_and_after.split_first_mut().unwrap() {
920 if let Some(EnrichedCandidates::Point((before, _, _))) = before_i.last() {
921 if let Some(EnrichedCandidates::Point((after, _, _))) = after_i.first() {
922 if let Some(before) = before.first() {
923 if let Some(after) = after.first() {
924 for route in routes.iter_mut() {
925 if let Some(trimmed) = route.between(before, after) {
926 *route = trimmed;
927 tracing::debug!(
928 "Trimmed airway '{}' between points {} and {}: {:?}",
929 route.name,
930 before,
931 after,
932 *route
933 );
934 }
935 }
936 }
937 }
938 }
939 }
940 }
941 }
942
943 for candidate in resolved.iter_mut() {
945 if let EnrichedCandidates::Airway((routes, name, _, _)) = candidate {
946 if routes.iter().all(|r| r.segments.is_empty()) {
947 tracing::warn!("No valid segments remaining for airway '{}'", name);
948 *candidate = EnrichedCandidates::Direct();
949 }
950 }
951 }
952
953 let mut last_known: Option<ResolvedPoint> = None;
955
956 for i in 0..resolved.len() {
957 if let EnrichedCandidates::Point((points, _, _)) = &resolved[i] {
958 if points.len() > 1 {
959 let mut next_definitive: Option<&ResolvedPoint> = None;
961 for candidate in resolved[i..].iter() {
962 match candidate {
963 EnrichedCandidates::Point((pts, _, _)) if pts.len() == 1 => {
964 next_definitive = pts.first();
965 break;
966 }
967 EnrichedCandidates::PointCoords((pt, _, _)) => {
968 next_definitive = Some(pt);
969 break;
970 }
971 _ => {}
972 }
973 }
974
975 match (&last_known, next_definitive) {
976 (None, None) => {
977 tracing::warn!("Cannot disambiguate point {:?}: no reference points available", points);
978 }
979 (None, Some(_)) => {
980 tracing::info!("Disambiguating point {:?} using only next definitive point", points);
981 }
982 (Some(a), None) => {
983 tracing::info!("Disambiguating point {:?} using only last known point", points);
984
985 let mut best_idx = 0;
987 let mut best_distance = f64::INFINITY;
988
989 for (idx, candidate) in points.iter().enumerate() {
990 let distance =
991 WGS84.distance(&Into::<Coor2D>::into(a), &Into::<Coor2D>::into(candidate));
992 if distance < best_distance {
993 best_distance = distance;
994 best_idx = idx;
995 }
996 }
997
998 if let EnrichedCandidates::Point((points, _, _)) = &mut resolved[i] {
1000 let best = points[best_idx].clone();
1001 points.clear();
1002 points.push(best);
1003 }
1004 }
1005 (Some(a), Some(b)) => {
1006 tracing::info!("Disambiguating point {:?} using both reference points", points);
1007
1008 let mut best_idx = 0;
1009 let mut best_score = f64::INFINITY;
1010
1011 for (idx, candidate) in points.iter().enumerate() {
1012 tracing::info!("Scoring candidate {}: {} ({}-{})", idx, candidate, a, b);
1013 let score = score_hybrid(&a.into(), &b.into(), &candidate.into());
1014 if score < best_score {
1015 best_score = score;
1016 best_idx = idx;
1017 }
1018 }
1019
1020 if let EnrichedCandidates::Point((points, _, _)) = &mut resolved[i] {
1022 let best = points[best_idx].clone();
1023 points.clear();
1024 points.push(best);
1025 }
1026 }
1027 }
1028 }
1029
1030 if let EnrichedCandidates::Point((pts, _, _)) = &resolved[i] {
1032 if let Some(pt) = pts.first() {
1033 last_known = Some(pt.clone());
1034 }
1035 }
1036 } else if let EnrichedCandidates::PointCoords((pt, _, _)) = &resolved[i] {
1037 last_known = Some(pt.clone());
1038 }
1039 }
1040
1041 let mut segments = Vec::new();
1043 let mut previous_point: Option<ResolvedPoint> = None;
1044
1045 for enriched in resolved {
1046 match enriched {
1047 EnrichedCandidates::Point((points, alt, spd)) => {
1048 if let Some(point) = points.first() {
1049 if let Some(prev) = &previous_point {
1050 if prev == point {
1051 continue;
1052 }
1053 segments.push(ResolvedRouteSegment {
1054 start: prev.clone(),
1055 end: point.clone(),
1056 name: None,
1057 altitude: alt,
1058 speed: spd,
1059 });
1060 }
1061 previous_point = Some(point.clone());
1062 }
1063 }
1064 EnrichedCandidates::PointCoords((point, alt, spd)) => {
1065 if let Some(prev) = previous_point {
1066 segments.push(ResolvedRouteSegment {
1067 start: prev,
1068 end: point.clone(),
1069 name: None,
1070 altitude: alt,
1071 speed: spd,
1072 });
1073 }
1074 previous_point = Some(point.clone());
1075 }
1076 EnrichedCandidates::Airway((routes, name, alt, spd)) => {
1077 if let Some(route) = routes.first() {
1078 for segment in &route.segments {
1079 segments.push(ResolvedRouteSegment {
1080 start: segment.start.clone(),
1081 end: segment.end.clone(),
1082 name: Some(name.clone()),
1083 altitude: alt.clone(),
1084 speed: spd.clone(),
1085 });
1086 }
1087 previous_point = Some(route.segments.last().unwrap().end.clone());
1088 }
1089 }
1090 EnrichedCandidates::Direct() => {
1091 }
1093 }
1094 }
1095 segments
1096 }
1097}
1098
1099fn score_hybrid(a: &Coor2D, b: &Coor2D, x: &Coor2D) -> f64 {
1100 let ab = WGS84.geodesic_inv(a, b).to_degrees();
1102 let ax = WGS84.geodesic_inv(a, x).to_degrees();
1103 let xb = WGS84.geodesic_inv(x, b).to_degrees();
1104
1105 let gap_ratio = (ax[2] + xb[2]) / ab[2].max(1e-9);
1107
1108 let delta_a = (ax[0] - ab[0]).abs().min(360.0 - (ax[0] - ab[0]).abs());
1109 let delta_b = (xb[0] - ab[0]).abs().min(360.0 - (xb[0] - ab[0]).abs());
1110 let bearing_diff = (delta_a + delta_b) / 2.0; tracing::info!(
1113 "Scoring point: {} = {} + {}; bearing_diff = {:.3}, gap_ratio = {:.3}",
1114 ab[2],
1115 ax[2],
1116 xb[2],
1117 bearing_diff,
1118 gap_ratio
1119 );
1120 bearing_diff / 180. + (gap_ratio - 1.0).max(0.)
1122}