mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-04 12:58:23 +00:00
progress
This commit is contained in:
@@ -31,7 +31,7 @@ impl TrackpointChunk {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct WaypointChunkId(Uuid);
|
||||
|
||||
impl Default for WaypointChunkId {
|
||||
|
||||
@@ -5,7 +5,7 @@ use uuid::Uuid;
|
||||
use crate::{Trackpoint, TrackpointChunk};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct TrackSegmentId(Uuid);
|
||||
pub struct TrackSegmentId(pub Uuid);
|
||||
|
||||
impl Default for TrackSegmentId {
|
||||
fn default() -> Self {
|
||||
@@ -14,7 +14,7 @@ impl Default for TrackSegmentId {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct TrackSegmentRevisionId(Uuid);
|
||||
pub struct TrackSegmentRevisionId(pub Uuid);
|
||||
|
||||
impl Default for TrackSegmentRevisionId {
|
||||
fn default() -> Self {
|
||||
|
||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
||||
use crate::{Link, TrackSegment};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct TrackId(Uuid);
|
||||
pub struct TrackId(pub Uuid);
|
||||
|
||||
impl Default for TrackId {
|
||||
fn default() -> Self {
|
||||
|
||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
||||
use crate::{Link, LngLat};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct WaypointId(Uuid);
|
||||
pub struct WaypointId(pub Uuid);
|
||||
|
||||
impl Default for WaypointId {
|
||||
fn default() -> Self {
|
||||
|
||||
@@ -3,7 +3,7 @@ mod command;
|
||||
mod edit;
|
||||
mod file;
|
||||
#[cfg(test)]
|
||||
mod fixture;
|
||||
pub(crate) mod fixture;
|
||||
mod pattern;
|
||||
mod tools;
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
FileId, StackEntry as Files, TrackSegmentId, TrackSegmentRevisionId, waypoints_revision,
|
||||
};
|
||||
|
||||
/// Coordinates buffers, as flat `[lng, lat, lng, lat, ...]` arrays so that they can be handed
|
||||
/// to the UI without per-point calls: one per track segment (its trackpoints) and one per file
|
||||
/// (its waypoints). A buffer is only rebuilt when the revision of its segment (or the waypoints
|
||||
/// of its file) changes, so editing a segment does not touch the buffers of the others.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CoordinatesCache {
|
||||
segments: HashMap<TrackSegmentId, (TrackSegmentRevisionId, Vec<f64>)>,
|
||||
waypoints: HashMap<FileId, (u64, Vec<f64>)>,
|
||||
}
|
||||
|
||||
impl CoordinatesCache {
|
||||
pub fn update(&mut self, files: Option<&Files>) {
|
||||
let mut segments = HashSet::new();
|
||||
let mut waypoints = HashSet::new();
|
||||
for file in files.into_iter().flat_map(|files| files.values()) {
|
||||
waypoints.insert(file.id);
|
||||
let rev = waypoints_revision(file);
|
||||
if self.waypoints.get(&file.id).is_none_or(|(r, _)| *r != rev) {
|
||||
let coordinates = file
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.flat_map(|wpt| [wpt.coordinates.lng, wpt.coordinates.lat])
|
||||
.collect();
|
||||
self.waypoints.insert(file.id, (rev, coordinates));
|
||||
}
|
||||
for seg in file.trk.iter().flat_map(|trk| &trk.trkseg) {
|
||||
segments.insert(seg.id);
|
||||
if self
|
||||
.segments
|
||||
.get(&seg.id)
|
||||
.is_none_or(|(r, _)| *r != seg.rev_id)
|
||||
{
|
||||
let coordinates = seg
|
||||
.iter()
|
||||
.flat_map(|trkpt| [trkpt.coordinates.lng, trkpt.coordinates.lat])
|
||||
.collect();
|
||||
self.segments.insert(seg.id, (seg.rev_id, coordinates));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.segments.retain(|id, _| segments.contains(id));
|
||||
self.waypoints.retain(|id, _| waypoints.contains(id));
|
||||
}
|
||||
|
||||
/// Coordinates of the trackpoints of a segment, empty if it does not exist.
|
||||
pub fn segment(&self, id: &TrackSegmentId) -> &[f64] {
|
||||
self.segments.get(id).map_or(&[], |(_, c)| c)
|
||||
}
|
||||
|
||||
/// Coordinates of the waypoints of a file, empty if it does not exist.
|
||||
pub fn waypoints(&self, id: &FileId) -> &[f64] {
|
||||
self.waypoints.get(id).map_or(&[], |(_, c)| c)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, LngLat, Load, Waypoint, WaypointChunk, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded() -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||
let id = fx.order.0[0];
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_buffers_match_trackpoints() {
|
||||
let (fx, id) = loaded();
|
||||
let mut cache = CoordinatesCache::default();
|
||||
cache.update(Some(&fx.files));
|
||||
for seg in fx.files[&id].trk.iter().flat_map(|t| &t.trkseg) {
|
||||
let buffer = cache.segment(&seg.id);
|
||||
assert_eq!(buffer.len(), seg.len() * 2);
|
||||
for (i, p) in seg.iter().enumerate() {
|
||||
assert_eq!(buffer[2 * i], p.coordinates.lng);
|
||||
assert_eq!(buffer[2 * i + 1], p.coordinates.lat);
|
||||
}
|
||||
}
|
||||
assert!(cache.segment(&TrackSegmentId::default()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_changed_buffers_are_rebuilt_and_stale_ones_dropped() {
|
||||
let (mut fx, id) = loaded();
|
||||
let mut cache = CoordinatesCache::default();
|
||||
cache.update(Some(&fx.files));
|
||||
let ids: Vec<_> = fx.files[&id]
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|t| &t.trkseg)
|
||||
.map(|s| s.id)
|
||||
.collect();
|
||||
assert!(ids.len() >= 2);
|
||||
let ptr = |c: &CoordinatesCache, i: usize| c.segment(&ids[i]).as_ptr();
|
||||
let (p0, p1) = (ptr(&cache, 0), ptr(&cache, 1));
|
||||
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
let seg = &mut file.trk[0].trkseg[0];
|
||||
seg.splice(0, 0, vec![Default::default()]);
|
||||
seg.rev_id = Default::default();
|
||||
let len = seg.len();
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
cache.update(Some(&fx.files));
|
||||
assert_eq!(cache.segment(&ids[0]).len(), len * 2);
|
||||
assert_ne!(ptr(&cache, 0), p0);
|
||||
assert_eq!(ptr(&cache, 1), p1);
|
||||
|
||||
cache.update(None);
|
||||
assert!(cache.segment(&ids[0]).is_empty());
|
||||
assert!(cache.waypoints(&id).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_waypoint_buffer() {
|
||||
let (mut fx, id) = loaded();
|
||||
let wpt = |lng, lat| Waypoint {
|
||||
coordinates: LngLat { lng, lat },
|
||||
..Default::default()
|
||||
};
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
file.wpt = vec![
|
||||
Rc::new(WaypointChunk {
|
||||
wpt: vec![wpt(1.0, 2.0)],
|
||||
..Default::default()
|
||||
}),
|
||||
Rc::new(WaypointChunk {
|
||||
wpt: vec![wpt(3.0, 4.0)],
|
||||
..Default::default()
|
||||
}),
|
||||
];
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
let mut cache = CoordinatesCache::default();
|
||||
cache.update(Some(&fx.files));
|
||||
assert_eq!(cache.waypoints(&id), [1.0, 2.0, 3.0, 4.0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use crate::{File, FileId, TrackId, TrackSegmentId, TrackSegmentRevisionId, WaypointId};
|
||||
|
||||
/// What the UI needs to display a file: its name and the structure of its tracks and waypoints,
|
||||
/// with the ids that reference every element. Coordinates are not part of it, they are read
|
||||
/// from the buffers of the [`CoordinatesCache`](crate::CoordinatesCache) (the `i`-th waypoint
|
||||
/// of a file is the `i`-th coordinates pair of its waypoint buffer, same for the trackpoints of
|
||||
/// a segment).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FileStructure {
|
||||
pub id: FileId,
|
||||
pub name: String,
|
||||
pub tracks: Vec<TrackNode>,
|
||||
pub waypoints: Vec<WaypointNode>,
|
||||
/// Changes whenever the waypoints of the file (hence their buffer) change.
|
||||
pub waypoints_rev: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TrackNode {
|
||||
pub id: TrackId,
|
||||
pub name: Option<String>,
|
||||
pub segments: Vec<SegmentNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SegmentNode {
|
||||
pub id: TrackSegmentId,
|
||||
/// Changes whenever the trackpoints of the segment (hence their buffer) change.
|
||||
pub rev_id: TrackSegmentRevisionId,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct WaypointNode {
|
||||
pub id: WaypointId,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl FileStructure {
|
||||
pub fn new(file: &File) -> Self {
|
||||
Self {
|
||||
id: file.id,
|
||||
name: file.info.name.clone(),
|
||||
tracks: file
|
||||
.trk
|
||||
.iter()
|
||||
.map(|trk| TrackNode {
|
||||
id: trk.id,
|
||||
name: trk.info.name.clone(),
|
||||
segments: trk
|
||||
.trkseg
|
||||
.iter()
|
||||
.map(|seg| SegmentNode {
|
||||
id: seg.id,
|
||||
rev_id: seg.rev_id,
|
||||
len: seg.len(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
waypoints: file
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.map(|wpt| WaypointNode {
|
||||
id: wpt.id,
|
||||
name: wpt.name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
waypoints_rev: waypoints_revision(file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies the current waypoints of a file: chunks are immutable, so their ids are enough.
|
||||
pub fn waypoints_revision(file: &File) -> u64 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
for chunk in &file.wpt {
|
||||
chunk.id.hash(&mut hasher);
|
||||
}
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, Load, Waypoint, WaypointChunk, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_structure_of_a_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||
let file = &fx.files[&fx.order.0[0]];
|
||||
let node = FileStructure::new(file);
|
||||
assert_eq!((node.id, &node.name), (file.id, &file.info.name));
|
||||
assert_eq!(node.tracks.len(), file.trk.len());
|
||||
for (n, t) in node.tracks.iter().zip(&file.trk) {
|
||||
assert_eq!((n.id, &n.name), (t.id, &t.info.name));
|
||||
assert_eq!(n.segments.len(), t.trkseg.len());
|
||||
for (n, s) in n.segments.iter().zip(&t.trkseg) {
|
||||
assert_eq!((n.id, n.rev_id, n.len), (s.id, s.rev_id, s.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_waypoints_in_buffer_order_and_revision() {
|
||||
let mut file = crate::File::default();
|
||||
let wpt = |n: &str| Waypoint {
|
||||
name: Some(n.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let before = waypoints_revision(&file);
|
||||
file.wpt = vec![
|
||||
Rc::new(WaypointChunk {
|
||||
wpt: vec![wpt("a"), wpt("b")],
|
||||
..Default::default()
|
||||
}),
|
||||
Rc::new(WaypointChunk {
|
||||
wpt: vec![wpt("c")],
|
||||
..Default::default()
|
||||
}),
|
||||
];
|
||||
let node = FileStructure::new(&file);
|
||||
let names: Vec<_> = node
|
||||
.waypoints
|
||||
.iter()
|
||||
.map(|w| w.name.clone().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, ["a", "b", "c"]);
|
||||
assert_ne!(node.waypoints_rev, before);
|
||||
assert_eq!(node.waypoints_rev, waypoints_revision(&file));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Diff, FileId, FileStructure, StackEntry as Files};
|
||||
|
||||
/// The structure of each file, only recomputed for the files that changed according to the
|
||||
/// [`Diff`] of the stack.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FileStructureCache {
|
||||
map: HashMap<FileId, FileStructure>,
|
||||
}
|
||||
|
||||
impl FileStructureCache {
|
||||
/// Brings the cache in line with `files`, given what changed since the last update.
|
||||
pub fn update(&mut self, files: Option<&Files>, diff: &Option<Diff>) {
|
||||
if let Some(diff) = diff {
|
||||
for id in diff.removed.iter() {
|
||||
self.map.remove(id);
|
||||
}
|
||||
for id in diff.added.iter().chain(&diff.modified) {
|
||||
if let Some(file) = files.and_then(|files| files.get(id)) {
|
||||
self.map.insert(*id, FileStructure::new(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &FileId) -> Option<&FileStructure> {
|
||||
self.map.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, Load, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_only_files_of_the_diff_are_recomputed() {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||
let (a, b) = (fx.order.0[0], fx.order.0[1]);
|
||||
|
||||
let mut cache = FileStructureCache::default();
|
||||
cache.update(
|
||||
Some(&fx.files),
|
||||
&Some(Diff {
|
||||
added: vec![a, b],
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
let before = cache.get(&b).unwrap() as *const FileStructure;
|
||||
|
||||
let mut file = (*fx.files[&a]).clone();
|
||||
file.info.name = "renamed".into();
|
||||
fx.files.insert(a, Rc::new(file));
|
||||
cache.update(
|
||||
Some(&fx.files),
|
||||
&Some(Diff {
|
||||
modified: vec![a],
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
assert_eq!(cache.get(&a).unwrap().name, "renamed");
|
||||
assert!(std::ptr::eq(cache.get(&b).unwrap(), before));
|
||||
|
||||
fx.files.remove(&b);
|
||||
cache.update(
|
||||
Some(&fx.files),
|
||||
&Some(Diff {
|
||||
removed: vec![b],
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
assert!(cache.get(&b).is_none());
|
||||
assert!(cache.get(&a).is_some());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
mod coordinates_cache;
|
||||
mod file_structure;
|
||||
mod file_structure_cache;
|
||||
mod statistics_buffer;
|
||||
mod statistics_cache;
|
||||
|
||||
pub use coordinates_cache::*;
|
||||
pub use file_structure::*;
|
||||
pub use file_structure_cache::*;
|
||||
pub use statistics_buffer::*;
|
||||
pub use statistics_cache::*;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
Action, Apply, Command, FileId, FileOrder, Selection, Stack, State, StatisticsBuffer,
|
||||
StatisticsCache,
|
||||
Action, Apply, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||
FileStructureCache, Selection, Stack, State, StatisticsBuffer, StatisticsCache, TrackSegmentId,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -13,6 +13,10 @@ pub struct Engine {
|
||||
selection: Selection,
|
||||
order: FileOrder,
|
||||
statistics_cache: StatisticsCache,
|
||||
coordinates_cache: CoordinatesCache,
|
||||
structure_cache: FileStructureCache,
|
||||
diff: Option<Diff>,
|
||||
order_changed: bool,
|
||||
statistics_buffer: StatisticsBuffer,
|
||||
}
|
||||
|
||||
@@ -21,18 +25,51 @@ impl Engine {
|
||||
&self.statistics_buffer
|
||||
}
|
||||
|
||||
/// Files in display order.
|
||||
pub fn order(&self) -> &[FileId] {
|
||||
&self.order.0
|
||||
}
|
||||
|
||||
/// Name, tracks, segments and waypoints (with their ids) of a file.
|
||||
pub fn file_structure(&self, id: &FileId) -> Option<&FileStructure> {
|
||||
self.structure_cache.get(id)
|
||||
}
|
||||
|
||||
/// Which files the last action added, removed or modified.
|
||||
pub fn last_diff(&self) -> &Option<Diff> {
|
||||
&self.diff
|
||||
}
|
||||
|
||||
/// Whether the last action changed the order of the files.
|
||||
pub fn order_changed(&self) -> bool {
|
||||
self.order_changed
|
||||
}
|
||||
|
||||
/// Coordinates (`[lng, lat, ...]`) of the trackpoints of a segment.
|
||||
pub fn segment_coordinates(&self, id: &TrackSegmentId) -> &[f64] {
|
||||
self.coordinates_cache.segment(id)
|
||||
}
|
||||
|
||||
/// Coordinates (`[lng, lat, ...]`) of the waypoints of a file.
|
||||
pub fn waypoint_coordinates(&self, id: &FileId) -> &[f64] {
|
||||
self.coordinates_cache.waypoints(id)
|
||||
}
|
||||
|
||||
/// Single entry point for every action coming from the frontend. Returns whether anything
|
||||
/// changed.
|
||||
pub fn execute(&mut self, action: Action) -> bool {
|
||||
let changed = match action {
|
||||
let selection_before = self.selection.clone();
|
||||
let order_before = self.order.0.clone();
|
||||
// the files change iff there is a diff, but selection and order actions have none
|
||||
self.diff = match action {
|
||||
Action::Edit(command) => self.edit(command),
|
||||
Action::Undo => self.stack.undo().is_some(),
|
||||
Action::Redo => self.stack.redo().is_some(),
|
||||
Action::Undo => self.stack.undo(),
|
||||
Action::Redo => self.stack.redo(),
|
||||
Action::Select { file_ids } => {
|
||||
self.selection = Selection::File {
|
||||
file_ids: file_ids.into_iter().collect(),
|
||||
};
|
||||
true
|
||||
None
|
||||
}
|
||||
Action::AddSelect { file_ids } => {
|
||||
match &mut self.selection {
|
||||
@@ -43,33 +80,37 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
None
|
||||
}
|
||||
Action::Reorder { file_ids, index } => {
|
||||
self.order.move_files(&file_ids, index);
|
||||
None
|
||||
}
|
||||
Action::Reorder { file_ids, index } => self.order.move_files(&file_ids, index),
|
||||
Action::SelectAll => {
|
||||
self.selection = Selection::File {
|
||||
file_ids: self.order.0.iter().copied().collect(),
|
||||
};
|
||||
true
|
||||
None
|
||||
}
|
||||
};
|
||||
let selection_changed = self.selection != selection_before;
|
||||
self.order_changed = self.order.0 != order_before;
|
||||
let changed = self.diff.is_some() || selection_changed || self.order_changed;
|
||||
if changed {
|
||||
self.refresh();
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn edit(&mut self, command: Command) -> bool {
|
||||
self.stack
|
||||
.create_and_push_next(|files| {
|
||||
let mut state = State {
|
||||
files,
|
||||
selection: &mut self.selection,
|
||||
order: &mut self.order,
|
||||
};
|
||||
command.apply(&mut state).map_err(|err| err.to_string())
|
||||
})
|
||||
.is_some()
|
||||
fn edit(&mut self, command: Command) -> Option<Diff> {
|
||||
self.stack.create_and_push_next(|files| {
|
||||
let mut state = State {
|
||||
files,
|
||||
selection: &mut self.selection,
|
||||
order: &mut self.order,
|
||||
};
|
||||
command.apply(&mut state).map_err(|err| err.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Brings everything derived from the files back in line with the current stack entry.
|
||||
@@ -87,6 +128,8 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
self.statistics_cache.update(current);
|
||||
self.coordinates_cache.update(current);
|
||||
self.structure_cache.update(current, &self.diff);
|
||||
self.statistics_buffer
|
||||
.update(&self.statistics_cache.get(current, &self.selection));
|
||||
}
|
||||
@@ -280,6 +323,64 @@ mod tests {
|
||||
assert_eq!(engine.order.0, vec![a, b, c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_structures_and_coordinates_follow_actions() {
|
||||
let mut engine = Engine::default();
|
||||
assert!(engine.order().is_empty());
|
||||
load(&mut engine, "data/simple.gpx");
|
||||
let loaded_id = engine.order()[0];
|
||||
let diff = engine.last_diff();
|
||||
assert!(diff.is_some());
|
||||
let diff = diff.as_ref().unwrap();
|
||||
assert_eq!(diff.added, vec![loaded_id]);
|
||||
assert!(engine.order_changed());
|
||||
|
||||
new(&mut engine, "empty");
|
||||
let empty_id = engine.order()[1];
|
||||
// only the new file is reported, the other one is not recomputed
|
||||
let diff = engine.last_diff();
|
||||
assert!(diff.is_some());
|
||||
let diff = diff.as_ref().unwrap();
|
||||
assert_eq!(diff.added, vec![empty_id]);
|
||||
assert!(diff.modified.is_empty() && diff.removed.is_empty());
|
||||
assert_eq!(engine.file_structure(&empty_id).unwrap().name, "empty");
|
||||
|
||||
let structure = engine.file_structure(&loaded_id).unwrap();
|
||||
let seg = &structure.tracks[0].segments[0];
|
||||
let (seg_id, len) = (seg.id, seg.len);
|
||||
assert_eq!(engine.segment_coordinates(&seg_id).len(), len * 2);
|
||||
assert!(engine.waypoint_coordinates(&loaded_id).is_empty());
|
||||
|
||||
// reordering only changes the order
|
||||
assert!(engine.execute(Action::Reorder {
|
||||
file_ids: vec![loaded_id],
|
||||
index: 1
|
||||
}));
|
||||
assert_eq!(engine.order(), [empty_id, loaded_id]);
|
||||
assert!(engine.last_diff().is_none());
|
||||
assert!(engine.order_changed());
|
||||
|
||||
// an action that changes nothing reports nothing
|
||||
assert!(!engine.execute(Action::Reorder {
|
||||
file_ids: vec![loaded_id],
|
||||
index: 1
|
||||
}));
|
||||
assert!(engine.last_diff().is_none());
|
||||
assert!(!engine.order_changed());
|
||||
|
||||
// undo removes the empty file, redoing the load removes the other one too
|
||||
assert!(engine.execute(Action::Undo));
|
||||
let diff = engine.last_diff();
|
||||
assert!(diff.is_some());
|
||||
let diff = diff.as_ref().unwrap();
|
||||
assert_eq!(diff.removed, vec![empty_id]);
|
||||
assert!(engine.file_structure(&empty_id).is_none());
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(engine.order().is_empty());
|
||||
assert!(engine.file_structure(&loaded_id).is_none());
|
||||
assert!(engine.segment_coordinates(&seg_id).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_updates_statistics() {
|
||||
let mut engine = Engine::default();
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, StackEntry, TrackId, TrackSegmentId, WaypointId};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub enum Selection {
|
||||
#[default]
|
||||
Empty,
|
||||
|
||||
@@ -117,7 +117,8 @@ impl Stack {
|
||||
|
||||
pub type StackEntry = HashMap<FileId, Rc<File>>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// Which files were added, removed or modified by a change of the current stack entry.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Diff {
|
||||
pub added: Vec<FileId>,
|
||||
pub removed: Vec<FileId>,
|
||||
|
||||
+179
-1
@@ -6,13 +6,15 @@
|
||||
//! - File bytes cross as `Uint8Array` (`&[u8]`), strings as `&str`.
|
||||
//! - File ids cross as one flat `Uint8Array` of concatenated 16-byte UUIDs (no `Array<string>`).
|
||||
//! - Rectangles cross as four numbers instead of an object.
|
||||
//! - Ids read from the file tree are hyphenated UUID strings; the functions reading buffers take
|
||||
//! them as such.
|
||||
//! - Every function returns `false` when its arguments are invalid or the command did nothing.
|
||||
use std::cell::RefCell;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use gpx_engine::{self as engine, Action, Command, Engine, FileId, LngLat, LngLatBounds};
|
||||
use js_sys::{Float64Array, Int32Array};
|
||||
use js_sys::{Array, Float64Array, Int32Array, Object, Reflect};
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -319,3 +321,179 @@ pub fn add_select(file_ids_bytes: &[u8]) -> bool {
|
||||
pub fn select_all() -> bool {
|
||||
execute(Action::SelectAll)
|
||||
}
|
||||
|
||||
// File order and structures
|
||||
//
|
||||
// The files are described by one ordered list of ids and one structure object per file, so that
|
||||
// the UI can react to what changed only: after each action, `last_update` tells which
|
||||
// structures to (re)read or drop and whether the order changed. Ids are UUID strings. The
|
||||
// trackpoints of a segment and the waypoints of a file are in the same order as the coordinates
|
||||
// of their buffers (see below).
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const FILE_STRUCTURE_TS: &str = r#"
|
||||
export interface FilesUpdate {
|
||||
orderChanged: boolean;
|
||||
/** Files to read the structure of. */
|
||||
added: string[];
|
||||
/** Files whose structure changed: reread it. */
|
||||
modified: string[];
|
||||
/** Files that do not exist anymore: drop their structure. */
|
||||
removed: string[];
|
||||
}
|
||||
export interface FileStructure {
|
||||
id: string;
|
||||
name: string;
|
||||
tracks: TrackNode[];
|
||||
waypoints: WaypointNode[];
|
||||
/** Changes when the waypoints of the file change: refetch their coordinates. */
|
||||
waypointsRev: string;
|
||||
}
|
||||
export interface TrackNode {
|
||||
id: string;
|
||||
name?: string;
|
||||
segments: SegmentNode[];
|
||||
}
|
||||
export interface SegmentNode {
|
||||
id: string;
|
||||
/** Changes when the trackpoints of the segment change: refetch their coordinates. */
|
||||
rev: string;
|
||||
/** Number of trackpoints. */
|
||||
length: number;
|
||||
}
|
||||
export interface WaypointNode {
|
||||
id: string;
|
||||
name?: string;
|
||||
}
|
||||
"#;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(typescript_type = "FilesUpdate")]
|
||||
pub type FilesUpdate;
|
||||
#[wasm_bindgen(typescript_type = "FileStructure | undefined")]
|
||||
pub type FileStructure;
|
||||
#[wasm_bindgen(typescript_type = "string[]")]
|
||||
pub type FileOrder;
|
||||
}
|
||||
|
||||
fn set(object: &Object, key: &str, value: impl Into<JsValue>) {
|
||||
Reflect::set(object, &key.into(), &value.into()).unwrap();
|
||||
}
|
||||
|
||||
fn array<T>(items: &[T], f: impl Fn(&T) -> JsValue) -> Array {
|
||||
items.iter().map(f).collect()
|
||||
}
|
||||
|
||||
fn ids(items: &[FileId]) -> Array {
|
||||
array(items, |id| id.0.to_string().into())
|
||||
}
|
||||
|
||||
fn named_node(id: uuid::Uuid, name: Option<&str>) -> Object {
|
||||
let node = Object::new();
|
||||
set(&node, "id", id.to_string());
|
||||
if let Some(name) = name {
|
||||
set(&node, "name", name);
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
fn structure_object(file: &engine::FileStructure) -> Object {
|
||||
let node = named_node(file.id.0, Some(&file.name));
|
||||
set(
|
||||
&node,
|
||||
"tracks",
|
||||
array(&file.tracks, |trk| {
|
||||
let node = named_node(trk.id.0, trk.name.as_deref());
|
||||
set(
|
||||
&node,
|
||||
"segments",
|
||||
array(&trk.segments, |seg| {
|
||||
let node = named_node(seg.id.0, None);
|
||||
set(&node, "rev", seg.rev_id.0.to_string());
|
||||
set(&node, "length", seg.len as f64);
|
||||
node.into()
|
||||
}),
|
||||
);
|
||||
node.into()
|
||||
}),
|
||||
);
|
||||
set(
|
||||
&node,
|
||||
"waypoints",
|
||||
array(&file.waypoints, |wpt| {
|
||||
named_node(wpt.id.0, wpt.name.as_deref()).into()
|
||||
}),
|
||||
);
|
||||
set(&node, "waypointsRev", format!("{:x}", file.waypoints_rev));
|
||||
node
|
||||
}
|
||||
|
||||
fn with_engine<T>(f: impl FnOnce(&Engine) -> T) -> Option<T> {
|
||||
ENGINE.with(|engine| engine.borrow().as_ref().map(f))
|
||||
}
|
||||
|
||||
/// Ids of the files in display order.
|
||||
#[wasm_bindgen]
|
||||
pub fn file_order() -> FileOrder {
|
||||
let order = with_engine(|e| ids(e.order())).unwrap_or_default();
|
||||
order.unchecked_into()
|
||||
}
|
||||
|
||||
/// Structure of a file, `undefined` if the id is unknown.
|
||||
#[wasm_bindgen]
|
||||
pub fn file_structure(file_id: &str) -> FileStructure {
|
||||
let structure = uuid::Uuid::parse_str(file_id)
|
||||
.ok()
|
||||
.and_then(|id| {
|
||||
with_engine(|e| e.file_structure(&FileId(id)).map(structure_object)).flatten()
|
||||
})
|
||||
.map_or(JsValue::UNDEFINED, JsValue::from);
|
||||
structure.unchecked_into()
|
||||
}
|
||||
|
||||
/// What the last action changed. Read it right after each action.
|
||||
#[wasm_bindgen]
|
||||
pub fn last_update() -> FilesUpdate {
|
||||
let update = Object::new();
|
||||
with_engine(|e| {
|
||||
let diff = e.last_diff().clone().unwrap_or_default();
|
||||
set(&update, "orderChanged", e.order_changed());
|
||||
set(&update, "added", ids(&diff.added));
|
||||
set(&update, "modified", ids(&diff.modified));
|
||||
set(&update, "removed", ids(&diff.removed));
|
||||
});
|
||||
update.unchecked_into()
|
||||
}
|
||||
|
||||
// Coordinates buffers
|
||||
//
|
||||
// Like the statistics buffers, these are views into wasm memory, invalidated by the next
|
||||
// command (and any allocation): read or copy them right away. They are flat `[lng, lat, ...]`
|
||||
// arrays. Compare `rev` / `waypointsRev` of the file structures with the previous ones to know which
|
||||
// buffers actually changed.
|
||||
|
||||
fn coordinates_view(f: impl FnOnce(&Engine) -> Option<&[f64]>) -> Float64Array {
|
||||
ENGINE.with(|engine| match engine.borrow().as_ref().and_then(f) {
|
||||
Some(coordinates) => unsafe { Float64Array::view(coordinates) },
|
||||
None => Float64Array::new_with_length(0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Coordinates of the trackpoints of a segment (empty if the id is unknown).
|
||||
#[wasm_bindgen]
|
||||
pub fn segment_coordinates(segment_id: &str) -> Float64Array {
|
||||
match uuid::Uuid::parse_str(segment_id) {
|
||||
Ok(id) => coordinates_view(|e| Some(e.segment_coordinates(&engine::TrackSegmentId(id)))),
|
||||
Err(_) => Float64Array::new_with_length(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Coordinates of the waypoints of a file (empty if the id is unknown).
|
||||
#[wasm_bindgen]
|
||||
pub fn waypoint_coordinates(file_id: &str) -> Float64Array {
|
||||
match uuid::Uuid::parse_str(file_id) {
|
||||
Ok(id) => coordinates_view(|e| Some(e.waypoint_coordinates(&FileId(id)))),
|
||||
Err(_) => Float64Array::new_with_length(0),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user