use aerocontext_core::{Area, GeoPoint, geo};
use crate::route::ExpandedRoute;
const CAP_SEGMENTS: u32 = 8;
const MITER_LIMIT: f64 = 2.5;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CorridorError {
#[error("corridor needs at least 2 centerline points, got {got}")]
TooFewPoints {
got: usize,
},
#[error("invalid corridor half-width {half_width_nm} NM")]
InvalidWidth {
half_width_nm: f64,
},
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Corridor {
pub centerline: Vec<GeoPoint>,
pub half_width_nm: f64,
pub polygon: Vec<GeoPoint>,
}
impl Corridor {
pub fn around_route(route: &ExpandedRoute, half_width_nm: f64) -> Result<Self, CorridorError> {
let centerline: Vec<GeoPoint> = route.points.iter().map(|point| point.position).collect();
Self::around_points(¢erline, half_width_nm)
}
pub fn around_points(
centerline: &[GeoPoint],
half_width_nm: f64,
) -> Result<Self, CorridorError> {
if centerline.len() < 2 {
return Err(CorridorError::TooFewPoints {
got: centerline.len(),
});
}
if !(half_width_nm.is_finite() && half_width_nm > 0.0) {
return Err(CorridorError::InvalidWidth { half_width_nm });
}
let polygon = build_ring(centerline, half_width_nm);
Ok(Self {
centerline: centerline.to_vec(),
half_width_nm,
polygon,
})
}
pub fn contains(&self, point: GeoPoint) -> bool {
self.distance_to_centerline_nm(point) <= self.half_width_nm
}
pub fn distance_to_centerline_nm(&self, point: GeoPoint) -> f64 {
let mut best = f64::INFINITY;
for leg in self.centerline.windows(2) {
best = best.min(leg_distance_nm(point, leg[0], leg[1]));
}
best
}
pub fn area(&self) -> Area {
Area::Polygon {
vertices: self.polygon.clone(),
}
}
}
fn leg_distance_nm(point: GeoPoint, start: GeoPoint, end: GeoPoint) -> f64 {
let leg_length = geo::distance_nm(start, end);
if leg_length < 1e-9 {
return geo::distance_nm(point, start);
}
let along = geo::along_track_nm(point, start, end);
if along <= 0.0 {
geo::distance_nm(point, start)
} else if along >= leg_length {
geo::distance_nm(point, end)
} else {
geo::cross_track_nm(point, start, end).abs()
}
}
fn build_ring(centerline: &[GeoPoint], half_width_nm: f64) -> Vec<GeoPoint> {
let mut left = Vec::with_capacity(centerline.len());
let mut right = Vec::with_capacity(centerline.len());
let last = centerline.len() - 1;
for (index, vertex) in centerline.iter().enumerate() {
let inbound = if index > 0 {
Some(geo::initial_bearing_deg(centerline[index - 1], *vertex))
} else {
None
};
let outbound = if index < last {
Some(geo::initial_bearing_deg(*vertex, centerline[index + 1]))
} else {
None
};
let (offset_bearing, scale) = join_offset(inbound, outbound);
let distance = (half_width_nm * scale).min(half_width_nm * MITER_LIMIT);
left.push(geo::destination(*vertex, offset_bearing - 90.0, distance));
right.push(geo::destination(*vertex, offset_bearing + 90.0, distance));
}
let mut ring = left;
let end_heading = geo::initial_bearing_deg(centerline[last - 1], centerline[last]);
ring.extend(cap(centerline[last], end_heading - 90.0, half_width_nm));
ring.extend(right.into_iter().rev());
let start_heading = geo::initial_bearing_deg(centerline[0], centerline[1]);
ring.extend(cap(centerline[0], start_heading + 90.0, half_width_nm));
ring
}
fn join_offset(inbound: Option<f64>, outbound: Option<f64>) -> (f64, f64) {
match (inbound, outbound) {
(Some(inbound), Some(outbound)) => {
let mut delta = outbound - inbound;
while delta > 180.0 {
delta -= 360.0;
}
while delta < -180.0 {
delta += 360.0;
}
let mean = inbound + delta / 2.0;
let scale = 1.0
/ (delta.to_radians() / 2.0)
.cos()
.abs()
.max(1.0 / MITER_LIMIT);
(mean, scale)
}
(Some(bearing), None) | (None, Some(bearing)) => (bearing, 1.0),
(None, None) => (0.0, 1.0),
}
}
fn cap(center: GeoPoint, from_bearing: f64, radius_nm: f64) -> Vec<GeoPoint> {
(1..CAP_SEGMENTS)
.map(|step| {
let bearing = from_bearing + 180.0 * f64::from(step) / f64::from(CAP_SEGMENTS);
geo::destination(center, bearing, radius_nm)
})
.collect()
}
#[cfg(test)]
mod tests;