1use std::collections::HashMap;
2
3use js_sys::Array;
4use serde_json::Value;
5use thrust::data::faa::arcgis::parse_arcgis_features;
6use wasm_bindgen::prelude::*;
7
8use crate::models::{
9 normalize_airway_name, AirportRecord, AirspaceCompositeRecord, AirspaceLayerRecord, AirspaceRecord, AirwayRecord,
10 NavpointRecord,
11};
12
13fn compose_airspace(records: Vec<AirspaceRecord>) -> Option<AirspaceCompositeRecord> {
14 let first = records.first()?;
15 let designator = first.designator.clone();
16 let source = first.source.clone();
17 let name = records.iter().find_map(|r| r.name.clone());
18 let type_ = records.iter().find_map(|r| r.type_.clone());
19 let layers = records
20 .into_iter()
21 .map(|r| AirspaceLayerRecord {
22 lower: r.lower,
23 upper: r.upper,
24 coordinates: r.coordinates,
25 })
26 .collect();
27
28 Some(AirspaceCompositeRecord {
29 designator,
30 name,
31 type_,
32 layers,
33 source,
34 })
35}
36
37#[wasm_bindgen]
38pub struct FaaArcgisResolver {
39 airports: Vec<AirportRecord>,
40 airspaces: Vec<AirspaceRecord>,
41 navaids: Vec<NavpointRecord>,
42 airways: Vec<AirwayRecord>,
43 airport_index: HashMap<String, Vec<usize>>,
44 airspace_index: HashMap<String, Vec<usize>>,
45 navaid_index: HashMap<String, Vec<usize>>,
46 airway_index: HashMap<String, Vec<usize>>,
47 sid_index: HashMap<String, Vec<usize>>,
48 star_index: HashMap<String, Vec<usize>>,
49}
50
51fn procedure_lookup_keys(name: &str) -> Vec<String> {
52 let upper = name.trim().to_uppercase();
53 if upper.is_empty() {
54 return Vec::new();
55 }
56 let mut out = vec![upper.clone()];
57 let compact = upper.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>();
58 if !compact.is_empty() {
59 out.push(compact.clone());
60 if compact.len() > 4 && compact[compact.len() - 4..].chars().all(|c| c.is_ascii_alphabetic()) {
61 out.push(compact[..compact.len() - 4].to_string());
62 }
63 }
64 out.sort();
65 out.dedup();
66 out
67}
68
69#[wasm_bindgen]
70impl FaaArcgisResolver {
71 #[wasm_bindgen(constructor)]
72 pub fn new(feature_collections_json: JsValue) -> Result<FaaArcgisResolver, JsValue> {
73 let payloads = Array::from(&feature_collections_json);
74 let mut features: Vec<Value> = Vec::new();
75 for payload in payloads.iter() {
76 let value: Value =
77 serde_wasm_bindgen::from_value(payload).map_err(|e| JsValue::from_str(&e.to_string()))?;
78 let arr = value
79 .get("features")
80 .and_then(|x| x.as_array())
81 .cloned()
82 .unwrap_or_default();
83 features.extend(arr);
84 }
85
86 let dataset = parse_arcgis_features(&features);
87 let airports: Vec<AirportRecord> = dataset.airports.into_iter().map(Into::into).collect();
88 let airspaces: Vec<AirspaceRecord> = dataset.airspaces.into_iter().map(Into::into).collect();
89 let navaids: Vec<NavpointRecord> = dataset.navaids.into_iter().map(Into::into).collect();
90 let airways: Vec<AirwayRecord> = dataset.airways.into_iter().map(Into::into).collect();
91
92 let mut airport_index: HashMap<String, Vec<usize>> = HashMap::new();
93 for (i, a) in airports.iter().enumerate() {
94 airport_index.entry(a.code.clone()).or_default().push(i);
95 if let Some(v) = &a.iata {
96 airport_index.entry(v.clone()).or_default().push(i);
97 }
98 if let Some(v) = &a.icao {
99 airport_index.entry(v.clone()).or_default().push(i);
100 }
101 }
102
103 let mut airspace_index: HashMap<String, Vec<usize>> = HashMap::new();
104 for (i, a) in airspaces.iter().enumerate() {
105 airspace_index.entry(a.designator.to_uppercase()).or_default().push(i);
106 }
107
108 let mut navaid_index: HashMap<String, Vec<usize>> = HashMap::new();
109 for (i, n) in navaids.iter().enumerate() {
110 navaid_index.entry(n.code.clone()).or_default().push(i);
111 }
112
113 let mut airway_index: HashMap<String, Vec<usize>> = HashMap::new();
114 let mut sid_index: HashMap<String, Vec<usize>> = HashMap::new();
115 let mut star_index: HashMap<String, Vec<usize>> = HashMap::new();
116 for (i, a) in airways.iter().enumerate() {
117 airway_index.entry(normalize_airway_name(&a.name)).or_default().push(i);
118 airway_index.entry(a.name.to_uppercase()).or_default().push(i);
119 match a.route_class.as_deref().map(|s| s.to_uppercase()) {
120 Some(rc) if rc == "DP" => {
121 for key in procedure_lookup_keys(&a.name) {
122 sid_index.entry(key).or_default().push(i);
123 }
124 }
125 Some(rc) if rc == "AP" => {
126 for key in procedure_lookup_keys(&a.name) {
127 star_index.entry(key).or_default().push(i);
128 }
129 }
130 _ => {}
131 }
132 }
133
134 Ok(Self {
135 airports,
136 airspaces,
137 navaids,
138 airways,
139 airport_index,
140 airspace_index,
141 navaid_index,
142 airway_index,
143 sid_index,
144 star_index,
145 })
146 }
147
148 fn resolve_procedure_airway_by_kind(&self, kind: &str, name: &str) -> Option<AirwayRecord> {
149 let index = match kind {
150 "SID" => &self.sid_index,
151 "STAR" => &self.star_index,
152 _ => return None,
153 };
154 for key in procedure_lookup_keys(name) {
155 if let Some(i) = index.get(&key).and_then(|idx| idx.first()).copied() {
156 if let Some(item) = self.airways.get(i) {
157 return Some(item.clone());
158 }
159 }
160 }
161 None
162 }
163
164 pub fn airports(&self) -> Result<JsValue, JsValue> {
165 serde_wasm_bindgen::to_value(&self.airports).map_err(|e| JsValue::from_str(&e.to_string()))
166 }
167
168 pub fn fixes(&self) -> Result<JsValue, JsValue> {
169 serde_wasm_bindgen::to_value(&self.navaids).map_err(|e| JsValue::from_str(&e.to_string()))
170 }
171
172 pub fn navaids(&self) -> Result<JsValue, JsValue> {
173 serde_wasm_bindgen::to_value(&self.navaids).map_err(|e| JsValue::from_str(&e.to_string()))
174 }
175
176 pub fn airways(&self) -> Result<JsValue, JsValue> {
177 serde_wasm_bindgen::to_value(&self.airways).map_err(|e| JsValue::from_str(&e.to_string()))
178 }
179
180 pub fn airspaces(&self) -> Result<JsValue, JsValue> {
181 let mut keys = self.airspace_index.keys().cloned().collect::<Vec<_>>();
182 keys.sort();
183 let rows = keys
184 .into_iter()
185 .filter_map(|key| {
186 let records = self
187 .airspace_index
188 .get(&key)
189 .into_iter()
190 .flat_map(|indices| indices.iter().copied())
191 .filter_map(|idx| self.airspaces.get(idx).cloned())
192 .collect::<Vec<_>>();
193 compose_airspace(records)
194 })
195 .collect::<Vec<_>>();
196 serde_wasm_bindgen::to_value(&rows).map_err(|e| JsValue::from_str(&e.to_string()))
197 }
198
199 pub fn resolve_airspace(&self, designator: String) -> Result<JsValue, JsValue> {
200 let key = designator.to_uppercase();
201 let records = self
202 .airspace_index
203 .get(&key)
204 .into_iter()
205 .flat_map(|indices| indices.iter().copied())
206 .filter_map(|idx| self.airspaces.get(idx).cloned())
207 .collect::<Vec<_>>();
208
209 serde_wasm_bindgen::to_value(&compose_airspace(records)).map_err(|e| JsValue::from_str(&e.to_string()))
210 }
211
212 pub fn resolve_fix(&self, code: String) -> Result<JsValue, JsValue> {
213 let key = code.to_uppercase();
214 let item = self
216 .navaid_index
217 .get(&key)
218 .and_then(|indices| {
219 indices
220 .iter()
221 .filter_map(|&i| self.navaids.get(i))
222 .find(|r| r.kind == "fix")
223 .or_else(|| indices.first().and_then(|&i| self.navaids.get(i)))
224 })
225 .cloned();
226
227 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
228 }
229
230 pub fn resolve_navaid(&self, code: String) -> Result<JsValue, JsValue> {
231 let key = code.to_uppercase();
232 let item = self
234 .navaid_index
235 .get(&key)
236 .and_then(|indices| {
237 indices
238 .iter()
239 .filter_map(|&i| self.navaids.get(i))
240 .find(|r| r.kind == "navaid")
241 .or_else(|| indices.first().and_then(|&i| self.navaids.get(i)))
242 })
243 .cloned();
244
245 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
246 }
247
248 pub fn resolve_airway(&self, name: String) -> Result<JsValue, JsValue> {
249 let key = normalize_airway_name(&name);
250 let item = self
251 .airway_index
252 .get(&key)
253 .and_then(|idx| idx.first().copied())
254 .and_then(|i| self.airways.get(i))
255 .cloned();
256
257 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
258 }
259
260 pub fn resolve_sid(&self, name: String) -> Result<JsValue, JsValue> {
261 let item = self.resolve_procedure_airway_by_kind("SID", &name);
262 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
263 }
264
265 pub fn resolve_star(&self, name: String) -> Result<JsValue, JsValue> {
266 let item = self.resolve_procedure_airway_by_kind("STAR", &name);
267 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
268 }
269
270 pub fn resolve_airport(&self, code: String) -> Result<JsValue, JsValue> {
271 let key = code.to_uppercase();
272 let item = self
273 .airport_index
274 .get(&key)
275 .and_then(|idx| idx.first().copied())
276 .and_then(|i| self.airports.get(i))
277 .cloned();
278
279 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
280 }
281
282 #[wasm_bindgen(js_name = enrichRoute)]
288 pub fn enrich_route(&self, route: String) -> Result<JsValue, JsValue> {
289 use crate::field15::ResolvedPoint as WasmPoint;
290 use crate::field15::RouteSegment;
291 use thrust::data::field15::{Connector, Field15Element, Field15Parser, Point};
292
293 let elements = Field15Parser::parse(&route);
294 let mut segments: Vec<RouteSegment> = Vec::new();
295 let mut last_point: Option<WasmPoint> = None;
296 let mut pending_airway: Option<(String, WasmPoint)> = None;
297 let mut current_connector: Option<String> = None;
298 let mut current_segment_type: Option<String> = None;
299
300 let resolve_code = |code: &str| -> Option<WasmPoint> {
301 let key = code.split('/').next().unwrap_or(code).trim().to_uppercase();
302 if let Some(idx) = self.airport_index.get(&key).and_then(|v| v.first()) {
303 if let Some(a) = self.airports.get(*idx) {
304 return Some(WasmPoint {
305 latitude: a.latitude,
306 longitude: a.longitude,
307 name: Some(a.code.clone()),
308 kind: Some("airport".to_string()),
309 });
310 }
311 }
312 if let Some(idx) = self.navaid_index.get(&key).and_then(|v| v.first()) {
313 if let Some(n) = self.navaids.get(*idx) {
314 return Some(WasmPoint {
315 latitude: n.latitude,
316 longitude: n.longitude,
317 name: Some(n.code.clone()),
318 kind: Some(n.kind.clone()),
319 });
320 }
321 }
322 None
323 };
324
325 let expand_airway =
326 |airway_name: &str, entry: &WasmPoint, exit: &WasmPoint, segs: &mut Vec<RouteSegment>| -> bool {
327 let key = crate::models::normalize_airway_name(airway_name);
328 let airway = match self
329 .airway_index
330 .get(&key)
331 .and_then(|v| v.first())
332 .and_then(|i| self.airways.get(*i))
333 {
334 Some(a) => a,
335 None => return false,
336 };
337 let pts = &airway.points;
338 let entry_name = entry.name.as_deref().unwrap_or("").to_uppercase();
339 let exit_name = exit.name.as_deref().unwrap_or("").to_uppercase();
340 let entry_pos = pts.iter().position(|p| p.code.to_uppercase() == entry_name);
341 let exit_pos = pts.iter().position(|p| p.code.to_uppercase() == exit_name);
342 let (from, to) = match (entry_pos, exit_pos) {
343 (Some(f), Some(t)) => (f, t),
344 _ => return false,
345 };
346 let slice: Vec<&crate::models::AirwayPointRecord> = if from <= to {
347 pts[from..=to].iter().collect()
348 } else {
349 pts[to..=from].iter().rev().collect()
350 };
351 if slice.len() < 2 {
352 return false;
353 }
354 let mut prev = entry.clone();
355 for pt in &slice[1..] {
356 let next = WasmPoint {
357 latitude: pt.latitude,
358 longitude: pt.longitude,
359 name: Some(pt.code.clone()),
360 kind: Some(pt.kind.clone()),
361 };
362 segs.push(RouteSegment {
363 start: prev,
364 end: next.clone(),
365 name: Some(airway_name.to_string()),
366 segment_type: Some("route".to_string()),
367 connector: Some(airway_name.to_string()),
368 });
369 prev = next;
370 }
371 true
372 };
373
374 let expand_procedure_from_entry =
375 |kind: &str, procedure_name: &str, entry: &WasmPoint, segs: &mut Vec<RouteSegment>| -> Option<WasmPoint> {
376 let airway = self.resolve_procedure_airway_by_kind(kind, procedure_name)?;
377 let pts = &airway.points;
378 if pts.len() < 2 {
379 return None;
380 }
381 let entry_name = entry.name.as_deref().unwrap_or("").to_uppercase();
382 let start_idx = pts.iter().position(|p| p.code.to_uppercase() == entry_name)?;
383 if start_idx >= pts.len() - 1 {
384 return None;
385 }
386 let mut prev = entry.clone();
387 for pt in &pts[start_idx + 1..] {
388 let next = WasmPoint {
389 latitude: pt.latitude,
390 longitude: pt.longitude,
391 name: Some(pt.code.clone()),
392 kind: Some(pt.kind.clone()),
393 };
394 segs.push(RouteSegment {
395 start: prev,
396 end: next.clone(),
397 name: Some(procedure_name.to_string()),
398 segment_type: Some(kind.to_string()),
399 connector: Some(procedure_name.to_string()),
400 });
401 prev = next;
402 }
403 Some(prev)
404 };
405
406 for element in &elements {
407 match element {
408 Field15Element::Point(point) => {
409 let resolved = match point {
410 Point::Waypoint(name) | Point::Aerodrome(name) => resolve_code(name),
411 Point::Coordinates((lat, lon)) => Some(WasmPoint {
412 latitude: *lat,
413 longitude: *lon,
414 name: None,
415 kind: Some("coords".to_string()),
416 }),
417 Point::BearingDistance { point, .. } => match point.as_ref() {
418 Point::Waypoint(name) | Point::Aerodrome(name) => resolve_code(name),
419 Point::Coordinates((lat, lon)) => Some(WasmPoint {
420 latitude: *lat,
421 longitude: *lon,
422 name: None,
423 kind: Some("coords".to_string()),
424 }),
425 _ => None,
426 },
427 };
428 if let Some(exit) = resolved {
429 if let Some((airway_name, entry)) = pending_airway.take() {
430 let expanded = expand_airway(&airway_name, &entry, &exit, &mut segments);
431 if !expanded {
432 segments.push(RouteSegment {
433 start: entry,
434 end: exit.clone(),
435 name: Some(airway_name.clone()),
436 segment_type: Some("unresolved".to_string()),
437 connector: Some(airway_name),
438 });
439 }
440 } else if let Some(prev) = last_point.take() {
441 let seg_name = current_connector.take();
442 let seg_type = current_segment_type.take();
443 let seg_connector = if seg_type.as_deref() == Some("dct") {
444 Some("DCT".to_string())
445 } else {
446 seg_name.clone()
447 };
448 segments.push(RouteSegment {
449 start: prev,
450 end: exit.clone(),
451 name: seg_name,
452 segment_type: seg_type,
453 connector: seg_connector,
454 });
455 } else {
456 current_connector = None;
457 current_segment_type = None;
458 }
459 last_point = Some(exit);
460 }
461 }
462 Field15Element::Connector(connector) => match connector {
463 Connector::Airway(name) => {
464 if let Some(entry) = last_point.take() {
465 pending_airway = Some((name.clone(), entry));
466 current_segment_type = None;
467 } else {
468 current_connector = Some(name.clone());
469 current_segment_type = Some("unresolved".to_string());
470 }
471 }
472 Connector::Direct => {
473 current_connector = None;
474 current_segment_type = Some("dct".to_string());
475 }
476 Connector::Sid(name) => {
477 if let Some(entry) = last_point.clone() {
478 if let Some(end) = expand_procedure_from_entry("SID", name, &entry, &mut segments) {
479 last_point = Some(end);
480 current_connector = None;
481 pending_airway = None;
482 current_segment_type = None;
483 } else {
484 current_connector = Some(name.clone());
485 current_segment_type = Some("unresolved".to_string());
486 }
487 } else {
488 current_connector = Some(name.clone());
489 current_segment_type = Some("unresolved".to_string());
490 }
491 }
492 Connector::Star(name) => {
493 if let Some(entry) = last_point.clone() {
494 if let Some(end) = expand_procedure_from_entry("STAR", name, &entry, &mut segments) {
495 last_point = Some(end);
496 current_connector = None;
497 pending_airway = None;
498 current_segment_type = None;
499 } else {
500 current_connector = Some(name.clone());
501 current_segment_type = Some("unresolved".to_string());
502 }
503 } else {
504 current_connector = Some(name.clone());
505 current_segment_type = Some("unresolved".to_string());
506 }
507 }
508 Connector::Nat(name) => {
509 current_connector = Some(name.clone());
510 current_segment_type = Some("NAT".to_string());
511 }
512 Connector::Pts(name) => {
513 current_connector = Some(name.clone());
514 current_segment_type = Some("PTS".to_string());
515 }
516 _ => {}
517 },
518 Field15Element::Modifier(_) => {}
519 }
520 }
521
522 serde_wasm_bindgen::to_value(&segments).map_err(|e| JsValue::from_str(&e.to_string()))
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::{procedure_lookup_keys, FaaArcgisResolver};
529 use crate::models::{AirwayPointRecord, AirwayRecord};
530
531 #[test]
532 fn procedure_lookup_keys_extracts_base_designator() {
533 let keys = procedure_lookup_keys("KEPER9ELFBO");
534 assert!(keys.contains(&"KEPER9ELFBO".to_string()));
535 assert!(keys.contains(&"KEPER9E".to_string()));
536 }
537
538 #[test]
539 fn resolve_star_uses_ap_airway_records() {
540 let resolver = FaaArcgisResolver {
541 airports: Vec::new(),
542 airspaces: Vec::new(),
543 navaids: Vec::new(),
544 airways: vec![AirwayRecord {
545 name: "KEPER9ELFBO".to_string(),
546 source: "faa_arcgis".to_string(),
547 route_class: Some("AP".to_string()),
548 points: vec![
549 AirwayPointRecord {
550 code: "KEPER".to_string(),
551 raw_code: "KEPER".to_string(),
552 kind: "fix".to_string(),
553 latitude: 44.0,
554 longitude: 2.0,
555 },
556 AirwayPointRecord {
557 code: "LFBO".to_string(),
558 raw_code: "LFBO".to_string(),
559 kind: "airport".to_string(),
560 latitude: 43.6,
561 longitude: 1.4,
562 },
563 ],
564 }],
565 airport_index: std::collections::HashMap::new(),
566 airspace_index: std::collections::HashMap::new(),
567 navaid_index: std::collections::HashMap::new(),
568 airway_index: std::collections::HashMap::new(),
569 sid_index: std::collections::HashMap::new(),
570 star_index: {
571 let mut m = std::collections::HashMap::new();
572 m.insert("KEPER9E".to_string(), vec![0]);
573 m
574 },
575 };
576
577 let star = resolver
578 .resolve_procedure_airway_by_kind("STAR", "KEPER9E")
579 .expect("missing STAR");
580 assert_eq!(star.route_class.as_deref(), Some("AP"));
581 assert_eq!(star.name, "KEPER9ELFBO");
582 }
583}