vincenty_core/
lib.rs

1use anyhow::{bail, Result};
2use geo_types::geometry::Coord;
3
4const RADIUS_AT_EQUATOR: f64 = 6_378_137.0;
5const FLATTENING_ELIPSOID: f64 = 1.0 / 298.257_223_563;
6const RADIUS_AT_POLES: f64 = (1.0 - FLATTENING_ELIPSOID) * RADIUS_AT_EQUATOR;
7const MAX_ITERATIONS: u32 = 200;
8const CONVERGENCE_THRESHOLD: f64 = 0.000_000_000_001;
9const PRECISION: i32 = 6;
10
11/// Calculate the geodesic distance in Km using geo_types::geometry::Coord.
12///
13/// # Arguments
14///
15/// * `c1` - A geo_types::geometry::Coord object representing the first point.
16/// * `c2` - A geo_types::geometry::Coord object representing the second point.
17///
18/// # Returns
19///
20/// * `Result<f64>` - The geodesic distance between the points in kilometers.
21/// Returns an error if the approximation does not finish within the maximum number of iterations.
22pub fn distance_from_coords(c1: &Coord, c2: &Coord) -> Result<f64> {
23    distance_from_points(c1.x, c1.y, c2.x, c2.y)
24}
25
26/// Calculate the geodesic distance in Km using points.
27///
28/// # Arguments
29///
30/// * `x1` - An f64 representing latitude of point 1.
31/// * `y1` - An f64 representing longitude of point 1.
32/// * `x2` - An f64 representing latitude of point 2.
33/// * `y2` - An f64 representing longitude of point 2.
34///
35/// # Returns
36///
37/// * `Result<f64>` - The geodesic distance between the points in kilometers.
38/// Returns an error if the approximation does not finish within the maximum number of iterations.
39pub fn distance_from_points(x1: f64, y1: f64, x2: f64, y2: f64) -> Result<f64> {
40    let u1 = f64::atan((1.0 - FLATTENING_ELIPSOID) * f64::tan(f64::to_radians(x1)));
41    let u2 = f64::atan((1.0 - FLATTENING_ELIPSOID) * f64::tan(f64::to_radians(x2)));
42    let init_lambda = f64::to_radians(y2 - y1);
43    let lambda = init_lambda;
44    let sin_u1 = f64::sin(u1);
45    let cos_u1 = f64::cos(u1);
46    let sin_u2 = f64::sin(u2);
47    let cos_u2 = f64::cos(u2);
48
49    // approximate till ?MAX_ITERATIONS
50    approximate(init_lambda, lambda, sin_u1, cos_u1, sin_u2, cos_u2)
51}
52
53/// Internal function to run approximation upto 200 max iterations.
54fn approximate(
55    init_lambda: f64,
56    mut lambda: f64,
57    sin_u1: f64,
58    cos_u1: f64,
59    sin_u2: f64,
60    cos_u2: f64,
61) -> Result<f64> {
62    for _ in 0..MAX_ITERATIONS {
63        let sin_lambda = f64::sin(lambda);
64        let cos_lambda = f64::cos(lambda);
65        let sin_sigma = f64::sqrt(
66            f64::powi(cos_u2 * sin_lambda, 2)
67                + f64::powi(cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda, 2),
68        );
69
70        if sin_sigma == 0.0 {
71            return Ok(0.0);
72        }
73
74        let cos_sigma = sin_u1.mul_add(sin_u2, cos_u1 * cos_u2 * cos_lambda);
75
76        let sigma = f64::atan2(sin_sigma, cos_sigma);
77        let sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma;
78        let cos_sqalpha = 1.0 - f64::powi(sin_alpha, 2);
79
80        let cos2_sigma_m = if cos_sqalpha == 0.0 {
81            0.0
82        } else {
83            cos_sigma - 2.0 * sin_u1 * sin_u2 / cos_sqalpha
84        };
85
86        let c = (FLATTENING_ELIPSOID / 16.0)
87            * cos_sqalpha
88            * (4.0 + FLATTENING_ELIPSOID - 3.0 * cos_sqalpha);
89
90        let new_lambda = ((1.0 - c) * FLATTENING_ELIPSOID * sin_alpha).mul_add(
91            (c * sin_sigma).mul_add(
92                (c * cos_sigma).mul_add(
93                    2.0_f64.mul_add(f64::powi(cos2_sigma_m, 2), -1.0),
94                    cos2_sigma_m,
95                ),
96                sigma,
97            ),
98            init_lambda,
99        );
100
101        if f64::abs(new_lambda - lambda) < CONVERGENCE_THRESHOLD {
102            // successful
103            return Ok(round(
104                evaluate(cos_sqalpha, sin_sigma, cos2_sigma_m, cos_sigma, sigma),
105                PRECISION,
106            ));
107        }
108
109        lambda = new_lambda;
110    }
111
112    bail!("unable to finish approximation!")
113}
114
115/// Internal function to evaluate once convergence threshold is reached.
116fn evaluate(
117    cos_sqalpha: f64,
118    sin_sigma: f64,
119    cos2_sigma_m: f64,
120    cos_sigma: f64,
121    sigma: f64,
122) -> f64 {
123    let usq = cos_sqalpha * (f64::powi(RADIUS_AT_EQUATOR, 2) - f64::powi(RADIUS_AT_POLES, 2))
124        / f64::powi(RADIUS_AT_POLES, 2);
125    let a = (usq / 16384.0).mul_add(
126        usq.mul_add(usq.mul_add(320.0 - 175.0 * usq, -768.0), 4096.0),
127        1.0,
128    );
129    let b = (usq / 1024.0) * usq.mul_add(usq.mul_add(74.0 - 47.0 * usq, -128.0), 256.0);
130    let delta_sigma = b
131        * sin_sigma
132        * (b / 4.0).mul_add(
133            cos_sigma * 2.0_f64.mul_add(f64::powi(cos2_sigma_m, 2), -1.0)
134                - (b / 6.0)
135                    * cos2_sigma_m
136                    * (4.0_f64.mul_add(f64::powi(sin_sigma, 2), -3.0))
137                    * (4.0_f64.mul_add(f64::powi(cos2_sigma_m, 2), -3.0)),
138            cos2_sigma_m,
139        );
140    RADIUS_AT_POLES * a * (sigma - delta_sigma) / 1000.0
141}
142
143fn round(number: f64, precision: i32) -> f64 {
144    let p = f64::powi(10.0, precision);
145    f64::round(number * p) / p
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use geo_types::coord;
152
153    #[test]
154    fn identity() {
155        assert_eq!(
156            distance_from_coords(&coord! {x: 0.0, y: 0.0}, &coord! {x: 0.0, y: 0.0}).unwrap(),
157            0.0
158        );
159        assert_eq!(distance_from_points(0.0, 0.0, 0.0, 0.0).unwrap(), 0.0);
160    }
161
162    #[test]
163    fn basic() {
164        assert_eq!(
165            distance_from_coords(
166                &coord! {x: 42.3541165, y: -71.0693514},
167                &coord! {x: 40.7791472, y: -73.9680804},
168            )
169            .unwrap(),
170            298.396186
171        );
172        assert_eq!(
173            distance_from_points(42.3541165, -71.0693514, 40.7791472, -73.9680804).unwrap(),
174            298.396186
175        )
176    }
177
178    #[test]
179    fn known() {
180        assert_eq!(
181            distance_from_coords(
182                &coord! {x: 39.152501, y: -84.412977},
183                &coord! {x: 39.152505, y: -84.412946},
184            )
185            .unwrap(),
186            0.002716
187        );
188        assert_eq!(
189            distance_from_points(39.152501, -84.412977, 39.152505, -84.412946).unwrap(),
190            0.002716
191        );
192        // x1, y1: the white house - x2, y2: statue of liberty
193        assert_eq!(
194            distance_from_points(38.89785, -77.03653, 40.68943, -74.04449).unwrap(),
195            324.377429
196        )
197    }
198}