From 497ac120ec1a3e40700039fcf1a4268c2cff9ccf Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 23 Sep 2026 19:53:39 +0200 Subject: [PATCH] progress --- gpx-rs/Cargo.lock | 29 ++ gpx-rs/Cargo.toml | 1 + gpx-rs/src/actions/parse.rs | 108 +++----- gpx-rs/src/algorithms/simplify.rs | 89 +++--- gpx-rs/src/algorithms/smooth.rs | 53 ++-- gpx-rs/src/controller.rs | 48 +++- gpx-rs/src/gpx/chunk.rs | 27 ++ gpx-rs/src/gpx/file.rs | 16 +- gpx-rs/src/gpx/mod.rs | 2 + gpx-rs/src/gpx/segment.rs | 255 +++++++++++++++--- gpx-rs/src/gpx/trackpoint.rs | 8 +- gpx-rs/src/gpx/waypoint.rs | 13 - gpx-rs/src/stack.rs | 63 +++-- gpx-rs/src/statistics.rs | 158 ++++++----- website/src/lib/controller.ts | 10 + .../src/routes/[[language]]/app/+page.svelte | 8 - 16 files changed, 604 insertions(+), 284 deletions(-) create mode 100644 gpx-rs/src/gpx/chunk.rs create mode 100644 website/src/lib/controller.ts diff --git a/gpx-rs/Cargo.lock b/gpx-rs/Cargo.lock index 05eaed167..3f49f0782 100644 --- a/gpx-rs/Cargo.lock +++ b/gpx-rs/Cargo.lock @@ -88,12 +88,24 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "gpx-rs" version = "0.1.0" dependencies = [ "chrono", "quick-xml", + "uuid", "wasm-bindgen", ] @@ -198,6 +210,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rustversion" version = "1.0.23" @@ -233,6 +251,17 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.126" diff --git a/gpx-rs/Cargo.toml b/gpx-rs/Cargo.toml index 2e7a55fe1..6d290ddd2 100644 --- a/gpx-rs/Cargo.toml +++ b/gpx-rs/Cargo.toml @@ -9,4 +9,5 @@ crate-type = ["cdylib"] [dependencies] chrono = "0.4.45" quick-xml = "0.42.0" +uuid = {version = "1.26.1", features = ["v4", "js"]} wasm-bindgen = "0.2" diff --git a/gpx-rs/src/actions/parse.rs b/gpx-rs/src/actions/parse.rs index 1b9d4084d..ebbb40a71 100644 --- a/gpx-rs/src/actions/parse.rs +++ b/gpx-rs/src/actions/parse.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use crate::gpx::{ - Author, GPXFile, Link, LngLat, Track, TrackPoint, TrackPointChunk, TrackSegment, Waypoint, + Author, GPXFile, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointChunk, Waypoint, WaypointChunk, }; use chrono::DateTime; @@ -21,7 +21,7 @@ enum GPXElement { TEXT, TRACK(Track), SEGMENT(TrackSegment), - TRACKPOINT(TrackPoint), + TRACKPOINT(Trackpoint), WAYPOINT(Waypoint), ELEVATION, TIME, @@ -55,7 +55,7 @@ pub fn parse(data: &[u8]) -> Result { let mut buf = vec![]; let mut gpx = GPXFile::default(); let mut stack: Vec = vec![]; - let mut trkpt_chunk = TrackPointChunk::default(); + let mut trkpt_chunk = TrackpointChunk::default(); let mut wpt_chunk = WaypointChunk::default(); loop { match reader.read_event_into(&mut buf) { @@ -83,7 +83,7 @@ pub fn parse(data: &[u8]) -> Result { stack.push(GPXElement::SEGMENT(TrackSegment::default())); } "trkpt" => { - let mut trkpt = TrackPoint::default(); + let mut trkpt = Trackpoint::default(); trkpt.coordinates = parse_coordinates(e.attributes()); stack.push(GPXElement::TRACKPOINT(trkpt)); } @@ -104,7 +104,7 @@ pub fn parse(data: &[u8]) -> Result { 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), - _ => println!("{:?}", e), + _ => (), }, Ok(Event::End(e)) => match e.name().as_ref() { "gpx" => { @@ -146,8 +146,8 @@ pub fn parse(data: &[u8]) -> Result { 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.chunks.push(Rc::new(trkpt_chunk)); - trkpt_chunk = TrackPointChunk::default(); + trkseg.push(trkpt_chunk); + trkpt_chunk = TrackpointChunk::default(); } trk.trkseg.push(trkseg); } @@ -158,8 +158,8 @@ pub fn parse(data: &[u8]) -> Result { if let Some(GPXElement::SEGMENT(trkseg)) = stack.last_mut() { trkpt_chunk.trkpt.push(trkpt); if trkpt_chunk.is_full() { - trkseg.chunks.push(Rc::new(trkpt_chunk)); - trkpt_chunk = TrackPointChunk::default(); + trkseg.push(trkpt_chunk); + trkpt_chunk = TrackpointChunk::default(); } } } @@ -383,10 +383,8 @@ mod tests { assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 80); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 80); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.790867); assert_eq!(trkpt.coordinates.lng, 4.404968); assert_eq!(trkpt.ele, 109.0); @@ -406,10 +404,8 @@ mod tests { assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 49); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 49); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.790867); assert_eq!(trkpt.coordinates.lng, 4.404968); assert_eq!(trkpt.ele, 109.0); @@ -420,10 +416,8 @@ mod tests { assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 28); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 28); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.782212); assert_eq!(trkpt.coordinates.lng, 4.406377); assert_eq!(trkpt.ele, 115.5); @@ -441,19 +435,15 @@ mod tests { assert_eq!(trk.trkseg.len(), 2); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 49); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 49); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.790867); assert_eq!(trkpt.coordinates.lng, 4.404968); assert_eq!(trkpt.ele, 109.0); let trkseg = &trk.trkseg[1]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 28); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 28); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.782212); assert_eq!(trkpt.coordinates.lng, 4.406377); assert_eq!(trkpt.ele, 115.5); @@ -471,19 +461,15 @@ mod tests { assert_eq!(trk.trkseg.len(), 2); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 16); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 16); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.790867); assert_eq!(trkpt.coordinates.lng, 4.404968); assert_eq!(trkpt.ele, 109.0); let trkseg = &trk.trkseg[1]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 34); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 34); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.78727108169855); assert_eq!(trkpt.coordinates.lng, 4.406133681127736); assert_eq!(trkpt.ele, 115.0); @@ -492,19 +478,15 @@ mod tests { assert_eq!(trk.trkseg.len(), 2); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 19); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 19); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.782212); assert_eq!(trkpt.coordinates.lng, 4.406377); assert_eq!(trkpt.ele, 115.5); let trkseg = &trk.trkseg[1]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert_eq!(chunk.trkpt.len(), 10); - let trkpt = &chunk.trkpt[0]; + assert_eq!(trkseg.len(), 10); + let trkpt = &trkseg[0]; assert_eq!(trkpt.coordinates.lat, 50.77906316558724); assert_eq!(trkpt.coordinates.lng, 4.412547477922485); assert_eq!(trkpt.ele, 133.3); @@ -553,10 +535,8 @@ mod tests { let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert!(!chunk.trkpt.is_empty()); - let trkpt = &chunk.trkpt[0]; + assert!(trkseg.len() > 0); + let trkpt = &trkseg[0]; assert!(trkpt.time.is_some_and(|t| t == 1704063600000)); } @@ -571,10 +551,8 @@ mod tests { let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert!(!chunk.trkpt.is_empty()); - let trkpt = &chunk.trkpt[0]; + assert!(trkseg.len() > 0); + let trkpt = &trkseg[0]; assert!(trkpt.hr.is_some_and(|h| h == 150)); } @@ -589,10 +567,8 @@ mod tests { let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert!(!chunk.trkpt.is_empty()); - let trkpt = &chunk.trkpt[0]; + assert!(trkseg.len() > 0); + let trkpt = &trkseg[0]; assert!(trkpt.cad.is_some_and(|c| c == 80)); } @@ -607,10 +583,8 @@ mod tests { let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert!(!chunk.trkpt.is_empty()); - let trkpt = &chunk.trkpt[0]; + assert!(trkseg.len() > 0); + let trkpt = &trkseg[0]; assert!(trkpt.power.is_some_and(|p| p == 200)); } @@ -625,10 +599,8 @@ mod tests { let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert!(!chunk.trkpt.is_empty()); - let trkpt = &chunk.trkpt[0]; + assert!(trkseg.len() > 0); + let trkpt = &trkseg[0]; assert!(trkpt.power.is_some_and(|p| p == 200)); } @@ -643,10 +615,8 @@ mod tests { let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert_eq!(trkseg.chunks.len(), 1); - let chunk = &trkseg.chunks[0]; - assert!(!chunk.trkpt.is_empty()); - let trkpt = &chunk.trkpt[0]; + assert!(trkseg.len() > 0); + let trkpt = &trkseg[0]; assert!(trkpt.atemp.is_some_and(|t| t == 21)); } diff --git a/gpx-rs/src/algorithms/simplify.rs b/gpx-rs/src/algorithms/simplify.rs index 1232dfc40..7951a22a8 100644 --- a/gpx-rs/src/algorithms/simplify.rs +++ b/gpx-rs/src/algorithms/simplify.rs @@ -1,42 +1,65 @@ -use crate::gpx::TrackPoint; +use crate::gpx::{TrackSegment, TrackSegmentIndex}; -pub fn ramer_douglas_peucker(n: usize, distance: &F, epsilon: f64) -> Vec -where - F: Fn(usize, usize, usize) -> f64, -{ - if n <= 2 { - (0..n).collect() - } else { - let mut indices = vec![0]; - ramer_douglas_peucker_helper(0, n - 1, distance, epsilon, &mut indices); - indices.push(n - 1); - indices - } -} - -fn ramer_douglas_peucker_helper( - start: usize, - end: usize, +pub fn ramer_douglas_peucker( + trkseg: &TrackSegment, distance: &F, - epsilon: f64, - indices: &mut Vec, -) where - F: Fn(usize, usize, usize) -> f64, + epsilon: T, +) -> Vec +where + F: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> T, + T: Default + PartialOrd + Copy, { - let mut idx = 0; - let mut max_dist = 0.0; + match trkseg.len() { + 0 => vec![], + 1 => vec![trkseg.first_index().unwrap()], + 2 => vec![trkseg.last_index().unwrap()], + _ => { + let first = trkseg.first_index().unwrap(); + let last = trkseg.last_index().unwrap(); - for i in (start + 1)..end { - let dist = distance(start, end, i); - if dist > max_dist { - idx = i; - max_dist = dist; + let mut indices = vec![first]; + ramer_douglas_peucker_helper(trkseg, first, last, distance, epsilon, &mut indices); + indices.push(last); + indices } } +} - if max_dist > epsilon && idx != 0 { - ramer_douglas_peucker_helper(start, idx, distance, epsilon, indices); - indices.push(idx); - ramer_douglas_peucker_helper(idx, end, distance, epsilon, indices); +fn ramer_douglas_peucker_helper( + trkseg: &TrackSegment, + start: TrackSegmentIndex, + end: TrackSegmentIndex, + distance: &F, + epsilon: T, + indices: &mut Vec, +) where + F: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> T, + T: Default + PartialOrd + Copy, +{ + let mut max_idx = None; + let mut max_dist = T::default(); + + let mut cur = trkseg.next_index(Some(start)); + + while let Some(idx) = cur { + if idx == end { + break; + } + + let dist = distance(start, end, idx); + if dist > max_dist { + max_idx = Some(idx); + max_dist = dist; + } + + cur = trkseg.next_index(cur); + } + + if let Some(idx) = max_idx { + if max_dist > epsilon { + ramer_douglas_peucker_helper(trkseg, start, idx, distance, epsilon, indices); + indices.push(idx); + ramer_douglas_peucker_helper(trkseg, idx, end, distance, epsilon, indices); + } } } diff --git a/gpx-rs/src/algorithms/smooth.rs b/gpx-rs/src/algorithms/smooth.rs index 40d824692..636e6948b 100644 --- a/gpx-rs/src/algorithms/smooth.rs +++ b/gpx-rs/src/algorithms/smooth.rs @@ -1,39 +1,50 @@ #[macro_export] macro_rules! for_each_window { ( + $trkseg:expr, $left:expr, $right:expr, $window:expr, |$a:ident, $b:ident| $distance:expr, |$i:ident, $l:ident, $r:ident| $body:block, ) => {{ - let mut start = $left; - - for $i in $left..$right { - while start + 1 < $i && { + let mut start = $left.unwrap_or_default(); + let mut cur = $left; + while let Some(idx) = cur { + // advance left if needed + while start != idx && { let $a = start; - let $b = $i; + let $b = idx; $distance - } > $window - { - start += 1; + } > $window { + if let Some(next) = $trkseg.next_index(Some(start)) { + if next == idx { + break; + } + start = next; + } else { + break; + } } - - let mut end = $right.min($i + 2); - - while end < $right && { - let $a = $i; - let $b = end; - $distance - } <= $window - { - end += 1; + // advance right if needed + let mut end = idx; + while let Some(next) = $trkseg.next_index(Some(end)) { + if { + let $a = idx; + let $b = end; + $distance + } > $window { + break; + } + end = next; } - + // apply body + let $i = idx; let $l = start; - let $r = end - 1; - + let $r = end; $body + // go next + cur = $trkseg.next_index(cur); } }}; } diff --git a/gpx-rs/src/controller.rs b/gpx-rs/src/controller.rs index 3c2783ddc..e0e6279f3 100644 --- a/gpx-rs/src/controller.rs +++ b/gpx-rs/src/controller.rs @@ -1,8 +1,21 @@ use std::rc::Rc; +use uuid::Uuid; use wasm_bindgen::prelude::*; -use crate::stack::Stack; +use crate::{ + actions::parse, + gpx::GPXFile, + stack::{Stack, StackEntry}, +}; + +// Control flow +// - get lock to cancel any other updates +// - update files +// - update separate statistics +// - update selection +// - update selection statistics +// - notify UI (changed files, updated selection, new stats) #[wasm_bindgen] pub struct Controller { @@ -18,10 +31,31 @@ impl Controller { } } - // #[wasm_bindgen] - // pub fn create_file(&mut self, name: &str) { - // let id = self.stack.get_new_file_id(); - // let file = Rc::new(GPXFile::new(id, name)); - // self.stack.update(&[file]); - // } + #[wasm_bindgen] + pub fn create_file(&mut self, name: &str) { + let mut file = GPXFile::default(); + file.info.name = name.to_string(); + self.stack.create_and_push_next(|entry: &mut StackEntry| { + entry.insert(file.id, Rc::new(file)); + true + }); + } + + #[wasm_bindgen] + pub fn load_file(&mut self, data: &[u8]) { + if let Ok(file) = parse(data) { + self.stack.create_and_push_next(|entry: &mut StackEntry| { + entry.insert(file.id, Rc::new(file)); + true + }); + } + } + + #[wasm_bindgen] + pub fn delete_file(&mut self, id: &[u8]) { + if let Ok(id) = Uuid::from_slice(id) { + self.stack + .create_and_push_next(|entry: &mut StackEntry| entry.remove(&id).is_some()); + } + } } diff --git a/gpx-rs/src/gpx/chunk.rs b/gpx-rs/src/gpx/chunk.rs new file mode 100644 index 000000000..793974d05 --- /dev/null +++ b/gpx-rs/src/gpx/chunk.rs @@ -0,0 +1,27 @@ +use crate::gpx::{Trackpoint, Waypoint}; + +static MAX_TRKPT_CHUNK_SIZE: usize = 4096; + +#[derive(Debug, Default)] +pub struct TrackpointChunk { + pub trkpt: Vec, +} + +impl TrackpointChunk { + pub fn is_full(&self) -> bool { + self.trkpt.len() == MAX_TRKPT_CHUNK_SIZE + } +} + +static MAX_WPT_CHUNK_SIZE: usize = 128; + +#[derive(Debug, Default)] +pub struct WaypointChunk { + pub wpt: Vec, +} + +impl WaypointChunk { + pub fn is_full(&self) -> bool { + self.wpt.len() == MAX_WPT_CHUNK_SIZE + } +} diff --git a/gpx-rs/src/gpx/file.rs b/gpx-rs/src/gpx/file.rs index 092cccf55..9b277fe11 100644 --- a/gpx-rs/src/gpx/file.rs +++ b/gpx-rs/src/gpx/file.rs @@ -1,15 +1,29 @@ use std::rc::Rc; +use uuid::Uuid; + use crate::gpx::{Link, Track, WaypointChunk}; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct GPXFile { + pub id: Uuid, pub info: GPXFileInfo, pub trk: Vec, pub wpt: Vec>, // TODO routes } +impl Default for GPXFile { + fn default() -> Self { + Self { + id: Uuid::new_v4(), + info: Default::default(), + trk: Default::default(), + wpt: Default::default(), + } + } +} + #[derive(Debug, Default)] pub struct GPXFileInfo { pub name: String, diff --git a/gpx-rs/src/gpx/mod.rs b/gpx-rs/src/gpx/mod.rs index 4e44d84b6..e8b58a9f1 100644 --- a/gpx-rs/src/gpx/mod.rs +++ b/gpx-rs/src/gpx/mod.rs @@ -1,3 +1,4 @@ +mod chunk; mod common; mod file; mod segment; @@ -5,6 +6,7 @@ mod track; mod trackpoint; mod waypoint; +pub use chunk::*; pub use common::*; pub use file::*; pub use segment::*; diff --git a/gpx-rs/src/gpx/segment.rs b/gpx-rs/src/gpx/segment.rs index 2e9272bec..06418a6d4 100644 --- a/gpx-rs/src/gpx/segment.rs +++ b/gpx-rs/src/gpx/segment.rs @@ -1,60 +1,253 @@ -use std::rc::Rc; +use std::{ops::Index, rc::Rc}; -use crate::gpx::TrackPoint; +use crate::gpx::{Trackpoint, TrackpointChunk}; #[derive(Debug, Default)] pub struct TrackSegment { - pub chunks: Vec>, + chunks: Vec>, + cumul_length: Vec, } impl TrackSegment { - pub fn iter(&self) -> TrackSegmentIterator { + pub fn push(&mut self, chunk: TrackpointChunk) { + if chunk.trkpt.is_empty() { + return; + } + self.cumul_length + .push(self.cumul_length.last().copied().unwrap_or_default() + chunk.trkpt.len()); + self.chunks.push(Rc::new(chunk)); + } + + pub fn len(&self) -> usize { + self.cumul_length.last().copied().unwrap_or_default() + } + + pub fn iter(&self) -> TrackSegmentIterator<'_> { TrackSegmentIterator::new(self) } + + pub fn first_index(&self) -> Option { + self.next_index(None) + } + + pub fn last_index(&self) -> Option { + self.prev_index(None) + } + + pub fn next_index(&self, cur: Option) -> Option { + let mut next = cur.map_or_default(|idx| TrackSegmentIndex { + chunk: idx.chunk, + pos: idx.pos + 1, + }); + loop { + if next.chunk >= self.chunks.len() { + return None; + } + if next.pos == self.chunks[next.chunk].trkpt.len() { + next.chunk += 1; + next.pos = 0; + } else { + return Some(next); + } + } + } + + pub fn prev_index(&self, cur: Option) -> Option { + let mut prev = cur.unwrap_or(TrackSegmentIndex { + chunk: self.chunks.len(), + pos: 0, + }); + loop { + if prev.pos == 0 { + if prev.chunk == 0 { + return None; + } + prev.chunk -= 1; + prev.pos = self.chunks[prev.chunk].trkpt.len(); + } else { + prev.pos -= 1; + return Some(prev); + } + } + } + + pub fn to_flat_index(&self, idx: TrackSegmentIndex) -> usize { + idx.pos + + if idx.chunk > 0 { + self.cumul_length[idx.chunk - 1] + } else { + 0 + } + } + + fn locate(&self, idx: usize) -> Option { + let chunk = self.cumul_length.partition_point(|l| idx >= *l); + if chunk >= self.chunks.len() { + return None; + } + let pos = if chunk > 0 { + idx - self.cumul_length[chunk - 1] + } else { + idx + }; + if pos >= self.chunks[chunk].trkpt.len() { + None + } else { + Some(TrackSegmentIndex { chunk, pos }) + } + } } +impl Index for TrackSegment { + type Output = Trackpoint; + + fn index(&self, idx: TrackSegmentIndex) -> &Self::Output { + &self.chunks[idx.chunk].trkpt[idx.pos] + } +} + +impl Index for TrackSegment { + type Output = Trackpoint; + + fn index(&self, idx: usize) -> &Self::Output { + &self[self.locate(idx).unwrap()] + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)] +pub struct TrackSegmentIndex { + pub chunk: usize, + pub pos: usize, +} + +#[derive(Debug, Clone)] pub struct TrackSegmentIterator<'a> { trkseg: &'a TrackSegment, - chunk_idx: usize, - trkpt_idx: usize, + idx: Option, } impl<'a> TrackSegmentIterator<'a> { pub fn new(trkseg: &'a TrackSegment) -> Self { Self { trkseg, - chunk_idx: 0, - trkpt_idx: 0, + idx: Default::default(), } } } impl<'a> Iterator for TrackSegmentIterator<'a> { - type Item = &'a TrackPoint; + type Item = &'a Trackpoint; fn next(&mut self) -> Option { - if self.chunk_idx >= self.trkseg.chunks.len() { - None - } else if self.trkpt_idx >= self.trkseg.chunks[self.chunk_idx].trkpt.len() { - self.chunk_idx += 1; - self.trkpt_idx = 0; - self.next() - } else { - self.trkpt_idx += 1; - Some(&self.trkseg.chunks[self.chunk_idx].trkpt[self.trkpt_idx - 1]) + self.idx = self.trkseg.next_index(self.idx); + self.idx.map(|idx| &self.trkseg[idx]) + } + + fn nth(&mut self, n: usize) -> Option { + let idx = self + .idx + .map_or_default(|idx| self.trkseg.to_flat_index(idx)) + + n; + self.idx = self.trkseg.locate(idx); + self.idx.map(|idx| &self.trkseg[idx]) + } +} + +#[cfg(test)] +mod tests { + use crate::gpx::TrackpointChunk; + + use super::*; + + fn create_track_segment(nb_chunks: usize) -> TrackSegment { + let mut trkseg = TrackSegment::default(); + let mut count = 0; + for n in 1..=nb_chunks { + let mut chunk = TrackpointChunk::default(); + for _ in 0..n { + let mut trkpt = Trackpoint::default(); + trkpt.ele = count as f64; + chunk.trkpt.push(trkpt); + count += 1; + } + trkseg.push(chunk); + } + trkseg + } + + #[test] + fn test_len() { + let nb_chunks = 10; + let trkseg = create_track_segment(nb_chunks); + assert_eq!(trkseg.len(), nb_chunks * (nb_chunks + 1) / 2); + } + + #[test] + fn test_locate() { + let nb_chunks = 10; + let trkseg = create_track_segment(nb_chunks); + for i in 0..trkseg.len() { + let idx = trkseg.locate(i); + assert!(idx.is_some()); + assert_eq!(trkseg[idx.unwrap()].ele, i as f64); + } + + assert!(trkseg.locate(trkseg.len()).is_none()); + } + + #[test] + fn test_index() { + let nb_chunks = 10; + let trkseg = create_track_segment(nb_chunks); + for i in 0..trkseg.len() { + assert_eq!(trkseg[i].ele, i as f64); + } + } + + #[test] + #[should_panic] + fn test_index_out_of_bounds_1() { + let nb_chunks = 10; + let trkseg = create_track_segment(nb_chunks); + trkseg[trkseg.len()].ele; + } + + #[test] + #[should_panic] + fn test_index_out_of_bounds_2() { + let nb_chunks = 10; + let trkseg = create_track_segment(nb_chunks); + trkseg[TrackSegmentIndex { + chunk: trkseg.chunks.len(), + pos: 0, + }] + .ele; + } + + #[test] + fn test_iter() { + let nb_chunks = 10; + let trkseg = create_track_segment(nb_chunks); + for (i, trkpt) in trkseg.iter().enumerate() { + assert!(std::ptr::eq(&trkseg[i], trkpt)); + } + } + + #[test] + fn test_iter_nth() { + let nb_chunks = 10; + let trkseg = create_track_segment(nb_chunks); + let tenth = trkseg.iter().nth(10); + assert!(tenth.is_some()); + assert!(std::ptr::eq(&trkseg[10], tenth.unwrap())); + } + + #[test] + fn test_iter_skip() { + let nb_chunks = 10; + let trkseg = create_track_segment(nb_chunks); + for (i, trkpt) in trkseg.iter().enumerate().skip(5) { + assert!(std::ptr::eq(&trkseg[i], trkpt)); } } } - -static MAX_CHUNK_SIZE: usize = 4096; - -#[derive(Debug, Default)] -pub struct TrackPointChunk { - pub trkpt: Vec, -} - -impl TrackPointChunk { - pub fn is_full(&self) -> bool { - self.trkpt.len() == MAX_CHUNK_SIZE - } -} diff --git a/gpx-rs/src/gpx/trackpoint.rs b/gpx-rs/src/gpx/trackpoint.rs index cae899d1c..35dac699c 100644 --- a/gpx-rs/src/gpx/trackpoint.rs +++ b/gpx-rs/src/gpx/trackpoint.rs @@ -1,7 +1,7 @@ use crate::gpx::LngLat; #[derive(Debug, Default)] -pub struct TrackPoint { +pub struct Trackpoint { pub coordinates: LngLat, pub ele: f64, pub time: Option, @@ -11,3 +11,9 @@ pub struct TrackPoint { pub power: Option, // TODO OSM data? or store intervals at a higher level? } + +impl Trackpoint { + pub fn time_diff(&self, other: &Trackpoint) -> Option { + self.time.zip(other.time).map(|(t1, t2)| t1 - t2) + } +} diff --git a/gpx-rs/src/gpx/waypoint.rs b/gpx-rs/src/gpx/waypoint.rs index 5531507dd..9a2fc46d3 100644 --- a/gpx-rs/src/gpx/waypoint.rs +++ b/gpx-rs/src/gpx/waypoint.rs @@ -12,16 +12,3 @@ pub struct Waypoint { pub sym: Option, pub type_: Option, } - -static MAX_CHUNK_SIZE: usize = 128; - -#[derive(Debug, Default)] -pub struct WaypointChunk { - pub wpt: Vec, -} - -impl WaypointChunk { - pub fn is_full(&self) -> bool { - self.wpt.len() == MAX_CHUNK_SIZE - } -} diff --git a/gpx-rs/src/stack.rs b/gpx-rs/src/stack.rs index 2b696c02f..8e7fef1fe 100644 --- a/gpx-rs/src/stack.rs +++ b/gpx-rs/src/stack.rs @@ -1,8 +1,8 @@ use std::{collections::HashMap, rc::Rc}; -use crate::gpx::GPXFile; +use uuid::Uuid; -pub type GPXFileId = usize; +use crate::gpx::GPXFile; #[derive(Default)] pub struct Stack { @@ -18,40 +18,37 @@ impl Stack { } } - pub fn get_new_file_id(&self) -> GPXFileId { - if let Some(current) = self.current() { - for id in 0..current.len() { - if !current.contains_key(&id) { - return id; - } - } - current.len() as GPXFileId - } else { - 0 - } - } - - pub fn update(&mut self, files: &[Rc], ids: &[GPXFileId]) { - let mut next = match self.current() { - Some(current) => current.clone(), - None => StackEntry::default(), - }; - for (file, id) in files.iter().zip(ids) { - next.insert(*id, file.clone()); - } - self.push(next); - } - - pub fn delete(&mut self, files: &[GPXFileId]) { - if let Some(current) = self.current() { - let mut next = current.clone(); - for file in files { - next.remove(file); - } + pub fn create_and_push_next(&mut self, f: F) + where + F: FnOnce(&mut StackEntry) -> bool, + { + let mut next = self.current().map_or_default(|c| c.clone()); + if f(&mut next) { self.push(next); } } + // pub fn update(&mut self, files: &[Rc], ids: &[GPXFileId]) { + // let mut next = match self.current() { + // Some(current) => current.clone(), + // None => StackEntry::default(), + // }; + // for (file, id) in files.iter().zip(ids) { + // next.insert(*id, file.clone()); + // } + // self.push(next); + // } + + // pub fn delete(&mut self, files: &[GPXFileId]) { + // if let Some(current) = self.current() { + // let mut next = current.clone(); + // for file in files { + // next.remove(file); + // } + // self.push(next); + // } + // } + pub fn can_undo(&self) -> bool { self.index.is_some() } @@ -100,4 +97,4 @@ impl Stack { } } -pub type StackEntry = HashMap>; +pub type StackEntry = HashMap>; diff --git a/gpx-rs/src/statistics.rs b/gpx-rs/src/statistics.rs index 9d3f72f0e..a63050ffa 100644 --- a/gpx-rs/src/statistics.rs +++ b/gpx-rs/src/statistics.rs @@ -1,7 +1,7 @@ use crate::{ algorithms::ramer_douglas_peucker, for_each_window, - gpx::{LngLat, LngLatBounds, TrackPoint, TrackPointChunk}, + gpx::{LngLat, LngLatBounds, TrackSegment, Trackpoint}, utils::{distance, slope, speed}, }; @@ -33,26 +33,25 @@ impl GPXStatistics { .map(|(d, t)| speed(d, t)) } - pub fn compute(chunk: &TrackPointChunk) -> Self { + pub fn compute(trkseg: &TrackSegment) -> Self { let mut stats = Self::default(); - if chunk.trkpt.is_empty() { + if trkseg.len() == 0 { return stats; } - let mut prev = &chunk.trkpt[0]; - for i in 0..chunk.trkpt.len() { - let cur = &chunk.trkpt[i]; + let mut prev = &trkseg[0]; + for cur in trkseg.iter() { stats.accumulate(prev, cur); prev = cur; } - stats.compute_smoothed_speed(chunk); - stats.compute_smoothed_elevation_gain(chunk); + stats.compute_smoothed_speed(trkseg); + stats.compute_smoothed_elevation_gain(trkseg); stats } - fn accumulate(&mut self, prev: &TrackPoint, cur: &TrackPoint) { + 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); @@ -60,9 +59,9 @@ impl GPXStatistics { .push(TrackpointStatistics::from_partial_stats(&self)); } - fn accumulate_distance_and_time(&mut self, prev: &TrackPoint, cur: &TrackPoint) { + fn accumulate_distance_and_time(&mut self, prev: &Trackpoint, cur: &Trackpoint) { let dist = distance(prev.coordinates, cur.coordinates); - let time = prev.time.zip(cur.time).map(|(t1, t2)| t2 - t1); + let time = cur.time_diff(prev); self.total_distance += dist; @@ -91,42 +90,37 @@ impl GPXStatistics { self.bounds.ne.lat = self.bounds.ne.lat.min(coordinates.lat); } - fn compute_smoothed_speed(&mut self, chunk: &TrackPointChunk) { + fn compute_smoothed_speed(&mut self, trkseg: &TrackSegment) { for_each_window!( - 0, - chunk.trkpt.len(), + trkseg, + trkseg.first_index(), + trkseg.last_index(), Some(10000), - |i, j| { - chunk.trkpt[i] - .time - .zip(chunk.trkpt[j].time) - .map(|(t1, t2)| t2 - t1) - }, + |i, j| trkseg[i].time_diff(&trkseg[j]), |i, left, right| { - self.local[i].speed = - chunk.trkpt[left] - .time - .zip(chunk.trkpt[right].time) - .map(|(t1, t2)| { - speed( - self.local[right].total_distance - self.local[left].total_distance, - t2 - t1, - ) - }); + let i = trkseg.to_flat_index(i); + let left = trkseg.to_flat_index(left); + let right = trkseg.to_flat_index(right); + self.local[i].speed = trkseg[right].time_diff(&trkseg[left]).map(|t| { + speed( + self.local[right].total_distance - self.local[left].total_distance, + t, + ) + }); }, ); } - fn compute_smoothed_elevation_gain(&mut self, chunk: &TrackPointChunk) { + fn compute_smoothed_elevation_gain(&mut self, trkseg: &TrackSegment) { let simplified = ramer_douglas_peucker( - chunk.trkpt.len(), + trkseg, &|i, j, k| { - let x1 = self.local[i].total_distance * 1000.0; - let x2 = self.local[j].total_distance * 1000.0; - let x3 = self.local[k].total_distance * 1000.0; - let y1 = chunk.trkpt[i].ele; - let y2 = chunk.trkpt[j].ele; - let y3 = chunk.trkpt[k].ele; + let x1 = self.local[trkseg.to_flat_index(i)].total_distance * 1000.0; + let x2 = self.local[trkseg.to_flat_index(j)].total_distance * 1000.0; + let x3 = self.local[trkseg.to_flat_index(k)].total_distance * 1000.0; + let y1 = trkseg[i].ele; + let y2 = trkseg[j].ele; + let y3 = trkseg[k].ele; let dist = ((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt(); if dist == 0.0 { @@ -145,28 +139,40 @@ impl GPXStatistics { let mut cumul_ele = 0.0; let mut current_left = start; - let mut current_right = start; - let mut prev_smoothed_ele = chunk.trkpt[start].ele; + let mut current_right = Some(start); + let mut prev_smoothed_ele = trkseg[start].ele; for_each_window!( - start, - end, + trkseg, + Some(start), + Some(end), 0.1, - |i, j| self.local[j].total_distance - self.local[i].total_distance, + |i, j| { + let i = trkseg.to_flat_index(i); + let j = trkseg.to_flat_index(j); + self.local[j].total_distance - self.local[i].total_distance + }, |i, left, right| { - for i in current_left..left { - cumul_ele -= chunk.trkpt[i].ele; + while current_left != left { + cumul_ele -= trkseg[current_left].ele; + current_left = trkseg.next_index(Some(current_left)).unwrap(); } - for i in current_right..=right { - cumul_ele += chunk.trkpt[i].ele; + while let Some(current) = current_right { + if current > right { + break; + } + cumul_ele += trkseg[current].ele; + current_right = trkseg.next_index(current_right); } - current_left = left; - current_right = right + 1; + + let flat_i = trkseg.to_flat_index(i); + let flat_left = trkseg.to_flat_index(left); + let flat_right = trkseg.to_flat_index(right); let smoothed_ele: f64 = if i == start || i == end { - chunk.trkpt[i].ele + trkseg[i].ele } else { - cumul_ele / (right - left + 1) as f64 + cumul_ele / (flat_right - flat_left + 1) as f64 }; let delta = smoothed_ele - prev_smoothed_ele; @@ -177,18 +183,22 @@ impl GPXStatistics { } if i < end || last { - self.local[i].elevation_gain = self.elevation_gain; - self.local[i].elevation_loss = self.elevation_loss; + self.local[flat_i].elevation_gain = self.elevation_gain; + self.local[flat_i].elevation_loss = self.elevation_loss; } prev_smoothed_ele = smoothed_ele; }, ); - let segment_dist = self.local[end].total_distance - self.local[start].total_distance; - let segment_ele = chunk.trkpt[end].ele - chunk.trkpt[start].ele; + let flat_start = trkseg.to_flat_index(start); + let flat_end = trkseg.to_flat_index(end); + + let segment_dist = + self.local[flat_end].total_distance - self.local[flat_start].total_distance; + let segment_ele = trkseg[end].ele - trkseg[start].ele; let segment_slope = slope(segment_ele, segment_dist); - for k in start..(end + last as usize) { + for k in flat_start..(flat_end + last as usize) { self.local[k].slope_segment = SlopeSegment { slope: segment_slope, distance: segment_dist, @@ -197,14 +207,23 @@ impl GPXStatistics { } for_each_window!( - 0, - chunk.trkpt.len(), + trkseg, + trkseg.first_index(), + trkseg.last_index(), 0.05, - |i, j| self.local[j].total_distance - self.local[i].total_distance, + |i, j| { + let i = trkseg.to_flat_index(i); + let j = trkseg.to_flat_index(j); + self.local[j].total_distance - self.local[i].total_distance + }, |i, left, right| { - let dist = self.local[right].total_distance - self.local[left].total_distance; - let ele = chunk.trkpt[right].ele - chunk.trkpt[left].ele; - self.local[i].slope = slope(ele, dist); + let flat_i = trkseg.to_flat_index(i); + let flat_left = trkseg.to_flat_index(left); + let flat_right = trkseg.to_flat_index(right); + let dist = + self.local[flat_right].total_distance - self.local[flat_left].total_distance; + let ele = trkseg[right].ele - trkseg[left].ele; + self.local[flat_i].slope = slope(ele, dist); }, ); } @@ -254,15 +273,20 @@ mod tests { use super::*; #[test] - fn test_parse_simple() { + fn test_compute_smoothed_speed() { 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(); - println!( - "{:?}", - GPXStatistics::compute(&gpx.trk[0].trkseg[0].chunks[0]) - ); + let trkseg = &gpx.trk[0].trkseg[0]; + let stats = GPXStatistics::compute(trkseg); + assert_eq!(stats.local.len(), trkseg.len()); + for trkpt_stats in stats.local.iter() { + assert!(trkpt_stats.speed.is_some()); + let speed = trkpt_stats.speed.unwrap(); + assert_ne!(speed, f64::NAN); + assert!((speed - 20.0).abs() < 0.1); + } } } diff --git a/website/src/lib/controller.ts b/website/src/lib/controller.ts new file mode 100644 index 000000000..2ab254432 --- /dev/null +++ b/website/src/lib/controller.ts @@ -0,0 +1,10 @@ +class Engine { + _engine: Writable; + + constructor() { + this._engine = null; + import('gpx-rs').then((wasm) => { + this._engine = new wasm.Controller(); + }); + } +} diff --git a/website/src/routes/[[language]]/app/+page.svelte b/website/src/routes/[[language]]/app/+page.svelte index 5b5c73deb..6af1ad091 100644 --- a/website/src/routes/[[language]]/app/+page.svelte +++ b/website/src/routes/[[language]]/app/+page.svelte @@ -37,14 +37,6 @@ ); onMount(async () => { - if (browser) { - const wasm = await import('gpx-rs'); - const controller = new wasm.Controller(); - - console.log(controller); - - controller.create_file(); - } settings.connectToDatabase(db); fileStateCollection.connectToDatabase(db).then(() => { let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]');