1use serde::{Deserialize, Serialize};
64use std::fmt;
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(untagged)]
69pub enum Field15Element {
70 Point(Point),
72 Connector(Connector),
74 Modifier(Modifier),
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub enum Point {
81 #[serde(rename = "waypoint")]
85 Waypoint(String),
86 #[serde(rename = "coords")]
90 Coordinates((f64, f64)),
91 #[serde(rename = "point_bearing_distance")]
96 BearingDistance {
97 point: Box<Point>,
99 bearing: u16,
101 distance: u16,
103 },
104 #[serde(rename = "aerodrome")]
106 Aerodrome(String),
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub enum Connector {
112 #[serde(rename = "airway")]
127 Airway(String),
128 #[serde(rename = "DCT")]
130 Direct,
131 #[serde(rename = "SID")]
137 Sid(String),
138 #[serde(rename = "STAR")]
144 Star(String),
145 #[serde(rename = "VFR")]
147 Vfr,
148 #[serde(rename = "IFR")]
150 Ifr,
151 #[serde(rename = "OAT")]
153 Oat,
154 #[serde(rename = "GAT")]
156 Gat,
157 #[serde(rename = "IFPSTOP")]
159 IfpStop,
160 #[serde(rename = "IFPSTART")]
162 IfpStart,
163 #[serde(rename = "STAY")]
165 StayTime { minutes: Option<u16> },
166 #[serde(rename = "NAT")]
170 Nat(String),
171 #[serde(rename = "PTS")]
174 Pts(String),
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct Modifier {
180 pub speed: Option<Speed>,
182 pub altitude: Option<Altitude>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub altitude_cruise_to: Option<Altitude>,
187 #[serde(skip_serializing_if = "std::ops::Not::not")]
189 pub cruise_climb: bool,
190}
191
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub enum Speed {
195 #[serde(rename = "kts")]
197 Knots(u16),
198 #[serde(rename = "Mach")]
200 Mach(f32),
201 #[serde(rename = "km/h")]
203 KilometersPerHour(u16),
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
208pub enum Altitude {
209 #[serde(rename = "FL")]
211 FlightLevel(u16),
212 #[serde(rename = "S")]
214 MetricLevel(u16),
215 #[serde(rename = "ft")]
217 Altitude(u16),
218 #[serde(rename = "m")]
220 MetricAltitude(u16),
221 #[serde(rename = "VFR")]
223 Vfr,
224}
225
226impl fmt::Display for Field15Element {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 match self {
229 Field15Element::Point(p) => write!(f, "Point({})", p),
230 Field15Element::Connector(c) => write!(f, "Connector({})", c),
231 Field15Element::Modifier(m) => write!(f, "Modifier({})", m),
232 }
233 }
234}
235
236impl fmt::Display for Point {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238 match self {
239 Point::Waypoint(s) => write!(f, "Waypoint({})", s),
240 Point::Coordinates((lat, lon)) => write!(f, "Coordinate({:.5},{:.5})", lat, lon),
241 Point::BearingDistance {
242 point,
243 bearing,
244 distance,
245 } => {
246 write!(f, "BearingDistance({}/{:03}/{:03})", point, bearing, distance)
247 }
248 Point::Aerodrome(s) => write!(f, "Aerodrome({})", s),
249 }
250 }
251}
252
253impl fmt::Display for Connector {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 match self {
256 Connector::Airway(s) => write!(f, "Airway({})", s),
257 Connector::Direct => write!(f, "DCT"),
258 Connector::Vfr => write!(f, "VFR"),
259 Connector::Ifr => write!(f, "IFR"),
260 Connector::Oat => write!(f, "OAT"),
261 Connector::Gat => write!(f, "GAT"),
262 Connector::IfpStop => write!(f, "IFPSTOP"),
263 Connector::IfpStart => write!(f, "IFPSTART"),
264 Connector::StayTime { minutes } => {
265 if let Some(m) = minutes {
266 write!(f, "STAY({})", m)
267 } else {
268 write!(f, "STAY")
269 }
270 }
271 Connector::Sid(s) => write!(f, "SID({})", s),
272 Connector::Star(s) => write!(f, "STAR({})", s),
273 Connector::Nat(s) => write!(f, "NAT({})", s),
274 Connector::Pts(s) => write!(f, "PTS({})", s),
275 }
276 }
277}
278
279impl fmt::Display for Modifier {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 match (&self.speed, &self.altitude, self.cruise_climb) {
282 (Some(s), Some(a), true) => write!(f, "{}{}PLUS", s, a),
283 (Some(s), Some(a), false) => write!(f, "{}{}", s, a),
284 (Some(s), None, true) => write!(f, "{}PLUS", s),
285 (Some(s), None, false) => write!(f, "{}", s),
286 (None, Some(a), true) => write!(f, "{}PLUS", a),
287 (None, Some(a), false) => write!(f, "{}", a),
288 (None, None, _) => write!(f, ""),
289 }
290 }
291}
292
293impl fmt::Display for Speed {
294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295 match self {
296 Speed::Knots(n) => write!(f, "N{:04}", n),
297 Speed::Mach(m) => write!(f, "M{:0>5.2}", m),
298 Speed::KilometersPerHour(k) => write!(f, "K{:04}", k),
299 }
300 }
301}
302
303impl fmt::Display for Altitude {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 match self {
306 Altitude::FlightLevel(fl) => write!(f, "F{:03}", fl),
307 Altitude::MetricLevel(s) => write!(f, "S{:04}", s),
308 Altitude::Altitude(a) => write!(f, "A{:04}", a),
309 Altitude::MetricAltitude(m) => write!(f, "M{:04}", m),
310 Altitude::Vfr => write!(f, "VFR"),
311 }
312 }
313}
314
315pub struct Field15Parser;
317
318impl Field15Parser {
319 pub fn parse(route: &str) -> Vec<Field15Element> {
324 let mut elements = Vec::new();
325 let tokens = Self::tokenize(route);
326 let mut i = 0;
327 let mut first_point_parsed = false;
328
329 while i < tokens.len() {
330 let token = tokens[i];
331
332 if token == "T" {
334 if i + 1 < tokens.len() {
336 }
338 break;
339 }
340
341 if token == "/" {
343 i += 1;
344 continue;
345 }
346
347 if Self::is_stay(token) {
349 let stay_minutes = if i + 2 < tokens.len() && tokens[i + 1] == "/" {
351 if let Some(mins) = Self::parse_stay_time(tokens[i + 2]) {
352 i += 2; Some(mins)
354 } else {
355 None
356 }
357 } else {
358 None
359 };
360 elements.push(Field15Element::Connector(Connector::StayTime { minutes: stay_minutes }));
361 i += 1;
362 continue;
363 }
364
365 if token == "C" && i + 1 < tokens.len() && tokens[i + 1] == "/" {
367 i += 1;
369 continue;
370 }
371
372 if let Some(modifier) = Self::parse_modifier(token) {
374 elements.push(Field15Element::Modifier(modifier));
375 }
376 else if token == "DCT" {
378 elements.push(Field15Element::Connector(Connector::Direct));
379 } else if token == "VFR" {
380 elements.push(Field15Element::Connector(Connector::Vfr));
381 } else if token == "IFR" {
382 elements.push(Field15Element::Connector(Connector::Ifr));
383 } else if token == "OAT" {
384 elements.push(Field15Element::Connector(Connector::Oat));
385 } else if token == "GAT" {
386 elements.push(Field15Element::Connector(Connector::Gat));
387 } else if token == "IFPSTOP" {
388 elements.push(Field15Element::Connector(Connector::IfpStop));
389 } else if token == "IFPSTART" {
390 elements.push(Field15Element::Connector(Connector::IfpStart));
391 } else if token == "SID" {
392 elements.push(Field15Element::Connector(Connector::Sid("SID".to_string())));
394 first_point_parsed = true;
395 } else if token == "STAR" {
396 elements.push(Field15Element::Connector(Connector::Star("STAR".to_string())));
398 first_point_parsed = true;
399 }
400 else if !first_point_parsed && Self::is_procedure(token) {
402 elements.push(Field15Element::Connector(Connector::Sid(token.to_string())));
404 first_point_parsed = true;
405 } else if Self::is_procedure(token) && i == tokens.len() - 1 {
406 elements.push(Field15Element::Connector(Connector::Star(token.to_string())));
408 first_point_parsed = true;
409 }
410 else if !elements.is_empty()
412 && matches!(elements.last(), Some(Field15Element::Connector(Connector::Direct)))
413 {
414 if let Some(point) = Self::parse_point(token) {
416 elements.push(Field15Element::Point(point));
417 first_point_parsed = true;
418 }
419 }
420 else if Self::is_nat(token) {
422 elements.push(Field15Element::Connector(Connector::Nat(token.to_string())));
423 } else if Self::is_pts(token) {
424 elements.push(Field15Element::Connector(Connector::Pts(token.to_string())));
425 }
426 else if Self::is_airway(token) {
428 if i == tokens.len() - 1 && Self::is_procedure(token) {
430 elements.push(Field15Element::Connector(Connector::Star(token.to_string())));
431 first_point_parsed = true;
432 } else {
433 elements.push(Field15Element::Connector(Connector::Airway(token.to_string())));
434 }
435 }
436 else if let Some(point) = Self::parse_point(token) {
438 elements.push(Field15Element::Point(point));
439 first_point_parsed = true;
440 }
441
442 i += 1;
443 }
444
445 elements
446 }
447
448 fn tokenize(route: &str) -> Vec<&str> {
453 let mut tokens = Vec::new();
454 let mut current_token_start = 0;
455 let mut in_token = false;
456
457 for (i, ch) in route.char_indices() {
458 let is_whitespace = ch == ' ' || ch == '\n' || ch == '\t' || ch == '\r';
459 let is_slash = ch == '/';
460
461 if is_whitespace || is_slash {
462 if in_token {
464 tokens.push(&route[current_token_start..i]);
465 in_token = false;
466 }
467
468 if is_slash {
470 tokens.push("/");
471 }
472 } else if !in_token {
473 current_token_start = i;
475 in_token = true;
476 }
477 }
478
479 if in_token {
481 tokens.push(&route[current_token_start..]);
482 }
483
484 tokens
485 }
486
487 fn parse_modifier(token: &str) -> Option<Modifier> {
489 if token.len() < 4 {
490 return None;
491 }
492
493 let (base_token, cruise_climb) = if let Some(stripped) = token.strip_suffix("PLUS") {
495 (stripped, true)
496 } else {
497 (token, false)
498 };
499
500 if base_token.len() < 4 {
501 return None;
502 }
503
504 let speed_lengths: &[usize] = if base_token.starts_with('M') { &[4] } else { &[5, 4] };
506
507 for speed_len in speed_lengths {
508 if base_token.len() < *speed_len {
509 continue;
510 }
511 let speed = Self::parse_speed(&base_token[..*speed_len]);
512 if speed.is_none() || base_token.len() <= *speed_len {
513 continue;
514 }
515 let remaining = &base_token[*speed_len..];
516 let (altitude, altitude_cruise_to) = if remaining.len() >= 7 {
518 let first_alt_len = if remaining.starts_with('F') || remaining.starts_with('A') {
519 4
520 } else {
521 5
522 };
523 if remaining.len() >= first_alt_len + 3 {
524 let alt1 = Self::parse_altitude(&remaining[..first_alt_len]);
525 let alt2 = Self::parse_altitude(&remaining[first_alt_len..]);
526 if alt1.is_some() && alt2.is_some() {
527 (alt1, alt2)
528 } else {
529 (Self::parse_altitude(remaining), None)
530 }
531 } else {
532 (Self::parse_altitude(remaining), None)
533 }
534 } else {
535 (Self::parse_altitude(remaining), None)
536 };
537
538 if altitude.is_some() {
539 return Some(Modifier {
540 speed,
541 altitude,
542 altitude_cruise_to,
543 cruise_climb,
544 });
545 }
546 }
547
548 let (altitude, altitude_cruise_to) = if base_token.len() >= 3 {
550 (Self::parse_altitude(base_token), None)
551 } else {
552 (None, None)
553 };
554
555 if altitude.is_some() {
557 Some(Modifier {
558 speed: None,
559 altitude,
560 altitude_cruise_to,
561 cruise_climb,
562 })
563 } else {
564 None
565 }
566 }
567
568 fn parse_speed(s: &str) -> Option<Speed> {
570 if s.len() < 4 {
571 return None;
572 }
573
574 let speed_type = s.chars().next()?;
575 let value_str = &s[1..];
576
577 match speed_type {
578 'N' if value_str.len() == 4 || value_str.len() == 3 => value_str.parse::<u16>().ok().map(Speed::Knots),
579 'M' if value_str.len() == 3 => {
580 value_str.parse::<u16>().ok().map(|v| Speed::Mach((v as f32) / 100.0))
582 }
583 'K' if value_str.len() == 4 || value_str.len() == 3 => {
584 value_str.parse::<u16>().ok().map(Speed::KilometersPerHour)
585 }
586 _ => None,
587 }
588 }
589
590 fn parse_altitude(s: &str) -> Option<Altitude> {
592 if s == "VFR" {
593 return Some(Altitude::Vfr);
594 }
595
596 if s.len() < 4 {
597 return None;
598 }
599
600 let alt_type = s.chars().next()?;
601 let value_str = &s[1..];
602
603 match alt_type {
604 'F' if value_str.len() == 3 => value_str.parse::<u16>().ok().map(Altitude::FlightLevel),
605 'S' if value_str.len() == 4 => value_str.parse::<u16>().ok().map(Altitude::MetricLevel),
606 'A' if value_str.len() == 4 => value_str.parse::<u16>().ok().map(Altitude::Altitude),
607 'M' if value_str.len() == 4 => value_str.parse::<u16>().ok().map(Altitude::MetricAltitude),
608 _ => None,
609 }
610 }
611
612 fn is_nat(token: &str) -> bool {
614 if token.len() == 4 && token.starts_with("NAT") {
616 let c = token.chars().nth(3).unwrap();
617 c.is_ascii_uppercase()
618 } else if token.len() == 5 && token.starts_with("NAT") {
619 let c = token.chars().nth(3).unwrap();
620 let d = token.chars().nth(4).unwrap();
621 c.is_ascii_uppercase() && d.is_ascii_digit()
622 } else {
623 false
624 }
625 }
626
627 fn is_pts(token: &str) -> bool {
629 if token.len() == 4 && token.starts_with("PTS") {
631 let c = token.chars().nth(3).unwrap();
632 c.is_ascii_digit() || c.is_ascii_uppercase()
633 } else {
634 false
635 }
636 }
637
638 fn is_airway(token: &str) -> bool {
640 if token.len() < 2 || token.len() > 7 {
641 return false;
642 }
643
644 if Self::is_nat(token) || Self::is_pts(token) {
646 return false;
647 }
648
649 let first_char = token.chars().next().unwrap();
650 if !first_char.is_alphabetic() {
651 return false;
652 }
653
654 let has_digit = token.chars().any(|c| c.is_ascii_digit());
656 if !has_digit {
657 return false;
658 }
659
660 let valid_prefixes = [
661 "UN", "UM", "UL", "UT", "UZ", "UY", "UP", "UA", "UB", "UG", "UH", "UJ", "UQ", "UR", "UV", "UW", "L", "A",
662 "B", "G", "H", "J", "Q", "R", "T", "V", "W", "Y", "Z", "M", "N", "P",
663 ];
664
665 valid_prefixes.iter().any(|&p| token.starts_with(p))
666 }
667
668 fn parse_point(token: &str) -> Option<Point> {
670 if token.is_empty() {
671 return None;
672 }
673
674 if Self::is_coordinate(token) {
676 if let Some(coord) = Self::parse_coordinate(token) {
677 return Some(Point::Coordinates(coord));
678 } else {
679 return None;
680 }
681 }
682
683 if token.len() > 6 {
686 let potential_digits = &token[token.len() - 6..];
687 if potential_digits.chars().all(|c| c.is_ascii_digit()) {
688 let point_name = &token[..token.len() - 6];
689
690 if Self::is_coordinate(point_name) {
692 if let Some(coord) = Self::parse_coordinate(point_name) {
693 if let (Ok(bearing), Ok(distance)) = (
694 potential_digits[..3].parse::<u16>(),
695 potential_digits[3..].parse::<u16>(),
696 ) {
697 if bearing <= 360 && distance <= 999 {
698 return Some(Point::BearingDistance {
699 point: Box::new(Point::Coordinates(coord)),
700 bearing,
701 distance,
702 });
703 }
704 }
705 }
706 } else if !point_name.is_empty() && point_name.chars().all(|c| c.is_ascii_alphabetic()) {
707 if let (Ok(bearing), Ok(distance)) = (
709 potential_digits[..3].parse::<u16>(),
710 potential_digits[3..].parse::<u16>(),
711 ) {
712 if bearing <= 360 && distance <= 999 {
713 return Some(Point::BearingDistance {
714 point: Box::new(Point::Waypoint(point_name.to_string())),
715 bearing,
716 distance,
717 });
718 }
719 }
720 }
721 }
722 }
723
724 if token.len() == 4
726 && token.chars().all(|c| c.is_ascii_uppercase())
727 && !token.chars().any(|c| c.is_ascii_digit())
728 {
729 return Some(Point::Aerodrome(token.to_string()));
730 }
731
732 Some(Point::Waypoint(token.to_string()))
734 }
735
736 fn parse_coordinate(token: &str) -> Option<(f64, f64)> {
739 let n_idx = token.find('N');
741 let s_idx = token.find('S');
742 let e_idx = token.find('E');
743 let w_idx = token.find('W');
744
745 let (lat_val, lat_sign, lat_end) = match (n_idx, s_idx) {
747 (Some(idx), _) => (&token[..idx], 1.0, idx + 1),
748 (_, Some(idx)) => (&token[..idx], -1.0, idx + 1),
749 _ => return None,
750 };
751 let lat = match lat_val.len() {
752 2 => lat_val.parse::<f64>().ok()? * lat_sign,
753 4 => {
754 let deg = lat_val[..2].parse::<f64>().ok()?;
755 let min = lat_val[2..4].parse::<f64>().ok()?;
756 (deg + min / 60.0) * lat_sign
757 }
758 _ => return None,
759 };
760
761 let (lon_val, lon_sign) = match (e_idx, w_idx) {
763 (Some(idx), _) => (&token[lat_end..idx], 1.0),
764 (_, Some(idx)) => (&token[lat_end..idx], -1.0),
765 _ => return None,
766 };
767 let lon = match lon_val.len() {
768 3 => lon_val.parse::<f64>().ok()? * lon_sign,
769 5 => {
770 let deg = lon_val[..3].parse::<f64>().ok()?;
771 let min = lon_val[3..5].parse::<f64>().ok()?;
772 (deg + min / 60.0) * lon_sign
773 }
774 _ => return None,
775 };
776
777 Some((lat, lon))
778 }
779
780 fn is_coordinate(token: &str) -> bool {
788 if token.len() < 4 {
789 return false;
790 }
791
792 let has_lat = token.contains('N') || token.contains('S');
794 let has_lon = token.contains('E') || token.contains('W');
795
796 if !has_lat && !has_lon {
797 return false;
798 }
799
800 let lat_pos = token.find('N').or_else(|| token.find('S'));
802 let lon_pos = token.find('E').or_else(|| token.find('W'));
803
804 match (lat_pos, lon_pos) {
806 (Some(lat_idx), Some(lon_idx)) => {
807 if lat_idx >= lon_idx {
809 return false;
810 }
811
812 let lat_part = &token[..lat_idx];
814 if lat_part.is_empty() || !lat_part.chars().all(|c| c.is_ascii_digit()) {
815 return false;
816 }
817
818 let lon_part = &token[lat_idx + 1..lon_idx];
820 if lon_part.is_empty() || !lon_part.chars().all(|c| c.is_ascii_digit()) {
821 return false;
822 }
823
824 lon_idx == token.len() - 1
826 }
827 (Some(lat_idx), None) => {
828 let lat_part = &token[..lat_idx];
830 !lat_part.is_empty() && lat_part.chars().all(|c| c.is_ascii_digit()) && lat_idx == token.len() - 1
831 }
832 (None, Some(lon_idx)) => {
833 let lon_part = &token[..lon_idx];
835 !lon_part.is_empty() && lon_part.chars().all(|c| c.is_ascii_digit()) && lon_idx == token.len() - 1
836 }
837 (None, None) => false,
838 }
839 }
840
841 fn is_procedure(token: &str) -> bool {
843 if token.len() >= 5 && token.len() <= 7 {
845 let bytes = token.as_bytes();
846 if bytes.len() >= 5 && bytes[0..3].iter().all(|b| b.is_ascii_uppercase()) && (bytes[3].is_ascii_digit()) {
847 if bytes.len() == 5 && bytes[4].is_ascii_uppercase() {
849 return true;
850 }
851 if bytes.len() == 6 && bytes[4].is_ascii_digit() && bytes[5].is_ascii_uppercase() {
852 return true;
853 }
854 }
855 }
856 if token.len() == 6 || token.len() == 7 {
858 let bytes = token.as_bytes();
859 if bytes[0..5].iter().all(|b| b.is_ascii_uppercase())
860 && bytes[5].is_ascii_digit()
861 && (token.len() == 6 || (token.len() == 7 && bytes[6].is_ascii_digit()))
862 {
863 return true;
864 }
865 }
866 if token.len() >= 6 && token.len() <= 8 {
868 let bytes = token.as_bytes();
869 let prefix_len = token.len() - 2;
870 if (4..=6).contains(&prefix_len)
871 && bytes[0..prefix_len].iter().all(|b| b.is_ascii_uppercase())
872 && bytes[prefix_len].is_ascii_digit()
873 && bytes[prefix_len + 1].is_ascii_uppercase()
874 {
875 return true;
876 }
877 }
878 if token.len() == 8 {
880 let bytes = token.as_bytes();
881 if bytes[0..5].iter().all(|b| b.is_ascii_uppercase())
882 && bytes[5].is_ascii_digit()
883 && bytes[6].is_ascii_digit()
884 && bytes[7].is_ascii_uppercase()
885 {
886 return true;
887 }
888 }
889 false
890 }
891
892 fn is_stay(token: &str) -> bool {
894 token.len() == 5 && token.starts_with("STAY") && token.chars().nth(4).unwrap().is_ascii_digit()
895 }
896
897 fn parse_stay_time(time_str: &str) -> Option<u16> {
899 if time_str.len() != 4 {
900 return None;
901 }
902 let hh = time_str[..2].parse::<u16>().ok()?;
904 let mm = time_str[2..].parse::<u16>().ok()?;
905 if hh <= 23 && mm <= 59 {
906 Some(hh * 60 + mm)
907 } else {
908 None
909 }
910 }
911}
912
913#[cfg(test)]
914mod tests {
915
916 use super::*;
917
918 #[test]
919 fn test_speed_parsing() {
920 assert_eq!(Field15Parser::parse_speed("N0456"), Some(Speed::Knots(456)));
921 assert_eq!(Field15Parser::parse_speed("M079"), Some(Speed::Mach(0.79)));
922 assert_eq!(Field15Parser::parse_speed("K0893"), Some(Speed::KilometersPerHour(893)));
923 }
924
925 #[test]
926 fn test_altitude_parsing() {
927 assert_eq!(Field15Parser::parse_altitude("F340"), Some(Altitude::FlightLevel(340)));
928 assert_eq!(
929 Field15Parser::parse_altitude("S1130"),
930 Some(Altitude::MetricLevel(1130))
931 );
932 }
933
934 #[test]
935 fn test_coordinate_detection() {
936 assert!(Field15Parser::is_coordinate("62N010W"));
937 assert!(Field15Parser::is_coordinate("5430N"));
938 assert!(Field15Parser::is_coordinate("53N100W"));
939 assert!(!Field15Parser::is_coordinate("LACOU"));
940 }
941
942 #[test]
943 fn test_procedure_detection() {
944 assert!(Field15Parser::is_procedure("LACOU5A"));
945 assert!(Field15Parser::is_procedure("ROXOG1H"));
946 assert!(Field15Parser::is_procedure("RANUX3D"));
947 assert!(!Field15Parser::is_procedure("LACOU"));
948 assert!(!Field15Parser::is_procedure("CNA"));
949 }
950
951 #[test]
952 fn test_airway_detection() {
953 assert!(Field15Parser::is_airway("UM184"));
954 assert!(Field15Parser::is_airway("UN863"));
955 assert!(Field15Parser::is_airway("L738"));
956 assert!(Field15Parser::is_airway("A308"));
957 assert!(!Field15Parser::is_airway("DCT"));
958 assert!(!Field15Parser::is_airway("LACOU"));
959 }
960
961 #[test]
962 fn test_tokenization() {
963 let tokens = Field15Parser::tokenize("N0450F100 POINT/M079F200 DCT");
964 assert_eq!(tokens, vec!["N0450F100", "POINT", "/", "M079F200", "DCT"]);
965 }
966
967 #[test]
968 fn test_tokenization_multiple_whitespace() {
969 let tokens = Field15Parser::tokenize("A B\tC\nD\rE");
970 assert_eq!(tokens, vec!["A", "B", "C", "D", "E"]);
971 }
972
973 #[test]
974 fn test_slash_handling() {
975 let route = "N0450F100 POINT/M079F200";
976 let elements = Field15Parser::parse(route);
977
978 assert!(elements.len() >= 3);
980
981 let modifiers: Vec<_> = elements
982 .iter()
983 .filter(|e| matches!(e, Field15Element::Modifier(_)))
984 .collect();
985 assert_eq!(modifiers.len(), 2);
986 }
987
988 #[test]
989 fn test_coordinate_validation() {
990 assert!(Field15Parser::is_coordinate("5020N"));
991 assert!(Field15Parser::is_coordinate("5020N00130W"));
992 assert!(Field15Parser::is_coordinate("50N005W"));
993 assert!(Field15Parser::is_coordinate("00N000E"));
994
995 assert!(!Field15Parser::is_coordinate("N5020")); assert!(!Field15Parser::is_coordinate("5020W00130N")); assert!(!Field15Parser::is_coordinate("ABC")); assert!(!Field15Parser::is_coordinate("50N")); }
1000
1001 #[test]
1002 fn test_bearing_distance_with_coordinate() {
1003 let route = "N0450F100 02S001W180060";
1004 let elements = Field15Parser::parse(route);
1005
1006 let bearing_dist = elements
1007 .iter()
1008 .find(|e| matches!(e, Field15Element::Point(Point::BearingDistance { .. })));
1009
1010 assert!(bearing_dist.is_some());
1011 if let Some(Field15Element::Point(Point::BearingDistance {
1012 point,
1013 bearing,
1014 distance,
1015 })) = bearing_dist
1016 {
1017 assert_eq!(**point, Point::Coordinates((-2.0, -1.0)));
1018 assert_eq!(*bearing, 180);
1019 assert_eq!(*distance, 60);
1020 }
1021 }
1022
1023 #[test]
1024 fn test_simple_route() {
1025 let route = "N0456F340 LACOU5A LACOU UM184 CNA UN863 MANAK UY110 REVTU UP87 ROXOG ROXOG1H";
1026 let elements = Field15Parser::parse(route);
1027
1028 assert_eq!(elements.len(), 12);
1029 assert_eq!(
1030 elements,
1031 vec![
1032 Field15Element::Modifier(Modifier {
1033 speed: Some(Speed::Knots(456)),
1034 altitude: Some(Altitude::FlightLevel(340)),
1035 cruise_climb: false,
1036 altitude_cruise_to: None
1037 }),
1038 Field15Element::Connector(Connector::Sid("LACOU5A".to_string())),
1039 Field15Element::Point(Point::Waypoint("LACOU".to_string())),
1040 Field15Element::Connector(Connector::Airway("UM184".to_string())),
1041 Field15Element::Point(Point::Waypoint("CNA".to_string())),
1042 Field15Element::Connector(Connector::Airway("UN863".to_string())),
1043 Field15Element::Point(Point::Waypoint("MANAK".to_string())),
1044 Field15Element::Connector(Connector::Airway("UY110".to_string())),
1045 Field15Element::Point(Point::Waypoint("REVTU".to_string())),
1046 Field15Element::Connector(Connector::Airway("UP87".to_string())),
1047 Field15Element::Point(Point::Waypoint("ROXOG".to_string())),
1048 Field15Element::Connector(Connector::Star("ROXOG1H".to_string())),
1049 ]
1050 );
1051 }
1052
1053 #[test]
1054 fn test_readme_example() {
1055 let route = "N0450M0825 00N000E B9 00N001E VFR IFR 00N001W/N0350F100 01N001W 01S001W 02S001W180060";
1058 let elements = Field15Parser::parse(route);
1059
1060 assert_eq!(
1061 elements,
1062 vec![
1063 Field15Element::Modifier(Modifier {
1064 speed: Some(Speed::Knots(450)),
1065 altitude: Some(Altitude::MetricAltitude(825)),
1066 cruise_climb: false,
1067 altitude_cruise_to: None
1068 }),
1069 Field15Element::Point(Point::Coordinates((0., 0.))),
1070 Field15Element::Connector(Connector::Airway("B9".to_string())),
1071 Field15Element::Point(Point::Coordinates((0., 1.))),
1072 Field15Element::Connector(Connector::Vfr),
1073 Field15Element::Connector(Connector::Ifr),
1074 Field15Element::Point(Point::Coordinates((0., -1.))),
1075 Field15Element::Modifier(Modifier {
1076 speed: Some(Speed::Knots(350)),
1077 altitude: Some(Altitude::FlightLevel(100)),
1078 cruise_climb: false,
1079 altitude_cruise_to: None
1080 }),
1081 Field15Element::Point(Point::Coordinates((1., -1.))),
1082 Field15Element::Point(Point::Coordinates((-1., -1.))),
1083 Field15Element::Point(Point::BearingDistance {
1084 point: Box::new(Point::Coordinates((-2., -1.))),
1085 bearing: 180,
1086 distance: 60,
1087 }),
1088 ]
1089 );
1090 }
1091
1092 #[test]
1093 fn test_oat_gat_connectors() {
1094 let route = "N0450F100 POINT OAT POINT GAT POINT";
1095 let elements = Field15Parser::parse(route);
1096
1097 assert_eq!(elements.len(), 6);
1098 assert_eq!(
1099 elements,
1100 vec![
1101 Field15Element::Modifier(Modifier {
1102 speed: Some(Speed::Knots(450)),
1103 altitude: Some(Altitude::FlightLevel(100)),
1104 cruise_climb: false,
1105 altitude_cruise_to: None
1106 }),
1107 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1108 Field15Element::Connector(Connector::Oat),
1109 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1110 Field15Element::Connector(Connector::Gat),
1111 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1112 ]
1113 );
1114 }
1115
1116 #[test]
1117 fn test_ifp_stop_start() {
1118 let route = "N0450F100 POINT IFPSTOP POINT IFPSTART POINT";
1119 let elements = Field15Parser::parse(route);
1120 assert_eq!(elements.len(), 6);
1121 assert_eq!(
1122 elements,
1123 vec![
1124 Field15Element::Modifier(Modifier {
1125 speed: Some(Speed::Knots(450)),
1126 altitude: Some(Altitude::FlightLevel(100)),
1127 cruise_climb: false,
1128 altitude_cruise_to: None
1129 }),
1130 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1131 Field15Element::Connector(Connector::IfpStop),
1132 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1133 Field15Element::Connector(Connector::IfpStart),
1134 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1135 ]
1136 );
1137 }
1138
1139 #[test]
1140 fn test_bearing_distance_format() {
1141 let route = "N0450F100 POINT WAYPOINT180060";
1142 let elements = Field15Parser::parse(route);
1143
1144 assert_eq!(elements.len(), 3);
1145 assert_eq!(
1146 elements,
1147 vec![
1148 Field15Element::Modifier(Modifier {
1149 speed: Some(Speed::Knots(450)),
1150 altitude: Some(Altitude::FlightLevel(100)),
1151 cruise_climb: false,
1152 altitude_cruise_to: None
1153 }),
1154 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1155 Field15Element::Point(Point::BearingDistance {
1156 point: Box::new(Point::Waypoint("WAYPOINT".to_string())),
1157 bearing: 180,
1158 distance: 60,
1159 }),
1160 ]
1161 );
1162 }
1163
1164 #[test]
1165 fn test_aerodrome_detection() {
1166 let route = "N0450F100 LFPG DCT EGLL";
1167 let elements = Field15Parser::parse(route);
1168 assert_eq!(elements.len(), 4);
1169 assert_eq!(
1170 elements,
1171 vec![
1172 Field15Element::Modifier(Modifier {
1173 speed: Some(Speed::Knots(450)),
1174 altitude: Some(Altitude::FlightLevel(100)),
1175 cruise_climb: false,
1176 altitude_cruise_to: None
1177 }),
1178 Field15Element::Point(Point::Aerodrome("LFPG".to_string())),
1179 Field15Element::Connector(Connector::Direct),
1180 Field15Element::Point(Point::Aerodrome("EGLL".to_string())),
1181 ]
1182 );
1183 }
1184
1185 #[test]
1186 fn test_truncate_indicator() {
1187 let route = "N0450F100 POINT DCT POINT2 T";
1188 let elements = Field15Parser::parse(route);
1189 assert_eq!(
1190 elements,
1191 vec![
1192 Field15Element::Modifier(Modifier {
1193 speed: Some(Speed::Knots(450)),
1194 altitude: Some(Altitude::FlightLevel(100)),
1195 cruise_climb: false,
1196 altitude_cruise_to: None
1197 }),
1198 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1199 Field15Element::Connector(Connector::Direct),
1200 Field15Element::Point(Point::Waypoint("POINT2".to_string())),
1201 ]
1202 );
1203 }
1204
1205 #[test]
1206 fn test_literal_sid_star() {
1207 let route = "N0450F100 SID POINT DCT POINT2 STAR";
1208 let elements = Field15Parser::parse(route);
1209
1210 assert_eq!(
1211 elements,
1212 vec![
1213 Field15Element::Modifier(Modifier {
1214 speed: Some(Speed::Knots(450)),
1215 altitude: Some(Altitude::FlightLevel(100)),
1216 cruise_climb: false,
1217 altitude_cruise_to: None
1218 }),
1219 Field15Element::Connector(Connector::Sid("SID".to_string())),
1220 Field15Element::Point(Point::Waypoint("POINT".to_string())),
1221 Field15Element::Connector(Connector::Direct),
1222 Field15Element::Point(Point::Waypoint("POINT2".to_string())),
1223 Field15Element::Connector(Connector::Star("STAR".to_string())),
1224 ]
1225 );
1226 }
1227
1228 #[test]
1229 fn test_aerodrome_no_digits() {
1230 let route = "N0450F100 LFPG DCT EGLL";
1231 let elements = Field15Parser::parse(route);
1232
1233 assert_eq!(
1234 elements,
1235 vec![
1236 Field15Element::Modifier(Modifier {
1237 speed: Some(Speed::Knots(450)),
1238 altitude: Some(Altitude::FlightLevel(100)),
1239 cruise_climb: false,
1240 altitude_cruise_to: None
1241 }),
1242 Field15Element::Point(Point::Aerodrome("LFPG".to_string())),
1243 Field15Element::Connector(Connector::Direct),
1244 Field15Element::Point(Point::Aerodrome("EGLL".to_string())),
1245 ]
1246 );
1247 }
1248
1249 #[test]
1250 fn test_star_must_be_last() {
1251 let route = "N0450F100 POINT1A POINT DCT POINT";
1253 let elements = Field15Parser::parse(route);
1254
1255 assert!(!elements
1257 .iter()
1258 .any(|e| { matches!(e, Field15Element::Connector(Connector::Star(s)) if s == "POINT1A") }));
1259 }
1260
1261 #[test]
1262 fn test_single_char_point() {
1263 let route = "N0450F100 POINT DCT C DCT POINT";
1265 let elements = Field15Parser::parse(route);
1266
1267 assert!(elements
1269 .iter()
1270 .any(|e| { matches!(e, Field15Element::Point(Point::Waypoint(s)) if s == "C") }));
1271 }
1272
1273 #[test]
1274 fn test_readme_tokenization() {
1275 let route = "N0450M0825\n00N000E\tB9 00N001E/VFR";
1277 let elements = Field15Parser::parse(route);
1278
1279 assert!(elements.len() >= 5);
1280 assert!(elements
1281 .iter()
1282 .any(|e| matches!(e, Field15Element::Connector(Connector::Vfr))));
1283 }
1284
1285 #[test]
1286 fn test_bearing_distance_validation() {
1287 let route = "N0450F100 POINT999999";
1289 let elements = Field15Parser::parse(route);
1290
1291 assert!(!elements
1293 .iter()
1294 .any(|e| { matches!(e, Field15Element::Point(Point::BearingDistance { bearing, .. }) if *bearing > 360) }));
1295 }
1296
1297 #[test]
1298 fn test_route_with_speed_changes() {
1299 let route = "N0495F320 RANUX3D RANUX UN858 VALEK/N0491F330 UM163 DIK UN853 ARCKY DCT NVO DCT BERIM DCT BIKRU/N0482F350 DCT VEDEN";
1300 let elements = Field15Parser::parse(route);
1301
1302 assert_eq!(elements.len(), 19);
1303 assert_eq!(
1304 elements,
1305 vec![
1306 Field15Element::Modifier(Modifier {
1307 speed: Some(Speed::Knots(495)),
1308 altitude: Some(Altitude::FlightLevel(320)),
1309 cruise_climb: false,
1310 altitude_cruise_to: None
1311 }),
1312 Field15Element::Connector(Connector::Sid("RANUX3D".to_string())),
1313 Field15Element::Point(Point::Waypoint("RANUX".to_string())),
1314 Field15Element::Connector(Connector::Airway("UN858".to_string())),
1315 Field15Element::Point(Point::Waypoint("VALEK".to_string())),
1316 Field15Element::Modifier(Modifier {
1317 speed: Some(Speed::Knots(491)),
1318 altitude: Some(Altitude::FlightLevel(330)),
1319 cruise_climb: false,
1320 altitude_cruise_to: None
1321 }),
1322 Field15Element::Connector(Connector::Airway("UM163".to_string())),
1323 Field15Element::Point(Point::Waypoint("DIK".to_string())),
1324 Field15Element::Connector(Connector::Airway("UN853".to_string())),
1325 Field15Element::Point(Point::Waypoint("ARCKY".to_string())),
1326 Field15Element::Connector(Connector::Direct),
1327 Field15Element::Point(Point::Waypoint("NVO".to_string())),
1328 Field15Element::Connector(Connector::Direct),
1329 Field15Element::Point(Point::Waypoint("BERIM".to_string())),
1330 Field15Element::Connector(Connector::Direct),
1331 Field15Element::Point(Point::Waypoint("BIKRU".to_string())),
1332 Field15Element::Modifier(Modifier {
1333 speed: Some(Speed::Knots(482)),
1334 altitude: Some(Altitude::FlightLevel(350)),
1335 cruise_climb: false,
1336 altitude_cruise_to: None
1337 }),
1338 Field15Element::Connector(Connector::Direct),
1339 Field15Element::Point(Point::Waypoint("VEDEN".to_string())),
1340 ]
1341 );
1342 }
1343
1344 #[test]
1345 fn test_route_with_coordinates() {
1346 let route = "N0458F320 BERGI UL602 SUPUR UP1 GODOS P1 ROLUM P13 ASKAM L7 SUM DCT PEMOS/M079F320 DCT 62N010W 63N020W 63N030W 64N040W 64N050W";
1347 let elements = Field15Parser::parse(route);
1348 assert_eq!(
1349 elements,
1350 vec![
1351 Field15Element::Modifier(Modifier {
1352 speed: Some(Speed::Knots(458)),
1353 altitude: Some(Altitude::FlightLevel(320)),
1354 cruise_climb: false,
1355 altitude_cruise_to: None
1356 }),
1357 Field15Element::Point(Point::Waypoint("BERGI".to_string())),
1358 Field15Element::Connector(Connector::Airway("UL602".to_string())),
1359 Field15Element::Point(Point::Waypoint("SUPUR".to_string())),
1360 Field15Element::Connector(Connector::Airway("UP1".to_string())),
1361 Field15Element::Point(Point::Waypoint("GODOS".to_string())),
1362 Field15Element::Connector(Connector::Airway("P1".to_string())),
1363 Field15Element::Point(Point::Waypoint("ROLUM".to_string())),
1364 Field15Element::Connector(Connector::Airway("P13".to_string())),
1365 Field15Element::Point(Point::Waypoint("ASKAM".to_string())),
1366 Field15Element::Connector(Connector::Airway("L7".to_string())),
1367 Field15Element::Point(Point::Waypoint("SUM".to_string())),
1368 Field15Element::Connector(Connector::Direct),
1369 Field15Element::Point(Point::Waypoint("PEMOS".to_string())),
1370 Field15Element::Modifier(Modifier {
1371 speed: Some(Speed::Mach(0.79)),
1372 altitude: Some(Altitude::FlightLevel(320)),
1373 cruise_climb: false,
1374 altitude_cruise_to: None
1375 }),
1376 Field15Element::Connector(Connector::Direct),
1377 Field15Element::Point(Point::Coordinates((62., -10.))),
1378 Field15Element::Point(Point::Coordinates((63., -20.))),
1379 Field15Element::Point(Point::Coordinates((63., -30.))),
1380 Field15Element::Point(Point::Coordinates((64., -40.))),
1381 Field15Element::Point(Point::Coordinates((64., -50.))),
1382 ]
1383 );
1384 }
1385
1386 #[test]
1387 fn test_route_with_modifiers() {
1388 let route = "N0427F230 DET1J DET L6 DVR L9 KONAN/N0470F350 UL607 MATUG";
1389 let elements = Field15Parser::parse(route);
1390
1391 assert_eq!(
1392 elements,
1393 vec![
1394 Field15Element::Modifier(Modifier {
1395 speed: Some(Speed::Knots(427)),
1396 altitude: Some(Altitude::FlightLevel(230)),
1397 cruise_climb: false,
1398 altitude_cruise_to: None
1399 }),
1400 Field15Element::Connector(Connector::Sid("DET1J".to_string())),
1401 Field15Element::Point(Point::Waypoint("DET".to_string())),
1402 Field15Element::Connector(Connector::Airway("L6".to_string())),
1403 Field15Element::Point(Point::Waypoint("DVR".to_string())),
1404 Field15Element::Connector(Connector::Airway("L9".to_string())),
1405 Field15Element::Point(Point::Waypoint("KONAN".to_string())),
1406 Field15Element::Modifier(Modifier {
1407 speed: Some(Speed::Knots(470)),
1408 altitude: Some(Altitude::FlightLevel(350)),
1409 cruise_climb: false,
1410 altitude_cruise_to: None
1411 }),
1412 Field15Element::Connector(Connector::Airway("UL607".to_string())),
1413 Field15Element::Point(Point::Waypoint("MATUG".to_string())),
1414 ]
1415 );
1416 }
1417
1418 #[test]
1419 fn test_multiple_airways() {
1420 let route =
1421 "N0463F350 ERIXU3B ERIXU UN860 ETAMO UZ271 ADEKA UT18 AMLIR/N0461F370 UT18 BADAM UZ151 FJR UM731 DIVKO";
1422 let elements = Field15Parser::parse(route);
1423
1424 assert_eq!(
1425 elements,
1426 vec![
1427 Field15Element::Modifier(Modifier {
1428 speed: Some(Speed::Knots(463)),
1429 altitude: Some(Altitude::FlightLevel(350)),
1430 cruise_climb: false,
1431 altitude_cruise_to: None
1432 }),
1433 Field15Element::Connector(Connector::Sid("ERIXU3B".to_string())),
1434 Field15Element::Point(Point::Waypoint("ERIXU".to_string())),
1435 Field15Element::Connector(Connector::Airway("UN860".to_string())),
1436 Field15Element::Point(Point::Waypoint("ETAMO".to_string())),
1437 Field15Element::Connector(Connector::Airway("UZ271".to_string())),
1438 Field15Element::Point(Point::Waypoint("ADEKA".to_string())),
1439 Field15Element::Connector(Connector::Airway("UT18".to_string())),
1440 Field15Element::Point(Point::Waypoint("AMLIR".to_string())),
1441 Field15Element::Modifier(Modifier {
1442 speed: Some(Speed::Knots(461)),
1443 altitude: Some(Altitude::FlightLevel(370)),
1444 cruise_climb: false,
1445 altitude_cruise_to: None
1446 }),
1447 Field15Element::Connector(Connector::Airway("UT18".to_string())),
1448 Field15Element::Point(Point::Waypoint("BADAM".to_string())),
1449 Field15Element::Connector(Connector::Airway("UZ151".to_string())),
1450 Field15Element::Point(Point::Waypoint("FJR".to_string())),
1451 Field15Element::Connector(Connector::Airway("UM731".to_string())),
1452 Field15Element::Point(Point::Waypoint("DIVKO".to_string())),
1453 ]
1454 );
1455 }
1456
1457 #[test]
1458 fn test_long_complex_route() {
1459 let route = "N0459F320 OBOKA UZ29 TORNU DCT RAVLO Y70 OTBED L60 PENIL M144 BAGSO DCT RINUS DCT GISTI/M079F330 DCT MALOT/M079F340 DCT 54N020W 55N030W 54N040W 51N050W DCT ALLRY/N0463F360 DCT YQX";
1460 let elements = Field15Parser::parse(route);
1461 assert_eq!(
1462 elements,
1463 vec![
1464 Field15Element::Modifier(Modifier {
1465 speed: Some(Speed::Knots(459)),
1466 altitude: Some(Altitude::FlightLevel(320)),
1467 cruise_climb: false,
1468 altitude_cruise_to: None
1469 }),
1470 Field15Element::Point(Point::Waypoint("OBOKA".to_string())),
1471 Field15Element::Connector(Connector::Airway("UZ29".to_string())),
1472 Field15Element::Point(Point::Waypoint("TORNU".to_string())),
1473 Field15Element::Connector(Connector::Direct),
1474 Field15Element::Point(Point::Waypoint("RAVLO".to_string())),
1475 Field15Element::Connector(Connector::Airway("Y70".to_string())),
1476 Field15Element::Point(Point::Waypoint("OTBED".to_string())),
1477 Field15Element::Connector(Connector::Airway("L60".to_string())),
1478 Field15Element::Point(Point::Waypoint("PENIL".to_string())),
1479 Field15Element::Connector(Connector::Airway("M144".to_string())),
1480 Field15Element::Point(Point::Waypoint("BAGSO".to_string())),
1481 Field15Element::Connector(Connector::Direct),
1482 Field15Element::Point(Point::Waypoint("RINUS".to_string())),
1483 Field15Element::Connector(Connector::Direct),
1484 Field15Element::Point(Point::Waypoint("GISTI".to_string())),
1485 Field15Element::Modifier(Modifier {
1486 speed: Some(Speed::Mach(0.79)),
1487 altitude: Some(Altitude::FlightLevel(330)),
1488 cruise_climb: false,
1489 altitude_cruise_to: None
1490 }),
1491 Field15Element::Connector(Connector::Direct),
1492 Field15Element::Point(Point::Waypoint("MALOT".to_string())),
1493 Field15Element::Modifier(Modifier {
1494 speed: Some(Speed::Mach(0.79)),
1495 altitude: Some(Altitude::FlightLevel(340)),
1496 cruise_climb: false,
1497 altitude_cruise_to: None
1498 }),
1499 Field15Element::Connector(Connector::Direct),
1500 Field15Element::Point(Point::Coordinates((54., -20.))),
1502 Field15Element::Point(Point::Coordinates((55., -30.))),
1503 Field15Element::Point(Point::Coordinates((54., -40.))),
1504 Field15Element::Point(Point::Coordinates((51., -50.))),
1505 Field15Element::Connector(Connector::Direct),
1506 Field15Element::Point(Point::Waypoint("ALLRY".to_string())),
1507 Field15Element::Modifier(Modifier {
1508 speed: Some(Speed::Knots(463)),
1509 altitude: Some(Altitude::FlightLevel(360)),
1510 cruise_climb: false,
1511 altitude_cruise_to: None
1512 }),
1513 Field15Element::Connector(Connector::Direct),
1514 Field15Element::Point(Point::Waypoint("YQX".to_string())),
1515 ]
1516 );
1517 }
1518
1519 #[test]
1520 fn test_complex_route_for_tokenization() {
1521 let route = "N0450M0825 00N000E B9 00N001E VFR IFR 00N001W/N0350F100 01N001W 01S001W 02S001W180060";
1523 let elements = Field15Parser::parse(route);
1524 assert_eq!(
1525 elements,
1526 vec![
1527 Field15Element::Modifier(Modifier {
1528 speed: Some(Speed::Knots(450)),
1529 altitude: Some(Altitude::MetricAltitude(825)),
1530 cruise_climb: false,
1531 altitude_cruise_to: None
1532 }),
1533 Field15Element::Point(Point::Coordinates((0., 0.))),
1534 Field15Element::Connector(Connector::Airway("B9".to_string())),
1535 Field15Element::Point(Point::Coordinates((0., 1.))),
1536 Field15Element::Connector(Connector::Vfr),
1537 Field15Element::Connector(Connector::Ifr),
1538 Field15Element::Point(Point::Coordinates((0., -1.))),
1539 Field15Element::Modifier(Modifier {
1540 speed: Some(Speed::Knots(350)),
1541 altitude: Some(Altitude::FlightLevel(100)),
1542 cruise_climb: false,
1543 altitude_cruise_to: None
1544 }),
1545 Field15Element::Point(Point::Coordinates((1., -1.))),
1546 Field15Element::Point(Point::Coordinates((-1., -1.))),
1547 Field15Element::Point(Point::BearingDistance {
1548 point: Box::new(Point::Coordinates((-2., -1.))),
1549 bearing: 180,
1550 distance: 60,
1551 }),
1552 ]
1553 );
1554 }
1555
1556 #[test]
1557 fn test_nat_track_is_nat() {
1558 assert!(Field15Parser::is_nat("NATD"));
1560 assert!(Field15Parser::is_nat("NATA"));
1561 assert!(Field15Parser::is_nat("NATZ"));
1562 assert!(Field15Parser::is_nat("NATZ1"));
1563 assert!(!Field15Parser::is_nat("NAT1")); assert!(!Field15Parser::is_nat("NAT")); }
1566
1567 #[test]
1568 fn test_nat_track_in_route() {
1569 let route = "N0490F360 ELCOB6B ELCOB UT300 SENLO UN502 JSY DCT LIZAD DCT MOPAT DCT LUNIG DCT MOMIN DCT PIKIL/M084F380 NATD HOIST/N0490F380 N756C ANATI/N0441F340 DCT MIVAX DCT OBTEK DCT XORLO ROCKT2";
1570 let elements = Field15Parser::parse(route);
1571
1572 assert_eq!(
1573 elements,
1574 vec![
1575 Field15Element::Modifier(Modifier {
1576 speed: Some(Speed::Knots(490)),
1577 altitude: Some(Altitude::FlightLevel(360)),
1578 cruise_climb: false,
1579 altitude_cruise_to: None
1580 }),
1581 Field15Element::Connector(Connector::Sid("ELCOB6B".to_string())),
1582 Field15Element::Point(Point::Waypoint("ELCOB".to_string())),
1583 Field15Element::Connector(Connector::Airway("UT300".to_string())),
1584 Field15Element::Point(Point::Waypoint("SENLO".to_string())),
1585 Field15Element::Connector(Connector::Airway("UN502".to_string())),
1586 Field15Element::Point(Point::Waypoint("JSY".to_string())),
1587 Field15Element::Connector(Connector::Direct),
1588 Field15Element::Point(Point::Waypoint("LIZAD".to_string())),
1589 Field15Element::Connector(Connector::Direct),
1590 Field15Element::Point(Point::Waypoint("MOPAT".to_string())),
1591 Field15Element::Connector(Connector::Direct),
1592 Field15Element::Point(Point::Waypoint("LUNIG".to_string())),
1593 Field15Element::Connector(Connector::Direct),
1594 Field15Element::Point(Point::Waypoint("MOMIN".to_string())),
1595 Field15Element::Connector(Connector::Direct),
1596 Field15Element::Point(Point::Waypoint("PIKIL".to_string())),
1597 Field15Element::Modifier(Modifier {
1598 speed: Some(Speed::Mach(0.84)),
1599 altitude: Some(Altitude::FlightLevel(380)),
1600 cruise_climb: false,
1601 altitude_cruise_to: None
1602 }),
1603 Field15Element::Connector(Connector::Nat("NATD".to_string())),
1604 Field15Element::Point(Point::Waypoint("HOIST".to_string())),
1605 Field15Element::Modifier(Modifier {
1606 speed: Some(Speed::Knots(490)),
1607 altitude: Some(Altitude::FlightLevel(380)),
1608 cruise_climb: false,
1609 altitude_cruise_to: None
1610 }),
1611 Field15Element::Connector(Connector::Airway("N756C".to_string())),
1612 Field15Element::Point(Point::Waypoint("ANATI".to_string())),
1613 Field15Element::Modifier(Modifier {
1614 speed: Some(Speed::Knots(441)),
1615 altitude: Some(Altitude::FlightLevel(340)),
1616 cruise_climb: false,
1617 altitude_cruise_to: None
1618 }),
1619 Field15Element::Connector(Connector::Direct),
1620 Field15Element::Point(Point::Waypoint("MIVAX".to_string())),
1621 Field15Element::Connector(Connector::Direct),
1622 Field15Element::Point(Point::Waypoint("OBTEK".to_string())),
1623 Field15Element::Connector(Connector::Direct),
1624 Field15Element::Point(Point::Waypoint("XORLO".to_string())),
1625 Field15Element::Connector(Connector::Star("ROCKT2".to_string())),
1626 ]
1627 );
1628 }
1629}