Skip to main content

trailgen_core/
geo.rs

1use crate::{Result, TrailgenError};
2use serde::{Deserialize, Serialize};
3
4const EARTH_R_M: f64 = 6_371_008.8;
5
6#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
7pub struct Coord {
8    pub lon: f64,
9    pub lat: f64,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub ele: Option<f64>,
12}
13
14impl Coord {
15    #[must_use]
16    pub const fn new(lon: f64, lat: f64) -> Self {
17        Self {
18            lon,
19            lat,
20            ele: None,
21        }
22    }
23
24    #[must_use]
25    pub const fn with_ele(lon: f64, lat: f64, ele: f64) -> Self {
26        Self {
27            lon,
28            lat,
29            ele: Some(ele),
30        }
31    }
32
33    #[must_use]
34    pub fn haversine_m(self, rhs: Self) -> f64 {
35        let φ1 = self.lat.to_radians();
36        let φ2 = rhs.lat.to_radians();
37        let δφ = (rhs.lat - self.lat).to_radians();
38        let δλ = (rhs.lon - self.lon).to_radians();
39        let sin_half_lat = (δφ / 2.0).sin();
40        let sin_half_lon = (δλ / 2.0).sin();
41        let a =
42            (φ1.cos() * φ2.cos()).mul_add(sin_half_lon * sin_half_lon, sin_half_lat * sin_half_lat);
43        2.0 * EARTH_R_M * a.sqrt().asin()
44    }
45
46    #[must_use]
47    pub fn lerp(self, rhs: Self, t: f64) -> Self {
48        let ele = match (self.ele, rhs.ele) {
49            (Some(a), Some(b)) => Some((b - a).mul_add(t, a)),
50            _ => None,
51        };
52        Self {
53            lon: (rhs.lon - self.lon).mul_add(t, self.lon),
54            lat: (rhs.lat - self.lat).mul_add(t, self.lat),
55            ele,
56        }
57    }
58
59    #[must_use]
60    pub fn planar_distance2(self, rhs: Self) -> f64 {
61        let dx = self.lon - rhs.lon;
62        let dy = self.lat - rhs.lat;
63        dx.mul_add(dx, dy * dy)
64    }
65}
66
67#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68#[serde(transparent)]
69pub struct LineString {
70    pub points: Vec<Coord>,
71}
72
73impl LineString {
74    pub fn new(points: Vec<Coord>) -> Result<Self> {
75        if points.len() < 2 {
76            return Err(TrailgenError::InvalidGeometry(
77                "line string needs at least two coordinates".to_owned(),
78            ));
79        }
80        Ok(Self { points })
81    }
82
83    #[must_use]
84    pub fn unchecked(points: Vec<Coord>) -> Self {
85        debug_assert!(points.len() >= 2);
86        Self { points }
87    }
88
89    #[must_use]
90    pub fn length_m(&self) -> f64 {
91        self.points.windows(2).map(|w| w[0].haversine_m(w[1])).sum()
92    }
93
94    #[must_use]
95    pub fn ascent_descent_m(&self) -> (f64, f64) {
96        self.points
97            .windows(2)
98            .filter_map(|w| Some((w[0].ele?, w[1].ele?)))
99            .fold((0.0, 0.0), |(up, down), (a, b)| {
100                let d = b - a;
101                if d >= 0.0 {
102                    (up + d, down)
103                } else {
104                    (up, down - d)
105                }
106            })
107    }
108
109    #[must_use]
110    pub fn reversed(&self) -> Self {
111        let mut points = self.points.clone();
112        points.reverse();
113        Self { points }
114    }
115
116    #[must_use]
117    pub fn start(&self) -> Coord {
118        self.points[0]
119    }
120
121    #[must_use]
122    pub fn end(&self) -> Coord {
123        self.points[self.points.len() - 1]
124    }
125}