unit_sphere/
lib.rs

1// Copyright (c) 2024-2025 Ken Barker
2
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"),
5// to deal in the Software without restriction, including without limitation the
6// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7// sell copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21//! # unit-sphere
22//!
23//! [![crates.io](https://img.shields.io/crates/v/unit-sphere.svg)](https://crates.io/crates/unit-sphere)
24//! [![docs.io](https://docs.rs/unit-sphere/badge.svg)](https://docs.rs/unit-sphere/)
25//! [![License](https://img.shields.io/badge/License-MIT-blue)](https://opensource.org/license/mit/)
26//! [![Rust](https://github.com/kenba/unit-sphere-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/kenba/unit-sphere-rs/actions)
27//! [![codecov](https://codecov.io/gh/kenba/unit-sphere-rs/graph/badge.svg?token=G1H1XINERW)](https://codecov.io/gh/kenba/unit-sphere-rs)
28//!
29//! A library for performing geometric calculations on the surface of a sphere.
30//!
31//! The library uses a combination of spherical trigonometry and vector geometry
32//! to perform [great-circle navigation](https://en.wikipedia.org/wiki/Great-circle_navigation)
33//! on the surface of a unit sphere, see *Figure 1*.
34//!
35//! ![great circle arc](https://via-technology.aero/img/navigation/sphere/great_circle_arc.svg)
36//!
37//! *Figure 1 A Great Circle Arc*
38//!
39//! A [great circle](https://en.wikipedia.org/wiki/Great_circle) is the
40//! shortest path between positions on the surface of a sphere.
41//! It is the spherical equivalent of a straight line in planar geometry.
42//!
43//! ## Spherical trigonometry
44//!
45//! A great circle path between positions may be found using
46//! [spherical trigonometry](https://en.wikipedia.org/wiki/Spherical_trigonometry).
47//!
48//! The [course](https://en.wikipedia.org/wiki/Great-circle_navigation#Course)
49//! (initial azimuth) of a great circle can be calculated from the
50//! latitudes and longitudes of the start and end points.
51//! While great circle distance can also be calculated from the latitudes and
52//! longitudes of the start and end points using the
53//! [haversine formula](https://en.wikipedia.org/wiki/Haversine_formula).
54//! The resulting distance in `Radians` can be converted to the required units by
55//! multiplying the distance by the Earth radius measured in the required units.
56//!
57//! ## Vector geometry
58//!
59//! Points on the surface of a sphere and great circle poles may be represented
60//! by 3D [vectors](https://www.movable-type.co.uk/scripts/latlong-vectors.html).
61//! Many calculations are simpler using vectors than spherical trigonometry.
62//!
63//! ![Spherical Vector Coordinates](https://via-technology.aero/img/navigation/sphere/ecef_coordinates.svg)
64//!
65//! *Figure 2 Spherical Vector Coordinates*
66//!
67//! For example, the across track distance of a point from a great circle can
68//! be calculated from the [dot product](https://en.wikipedia.org/wiki/Dot_product)
69//! of the point and the great circle pole vectors.
70//! While intersection points of great circles can simply be calculated from
71//! the [cross product](https://en.wikipedia.org/wiki/Cross_product) of their
72//! pole vectors.
73//!
74//! ## Design
75//!
76//! The `great_circle` module performs spherical trigonometric calculations
77//! and the `vector` module performs vector geometry calculations.
78//! See: [spherical vector geometry](https://via-technology.aero/navigation/spherical-vector-geometry/).
79//!
80//! The software uses types: `Angle`, `Degrees` and `Radians` from the
81//! [angle-sc](https://crates.io/crates/angle-sc) crate.
82//!
83//! The library is declared [no_std](https://docs.rust-embedded.org/book/intro/no-std.html)
84//! so it can be used in embedded applications.
85//!
86//! ## Example
87//!
88//! The following example calculates the intersection between two Great Circle `Arc`s
89//! it is taken from Charles Karney's original solution to
90//! [Intersection between two geodesic lines](https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a).
91//!
92//! ```rust
93//! use unit_sphere::{Arc, Degrees, LatLong, calculate_intersection_point};
94//! use angle_sc::is_within_tolerance;
95//!
96//! let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
97//! let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
98//! let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
99//! let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
100//!
101//! let arc_0 = Arc::try_from((&istanbul, &washington)).unwrap();
102//! let arc_1 = Arc::try_from((&reyjavik, &accra)).unwrap();
103//!
104//! let intersection_point = calculate_intersection_point(&arc_0, &arc_1).unwrap();
105//! let lat_long = LatLong::from(&intersection_point);
106//! // Geodesic intersection latitude is 54.7170296089477
107//! assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
108//! // Geodesic intersection longitude is -14.56385574430775
109//! assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
110//! ```
111
112#![cfg_attr(not(test), no_std)]
113
114extern crate angle_sc;
115extern crate nalgebra as na;
116
117pub mod great_circle;
118pub mod vector;
119
120pub use angle_sc::{Angle, Degrees, Radians, Validate};
121use thiserror::Error;
122
123/// Test whether a latitude in degrees is a valid latitude.
124///
125/// I.e. whether it lies in the range: -90.0 <= degrees <= 90.0
126#[must_use]
127pub fn is_valid_latitude(degrees: f64) -> bool {
128    (-90.0..=90.0).contains(&degrees)
129}
130
131/// Test whether a longitude in degrees is a valid longitude.
132///
133/// I.e. whether it lies in the range: -180.0 <= degrees <= 180.0
134#[must_use]
135pub fn is_valid_longitude(degrees: f64) -> bool {
136    (-180.0..=180.0).contains(&degrees)
137}
138
139/// A position as a latitude and longitude pair of `Degrees`.
140#[derive(Clone, Copy, Debug, PartialEq)]
141pub struct LatLong {
142    lat: Degrees,
143    lon: Degrees,
144}
145
146impl Validate for LatLong {
147    /// Test whether a `LatLong` is valid.
148    ///
149    /// I.e. whether the latitude lies in the range: -90.0 <= lat <= 90.0
150    /// and the longitude lies in the range: -180.0 <= lon <= 180.0
151    fn is_valid(&self) -> bool {
152        is_valid_latitude(self.lat.0) && is_valid_longitude(self.lon.0)
153    }
154}
155
156impl LatLong {
157    #[must_use]
158    pub const fn new(lat: Degrees, lon: Degrees) -> Self {
159        Self { lat, lon }
160    }
161
162    #[must_use]
163    pub const fn lat(&self) -> Degrees {
164        self.lat
165    }
166
167    #[must_use]
168    pub const fn lon(&self) -> Degrees {
169        self.lon
170    }
171
172    /// Determine whether the `LatLong` is South of  a.
173    ///
174    /// It compares the latitude of the two points.
175    /// * `a` - the other `LatLong`.
176    ///
177    /// returns true if South of a, false otherwise.
178    #[must_use]
179    pub fn is_south_of(&self, a: &Self) -> bool {
180        self.lat.0 < a.lat.0
181    }
182
183    /// Determine whether the `LatLong` is West of `LatLong` a.
184    ///
185    /// It compares the longitude difference of the two points.
186    /// * `a`, `b` - the points.
187    ///
188    /// returns true if a is West of b, false otherwise.
189    #[must_use]
190    pub fn is_west_of(&self, a: &Self) -> bool {
191        (a.lon() - self.lon).0 < 0.0
192    }
193}
194
195/// A Error type for an invalid `LatLong`.
196#[derive(Error, Debug, PartialEq)]
197pub enum LatLongError {
198    #[error("invalid latitude value: `{0}`")]
199    Latitude(f64),
200    #[error("invalid longitude value: `{0}`")]
201    Longitude(f64),
202}
203
204impl TryFrom<(f64, f64)> for LatLong {
205    type Error = LatLongError;
206
207    /// Attempt to convert a pair of f64 values in latitude, longitude order.
208    ///
209    /// return a valid `LatLong` or a `LatLongError`.
210    fn try_from(lat_long: (f64, f64)) -> Result<Self, Self::Error> {
211        if !is_valid_latitude(lat_long.0) {
212            Err(LatLongError::Latitude(lat_long.0))
213        } else if !is_valid_longitude(lat_long.1) {
214            Err(LatLongError::Longitude(lat_long.1))
215        } else {
216            Ok(Self::new(Degrees(lat_long.0), Degrees(lat_long.1)))
217        }
218    }
219}
220
221/// Calculate the azimuth and distance along the great circle of point b from
222/// point a.
223/// * `a`, `b` - the start and end positions
224///
225/// returns the great-circle azimuth relative to North and distance of point b
226/// from point a.
227#[must_use]
228pub fn calculate_azimuth_and_distance(a: &LatLong, b: &LatLong) -> (Angle, Radians) {
229    let a_lat = Angle::from(a.lat);
230    let b_lat = Angle::from(b.lat);
231    let delta_long = Angle::from((b.lon, a.lon));
232    (
233        great_circle::calculate_gc_azimuth(a_lat, b_lat, delta_long),
234        great_circle::calculate_gc_distance(a_lat, b_lat, delta_long),
235    )
236}
237
238/// Calculate the distance along the great circle of point b from point a.
239///
240/// See: [Haversine formula](https://en.wikipedia.org/wiki/Haversine_formula).
241/// This function is less accurate than `calculate_azimuth_and_distance`.
242/// * `a`, `b` - the start and end positions
243///
244/// returns the great-circle distance of point b from point a in `Radians`.
245#[must_use]
246pub fn haversine_distance(a: &LatLong, b: &LatLong) -> Radians {
247    let a_lat = Angle::from(a.lat);
248    let b_lat = Angle::from(b.lat);
249    let delta_lat = Angle::from((b.lat, a.lat));
250    let delta_long = Angle::from(b.lon - a.lon);
251    great_circle::calculate_haversine_distance(a_lat, b_lat, delta_long, delta_lat)
252}
253
254/// A `Vector3d` is a [nalgebra](https://crates.io/crates/nalgebra) `Vector3<f64>`.
255#[allow(clippy::module_name_repetitions)]
256pub type Vector3d = na::Vector3<f64>;
257
258impl From<&LatLong> for Vector3d {
259    /// Convert a `LatLong` to a point on the unit sphere.
260    ///
261    /// @pre |lat| <= 90.0 degrees.
262    /// * `lat` - the latitude.
263    /// * `lon` - the longitude.
264    ///
265    /// returns a `Vector3d` of the point on the unit sphere.
266    fn from(a: &LatLong) -> Self {
267        vector::to_point(Angle::from(a.lat), Angle::from(a.lon))
268    }
269}
270
271impl From<&Vector3d> for LatLong {
272    /// Convert a point to a `LatLong`
273    fn from(value: &Vector3d) -> Self {
274        Self::new(
275            Degrees::from(vector::latitude(value)),
276            Degrees::from(vector::longitude(value)),
277        )
278    }
279}
280
281/// An `Arc` of a Great Circle on a unit sphere.
282#[derive(Clone, Copy, Debug, PartialEq)]
283pub struct Arc {
284    /// The start point of the `Arc`.
285    a: Vector3d,
286    /// The right hand pole of the Great Circle of the `Arc`.
287    pole: Vector3d,
288    /// The length of the `Arc`.
289    length: Radians,
290    /// The half width of the `Arc`.
291    half_width: Radians,
292}
293
294impl Validate for Arc {
295    /// Test whether an `Arc` is valid.
296    ///
297    /// I.e. both a and pole are on the unit sphere and are orthogonal and
298    /// both length and `half_width` are not negative.
299    fn is_valid(&self) -> bool {
300        vector::is_unit(&self.a)
301            && vector::is_unit(&self.pole)
302            && vector::are_orthogonal(&self.a, &self.pole)
303            && !self.length.0.is_sign_negative()
304            && !self.half_width.0.is_sign_negative()
305    }
306}
307
308impl Arc {
309    /// Construct an `Arc`
310    ///
311    /// * `a` - the start point of the `Arc`.
312    /// * `pole` - the right hand pole of the Great Circle of the `Arc`.
313    /// * `length` - the length of the `Arc`.
314    /// * `half_width` - the half width of the `Arc`.
315    #[must_use]
316    pub const fn new(a: Vector3d, pole: Vector3d, length: Radians, half_width: Radians) -> Self {
317        Self {
318            a,
319            pole,
320            length,
321            half_width,
322        }
323    }
324
325    /// Construct an `Arc`
326    ///
327    /// * `a` - the start position
328    /// * `azimuth` - the azimuth at a.
329    /// * `length` - the length of the `Arc`.
330    #[must_use]
331    pub fn from_lat_lon_azi_length(a: &LatLong, azimuth: Angle, length: Radians) -> Self {
332        Self::new(
333            Vector3d::from(a),
334            vector::calculate_pole(Angle::from(a.lat()), Angle::from(a.lon()), azimuth),
335            length,
336            Radians(0.0),
337        )
338    }
339
340    /// Construct an `Arc` from the start and end positions.
341    ///
342    /// Note: if the points are the same or antipodal, the pole will be invalid.
343    /// * `a`, `b` - the start and end positions
344    #[must_use]
345    pub fn between_positions(a: &LatLong, b: &LatLong) -> Self {
346        let (azimuth, length) = calculate_azimuth_and_distance(a, b);
347        let a_lat = Angle::from(a.lat());
348        // if a is at the North or South pole
349        if a_lat.cos().0 < great_circle::MIN_VALUE {
350            // use b's longitude
351            Self::from_lat_lon_azi_length(&LatLong::new(a.lat(), b.lon()), azimuth, length)
352        } else {
353            Self::from_lat_lon_azi_length(a, azimuth, length)
354        }
355    }
356
357    /// Set the `half_width` of an `Arc`.
358    ///
359    /// * `half_width` - the half width of the `Arc`.
360    #[must_use]
361    pub const fn set_half_width(&mut self, half_width: Radians) -> &mut Self {
362        self.half_width = half_width;
363        self
364    }
365
366    /// The start point of the `Arc`.
367    #[must_use]
368    pub const fn a(&self) -> Vector3d {
369        self.a
370    }
371
372    /// The right hand pole of the Great Circle at the start point of the `Arc`.
373    #[must_use]
374    pub const fn pole(&self) -> Vector3d {
375        self.pole
376    }
377
378    /// The length of the `Arc`.
379    #[must_use]
380    pub const fn length(&self) -> Radians {
381        self.length
382    }
383
384    /// The half width of the `Arc`.
385    #[must_use]
386    pub const fn half_width(&self) -> Radians {
387        self.half_width
388    }
389
390    /// The azimuth at the start point.
391    #[must_use]
392    pub fn azimuth(&self) -> Angle {
393        vector::calculate_azimuth(&self.a, &self.pole)
394    }
395
396    /// The direction vector of the `Arc` at the start point.
397    #[must_use]
398    pub fn direction(&self) -> Vector3d {
399        vector::direction(&self.a, &self.pole)
400    }
401
402    /// A position vector at distance along the `Arc`.
403    #[must_use]
404    pub fn position(&self, distance: Radians) -> Vector3d {
405        vector::position(&self.a, &self.direction(), Angle::from(distance))
406    }
407
408    /// The end point of the `Arc`.
409    #[must_use]
410    pub fn b(&self) -> Vector3d {
411        self.position(self.length)
412    }
413
414    /// The mid point of the `Arc`.
415    #[must_use]
416    pub fn mid_point(&self) -> Vector3d {
417        self.position(self.length.half())
418    }
419
420    /// The position of a perpendicular point at distance from the `Arc`.
421    ///
422    /// * `point` a point on the `Arc`'s great circle.
423    /// * `distance` the perpendicular distance from the `Arc`'s great circle.
424    ///
425    /// returns the point at perpendicular distance from point.
426    #[must_use]
427    pub fn perp_position(&self, point: &Vector3d, distance: Radians) -> Vector3d {
428        vector::position(point, &self.pole, Angle::from(distance))
429    }
430
431    /// The position of a point at angle from the `Arc` start, at `Arc` length.
432    ///
433    /// * `angle` the angle from the `Arc` start.
434    ///
435    /// returns the point at angle from the `Arc` start, at `Arc` length.
436    #[must_use]
437    pub fn angle_position(&self, angle: Angle) -> Vector3d {
438        vector::rotate_position(&self.a, &self.pole, angle, Angle::from(self.length))
439    }
440
441    /// The `Arc` at the end of an `Arc`, just the point if `half_width` is zero.
442    ///
443    /// @param `at_b` if true the `Arc` at b, else the `Arc` at a.
444    ///
445    /// @return the end `Arc` at a or b.
446    #[must_use]
447    pub fn end_arc(&self, at_b: bool) -> Self {
448        let p = if at_b { self.b() } else { self.a };
449        let pole = vector::direction(&p, &self.pole);
450        if self.half_width.0 < great_circle::MIN_VALUE {
451            Self::new(p, pole, Radians(0.0), Radians(0.0))
452        } else {
453            let a = self.perp_position(&p, self.half_width);
454            Self::new(a, pole, self.half_width + self.half_width, Radians(0.0))
455        }
456    }
457
458    /// Calculate great-circle along and across track distances of point from
459    /// the `Arc`.
460    ///
461    /// * `point` - the point.
462    ///
463    /// returns the along and across track distances of the point in Radians.
464    #[must_use]
465    pub fn calculate_atd_and_xtd(&self, point: &Vector3d) -> (Radians, Radians) {
466        vector::calculate_atd_and_xtd(&self.a, &self.pole(), point)
467    }
468
469    /// Calculate the shortest great-circle distance of a point from the `Arc`.
470    ///
471    /// * `point` - the point.
472    ///
473    /// returns the shortest distance of a point from the `Arc` in Radians.
474    #[must_use]
475    pub fn shortest_distance(&self, point: &Vector3d) -> Radians {
476        let (atd, xtd) = self.calculate_atd_and_xtd(point);
477        if vector::intersection::is_alongside(atd, self.length, Radians(4.0 * f64::EPSILON)) {
478            xtd.abs()
479        } else {
480            // adjust atd to measure the distance from the centre of the Arc to the point
481            let atd_centre = atd - self.length.half();
482            let p = if atd_centre.0.is_sign_negative() {
483                self.a
484            } else {
485                self.b()
486            };
487            great_circle::e2gc_distance(vector::distance(&p, point))
488        }
489    }
490}
491
492/// A Error type for an invalid `Arc`.
493#[derive(Error, Debug, PartialEq)]
494pub enum ArcError {
495    #[error("positions are too close: `{0}`")]
496    PositionsTooClose(f64),
497    #[error("positions are too far apart: `{0}`")]
498    PositionsTooFar(f64),
499}
500
501impl TryFrom<(&LatLong, &LatLong)> for Arc {
502    type Error = ArcError;
503
504    /// Construct an `Arc` from a pair of positions.
505    ///
506    /// * `params` - the start and end positions
507    fn try_from(params: (&LatLong, &LatLong)) -> Result<Self, Self::Error> {
508        // Convert positions to vectors
509        let a = Vector3d::from(params.0);
510        let b = Vector3d::from(params.1);
511        // Calculate the great circle pole
512        vector::normalise(&a.cross(&b), vector::MIN_SQ_NORM).map_or_else(
513            || {
514                let sq_d = vector::sq_distance(&a, &b);
515                if sq_d < 1.0 {
516                    Err(ArcError::PositionsTooClose(sq_d))
517                } else {
518                    Err(ArcError::PositionsTooFar(sq_d))
519                }
520            },
521            |pole| {
522                Ok(Self::new(
523                    a,
524                    pole,
525                    great_circle::e2gc_distance(vector::distance(&a, &b)),
526                    Radians(0.0),
527                ))
528            },
529        )
530    }
531}
532
533/// Calculate the great-circle distances along a pair of `Arc`s to their
534/// closest intersection point or their coincident arc distances if the
535/// `Arc`s are on coincident Great Circles.
536///
537/// * `arc_0`, `arc_1` the `Arc`s.
538///
539/// returns the distances along the first `Arc` and second `Arc` to the intersection
540/// point or to their coincident arc distances if the `Arc`s do not intersect.
541#[must_use]
542pub fn calculate_intersection_distances(arc_0: &Arc, arc_1: &Arc) -> (Radians, Radians) {
543    vector::intersection::calculate_intersection_point_distances(
544        &arc_0.a,
545        &arc_0.pole,
546        arc_0.length(),
547        &arc_1.a,
548        &arc_1.pole,
549        arc_1.length(),
550        &(0.5 * (arc_0.mid_point() + arc_1.mid_point())),
551    )
552}
553
554/// Calculate whether a pair of `Arc`s intersect and (if so) where.
555///
556/// * `arc_0`, `arc_1` the `Arc`s.
557///
558/// returns the distance along the first `Arc` to the second `Arc` or None if they
559/// don't intersect.
560///
561/// # Examples
562/// ```
563/// use unit_sphere::{Arc, Degrees, LatLong, calculate_intersection_point};
564/// use angle_sc::is_within_tolerance;
565///
566/// let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
567/// let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
568/// let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
569/// let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
570///
571/// let arc_0 = Arc::try_from((&istanbul, &washington)).unwrap();
572/// let arc_1 = Arc::try_from((&reyjavik, &accra)).unwrap();
573///
574/// // Calculate the intersection point position
575/// let intersection_point = calculate_intersection_point(&arc_0, &arc_1).unwrap();
576/// let lat_long = LatLong::from(&intersection_point);
577///
578/// // The expected latitude and longitude are from:
579/// // <https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a>
580///
581/// // Geodesic intersection latitude is 54.7170296089477
582/// assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
583/// // Geodesic intersection longitude is -14.56385574430775
584/// assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
585/// ```
586#[must_use]
587pub fn calculate_intersection_point(arc_0: &Arc, arc_1: &Arc) -> Option<Vector3d> {
588    let (distance1, distance2) = calculate_intersection_distances(arc_0, arc_1);
589
590    // Determine whether both distances are within both arcs
591    if vector::intersection::is_alongside(distance1, arc_0.length(), Radians(4.0 * f64::EPSILON))
592        && vector::intersection::is_alongside(
593            distance2,
594            arc_1.length(),
595            Radians(4.0 * f64::EPSILON),
596        )
597    {
598        Some(arc_0.position(distance1.clamp(arc_0.length())))
599    } else {
600        None
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607    use angle_sc::{Degrees, is_within_tolerance};
608
609    #[test]
610    fn test_is_valid_latitude() {
611        // value < -90
612        assert!(!is_valid_latitude(-90.0001));
613        // value = -90
614        assert!(is_valid_latitude(-90.0));
615        // value = 90
616        assert!(is_valid_latitude(90.0));
617        // value > 90
618        assert!(!is_valid_latitude(90.0001));
619    }
620
621    #[test]
622    fn test_is_valid_longitude() {
623        // value < -180
624        assert!(!is_valid_longitude(-180.0001));
625        // value = -180
626        assert!(is_valid_longitude(-180.0));
627        // value = 180
628        assert!(is_valid_longitude(180.0));
629        // value > 180
630        assert!(!is_valid_longitude(180.0001));
631    }
632
633    #[test]
634    fn test_latlong_traits() {
635        let a = LatLong::try_from((0.0, 90.0)).unwrap();
636
637        assert!(a.is_valid());
638
639        let a_clone = a.clone();
640        assert!(a_clone == a);
641
642        assert_eq!(Degrees(0.0), a.lat());
643        assert_eq!(Degrees(90.0), a.lon());
644
645        assert!(!a.is_south_of(&a));
646        assert!(!a.is_west_of(&a));
647
648        let b = LatLong::try_from((-10.0, -91.0)).unwrap();
649        assert!(b.is_south_of(&a));
650        assert!(b.is_west_of(&a));
651
652        println!("LatLong: {:?}", a);
653
654        let invalid_lat = LatLong::try_from((91.0, 0.0));
655        assert_eq!(Err(LatLongError::Latitude(91.0)), invalid_lat);
656        println!("invalid_lat: {:?}", invalid_lat);
657
658        let invalid_lon = LatLong::try_from((0.0, 181.0));
659        assert_eq!(Err(LatLongError::Longitude(181.0)), invalid_lon);
660        println!("invalid_lon: {:?}", invalid_lon);
661    }
662
663    #[test]
664    fn test_vector3d_traits() {
665        let a = LatLong::try_from((0.0, 90.0)).unwrap();
666        let point = Vector3d::from(&a);
667
668        assert_eq!(0.0, point.x);
669        assert_eq!(1.0, point.y);
670        assert_eq!(0.0, point.z);
671
672        assert_eq!(Degrees(0.0), Degrees::from(vector::latitude(&point)));
673        assert_eq!(Degrees(90.0), Degrees::from(vector::longitude(&point)));
674
675        let result = LatLong::from(&point);
676        assert_eq!(a, result);
677    }
678
679    #[test]
680    fn test_great_circle_90n_0n_0e() {
681        let a = LatLong::new(Degrees(90.0), Degrees(0.0));
682        let b = LatLong::new(Degrees(0.0), Degrees(0.0));
683        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
684
685        assert!(is_within_tolerance(
686            core::f64::consts::FRAC_PI_2,
687            dist.0,
688            f64::EPSILON
689        ));
690        assert_eq!(180.0, Degrees::from(azimuth).0);
691
692        let dist = haversine_distance(&a, &b);
693        assert!(is_within_tolerance(
694            core::f64::consts::FRAC_PI_2,
695            dist.0,
696            f64::EPSILON
697        ));
698    }
699
700    #[test]
701    fn test_great_circle_90s_0n_50e() {
702        let a = LatLong::new(Degrees(-90.0), Degrees(0.0));
703        let b = LatLong::new(Degrees(0.0), Degrees(50.0));
704        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
705
706        assert!(is_within_tolerance(
707            core::f64::consts::FRAC_PI_2,
708            dist.0,
709            f64::EPSILON
710        ));
711        assert_eq!(0.0, Degrees::from(azimuth).0);
712
713        let dist = haversine_distance(&a, &b);
714        assert!(is_within_tolerance(
715            core::f64::consts::FRAC_PI_2,
716            dist.0,
717            f64::EPSILON
718        ));
719    }
720
721    #[test]
722    fn test_great_circle_0n_60e_0n_60w() {
723        let a = LatLong::new(Degrees(0.0), Degrees(60.0));
724        let b = LatLong::new(Degrees(0.0), Degrees(-60.0));
725        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
726
727        assert!(is_within_tolerance(
728            2.0 * core::f64::consts::FRAC_PI_3,
729            dist.0,
730            2.0 * f64::EPSILON
731        ));
732        assert_eq!(-90.0, Degrees::from(azimuth).0);
733
734        let dist = haversine_distance(&a, &b);
735        assert!(is_within_tolerance(
736            2.0 * core::f64::consts::FRAC_PI_3,
737            dist.0,
738            2.0 * f64::EPSILON
739        ));
740    }
741
742    #[test]
743    fn test_arc() {
744        // Greenwich equator
745        let g_eq = LatLong::new(Degrees(0.0), Degrees(0.0));
746
747        // 90 degrees East on the equator
748        let e_eq = LatLong::new(Degrees(0.0), Degrees(90.0));
749
750        let mut arc = Arc::between_positions(&g_eq, &e_eq);
751        let arc = arc.set_half_width(Radians(0.01));
752        assert!(arc.is_valid());
753        assert_eq!(Radians(0.01), arc.half_width());
754
755        assert_eq!(Vector3d::from(&g_eq), arc.a());
756        assert_eq!(Vector3d::new(0.0, 0.0, 1.0), arc.pole());
757        assert!(is_within_tolerance(
758            core::f64::consts::FRAC_PI_2,
759            arc.length().0,
760            f64::EPSILON
761        ));
762        assert_eq!(Angle::from(Degrees(90.0)), arc.azimuth());
763        let b = Vector3d::from(&e_eq);
764        assert!(is_within_tolerance(
765            0.0,
766            vector::distance(&b, &arc.b()),
767            f64::EPSILON
768        ));
769
770        let mid_point = arc.mid_point();
771        assert_eq!(0.0, mid_point.z);
772        assert!(is_within_tolerance(
773            45.0,
774            Degrees::from(vector::longitude(&mid_point)).0,
775            32.0 * f64::EPSILON
776        ));
777
778        let start_arc = arc.end_arc(false);
779        assert_eq!(0.02, start_arc.length().0);
780
781        let start_arc_a = start_arc.a();
782        assert_eq!(start_arc_a, arc.perp_position(&arc.a(), Radians(0.01)));
783
784        let angle_90 = Angle::from(Degrees(90.0));
785        let pole_0 = Vector3d::new(0.0, 0.0, 1.0);
786        assert!(vector::distance(&pole_0, &arc.angle_position(angle_90)) <= f64::EPSILON);
787
788        let end_arc = arc.end_arc(true);
789        assert_eq!(0.02, end_arc.length().0);
790
791        let end_arc_a = end_arc.a();
792        assert_eq!(end_arc_a, arc.perp_position(&arc.b(), Radians(0.01)));
793    }
794
795    #[test]
796    fn test_north_and_south_poles() {
797        let north_pole = LatLong::new(Degrees(90.0), Degrees(0.0));
798        let south_pole = LatLong::new(Degrees(-90.0), Degrees(0.0));
799
800        let (azimuth, distance) = calculate_azimuth_and_distance(&south_pole, &north_pole);
801        assert_eq!(0.0, Degrees::from(azimuth).0);
802        assert_eq!(core::f64::consts::PI, distance.0);
803
804        let (azimuth, distance) = calculate_azimuth_and_distance(&north_pole, &south_pole);
805        assert_eq!(180.0, Degrees::from(azimuth).0);
806        assert_eq!(core::f64::consts::PI, distance.0);
807
808        // 90 degrees East on the equator
809        let e_eq = LatLong::new(Degrees(0.0), Degrees(50.0));
810
811        let arc = Arc::between_positions(&north_pole, &e_eq);
812        assert!(is_within_tolerance(
813            e_eq.lat().0,
814            LatLong::from(&arc.b()).lat().abs().0,
815            1e-13
816        ));
817        assert!(is_within_tolerance(
818            e_eq.lon().0,
819            LatLong::from(&arc.b()).lon().0,
820            50.0 * f64::EPSILON
821        ));
822
823        let arc = Arc::between_positions(&south_pole, &e_eq);
824        assert!(is_within_tolerance(
825            e_eq.lat().0,
826            LatLong::from(&arc.b()).lat().abs().0,
827            1e-13
828        ));
829        assert!(is_within_tolerance(
830            e_eq.lon().0,
831            LatLong::from(&arc.b()).lon().0,
832            50.0 * f64::EPSILON
833        ));
834
835        let w_eq = LatLong::new(Degrees(0.0), Degrees(-140.0));
836
837        let arc = Arc::between_positions(&north_pole, &w_eq);
838        assert!(is_within_tolerance(
839            w_eq.lat().0,
840            LatLong::from(&arc.b()).lat().abs().0,
841            1e-13
842        ));
843        assert!(is_within_tolerance(
844            w_eq.lon().0,
845            LatLong::from(&arc.b()).lon().0,
846            256.0 * f64::EPSILON
847        ));
848
849        let arc = Arc::between_positions(&south_pole, &w_eq);
850        assert!(is_within_tolerance(
851            w_eq.lat().0,
852            LatLong::from(&arc.b()).lat().abs().0,
853            1e-13
854        ));
855        assert!(is_within_tolerance(
856            w_eq.lon().0,
857            LatLong::from(&arc.b()).lon().0,
858            256.0 * f64::EPSILON
859        ));
860
861        let invalid_arc = Arc::try_from((&north_pole, &north_pole));
862        assert_eq!(Err(ArcError::PositionsTooClose(0.0)), invalid_arc);
863        println!("invalid_arc: {:?}", invalid_arc);
864
865        let arc = Arc::between_positions(&north_pole, &north_pole);
866        assert_eq!(north_pole, LatLong::from(&arc.b()));
867
868        let invalid_arc = Arc::try_from((&north_pole, &south_pole));
869        assert_eq!(Err(ArcError::PositionsTooFar(4.0)), invalid_arc);
870        println!("invalid_arc: {:?}", invalid_arc);
871
872        let arc = Arc::between_positions(&north_pole, &south_pole);
873        assert_eq!(south_pole, LatLong::from(&arc.b()));
874
875        let arc = Arc::between_positions(&south_pole, &north_pole);
876        assert_eq!(north_pole, LatLong::from(&arc.b()));
877
878        let arc = Arc::between_positions(&south_pole, &south_pole);
879        assert_eq!(south_pole, LatLong::from(&arc.b()));
880    }
881
882    #[test]
883    fn test_arc_atd_and_xtd() {
884        // Greenwich equator
885        let g_eq = LatLong::new(Degrees(0.0), Degrees(0.0));
886
887        // 90 degrees East on the equator
888        let e_eq = LatLong::new(Degrees(0.0), Degrees(90.0));
889
890        let arc = Arc::try_from((&g_eq, &e_eq)).unwrap();
891        assert!(arc.is_valid());
892
893        let start_arc = arc.end_arc(false);
894        assert_eq!(0.0, start_arc.length().0);
895
896        let start_arc_a = start_arc.a();
897        assert_eq!(arc.a(), start_arc_a);
898
899        let longitude = Degrees(1.0);
900
901        // Test across track distance
902        // Accuracy drops off outside of this range
903        for lat in -83..84 {
904            let latitude = Degrees(lat as f64);
905            let latlong = LatLong::new(latitude, longitude);
906            let point = Vector3d::from(&latlong);
907
908            let expected = (lat as f64).to_radians();
909            let (atd, xtd) = arc.calculate_atd_and_xtd(&point);
910            assert!(is_within_tolerance(1_f64.to_radians(), atd.0, f64::EPSILON));
911            assert!(is_within_tolerance(expected, xtd.0, 2.0 * f64::EPSILON));
912
913            let d = arc.shortest_distance(&point);
914            assert!(is_within_tolerance(expected.abs(), d.0, 2.0 * f64::EPSILON));
915        }
916
917        let point = Vector3d::from(&g_eq);
918        let d = arc.shortest_distance(&point);
919        assert_eq!(0.0, d.0);
920
921        let point = Vector3d::from(&e_eq);
922        let d = arc.shortest_distance(&point);
923        assert_eq!(0.0, d.0);
924
925        let latlong = LatLong::new(Degrees(0.0), Degrees(-1.0));
926        let point = Vector3d::from(&latlong);
927        let d = arc.shortest_distance(&point);
928        assert!(is_within_tolerance(1_f64.to_radians(), d.0, f64::EPSILON));
929
930        let point = -point;
931        let d = arc.shortest_distance(&point);
932        assert!(is_within_tolerance(89_f64.to_radians(), d.0, f64::EPSILON));
933
934        // a point closer to the end of the arc than the start
935        let latlong = LatLong::new(Degrees(0.0), Degrees(-160.0));
936        let point = Vector3d::from(&latlong);
937        let d = arc.shortest_distance(&point);
938        // shortest distance is from the end of the arc to the point
939        assert_eq!(
940            great_circle::e2gc_distance(vector::distance(&arc.b(), &point)),
941            d
942        );
943    }
944
945    #[test]
946    fn test_arc_intersection_point() {
947        // Karney's example:
948        // Istanbul, Washington, Reyjavik and Accra
949        // from: <https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a>
950        let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
951        let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
952        let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
953        let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
954
955        let arc_0 = Arc::try_from((&istanbul, &washington)).unwrap();
956        let arc_1 = Arc::try_from((&reyjavik, &accra)).unwrap();
957
958        let intersection_point = calculate_intersection_point(&arc_0, &arc_1).unwrap();
959        let lat_long = LatLong::from(&intersection_point);
960        // Geodesic intersection latitude is 54.7170296089477
961        assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
962        // Geodesic intersection longitude is -14.56385574430775
963        assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
964
965        // Switch arcs
966        let intersection_point = calculate_intersection_point(&arc_1, &arc_0).unwrap();
967        let lat_long = LatLong::from(&intersection_point);
968        // Geodesic intersection latitude is 54.7170296089477
969        assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
970        // Geodesic intersection longitude is -14.56385574430775
971        assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
972    }
973
974    #[test]
975    fn test_arc_intersection_same_great_circles() {
976        let south_pole_1 = LatLong::new(Degrees(-88.0), Degrees(-180.0));
977        let south_pole_2 = LatLong::new(Degrees(-87.0), Degrees(0.0));
978
979        let arc_0 = Arc::try_from((&south_pole_1, &south_pole_2)).unwrap();
980
981        let intersection_lengths = calculate_intersection_distances(&arc_0, &arc_0);
982        assert_eq!(Radians(0.0), intersection_lengths.0);
983        assert_eq!(Radians(0.0), intersection_lengths.1);
984
985        let intersection_point = calculate_intersection_point(&arc_0, &arc_0).unwrap();
986        assert_eq!(0.0, vector::sq_distance(&arc_0.a(), &intersection_point));
987
988        let south_pole_3 = LatLong::new(Degrees(-85.0), Degrees(0.0));
989        let south_pole_4 = LatLong::new(Degrees(-86.0), Degrees(0.0));
990        let arc_1 = Arc::try_from((&south_pole_3, &south_pole_4)).unwrap();
991        let intersection_point = calculate_intersection_point(&arc_0, &arc_1);
992        assert!(intersection_point.is_none());
993    }
994}