diff --git a/gpx-rs/engine/src/engine/command/tools/elevation.rs b/gpx-rs/engine/src/engine/command/tools/elevation.rs index e118d4c9e..6d6828e56 100644 --- a/gpx-rs/engine/src/engine/command/tools/elevation.rs +++ b/gpx-rs/engine/src/engine/command/tools/elevation.rs @@ -1,13 +1,143 @@ -use crate::{Apply, CommandError, State}; +use std::rc::Rc; +use crate::{Apply, CommandError, File, State}; + +/// Sets the elevation of the trackpoints of the selected segments (the ones of the statistics of +/// the selection, in the same order): `ele` has one elevation per trackpoint. #[derive(Debug)] pub struct Elevation<'a> { pub ele: &'a [f64], } impl Apply for Elevation<'_> { - fn apply(self, _state: &mut State) -> Result<(), CommandError> { - // TODO - Err(CommandError::NotImplemented("elevation")) + fn apply(self, state: &mut State) -> Result<(), CommandError> { + let locations = state + .selection + .segment_locations(state.files, &state.order.0); + let count: usize = locations + .iter() + .map(|location| { + state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len() + }) + .sum(); + if count == 0 { + return Err(CommandError::NothingToDo); + } + if count != self.ele.len() { + return Err(CommandError::InvalidData(format!( + "{} elevations for {count} trackpoints", + self.ele.len() + ))); + } + + let mut ele = self.ele.iter().copied(); + // the segments of a file are consecutive + for group in locations.chunk_by(|a, b| a.file_id == b.file_id) { + let file_id = group[0].file_id; + let mut file: File = (*state.files[&file_id]).clone(); + for location in group { + let segment = &mut file.trk[location.trk].trkseg[location.seg]; + segment.update_all(|_, pt| pt.ele = ele.next().unwrap_or(pt.ele)); + segment.rev_id = Default::default(); + } + state.files.insert(file_id, Rc::new(file)); + } + 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 trackpoint_elevations(fx: &Fixture, id: FileId) -> Vec { + fx.files[&id] + .trk + .iter() + .flat_map(|trk| { + trk.trkseg + .iter() + .flat_map(|seg| seg.iter().map(|pt| pt.ele)) + }) + .collect() + } + + #[test] + fn test_a_file_gets_elevation_on_all_its_trackpoints() { + let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx"); + let count = trackpoint_elevations(&fx, id).len(); + let ele: Vec = (0..count).map(|i| 100.0 + i as f64).collect(); + + Elevation { ele: &ele }.apply(&mut fx.state()).unwrap(); + + assert_eq!(trackpoint_elevations(&fx, id), ele); + } + + #[test] + fn test_segments_only_touch_their_trackpoints() { + let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx"); + let before = trackpoint_elevations(&fx, id); + let track = &fx.files[&id].trk[0]; + fx.selection = Selection::TrackSegment { + file_id: id, + trk_id: track.id, + trkseg_ids: HashSet::from([track.trkseg[0].id]), + }; + let rev_ids: Vec<_> = track.trkseg.iter().map(|seg| seg.rev_id).collect(); + let len = track.trkseg[0].len(); + + Elevation { + ele: &vec![5.0; len], + } + .apply(&mut fx.state()) + .unwrap(); + + let after = trackpoint_elevations(&fx, id); + assert_eq!(after[..len], vec![5.0; len]); + assert_eq!(after[len..], before[len..]); + let segments = &fx.files[&id].trk[0].trkseg; + assert_ne!(segments[0].rev_id, rev_ids[0]); + assert_eq!(segments[1].rev_id, rev_ids[1]); + } + + #[test] + fn test_wrong_number_of_elevations() { + let (mut fx, id) = loaded("data/simple.gpx"); + let before = trackpoint_elevations(&fx, id); + assert!(matches!( + Elevation { ele: &[1.0] }.apply(&mut fx.state()), + Err(CommandError::InvalidData(_)) + )); + assert_eq!(trackpoint_elevations(&fx, id), before); + } + + #[test] + fn test_nothing_selected() { + let (mut fx, _) = loaded("data/simple.gpx"); + fx.selection = Selection::Empty; + assert!(matches!( + Elevation { ele: &[] }.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + )); } } diff --git a/gpx-rs/engine/src/engine/command/tools/extract.rs b/gpx-rs/engine/src/engine/command/tools/extract.rs index a04273e70..1a6aef282 100644 --- a/gpx-rs/engine/src/engine/command/tools/extract.rs +++ b/gpx-rs/engine/src/engine/command/tools/extract.rs @@ -1,11 +1,570 @@ -use crate::{Apply, CommandError, State}; +use std::{collections::HashSet, rc::Rc}; +use crate::{ + Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId, + TrackSegment, Waypoint, Waypoints, copy_waypoint, crossarc_lnglat, distance, +}; + +/// Splits the selected elements that are made of several segments into elements that have one +/// each: +/// +/// - files: a file with several tracks becomes one file per track, in which the track is split +/// into one track per segment. A file with a single track becomes one file per segment. The new +/// files are in the place of the file, which goes, and are selected. They are named after the +/// track, or the file, and numbered. The waypoints go to the files that have the segments +/// closest to them (to the first file if the file has no trackpoint). +/// - tracks: each selected track is replaced by one track per segment, which are selected. They +/// are named after the track and numbered. +/// +/// What has a single segment is left as it is. There is nothing to do if nothing was changed. #[derive(Debug)] pub struct Extract; impl Apply for Extract { - fn apply(self, _state: &mut State) -> Result<(), CommandError> { - // TODO - Err(CommandError::NotImplemented("extract")) + fn apply(self, state: &mut State) -> Result<(), CommandError> { + let selection = state.selection.clone(); + match &selection { + Selection::File { file_ids } => extract_files(state, file_ids), + Selection::Track { file_id, trk_ids } => extract_tracks(state, *file_id, trk_ids), + _ => Err(CommandError::NothingToDo), + } + } +} + +fn extract_files(state: &mut State, file_ids: &HashSet) -> Result<(), CommandError> { + // the files that have something to extract, with the files that come out of them + let mut extracted: Vec<(FileId, Vec)> = vec![]; + for id in state.order.0.iter().filter(|id| file_ids.contains(id)) { + let file = &state.files[id]; + let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum(); + if segments > 1 { + extracted.push((*id, extract_file(file))); + } + } + if extracted.is_empty() { + return Err(CommandError::NothingToDo); + } + + let mut order = vec![]; + for id in state.order.0.iter() { + match extracted.iter().find(|(original, _)| original == id) { + Some((_, files)) => order.extend(files.iter().map(|file| file.id)), + None => order.push(*id), + } + } + let mut selected: HashSet = file_ids.clone(); + for (original, files) in extracted { + state.files.remove(&original); + selected.remove(&original); + for file in files { + selected.insert(file.id); + state.files.insert(file.id, Rc::new(file)); + } + } + state.order.0 = order; + *state.selection = Selection::File { file_ids: selected }; + Ok(()) +} + +/// The files that a file with several segments becomes. +fn extract_file(file: &File) -> Vec { + // for each new file: its tracks and its name, and the segments that go with it + struct Part { + tracks: Vec, + name: String, + } + let (parts, owner): (Vec, Vec) = if file.trk.len() > 1 { + // one file per track, the segments of a track follow it + let parts = file + .trk + .iter() + .enumerate() + .map(|(i, track)| Part { + tracks: split_track(track), + name: track + .info + .name + .clone() + .unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1)), + }) + .collect(); + let owner = file + .trk + .iter() + .enumerate() + .flat_map(|(i, track)| track.trkseg.iter().map(move |_| i)) + .collect(); + (parts, owner) + } else { + // one file per segment + let track = &file.trk[0]; + let base = track + .info + .name + .clone() + .unwrap_or_else(|| file.info.name.clone()); + let parts = track + .trkseg + .iter() + .enumerate() + .map(|(i, segment)| Part { + tracks: vec![Track { + id: if i == 0 { track.id } else { Default::default() }, + trkseg: vec![segment.clone()], + ..track.clone() + }], + name: format!("{base} ({})", i + 1), + }) + .collect(); + let owner = (0..track.trkseg.len()).collect(); + (parts, owner) + }; + + // the waypoints go with the segments that are the closest to them + let segments: Vec<&TrackSegment> = file.trk.iter().flat_map(|track| &track.trkseg).collect(); + let mut waypoints: Vec> = vec![vec![]; parts.len()]; + for waypoint in file.wpt.iter() { + let mut owners: Vec = closest_segments(&segments, waypoint.coordinates) + .into_iter() + .map(|segment| owner[segment]) + .collect(); + owners.sort_unstable(); + owners.dedup(); + if owners.is_empty() { + owners.push(0); + } + for (n, part) in owners.into_iter().enumerate() { + // a waypoint that goes to several files has a new id in all but the first + waypoints[part].push(if n == 0 { + waypoint.clone() + } else { + copy_waypoint(waypoint) + }); + } + } + + parts + .into_iter() + .zip(waypoints) + .map(|(part, waypoints)| { + let mut extracted = File { + id: Default::default(), + trk: part.tracks, + wpt: Waypoints::default(), + ..file.clone() + }; + extracted.info.name = part.name; + extracted.wpt.insert_at(0, waypoints); + extracted + }) + .collect() +} + +fn extract_tracks( + state: &mut State, + file_id: FileId, + trk_ids: &HashSet, +) -> Result<(), CommandError> { + let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?; + let mut changed = false; + let mut tracks = vec![]; + let mut selected: HashSet = trk_ids.clone(); + for track in &file.trk { + if trk_ids.contains(&track.id) && track.trkseg.len() > 1 { + changed = true; + selected.remove(&track.id); + let pieces = split_track(track); + selected.extend(pieces.iter().map(|piece| piece.id)); + tracks.extend(pieces); + } else { + tracks.push(track.clone()); + } + } + if !changed { + return Err(CommandError::NothingToDo); + } + let mut file = (**file).clone(); + file.trk = tracks; + state.files.insert(file_id, Rc::new(file)); + *state.selection = Selection::Track { + file_id, + trk_ids: selected, + }; + Ok(()) +} + +/// The tracks that a track becomes: one per segment, the first one with the id of the track. The +/// others have the same info, with the name of the track and the number of the segment, if it has +/// a name. A track with at most one segment stays as it is. +fn split_track(track: &Track) -> Vec { + if track.trkseg.len() <= 1 { + return vec![track.clone()]; + } + track + .trkseg + .iter() + .enumerate() + .map(|(i, segment)| { + let mut piece = Track { + id: if i == 0 { track.id } else { Default::default() }, + trkseg: vec![segment.clone()], + ..track.clone() + }; + piece.info.name = track + .info + .name + .as_ref() + .map(|name| format!("{name} ({})", i + 1)); + piece + }) + .collect() +} + +/// The indices of the segments that have the trackpoint that is the closest to `point`, which are +/// several when the closest trackpoints are as close. +/// +/// The segments are looked at from the closest to the farthest bounds, which is how they are +/// left out: the ones whose bounds are farther than the closest trackpoint found. +fn closest_segments(segments: &[&TrackSegment], point: LngLat) -> Vec { + let mut by_bounds: Vec<(f64, usize)> = segments + .iter() + .enumerate() + .filter(|(_, segment)| !segment.is_empty()) + .map(|(i, segment)| { + let mut bounds = LngLatBounds::default(); + segment + .iter() + .for_each(|trkpt| bounds.extend(trkpt.coordinates)); + (distance_to_bounds(&bounds, point), i) + }) + .collect(); + by_bounds.sort_by(|a, b| a.0.total_cmp(&b.0)); + + let mut closest = f64::MAX; + let mut indices: Vec = vec![]; + for (bounds_distance, i) in by_bounds { + if bounds_distance > closest { + break; + } + for trkpt in segments[i].iter() { + // in meters, like the distances to the bounds + let d = distance(trkpt.coordinates, point) * 1000.0; + if d < closest { + closest = d; + indices = vec![i]; + } else if d == closest { + indices.push(i); + } + } + } + indices.sort_unstable(); + indices.dedup(); + indices +} + +/// 0 inside the bounds, else the distance in meters to the closest side. +fn distance_to_bounds(bounds: &LngLatBounds, point: LngLat) -> f64 { + let (sw, ne) = (bounds.sw, bounds.ne); + if (sw.lng..=ne.lng).contains(&point.lng) && (sw.lat..=ne.lat).contains(&point.lat) { + return 0.0; + } + let nw = LngLat { + lng: sw.lng, + lat: ne.lat, + }; + let se = LngLat { + lng: ne.lng, + lat: sw.lat, + }; + [(nw, ne), (ne, se), (se, sw), (sw, nw)] + .into_iter() + .map(|(a, b)| crossarc_lnglat(a, b, point)) + .fold(f64::MAX, f64::min) +} + +#[cfg(test)] +mod tests { + use crate::{TrackInfo, Trackpoint, engine::command::fixture::Fixture}; + + use super::*; + + /// A segment of 5 trackpoints along the equator, from the longitude `from`. + fn segment(from: f64) -> TrackSegment { + let mut segment = TrackSegment::default(); + let points = (0..5) + .map(|i| Trackpoint { + coordinates: LngLat { + lng: from + i as f64 * 0.001, + lat: 0.0, + }, + ..Default::default() + }) + .collect(); + segment.splice(0, 0, points); + segment + } + + fn track(name: Option<&str>, from: &[f64]) -> Track { + Track { + info: TrackInfo { + name: name.map(str::to_string), + ..Default::default() + }, + trkseg: from.iter().map(|from| segment(*from)).collect(), + ..Default::default() + } + } + + fn waypoint(name: &str, lng: f64) -> Waypoint { + Waypoint { + name: Some(name.into()), + coordinates: LngLat { lng, lat: 0.001 }, + ..Default::default() + } + } + + fn fixture(file: File) -> (Fixture, FileId) { + let mut fx = Fixture::default(); + let id = file.id; + fx.files.insert(id, Rc::new(file)); + fx.order.0.push(id); + fx.selection = Selection::File { + file_ids: [id].into(), + }; + (fx, id) + } + + fn file(name: &str, tracks: Vec, waypoints: Vec) -> File { + let mut file = File { + trk: tracks, + ..Default::default() + }; + file.info.name = name.into(); + file.wpt.insert_at(0, waypoints); + file + } + + fn names(fx: &Fixture) -> Vec { + fx.order + .0 + .iter() + .map(|id| fx.files[id].info.name.clone()) + .collect() + } + + #[test] + fn test_a_file_with_one_track_becomes_one_file_per_segment() { + let original = file( + "route", + vec![track(Some("climb"), &[0.0, 10.0, 20.0])], + // near the first segment, near the last one, and in between, closer to the middle one + vec![ + waypoint("a", 0.002), + waypoint("c", 20.002), + waypoint("b", 10.0), + ], + ); + let track_id = original.trk[0].id; + let segment_ids: Vec<_> = original.trk[0].trkseg.iter().map(|s| s.id).collect(); + let (mut fx, id) = fixture(original); + let other = file("other", vec![track(None, &[50.0])], vec![]); + let other_id = other.id; + fx.files.insert(other_id, Rc::new(other)); + fx.order.0.push(other_id); + + Extract.apply(&mut fx.state()).unwrap(); + + // in the place of the file, in order, named after the track + assert!(!fx.files.contains_key(&id)); + assert_eq!(names(&fx), ["climb (1)", "climb (2)", "climb (3)", "other"]); + assert_eq!(fx.order.0[3], other_id); + for (i, file_id) in fx.order.0[..3].iter().enumerate() { + let file = &fx.files[file_id]; + assert_eq!(file.trk.len(), 1); + assert_eq!(file.trk[0].trkseg.len(), 1); + assert_eq!(file.trk[0].trkseg[0].id, segment_ids[i]); + assert_eq!(file.trk[0].info.name.as_deref(), Some("climb")); + } + assert_eq!(fx.files[&fx.order.0[0]].trk[0].id, track_id); + // the waypoints are with the closest segment + let waypoints = |i: usize| -> Vec { + fx.files[&fx.order.0[i]] + .wpt + .iter() + .map(|w| w.name.clone().unwrap()) + .collect() + }; + assert_eq!(waypoints(0), ["a"]); + assert_eq!(waypoints(1), ["b"]); + assert_eq!(waypoints(2), ["c"]); + // the new files are selected, the other one is not + assert!(matches!(&fx.selection, Selection::File { file_ids } + if file_ids.len() == 3 && !file_ids.contains(&other_id))); + } + + #[test] + fn test_a_file_with_several_tracks_becomes_one_file_per_track() { + let original = file( + "route", + vec![ + track(Some("first"), &[0.0, 10.0]), + track(None, &[20.0]), + track(Some("third"), &[30.0, 40.0]), + ], + vec![], + ); + let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect(); + let (mut fx, _) = fixture(original); + Extract.apply(&mut fx.state()).unwrap(); + + // named after the track, or the file + assert_eq!(names(&fx), ["first", "route (2)", "third"]); + let tracks = |i: usize| &fx.files[&fx.order.0[i]].trk; + // a track with several segments is split in as many tracks + assert_eq!(tracks(0).len(), 2); + assert_eq!(tracks(0)[0].info.name.as_deref(), Some("first (1)")); + assert_eq!(tracks(0)[1].info.name.as_deref(), Some("first (2)")); + assert_eq!(tracks(0)[0].id, ids[0]); + // and one that has a single segment is not renamed + assert_eq!(tracks(1).len(), 1); + assert_eq!(tracks(1)[0].id, ids[1]); + assert_eq!(tracks(1)[0].info.name, None); + assert_eq!(tracks(2).len(), 2); + } + + #[test] + fn test_waypoints_with_closest_segments_that_tie_are_copied() { + // the segments share a trackpoint, where the waypoint is: it is as close to both + let a = segment(0.0); + let b = segment(a[4].coordinates.lng); + let shared = Waypoint { + name: Some("mid".into()), + coordinates: a[4].coordinates, + ..Default::default() + }; + let original = file( + "f", + vec![Track { + trkseg: vec![a, b], + ..Default::default() + }], + vec![shared, waypoint("lost", 100.0)], + ); + let (mut fx, _) = fixture(original); + Extract.apply(&mut fx.state()).unwrap(); + let first = &fx.files[&fx.order.0[0]]; + let second = &fx.files[&fx.order.0[1]]; + // the tie goes to both, with different ids + let mid_in = |f: &File| { + f.wpt + .iter() + .find(|w| w.name.as_deref() == Some("mid")) + .map(|w| w.id) + }; + assert!(mid_in(first).is_some() && mid_in(second).is_some()); + assert_ne!(mid_in(first), mid_in(second)); + // a far waypoint goes to its closest segment, which is the last one + assert!(second.wpt.iter().any(|w| w.name.as_deref() == Some("lost"))); + } + + #[test] + fn test_a_waypoint_of_a_file_without_trackpoints_is_kept() { + let mut empty = TrackSegment::default(); + empty.rev_id = Default::default(); + let original = file( + "f", + vec![Track { + trkseg: vec![empty.clone(), TrackSegment::default()], + ..Default::default() + }], + vec![waypoint("w", 1.0)], + ); + let (mut fx, _) = fixture(original); + Extract.apply(&mut fx.state()).unwrap(); + assert_eq!(fx.files[&fx.order.0[0]].wpt.len(), 1); + assert_eq!(fx.files[&fx.order.0[1]].wpt.len(), 0); + } + + #[test] + fn test_extract_tracks_in_place() { + let original = file( + "route", + vec![ + track(Some("a"), &[0.0]), + track(Some("b"), &[10.0, 20.0, 30.0]), + track(None, &[40.0, 50.0]), + ], + vec![], + ); + let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect(); + let (mut fx, id) = fixture(original); + fx.selection = Selection::Track { + file_id: id, + trk_ids: [ids[1], ids[2]].into(), + }; + + Extract.apply(&mut fx.state()).unwrap(); + + let file = &fx.files[&id]; + let names: Vec<_> = file.trk.iter().map(|t| t.info.name.clone()).collect(); + assert_eq!( + names, + [ + Some("a".into()), + Some("b (1)".into()), + Some("b (2)".into()), + Some("b (3)".into()), + None, + None + ] + ); + assert_eq!(file.trk.len(), 6); + assert!(file.trk.iter().all(|t| t.trkseg.len() == 1)); + assert_eq!(file.trk[0].id, ids[0]); + assert_eq!(file.trk[1].id, ids[1]); + // the new tracks are selected + let Selection::Track { trk_ids, .. } = &fx.selection else { + panic!("tracks are selected"); + }; + assert_eq!(trk_ids.len(), 5); + assert!(trk_ids.contains(&ids[1]) && !trk_ids.contains(&ids[0])); + } + + #[test] + fn test_nothing_to_extract() { + // a single segment + let original = file("f", vec![track(Some("t"), &[0.0])], vec![]); + let track_id = original.trk[0].id; + let segment_id = original.trk[0].trkseg[0].id; + let (mut fx, id) = fixture(original); + let before = fx.files[&id].clone(); + assert_eq!( + Extract.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + fx.selection = Selection::Track { + file_id: id, + trk_ids: [track_id].into(), + }; + assert_eq!( + Extract.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + for selection in [ + Selection::Empty, + Selection::Waypoints { file_id: id }, + Selection::TrackSegment { + file_id: id, + trk_id: track_id, + trkseg_ids: [segment_id].into(), + }, + ] { + fx.selection = selection; + assert_eq!( + Extract.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + } + assert!(Rc::ptr_eq(&fx.files[&id], &before)); } } diff --git a/gpx-rs/wasm/src/lib.rs b/gpx-rs/wasm/src/lib.rs index e74c02bf8..7651011e6 100644 --- a/gpx-rs/wasm/src/lib.rs +++ b/gpx-rs/wasm/src/lib.rs @@ -633,6 +633,7 @@ pub fn extract() -> bool { edit(Command::Extract(engine::Extract)) } +/// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint. #[wasm_bindgen] pub fn elevation(ele: &[f64]) -> bool { edit(Command::Elevation(engine::Elevation { ele })) diff --git a/website/src/lib/components/toolbar/tools/Elevation.svelte b/website/src/lib/components/toolbar/tools/Elevation.svelte index 1217d5cd6..876c8c3da 100644 --- a/website/src/lib/components/toolbar/tools/Elevation.svelte +++ b/website/src/lib/components/toolbar/tools/Elevation.svelte @@ -3,15 +3,28 @@ import Help from '$lib/components/Help.svelte'; import { MountainSnow } from '@lucide/svelte'; import { i18n } from '$lib/i18n.svelte'; - import { getURLForLanguage } from '$lib/utils'; - import { selection } from '$lib/logic/selection'; - import { fileActions } from '$lib/logic/file-actions'; + import { getElevation, getURLForLanguage } from '$lib/utils'; + import { engine } from '$lib/engine'; + import { get } from 'svelte/store'; let props: { class?: string; } = $props(); - let validSelection = $derived($selection.size > 0); + const statistics = engine.statistics; + + let validSelection = $derived($statistics.length > 0); + + async function addElevation() { + const stats = get(statistics); + const points = Array.from(stats.lng, (lon, i) => ({ lon, lat: stats.lat[i] })); + const ele = await getElevation(points); + + // the selection or the files may have changed while the elevation was loading + if (get(statistics) === stats) { + await engine.setElevation(Float64Array.from(ele)); + } + }
@@ -19,7 +32,7 @@ variant="outline" class="whitespace-normal h-fit min-h-8 py-1" disabled={!validSelection} - onclick={() => fileActions.addElevationToSelection()} + onclick={() => addElevation()} > {i18n._('toolbar.elevation.button')} diff --git a/website/src/lib/components/toolbar/tools/Extract.svelte b/website/src/lib/components/toolbar/tools/Extract.svelte index 3f8601d5f..cdaf9be6e 100644 --- a/website/src/lib/components/toolbar/tools/Extract.svelte +++ b/website/src/lib/components/toolbar/tools/Extract.svelte @@ -1,51 +1,54 @@
- diff --git a/website/src/lib/engine.ts b/website/src/lib/engine.ts index b8a012717..97bd59201 100644 --- a/website/src/lib/engine.ts +++ b/website/src/lib/engine.ts @@ -654,6 +654,24 @@ class Engine { return this.run((w) => w.reverse()); } + /** + * Splits the selected files and tracks that have several segments into elements that have + * one each (one file per track or per segment, one track per segment), which get selected. + * Resolves to `false` when there is nothing to extract. + */ + extract() { + return this.run((w) => w.extract()); + } + + /** + * Sets the elevation of the trackpoints of the selection, one elevation (m) per trackpoint, in + * the order of the statistics. Resolves to `false` when there is nothing to set, or when the + * number of elevations is not the number of trackpoints. + */ + setElevation(ele: Float64Array) { + return this.run((w) => w.elevation(ele)); + } + /** * Merges the selected files, tracks or segments into the first of them (see `MergeType`). With * `removeGaps`, the parts that are connected get closer in time if they have timestamps. diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index 2a167f887..aff8d0ff6 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -99,90 +99,6 @@ export async function loadFiles(list: FileList | File[]): Promise { // Helper functions for file operations export const fileActions = { - extractSelection: () => { - return fileActionManager.applyGlobal((draft) => { - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - if (level === ListLevel.FILE) { - let file = fileStateCollection.getFile(fileId); - let statistics = fileStateCollection.getStatistics(fileId); - if (file && statistics) { - if (file.trk.length > 1) { - let fileIds = getFileIds(file.trk.length); - let closest = file.wpt.map((wpt) => - getClosestTrackSegments(file, statistics, wpt.getCoordinates()) - ); - file.trk.forEach((track, index) => { - let newFile = file.clone(); - let tracks = track.trkseg.map((segment, segmentIndex) => { - let t = track.clone(); - t.replaceTrackSegments(0, track.trkseg.length - 1, [segment]); - if (track.name) { - t.name = `${track.name} (${segmentIndex + 1})`; - } - return t; - }); - newFile.replaceTracks(0, file.trk.length - 1, tracks); - newFile.replaceWaypoints( - 0, - file.wpt.length - 1, - file.wpt.filter((wpt, wptIndex) => - closest[wptIndex].some( - ([trackIndex, segmentIndex]) => trackIndex === index - ) - ) - ); - newFile._data.id = fileIds[index]; - newFile.metadata.name = - track.name ?? `${file.metadata.name} (${index + 1})`; - draft.set(newFile._data.id, freeze(newFile)); - }); - } else if (file.trk.length === 1) { - let fileIds = getFileIds(file.trk[0].trkseg.length); - let closest = file.wpt.map((wpt) => - getClosestTrackSegments(file, statistics, wpt.getCoordinates()) - ); - file.trk[0].trkseg.forEach((segment, index) => { - let newFile = file.clone(); - newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [ - segment, - ]); - newFile.replaceWaypoints( - 0, - file.wpt.length - 1, - file.wpt.filter((wpt, wptIndex) => - closest[wptIndex].some( - ([trackIndex, segmentIndex]) => segmentIndex === index - ) - ) - ); - newFile._data.id = fileIds[index]; - newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`; - draft.set(newFile._data.id, freeze(newFile)); - }); - } - draft.delete(fileId); - } - } else if (level === ListLevel.TRACK) { - let file = draft.get(fileId); - if (file) { - for (let item of items) { - let trackIndex = (item as ListTrackItem).getTrackIndex(); - let track = file.trk[trackIndex]; - let tracks = track.trkseg.map((segment, segmentIndex) => { - let t = track.clone(); - t.replaceTrackSegments(0, track.trkseg.length - 1, [segment]); - if (track.name) { - t.name = `${track.name} (${segmentIndex + 1})`; - } - return t; - }); - file.replaceTracks(trackIndex, trackIndex, tracks); - } - } - } - }); - }); - }, reduce: (itemsAndPoints: Map) => { if (itemsAndPoints.size === 0) { return; @@ -214,72 +130,4 @@ export const fileActions = { }); }); }, - addElevationToSelection: async () => { - if (get(selection).size === 0) { - return; - } - let points: (TrackPoint | Waypoint)[] = []; - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = fileStateCollection.getFile(fileId); - if (file) { - if (level === ListLevel.FILE) { - points.push(...file.getTrackPoints()); - points.push(...file.wpt); - } else if (level === ListLevel.TRACK) { - let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex()); - trackIndices.forEach((trackIndex) => { - points.push(...file.trk[trackIndex].getTrackPoints()); - }); - } else if (level === ListLevel.SEGMENT) { - let trackIndex = (items[0] as ListTrackSegmentItem).getTrackIndex(); - let segmentIndices = items.map((item) => - (item as ListTrackSegmentItem).getSegmentIndex() - ); - segmentIndices.forEach((segmentIndex) => { - points.push(...file.trk[trackIndex].trkseg[segmentIndex].getTrackPoints()); - }); - } else if (level === ListLevel.WAYPOINTS) { - points.push(...file.wpt); - } else if (level === ListLevel.WAYPOINT) { - let waypointIndices = items.map((item) => - (item as ListWaypointItem).getWaypointIndex() - ); - points.push(...waypointIndices.map((waypointIndex) => file.wpt[waypointIndex])); - } - } - }); - if (points.length === 0) { - return; - } - getElevation(points).then((elevations) => { - fileActionManager.applyGlobal((draft) => { - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = draft.get(fileId); - if (file) { - if (level === ListLevel.FILE) { - file.addElevation(elevations); - } else if (level === ListLevel.TRACK) { - let trackIndices = items.map((item) => - (item as ListTrackItem).getTrackIndex() - ); - file.addElevation(elevations, trackIndices, undefined, []); - } else if (level === ListLevel.SEGMENT) { - let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()]; - let segmentIndices = items.map((item) => - (item as ListTrackSegmentItem).getSegmentIndex() - ); - file.addElevation(elevations, trackIndices, segmentIndices, []); - } else if (level === ListLevel.WAYPOINTS) { - file.addElevation(elevations, [], [], undefined); - } else if (level === ListLevel.WAYPOINT) { - let waypointIndices = items.map((item) => - (item as ListWaypointItem).getWaypointIndex() - ); - file.addElevation(elevations, [], [], waypointIndices); - } - } - }); - }); - }); - }, };