diff --git a/crates/core/src/routing/discover.rs b/crates/core/src/routing/discover.rs index 71a12b6..28b11f4 100644 --- a/crates/core/src/routing/discover.rs +++ b/crates/core/src/routing/discover.rs @@ -58,11 +58,33 @@ pub struct DiscoverResult { pub log: Vec, } +/// A computer-navigation fix: an unnamed ARINC waypoint our navdata carries on an +/// airway (e.g. `GT27A`, `BS25B`). Their idents contain a digit; real ICAO enroute +/// points are all-letter 5LNCs (`SOVOS`) or letter navaids (`EPL`). IFPS rejects +/// these designators (`ROUTE130`) because the airway already implies them. +fn is_computer_fix(ident: &str) -> bool { + ident.chars().any(|c| c.is_ascii_digit()) +} + /// ICAO item-15 (enroute) string for a route: start at the SID exit fix, then /// `airway to` for each enroute leg, ending at the STAR entry fix. The leading /// SID and trailing STAR (first/last leg) are omitted — IFPS derives them. +/// Computer-nav fixes are stripped (collapsing same-airway legs) so we never file +/// a designator IFPS would reject. pub fn route_item15(route: &Route) -> String { - let legs = &route.legs; + let cn: std::collections::HashSet = route + .legs + .iter() + .flat_map(|l| [l.from.clone(), l.to.clone()]) + .filter(|id| is_computer_fix(id)) + .collect(); + let collapsed; + let legs: &[Leg] = if cn.is_empty() { + &route.legs + } else { + collapsed = drop_designators(&route.legs, &cn); + &collapsed + }; match legs.len() { 0 => String::new(), 1 => "DCT".to_owned(), diff --git a/crates/core/src/routing/mod.rs b/crates/core/src/routing/mod.rs index 34e5e88..34fb32f 100644 --- a/crates/core/src/routing/mod.rs +++ b/crates/core/src/routing/mod.rs @@ -23,6 +23,14 @@ use graph::{EdgeData, NodeData, RouteGraph}; const CONNECT_MAX_NM: f64 = 100.0; const CONNECT_K: usize = 30; +/// A spliced airway sub-path is rejected if it exceeds this multiple of the direct +/// distance between its endpoints (guards against a fragmented FL graph routing +/// two nearby fixes the long way around the network). +const SPLICE_MAX_RATIO: f64 = 3.0; +/// Floor for the splice bound so a short splice (a few nm direct) still has room +/// to route around terminal airspace. +const SPLICE_MIN_NM: f64 = 40.0; + /// One leg of a computed route. #[derive(Debug, Clone, PartialEq)] pub struct Leg { @@ -412,12 +420,20 @@ pub fn airway_path( |e| e.weight().dist_nm, |n| rg.g[n].pos.distance_nm(&target), ); - let Some((_, path)) = result else { + let Some((total, path)) = result else { return Ok(None); }; if path.len() < 2 { return Ok(None); } + // Reject a splice that balloons far beyond the direct distance: at a fragmented + // FL the airway graph can force A* to wander across the network (even around + // the globe) to connect two nearby fixes. Such a path is never the real fix — + // better to leave the original leg for another repair than file garbage. + let direct = rg.g[a].pos.distance_nm(&target); + if total > SPLICE_MAX_RATIO * direct.max(SPLICE_MIN_NM) { + return Ok(None); + } let mut legs = Vec::with_capacity(path.len() - 1); for pair in path.windows(2) { let (x, y) = (pair[0], pair[1]);