Skip to main content

thrust_py/
airspaces.rs

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}