mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-06 22:04:06 +00:00
progress
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx xmlns="http://www.topografix.com/GPX/1/1"
|
||||
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" version="1.1" creator="https://gpx.studio">
|
||||
<metadata>
|
||||
<name>with_highway</name>
|
||||
</metadata>
|
||||
<trk>
|
||||
<name>with_highway</name>
|
||||
<trkseg>
|
||||
<trkpt lat="50.790867" lon="4.404968">
|
||||
<ele>109.0</ele>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:Extensions>
|
||||
<surface>asphalt</surface>
|
||||
<highway>residential</highway>
|
||||
</gpxtpx:Extensions>
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>
|
||||
<trkpt lat="50.791000" lon="4.405100">
|
||||
<ele>110.0</ele>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:Extensions>
|
||||
<surface>asphalt</surface>
|
||||
<highway>residential</highway>
|
||||
</gpxtpx:Extensions>
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>
|
||||
<trkpt lat="50.791200" lon="4.405300">
|
||||
<ele>111.0</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="50.791400" lon="4.405500">
|
||||
<ele>112.0</ele>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:Extensions>
|
||||
<surface>gravel</surface>
|
||||
<highway>track</highway>
|
||||
</gpxtpx:Extensions>
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>
|
||||
<trkpt lat="50.791600" lon="4.405700">
|
||||
<ele>113.0</ele>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:Extensions>
|
||||
<surface>gravel</surface>
|
||||
</gpxtpx:Extensions>
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Apply, CommandError, File, State, parse, produce};
|
||||
use crate::{Apply, CommandError, File, State, TrackpointCategories, parse, produce};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Load<'a> {
|
||||
@@ -9,9 +9,9 @@ pub struct Load<'a> {
|
||||
}
|
||||
|
||||
impl Load<'_> {
|
||||
fn parse(self) -> Result<File, CommandError> {
|
||||
let mut file =
|
||||
parse(self.data).map_err(|err| CommandError::InvalidData(err.to_string()))?;
|
||||
fn parse(self, categories: &mut TrackpointCategories) -> Result<File, CommandError> {
|
||||
let mut file = parse(self.data, categories)
|
||||
.map_err(|err| CommandError::InvalidData(err.to_string()))?;
|
||||
if file.info.name.trim().is_empty() {
|
||||
file.info.name = self.name.to_owned();
|
||||
}
|
||||
@@ -40,7 +40,7 @@ impl Apply for LoadFiles<'_> {
|
||||
let mut files = Vec::with_capacity(self.files.len());
|
||||
let mut first_error = None;
|
||||
for load in self.files {
|
||||
match load.parse() {
|
||||
match load.parse(state.categories) {
|
||||
Ok(file) => files.push(file),
|
||||
Err(error) => {
|
||||
first_error.get_or_insert(error);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{Clipboard, FileId, FileOrder, Selection, StackEntry, State};
|
||||
use crate::{Clipboard, FileId, FileOrder, Selection, StackEntry, State, TrackpointCategories};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Fixture {
|
||||
@@ -10,11 +10,13 @@ pub struct Fixture {
|
||||
pub selection: Selection,
|
||||
pub order: FileOrder,
|
||||
pub clipboard: Option<Clipboard>,
|
||||
pub categories: TrackpointCategories,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
pub fn state(&mut self) -> State<'_> {
|
||||
State {
|
||||
categories: &mut self.categories,
|
||||
files: &mut self.files,
|
||||
selection: &mut self.selection,
|
||||
order: &mut self.order,
|
||||
|
||||
@@ -179,7 +179,7 @@ mod tests {
|
||||
fn fixture_with_tracks() -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
let file = Rc::new(crate::parse(&data).unwrap());
|
||||
let file = Rc::new(crate::parse(&data, &mut Default::default()).unwrap());
|
||||
let id = file.id;
|
||||
fx.files.insert(id, file);
|
||||
(fx, id)
|
||||
|
||||
@@ -33,6 +33,17 @@ pub struct StatisticsBuffer {
|
||||
pub cad: Vec<f64>,
|
||||
pub atemp: Vec<f64>,
|
||||
pub power: Vec<f64>,
|
||||
/// Surface of the trackpoints: 0 when unknown, else 1 + its code in the categories of the
|
||||
/// engine (so that it is never 0 for a known surface).
|
||||
pub surface: Vec<u8>,
|
||||
/// Highway of the trackpoints, as the surface.
|
||||
pub highway: Vec<u8>,
|
||||
}
|
||||
|
||||
/// 0 for an unknown value, else the code plus one. The categories hold at most 255 values, so it
|
||||
/// fits.
|
||||
fn unknown_or_next(code: Option<u8>) -> u8 {
|
||||
code.and_then(|code| code.checked_add(1)).unwrap_or(0)
|
||||
}
|
||||
|
||||
impl StatisticsBuffer {
|
||||
@@ -65,6 +76,8 @@ impl StatisticsBuffer {
|
||||
self.cad.clear();
|
||||
self.atemp.clear();
|
||||
self.power.clear();
|
||||
self.surface.clear();
|
||||
self.highway.clear();
|
||||
|
||||
let optional = |value: Option<f64>| value.unwrap_or(f64::NAN);
|
||||
|
||||
@@ -102,6 +115,8 @@ impl StatisticsBuffer {
|
||||
self.cad.push(optional(trkpt.cad.map(f64::from)));
|
||||
self.atemp.push(optional(trkpt.atemp.map(f64::from)));
|
||||
self.power.push(optional(trkpt.power.map(f64::from)));
|
||||
self.surface.push(unknown_or_next(trkpt.surface));
|
||||
self.highway.push(unknown_or_next(trkpt.highway));
|
||||
}
|
||||
self.global.merge(&stats.global);
|
||||
}
|
||||
@@ -153,7 +168,7 @@ mod tests {
|
||||
|
||||
fn computed(path: &str) -> (TrackSegment, Statistics) {
|
||||
let data = std::fs::read(path).unwrap();
|
||||
let file = parse(&data).unwrap();
|
||||
let file = parse(&data, &mut Default::default()).unwrap();
|
||||
let segment = file.trk[0].trkseg[0].clone();
|
||||
let stats = Statistics::compute(&segment);
|
||||
(segment, stats)
|
||||
@@ -179,6 +194,8 @@ mod tests {
|
||||
buffer.cad.len(),
|
||||
buffer.atemp.len(),
|
||||
buffer.power.len(),
|
||||
buffer.surface.len(),
|
||||
buffer.highway.len(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -278,6 +295,45 @@ mod tests {
|
||||
assert_eq!(buffer.global.total_time, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_surface_and_highway_codes() {
|
||||
let (segment, s) = computed("data/with_highway.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
|
||||
// 0 for the trackpoints that have none, else the code of the engine plus one
|
||||
assert_eq!(buffer.surface, [1, 1, 0, 2, 2]);
|
||||
assert_eq!(buffer.highway, [1, 1, 0, 2, 0]);
|
||||
assert!(all_lengths(&buffer).iter().all(|len| *len == 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codes_of_the_trackpoints_are_used_as_they_are() {
|
||||
let (mut segment, _) = computed("data/simple.gpx");
|
||||
let mut point = segment[0].clone();
|
||||
point.surface = Some(7);
|
||||
point.highway = Some(254);
|
||||
segment.splice(0, 1, vec![point]);
|
||||
let s = Statistics::compute(&segment);
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
assert_eq!((buffer.surface[0], buffer.highway[0]), (8, 255));
|
||||
assert_eq!((buffer.surface[1], buffer.highway[1]), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_codes_without_surface_and_highway() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
assert_eq!(buffer.surface.len(), s.local.len());
|
||||
assert!(buffer.surface.iter().all(|c| *c == 0));
|
||||
assert!(buffer.highway.iter().all(|c| *c == 0));
|
||||
// and they are reset by the next update
|
||||
buffer.update(&[]);
|
||||
assert!(buffer.surface.is_empty() && buffer.highway.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_replaces_previous_content() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
@@ -344,7 +400,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_slice_keeps_the_bounds_and_averages_of_the_selection() {
|
||||
let data = std::fs::read("data/with_hr.gpx").unwrap();
|
||||
let file = parse(&data).unwrap();
|
||||
let file = parse(&data, &mut Default::default()).unwrap();
|
||||
let segment = file.trk[0].trkseg[0].clone();
|
||||
let s = Statistics::compute(&segment);
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
|
||||
@@ -161,7 +161,7 @@ mod tests {
|
||||
|
||||
fn state(path: &str) -> (StackEntry, Rc<File>) {
|
||||
let data = std::fs::read(path).unwrap();
|
||||
let file = Rc::new(parse(&data).unwrap());
|
||||
let file = Rc::new(parse(&data, &mut Default::default()).unwrap());
|
||||
let mut state = StackEntry::default();
|
||||
state.insert(file.id, file.clone());
|
||||
(state, file)
|
||||
@@ -273,7 +273,7 @@ mod tests {
|
||||
let (mut state, first) = state("data/simple.gpx");
|
||||
let (other, second) = {
|
||||
let data = std::fs::read("data/with_time.gpx").unwrap();
|
||||
let file = Rc::new(parse(&data).unwrap());
|
||||
let file = Rc::new(parse(&data, &mut Default::default()).unwrap());
|
||||
(file.clone(), file)
|
||||
};
|
||||
state.insert(other.id, other);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::{
|
||||
Action, Apply, Clipboard, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||
FileStructureCache, GlobalStatistics, SelectMode, Selection, Stack, State, StatisticsBuffer,
|
||||
StatisticsCache, TrackSegmentId, Trackpoint, Waypoint, WaypointId,
|
||||
StatisticsCache, TrackSegmentId, Trackpoint, TrackpointCategories, Waypoint, WaypointId,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -18,6 +18,7 @@ pub struct Engine {
|
||||
order_changed: bool,
|
||||
selection_changed: bool,
|
||||
statistics_buffer: StatisticsBuffer,
|
||||
categories: TrackpointCategories,
|
||||
clipboard: Option<Clipboard>,
|
||||
clipboard_changed: bool,
|
||||
}
|
||||
@@ -27,6 +28,11 @@ impl Engine {
|
||||
&self.statistics_buffer
|
||||
}
|
||||
|
||||
/// The names of the surfaces and highways that the trackpoints refer to by code.
|
||||
pub fn categories(&self) -> &TrackpointCategories {
|
||||
&self.categories
|
||||
}
|
||||
|
||||
/// Files in display order.
|
||||
pub fn order(&self) -> &[FileId] {
|
||||
&self.order.0
|
||||
@@ -216,6 +222,7 @@ impl Engine {
|
||||
fn edit(&mut self, command: Command) -> Option<Diff> {
|
||||
self.stack.create_and_push_next(|files| {
|
||||
let mut state = State {
|
||||
categories: &mut self.categories,
|
||||
files,
|
||||
selection: &mut self.selection,
|
||||
order: &mut self.order,
|
||||
@@ -1231,6 +1238,52 @@ mod tests {
|
||||
assert!(engine.waypoint(&file, &id).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_categories_are_shared_and_kept_by_the_history() {
|
||||
let mut engine = Engine::default();
|
||||
assert!(engine.categories().surface.names().is_empty());
|
||||
|
||||
load(&mut engine, "data/with_highway.gpx");
|
||||
assert_eq!(engine.categories().surface.names(), ["asphalt", "gravel"]);
|
||||
assert_eq!(
|
||||
engine.categories().highway.names(),
|
||||
["residential", "track"]
|
||||
);
|
||||
// the trackpoints of the selection refer to them
|
||||
assert_eq!(engine.statistics().surface, [1, 1, 0, 2, 2]);
|
||||
assert_eq!(engine.statistics().highway, [1, 1, 0, 2, 0]);
|
||||
|
||||
// a second file goes on with the same table
|
||||
load(&mut engine, "data/with_surface.gpx");
|
||||
assert_eq!(
|
||||
engine.categories().surface.names(),
|
||||
["asphalt", "gravel", "cobblestone"]
|
||||
);
|
||||
assert_eq!(engine.statistics().surface.len(), 80);
|
||||
assert_eq!(engine.statistics().surface[0], 1);
|
||||
assert_eq!(
|
||||
engine
|
||||
.statistics()
|
||||
.surface
|
||||
.iter()
|
||||
.filter(|c| **c == 3)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
|
||||
// undoing does not forget what was learned: the codes stay valid in every state
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert_eq!(
|
||||
engine.categories().surface.names(),
|
||||
["asphalt", "gravel", "cobblestone"]
|
||||
);
|
||||
assert!(engine.execute(Action::Redo));
|
||||
let first = engine.order()[0];
|
||||
select_files(&mut engine, &[first]);
|
||||
assert_eq!(engine.statistics().surface, [1, 1, 0, 2, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trackpoint() {
|
||||
let mut engine = Engine::default();
|
||||
|
||||
@@ -9,6 +9,7 @@ pub use selection::*;
|
||||
pub use stack::*;
|
||||
|
||||
pub struct State<'a> {
|
||||
pub categories: &'a mut crate::TrackpointCategories,
|
||||
pub files: &'a mut StackEntry,
|
||||
pub selection: &'a mut Selection,
|
||||
pub order: &'a mut FileOrder,
|
||||
|
||||
+25
-1
@@ -14,7 +14,7 @@ use std::cell::RefCell;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use gpx_engine::{self as engine, Action, Command, Engine, FileId, LngLat, LngLatBounds};
|
||||
use js_sys::{Array, BigInt64Array, Float64Array, Object, Reflect};
|
||||
use js_sys::{Array, BigInt64Array, Float64Array, Object, Reflect, Uint8Array};
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -164,6 +164,28 @@ stats_getter!(cad, Float64Array);
|
||||
stats_getter!(atemp, Float64Array);
|
||||
stats_getter!(power, Float64Array);
|
||||
|
||||
// Surface and highway of the trackpoints: 0 when unknown, else 1 + the index in `surfaces()` or
|
||||
// `highways()`.
|
||||
stats_getter!(surface, Uint8Array);
|
||||
stats_getter!(highway, Uint8Array);
|
||||
|
||||
/// The names of the surfaces, in the order of their codes (see `surface`). The list only grows:
|
||||
/// a name keeps its index.
|
||||
#[wasm_bindgen]
|
||||
pub fn surfaces() -> StringList {
|
||||
with_engine(|e| array(e.categories().surface.names(), |name| name.as_str().into()))
|
||||
.unwrap_or_default()
|
||||
.unchecked_into()
|
||||
}
|
||||
|
||||
/// The names of the highways, in the order of their codes (see `highway`).
|
||||
#[wasm_bindgen]
|
||||
pub fn highways() -> StringList {
|
||||
with_engine(|e| array(e.categories().highway.names(), |name| name.as_str().into()))
|
||||
.unwrap_or_default()
|
||||
.unchecked_into()
|
||||
}
|
||||
|
||||
// File commands
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -803,6 +825,8 @@ extern "C" {
|
||||
pub type GlobalStatistics;
|
||||
#[wasm_bindgen(typescript_type = "string[]")]
|
||||
pub type FileOrder;
|
||||
#[wasm_bindgen(typescript_type = "string[]")]
|
||||
pub type StringList;
|
||||
}
|
||||
|
||||
fn set(object: &Object, key: &str, value: impl Into<JsValue>) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { Coordinates } from 'gpx';
|
||||
import { NO_TIME, type SelectionStatistics } from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { categoryAt } from '$lib/trackpoint-categories';
|
||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||
|
||||
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
||||
@@ -41,7 +42,8 @@ interface ElevationProfilePoint {
|
||||
segment: number;
|
||||
length: number;
|
||||
};
|
||||
extensions: Record<string, any>;
|
||||
surface?: string;
|
||||
highway?: string;
|
||||
coordinates: Coordinates;
|
||||
index: number;
|
||||
}
|
||||
@@ -185,14 +187,11 @@ export class ElevationProfile {
|
||||
segment: point.slope.segment.toFixed(1),
|
||||
length: getDistanceWithUnits(point.slope.length),
|
||||
};
|
||||
let surface = point.extensions.surface
|
||||
? point.extensions.surface
|
||||
: 'unknown';
|
||||
let highway = point.extensions.highway
|
||||
? point.extensions.highway
|
||||
: 'unknown';
|
||||
let sacScale = point.extensions.sac_scale;
|
||||
let mtbScale = point.extensions.mtb_scale;
|
||||
let surface = point.surface ?? 'unknown';
|
||||
let highway = point.highway ?? 'unknown';
|
||||
// TODO the engine does not store the sac and mtb scales yet
|
||||
let sacScale: string | undefined = undefined;
|
||||
let mtbScale: string | undefined = undefined;
|
||||
|
||||
let labels = [
|
||||
` ${i18n._('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
|
||||
@@ -413,8 +412,8 @@ export class ElevationProfile {
|
||||
segment: data.slopeSegmentSlope[index],
|
||||
length: data.slopeSegmentDistance[index],
|
||||
},
|
||||
// TODO the engine does not store the extensions (surface, highway...) yet
|
||||
extensions: data.extensions[index] ?? {},
|
||||
surface: categoryAt(data.surface, data.surfaces, index),
|
||||
highway: categoryAt(data.highway, data.highways, index),
|
||||
coordinates: { lat: data.lat[index], lon: data.lng[index] },
|
||||
index,
|
||||
});
|
||||
@@ -602,16 +601,13 @@ export class ElevationProfile {
|
||||
|
||||
surfaceFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getSurfaceColor(point.extensions.surface);
|
||||
return getSurfaceColor(point.surface ?? '');
|
||||
}
|
||||
|
||||
highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getHighwayColor(
|
||||
point.extensions.highway,
|
||||
point.extensions.sac_scale,
|
||||
point.extensions.mtb_scale
|
||||
);
|
||||
// TODO the engine does not store the sac and mtb scales yet
|
||||
return getHighwayColor(point.highway ?? '', undefined, undefined);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
@@ -140,11 +140,14 @@ export type SelectionStatistics = {
|
||||
cad: Float64Array;
|
||||
atemp: Float64Array;
|
||||
power: Float64Array;
|
||||
/**
|
||||
* Extensions of the trackpoints (surface, highway...), or an empty array when they are not
|
||||
* known. TODO the engine does not store them yet.
|
||||
*/
|
||||
extensions: Record<string, string>[];
|
||||
/** Surface of the trackpoints: 0 when unknown, else 1 + the index in `surfaces`. */
|
||||
surface: Uint8Array;
|
||||
/** Names of the surfaces, by code: a name keeps its index. */
|
||||
surfaces: string[];
|
||||
/** Highway of the trackpoints: 0 when unknown, else 1 + the index in `highways`. */
|
||||
highway: Uint8Array;
|
||||
/** Names of the highways, by code. */
|
||||
highways: string[];
|
||||
/**
|
||||
* Global statistics of the trackpoints from `start` to `end` (both included), for example
|
||||
* the part of the elevation profile that was dragged over. `undefined` if the range is not
|
||||
@@ -174,7 +177,10 @@ const EMPTY_SELECTION_STATISTICS: SelectionStatistics = {
|
||||
cad: new Float64Array(),
|
||||
atemp: new Float64Array(),
|
||||
power: new Float64Array(),
|
||||
extensions: [],
|
||||
surface: new Uint8Array(),
|
||||
surfaces: [],
|
||||
highway: new Uint8Array(),
|
||||
highways: [],
|
||||
slice: () => undefined,
|
||||
};
|
||||
|
||||
@@ -625,7 +631,10 @@ class Engine {
|
||||
cad: wasm.cad().slice(),
|
||||
atemp: wasm.atemp().slice(),
|
||||
power: wasm.power().slice(),
|
||||
extensions: [],
|
||||
surface: wasm.surface().slice(),
|
||||
surfaces: wasm.surfaces(),
|
||||
highway: wasm.highway().slice(),
|
||||
highways: wasm.highways(),
|
||||
slice: (start, end) =>
|
||||
version === this._statisticsVersion ? wasm.slice_statistics(start, end) : undefined,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { categoryAt } from './trackpoint-categories';
|
||||
|
||||
describe('categoryAt', () => {
|
||||
const codes = new Uint8Array([1, 1, 0, 2, 0]);
|
||||
const names = ['asphalt', 'gravel'];
|
||||
|
||||
it('is the name of the code', () => {
|
||||
assert.equal(categoryAt(codes, names, 0), 'asphalt');
|
||||
assert.equal(categoryAt(codes, names, 3), 'gravel');
|
||||
});
|
||||
|
||||
it('is undefined when the value is unknown', () => {
|
||||
assert.equal(categoryAt(codes, names, 2), undefined);
|
||||
assert.equal(categoryAt(codes, names, 4), undefined);
|
||||
});
|
||||
|
||||
it('is undefined out of the trackpoints, or for a code without name', () => {
|
||||
assert.equal(categoryAt(codes, names, 5), undefined);
|
||||
assert.equal(categoryAt(new Uint8Array([3]), names, 0), undefined);
|
||||
assert.equal(categoryAt(new Uint8Array(), [], 0), undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The value of a trackpoint for a category of the engine (surface, highway): `codes` has one entry
|
||||
* per trackpoint, 0 when the value is unknown, else 1 + the index of the value in `names`.
|
||||
*/
|
||||
export function categoryAt(
|
||||
codes: ArrayLike<number>,
|
||||
names: readonly string[],
|
||||
index: number
|
||||
): string | undefined {
|
||||
const code = codes[index];
|
||||
return code ? names[code - 1] : undefined;
|
||||
}
|
||||
@@ -45,7 +45,10 @@
|
||||
cad: new Float64Array(),
|
||||
atemp: new Float64Array(),
|
||||
power: new Float64Array(),
|
||||
extensions: [],
|
||||
surface: new Uint8Array(),
|
||||
surfaces: [],
|
||||
highway: new Uint8Array(),
|
||||
highways: [],
|
||||
slice: () => undefined,
|
||||
});
|
||||
let slicedStatistics = writable(undefined);
|
||||
|
||||
Reference in New Issue
Block a user