2025-10-17 23:54:45 +02:00
|
|
|
import { fileStateCollection } from '$lib/logic/file-state';
|
|
|
|
|
import { fileActionManager } from '$lib/logic/file-action-manager';
|
2025-10-18 16:10:08 +02:00
|
|
|
import { applyToOrderedItemsFromFile, copied, cut, selection } from '$lib/logic/selection';
|
2025-10-17 23:54:45 +02:00
|
|
|
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
2025-10-05 19:34:05 +02:00
|
|
|
import {
|
|
|
|
|
ListFileItem,
|
|
|
|
|
ListLevel,
|
|
|
|
|
ListRootItem,
|
|
|
|
|
ListTrackItem,
|
|
|
|
|
ListTrackSegmentItem,
|
|
|
|
|
ListWaypointItem,
|
|
|
|
|
type ListItem,
|
|
|
|
|
} from '$lib/components/file-list/file-list';
|
|
|
|
|
import { i18n } from '$lib/i18n.svelte';
|
2026-10-04 16:11:44 +02:00
|
|
|
import { freeze } from 'immer';
|
2025-10-05 19:34:05 +02:00
|
|
|
import {
|
|
|
|
|
GPXFile,
|
|
|
|
|
Track,
|
|
|
|
|
TrackPoint,
|
|
|
|
|
TrackSegment,
|
|
|
|
|
Waypoint,
|
|
|
|
|
type LineStyleExtension,
|
|
|
|
|
type WaypointType,
|
|
|
|
|
} from 'gpx';
|
2025-10-17 23:54:45 +02:00
|
|
|
import { get } from 'svelte/store';
|
2025-10-18 00:46:59 +02:00
|
|
|
import { settings } from '$lib/logic/settings';
|
2026-10-07 19:38:21 +02:00
|
|
|
import { getClosestTrackSegments, getElevation } from '$lib/utils';
|
2025-10-18 16:10:08 +02:00
|
|
|
import { gpxStatistics } from '$lib/logic/statistics';
|
2025-10-19 16:14:05 +02:00
|
|
|
import { boundsManager } from './bounds';
|
2026-10-04 16:11:44 +02:00
|
|
|
import { engine } from '$lib/engine';
|
|
|
|
|
import { defaultFileName } from '$lib/default-file-name';
|
2025-10-05 19:34:05 +02:00
|
|
|
|
|
|
|
|
// Generate unique file ids, different from the ones in the database
|
|
|
|
|
export function getFileIds(n: number) {
|
|
|
|
|
let ids = [];
|
|
|
|
|
for (let index = 0; ids.length < n; index++) {
|
|
|
|
|
let id = `gpx-${index}`;
|
2025-10-17 23:54:45 +02:00
|
|
|
if (!fileStateCollection.getFile(id)) {
|
2025-10-05 19:34:05 +02:00
|
|
|
ids.push(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ids;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 16:11:44 +02:00
|
|
|
/** 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 19:34:05 +02:00
|
|
|
export function newGPXFile() {
|
|
|
|
|
let file = new GPXFile();
|
2026-10-04 16:11:44 +02:00
|
|
|
file.metadata.name = newFileName();
|
2025-10-05 19:34:05 +02:00
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createFile() {
|
2026-10-04 16:11:44 +02:00
|
|
|
// the engine selects the new file
|
|
|
|
|
engine.newFile(newFileName());
|
2025-10-18 00:46:59 +02:00
|
|
|
currentTool.set(Tool.ROUTING);
|
2025-10-05 19:34:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function triggerFileInput() {
|
|
|
|
|
const input = document.createElement('input');
|
|
|
|
|
input.type = 'file';
|
|
|
|
|
input.accept = '.gpx';
|
|
|
|
|
input.multiple = true;
|
|
|
|
|
input.className = 'hidden';
|
|
|
|
|
input.onchange = () => {
|
|
|
|
|
if (input.files) {
|
|
|
|
|
loadFiles(input.files);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
input.click();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 16:11:44 +02:00
|
|
|
/**
|
|
|
|
|
* 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<string[]> {
|
|
|
|
|
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);
|
2025-10-05 19:34:05 +02:00
|
|
|
}
|
2026-10-04 16:11:44 +02:00
|
|
|
return ids;
|
2025-10-05 19:34:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper functions for file operations
|
|
|
|
|
export const fileActions = {
|
|
|
|
|
extractSelection: () => {
|
2025-10-18 16:10:08 +02:00
|
|
|
return fileActionManager.applyGlobal((draft) => {
|
|
|
|
|
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
|
|
|
if (level === ListLevel.FILE) {
|
|
|
|
|
let file = fileStateCollection.getFile(fileId);
|
2025-12-24 12:43:24 +01:00
|
|
|
let statistics = fileStateCollection.getStatistics(fileId);
|
|
|
|
|
if (file && statistics) {
|
2025-10-18 16:10:08 +02:00
|
|
|
if (file.trk.length > 1) {
|
|
|
|
|
let fileIds = getFileIds(file.trk.length);
|
2025-12-24 12:43:24 +01:00
|
|
|
let closest = file.wpt.map((wpt) =>
|
|
|
|
|
getClosestTrackSegments(file, statistics, wpt.getCoordinates())
|
|
|
|
|
);
|
2025-10-18 16:10:08 +02:00
|
|
|
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,
|
2025-12-24 13:07:22 +01:00
|
|
|
file.wpt.filter((wpt, wptIndex) =>
|
|
|
|
|
closest[wptIndex].some(
|
|
|
|
|
([trackIndex, segmentIndex]) => trackIndex === index
|
2025-12-24 12:43:24 +01:00
|
|
|
)
|
2025-12-24 13:07:22 +01:00
|
|
|
)
|
2025-10-18 16:10:08 +02:00
|
|
|
);
|
|
|
|
|
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);
|
2025-12-24 12:43:24 +01:00
|
|
|
let closest = file.wpt.map((wpt) =>
|
|
|
|
|
getClosestTrackSegments(file, statistics, wpt.getCoordinates())
|
|
|
|
|
);
|
2025-10-18 16:10:08 +02:00
|
|
|
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,
|
2025-12-24 13:07:22 +01:00
|
|
|
file.wpt.filter((wpt, wptIndex) =>
|
|
|
|
|
closest[wptIndex].some(
|
|
|
|
|
([trackIndex, segmentIndex]) => segmentIndex === index
|
2025-12-24 12:43:24 +01:00
|
|
|
)
|
2025-12-24 13:07:22 +01:00
|
|
|
)
|
2025-10-18 16:10:08 +02:00
|
|
|
);
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-10-05 19:34:05 +02:00
|
|
|
},
|
|
|
|
|
reduce: (itemsAndPoints: Map<ListItem, TrackPoint[]>) => {
|
2025-10-18 16:10:08 +02:00
|
|
|
if (itemsAndPoints.size === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fileActionManager.applyGlobal((draft) => {
|
|
|
|
|
let allItems = Array.from(itemsAndPoints.keys());
|
|
|
|
|
applyToOrderedItemsFromFile(allItems, (fileId, level, items) => {
|
|
|
|
|
let file = draft.get(fileId);
|
|
|
|
|
if (file) {
|
|
|
|
|
for (let item of items) {
|
|
|
|
|
if (item instanceof ListTrackSegmentItem) {
|
|
|
|
|
let trackIndex = item.getTrackIndex();
|
|
|
|
|
let segmentIndex = item.getSegmentIndex();
|
|
|
|
|
let points = itemsAndPoints.get(item);
|
|
|
|
|
if (points) {
|
|
|
|
|
file.replaceTrackPoints(
|
|
|
|
|
trackIndex,
|
|
|
|
|
segmentIndex,
|
|
|
|
|
0,
|
|
|
|
|
file.trk[trackIndex].trkseg[
|
|
|
|
|
segmentIndex
|
|
|
|
|
].getNumberOfTrackPoints() - 1,
|
|
|
|
|
points
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-10-05 19:34:05 +02:00
|
|
|
},
|
2026-01-30 21:17:11 +01:00
|
|
|
addElevationToSelection: async () => {
|
2025-10-18 16:10:08 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-10-05 19:34:05 +02:00
|
|
|
},
|
|
|
|
|
};
|