2 Commits
Author SHA1 Message Date
vcoppe a921d4b9cd fixes 2026-10-02 18:50:10 +02:00
vcoppe b7db73cf77 progress 2026-10-02 18:06:45 +02:00
20 changed files with 491 additions and 236 deletions

No files matched your search

+21 -17
View File
@@ -1,13 +1,15 @@
use crate::gpx::{TrackSegment, TrackSegmentIndex}; use crate::{
gpx::{TrackSegment, TrackSegmentIndex},
utils::crossarc,
};
pub fn ramer_douglas_peucker<F, T>( pub fn ramer_douglas_peucker<F>(
trkseg: &TrackSegment, trkseg: &TrackSegment,
distance: &F, mapping: &F,
epsilon: T, epsilon: f64,
) -> Vec<TrackSegmentIndex> ) -> Vec<TrackSegmentIndex>
where where
F: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> T, F: Fn(TrackSegmentIndex) -> (f64, f64),
T: Default + PartialOrd + Copy,
{ {
match trkseg.len() { match trkseg.len() {
0 => vec![], 0 => vec![],
@@ -18,35 +20,37 @@ where
let last = trkseg.last_index().unwrap(); let last = trkseg.last_index().unwrap();
let mut indices = vec![first]; let mut indices = vec![first];
ramer_douglas_peucker_helper(trkseg, first, last, distance, epsilon, &mut indices); ramer_douglas_peucker_helper(trkseg, first, last, mapping, epsilon, &mut indices);
indices.push(last); indices.push(last);
indices indices
} }
} }
} }
fn ramer_douglas_peucker_helper<F, T>( fn ramer_douglas_peucker_helper<F>(
trkseg: &TrackSegment, trkseg: &TrackSegment,
start: TrackSegmentIndex, start: TrackSegmentIndex,
end: TrackSegmentIndex, end: TrackSegmentIndex,
distance: &F, mapping: &F,
epsilon: T, epsilon: f64,
indices: &mut Vec<TrackSegmentIndex>, indices: &mut Vec<TrackSegmentIndex>,
) where ) where
F: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> T, F: Fn(TrackSegmentIndex) -> (f64, f64),
T: Default + PartialOrd + Copy,
{ {
let mut max_idx = None; let mut max_idx = None;
let mut max_dist = T::default(); let mut max_dist = 0.0;
let start_pt = mapping(start);
let end_pt = mapping(end);
let mut cur = trkseg.next_index(Some(start)); let mut cur = trkseg.next_index(Some(start));
while let Some(idx) = cur { while let Some(idx) = cur {
if idx == end { if idx == end {
break; break;
} }
let dist = distance(start, end, idx); let pt = mapping(idx);
let dist = crossarc(start_pt.0, start_pt.1, end_pt.0, end_pt.1, pt.0, pt.1);
if dist > max_dist { if dist > max_dist {
max_idx = Some(idx); max_idx = Some(idx);
max_dist = dist; max_dist = dist;
@@ -57,9 +61,9 @@ fn ramer_douglas_peucker_helper<F, T>(
if let Some(idx) = max_idx { if let Some(idx) = max_idx {
if max_dist > epsilon { if max_dist > epsilon {
ramer_douglas_peucker_helper(trkseg, start, idx, distance, epsilon, indices); ramer_douglas_peucker_helper(trkseg, start, idx, mapping, epsilon, indices);
indices.push(idx); indices.push(idx);
ramer_douglas_peucker_helper(trkseg, idx, end, distance, epsilon, indices); ramer_douglas_peucker_helper(trkseg, idx, end, mapping, epsilon, indices);
} }
} }
} }
+19 -15
View File
@@ -8,43 +8,47 @@ macro_rules! for_each_window {
|$a:ident, $b:ident| $distance:expr, |$a:ident, $b:ident| $distance:expr,
|$i:ident, $l:ident, $r:ident| $body:block, |$i:ident, $l:ident, $r:ident| $body:block,
) => {{ ) => {{
let mut start = $left.unwrap_or_default(); let mut window_start = $left.unwrap_or_default();
let mut cur = $left; let mut window_center = $left;
while let Some(idx) = cur { let mut window_end = $left.unwrap_or_default();
while let Some(idx) = window_center {
// advance left if needed // advance left if needed
while start != idx && { while window_start != idx && {
let $a = start; let $a = window_start;
let $b = idx; let $b = idx;
$distance $distance
} > $window { } > $window {
if let Some(next) = $trkseg.next_index(Some(start)) { if let Some(next) = $trkseg.next_index(Some(window_start)) {
if next == idx { if next == idx {
break; break;
} }
start = next; window_start = next;
} else { } else {
break; break;
} }
} }
// advance right if needed // advance right if needed
let mut end = idx; window_end = window_end.max(idx);
while let Some(next) = $trkseg.next_index(Some(end)) { while let Some(next) = $trkseg.next_index(Some(window_end)) {
if { if window_end != idx && {
let $a = idx; let $a = idx;
let $b = end; let $b = next;
$distance $distance
} > $window { } > $window {
break; break;
} }
end = next; window_end = next;
} }
// apply body // apply body
let $i = idx; let $i = idx;
let $l = start; let $l = window_start;
let $r = end; let $r = window_end;
$body $body
// go next // go next
cur = $trkseg.next_index(cur); if window_center == $right {
break;
}
window_center = $trkseg.next_index(window_center);
} }
}}; }};
} }
+4
View File
@@ -0,0 +1,4 @@
pub struct CoordinatesBuffer {
pub lng: Vec<f64>,
pub lat: Vec<f64>,
}
+5
View File
@@ -0,0 +1,5 @@
mod coordinates;
mod statistics;
pub use coordinates::*;
pub use statistics::*;
@@ -1,11 +1,11 @@
use crate::statistics::{GPXStatistics, GlobalStatistics, sum_options}; use crate::statistics::{GlobalStatistics, Statistics, sum_options};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct StatisticsBuffer { pub struct StatisticsBuffer {
pub total_distance: Vec<f64>, pub total_distance: Vec<f64>,
pub moving_distance: Vec<f64>, pub moving_distance: Vec<f64>,
pub total_time: Vec<i64>, pub total_time: Vec<i32>,
pub moving_time: Vec<i64>, pub moving_time: Vec<i32>,
pub speed: Vec<f64>, pub speed: Vec<f64>,
pub elevation_gain: Vec<f64>, pub elevation_gain: Vec<f64>,
pub elevation_loss: Vec<f64>, pub elevation_loss: Vec<f64>,
@@ -15,7 +15,7 @@ pub struct StatisticsBuffer {
} }
impl StatisticsBuffer { impl StatisticsBuffer {
pub fn update(&mut self, stats: &[&GPXStatistics]) { pub fn update(&mut self, stats: &[&Statistics]) {
self.total_distance.clear(); self.total_distance.clear();
self.moving_distance.clear(); self.moving_distance.clear();
self.total_time.clear(); self.total_time.clear();
+102 -26
View File
@@ -2,75 +2,151 @@
use std::rc::Rc; use std::rc::Rc;
use js_sys::Function; use js_sys::{Float64Array, Function, Int32Array};
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::{ use crate::{
gpx::GPXFile, buffer::StatisticsBuffer,
gpx::{File, FileId},
io::parse, io::parse,
selection::Selection, selection::Selection,
state::{Stack, StackEntry}, state::{Stack, StackEntry},
statistics::StatisticsBuffer, statistics::StatisticsCache,
}; };
extern crate console_error_panic_hook;
#[derive(Debug, Default)]
#[wasm_bindgen] #[wasm_bindgen]
pub struct Engine { pub struct Engine {
stack: Stack, stack: Stack,
selection: Selection, selection: Selection,
statistics: StatisticsBuffer, statistics_cache: StatisticsCache,
callback: Function, statistics_buffer: StatisticsBuffer,
callback: Option<Function>,
} }
#[wasm_bindgen] #[wasm_bindgen]
impl Engine { impl Engine {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(callback: Function) -> Self { pub fn new(callback: Function) -> Self {
console_error_panic_hook::set_once();
Self { Self {
stack: Default::default(), stack: Default::default(),
selection: Default::default(), selection: Default::default(),
statistics: Default::default(), statistics_cache: Default::default(),
callback, statistics_buffer: Default::default(),
callback: Some(callback),
} }
} }
#[wasm_bindgen] pub fn total_distance(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.total_distance) }
}
pub fn moving_distance(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.moving_distance) }
}
pub fn total_time(&self) -> Int32Array {
unsafe { Int32Array::view(&self.statistics_buffer.total_time) }
}
pub fn moving_time(&self) -> Int32Array {
unsafe { Int32Array::view(&self.statistics_buffer.moving_time) }
}
pub fn speed(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.speed) }
}
pub fn elevation_gain(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.elevation_gain) }
}
pub fn elevation_loss(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.elevation_loss) }
}
pub fn slope(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.slope) }
}
pub fn slope_segment_slope(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.slope_segment_slope) }
}
pub fn slope_segment_distance(&self) -> Float64Array {
unsafe { Float64Array::view(&self.statistics_buffer.slope_segment_distance) }
}
pub fn create_file(&mut self, name: &str) { pub fn create_file(&mut self, name: &str) {
let mut file = GPXFile::default(); self.event_loop(|entry| {
let mut file = File::default();
file.info.name = name.to_string(); file.info.name = name.to_string();
self.stack.create_and_push_next(|entry: &mut StackEntry| {
entry.insert(file.id, Rc::new(file)); entry.insert(file.id, Rc::new(file));
true true
}); });
} }
#[wasm_bindgen]
pub fn load_file(&mut self, data: &[u8]) { pub fn load_file(&mut self, data: &[u8]) {
self.event_loop(|entry| {
if let Ok(file) = parse(data) { if let Ok(file) = parse(data) {
self.stack.create_and_push_next(|entry: &mut StackEntry| {
entry.insert(file.id, Rc::new(file)); entry.insert(file.id, Rc::new(file));
true true
} else {
false
}
}); });
} }
let _ = self.callback.call1(&JsValue::NULL, &JsValue::from("done!"));
}
#[wasm_bindgen]
pub fn delete_file(&mut self, id: &[u8]) { pub fn delete_file(&mut self, id: &[u8]) {
self.event_loop(|entry| {
if let Ok(id) = Uuid::from_slice(id) { if let Ok(id) = Uuid::from_slice(id) {
self.stack let id = FileId(id);
.create_and_push_next(|entry: &mut StackEntry| entry.remove(&id).is_some()); entry.remove(&id).is_some()
} else {
false
} }
});
} }
fn event_loop(&mut self) { fn event_loop<F>(&mut self, f: F)
// Control flow where
// - get lock to cancel any other updates F: FnOnce(&mut StackEntry) -> bool,
// - update files {
// - update separate statistics if let Some(diff) = self.stack.create_and_push_next(f) {
// - update selection self.selection.select(diff.added[0]);
// - update selection statistics self.statistics_cache.update(self.stack.current());
// self.statistics.update(stats); self.statistics_buffer.update(
// - notify UI through callback (changed files, updated selection, new stats) &self
.statistics_cache
.get(self.stack.current(), &self.selection),
);
if let Some(c) = &self.callback {
let _ = c.call0(&JsValue::NULL);
}
}
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::Read};
use super::*;
#[test]
fn test_load_file() {
let mut engine = Engine::default();
let mut f = File::open("data/simple.gpx").unwrap();
let mut data = String::new();
let _ = f.read_to_string(&mut data);
engine.load_file(data.as_bytes());
assert_eq!(engine.statistics_buffer.total_distance.len(), 80);
} }
} }
+23 -23
View File
@@ -4,19 +4,19 @@ use crate::gpx::{Trackpoint, Waypoint};
static MAX_TRKPT_CHUNK_SIZE: usize = 4096; static MAX_TRKPT_CHUNK_SIZE: usize = 4096;
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
pub struct TrackpointChunk { pub struct TrackpointChunkId(Uuid);
pub id: Uuid,
pub trkpt: Vec<Trackpoint>, impl Default for TrackpointChunkId {
fn default() -> Self {
Self(Uuid::new_v4())
}
} }
impl Default for TrackpointChunk { #[derive(Debug, Default)]
fn default() -> Self { pub struct TrackpointChunk {
Self { pub id: TrackpointChunkId,
id: Uuid::new_v4(), pub trkpt: Vec<Trackpoint>,
trkpt: Default::default(),
}
}
} }
impl PartialEq for TrackpointChunk { impl PartialEq for TrackpointChunk {
@@ -31,21 +31,21 @@ impl TrackpointChunk {
} }
} }
static MAX_WPT_CHUNK_SIZE: usize = 128; #[derive(Debug, PartialEq, Eq)]
pub struct WaypointChunkId(Uuid);
#[derive(Debug)] impl Default for WaypointChunkId {
pub struct WaypointChunk { fn default() -> Self {
pub id: Uuid, Self(Uuid::new_v4())
pub wpt: Vec<Waypoint>, }
} }
impl Default for WaypointChunk { static MAX_WPT_CHUNK_SIZE: usize = 128;
fn default() -> Self {
Self { #[derive(Debug, Default)]
id: Uuid::new_v4(), pub struct WaypointChunk {
wpt: Default::default(), pub id: WaypointChunkId,
} pub wpt: Vec<Waypoint>,
}
} }
impl PartialEq for WaypointChunk { impl PartialEq for WaypointChunk {
+14 -16
View File
@@ -4,28 +4,26 @@ use uuid::Uuid;
use crate::gpx::{Link, Track, WaypointChunk}; use crate::gpx::{Link, Track, WaypointChunk};
#[derive(Debug, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GPXFile { pub struct FileId(pub Uuid);
pub id: Uuid,
pub info: GPXFileInfo, impl Default for FileId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default, PartialEq)]
pub struct File {
pub id: FileId,
pub info: FileInfo,
pub trk: Vec<Track>, pub trk: Vec<Track>,
pub wpt: Vec<Rc<WaypointChunk>>, pub wpt: Vec<Rc<WaypointChunk>>,
// TODO routes // TODO routes
} }
impl Default for GPXFile {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
info: Default::default(),
trk: Default::default(),
wpt: Default::default(),
}
}
}
#[derive(Debug, Default, PartialEq)] #[derive(Debug, Default, PartialEq)]
pub struct GPXFileInfo { pub struct FileInfo {
pub name: String, pub name: String,
pub desc: Option<String>, pub desc: Option<String>,
pub author: Option<Author>, pub author: Option<Author>,
+34 -14
View File
@@ -1,9 +1,31 @@
use std::{ops::Index, rc::Rc}; use std::{ops::Index, rc::Rc};
use uuid::Uuid;
use crate::gpx::{Trackpoint, TrackpointChunk}; use crate::gpx::{Trackpoint, TrackpointChunk};
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct TrackSegmentId(Uuid);
impl Default for TrackSegmentId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TrackSegmentRevisionId(Uuid);
impl Default for TrackSegmentRevisionId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default, PartialEq)] #[derive(Debug, Default, PartialEq)]
pub struct TrackSegment { pub struct TrackSegment {
pub id: TrackSegmentId,
pub rev_id: TrackSegmentRevisionId,
chunks: Vec<Rc<TrackpointChunk>>, chunks: Vec<Rc<TrackpointChunk>>,
cumul_length: Vec<usize>, cumul_length: Vec<usize>,
} }
@@ -38,6 +60,7 @@ impl TrackSegment {
let mut next = cur.map_or_default(|idx| TrackSegmentIndex { let mut next = cur.map_or_default(|idx| TrackSegmentIndex {
chunk: idx.chunk, chunk: idx.chunk,
pos: idx.pos + 1, pos: idx.pos + 1,
flat: idx.flat + 1,
}); });
loop { loop {
if next.chunk >= self.chunks.len() { if next.chunk >= self.chunks.len() {
@@ -56,6 +79,7 @@ impl TrackSegment {
let mut prev = cur.unwrap_or(TrackSegmentIndex { let mut prev = cur.unwrap_or(TrackSegmentIndex {
chunk: self.chunks.len(), chunk: self.chunks.len(),
pos: 0, pos: 0,
flat: self.cumul_length.last().copied().unwrap_or_default(),
}); });
loop { loop {
if prev.pos == 0 { if prev.pos == 0 {
@@ -64,22 +88,15 @@ impl TrackSegment {
} }
prev.chunk -= 1; prev.chunk -= 1;
prev.pos = self.chunks[prev.chunk].trkpt.len(); prev.pos = self.chunks[prev.chunk].trkpt.len();
prev.flat -= 1;
} else { } else {
prev.pos -= 1; prev.pos -= 1;
prev.flat -= 1;
return Some(prev); return Some(prev);
} }
} }
} }
pub fn to_flat_index(&self, idx: TrackSegmentIndex) -> usize {
idx.pos
+ if idx.chunk > 0 {
self.cumul_length[idx.chunk - 1]
} else {
0
}
}
fn locate(&self, idx: usize) -> Option<TrackSegmentIndex> { fn locate(&self, idx: usize) -> Option<TrackSegmentIndex> {
let chunk = self.cumul_length.partition_point(|l| idx >= *l); let chunk = self.cumul_length.partition_point(|l| idx >= *l);
if chunk >= self.chunks.len() { if chunk >= self.chunks.len() {
@@ -93,7 +110,11 @@ impl TrackSegment {
if pos >= self.chunks[chunk].trkpt.len() { if pos >= self.chunks[chunk].trkpt.len() {
None None
} else { } else {
Some(TrackSegmentIndex { chunk, pos }) Some(TrackSegmentIndex {
chunk,
pos,
flat: idx,
})
} }
} }
} }
@@ -118,6 +139,7 @@ impl Index<usize> for TrackSegment {
pub struct TrackSegmentIndex { pub struct TrackSegmentIndex {
pub chunk: usize, pub chunk: usize,
pub pos: usize, pub pos: usize,
pub flat: usize,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -144,10 +166,7 @@ impl<'a> Iterator for TrackSegmentIterator<'a> {
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> { fn nth(&mut self, n: usize) -> Option<Self::Item> {
let idx = self let idx = self.idx.map_or_default(|idx| idx.flat) + n;
.idx
.map_or_default(|idx| self.trkseg.to_flat_index(idx))
+ n;
self.idx = self.trkseg.locate(idx); self.idx = self.trkseg.locate(idx);
self.idx.map(|idx| &self.trkseg[idx]) self.idx.map(|idx| &self.trkseg[idx])
} }
@@ -220,6 +239,7 @@ mod tests {
trkseg[TrackSegmentIndex { trkseg[TrackSegmentIndex {
chunk: trkseg.chunks.len(), chunk: trkseg.chunks.len(),
pos: 0, pos: 0,
flat: 0,
}] }]
.ele; .ele;
} }
+12
View File
@@ -1,7 +1,19 @@
use uuid::Uuid;
use crate::gpx::{Link, TrackSegment}; use crate::gpx::{Link, TrackSegment};
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct TrackId(Uuid);
impl Default for TrackId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default, PartialEq)] #[derive(Debug, Default, PartialEq)]
pub struct Track { pub struct Track {
pub id: TrackId,
pub info: TrackInfo, pub info: TrackInfo,
pub trkseg: Vec<TrackSegment>, pub trkseg: Vec<TrackSegment>,
} }
-6
View File
@@ -11,9 +11,3 @@ pub struct Trackpoint {
pub power: Option<u16>, pub power: Option<u16>,
// TODO OSM data? or store intervals at a higher level? // TODO OSM data? or store intervals at a higher level?
} }
impl Trackpoint {
pub fn time_diff(&self, other: &Trackpoint) -> Option<i64> {
self.time.zip(other.time).map(|(t1, t2)| t1 - t2)
}
}
+12
View File
@@ -1,7 +1,19 @@
use uuid::Uuid;
use crate::gpx::{Link, LngLat}; use crate::gpx::{Link, LngLat};
#[derive(Debug, PartialEq, Eq)]
pub struct WaypointId(Uuid);
impl Default for WaypointId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Waypoint { pub struct Waypoint {
pub id: WaypointId,
pub coordinates: LngLat, pub coordinates: LngLat,
pub ele: f64, pub ele: f64,
pub time: Option<i64>, pub time: Option<i64>,
+3 -3
View File
@@ -1,7 +1,7 @@
use std::rc::Rc; use std::rc::Rc;
use crate::gpx::{ use crate::gpx::{
Author, GPXFile, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointChunk, Waypoint, Author, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointChunk, Waypoint,
WaypointChunk, WaypointChunk,
}; };
use chrono::DateTime; use chrono::DateTime;
@@ -50,10 +50,10 @@ fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
coordinates coordinates
} }
pub fn parse(data: &[u8]) -> Result<GPXFile, Error> { pub fn parse(data: &[u8]) -> Result<File, Error> {
let mut reader = Reader::from_reader(data); let mut reader = Reader::from_reader(data);
let mut buf = vec![]; let mut buf = vec![];
let mut gpx = GPXFile::default(); let mut gpx = File::default();
let mut stack: Vec<GPXElement> = vec![]; let mut stack: Vec<GPXElement> = vec![];
let mut trkpt_chunk = TrackpointChunk::default(); let mut trkpt_chunk = TrackpointChunk::default();
let mut wpt_chunk = WaypointChunk::default(); let mut wpt_chunk = WaypointChunk::default();
+3
View File
@@ -1,5 +1,6 @@
mod action; mod action;
mod algorithm; mod algorithm;
mod buffer;
mod engine; mod engine;
mod gpx; mod gpx;
mod io; mod io;
@@ -7,3 +8,5 @@ mod selection;
mod state; mod state;
mod statistics; mod statistics;
mod utils; mod utils;
pub use engine::*;
+23 -10
View File
@@ -1,29 +1,34 @@
use std::collections::HashSet; use std::collections::HashSet;
use uuid::Uuid; use crate::gpx::{FileId, TrackId, TrackSegmentId, WaypointId};
#[derive(Debug)] #[derive(Debug)]
pub struct FileSelection { pub struct FileSelection {
files: HashSet<Uuid>, pub files: HashSet<FileId>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct TrackSelection { pub struct TrackSelection {
file: Uuid, pub file: FileId,
trk: HashSet<Uuid>, pub trk: HashSet<TrackId>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct TrackSegmentSelection { pub struct TrackSegmentSelection {
file: Uuid, pub file: FileId,
trk: Uuid, pub trk: TrackId,
trkseg: HashSet<Uuid>, pub trkseg: HashSet<TrackSegmentId>,
}
#[derive(Debug)]
pub struct WaypointsSelection {
pub file: FileId,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct WaypointSelection { pub struct WaypointSelection {
file: Uuid, pub file: FileId,
wpt: HashSet<Uuid>, pub wpt: HashSet<WaypointId>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@@ -33,6 +38,14 @@ pub enum Selection {
File(FileSelection), File(FileSelection),
Track(TrackSelection), Track(TrackSelection),
TrackSegment(TrackSegmentSelection), TrackSegment(TrackSegmentSelection),
Waypoints, Waypoints(WaypointsSelection),
Waypoint(WaypointSelection), Waypoint(WaypointSelection),
} }
impl Selection {
pub fn select(&mut self, id: FileId) {
let mut files = HashSet::default();
files.insert(id);
*self = Selection::File(FileSelection { files });
}
}
+9 -11
View File
@@ -3,11 +3,9 @@ use std::{
rc::Rc, rc::Rc,
}; };
use uuid::Uuid; use crate::gpx::{File, FileId};
use crate::gpx::GPXFile; #[derive(Debug, Default)]
#[derive(Default)]
pub struct Stack { pub struct Stack {
entries: Vec<StackEntry>, entries: Vec<StackEntry>,
index: Option<usize>, index: Option<usize>,
@@ -99,8 +97,8 @@ impl Stack {
} }
let prev = prev.map_or_default(|i| self.get(i)); let prev = prev.map_or_default(|i| self.get(i));
let cur = cur.map_or_default(|i| self.get(i)); let cur = cur.map_or_default(|i| self.get(i));
let prev_ids: HashSet<Uuid> = prev.map_or_default(|e| e.keys().copied().collect()); let prev_ids: HashSet<FileId> = prev.map_or_default(|e| e.keys().copied().collect());
let cur_ids: HashSet<Uuid> = cur.map_or_default(|e| e.keys().copied().collect()); let cur_ids: HashSet<FileId> = cur.map_or_default(|e| e.keys().copied().collect());
let mut modified = vec![]; let mut modified = vec![];
for id in prev_ids.intersection(&cur_ids) { for id in prev_ids.intersection(&cur_ids) {
let before = prev.map_or_default(|e| e.get(id)); let before = prev.map_or_default(|e| e.get(id));
@@ -117,13 +115,13 @@ impl Stack {
} }
} }
pub type StackEntry = HashMap<Uuid, Rc<GPXFile>>; pub type StackEntry = HashMap<FileId, Rc<File>>;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Diff { pub struct Diff {
pub added: Vec<Uuid>, pub added: Vec<FileId>,
pub removed: Vec<Uuid>, pub removed: Vec<FileId>,
pub modified: Vec<Uuid>, pub modified: Vec<FileId>,
} }
#[cfg(test)] #[cfg(test)]
@@ -139,7 +137,7 @@ mod tests {
assert!(stack.current().is_none()); assert!(stack.current().is_none());
let diff = stack.create_and_push_next(|e| { let diff = stack.create_and_push_next(|e| {
let file = Rc::new(GPXFile::default()); let file = Rc::new(File::default());
e.insert(file.id, file); e.insert(file.id, file);
true true
}); });
+113
View File
@@ -0,0 +1,113 @@
use std::{
collections::{HashMap, HashSet, hash_map::Entry},
rc::Rc,
};
use crate::{
gpx::{File, Track, TrackSegment, TrackSegmentRevisionId},
selection::Selection,
state::StackEntry,
statistics::Statistics,
};
#[derive(Debug, Default)]
pub struct StatisticsCache {
map: HashMap<TrackSegmentRevisionId, Statistics>,
}
impl StatisticsCache {
pub fn update(&mut self, state: Option<&StackEntry>) {
let mut stale: HashSet<TrackSegmentRevisionId> = self.map.keys().copied().collect();
if let Some(state) = state {
for file in state.values() {
for trk in file.trk.iter() {
for trkseg in trk.trkseg.iter() {
match self.map.entry(trkseg.rev_id) {
Entry::Occupied(_) => {
stale.remove(&trkseg.rev_id);
}
Entry::Vacant(e) => {
e.insert(Statistics::compute(trkseg));
}
}
}
}
}
}
for id in stale.iter() {
self.map.remove(id);
}
}
pub fn get<'a>(
&'a self,
state: Option<&StackEntry>,
selection: &Selection,
) -> Vec<&'a Statistics> {
let mut stats = vec![];
if let Some(state) = state {
match selection {
Selection::Empty => (),
Selection::File(s) => {
for id in s.files.iter() {
if let Some(file) = state.get(id) {
self.add_file_stats(file, &mut stats);
}
}
}
Selection::Track(s) => {
if let Some(file) = state.get(&s.file) {
for trk in file.trk.iter() {
if s.trk.contains(&trk.id) {
self.add_track_stats(trk, &mut stats);
}
}
}
}
Selection::TrackSegment(s) => {
if let Some(file) = state.get(&s.file) {
for trk in file.trk.iter() {
if s.trk == trk.id {
for trkseg in trk.trkseg.iter() {
if s.trkseg.contains(&trkseg.id) {
self.add_segment_stats(trkseg, &mut stats);
}
}
break;
}
}
}
}
Selection::Waypoints(s) => {
if let Some(file) = state.get(&s.file) {
self.add_file_stats(file, &mut stats);
}
}
Selection::Waypoint(s) => {
if let Some(file) = state.get(&s.file) {
self.add_file_stats(file, &mut stats);
}
}
}
}
stats
}
fn add_file_stats<'a>(&'a self, file: &Rc<File>, stats: &mut Vec<&'a Statistics>) {
for trk in file.trk.iter() {
self.add_track_stats(trk, stats);
}
}
fn add_track_stats<'a>(&'a self, trk: &Track, stats: &mut Vec<&'a Statistics>) {
for trkseg in trk.trkseg.iter() {
self.add_segment_stats(trkseg, stats);
}
}
fn add_segment_stats<'a>(&'a self, trkseg: &TrackSegment, stats: &mut Vec<&'a Statistics>) {
if let Some(s) = self.map.get(&trkseg.rev_id) {
stats.push(s);
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
mod buffer; mod cache;
mod statistics; mod statistics;
mod utils; mod utils;
pub use buffer::*; pub use cache::*;
pub use statistics::*; pub use statistics::*;
pub use utils::*; pub use utils::*;
+66 -77
View File
@@ -1,19 +1,19 @@
use crate::{ use crate::{
algorithm::ramer_douglas_peucker, algorithm::ramer_douglas_peucker,
for_each_window, for_each_window,
gpx::{LngLat, LngLatBounds, TrackSegment, Trackpoint}, gpx::{LngLat, LngLatBounds, TrackSegment, TrackSegmentIndex, Trackpoint},
statistics::sum_options, statistics::sum_options,
utils::{distance, slope, speed}, utils::{distance, slope, speed, time_diff},
}; };
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct GPXStatistics { pub struct Statistics {
pub global: GlobalStatistics, pub global: GlobalStatistics,
pub local: Vec<TrackpointStatistics>, pub local: Vec<TrackpointStatistics>,
} }
impl GPXStatistics { impl Statistics {
pub fn total_time(&self) -> Option<i64> { pub fn total_time(&self) -> Option<i32> {
self.global.total_time() self.global.total_time()
} }
@@ -38,7 +38,8 @@ impl GPXStatistics {
} }
stats.compute_smoothed_speed(trkseg); stats.compute_smoothed_speed(trkseg);
stats.compute_smoothed_elevation_gain(trkseg); stats.compute_smoothed_elevation_segments(trkseg);
stats.compute_smoothed_slope(trkseg);
stats stats
} }
@@ -53,7 +54,7 @@ impl GPXStatistics {
fn accumulate_distance_and_time(&mut self, prev: &Trackpoint, cur: &Trackpoint) { fn accumulate_distance_and_time(&mut self, prev: &Trackpoint, cur: &Trackpoint) {
let dist = distance(prev.coordinates, cur.coordinates); let dist = distance(prev.coordinates, cur.coordinates);
let time = cur.time_diff(prev); let time = time_diff(&cur.time, &prev.time);
self.global.total_distance += dist; self.global.total_distance += dist;
@@ -91,14 +92,13 @@ impl GPXStatistics {
trkseg.first_index(), trkseg.first_index(),
trkseg.last_index(), trkseg.last_index(),
Some(10000), Some(10000),
|i, j| trkseg[i].time_diff(&trkseg[j]), |i, j| time_diff(&trkseg[i].time, &trkseg[j].time),
|i, left, right| { |i, left, right| {
let i = trkseg.to_flat_index(i); self.local[i.flat].speed =
let left = trkseg.to_flat_index(left); time_diff(&trkseg[right].time, &trkseg[left].time).map(|t| {
let right = trkseg.to_flat_index(right);
self.local[i].speed = trkseg[right].time_diff(&trkseg[left]).map(|t| {
speed( speed(
self.local[right].total_distance - self.local[left].total_distance, self.local[right.flat].total_distance
- self.local[left.flat].total_distance,
t, t,
) )
}); });
@@ -106,32 +106,36 @@ impl GPXStatistics {
); );
} }
fn compute_smoothed_elevation_gain(&mut self, trkseg: &TrackSegment) { fn compute_smoothed_elevation_segments(&mut self, trkseg: &TrackSegment) {
let simplified = ramer_douglas_peucker( let simplified = self.get_elevation_extremas(trkseg);
trkseg,
&|i, j, k| {
let x1 = self.local[trkseg.to_flat_index(i)].total_distance * 1000.0;
let x2 = self.local[trkseg.to_flat_index(j)].total_distance * 1000.0;
let x3 = self.local[trkseg.to_flat_index(k)].total_distance * 1000.0;
let y1 = trkseg[i].ele;
let y2 = trkseg[j].ele;
let y3 = trkseg[k].ele;
let dist = ((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt();
if dist == 0.0 {
((x3 - x1).powi(2) + (y3 - y1).powi(2)).sqrt()
} else {
((y2 - y1) * x3 - (x2 - x1) * y3 + x2 * y1 - y2 * x1).abs() / dist
}
},
20.0,
);
for i in 0..(simplified.len() - 1) { for i in 0..(simplified.len() - 1) {
let start = simplified[i]; let start = simplified[i];
let end = simplified[i + 1]; let end = simplified[i + 1];
let last = i + 1 == simplified.len() - 1; let last = i + 1 == simplified.len() - 1;
self.compute_smoothed_elevation_gain(trkseg, start, end, last);
let segment_dist =
self.local[end.flat].total_distance - self.local[start.flat].total_distance;
let segment_ele = trkseg[end].ele - trkseg[start].ele;
let segment_slope = slope(segment_ele, segment_dist);
for k in start.flat..(end.flat + last as usize) {
self.local[k].slope_segment = SlopeSegment {
slope: segment_slope,
distance: segment_dist,
};
}
}
}
fn compute_smoothed_elevation_gain(
&mut self,
trkseg: &TrackSegment,
start: TrackSegmentIndex,
end: TrackSegmentIndex,
last: bool,
) {
let mut cumul_ele = 0.0; let mut cumul_ele = 0.0;
let mut current_left = start; let mut current_left = start;
let mut current_right = Some(start); let mut current_right = Some(start);
@@ -142,11 +146,7 @@ impl GPXStatistics {
Some(start), Some(start),
Some(end), Some(end),
0.1, 0.1,
|i, j| { |i, j| self.local[j.flat].total_distance - self.local[i.flat].total_distance,
let i = trkseg.to_flat_index(i);
let j = trkseg.to_flat_index(j);
self.local[j].total_distance - self.local[i].total_distance
},
|i, left, right| { |i, left, right| {
while current_left != left { while current_left != left {
cumul_ele -= trkseg[current_left].ele; cumul_ele -= trkseg[current_left].ele;
@@ -160,14 +160,10 @@ impl GPXStatistics {
current_right = trkseg.next_index(current_right); current_right = trkseg.next_index(current_right);
} }
let flat_i = trkseg.to_flat_index(i); let smoothed_ele = if i == start || i == end {
let flat_left = trkseg.to_flat_index(left);
let flat_right = trkseg.to_flat_index(right);
let smoothed_ele: f64 = if i == start || i == end {
trkseg[i].ele trkseg[i].ele
} else { } else {
cumul_ele / (flat_right - flat_left + 1) as f64 cumul_ele / (right.flat - left.flat + 1) as f64
}; };
let delta = smoothed_ele - prev_smoothed_ele; let delta = smoothed_ele - prev_smoothed_ele;
@@ -178,57 +174,50 @@ impl GPXStatistics {
} }
if i < end || last { if i < end || last {
self.local[flat_i].elevation_gain = self.global.elevation_gain; self.local[i.flat].elevation_gain = self.global.elevation_gain;
self.local[flat_i].elevation_loss = self.global.elevation_loss; self.local[i.flat].elevation_loss = self.global.elevation_loss;
} }
prev_smoothed_ele = smoothed_ele; prev_smoothed_ele = smoothed_ele;
}, },
); );
let flat_start = trkseg.to_flat_index(start);
let flat_end = trkseg.to_flat_index(end);
let segment_dist =
self.local[flat_end].total_distance - self.local[flat_start].total_distance;
let segment_ele = trkseg[end].ele - trkseg[start].ele;
let segment_slope = slope(segment_ele, segment_dist);
for k in flat_start..(flat_end + last as usize) {
self.local[k].slope_segment = SlopeSegment {
slope: segment_slope,
distance: segment_dist,
};
}
} }
fn compute_smoothed_slope(&mut self, trkseg: &TrackSegment) {
for_each_window!( for_each_window!(
trkseg, trkseg,
trkseg.first_index(), trkseg.first_index(),
trkseg.last_index(), trkseg.last_index(),
0.05, 0.05,
|i, j| { |i, j| self.local[j.flat].total_distance - self.local[i.flat].total_distance,
let i = trkseg.to_flat_index(i);
let j = trkseg.to_flat_index(j);
self.local[j].total_distance - self.local[i].total_distance
},
|i, left, right| { |i, left, right| {
let flat_i = trkseg.to_flat_index(i);
let flat_left = trkseg.to_flat_index(left);
let flat_right = trkseg.to_flat_index(right);
let dist = let dist =
self.local[flat_right].total_distance - self.local[flat_left].total_distance; self.local[right.flat].total_distance - self.local[left.flat].total_distance;
let ele = trkseg[right].ele - trkseg[left].ele; let ele = trkseg[right].ele - trkseg[left].ele;
self.local[flat_i].slope = slope(ele, dist); self.local[i.flat].slope = slope(ele, dist);
}, },
); );
} }
fn get_elevation_extremas(&self, trkseg: &TrackSegment) -> Vec<TrackSegmentIndex> {
ramer_douglas_peucker(
trkseg,
&|idx| {
(
self.local[idx.flat].total_distance * 1000.0,
trkseg[idx].ele,
)
},
20.0,
)
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct GlobalStatistics { pub struct GlobalStatistics {
pub total_distance: f64, pub total_distance: f64,
pub moving_distance: Option<f64>, pub moving_distance: Option<f64>,
pub moving_time: Option<i64>, pub moving_time: Option<i32>,
pub elevation_gain: f64, pub elevation_gain: f64,
pub elevation_loss: f64, pub elevation_loss: f64,
pub start_time: Option<i64>, pub start_time: Option<i64>,
@@ -237,8 +226,8 @@ pub struct GlobalStatistics {
} }
impl GlobalStatistics { impl GlobalStatistics {
pub fn total_time(&self) -> Option<i64> { pub fn total_time(&self) -> Option<i32> {
self.start_time.zip(self.end_time).map(|(t1, t2)| t2 - t1) time_diff(&self.start_time, &self.end_time)
} }
pub fn total_speed(&self) -> Option<f64> { pub fn total_speed(&self) -> Option<f64> {
@@ -265,8 +254,8 @@ impl GlobalStatistics {
pub struct TrackpointStatistics { pub struct TrackpointStatistics {
pub total_distance: f64, pub total_distance: f64,
pub moving_distance: Option<f64>, pub moving_distance: Option<f64>,
pub total_time: Option<i64>, pub total_time: Option<i32>,
pub moving_time: Option<i64>, pub moving_time: Option<i32>,
pub speed: Option<f64>, pub speed: Option<f64>,
pub elevation_gain: f64, pub elevation_gain: f64,
pub elevation_loss: f64, pub elevation_loss: f64,
@@ -275,7 +264,7 @@ pub struct TrackpointStatistics {
} }
impl TrackpointStatistics { impl TrackpointStatistics {
fn from_partial_stats(stats: &GPXStatistics) -> Self { fn from_partial_stats(stats: &Statistics) -> Self {
Self { Self {
total_distance: stats.global.total_distance, total_distance: stats.global.total_distance,
moving_distance: stats.global.moving_distance, moving_distance: stats.global.moving_distance,
@@ -313,7 +302,7 @@ mod tests {
let gpx = parse(data.as_bytes()).unwrap(); let gpx = parse(data.as_bytes()).unwrap();
let trkseg = &gpx.trk[0].trkseg[0]; let trkseg = &gpx.trk[0].trkseg[0];
let stats = GPXStatistics::compute(trkseg); let stats = Statistics::compute(trkseg);
assert_eq!(stats.local.len(), trkseg.len()); assert_eq!(stats.local.len(), trkseg.len());
for trkpt_stats in stats.local.iter() { for trkpt_stats in stats.local.iter() {
assert!(trkpt_stats.speed.is_some()); assert!(trkpt_stats.speed.is_some());
+22 -12
View File
@@ -18,8 +18,15 @@ pub fn distance(p1: LngLat, p2: LngLat) -> f64 {
EARTH_RADIUS * c EARTH_RADIUS * c
} }
pub fn time_diff(a: &Option<i64>, b: &Option<i64>) -> Option<i32> {
match (a, b) {
(Some(t1), Some(t2)) => Some((t1 - t2) as i32),
_ => None,
}
}
/// Computes the speed for a given distance in kilometers and a time in milliseconds /// Computes the speed for a given distance in kilometers and a time in milliseconds
pub fn speed(distance: f64, time: i64) -> f64 { pub fn speed(distance: f64, time: i32) -> f64 {
distance / (time as f64 / 3600_000.0) distance / (time as f64 / 3600_000.0)
} }
@@ -40,7 +47,7 @@ fn get_meters_per_longitude_degree(latitude: f64) -> f64 {
// Calculates the point on the line segment defined by p1 and p2 // Calculates the point on the line segment defined by p1 and p2
// that is closest to the third point, p3. // that is closest to the third point, p3.
// Uses simple planar geometry (ignores earth curvature). // Uses simple planar geometry (ignores earth curvature).
fn projected(p1: LngLat, p2: LngLat, coord3: LngLat) -> LngLat { fn projected(p1: LngLat, p2: LngLat, p3: LngLat) -> LngLat {
// Convert to meters using approximate scaling // Convert to meters using approximate scaling
let meters_per_longitude_degree = get_meters_per_longitude_degree(p1.lat); let meters_per_longitude_degree = get_meters_per_longitude_degree(p1.lat);
@@ -48,8 +55,8 @@ fn projected(p1: LngLat, p2: LngLat, coord3: LngLat) -> LngLat {
let y1 = p1.lat * METERS_PER_LATITUDE_DEGREE; let y1 = p1.lat * METERS_PER_LATITUDE_DEGREE;
let x2 = p2.lng * meters_per_longitude_degree; let x2 = p2.lng * meters_per_longitude_degree;
let y2 = p2.lat * METERS_PER_LATITUDE_DEGREE; let y2 = p2.lat * METERS_PER_LATITUDE_DEGREE;
let x3 = coord3.lng * meters_per_longitude_degree; let x3 = p3.lng * meters_per_longitude_degree;
let y3 = coord3.lat * METERS_PER_LATITUDE_DEGREE; let y3 = p3.lat * METERS_PER_LATITUDE_DEGREE;
let dx = x2 - x1; let dx = x2 - x1;
let dy = y2 - y1; let dy = y2 - y1;
@@ -78,17 +85,20 @@ fn projected(p1: LngLat, p2: LngLat, coord3: LngLat) -> LngLat {
/// Calculates the perpendicular distance in meters /// Calculates the perpendicular distance in meters
/// between a line segment (defined by p1 and p2) and a third point, p3. /// between a line segment (defined by p1 and p2) and a third point, p3.
/// Uses simple planar geometry (ignores earth curvature). /// Uses simple planar geometry (ignores earth curvature).
fn crossarc(p1: LngLat, p2: LngLat, p3: LngLat) -> f64 { fn crossarc_lnglat(p1: LngLat, p2: LngLat, p3: LngLat) -> f64 {
// Convert to meters using approximate scaling // Convert to meters using approximate scaling
let meters_per_longitude_degree = get_meters_per_longitude_degree(p1.lat); let meters_per_longitude_degree = get_meters_per_longitude_degree(p1.lat);
crossarc(
p1.lng * meters_per_longitude_degree,
p1.lat * METERS_PER_LATITUDE_DEGREE,
p2.lng * meters_per_longitude_degree,
p2.lat * METERS_PER_LATITUDE_DEGREE,
p3.lng * meters_per_longitude_degree,
p3.lat * METERS_PER_LATITUDE_DEGREE,
)
}
let x1 = p1.lng * meters_per_longitude_degree; pub fn crossarc(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> f64 {
let y1 = p1.lat * METERS_PER_LATITUDE_DEGREE;
let x2 = p2.lng * meters_per_longitude_degree;
let y2 = p2.lat * METERS_PER_LATITUDE_DEGREE;
let x3 = p3.lng * meters_per_longitude_degree;
let y3 = p3.lat * METERS_PER_LATITUDE_DEGREE;
let dx = x2 - x1; let dx = x2 - x1;
let dy = y2 - y1; let dy = y2 - y1;
let segment_length_squared = dx * dx + dy * dy; let segment_length_squared = dx * dx + dy * dy;