#![cfg_attr(not(test), no_std)]
extern crate angle_sc;
extern crate nalgebra as na;
pub mod great_circle;
pub mod vector;
pub use angle_sc::{Angle, Degrees, Radians, Validate};
use thiserror::Error;
#[must_use]
pub fn is_valid_latitude(degrees: f64) -> bool {
(-90.0..=90.0).contains(°rees)
}
#[must_use]
pub fn is_valid_longitude(degrees: f64) -> bool {
(-180.0..=180.0).contains(°rees)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LatLong {
lat: Degrees,
lon: Degrees,
}
impl Validate for LatLong {
fn is_valid(&self) -> bool {
is_valid_latitude(self.lat.0) && is_valid_longitude(self.lon.0)
}
}
impl LatLong {
#[must_use]
pub const fn new(lat: Degrees, lon: Degrees) -> Self {
Self { lat, lon }
}
#[must_use]
pub const fn lat(&self) -> Degrees {
self.lat
}
#[must_use]
pub const fn lon(&self) -> Degrees {
self.lon
}
}
#[derive(Error, Debug, PartialEq)]
pub enum LatLongError {
#[error("invalid latitude value: `{0}`")]
Latitude(f64),
#[error("invalid longitude value: `{0}`")]
Longitude(f64),
}
impl TryFrom<(f64, f64)> for LatLong {
type Error = LatLongError;
fn try_from(lat_long: (f64, f64)) -> Result<Self, Self::Error> {
if !is_valid_latitude(lat_long.0) {
Err(LatLongError::Latitude(lat_long.0))
} else if !is_valid_longitude(lat_long.1) {
Err(LatLongError::Longitude(lat_long.1))
} else {
Ok(Self::new(Degrees(lat_long.0), Degrees(lat_long.1)))
}
}
}
#[must_use]
pub fn calculate_azimuth_and_distance(a: &LatLong, b: &LatLong) -> (Angle, Radians) {
let a_lat = Angle::from(a.lat);
let b_lat = Angle::from(b.lat);
let delta_long = Angle::from((b.lon, a.lon));
(
great_circle::calculate_gc_azimuth(a_lat, b_lat, delta_long),
great_circle::calculate_gc_distance(a_lat, b_lat, delta_long),
)
}
#[must_use]
pub fn haversine_distance(a: &LatLong, b: &LatLong) -> Radians {
let a_lat = Angle::from(a.lat);
let b_lat = Angle::from(b.lat);
let delta_lat = Angle::from((b.lat, a.lat));
let delta_long = Angle::from(b.lon - a.lon);
great_circle::calculate_haversine_distance(a_lat, b_lat, delta_long, delta_lat)
}
#[allow(clippy::module_name_repetitions)]
pub type Vector3d = na::Vector3<f64>;
impl From<&LatLong> for Vector3d {
fn from(a: &LatLong) -> Self {
vector::to_point(Angle::from(a.lat), Angle::from(a.lon))
}
}
impl From<&Vector3d> for LatLong {
fn from(value: &Vector3d) -> Self {
Self::new(
Degrees::from(vector::latitude(value)),
Degrees::from(vector::longitude(value)),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Arc {
a: Vector3d,
pole: Vector3d,
length: Radians,
half_width: Radians,
}
impl Validate for Arc {
fn is_valid(&self) -> bool {
vector::is_unit(&self.a)
&& vector::is_unit(&self.pole)
&& vector::are_orthogonal(&self.a, &self.pole)
&& !self.length.0.is_sign_negative()
&& !self.half_width.0.is_sign_negative()
}
}
impl Arc {
#[must_use]
pub const fn new(a: Vector3d, pole: Vector3d, length: Radians, half_width: Radians) -> Self {
Self {
a,
pole,
length,
half_width,
}
}
#[must_use]
pub fn from_lat_lon_azi_length(a: &LatLong, azimuth: Angle, length: Radians) -> Self {
Self::new(
Vector3d::from(a),
vector::calculate_pole(Angle::from(a.lat()), Angle::from(a.lon()), azimuth),
length,
Radians(0.0),
)
}
#[must_use]
pub fn between_positions(a: &LatLong, b: &LatLong) -> Self {
let (azimuth, length) = calculate_azimuth_and_distance(a, b);
let a_lat = Angle::from(a.lat());
if a_lat.cos().0 < great_circle::MIN_VALUE {
Self::from_lat_lon_azi_length(&LatLong::new(a.lat(), b.lon()), azimuth, length)
} else {
Self::from_lat_lon_azi_length(a, azimuth, length)
}
}
#[must_use]
pub const fn set_half_width(&mut self, half_width: Radians) -> &mut Self {
self.half_width = half_width;
self
}
#[must_use]
pub const fn a(&self) -> Vector3d {
self.a
}
#[must_use]
pub const fn pole(&self) -> Vector3d {
self.pole
}
#[must_use]
pub const fn length(&self) -> Radians {
self.length
}
#[must_use]
pub const fn half_width(&self) -> Radians {
self.half_width
}
#[must_use]
pub fn azimuth(&self) -> Angle {
vector::calculate_azimuth(&self.a, &self.pole)
}
#[must_use]
pub fn direction(&self) -> Vector3d {
vector::direction(&self.a, &self.pole)
}
#[must_use]
pub fn position(&self, distance: Radians) -> Vector3d {
vector::position(&self.a, &self.direction(), Angle::from(distance))
}
#[must_use]
pub fn b(&self) -> Vector3d {
self.position(self.length)
}
#[must_use]
pub fn mid_point(&self) -> Vector3d {
self.position(Radians(0.5 * self.length.0))
}
#[must_use]
pub fn perp_position(&self, point: &Vector3d, distance: Radians) -> Vector3d {
vector::position(point, &self.pole, Angle::from(distance))
}
#[must_use]
pub fn angle_position(&self, angle: Angle) -> Vector3d {
vector::rotate_position(&self.a, &self.pole, angle, Angle::from(self.length))
}
#[must_use]
pub fn end_arc(&self, at_b: bool) -> Self {
let p = if at_b { self.b() } else { self.a };
let pole = vector::direction(&p, &self.pole);
if self.half_width.0 < great_circle::MIN_VALUE {
Self::new(p, pole, Radians(0.0), Radians(0.0))
} else {
let a = self.perp_position(&p, self.half_width);
Self::new(a, pole, self.half_width + self.half_width, Radians(0.0))
}
}
#[must_use]
pub fn calculate_atd_and_xtd(&self, point: &Vector3d) -> (Radians, Radians) {
vector::calculate_atd_and_xtd(&self.a, &self.pole(), point)
}
}
#[derive(Error, Debug, PartialEq)]
pub enum ArcError {
#[error("positions are too close: `{0}`")]
PositionsTooClose(f64),
#[error("positions are too far apart: `{0}`")]
PositionsTooFar(f64),
}
impl TryFrom<(&LatLong, &LatLong)> for Arc {
type Error = ArcError;
fn try_from(params: (&LatLong, &LatLong)) -> Result<Self, Self::Error> {
let a = Vector3d::from(params.0);
let b = Vector3d::from(params.1);
vector::normalise(&a.cross(&b), vector::MIN_SQ_NORM).map_or_else(
|| {
let sq_d = vector::sq_distance(&a, &b);
if sq_d < 1.0 {
Err(ArcError::PositionsTooClose(sq_d))
} else {
Err(ArcError::PositionsTooFar(sq_d))
}
},
|pole| {
Ok(Self::new(
a,
pole,
great_circle::e2gc_distance(vector::distance(&a, &b)),
Radians(0.0),
))
},
)
}
}
#[must_use]
pub fn calculate_intersection_distances(arc1: &Arc, arc2: &Arc) -> (Radians, Radians) {
vector::intersection::calculate_intersection_point_distances(
&arc1.a,
&arc1.pole,
arc1.length(),
&arc2.a,
&arc2.pole,
arc2.length(),
&(0.5 * (arc1.mid_point() + arc2.mid_point())),
)
}
#[must_use]
pub fn calculate_intersection_point(arc1: &Arc, arc2: &Arc) -> Option<Vector3d> {
let (distance1, distance2) = calculate_intersection_distances(arc1, arc2);
if vector::intersection::is_within(distance1.0, arc1.length().0)
&& vector::intersection::is_within(distance2.0, arc2.length().0)
{
Some(arc1.position(distance1))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use angle_sc::{is_within_tolerance, Degrees};
#[test]
fn test_is_valid_latitude() {
assert!(!is_valid_latitude(-90.0001));
assert!(is_valid_latitude(-90.0));
assert!(is_valid_latitude(90.0));
assert!(!is_valid_latitude(90.0001));
}
#[test]
fn test_is_valid_longitude() {
assert!(!is_valid_longitude(-180.0001));
assert!(is_valid_longitude(-180.0));
assert!(is_valid_longitude(180.0));
assert!(!is_valid_longitude(180.0001));
}
#[test]
fn test_latlong_traits() {
let a = LatLong::try_from((0.0, 90.0)).unwrap();
assert!(a.is_valid());
let a_clone = a.clone();
assert!(a_clone == a);
assert_eq!(Degrees(0.0), a.lat());
assert_eq!(Degrees(90.0), a.lon());
println!("LatLong: {:?}", a);
let invalid_lat = LatLong::try_from((91.0, 0.0));
assert_eq!(Err(LatLongError::Latitude(91.0)), invalid_lat);
println!("invalid_lat: {:?}", invalid_lat);
let invalid_lon = LatLong::try_from((0.0, 181.0));
assert_eq!(Err(LatLongError::Longitude(181.0)), invalid_lon);
println!("invalid_lon: {:?}", invalid_lon);
}
#[test]
fn test_vector3d_traits() {
let a = LatLong::try_from((0.0, 90.0)).unwrap();
let point = Vector3d::from(&a);
assert_eq!(0.0, point.x);
assert_eq!(1.0, point.y);
assert_eq!(0.0, point.z);
assert_eq!(Degrees(0.0), Degrees::from(vector::latitude(&point)));
assert_eq!(Degrees(90.0), Degrees::from(vector::longitude(&point)));
let result = LatLong::from(&point);
assert_eq!(a, result);
}
#[test]
fn test_great_circle_90n_0n_0e() {
let a = LatLong::new(Degrees(90.0), Degrees(0.0));
let b = LatLong::new(Degrees(0.0), Degrees(0.0));
let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
assert!(is_within_tolerance(
core::f64::consts::FRAC_PI_2,
dist.0,
f64::EPSILON
));
assert_eq!(180.0, Degrees::from(azimuth).0);
let dist = haversine_distance(&a, &b);
assert!(is_within_tolerance(
core::f64::consts::FRAC_PI_2,
dist.0,
f64::EPSILON
));
}
#[test]
fn test_great_circle_90s_0n_50e() {
let a = LatLong::new(Degrees(-90.0), Degrees(0.0));
let b = LatLong::new(Degrees(0.0), Degrees(50.0));
let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
assert!(is_within_tolerance(
core::f64::consts::FRAC_PI_2,
dist.0,
f64::EPSILON
));
assert_eq!(0.0, Degrees::from(azimuth).0);
let dist = haversine_distance(&a, &b);
assert!(is_within_tolerance(
core::f64::consts::FRAC_PI_2,
dist.0,
f64::EPSILON
));
}
#[test]
fn test_great_circle_0n_60e_0n_60w() {
let a = LatLong::new(Degrees(0.0), Degrees(60.0));
let b = LatLong::new(Degrees(0.0), Degrees(-60.0));
let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
assert!(is_within_tolerance(
2.0 * core::f64::consts::FRAC_PI_3,
dist.0,
2.0 * f64::EPSILON
));
assert_eq!(-90.0, Degrees::from(azimuth).0);
let dist = haversine_distance(&a, &b);
assert!(is_within_tolerance(
2.0 * core::f64::consts::FRAC_PI_3,
dist.0,
2.0 * f64::EPSILON
));
}
#[test]
fn test_arc() {
let g_eq = LatLong::new(Degrees(0.0), Degrees(0.0));
let e_eq = LatLong::new(Degrees(0.0), Degrees(90.0));
let mut arc = Arc::between_positions(&g_eq, &e_eq);
let arc = arc.set_half_width(Radians(0.01));
assert!(arc.is_valid());
assert_eq!(Radians(0.01), arc.half_width());
assert_eq!(Vector3d::from(&g_eq), arc.a());
assert_eq!(Vector3d::new(0.0, 0.0, 1.0), arc.pole());
assert!(is_within_tolerance(
core::f64::consts::FRAC_PI_2,
arc.length().0,
f64::EPSILON
));
assert_eq!(Angle::from(Degrees(90.0)), arc.azimuth());
let b = Vector3d::from(&e_eq);
assert!(is_within_tolerance(
0.0,
vector::distance(&b, &arc.b()),
f64::EPSILON
));
let mid_point = arc.mid_point();
assert_eq!(0.0, mid_point.z);
assert!(is_within_tolerance(
45.0,
Degrees::from(vector::longitude(&mid_point)).0,
32.0 * f64::EPSILON
));
let start_arc = arc.end_arc(false);
assert_eq!(0.02, start_arc.length().0);
let start_arc_a = start_arc.a();
assert_eq!(start_arc_a, arc.perp_position(&arc.a(), Radians(0.01)));
let angle_90 = Angle::from(Degrees(90.0));
let pole_0 = Vector3d::new(0.0, 0.0, 1.0);
assert!(vector::distance(&pole_0, &arc.angle_position(angle_90)) <= f64::EPSILON);
let end_arc = arc.end_arc(true);
assert_eq!(0.02, end_arc.length().0);
let end_arc_a = end_arc.a();
assert_eq!(end_arc_a, arc.perp_position(&arc.b(), Radians(0.01)));
}
#[test]
fn test_north_and_south_poles() {
let north_pole = LatLong::new(Degrees(90.0), Degrees(0.0));
let south_pole = LatLong::new(Degrees(-90.0), Degrees(0.0));
let (azimuth, distance) = calculate_azimuth_and_distance(&south_pole, &north_pole);
assert_eq!(0.0, Degrees::from(azimuth).0);
assert_eq!(core::f64::consts::PI, distance.0);
let (azimuth, distance) = calculate_azimuth_and_distance(&north_pole, &south_pole);
assert_eq!(180.0, Degrees::from(azimuth).0);
assert_eq!(core::f64::consts::PI, distance.0);
let e_eq = LatLong::new(Degrees(0.0), Degrees(50.0));
let arc = Arc::between_positions(&north_pole, &e_eq);
assert!(is_within_tolerance(
e_eq.lat().0,
LatLong::from(&arc.b()).lat().abs().0,
1e-13
));
assert!(is_within_tolerance(
e_eq.lon().0,
LatLong::from(&arc.b()).lon().0,
50.0 * f64::EPSILON
));
let arc = Arc::between_positions(&south_pole, &e_eq);
assert!(is_within_tolerance(
e_eq.lat().0,
LatLong::from(&arc.b()).lat().abs().0,
1e-13
));
assert!(is_within_tolerance(
e_eq.lon().0,
LatLong::from(&arc.b()).lon().0,
50.0 * f64::EPSILON
));
let w_eq = LatLong::new(Degrees(0.0), Degrees(-140.0));
let arc = Arc::between_positions(&north_pole, &w_eq);
assert!(is_within_tolerance(
w_eq.lat().0,
LatLong::from(&arc.b()).lat().abs().0,
1e-13
));
assert!(is_within_tolerance(
w_eq.lon().0,
LatLong::from(&arc.b()).lon().0,
256.0 * f64::EPSILON
));
let arc = Arc::between_positions(&south_pole, &w_eq);
assert!(is_within_tolerance(
w_eq.lat().0,
LatLong::from(&arc.b()).lat().abs().0,
1e-13
));
assert!(is_within_tolerance(
w_eq.lon().0,
LatLong::from(&arc.b()).lon().0,
256.0 * f64::EPSILON
));
let invalid_arc = Arc::try_from((&north_pole, &north_pole));
assert_eq!(Err(ArcError::PositionsTooClose(0.0)), invalid_arc);
println!("invalid_arc: {:?}", invalid_arc);
let arc = Arc::between_positions(&north_pole, &north_pole);
assert_eq!(north_pole, LatLong::from(&arc.b()));
let invalid_arc = Arc::try_from((&north_pole, &south_pole));
assert_eq!(Err(ArcError::PositionsTooFar(4.0)), invalid_arc);
println!("invalid_arc: {:?}", invalid_arc);
let arc = Arc::between_positions(&north_pole, &south_pole);
assert_eq!(south_pole, LatLong::from(&arc.b()));
let arc = Arc::between_positions(&south_pole, &north_pole);
assert_eq!(north_pole, LatLong::from(&arc.b()));
let arc = Arc::between_positions(&south_pole, &south_pole);
assert_eq!(south_pole, LatLong::from(&arc.b()));
}
#[test]
fn test_arc_atd_and_xtd() {
let g_eq = LatLong::new(Degrees(0.0), Degrees(0.0));
let e_eq = LatLong::new(Degrees(0.0), Degrees(90.0));
let arc = Arc::try_from((&g_eq, &e_eq)).unwrap();
assert!(arc.is_valid());
let start_arc = arc.end_arc(false);
assert_eq!(0.0, start_arc.length().0);
let start_arc_a = start_arc.a();
assert_eq!(arc.a(), start_arc_a);
let longitude = Degrees(1.0);
for lat in -83..84 {
let latitude = Degrees(lat as f64);
let latlong = LatLong::new(latitude, longitude);
let point = Vector3d::from(&latlong);
let expected = (lat as f64).to_radians();
let (atd, xtd) = arc.calculate_atd_and_xtd(&point);
assert!(is_within_tolerance(1_f64.to_radians(), atd.0, f64::EPSILON));
assert!(is_within_tolerance(expected, xtd.0, 2.0 * f64::EPSILON));
}
}
#[test]
fn test_arc_intersection_point() {
let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
let arc1 = Arc::try_from((&istanbul, &washington)).unwrap();
let arc2 = Arc::try_from((&reyjavik, &accra)).unwrap();
let intersection_point = calculate_intersection_point(&arc1, &arc2).unwrap();
let lat_long = LatLong::from(&intersection_point);
assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
let intersection_point = calculate_intersection_point(&arc2, &arc1).unwrap();
let lat_long = LatLong::from(&intersection_point);
assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
}
#[test]
fn test_arc_intersection_same_great_circles() {
let south_pole_1 = LatLong::new(Degrees(-88.0), Degrees(-180.0));
let south_pole_2 = LatLong::new(Degrees(-87.0), Degrees(0.0));
let arc1 = Arc::try_from((&south_pole_1, &south_pole_2)).unwrap();
let intersection_lengths = calculate_intersection_distances(&arc1, &arc1);
assert_eq!(Radians(0.0), intersection_lengths.0);
assert_eq!(Radians(0.0), intersection_lengths.1);
let intersection_point = calculate_intersection_point(&arc1, &arc1).unwrap();
assert_eq!(0.0, vector::sq_distance(&arc1.a(), &intersection_point));
let south_pole_3 = LatLong::new(Degrees(-85.0), Degrees(0.0));
let south_pole_4 = LatLong::new(Degrees(-86.0), Degrees(0.0));
let arc2 = Arc::try_from((&south_pole_3, &south_pole_4)).unwrap();
let intersection_point = calculate_intersection_point(&arc1, &arc2);
assert!(intersection_point.is_none());
}
}