aerocontext_core/
route_request.rs1use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13use crate::geo;
14use crate::model::{Area, GeoPoint, ProductKind};
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19#[non_exhaustive]
20pub struct RouteWaypoint {
21 pub ident: Option<String>,
24 pub position: GeoPoint,
26}
27
28impl RouteWaypoint {
29 pub fn new(position: GeoPoint) -> Self {
31 Self {
32 ident: None,
33 position,
34 }
35 }
36
37 #[must_use]
39 pub fn with_ident(mut self, ident: Option<String>) -> Self {
40 self.ident = ident;
41 self
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[non_exhaustive]
49pub enum FlightRules {
50 Vfr,
52 Ifr,
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62#[non_exhaustive]
63pub struct RouteBriefingRequest {
64 pub waypoints: Vec<RouteWaypoint>,
66 pub corridor_half_width_nm: f64,
68 pub cruise_altitude_ft: Option<i32>,
70 pub cruise_tas_kt: Option<f64>,
72 pub departure_at: Option<DateTime<Utc>>,
74 pub flight_rules: FlightRules,
76 pub products: Vec<ProductKind>,
78}
79
80impl RouteBriefingRequest {
81 pub fn new(waypoints: Vec<RouteWaypoint>, corridor_half_width_nm: f64) -> Self {
84 Self {
85 waypoints,
86 corridor_half_width_nm,
87 cruise_altitude_ft: None,
88 cruise_tas_kt: None,
89 departure_at: None,
90 flight_rules: FlightRules::Ifr,
91 products: Vec::new(),
92 }
93 }
94
95 #[must_use]
97 pub fn with_cruise_altitude_ft(mut self, ft: Option<i32>) -> Self {
98 self.cruise_altitude_ft = ft;
99 self
100 }
101
102 #[must_use]
104 pub fn with_cruise_tas_kt(mut self, kt: Option<f64>) -> Self {
105 self.cruise_tas_kt = kt;
106 self
107 }
108
109 #[must_use]
111 pub fn with_departure_at(mut self, at: Option<DateTime<Utc>>) -> Self {
112 self.departure_at = at;
113 self
114 }
115
116 #[must_use]
118 pub fn with_flight_rules(mut self, rules: FlightRules) -> Self {
119 self.flight_rules = rules;
120 self
121 }
122
123 #[must_use]
125 pub fn with_products(mut self, products: Vec<ProductKind>) -> Self {
126 self.products = products;
127 self
128 }
129
130 #[must_use]
132 pub fn total_distance_nm(&self) -> f64 {
133 self.waypoints
134 .windows(2)
135 .map(|leg| geo::distance_nm(leg[0].position, leg[1].position))
136 .sum()
137 }
138
139 #[must_use]
142 pub fn idents(&self) -> Vec<&str> {
143 self.waypoints
144 .iter()
145 .filter_map(|w| w.ident.as_deref())
146 .collect()
147 }
148
149 #[must_use]
154 pub fn segment_bboxes(&self, max_segment_nm: f64) -> Vec<Area> {
155 let max_segment_nm = max_segment_nm.max(1.0);
156 let centerline = self.densified_centerline(max_segment_nm);
157 let Some((first, rest)) = centerline.split_first() else {
158 return Vec::new();
159 };
160 if rest.is_empty() {
161 return vec![padded_bbox(&[*first], self.corridor_half_width_nm.max(1.0))];
162 }
163 let mut boxes = Vec::new();
164 let mut segment = vec![*first];
165 let mut span = 0.0;
166 let mut prev = *first;
167 for &point in rest {
168 span += geo::distance_nm(prev, point);
169 segment.push(point);
170 prev = point;
171 if span >= max_segment_nm {
172 boxes.push(padded_bbox(&segment, self.corridor_half_width_nm));
173 segment = vec![point];
174 span = 0.0;
175 }
176 }
177 if segment.len() >= 2 {
178 boxes.push(padded_bbox(&segment, self.corridor_half_width_nm));
179 }
180 boxes
181 }
182
183 fn densified_centerline(&self, step_nm: f64) -> Vec<GeoPoint> {
187 let mut points = Vec::new();
188 let mut iter = self.waypoints.iter();
189 let Some(first) = iter.next() else {
190 return points;
191 };
192 points.push(first.position);
193 let mut from = first.position;
194 for next in iter {
195 let to = next.position;
196 let leg = geo::distance_nm(from, to);
197 if leg > step_nm {
198 let bearing = geo::initial_bearing_deg(from, to);
199 let steps = (leg / step_nm).floor() as u32;
200 for i in 1..=steps {
201 let along = step_nm * f64::from(i);
202 if along < leg {
203 points.push(geo::destination(from, bearing, along));
204 }
205 }
206 }
207 points.push(to);
208 from = to;
209 }
210 points
211 }
212}
213
214fn padded_bbox(points: &[GeoPoint], pad_nm: f64) -> Area {
218 let mut min_lat = f64::INFINITY;
219 let mut max_lat = f64::NEG_INFINITY;
220 let mut min_lon = f64::INFINITY;
221 let mut max_lon = f64::NEG_INFINITY;
222 for p in points {
223 min_lat = min_lat.min(p.lat);
224 max_lat = max_lat.max(p.lat);
225 min_lon = min_lon.min(p.lon);
226 max_lon = max_lon.max(p.lon);
227 }
228 let lat_pad = pad_nm / 60.0;
229 let worst_lat = min_lat.abs().max(max_lat.abs()).min(89.0);
230 let lon_pad = pad_nm / (60.0 * worst_lat.to_radians().cos().max(0.01));
231 Area::BoundingBox {
232 south_west: GeoPoint {
233 lat: (min_lat - lat_pad).max(-90.0),
234 lon: min_lon - lon_pad,
235 },
236 north_east: GeoPoint {
237 lat: (max_lat + lat_pad).min(90.0),
238 lon: max_lon + lon_pad,
239 },
240 }
241}
242
243#[cfg(test)]
244mod tests;