refactoring

This commit is contained in:
vcoppe committed 2026-10-03 13:56:16 +02:00
1 parent 17533586ef
commit 888f41e979
49 files changed
+120 -66

No files matched your search

-106
View File
@@ -1,106 +0,0 @@
use wasm_bindgen::prelude::*;
use crate::{FileId, LngLatBounds};
/// A user action, decoded from the WASM boundary.
///
/// Bulk data (coordinates, file bytes) is borrowed straight from the wasm-bindgen argument
/// buffers, so a command is built without copying; the engine only copies what it keeps.
pub enum Command<'a> {
// File commands
New {
name: &'a str,
},
Load {
data: &'a [u8],
},
Duplicate,
Delete,
DeleteAll,
// Edit commands
Metadata {
name: &'a str,
desc: &'a str,
},
Style {
color: Option<&'a str>,
opacity: Option<f64>,
width: Option<f64>,
},
NewTrack,
NewTrackSegment,
// Tools
Reverse,
Append {
lng: &'a [f64],
lat: &'a [f64],
ele: &'a [f64],
},
Replace {
start: u32,
end: u32,
lng: &'a [f64],
lat: &'a [f64],
ele: &'a [f64],
},
NewWaypoint {
lng: f64,
lat: f64,
ele: f64,
name: &'a str,
desc: &'a str,
icon: &'a str,
link: &'a str,
},
MoveWaypoint {
lng: f64,
lat: f64,
ele: f64,
},
Crop {
start: u32,
end: u32,
},
Split {
at: u32,
},
Time,
Merge {
type_: MergeType,
},
Extract,
Elevation {
ele: &'a [f64],
},
Clean {
bounds: LngLatBounds,
type_: CleanType,
trkpt: bool,
wpt: bool,
},
// Undo-redo
Undo,
Redo,
// Selection
Select {
file_ids: Vec<FileId>,
},
AddSelect {
file_ids: Vec<FileId>,
},
SelectAll,
}
#[wasm_bindgen]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeType {
Connect,
Group,
}
#[wasm_bindgen]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CleanType {
Inside,
Outside,
}
-3
View File
@@ -1,3 +0,0 @@
mod command;
pub use command::*;
-5
View File
@@ -1,5 +0,0 @@
mod statistics_buffer;
mod statistics_cache;
pub use statistics_buffer::*;
pub use statistics_cache::*;
@@ -1,61 +0,0 @@
use crate::{GlobalStatistics, Statistics, sum_options};
#[derive(Debug, Default)]
pub struct StatisticsBuffer {
pub total_distance: Vec<f64>,
pub moving_distance: Vec<f64>,
pub total_time: Vec<i32>,
pub moving_time: Vec<i32>,
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 StatisticsBuffer {
pub fn update(&mut self, stats: &[&Statistics]) {
self.total_distance.clear();
self.moving_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 {
for trkpt_stats in stats.local.iter() {
self.total_distance
.push(cumul_stats.total_distance + trkpt_stats.total_distance);
self.moving_distance.push(
sum_options(cumul_stats.moving_distance, trkpt_stats.moving_distance)
.unwrap_or_default(),
);
self.total_time.push(
sum_options(cumul_stats.total_time(), trkpt_stats.total_time)
.unwrap_or_default(),
);
self.moving_time.push(
sum_options(cumul_stats.moving_time, trkpt_stats.moving_time)
.unwrap_or_default(),
);
self.speed.push(trkpt_stats.speed.unwrap_or_default());
self.elevation_gain
.push(cumul_stats.elevation_gain + trkpt_stats.elevation_gain);
self.elevation_loss
.push(cumul_stats.elevation_loss + trkpt_stats.elevation_loss);
self.slope.push(trkpt_stats.slope);
self.slope_segment_slope
.push(trkpt_stats.slope_segment.slope);
self.slope_segment_distance
.push(trkpt_stats.slope_segment.distance);
}
cumul_stats.merge(&stats.global);
}
}
}
@@ -1,115 +0,0 @@
use std::{
collections::{HashMap, HashSet, hash_map::Entry},
rc::Rc,
};
use crate::{File, Selection, StackEntry, Statistics, Track, TrackSegment, TrackSegmentRevisionId};
#[derive(Debug, Default)]
pub struct StatisticsCache {
map: HashMap<TrackSegmentRevisionId, Statistics>,
}
impl StatisticsCache {
pub fn update(&mut self, state: Option<&StackEntry>) {
let mut stale: HashSet<TrackSegmentRevisionId> = self.map.keys().copied().collect();
if let Some(state) = state {
for file in state.values() {
for trk in file.trk.iter() {
for trkseg in trk.trkseg.iter() {
match self.map.entry(trkseg.rev_id) {
Entry::Occupied(_) => {
stale.remove(&trkseg.rev_id);
}
Entry::Vacant(e) => {
e.insert(Statistics::compute(trkseg));
}
}
}
}
}
}
for id in stale.iter() {
self.map.remove(id);
}
}
pub fn get<'a>(
&'a self,
state: Option<&StackEntry>,
selection: &Selection,
) -> Vec<&'a Statistics> {
let mut stats = vec![];
if let Some(state) = state {
match selection {
Selection::Empty => (),
Selection::File { file_ids } => {
for id in file_ids.iter() {
if let Some(file) = state.get(id) {
self.add_file_stats(file, &mut stats);
}
}
}
Selection::Track { file_id, trk_ids } => {
if let Some(file) = state.get(file_id) {
for trk in file.trk.iter() {
if trk_ids.contains(&trk.id) {
self.add_track_stats(trk, &mut stats);
}
}
}
}
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids,
} => {
if let Some(file) = state.get(file_id) {
for trk in file.trk.iter() {
if *trk_id == trk.id {
for trkseg in trk.trkseg.iter() {
if trkseg_ids.contains(&trkseg.id) {
self.add_segment_stats(trkseg, &mut stats);
}
}
break;
}
}
}
}
Selection::Waypoints { file_id } => {
if let Some(file) = state.get(file_id) {
self.add_file_stats(file, &mut stats);
}
}
Selection::Waypoint {
file_id,
wpt_ids: _,
} => {
if let Some(file) = state.get(file_id) {
self.add_file_stats(file, &mut stats);
}
}
}
}
stats
}
fn add_file_stats<'a>(&'a self, file: &Rc<File>, stats: &mut Vec<&'a Statistics>) {
for trk in file.trk.iter() {
self.add_track_stats(trk, stats);
}
}
fn add_track_stats<'a>(&'a self, trk: &Track, stats: &mut Vec<&'a Statistics>) {
for trkseg in trk.trkseg.iter() {
self.add_segment_stats(trkseg, stats);
}
}
fn add_segment_stats<'a>(&'a self, trkseg: &TrackSegment, stats: &mut Vec<&'a Statistics>) {
if let Some(s) = self.map.get(&trkseg.rev_id) {
stats.push(s);
}
}
}
-163
View File
@@ -1,163 +0,0 @@
// TODO license in every file
use std::rc::Rc;
use js_sys::{Float64Array, Function, Int32Array};
use uuid::Uuid;
use crate::{
Command, File, FileId, Selection, Stack, StackEntry, StatisticsBuffer, StatisticsCache, parse,
};
extern crate console_error_panic_hook;
#[derive(Debug, Default)]
pub struct Engine {
stack: Stack,
selection: Selection,
statistics_cache: StatisticsCache,
statistics_buffer: StatisticsBuffer,
}
impl Engine {
pub fn total_distance(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.total_distance) }
}
pub fn moving_distance(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.moving_distance) }
}
pub fn total_time(&self) -> Int32Array {
unsafe { Int32Array::view(&self.statistics_buffer.total_time) }
}
pub fn moving_time(&self) -> Int32Array {
unsafe { Int32Array::view(&self.statistics_buffer.moving_time) }
}
pub fn speed(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.speed) }
}
pub fn elevation_gain(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.elevation_gain) }
}
pub fn elevation_loss(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.elevation_loss) }
}
pub fn slope(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.slope) }
}
pub fn slope_segment_slope(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.slope_segment_slope) }
}
pub fn slope_segment_distance(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.slope_segment_distance) }
}
pub fn create_file(&mut self, name: &str) -> bool {
self.event_loop(|entry| {
let mut file = File::default();
file.info.name = name.to_string();
entry.insert(file.id, Rc::new(file));
true
})
}
pub fn load_file(&mut self, data: &[u8]) -> bool {
self.event_loop(|entry| {
if let Ok(file) = parse(data) {
entry.insert(file.id, Rc::new(file));
true
} else {
false
}
})
}
pub fn delete_file(&mut self, id: &[u8]) -> bool {
self.event_loop(|entry| {
if let Ok(id) = Uuid::from_slice(id) {
let id = FileId(id);
entry.remove(&id).is_some()
} else {
false
}
})
}
/// Single entry point for every user action coming from the frontend.
pub fn execute(&mut self, command: Command) -> bool {
match command {
Command::New { name } => self.create_file(name),
Command::Load { data } => self.load_file(data),
Command::Delete
| Command::DeleteAll
| Command::Duplicate
| Command::Metadata { .. }
| Command::Style { .. }
| Command::NewTrack
| Command::NewTrackSegment
| Command::Reverse
| Command::Append { .. }
| Command::Replace { .. }
| Command::NewWaypoint { .. }
| Command::MoveWaypoint { .. }
| Command::Crop { .. }
| Command::Split { .. }
| Command::Time
| Command::Merge { .. }
| Command::Extract
| Command::Elevation { .. }
| Command::Clean { .. }
| Command::Undo
| Command::Redo
| Command::Select { .. }
| Command::AddSelect { .. }
| Command::SelectAll => todo!("command not implemented yet"),
}
}
fn event_loop<F>(&mut self, f: F) -> bool
where
F: FnOnce(&mut StackEntry) -> bool,
{
if let Some(diff) = self.stack.create_and_push_next(f) {
self.selection.select(diff.added[0]);
self.statistics_cache.update(self.stack.current());
self.statistics_buffer.update(
&self
.statistics_cache
.get(self.stack.current(), &self.selection),
);
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::Read};
use super::*;
#[test]
fn test_load_file() {
let mut engine = Engine::default();
let mut f = File::open("data/simple.gpx").unwrap();
let mut data = String::new();
let _ = f.read_to_string(&mut data);
engine.load_file(data.as_bytes());
assert_eq!(engine.statistics_buffer.total_distance.len(), 80);
}
}
-11
View File
@@ -1,11 +0,0 @@
mod command;
mod derived;
mod engine;
mod selection;
mod state;
pub use command::*;
pub use derived::*;
pub use engine::*;
pub use selection::*;
pub use state::*;
-36
View File
@@ -1,36 +0,0 @@
use std::collections::HashSet;
use crate::{FileId, TrackId, TrackSegmentId, WaypointId};
#[derive(Debug, Default)]
pub enum Selection {
#[default]
Empty,
File {
file_ids: HashSet<FileId>,
},
Track {
file_id: FileId,
trk_ids: HashSet<TrackId>,
},
TrackSegment {
file_id: FileId,
trk_id: TrackId,
trkseg_ids: HashSet<TrackSegmentId>,
},
Waypoints {
file_id: FileId,
},
Waypoint {
file_id: FileId,
wpt_ids: HashSet<WaypointId>,
},
}
impl Selection {
pub fn select(&mut self, id: FileId) {
let mut file_ids = HashSet::default();
file_ids.insert(id);
*self = Selection::File { file_ids };
}
}
-3
View File
@@ -1,3 +0,0 @@
mod stack;
pub use stack::*;
-158
View File
@@ -1,158 +0,0 @@
use std::{
collections::{HashMap, HashSet},
rc::Rc,
};
use crate::{File, FileId};
#[derive(Debug, Default)]
pub struct Stack {
entries: Vec<StackEntry>,
index: Option<usize>,
}
impl Stack {
pub fn current(&self) -> Option<&StackEntry> {
self.index.map_or_default(|i| self.get(i))
}
pub fn create_and_push_next<F>(&mut self, f: F) -> Option<Diff>
where
F: FnOnce(&mut StackEntry) -> bool,
{
self.record_diff(|stack| {
let mut next = stack.current().map_or_default(|c| c.clone());
if f(&mut next) {
stack.push(next);
}
})
}
pub fn undo(&mut self) -> Option<Diff> {
self.record_diff(|stack| {
if let Some(i) = stack.index {
if i == 0 {
stack.index = None;
} else {
stack.index = Some(i - 1);
}
}
})
}
pub fn redo(&mut self) -> Option<Diff> {
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()
}
pub fn can_redo(&self) -> bool {
match self.index {
Some(i) => i + 1 < self.entries.len(),
None => !self.entries.is_empty(),
}
}
fn get(&self, index: usize) -> Option<&StackEntry> {
if index < self.entries.len() {
Some(&self.entries[index])
} else {
None
}
}
fn push(&mut self, entry: StackEntry) {
if let Some(i) = self.index {
if i + 1 < self.entries.len() {
self.entries.truncate(i + 1);
}
}
self.entries.push(entry);
self.index = Some(self.entries.len() - 1);
}
fn record_diff<F>(&mut self, f: F) -> Option<Diff>
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<FileId> = prev.map_or_default(|e| e.keys().copied().collect());
let cur_ids: HashSet<FileId> = 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<FileId, Rc<File>>;
#[derive(Debug, Default)]
pub struct Diff {
pub added: Vec<FileId>,
pub removed: Vec<FileId>,
pub modified: Vec<FileId>,
}
#[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(File::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
}