This commit is contained in:
vcoppe
2026-09-24 20:55:30 +02:00
parent 497ac120ec
commit 27c0a57cc1
13 changed files with 296 additions and 143 deletions
+1
View File
@@ -0,0 +1 @@
@@ -4,8 +4,9 @@ use uuid::Uuid;
use wasm_bindgen::prelude::*;
use crate::{
actions::parse,
gpx::GPXFile,
io::parse,
selection::Selection,
stack::{Stack, StackEntry},
};
@@ -20,6 +21,7 @@ use crate::{
#[wasm_bindgen]
pub struct Controller {
stack: Stack,
selection: Selection,
}
#[wasm_bindgen]
@@ -27,7 +29,8 @@ impl Controller {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
stack: Stack::default(),
stack: Default::default(),
selection: Default::default(),
}
}
+32 -1
View File
@@ -10,8 +10,39 @@ pub struct LngLat {
pub lat: f64,
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct LngLatBounds {
pub sw: LngLat,
pub ne: LngLat,
}
impl Default for LngLatBounds {
fn default() -> Self {
Self {
sw: LngLat {
lng: 180.0,
lat: 90.0,
},
ne: LngLat {
lng: -180.0,
lat: -90.0,
},
}
}
}
impl LngLatBounds {
pub fn extend(&mut self, coordinates: LngLat) {
self.sw.lng = self.sw.lng.min(coordinates.lng);
self.sw.lat = self.sw.lat.min(coordinates.lat);
self.ne.lng = self.ne.lng.min(coordinates.lng);
self.ne.lat = self.ne.lat.min(coordinates.lat);
}
pub fn merge(&mut self, other: &LngLatBounds) {
self.sw.lng = self.sw.lng.min(other.sw.lng);
self.sw.lat = self.sw.lat.min(other.sw.lat);
self.ne.lng = self.ne.lng.min(other.ne.lng);
self.ne.lat = self.ne.lat.min(other.ne.lat);
}
}
@@ -11,29 +11,29 @@ use quick_xml::events::attributes::Attributes;
use quick_xml::reader::Reader;
enum GPXElement {
METADATA,
NAME,
COMMENT,
DESCRIPTION,
SOURCE,
AUTHOR(Author),
LINK(Link),
TEXT,
TRACK(Track),
SEGMENT(TrackSegment),
TRACKPOINT(Trackpoint),
WAYPOINT(Waypoint),
ELEVATION,
TIME,
TEMPERATURE,
HEARTRATE,
CADENCE,
POWER,
SYMBOL,
TYPE,
COLOR,
OPACITY,
WIDTH,
Metadata,
Name,
Comment,
Description,
Source,
Author(Author),
Link(Link),
Text,
Track(Track),
Segment(TrackSegment),
Trackpoint(Trackpoint),
Waypoint(Waypoint),
Elevation,
Time,
Temperature,
Heartrate,
Cadence,
Power,
Symbol,
Type,
Color,
Opacity,
Width,
}
fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
@@ -60,12 +60,12 @@ pub fn parse(data: &[u8]) -> Result<GPXFile, Error> {
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => match e.name().as_ref() {
"metadata" => stack.push(GPXElement::METADATA),
"name" => stack.push(GPXElement::NAME),
"cmt" => stack.push(GPXElement::COMMENT),
"desc" => stack.push(GPXElement::DESCRIPTION),
"src" => stack.push(GPXElement::SOURCE),
"author" => stack.push(GPXElement::AUTHOR(Author::default())),
"metadata" => stack.push(GPXElement::Metadata),
"name" => stack.push(GPXElement::Name),
"cmt" => stack.push(GPXElement::Comment),
"desc" => stack.push(GPXElement::Description),
"src" => stack.push(GPXElement::Source),
"author" => stack.push(GPXElement::Author(Author::default())),
"link" => {
let mut link = Link::default();
for attr in e.attributes() {
@@ -75,35 +75,35 @@ pub fn parse(data: &[u8]) -> Result<GPXFile, Error> {
}
}
}
stack.push(GPXElement::LINK(link));
stack.push(GPXElement::Link(link));
}
"text" => stack.push(GPXElement::TEXT),
"trk" => stack.push(GPXElement::TRACK(Track::default())),
"text" => stack.push(GPXElement::Text),
"trk" => stack.push(GPXElement::Track(Track::default())),
"trkseg" => {
stack.push(GPXElement::SEGMENT(TrackSegment::default()));
stack.push(GPXElement::Segment(TrackSegment::default()));
}
"trkpt" => {
let mut trkpt = Trackpoint::default();
trkpt.coordinates = parse_coordinates(e.attributes());
stack.push(GPXElement::TRACKPOINT(trkpt));
stack.push(GPXElement::Trackpoint(trkpt));
}
"wpt" => {
let mut wpt = Waypoint::default();
wpt.coordinates = parse_coordinates(e.attributes());
stack.push(GPXElement::WAYPOINT(wpt));
stack.push(GPXElement::Waypoint(wpt));
}
"ele" => stack.push(GPXElement::ELEVATION),
"time" => stack.push(GPXElement::TIME),
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("PowerInWatts") => stack.push(GPXElement::POWER),
"sym" => stack.push(GPXElement::SYMBOL),
"type" => stack.push(GPXElement::TYPE),
e if e.ends_with("color") => stack.push(GPXElement::COLOR),
e if e.ends_with("opacity") => stack.push(GPXElement::OPACITY),
e if e.ends_with("width") => stack.push(GPXElement::WIDTH),
"ele" => stack.push(GPXElement::Elevation),
"time" => stack.push(GPXElement::Time),
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("PowerInWatts") => stack.push(GPXElement::Power),
"sym" => stack.push(GPXElement::Symbol),
"type" => stack.push(GPXElement::Type),
e if e.ends_with("color") => stack.push(GPXElement::Color),
e if e.ends_with("opacity") => stack.push(GPXElement::Opacity),
e if e.ends_with("width") => stack.push(GPXElement::Width),
_ => (),
},
Ok(Event::End(e)) => match e.name().as_ref() {
@@ -117,20 +117,20 @@ pub fn parse(data: &[u8]) -> Result<GPXFile, Error> {
stack.pop();
}
"author" => {
if let Some(GPXElement::AUTHOR(author)) = stack.pop() {
if let Some(GPXElement::Author(author)) = stack.pop() {
gpx.info.author = Some(author);
}
}
"link" => {
if let Some(GPXElement::LINK(link)) = stack.pop() {
if let Some(GPXElement::Link(link)) = stack.pop() {
match stack.last_mut() {
Some(GPXElement::AUTHOR(author)) => {
Some(GPXElement::Author(author)) => {
author.link = Some(link);
}
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
trk.info.link = Some(link);
}
Some(GPXElement::WAYPOINT(wpt)) => {
Some(GPXElement::Waypoint(wpt)) => {
wpt.link = Some(link);
}
_ => (),
@@ -138,13 +138,13 @@ pub fn parse(data: &[u8]) -> Result<GPXFile, Error> {
}
}
"trk" => {
if let Some(GPXElement::TRACK(trk)) = stack.pop() {
if let Some(GPXElement::Track(trk)) = stack.pop() {
gpx.trk.push(trk);
}
}
"trkseg" => {
if let Some(GPXElement::SEGMENT(mut trkseg)) = stack.pop() {
if let Some(GPXElement::TRACK(trk)) = stack.last_mut() {
if let Some(GPXElement::Segment(mut trkseg)) = stack.pop() {
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
if !trkpt_chunk.trkpt.is_empty() {
trkseg.push(trkpt_chunk);
trkpt_chunk = TrackpointChunk::default();
@@ -154,8 +154,8 @@ pub fn parse(data: &[u8]) -> Result<GPXFile, Error> {
}
}
"trkpt" => {
if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.pop() {
if let Some(GPXElement::SEGMENT(trkseg)) = stack.last_mut() {
if let Some(GPXElement::Trackpoint(trkpt)) = stack.pop() {
if let Some(GPXElement::Segment(trkseg)) = stack.last_mut() {
trkpt_chunk.trkpt.push(trkpt);
if trkpt_chunk.is_full() {
trkseg.push(trkpt_chunk);
@@ -165,7 +165,7 @@ pub fn parse(data: &[u8]) -> Result<GPXFile, Error> {
}
}
"wpt" => {
if let Some(GPXElement::WAYPOINT(wpt)) = stack.pop() {
if let Some(GPXElement::Waypoint(wpt)) = stack.pop() {
wpt_chunk.wpt.push(wpt);
if wpt_chunk.is_full() {
gpx.wpt.push(Rc::new(wpt_chunk));
@@ -176,135 +176,135 @@ pub fn parse(data: &[u8]) -> Result<GPXFile, Error> {
_ => (),
},
Ok(Event::Text(e)) => match stack.last_mut() {
Some(GPXElement::NAME) => {
Some(GPXElement::Name) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::METADATA) => {
Some(GPXElement::Metadata) => {
gpx.info.name = e.to_string();
}
Some(GPXElement::AUTHOR(author)) => {
Some(GPXElement::Author(author)) => {
author.name = Some(e.to_string());
}
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
trk.info.name = Some(e.to_string());
}
Some(GPXElement::WAYPOINT(wpt)) => {
Some(GPXElement::Waypoint(wpt)) => {
wpt.name = Some(e.to_string());
}
_ => (),
}
}
Some(GPXElement::COMMENT) => {
Some(GPXElement::Comment) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
trk.info.cmt = Some(e.to_string());
}
Some(GPXElement::WAYPOINT(wpt)) => {
Some(GPXElement::Waypoint(wpt)) => {
wpt.cmt = Some(e.to_string());
}
_ => (),
}
}
Some(GPXElement::DESCRIPTION) => {
Some(GPXElement::Description) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::METADATA) => {
Some(GPXElement::Metadata) => {
gpx.info.desc = Some(e.to_string());
}
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
trk.info.desc = Some(e.to_string());
}
Some(GPXElement::WAYPOINT(wpt)) => {
Some(GPXElement::Waypoint(wpt)) => {
wpt.desc = Some(e.to_string());
}
_ => (),
}
}
Some(GPXElement::SOURCE) => {
Some(GPXElement::Source) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
trk.info.src = Some(e.to_string());
}
_ => (),
}
}
Some(GPXElement::TEXT) => {
Some(GPXElement::Text) => {
stack.pop();
if let Some(GPXElement::LINK(link)) = stack.last_mut() {
if let Some(GPXElement::Link(link)) = stack.last_mut() {
link.text = Some(e.to_string());
}
}
Some(GPXElement::ELEVATION) => {
Some(GPXElement::Elevation) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::TRACKPOINT(trkpt)) => {
Some(GPXElement::Trackpoint(trkpt)) => {
trkpt.ele = e.parse().unwrap_or_default();
}
Some(GPXElement::WAYPOINT(wpt)) => {
Some(GPXElement::Waypoint(wpt)) => {
wpt.ele = e.parse().unwrap_or_default();
}
_ => (),
}
}
Some(GPXElement::TIME) => {
Some(GPXElement::Time) => {
stack.pop();
if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() {
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
if let Ok(time) = DateTime::parse_from_rfc3339(e.as_ref()) {
trkpt.time = Some(time.timestamp_millis());
}
}
}
Some(GPXElement::TEMPERATURE) => {
Some(GPXElement::Temperature) => {
stack.pop();
if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() {
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
trkpt.atemp = e.parse().ok();
}
}
Some(GPXElement::HEARTRATE) => {
Some(GPXElement::Heartrate) => {
stack.pop();
if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() {
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
trkpt.hr = e.parse().ok();
}
}
Some(GPXElement::CADENCE) => {
Some(GPXElement::Cadence) => {
stack.pop();
if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() {
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
trkpt.cad = e.parse().ok();
}
}
Some(GPXElement::POWER) => {
Some(GPXElement::Power) => {
stack.pop();
if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() {
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
trkpt.power = e.parse().ok();
}
}
Some(GPXElement::SYMBOL) => {
Some(GPXElement::Symbol) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::WAYPOINT(wpt)) => {
Some(GPXElement::Waypoint(wpt)) => {
wpt.sym = Some(e.to_string());
}
_ => (),
}
}
Some(GPXElement::TYPE) => {
Some(GPXElement::Type) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
trk.info.type_ = Some(e.to_string());
}
Some(GPXElement::WAYPOINT(wpt)) => {
Some(GPXElement::Waypoint(wpt)) => {
wpt.type_ = Some(e.to_string());
}
_ => (),
}
}
Some(GPXElement::COLOR) => {
Some(GPXElement::Color) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
let mut color = "#".to_string();
color.push_str(&e);
trk.info.color = Some(color);
@@ -312,19 +312,19 @@ pub fn parse(data: &[u8]) -> Result<GPXFile, Error> {
_ => (),
}
}
Some(GPXElement::OPACITY) => {
Some(GPXElement::Opacity) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
trk.info.opacity = e.parse().ok();
}
_ => (),
}
}
Some(GPXElement::WIDTH) => {
Some(GPXElement::Width) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::TRACK(trk)) => {
Some(GPXElement::Track(trk)) => {
trk.info.width = e.parse().ok();
}
_ => (),
+5 -3
View File
@@ -1,7 +1,9 @@
mod actions;
mod controller;
mod action;
mod algorithm;
mod engine;
mod gpx;
mod io;
mod selection;
mod stack;
mod statistics;
mod utils;
mod algorithms;
+38
View File
@@ -0,0 +1,38 @@
use std::collections::HashSet;
use uuid::Uuid;
#[derive(Debug)]
pub struct FileSelection {
files: HashSet<Uuid>,
}
#[derive(Debug)]
pub struct TrackSelection {
file: Uuid,
trk: HashSet<Uuid>,
}
#[derive(Debug)]
pub struct TrackSegmentSelection {
file: Uuid,
trk: Uuid,
trkseg: HashSet<Uuid>,
}
#[derive(Debug)]
pub struct WaypointSelection {
file: Uuid,
wpt: HashSet<Uuid>,
}
#[derive(Debug, Default)]
pub enum Selection {
#[default]
Empty,
File(FileSelection),
Track(TrackSelection),
TrackSegment(TrackSegmentSelection),
Waypoints,
Waypoint(WaypointSelection),
}
+34
View File
@@ -0,0 +1,34 @@
use crate::statistics::{GPXStatistics, GlobalStatistics};
pub struct Buffer {
pub total_distance: Vec<f64>,
pub total_time: Vec<i64>,
pub moving_time: Vec<i64>,
pub speed: Vec<f64>,
pub elevation_gain: Vec<f64>,
pub elevation_loss: Vec<f64>,
pub slope: Vec<f64>,
pub slope_segment_slope: Vec<f64>,
pub slope_segment_distance: Vec<f64>,
}
impl Buffer {
pub fn update(&mut self, stats: &[&GPXStatistics]) {
self.total_distance.clear();
self.total_time.clear();
self.moving_time.clear();
self.speed.clear();
self.elevation_gain.clear();
self.elevation_loss.clear();
self.slope.clear();
self.slope_segment_slope.clear();
self.slope_segment_distance.clear();
let mut cumul_stats = GlobalStatistics::default();
for stats in stats {
// add stats
// merge global ones into cumul
cumul_stats.merge(&stats.global);
}
}
}
+5
View File
@@ -0,0 +1,5 @@
mod buffer;
mod statistics;
pub use buffer::*;
pub use statistics::*;
@@ -1,5 +1,7 @@
use std::ops::Add;
use crate::{
algorithms::ramer_douglas_peucker,
algorithm::ramer_douglas_peucker,
for_each_window,
gpx::{LngLat, LngLatBounds, TrackSegment, Trackpoint},
utils::{distance, slope, speed},
@@ -7,29 +9,27 @@ use crate::{
#[derive(Default, Debug)]
pub struct GPXStatistics {
pub total_distance: f64,
pub moving_distance: Option<f64>,
pub moving_time: Option<i64>,
pub elevation_gain: f64,
pub elevation_loss: f64,
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub bounds: LngLatBounds,
pub global: GlobalStatistics,
pub local: Vec<TrackpointStatistics>,
}
impl GPXStatistics {
pub fn total_time(&self) -> Option<i64> {
self.start_time.zip(self.end_time).map(|(t1, t2)| t2 - t1)
self.global
.start_time
.zip(self.global.end_time)
.map(|(t1, t2)| t2 - t1)
}
pub fn total_speed(&self) -> Option<f64> {
self.total_time().map(|t| speed(self.total_distance, t))
self.total_time()
.map(|t| speed(self.global.total_distance, t))
}
pub fn moving_speed(&self) -> Option<f64> {
self.moving_distance
.zip(self.moving_time)
self.global
.moving_distance
.zip(self.global.moving_time)
.map(|(d, t)| speed(d, t))
}
@@ -54,7 +54,7 @@ impl GPXStatistics {
fn accumulate(&mut self, prev: &Trackpoint, cur: &Trackpoint) {
self.accumulate_distance_and_time(prev, cur);
self.update_time_bounds(cur.time);
self.update_bounds(&cur.coordinates);
self.update_bounds(cur.coordinates);
self.local
.push(TrackpointStatistics::from_partial_stats(&self));
}
@@ -63,31 +63,34 @@ impl GPXStatistics {
let dist = distance(prev.coordinates, cur.coordinates);
let time = cur.time_diff(prev);
self.total_distance += dist;
self.global.total_distance += dist;
if let Some(time) = time {
let speed = speed(dist, time);
if speed >= 0.5 && speed <= 1500.0 {
self.moving_distance = self.moving_distance.map_or(Some(dist), |d| Some(d + dist));
self.moving_time = self.moving_time.map_or(Some(time), |t| Some(t + time));
self.global.moving_distance = self
.global
.moving_distance
.map_or(Some(dist), |d| Some(d + dist));
self.global.moving_time = self
.global
.moving_time
.map_or(Some(time), |t| Some(t + time));
}
}
}
fn update_time_bounds(&mut self, time: Option<i64>) {
if let Some(time) = time {
if self.start_time.is_none() {
self.start_time = Some(time);
if self.global.start_time.is_none() {
self.global.start_time = Some(time);
}
self.end_time = Some(time);
self.global.end_time = Some(time);
}
}
fn update_bounds(&mut self, coordinates: &LngLat) {
self.bounds.sw.lng = self.bounds.sw.lng.min(coordinates.lng);
self.bounds.sw.lat = self.bounds.sw.lat.min(coordinates.lat);
self.bounds.ne.lng = self.bounds.ne.lng.min(coordinates.lng);
self.bounds.ne.lat = self.bounds.ne.lat.min(coordinates.lat);
fn update_bounds(&mut self, coordinates: LngLat) {
self.global.bounds.extend(coordinates);
}
fn compute_smoothed_speed(&mut self, trkseg: &TrackSegment) {
@@ -177,14 +180,14 @@ impl GPXStatistics {
let delta = smoothed_ele - prev_smoothed_ele;
if delta > 0.0 {
self.elevation_gain += delta;
self.global.elevation_gain += delta;
} else if delta < 0.0 {
self.elevation_loss -= delta;
self.global.elevation_loss -= delta;
}
if i < end || last {
self.local[flat_i].elevation_gain = self.elevation_gain;
self.local[flat_i].elevation_loss = self.elevation_loss;
self.local[flat_i].elevation_gain = self.global.elevation_gain;
self.local[flat_i].elevation_loss = self.global.elevation_loss;
}
prev_smoothed_ele = smoothed_ele;
@@ -230,9 +233,38 @@ impl GPXStatistics {
}
#[derive(Default, Debug)]
pub struct SlopeSegment {
pub slope: f64,
pub distance: f64,
pub struct GlobalStatistics {
pub total_distance: f64,
pub moving_distance: Option<f64>,
pub moving_time: Option<i64>,
pub elevation_gain: f64,
pub elevation_loss: f64,
pub start_time: Option<i64>,
pub end_time: Option<i64>,
pub bounds: LngLatBounds,
}
fn sum_options<T>(a: Option<T>, b: Option<T>) -> Option<T>
where
T: Add<Output = T>,
{
match (a, b) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
impl GlobalStatistics {
pub fn merge(&mut self, other: &GlobalStatistics) {
self.total_distance += other.total_distance;
self.moving_distance = sum_options(self.moving_distance, other.moving_distance);
self.moving_time = sum_options(self.moving_time, other.moving_time);
self.elevation_gain += other.elevation_gain;
self.elevation_loss += other.elevation_loss;
self.bounds.merge(&other.bounds);
}
}
#[derive(Default, Debug)]
@@ -251,24 +283,31 @@ pub struct TrackpointStatistics {
impl TrackpointStatistics {
fn from_partial_stats(stats: &GPXStatistics) -> Self {
Self {
total_distance: stats.total_distance,
moving_distance: stats.moving_distance,
total_time: stats.start_time.zip(stats.end_time).map(|(t1, t2)| t2 - t1),
moving_time: stats.moving_time,
total_distance: stats.global.total_distance,
moving_distance: stats.global.moving_distance,
total_time: stats.total_time(),
moving_time: stats.global.moving_time,
// stats below are computed later
speed: None,
elevation_gain: stats.elevation_gain,
elevation_loss: stats.elevation_loss,
elevation_gain: Default::default(),
elevation_loss: Default::default(),
slope: Default::default(),
slope_segment: Default::default(),
}
}
}
#[derive(Default, Debug)]
pub struct SlopeSegment {
pub slope: f64,
pub distance: f64,
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::Read};
use crate::actions::parse;
use crate::io::parse;
use super::*;