mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-17 08:42:28 +00:00
refactoring
This commit is contained in:
1 parent
17533586ef
commit
888f41e979
49 files changed
+120
-66
No files matched your search
@@ -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,
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mod command;
|
||||
|
||||
pub use command::*;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mod stack;
|
||||
|
||||
pub use stack::*;
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user