Skip to main content

thrust_wasm/
faa_arcgis.rs

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