use crate::angle::TrueCourse;
use crate::error::{ensure_range, KernelError, NavigationError, Result};
use crate::event::{AnchorEvent, EventList};
use crate::math;
use crate::position::Position;
use crate::sailings::{great_circle, rhumb_destination};
use crate::snapshot::NavigationSnapshot;
use crate::time::{Instant, Utc};
use crate::units::Distance;
pub fn anchor_position(
antenna: Position,
heading: TrueCourse,
antenna_to_bow: Distance,
) -> Result<Position> {
ensure_range("antenna to bow", antenna_to_bow.metres(), 0.0, f64::MAX)?;
rhumb_destination(antenna, heading, antenna_to_bow)
}
pub fn swinging_radius(
cable: Distance,
depth_at_hawse: Distance,
antenna_to_bow: Distance,
) -> Result<Distance> {
ensure_range("depth at hawse", depth_at_hawse.metres(), 0.0, f64::MAX)?;
ensure_range("antenna to bow", antenna_to_bow.metres(), 0.0, f64::MAX)?;
ensure_range("cable", cable.metres(), depth_at_hawse.metres(), f64::MAX)?;
let scope = math::sqrt(
cable.metres() * cable.metres() - depth_at_hawse.metres() * depth_at_hawse.metres(),
);
Ok(Distance::from_metres(scope)? + antenna_to_bow)
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(try_from = "StoredWatch", into = "StoredWatch")
)]
pub struct AnchorWatch {
anchor: Position,
swinging_radius: Distance,
allowance: Distance,
}
impl AnchorWatch {
pub fn new(anchor: Position, swinging_radius: Distance) -> Result<Self> {
ensure_range("swinging radius", swinging_radius.metres(), 0.0, f64::MAX)?;
Ok(Self {
anchor,
swinging_radius,
allowance: Distance::ZERO,
})
}
pub fn with_allowance(mut self, allowance: Distance) -> Result<Self> {
ensure_range("allowance", allowance.metres(), 0.0, f64::MAX)?;
self.allowance = allowance;
Ok(self)
}
#[must_use]
pub const fn anchor(&self) -> Position {
self.anchor
}
#[must_use]
pub const fn swinging_radius(&self) -> Distance {
self.swinging_radius
}
#[must_use]
pub const fn allowance(&self) -> Distance {
self.allowance
}
#[must_use]
pub fn alarm_radius(&self) -> Distance {
self.swinging_radius + self.allowance
}
pub fn check(
&self,
state: &NavigationSnapshot,
) -> Result<(AnchorView, EventList<AnchorEvent>)> {
let observed = state
.position()
.ok_or(NavigationError::Kernel(KernelError::Missing {
what: "the vessel's position",
}))?;
let from_anchor = great_circle(self.anchor, *observed.value())?;
let view = AnchorView {
at: observed.taken_at(),
distance_from_anchor: from_anchor.distance,
bearing_from_anchor: from_anchor.initial_course,
alarm_radius: self.alarm_radius(),
};
let mut events = EventList::new();
if view.is_dragging() {
events.push(AnchorEvent::AnchorDragging {
distance: view.distance_from_anchor,
radius: view.alarm_radius,
at: view.at,
});
}
Ok((view, events))
}
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct StoredWatch {
anchor: Position,
swinging_radius: Distance,
allowance: Distance,
}
#[cfg(feature = "serde")]
impl TryFrom<StoredWatch> for AnchorWatch {
type Error = NavigationError;
fn try_from(stored: StoredWatch) -> Result<Self> {
Self::new(stored.anchor, stored.swinging_radius)?.with_allowance(stored.allowance)
}
}
#[cfg(feature = "serde")]
impl From<AnchorWatch> for StoredWatch {
fn from(watch: AnchorWatch) -> Self {
Self {
anchor: watch.anchor,
swinging_radius: watch.swinging_radius,
allowance: watch.allowance,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnchorView {
at: Instant<Utc>,
distance_from_anchor: Distance,
bearing_from_anchor: TrueCourse,
alarm_radius: Distance,
}
impl AnchorView {
#[must_use]
pub const fn at(&self) -> Instant<Utc> {
self.at
}
#[must_use]
pub const fn distance_from_anchor(&self) -> Distance {
self.distance_from_anchor
}
#[must_use]
pub const fn bearing_from_anchor(&self) -> TrueCourse {
self.bearing_from_anchor
}
#[must_use]
pub const fn alarm_radius(&self) -> Distance {
self.alarm_radius
}
#[must_use]
pub fn slack(&self) -> Distance {
self.alarm_radius - self.distance_from_anchor
}
#[must_use]
pub fn is_dragging(&self) -> bool {
self.slack().is_negative()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::indexing_slicing)]
mod tests {
use super::*;
use crate::event::PositionSource;
use crate::observation::{ObservationStatus, Observed, Quality};
fn metres(value: f64) -> Distance {
Distance::from_metres(value).unwrap()
}
fn at(latitude: f64, longitude: f64) -> Position {
Position::from_degrees(latitude, longitude).unwrap()
}
fn noon() -> Instant<Utc> {
Instant::from_unix_seconds(1_789_000_000)
}
fn snapshot(position: Position) -> NavigationSnapshot {
NavigationSnapshot::EMPTY.with_position(
Observed::new(
position,
noon(),
Quality::<Distance>::new(ObservationStatus::Valid),
),
PositionSource::Gnss,
)
}
#[test]
fn the_circle_is_the_horizontal_scope_plus_the_ship() {
let radius = swinging_radius(metres(130.0), metres(50.0), metres(30.0)).unwrap();
assert!((radius.metres() - 150.0).abs() < 1e-6);
let plumb = swinging_radius(metres(50.0), metres(50.0), metres(30.0)).unwrap();
assert!((plumb.metres() - 30.0).abs() < 1e-6);
assert!(swinging_radius(metres(40.0), metres(50.0), metres(30.0)).is_err());
assert!(swinging_radius(metres(130.0), metres(-1.0), metres(30.0)).is_err());
assert!(swinging_radius(metres(130.0), metres(50.0), metres(-1.0)).is_err());
}
#[test]
fn the_anchor_lies_ahead_of_the_antenna_by_the_length_of_the_bow() {
let antenna = at(50.0, -1.0);
let anchor = anchor_position(antenna, TrueCourse::EAST, metres(50.0)).unwrap();
let back = great_circle(antenna, anchor).unwrap();
assert!((back.distance.metres() - 50.0).abs() < 1e-3);
assert!((back.initial_course.degrees() - 90.0).abs() < 1e-3);
assert!(anchor_position(antenna, TrueCourse::EAST, metres(-1.0)).is_err());
}
#[test]
fn inside_the_circle_is_quiet_and_outside_is_an_alarm() {
let anchor = at(50.0, -1.0);
let watch = AnchorWatch::new(anchor, metres(100.0))
.unwrap()
.with_allowance(metres(20.0))
.unwrap();
assert!((watch.alarm_radius().metres() - 120.0).abs() < 1e-9);
let (inside, events) = watch
.check(&snapshot(at(50.0 - 0.05 / 60.0, -1.0)))
.unwrap();
assert!(!inside.is_dragging());
assert!(inside.slack().metres() > 20.0);
assert!((inside.bearing_from_anchor().degrees() - 180.0).abs() < 1e-6);
assert_eq!(inside.at(), noon());
assert!(events.is_empty());
let (outside, events) = watch
.check(&snapshot(at(50.0 + 0.08 / 60.0, -1.0)))
.unwrap();
assert!(outside.is_dragging());
assert!(outside.slack().is_negative());
assert!(matches!(
events[0],
AnchorEvent::AnchorDragging { distance, radius, at }
if distance == outside.distance_from_anchor()
&& radius == outside.alarm_radius()
&& at == noon()
));
}
#[test]
fn the_allowance_is_the_difference_between_a_swing_and_a_drag() {
let anchor = at(50.0, -1.0);
let here = at(50.0, -1.0 + 0.09 / 60.0 / math::cos(50.0_f64.to_radians()));
let tight = AnchorWatch::new(anchor, metres(150.0)).unwrap();
let easy = tight.with_allowance(metres(30.0)).unwrap();
assert!(tight.check(&snapshot(here)).unwrap().0.is_dragging());
assert!(!easy.check(&snapshot(here)).unwrap().0.is_dragging());
}
#[test]
fn a_watch_needs_a_position_and_sensible_radii() {
let watch = AnchorWatch::new(at(50.0, -1.0), metres(100.0)).unwrap();
assert!(matches!(
watch.check(&NavigationSnapshot::EMPTY).unwrap_err(),
NavigationError::Kernel(KernelError::Missing { .. })
));
assert!(AnchorWatch::new(at(50.0, -1.0), metres(-1.0)).is_err());
assert!(watch.with_allowance(metres(-1.0)).is_err());
assert_eq!(watch.allowance(), Distance::ZERO);
assert_eq!(watch.anchor(), at(50.0, -1.0));
assert_eq!(watch.swinging_radius(), metres(100.0));
}
}