1const WGS84_A: f64 = 6_378_137.0; const WGS84_F: f64 = 1.0 / 298.257_223_563; const WGS84_E2: f64 = WGS84_F * (2.0 - WGS84_F);
17const WGS84_AUTHALIC_R: f64 = 6_371_007.180_918_47;
20pub fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
25 const R: f64 = 6371.0;
26 let d_lat = (lat2 - lat1).to_radians();
27 let d_lon = (lon2 - lon1).to_radians();
28 let a = (d_lat / 2.0).sin().powi(2)
29 + lat1.to_radians().cos() * lat2.to_radians().cos() * (d_lon / 2.0).sin().powi(2);
30 R * 2.0 * a.sqrt().asin()
31}
32
33
34fn authalic_lat(phi: f64) -> f64 {
39 let e2 = WGS84_E2;
40 let e = e2.sqrt();
41 let s = phi.sin();
42 let q = |s: f64| {
44 (1.0 - e2) * (s / (1.0 - e2 * s * s) - (1.0 / (2.0 * e)) * ((1.0 - e * s) / (1.0 + e * s)).ln())
45 };
46 let qp = q(1.0); (q(s) / qp).clamp(-1.0, 1.0).asin()
48}
49
50pub fn geodesic_distance_m(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
53 let a = WGS84_A;
54 let f = WGS84_F;
55 let b = a * (1.0 - f);
56
57 let l = (lon2 - lon1).to_radians();
58 let u1 = ((1.0 - f) * lat1.to_radians().tan()).atan();
59 let u2 = ((1.0 - f) * lat2.to_radians().tan()).atan();
60 let (sin_u1, cos_u1) = (u1.sin(), u1.cos());
61 let (sin_u2, cos_u2) = (u2.sin(), u2.cos());
62
63 let mut lambda = l;
64 let mut sin_sigma = 0.0;
65 let mut cos_sigma = 0.0;
66 let mut sigma = 0.0;
67 let mut cos_sq_alpha = 0.0;
68 let mut cos2_sigma_m = 0.0;
69
70 for _ in 0..200 {
71 let (sin_lambda, cos_lambda) = (lambda.sin(), lambda.cos());
72 sin_sigma = ((cos_u2 * sin_lambda).powi(2)
73 + (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
74 .sqrt();
75 if sin_sigma == 0.0 {
76 return 0.0; }
78 cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
79 sigma = sin_sigma.atan2(cos_sigma);
80 let sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma;
81 cos_sq_alpha = 1.0 - sin_alpha * sin_alpha;
82 cos2_sigma_m = if cos_sq_alpha != 0.0 {
83 cos_sigma - 2.0 * sin_u1 * sin_u2 / cos_sq_alpha
84 } else {
85 0.0 };
87 let c = f / 16.0 * cos_sq_alpha * (4.0 + f * (4.0 - 3.0 * cos_sq_alpha));
88 let lambda_prev = lambda;
89 lambda = l
90 + (1.0 - c)
91 * f
92 * sin_alpha
93 * (sigma
94 + c * sin_sigma
95 * (cos2_sigma_m + c * cos_sigma * (-1.0 + 2.0 * cos2_sigma_m * cos2_sigma_m)));
96 if (lambda - lambda_prev).abs() < 1e-12 {
97 break;
98 }
99 }
100
101 let u_sq = cos_sq_alpha * (a * a - b * b) / (b * b);
102 let cap_a = 1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
103 let cap_b = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
104 let delta_sigma = cap_b
105 * sin_sigma
106 * (cos2_sigma_m
107 + cap_b / 4.0
108 * (cos_sigma * (-1.0 + 2.0 * cos2_sigma_m * cos2_sigma_m)
109 - cap_b / 6.0
110 * cos2_sigma_m
111 * (-3.0 + 4.0 * sin_sigma * sin_sigma)
112 * (-3.0 + 4.0 * cos2_sigma_m * cos2_sigma_m)));
113 b * cap_a * (sigma - delta_sigma)
114}
115
116pub fn geodesic_path_length_m(coords: &[[f64; 2]]) -> f64 {
119 coords
120 .windows(2)
121 .map(|w| geodesic_distance_m(w[0][0], w[0][1], w[1][0], w[1][1]))
122 .sum()
123}
124
125pub fn geodesic_ring_area_m2(ring: &[[f64; 2]]) -> f64 {
130 let n = ring.len();
131 if n < 3 {
132 return 0.0;
133 }
134 let mut excess = 0.0;
137 for i in 0..n {
138 let (lat1, lon1) = (authalic_lat(ring[i][0].to_radians()), ring[i][1].to_radians());
141 let j = (i + 1) % n;
142 let (lat2, lon2) = (authalic_lat(ring[j][0].to_radians()), ring[j][1].to_radians());
143 let d_lon = lon2 - lon1;
144 let t1 = (lat1 / 2.0).tan();
145 let t2 = (lat2 / 2.0).tan();
146 excess += 2.0 * ((d_lon / 2.0).tan() * (t1 + t2)).atan2(1.0 + t1 * t2);
147 }
148 (excess.abs()) * WGS84_AUTHALIC_R * WGS84_AUTHALIC_R
149}
150
151pub fn point_in_polygon(lat: f64, lon: f64, ring: &[[f64; 2]]) -> bool {
157 let n = ring.len();
158 if n < 3 {
159 return false;
160 }
161 let mut inside = false;
162 let mut j = n - 1;
163 for i in 0..n {
164 let (yi, xi) = (ring[i][0], ring[i][1]);
165 let (yj, xj) = (ring[j][0], ring[j][1]);
166 if ((yi > lat) != (yj > lat)) && (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) {
167 inside = !inside;
168 }
169 j = i;
170 }
171 inside
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
182 fn vincenty_matches_postgis_geography() {
183 let d = geodesic_distance_m(-37.95103, 144.42487, -37.65282, 143.92650);
185 assert!((d - 54972.0).abs() < 2.0, "got {d}");
186 let d = geodesic_distance_m(40.70, -74.00, 40.75, -73.95);
188 assert!((d - 6976.62506433).abs() < 0.001, "dist {d}");
189 assert_eq!(geodesic_distance_m(40.0, -73.0, 40.0, -73.0), 0.0);
190 }
191
192 #[test]
193 fn ring_area_and_perimeter_match_postgis() {
194 let ring = vec![
196 [40.70, -74.00], [40.70, -73.99], [40.71, -73.99],
197 [40.71, -74.00], [40.70, -74.00],
198 ];
199 let a = geodesic_ring_area_m2(&ring);
200 assert!((a - 938459.4059114456).abs() / 938459.406 < 1e-6, "area {a}");
201 let p = geodesic_path_length_m(&ring);
202 assert!((p - 3911.147957345263).abs() / 3911.148 < 1e-6, "perimeter {p}");
203 }
204
205 #[test]
206 fn point_in_polygon_in_and_out() {
207 let ring = [
208 [-37.80, 144.95], [-37.80, 144.98],
209 [-37.83, 144.98], [-37.83, 144.95],
210 ];
211 assert!(point_in_polygon(-37.81, 144.96, &ring));
212 assert!(!point_in_polygon(-38.15, 144.36, &ring));
213 }
214}