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