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