use crate::local::Enu;
use crate::position::Ecef;
use crate::vector::Vec3;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Course {
pub course_deg: f64,
pub ground_speed_mps: f64,
pub climb_mps: f64,
}
impl Course {
#[inline]
pub const fn new(course_deg: f64, ground_speed_mps: f64, climb_mps: f64) -> Self {
Course {
course_deg,
ground_speed_mps,
climb_mps,
}
}
pub fn from_enu_velocity(v: Enu) -> Course {
let ground = (v.east * v.east + v.north * v.north).sqrt();
let course_deg = if ground == 0.0 {
0.0
} else {
v.east.atan2(v.north).to_degrees().rem_euclid(360.0)
};
Course {
course_deg,
ground_speed_mps: ground,
climb_mps: v.up,
}
}
#[inline]
pub fn to_enu_velocity(self) -> Enu {
let (s, c) = self.course_deg.to_radians().sin_cos();
Enu::new(s * self.ground_speed_mps, c * self.ground_speed_mps, self.climb_mps)
}
#[inline]
pub fn speed_mps(self) -> f64 {
(self.ground_speed_mps * self.ground_speed_mps + self.climb_mps * self.climb_mps).sqrt()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Track {
pub position: Ecef,
pub velocity: Vec3,
}
impl Track {
#[inline]
pub const fn new(position: Ecef, velocity: Vec3) -> Self {
Track { position, velocity }
}
pub fn from_fixes(earlier: Ecef, now: Ecef, dt_s: f64) -> Self {
let velocity = if dt_s > 0.0 {
(now.vec() - earlier.vec()) / dt_s
} else {
Vec3::ZERO
};
Track {
position: now,
velocity,
}
}
#[inline]
pub fn at(self, dt_s: f64) -> Ecef {
self.position.offset(self.velocity * dt_s)
}
#[inline]
pub fn closest_approach(self, other: Track) -> Cpa {
closest_approach(self.position, self.velocity, other.position, other.velocity)
}
pub fn conflict(self, other: Track, threshold_m: f64) -> Option<Cpa> {
let cpa = self.closest_approach(other);
(cpa.time_s >= 0.0 && cpa.distance_m <= threshold_m).then_some(cpa)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cpa {
pub time_s: f64,
pub distance_m: f64,
}
#[doc(alias = "cpa")]
pub fn closest_approach(p1: Ecef, v1: Vec3, p2: Ecef, v2: Vec3) -> Cpa {
let prel = p1.vec() - p2.vec();
let vrel = v1 - v2;
let vv = vrel.dot(vrel);
let time_s = if vv == 0.0 { 0.0 } else { -prel.dot(vrel) / vv };
Cpa {
time_s,
distance_m: (prel + vrel * time_s).norm(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::close;
#[test]
fn parallel_non_closing_is_now() {
let c = closest_approach(
Ecef::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Ecef::new(0.0, 3.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
);
assert_eq!(c.time_s, 0.0);
assert!(close(c.distance_m, 3.0, 1e-9));
}
#[test]
fn diverging_reports_a_past_time() {
let c = closest_approach(
Ecef::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Ecef::new(-10.0, 0.0, 0.0),
Vec3::new(-1.0, 0.0, 0.0),
);
assert!(c.time_s < 0.0 && c.distance_m < 1e-9);
}
#[test]
fn tracks_dead_reckon_and_detect_conflicts() {
let a = Track::new(Ecef::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
assert_eq!(a.at(10.0), Ecef::new(10.0, 0.0, 0.0));
assert_eq!(a.at(-2.0), Ecef::new(-2.0, 0.0, 0.0));
let b = Track::new(Ecef::new(10.0, 5.0, 0.0), Vec3::new(-1.0, 0.0, 0.0));
let cpa = a.closest_approach(b);
assert!(close(cpa.time_s, 5.0, 1e-9) && close(cpa.distance_m, 5.0, 1e-9));
assert!(a.conflict(b, 6.0).is_some());
assert!(a.conflict(b, 4.0).is_none());
let past = Track::new(Ecef::new(-10.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0));
assert!(a.conflict(past, 100.0).is_none());
}
#[test]
fn from_fixes_infers_velocity() {
let t = Track::from_fixes(Ecef::new(0.0, 0.0, 0.0), Ecef::new(20.0, 0.0, 0.0), 10.0);
assert_eq!(t.velocity, Vec3::new(2.0, 0.0, 0.0));
assert_eq!(t.position, Ecef::new(20.0, 0.0, 0.0));
assert_eq!(
Track::from_fixes(Ecef::new(0.0, 0.0, 0.0), Ecef::new(20.0, 0.0, 0.0), 0.0).velocity,
Vec3::ZERO
);
}
#[test]
fn course_conventions() {
assert_eq!(
Course::from_enu_velocity(Enu::new(0.0, 0.0, 2.0)),
Course::new(0.0, 0.0, 2.0)
);
assert!(close(Course::new(270.0, 3.0, 4.0).speed_mps(), 5.0, 1e-12));
let w = Course::new(270.0, 10.0, 0.0).to_enu_velocity();
assert!(close(w.east, -10.0, 1e-9) && close(w.north, 0.0, 1e-9));
let c = Course::from_enu_velocity(Course::new(123.0, 50.0, -3.0).to_enu_velocity());
assert!(close(c.course_deg, 123.0, 1e-9) && close(c.ground_speed_mps, 50.0, 1e-9) && c.climb_mps == -3.0);
}
}