1use crate::error::ThrustError;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5#[cfg(feature = "net")]
6const OPENDATA_BASE: &str = "https://opendata.arcgis.com/datasets";
7
8const ATS_ROUTE_DATASET: &str = "acf64966af5f48a1a40fdbcb31238ba7_0";
9const DESIGNATED_POINTS_DATASET: &str = "861043a88ff4486c97c3789e7dcdccc6_0";
10const NAVAID_COMPONENTS_DATASET: &str = "c9254c171b6741d3a5e494860761443a_0";
11const AIRSPACE_BOUNDARY_DATASET: &str = "67885972e4e940b2aa6d74024901c561_0";
12const CLASS_AIRSPACE_DATASET: &str = "c6a62360338e408cb1512366ad61559e_0";
13const SPECIAL_USE_AIRSPACE_DATASET: &str = "dd0d1b726e504137ab3c41b21835d05b_0";
14const ROUTE_AIRSPACE_DATASET: &str = "8bf861bb9b414f4ea9f0ff2ca0f1a851_0";
15const PROHIBITED_AIRSPACE_DATASET: &str = "354ee0c77484461198ebf728a2fca50c_0";
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct FaaFeature {
38 pub properties: Value,
39 pub geometry: Value,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
65pub struct FaaOpenData {
66 pub ats_routes: Vec<FaaFeature>,
67 pub designated_points: Vec<FaaFeature>,
68 pub navaid_components: Vec<FaaFeature>,
69 pub airspace_boundary: Vec<FaaFeature>,
70 pub class_airspace: Vec<FaaFeature>,
71 pub special_use_airspace: Vec<FaaFeature>,
72 pub route_airspace: Vec<FaaFeature>,
73 pub prohibited_airspace: Vec<FaaFeature>,
74}
75
76fn fetch_geojson(dataset_id: &str) -> Result<Vec<FaaFeature>, ThrustError> {
77 #[cfg(not(feature = "net"))]
78 {
79 let _ = dataset_id;
80 Err("FAA ArcGIS network fetch is disabled; enable feature 'net'".into())
81 }
82
83 #[cfg(feature = "net")]
84 {
85 let url = format!("{OPENDATA_BASE}/{dataset_id}.geojson");
86 let payload = reqwest::blocking::get(url)?.error_for_status()?.json::<Value>()?;
87
88 let features = payload
89 .get("features")
90 .and_then(|x| x.as_array())
91 .map(|arr| {
92 arr.iter()
93 .map(|feature| FaaFeature {
94 properties: feature.get("properties").cloned().unwrap_or(Value::Null),
95 geometry: feature.get("geometry").cloned().unwrap_or(Value::Null),
96 })
97 .collect::<Vec<_>>()
98 })
99 .unwrap_or_default();
100
101 Ok(features)
102 }
103}
104
105pub fn parse_faa_ats_routes() -> Result<Vec<FaaFeature>, ThrustError> {
106 fetch_geojson(ATS_ROUTE_DATASET)
107}
108
109pub fn parse_faa_designated_points() -> Result<Vec<FaaFeature>, ThrustError> {
110 fetch_geojson(DESIGNATED_POINTS_DATASET)
111}
112
113pub fn parse_faa_navaid_components() -> Result<Vec<FaaFeature>, ThrustError> {
114 fetch_geojson(NAVAID_COMPONENTS_DATASET)
115}
116
117pub fn parse_faa_airspace_boundary() -> Result<Vec<FaaFeature>, ThrustError> {
118 fetch_geojson(AIRSPACE_BOUNDARY_DATASET)
119}
120
121pub fn parse_faa_class_airspace() -> Result<Vec<FaaFeature>, ThrustError> {
122 fetch_geojson(CLASS_AIRSPACE_DATASET)
123}
124
125pub fn parse_faa_special_use_airspace() -> Result<Vec<FaaFeature>, ThrustError> {
126 fetch_geojson(SPECIAL_USE_AIRSPACE_DATASET)
127}
128
129pub fn parse_faa_route_airspace() -> Result<Vec<FaaFeature>, ThrustError> {
130 fetch_geojson(ROUTE_AIRSPACE_DATASET)
131}
132
133pub fn parse_faa_prohibited_airspace() -> Result<Vec<FaaFeature>, ThrustError> {
134 fetch_geojson(PROHIBITED_AIRSPACE_DATASET)
135}
136
137pub fn parse_all_faa_open_data() -> Result<FaaOpenData, ThrustError> {
138 Ok(FaaOpenData {
139 ats_routes: parse_faa_ats_routes()?,
140 designated_points: parse_faa_designated_points()?,
141 navaid_components: parse_faa_navaid_components()?,
142 airspace_boundary: parse_faa_airspace_boundary()?,
143 class_airspace: parse_faa_class_airspace()?,
144 special_use_airspace: parse_faa_special_use_airspace()?,
145 route_airspace: parse_faa_route_airspace()?,
146 prohibited_airspace: parse_faa_prohibited_airspace()?,
147 })
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ArcgisAirportRecord {
152 pub code: String,
153 pub iata: Option<String>,
154 pub icao: Option<String>,
155 pub name: Option<String>,
156 pub latitude: f64,
157 pub longitude: f64,
158 pub region: Option<String>,
159 pub source: String,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ArcgisNavpointRecord {
164 pub code: String,
165 pub identifier: String,
166 pub kind: String,
167 pub name: Option<String>,
168 pub latitude: f64,
169 pub longitude: f64,
170 pub description: Option<String>,
171 pub frequency: Option<f64>,
172 pub point_type: Option<String>,
173 pub region: Option<String>,
174 pub source: String,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ArcgisAirwayPointRecord {
179 pub code: String,
180 pub raw_code: String,
181 pub kind: String,
182 pub latitude: f64,
183 pub longitude: f64,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct ArcgisAirwayRecord {
188 pub name: String,
189 pub source: String,
190 pub route_class: Option<String>,
191 pub points: Vec<ArcgisAirwayPointRecord>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ArcgisAirspaceRecord {
196 pub designator: String,
197 pub name: Option<String>,
198 pub type_: Option<String>,
199 pub lower: Option<f64>,
200 pub upper: Option<f64>,
201 pub coordinates: Vec<(f64, f64)>,
202 pub source: String,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, Default)]
206pub struct ArcgisDataset {
207 pub airports: Vec<ArcgisAirportRecord>,
208 pub navaids: Vec<ArcgisNavpointRecord>,
209 pub airways: Vec<ArcgisAirwayRecord>,
210 pub airspaces: Vec<ArcgisAirspaceRecord>,
211}
212
213pub fn parse_arcgis_features(features: &[Value]) -> ArcgisDataset {
214 let airports = arcgis_features_to_airports(features);
215 let airspaces = arcgis_features_to_airspaces(features);
216 let (fixes, mut navaids) = arcgis_features_to_navpoints(features);
217 navaids.extend(fixes.iter().cloned());
218 navaids.sort_by(|a, b| a.code.cmp(&b.code).then(a.point_type.cmp(&b.point_type)));
219 navaids.dedup_by(|a, b| {
220 a.code == b.code && a.point_type == b.point_type && a.latitude == b.latitude && a.longitude == b.longitude
221 });
222 let airways = arcgis_features_to_airways(features);
223
224 ArcgisDataset {
225 airports,
226 navaids,
227 airways,
228 airspaces,
229 }
230}
231
232fn value_to_f64(v: Option<&Value>) -> Option<f64> {
233 v.and_then(|x| x.as_f64().or_else(|| x.as_i64().map(|n| n as f64)))
234}
235
236fn parse_coord(value: Option<&Value>) -> Option<f64> {
237 let value = value?;
238 if let Some(v) = value.as_f64() {
239 return Some(v);
240 }
241 let s = value.as_str()?.trim();
242 let hemi = s.chars().last()?;
243 let sign = match hemi {
244 'N' | 'E' => 1.0,
245 'S' | 'W' => -1.0,
246 _ => 1.0,
247 };
248 let core = s.strip_suffix(hemi).unwrap_or(s);
249 let parts: Vec<&str> = core.split('-').collect();
250 if parts.len() != 3 {
251 return core.parse::<f64>().ok();
252 }
253 let deg = parts[0].parse::<f64>().ok()?;
254 let min = parts[1].parse::<f64>().ok()?;
255 let sec = parts[2].parse::<f64>().ok()?;
256 Some(sign * (deg + min / 60.0 + sec / 3600.0))
257}
258
259fn value_to_string(v: Option<&Value>) -> Option<String> {
260 v.and_then(|x| x.as_str().map(|s| s.to_string()))
261}
262
263fn value_to_i64(v: Option<&Value>) -> Option<i64> {
264 v.and_then(|x| x.as_i64().or_else(|| x.as_f64().map(|n| n as i64)))
265}
266
267fn geometry_to_polygons(geometry: &Value) -> Vec<Vec<(f64, f64)>> {
268 let gtype = geometry.get("type").and_then(|v| v.as_str());
269 let coords = geometry.get("coordinates");
270
271 match (gtype, coords) {
272 (Some("Polygon"), Some(c)) => c
273 .as_array()
274 .and_then(|rings| rings.first())
275 .and_then(|ring| ring.as_array())
276 .map(|ring| {
277 ring.iter()
278 .filter_map(|pt| {
279 let arr = pt.as_array()?;
280 if arr.len() < 2 {
281 return None;
282 }
283 let lon = arr[0].as_f64()?;
284 let lat = arr[1].as_f64()?;
285 Some((lon, lat))
286 })
287 .collect::<Vec<_>>()
288 })
289 .into_iter()
290 .collect(),
291 (Some("MultiPolygon"), Some(c)) => c
292 .as_array()
293 .map(|polys| {
294 polys
295 .iter()
296 .filter_map(|poly| {
297 let ring = poly.as_array()?.first()?.as_array()?;
298 Some(
299 ring.iter()
300 .filter_map(|pt| {
301 let arr = pt.as_array()?;
302 if arr.len() < 2 {
303 return None;
304 }
305 let lon = arr[0].as_f64()?;
306 let lat = arr[1].as_f64()?;
307 Some((lon, lat))
308 })
309 .collect::<Vec<_>>(),
310 )
311 })
312 .collect::<Vec<_>>()
313 })
314 .unwrap_or_default(),
315 _ => vec![],
316 }
317}
318
319fn arcgis_features_to_airspaces(features: &[Value]) -> Vec<ArcgisAirspaceRecord> {
320 let mut out = Vec::new();
321 for feature in features {
322 let properties = feature.get("properties").unwrap_or(&Value::Null);
323 let geometry = feature.get("geometry").unwrap_or(&Value::Null);
324 let polygons = geometry_to_polygons(geometry);
325 if polygons.is_empty() {
326 continue;
327 }
328
329 let designator = value_to_string(properties.get("IDENT"))
330 .or_else(|| value_to_string(properties.get("NAME")))
331 .unwrap_or_else(|| "UNKNOWN".to_string());
332 let name = value_to_string(properties.get("NAME"));
333 let type_ = value_to_string(properties.get("TYPE_CODE"));
334 let lower =
335 value_to_f64(properties.get("LOWER_VAL")).map(|v| if (v + 9998.0).abs() < f64::EPSILON { 0.0 } else { v });
336 let upper = value_to_f64(properties.get("UPPER_VAL")).map(|v| {
337 if (v + 9998.0).abs() < f64::EPSILON {
338 f64::INFINITY
339 } else {
340 v
341 }
342 });
343
344 for coords in polygons {
345 if coords.len() < 3 {
346 continue;
347 }
348 out.push(ArcgisAirspaceRecord {
349 designator: designator.clone(),
350 name: name.clone(),
351 type_: type_.clone(),
352 lower,
353 upper,
354 coordinates: coords,
355 source: "faa_arcgis".to_string(),
356 });
357 }
358 }
359 out
360}
361
362fn arcgis_features_to_navpoints(features: &[Value]) -> (Vec<ArcgisNavpointRecord>, Vec<ArcgisNavpointRecord>) {
363 let mut fixes = Vec::new();
364 let mut navaid_groups: std::collections::HashMap<String, ArcgisNavpointRecord> = std::collections::HashMap::new();
365 let mut navaid_components: std::collections::HashMap<String, (bool, bool, bool, bool)> =
366 std::collections::HashMap::new();
367
368 for feature in features {
369 let props = feature.get("properties").unwrap_or(&Value::Null);
370 let ident = value_to_string(props.get("IDENT")).unwrap_or_default().to_uppercase();
371 if ident.is_empty() {
372 continue;
373 }
374
375 if props.get("NAV_TYPE").is_some() || props.get("FREQUENCY").is_some() {
376 let latitude = parse_coord(props.get("LATITUDE")).unwrap_or(0.0);
377 let longitude = parse_coord(props.get("LONGITUDE")).unwrap_or(0.0);
378 let group_key = value_to_string(props.get("NAVSYS_ID"))
379 .filter(|s| !s.is_empty())
380 .unwrap_or_else(|| ident.clone());
381
382 navaid_groups
383 .entry(group_key.clone())
384 .or_insert_with(|| ArcgisNavpointRecord {
385 code: ident.clone(),
386 identifier: ident,
387 kind: "navaid".to_string(),
388 name: value_to_string(props.get("NAME")),
389 latitude,
390 longitude,
391 description: value_to_string(props.get("NAME")),
392 frequency: value_to_f64(props.get("FREQUENCY")),
393 point_type: value_to_string(props.get("TYPE_CODE")),
394 region: value_to_string(props.get("US_AREA")),
395 source: "faa_arcgis".to_string(),
396 });
397
398 let entry = navaid_components
399 .entry(group_key)
400 .or_insert((false, false, false, false));
401 match value_to_i64(props.get("NAV_TYPE")) {
402 Some(1) => entry.0 = true,
403 Some(2) => entry.1 = true,
404 Some(3) => entry.2 = true,
405 Some(4) => entry.3 = true,
406 _ => {}
407 }
408 } else {
409 let latitude = parse_coord(props.get("LATITUDE")).unwrap_or(0.0);
410 let longitude = parse_coord(props.get("LONGITUDE")).unwrap_or(0.0);
411 fixes.push(ArcgisNavpointRecord {
412 code: ident.clone(),
413 identifier: ident.clone(),
414 kind: "fix".to_string(),
415 name: Some(ident),
416 latitude,
417 longitude,
418 description: value_to_string(props.get("REMARKS")),
419 frequency: None,
420 point_type: value_to_string(props.get("TYPE_CODE")).map(|s| s.to_uppercase()),
421 region: value_to_string(props.get("US_AREA")).or_else(|| value_to_string(props.get("STATE"))),
422 source: "faa_arcgis".to_string(),
423 });
424 }
425 }
426
427 let mut navaids: Vec<ArcgisNavpointRecord> = navaid_groups
428 .into_iter()
429 .map(|(group_key, mut record)| {
430 if let Some((has_ndb, has_dme, has_vor, has_tacan)) = navaid_components.get(&group_key).copied() {
431 record.point_type = Some(
432 if has_vor && has_tacan {
433 "VORTAC"
434 } else if has_vor && has_dme {
435 "VOR_DME"
436 } else if has_vor {
437 "VOR"
438 } else if has_tacan {
439 "TACAN"
440 } else if has_dme {
441 "DME"
442 } else if has_ndb {
443 "NDB"
444 } else {
445 record.point_type.as_deref().unwrap_or("NAVAID")
446 }
447 .to_string(),
448 );
449 }
450 record
451 })
452 .collect();
453 navaids.sort_by(|a, b| a.code.cmp(&b.code));
454
455 (fixes, navaids)
456}
457
458fn arcgis_features_to_airports(features: &[Value]) -> Vec<ArcgisAirportRecord> {
459 let mut airports = Vec::new();
460
461 for feature in features {
462 let props = feature.get("properties").unwrap_or(&Value::Null);
463 let ident = value_to_string(props.get("IDENT")).unwrap_or_default().to_uppercase();
464 let icao = value_to_string(props.get("ICAO_ID")).map(|x| x.to_uppercase());
465 if ident.is_empty() && icao.is_none() {
466 continue;
467 }
468
469 let latitude = parse_coord(props.get("LATITUDE"));
470 let longitude = parse_coord(props.get("LONGITUDE"));
471 let (latitude, longitude) = match (latitude, longitude) {
472 (Some(lat), Some(lon)) => (lat, lon),
473 _ => continue,
474 };
475
476 let code = if ident.is_empty() {
477 icao.clone().unwrap_or_default()
478 } else {
479 ident.clone()
480 };
481 if code.is_empty() {
482 continue;
483 }
484
485 airports.push(ArcgisAirportRecord {
486 code,
487 iata: if ident.len() == 3 { Some(ident) } else { None },
488 icao,
489 name: value_to_string(props.get("NAME")),
490 latitude,
491 longitude,
492 region: value_to_string(props.get("STATE")).or_else(|| value_to_string(props.get("US_AREA"))),
493 source: "faa_arcgis".to_string(),
494 });
495 }
496
497 airports
498}
499
500fn arcgis_features_to_airways(features: &[Value]) -> Vec<ArcgisAirwayRecord> {
501 let mut grouped: std::collections::HashMap<String, Vec<ArcgisAirwayPointRecord>> = std::collections::HashMap::new();
502 let mut point_id_to_ident: std::collections::HashMap<String, String> = std::collections::HashMap::new();
503
504 for feature in features {
505 let props = feature.get("properties").unwrap_or(&Value::Null);
506 let global_id = value_to_string(props.get("GLOBAL_ID")).map(|s| s.to_uppercase());
507 let ident = value_to_string(props.get("IDENT")).map(|s| s.to_uppercase());
508 if let (Some(gid), Some(idt)) = (global_id, ident) {
509 if !gid.is_empty() && !idt.is_empty() {
510 point_id_to_ident.entry(gid).or_insert(idt);
511 }
512 }
513 }
514
515 for feature in features {
516 let props = feature.get("properties").unwrap_or(&Value::Null);
517 let name = value_to_string(props.get("IDENT")).unwrap_or_default().to_uppercase();
518 if name.is_empty() {
519 continue;
520 }
521
522 let geom = feature.get("geometry").unwrap_or(&Value::Null);
523 if geom.get("type").and_then(|x| x.as_str()) != Some("LineString") {
524 continue;
525 }
526 let coords = geom
527 .get("coordinates")
528 .and_then(|x| x.as_array())
529 .cloned()
530 .unwrap_or_default();
531
532 let start_id = value_to_string(props.get("STARTPT_ID")).map(|s| s.to_uppercase());
533 let end_id = value_to_string(props.get("ENDPT_ID")).map(|s| s.to_uppercase());
534 let start_code = start_id
535 .as_ref()
536 .and_then(|id| point_id_to_ident.get(id).cloned())
537 .or(start_id.clone());
538 let end_code = end_id
539 .as_ref()
540 .and_then(|id| point_id_to_ident.get(id).cloned())
541 .or(end_id.clone());
542
543 let entry = grouped.entry(name).or_default();
544 let coord_len = coords.len();
545 for (idx, p) in coords.into_iter().enumerate() {
546 let arr = match p.as_array() {
547 Some(v) if v.len() >= 2 => v,
548 _ => continue,
549 };
550 let lon = arr[0].as_f64().unwrap_or(0.0);
551 let lat = arr[1].as_f64().unwrap_or(0.0);
552 if entry
553 .last()
554 .map(|x| (x.latitude, x.longitude) == (lat, lon))
555 .unwrap_or(false)
556 {
557 continue;
558 }
559
560 let raw_code = if idx == 0 {
561 start_code.clone().unwrap_or_default()
562 } else if idx + 1 == coord_len {
563 end_code.clone().unwrap_or_default()
564 } else {
565 String::new()
566 };
567 let code = if raw_code.is_empty() {
568 format!("{},{}", lat, lon)
569 } else {
570 raw_code.clone()
571 };
572
573 entry.push(ArcgisAirwayPointRecord {
574 code,
575 raw_code,
576 kind: "point".to_string(),
577 latitude: lat,
578 longitude: lon,
579 });
580 }
581 }
582
583 grouped
584 .into_iter()
585 .map(|(name, points)| ArcgisAirwayRecord {
586 name,
587 source: "faa_arcgis".to_string(),
588 route_class: None,
589 points,
590 })
591 .collect()
592}