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