reorganize

This commit is contained in:
vcoppe committed 2026-10-03 10:03:22 +02:00
1 parent a921d4b9cd
commit dbe894b3a3
32 files changed
+45 -66

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
mod statistics_buffer;
mod statistics_cache;
pub use statistics_buffer::*;
pub use statistics_cache::*;
@@ -0,0 +1,61 @@
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);
}
}
}
@@ -0,0 +1,108 @@
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(s) => {
for id in s.files.iter() {
if let Some(file) = state.get(id) {
self.add_file_stats(file, &mut stats);
}
}
}
Selection::Track(s) => {
if let Some(file) = state.get(&s.file) {
for trk in file.trk.iter() {
if s.trk.contains(&trk.id) {
self.add_track_stats(trk, &mut stats);
}
}
}
}
Selection::TrackSegment(s) => {
if let Some(file) = state.get(&s.file) {
for trk in file.trk.iter() {
if s.trk == trk.id {
for trkseg in trk.trkseg.iter() {
if s.trkseg.contains(&trkseg.id) {
self.add_segment_stats(trkseg, &mut stats);
}
}
break;
}
}
}
}
Selection::Waypoints(s) => {
if let Some(file) = state.get(&s.file) {
self.add_file_stats(file, &mut stats);
}
}
Selection::Waypoint(s) => {
if let Some(file) = state.get(&s.file) {
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);
}
}
}
+145
View File
@@ -0,0 +1,145 @@
// TODO license in every file
use std::rc::Rc;
use js_sys::{Float64Array, Function, Int32Array};
use uuid::Uuid;
use wasm_bindgen::prelude::*;
use crate::{File, FileId, Selection, Stack, StackEntry, StatisticsBuffer, StatisticsCache, parse};
extern crate console_error_panic_hook;
#[derive(Debug, Default)]
#[wasm_bindgen]
pub struct Engine {
stack: Stack,
selection: Selection,
statistics_cache: StatisticsCache,
statistics_buffer: StatisticsBuffer,
callback: Option<Function>,
}
#[wasm_bindgen]
impl Engine {
#[wasm_bindgen(constructor)]
pub fn new(callback: Function) -> Self {
console_error_panic_hook::set_once();
Self {
stack: Default::default(),
selection: Default::default(),
statistics_cache: Default::default(),
statistics_buffer: Default::default(),
callback: Some(callback),
}
}
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) {
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]) {
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]) {
self.event_loop(|entry| {
if let Ok(id) = Uuid::from_slice(id) {
let id = FileId(id);
entry.remove(&id).is_some()
} else {
false
}
});
}
fn event_loop<F>(&mut self, f: F)
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),
);
if let Some(c) = &self.callback {
let _ = c.call0(&JsValue::NULL);
}
}
}
}
#[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);
}
}
+9
View File
@@ -0,0 +1,9 @@
mod derived;
mod engine;
mod selection;
mod state;
pub use derived::*;
pub use engine::*;
pub use selection::*;
pub use state::*;
+51
View File
@@ -0,0 +1,51 @@
use std::collections::HashSet;
use crate::{FileId, TrackId, TrackSegmentId, WaypointId};
#[derive(Debug)]
pub struct FileSelection {
pub files: HashSet<FileId>,
}
#[derive(Debug)]
pub struct TrackSelection {
pub file: FileId,
pub trk: HashSet<TrackId>,
}
#[derive(Debug)]
pub struct TrackSegmentSelection {
pub file: FileId,
pub trk: TrackId,
pub trkseg: HashSet<TrackSegmentId>,
}
#[derive(Debug)]
pub struct WaypointsSelection {
pub file: FileId,
}
#[derive(Debug)]
pub struct WaypointSelection {
pub file: FileId,
pub wpt: HashSet<WaypointId>,
}
#[derive(Debug, Default)]
pub enum Selection {
#[default]
Empty,
File(FileSelection),
Track(TrackSelection),
TrackSegment(TrackSegmentSelection),
Waypoints(WaypointsSelection),
Waypoint(WaypointSelection),
}
impl Selection {
pub fn select(&mut self, id: FileId) {
let mut files = HashSet::default();
files.insert(id);
*self = Selection::File(FileSelection { files });
}
}
+3
View File
@@ -0,0 +1,3 @@
mod stack;
pub use stack::*;
+158
View File
@@ -0,0 +1,158 @@
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
}