use core::fmt;
use core::time::Duration;
use crate::angle::TrueCourse;
use crate::error::{ensure_range, Result};
use crate::event::{NavigationIntegrity, PositionSource};
use crate::math;
use crate::observation::Observed;
use crate::position::Position;
use crate::units::{Angle, Distance, Speed};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GroundTrack {
pub course_over_ground: TrueCourse,
pub speed_over_ground: Speed,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(try_from = "StoredErrorEllipse", into = "StoredErrorEllipse")
)]
pub struct ErrorEllipse {
semi_major: Distance,
semi_minor: Distance,
orientation: TrueCourse,
}
impl ErrorEllipse {
#[must_use]
pub fn from_covariance(north: f64, north_east: f64, east: f64) -> Option<Self> {
if !(north.is_finite() && north_east.is_finite() && east.is_finite())
|| north < 0.0
|| east < 0.0
|| north_east * north_east > north * east * (1.0 + 1e-9)
{
return None;
}
let half_trace = f64::midpoint(north, east);
let half_difference = 0.5 * (north - east);
let radius = math::hypot(half_difference, north_east);
let major = (half_trace + radius).max(0.0);
let minor = (half_trace - radius).max(0.0);
let orientation = if radius < f64::MIN_POSITIVE {
0.0
} else {
0.5 * math::atan2(2.0 * north_east, north - east)
};
Some(Self {
semi_major: Distance::from_metres(math::sqrt(major)).ok()?,
semi_minor: Distance::from_metres(math::sqrt(minor)).ok()?,
orientation: TrueCourse::wrap(math::to_degrees(orientation)).ok()?,
})
}
pub fn new(
semi_major: Distance,
semi_minor: Distance,
orientation: TrueCourse,
) -> Result<Self> {
ensure_range("semi-major axis", semi_major.metres(), 0.0, f64::MAX)?;
ensure_range(
"semi-minor axis",
semi_minor.metres(),
0.0,
semi_major.metres(),
)?;
Ok(Self {
semi_major,
semi_minor,
orientation,
})
}
#[must_use]
pub fn circular(radius: Distance) -> Self {
let radius = Distance::from_metres(math::abs(radius.metres())).unwrap_or(radius);
Self {
semi_major: radius,
semi_minor: radius,
orientation: TrueCourse::NORTH,
}
}
#[must_use]
pub const fn semi_major(&self) -> Distance {
self.semi_major
}
#[must_use]
pub const fn semi_minor(&self) -> Distance {
self.semi_minor
}
#[must_use]
pub fn orientation(&self) -> TrueCourse {
let degrees = self.orientation.degrees();
if degrees >= 180.0 {
TrueCourse::wrap(degrees - 180.0).unwrap_or(self.orientation)
} else {
self.orientation
}
}
#[must_use]
pub fn equivalent_radius(&self) -> Distance {
Distance::from_metres(math::sqrt(
self.semi_major.metres() * self.semi_minor.metres(),
))
.unwrap_or(Distance::ZERO)
}
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct StoredErrorEllipse {
semi_major: Distance,
semi_minor: Distance,
orientation: TrueCourse,
}
#[cfg(feature = "serde")]
impl TryFrom<StoredErrorEllipse> for ErrorEllipse {
type Error = crate::error::KernelError;
fn try_from(stored: StoredErrorEllipse) -> Result<Self> {
Self::new(stored.semi_major, stored.semi_minor, stored.orientation)
}
}
#[cfg(feature = "serde")]
impl From<ErrorEllipse> for StoredErrorEllipse {
fn from(ellipse: ErrorEllipse) -> Self {
Self {
semi_major: ellipse.semi_major,
semi_minor: ellipse.semi_minor,
orientation: ellipse.orientation,
}
}
}
impl fmt::Display for ErrorEllipse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:.1} m × {:.1} m at {:.0}",
self.semi_major.metres(),
self.semi_minor.metres(),
self.orientation()
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NavigationSnapshot {
position: Option<Observed<Position, Distance>>,
source: Option<PositionSource>,
ground_track: Option<GroundTrack>,
heading: Option<TrueCourse>,
heading_sigma: Option<Angle>,
horizontal_error: Option<ErrorEllipse>,
integrity: Option<NavigationIntegrity>,
age: Option<Duration>,
stale: bool,
}
impl NavigationSnapshot {
pub const EMPTY: Self = Self {
position: None,
source: None,
ground_track: None,
heading: None,
heading_sigma: None,
horizontal_error: None,
integrity: None,
age: None,
stale: false,
};
#[must_use]
pub const fn with_position(
mut self,
position: Observed<Position, Distance>,
source: PositionSource,
) -> Self {
self.position = Some(position);
self.source = Some(source);
self
}
#[must_use]
pub const fn with_ground_track(mut self, track: GroundTrack) -> Self {
self.ground_track = Some(track);
self
}
#[must_use]
pub const fn with_heading(mut self, heading: TrueCourse, sigma: Option<Angle>) -> Self {
self.heading = Some(heading);
self.heading_sigma = sigma;
self
}
#[must_use]
pub const fn with_horizontal_error(mut self, ellipse: ErrorEllipse) -> Self {
self.horizontal_error = Some(ellipse);
self
}
#[must_use]
pub const fn with_integrity(mut self, integrity: NavigationIntegrity) -> Self {
self.integrity = Some(integrity);
self
}
#[must_use]
pub const fn with_age(mut self, age: Option<Duration>, stale: bool) -> Self {
self.age = age;
self.stale = stale;
self
}
#[must_use]
pub const fn position(&self) -> Option<&Observed<Position, Distance>> {
self.position.as_ref()
}
#[must_use]
pub const fn source(&self) -> Option<PositionSource> {
self.source
}
#[must_use]
pub const fn ground_track(&self) -> Option<GroundTrack> {
self.ground_track
}
#[must_use]
pub const fn heading(&self) -> Option<TrueCourse> {
self.heading
}
#[must_use]
pub const fn heading_sigma(&self) -> Option<Angle> {
self.heading_sigma
}
#[must_use]
pub const fn horizontal_error(&self) -> Option<ErrorEllipse> {
self.horizontal_error
}
#[must_use]
pub const fn integrity(&self) -> Option<NavigationIntegrity> {
self.integrity
}
#[must_use]
pub const fn age(&self) -> Option<Duration> {
self.age
}
#[must_use]
pub const fn is_stale(&self) -> bool {
self.stale
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
use super::*;
use alloc::format;
#[test]
fn a_diagonal_covariance_gives_axes_along_north_and_east() {
let ellipse = ErrorEllipse::from_covariance(16.0, 0.0, 4.0).unwrap();
assert_eq!(ellipse.semi_major().metres(), 4.0);
assert_eq!(ellipse.semi_minor().metres(), 2.0);
assert_eq!(ellipse.orientation().degrees(), 0.0);
let across = ErrorEllipse::from_covariance(4.0, 0.0, 16.0).unwrap();
assert_eq!(across.orientation().degrees(), 90.0);
assert!((across.equivalent_radius().metres() - math::sqrt(8.0)).abs() < 1e-12);
assert_eq!(format!("{across}"), "4.0 m × 2.0 m at 090°T");
}
#[test]
fn a_correlated_covariance_tilts_the_ellipse() {
let ellipse = ErrorEllipse::from_covariance(1.0, 1.0, 1.0).unwrap();
assert!((ellipse.semi_major().metres() - math::sqrt(2.0)).abs() < 1e-12);
assert!(ellipse.semi_minor().metres() < 1e-9);
assert!((ellipse.orientation().degrees() - 45.0).abs() < 1e-9);
let other = ErrorEllipse::from_covariance(1.0, -0.5, 1.0).unwrap();
assert!((other.orientation().degrees() - 135.0).abs() < 1e-9);
}
#[test]
fn what_is_not_a_covariance_is_refused() {
assert!(ErrorEllipse::from_covariance(-1.0, 0.0, 1.0).is_none());
assert!(ErrorEllipse::from_covariance(1.0, 2.0, 1.0).is_none());
assert!(ErrorEllipse::from_covariance(f64::NAN, 0.0, 1.0).is_none());
assert!(ErrorEllipse::from_covariance(0.0, 0.0, 0.0).is_some());
}
#[test]
fn an_empty_snapshot_knows_nothing_and_is_not_stale() {
let empty = NavigationSnapshot::EMPTY;
assert!(empty.position().is_none());
assert!(empty.source().is_none());
assert!(empty.ground_track().is_none());
assert!(empty.heading().is_none());
assert!(empty.horizontal_error().is_none());
assert!(empty.integrity().is_none());
assert_eq!(empty.age(), None);
assert!(!empty.is_stale());
}
}