Skip to main content

thrust_wasm/
nasr.rs

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,
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    airspaces: Vec<AirspaceRecord>,
42    airport_index: HashMap<String, Vec<usize>>,
43    navaid_index: HashMap<String, Vec<usize>>,
44    airway_index: HashMap<String, Vec<usize>>,
45    airspace_index: HashMap<String, Vec<usize>>,
46}
47
48#[wasm_bindgen]
49impl NasrResolver {
50    #[wasm_bindgen(constructor)]
51    pub fn new(zip_bytes: &[u8]) -> Result<NasrResolver, JsValue> {
52        let dataset = parse_resolver_data_from_nasr_bytes(zip_bytes).map_err(|e| JsValue::from_str(&e.to_string()))?;
53        let airports: Vec<AirportRecord> = dataset.airports.into_iter().map(Into::into).collect();
54        let navaids: Vec<NavpointRecord> = dataset.navaids.into_iter().map(Into::into).collect();
55        let airways: Vec<AirwayRecord> = dataset.airways.into_iter().map(Into::into).collect();
56        let airspaces: Vec<AirspaceRecord> = dataset
57            .airspaces
58            .into_iter()
59            .map(|a| AirspaceRecord {
60                designator: a.designator,
61                name: a.name,
62                type_: a.type_,
63                lower: a.lower,
64                upper: a.upper,
65                coordinates: a.coordinates,
66                source: "faa_nasr".to_string(),
67            })
68            .collect();
69
70        let mut airport_index: HashMap<String, Vec<usize>> = HashMap::new();
71        for (i, a) in airports.iter().enumerate() {
72            airport_index.entry(a.code.clone()).or_default().push(i);
73            if let Some(v) = &a.iata {
74                airport_index.entry(v.clone()).or_default().push(i);
75            }
76            if let Some(v) = &a.icao {
77                airport_index.entry(v.clone()).or_default().push(i);
78            }
79        }
80
81        let mut navaid_index: HashMap<String, Vec<usize>> = HashMap::new();
82        for (i, n) in navaids.iter().enumerate() {
83            navaid_index.entry(n.code.clone()).or_default().push(i);
84            navaid_index.entry(n.identifier.clone()).or_default().push(i);
85        }
86
87        let mut airway_index: HashMap<String, Vec<usize>> = HashMap::new();
88        for (i, a) in airways.iter().enumerate() {
89            airway_index.entry(normalize_airway_name(&a.name)).or_default().push(i);
90            airway_index.entry(a.name.to_uppercase()).or_default().push(i);
91        }
92
93        let mut airspace_index: HashMap<String, Vec<usize>> = HashMap::new();
94        for (i, a) in airspaces.iter().enumerate() {
95            airspace_index.entry(a.designator.to_uppercase()).or_default().push(i);
96        }
97
98        Ok(Self {
99            airports,
100            navaids,
101            airways,
102            airspaces,
103            airport_index,
104            navaid_index,
105            airway_index,
106            airspace_index,
107        })
108    }
109
110    pub fn airports(&self) -> Result<JsValue, JsValue> {
111        serde_wasm_bindgen::to_value(&self.airports).map_err(|e| JsValue::from_str(&e.to_string()))
112    }
113
114    pub fn resolve_airport(&self, code: String) -> Result<JsValue, JsValue> {
115        let key = code.to_uppercase();
116        let item = self
117            .airport_index
118            .get(&key)
119            .and_then(|idx| idx.first().copied())
120            .and_then(|i| self.airports.get(i))
121            .cloned();
122
123        serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
124    }
125
126    pub fn navaids(&self) -> Result<JsValue, JsValue> {
127        serde_wasm_bindgen::to_value(&self.navaids).map_err(|e| JsValue::from_str(&e.to_string()))
128    }
129
130    pub fn fixes(&self) -> Result<JsValue, JsValue> {
131        serde_wasm_bindgen::to_value(&self.navaids).map_err(|e| JsValue::from_str(&e.to_string()))
132    }
133
134    pub fn airways(&self) -> Result<JsValue, JsValue> {
135        serde_wasm_bindgen::to_value(&self.airways).map_err(|e| JsValue::from_str(&e.to_string()))
136    }
137
138    pub fn airspaces(&self) -> Result<JsValue, JsValue> {
139        let mut keys = self.airspace_index.keys().cloned().collect::<Vec<_>>();
140        keys.sort();
141        let rows = keys
142            .into_iter()
143            .filter_map(|key| {
144                let records = self
145                    .airspace_index
146                    .get(&key)
147                    .into_iter()
148                    .flat_map(|indices| indices.iter().copied())
149                    .filter_map(|idx| self.airspaces.get(idx).cloned())
150                    .collect::<Vec<_>>();
151                compose_airspace(records)
152            })
153            .collect::<Vec<_>>();
154        serde_wasm_bindgen::to_value(&rows).map_err(|e| JsValue::from_str(&e.to_string()))
155    }
156
157    pub fn resolve_navaid(&self, code: String) -> Result<JsValue, JsValue> {
158        let key = code.to_uppercase();
159        let item = self
160            .navaid_index
161            .get(&key)
162            .and_then(|idx| idx.first().copied())
163            .and_then(|i| self.navaids.get(i))
164            .cloned();
165
166        serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
167    }
168
169    pub fn resolve_fix(&self, code: String) -> Result<JsValue, JsValue> {
170        let key = code.to_uppercase();
171        let item = self
172            .navaid_index
173            .get(&key)
174            .and_then(|idx| idx.first().copied())
175            .and_then(|i| self.navaids.get(i))
176            .cloned();
177
178        serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
179    }
180
181    pub fn resolve_airway(&self, name: String) -> Result<JsValue, JsValue> {
182        let key = normalize_airway_name(&name);
183        let item = self
184            .airway_index
185            .get(&key)
186            .and_then(|idx| idx.first().copied())
187            .and_then(|i| self.airways.get(i))
188            .cloned();
189
190        serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string()))
191    }
192
193    pub fn resolve_airspace(&self, designator: String) -> Result<JsValue, JsValue> {
194        let key = designator.to_uppercase();
195        let records = self
196            .airspace_index
197            .get(&key)
198            .into_iter()
199            .flat_map(|indices| indices.iter().copied())
200            .filter_map(|idx| self.airspaces.get(idx).cloned())
201            .collect::<Vec<_>>();
202
203        serde_wasm_bindgen::to_value(&compose_airspace(records)).map_err(|e| JsValue::from_str(&e.to_string()))
204    }
205
206    /// Parse and resolve a raw ICAO field 15 route string into geographic segments.
207    ///
208    /// Same contract as `EurocontrolResolver::enrichRoute` — returns a JS array of
209    /// `{ start, end, name? }` segment objects resolved against the FAA NASR nav data.
210    #[wasm_bindgen(js_name = enrichRoute)]
211    pub fn enrich_route(&self, route: String) -> Result<JsValue, JsValue> {
212        use crate::field15::ResolvedPoint as WasmPoint;
213        use crate::field15::RouteSegment;
214        use thrust::data::field15::{Connector, Field15Element, Field15Parser, Point};
215
216        let elements = Field15Parser::parse(&route);
217        let mut segments: Vec<RouteSegment> = Vec::new();
218        let mut last_point: Option<WasmPoint> = None;
219        let mut pending_airway: Option<(String, WasmPoint)> = None;
220        let mut current_connector: Option<String> = None;
221
222        let resolve_code = |code: &str| -> Option<WasmPoint> {
223            let key = code.to_uppercase();
224            if let Some(idx) = self.airport_index.get(&key).and_then(|v| v.first()) {
225                if let Some(a) = self.airports.get(*idx) {
226                    return Some(WasmPoint {
227                        latitude: a.latitude,
228                        longitude: a.longitude,
229                        name: Some(a.code.clone()),
230                        kind: Some("airport".to_string()),
231                    });
232                }
233            }
234            if let Some(idx) = self.navaid_index.get(&key).and_then(|v| v.first()) {
235                if let Some(n) = self.navaids.get(*idx) {
236                    return Some(WasmPoint {
237                        latitude: n.latitude,
238                        longitude: n.longitude,
239                        name: Some(n.code.clone()),
240                        kind: Some(n.kind.clone()),
241                    });
242                }
243            }
244            None
245        };
246
247        let expand_airway =
248            |airway_name: &str, entry: &WasmPoint, exit: &WasmPoint, segs: &mut Vec<RouteSegment>| -> bool {
249                let key = crate::models::normalize_airway_name(airway_name);
250                let airway = match self
251                    .airway_index
252                    .get(&key)
253                    .and_then(|v| v.first())
254                    .and_then(|i| self.airways.get(*i))
255                {
256                    Some(a) => a,
257                    None => return false,
258                };
259                let pts = &airway.points;
260                let entry_name = entry.name.as_deref().unwrap_or("").to_uppercase();
261                let exit_name = exit.name.as_deref().unwrap_or("").to_uppercase();
262                let entry_pos = pts.iter().position(|p| p.code.to_uppercase() == entry_name);
263                let exit_pos = pts.iter().position(|p| p.code.to_uppercase() == exit_name);
264                let (from, to) = match (entry_pos, exit_pos) {
265                    (Some(f), Some(t)) => (f, t),
266                    _ => return false,
267                };
268                let slice: Vec<&crate::models::AirwayPointRecord> = if from <= to {
269                    pts[from..=to].iter().collect()
270                } else {
271                    pts[to..=from].iter().rev().collect()
272                };
273                if slice.len() < 2 {
274                    return false;
275                }
276                let mut prev = entry.clone();
277                for pt in &slice[1..] {
278                    let next = WasmPoint {
279                        latitude: pt.latitude,
280                        longitude: pt.longitude,
281                        name: Some(pt.code.clone()),
282                        kind: Some(pt.kind.clone()),
283                    };
284                    segs.push(RouteSegment {
285                        start: prev,
286                        end: next.clone(),
287                        name: Some(airway_name.to_string()),
288                    });
289                    prev = next;
290                }
291                true
292            };
293
294        for element in &elements {
295            match element {
296                Field15Element::Point(point) => {
297                    let resolved = match point {
298                        Point::Waypoint(name) | Point::Aerodrome(name) => resolve_code(name),
299                        Point::Coordinates((lat, lon)) => Some(WasmPoint {
300                            latitude: *lat,
301                            longitude: *lon,
302                            name: None,
303                            kind: Some("coords".to_string()),
304                        }),
305                        Point::BearingDistance { point, .. } => match point.as_ref() {
306                            Point::Waypoint(name) | Point::Aerodrome(name) => resolve_code(name),
307                            Point::Coordinates((lat, lon)) => Some(WasmPoint {
308                                latitude: *lat,
309                                longitude: *lon,
310                                name: None,
311                                kind: Some("coords".to_string()),
312                            }),
313                            _ => None,
314                        },
315                    };
316                    if let Some(exit) = resolved {
317                        if let Some((airway_name, entry)) = pending_airway.take() {
318                            let expanded = expand_airway(&airway_name, &entry, &exit, &mut segments);
319                            if !expanded {
320                                segments.push(RouteSegment {
321                                    start: entry,
322                                    end: exit.clone(),
323                                    name: Some(airway_name),
324                                });
325                            }
326                        } else if let Some(prev) = last_point.take() {
327                            segments.push(RouteSegment {
328                                start: prev,
329                                end: exit.clone(),
330                                name: current_connector.take(),
331                            });
332                        } else {
333                            current_connector = None;
334                        }
335                        last_point = Some(exit);
336                    }
337                }
338                Field15Element::Connector(connector) => match connector {
339                    Connector::Airway(name) => {
340                        if let Some(entry) = last_point.take() {
341                            pending_airway = Some((name.clone(), entry));
342                        } else {
343                            current_connector = Some(name.clone());
344                        }
345                    }
346                    Connector::Direct => {
347                        current_connector = None;
348                    }
349                    Connector::Sid(name) | Connector::Star(name) => {
350                        current_connector = Some(name.clone());
351                    }
352                    _ => {}
353                },
354                Field15Element::Modifier(_) => {}
355            }
356        }
357
358        serde_wasm_bindgen::to_value(&segments).map_err(|e| JsValue::from_str(&e.to_string()))
359    }
360}