gistools/tools/
orthodrome.rs

1use libm::{atan2, cos, sin, sqrt};
2use s2json::{GetXY, NewXY};
3
4/// # Orthodrome
5///
6/// ## Description
7/// Represents an orthodrome, which is the shortest path between two points on a sphere.
8///
9/// [Learn more here](http://www.movable-type.co.uk/scripts/latlong.html)
10///
11/// ## Usage
12///
13/// The methods you have access to:
14/// - [`Orthodrome::new`]: Create a new Orthodrome
15/// - [`Orthodrome::from_points`]: Create an orthodrome from two points. The points must implement the [`GetXY`] trait
16/// - [`Orthodrome::intermediate_point`]: input t 0->1. Find a point along the orthodrome.
17/// - [`Orthodrome::bearing`]: returns the bearing in degrees between the two points
18/// - [`Orthodrome::distance_to`]: Finds the distance between the two points in kilometers projected normalized (0->1)
19///
20/// The properties you have access to:
21/// - [`Orthodrome::lon1`]: start longitude in radians
22/// - [`Orthodrome::lat1`]: start latitude in radians
23/// - [`Orthodrome::lon2`]: end longitude in radians
24/// - [`Orthodrome::lat2`]: end latitude in radians
25/// - [`Orthodrome::a`]: distance property
26/// - [`Orthodrome::dist`]: distance property
27///
28/// ```rust
29/// use gistools::{geometry::LonLat, tools::Orthodrome};
30///
31/// let ortho = Orthodrome::new(-60., -40., 20., 10.);
32/// assert_eq!(ortho.intermediate_point::<LonLat>(0.), LonLat::new(-59.99999999999999, -40., None));
33/// assert_eq!(
34///     ortho.intermediate_point::<LonLat>(0.2),
35///     LonLat::new(-39.13793657428956, -33.728521975616516, None)
36/// );
37/// ```
38///
39/// ## NOTE
40/// There is no reason to use this outside verbosity. You can create an S1Angle or use the utility functions in LonLat
41///
42/// ## Links
43/// - <http://www.movable-type.co.uk/scripts/latlong.html>
44#[derive(Debug, Clone, Default)]
45pub struct Orthodrome {
46    /// start longitude in radians
47    pub lon1: f64,
48    /// start latitude in radians
49    pub lat1: f64,
50    /// end longitude in radians
51    pub lon2: f64,
52    /// end latitude in radians
53    pub lat2: f64,
54    /// distance property
55    pub a: f64,
56    /// distance property
57    pub dist: f64,
58}
59impl Orthodrome {
60    /// Create an orthodrome
61    pub fn new(start_lon: f64, start_lat: f64, end_lon: f64, end_lat: f64) -> Orthodrome {
62        let lon1 = start_lon.to_radians();
63        let lat1 = start_lat.to_radians();
64        let lon2 = end_lon.to_radians();
65        let lat2 = end_lat.to_radians();
66        let d_lat = lat2 - lat1;
67        let d_lon = lon2 - lon1;
68        let a = sin(d_lat / 2.) * sin(d_lat / 2.)
69            + cos(lat1) * cos(lat2) * sin(d_lon / 2.) * sin(d_lon / 2.);
70        let dist = 2. * atan2(sqrt(a), sqrt(1. - a));
71
72        Orthodrome { lon1, lat1, lon2, lat2, a, dist }
73    }
74
75    /// Create an orthodrome from two points
76    ///
77    /// The points must implement the [`GetXY`] trait
78    pub fn from_points<P1: GetXY, P2: GetXY>(p1: &P1, p2: &P2) -> Orthodrome {
79        Orthodrome::new(p1.x(), p1.y(), p2.x(), p2.y())
80    }
81
82    /// input t 0->1. Find a point along the orthodrome.
83    ///
84    /// ## Parameters
85    /// - `t`: distance along the orthodrome to find
86    ///
87    /// ## Returns
88    /// Returns any Point type that implements the [`NewXY`].
89    /// X is the longitude in degrees.
90    /// Y is the latitude in degrees.
91    pub fn intermediate_point<P: NewXY>(&self, t: f64) -> P {
92        let Self { lon1, lon2, lat1, lat2, dist, .. } = self;
93
94        // check corner cases first
95        if t == 0. {
96            return P::new_xy(lon1.to_degrees(), lat1.to_degrees());
97        } else if t == 1. {
98            return P::new_xy(lon2.to_degrees(), lat2.to_degrees());
99        }
100        // check if points are equal
101        else if lon1 == lon2 && lat1 == lat2 {
102            return P::new_xy(lon1.to_degrees(), lat1.to_degrees());
103        }
104
105        let a = sin((1. - t) * dist) / sin(*dist);
106        let b = sin(t * dist) / sin(*dist);
107
108        let x = a * cos(*lat1) * cos(*lon1) + b * cos(*lat2) * cos(*lon2);
109        let y = a * cos(*lat1) * sin(*lon1) + b * cos(*lat2) * sin(*lon2);
110        let z = a * sin(*lat1) + b * sin(*lat2);
111
112        let lat = atan2(z, sqrt(x * x + y * y));
113        let lon = atan2(y, x);
114
115        P::new_xy(lon.to_degrees(), lat.to_degrees())
116    }
117
118    /// returns the bearing in degrees between the two points
119    pub fn bearing(&self) -> f64 {
120        let Self { lon1, lat1, lon2, lat2, .. } = self;
121
122        let y = sin(lon2 - lon1) * cos(*lat2);
123        let x = cos(*lat1) * sin(*lat2) - sin(*lat1) * cos(*lat2) * cos(lon2 - lon1);
124        let angle_rad = atan2(y, x);
125
126        (angle_rad.to_degrees() + 360.) % 360. // in degrees
127    }
128
129    /// Finds the distance between the two points in kilometers projected normalized (0->1)
130    ///
131    /// ## Returns
132    /// The total distance between the two points
133    pub fn distance_to(&self) -> f64 {
134        2. * atan2(sqrt(self.a), sqrt(1. - self.a))
135    }
136}