//! GPX data extraction functions. //! //! We *could* use the [gpx](https://github.com/georust/gpx) crate, but we don't care about much //! other than the coordinates of the tracks. By implementing the little functionality ourselves, //! we can use a fast XML parser ([roxmltree](https://github.com/RazrFalcon/roxmltree)). use std::{ f64::consts::PI, ffi::OsStr, fs::{self, File}, io::{BufReader, Read}, path::Path, }; use color_eyre::eyre::{eyre, Result}; use flate2::bufread::GzDecoder; use roxmltree::{Document, Node, NodeType}; #[derive(Debug, Copy, Clone, PartialEq)] pub struct Coordinates { longitude: f64, latitude: f64, } impl Coordinates { /// Calculates the [Web Mercator /// projection](https://en.wikipedia.org/wiki/Web_Mercator_projection) of the coordinates. /// Returns the `(x, y)` coordinates. pub fn web_mercator(self, zoom: u32) -> (u64, u64) { let lambda = self.longitude.to_radians(); let phi = self.latitude.to_radians(); let x = 2u64.pow(zoom) as f64 / (2.0 * PI) * 256.0 * (lambda + PI); let y = 2u64.pow(zoom) as f64 / (2.0 * PI) * 256.0 * (PI - (PI / 4.0 + phi / 2.0).tan().ln()); (x.floor() as u64, y.floor() as u64) } } fn is_track_node(node: &Node) -> bool { node.node_type() == NodeType::Element && node.tag_name().name() == "trk" } fn is_track_segment(node: &Node) -> bool { node.node_type() == NodeType::Element && node.tag_name().name() == "trkseg" } fn is_track_point(node: &Node) -> bool { node.node_type() == NodeType::Element && node.tag_name().name() == "trkpt" } pub fn extract_from_str(input: &str) -> Result> { let mut result = Vec::new(); let document = Document::parse(input)?; for node in document.root_element().children().filter(is_track_node) { for segment in node.children().filter(is_track_segment) { for point in segment.children().filter(is_track_point) { let latitude = point .attribute("lat") .and_then(|l| l.parse::().ok()) .ok_or_else(|| eyre!("Invalid latitude"))?; let longitude = point .attribute("lon") .and_then(|l| l.parse::().ok()) .ok_or_else(|| eyre!("Invalid longitude"))?; result.push(Coordinates { latitude, longitude, }); } } } Ok(result) } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Compression { None, Gzip, Brotli, } impl Compression { pub fn suggest_from_path>(path: P) -> Option { let Some(ext) = path.as_ref().extension() else { return None }; if OsStr::new("br") == ext { Some(Compression::Brotli) } else if [OsStr::new("gz"), OsStr::new("gzip")].contains(&ext) { Some(Compression::Gzip) } else if OsStr::new("gpx") == ext { Some(Compression::None) } else { None } } } pub fn extract_from_file>( path: P, compression: Compression, ) -> Result> { let content = match compression { Compression::None => fs::read_to_string(path)?, Compression::Gzip => { let mut result = String::new(); GzDecoder::new(BufReader::new(File::open(path)?)).read_to_string(&mut result)?; result } Compression::Brotli => { let mut result = Vec::new(); brotli::BrotliDecompress(&mut BufReader::new(File::open(path)?), &mut result)?; String::from_utf8(result)? } }; extract_from_str(&content) }