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 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 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 #[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}