From 5868595a6ca25905f9067cec116608c065091690 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Thu, 1 Oct 2026 18:42:31 +0200 Subject: [PATCH] progress --- gpx-rs/src/engine.rs | 24 +++-- gpx-rs/src/gpx/chunk.rs | 38 ++++++- gpx-rs/src/gpx/common.rs | 2 +- gpx-rs/src/gpx/file.rs | 6 +- gpx-rs/src/gpx/segment.rs | 2 +- gpx-rs/src/gpx/track.rs | 4 +- gpx-rs/src/state/stack.rs | 162 +++++++++++++++++++--------- gpx-rs/src/statistics/buffer.rs | 5 +- gpx-rs/src/statistics/statistics.rs | 2 + 9 files changed, 175 insertions(+), 70 deletions(-) diff --git a/gpx-rs/src/engine.rs b/gpx-rs/src/engine.rs index 9c1e0a7ca..fd26c09dd 100644 --- a/gpx-rs/src/engine.rs +++ b/gpx-rs/src/engine.rs @@ -1,3 +1,5 @@ +// TODO license in every file + use std::rc::Rc; use js_sys::Function; @@ -9,20 +11,14 @@ use crate::{ io::parse, selection::Selection, state::{Stack, StackEntry}, + statistics::StatisticsBuffer, }; -// Control flow -// - get lock to cancel any other updates -// - update files -// - update separate statistics -// - update selection -// - update selection statistics -// - notify UI through callback (changed files, updated selection, new stats) - #[wasm_bindgen] pub struct Engine { stack: Stack, selection: Selection, + statistics: StatisticsBuffer, callback: Function, } @@ -33,6 +29,7 @@ impl Engine { Self { stack: Default::default(), selection: Default::default(), + statistics: Default::default(), callback, } } @@ -65,4 +62,15 @@ impl Engine { .create_and_push_next(|entry: &mut StackEntry| entry.remove(&id).is_some()); } } + + fn event_loop(&mut self) { + // Control flow + // - get lock to cancel any other updates + // - update files + // - update separate statistics + // - update selection + // - update selection statistics + // self.statistics.update(stats); + // - notify UI through callback (changed files, updated selection, new stats) + } } diff --git a/gpx-rs/src/gpx/chunk.rs b/gpx-rs/src/gpx/chunk.rs index 793974d05..31b8a11d7 100644 --- a/gpx-rs/src/gpx/chunk.rs +++ b/gpx-rs/src/gpx/chunk.rs @@ -1,12 +1,30 @@ +use uuid::Uuid; + use crate::gpx::{Trackpoint, Waypoint}; static MAX_TRKPT_CHUNK_SIZE: usize = 4096; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct TrackpointChunk { + pub id: Uuid, pub trkpt: Vec, } +impl Default for TrackpointChunk { + fn default() -> Self { + Self { + id: Uuid::new_v4(), + trkpt: Default::default(), + } + } +} + +impl PartialEq for TrackpointChunk { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + impl TrackpointChunk { pub fn is_full(&self) -> bool { self.trkpt.len() == MAX_TRKPT_CHUNK_SIZE @@ -15,11 +33,27 @@ impl TrackpointChunk { static MAX_WPT_CHUNK_SIZE: usize = 128; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct WaypointChunk { + pub id: Uuid, pub wpt: Vec, } +impl Default for WaypointChunk { + fn default() -> Self { + Self { + id: Uuid::new_v4(), + wpt: Default::default(), + } + } +} + +impl PartialEq for WaypointChunk { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + impl WaypointChunk { pub fn is_full(&self) -> bool { self.wpt.len() == MAX_WPT_CHUNK_SIZE diff --git a/gpx-rs/src/gpx/common.rs b/gpx-rs/src/gpx/common.rs index c074e3e69..bf6883ed1 100644 --- a/gpx-rs/src/gpx/common.rs +++ b/gpx-rs/src/gpx/common.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct Link { pub href: String, pub text: Option, diff --git a/gpx-rs/src/gpx/file.rs b/gpx-rs/src/gpx/file.rs index 9b277fe11..35459846a 100644 --- a/gpx-rs/src/gpx/file.rs +++ b/gpx-rs/src/gpx/file.rs @@ -4,7 +4,7 @@ use uuid::Uuid; use crate::gpx::{Link, Track, WaypointChunk}; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct GPXFile { pub id: Uuid, pub info: GPXFileInfo, @@ -24,7 +24,7 @@ impl Default for GPXFile { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct GPXFileInfo { pub name: String, pub desc: Option, @@ -33,7 +33,7 @@ pub struct GPXFileInfo { pub time: Option, } -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct Author { pub name: Option, pub email: Option, diff --git a/gpx-rs/src/gpx/segment.rs b/gpx-rs/src/gpx/segment.rs index 06418a6d4..50dd59723 100644 --- a/gpx-rs/src/gpx/segment.rs +++ b/gpx-rs/src/gpx/segment.rs @@ -2,7 +2,7 @@ use std::{ops::Index, rc::Rc}; use crate::gpx::{Trackpoint, TrackpointChunk}; -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct TrackSegment { chunks: Vec>, cumul_length: Vec, diff --git a/gpx-rs/src/gpx/track.rs b/gpx-rs/src/gpx/track.rs index 51e5c79f5..86d1c761c 100644 --- a/gpx-rs/src/gpx/track.rs +++ b/gpx-rs/src/gpx/track.rs @@ -1,12 +1,12 @@ use crate::gpx::{Link, TrackSegment}; -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct Track { pub info: TrackInfo, pub trkseg: Vec, } -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct TrackInfo { pub name: Option, pub cmt: Option, diff --git a/gpx-rs/src/state/stack.rs b/gpx-rs/src/state/stack.rs index 8e7fef1fe..7f0bec870 100644 --- a/gpx-rs/src/state/stack.rs +++ b/gpx-rs/src/state/stack.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, rc::Rc}; +use std::{ + collections::{HashMap, HashSet}, + rc::Rc, +}; use uuid::Uuid; @@ -12,42 +15,47 @@ pub struct Stack { impl Stack { pub fn current(&self) -> Option<&StackEntry> { - match self.index { - Some(i) => Some(&self.entries[i]), - None => None, - } + self.index.map_or_default(|i| self.get(i)) } - pub fn create_and_push_next(&mut self, f: F) + pub fn create_and_push_next(&mut self, f: F) -> Option where F: FnOnce(&mut StackEntry) -> bool, { - let mut next = self.current().map_or_default(|c| c.clone()); - if f(&mut next) { - self.push(next); - } + self.record_diff(|stack| { + let mut next = stack.current().map_or_default(|c| c.clone()); + if f(&mut next) { + stack.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 undo(&mut self) -> Option { + self.record_diff(|stack| { + if let Some(i) = stack.index { + if i == 0 { + stack.index = None; + } else { + stack.index = Some(i - 1); + } + } + }) + } - // 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 redo(&mut self) -> Option { + self.record_diff(|stack| match stack.index { + Some(i) => { + if i + 1 < stack.entries.len() { + stack.index = Some(i + 1); + } + } + None => { + if !stack.entries.is_empty() { + stack.index = Some(0); + } + } + }) + } pub fn can_undo(&self) -> bool { self.index.is_some() @@ -60,28 +68,11 @@ impl Stack { } } - pub fn undo(&mut self) { - if let Some(i) = self.index { - if i == 0 { - self.index = None; - } else { - self.index = Some(i - 1); - } - } - } - - pub fn redo(&mut self) { - match self.index { - Some(i) => { - if i + 1 < self.entries.len() { - self.index = Some(i + 1); - } - } - None => { - if !self.entries.is_empty() { - self.index = Some(0); - } - } + fn get(&self, index: usize) -> Option<&StackEntry> { + if index < self.entries.len() { + Some(&self.entries[index]) + } else { + None } } @@ -95,6 +86,75 @@ impl Stack { self.entries.push(entry); self.index = Some(self.entries.len() - 1); } + + fn record_diff(&mut self, f: F) -> Option + where + F: FnOnce(&mut Self), + { + let prev = self.index; + f(self); + let cur = self.index; + if prev == cur { + return None; + } + let prev = prev.map_or_default(|i| self.get(i)); + let cur = cur.map_or_default(|i| self.get(i)); + let prev_ids: HashSet = prev.map_or_default(|e| e.keys().copied().collect()); + let cur_ids: HashSet = cur.map_or_default(|e| e.keys().copied().collect()); + let mut modified = vec![]; + for id in prev_ids.intersection(&cur_ids) { + let before = prev.map_or_default(|e| e.get(id)); + let after = cur.map_or_default(|e| e.get(id)); + if before != after { + modified.push(*id); + } + } + Some(Diff { + added: cur_ids.difference(&prev_ids).copied().collect(), + removed: prev_ids.difference(&cur_ids).copied().collect(), + modified, + }) + } } pub type StackEntry = HashMap>; + +#[derive(Debug, Default)] +pub struct Diff { + pub added: Vec, + pub removed: Vec, + pub modified: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_file() { + let mut stack = Stack::default(); + + assert!(!stack.can_undo()); + assert!(!stack.can_redo()); + assert!(stack.current().is_none()); + + let diff = stack.create_and_push_next(|e| { + let file = Rc::new(GPXFile::default()); + e.insert(file.id, file); + true + }); + + assert!(stack.can_undo()); + assert!(!stack.can_redo()); + assert!(stack.current().is_some()); + + assert!(diff.is_some()); + + let diff = diff.unwrap(); + assert_eq!(diff.added.len(), 1); + assert!(diff.removed.is_empty()); + assert!(diff.modified.is_empty()); + } + + // TODO more tests +} diff --git a/gpx-rs/src/statistics/buffer.rs b/gpx-rs/src/statistics/buffer.rs index 74768be37..89057d1f3 100644 --- a/gpx-rs/src/statistics/buffer.rs +++ b/gpx-rs/src/statistics/buffer.rs @@ -1,6 +1,7 @@ use crate::statistics::{GPXStatistics, GlobalStatistics, sum_options}; -pub struct Buffer { +#[derive(Debug, Default)] +pub struct StatisticsBuffer { pub total_distance: Vec, pub moving_distance: Vec, pub total_time: Vec, @@ -13,7 +14,7 @@ pub struct Buffer { pub slope_segment_distance: Vec, } -impl Buffer { +impl StatisticsBuffer { pub fn update(&mut self, stats: &[&GPXStatistics]) { self.total_distance.clear(); self.moving_distance.clear(); diff --git a/gpx-rs/src/statistics/statistics.rs b/gpx-rs/src/statistics/statistics.rs index 1bac18b02..7bf6e1454 100644 --- a/gpx-rs/src/statistics/statistics.rs +++ b/gpx-rs/src/statistics/statistics.rs @@ -322,4 +322,6 @@ mod tests { assert!((speed - 20.0).abs() < 0.1); } } + + // TODO more tests }