1use pyo3::{exceptions::PyOSError, prelude::*, types::PyDict};
2use serde_json::Value;
3use std::fs::File;
4use std::path::PathBuf;
5use thrust::data::eurocontrol::database::{AirwayDatabase, ResolvedPoint, ResolvedRoute};
6use thrust::data::eurocontrol::ddr::routes::parse_routes_path;
7use thrust::data::faa::nasr::parse_field15_data_from_nasr_zip;
8
9fn normalize_name(value: &str) -> String {
10 value
11 .chars()
12 .filter(|c| c.is_ascii_alphanumeric())
13 .collect::<String>()
14 .to_uppercase()
15}
16
17fn normalize_point_code(value: &str) -> String {
18 value.split(':').next().unwrap_or(value).to_uppercase()
19}
20
21#[pyclass(get_all)]
22#[derive(Debug, Clone)]
23pub struct AirwayPointRecord {
24 code: String,
25 raw_code: Option<String>,
26 kind: String,
27 latitude: f64,
28 longitude: f64,
29}
30
31#[pymethods]
32impl AirwayPointRecord {
33 fn to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
34 let d = PyDict::new(py);
35 d.set_item("code", &self.code)?;
36 if let Some(raw_code) = &self.raw_code {
37 d.set_item("raw_code", raw_code)?;
38 }
39 d.set_item("kind", &self.kind)?;
40 d.set_item("latitude", self.latitude)?;
41 d.set_item("longitude", self.longitude)?;
42 Ok(d.into())
43 }
44}
45
46#[pyclass(get_all)]
47#[derive(Debug, Clone)]
48pub struct AirwayRecord {
49 name: String,
50 points: Vec<AirwayPointRecord>,
51 source: String,
52}
53
54#[pymethods]
55impl AirwayRecord {
56 fn to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
57 let d = PyDict::new(py);
58 d.set_item("name", &self.name)?;
59 d.set_item("source", &self.source)?;
60 let pts = self
61 .points
62 .iter()
63 .map(|p| p.to_dict(py))
64 .collect::<PyResult<Vec<_>>>()?;
65 d.set_item("points", pts)?;
66 Ok(d.into())
67 }
68}
69
70fn point_to_record(point: &ResolvedPoint) -> Option<AirwayPointRecord> {
71 match point {
72 ResolvedPoint::AirportHeliport(airport) => Some(AirwayPointRecord {
73 code: normalize_point_code(&airport.icao),
74 raw_code: Some(airport.icao.clone()),
75 kind: "airport".to_string(),
76 latitude: airport.latitude,
77 longitude: airport.longitude,
78 }),
79 ResolvedPoint::Navaid(navaid) => navaid.name.clone().map(|name| AirwayPointRecord {
80 code: normalize_point_code(&name),
81 raw_code: Some(name),
82 kind: "navaid".to_string(),
83 latitude: navaid.latitude,
84 longitude: navaid.longitude,
85 }),
86 ResolvedPoint::DesignatedPoint(point) => Some(AirwayPointRecord {
87 code: normalize_point_code(&point.designator),
88 raw_code: Some(point.designator.clone()),
89 kind: "fix".to_string(),
90 latitude: point.latitude,
91 longitude: point.longitude,
92 }),
93 ResolvedPoint::Coordinates { latitude, longitude } => Some(AirwayPointRecord {
94 code: format!("{latitude:.6},{longitude:.6}"),
95 raw_code: None,
96 kind: "coordinates".to_string(),
97 latitude: *latitude,
98 longitude: *longitude,
99 }),
100 ResolvedPoint::None => None,
101 }
102}
103
104fn route_to_record(route: ResolvedRoute, source: &str) -> AirwayRecord {
105 let mut points: Vec<AirwayPointRecord> = Vec::new();
106 for segment in route.segments {
107 if let Some(start) = point_to_record(&segment.start) {
108 if points.last().map(|x| &x.code) != Some(&start.code) {
109 points.push(start);
110 }
111 }
112 if let Some(end) = point_to_record(&segment.end) {
113 if points.last().map(|x| &x.code) != Some(&end.code) {
114 points.push(end);
115 }
116 }
117 }
118 AirwayRecord {
119 name: route.name,
120 points,
121 source: source.to_string(),
122 }
123}
124
125#[pyclass]
126pub struct AixmAirwaysSource {
127 database: AirwayDatabase,
128}
129
130#[pymethods]
131impl AixmAirwaysSource {
132 #[new]
133 fn new(path: PathBuf) -> PyResult<Self> {
134 let database = AirwayDatabase::new(&path).map_err(|e| PyOSError::new_err(e.to_string()))?;
135 Ok(Self { database })
136 }
137
138 fn resolve_airway(&self, name: String) -> Vec<AirwayRecord> {
139 ResolvedRoute::lookup(&name, &self.database)
140 .into_iter()
141 .map(|route| route_to_record(route, "eurocontrol_aixm"))
142 .collect()
143 }
144
145 fn list_airways(&self) -> Vec<String> {
146 Vec::new()
148 }
149}
150
151#[pyclass]
152pub struct NasrAirwaysSource {
153 routes: Vec<AirwayRecord>,
154}
155
156#[pymethods]
157impl NasrAirwaysSource {
158 #[new]
159 fn new(path: PathBuf) -> PyResult<Self> {
160 let data = parse_field15_data_from_nasr_zip(path).map_err(|e| PyOSError::new_err(e.to_string()))?;
161
162 let mut point_index: std::collections::HashMap<String, AirwayPointRecord> = std::collections::HashMap::new();
163 for point in &data.points {
164 let kind = match point.kind.as_str() {
165 "FIX" => "fix",
166 "NAVAID" => "navaid",
167 "AIRPORT" => "airport",
168 _ => "point",
169 }
170 .to_string();
171
172 let record = AirwayPointRecord {
173 code: normalize_point_code(&point.identifier),
174 raw_code: Some(point.identifier.to_uppercase()),
175 kind,
176 latitude: point.latitude,
177 longitude: point.longitude,
178 };
179
180 point_index.insert(point.identifier.to_uppercase(), record.clone());
181 if let Some((base, _suffix)) = point.identifier.split_once(':') {
182 point_index.entry(base.to_uppercase()).or_insert(record);
183 }
184 }
185
186 let mut by_name: std::collections::HashMap<String, Vec<AirwayPointRecord>> = std::collections::HashMap::new();
187
188 for seg in data.airways {
189 let route_name = if seg.airway_id.trim().is_empty() {
190 seg.airway_name.clone()
191 } else {
192 seg.airway_id.clone()
193 };
194 let entry = by_name.entry(route_name).or_default();
195 let from_key = seg.from_point.to_uppercase();
196 let to_key = seg.to_point.to_uppercase();
197 let from = point_index.get(&from_key).cloned().unwrap_or(AirwayPointRecord {
198 code: normalize_point_code(&from_key),
199 raw_code: Some(from_key.clone()),
200 kind: "point".to_string(),
201 latitude: 0.0,
202 longitude: 0.0,
203 });
204 let to = point_index.get(&to_key).cloned().unwrap_or(AirwayPointRecord {
205 code: normalize_point_code(&to_key),
206 raw_code: Some(to_key.clone()),
207 kind: "point".to_string(),
208 latitude: 0.0,
209 longitude: 0.0,
210 });
211
212 if entry.last().map(|x| &x.code) != Some(&from.code) {
213 entry.push(from);
214 }
215 if entry.last().map(|x| &x.code) != Some(&to.code) {
216 entry.push(to);
217 }
218 }
219
220 let routes = by_name
221 .into_iter()
222 .map(|(name, points)| AirwayRecord {
223 name,
224 points,
225 source: "faa_nasr".to_string(),
226 })
227 .collect();
228 Ok(Self { routes })
229 }
230
231 fn resolve_airway(&self, name: String) -> Vec<AirwayRecord> {
232 let upper = normalize_name(&name);
233 self.routes
234 .iter()
235 .filter(|route| normalize_name(&route.name) == upper)
236 .cloned()
237 .collect()
238 }
239
240 fn list_airways(&self) -> Vec<AirwayRecord> {
241 self.routes.clone()
242 }
243}
244
245fn value_to_string(v: Option<&Value>) -> Option<String> {
246 v.and_then(|x| x.as_str().map(|s| s.to_string()))
247}
248
249fn read_features(path: &std::path::Path) -> Result<Vec<Value>, Box<dyn std::error::Error>> {
250 let file = File::open(path)?;
251 let payload: Value = serde_json::from_reader(file)?;
252 Ok(payload
253 .get("features")
254 .and_then(|x| x.as_array())
255 .cloned()
256 .unwrap_or_default())
257}
258
259#[pyclass]
260pub struct FaaArcgisAirwaysSource {
261 routes: Vec<AirwayRecord>,
262}
263
264#[pymethods]
265impl FaaArcgisAirwaysSource {
266 #[new]
267 fn new(path: PathBuf) -> PyResult<Self> {
268 let features =
269 read_features(&path.join("faa_ats_routes.json")).map_err(|e| PyOSError::new_err(e.to_string()))?;
270
271 let mut by_name: std::collections::HashMap<String, Vec<AirwayPointRecord>> = std::collections::HashMap::new();
272
273 for feature in features {
274 let props = feature.get("properties").unwrap_or(&Value::Null);
275 let route_name = value_to_string(props.get("IDENT")).unwrap_or_default().to_uppercase();
276 if route_name.is_empty() {
277 continue;
278 }
279
280 let geom = feature.get("geometry").unwrap_or(&Value::Null);
281 if geom.get("type").and_then(|x| x.as_str()) != Some("LineString") {
282 continue;
283 }
284 let coordinates = geom
285 .get("coordinates")
286 .and_then(|x| x.as_array())
287 .cloned()
288 .unwrap_or_default();
289
290 let entry = by_name.entry(route_name).or_default();
291 for (idx, point) in coordinates.iter().enumerate() {
292 let arr = match point.as_array() {
293 Some(v) if v.len() >= 2 => v,
294 _ => continue,
295 };
296 let lon = arr[0].as_f64().unwrap_or(0.0);
297 let lat = arr[1].as_f64().unwrap_or(0.0);
298 let p = AirwayPointRecord {
299 code: format!("{}:{}", entry.len() + idx, "PT"),
300 raw_code: None,
301 kind: "point".to_string(),
302 latitude: lat,
303 longitude: lon,
304 };
305 if entry
306 .last()
307 .map(|x| (x.latitude, x.longitude) != (p.latitude, p.longitude))
308 .unwrap_or(true)
309 {
310 entry.push(p);
311 }
312 }
313 }
314
315 let routes = by_name
316 .into_iter()
317 .map(|(name, points)| AirwayRecord {
318 name,
319 points,
320 source: "faa_arcgis".to_string(),
321 })
322 .collect();
323
324 Ok(Self { routes })
325 }
326
327 fn resolve_airway(&self, name: String) -> Vec<AirwayRecord> {
328 let upper = normalize_name(&name);
329 self.routes
330 .iter()
331 .filter(|route| normalize_name(&route.name) == upper)
332 .cloned()
333 .collect()
334 }
335
336 fn list_airways(&self) -> Vec<AirwayRecord> {
337 self.routes.clone()
338 }
339}
340
341#[pyclass]
342pub struct DdrAirwaysSource {
343 routes: Vec<AirwayRecord>,
344}
345
346#[pymethods]
347impl DdrAirwaysSource {
348 #[new]
349 fn new(path: PathBuf) -> PyResult<Self> {
350 let parsed = parse_routes_path(path).map_err(|e| PyOSError::new_err(e.to_string()))?;
351 let mut by_name: std::collections::HashMap<String, Vec<AirwayPointRecord>> = std::collections::HashMap::new();
352
353 for point in parsed {
354 let route = point.route.to_uppercase();
355 let entry = by_name.entry(route).or_default();
356 entry.push(AirwayPointRecord {
357 code: point.navaid.to_uppercase(),
358 raw_code: Some(point.navaid.to_uppercase()),
359 kind: if point.point_type.to_uppercase().contains("FIX") {
360 "fix".to_string()
361 } else {
362 "navaid".to_string()
363 },
364 latitude: point.latitude.unwrap_or(0.0),
365 longitude: point.longitude.unwrap_or(0.0),
366 });
367 }
368
369 let mut routes: Vec<AirwayRecord> = by_name
370 .into_iter()
371 .map(|(name, points)| AirwayRecord {
372 name,
373 points,
374 source: "eurocontrol_ddr".to_string(),
375 })
376 .collect();
377 routes.sort_by(|a, b| a.name.cmp(&b.name));
378
379 Ok(Self { routes })
380 }
381
382 fn resolve_airway(&self, name: String) -> Vec<AirwayRecord> {
383 let upper = normalize_name(&name);
384 self.routes
385 .iter()
386 .filter(|route| normalize_name(&route.name) == upper)
387 .cloned()
388 .collect()
389 }
390
391 fn list_airways(&self) -> Vec<AirwayRecord> {
392 self.routes.clone()
393 }
394}
395
396pub fn init(py: Python<'_>) -> PyResult<Bound<'_, PyModule>> {
397 let m = PyModule::new(py, "airways")?;
398 m.add_class::<AirwayPointRecord>()?;
399 m.add_class::<AirwayRecord>()?;
400 m.add_class::<AixmAirwaysSource>()?;
401 m.add_class::<NasrAirwaysSource>()?;
402 m.add_class::<FaaArcgisAirwaysSource>()?;
403 m.add_class::<DdrAirwaysSource>()?;
404 Ok(m)
405}