//! 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)). //! //! Note that we throw away all information that we don't care about. Since we need only the //! coordinates of a track, we simply use a `Vec` to represent a track. 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}; /// World coordinates. #[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)` projection, where both are in the range `[0, 256 * 2^zoom)`. pub fn web_mercator(self, zoom: u32) -> (u64, u64) { const WIDTH: f64 = super::layer::TILE_WIDTH as f64; const HEIGHT: f64 = super::layer::TILE_HEIGHT as f64; let lambda = self.longitude.to_radians(); let phi = self.latitude.to_radians(); let x = 2u64.pow(zoom) as f64 / (2.0 * PI) * WIDTH * (lambda + PI); let y = 2u64.pow(zoom) as f64 / (2.0 * PI) * HEIGHT * (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" } /// Extracts a track from the given string. 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) } /// Compression format of the data. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Compression { /// Indicates that no compression is applied, and the file is plain GPX. None, /// Indicates that the file is gzip compressed. Gzip, /// Indicates that the file is brotli compressed. Brotli, } impl Compression { /// Suggests a [`Compression`] from the given path name. /// /// This will suggest [`Compression::Brotli`] for files ending in `.br`, [`Compression::Gzip`] /// for files ending with `.gz` or `.gzip`, and [`Compression::None`] for files ending with /// `.gpx`. /// /// If the file does not end with any of the aforementioned extensions, an error is returned /// instead. 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 } } } /// Extracts the relevant GPX data from the given file. /// /// Note that the content must be valid UTF-8, as that is what our parser expects. 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) }