use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::geo;
use crate::model::{Area, GeoPoint, ProductKind};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RouteWaypoint {
pub ident: Option<String>,
pub position: GeoPoint,
}
impl RouteWaypoint {
pub fn new(position: GeoPoint) -> Self {
Self {
ident: None,
position,
}
}
#[must_use]
pub fn with_ident(mut self, ident: Option<String>) -> Self {
self.ident = ident;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum FlightRules {
Vfr,
Ifr,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RouteBriefingRequest {
pub waypoints: Vec<RouteWaypoint>,
pub corridor_half_width_nm: f64,
pub cruise_altitude_ft: Option<i32>,
pub cruise_tas_kt: Option<f64>,
pub departure_at: Option<DateTime<Utc>>,
pub flight_rules: FlightRules,
pub products: Vec<ProductKind>,
}
impl RouteBriefingRequest {
pub fn new(waypoints: Vec<RouteWaypoint>, corridor_half_width_nm: f64) -> Self {
Self {
waypoints,
corridor_half_width_nm,
cruise_altitude_ft: None,
cruise_tas_kt: None,
departure_at: None,
flight_rules: FlightRules::Ifr,
products: Vec::new(),
}
}
#[must_use]
pub fn with_cruise_altitude_ft(mut self, ft: Option<i32>) -> Self {
self.cruise_altitude_ft = ft;
self
}
#[must_use]
pub fn with_cruise_tas_kt(mut self, kt: Option<f64>) -> Self {
self.cruise_tas_kt = kt;
self
}
#[must_use]
pub fn with_departure_at(mut self, at: Option<DateTime<Utc>>) -> Self {
self.departure_at = at;
self
}
#[must_use]
pub fn with_flight_rules(mut self, rules: FlightRules) -> Self {
self.flight_rules = rules;
self
}
#[must_use]
pub fn with_products(mut self, products: Vec<ProductKind>) -> Self {
self.products = products;
self
}
#[must_use]
pub fn total_distance_nm(&self) -> f64 {
self.waypoints
.windows(2)
.map(|leg| geo::distance_nm(leg[0].position, leg[1].position))
.sum()
}
#[must_use]
pub fn idents(&self) -> Vec<&str> {
self.waypoints
.iter()
.filter_map(|w| w.ident.as_deref())
.collect()
}
#[must_use]
pub fn segment_bboxes(&self, max_segment_nm: f64) -> Vec<Area> {
let max_segment_nm = max_segment_nm.max(1.0);
let centerline = self.densified_centerline(max_segment_nm);
let Some((first, rest)) = centerline.split_first() else {
return Vec::new();
};
if rest.is_empty() {
return vec![padded_bbox(&[*first], self.corridor_half_width_nm.max(1.0))];
}
let mut boxes = Vec::new();
let mut segment = vec![*first];
let mut span = 0.0;
let mut prev = *first;
for &point in rest {
span += geo::distance_nm(prev, point);
segment.push(point);
prev = point;
if span >= max_segment_nm {
boxes.push(padded_bbox(&segment, self.corridor_half_width_nm));
segment = vec![point];
span = 0.0;
}
}
if segment.len() >= 2 {
boxes.push(padded_bbox(&segment, self.corridor_half_width_nm));
}
boxes
}
fn densified_centerline(&self, step_nm: f64) -> Vec<GeoPoint> {
let mut points = Vec::new();
let mut iter = self.waypoints.iter();
let Some(first) = iter.next() else {
return points;
};
points.push(first.position);
let mut from = first.position;
for next in iter {
let to = next.position;
let leg = geo::distance_nm(from, to);
if leg > step_nm {
let bearing = geo::initial_bearing_deg(from, to);
let steps = (leg / step_nm).floor() as u32;
for i in 1..=steps {
let along = step_nm * f64::from(i);
if along < leg {
points.push(geo::destination(from, bearing, along));
}
}
}
points.push(to);
from = to;
}
points
}
}
fn padded_bbox(points: &[GeoPoint], pad_nm: f64) -> Area {
let mut min_lat = f64::INFINITY;
let mut max_lat = f64::NEG_INFINITY;
let mut min_lon = f64::INFINITY;
let mut max_lon = f64::NEG_INFINITY;
for p in points {
min_lat = min_lat.min(p.lat);
max_lat = max_lat.max(p.lat);
min_lon = min_lon.min(p.lon);
max_lon = max_lon.max(p.lon);
}
let lat_pad = pad_nm / 60.0;
let worst_lat = min_lat.abs().max(max_lat.abs()).min(89.0);
let lon_pad = pad_nm / (60.0 * worst_lat.to_radians().cos().max(0.01));
Area::BoundingBox {
south_west: GeoPoint {
lat: (min_lat - lat_pad).max(-90.0),
lon: min_lon - lon_pad,
},
north_east: GeoPoint {
lat: (max_lat + lat_pad).min(90.0),
lon: max_lon + lon_pad,
},
}
}
#[cfg(test)]
mod tests;