use aerocontext_core::{Airway, NavDataSnapshot};
use super::{ExpandedRoute, RouteError, RoutePoint, RouteToken, token};
pub fn expand_str(route: &str, snapshot: &NavDataSnapshot) -> Result<ExpandedRoute, RouteError> {
expand(&token::parse(route)?, snapshot)
}
pub fn expand(
tokens: &[RouteToken],
snapshot: &NavDataSnapshot,
) -> Result<ExpandedRoute, RouteError> {
let mut route = ExpandedRoute::default();
let last_index = tokens.len().saturating_sub(1);
let mut pending_airway: Option<String> = None;
for (index, current) in tokens.iter().enumerate() {
match current {
RouteToken::Dct => {}
RouteToken::Procedure { .. } => route.procedures.push(current.clone()),
RouteToken::LatLon(position) => {
if let Some(airway) = pending_airway.take() {
return Err(RouteError::ExitFixNotOnAirway {
fix: format!("{:.4},{:.4}", position.lat, position.lon),
airway,
});
}
route.points.push(RoutePoint::new(*position));
}
RouteToken::Airway(designator) => {
pending_airway = Some(designator.clone());
}
RouteToken::Ident(ident) => {
let is_edge = index == 0 || index == last_index;
match resolve_ident(snapshot, ident, None) {
Ok(point) => {
if let Some(airway) = pending_airway.take() {
traverse_airway(snapshot, &airway, &mut route, ident)?;
} else {
route.points.push(point);
}
}
Err(error) => {
if is_edge
&& ident.chars().last().is_some_and(|c| c.is_ascii_digit())
&& pending_airway.is_none()
{
route.procedures.push(RouteToken::Procedure {
name: ident.clone(),
transition: None,
});
} else {
return Err(error);
}
}
}
}
}
}
Ok(route)
}
fn resolve_ident(
snapshot: &NavDataSnapshot,
ident: &str,
region: Option<&str>,
) -> Result<RoutePoint, RouteError> {
snapshot
.resolve_preferring_region(ident, region)
.map(|point| RoutePoint::new(point.position).with_ident(Some(point.ident.clone())))
.ok_or_else(|| RouteError::UnresolvedIdent {
ident: ident.to_owned(),
cycle: snapshot.cycle.effective_on,
})
}
fn traverse_airway(
snapshot: &NavDataSnapshot,
designator: &str,
route: &mut ExpandedRoute,
exit_ident: &str,
) -> Result<(), RouteError> {
let entry_ident = route
.points
.last()
.and_then(|point| point.ident.clone())
.ok_or_else(|| RouteError::AirwayAtRouteEdge {
airway: designator.to_owned(),
})?;
let candidates: Vec<&Airway> = snapshot.airways_named(designator).collect();
if candidates.is_empty() {
return Err(RouteError::UnknownAirway {
airway: designator.to_owned(),
});
}
let mut traversals = Vec::new();
for airway in &candidates {
if let Some(span) = best_span(airway, &entry_ident, exit_ident) {
traversals.push((airway, span));
}
}
match traversals.len() {
0 => {
let on_airway = |ident: &str| {
candidates.iter().any(|airway| {
airway
.points
.iter()
.any(|point| point.ident.eq_ignore_ascii_case(ident))
})
};
if !on_airway(&entry_ident) {
Err(RouteError::EntryFixNotOnAirway {
fix: entry_ident,
airway: designator.to_owned(),
})
} else {
Err(RouteError::ExitFixNotOnAirway {
fix: exit_ident.to_owned(),
airway: designator.to_owned(),
})
}
}
1 => {
let (airway, (entry_idx, exit_idx)) = traversals.remove(0);
emit_span(snapshot, airway, entry_idx, exit_idx, route)
}
_ => Err(RouteError::AmbiguousAirway {
airway: designator.to_owned(),
entry: entry_ident,
exit: exit_ident.to_owned(),
}),
}
}
fn best_span(airway: &Airway, entry: &str, exit: &str) -> Option<(usize, usize)> {
let positions = |ident: &str| {
airway
.points
.iter()
.enumerate()
.filter(|(_, point)| point.ident.eq_ignore_ascii_case(ident))
.map(|(index, _)| index)
.collect::<Vec<_>>()
};
let entries = positions(entry);
let exits = positions(exit);
let mut best: Option<(usize, usize)> = None;
for &entry_idx in &entries {
for &exit_idx in &exits {
if entry_idx == exit_idx {
continue;
}
let span = entry_idx.abs_diff(exit_idx);
if best.is_none_or(|(a, b)| span < a.abs_diff(b)) {
best = Some((entry_idx, exit_idx));
}
}
}
best
}
fn emit_span(
snapshot: &NavDataSnapshot,
airway: &Airway,
entry_idx: usize,
exit_idx: usize,
route: &mut ExpandedRoute,
) -> Result<(), RouteError> {
let forward = entry_idx < exit_idx;
let indices: Vec<usize> = if forward {
(entry_idx..=exit_idx).collect()
} else {
(exit_idx..=entry_idx).rev().collect()
};
for window in indices.windows(2) {
let (from, to) = (window[0], window[1]);
let gap_index = from.min(to);
if airway.points[gap_index].gap_to_next {
return Err(RouteError::DiscontinuedSegment {
airway: airway.ident.clone(),
from: airway.points[from].ident.clone(),
to: airway.points[to].ident.clone(),
});
}
}
for &index in indices.iter().skip(1) {
let airway_point = &airway.points[index];
let resolved = resolve_ident(
snapshot,
&airway_point.ident,
airway_point.icao_region.as_deref(),
)?;
route
.points
.push(resolved.with_via_airway(Some(airway.ident.clone())));
}
Ok(())
}