Skip to main content

thrust_py/
navpoints.rs

1use pyo3::{exceptions::PyOSError, prelude::*, types::PyDict};
2use serde_json::Value;
3use std::fs::File;
4use std::path::PathBuf;
5use thrust::data::eurocontrol::aixm::designated_point::parse_designated_point_zip_file;
6use thrust::data::eurocontrol::aixm::navaid::parse_navaid_zip_file;
7use thrust::data::eurocontrol::ddr::navpoints::parse_navpoints_path;
8use thrust::data::faa::nasr::parse_field15_data_from_nasr_zip;
9
10#[pyclass(get_all)]
11#[derive(Debug, Clone)]
12pub struct NavpointRecord {
13    code: String,
14    kind: String,
15    latitude: f64,
16    longitude: f64,
17    name: Option<String>,
18    identifier: Option<String>,
19    point_type: Option<String>,
20    description: Option<String>,
21    frequency: Option<f64>,
22    region: Option<String>,
23    source: String,
24}
25
26#[pymethods]
27impl NavpointRecord {
28    fn to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
29        let d = PyDict::new(py);
30        d.set_item("code", &self.code)?;
31        d.set_item("kind", &self.kind)?;
32        d.set_item("latitude", self.latitude)?;
33        d.set_item("longitude", self.longitude)?;
34        d.set_item("source", &self.source)?;
35        if let Some(name) = &self.name {
36            d.set_item("name", name)?;
37        }
38        if let Some(identifier) = &self.identifier {
39            d.set_item("identifier", identifier)?;
40        }
41        if let Some(point_type) = &self.point_type {
42            d.set_item("point_type", point_type)?;
43        }
44        if let Some(description) = &self.description {
45            d.set_item("description", description)?;
46        }
47        if let Some(frequency) = self.frequency {
48            d.set_item("frequency", frequency)?;
49        }
50        if let Some(region) = &self.region {
51            d.set_item("region", region)?;
52        }
53        Ok(d.into())
54    }
55}
56
57#[pyclass]
58pub struct AixmNavpointsSource {
59    points: Vec<NavpointRecord>,
60}
61
62#[pymethods]
63impl AixmNavpointsSource {
64    #[new]
65    fn new(path: PathBuf) -> PyResult<Self> {
66        let root = path.as_path();
67        let designated = parse_designated_point_zip_file(root.join("DesignatedPoint.BASELINE.zip"))
68            .map_err(|e| PyOSError::new_err(e.to_string()))?;
69        let navaids =
70            parse_navaid_zip_file(root.join("Navaid.BASELINE.zip")).map_err(|e| PyOSError::new_err(e.to_string()))?;
71
72        let mut points: Vec<NavpointRecord> = designated
73            .into_values()
74            .map(|point| NavpointRecord {
75                code: point.designator.to_uppercase(),
76                kind: "fix".to_string(),
77                latitude: point.latitude,
78                longitude: point.longitude,
79                name: point.name,
80                identifier: Some(point.identifier),
81                point_type: Some(point.r#type),
82                description: None,
83                frequency: None,
84                region: None,
85                source: "eurocontrol_aixm".to_string(),
86            })
87            .collect();
88
89        points.extend(navaids.into_values().filter_map(|navaid| {
90            let designator = navaid.name.clone()?;
91            Some(NavpointRecord {
92                code: designator.to_uppercase(),
93                kind: "navaid".to_string(),
94                latitude: navaid.latitude,
95                longitude: navaid.longitude,
96                name: navaid.name,
97                identifier: Some(navaid.identifier),
98                point_type: Some(navaid.r#type),
99                description: navaid.description,
100                frequency: None,
101                region: None,
102                source: "eurocontrol_aixm".to_string(),
103            })
104        }));
105
106        Ok(Self { points })
107    }
108
109    fn resolve_point(&self, code: String, kind: Option<String>) -> Vec<NavpointRecord> {
110        let upper = code.to_uppercase();
111        self.points
112            .iter()
113            .filter(|record| record.code == upper)
114            .filter(|record| match &kind {
115                Some(filter) => record.kind == filter.as_str(),
116                None => true,
117            })
118            .cloned()
119            .collect()
120    }
121
122    fn list_points(&self, kind: Option<String>) -> Vec<NavpointRecord> {
123        self.points
124            .iter()
125            .filter(|record| match &kind {
126                Some(filter) => record.kind == filter.as_str(),
127                None => true,
128            })
129            .cloned()
130            .collect()
131    }
132}
133
134#[pyclass]
135pub struct NasrNavpointsSource {
136    points: Vec<NavpointRecord>,
137}
138
139#[pymethods]
140impl NasrNavpointsSource {
141    #[new]
142    fn new(path: PathBuf) -> PyResult<Self> {
143        let data = parse_field15_data_from_nasr_zip(path).map_err(|e| PyOSError::new_err(e.to_string()))?;
144
145        let points = data
146            .points
147            .into_iter()
148            .filter_map(|point| {
149                let kind = match point.kind.as_str() {
150                    "FIX" => Some("fix".to_string()),
151                    "NAVAID" => Some("navaid".to_string()),
152                    _ => None,
153                }?;
154
155                let code = point
156                    .identifier
157                    .split(':')
158                    .next()
159                    .unwrap_or(point.identifier.as_str())
160                    .to_uppercase();
161
162                Some(NavpointRecord {
163                    code,
164                    kind,
165                    latitude: point.latitude,
166                    longitude: point.longitude,
167                    name: point.name,
168                    identifier: Some(point.identifier),
169                    point_type: point.point_type.or(Some(point.kind)),
170                    description: point.description,
171                    frequency: point.frequency,
172                    region: point.region,
173                    source: "faa_nasr".to_string(),
174                })
175            })
176            .collect();
177
178        Ok(Self { points })
179    }
180
181    fn resolve_point(&self, code: String, kind: Option<String>) -> Vec<NavpointRecord> {
182        let upper = code.to_uppercase();
183        self.points
184            .iter()
185            .filter(|record| record.code == upper)
186            .filter(|record| match &kind {
187                Some(filter) => record.kind == filter.as_str(),
188                None => true,
189            })
190            .cloned()
191            .collect()
192    }
193
194    fn list_points(&self, kind: Option<String>) -> Vec<NavpointRecord> {
195        self.points
196            .iter()
197            .filter(|record| match &kind {
198                Some(filter) => record.kind == filter.as_str(),
199                None => true,
200            })
201            .cloned()
202            .collect()
203    }
204}
205
206fn value_to_f64(v: Option<&Value>) -> Option<f64> {
207    v.and_then(|x| x.as_f64().or_else(|| x.as_i64().map(|n| n as f64)))
208}
209
210fn value_to_string(v: Option<&Value>) -> Option<String> {
211    v.and_then(|x| x.as_str().map(|s| s.to_string()))
212}
213
214fn read_features(path: &std::path::Path) -> Result<Vec<Value>, Box<dyn std::error::Error>> {
215    let file = File::open(path)?;
216    let payload: Value = serde_json::from_reader(file)?;
217    Ok(payload
218        .get("features")
219        .and_then(|x| x.as_array())
220        .cloned()
221        .unwrap_or_default())
222}
223
224#[pyclass]
225pub struct FaaArcgisNavpointsSource {
226    points: Vec<NavpointRecord>,
227}
228
229#[pymethods]
230impl FaaArcgisNavpointsSource {
231    #[new]
232    fn new(path: PathBuf) -> PyResult<Self> {
233        let root = path.as_path();
234
235        let mut points = Vec::new();
236
237        let designated =
238            read_features(&root.join("faa_designated_points.json")).map_err(|e| PyOSError::new_err(e.to_string()))?;
239        for feature in designated {
240            let props = feature.get("properties").unwrap_or(&Value::Null);
241            let code = value_to_string(props.get("IDENT")).unwrap_or_default().to_uppercase();
242            if code.is_empty() {
243                continue;
244            }
245            let lat = value_to_f64(props.get("LATITUDE")).unwrap_or(0.0);
246            let lon = value_to_f64(props.get("LONGITUDE")).unwrap_or(0.0);
247            points.push(NavpointRecord {
248                code: code.clone(),
249                kind: "fix".to_string(),
250                latitude: lat,
251                longitude: lon,
252                name: Some(code),
253                identifier: value_to_string(props.get("IDENT")),
254                point_type: value_to_string(props.get("TYPE_CODE")),
255                description: value_to_string(props.get("REMARKS")),
256                frequency: None,
257                region: value_to_string(props.get("US_AREA")).or_else(|| value_to_string(props.get("STATE"))),
258                source: "faa_arcgis".to_string(),
259            });
260        }
261
262        let navaid =
263            read_features(&root.join("faa_navaid_components.json")).map_err(|e| PyOSError::new_err(e.to_string()))?;
264        for feature in navaid {
265            let props = feature.get("properties").unwrap_or(&Value::Null);
266            let code = value_to_string(props.get("IDENT")).unwrap_or_default().to_uppercase();
267            if code.is_empty() {
268                continue;
269            }
270            let lat = value_to_f64(props.get("LATITUDE")).unwrap_or(0.0);
271            let lon = value_to_f64(props.get("LONGITUDE")).unwrap_or(0.0);
272            points.push(NavpointRecord {
273                code,
274                kind: "navaid".to_string(),
275                latitude: lat,
276                longitude: lon,
277                name: value_to_string(props.get("NAME")),
278                identifier: value_to_string(props.get("IDENT")),
279                point_type: value_to_string(props.get("NAV_TYPE")).or_else(|| value_to_string(props.get("TYPE_CODE"))),
280                description: value_to_string(props.get("NAME")),
281                frequency: value_to_f64(props.get("FREQUENCY")),
282                region: value_to_string(props.get("US_AREA")),
283                source: "faa_arcgis".to_string(),
284            });
285        }
286
287        Ok(Self { points })
288    }
289
290    fn resolve_point(&self, code: String, kind: Option<String>) -> Vec<NavpointRecord> {
291        let upper = code.to_uppercase();
292        self.points
293            .iter()
294            .filter(|record| record.code == upper)
295            .filter(|record| match &kind {
296                Some(filter) => record.kind == filter.as_str(),
297                None => true,
298            })
299            .cloned()
300            .collect()
301    }
302
303    fn list_points(&self, kind: Option<String>) -> Vec<NavpointRecord> {
304        self.points
305            .iter()
306            .filter(|record| match &kind {
307                Some(filter) => record.kind == filter.as_str(),
308                None => true,
309            })
310            .cloned()
311            .collect()
312    }
313}
314
315#[pyclass]
316pub struct DdrNavpointsSource {
317    points: Vec<NavpointRecord>,
318}
319
320#[pymethods]
321impl DdrNavpointsSource {
322    #[new]
323    fn new(path: PathBuf) -> PyResult<Self> {
324        let parsed = parse_navpoints_path(path).map_err(|e| PyOSError::new_err(e.to_string()))?;
325        let points = parsed
326            .into_iter()
327            .map(|point| {
328                let kind = {
329                    let t = point.point_type.to_uppercase();
330                    if t.contains("FIX") || t == "WPT" {
331                        "fix".to_string()
332                    } else {
333                        "navaid".to_string()
334                    }
335                };
336                NavpointRecord {
337                    code: point.name.to_uppercase(),
338                    kind,
339                    latitude: point.latitude,
340                    longitude: point.longitude,
341                    name: Some(point.name.clone()),
342                    identifier: Some(point.name),
343                    point_type: Some(point.point_type),
344                    description: point.description,
345                    frequency: None,
346                    region: None,
347                    source: "eurocontrol_ddr".to_string(),
348                }
349            })
350            .collect();
351
352        Ok(Self { points })
353    }
354
355    fn resolve_point(&self, code: String, kind: Option<String>) -> Vec<NavpointRecord> {
356        let upper = code.to_uppercase();
357        self.points
358            .iter()
359            .filter(|record| record.code == upper)
360            .filter(|record| match &kind {
361                Some(filter) => record.kind == filter.as_str(),
362                None => true,
363            })
364            .cloned()
365            .collect()
366    }
367
368    fn list_points(&self, kind: Option<String>) -> Vec<NavpointRecord> {
369        self.points
370            .iter()
371            .filter(|record| match &kind {
372                Some(filter) => record.kind == filter.as_str(),
373                None => true,
374            })
375            .cloned()
376            .collect()
377    }
378}
379
380pub fn init(py: Python<'_>) -> PyResult<Bound<'_, PyModule>> {
381    let m = PyModule::new(py, "navpoints")?;
382    m.add_class::<NavpointRecord>()?;
383    m.add_class::<AixmNavpointsSource>()?;
384    m.add_class::<NasrNavpointsSource>()?;
385    m.add_class::<FaaArcgisNavpointsSource>()?;
386    m.add_class::<DdrNavpointsSource>()?;
387    Ok(m)
388}