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 arc1 = Arc::try_from((&istanbul, &washington)).unwrap();
102//! let arc2 = Arc::try_from((&reyjavik, &accra)).unwrap();
103//!
104//! let intersection_point = calculate_intersection_point(&arc1, &arc2).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
173/// A Error type for an invalid `LatLong`.
174#[derive(Error, Debug, PartialEq)]
175pub enum LatLongError {
176    #[error("invalid latitude value: `{0}`")]
177    Latitude(f64),
178    #[error("invalid longitude value: `{0}`")]
179    Longitude(f64),
180}
181
182impl TryFrom<(f64, f64)> for LatLong {
183    type Error = LatLongError;
184
185    /// Attempt to convert a pair of f64 values in latitude, longitude order.
186    ///
187    /// return a valid `LatLong` or a `LatLongError`.
188    fn try_from(lat_long: (f64, f64)) -> Result<Self, Self::Error> {
189        if !is_valid_latitude(lat_long.0) {
190            Err(LatLongError::Latitude(lat_long.0))
191        } else if !is_valid_longitude(lat_long.1) {
192            Err(LatLongError::Longitude(lat_long.1))
193        } else {
194            Ok(Self::new(Degrees(lat_long.0), Degrees(lat_long.1)))
195        }
196    }
197}
198
199/// Calculate the azimuth and distance along the great circle of point b from
200/// point a.
201/// * `a`, `b` - the start and end positions
202///
203/// returns the great-circle azimuth relative to North and distance of point b
204/// from point a.
205#[must_use]
206pub fn calculate_azimuth_and_distance(a: &LatLong, b: &LatLong) -> (Angle, Radians) {
207    let a_lat = Angle::from(a.lat);
208    let b_lat = Angle::from(b.lat);
209    let delta_long = Angle::from((b.lon, a.lon));
210    (
211        great_circle::calculate_gc_azimuth(a_lat, b_lat, delta_long),
212        great_circle::calculate_gc_distance(a_lat, b_lat, delta_long),
213    )
214}
215
216/// Calculate the distance along the great circle of point b from point a.
217///
218/// See: [Haversine formula](https://en.wikipedia.org/wiki/Haversine_formula).
219/// This function is less accurate than `calculate_azimuth_and_distance`.
220/// * `a`, `b` - the start and end positions
221///
222/// returns the great-circle distance of point b from point a in `Radians`.
223#[must_use]
224pub fn haversine_distance(a: &LatLong, b: &LatLong) -> Radians {
225    let a_lat = Angle::from(a.lat);
226    let b_lat = Angle::from(b.lat);
227    let delta_lat = Angle::from((b.lat, a.lat));
228    let delta_long = Angle::from(b.lon - a.lon);
229    great_circle::calculate_haversine_distance(a_lat, b_lat, delta_long, delta_lat)
230}
231
232/// A `Vector3d` is a [nalgebra](https://crates.io/crates/nalgebra) `Vector3<f64>`.
233#[allow(clippy::module_name_repetitions)]
234pub type Vector3d = na::Vector3<f64>;
235
236impl From<&LatLong> for Vector3d {
237    /// Convert a `LatLong` to a point on the unit sphere.
238    ///
239    /// @pre |lat| <= 90.0 degrees.
240    /// * `lat` - the latitude.
241    /// * `lon` - the longitude.
242    ///
243    /// returns a `Vector3d` of the point on the unit sphere.
244    fn from(a: &LatLong) -> Self {
245        vector::to_point(Angle::from(a.lat), Angle::from(a.lon))
246    }
247}
248
249impl From<&Vector3d> for LatLong {
250    /// Convert a point to a `LatLong`
251    fn from(value: &Vector3d) -> Self {
252        Self::new(
253            Degrees::from(vector::latitude(value)),
254            Degrees::from(vector::longitude(value)),
255        )
256    }
257}
258
259/// An `Arc` of a Great Circle on a unit sphere.
260#[derive(Clone, Copy, Debug, PartialEq)]
261pub struct Arc {
262    /// The start point of the `Arc`.
263    a: Vector3d,
264    /// The right hand pole of the Great Circle of the `Arc`.
265    pole: Vector3d,
266    /// The length of the `Arc`.
267    length: Radians,
268    /// The half width of the `Arc`.
269    half_width: Radians,
270}
271
272impl Validate for Arc {
273    /// Test whether an `Arc` is valid.
274    ///
275    /// I.e. both a and pole are on the unit sphere and are orthogonal and
276    /// both length and `half_width` are not negative.
277    fn is_valid(&self) -> bool {
278        vector::is_unit(&self.a)
279            && vector::is_unit(&self.pole)
280            && vector::are_orthogonal(&self.a, &self.pole)
281            && !self.length.0.is_sign_negative()
282            && !self.half_width.0.is_sign_negative()
283    }
284}
285
286impl Arc {
287    /// Construct an `Arc`
288    ///
289    /// * `a` - the start point of the `Arc`.
290    /// * `pole` - the right hand pole of the Great Circle of the `Arc`.
291    /// * `length` - the length of the `Arc`.
292    /// * `half_width` - the half width of the `Arc`.
293    #[must_use]
294    pub const fn new(a: Vector3d, pole: Vector3d, length: Radians, half_width: Radians) -> Self {
295        Self {
296            a,
297            pole,
298            length,
299            half_width,
300        }
301    }
302
303    /// Construct an `Arc`
304    ///
305    /// * `a` - the start position
306    /// * `azimuth` - the azimuth at a.
307    /// * `length` - the length of the `Arc`.
308    #[must_use]
309    pub fn from_lat_lon_azi_length(a: &LatLong, azimuth: Angle, length: Radians) -> Self {
310        Self::new(
311            Vector3d::from(a),
312            vector::calculate_pole(Angle::from(a.lat()), Angle::from(a.lon()), azimuth),
313            length,
314            Radians(0.0),
315        )
316    }
317
318    /// Construct an `Arc` from the start and end positions.
319    ///
320    /// Note: if the points are the same or antipodal, the pole will be invalid.
321    /// * `a`, `b` - the start and end positions
322    #[must_use]
323    pub fn between_positions(a: &LatLong, b: &LatLong) -> Self {
324        let (azimuth, length) = calculate_azimuth_and_distance(a, b);
325        let a_lat = Angle::from(a.lat());
326        // if a is at the North or South pole
327        if a_lat.cos().0 < great_circle::MIN_VALUE {
328            // use b's longitude
329            Self::from_lat_lon_azi_length(&LatLong::new(a.lat(), b.lon()), azimuth, length)
330        } else {
331            Self::from_lat_lon_azi_length(a, azimuth, length)
332        }
333    }
334
335    /// Set the `half_width` of an `Arc`.
336    ///
337    /// * `half_width` - the half width of the `Arc`.
338    #[must_use]
339    pub const fn set_half_width(&mut self, half_width: Radians) -> &mut Self {
340        self.half_width = half_width;
341        self
342    }
343
344    /// The start point of the `Arc`.
345    #[must_use]
346    pub const fn a(&self) -> Vector3d {
347        self.a
348    }
349
350    /// The right hand pole of the Great Circle at the start point of the `Arc`.
351    #[must_use]
352    pub const fn pole(&self) -> Vector3d {
353        self.pole
354    }
355
356    /// The length of the `Arc`.
357    #[must_use]
358    pub const fn length(&self) -> Radians {
359        self.length
360    }
361
362    /// The half width of the `Arc`.
363    #[must_use]
364    pub const fn half_width(&self) -> Radians {
365        self.half_width
366    }
367
368    /// The azimuth at the start point.
369    #[must_use]
370    pub fn azimuth(&self) -> Angle {
371        vector::calculate_azimuth(&self.a, &self.pole)
372    }
373
374    /// The direction vector of the `Arc` at the start point.
375    #[must_use]
376    pub fn direction(&self) -> Vector3d {
377        vector::direction(&self.a, &self.pole)
378    }
379
380    /// A position vector at distance along the `Arc`.
381    #[must_use]
382    pub fn position(&self, distance: Radians) -> Vector3d {
383        vector::position(&self.a, &self.direction(), Angle::from(distance))
384    }
385
386    /// The end point of the `Arc`.
387    #[must_use]
388    pub fn b(&self) -> Vector3d {
389        self.position(self.length)
390    }
391
392    /// The mid point of the `Arc`.
393    #[must_use]
394    pub fn mid_point(&self) -> Vector3d {
395        self.position(Radians(0.5 * self.length.0))
396    }
397
398    /// The position of a perpendicular point at distance from the `Arc`.
399    ///
400    /// * `point` a point on the `Arc`'s great circle.
401    /// * `distance` the perpendicular distance from the `Arc`'s great circle.
402    ///
403    /// returns the point at perpendicular distance from point.
404    #[must_use]
405    pub fn perp_position(&self, point: &Vector3d, distance: Radians) -> Vector3d {
406        vector::position(point, &self.pole, Angle::from(distance))
407    }
408
409    /// The position of a point at angle from the `Arc` start, at `Arc` length.
410    ///
411    /// * `angle` the angle from the `Arc` start.
412    ///
413    /// returns the point at angle from the `Arc` start, at `Arc` length.
414    #[must_use]
415    pub fn angle_position(&self, angle: Angle) -> Vector3d {
416        vector::rotate_position(&self.a, &self.pole, angle, Angle::from(self.length))
417    }
418
419    /// The `Arc` at the end of an `Arc`, just the point if `half_width` is zero.
420    ///
421    /// @param `at_b` if true the `Arc` at b, else the `Arc` at a.
422    ///
423    /// @return the end `Arc` at a or b.
424    #[must_use]
425    pub fn end_arc(&self, at_b: bool) -> Self {
426        let p = if at_b { self.b() } else { self.a };
427        let pole = vector::direction(&p, &self.pole);
428        if self.half_width.0 < great_circle::MIN_VALUE {
429            Self::new(p, pole, Radians(0.0), Radians(0.0))
430        } else {
431            let a = self.perp_position(&p, self.half_width);
432            Self::new(a, pole, self.half_width + self.half_width, Radians(0.0))
433        }
434    }
435
436    /// Calculate great-circle along and across track distances of point from
437    /// the `Arc`.
438    ///
439    /// * `point` - the point.
440    ///
441    /// returns the along and across track distances of the point in Radians.
442    #[must_use]
443    pub fn calculate_atd_and_xtd(&self, point: &Vector3d) -> (Radians, Radians) {
444        vector::calculate_atd_and_xtd(&self.a, &self.pole(), point)
445    }
446}
447
448/// A Error type for an invalid `Arc`.
449#[derive(Error, Debug, PartialEq)]
450pub enum ArcError {
451    #[error("positions are too close: `{0}`")]
452    PositionsTooClose(f64),
453    #[error("positions are too far apart: `{0}`")]
454    PositionsTooFar(f64),
455}
456
457impl TryFrom<(&LatLong, &LatLong)> for Arc {
458    type Error = ArcError;
459
460    /// Construct an `Arc` from a pair of positions.
461    ///
462    /// * `params` - the start and end positions
463    fn try_from(params: (&LatLong, &LatLong)) -> Result<Self, Self::Error> {
464        // Convert positions to vectors
465        let a = Vector3d::from(params.0);
466        let b = Vector3d::from(params.1);
467        // Calculate the great circle pole
468        vector::normalise(&a.cross(&b), vector::MIN_SQ_NORM).map_or_else(
469            || {
470                let sq_d = vector::sq_distance(&a, &b);
471                if sq_d < 1.0 {
472                    Err(ArcError::PositionsTooClose(sq_d))
473                } else {
474                    Err(ArcError::PositionsTooFar(sq_d))
475                }
476            },
477            |pole| {
478                Ok(Self::new(
479                    a,
480                    pole,
481                    great_circle::e2gc_distance(vector::distance(&a, &b)),
482                    Radians(0.0),
483                ))
484            },
485        )
486    }
487}
488
489/// Calculate the great-circle distances along a pair of `Arc`s to their
490/// closest intersection point or their coincident arc distances if the
491/// `Arc`s are on coincident Great Circles.
492///
493/// * `arc1`, `arc2` the `Arc`s.
494///
495/// returns the distances along the first `Arc` and second `Arc` to the intersection
496/// point or to their coincident arc distances if the `Arc`s do not intersect.
497#[must_use]
498pub fn calculate_intersection_distances(arc1: &Arc, arc2: &Arc) -> (Radians, Radians) {
499    vector::intersection::calculate_intersection_point_distances(
500        &arc1.a,
501        &arc1.pole,
502        arc1.length(),
503        &arc2.a,
504        &arc2.pole,
505        arc2.length(),
506        &(0.5 * (arc1.mid_point() + arc2.mid_point())),
507    )
508}
509
510/// Calculate whether a pair of `Arc`s intersect and (if so) where.
511///
512/// * `arc1`, `arc2` the `Arc`s.
513///
514/// returns the distance along the first `Arc` to the second `Arc` or None if they
515/// don't intersect.
516///
517/// # Examples
518/// ```
519/// use unit_sphere::{Arc, Degrees, LatLong, calculate_intersection_point};
520/// use angle_sc::is_within_tolerance;
521///
522/// let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
523/// let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
524/// let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
525/// let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
526///
527/// let arc1 = Arc::try_from((&istanbul, &washington)).unwrap();
528/// let arc2 = Arc::try_from((&reyjavik, &accra)).unwrap();
529///
530/// // Calculate the intersection point position
531/// let intersection_point = calculate_intersection_point(&arc1, &arc2).unwrap();
532/// let lat_long = LatLong::from(&intersection_point);
533///
534/// // The expected latitude and longitude are from:
535/// // <https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a>
536///
537/// // Geodesic intersection latitude is 54.7170296089477
538/// assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
539/// // Geodesic intersection longitude is -14.56385574430775
540/// assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
541/// ```
542#[must_use]
543pub fn calculate_intersection_point(arc1: &Arc, arc2: &Arc) -> Option<Vector3d> {
544    let (distance1, distance2) = calculate_intersection_distances(arc1, arc2);
545
546    // Determine whether both distances are within both arcs
547    if vector::intersection::is_within(distance1.0, arc1.length().0)
548        && vector::intersection::is_within(distance2.0, arc2.length().0)
549    {
550        Some(arc1.position(distance1))
551    } else {
552        None
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use angle_sc::{is_within_tolerance, Degrees};
560
561    #[test]
562    fn test_is_valid_latitude() {
563        // value < -90
564        assert!(!is_valid_latitude(-90.0001));
565        // value = -90
566        assert!(is_valid_latitude(-90.0));
567        // value = 90
568        assert!(is_valid_latitude(90.0));
569        // value > 90
570        assert!(!is_valid_latitude(90.0001));
571    }
572
573    #[test]
574    fn test_is_valid_longitude() {
575        // value < -180
576        assert!(!is_valid_longitude(-180.0001));
577        // value = -180
578        assert!(is_valid_longitude(-180.0));
579        // value = 180
580        assert!(is_valid_longitude(180.0));
581        // value > 180
582        assert!(!is_valid_longitude(180.0001));
583    }
584
585    #[test]
586    fn test_latlong_traits() {
587        let a = LatLong::try_from((0.0, 90.0)).unwrap();
588
589        assert!(a.is_valid());
590
591        let a_clone = a.clone();
592        assert!(a_clone == a);
593
594        assert_eq!(Degrees(0.0), a.lat());
595        assert_eq!(Degrees(90.0), a.lon());
596
597        println!("LatLong: {:?}", a);
598
599        let invalid_lat = LatLong::try_from((91.0, 0.0));
600        assert_eq!(Err(LatLongError::Latitude(91.0)), invalid_lat);
601        println!("invalid_lat: {:?}", invalid_lat);
602
603        let invalid_lon = LatLong::try_from((0.0, 181.0));
604        assert_eq!(Err(LatLongError::Longitude(181.0)), invalid_lon);
605        println!("invalid_lon: {:?}", invalid_lon);
606    }
607
608    #[test]
609    fn test_vector3d_traits() {
610        let a = LatLong::try_from((0.0, 90.0)).unwrap();
611        let point = Vector3d::from(&a);
612
613        assert_eq!(0.0, point.x);
614        assert_eq!(1.0, point.y);
615        assert_eq!(0.0, point.z);
616
617        assert_eq!(Degrees(0.0), Degrees::from(vector::latitude(&point)));
618        assert_eq!(Degrees(90.0), Degrees::from(vector::longitude(&point)));
619
620        let result = LatLong::from(&point);
621        assert_eq!(a, result);
622    }
623
624    #[test]
625    fn test_great_circle_90n_0n_0e() {
626        let a = LatLong::new(Degrees(90.0), Degrees(0.0));
627        let b = LatLong::new(Degrees(0.0), Degrees(0.0));
628        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
629
630        assert!(is_within_tolerance(
631            core::f64::consts::FRAC_PI_2,
632            dist.0,
633            f64::EPSILON
634        ));
635        assert_eq!(180.0, Degrees::from(azimuth).0);
636
637        let dist = haversine_distance(&a, &b);
638        assert!(is_within_tolerance(
639            core::f64::consts::FRAC_PI_2,
640            dist.0,
641            f64::EPSILON
642        ));
643    }
644
645    #[test]
646    fn test_great_circle_90s_0n_50e() {
647        let a = LatLong::new(Degrees(-90.0), Degrees(0.0));
648        let b = LatLong::new(Degrees(0.0), Degrees(50.0));
649        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
650
651        assert!(is_within_tolerance(
652            core::f64::consts::FRAC_PI_2,
653            dist.0,
654            f64::EPSILON
655        ));
656        assert_eq!(0.0, Degrees::from(azimuth).0);
657
658        let dist = haversine_distance(&a, &b);
659        assert!(is_within_tolerance(
660            core::f64::consts::FRAC_PI_2,
661            dist.0,
662            f64::EPSILON
663        ));
664    }
665
666    #[test]
667    fn test_great_circle_0n_60e_0n_60w() {
668        let a = LatLong::new(Degrees(0.0), Degrees(60.0));
669        let b = LatLong::new(Degrees(0.0), Degrees(-60.0));
670        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
671
672        assert!(is_within_tolerance(
673            2.0 * core::f64::consts::FRAC_PI_3,
674            dist.0,
675            2.0 * f64::EPSILON
676        ));
677        assert_eq!(-90.0, Degrees::from(azimuth).0);
678
679        let dist = haversine_distance(&a, &b);
680        assert!(is_within_tolerance(
681            2.0 * core::f64::consts::FRAC_PI_3,
682            dist.0,
683            2.0 * f64::EPSILON
684        ));
685    }
686
687    #[test]
688    fn test_arc() {
689        // Greenwich equator
690        let g_eq = LatLong::new(Degrees(0.0), Degrees(0.0));
691
692        // 90 degrees East on the equator
693        let e_eq = LatLong::new(Degrees(0.0), Degrees(90.0));
694
695        let mut arc = Arc::between_positions(&g_eq, &e_eq);
696        let arc = arc.set_half_width(Radians(0.01));
697        assert!(arc.is_valid());
698        assert_eq!(Radians(0.01), arc.half_width());
699
700        assert_eq!(Vector3d::from(&g_eq), arc.a());
701        assert_eq!(Vector3d::new(0.0, 0.0, 1.0), arc.pole());
702        assert!(is_within_tolerance(
703            core::f64::consts::FRAC_PI_2,
704            arc.length().0,
705            f64::EPSILON
706        ));
707        assert_eq!(Angle::from(Degrees(90.0)), arc.azimuth());
708        let b = Vector3d::from(&e_eq);
709        assert!(is_within_tolerance(
710            0.0,
711            vector::distance(&b, &arc.b()),
712            f64::EPSILON
713        ));
714
715        let mid_point = arc.mid_point();
716        assert_eq!(0.0, mid_point.z);
717        assert!(is_within_tolerance(
718            45.0,
719            Degrees::from(vector::longitude(&mid_point)).0,
720            32.0 * f64::EPSILON
721        ));
722
723        let start_arc = arc.end_arc(false);
724        assert_eq!(0.02, start_arc.length().0);
725
726        let start_arc_a = start_arc.a();
727        assert_eq!(start_arc_a, arc.perp_position(&arc.a(), Radians(0.01)));
728
729        let angle_90 = Angle::from(Degrees(90.0));
730        let pole_0 = Vector3d::new(0.0, 0.0, 1.0);
731        assert!(vector::distance(&pole_0, &arc.angle_position(angle_90)) <= f64::EPSILON);
732
733        let end_arc = arc.end_arc(true);
734        assert_eq!(0.02, end_arc.length().0);
735
736        let end_arc_a = end_arc.a();
737        assert_eq!(end_arc_a, arc.perp_position(&arc.b(), Radians(0.01)));
738    }
739
740    #[test]
741    fn test_north_and_south_poles() {
742        let north_pole = LatLong::new(Degrees(90.0), Degrees(0.0));
743        let south_pole = LatLong::new(Degrees(-90.0), Degrees(0.0));
744
745        let (azimuth, distance) = calculate_azimuth_and_distance(&south_pole, &north_pole);
746        assert_eq!(0.0, Degrees::from(azimuth).0);
747        assert_eq!(core::f64::consts::PI, distance.0);
748
749        let (azimuth, distance) = calculate_azimuth_and_distance(&north_pole, &south_pole);
750        assert_eq!(180.0, Degrees::from(azimuth).0);
751        assert_eq!(core::f64::consts::PI, distance.0);
752
753        // 90 degrees East on the equator
754        let e_eq = LatLong::new(Degrees(0.0), Degrees(50.0));
755
756        let arc = Arc::between_positions(&north_pole, &e_eq);
757        assert!(is_within_tolerance(
758            e_eq.lat().0,
759            LatLong::from(&arc.b()).lat().abs().0,
760            1e-13
761        ));
762        assert!(is_within_tolerance(
763            e_eq.lon().0,
764            LatLong::from(&arc.b()).lon().0,
765            50.0 * f64::EPSILON
766        ));
767
768        let arc = Arc::between_positions(&south_pole, &e_eq);
769        assert!(is_within_tolerance(
770            e_eq.lat().0,
771            LatLong::from(&arc.b()).lat().abs().0,
772            1e-13
773        ));
774        assert!(is_within_tolerance(
775            e_eq.lon().0,
776            LatLong::from(&arc.b()).lon().0,
777            50.0 * f64::EPSILON
778        ));
779
780        let w_eq = LatLong::new(Degrees(0.0), Degrees(-140.0));
781
782        let arc = Arc::between_positions(&north_pole, &w_eq);
783        assert!(is_within_tolerance(
784            w_eq.lat().0,
785            LatLong::from(&arc.b()).lat().abs().0,
786            1e-13
787        ));
788        assert!(is_within_tolerance(
789            w_eq.lon().0,
790            LatLong::from(&arc.b()).lon().0,
791            256.0 * f64::EPSILON
792        ));
793
794        let arc = Arc::between_positions(&south_pole, &w_eq);
795        assert!(is_within_tolerance(
796            w_eq.lat().0,
797            LatLong::from(&arc.b()).lat().abs().0,
798            1e-13
799        ));
800        assert!(is_within_tolerance(
801            w_eq.lon().0,
802            LatLong::from(&arc.b()).lon().0,
803            256.0 * f64::EPSILON
804        ));
805
806        let invalid_arc = Arc::try_from((&north_pole, &north_pole));
807        assert_eq!(Err(ArcError::PositionsTooClose(0.0)), invalid_arc);
808        println!("invalid_arc: {:?}", invalid_arc);
809
810        let arc = Arc::between_positions(&north_pole, &north_pole);
811        assert_eq!(north_pole, LatLong::from(&arc.b()));
812
813        let invalid_arc = Arc::try_from((&north_pole, &south_pole));
814        assert_eq!(Err(ArcError::PositionsTooFar(4.0)), invalid_arc);
815        println!("invalid_arc: {:?}", invalid_arc);
816
817        let arc = Arc::between_positions(&north_pole, &south_pole);
818        assert_eq!(south_pole, LatLong::from(&arc.b()));
819
820        let arc = Arc::between_positions(&south_pole, &north_pole);
821        assert_eq!(north_pole, LatLong::from(&arc.b()));
822
823        let arc = Arc::between_positions(&south_pole, &south_pole);
824        assert_eq!(south_pole, LatLong::from(&arc.b()));
825    }
826
827    #[test]
828    fn test_arc_atd_and_xtd() {
829        // Greenwich equator
830        let g_eq = LatLong::new(Degrees(0.0), Degrees(0.0));
831
832        // 90 degrees East on the equator
833        let e_eq = LatLong::new(Degrees(0.0), Degrees(90.0));
834
835        let arc = Arc::try_from((&g_eq, &e_eq)).unwrap();
836        assert!(arc.is_valid());
837
838        let start_arc = arc.end_arc(false);
839        assert_eq!(0.0, start_arc.length().0);
840
841        let start_arc_a = start_arc.a();
842        assert_eq!(arc.a(), start_arc_a);
843
844        let longitude = Degrees(1.0);
845
846        // Test across track distance
847        // Accuracy drops off outside of this range
848        for lat in -83..84 {
849            let latitude = Degrees(lat as f64);
850            let latlong = LatLong::new(latitude, longitude);
851            let point = Vector3d::from(&latlong);
852
853            let expected = (lat as f64).to_radians();
854            let (atd, xtd) = arc.calculate_atd_and_xtd(&point);
855            assert!(is_within_tolerance(1_f64.to_radians(), atd.0, f64::EPSILON));
856            assert!(is_within_tolerance(expected, xtd.0, 2.0 * f64::EPSILON));
857        }
858    }
859
860    #[test]
861    fn test_arc_intersection_point() {
862        // Karney's example:
863        // Istanbul, Washington, Reyjavik and Accra
864        // from: <https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a>
865        let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
866        let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
867        let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
868        let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
869
870        let arc1 = Arc::try_from((&istanbul, &washington)).unwrap();
871        let arc2 = Arc::try_from((&reyjavik, &accra)).unwrap();
872
873        let intersection_point = calculate_intersection_point(&arc1, &arc2).unwrap();
874        let lat_long = LatLong::from(&intersection_point);
875        // Geodesic intersection latitude is 54.7170296089477
876        assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
877        // Geodesic intersection longitude is -14.56385574430775
878        assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
879
880        // Switch arcs
881        let intersection_point = calculate_intersection_point(&arc2, &arc1).unwrap();
882        let lat_long = LatLong::from(&intersection_point);
883        // Geodesic intersection latitude is 54.7170296089477
884        assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
885        // Geodesic intersection longitude is -14.56385574430775
886        assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
887    }
888
889    #[test]
890    fn test_arc_intersection_same_great_circles() {
891        let south_pole_1 = LatLong::new(Degrees(-88.0), Degrees(-180.0));
892        let south_pole_2 = LatLong::new(Degrees(-87.0), Degrees(0.0));
893
894        let arc1 = Arc::try_from((&south_pole_1, &south_pole_2)).unwrap();
895
896        let intersection_lengths = calculate_intersection_distances(&arc1, &arc1);
897        assert_eq!(Radians(0.0), intersection_lengths.0);
898        assert_eq!(Radians(0.0), intersection_lengths.1);
899
900        let intersection_point = calculate_intersection_point(&arc1, &arc1).unwrap();
901        assert_eq!(0.0, vector::sq_distance(&arc1.a(), &intersection_point));
902
903        let south_pole_3 = LatLong::new(Degrees(-85.0), Degrees(0.0));
904        let south_pole_4 = LatLong::new(Degrees(-86.0), Degrees(0.0));
905        let arc2 = Arc::try_from((&south_pole_3, &south_pole_4)).unwrap();
906        let intersection_point = calculate_intersection_point(&arc1, &arc2);
907        assert!(intersection_point.is_none());
908    }
909}