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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
//! 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<Coordinates>` 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<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)
}
/// 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<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
}
}
}
/// 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<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)
}
|