This commit is contained in:
vcoppe
2026-10-03 19:08:56 +02:00
parent a994e16f51
commit 438a87f06f
13 changed files with 686 additions and 29 deletions
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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());
}
}
+6
View File
@@ -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::*;
+121 -20
View File
@@ -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,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 => { 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, order: &mut self.order,
order: &mut self.order, };
}; 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();
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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
View File
@@ -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),
}
}