mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-09 23:26:31 +00:00
split
This commit is contained in:
1 parent
e07ec97e12
commit
24b877c4ee
8 files changed
+532
-180
No files matched your search
@@ -1,13 +1,332 @@
|
|||||||
use crate::{Apply, CommandError, State};
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Apply, CommandError, File, State, Track, TrackSegment, Waypoints, copy_segment, copy_track,
|
||||||
|
copy_waypoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::route::find_target;
|
||||||
|
|
||||||
|
/// What is cut in two where the trackpoint is.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SplitType {
|
||||||
|
/// The file becomes two files, which both have the waypoints (copies, in the second one).
|
||||||
|
Files,
|
||||||
|
/// The track becomes two tracks.
|
||||||
|
Tracks,
|
||||||
|
/// The segment becomes two segments.
|
||||||
|
Segments,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cuts the file, the track or the segment of the trackpoint `at` of the selection (see
|
||||||
|
/// `RoutingBuffer`) in two, there: the trackpoint is the last one of the first part and the first
|
||||||
|
/// one of the second part.
|
||||||
|
///
|
||||||
|
/// The first part keeps the ids of what is cut, the second one has new ids, as the elements that
|
||||||
|
/// come after the trackpoint (the following segments of the track, the following tracks of the
|
||||||
|
/// file), which are moved to it. When the file is cut, the new file comes right after it. The
|
||||||
|
/// selection does not change.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Split {
|
pub struct Split {
|
||||||
pub at: u32,
|
pub at: u32,
|
||||||
|
pub split_type: SplitType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Apply for Split {
|
impl Apply for Split {
|
||||||
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
// TODO
|
let at = self.at as usize;
|
||||||
Err(CommandError::NotImplemented("split"))
|
let target = find_target(state, at, at)?.ok_or(CommandError::NothingToDo)?;
|
||||||
|
let location = target.location;
|
||||||
|
let (file_id, trk, seg, index) =
|
||||||
|
(location.file_id, location.trk, location.seg, target.start);
|
||||||
|
let mut file = (*state.files[&file_id]).clone();
|
||||||
|
if index >= file.trk[trk].trkseg[seg].len() {
|
||||||
|
return Err(CommandError::InvalidData(
|
||||||
|
"trackpoint out of the segment".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let track = file.trk[trk].clone();
|
||||||
|
let segment = &track.trkseg[seg];
|
||||||
|
let (first, second) = (keep_up_to(segment, index), keep_from(segment, index));
|
||||||
|
// the track is cut: what is before the segment, and its first part, then the second part
|
||||||
|
// and what is after it
|
||||||
|
let split_track = || {
|
||||||
|
let mut first_track = track.clone();
|
||||||
|
first_track.trkseg.truncate(seg);
|
||||||
|
first_track.trkseg.push(first.clone());
|
||||||
|
let mut second_track = Track {
|
||||||
|
id: Default::default(),
|
||||||
|
trkseg: vec![second.clone()],
|
||||||
|
..track.clone()
|
||||||
|
};
|
||||||
|
second_track
|
||||||
|
.trkseg
|
||||||
|
.extend(track.trkseg[seg + 1..].iter().map(copy_segment));
|
||||||
|
(first_track, second_track)
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.split_type {
|
||||||
|
SplitType::Segments => {
|
||||||
|
file.trk[trk].trkseg.splice(seg..=seg, [first, second]);
|
||||||
|
}
|
||||||
|
SplitType::Tracks => {
|
||||||
|
let (first_track, second_track) = split_track();
|
||||||
|
file.trk.splice(trk..=trk, [first_track, second_track]);
|
||||||
|
}
|
||||||
|
SplitType::Files => {
|
||||||
|
let (first_track, second_track) = split_track();
|
||||||
|
let mut second_file = File {
|
||||||
|
id: Default::default(),
|
||||||
|
trk: vec![second_track],
|
||||||
|
wpt: Waypoints::default(),
|
||||||
|
..file.clone()
|
||||||
|
};
|
||||||
|
second_file
|
||||||
|
.trk
|
||||||
|
.extend(file.trk[trk + 1..].iter().map(copy_track));
|
||||||
|
second_file
|
||||||
|
.wpt
|
||||||
|
.insert_at(0, file.wpt.iter().map(copy_waypoint).collect());
|
||||||
|
|
||||||
|
file.trk.truncate(trk);
|
||||||
|
file.trk.push(first_track);
|
||||||
|
let position = state.order.0.iter().position(|id| *id == file_id);
|
||||||
|
state.order.0.insert(
|
||||||
|
position.map_or(state.order.0.len(), |p| p + 1),
|
||||||
|
second_file.id,
|
||||||
|
);
|
||||||
|
state.files.insert(second_file.id, Rc::new(second_file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.files.insert(file_id, Rc::new(file));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The trackpoints of the segment up to `index` (included), in the same segment.
|
||||||
|
fn keep_up_to(segment: &TrackSegment, index: usize) -> TrackSegment {
|
||||||
|
let mut part = segment.clone();
|
||||||
|
let len = part.len();
|
||||||
|
part.splice(index + 1, len, vec![]);
|
||||||
|
part.rev_id = Default::default();
|
||||||
|
part
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The trackpoints of the segment from `index` (included), in a new segment.
|
||||||
|
fn keep_from(segment: &TrackSegment, index: usize) -> TrackSegment {
|
||||||
|
let mut part = copy_segment(segment);
|
||||||
|
part.splice(0, index, vec![]);
|
||||||
|
part.rev_id = Default::default();
|
||||||
|
part
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{FileId, Load, Selection, TrackSegmentId, 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,
|
||||||
|
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 split(at: u32, split_type: SplitType) -> Split {
|
||||||
|
Split { at, split_type }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lngs(segment: &TrackSegment) -> Vec<f64> {
|
||||||
|
segment.iter().map(|p| p.coordinates.lng).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(file: &File) -> Vec<f64> {
|
||||||
|
file.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| &t.trkseg)
|
||||||
|
.flat_map(lngs)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The index in the selection of the trackpoint `local` of the segment `seg` of the track `trk`.
|
||||||
|
fn index(file: &File, trk: usize, seg: usize, local: usize) -> u32 {
|
||||||
|
let before: usize = file
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(t, track)| {
|
||||||
|
track
|
||||||
|
.trkseg
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(move |(s, segment)| ((t, s), segment.len()))
|
||||||
|
})
|
||||||
|
.take_while(|((t, s), _)| (*t, *s) != (trk, seg))
|
||||||
|
.map(|(_, len)| len)
|
||||||
|
.sum();
|
||||||
|
(before + local) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A segment with enough trackpoints, and its position.
|
||||||
|
fn long_segment(file: &File) -> (usize, usize) {
|
||||||
|
file.trk
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(t, track)| (0..track.trkseg.len()).map(move |s| (t, s)))
|
||||||
|
.find(|(t, s)| file.trk[*t].trkseg[*s].len() > 4)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_segments() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let (trk, seg) = long_segment(&before);
|
||||||
|
let original = &before.trk[trk].trkseg[seg];
|
||||||
|
let len = original.len();
|
||||||
|
|
||||||
|
split(index(&before, trk, seg, 2), SplitType::Segments)
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let after = &fx.files[&id];
|
||||||
|
assert_eq!(after.trk.len(), before.trk.len());
|
||||||
|
let segments = &after.trk[trk].trkseg;
|
||||||
|
assert_eq!(segments.len(), before.trk[trk].trkseg.len() + 1);
|
||||||
|
let (a, b) = (&segments[seg], &segments[seg + 1]);
|
||||||
|
// the point is the end of the first part and the start of the second
|
||||||
|
assert_eq!(lngs(a), lngs(original)[..=2]);
|
||||||
|
assert_eq!(lngs(b), lngs(original)[2..]);
|
||||||
|
assert_eq!(a.len() + b.len(), len + 1);
|
||||||
|
// the first part keeps the id, the second one is new
|
||||||
|
assert_eq!(a.id, original.id);
|
||||||
|
assert_ne!(b.id, original.id);
|
||||||
|
assert_ne!(a.rev_id, original.rev_id);
|
||||||
|
// the ends are anchors
|
||||||
|
assert_eq!(a[a.len() - 1].anchor, Some(0));
|
||||||
|
assert_eq!(b[0].anchor, Some(0));
|
||||||
|
// the rest of the file is untouched
|
||||||
|
for (i, segment) in before.trk[trk].trkseg.iter().enumerate() {
|
||||||
|
if i != seg {
|
||||||
|
let moved = if i > seg { i + 1 } else { i };
|
||||||
|
assert_eq!(segments[moved].rev_id, segment.rev_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_tracks() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let (trk, seg) = long_segment(&before);
|
||||||
|
|
||||||
|
split(index(&before, trk, seg, 3), SplitType::Tracks)
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let after = &fx.files[&id];
|
||||||
|
assert_eq!(after.trk.len(), before.trk.len() + 1);
|
||||||
|
let (a, b) = (&after.trk[trk], &after.trk[trk + 1]);
|
||||||
|
assert_eq!(a.id, before.trk[trk].id);
|
||||||
|
assert_ne!(b.id, a.id);
|
||||||
|
// the segments before are in the first track, the ones after in the second one
|
||||||
|
assert_eq!(a.trkseg.len(), seg + 1);
|
||||||
|
assert_eq!(b.trkseg.len(), before.trk[trk].trkseg.len() - seg);
|
||||||
|
assert_eq!(a.trkseg[..seg], before.trk[trk].trkseg[..seg]);
|
||||||
|
// new ids for everything that moved, so that they are unique
|
||||||
|
let ids: HashSet<TrackSegmentId> = after
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| t.trkseg.iter().map(|s| s.id))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
ids.len(),
|
||||||
|
after.trk.iter().map(|t| t.trkseg.len()).sum::<usize>()
|
||||||
|
);
|
||||||
|
// the path goes on, with the point twice
|
||||||
|
let mut expected = path(&before);
|
||||||
|
let at = index(&before, trk, seg, 3) as usize;
|
||||||
|
expected.insert(at, expected[at]);
|
||||||
|
assert_eq!(path(after), expected);
|
||||||
|
// the info of the track is the same
|
||||||
|
assert_eq!(a.info, b.info);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_files() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let mut file = (*fx.files[&id]).clone();
|
||||||
|
let wpt = crate::Waypoint {
|
||||||
|
name: Some("w".into()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
file.wpt.insert_at(0, vec![wpt.clone()]);
|
||||||
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let (trk, seg) = long_segment(&before);
|
||||||
|
let at = index(&before, trk, seg, 1);
|
||||||
|
|
||||||
|
split(at, SplitType::Files).apply(&mut fx.state()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fx.files.len(), 2);
|
||||||
|
let second_id = fx.order.0[1];
|
||||||
|
assert_eq!(fx.order.0[0], id);
|
||||||
|
assert_ne!(second_id, id);
|
||||||
|
let (a, b) = (&fx.files[&id], &fx.files[&second_id]);
|
||||||
|
// the first file stops at the point, the second one starts there
|
||||||
|
let all = path(&before);
|
||||||
|
assert_eq!(path(a), all[..=at as usize]);
|
||||||
|
assert_eq!(path(b), all[at as usize..]);
|
||||||
|
assert_eq!(a.info, b.info);
|
||||||
|
// each has the waypoints, the copy has its own ids
|
||||||
|
assert_eq!(a.wpt.len(), 1);
|
||||||
|
assert_eq!(b.wpt.len(), 1);
|
||||||
|
assert_ne!(a.wpt[0].id, b.wpt[0].id);
|
||||||
|
assert_eq!(b.wpt[0].name, wpt.name);
|
||||||
|
// nothing is shared by id between the two
|
||||||
|
let ids = |f: &File| -> HashSet<_> {
|
||||||
|
f.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| std::iter::once(t.id.0).chain(t.trkseg.iter().map(|s| s.id.0)))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
assert!(ids(a).is_disjoint(&ids(b)));
|
||||||
|
// the selection is the same
|
||||||
|
assert!(matches!(&fx.selection, Selection::File { file_ids } if file_ids.contains(&id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_or_nothing() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let total = path(&before).len() as u32;
|
||||||
|
assert!(matches!(
|
||||||
|
split(total + 5, SplitType::Segments).apply(&mut fx.state()),
|
||||||
|
Err(CommandError::InvalidData(_))
|
||||||
|
));
|
||||||
|
// after the last trackpoint: appending, not a trackpoint
|
||||||
|
assert!(matches!(
|
||||||
|
split(total, SplitType::Segments).apply(&mut fx.state()),
|
||||||
|
Err(CommandError::InvalidData(_))
|
||||||
|
));
|
||||||
|
fx.selection = Selection::Empty;
|
||||||
|
assert_eq!(
|
||||||
|
split(0, SplitType::Files).apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-2
@@ -23,6 +23,25 @@ pub enum MergeType {
|
|||||||
Group,
|
Group,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a split cuts in two.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum SplitType {
|
||||||
|
Files,
|
||||||
|
Tracks,
|
||||||
|
Segments,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SplitType> for engine::SplitType {
|
||||||
|
fn from(t: SplitType) -> Self {
|
||||||
|
match t {
|
||||||
|
SplitType::Files => Self::Files,
|
||||||
|
SplitType::Tracks => Self::Tracks,
|
||||||
|
SplitType::Segments => Self::Segments,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum CleanType {
|
pub enum CleanType {
|
||||||
@@ -582,9 +601,16 @@ pub fn crop(start: u32, end: u32) -> bool {
|
|||||||
start <= end && edit(Command::Crop(engine::Crop { start, end }))
|
start <= end && edit(Command::Crop(engine::Crop { start, end }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the
|
||||||
|
/// selection in two, there: the trackpoint ends the first part and starts the second one. Does
|
||||||
|
/// nothing, and returns false, if the revision of the routing buffers is not the given one.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn split(at: u32) -> bool {
|
pub fn split(revision: u32, at: u32, split_type: SplitType) -> bool {
|
||||||
edit(Command::Split(engine::Split { at }))
|
routing_revision_is(revision)
|
||||||
|
&& edit(Command::Split(engine::Split {
|
||||||
|
at,
|
||||||
|
split_type: split_type.into(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { isSegmentHidden, isWaypointHidden } from '$lib/file-visibility';
|
|||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
import { splitAtPoint } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||||
|
|
||||||
@@ -415,7 +415,7 @@ export class GPXLayer {
|
|||||||
|
|
||||||
layerOnClick(e: MapLayerMouseEvent) {
|
layerOnClick(e: MapLayerMouseEvent) {
|
||||||
const properties = e.features![0].properties!;
|
const properties = e.features![0].properties!;
|
||||||
const { trackId, segmentId, trackIndex, segmentIndex } = properties;
|
const { trackId, segmentId } = properties;
|
||||||
const selectionType = this.selectionState.type;
|
const selectionType = this.selectionState.type;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -436,11 +436,7 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO the engine can split (Command::Split) but needs the index of the trackpoint
|
splitAtPoint(segmentId, { lng: e.lngLat.lng, lat: e.lngLat.lat });
|
||||||
// fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, {
|
|
||||||
// lat: e.lngLat.lat,
|
|
||||||
// lon: e.lngLat.lng,
|
|
||||||
// });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { splitAs, SplitType } from '$lib/components/toolbar/tools/scissors/scissors';
|
import { splitAs, SplitType } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { ListRootItem } from '$lib/components/file-list/file-list';
|
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Slider } from '$lib/components/ui/slider';
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { get } from 'svelte/store';
|
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { onDestroy, onMount, untrack } from 'svelte';
|
import { onDestroy, onMount, untrack } from 'svelte';
|
||||||
import { Crop } from '@lucide/svelte';
|
import { Crop } from '@lucide/svelte';
|
||||||
import { SplitControls } from './split-controls';
|
import { SplitControls } from './split-controls';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { slicedStatistics } from '$lib/logic/selection-statistics';
|
||||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
|
||||||
import { engine } from '$lib/engine';
|
import { engine } from '$lib/engine';
|
||||||
|
|
||||||
let props: {
|
let props: {
|
||||||
@@ -24,15 +21,35 @@
|
|||||||
|
|
||||||
let splitControls: SplitControls | undefined = undefined;
|
let splitControls: SplitControls | undefined = undefined;
|
||||||
|
|
||||||
|
const selection = engine.selection;
|
||||||
|
const statistics = engine.statistics;
|
||||||
|
|
||||||
|
// the tool works on files, tracks and segments, not on waypoints
|
||||||
let validSelection = $derived(
|
let validSelection = $derived(
|
||||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
($selection.type === 'file' ||
|
||||||
$gpxStatistics.global.length > 0
|
$selection.type === 'track' ||
|
||||||
|
$selection.type === 'segment') &&
|
||||||
|
$statistics.length > 0
|
||||||
);
|
);
|
||||||
let maxSliderValue = $derived(
|
// The slider does work for each of its steps every time it changes, which is too slow with
|
||||||
validSelection && $gpxStatistics.global.length > 0 ? $gpxStatistics.global.length - 1 : 1
|
// the thousands of trackpoints of a long track: it has a limited number of steps, which are
|
||||||
);
|
// spread over the trackpoints.
|
||||||
let sliderValues = $derived([0, maxSliderValue]);
|
const MAX_STEPS = 500;
|
||||||
let canCrop = $derived(sliderValues[0] != 0 || sliderValues[1] != maxSliderValue);
|
let length = $derived($statistics.length);
|
||||||
|
let steps = $derived(validSelection ? Math.min(length - 1, MAX_STEPS) : 1);
|
||||||
|
let sliderValues = $state([0, 1]);
|
||||||
|
|
||||||
|
/** The index of the trackpoint of a step of the slider. */
|
||||||
|
function toIndex(step: number) {
|
||||||
|
return steps <= 0 || steps >= length - 1 ? step : Math.round((step * (length - 1)) / steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The step of the slider that is the closest to a trackpoint. */
|
||||||
|
function toStep(index: number) {
|
||||||
|
return steps <= 0 || steps >= length - 1
|
||||||
|
? index
|
||||||
|
: Math.round((index * steps) / (length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
@@ -40,42 +57,33 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateSlicedGPXStatistics() {
|
/** The user moved the slider: the part of the selection between its bounds is highlighted. */
|
||||||
if (validSelection && canCrop) {
|
function updateSlicedStatistics([startStep, endStep]: number[]) {
|
||||||
$slicedGPXStatistics = [
|
if (validSelection && (startStep !== 0 || endStep !== steps)) {
|
||||||
get(gpxStatistics).sliced(sliderValues[0], sliderValues[1]),
|
const [start, end] = [toIndex(startStep), toIndex(endStep)];
|
||||||
sliderValues[0],
|
const global = $statistics.slice(start, end);
|
||||||
sliderValues[1],
|
slicedStatistics.set(global && { global, start, end });
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
$slicedGPXStatistics = undefined;
|
slicedStatistics.set(undefined);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSliderValues() {
|
|
||||||
if ($slicedGPXStatistics !== undefined) {
|
|
||||||
sliderValues = [$slicedGPXStatistics[1], $slicedGPXStatistics[2]];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The slider follows the highlighted part, which can also be chosen on the elevation profile or
|
||||||
|
// reset (when the selection changes): it covers the whole selection when there is none.
|
||||||
|
// It only goes this way, the slider tells about its own changes (`onValueChange`).
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (sliderValues) {
|
const sliced = $slicedStatistics;
|
||||||
untrack(() => updateSlicedGPXStatistics());
|
const max = steps;
|
||||||
}
|
untrack(() => {
|
||||||
});
|
const bounds = sliced ? [toStep(sliced.start), toStep(sliced.end)] : [0, max];
|
||||||
|
if (sliderValues[0] !== bounds[0] || sliderValues[1] !== bounds[1]) {
|
||||||
$effect(() => {
|
sliderValues = bounds;
|
||||||
if (
|
}
|
||||||
$slicedGPXStatistics !== undefined &&
|
});
|
||||||
($slicedGPXStatistics[1] !== sliderValues[0] ||
|
|
||||||
$slicedGPXStatistics[2] !== sliderValues[1])
|
|
||||||
) {
|
|
||||||
untrack(() => updateSliderValues());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
$slicedGPXStatistics = undefined;
|
slicedStatistics.set(undefined);
|
||||||
if (splitControls) {
|
if (splitControls) {
|
||||||
splitControls.destroy();
|
splitControls.destroy();
|
||||||
}
|
}
|
||||||
@@ -86,7 +94,8 @@
|
|||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<Slider
|
<Slider
|
||||||
bind:value={sliderValues}
|
bind:value={sliderValues}
|
||||||
max={maxSliderValue}
|
onValueChange={updateSlicedStatistics}
|
||||||
|
max={steps}
|
||||||
step={1}
|
step={1}
|
||||||
type="multiple"
|
type="multiple"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
@@ -94,8 +103,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!validSelection || !canCrop}
|
disabled={!validSelection || !$slicedStatistics}
|
||||||
onclick={() => engine.crop(sliderValues[0], sliderValues[1])}
|
onclick={() =>
|
||||||
|
$slicedStatistics && engine.crop($slicedStatistics.start, $slicedStatistics.end)}
|
||||||
>
|
>
|
||||||
<Crop size="16" />{i18n._('toolbar.scissors.crop')}
|
<Crop size="16" />{i18n._('toolbar.scissors.crop')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
import { writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
|
import { engine, type SplitType as EngineSplitType } from '$lib/engine';
|
||||||
|
import { closestPointIndexIn } from '$lib/closest-point';
|
||||||
|
|
||||||
export enum SplitType {
|
/** What a split cuts in two, the values being the ones of the engine. */
|
||||||
FILES = 'files',
|
export const SplitType = {
|
||||||
TRACKS = 'tracks',
|
FILES: 'files',
|
||||||
SEGMENTS = 'segments',
|
TRACKS: 'tracks',
|
||||||
}
|
SEGMENTS: 'segments',
|
||||||
|
} as const satisfies Record<string, EngineSplitType>;
|
||||||
|
export type SplitType = (typeof SplitType)[keyof typeof SplitType];
|
||||||
|
|
||||||
export let splitAs: Writable<SplitType> = writable(SplitType.FILES);
|
export let splitAs: Writable<SplitType> = writable(SplitType.FILES);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits at the trackpoint of a selected segment that is the closest to `point`, as `splitAs` says.
|
||||||
|
* The statistics of the engine need to have the anchors (the scissors tool asks for them).
|
||||||
|
*/
|
||||||
|
export function splitAtPoint(segmentId: string, point: { lng: number; lat: number }) {
|
||||||
|
const statistics = get(engine.statistics);
|
||||||
|
const anchors = statistics.anchors;
|
||||||
|
const segment = anchors?.segmentIds.indexOf(segmentId) ?? -1;
|
||||||
|
if (!anchors || segment < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const starts = anchors.segmentStarts;
|
||||||
|
const end = segment + 1 < starts.length ? starts[segment + 1] : statistics.length;
|
||||||
|
const index = closestPointIndexIn(statistics.lng, statistics.lat, starts[segment], end, point);
|
||||||
|
if (index !== undefined) {
|
||||||
|
engine.split(anchors.revision, index, get(splitAs));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,32 @@
|
|||||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
|
||||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||||
import { Scissors } from 'lucide-static';
|
import { Scissors } from 'lucide-static';
|
||||||
import { selection } from '$lib/logic/selection';
|
|
||||||
import { gpxStatistics } from '$lib/logic/statistics';
|
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import type { GeoJSONSource } from 'maplibre-gl';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
import { loadSVGIcon } from '$lib/utils';
|
import { loadSVGIcon } from '$lib/utils';
|
||||||
|
import {
|
||||||
|
engine,
|
||||||
|
type Selection,
|
||||||
|
type SelectionStatistics,
|
||||||
|
type StatisticsRequest,
|
||||||
|
} from '$lib/engine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The anchors inside the segments of the selection, where a click splits the file, the track or
|
||||||
|
* the segment: a command of the engine, which finds them by the index of their trackpoint in the
|
||||||
|
* selection.
|
||||||
|
*/
|
||||||
export class SplitControls {
|
export class SplitControls {
|
||||||
map: maplibregl.Map;
|
map: maplibregl.Map;
|
||||||
layerEventManager: MapLayerEventManager;
|
layerEventManager: MapLayerEventManager;
|
||||||
unsubscribes: Function[] = [];
|
unsubscribes: Function[] = [];
|
||||||
|
/** What the tool needs from the engine, while it shows the anchors. */
|
||||||
|
request: StatisticsRequest = engine.requestStatistics();
|
||||||
|
selection: Selection = { type: 'empty' };
|
||||||
|
statistics: SelectionStatistics | undefined = undefined;
|
||||||
|
|
||||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||||
@@ -36,13 +46,29 @@ export class SplitControls {
|
|||||||
</svg>`
|
</svg>`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(
|
||||||
|
engine.statistics.subscribe((statistics) => {
|
||||||
|
this.statistics = statistics;
|
||||||
|
this.addIfNeeded();
|
||||||
|
})
|
||||||
|
);
|
||||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||||
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(
|
||||||
|
engine.selection.subscribe((selection) => {
|
||||||
|
this.selection = selection;
|
||||||
|
this.addIfNeeded();
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
addIfNeeded() {
|
addIfNeeded() {
|
||||||
let scissors = get(currentTool) === Tool.SCISSORS;
|
// the tool works on files, tracks and segments, not on waypoints
|
||||||
|
const scissors =
|
||||||
|
get(currentTool) === Tool.SCISSORS &&
|
||||||
|
(this.selection.type === 'file' ||
|
||||||
|
this.selection.type === 'track' ||
|
||||||
|
this.selection.type === 'segment');
|
||||||
|
this.request.set(scissors ? ['anchors'] : []);
|
||||||
if (!scissors) {
|
if (!scissors) {
|
||||||
this.remove();
|
this.remove();
|
||||||
return;
|
return;
|
||||||
@@ -51,44 +77,42 @@ export class SplitControls {
|
|||||||
this.updateControls();
|
this.updateControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateControls() {
|
/** The anchors that are inside a segment, which is not where it starts or ends. */
|
||||||
let data: GeoJSON.FeatureCollection = {
|
inner(): GeoJSON.Feature[] {
|
||||||
type: 'FeatureCollection',
|
const statistics = this.statistics;
|
||||||
features: [],
|
const anchors = statistics?.anchors;
|
||||||
};
|
if (!statistics || !anchors) {
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
return [];
|
||||||
let file = fileStateCollection.getFile(fileId);
|
}
|
||||||
|
const starts = anchors.segmentStarts;
|
||||||
if (file) {
|
const features: GeoJSON.Feature[] = [];
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
let segment = 0;
|
||||||
if (
|
for (let i = 0; i < anchors.indices.length; i++) {
|
||||||
get(selection).hasAnyParent(
|
const index = anchors.indices[i];
|
||||||
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
|
// the anchors are sorted: the segment can only be the same or a following one
|
||||||
)
|
while (segment + 1 < starts.length && starts[segment + 1] <= index) {
|
||||||
) {
|
segment++;
|
||||||
for (let i = 1; i < segment.trkpt.length - 1; i++) {
|
}
|
||||||
let point = segment.trkpt[i];
|
const end = segment + 1 < starts.length ? starts[segment + 1] : statistics.length;
|
||||||
if (point._data.anchor) {
|
if (index > starts[segment] && index < end - 1) {
|
||||||
data.features.push({
|
features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [point.getLongitude(), point.getLatitude()],
|
coordinates: [statistics.lng[index], statistics.lat[index]],
|
||||||
},
|
},
|
||||||
properties: {
|
properties: { index, minZoom: anchors.zooms[i] },
|
||||||
fileId: fileId,
|
|
||||||
trackIndex: trackIndex,
|
|
||||||
segmentIndex: segmentIndex,
|
|
||||||
pointIndex: i,
|
|
||||||
minZoom: point._data.zoom,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, false);
|
}
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateControls() {
|
||||||
|
const data: GeoJSON.FeatureCollection = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: this.inner(),
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let source = this.map.getSource('split-controls') as GeoJSONSource | undefined;
|
let source = this.map.getSource('split-controls') as GeoJSONSource | undefined;
|
||||||
@@ -161,19 +185,15 @@ export class SplitControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
layerOnClick(e: maplibregl.MapLayerMouseEvent) {
|
layerOnClick(e: maplibregl.MapLayerMouseEvent) {
|
||||||
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
|
const revision = this.statistics?.anchors?.revision;
|
||||||
fileActions.split(
|
if (revision !== undefined) {
|
||||||
get(splitAs),
|
engine.split(revision, e.features![0].properties!.index, get(splitAs));
|
||||||
e.features![0].properties!.fileId,
|
}
|
||||||
e.features![0].properties!.trackIndex,
|
|
||||||
e.features![0].properties!.segmentIndex,
|
|
||||||
{ lon: coordinates[0], lat: coordinates[1] },
|
|
||||||
e.features![0].properties!.pointIndex
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.remove();
|
this.remove();
|
||||||
|
this.request.release();
|
||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,9 @@ export type {
|
|||||||
|
|
||||||
type Wasm = typeof import('gpx-rs');
|
type Wasm = typeof import('gpx-rs');
|
||||||
|
|
||||||
|
/** What a split cuts in two: the file, the track or the segment of the trackpoint. */
|
||||||
|
export type SplitType = 'files' | 'tracks' | 'segments';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How elements combine with the current selection: they `replace` it, are `add`ed to it, or are
|
* How elements combine with the current selection: they `replace` it, are `add`ed to it, or are
|
||||||
* `toggle`d (the ones already selected are removed). Adding and toggling only merge with elements
|
* `toggle`d (the ones already selected are removed). Adding and toggling only merge with elements
|
||||||
@@ -645,6 +648,26 @@ class Engine {
|
|||||||
return this.run((w) => w.reverse());
|
return this.run((w) => w.reverse());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the
|
||||||
|
* selection in two, there: the trackpoint ends the first part and starts the second one. The
|
||||||
|
* first part keeps the ids, the second one (and what follows) gets new ones. `revision` is the
|
||||||
|
* one of `SelectionAnchors`, which `at` is an index of.
|
||||||
|
*/
|
||||||
|
split(revision: number, at: number, splitType: SplitType) {
|
||||||
|
return this.run((w) =>
|
||||||
|
w.split(
|
||||||
|
revision,
|
||||||
|
at,
|
||||||
|
{
|
||||||
|
files: w.SplitType.Files,
|
||||||
|
tracks: w.SplitType.Tracks,
|
||||||
|
segments: w.SplitType.Segments,
|
||||||
|
}[splitType]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keeps the trackpoints `start` to `end` (both included) of the selection, counted over its
|
* Keeps the trackpoints `start` to `end` (both included) of the selection, counted over its
|
||||||
* segments one after the other, and removes the others, with the segments, tracks and files
|
* segments one after the other, and removes the others, with the segments, tracks and files
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { fileStateCollection } from '$lib/logic/file-state';
|
|||||||
import { fileActionManager } from '$lib/logic/file-action-manager';
|
import { fileActionManager } from '$lib/logic/file-action-manager';
|
||||||
import { applyToOrderedItemsFromFile, copied, cut, selection } from '$lib/logic/selection';
|
import { applyToOrderedItemsFromFile, copied, cut, selection } from '$lib/logic/selection';
|
||||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||||
import { SplitType } from '$lib/components/toolbar/tools/scissors/scissors';
|
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListLevel,
|
ListLevel,
|
||||||
@@ -20,13 +19,12 @@ import {
|
|||||||
TrackPoint,
|
TrackPoint,
|
||||||
TrackSegment,
|
TrackSegment,
|
||||||
Waypoint,
|
Waypoint,
|
||||||
type Coordinates,
|
|
||||||
type LineStyleExtension,
|
type LineStyleExtension,
|
||||||
type WaypointType,
|
type WaypointType,
|
||||||
} from 'gpx';
|
} from 'gpx';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { getClosestLinePoint, getClosestTrackSegments, getElevation } from '$lib/utils';
|
import { getClosestTrackSegments, getElevation } from '$lib/utils';
|
||||||
import { gpxStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics } from '$lib/logic/statistics';
|
||||||
import { boundsManager } from './bounds';
|
import { boundsManager } from './bounds';
|
||||||
import { engine } from '$lib/engine';
|
import { engine } from '$lib/engine';
|
||||||
@@ -333,69 +331,6 @@ export const fileActions = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
split(
|
|
||||||
splitType: SplitType,
|
|
||||||
fileId: string,
|
|
||||||
trackIndex: number,
|
|
||||||
segmentIndex: number,
|
|
||||||
coordinates: Coordinates,
|
|
||||||
trkptIndex?: number
|
|
||||||
) {
|
|
||||||
return fileActionManager.applyGlobal((draft) => {
|
|
||||||
let file = fileStateCollection.getFile(fileId);
|
|
||||||
if (file) {
|
|
||||||
let segment = file.trk[trackIndex].trkseg[segmentIndex];
|
|
||||||
let minIndex = 0;
|
|
||||||
if (trkptIndex === undefined) {
|
|
||||||
// Find the point closest to split
|
|
||||||
let closest = getClosestLinePoint(segment.trkpt, coordinates);
|
|
||||||
minIndex = closest._data.index;
|
|
||||||
} else {
|
|
||||||
minIndex = trkptIndex;
|
|
||||||
}
|
|
||||||
let absoluteIndex = minIndex;
|
|
||||||
file.forEachSegment((seg, trkIndex, segIndex) => {
|
|
||||||
if (
|
|
||||||
(trkIndex < trackIndex && splitType === SplitType.FILES) ||
|
|
||||||
(trkIndex === trackIndex && segIndex < segmentIndex)
|
|
||||||
) {
|
|
||||||
absoluteIndex += seg.trkpt.length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (splitType === SplitType.FILES) {
|
|
||||||
let newFile = draft.get(fileId);
|
|
||||||
if (newFile) {
|
|
||||||
newFile.crop(0, absoluteIndex);
|
|
||||||
let newFile2 = file.clone();
|
|
||||||
newFile2._data.id = getFileIds(1)[0];
|
|
||||||
newFile2.crop(absoluteIndex, file.getNumberOfTrackPoints() - 1);
|
|
||||||
draft.set(newFile2._data.id, freeze(newFile2));
|
|
||||||
}
|
|
||||||
} else if (splitType === SplitType.TRACKS) {
|
|
||||||
let newFile = draft.get(fileId);
|
|
||||||
if (newFile) {
|
|
||||||
let start = file.trk[trackIndex].clone();
|
|
||||||
start.crop(0, absoluteIndex);
|
|
||||||
let end = file.trk[trackIndex].clone();
|
|
||||||
end.crop(absoluteIndex, file.trk[trackIndex].getNumberOfTrackPoints() - 1);
|
|
||||||
newFile.replaceTracks(trackIndex, trackIndex, [start, end]);
|
|
||||||
}
|
|
||||||
} else if (splitType === SplitType.SEGMENTS) {
|
|
||||||
let newFile = draft.get(fileId);
|
|
||||||
if (newFile) {
|
|
||||||
let start = segment.clone();
|
|
||||||
start.crop(0, minIndex);
|
|
||||||
let end = segment.clone();
|
|
||||||
end.crop(minIndex, segment.trkpt.length - 1);
|
|
||||||
newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, [
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
reduce: (itemsAndPoints: Map<ListItem, TrackPoint[]>) => {
|
reduce: (itemsAndPoints: Map<ListItem, TrackPoint[]>) => {
|
||||||
if (itemsAndPoints.size === 0) {
|
if (itemsAndPoints.size === 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in new issue
Block a user