use aerocontext_core::GeoPoint;
use super::{RouteError, RouteToken, UnsupportedKind};
const AIRWAY_PREFIXES: [&str; 15] = [
"TK", "V", "J", "T", "Q", "A", "B", "G", "R", "Y", "L", "M", "N", "W", "H",
];
pub fn parse(route: &str) -> Result<Vec<RouteToken>, RouteError> {
let raw: Vec<&str> = route.split_whitespace().collect();
if raw.is_empty() {
return Err(RouteError::Empty);
}
let mut tokens = Vec::with_capacity(raw.len());
for token in raw {
tokens.push(classify(&token.to_ascii_uppercase())?);
}
structural_checks(&tokens)?;
Ok(tokens)
}
fn classify(token: &str) -> Result<RouteToken, RouteError> {
if token.contains('/') {
return Err(RouteError::UnsupportedToken {
kind: slash_kind(token),
token: token.to_owned(),
});
}
if let Some(point) = lat_lon(token) {
return Ok(RouteToken::LatLon(point));
}
if is_fix_radial_distance(token) {
return Err(RouteError::UnsupportedToken {
kind: UnsupportedKind::FixRadialDistance,
token: token.to_owned(),
});
}
if token == "DCT" {
return Ok(RouteToken::Dct);
}
if let Some((name, transition)) = token.split_once('.') {
return Ok(RouteToken::Procedure {
name: name.to_owned(),
transition: Some(transition.to_owned()),
});
}
if is_airway(token) {
return Ok(RouteToken::Airway(token.to_owned()));
}
Ok(RouteToken::Ident(token.to_owned()))
}
fn structural_checks(tokens: &[RouteToken]) -> Result<(), RouteError> {
if let Some(RouteToken::Airway(airway)) = tokens.first() {
return Err(RouteError::AirwayAtRouteEdge {
airway: airway.clone(),
});
}
if let Some(RouteToken::Airway(airway)) = tokens.last() {
return Err(RouteError::AirwayAtRouteEdge {
airway: airway.clone(),
});
}
for pair in tokens.windows(2) {
if let [RouteToken::Airway(first), RouteToken::Airway(second)] = pair {
return Err(RouteError::AdjacentAirways {
first: first.clone(),
second: second.clone(),
});
}
}
Ok(())
}
fn slash_kind(token: &str) -> UnsupportedKind {
let after = token.split_once('/').map(|(_, rest)| rest).unwrap_or("");
let mut chars = after.chars();
match (chars.next(), chars.next()) {
(Some('N' | 'M' | 'K'), Some(d)) if d.is_ascii_digit() => {
UnsupportedKind::SpeedAltitudeChange
}
_ => UnsupportedKind::SlashToken,
}
}
fn lat_lon(token: &str) -> Option<GeoPoint> {
if !token.is_ascii() {
return None;
}
let bytes = token.as_bytes();
match bytes.len() {
7 => {
let lat = parse_digits(&token[0..2])?;
let ns = bytes[2];
let lon = parse_digits(&token[3..6])?;
let ew = bytes[6];
build_lat_lon(lat, 0, ns, lon, 0, ew)
}
11 => {
let lat = parse_digits(&token[0..2])?;
let lat_min = parse_digits(&token[2..4])?;
let ns = bytes[4];
let lon = parse_digits(&token[5..8])?;
let lon_min = parse_digits(&token[8..10])?;
let ew = bytes[10];
build_lat_lon(lat, lat_min, ns, lon, lon_min, ew)
}
_ => None,
}
}
fn parse_digits(text: &str) -> Option<u32> {
text.chars()
.all(|c| c.is_ascii_digit())
.then(|| text.parse().ok())
.flatten()
}
fn build_lat_lon(
lat_deg: u32,
lat_min: u32,
ns: u8,
lon_deg: u32,
lon_min: u32,
ew: u8,
) -> Option<GeoPoint> {
if lat_deg > 90 || lat_min >= 60 || lon_deg > 180 || lon_min >= 60 {
return None;
}
let lat_sign = match ns {
b'N' => 1.0,
b'S' => -1.0,
_ => return None,
};
let lon_sign = match ew {
b'E' => 1.0,
b'W' => -1.0,
_ => return None,
};
Some(GeoPoint {
lat: lat_sign * (f64::from(lat_deg) + f64::from(lat_min) / 60.0),
lon: lon_sign * (f64::from(lon_deg) + f64::from(lon_min) / 60.0),
})
}
fn is_fix_radial_distance(token: &str) -> bool {
let letters = token.chars().take_while(char::is_ascii_uppercase).count();
let digits = token.len() - letters;
(2..=5).contains(&letters)
&& (5..=6).contains(&digits)
&& token[letters..].chars().all(|c| c.is_ascii_digit())
}
fn is_airway(token: &str) -> bool {
AIRWAY_PREFIXES.iter().any(|prefix| {
token.len() > prefix.len()
&& token.len() <= prefix.len() + 3
&& token.starts_with(prefix)
&& token[prefix.len()..].chars().all(|c| c.is_ascii_digit())
})
}