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