use log::trace;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use geo::{Bearing, Distance, Geodesic};
use crate::fp::LegPerformance;
use crate::measurements::{Angle, AngleUnit, Duration, Length, LengthUnit, Speed};
use crate::nd::{Fix, NavAid};
use crate::{Fuel, VerticalDistance, Wind};
use super::LegFuel;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ClimbDescentAlongLeg {
from: Option<VerticalDistance>,
to: Option<VerticalDistance>,
reach_at: Option<VerticalDistance>,
}
impl ClimbDescentAlongLeg {
pub fn from(&self) -> Option<&VerticalDistance> {
self.from.as_ref()
}
pub fn to(&self) -> Option<&VerticalDistance> {
self.to.as_ref()
}
pub fn reach_at(&self) -> Option<&VerticalDistance> {
self.reach_at.as_ref()
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(super) struct LegBuilder {
level: Option<VerticalDistance>,
climb_descent: ClimbDescentAlongLeg,
tas: Option<Speed>,
wind: Option<Wind>,
}
impl LegBuilder {
pub fn build(&mut self, from: NavAid, to: NavAid) -> Leg {
self.climb_descent.from = self.level.or_else(|| match &from {
NavAid::Airport(arpt) => Some(arpt.elevation),
_ => None,
});
self.climb_descent
.to
.inspect(|level| trace!("climb/descent to {level} from {from}"));
self.climb_descent
.reach_at
.inspect(|level| trace!("reach {to} on {level}"));
let level = self.climb_descent.to.or(self.level);
let leg = Leg::new(from, to, self.climb_descent, level, self.tas, self.wind);
if let Some(reach_at) = self.climb_descent.reach_at.take() {
self.level = Some(reach_at);
} else if let Some(to) = self.climb_descent.to.take() {
self.level = Some(to);
}
self.climb_descent.to.take();
leg
}
pub fn cruise(&mut self, level: VerticalDistance) {
self.climb_descent.to = Some(level);
}
pub fn level_at_fix(&mut self, level: VerticalDistance) {
self.climb_descent.reach_at = Some(level);
}
pub fn tas(&mut self, tas: Speed) {
self.tas = Some(tas);
trace!("cruise speed set to {tas}");
}
pub fn wind(&mut self, wind: Wind) {
self.wind = Some(wind);
trace!("wind set to {wind}");
}
pub fn destination(&mut self, dest: &NavAid) {
if self.climb_descent.reach_at.is_none() {
if let NavAid::Airport(arpt) = dest {
self.climb_descent.reach_at = Some(arpt.elevation);
}
}
}
}
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Leg {
from: NavAid,
to: NavAid,
climb_descent: ClimbDescentAlongLeg,
level: Option<VerticalDistance>,
tas: Option<Speed>,
wind: Option<Wind>,
heading: Option<Angle>,
mh: Option<Angle>,
bearing: Angle,
mc: Angle,
dist: Length,
gs: Option<Speed>,
wca: Option<Angle>,
ete: Option<Duration>,
}
impl Leg {
pub(super) fn builder() -> LegBuilder {
LegBuilder::default()
}
pub fn divert(&self, alternate: NavAid) -> Leg {
Leg::new(
self.from.clone(),
alternate,
self.climb_descent,
self.level,
self.tas,
self.wind,
)
}
fn new(
from: NavAid,
to: NavAid,
climb_descent: ClimbDescentAlongLeg,
level: Option<VerticalDistance>,
tas: Option<Speed>,
wind: Option<Wind>,
) -> Leg {
let from_coord = from.coordinate();
let to_coord = to.coordinate();
let bearing_deg = Geodesic.bearing(from_coord, to_coord);
let bearing = Angle::t(bearing_deg as f32);
let mc = bearing + from.mag_var();
let distance_m = Geodesic.distance(from_coord, to_coord);
let dist = Length::m(distance_m as f32).convert_to(LengthUnit::NauticalMiles);
let (gs, wca) = {
match (tas, wind) {
(Some(tas), Some(wind)) => {
let wca = wind_correction_angle(&wind, &tas, &bearing);
let gs = ground_speed(&tas, &wind, &wca, &bearing);
(Some(gs), Some(wca))
}
_ => (None, None),
}
};
let heading = wca.map(|wca| bearing + wca);
let mh = heading.map(|heading| heading + from.mag_var());
let ete = gs.map(|gs| dist / gs);
trace!(
"leg {} -> {}: dist={:.1}, bearing={:.1}, gs={:?}, ete={:?}",
from.ident(),
to.ident(),
dist,
bearing,
gs,
ete
);
Self {
from,
to,
climb_descent,
level,
tas,
wind,
heading,
mh,
bearing,
mc,
dist,
gs,
wca,
ete,
}
}
pub fn from(&self) -> &NavAid {
&self.from
}
pub fn to(&self) -> &NavAid {
&self.to
}
pub fn level(&self) -> Option<&VerticalDistance> {
self.level.as_ref()
}
pub fn climb_descent(&self) -> &ClimbDescentAlongLeg {
&self.climb_descent
}
pub fn tas(&self) -> Option<&Speed> {
self.tas.as_ref()
}
pub fn wind(&self) -> Option<&Wind> {
self.wind.as_ref()
}
pub fn headwind(&self) -> Option<Speed> {
self.wind.map(|w| w.headwind(&self.bearing))
}
pub fn heading(&self) -> Option<&Angle> {
self.heading.as_ref()
}
pub fn mh(&self) -> Option<&Angle> {
self.mh.as_ref()
}
pub fn bearing(&self) -> &Angle {
&self.bearing
}
pub fn mc(&self) -> &Angle {
&self.mc
}
pub fn dist(&self) -> &Length {
&self.dist
}
pub fn gs(&self) -> Option<&Speed> {
self.gs.as_ref()
}
pub fn wca(&self) -> Option<&Angle> {
self.wca.as_ref()
}
pub fn ete(&self) -> Option<&Duration> {
self.ete.as_ref()
}
pub fn fuel(&self, perf: &LegPerformance) -> Option<LegFuel> {
let from_level = self.climb_descent.from;
let to_level = self.climb_descent.to;
let reach_at = self.climb_descent.reach_at;
let mut climb_time = Duration::s(0);
let mut descent_time = Duration::s(0);
let mut climb_fuel: Option<Fuel> = None;
let mut descent_fuel: Option<Fuel> = None;
let mut add_transition =
|current: &VerticalDistance, target: &VerticalDistance| -> Option<()> {
let (lo, hi) = if target > current {
(current, target)
} else {
(target, current)
};
let is_climb = target > current;
let cdp = if is_climb {
perf.climb()?
} else {
perf.descent()?
};
let hw = self.headwind().unwrap_or(Speed::kt(0.0));
let result = cdp.between(lo, hi)?.with_wind(hw);
if is_climb {
climb_time = climb_time + result.time;
climb_fuel = Some(match climb_fuel {
Some(f) => f + result.fuel,
None => result.fuel,
});
} else {
descent_time = descent_time + result.time;
descent_fuel = Some(match descent_fuel {
Some(f) => f + result.fuel,
None => result.fuel,
});
}
Some(())
};
let mut current = from_level;
if let (Some(from), Some(to)) = (current, to_level) {
if add_transition(&from, &to).is_some() {
current = Some(to);
}
}
if let (Some(from), Some(ra)) = (current.or(from_level), reach_at) {
add_transition(&from, &ra);
}
let cruise_fuel = match (self.ete, self.level) {
(Some(ete), Some(level)) => {
let climb_descent_time = climb_time + descent_time;
if climb_descent_time < ete {
let cruise_time = ete - climb_descent_time;
perf.cruise().map(|c| c.ff(&level) * cruise_time)
} else {
None
}
}
_ => None,
};
if climb_fuel.is_none() && cruise_fuel.is_none() && descent_fuel.is_none() {
return None;
}
Some(LegFuel::new(climb_fuel, cruise_fuel, descent_fuel))
}
}
fn wind_correction_angle(wind: &Wind, tas: &Speed, bearing: &Angle) -> Angle {
let wind_azimuth = wind.direction + Angle::t(180.0);
let wind_angle = *bearing - wind_azimuth;
Angle::from_si(
(wind.speed / *tas * wind_angle.to_si().sin()).asin(),
AngleUnit::TrueNorth,
)
}
fn ground_speed(tas: &Speed, wind: &Wind, wca: &Angle, bearing: &Angle) -> Speed {
Speed::from_si(
(*tas * *tas + wind.speed * wind.speed
- ((*tas * wind.speed * 2.0) * (*bearing - wind.direction + *wca).to_si().cos()))
.to_si()
.sqrt(),
*tas.unit(),
)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[test]
fn wind_correction_angle_left() {
let wca = wind_correction_angle(
&Wind::from_str("18050KT").unwrap(),
&Speed::from_str("N0100").unwrap(),
&Angle::t(90.0),
);
assert_eq!(wca.value().round(), 30.0);
}
#[test]
fn wind_correction_angle_right() {
let wca = wind_correction_angle(
&Wind::from_str("00050KT").unwrap(),
&Speed::from_str("N0100").unwrap(),
&Angle::t(90.0),
);
assert_eq!(wca.value().round(), 330.0);
}
}