1use std::f64::consts::PI;
4
5pub const EARTH_R: f64 = 6_371_008.8;
7
8#[inline]
9fn rad(d: f64) -> f64 {
10 d * PI / 180.0
11}
12#[inline]
13fn deg(r: f64) -> f64 {
14 r * 180.0 / PI
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct LonLat {
20 pub lon: f64,
21 pub lat: f64,
22}
23
24impl LonLat {
25 pub fn new(lon: f64, lat: f64) -> Self {
26 LonLat { lon, lat }
27 }
28}
29
30pub fn haversine(a: LonLat, b: LonLat) -> f64 {
32 let (la1, la2, dlo, dla) = (
33 rad(a.lat),
34 rad(b.lat),
35 rad(b.lon - a.lon),
36 rad(b.lat - a.lat),
37 );
38 let h = (dla / 2.0).sin().powi(2) + la1.cos() * la2.cos() * (dlo / 2.0).sin().powi(2);
39 2.0 * EARTH_R * h.sqrt().asin()
40}
41
42pub fn bearing(a: LonLat, b: LonLat) -> f64 {
44 let (la1, la2, dlo) = (rad(a.lat), rad(b.lat), rad(b.lon - a.lon));
45 let y = dlo.sin() * la2.cos();
46 let x = la1.cos() * la2.sin() - la1.sin() * la2.cos() * dlo.cos();
47 (deg(y.atan2(x)) + 360.0) % 360.0
48}
49
50pub fn destination(a: LonLat, dist_m: f64, bearing_deg: f64) -> LonLat {
52 let (la1, lo1, br, d) = (rad(a.lat), rad(a.lon), rad(bearing_deg), dist_m / EARTH_R);
53 let la2 = (la1.sin() * d.cos() + la1.cos() * d.sin() * br.cos()).asin();
54 let lo2 = lo1 + (br.sin() * d.sin() * la1.cos()).atan2(d.cos() - la1.sin() * la2.sin());
55 LonLat::new(deg(lo2), deg(la2))
56}
57
58pub fn polyline_length(points: &[LonLat]) -> f64 {
60 points.windows(2).map(|w| haversine(w[0], w[1])).sum()
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn hanoi_to_haiphong_distance() {
69 let hanoi = LonLat::new(105.85, 21.02);
70 let haiphong = LonLat::new(106.68, 20.86);
71 let d = haversine(hanoi, haiphong);
72 assert!((d - 88_000.0).abs() < 5_000.0, "got {d}");
73 }
74
75 #[test]
76 fn bearing_due_north() {
77 let a = LonLat::new(105.0, 21.0);
78 let b = LonLat::new(105.0, 22.0);
79 assert!((bearing(a, b) - 0.0).abs() < 0.5);
80 }
81
82 #[test]
83 fn destination_roundtrip() {
84 let a = LonLat::new(105.85, 21.02);
85 let b = destination(a, 10_000.0, 90.0);
86 let d = haversine(a, b);
87 assert!((d - 10_000.0).abs() < 1.0, "got {d}");
88 assert!((bearing(a, b) - 90.0).abs() < 0.5);
89 }
90
91 #[test]
92 fn polyline_sums_segments() {
93 let pts = [
94 LonLat::new(0.0, 0.0),
95 LonLat::new(0.0, 1.0),
96 LonLat::new(0.0, 2.0),
97 ];
98 let total = polyline_length(&pts);
99 let one = haversine(pts[0], pts[1]);
100 assert!((total - 2.0 * one).abs() < 1.0);
101 }
102}