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}