This commit is contained in:
vcoppe committed 2026-10-07 19:57:27 +02:00
1 parent 24b877c4ee
commit f642d60f42
6 files changed
+635 -183

No files matched your search

@@ -6,7 +6,6 @@
</script>
<script lang="ts">
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label/index.js';
@@ -16,43 +15,43 @@
import { Group } from '@lucide/svelte';
import { getURLForLanguage } from '$lib/utils';
import Shortcut from '$lib/components/Shortcut.svelte';
import { selection } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions';
import { gpxStatistics } from '$lib/logic/statistics';
import { onDestroy } from 'svelte';
import { engine } from '$lib/engine';
let props: {
class?: string;
} = $props();
const selection = engine.selection;
const statistics = engine.statistics;
// the number of segments of the selection that have trackpoints
const request = engine.requestStatistics();
request.set(['anchors']);
onDestroy(() => request.release());
let segmentCount = $derived($statistics.anchors?.segmentStarts.length ?? 0);
// several files, tracks or segments, or a single file or track with several segments
let canMergeTraces = $derived.by(() => {
if ($selection.size > 1) {
return true;
} else if ($selection.size === 1) {
let selected = $selection.getSelected()[0];
if (selected instanceof ListFileItem) {
let file = fileStateCollection.getFile(selected.getFileId());
if (file) {
return file.getSegments().length > 1;
}
} else if (selected instanceof ListTrackItem) {
let trackIndex = selected.getTrackIndex();
let file = fileStateCollection.getFile(selected.getFileId());
if (file && trackIndex < file.trk.length) {
return file.trk[trackIndex].getSegments().length > 1;
}
}
return false;
switch ($selection.type) {
case 'file':
return $selection.fileIds.length > 1 || segmentCount > 1;
case 'track':
return $selection.trackIds.length > 1 || segmentCount > 1;
case 'segment':
return $selection.segmentIds.length > 1;
default:
return false;
}
});
// several files or tracks, that can be put together
let canMergeContents = $derived(
$selection.size > 1 &&
$selection
.getSelected()
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
($selection.type === 'file' && $selection.fileIds.length > 1) ||
($selection.type === 'track' && $selection.trackIds.length > 1)
);
let hasTimes = $derived(($statistics.global.totalTime ?? 0) > 0);
let removeGaps = $state(false);
let mergeType = $state(MergeType.TRACES);
</script>
@@ -68,7 +67,7 @@
{i18n._('toolbar.merge.merge_contents')}
</Label>
</RadioGroup.Root>
{#if mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0}
{#if mergeType === MergeType.TRACES && hasTimes}
<div class="flex flex-row items-center gap-1.5">
<Checkbox id="remove-gaps" bind:checked={removeGaps} />
<Label for="remove-gaps">{i18n._('toolbar.merge.remove_gaps')}</Label>
@@ -80,9 +79,9 @@
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
onclick={() => {
fileActions.mergeSelection(
mergeType === MergeType.TRACES,
mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps
engine.merge(
mergeType === MergeType.TRACES ? 'connect' : 'group',
mergeType === MergeType.TRACES && hasTimes && removeGaps
);
}}
>
+17
View File
@@ -32,6 +32,12 @@ export type {
type Wasm = typeof import('gpx-rs');
/**
* How a merge puts the selected elements together: `connect` makes a single segment of their
* trackpoints, `group` keeps them as they are, in the first one.
*/
export type MergeType = 'connect' | 'group';
/** What a split cuts in two: the file, the track or the segment of the trackpoint. */
export type SplitType = 'files' | 'tracks' | 'segments';
@@ -648,6 +654,17 @@ class Engine {
return this.run((w) => w.reverse());
}
/**
* 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.
* Resolves to `false` when there is nothing to merge.
*/
merge(type: MergeType, removeGaps = false) {
return this.run((w) =>
w.merge(type === 'connect' ? w.MergeType.Connect : w.MergeType.Group, removeGaps)
);
}
/**
* 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
-148
View File
@@ -99,154 +99,6 @@ export async function loadFiles(list: FileList | File[]): Promise<string[]> {
// Helper functions for file operations
export const fileActions = {
mergeSelection: (mergeTraces: boolean, removeGaps: boolean) => {
fileActionManager.applyGlobal((draft) => {
let first = true;
let target: ListItem = new ListRootItem();
let targetFile: GPXFile | undefined = undefined;
let toMerge: {
trk: Track[];
trkseg: TrackSegment[];
wpt: Waypoint[];
} = {
trk: [],
trkseg: [],
wpt: [],
};
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = draft.get(fileId);
let originalFile = fileStateCollection.getFile(fileId);
if (file && originalFile) {
if (level === ListLevel.FILE) {
toMerge.trk.push(...originalFile.trk.map((track) => track.clone()));
for (const wpt of originalFile.wpt) {
if (!toMerge.wpt.some((w) => w.equals(wpt))) {
toMerge.wpt.push(wpt.clone());
}
}
if (first) {
target = items[0];
targetFile = file;
} else {
draft.delete(fileId);
}
} else {
if (level === ListLevel.TRACK) {
items.forEach((item, index) => {
let trackIndex = (item as ListTrackItem).getTrackIndex();
toMerge.trkseg.splice(
0,
0,
...originalFile.trk[trackIndex].trkseg.map((segment) =>
segment.clone()
)
);
if (index === items.length - 1) {
// Order is reversed, so the last track is the first one and the one to keep
target = item;
file.trk[trackIndex].trkseg = [];
} else {
file.trk.splice(trackIndex, 1);
}
});
} else if (level === ListLevel.SEGMENT) {
items.forEach((item, index) => {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
if (index === items.length - 1) {
// Order is reversed, so the last segment is the first one and the one to keep
target = item;
}
toMerge.trkseg.splice(
0,
0,
originalFile.trk[trackIndex].trkseg[segmentIndex].clone()
);
file.trk[trackIndex].trkseg.splice(segmentIndex, 1);
});
}
targetFile = file;
}
first = false;
}
});
if (mergeTraces) {
let statistics = get(gpxStatistics);
let speed =
statistics.global.speed.moving > 0 ? statistics.global.speed.moving : undefined;
let startTime: Date | undefined = undefined;
if (speed !== undefined) {
if (
statistics.global.length > 0 &&
statistics.getTrackPoint(0)!.trkpt.time !== undefined
) {
startTime = statistics.getTrackPoint(0)!.trkpt.time;
} else {
for (let i = 0; i < statistics.global.length; i++) {
const point = statistics.getTrackPoint(i)!;
if (point.trkpt.time !== undefined) {
startTime = new Date(
point.trkpt.time.getTime() -
(1000 * 3600 * point.distance.total) / speed
);
break;
}
}
}
}
if (toMerge.trk.length > 0 && toMerge.trk[0].trkseg.length > 0) {
let s = new TrackSegment();
toMerge.trk.map((track) => {
track.trkseg.forEach((segment) => {
s.replaceTrackPoints(
s.trkpt.length,
s.trkpt.length,
segment.trkpt.slice(),
speed,
startTime,
removeGaps
);
});
});
toMerge.trk = [toMerge.trk[0]];
toMerge.trk[0].trkseg = [s];
}
if (toMerge.trkseg.length > 0) {
let s = new TrackSegment();
toMerge.trkseg.forEach((segment) => {
s.replaceTrackPoints(
s.trkpt.length,
s.trkpt.length,
segment.trkpt.slice(),
speed,
startTime,
removeGaps
);
});
toMerge.trkseg = [s];
}
}
if (targetFile) {
targetFile = targetFile as GPXFile;
if (target instanceof ListFileItem) {
targetFile.replaceTracks(0, targetFile.trk.length - 1, toMerge.trk);
targetFile.replaceWaypoints(0, targetFile.wpt.length - 1, toMerge.wpt);
} else if (target instanceof ListTrackItem) {
let trackIndex = target.getTrackIndex();
targetFile.replaceTrackSegments(trackIndex, 0, -1, toMerge.trkseg);
} else if (target instanceof ListTrackSegmentItem) {
let trackIndex = target.getTrackIndex();
let segmentIndex = target.getSegmentIndex();
targetFile.replaceTrackSegments(
trackIndex,
segmentIndex,
segmentIndex - 1,
toMerge.trkseg
);
}
}
});
},
extractSelection: () => {
return fileActionManager.applyGlobal((draft) => {
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {