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}