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
11pub fn distance_from_coords(c1: &Coord, c2: &Coord) -> Result<f64> {
23 distance_from_points(c1.x, c1.y, c2.x, c2.y)
24}
25
26pub 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(init_lambda, lambda, sin_u1, cos_u1, sin_u2, cos_u2)
51}
52
53fn 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 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
115fn 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 assert_eq!(
194 distance_from_points(38.89785, -77.03653, 40.68943, -74.04449).unwrap(),
195 324.377429
196 )
197 }
198}