aboutsummaryrefslogtreecommitdiff
path: root/src/gpx.rs
blob: 9dd772575b51b55ec628e86444f855d60ba2ebd4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//! 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<Vec<Coordinates>> {
    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::<f64>().ok())
                    .ok_or_else(|| eyre!("Invalid latitude"))?;
                let longitude = point
                    .attribute("lon")
                    .and_then(|l| l.parse::<f64>().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<P: AsRef<Path>>(path: P) -> Option<Compression> {
        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<P: AsRef<Path>>(
    path: P,
    compression: Compression,
) -> Result<Vec<Coordinates>> {
    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)
}