1use std::collections::HashMap;
2
3use wasm_bindgen::prelude::*;
4
5use thrust::data::faa::nasr::parse_resolver_data_from_nasr_bytes;
6
7use crate::models::{
8 normalize_airway_name, AirportRecord, AirspaceCompositeRecord, AirspaceLayerRecord, AirspaceRecord, AirwayRecord,
9 NavpointRecord, ProcedureRecord,
10};
11
12fn compose_airspace(records: Vec<AirspaceRecord>) -> Option<AirspaceCompositeRecord> {
13 let first = records.first()?;
14 let designator = first.designator.clone();
15 let source = first.source.clone();
16 let name = records.iter().find_map(|r| r.name.clone());
17 let type_ = records.iter().find_map(|r| r.type_.clone());
18 let layers = records
19 .into_iter()
20 .map(|r| AirspaceLayerRecord {
21 lower: r.lower,
22 upper: r.upper,
23 coordinates: r.coordinates,
24 })
25 .collect();
26
27 Some(AirspaceCompositeRecord {
28 designator,
29 name,
30 type_,
31 layers,
32 source,
33 })
34}
35
36#[wasm_bindgen]
37pub struct NasrResolver {
38 airports: Vec<AirportRecord>,
39 navaids: Vec<NavpointRecord>,
40 airways: Vec<AirwayRecord>,
41 procedures: Vec<ProcedureRecord>,
42 airspaces: Vec<AirspaceRecord>,
43 airport_index: HashMap<String, Vec<usize>>,
44 navaid_index: HashMap<String, Vec<usize>>,
45 airway_index: HashMap<String, Vec<usize>>,
46 sid_index: HashMap<String, Vec<usize>>,
47 star_index: HashMap<String, Vec<usize>>,
48 airspace_index: HashMap<String, Vec<usize>>,
49}
50
51#[wasm_bindgen]
52impl NasrResolver {
53 fn resolve_procedure_by_kind(&self, kind: &str, name: &str) -> Option<ProcedureRecord> {
54 let key = name.to_uppercase();
55 let idx = match kind {
56 "SID" => self.sid_index.get(&key).and_then(|v| v.first()).copied(),
57 "STAR" => self.star_index.get(&key).and_then(|v| v.first()).copied(),
58 _ => None,
59 }?;
60 self.procedures.get(idx).cloned()
61 }
62
63 fn enrich_route_segments_internal(&self, route: &str) -> Vec<crate::field15::RouteSegment> {
64 use crate::field15::ResolvedPoint as WasmPoint;
65 use crate::field15::RouteSegment;
66 use thrust::data::field15::{Connector, Field15Element, Field15Parser, Point};
67
68 let elements = Field15Parser::parse(route);
69 let mut segments: Vec<RouteSegment> = Vec::new();
70 let mut last_point: Option<WasmPoint> = None;
71 let mut pending_airway: Option<(String, WasmPoint)> = None;
72 let mut pending_procedure: Option<String> = None;
73 let mut current_connector: Option<String> = None;
74 let mut current_segment_type: Option<String> = None;
75
76 let resolve_code = |code: &str| -> Option<WasmPoint> {
77 let key = code.split('/').next().unwrap_or(code).trim().to_uppercase();
78 if let Some(idx) = self.airport_index.get(&key).and_then(|v| v.first()) {
79 if let Some(a) = self.airports.get(*idx) {
80 return Some(WasmPoint {
81 latitude: a.latitude,
82 longitude: a.longitude,
83 name: Some(a.code.clone()),
84 kind: Some("airport".to_string()),
85 });
86 }
87 }
88 if let Some(idx) = self.navaid_index.get(&key).and_then(|v| v.first()) {
89 if let Some(n) = self.navaids.get(*idx) {
90 return Some(WasmPoint {
91 latitude: n.latitude,
92 longitude: n.longitude,
93 name: Some(n.code.clone()),
94 kind: Some(n.kind.clone()),
95 });
96 }
97 }
98 None
99 };
100
101 let expand_airway =
102 |airway_name: &str, entry: &WasmPoint, exit: &WasmPoint, segs: &mut Vec<RouteSegment>| -> bool {
103 let key = crate::models::normalize_airway_name(airway_name);
104 let airway = match self
105 .airway_index
106 .get(&key)
107 .and_then(|v| v.first())
108 .and_then(|i| self.airways.get(*i))
109 {
110 Some(a) => a,
111 None => return false,
112 };
113 let pts = &airway.points;
114 let entry_name = entry.name.as_deref().unwrap_or("").to_uppercase();
115 let exit_name = exit.name.as_deref().unwrap_or("").to_uppercase();
116 let entry_pos = pts.iter().position(|p| p.code.to_uppercase() == entry_name);
117 let exit_pos = pts.iter().position(|p| p.code.to_uppercase() == exit_name);
118 let (from, to) = match (entry_pos, exit_pos) {
119 (Some(f), Some(t)) => (f, t),
120 _ => return false,
121 };
122 let slice: Vec<&crate::models::AirwayPointRecord> = if from <= to {
123 pts[from..=to].iter().collect()
124 } else {
125 pts[to..=from].iter().rev().collect()
126 };
127 if slice.len() < 2 {
128 return false;
129 }
130 let mut prev = entry.clone();
131 for pt in &slice[1..] {
132 let next = WasmPoint {
133 latitude: pt.latitude,
134 longitude: pt.longitude,
135 name: Some(pt.code.clone()),
136 kind: Some(pt.kind.clone()),
137 };
138 segs.push(RouteSegment {
139 start: prev,
140 end: next.clone(),
141 name: Some(airway_name.to_string()),
142 segment_type: Some("route".to_string()),
143 connector: Some(airway_name.to_string()),
144 });
145 prev = next;
146 }
147 true
148 };
149
150 let resolve_procedure = |kind: &str, name: &str| -> Option<&ProcedureRecord> {
151 let key = name.to_uppercase();
152 let idx = match kind {
153 "SID" => self.sid_index.get(&key).and_then(|v| v.first()).copied(),
154 "STAR" => self.star_index.get(&key).and_then(|v| v.first()).copied(),
155 _ => None,
156 }?;
157 self.procedures.get(idx)
158 };
159
160 let expand_procedure_from_entry =
161 |procedure_name: &str, kind: &str, entry: &WasmPoint, segs: &mut Vec<RouteSegment>| -> Option<WasmPoint> {
162 let proc = resolve_procedure(kind, procedure_name)?;
163 let pts = &proc.points;
164 if pts.len() < 2 {
165 return None;
166 }
167 let entry_name = entry.name.as_deref().unwrap_or("").to_uppercase();
168 let start_idx = pts.iter().position(|p| p.code.to_uppercase() == entry_name)?;
169 if start_idx >= pts.len() - 1 {
170 return None;
171 }
172
173 let mut prev = entry.clone();
174 for pt in &pts[start_idx + 1..] {
175 let next = WasmPoint {
176 latitude: pt.latitude,
177 longitude: pt.longitude,
178 name: Some(pt.code.clone()),
179 kind: Some(pt.kind.clone()),
180 };
181 segs.push(RouteSegment {
182 start: prev,
183 end: next.clone(),
184 name: Some(procedure_name.to_string()),
185 segment_type: Some(kind.to_string()),
186 connector: Some(procedure_name.to_string()),
187 });
188 prev = next;
189 }
190 Some(prev)
191 };
192
193 for element in &elements {
194 match element {
195 Field15Element::Point(point) => {
196 let resolved = match point {
197 Point::Waypoint(name) | Point::Aerodrome(name) => resolve_code(name),
198 Point::Coordinates((lat, lon)) => Some(WasmPoint {
199 latitude: *lat,
200 longitude: *lon,
201 name: None,
202 kind: Some("coords".to_string()),
203 }),
204 Point::BearingDistance { point, .. } => match point.as_ref() {
205 Point::Waypoint(name) | Point::Aerodrome(name) => resolve_code(name),
206 Point::Coordinates((lat, lon)) => Some(WasmPoint {
207 latitude: *lat,
208 longitude: *lon,
209 name: None,
210 kind: Some("coords".to_string()),
211 }),
212 _ => None,
213 },
214 };
215 if let Some(exit) = resolved {
216 if let Some((airway_name, entry)) = pending_airway.take() {
217 let expanded = expand_airway(&airway_name, &entry, &exit, &mut segments);
218 if !expanded {
219 segments.push(RouteSegment {
220 start: entry,
221 end: exit.clone(),
222 name: Some(airway_name.clone()),
223 segment_type: Some("unresolved".to_string()),
224 connector: Some(airway_name),
225 });
226 }
227 } else if let Some(procedure_name) = pending_procedure.take() {
228 if let Some(prev) = last_point.take() {
229 segments.push(RouteSegment {
230 start: prev,
231 end: exit.clone(),
232 name: Some(procedure_name.clone()),
233 segment_type: Some("unresolved".to_string()),
234 connector: Some(procedure_name),
235 });
236 }
237 } else if let Some(prev) = last_point.take() {
238 let seg_name = current_connector.take();
239 let seg_type = current_segment_type.take();
240 let seg_connector = if seg_type.as_deref() == Some("dct") {
241 Some("DCT".to_string())
242 } else {
243 seg_name.clone()
244 };
245 segments.push(RouteSegment {
246 start: prev,
247 end: exit.clone(),
248 name: seg_name,
249 segment_type: seg_type,
250 connector: seg_connector,
251 });
252 } else {
253 current_connector = None;
254 current_segment_type = None;
255 }
256 last_point = Some(exit);
257 }
258 }
259 Field15Element::Connector(connector) => match connector {
260 Connector::Airway(name) => {
261 if let Some(entry) = last_point.take() {
262 pending_airway = Some((name.clone(), entry));
263 current_segment_type = None;
264 } else {
265 current_connector = Some(name.clone());
266 current_segment_type = Some("unresolved".to_string());
267 }
268 }
269 Connector::Direct => {
270 current_connector = None;
271 pending_procedure = None;
272 current_segment_type = Some("dct".to_string());
273 }
274 Connector::Sid(name) => {
275 if let Some(entry) = last_point.clone() {
276 if let Some(end) = expand_procedure_from_entry(name, "SID", &entry, &mut segments) {
277 last_point = Some(end);
278 current_connector = None;
279 pending_airway = None;
280 pending_procedure = None;
281 current_segment_type = None;
282 } else {
283 current_connector = Some(name.clone());
284 pending_procedure = Some(name.clone());
285 current_segment_type = Some("unresolved".to_string());
286 }
287 } else {
288 current_connector = Some(name.clone());
289 pending_procedure = Some(name.clone());
290 current_segment_type = Some("unresolved".to_string());
291 }
292 }
293 Connector::Star(name) => {
294 if let Some(entry) = last_point.clone() {
295 if let Some(end) = expand_procedure_from_entry(name, "STAR", &entry, &mut segments) {
296 last_point = Some(end);
297 current_connector = None;
298 pending_airway = None;
299 pending_procedure = None;
300 current_segment_type = None;
301 } else {
302 current_connector = Some(name.clone());
303 pending_procedure = Some(name.clone());
304 current_segment_type = Some("unresolved".to_string());
305 }
306 } else {
307 current_connector = Some(name.clone());
308 pending_procedure = Some(name.clone());
309 current_segment_type = Some("unresolved".to_string());
310 }
311 }
312 Connector::Nat(name) => {
313 current_connector = Some(name.clone());
314 pending_procedure = None;
315 current_segment_type = Some("NAT".to_string());
316 }
317 Connector::Pts(name) => {
318 current_connector = Some(name.clone());
319 pending_procedure = None;
320 current_segment_type = Some("PTS".to_string());
321 }
322 _ => {}
323 },
324 Field15Element::Modifier(_) => {}
325 }
326 }
327
328 segments
329 }
330
331 #[wasm_bindgen(constructor)]
332 pub fn new(zip_bytes: &[u8]) -> Result<NasrResolver, JsValue> {
333 let dataset = parse_resolver_data_from_nasr_bytes(zip_bytes).map_err(|e| JsValue::from_str(&e.to_string()))?;
334 let airports: Vec<AirportRecord> = dataset.airports.into_iter().map(Into::into).collect();
335 let navaids: Vec<NavpointRecord> = dataset.navaids.into_iter().map(Into::into).collect();
336 let airways: Vec<AirwayRecord> = dataset.airways.into_iter().map(Into::into).collect();
337 let procedures: Vec<ProcedureRecord> = dataset.procedures.into_iter().map(Into::into).collect();
338 let airspaces: Vec<AirspaceRecord> = dataset
339 .airspaces
340 .into_iter()
341 .map(|a| AirspaceRecord {
342 designator: a.designator,
343 name: a.name,
344 type_: a.type_,
345 lower: a.lower,
346 upper: a.upper,
347 coordinates: a.coordinates,
348 source: "faa_nasr".to_string(),
349 })
350 .collect();
351
352 let mut airport_index: HashMap<String, Vec<usize>> = HashMap::new();
353 for (i, a) in airports.iter().enumerate() {
354 airport_index.entry(a.code.clone()).or_default().push(i);
355 if let Some(v) = &a.iata {
356 airport_index.entry(v.clone()).or_default().push(i);
357 }
358 if let Some(v) = &a.icao {
359 airport_index.entry(v.clone()).or_default().push(i);
360 }
361 }
362
363 let mut navaid_index: HashMap<String, Vec<usize>> = HashMap::new();
364 for (i, n) in navaids.iter().enumerate() {
365 navaid_index.entry(n.code.clone()).or_default().push(i);
366 navaid_index.entry(n.identifier.clone()).or_default().push(i);
367 }
368
369 let mut airway_index: HashMap<String, Vec<usize>> = HashMap::new();
370 for (i, a) in airways.iter().enumerate() {
371 airway_index.entry(normalize_airway_name(&a.name)).or_default().push(i);
372 airway_index.entry(a.name.to_uppercase()).or_default().push(i);
373 }
374
375 let mut sid_index: HashMap<String, Vec<usize>> = HashMap::new();
376 let mut star_index: HashMap<String, Vec<usize>> = HashMap::new();
377 for (i, p) in procedures.iter().enumerate() {
378 let key = p.name.to_uppercase();
379 match p.procedure_kind.to_uppercase().as_str() {
380 "SID" => sid_index.entry(key).or_default().push(i),
381 "STAR" => star_index.entry(key).or_default().push(i),
382 _ => {}
383 }
384 }
385
386 let mut airspace_index: HashMap<String, Vec<usize>> = HashMap::new();
387 for (i, a) in airspaces.iter().enumerate() {
388 airspace_index.entry(a.designator.to_uppercase()).or_default().push(i);
389 }
390
391 Ok(Self {
392 airports,
393 navaids,
394 airways,
395 procedures,
396 airspaces,
397 airport_index,
398 navaid_index,
399 airway_index,
400 sid_index,
401 star_index,
402 airspace_index,
403 })
404 }
405
406 pub fn airports(&self) -> Result<JsValue, JsValue> {
407 serde_wasm_bindgen::to_value(&self.airports).map_err(|e| JsValue::from_str(&e.to_string()))
408 }
409
410 pub fn resolve_airport(&self, code: String) -> Result<JsValue, JsValue> {
411 let key = code.to_uppercase();
412 let item = self
413 .airport_index
414 .get(&key)
415 .and_then(|idx| idx.first().copied())
416 .and_then(|i| self.airports.get(i))
417 .cloned();
418
419 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
420 }
421
422 pub fn navaids(&self) -> Result<JsValue, JsValue> {
423 serde_wasm_bindgen::to_value(&self.navaids).map_err(|e| JsValue::from_str(&e.to_string()))
424 }
425
426 pub fn fixes(&self) -> Result<JsValue, JsValue> {
427 serde_wasm_bindgen::to_value(&self.navaids).map_err(|e| JsValue::from_str(&e.to_string()))
428 }
429
430 pub fn airways(&self) -> Result<JsValue, JsValue> {
431 serde_wasm_bindgen::to_value(&self.airways).map_err(|e| JsValue::from_str(&e.to_string()))
432 }
433
434 pub fn procedures(&self) -> Result<JsValue, JsValue> {
435 serde_wasm_bindgen::to_value(&self.procedures).map_err(|e| JsValue::from_str(&e.to_string()))
436 }
437
438 pub fn airspaces(&self) -> Result<JsValue, JsValue> {
439 let mut keys = self.airspace_index.keys().cloned().collect::<Vec<_>>();
440 keys.sort();
441 let rows = keys
442 .into_iter()
443 .filter_map(|key| {
444 let records = self
445 .airspace_index
446 .get(&key)
447 .into_iter()
448 .flat_map(|indices| indices.iter().copied())
449 .filter_map(|idx| self.airspaces.get(idx).cloned())
450 .collect::<Vec<_>>();
451 compose_airspace(records)
452 })
453 .collect::<Vec<_>>();
454 serde_wasm_bindgen::to_value(&rows).map_err(|e| JsValue::from_str(&e.to_string()))
455 }
456
457 pub fn resolve_navaid(&self, code: String) -> Result<JsValue, JsValue> {
458 let key = code.to_uppercase();
459 let item = self
460 .navaid_index
461 .get(&key)
462 .and_then(|idx| idx.first().copied())
463 .and_then(|i| self.navaids.get(i))
464 .cloned();
465
466 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
467 }
468
469 pub fn resolve_fix(&self, code: String) -> Result<JsValue, JsValue> {
470 let key = code.to_uppercase();
471 let item = self
472 .navaid_index
473 .get(&key)
474 .and_then(|idx| idx.first().copied())
475 .and_then(|i| self.navaids.get(i))
476 .cloned();
477
478 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
479 }
480
481 pub fn resolve_airway(&self, name: String) -> Result<JsValue, JsValue> {
482 let key = normalize_airway_name(&name);
483 let item = self
484 .airway_index
485 .get(&key)
486 .and_then(|idx| idx.first().copied())
487 .and_then(|i| self.airways.get(i))
488 .cloned();
489
490 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
491 }
492
493 pub fn resolve_sid(&self, name: String) -> Result<JsValue, JsValue> {
494 let item = self.resolve_procedure_by_kind("SID", &name);
495
496 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
497 }
498
499 pub fn resolve_star(&self, name: String) -> Result<JsValue, JsValue> {
500 let item = self.resolve_procedure_by_kind("STAR", &name);
501
502 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
503 }
504
505 pub fn resolve_airspace(&self, designator: String) -> Result<JsValue, JsValue> {
506 let key = designator.to_uppercase();
507 let records = self
508 .airspace_index
509 .get(&key)
510 .into_iter()
511 .flat_map(|indices| indices.iter().copied())
512 .filter_map(|idx| self.airspaces.get(idx).cloned())
513 .collect::<Vec<_>>();
514
515 serde_wasm_bindgen::to_value(&compose_airspace(records)).map_err(|e| JsValue::from_str(&e.to_string()))
516 }
517
518 #[wasm_bindgen(js_name = enrichRoute)]
523 pub fn enrich_route(&self, route: String) -> Result<JsValue, JsValue> {
524 let segments = self.enrich_route_segments_internal(&route);
525 serde_wasm_bindgen::to_value(&segments).map_err(|e| JsValue::from_str(&e.to_string()))
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use crate::field15::RouteSegment;
533 use crate::models::AirwayPointRecord;
534
535 fn sample_resolver() -> NasrResolver {
536 let airports = vec![AirportRecord {
537 code: "LFBO".to_string(),
538 iata: Some("TLS".to_string()),
539 icao: Some("LFBO".to_string()),
540 name: Some("Toulouse Blagnac".to_string()),
541 latitude: 43.6293,
542 longitude: 1.363,
543 region: None,
544 source: "faa_nasr".to_string(),
545 }];
546
547 let navaids = vec![
548 NavpointRecord {
549 code: "KEPER".to_string(),
550 identifier: "KEPER".to_string(),
551 kind: "fix".to_string(),
552 name: Some("KEPER".to_string()),
553 latitude: 44.0,
554 longitude: 2.0,
555 description: None,
556 frequency: None,
557 point_type: None,
558 region: None,
559 source: "faa_nasr".to_string(),
560 },
561 NavpointRecord {
562 code: "NIMER".to_string(),
563 identifier: "NIMER".to_string(),
564 kind: "fix".to_string(),
565 name: Some("NIMER".to_string()),
566 latitude: 44.5,
567 longitude: 2.2,
568 description: None,
569 frequency: None,
570 point_type: None,
571 region: None,
572 source: "faa_nasr".to_string(),
573 },
574 ];
575
576 let procedures = vec![ProcedureRecord {
577 name: "KEPER9E".to_string(),
578 source: "faa_nasr".to_string(),
579 procedure_kind: "STAR".to_string(),
580 route_class: Some("AP".to_string()),
581 airport: Some("LFBO".to_string()),
582 points: vec![
583 AirwayPointRecord {
584 code: "KEPER".to_string(),
585 raw_code: "KEPER".to_string(),
586 kind: "fix".to_string(),
587 latitude: 44.0,
588 longitude: 2.0,
589 },
590 AirwayPointRecord {
591 code: "NIMER".to_string(),
592 raw_code: "NIMER".to_string(),
593 kind: "fix".to_string(),
594 latitude: 44.5,
595 longitude: 2.2,
596 },
597 AirwayPointRecord {
598 code: "LFBO".to_string(),
599 raw_code: "LFBO".to_string(),
600 kind: "airport".to_string(),
601 latitude: 43.6293,
602 longitude: 1.363,
603 },
604 ],
605 }];
606
607 let mut airport_index = HashMap::new();
608 airport_index.insert("LFBO".to_string(), vec![0]);
609 airport_index.insert("TLS".to_string(), vec![0]);
610
611 let mut navaid_index = HashMap::new();
612 navaid_index.insert("KEPER".to_string(), vec![0]);
613 navaid_index.insert("NIMER".to_string(), vec![1]);
614
615 let mut sid_index = HashMap::new();
616 let mut star_index = HashMap::new();
617 sid_index.insert("FISTO5A".to_string(), vec![]);
618 star_index.insert("KEPER9E".to_string(), vec![0]);
619
620 NasrResolver {
621 airports,
622 navaids,
623 airways: Vec::new(),
624 procedures,
625 airspaces: Vec::new(),
626 airport_index,
627 navaid_index,
628 airway_index: HashMap::new(),
629 sid_index,
630 star_index,
631 airspace_index: HashMap::new(),
632 }
633 }
634
635 #[test]
636 fn resolve_star_returns_notebook_example_procedure() {
637 let resolver = sample_resolver();
638 let proc = resolver
639 .resolve_procedure_by_kind("STAR", "KEPER9E")
640 .expect("missing procedure");
641 assert_eq!(proc.name, "KEPER9E");
642 assert_eq!(proc.procedure_kind, "STAR");
643 assert_eq!(proc.route_class.as_deref(), Some("AP"));
644 assert_eq!(proc.points.first().map(|p| p.code.as_str()), Some("KEPER"));
645 }
646
647 #[test]
648 fn enrich_route_expands_terminal_star_segment() {
649 let resolver = sample_resolver();
650 let segments: Vec<RouteSegment> = resolver.enrich_route_segments_internal("N0430F300 KEPER KEPER9E");
651
652 assert_eq!(segments.len(), 2);
653 assert_eq!(segments[0].start.name.as_deref(), Some("KEPER"));
654 assert_eq!(segments[0].end.name.as_deref(), Some("NIMER"));
655 assert_eq!(segments[0].name.as_deref(), Some("KEPER9E"));
656 assert_eq!(segments[0].segment_type.as_deref(), Some("STAR"));
657 assert_eq!(segments[0].connector.as_deref(), Some("KEPER9E"));
658 assert_eq!(segments[1].start.name.as_deref(), Some("NIMER"));
659 assert_eq!(segments[1].end.name.as_deref(), Some("LFBO"));
660 assert_eq!(segments[1].name.as_deref(), Some("KEPER9E"));
661 assert_eq!(segments[1].segment_type.as_deref(), Some("STAR"));
662 assert_eq!(segments[1].connector.as_deref(), Some("KEPER9E"));
663 }
664
665 #[test]
666 fn enrich_route_nat_connector_is_preserved() {
667 let resolver = sample_resolver();
668 let segments: Vec<RouteSegment> = resolver.enrich_route_segments_internal("N0430F300 KEPER NATD NIMER");
669
670 assert_eq!(segments.len(), 1);
671 assert_eq!(segments[0].start.name.as_deref(), Some("KEPER"));
672 assert_eq!(segments[0].end.name.as_deref(), Some("NIMER"));
673 assert_eq!(segments[0].name.as_deref(), Some("NATD"));
674 assert_eq!(segments[0].segment_type.as_deref(), Some("NAT"));
675 assert_eq!(segments[0].connector.as_deref(), Some("NATD"));
676 }
677
678 #[test]
679 fn enrich_route_keeps_waypoint_with_slash_modifier_between_connectors() {
680 let resolver = sample_resolver();
681 let segments: Vec<RouteSegment> =
682 resolver.enrich_route_segments_internal("N0430F300 KEPER N756C NIMER/N0441F340 DCT LFBO");
683
684 assert_eq!(segments.len(), 2);
685 assert_eq!(segments[0].start.name.as_deref(), Some("KEPER"));
686 assert_eq!(segments[0].end.name.as_deref(), Some("NIMER"));
687 assert_eq!(segments[0].segment_type.as_deref(), Some("unresolved"));
688 assert_eq!(segments[0].connector.as_deref(), Some("N756C"));
689
690 assert_eq!(segments[1].start.name.as_deref(), Some("NIMER"));
691 assert_eq!(segments[1].end.name.as_deref(), Some("LFBO"));
692 assert_eq!(segments[1].segment_type.as_deref(), Some("dct"));
693 assert_eq!(segments[1].connector.as_deref(), Some("DCT"));
694 }
695}