Skip to main content

unit_sphere/
lib.rs

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