Skip to content

Commit 16d9cab

Browse files
committed
feat: split dropped GPX files into named layers
Dragging or dropping a GPX file now yields separate Waypoints, Tracks, and Routes layers with descriptive names instead of a single merged layer, so each geometry type can be styled and toggled independently.
1 parent 1e6293a commit 16d9cab

2 files changed

Lines changed: 73 additions & 10 deletions

File tree

apps/geolibre-desktop/src/components/layout/DesktopShell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export function DesktopShell({
156156
let lastLayerId: string | null = null;
157157
for (const layer of importedLayers) {
158158
lastLayerId = addGeoJsonLayer(
159-
layerNameFromPath(layer.path),
159+
layer.name ?? layerNameFromPath(layer.path),
160160
layer.data,
161161
layer.path,
162162
);

apps/geolibre-desktop/src/lib/tauri-io.ts

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { unzip } from "fflate";
1010
import type { FeatureCollection } from "geojson";
1111
import shp from "shpjs";
1212
import type { DuckDbVectorFile } from "./duckdb-vector-loader";
13+
import { parseGpxLayer } from "./gpx";
1314

1415
export function isTauri(): boolean {
1516
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
@@ -96,6 +97,12 @@ interface SaveBinaryFileOptions extends SaveTextFileOptions {}
9697

9798
const SHAPEFILE_SIDECAR_EXTENSIONS = ["dbf", "shx", "prj", "cpg"];
9899

100+
export interface LoadedVectorLayer {
101+
data: FeatureCollection;
102+
name?: string;
103+
path: string;
104+
}
105+
99106
// Auxiliary files that accompany Shapefiles (spatial indexes, metadata, etc.)
100107
// but are never standalone vector layers. Skipping them keeps a single such
101108
// file from aborting an otherwise valid drag-and-drop import.
@@ -191,6 +198,31 @@ async function parseGeoJsonText(
191198
return assertFeatureCollection(JSON.parse(text));
192199
}
193200

201+
function parseGpxText(text: string): FeatureCollection {
202+
const result = parseGpxLayer(text);
203+
return mergeFeatureCollections([
204+
result.waypoints,
205+
result.tracks,
206+
result.routes,
207+
]);
208+
}
209+
210+
function parseGpxTextLayers(text: string, path: string): LoadedVectorLayer[] {
211+
const result = parseGpxLayer(text);
212+
const baseName = pathWithoutExtension(browserSafeFileName(path)) || "GPX";
213+
return [
214+
{ data: result.waypoints, label: "Waypoints" },
215+
{ data: result.tracks, label: "Tracks" },
216+
{ data: result.routes, label: "Routes" },
217+
]
218+
.filter((layer) => layer.data.features.length > 0)
219+
.map((layer) => ({
220+
data: layer.data,
221+
name: `${baseName} ${layer.label}`,
222+
path,
223+
}));
224+
}
225+
194226
async function parseShapefileZip(
195227
data: ArrayBuffer | Uint8Array,
196228
): Promise<FeatureCollection> {
@@ -270,10 +302,7 @@ async function fileToDuckDbVectorFile(file: File): Promise<DuckDbVectorFile> {
270302
async function loadBrowserVectorFile(
271303
file: File,
272304
siblingFiles: DuckDbVectorFile[] = [],
273-
): Promise<{
274-
data: FeatureCollection;
275-
path: string;
276-
}> {
305+
): Promise<LoadedVectorLayer> {
277306
const extension = fileExtension(file.name);
278307
if (extension === "geojson" || extension === "json") {
279308
try {
@@ -305,6 +334,13 @@ async function loadBrowserVectorFile(
305334
};
306335
}
307336

337+
if (extension === "gpx") {
338+
return {
339+
data: parseGpxText(await file.text()),
340+
path: file.name,
341+
};
342+
}
343+
308344
return {
309345
data: await loadDuckDbVector({
310346
name: file.name,
@@ -391,6 +427,18 @@ async function loadTauriVectorFile(path: string): Promise<{
391427
}
392428
}
393429

430+
if (extension === "gpx") {
431+
try {
432+
return {
433+
data: parseGpxText(await readTextFile(path)),
434+
path,
435+
};
436+
} catch (error) {
437+
const detail = error instanceof Error ? error.message : "Unknown error";
438+
throw new Error(`Could not read this GPX file. ${detail}`);
439+
}
440+
}
441+
394442
try {
395443
const siblingFiles =
396444
extension === "shp" ? await readShapefileSiblings(path) : [];
@@ -884,7 +932,7 @@ export async function openVectorFileWithFallback(): Promise<{
884932

885933
export async function loadDroppedVectorFiles(
886934
droppedFiles: FileList | File[],
887-
): Promise<Array<{ data: FeatureCollection; path: string }>> {
935+
): Promise<LoadedVectorLayer[]> {
888936
const droppedFileArray = Array.from(droppedFiles);
889937
const files = droppedFileArray.filter((file) =>
890938
isVectorFileName(file.name),
@@ -900,11 +948,16 @@ export async function loadDroppedVectorFiles(
900948
]);
901949
}
902950

903-
const layers: Array<{ data: FeatureCollection; path: string }> = [];
951+
const layers: LoadedVectorLayer[] = [];
904952
for (const file of files) {
905953
const extension = fileExtension(file.name);
906954
if (SHAPEFILE_SIDECAR_EXTENSIONS.includes(extension)) continue;
907955

956+
if (extension === "gpx") {
957+
layers.push(...parseGpxTextLayers(await file.text(), file.name));
958+
continue;
959+
}
960+
908961
const siblingFiles =
909962
extension === "shp"
910963
? await Promise.all(
@@ -928,13 +981,23 @@ export async function loadDroppedVectorFiles(
928981

929982
export async function loadDroppedVectorPaths(
930983
paths: string[],
931-
): Promise<Array<{ data: FeatureCollection; path: string }>> {
984+
): Promise<LoadedVectorLayer[]> {
932985
const vectorPaths = paths.filter(isVectorFileName);
933986
if (!vectorPaths.length) return [];
934987

935-
const layers: Array<{ data: FeatureCollection; path: string }> = [];
988+
const layers: LoadedVectorLayer[] = [];
936989
for (const path of vectorPaths) {
937-
if (SHAPEFILE_SIDECAR_EXTENSIONS.includes(fileExtension(path))) continue;
990+
const extension = fileExtension(path);
991+
if (SHAPEFILE_SIDECAR_EXTENSIONS.includes(extension)) continue;
992+
if (extension === "gpx") {
993+
try {
994+
layers.push(...parseGpxTextLayers(await readTextFile(path), path));
995+
} catch (error) {
996+
const detail = error instanceof Error ? error.message : "Unknown error";
997+
throw new Error(`Could not read this GPX file. ${detail}`);
998+
}
999+
continue;
1000+
}
9381001
layers.push(await loadTauriVectorFile(path));
9391002
}
9401003

0 commit comments

Comments
 (0)