This commit is contained in:
vcoppe committed 2026-10-07 22:20:40 +02:00
1 parent f642d60f42
commit 82d2f79da0
7 files changed
+770 -198

No files matched your search

@@ -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));
}
}
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
@@ -19,7 +32,7 @@
variant="outline"
class="whitespace-normal h-fit min-h-8 py-1"
disabled={!validSelection}
onclick={() => fileActions.addElevationToSelection()}
onclick={() => addElevation()}
>
<MountainSnow size="16" class="shrink-0" />
{i18n._('toolbar.elevation.button')}
@@ -1,51 +1,54 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Ungroup } from '@lucide/svelte';
import {
ListFileItem,
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListWaypointsItem,
} from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions';
import { get } from 'svelte/store';
import { engine } from '$lib/engine';
let props: {
class?: string;
} = $props();
let validSelection = $derived(
$selection.size > 0 &&
$selection.getSelected().every((item) => {
if (
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem ||
item instanceof ListTrackSegmentItem
) {
return false;
}
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
if (item instanceof ListFileItem) {
return file.getSegments().length > 1;
} else if (item instanceof ListTrackItem) {
if (item.getTrackIndex() < file.trk.length) {
return file.trk[item.getTrackIndex()].getSegments().length > 1;
}
}
}
const selection = engine.selection;
const files = engine.files;
const statistics = engine.statistics;
/** The number of segments of a file or of one of its tracks. */
function segmentCount(fileId: string, trackId?: string) {
const file = $files.get(fileId);
const tracks = file ? get(file).structure.tracks : [];
return tracks
.filter((track) => trackId === undefined || track.id === trackId)
.reduce((count, track) => count + track.segments.length, 0);
}
// files or tracks, which all have several segments
let validSelection = $derived.by(() => {
// what is selected can change without the selection: the files are edited
void $statistics;
switch ($selection.type) {
case 'file':
return (
$selection.fileIds.length > 0 &&
$selection.fileIds.every((fileId) => segmentCount(fileId) > 1)
);
case 'track': {
const { fileId, trackIds } = $selection;
return (
trackIds.length > 0 &&
trackIds.every((trackId) => segmentCount(fileId, trackId) > 1)
);
}
default:
return false;
})
);
}
});
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<Button variant="outline" disabled={!validSelection} onclick={fileActions.extractSelection}>
<Button variant="outline" disabled={!validSelection} onclick={() => engine.extract()}>
<Ungroup size="16" />
{i18n._('toolbar.extract.button')}
</Button>
+18
View File
@@ -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.
-152
View File
@@ -99,90 +99,6 @@ export async function loadFiles(list: FileList | File[]): Promise<string[]> {
// 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<ListItem, TrackPoint[]>) => {
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);
}
}
});
});
});
},
};