trailgen-core 0.3.0

Typed trail graph, route scoring, loop search, and geospatial route I/O for adequate-trailgen.
Documentation
use crate::geo::{Coord, LineString};
use crate::io::route_file::{RouteFile, RouteFileMetadata, clean_text};
use crate::{Result, TrailgenError};

pub fn route_line_from_str(s: &str) -> Result<LineString> {
    route_file_from_str(s).map(|route| route.line)
}

pub fn route_file_from_str(s: &str) -> Result<RouteFile> {
    let doc = roxmltree::Document::parse(s).map_err(|e| TrailgenError::Xml(e.to_string()))?;
    let coords = doc
        .descendants()
        .find(|n| n.has_tag_name("coordinates"))
        .and_then(|n| n.text())
        .ok_or_else(|| TrailgenError::InvalidData("KML has no coordinates element".to_owned()))?;
    let points = coords
        .split_whitespace()
        .map(|tuple| {
            let mut xs = tuple.split(',');
            let lon = xs
                .next()
                .ok_or_else(|| TrailgenError::InvalidData("KML coordinate missing lon".to_owned()))?
                .parse::<f64>()
                .map_err(|e| TrailgenError::InvalidData(format!("invalid KML lon: {e}")))?;
            let lat = xs
                .next()
                .ok_or_else(|| TrailgenError::InvalidData("KML coordinate missing lat".to_owned()))?
                .parse::<f64>()
                .map_err(|e| TrailgenError::InvalidData(format!("invalid KML lat: {e}")))?;
            let ele = xs.next().and_then(|x| x.parse::<f64>().ok());
            Ok(Coord { lon, lat, ele })
        })
        .collect::<Result<Vec<_>>>()?;
    Ok(RouteFile::new(
        LineString::new(points)?,
        metadata_from_doc(&doc),
    ))
}

fn metadata_from_doc(doc: &roxmltree::Document<'_>) -> RouteFileMetadata {
    let placemark = doc.descendants().find(|n| n.has_tag_name("Placemark"));
    RouteFileMetadata {
        title: placemark.and_then(|n| child_text(n, "name")).or_else(|| {
            doc.descendants()
                .find(|n| n.has_tag_name("Document"))
                .and_then(|n| child_text(n, "name"))
        }),
        description: placemark.and_then(|n| child_text(n, "description")),
        recorded_at: doc
            .descendants()
            .find(|n| n.has_tag_name("when"))
            .and_then(|n| n.text())
            .and_then(clean_text),
        activity_type: None,
    }
}

fn child_text(node: roxmltree::Node<'_, '_>, tag: &str) -> Option<String> {
    node.children()
        .find(|child| child.has_tag_name(tag))
        .and_then(|child| child.text())
        .and_then(clean_text)
}