use std::rc::Rc;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use geo::{Distance, Geodesic, Point};
use rstar::primitives::{GeomWithData, Rectangle};
use rstar::{RTree, RTreeObject, AABB};
use super::{Airport, Airspace, NavAid, Waypoint};
use crate::measurements::{Length, LengthUnit};
const NM_TO_DEG: f64 = 1.0 / 60.0;
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AirspaceIndex {
tree: RTree<GeomWithData<Rectangle<Point<f64>>, Rc<Airspace>>>,
}
impl AirspaceIndex {
pub fn new<'a>(airspaces: impl Iterator<Item = &'a Rc<Airspace>>) -> Self {
let entries = airspaces
.map(|a| {
let rect = Rectangle::from_aabb(a.polygon.envelope());
GeomWithData::new(rect, Rc::clone(a))
})
.collect();
Self {
tree: RTree::bulk_load(entries),
}
}
pub fn candidates_at(&self, lon: f64, lat: f64) -> impl Iterator<Item = &Rc<Airspace>> + '_ {
let point_envelope = AABB::from_point(Point::new(lon, lat));
self.tree
.locate_in_envelope_intersecting(&point_envelope)
.map(|entry| &entry.data)
}
pub fn candidates_intersecting(
&self,
envelope: &AABB<Point<f64>>,
) -> impl Iterator<Item = &Rc<Airspace>> + '_ {
self.tree
.locate_in_envelope_intersecting(envelope)
.map(|entry| &entry.data)
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct NavAidIndex {
tree: RTree<GeomWithData<Point<f64>, NavAid>>,
}
impl NavAidIndex {
pub fn new<'a>(
airports: impl Iterator<Item = &'a Rc<Airport>>,
waypoints: impl Iterator<Item = &'a Rc<Waypoint>>,
) -> Self {
let mut entries = Vec::new();
for airport in airports {
entries.push(GeomWithData::new(
airport.coordinate,
NavAid::Airport(Rc::clone(airport)),
));
}
for waypoint in waypoints {
entries.push(GeomWithData::new(
waypoint.coordinate,
NavAid::Waypoint(Rc::clone(waypoint)),
));
}
Self {
tree: RTree::bulk_load(entries),
}
}
pub fn within_radius(
&self,
coord: &Point<f64>,
radius: Length,
) -> impl Iterator<Item = &NavAid> {
let radius_nm = *radius.convert_to(LengthUnit::NauticalMiles).value() as f64;
let radius_deg = radius_nm * NM_TO_DEG;
let lat_rad = coord.y().to_radians();
let lon_expansion = if lat_rad.cos().abs() > 0.01 {
radius_deg / lat_rad.cos()
} else {
radius_deg * 100.0 };
let envelope = AABB::from_corners(
Point::new(coord.x() - lon_expansion, coord.y() - radius_deg),
Point::new(coord.x() + lon_expansion, coord.y() + radius_deg),
);
let center = *coord;
let radius_m = radius.to_si() as f64;
self.tree
.locate_in_envelope_intersecting(&envelope)
.filter(move |entry| Geodesic.distance(center, *entry.geom()) <= radius_m)
.map(|entry| &entry.data)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nd::{AirspaceClassification, AirspaceType};
use crate::VerticalDistance;
fn test_airspace(name: &str, coords: &[(f64, f64)]) -> Rc<Airspace> {
let exterior: Vec<geo::Coord<f64>> = coords
.iter()
.map(|&(lat, lon)| geo::Coord { x: lon, y: lat })
.collect();
Rc::new(Airspace {
name: name.to_string(),
airspace_type: AirspaceType::CTA,
classification: Some(AirspaceClassification::D),
ceiling: VerticalDistance::Fl(65),
floor: VerticalDistance::Msl(1500),
polygon: geo::Polygon::new(geo::LineString::from(exterior), vec![]),
})
}
fn test_airport(ident: &str, lat: f64, lon: f64) -> Rc<Airport> {
Rc::new(Airport {
icao_ident: ident.to_string(),
iata_designator: String::new(),
name: ident.to_string(),
coordinate: Point::new(lon, lat),
mag_var: None,
elevation: VerticalDistance::Gnd,
runways: vec![],
location: None,
cycle: None,
})
}
fn test_waypoint(ident: &str, lat: f64, lon: f64) -> Rc<Waypoint> {
use crate::nd::waypoint::{Region, WaypointUsage};
Rc::new(Waypoint {
fix_ident: ident.to_string(),
desc: String::new(),
usage: WaypointUsage::Unknown,
coordinate: Point::new(lon, lat),
mag_var: None,
region: Region::Enroute,
location: None,
cycle: None,
})
}
#[test]
fn index_finds_point_inside_airspace() {
let airspaces = vec![test_airspace(
"Test",
&[
(53.0, 9.0),
(53.0, 10.0),
(54.0, 10.0),
(54.0, 9.0),
(53.0, 9.0),
],
)];
let index = AirspaceIndex::new(airspaces.iter());
let candidates: Vec<_> = index.candidates_at(9.5, 53.5).collect();
assert_eq!(candidates.len(), 1);
assert!(Rc::ptr_eq(candidates[0], &airspaces[0]));
}
#[test]
fn index_does_not_find_point_outside_airspace() {
let airspaces = vec![test_airspace(
"Test",
&[
(53.0, 9.0),
(53.0, 10.0),
(54.0, 10.0),
(54.0, 9.0),
(53.0, 9.0),
],
)];
let index = AirspaceIndex::new(airspaces.iter());
let candidates: Vec<_> = index.candidates_at(8.0, 52.0).collect();
assert!(candidates.is_empty());
}
#[test]
fn point_index_finds_airports_within_radius() {
let airports = vec![
test_airport("EDDH", 53.63, 9.99), test_airport("EDHL", 53.81, 10.70), ];
let waypoints: Vec<Rc<Waypoint>> = vec![];
let index = NavAidIndex::new(airports.iter(), waypoints.iter());
let center = Point::new(9.99, 53.63);
let results: Vec<_> = index.within_radius(¢er, Length::nm(10.0)).collect();
assert_eq!(results.len(), 1);
assert!(matches!(results[0], NavAid::Airport(a) if a.icao_ident == "EDDH"));
let results: Vec<_> = index.within_radius(¢er, Length::nm(50.0)).collect();
assert_eq!(results.len(), 2);
}
#[test]
fn point_index_finds_waypoints_within_radius() {
let airports: Vec<Rc<Airport>> = vec![];
let waypoints = vec![
test_waypoint("WP1", 53.5, 9.5),
test_waypoint("WP2", 53.6, 9.6),
test_waypoint("WP3", 54.5, 10.5),
];
let index = NavAidIndex::new(airports.iter(), waypoints.iter());
let center = Point::new(9.55, 53.55);
let results: Vec<_> = index.within_radius(¢er, Length::nm(10.0)).collect();
assert_eq!(results.len(), 2);
for r in &results {
assert!(matches!(r, NavAid::Waypoint(_)));
}
}
#[test]
fn point_index_finds_mixed_navaids() {
let airports = vec![test_airport("EDDH", 53.63, 9.99)];
let waypoints = vec![test_waypoint("DHN1", 53.60, 9.95)];
let index = NavAidIndex::new(airports.iter(), waypoints.iter());
let center = Point::new(9.97, 53.62);
let results: Vec<_> = index.within_radius(¢er, Length::nm(5.0)).collect();
assert_eq!(results.len(), 2);
let has_airport = results.iter().any(|r| matches!(r, NavAid::Airport(_)));
let has_waypoint = results.iter().any(|r| matches!(r, NavAid::Waypoint(_)));
assert!(has_airport);
assert!(has_waypoint);
}
}