mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-01 12:17:18 +00:00
Migration to new Rust-based engine (#401)
* wasm first steps * parsing start * parsing continued * progress * progress * progress * progress * progress * progress * progress * progress * fixes * reorganize * change type * fix * progress * refactoring * progress * progress * progress * keep style info * progress * progress * remove (copy) in name * progress * merge methods into 1 * splice * progress * progress * progress * cleaning * progress * progress * progress * visibility * progress * progress * progress * progress * progress * fixes * progress * fix markers * fix * progress * progress * progress * progress * progress * progress * small fix * use engine info * fix popups * small fix * progress * progress * progress * clean tool * handle self closing elements * reduce data exchanges across boundary * copy on wasm side to avoid any data corruption * speed fix * routing * refactoring * reverse * reverse * crop * split * merge * progress * time tool * fix bounds for waypoints * reduce * fix reactivity * handle corrupt time data * export * progress * remove gpx js package * remove todo * update readme * cleaning * progress * fix link fields * remove unused fields * refactoring and tests * import and export of routes, ref #192 * per-chunk categories storage * cross-tab sync * outdated state * refactoring * reorder methods * refactoring * fix * improve flow * renaming * progress * progress * fix * progress * per file history * keep original flag * use history variant * fix * progress * file explorer * progress * fix * sticky folders * improved sticky folders * fix sortable * fix sortable * fix * fix tab size * no file delete on desktop * fix routing on other file * fix sorting * remember language * open links in browser * commit missing changes * avoid selecting on resize * clarify filename * explorer actions * fix folder styling * drop anywhere in folder * cleaning * fixes * remove focus-ring * full screen * small fix * consistent highlight color * improved preview * performance improvements * sparser data transfers * sparser data transfers * show popup once data is loaded * fix routing control size * fix * sparse desktop build * remove custom ign styles * use new endpoint for openhikingmap * strip native menu * reorder context menu items * route full support * route - track conversion * create route file action * route export, filter out anchors at high zoom level * clarify name handling * move hide & center to view * routes continued * update docs * fix power format, closes #378 * lint fixes * fix display of additional data with gaps, closes #361 * fix linting issues * fix import modules * fix multidrag * change base to resolve * fix type errors * fix layer typings * fix last type error * each keys * improve typings * deploy to cloudflare --------- Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
1 parent
6a0d434371
commit
8fe7d8d64d
484 files changed
+53949
-41152
No files matched your search
@@ -0,0 +1,191 @@
|
||||
use crate::{
|
||||
Apply, CommandError, State, TrackSegment, artificial_segment_times, changed_segment_times,
|
||||
update_segments,
|
||||
};
|
||||
|
||||
/// How the timestamps of the selection are set.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TimeKind {
|
||||
/// Starts at `Time::start_time`. The trackpoints that have timestamps keep their durations
|
||||
/// multiplied by `ratio`, and the ones that have none follow the previous one at `speed`
|
||||
/// (km/h).
|
||||
Change { speed: f64, ratio: f64 },
|
||||
/// Makes timestamps up for the whole selection, which lasts `total_time` seconds: the longer
|
||||
/// and the steeper an interval, the more time it takes.
|
||||
Artificial { total_time: f64 },
|
||||
}
|
||||
|
||||
/// Sets the timestamps of the selected segments, one after the other: each one goes on from the
|
||||
/// end of the previous one, the first one starts at `start_time` (in milliseconds).
|
||||
#[derive(Debug)]
|
||||
pub struct Time {
|
||||
pub start_time: i64,
|
||||
pub kind: TimeKind,
|
||||
}
|
||||
|
||||
impl Apply for Time {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
match self.kind {
|
||||
TimeKind::Change { speed, ratio } => {
|
||||
if !(speed.is_finite() && speed > 0.0 && ratio.is_finite() && ratio > 0.0) {
|
||||
return Err(CommandError::InvalidData(format!(
|
||||
"speed {speed} and ratio {ratio} should be positive"
|
||||
)));
|
||||
}
|
||||
}
|
||||
TimeKind::Artificial { total_time } => {
|
||||
if !(total_time.is_finite() && total_time >= 0.0) {
|
||||
return Err(CommandError::InvalidData(format!(
|
||||
"total time {total_time} should not be negative"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let locations = state
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0);
|
||||
let segments: Vec<&TrackSegment> =
|
||||
locations.iter().map(|l| l.segment(state.files)).collect();
|
||||
if segments.iter().all(|segment| segment.is_empty()) {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
let times = match self.kind {
|
||||
TimeKind::Change { speed, ratio } => {
|
||||
changed_segment_times(&segments, self.start_time, speed, ratio)
|
||||
}
|
||||
TimeKind::Artificial { total_time } => {
|
||||
artificial_segment_times(&segments, self.start_time, total_time)
|
||||
}
|
||||
};
|
||||
|
||||
update_segments(state, &locations, |i, segment| {
|
||||
let times = ×[i];
|
||||
segment.update_all(|j, pt| pt.time = times.get(j).copied().flatten());
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, Load, Selection, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded(path: &str) -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read(path).unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn all_times(fx: &Fixture, id: FileId) -> Vec<Option<i64>> {
|
||||
fx.files[&id]
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|trk| {
|
||||
trk.trkseg
|
||||
.iter()
|
||||
.flat_map(|seg| seg.iter().map(|pt| pt.time))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artificial_timestamps_last_the_total_time() {
|
||||
let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx");
|
||||
|
||||
Time {
|
||||
start_time: 1_000_000,
|
||||
kind: TimeKind::Artificial { total_time: 3600.0 },
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let times: Vec<i64> = all_times(&fx, id).into_iter().map(Option::unwrap).collect();
|
||||
assert_eq!(times[0], 1_000_000);
|
||||
assert!(times.windows(2).all(|w| w[0] <= w[1]));
|
||||
assert!((times.last().unwrap() - 1_000_000 - 3_600_000).abs() <= times.len() as i64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_change_starts_at_the_start_time_and_follows_the_speed() {
|
||||
let (mut fx, id) = loaded("data/simple.gpx");
|
||||
Time {
|
||||
start_time: 5_000,
|
||||
kind: TimeKind::Change {
|
||||
speed: 10.0,
|
||||
ratio: 1.0,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let times: Vec<i64> = all_times(&fx, id).into_iter().map(Option::unwrap).collect();
|
||||
assert_eq!(times[0], 5_000);
|
||||
assert!(times.windows(2).all(|w| w[0] <= w[1]));
|
||||
assert!(times.last() > times.first());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_change_shifts_existing_timestamps_and_scales_them() {
|
||||
let (mut fx, id) = loaded("data/with_time.gpx");
|
||||
let before: Vec<i64> = all_times(&fx, id).into_iter().map(Option::unwrap).collect();
|
||||
|
||||
Time {
|
||||
start_time: 0,
|
||||
kind: TimeKind::Change {
|
||||
speed: 10.0,
|
||||
ratio: 2.0,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let after: Vec<i64> = all_times(&fx, id).into_iter().map(Option::unwrap).collect();
|
||||
assert_eq!(after[0], 0);
|
||||
let (d_before, d_after) = (before[1] - before[0], after[1] - after[0]);
|
||||
assert!((d_after - 2 * d_before).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_input_and_empty_selection() {
|
||||
let (mut fx, id) = loaded("data/simple.gpx");
|
||||
let before = all_times(&fx, id);
|
||||
assert!(matches!(
|
||||
Time {
|
||||
start_time: 0,
|
||||
kind: TimeKind::Change {
|
||||
speed: 0.0,
|
||||
ratio: 1.0
|
||||
}
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::InvalidData(_))
|
||||
));
|
||||
assert_eq!(all_times(&fx, id), before);
|
||||
|
||||
fx.selection = Selection::Empty;
|
||||
assert!(matches!(
|
||||
Time {
|
||||
start_time: 0,
|
||||
kind: TimeKind::Artificial { total_time: 10.0 }
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user