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);
|
pub struct WaypointChunkId(Uuid);
|
||||||
|
|
||||||
impl Default for WaypointChunkId {
|
impl Default for WaypointChunkId {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use uuid::Uuid;
|
|||||||
use crate::{Trackpoint, TrackpointChunk};
|
use crate::{Trackpoint, TrackpointChunk};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct TrackSegmentId(Uuid);
|
pub struct TrackSegmentId(pub Uuid);
|
||||||
|
|
||||||
impl Default for TrackSegmentId {
|
impl Default for TrackSegmentId {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@@ -14,7 +14,7 @@ impl Default for TrackSegmentId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct TrackSegmentRevisionId(Uuid);
|
pub struct TrackSegmentRevisionId(pub Uuid);
|
||||||
|
|
||||||
impl Default for TrackSegmentRevisionId {
|
impl Default for TrackSegmentRevisionId {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
use crate::{Link, TrackSegment};
|
use crate::{Link, TrackSegment};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct TrackId(Uuid);
|
pub struct TrackId(pub Uuid);
|
||||||
|
|
||||||
impl Default for TrackId {
|
impl Default for TrackId {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
use crate::{Link, LngLat};
|
use crate::{Link, LngLat};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct WaypointId(Uuid);
|
pub struct WaypointId(pub Uuid);
|
||||||
|
|
||||||
impl Default for WaypointId {
|
impl Default for WaypointId {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ mod command;
|
|||||||
mod edit;
|
mod edit;
|
||||||
mod file;
|
mod file;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod fixture;
|
pub(crate) mod fixture;
|
||||||
mod pattern;
|
mod pattern;
|
||||||
mod tools;
|
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_buffer;
|
||||||
mod statistics_cache;
|
mod statistics_cache;
|
||||||
|
|
||||||
|
pub use coordinates_cache::*;
|
||||||
|
pub use file_structure::*;
|
||||||
|
pub use file_structure_cache::*;
|
||||||
pub use statistics_buffer::*;
|
pub use statistics_buffer::*;
|
||||||
pub use statistics_cache::*;
|
pub use statistics_cache::*;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, Apply, Command, FileId, FileOrder, Selection, Stack, State, StatisticsBuffer,
|
Action, Apply, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||||
StatisticsCache,
|
FileStructureCache, Selection, Stack, State, StatisticsBuffer, StatisticsCache, TrackSegmentId,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@@ -13,6 +13,10 @@ pub struct Engine {
|
|||||||
selection: Selection,
|
selection: Selection,
|
||||||
order: FileOrder,
|
order: FileOrder,
|
||||||
statistics_cache: StatisticsCache,
|
statistics_cache: StatisticsCache,
|
||||||
|
coordinates_cache: CoordinatesCache,
|
||||||
|
structure_cache: FileStructureCache,
|
||||||
|
diff: Option<Diff>,
|
||||||
|
order_changed: bool,
|
||||||
statistics_buffer: StatisticsBuffer,
|
statistics_buffer: StatisticsBuffer,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,18 +25,51 @@ impl Engine {
|
|||||||
&self.statistics_buffer
|
&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
|
/// Single entry point for every action coming from the frontend. Returns whether anything
|
||||||
/// changed.
|
/// changed.
|
||||||
pub fn execute(&mut self, action: Action) -> bool {
|
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::Edit(command) => self.edit(command),
|
||||||
Action::Undo => self.stack.undo().is_some(),
|
Action::Undo => self.stack.undo(),
|
||||||
Action::Redo => self.stack.redo().is_some(),
|
Action::Redo => self.stack.redo(),
|
||||||
Action::Select { file_ids } => {
|
Action::Select { file_ids } => {
|
||||||
self.selection = Selection::File {
|
self.selection = Selection::File {
|
||||||
file_ids: file_ids.into_iter().collect(),
|
file_ids: file_ids.into_iter().collect(),
|
||||||
};
|
};
|
||||||
true
|
None
|
||||||
}
|
}
|
||||||
Action::AddSelect { file_ids } => {
|
Action::AddSelect { file_ids } => {
|
||||||
match &mut self.selection {
|
match &mut self.selection {
|
||||||
@@ -43,25 +80,30 @@ 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 => {
|
Action::SelectAll => {
|
||||||
self.selection = Selection::File {
|
self.selection = Selection::File {
|
||||||
file_ids: self.order.0.iter().copied().collect(),
|
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 {
|
if changed {
|
||||||
self.refresh();
|
self.refresh();
|
||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn edit(&mut self, command: Command) -> bool {
|
fn edit(&mut self, command: Command) -> Option<Diff> {
|
||||||
self.stack
|
self.stack.create_and_push_next(|files| {
|
||||||
.create_and_push_next(|files| {
|
|
||||||
let mut state = State {
|
let mut state = State {
|
||||||
files,
|
files,
|
||||||
selection: &mut self.selection,
|
selection: &mut self.selection,
|
||||||
@@ -69,7 +111,6 @@ impl Engine {
|
|||||||
};
|
};
|
||||||
command.apply(&mut state).map_err(|err| err.to_string())
|
command.apply(&mut state).map_err(|err| err.to_string())
|
||||||
})
|
})
|
||||||
.is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Brings everything derived from the files back in line with the current stack entry.
|
/// 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.statistics_cache.update(current);
|
||||||
|
self.coordinates_cache.update(current);
|
||||||
|
self.structure_cache.update(current, &self.diff);
|
||||||
self.statistics_buffer
|
self.statistics_buffer
|
||||||
.update(&self.statistics_cache.get(current, &self.selection));
|
.update(&self.statistics_cache.get(current, &self.selection));
|
||||||
}
|
}
|
||||||
@@ -280,6 +323,64 @@ mod tests {
|
|||||||
assert_eq!(engine.order.0, vec![a, b, c]);
|
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]
|
#[test]
|
||||||
fn test_undo_updates_statistics() {
|
fn test_undo_updates_statistics() {
|
||||||
let mut engine = Engine::default();
|
let mut engine = Engine::default();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use crate::{FileId, StackEntry, TrackId, TrackSegmentId, WaypointId};
|
use crate::{FileId, StackEntry, TrackId, TrackSegmentId, WaypointId};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
pub enum Selection {
|
pub enum Selection {
|
||||||
#[default]
|
#[default]
|
||||||
Empty,
|
Empty,
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ impl Stack {
|
|||||||
|
|
||||||
pub type StackEntry = HashMap<FileId, Rc<File>>;
|
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 struct Diff {
|
||||||
pub added: Vec<FileId>,
|
pub added: Vec<FileId>,
|
||||||
pub removed: Vec<FileId>,
|
pub removed: Vec<FileId>,
|
||||||
|
|||||||
+179
-1
@@ -6,13 +6,15 @@
|
|||||||
//! - File bytes cross as `Uint8Array` (`&[u8]`), strings as `&str`.
|
//! - File bytes cross as `Uint8Array` (`&[u8]`), strings as `&str`.
|
||||||
//! - File ids cross as one flat `Uint8Array` of concatenated 16-byte UUIDs (no `Array<string>`).
|
//! - File ids cross as one flat `Uint8Array` of concatenated 16-byte UUIDs (no `Array<string>`).
|
||||||
//! - Rectangles cross as four numbers instead of an object.
|
//! - 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.
|
//! - Every function returns `false` when its arguments are invalid or the command did nothing.
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use gpx_engine::{self as engine, Action, Command, Engine, FileId, LngLat, LngLatBounds};
|
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]
|
#[wasm_bindgen]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -319,3 +321,179 @@ pub fn add_select(file_ids_bytes: &[u8]) -> bool {
|
|||||||
pub fn select_all() -> bool {
|
pub fn select_all() -> bool {
|
||||||
execute(Action::SelectAll)
|
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