1use std::collections::HashMap;
2use std::io::{BufReader, Cursor, Read};
3
4use quick_xml::{events::Event, name::QName, Reader};
5use wasm_bindgen::prelude::*;
6use zip::read::ZipArchive;
7
8use crate::models::{normalize_airway_name, AirportRecord, AirwayPointRecord, AirwayRecord, NavpointRecord};
9
10const AIXM_EXPECTED_FILES: [&str; 10] = [
11 "AirportHeliport.BASELINE.zip",
12 "Navaid.BASELINE.zip",
13 "DesignatedPoint.BASELINE.zip",
14 "Route.BASELINE.zip",
15 "RouteSegment.BASELINE.zip",
16 "ArrivalLeg.BASELINE.zip",
17 "DepartureLeg.BASELINE.zip",
18 "StandardInstrumentArrival.BASELINE.zip",
19 "StandardInstrumentDeparture.BASELINE.zip",
20 "Airspace.BASELINE.zip",
21];
22
23const DDR_EXPECTED_FILES: [&str; 8] = [
24 "navpoints.nnpt",
25 "routes.routes",
26 "airports.arp",
27 "sectors.are",
28 "sectors.sls",
29 "free_route.are",
30 "free_route.sls",
31 "free_route.frp",
32];
33
34type DynError = Box<dyn std::error::Error>;
35type PointRefIndex = HashMap<String, AirwayPointRecord>;
36type NavpointsWithRefIndex = (Vec<NavpointRecord>, PointRefIndex);
37
38struct Node<'a> {
39 name: QName<'a>,
40 attributes: HashMap<String, String>,
41}
42
43fn find_node<'a, R: std::io::BufRead>(
44 reader: &mut Reader<R>,
45 lookup: &[QName<'a>],
46 end: Option<QName>,
47) -> Result<Node<'a>, Box<dyn std::error::Error>> {
48 let mut buf = Vec::new();
49 loop {
50 match reader.read_event_into(&mut buf) {
51 Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
52 let attributes = e
53 .attributes()
54 .flatten()
55 .map(|a| {
56 (
57 String::from_utf8_lossy(a.key.as_ref()).to_string(),
58 String::from_utf8_lossy(a.value.as_ref()).to_string(),
59 )
60 })
61 .collect::<HashMap<_, _>>();
62 for elt in lookup {
63 if e.name() == *elt {
64 return Ok(Node { name: *elt, attributes });
65 }
66 }
67 }
68 Ok(Event::End(ref e)) => {
69 if let Some(end_tag) = end {
70 if e.name() == end_tag {
71 break;
72 }
73 }
74 }
75 Ok(Event::Eof) => break,
76 Err(e) => return Err(Box::new(e)),
77 _ => {}
78 }
79 buf.clear();
80 }
81 Err(Box::new(std::io::Error::other("Node not found")))
82}
83
84fn read_text<R: std::io::BufRead>(reader: &mut Reader<R>, end: QName) -> Result<String, Box<dyn std::error::Error>> {
85 let mut buf = Vec::new();
86 let mut text = String::new();
87 loop {
88 match reader.read_event_into(&mut buf) {
89 Ok(Event::Text(e)) => text.push_str(&e.decode()?),
90 Ok(Event::End(e)) if e.name() == end => break,
91 Ok(Event::Eof) => break,
92 Err(e) => return Err(Box::new(e)),
93 _ => {}
94 }
95 buf.clear();
96 }
97 Ok(text)
98}
99
100fn read_baseline_xml_documents(zip_bytes: &[u8]) -> Result<Vec<String>, DynError> {
101 let cursor = Cursor::new(zip_bytes);
102 let mut archive = ZipArchive::new(cursor)?;
103 let mut xmls = Vec::new();
104 for i in 0..archive.len() {
105 let mut file = archive.by_index(i)?;
106 if !file.name().ends_with(".BASELINE") {
107 continue;
108 }
109 let mut xml = String::new();
110 file.read_to_string(&mut xml)?;
111 if !xml.is_empty() {
112 xmls.push(xml);
113 }
114 }
115 Ok(xmls)
116}
117
118fn parse_aixm_airports(zip_bytes: &[u8]) -> Result<Vec<AirportRecord>, DynError> {
119 let mut out = Vec::new();
120 for xml in read_baseline_xml_documents(zip_bytes)? {
121 let mut reader = Reader::from_reader(BufReader::new(Cursor::new(xml.into_bytes())));
122 while find_node(&mut reader, &[QName(b"aixm:AirportHeliport")], None).is_ok() {
123 let mut identifier = String::new();
124 let mut icao = String::new();
125 let mut iata = None;
126 let mut name = String::new();
127 let mut latitude = 0.0_f64;
128 let mut longitude = 0.0_f64;
129 while let Ok(node) = find_node(
130 &mut reader,
131 &[
132 QName(b"gml:identifier"),
133 QName(b"aixm:locationIndicatorICAO"),
134 QName(b"aixm:designatorIATA"),
135 QName(b"aixm:name"),
136 QName(b"aixm:ElevatedPoint"),
137 ],
138 Some(QName(b"aixm:AirportHeliport")),
139 ) {
140 match node.name {
141 QName(b"gml:identifier") => identifier = read_text(&mut reader, node.name)?,
142 QName(b"aixm:locationIndicatorICAO") => icao = read_text(&mut reader, node.name)?,
143 QName(b"aixm:designatorIATA") => iata = Some(read_text(&mut reader, node.name)?),
144 QName(b"aixm:name") => name = read_text(&mut reader, node.name)?,
145 QName(b"aixm:ElevatedPoint") => {
146 while let Ok(pos) = find_node(&mut reader, &[QName(b"gml:pos")], Some(node.name)) {
147 let coords: Vec<f64> = read_text(&mut reader, pos.name)?
148 .split_whitespace()
149 .filter_map(|s| s.parse::<f64>().ok())
150 .collect();
151 if coords.len() == 2 {
152 latitude = coords[0];
153 longitude = coords[1];
154 }
155 }
156 }
157 _ => {}
158 }
159 }
160
161 if !icao.is_empty() {
162 let name_value = if name.is_empty() { None } else { Some(name.clone()) };
163 out.push(AirportRecord {
164 code: icao.to_uppercase(),
165 iata: iata.clone().map(|v| v.to_uppercase()),
166 icao: Some(icao.to_uppercase()),
167 name: name_value.clone(),
168 latitude,
169 longitude,
170 region: None,
171 source: "eurocontrol_aixm".to_string(),
172 });
173 if let Some(iata_code) = iata {
174 out.push(AirportRecord {
175 code: iata_code.to_uppercase(),
176 iata: Some(iata_code.to_uppercase()),
177 icao: Some(icao.to_uppercase()),
178 name: name_value,
179 latitude,
180 longitude,
181 region: None,
182 source: "eurocontrol_aixm".to_string(),
183 });
184 }
185 } else if !identifier.is_empty() {
186 let _ = identifier;
187 }
188 }
189 }
190 Ok(out)
191}
192
193fn parse_aixm_designated_points(zip_bytes: &[u8]) -> Result<NavpointsWithRefIndex, DynError> {
194 let mut out = Vec::new();
195 let mut by_id: HashMap<String, AirwayPointRecord> = HashMap::new();
196 for xml in read_baseline_xml_documents(zip_bytes)? {
197 let mut reader = Reader::from_reader(BufReader::new(Cursor::new(xml.into_bytes())));
198 while find_node(&mut reader, &[QName(b"aixm:DesignatedPoint")], None).is_ok() {
199 let mut identifier = String::new();
200 let mut designator = String::new();
201 let mut name = None;
202 let mut latitude = 0.0_f64;
203 let mut longitude = 0.0_f64;
204 let mut point_type = None;
205 while let Ok(node) = find_node(
206 &mut reader,
207 &[
208 QName(b"gml:identifier"),
209 QName(b"aixm:name"),
210 QName(b"aixm:designator"),
211 QName(b"aixm:type"),
212 QName(b"aixm:Point"),
213 ],
214 Some(QName(b"aixm:DesignatedPoint")),
215 ) {
216 match node.name {
217 QName(b"gml:identifier") => identifier = read_text(&mut reader, node.name)?,
218 QName(b"aixm:name") => name = Some(read_text(&mut reader, node.name)?),
219 QName(b"aixm:designator") => designator = read_text(&mut reader, node.name)?,
220 QName(b"aixm:type") => point_type = Some(read_text(&mut reader, node.name)?),
221 QName(b"aixm:Point") => {
222 while let Ok(pos) = find_node(&mut reader, &[QName(b"gml:pos")], Some(node.name)) {
223 let coords: Vec<f64> = read_text(&mut reader, pos.name)?
224 .split_whitespace()
225 .filter_map(|s| s.parse::<f64>().ok())
226 .collect();
227 if coords.len() == 2 {
228 latitude = coords[0];
229 longitude = coords[1];
230 }
231 }
232 }
233 _ => {}
234 }
235 }
236
237 if !designator.is_empty() {
238 let code = designator.to_uppercase();
239 out.push(NavpointRecord {
240 code: code.clone(),
241 identifier: code.clone(),
242 kind: "fix".to_string(),
243 name,
244 latitude,
245 longitude,
246 description: None,
247 frequency: None,
248 point_type,
249 region: None,
250 source: "eurocontrol_aixm".to_string(),
251 });
252 if !identifier.is_empty() {
253 by_id.insert(
254 identifier,
255 AirwayPointRecord {
256 code,
257 raw_code: designator.to_uppercase(),
258 kind: "fix".to_string(),
259 latitude,
260 longitude,
261 },
262 );
263 }
264 }
265 }
266 }
267 Ok((out, by_id))
268}
269
270fn parse_aixm_navaids(zip_bytes: &[u8]) -> Result<NavpointsWithRefIndex, DynError> {
271 let mut out = Vec::new();
272 let mut by_id: HashMap<String, AirwayPointRecord> = HashMap::new();
273 for xml in read_baseline_xml_documents(zip_bytes)? {
274 let mut reader = Reader::from_reader(BufReader::new(Cursor::new(xml.into_bytes())));
275 while find_node(&mut reader, &[QName(b"aixm:Navaid")], None).is_ok() {
276 let mut identifier = String::new();
277 let mut designator = None;
278 let mut description = None;
279 let mut point_type = None;
280 let mut latitude = 0.0_f64;
281 let mut longitude = 0.0_f64;
282 while let Ok(node) = find_node(
283 &mut reader,
284 &[
285 QName(b"gml:identifier"),
286 QName(b"aixm:designator"),
287 QName(b"aixm:type"),
288 QName(b"aixm:name"),
289 QName(b"aixm:ElevatedPoint"),
290 ],
291 Some(QName(b"aixm:Navaid")),
292 ) {
293 match node.name {
294 QName(b"gml:identifier") => identifier = read_text(&mut reader, node.name)?,
295 QName(b"aixm:designator") => designator = Some(read_text(&mut reader, node.name)?),
296 QName(b"aixm:type") => point_type = Some(read_text(&mut reader, node.name)?),
297 QName(b"aixm:name") => description = Some(read_text(&mut reader, node.name)?),
298 QName(b"aixm:ElevatedPoint") => {
299 while let Ok(pos) = find_node(&mut reader, &[QName(b"gml:pos")], Some(node.name)) {
300 let coords: Vec<f64> = read_text(&mut reader, pos.name)?
301 .split_whitespace()
302 .filter_map(|s| s.parse::<f64>().ok())
303 .collect();
304 if coords.len() == 2 {
305 latitude = coords[0];
306 longitude = coords[1];
307 }
308 }
309 }
310 _ => {}
311 }
312 }
313
314 if let Some(code) = designator {
315 let upper = code.to_uppercase();
316 out.push(NavpointRecord {
317 code: upper.clone(),
318 identifier: upper.clone(),
319 kind: "navaid".to_string(),
320 name: Some(code),
321 latitude,
322 longitude,
323 description,
324 frequency: None,
325 point_type,
326 region: None,
327 source: "eurocontrol_aixm".to_string(),
328 });
329 if !identifier.is_empty() {
330 by_id.insert(
331 identifier,
332 AirwayPointRecord {
333 code: upper.clone(),
334 raw_code: upper,
335 kind: "navaid".to_string(),
336 latitude,
337 longitude,
338 },
339 );
340 }
341 }
342 }
343 }
344 Ok((out, by_id))
345}
346
347fn parse_aixm_airways(
348 route_zip_bytes: &[u8],
349 route_segment_zip_bytes: &[u8],
350 points_by_id: &HashMap<String, AirwayPointRecord>,
351) -> Result<Vec<AirwayRecord>, Box<dyn std::error::Error>> {
352 let mut route_name_by_id: HashMap<String, String> = HashMap::new();
353 for xml in read_baseline_xml_documents(route_zip_bytes)? {
354 let mut reader = Reader::from_reader(BufReader::new(Cursor::new(xml.into_bytes())));
355 while find_node(&mut reader, &[QName(b"aixm:Route")], None).is_ok() {
356 let mut identifier = String::new();
357 let mut prefix = String::new();
358 let mut second = String::new();
359 let mut number = String::new();
360 let mut multiple = String::new();
361 while let Ok(node) = find_node(
362 &mut reader,
363 &[
364 QName(b"gml:identifier"),
365 QName(b"aixm:designatorPrefix"),
366 QName(b"aixm:designatorSecondLetter"),
367 QName(b"aixm:designatorNumber"),
368 QName(b"aixm:multipleIdentifier"),
369 ],
370 Some(QName(b"aixm:Route")),
371 ) {
372 match node.name {
373 QName(b"gml:identifier") => identifier = read_text(&mut reader, node.name)?,
374 QName(b"aixm:designatorPrefix") => prefix = read_text(&mut reader, node.name)?,
375 QName(b"aixm:designatorSecondLetter") => second = read_text(&mut reader, node.name)?,
376 QName(b"aixm:designatorNumber") => number = read_text(&mut reader, node.name)?,
377 QName(b"aixm:multipleIdentifier") => multiple = read_text(&mut reader, node.name)?,
378 _ => {}
379 }
380 }
381 if !identifier.is_empty() {
382 route_name_by_id.insert(identifier, format!("{prefix}{second}{number}{multiple}").to_uppercase());
383 }
384 }
385 }
386
387 let mut grouped: HashMap<String, Vec<AirwayPointRecord>> = HashMap::new();
388 for xml in read_baseline_xml_documents(route_segment_zip_bytes)? {
389 let mut reader = Reader::from_reader(BufReader::new(Cursor::new(xml.into_bytes())));
390 while find_node(&mut reader, &[QName(b"aixm:RouteSegment")], None).is_ok() {
391 let mut route_id: Option<String> = None;
392 let mut start_id: Option<String> = None;
393 let mut end_id: Option<String> = None;
394
395 while let Ok(node) = find_node(
396 &mut reader,
397 &[
398 QName(b"aixm:routeFormed"),
399 QName(b"aixm:start"),
400 QName(b"aixm:end"),
401 QName(b"aixm:pointChoice_fixDesignatedPoint"),
402 QName(b"aixm:pointChoice_navaidSystem"),
403 QName(b"aixm:extension"),
404 QName(b"aixm:annotation"),
405 QName(b"aixm:availability"),
406 ],
407 Some(QName(b"aixm:RouteSegment")),
408 ) {
409 let href_id = |key: &str| {
410 node.attributes
411 .get(key)
412 .map(|s| s.trim_start_matches("urn:uuid:").to_string())
413 };
414 match node.name {
415 QName(b"aixm:routeFormed") => route_id = href_id("xlink:href"),
416 QName(b"aixm:start") => {
417 while let Ok(point_node) = find_node(
418 &mut reader,
419 &[
420 QName(b"aixm:pointChoice_fixDesignatedPoint"),
421 QName(b"aixm:pointChoice_navaidSystem"),
422 ],
423 Some(QName(b"aixm:start")),
424 ) {
425 start_id = point_node
426 .attributes
427 .get("xlink:href")
428 .map(|s| s.trim_start_matches("urn:uuid:").to_string());
429 }
430 }
431 QName(b"aixm:end") => {
432 while let Ok(point_node) = find_node(
433 &mut reader,
434 &[
435 QName(b"aixm:pointChoice_fixDesignatedPoint"),
436 QName(b"aixm:pointChoice_navaidSystem"),
437 ],
438 Some(QName(b"aixm:end")),
439 ) {
440 end_id = point_node
441 .attributes
442 .get("xlink:href")
443 .map(|s| s.trim_start_matches("urn:uuid:").to_string());
444 }
445 }
446 _ => {}
447 }
448 }
449
450 let Some(route_name) = route_id.and_then(|id| route_name_by_id.get(&id).cloned()) else {
451 continue;
452 };
453 let Some(start) = start_id.and_then(|id| points_by_id.get(&id).cloned()) else {
454 continue;
455 };
456 let Some(end) = end_id.and_then(|id| points_by_id.get(&id).cloned()) else {
457 continue;
458 };
459
460 let entry = grouped.entry(route_name).or_default();
461 if entry.last().map(|x| x.code.as_str()) != Some(start.code.as_str()) {
462 entry.push(start);
463 }
464 if entry.last().map(|x| x.code.as_str()) != Some(end.code.as_str()) {
465 entry.push(end);
466 }
467 }
468 }
469
470 Ok(grouped
471 .into_iter()
472 .map(|(name, points)| AirwayRecord {
473 name,
474 source: "eurocontrol_aixm".to_string(),
475 points,
476 })
477 .collect())
478}
479
480fn parse_ddr_navpoints(text: &str) -> Vec<NavpointRecord> {
481 let mut out = Vec::new();
482 for line in text.lines() {
483 let line = line.trim();
484 if line.is_empty() || line.starts_with('#') {
485 continue;
486 }
487 let fields: Vec<&str> = line.split(';').collect();
488 if fields.len() < 5 {
489 continue;
490 }
491 let lat = match fields[2].trim().parse::<f64>() {
492 Ok(v) => v,
493 Err(_) => continue,
494 };
495 let lon = match fields[3].trim().parse::<f64>() {
496 Ok(v) => v,
497 Err(_) => continue,
498 };
499 let code = fields[0].trim().to_uppercase();
500 let point_type = fields[1].trim().to_uppercase();
501 let kind = if point_type.contains("FIX") || point_type == "WPT" || point_type == "WP" {
502 "fix"
503 } else {
504 "navaid"
505 }
506 .to_string();
507 let description = fields
508 .get(4)
509 .map(|s| s.trim().to_string())
510 .filter(|s| !s.is_empty() && s != "_");
511
512 out.push(NavpointRecord {
513 code: code.clone(),
514 identifier: code,
515 kind,
516 name: description.clone(),
517 latitude: lat,
518 longitude: lon,
519 description,
520 frequency: None,
521 point_type: Some(point_type),
522 region: None,
523 source: "eurocontrol_ddr".to_string(),
524 });
525 }
526 out
527}
528
529fn parse_ddr_airports(text: &str) -> Vec<AirportRecord> {
530 let mut out = Vec::new();
531 for line in text.lines() {
532 let line = line.trim();
533 if line.is_empty() || line.starts_with('#') {
534 continue;
535 }
536 let parts: Vec<&str> = line.split_whitespace().collect();
537 if parts.len() < 3 {
538 continue;
539 }
540 let code = parts[0].trim().to_uppercase();
541 if code.len() != 4 {
542 continue;
543 }
544 let lat_raw = match parts[1].parse::<f64>() {
545 Ok(v) => v,
546 Err(_) => continue,
547 };
548 let lon_raw = match parts[2].parse::<f64>() {
549 Ok(v) => v,
550 Err(_) => continue,
551 };
552 out.push(AirportRecord {
553 code: code.clone(),
554 iata: None,
555 icao: Some(code),
556 name: None,
557 latitude: lat_raw / 100.0,
558 longitude: lon_raw / 100.0,
559 region: None,
560 source: "eurocontrol_ddr".to_string(),
561 });
562 }
563 out
564}
565
566fn parse_ddr_airways(text: &str, point_lookup: &HashMap<String, (f64, f64, String)>) -> Vec<AirwayRecord> {
567 let mut grouped: HashMap<String, Vec<(i32, AirwayPointRecord)>> = HashMap::new();
568 for line in text.lines() {
569 let line = line.trim();
570 if line.is_empty() || line.starts_with('#') {
571 continue;
572 }
573 let fields: Vec<&str> = line.split(';').collect();
574 if fields.len() < 8 {
575 continue;
576 }
577 let route = fields[1].trim().to_uppercase();
578 let navaid = fields[5].trim().to_uppercase();
579 let seq = fields[7].trim().parse::<i32>().unwrap_or(0);
580 let (lat, lon, kind) = point_lookup
581 .get(&navaid)
582 .cloned()
583 .unwrap_or((0.0, 0.0, "point".to_string()));
584
585 grouped.entry(route).or_default().push((
586 seq,
587 AirwayPointRecord {
588 code: navaid.clone(),
589 raw_code: navaid,
590 kind,
591 latitude: lat,
592 longitude: lon,
593 },
594 ));
595 }
596
597 let mut out = Vec::new();
598 for (name, mut points) in grouped {
599 points.sort_by_key(|(seq, _)| *seq);
600 let deduped = points
601 .into_iter()
602 .map(|(_, p)| p)
603 .fold(Vec::<AirwayPointRecord>::new(), |mut acc, p| {
604 if acc.last().map(|x| x.code.as_str()) != Some(p.code.as_str()) {
605 acc.push(p);
606 }
607 acc
608 });
609 out.push(AirwayRecord {
610 name,
611 source: "eurocontrol_ddr".to_string(),
612 points: deduped,
613 });
614 }
615 out
616}
617
618fn file_basename(name: &str) -> &str {
619 name.rsplit('/').next().unwrap_or(name)
620}
621
622fn find_zip_text_entry(ddr_archive: &[u8], predicate: impl Fn(&str) -> bool) -> Result<String, JsValue> {
623 let mut archive = ZipArchive::new(Cursor::new(ddr_archive))
624 .map_err(|e| JsValue::from_str(&format!("invalid DDR zip archive: {e}")))?;
625 for idx in 0..archive.len() {
626 let mut entry = archive
627 .by_index(idx)
628 .map_err(|e| JsValue::from_str(&format!("unable to read DDR zip entry: {e}")))?;
629 if entry.is_dir() {
630 continue;
631 }
632 let name = file_basename(entry.name()).to_string();
633 if !predicate(&name) {
634 continue;
635 }
636 let mut text = String::new();
637 entry
638 .read_to_string(&mut text)
639 .map_err(|e| JsValue::from_str(&format!("unable to decode DDR entry '{name}' as UTF-8 text: {e}")))?;
640 return Ok(text);
641 }
642 Err(JsValue::from_str("matching DDR file not found in archive"))
643}
644
645type DdrEntryMatcher = (&'static str, fn(&str) -> bool);
646
647fn ddr_file_key_and_matchers() -> [DdrEntryMatcher; 8] {
648 [
649 ("navpoints.nnpt", |name: &str| {
650 let lower = name.to_ascii_lowercase();
651 lower.starts_with("airac_") && lower.ends_with(".nnpt")
652 }),
653 ("routes.routes", |name: &str| {
654 let lower = name.to_ascii_lowercase();
655 lower.starts_with("airac_") && lower.ends_with(".routes")
656 }),
657 ("airports.arp", |name: &str| {
658 let lower = name.to_ascii_lowercase();
659 lower.starts_with("vst_") && lower.ends_with("_airports.arp")
660 }),
661 ("sectors.are", |name: &str| {
662 let lower = name.to_ascii_lowercase();
663 lower.starts_with("sectors_") && lower.ends_with(".are")
664 }),
665 ("sectors.sls", |name: &str| {
666 let lower = name.to_ascii_lowercase();
667 lower.starts_with("sectors_") && lower.ends_with(".sls")
668 }),
669 ("free_route.are", |name: &str| {
670 let lower = name.to_ascii_lowercase();
671 lower.starts_with("free_route_") && lower.ends_with(".are")
672 }),
673 ("free_route.sls", |name: &str| {
674 let lower = name.to_ascii_lowercase();
675 lower.starts_with("free_route_") && lower.ends_with(".sls")
676 }),
677 ("free_route.frp", |name: &str| {
678 let lower = name.to_ascii_lowercase();
679 lower.starts_with("free_route_") && lower.ends_with(".frp")
680 }),
681 ]
682}
683
684fn build_from_ddr_text_files(files: HashMap<String, String>) -> Result<EurocontrolResolver, JsValue> {
685 for name in DDR_EXPECTED_FILES {
686 if !files.contains_key(name) {
687 return Err(JsValue::from_str(&format!(
688 "missing DDR file '{name}' in dataset payload"
689 )));
690 }
691 }
692
693 let mut fixes = Vec::new();
694 let mut navaids = Vec::new();
695
696 let ddr_points = parse_ddr_navpoints(
697 files
698 .get("navpoints.nnpt")
699 .ok_or_else(|| JsValue::from_str("missing navpoints.nnpt"))?,
700 );
701 for p in &ddr_points {
702 if p.kind == "fix" {
703 fixes.push(p.clone());
704 } else {
705 navaids.push(p.clone());
706 }
707 }
708
709 let airports = parse_ddr_airports(
710 files
711 .get("airports.arp")
712 .ok_or_else(|| JsValue::from_str("missing airports.arp"))?,
713 );
714 let mut point_lookup: HashMap<String, (f64, f64, String)> = HashMap::new();
715 for p in &ddr_points {
716 point_lookup.insert(p.code.clone(), (p.latitude, p.longitude, p.kind.clone()));
717 }
718 let airways = parse_ddr_airways(
719 files
720 .get("routes.routes")
721 .ok_or_else(|| JsValue::from_str("missing routes.routes"))?,
722 &point_lookup,
723 );
724
725 EurocontrolResolver::build(airports, fixes, navaids, airways)
726}
727
728#[wasm_bindgen]
729pub struct EurocontrolResolver {
730 airports: Vec<AirportRecord>,
731 fixes: Vec<NavpointRecord>,
732 navaids: Vec<NavpointRecord>,
733 airways: Vec<AirwayRecord>,
734 airport_index: HashMap<String, Vec<usize>>,
735 fix_index: HashMap<String, Vec<usize>>,
736 navaid_index: HashMap<String, Vec<usize>>,
737 airway_index: HashMap<String, Vec<usize>>,
738}
739
740#[wasm_bindgen]
741impl EurocontrolResolver {
742 #[wasm_bindgen(constructor)]
743 pub fn new(aixm_folder: JsValue) -> Result<EurocontrolResolver, JsValue> {
744 let files: HashMap<String, Vec<u8>> =
745 serde_wasm_bindgen::from_value(aixm_folder).map_err(|e| JsValue::from_str(&e.to_string()))?;
746 for name in AIXM_EXPECTED_FILES {
747 if !files.contains_key(name) {
748 return Err(JsValue::from_str(&format!(
749 "missing AIXM file '{name}' in dataset folder payload"
750 )));
751 }
752 }
753
754 let airports = parse_aixm_airports(
755 files
756 .get("AirportHeliport.BASELINE.zip")
757 .ok_or_else(|| JsValue::from_str("missing AirportHeliport.BASELINE.zip"))?,
758 )
759 .map_err(|e| JsValue::from_str(&e.to_string()))?;
760 let (fixes, fixes_by_id) = parse_aixm_designated_points(
761 files
762 .get("DesignatedPoint.BASELINE.zip")
763 .ok_or_else(|| JsValue::from_str("missing DesignatedPoint.BASELINE.zip"))?,
764 )
765 .map_err(|e| JsValue::from_str(&e.to_string()))?;
766 let (navaids, navaids_by_id) = parse_aixm_navaids(
767 files
768 .get("Navaid.BASELINE.zip")
769 .ok_or_else(|| JsValue::from_str("missing Navaid.BASELINE.zip"))?,
770 )
771 .map_err(|e| JsValue::from_str(&e.to_string()))?;
772 let mut point_refs = fixes_by_id;
773 point_refs.extend(navaids_by_id);
774 let airways = parse_aixm_airways(
775 files
776 .get("Route.BASELINE.zip")
777 .ok_or_else(|| JsValue::from_str("missing Route.BASELINE.zip"))?,
778 files
779 .get("RouteSegment.BASELINE.zip")
780 .ok_or_else(|| JsValue::from_str("missing RouteSegment.BASELINE.zip"))?,
781 &point_refs,
782 )
783 .map_err(|e| JsValue::from_str(&e.to_string()))?;
784
785 Self::build(airports, fixes, navaids, airways)
786 }
787
788 #[wasm_bindgen(js_name = fromDdrFolder)]
789 pub fn from_ddr_folder(ddr_folder: JsValue) -> Result<EurocontrolResolver, JsValue> {
790 let files: HashMap<String, String> =
791 serde_wasm_bindgen::from_value(ddr_folder).map_err(|e| JsValue::from_str(&e.to_string()))?;
792 build_from_ddr_text_files(files)
793 }
794
795 #[wasm_bindgen(js_name = fromDdrArchive)]
796 pub fn from_ddr_archive(ddr_archive: Vec<u8>) -> Result<EurocontrolResolver, JsValue> {
797 let mut files: HashMap<String, String> = HashMap::new();
798 for (key, matcher) in ddr_file_key_and_matchers() {
799 let text = find_zip_text_entry(&ddr_archive, matcher)
800 .map_err(|_| JsValue::from_str(&format!("missing DDR file for key '{key}' in archive payload")))?;
801 files.insert(key.to_string(), text);
802 }
803 build_from_ddr_text_files(files)
804 }
805
806 fn build(
807 airports: Vec<AirportRecord>,
808 fixes: Vec<NavpointRecord>,
809 navaids: Vec<NavpointRecord>,
810 airways: Vec<AirwayRecord>,
811 ) -> Result<EurocontrolResolver, JsValue> {
812 let mut airport_index: HashMap<String, Vec<usize>> = HashMap::new();
813 for (i, a) in airports.iter().enumerate() {
814 airport_index.entry(a.code.clone()).or_default().push(i);
815 if let Some(v) = &a.iata {
816 airport_index.entry(v.clone()).or_default().push(i);
817 }
818 if let Some(v) = &a.icao {
819 airport_index.entry(v.clone()).or_default().push(i);
820 }
821 }
822
823 let mut fix_index: HashMap<String, Vec<usize>> = HashMap::new();
824 for (i, n) in fixes.iter().enumerate() {
825 fix_index.entry(n.code.clone()).or_default().push(i);
826 }
827
828 let mut navaid_index: HashMap<String, Vec<usize>> = HashMap::new();
829 for (i, n) in navaids.iter().enumerate() {
830 navaid_index.entry(n.code.clone()).or_default().push(i);
831 }
832
833 let mut airway_index: HashMap<String, Vec<usize>> = HashMap::new();
834 for (i, a) in airways.iter().enumerate() {
835 airway_index.entry(normalize_airway_name(&a.name)).or_default().push(i);
836 airway_index.entry(a.name.to_uppercase()).or_default().push(i);
837 }
838
839 Ok(EurocontrolResolver {
840 airports,
841 fixes,
842 navaids,
843 airways,
844 airport_index,
845 fix_index,
846 navaid_index,
847 airway_index,
848 })
849 }
850
851 pub fn airports(&self) -> Result<JsValue, JsValue> {
852 serde_wasm_bindgen::to_value(&self.airports).map_err(|e| JsValue::from_str(&e.to_string()))
853 }
854
855 pub fn fixes(&self) -> Result<JsValue, JsValue> {
856 serde_wasm_bindgen::to_value(&self.fixes).map_err(|e| JsValue::from_str(&e.to_string()))
857 }
858
859 pub fn navaids(&self) -> Result<JsValue, JsValue> {
860 serde_wasm_bindgen::to_value(&self.navaids).map_err(|e| JsValue::from_str(&e.to_string()))
861 }
862
863 pub fn airways(&self) -> Result<JsValue, JsValue> {
864 serde_wasm_bindgen::to_value(&self.airways).map_err(|e| JsValue::from_str(&e.to_string()))
865 }
866
867 pub fn resolve_airport(&self, code: String) -> Result<JsValue, JsValue> {
868 let key = code.to_uppercase();
869 let item = self
870 .airport_index
871 .get(&key)
872 .and_then(|idx| idx.first().copied())
873 .and_then(|i| self.airports.get(i))
874 .cloned();
875 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
876 }
877
878 pub fn resolve_fix(&self, code: String) -> Result<JsValue, JsValue> {
879 let key = code.to_uppercase();
880 let item = self
881 .fix_index
882 .get(&key)
883 .and_then(|idx| idx.first().copied())
884 .and_then(|i| self.fixes.get(i))
885 .cloned();
886 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
887 }
888
889 pub fn resolve_navaid(&self, code: String) -> Result<JsValue, JsValue> {
890 let key = code.to_uppercase();
891 let item = self
892 .navaid_index
893 .get(&key)
894 .and_then(|idx| idx.first().copied())
895 .and_then(|i| self.navaids.get(i))
896 .cloned();
897 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
898 }
899
900 pub fn resolve_airway(&self, name: String) -> Result<JsValue, JsValue> {
901 let key = normalize_airway_name(&name);
902 let item = self
903 .airway_index
904 .get(&key)
905 .and_then(|idx| idx.first().copied())
906 .and_then(|i| self.airways.get(i))
907 .cloned();
908 serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
909 }
910}