aerocontext_planning/
corridor.rs1use aerocontext_core::{Area, GeoPoint, geo};
13
14use crate::route::ExpandedRoute;
15
16const CAP_SEGMENTS: u32 = 8;
18const MITER_LIMIT: f64 = 2.5;
20
21#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum CorridorError {
25 #[error("corridor needs at least 2 centerline points, got {got}")]
27 TooFewPoints {
28 got: usize,
30 },
31 #[error("invalid corridor half-width {half_width_nm} NM")]
33 InvalidWidth {
34 half_width_nm: f64,
36 },
37}
38
39#[derive(Debug, Clone, PartialEq)]
41#[non_exhaustive]
42pub struct Corridor {
43 pub centerline: Vec<GeoPoint>,
45 pub half_width_nm: f64,
47 pub polygon: Vec<GeoPoint>,
50}
51
52impl Corridor {
53 pub fn around_route(route: &ExpandedRoute, half_width_nm: f64) -> Result<Self, CorridorError> {
55 let centerline: Vec<GeoPoint> = route.points.iter().map(|point| point.position).collect();
56 Self::around_points(¢erline, half_width_nm)
57 }
58
59 pub fn around_points(
61 centerline: &[GeoPoint],
62 half_width_nm: f64,
63 ) -> Result<Self, CorridorError> {
64 if centerline.len() < 2 {
65 return Err(CorridorError::TooFewPoints {
66 got: centerline.len(),
67 });
68 }
69 if !(half_width_nm.is_finite() && half_width_nm > 0.0) {
70 return Err(CorridorError::InvalidWidth { half_width_nm });
71 }
72 let polygon = build_ring(centerline, half_width_nm);
73 Ok(Self {
74 centerline: centerline.to_vec(),
75 half_width_nm,
76 polygon,
77 })
78 }
79
80 pub fn contains(&self, point: GeoPoint) -> bool {
82 self.distance_to_centerline_nm(point) <= self.half_width_nm
83 }
84
85 pub fn distance_to_centerline_nm(&self, point: GeoPoint) -> f64 {
88 let mut best = f64::INFINITY;
89 for leg in self.centerline.windows(2) {
90 best = best.min(leg_distance_nm(point, leg[0], leg[1]));
91 }
92 best
93 }
94
95 pub fn area(&self) -> Area {
99 Area::Polygon {
100 vertices: self.polygon.clone(),
101 }
102 }
103}
104
105fn leg_distance_nm(point: GeoPoint, start: GeoPoint, end: GeoPoint) -> f64 {
108 let leg_length = geo::distance_nm(start, end);
109 if leg_length < 1e-9 {
110 return geo::distance_nm(point, start);
111 }
112 let along = geo::along_track_nm(point, start, end);
113 if along <= 0.0 {
114 geo::distance_nm(point, start)
115 } else if along >= leg_length {
116 geo::distance_nm(point, end)
117 } else {
118 geo::cross_track_nm(point, start, end).abs()
119 }
120}
121
122fn build_ring(centerline: &[GeoPoint], half_width_nm: f64) -> Vec<GeoPoint> {
123 let mut left = Vec::with_capacity(centerline.len());
124 let mut right = Vec::with_capacity(centerline.len());
125 let last = centerline.len() - 1;
126 for (index, vertex) in centerline.iter().enumerate() {
127 let inbound = if index > 0 {
128 Some(geo::initial_bearing_deg(centerline[index - 1], *vertex))
129 } else {
130 None
131 };
132 let outbound = if index < last {
133 Some(geo::initial_bearing_deg(*vertex, centerline[index + 1]))
134 } else {
135 None
136 };
137 let (offset_bearing, scale) = join_offset(inbound, outbound);
138 let distance = (half_width_nm * scale).min(half_width_nm * MITER_LIMIT);
139 left.push(geo::destination(*vertex, offset_bearing - 90.0, distance));
140 right.push(geo::destination(*vertex, offset_bearing + 90.0, distance));
141 }
142
143 let mut ring = left;
144 let end_heading = geo::initial_bearing_deg(centerline[last - 1], centerline[last]);
147 ring.extend(cap(centerline[last], end_heading - 90.0, half_width_nm));
148 ring.extend(right.into_iter().rev());
149 let start_heading = geo::initial_bearing_deg(centerline[0], centerline[1]);
151 ring.extend(cap(centerline[0], start_heading + 90.0, half_width_nm));
152 ring
153}
154
155fn join_offset(inbound: Option<f64>, outbound: Option<f64>) -> (f64, f64) {
158 match (inbound, outbound) {
159 (Some(inbound), Some(outbound)) => {
160 let mut delta = outbound - inbound;
161 while delta > 180.0 {
162 delta -= 360.0;
163 }
164 while delta < -180.0 {
165 delta += 360.0;
166 }
167 let mean = inbound + delta / 2.0;
168 let scale = 1.0
169 / (delta.to_radians() / 2.0)
170 .cos()
171 .abs()
172 .max(1.0 / MITER_LIMIT);
173 (mean, scale)
174 }
175 (Some(bearing), None) | (None, Some(bearing)) => (bearing, 1.0),
176 (None, None) => (0.0, 1.0),
177 }
178}
179
180fn cap(center: GeoPoint, from_bearing: f64, radius_nm: f64) -> Vec<GeoPoint> {
183 (1..CAP_SEGMENTS)
184 .map(|step| {
185 let bearing = from_bearing + 180.0 * f64::from(step) / f64::from(CAP_SEGMENTS);
186 geo::destination(center, bearing, radius_nm)
187 })
188 .collect()
189}
190
191#[cfg(test)]
192mod tests;