1use pyo3::{exceptions::PyOSError, prelude::*, types::PyDict};
2use serde_json::Value;
3use std::path::PathBuf;
4use thrust::data::eurocontrol::aixm::airspace::parse_airspace_zip_file;
5use thrust::data::eurocontrol::ddr::airspaces::{parse_fra_layers_path, parse_sector_layers_path, DdrSectorLayer};
6use thrust::data::faa::arcgis::{
7 parse_faa_airspace_boundary, parse_faa_class_airspace, parse_faa_prohibited_airspace, parse_faa_route_airspace,
8 parse_faa_special_use_airspace, FaaFeature,
9};
10use thrust::data::faa::nasr::parse_airspaces_from_nasr_bytes;
11
12#[pyclass(get_all)]
13#[derive(Debug, Clone)]
14pub struct AirspaceRecord {
15 designator: String,
16 name: Option<String>,
17 type_: Option<String>,
18 lower: Option<f64>,
19 upper: Option<f64>,
20 coordinates: Vec<(f64, f64)>,
21 source: String,
22}
23
24#[pymethods]
25impl AirspaceRecord {
26 fn to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
27 let d = PyDict::new(py);
28 d.set_item("designator", &self.designator)?;
29 d.set_item("source", &self.source)?;
30 d.set_item("coordinates", &self.coordinates)?;
31 if let Some(name) = &self.name {
32 d.set_item("name", name)?;
33 }
34 if let Some(type_) = &self.type_ {
35 d.set_item("type", type_)?;
36 }
37 if let Some(lower) = self.lower {
38 d.set_item("lower", lower)?;
39 }
40 if let Some(upper) = self.upper {
41 d.set_item("upper", upper)?;
42 }
43 Ok(d.into())
44 }
45}
46
47#[pyclass]
48pub struct AixmAirspacesSource {
49 airspaces: Vec<AirspaceRecord>,
50}
51
52#[pymethods]
53impl AixmAirspacesSource {
54 #[new]
55 fn new(path: PathBuf) -> PyResult<Self> {
56 let zip_path = path.join("Airspace.BASELINE.zip");
57 let parsed = parse_airspace_zip_file(zip_path).map_err(|e| PyOSError::new_err(e.to_string()))?;
58
59 let mut airspaces = Vec::new();
60 for (_id, airspace) in parsed {
61 let designator = airspace
62 .designator
63 .clone()
64 .unwrap_or_else(|| airspace.identifier.clone());
65 for volume in airspace.volumes {
66 if volume.polygon.len() < 3 {
67 continue;
68 }
69 let coordinates = volume
70 .polygon
71 .into_iter()
72 .map(|(lat, lon)| (lon, lat))
73 .collect::<Vec<_>>();
74
75 airspaces.push(AirspaceRecord {
76 designator: designator.clone(),
77 name: airspace.name.clone(),
78 type_: airspace.type_.clone(),
79 lower: volume.lower_limit.and_then(|v| v.parse::<f64>().ok()),
80 upper: volume.upper_limit.and_then(|v| v.parse::<f64>().ok()),
81 coordinates,
82 source: "eurocontrol_aixm".to_string(),
83 });
84 }
85 }
86
87 Ok(Self { airspaces })
88 }
89
90 fn list_airspaces(&self) -> Vec<AirspaceRecord> {
91 self.airspaces.clone()
92 }
93
94 fn resolve_airspace(&self, designator: String) -> Vec<AirspaceRecord> {
95 let key = designator.to_uppercase();
96 self.airspaces
97 .iter()
98 .filter(|a| a.designator.to_uppercase() == key)
99 .cloned()
100 .collect()
101 }
102}
103
104#[pyclass]
105pub struct DdrAirspacesSource {
106 airspaces: Vec<AirspaceRecord>,
107}
108
109#[pyclass]
110pub struct AixmFraAirspacesSource {
111 airspaces: Vec<AirspaceRecord>,
112}
113
114#[pymethods]
115impl AixmFraAirspacesSource {
116 #[new]
117 fn new(path: PathBuf) -> PyResult<Self> {
118 let base = AixmAirspacesSource::new(path)?;
119 let airspaces = base
120 .airspaces
121 .into_iter()
122 .filter(|a| {
123 let d = a.designator.to_uppercase();
124 let n = a.name.clone().unwrap_or_default().to_uppercase();
125 let t = a.type_.clone().unwrap_or_default().to_uppercase();
126 d.contains("FRA") || n.contains("FRA") || t.contains("FRA")
127 })
128 .collect();
129 Ok(Self { airspaces })
130 }
131
132 fn list_airspaces(&self) -> Vec<AirspaceRecord> {
133 self.airspaces.clone()
134 }
135
136 fn resolve_airspace(&self, designator: String) -> Vec<AirspaceRecord> {
137 let key = designator.to_uppercase();
138 self.airspaces
139 .iter()
140 .filter(|a| a.designator.to_uppercase() == key)
141 .cloned()
142 .collect()
143 }
144}
145
146#[pyclass]
147pub struct DdrFraAirspacesSource {
148 airspaces: Vec<AirspaceRecord>,
149}
150
151#[pymethods]
152impl DdrFraAirspacesSource {
153 #[new]
154 fn new(path: PathBuf) -> PyResult<Self> {
155 let layers = parse_fra_layers_path(path).map_err(|e| PyOSError::new_err(e.to_string()))?;
156
157 let airspaces = layers
158 .into_iter()
159 .map(|layer: DdrSectorLayer| AirspaceRecord {
160 designator: layer.designator,
161 name: None,
162 type_: Some("FRA".to_string()),
163 lower: Some(layer.lower),
164 upper: Some(layer.upper),
165 coordinates: layer.coordinates,
166 source: "eurocontrol_ddr".to_string(),
167 })
168 .collect();
169
170 Ok(Self { airspaces })
171 }
172
173 fn list_airspaces(&self) -> Vec<AirspaceRecord> {
174 self.airspaces.clone()
175 }
176
177 fn resolve_airspace(&self, designator: String) -> Vec<AirspaceRecord> {
178 let key = designator.to_uppercase();
179 self.airspaces
180 .iter()
181 .filter(|a| a.designator.to_uppercase() == key)
182 .cloned()
183 .collect()
184 }
185}
186
187#[pymethods]
188impl DdrAirspacesSource {
189 #[new]
190 fn new(path: PathBuf) -> PyResult<Self> {
191 let layers = parse_sector_layers_path(path).map_err(|e| PyOSError::new_err(e.to_string()))?;
192
193 let airspaces = layers
194 .into_iter()
195 .map(|layer: DdrSectorLayer| AirspaceRecord {
196 designator: layer.designator,
197 name: None,
198 type_: None,
199 lower: Some(layer.lower),
200 upper: Some(layer.upper),
201 coordinates: layer.coordinates,
202 source: "eurocontrol_ddr".to_string(),
203 })
204 .collect();
205
206 Ok(Self { airspaces })
207 }
208
209 fn list_airspaces(&self) -> Vec<AirspaceRecord> {
210 self.airspaces.clone()
211 }
212
213 fn resolve_airspace(&self, designator: String) -> Vec<AirspaceRecord> {
214 let key = designator.to_uppercase();
215 self.airspaces
216 .iter()
217 .filter(|a| a.designator.to_uppercase() == key)
218 .cloned()
219 .collect()
220 }
221}
222
223fn value_to_f64(v: Option<&Value>) -> Option<f64> {
224 v.and_then(|x| x.as_f64().or_else(|| x.as_i64().map(|n| n as f64)))
225}
226
227fn value_to_string(v: Option<&Value>) -> Option<String> {
228 v.and_then(|x| x.as_str().map(|s| s.to_string()))
229}
230
231fn geometry_to_polygons(geometry: &Value) -> Vec<Vec<(f64, f64)>> {
232 let gtype = geometry.get("type").and_then(|v| v.as_str());
233 let coords = geometry.get("coordinates");
234
235 match (gtype, coords) {
236 (Some("Polygon"), Some(c)) => c
237 .as_array()
238 .and_then(|rings| rings.first().cloned())
239 .and_then(|ring| ring.as_array().cloned())
240 .map(|ring| {
241 ring.into_iter()
242 .filter_map(|pt| {
243 let arr = pt.as_array()?;
244 if arr.len() < 2 {
245 return None;
246 }
247 let lon = arr[0].as_f64()?;
248 let lat = arr[1].as_f64()?;
249 Some((lon, lat))
250 })
251 .collect::<Vec<_>>()
252 })
253 .into_iter()
254 .collect(),
255 (Some("MultiPolygon"), Some(c)) => c
256 .as_array()
257 .map(|polys| {
258 polys
259 .iter()
260 .filter_map(|poly| {
261 let ring = poly.as_array()?.first()?.as_array()?;
262 Some(
263 ring.iter()
264 .filter_map(|pt| {
265 let arr = pt.as_array()?;
266 if arr.len() < 2 {
267 return None;
268 }
269 let lon = arr[0].as_f64()?;
270 let lat = arr[1].as_f64()?;
271 Some((lon, lat))
272 })
273 .collect::<Vec<_>>(),
274 )
275 })
276 .collect::<Vec<_>>()
277 })
278 .unwrap_or_default(),
279 _ => vec![],
280 }
281}
282
283fn features_to_airspaces(features: Vec<FaaFeature>) -> Vec<AirspaceRecord> {
284 let mut out = Vec::new();
285 for feature in features {
286 let properties = feature.properties;
287 let polygons = geometry_to_polygons(&feature.geometry);
288 if polygons.is_empty() {
289 continue;
290 }
291
292 let designator = value_to_string(properties.get("IDENT"))
293 .or_else(|| value_to_string(properties.get("NAME")))
294 .unwrap_or_else(|| "UNKNOWN".to_string());
295 let name = value_to_string(properties.get("NAME"));
296 let type_ = value_to_string(properties.get("TYPE_CODE"));
297 let lower =
298 value_to_f64(properties.get("LOWER_VAL")).map(|v| if (v + 9998.0).abs() < f64::EPSILON { 0.0 } else { v });
299 let upper = value_to_f64(properties.get("UPPER_VAL")).map(|v| {
300 if (v + 9998.0).abs() < f64::EPSILON {
301 f64::INFINITY
302 } else {
303 v
304 }
305 });
306
307 for polygon in polygons {
308 if polygon.len() < 3 {
309 continue;
310 }
311 out.push(AirspaceRecord {
312 designator: designator.clone(),
313 name: name.clone(),
314 type_: type_.clone(),
315 lower,
316 upper,
317 coordinates: polygon,
318 source: "faa_arcgis".to_string(),
319 });
320 }
321 }
322 out
323}
324
325#[pyclass]
326pub struct FaaAirspacesSource {
327 airspaces: Vec<AirspaceRecord>,
328}
329
330#[pyclass]
331pub struct NasrAirspacesSource {
332 airspaces: Vec<AirspaceRecord>,
333}
334
335#[pymethods]
336impl NasrAirspacesSource {
337 #[new]
338 fn new(path: PathBuf) -> PyResult<Self> {
339 let bytes = std::fs::read(path).map_err(|e| PyOSError::new_err(e.to_string()))?;
340 let parsed = parse_airspaces_from_nasr_bytes(&bytes).map_err(|e| PyOSError::new_err(e.to_string()))?;
341
342 let airspaces = parsed
343 .into_iter()
344 .filter(|a| a.coordinates.len() >= 3)
345 .map(|a| AirspaceRecord {
346 designator: a.designator,
347 name: a.name,
348 type_: a.type_,
349 lower: a.lower,
350 upper: a.upper,
351 coordinates: a.coordinates,
352 source: "faa_nasr".to_string(),
353 })
354 .collect();
355
356 Ok(Self { airspaces })
357 }
358
359 fn list_airspaces(&self) -> Vec<AirspaceRecord> {
360 self.airspaces.clone()
361 }
362
363 fn resolve_airspace(&self, designator: String) -> Vec<AirspaceRecord> {
364 let key = designator.to_uppercase();
365 self.airspaces
366 .iter()
367 .filter(|a| a.designator.to_uppercase() == key)
368 .cloned()
369 .collect()
370 }
371}
372
373#[pymethods]
374impl FaaAirspacesSource {
375 #[new]
376 #[pyo3(signature = (path=None))]
377 fn new(path: Option<PathBuf>) -> PyResult<Self> {
378 let features: Vec<FaaFeature> = if let Some(root) = path {
379 let names = [
380 "faa_airspace_boundary.json",
381 "faa_class_airspace.json",
382 "faa_special_use_airspace.json",
383 "faa_route_airspace.json",
384 "faa_prohibited_airspace.json",
385 ];
386
387 let mut all = Vec::new();
388 for filename in names {
389 let path = root.join(filename);
390 if !path.exists() {
391 continue;
392 }
393 let file = std::fs::File::open(path).map_err(|e| PyOSError::new_err(e.to_string()))?;
394 let payload: Value = serde_json::from_reader(file).map_err(|e| PyOSError::new_err(e.to_string()))?;
395 let features = payload
396 .get("features")
397 .and_then(|x| x.as_array())
398 .map(|arr| {
399 arr.iter()
400 .map(|feature| FaaFeature {
401 properties: feature.get("properties").cloned().unwrap_or(Value::Null),
402 geometry: feature.get("geometry").cloned().unwrap_or(Value::Null),
403 })
404 .collect::<Vec<_>>()
405 })
406 .unwrap_or_default();
407 all.extend(features);
408 }
409 all
410 } else {
411 let mut all = Vec::new();
412 all.extend(parse_faa_airspace_boundary().map_err(|e| PyOSError::new_err(e.to_string()))?);
413 all.extend(parse_faa_class_airspace().map_err(|e| PyOSError::new_err(e.to_string()))?);
414 all.extend(parse_faa_special_use_airspace().map_err(|e| PyOSError::new_err(e.to_string()))?);
415 all.extend(parse_faa_route_airspace().map_err(|e| PyOSError::new_err(e.to_string()))?);
416 all.extend(parse_faa_prohibited_airspace().map_err(|e| PyOSError::new_err(e.to_string()))?);
417 all
418 };
419
420 let airspaces = features_to_airspaces(features);
421 Ok(Self { airspaces })
422 }
423
424 fn list_airspaces(&self) -> Vec<AirspaceRecord> {
425 self.airspaces.clone()
426 }
427
428 fn resolve_airspace(&self, designator: String) -> Vec<AirspaceRecord> {
429 let key = designator.to_uppercase();
430 self.airspaces
431 .iter()
432 .filter(|a| a.designator.to_uppercase() == key)
433 .cloned()
434 .collect()
435 }
436}
437
438pub fn init(py: Python<'_>) -> PyResult<Bound<'_, PyModule>> {
439 let m = PyModule::new(py, "airspaces")?;
440 m.add_class::<AirspaceRecord>()?;
441 m.add_class::<AixmAirspacesSource>()?;
442 m.add_class::<AixmFraAirspacesSource>()?;
443 m.add_class::<DdrAirspacesSource>()?;
444 m.add_class::<DdrFraAirspacesSource>()?;
445 m.add_class::<FaaAirspacesSource>()?;
446 m.add_class::<NasrAirspacesSource>()?;
447 Ok(m)
448}