diff --git a/gpx-rs/engine/src/engine/command/command.rs b/gpx-rs/engine/src/engine/command/command.rs index 54abd4159..a49400eeb 100644 --- a/gpx-rs/engine/src/engine/command/command.rs +++ b/gpx-rs/engine/src/engine/command/command.rs @@ -1,8 +1,8 @@ use crate::{Apply, CommandError, State}; use crate::{ - Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata, Move, - MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, SpliceTrackpoints, - Split, Style, Time, + Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, LoadFiles, Merge, + Metadata, Move, MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, + SpliceTrackpoints, Split, Style, Time, }; /// A user action that edits the files. Each variant wraps the command's own struct, whose @@ -14,6 +14,7 @@ use crate::{ pub enum Command<'a> { New(New<'a>), Load(Load<'a>), + LoadFiles(LoadFiles<'a>), Duplicate(Duplicate), Delete(Delete), Paste(Paste), @@ -41,6 +42,7 @@ impl Apply for Command<'_> { match self { Command::New(c) => c.apply(state), Command::Load(c) => c.apply(state), + Command::LoadFiles(c) => c.apply(state), Command::Duplicate(c) => c.apply(state), Command::Delete(c) => c.apply(state), Command::Paste(c) => c.apply(state), diff --git a/gpx-rs/engine/src/engine/command/file/load.rs b/gpx-rs/engine/src/engine/command/file/load.rs index 8522e1371..b94ed43fe 100644 --- a/gpx-rs/engine/src/engine/command/file/load.rs +++ b/gpx-rs/engine/src/engine/command/file/load.rs @@ -1,4 +1,4 @@ -use crate::{Apply, CommandError, State, parse, produce}; +use crate::{Apply, CommandError, File, State, parse, produce}; #[derive(Debug)] pub struct Load<'a> { @@ -8,14 +8,49 @@ pub struct Load<'a> { pub name: &'a str, } -impl Apply for Load<'_> { - fn apply(self, state: &mut State) -> Result<(), CommandError> { +impl Load<'_> { + fn parse(self) -> Result { let mut file = parse(self.data).map_err(|err| CommandError::InvalidData(err.to_string()))?; if file.info.name.trim().is_empty() { file.info.name = self.name.to_owned(); } - produce(state, |_| vec![file]); + Ok(file) + } +} + +impl Apply for Load<'_> { + fn apply(self, state: &mut State) -> Result<(), CommandError> { + LoadFiles { files: vec![self] }.apply(state) + } +} + +/// Loads several files at once, as a single command (one undo step): the files are added at the +/// end of the list, in the given order, and the first one is selected. +/// +/// The files that cannot be read are skipped, unless none of them can: then nothing is loaded +/// and the error is the one of the first file. +#[derive(Debug)] +pub struct LoadFiles<'a> { + pub files: Vec>, +} + +impl Apply for LoadFiles<'_> { + fn apply(self, state: &mut State) -> Result<(), CommandError> { + let mut files = Vec::with_capacity(self.files.len()); + let mut first_error = None; + for load in self.files { + match load.parse() { + Ok(file) => files.push(file), + Err(error) => { + first_error.get_or_insert(error); + } + } + } + if files.is_empty() { + return Err(first_error.unwrap_or(CommandError::NothingToDo)); + } + produce(state, |_| files); Ok(()) } } @@ -85,4 +120,101 @@ mod tests { let empty = br#" "#; assert_eq!(load_with_name(empty), "from disk"); } + + fn gpx(name: &str) -> Vec { + format!(r#"{name}"#).into_bytes() + } + + fn load<'a>(data: &'a [u8], name: &'a str) -> Load<'a> { + Load { data, name } + } + + #[test] + fn test_load_several_files() { + let mut fx = Fixture::default(); + crate::New { name: "before" } + .apply(&mut fx.state()) + .unwrap(); + let before = fx.order.0[0]; + let (a, b, c) = (gpx("a"), gpx("b"), gpx("c")); + + LoadFiles { + files: vec![load(&a, "x"), load(&b, "x"), load(&c, "x")], + } + .apply(&mut fx.state()) + .unwrap(); + + // added after the others, in the order they were given + let names: Vec<_> = fx + .order + .0 + .iter() + .map(|id| fx.files[id].info.name.clone()) + .collect(); + assert_eq!(names, ["before", "a", "b", "c"]); + assert_eq!(fx.order.0[0], before); + // the first one is selected + assert_eq!(fx.selected_files(), [fx.order.0[1]].into()); + } + + #[test] + fn test_load_several_files_skips_the_invalid_ones() { + let mut fx = Fixture::default(); + let (a, c) = (gpx("a"), gpx("c")); + LoadFiles { + files: vec![load(&a, "x"), load(b"", "x"), load(&c, "x")], + } + .apply(&mut fx.state()) + .unwrap(); + let names: Vec<_> = fx + .order + .0 + .iter() + .map(|id| fx.files[id].info.name.clone()) + .collect(); + assert_eq!(names, ["a", "c"]); + } + + #[test] + fn test_load_several_invalid_files_changes_nothing() { + let mut fx = Fixture::default(); + crate::New { name: "keep" }.apply(&mut fx.state()).unwrap(); + let selected = fx.selected_files(); + + let result = LoadFiles { + files: vec![ + load(b"", "x"), + load(b"", "y"), + ], + } + .apply(&mut fx.state()); + assert!(matches!(result, Err(CommandError::InvalidData(_)))); + assert_eq!(fx.files.len(), 1); + assert_eq!(fx.order.0.len(), 1); + assert_eq!(fx.selected_files(), selected); + + // nothing to load + assert_eq!( + LoadFiles { files: vec![] }.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + } + + #[test] + fn test_each_file_without_name_gets_its_own_name() { + let mut fx = Fixture::default(); + let unnamed = br#""#; + LoadFiles { + files: vec![load(unnamed, "first"), load(unnamed, "second")], + } + .apply(&mut fx.state()) + .unwrap(); + let names: Vec<_> = fx + .order + .0 + .iter() + .map(|id| fx.files[id].info.name.clone()) + .collect(); + assert_eq!(names, ["first", "second"]); + } } diff --git a/gpx-rs/engine/src/engine/engine.rs b/gpx-rs/engine/src/engine/engine.rs index 1ec33777c..6a1918c59 100644 --- a/gpx-rs/engine/src/engine/engine.rs +++ b/gpx-rs/engine/src/engine/engine.rs @@ -1071,4 +1071,43 @@ mod tests { assert!(engine.execute(Action::Undo)); assert_eq!(track_ids(&engine, a), tracks); } + + #[test] + fn test_load_several_files_is_one_edit() { + use crate::LoadFiles; + + let mut engine = Engine::default(); + new(&mut engine, "before"); + let gpx = |name: &str| { + format!(r#"{name}"#) + .into_bytes() + }; + let (a, b) = (gpx("a"), gpx("b")); + assert!(edit( + &mut engine, + Command::LoadFiles(LoadFiles { + files: vec![ + Load { + data: &a, + name: "x" + }, + Load { + data: &b, + name: "x" + }, + ], + }) + )); + assert_eq!(engine.order().len(), 3); + let diff = engine.last_diff().unwrap(); + assert_eq!(diff.added.len(), 2); + // the first one that was loaded is selected + assert_eq!(engine.selection(), &files(&[engine.order()[1]])); + + // a single undo removes both + assert!(engine.execute(Action::Undo)); + assert_eq!(engine.order().len(), 1); + assert!(engine.execute(Action::Redo)); + assert_eq!(engine.order().len(), 3); + } } diff --git a/gpx-rs/wasm/src/lib.rs b/gpx-rs/wasm/src/lib.rs index f3de26bd0..162881f86 100644 --- a/gpx-rs/wasm/src/lib.rs +++ b/gpx-rs/wasm/src/lib.rs @@ -171,10 +171,26 @@ pub fn new_file(name: &str) -> bool { edit(Command::New(engine::New { name })) } -/// `name`: the name of the file when the data has none (the name on disk, without extension). +/// Loads files as a single command (one undo step). The files are in `data`, one after the +/// other, `lengths` being the number of bytes of each one. `names`: the name of each file when +/// its data has none (the name on disk, without extension). The files that cannot be read are +/// skipped, the first one that was read is selected. #[wasm_bindgen] -pub fn load_file(data: &[u8], name: &str) -> bool { - edit(Command::Load(engine::Load { data, name })) +pub fn load_files(data: &[u8], lengths: &[u32], names: Vec) -> bool { + if lengths.len() != names.len() { + return false; + } + let mut files = Vec::with_capacity(names.len()); + let mut offset = 0; + for (length, name) in lengths.iter().zip(&names) { + let end = offset + *length as usize; + let Some(data) = data.get(offset..end) else { + return false; + }; + files.push(engine::Load { data, name }); + offset = end; + } + edit(Command::LoadFiles(engine::LoadFiles { files })) } #[wasm_bindgen] diff --git a/website/src/lib/components/embedding/Embedding.svelte b/website/src/lib/components/embedding/Embedding.svelte index 3387ff5e0..e3cd1f72b 100644 --- a/website/src/lib/components/embedding/Embedding.svelte +++ b/website/src/lib/components/embedding/Embedding.svelte @@ -7,7 +7,6 @@ import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte'; import OpenIn from '$lib/components/embedding/OpenIn.svelte'; import { writable } from 'svelte/store'; - import type { GPXFile } from 'gpx'; import { allowedEmbeddingBasemaps, getFilesFromEmbeddingOptions, @@ -15,9 +14,7 @@ } from './embedding'; import { setMode } from 'mode-watcher'; import { settings } from '$lib/logic/settings'; - import { fileStateCollection } from '$lib/logic/file-state'; - import { loadFile } from '$lib/logic/file-actions'; - import { selection } from '$lib/logic/selection'; + import { loadFiles } from '$lib/logic/file-actions'; import { untrack } from 'svelte'; import { isSelected, toggle } from '$lib/components/map/layer-control/utils'; import { engine } from '$lib/engine'; @@ -30,6 +27,8 @@ hash = $bindable(), }: { useHash?: boolean; options: EmbeddingOptions; hash: string } = $props(); + const { files } = engine; + let additionalDatasets = writable([]); let elevationFill = writable<'slope' | 'surface' | 'highway' | undefined>(undefined); @@ -74,26 +73,14 @@ ); elevationFill.set(options.elevation.fill == 'none' ? undefined : options.elevation.fill); - let downloads: Promise[] = getFilesFromEmbeddingOptions(options).map( - (url) => { - return fetch(url) - .then((response) => response.blob()) - .then((blob) => new File([blob], url.split('/').pop() ?? url)) - .then(loadFile); - } - ); - Promise.all(downloads).then((answers) => { - const files = answers.filter((file) => file !== null) as GPXFile[]; - let ids: string[] = []; - files.forEach((file, index) => { - let id = `gpx-${index}-embed`; - file._data.id = id; - ids.push(id); - }); - fileStateCollection.setEmbeddedFiles(files); - $fileOrder = ids; - selection.selectAll(); + let downloads: Promise[] = getFilesFromEmbeddingOptions(options).map((url) => { + return fetch(url) + .then((response) => response.blob()) + .then((blob) => new File([blob], url.split('/').pop() ?? url)); }); + Promise.all(downloads) + .then(loadFiles) + .then(() => engine.selectAll()); } $effect(() => { @@ -105,7 +92,7 @@
- {#if $fileStateCollection.size > 1} + {#if $files.size > 1}
diff --git a/website/src/lib/default-file-name.test.ts b/website/src/lib/default-file-name.test.ts new file mode 100644 index 000000000..bae395d44 --- /dev/null +++ b/website/src/lib/default-file-name.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { defaultFileName } from './default-file-name'; + +describe('default file name', () => { + it('is the default name when there is no other file with it', () => { + assert.equal(defaultFileName('New file', []), 'New file'); + assert.equal(defaultFileName('New file', ['Morning ride', 'track']), 'New file'); + }); + + it('is numbered when other files have the default name', () => { + assert.equal(defaultFileName('New file', ['New file']), 'New file 2'); + assert.equal(defaultFileName('New file', ['New file', 'New file 2']), 'New file 3'); + }); + + it('goes after the highest number', () => { + assert.equal(defaultFileName('New file', ['New file 4', 'New file']), 'New file 5'); + // a number that was freed is not used again + assert.equal(defaultFileName('New file', ['New file', 'New file 3']), 'New file 4'); + // the numbered ones count even if the first one was renamed + assert.equal(defaultFileName('New file', ['New file 2']), 'New file 3'); + }); + + it('ignores the names that only look like it', () => { + assert.equal(defaultFileName('New file', ['New file copy', 'New files 3']), 'New file'); + assert.equal( + defaultFileName('New file', ['New file 2b', 'New file 0', 'New file -3']), + 'New file' + ); + assert.equal(defaultFileName('New file', ['New file ', 'New file 2']), 'New file'); + assert.equal(defaultFileName('New file', ['my New file']), 'New file'); + }); + + it('works with any language', () => { + assert.equal(defaultFileName('Nouveau fichier', ['Nouveau fichier']), 'Nouveau fichier 2'); + assert.equal(defaultFileName('新文件', ['新文件', '新文件 2']), '新文件 3'); + // the default name of another language is just a name + assert.equal( + defaultFileName('Nouveau fichier', ['New file', 'New file 2']), + 'Nouveau fichier' + ); + }); +}); diff --git a/website/src/lib/default-file-name.ts b/website/src/lib/default-file-name.ts new file mode 100644 index 000000000..7f27ea3b9 --- /dev/null +++ b/website/src/lib/default-file-name.ts @@ -0,0 +1,19 @@ +/** + * The name of a new file: the default name (translated), followed by a number when other files + * already have a default name (the name alone counts as number 1): "New file", "New file 2", + * "New file 3"... + */ +export function defaultFileName(defaultName: string, existingNames: string[]): string { + let highest = 0; + for (const name of existingNames) { + if (name === defaultName) { + highest = Math.max(highest, 1); + } else if (name.startsWith(`${defaultName} `)) { + const suffix = name.slice(defaultName.length + 1); + if (/^[1-9][0-9]*$/.test(suffix)) { + highest = Math.max(highest, parseInt(suffix)); + } + } + } + return highest === 0 ? defaultName : `${defaultName} ${highest + 1}`; +} diff --git a/website/src/lib/engine.ts b/website/src/lib/engine.ts index 737572769..459de0c58 100644 --- a/website/src/lib/engine.ts +++ b/website/src/lib/engine.ts @@ -242,9 +242,26 @@ class Engine { return this.run((w) => w.new_file(name)); } - /** `name` is the name of the file when the data has none (the name on disk, without extension). */ - loadFile(data: Uint8Array, name: string) { - return this.run((w) => w.load_file(data, name)); + /** + * Loads files as a single command (one undo step). `name` is the name of a file when its data + * has none (the name on disk, without extension). The files that cannot be read are skipped, + * the first one that was read is selected. + */ + loadFiles(files: { data: Uint8Array; name: string }[]) { + const lengths = Uint32Array.from(files.map((file) => file.data.length)); + const data = new Uint8Array(lengths.reduce((sum, length) => sum + length, 0)); + let offset = 0; + for (const file of files) { + data.set(file.data, offset); + offset += file.data.length; + } + return this.run((w) => + w.load_files( + data, + lengths, + files.map((file) => file.name) + ) + ); } duplicate() { diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index dd9b19b0f..57a6940cd 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -10,15 +10,12 @@ import { ListTrackItem, ListTrackSegmentItem, ListWaypointItem, - ListWaypointsItem, - sortItems, type ListItem, } from '$lib/components/file-list/file-list'; import { i18n } from '$lib/i18n.svelte'; -import { freeze, type WritableDraft } from 'immer'; +import { freeze } from 'immer'; import { GPXFile, - parseGPX, Track, TrackPoint, TrackSegment, @@ -32,6 +29,8 @@ import { settings } from '$lib/logic/settings'; import { getClosestLinePoint, getClosestTrackSegments, getElevation } from '$lib/utils'; import { gpxStatistics } from '$lib/logic/statistics'; import { boundsManager } from './bounds'; +import { engine } from '$lib/engine'; +import { defaultFileName } from '$lib/default-file-name'; // Generate unique file ids, different from the ones in the database export function getFileIds(n: number) { @@ -45,31 +44,21 @@ export function getFileIds(n: number) { return ids; } +/** The name of a new file: the translated default name, numbered if other files have it. */ +export function newFileName() { + const names = [...get(engine.files).values()].map((file) => get(file).structure.name); + return defaultFileName(i18n._('menu.new_file'), names); +} + export function newGPXFile() { - const newFileName = i18n._('menu.new_file'); - let file = new GPXFile(); - - let maxNewFileNumber = 0; - fileStateCollection.forEach((fileId, file) => { - if (file.metadata.name && file.metadata.name.startsWith(newFileName)) { - let number = parseInt(file.metadata.name.split(' ').pop() ?? '0'); - if (!isNaN(number) && number > maxNewFileNumber) { - maxNewFileNumber = number; - } - } - }); - - file.metadata.name = `${newFileName} ${maxNewFileNumber + 1}`; - + file.metadata.name = newFileName(); return file; } export function createFile() { - let file = newGPXFile(); - - fileActions.add(file); - selection.selectFileWhenLoaded(file._data.id); + // the engine selects the new file + engine.newFile(newFileName()); currentTool.set(Tool.ROUTING); } @@ -87,41 +76,27 @@ export function triggerFileInput() { input.click(); } -export async function loadFiles(list: FileList | File[]) { - let files: GPXFile[] = []; - for (let i = 0; i < list.length; i++) { - let file = await loadFile(list[i]); - if (file) { - files.push(file); - } +/** + * Loads GPX files in the engine, as a single command: one undo step, the first file that could be + * read is selected, and the map fits the files. A file without name is named like the file on + * disk. Returns the ids of the files that were added. + */ +export async function loadFiles(list: FileList | File[]): Promise { + const files = await Promise.all( + Array.from(list).map(async (file) => ({ + data: new Uint8Array(await file.arrayBuffer()), + name: file.name.split('.').slice(0, -1).join('.'), + })) + ); + + const before = new Set(get(engine.order)); + await engine.loadFiles(files); + + const ids = get(engine.order).filter((id) => !before.has(id)); + if (ids.length > 0) { + boundsManager.fitBoundsOnLoad(ids); } - - let ids = fileActions.addMultiple(files); - selection.selectFileWhenLoaded(ids[0]); - boundsManager.fitBoundsOnLoad(ids); -} - -export async function loadFile(file: File): Promise { - let result = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = () => { - let data = reader.result?.toString() ?? null; - if (data) { - let gpx = parseGPX(data); - if (gpx.metadata === undefined) { - gpx.metadata = {}; - } - if (gpx.metadata.name === undefined || gpx.metadata.name.trim() === '') { - gpx.metadata.name = file.name.split('.').slice(0, -1).join('.'); - } - resolve(gpx); - } else { - resolve(null); - } - }; - reader.readAsText(file); - }); - return result; + return ids; } // Helper functions for file operations @@ -891,249 +866,3 @@ export const fileActions = { }); }, }; - -export function pasteSelection() { - let fromItems = get(copied); - if (fromItems === undefined || fromItems.length === 0) { - return; - } - - let selected = get(selection).getSelected(); - if (selected.length === 0) { - selected = [new ListRootItem()]; - } - - let fromParent = fromItems[0].getParent(); - let toParent = selected[selected.length - 1]; - - let startIndex: number | undefined = undefined; - - if (fromItems[0].level === toParent.level) { - if ( - toParent instanceof ListTrackItem || - toParent instanceof ListTrackSegmentItem || - toParent instanceof ListWaypointItem - ) { - startIndex = toParent.getId() + 1; - } - toParent = toParent.getParent(); - } - - let toItems: ListItem[] = []; - if (toParent.level === ListLevel.ROOT) { - let fileIds = getFileIds(fromItems.length); - fileIds.forEach((fileId) => { - toItems.push(new ListFileItem(fileId)); - }); - } else { - let toFile = fileStateCollection.getFile(toParent.getFileId()); - if (toFile) { - fromItems.forEach((item, index) => { - if (toParent instanceof ListFileItem) { - if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) { - toItems.push( - new ListTrackItem( - toParent.getFileId(), - (startIndex ?? toFile.trk.length) + index - ) - ); - } else if (item instanceof ListWaypointsItem) { - toItems.push(new ListWaypointsItem(toParent.getFileId())); - } else if (item instanceof ListWaypointItem) { - toItems.push( - new ListWaypointItem( - toParent.getFileId(), - (startIndex ?? toFile.wpt.length) + index - ) - ); - } - } else if (toParent instanceof ListTrackItem) { - if (item instanceof ListTrackSegmentItem) { - let toTrackIndex = toParent.getTrackIndex(); - toItems.push( - new ListTrackSegmentItem( - toParent.getFileId(), - toTrackIndex, - (startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index - ) - ); - } - } else if (toParent instanceof ListWaypointsItem) { - if (item instanceof ListWaypointItem) { - toItems.push( - new ListWaypointItem( - toParent.getFileId(), - (startIndex ?? toFile.wpt.length) + index - ) - ); - } - } - }); - } - } - - if (fromItems.length === toItems.length) { - moveItems(fromParent, toParent, fromItems, toItems, get(cut)); - selection.resetCopied(); - } -} - -export function moveItems( - fromParent: ListItem, - toParent: ListItem, - fromItems: ListItem[], - toItems: ListItem[], - remove: boolean = true -) { - if (fromItems.length === 0) { - return; - } - - sortItems(fromItems, false); - sortItems(toItems, false); - - let context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] = []; - fromItems.forEach((item) => { - let file = fileStateCollection.getFile(item.getFileId()); - if (file) { - if (item instanceof ListFileItem) { - context.push(file.clone()); - } else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) { - context.push(file.trk[item.getTrackIndex()].clone()); - } else if ( - item instanceof ListTrackSegmentItem && - item.getTrackIndex() < file.trk.length && - item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length - ) { - context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone()); - } else if (item instanceof ListWaypointsItem) { - context.push(file.wpt.map((wpt) => wpt.clone())); - } else if ( - item instanceof ListWaypointItem && - item.getWaypointIndex() < file.wpt.length - ) { - context.push(file.wpt[item.getWaypointIndex()].clone()); - } - } - }); - - if (remove && !(fromParent instanceof ListRootItem)) { - sortItems(fromItems, true); - } - - let files = [fromParent.getFileId(), toParent.getFileId()]; - let callbacks = [ - ( - file: WritableDraft, - context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] - ) => { - fromItems.forEach((item) => { - if (item instanceof ListTrackItem) { - file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []); - } else if (item instanceof ListTrackSegmentItem) { - file.replaceTrackSegments( - item.getTrackIndex(), - item.getSegmentIndex(), - item.getSegmentIndex(), - [] - ); - } else if (item instanceof ListWaypointsItem) { - file.replaceWaypoints(0, file.wpt.length - 1, []); - } else if (item instanceof ListWaypointItem) { - file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []); - } - }); - }, - ( - file: WritableDraft, - context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] - ) => { - toItems.forEach((item, i) => { - if (item instanceof ListTrackItem) { - if (context[i] instanceof Track) { - file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [ - context[i], - ]); - } else if (context[i] instanceof TrackSegment) { - file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [ - new Track({ - trkseg: [context[i]], - }), - ]); - } - } else if ( - item instanceof ListTrackSegmentItem && - context[i] instanceof TrackSegment - ) { - file.replaceTrackSegments( - item.getTrackIndex(), - item.getSegmentIndex(), - item.getSegmentIndex() - 1, - [context[i]] - ); - } else if (item instanceof ListWaypointsItem) { - if ( - Array.isArray(context[i]) && - context[i].length > 0 && - context[i][0] instanceof Waypoint - ) { - file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]); - } else if (context[i] instanceof Waypoint) { - file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]); - } - } else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) { - file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [ - context[i], - ]); - } - }); - }, - ]; - - if (fromParent instanceof ListRootItem) { - files = []; - callbacks = []; - } else if (!remove) { - files.splice(0, 1); - callbacks.splice(0, 1); - } - - fileActionManager.applyEachToFilesAndGlobal( - files, - callbacks, - (files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => { - toItems.forEach((item, i) => { - if (item instanceof ListFileItem) { - if (context[i] instanceof GPXFile) { - let newFile = context[i]; - if (remove) { - files.delete(newFile._data.id); - } - newFile._data.id = item.getFileId(); - files.set(item.getFileId(), freeze(newFile)); - } else if (context[i] instanceof Track) { - let newFile = newGPXFile(); - newFile._data.id = item.getFileId(); - if (context[i].name) { - newFile.metadata.name = context[i].name; - } - newFile.replaceTracks(0, 0, [context[i]]); - files.set(item.getFileId(), freeze(newFile)); - } else if (context[i] instanceof TrackSegment) { - let newFile = newGPXFile(); - newFile._data.id = item.getFileId(); - newFile.replaceTracks(0, 0, [ - new Track({ - trkseg: [context[i]], - }), - ]); - files.set(item.getFileId(), freeze(newFile)); - } - } - }); - }, - context - ); - - selection.set(toItems); -}