use core::time::Duration;
use crate::angle::{Direction, True, TrueCourse};
use crate::environment::EnvironmentSample;
use crate::error::{ensure_range, Result};
use crate::math;
use crate::position::Position;
use crate::sailings::great_circle_destination;
use crate::time::{Instant, Utc};
use crate::units::Distance;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ManOverboard {
position: Position,
at: Instant<Utc>,
}
impl ManOverboard {
#[must_use]
pub const fn new(position: Position, at: Instant<Utc>) -> Self {
Self { position, at }
}
#[must_use]
pub const fn position(&self) -> Position {
self.position
}
#[must_use]
pub const fn at(&self) -> Instant<Utc> {
self.at
}
pub fn datum(
&self,
env: &EnvironmentSample,
now: Instant<Utc>,
wind_factor: f64,
) -> Result<MobDatum> {
ensure_range("wind factor", wind_factor, 0.0, 1.0)?;
let elapsed = now.duration_since(self.at)?;
let hours = elapsed.as_secs_f64() / 3600.0;
let mut north = 0.0;
let mut east = 0.0;
let current = env.current().filter(|current| current.drift.knots() > 0.0);
if let Some(current) = current {
let (n, e) = current.set.components(current.drift.knots() * hours);
north += n;
east += e;
}
let wind = env.wind().filter(|wind| wind.speed().knots() > 0.0);
if let Some(wind) = wind {
let (n, e) = wind
.towards()
.components((wind.speed() * wind_factor).knots() * hours);
north += n;
east += e;
}
let drift = Distance::from_nautical_miles(math::hypot(north, east))?;
let (set, position) = if drift.nautical_miles() > 0.0 {
let set =
Direction::<True>::from_degrees_wrapped(math::to_degrees(math::atan2(east, north)));
(
Some(set),
great_circle_destination(self.position, set, drift)?.position,
)
} else {
(None, self.position)
};
Ok(MobDatum {
at: now,
position,
elapsed,
drift,
set,
current_applied: current.is_some(),
wind_applied: wind.is_some() && wind_factor > 0.0,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MobDatum {
at: Instant<Utc>,
position: Position,
elapsed: Duration,
drift: Distance,
set: Option<TrueCourse>,
current_applied: bool,
wind_applied: bool,
}
impl MobDatum {
#[must_use]
pub const fn at(&self) -> Instant<Utc> {
self.at
}
#[must_use]
pub const fn position(&self) -> Position {
self.position
}
#[must_use]
pub const fn elapsed(&self) -> Duration {
self.elapsed
}
#[must_use]
pub const fn drift(&self) -> Distance {
self.drift
}
#[must_use]
pub const fn set(&self) -> Option<TrueCourse> {
self.set
}
#[must_use]
pub const fn current_applied(&self) -> bool {
self.current_applied
}
#[must_use]
pub const fn wind_applied(&self) -> bool {
self.wind_applied
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
use super::*;
use crate::environment::{Current, Wind};
use crate::error::{KernelError, NavigationError};
use crate::geodesy::{GeodeticPoint, Height};
use crate::units::Speed;
fn at(latitude: f64, longitude: f64) -> Position {
Position::from_degrees(latitude, longitude).unwrap()
}
fn alarm() -> Instant<Utc> {
Instant::from_unix_seconds(1_789_000_000)
}
fn later(minutes: u64) -> Instant<Utc> {
alarm()
.checked_add(Duration::from_secs(minutes * 60))
.unwrap()
}
fn knots(value: f64) -> Speed {
Speed::from_knots(value).unwrap()
}
fn sample(position: Position) -> EnvironmentSample {
EnvironmentSample::at(
GeodeticPoint::new(position, Height::above_ellipsoid(Distance::ZERO)),
alarm(),
)
}
#[test]
fn with_nothing_resolved_the_datum_is_where_they_went_in() {
let here = at(50.0, -1.0);
let mob = ManOverboard::new(here, alarm());
let datum = mob.datum(&sample(here), later(30), 0.02).unwrap();
assert_eq!(datum.position(), here);
assert_eq!(datum.drift(), Distance::ZERO);
assert_eq!(datum.set(), None);
assert!(!datum.current_applied());
assert!(!datum.wind_applied());
assert_eq!(datum.elapsed(), Duration::from_secs(1800));
assert_eq!(datum.at(), later(30));
assert_eq!(mob.position(), here);
assert_eq!(mob.at(), alarm());
}
#[test]
fn the_current_carries_the_datum_at_its_full_rate() {
let here = at(50.0, -1.0);
let mob = ManOverboard::new(here, alarm());
let environment = sample(here).with_current(Current {
set: TrueCourse::EAST,
drift: knots(2.0),
});
let datum = mob.datum(&environment, later(30), 0.02).unwrap();
assert!((datum.drift().nautical_miles() - 1.0).abs() < 1e-9);
assert!((datum.set().unwrap().degrees() - 90.0).abs() < 1e-9);
assert!(datum.current_applied());
assert!(!datum.wind_applied());
assert!(datum.position().longitude().degrees() > -1.0);
assert!((datum.position().latitude().degrees() - 50.0).abs() < 1e-4);
}
#[test]
fn the_wind_carries_it_downwind_by_its_factor() {
let here = at(50.0, -1.0);
let mob = ManOverboard::new(here, alarm());
let environment =
sample(here).with_wind(Wind::new(TrueCourse::NORTH, knots(20.0)).unwrap());
let datum = mob.datum(&environment, later(60), 0.02).unwrap();
assert!((datum.drift().nautical_miles() - 0.4).abs() < 1e-9);
assert!((datum.set().unwrap().degrees() - 180.0).abs() < 1e-9);
assert!(datum.wind_applied());
let still = mob.datum(&environment, later(60), 0.0).unwrap();
assert_eq!(still.drift(), Distance::ZERO);
assert!(!still.wind_applied());
}
#[test]
fn the_two_drifts_add_as_vectors() {
let here = at(50.0, -1.0);
let mob = ManOverboard::new(here, alarm());
let environment = sample(here)
.with_current(Current {
set: TrueCourse::EAST,
drift: knots(1.0),
})
.with_wind(Wind::new(TrueCourse::NORTH, knots(20.0)).unwrap());
let datum = mob.datum(&environment, later(60), 0.02).unwrap();
assert!((datum.drift().nautical_miles() - math::hypot(1.0, 0.4)).abs() < 1e-9);
assert!((datum.set().unwrap().degrees() - 111.8).abs() < 0.1);
}
#[test]
fn the_datum_grows_with_time_and_refuses_to_go_backwards() {
let here = at(50.0, -1.0);
let mob = ManOverboard::new(here, alarm());
let environment = sample(here).with_current(Current {
set: TrueCourse::EAST,
drift: knots(1.0),
});
let mut previous = 0.0;
for minutes in [10, 20, 60, 180] {
let drift = mob
.datum(&environment, later(minutes), 0.0)
.unwrap()
.drift()
.nautical_miles();
assert!(drift > previous);
previous = drift;
}
assert!(matches!(
mob.datum(
&environment,
alarm().checked_sub(Duration::from_secs(1)).unwrap(),
0.0
)
.unwrap_err(),
NavigationError::Kernel(KernelError::TimeReversed { .. })
));
let now = mob.datum(&environment, alarm(), 0.0).unwrap();
assert_eq!(now.drift(), Distance::ZERO);
assert!(now.current_applied());
}
#[test]
fn the_wind_factor_is_a_fraction() {
let here = at(50.0, -1.0);
let mob = ManOverboard::new(here, alarm());
for factor in [-0.1, 1.5, f64::NAN] {
assert!(mob.datum(&sample(here), later(10), factor).is_err());
}
}
}