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>,
|
||||
|
||||
Reference in New Issue
Block a user