mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-10 07:36:29 +00:00
progress
This commit is contained in:
1 parent
c536e6247b
commit
5e2f506120
19 files changed
+484
-47
No files matched your search
@@ -0,0 +1,90 @@
|
||||
/// Maps the values of a category of trackpoint data (the surface, the highway...) to small codes,
|
||||
/// so that the trackpoints only hold a `u8` instead of a string.
|
||||
///
|
||||
/// The codes are the positions of the names in the order of first appearance. The table only
|
||||
/// grows: a code never changes meaning, so it stays valid in every state of the history.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Categories(Vec<String>);
|
||||
|
||||
impl Categories {
|
||||
/// Maximum number of distinct values.
|
||||
pub const CAPACITY: usize = u8::MAX as usize;
|
||||
|
||||
/// The code of `name`, which is added to the table if it is new. `None` if the table is full.
|
||||
pub fn code(&mut self, name: &str) -> Option<u8> {
|
||||
let index = match self.0.iter().position(|known| known == name) {
|
||||
Some(index) => index,
|
||||
None if self.0.len() < Self::CAPACITY => {
|
||||
self.0.push(name.to_owned());
|
||||
self.0.len() - 1
|
||||
}
|
||||
None => return None,
|
||||
};
|
||||
u8::try_from(index).ok()
|
||||
}
|
||||
|
||||
/// The name of a code, `None` if it is unknown.
|
||||
pub fn name(&self, code: u8) -> Option<&str> {
|
||||
self.0.get(usize::from(code)).map(String::as_str)
|
||||
}
|
||||
|
||||
/// All the names, in the order of their codes.
|
||||
pub fn names(&self) -> &[String] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The categories of the data of the trackpoints. There is one table per engine, shared by all
|
||||
/// its files.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct TrackpointCategories {
|
||||
pub surface: Categories,
|
||||
pub highway: Categories,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_codes_follow_the_order_of_appearance() {
|
||||
let mut categories = Categories::default();
|
||||
assert_eq!(categories.code("asphalt"), Some(0));
|
||||
assert_eq!(categories.code("gravel"), Some(1));
|
||||
// a known name keeps its code
|
||||
assert_eq!(categories.code("asphalt"), Some(0));
|
||||
assert_eq!(categories.names(), ["asphalt", "gravel"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_names_of_codes() {
|
||||
let mut categories = Categories::default();
|
||||
let code = categories.code("asphalt").unwrap();
|
||||
assert_eq!(categories.name(code), Some("asphalt"));
|
||||
assert_eq!(categories.name(code + 1), None);
|
||||
assert_eq!(Categories::default().name(0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_full_table_gives_no_code_to_new_names() {
|
||||
let mut categories = Categories::default();
|
||||
for i in 0..Categories::CAPACITY {
|
||||
assert_eq!(categories.code(&i.to_string()), Some(i as u8));
|
||||
}
|
||||
assert_eq!(categories.code("one too many"), None);
|
||||
assert_eq!(categories.names().len(), Categories::CAPACITY);
|
||||
// the known ones still have their code, the last one included
|
||||
assert_eq!(categories.code("0"), Some(0));
|
||||
assert_eq!(categories.code("254"), Some(254));
|
||||
assert_eq!(categories.name(254), Some("254"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_categories_are_independent() {
|
||||
let mut categories = TrackpointCategories::default();
|
||||
categories.surface.code("asphalt");
|
||||
assert_eq!(categories.highway.code("path"), Some(0));
|
||||
assert_eq!(categories.surface.names(), ["asphalt"]);
|
||||
assert_eq!(categories.highway.names(), ["path"]);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod categories;
|
||||
mod chunk;
|
||||
mod common;
|
||||
mod file;
|
||||
@@ -6,6 +7,7 @@ mod track;
|
||||
mod trackpoint;
|
||||
mod waypoint;
|
||||
|
||||
pub use categories::*;
|
||||
pub use chunk::*;
|
||||
pub use common::*;
|
||||
pub use file::*;
|
||||
|
||||
@@ -9,5 +9,8 @@ pub struct Trackpoint {
|
||||
pub hr: Option<u16>,
|
||||
pub cad: Option<u16>,
|
||||
pub power: Option<u16>,
|
||||
// TODO OSM data? or store intervals at a higher level?
|
||||
/// Code of the surface, see [`crate::TrackpointCategories`].
|
||||
pub surface: Option<u8>,
|
||||
/// Code of the highway, see [`crate::TrackpointCategories`].
|
||||
pub highway: Option<u8>,
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Author, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointChunk, Waypoint,
|
||||
WaypointChunk,
|
||||
Author, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointCategories,
|
||||
TrackpointChunk, Waypoint, WaypointChunk,
|
||||
};
|
||||
use chrono::DateTime;
|
||||
use quick_xml::Error;
|
||||
@@ -29,6 +29,8 @@ enum GPXElement {
|
||||
Heartrate,
|
||||
Cadence,
|
||||
Power,
|
||||
Surface,
|
||||
Highway,
|
||||
Symbol,
|
||||
Type,
|
||||
Color,
|
||||
@@ -48,7 +50,9 @@ fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
|
||||
coordinates
|
||||
}
|
||||
|
||||
pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
/// Parses a GPX file. The surface and the highway of the trackpoints are stored as codes of
|
||||
/// `categories`, which learns the values it does not know yet.
|
||||
pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File, Error> {
|
||||
let mut reader = Reader::from_reader(data);
|
||||
let mut buf = vec![];
|
||||
let mut gpx = File::default();
|
||||
@@ -95,8 +99,10 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
e if e.ends_with("atemp") => stack.push(GPXElement::Temperature),
|
||||
e if e.ends_with("hr") => stack.push(GPXElement::Heartrate),
|
||||
e if e.ends_with("cad") => stack.push(GPXElement::Cadence),
|
||||
"power" => stack.push(GPXElement::Power),
|
||||
e if e.ends_with("power") => stack.push(GPXElement::Power),
|
||||
e if e.ends_with("PowerInWatts") => stack.push(GPXElement::Power),
|
||||
"surface" => stack.push(GPXElement::Surface),
|
||||
"highway" => stack.push(GPXElement::Highway),
|
||||
"sym" => stack.push(GPXElement::Symbol),
|
||||
"type" => stack.push(GPXElement::Type),
|
||||
e if e.ends_with("color") => stack.push(GPXElement::Color),
|
||||
@@ -270,6 +276,18 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
trkpt.power = e.parse().ok();
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Surface) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.surface = categories.surface.code(&e);
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Highway) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.highway = categories.highway.code(&e);
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Symbol) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Waypoint(wpt)) = stack.last_mut() {
|
||||
@@ -322,7 +340,11 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_data(name: &str) -> File {
|
||||
parse(&std::fs::read(format!("data/{name}.gpx")).unwrap()).unwrap()
|
||||
parse(
|
||||
&std::fs::read(format!("data/{name}.gpx")).unwrap(),
|
||||
&mut Default::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -513,6 +535,88 @@ mod tests {
|
||||
assert!(trkpt.hr.is_some_and(|h| h == 150));
|
||||
}
|
||||
|
||||
fn parse_data_with_categories(name: &str) -> (File, TrackpointCategories) {
|
||||
let mut categories = TrackpointCategories::default();
|
||||
let file = parse(
|
||||
&std::fs::read(format!("data/{name}.gpx")).unwrap(),
|
||||
&mut categories,
|
||||
)
|
||||
.unwrap();
|
||||
(file, categories)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trackpoint_surface() {
|
||||
let (gpx, categories) = parse_data_with_categories("with_surface");
|
||||
|
||||
let trkseg = &gpx.trk[0].trkseg[0];
|
||||
assert_eq!(trkseg.len(), 80);
|
||||
// the codes follow the order of appearance in the file
|
||||
assert_eq!(categories.surface.names(), ["asphalt", "cobblestone"]);
|
||||
let asphalt = Some(0);
|
||||
let cobblestone = Some(1);
|
||||
assert_eq!(trkseg.iter().filter(|p| p.surface == asphalt).count(), 79);
|
||||
assert_eq!(
|
||||
trkseg.iter().filter(|p| p.surface == cobblestone).count(),
|
||||
1
|
||||
);
|
||||
// no highway in this file
|
||||
assert!(trkseg.iter().all(|trkpt| trkpt.highway.is_none()));
|
||||
assert!(categories.highway.names().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trackpoint_surface_and_highway() {
|
||||
let (gpx, categories) = parse_data_with_categories("with_highway");
|
||||
|
||||
let trkseg = &gpx.trk[0].trkseg[0];
|
||||
let names = |code: Option<u8>, names: &crate::Categories| {
|
||||
code.and_then(|code| names.name(code)).map(str::to_owned)
|
||||
};
|
||||
let values: Vec<_> = trkseg
|
||||
.iter()
|
||||
.map(|trkpt| {
|
||||
(
|
||||
names(trkpt.surface, &categories.surface),
|
||||
names(trkpt.highway, &categories.highway),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let some = |s: &str| Some(s.to_owned());
|
||||
assert_eq!(
|
||||
values,
|
||||
[
|
||||
(some("asphalt"), some("residential")),
|
||||
(some("asphalt"), some("residential")),
|
||||
(None, None),
|
||||
(some("gravel"), some("track")),
|
||||
(some("gravel"), None),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_shares_the_categories_between_files() {
|
||||
let mut categories = TrackpointCategories::default();
|
||||
let mut parsed = |name: &str| {
|
||||
parse(
|
||||
&std::fs::read(format!("data/{name}.gpx")).unwrap(),
|
||||
&mut categories,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let first = parsed("with_highway");
|
||||
let second = parsed("with_surface");
|
||||
let code = |file: &File, i: usize| file.trk[0].trkseg[0][i].surface;
|
||||
// "asphalt" is the same in both, "cobblestone" comes after "gravel"
|
||||
assert_eq!(code(&first, 0), Some(0));
|
||||
assert_eq!(code(&second, 0), Some(0));
|
||||
assert_eq!(
|
||||
categories.surface.names(),
|
||||
["asphalt", "gravel", "cobblestone"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trackpoint_cad() {
|
||||
let gpx = parse_data("with_cad");
|
||||
|
||||
@@ -331,7 +331,7 @@ mod tests {
|
||||
let mut f = File::open("data/with_time.gpx").unwrap();
|
||||
let mut data = String::new();
|
||||
let _ = f.read_to_string(&mut data);
|
||||
let gpx = parse(data.as_bytes()).unwrap();
|
||||
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
|
||||
|
||||
let trkseg = &gpx.trk[0].trkseg[0];
|
||||
let stats = Statistics::compute(trkseg);
|
||||
@@ -348,7 +348,7 @@ mod tests {
|
||||
let mut f = File::open(path).unwrap();
|
||||
let mut data = String::new();
|
||||
let _ = f.read_to_string(&mut data);
|
||||
parse(data.as_bytes()).unwrap()
|
||||
parse(data.as_bytes(), &mut Default::default()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -482,7 +482,7 @@ mod tests {
|
||||
("data/with_power_1.gpx", 3),
|
||||
] {
|
||||
let data = std::fs::read(path).unwrap();
|
||||
let gpx = parse(&data).unwrap();
|
||||
let gpx = parse(&data, &mut Default::default()).unwrap();
|
||||
let trkseg = &gpx.trk[0].trkseg[0];
|
||||
let global = Statistics::compute(trkseg).global;
|
||||
let values: Vec<f64> = trkseg
|
||||
|
||||
Reference in new issue
Block a user