trailgen-core 0.3.0

Typed trail graph, route scoring, loop search, and geospatial route I/O for adequate-trailgen.
Documentation
use crate::{Result, TrailgenError};
use serde::{Deserialize, Serialize};

const EARTH_R_M: f64 = 6_371_008.8;

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Coord {
    pub lon: f64,
    pub lat: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ele: Option<f64>,
}

impl Coord {
    #[must_use]
    pub const fn new(lon: f64, lat: f64) -> Self {
        Self {
            lon,
            lat,
            ele: None,
        }
    }

    #[must_use]
    pub const fn with_ele(lon: f64, lat: f64, ele: f64) -> Self {
        Self {
            lon,
            lat,
            ele: Some(ele),
        }
    }

    #[must_use]
    pub fn haversine_m(self, rhs: Self) -> f64 {
        let φ1 = self.lat.to_radians();
        let φ2 = rhs.lat.to_radians();
        let δφ = (rhs.lat - self.lat).to_radians();
        let δλ = (rhs.lon - self.lon).to_radians();
        let sin_half_lat = (δφ / 2.0).sin();
        let sin_half_lon = (δλ / 2.0).sin();
        let a =
            (φ1.cos() * φ2.cos()).mul_add(sin_half_lon * sin_half_lon, sin_half_lat * sin_half_lat);
        2.0 * EARTH_R_M * a.sqrt().asin()
    }

    #[must_use]
    pub fn lerp(self, rhs: Self, t: f64) -> Self {
        let ele = match (self.ele, rhs.ele) {
            (Some(a), Some(b)) => Some((b - a).mul_add(t, a)),
            _ => None,
        };
        Self {
            lon: (rhs.lon - self.lon).mul_add(t, self.lon),
            lat: (rhs.lat - self.lat).mul_add(t, self.lat),
            ele,
        }
    }

    #[must_use]
    pub fn planar_distance2(self, rhs: Self) -> f64 {
        let dx = self.lon - rhs.lon;
        let dy = self.lat - rhs.lat;
        dx.mul_add(dx, dy * dy)
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct LineString {
    pub points: Vec<Coord>,
}

impl LineString {
    pub fn new(points: Vec<Coord>) -> Result<Self> {
        if points.len() < 2 {
            return Err(TrailgenError::InvalidGeometry(
                "line string needs at least two coordinates".to_owned(),
            ));
        }
        Ok(Self { points })
    }

    #[must_use]
    pub fn unchecked(points: Vec<Coord>) -> Self {
        debug_assert!(points.len() >= 2);
        Self { points }
    }

    #[must_use]
    pub fn length_m(&self) -> f64 {
        self.points.windows(2).map(|w| w[0].haversine_m(w[1])).sum()
    }

    #[must_use]
    pub fn ascent_descent_m(&self) -> (f64, f64) {
        self.points
            .windows(2)
            .filter_map(|w| Some((w[0].ele?, w[1].ele?)))
            .fold((0.0, 0.0), |(up, down), (a, b)| {
                let d = b - a;
                if d >= 0.0 {
                    (up + d, down)
                } else {
                    (up, down - d)
                }
            })
    }

    #[must_use]
    pub fn reversed(&self) -> Self {
        let mut points = self.points.clone();
        points.reverse();
        Self { points }
    }

    #[must_use]
    pub fn start(&self) -> Coord {
        self.points[0]
    }

    #[must_use]
    pub fn end(&self) -> Coord {
        self.points[self.points.len() - 1]
    }
}