#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::cast_precision_loss,
clippy::unreadable_literal
)]
use kinavis::navigation_solutions::{convert_compass_course_to_true_course, course_to_steer};
use kinavis::relative_motion::{closest_point_of_approach, Approach, Contact, Vessel};
use kinavis::sailings::{cross_track, geodesic, great_circle, rhumb_line};
use kinavis::{
CompassCourse, DeviationTable, Distance, InterpolationMethod, Latitude, Longitude, Position,
Speed, TrueCourse, Variation,
};
const RELATIVE_TOLERANCE: f64 = 1e-13;
const EXPECTED: &[(&str, f64)] = &[
("geodesic distance", 4.400569641545919e6),
("geodesic initial course", 2.855186098598503e2),
("geodesic final course", 2.398290723772108e2),
("great circle distance", 4.387957396422941e6),
("great circle initial course", 2.8549288870176326e2),
("rhumb distance", 4.502369467231812e6),
("rhumb course", 2.6190742239797544e2),
("cross track", 5.428876433675403e5),
("along track", 1.8480527232796166e6),
("linear true course", 1.1864609563147455e2),
("linear error", 9.867653873996574e-3),
("cubic true course", 1.1865453885696725e2),
("cubic error", 9.867653873996574e-3),
("parametric true course", 1.1865454358979022e2),
("parametric error", 5.576907401953478e-16),
("shape preserving true course", 1.1865521326777915e2),
("shape preserving error", 9.867653873996574e-3),
("heading to steer", 2.640165414566188e2),
("speed made good", 1.0896027870557544e1),
("drift angle", -1.0983458543381188e1),
("cpa distance", 3.6462417517518966e0),
("cpa minutes", 1.6447758083983334e1),
("cpa bearing", 3.171151461328941e2),
];
#[test]
fn the_two_maths_libraries_agree() {
let measured = measurements();
assert_eq!(
measured.len(),
EXPECTED.len(),
"the snapshot is stale: regenerate it"
);
let mut worst = (0.0_f64, "");
for ((name, found), (stored, expected)) in measured.iter().zip(EXPECTED) {
assert_eq!(name, stored, "the snapshot is out of order: regenerate it");
let error = (found - expected).abs() / expected.abs().max(1.0);
if error > worst.0 {
worst = (error, name);
}
assert!(
error <= RELATIVE_TOLERANCE,
"{name}: {found:e} vs {expected:e}, relative error {error:e}"
);
}
println!("worst relative difference {:e}, at {}", worst.0, worst.1);
}
#[test]
#[ignore = "regenerates the snapshot rather than checking it"]
fn print_the_snapshot() {
for (name, value) in measurements() {
println!(" (\"{name}\", {value:e}),");
}
}
fn at(latitude: f64, longitude: f64) -> Position {
Position::new(
Latitude::from_degrees(latitude).expect("in range"),
Longitude::from_degrees(longitude).expect("in range"),
)
}
fn table() -> DeviationTable {
let values: [f64; 36] =
core::array::from_fn(|index| 3.0 * ((index as f64 * 10.0).to_radians()).sin());
DeviationTable::from_deviations(&values).expect("36 values is a valid swing")
}
fn measurements() -> Vec<(String, f64)> {
let mut out = Vec::new();
let mut record = |name: &str, value: f64| out.push((name.to_owned(), value));
let plymouth = at(50.35, -4.15);
let halifax = at(44.65, -63.57);
let g = geodesic(plymouth, halifax).unwrap();
record("geodesic distance", g.distance.metres());
record("geodesic initial course", g.initial_course.degrees());
record("geodesic final course", g.final_course.degrees());
let c = great_circle(plymouth, halifax).unwrap();
record("great circle distance", c.distance.metres());
record("great circle initial course", c.initial_course.degrees());
let r = rhumb_line(plymouth, halifax).unwrap();
record("rhumb distance", r.distance.metres());
record("rhumb course", r.initial_course.degrees());
let off = cross_track(at(47.0, -30.0), plymouth, halifax).unwrap();
record("cross track", off.distance.metres());
record("along track", off.along_track.metres());
let table = table();
for (name, method) in [
("linear", InterpolationMethod::Linear),
("cubic", InterpolationMethod::Cubic),
("parametric", InterpolationMethod::Parametric),
("shape preserving", InterpolationMethod::ShapePreserving),
] {
let solution = convert_compass_course_to_true_course(
CompassCourse::new(123.4).unwrap(),
Variation::new(-7.25).unwrap(),
&table,
method,
)
.unwrap();
record(&format!("{name} true course"), solution.course.degrees());
record(&format!("{name} error"), solution.estimated_error);
}
let steer = course_to_steer(
TrueCourse::new(275.0).unwrap(),
Speed::from_knots(12.5).unwrap(),
TrueCourse::new(35.0).unwrap(),
Speed::from_knots(2.75).unwrap(),
)
.unwrap();
record("heading to steer", steer.heading.degrees());
record("speed made good", steer.speed_over_ground.knots());
record("drift angle", steer.drift_angle.degrees());
let own = Vessel {
course: TrueCourse::new(45.0).unwrap(),
speed: Speed::from_knots(15.0).unwrap(),
};
let target = Vessel {
course: TrueCourse::new(230.0).unwrap(),
speed: Speed::from_knots(11.0).unwrap(),
};
let contact = Contact {
bearing: kinavis::TrueBearing::new(20.0).expect("in range"),
range: Distance::from_nautical_miles(8.0).unwrap(),
};
match closest_point_of_approach(own, contact, target).unwrap() {
Approach::Closing(cpa) => {
record("cpa distance", cpa.distance.nautical_miles());
record("cpa minutes", cpa.time_to_go.as_secs_f64() / 60.0);
record("cpa bearing", cpa.bearing.degrees());
}
other => panic!("the fixture must be a closing contact, not {other:?}"),
}
out
}