use arinc424::fields::BoundaryPath;
use arinc424::records::{ControlledAirspace, RestrictiveAirspace};
use geo::{Bearing, Destination, Geodesic, Point};
use super::fields::parse_classification;
use crate::measurements::{Angle, Length};
use crate::nd::{Airspace, AirspaceClassification, AirspaceType};
use crate::VerticalDistance;
const ARC_POINTS_PER_QUADRANT: usize = 6;
#[derive(Debug)]
struct BoundarySegment {
path: BoundaryPath,
end_point: Point<f64>,
arc_center: Option<Point<f64>>,
arc_radius: Option<Length>,
}
#[derive(Debug, Default)]
pub struct AirspaceBuilder {
name: Option<String>,
airspace_type: Option<AirspaceType>,
classification: Option<AirspaceClassification>,
ceiling: Option<VerticalDistance>,
floor: Option<VerticalDistance>,
segments: Vec<BoundarySegment>,
start_point: Option<Point<f64>>,
}
impl AirspaceBuilder {
pub fn add_controlled_record(
&mut self,
record: ControlledAirspace,
) -> Result<(), arinc424::Error> {
let coord = match (record.latitude, record.longitude) {
(Some(lat), Some(lon)) => {
Some(Point::new(lon.as_decimal()?, lat.as_decimal()?))
}
_ => None,
};
if self.start_point.is_none() {
self.start_point = coord;
self.name = record.arsp_name.map(|n| n.to_string());
self.airspace_type = Some(record.arsp_type.into());
self.classification =
parse_classification(record.arsp_type, record.arsp_class.as_ref());
self.ceiling = record.upper_limit.map(Into::into);
self.floor = record.lower_limit.map(Into::into);
}
self.add_segment(
coord,
record.bdry_via.path,
record.arc_origin_latitude,
record.arc_origin_longitude,
record.arc_dist,
)
}
pub fn add_restrictive_record(
&mut self,
record: RestrictiveAirspace,
) -> Result<(), arinc424::Error> {
let coord = match (record.latitude, record.longitude) {
(Some(lat), Some(lon)) => Some(Point::new(lon.as_decimal()?, lat.as_decimal()?)),
_ => None,
};
if self.start_point.is_none() {
self.start_point = coord;
self.name = record.arsp_name.map(|n| n.to_string());
self.airspace_type = Some(record.restrictive_type.into());
self.classification = None;
self.ceiling = record.upper_limit.map(Into::into);
self.floor = record.lower_limit.map(Into::into);
}
self.add_segment(
coord,
record.bdry_via.path,
record.arc_origin_latitude,
record.arc_origin_longitude,
record.arc_dist,
)
}
fn add_segment(
&mut self,
coord: Option<Point<f64>>,
path: BoundaryPath,
arc_origin_lat: Option<arinc424::fields::Latitude<'_>>,
arc_origin_lon: Option<arinc424::fields::Longitude<'_>>,
arc_dist: Option<arinc424::fields::ArcDistance<'_>>,
) -> Result<(), arinc424::Error> {
let arc_center = match (arc_origin_lat, arc_origin_lon) {
(Some(lat), Some(lon)) => Some(Point::new(lon.as_decimal()?, lat.as_decimal()?)),
_ => None,
};
let arc_radius = arc_dist.map(|d| d.dist()).transpose()?.map(Length::nm);
self.segments.push(BoundarySegment {
path,
end_point: coord
.or(arc_center)
.expect("record should either have coordinates or arc center"),
arc_center,
arc_radius,
});
Ok(())
}
pub fn build(self) -> Result<Airspace, arinc424::Error> {
let polygon = self.build_polygon()?;
Ok(Airspace {
name: self.name.unwrap_or_default(),
airspace_type: self.airspace_type.unwrap_or(AirspaceType::CTA),
classification: self.classification,
ceiling: self.ceiling.unwrap_or(VerticalDistance::Unlimited),
floor: self.floor.unwrap_or(VerticalDistance::Gnd),
polygon,
})
}
fn build_polygon(&self) -> Result<geo::Polygon<f64>, arinc424::Error> {
let mut coords: Vec<geo::Coord<f64>> = Vec::new();
if self.segments.len() == 1 && self.segments[0].path == BoundaryPath::Circle {
return self.build_circle(&self.segments[0]);
}
for (i, segment) in self.segments.iter().enumerate() {
let prev_point = if i == 0 {
self.start_point.unwrap_or(segment.end_point)
} else {
self.segments[i - 1].end_point
};
match segment.path {
BoundaryPath::Circle => {
coords.push(geo::Coord {
x: segment.end_point.x(),
y: segment.end_point.y(),
});
}
BoundaryPath::GreatCircle | BoundaryPath::RhumbLine => {
coords.push(geo::Coord {
x: segment.end_point.x(),
y: segment.end_point.y(),
});
}
BoundaryPath::ClockwiseArc => {
let arc_coords = self.interpolate_arc(prev_point, segment, true)?;
coords.extend(arc_coords);
}
BoundaryPath::CounterClockwiseArc => {
let arc_coords = self.interpolate_arc(prev_point, segment, false)?;
coords.extend(arc_coords);
}
}
}
if let (Some(first), Some(last)) = (coords.first(), coords.last()) {
if first != last {
coords.push(*first);
}
}
Ok(geo::Polygon::new(geo::LineString::from(coords), vec![]))
}
fn build_circle(
&self,
segment: &BoundarySegment,
) -> Result<geo::Polygon<f64>, arinc424::Error> {
let center = segment.end_point;
let radius_m = segment.arc_radius.map(|r| r.to_si()).unwrap_or(0.0) as f64;
let num_points = ARC_POINTS_PER_QUADRANT * 4;
let mut coords = Vec::with_capacity(num_points + 1);
for i in 0..num_points {
let bearing = Angle::t((i as f32) * 360.0 / (num_points as f32));
let point = Geodesic.destination(center, *bearing.value() as f64, radius_m);
coords.push(geo::Coord {
x: point.x(),
y: point.y(),
});
}
if let Some(first) = coords.first() {
coords.push(*first);
}
Ok(geo::Polygon::new(geo::LineString::from(coords), vec![]))
}
fn interpolate_arc(
&self,
start: Point<f64>,
segment: &BoundarySegment,
clockwise: bool,
) -> Result<Vec<geo::Coord<f64>>, arinc424::Error> {
let (Some(center), Some(radius)) = (segment.arc_center, segment.arc_radius) else {
return Ok(vec![geo::Coord {
x: segment.end_point.x(),
y: segment.end_point.y(),
}]);
};
let start_bearing = Angle::t(Geodesic.bearing(center, start) as f32);
let end_bearing = Angle::t(Geodesic.bearing(center, segment.end_point) as f32);
let sweep = calculate_arc_sweep(start_bearing, end_bearing, clockwise);
let sweep_rad = sweep.to_si();
let num_points = ((sweep_rad.abs() / std::f32::consts::FRAC_PI_2)
* ARC_POINTS_PER_QUADRANT as f32)
.ceil() as usize;
let num_points = num_points.max(2);
let mut coords = Vec::with_capacity(num_points);
let radius_m = radius.to_si() as f64;
let start_rad = start_bearing.to_si();
for i in 1..=num_points {
let fraction = i as f32 / num_points as f32;
let bearing_deg = (start_rad + sweep_rad * fraction).to_degrees() as f64;
let point = Geodesic.destination(center, bearing_deg, radius_m);
coords.push(geo::Coord {
x: point.x(),
y: point.y(),
});
}
Ok(coords)
}
}
fn calculate_arc_sweep(start: Angle, end: Angle, clockwise: bool) -> Angle {
let mut diff = end.value() - start.value();
if clockwise {
if diff <= 0.0 {
diff += 360.0;
}
} else {
if diff >= 0.0 {
diff -= 360.0;
}
}
Angle::rad(diff.to_radians())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_arc_sweep_clockwise() {
let sweep = calculate_arc_sweep(Angle::t(0.0), Angle::t(90.0), true);
assert!((sweep.to_si().to_degrees() - 90.0).abs() < 0.001);
let sweep = calculate_arc_sweep(Angle::t(90.0), Angle::t(0.0), true);
assert!((sweep.to_si().to_degrees() - 270.0).abs() < 0.001);
let sweep = calculate_arc_sweep(Angle::t(350.0), Angle::t(10.0), true);
assert!((sweep.to_si().to_degrees() - 20.0).abs() < 0.001);
}
#[test]
fn test_calculate_arc_sweep_counterclockwise() {
let sweep = calculate_arc_sweep(Angle::t(90.0), Angle::t(0.0), false);
assert!((sweep.to_si().to_degrees() - (-90.0)).abs() < 0.001);
let sweep = calculate_arc_sweep(Angle::t(0.0), Angle::t(90.0), false);
assert!((sweep.to_si().to_degrees() - (-270.0)).abs() < 0.001);
}
}