Skip to content

feat: split dropped GPX files into named layers - #109

Merged
giswqs merged 1 commit into
mainfrom
feat/gpx-split-dropped-layers
Jun 3, 2026
Merged

feat: split dropped GPX files into named layers#109
giswqs merged 1 commit into
mainfrom
feat/gpx-split-dropped-layers

Conversation

@giswqs

@giswqs giswqs commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Dropping or dragging a GPX file now creates separate Waypoints, Tracks, and Routes layers instead of one merged layer.
  • Each split layer gets a descriptive name derived from the file (e.g. "trip Tracks"), and the shell honors the provided layer name when adding it to the map.

Test plan

  • Drag a GPX file containing waypoints, tracks, and routes onto the desktop app and confirm three separately named layers appear.
  • Drop a GPX file with only one geometry type and confirm a single correctly named layer appears.
  • Open a GPX file via the Add Data dialog and confirm it still loads as a merged layer.

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.
Copilot AI review requested due to automatic review settings June 3, 2026 10:34
@giswqs giswqs linked an issue Jun 3, 2026 that may be closed by this pull request
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Netlify preview: https://pr-109--opengeos.netlify.app

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves GPX drag-and-drop imports in the GeoLibre Desktop app by splitting a dropped GPX into separate vector layers for Waypoints, Tracks, and Routes, while keeping the “Add Data” vector-file flow loading GPX as a single merged layer.

Changes:

  • Added GPX parsing helpers that can either merge GPX content into one FeatureCollection or return multiple named LoadedVectorLayers (Waypoints/Tracks/Routes) for drag-and-drop imports.
  • Updated dropped-file loading (browser + Tauri paths) to split GPX into multiple layers with descriptive names derived from the file name.
  • Updated the desktop shell import path so it honors an optional provided layer.name when adding layers to the map.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
apps/geolibre-desktop/src/lib/tauri-io.ts Adds LoadedVectorLayer support and GPX splitting for drag-and-drop while preserving merged GPX loading for “open vector file” flows.
apps/geolibre-desktop/src/components/layout/DesktopShell.tsx Uses layer.name (when present) instead of always deriving the layer name from the source path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

for (const layer of importedLayers) {
lastLayerId = addGeoJsonLayer(
layerNameFromPath(layer.path),
layer.name ?? layerNameFromPath(layer.path),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX regression (medium confidence): addImportedVectorLayers only calls fitLayer on the last added layer. Before this PR a GPX always produced one layer, so the fit was always correct. Now it can produce up to three — the last one is always Routes (after the [Waypoints, Tracks, Routes] ordering in parseGpxTextLayers). For a file with only waypoints and tracks the view zooms to the tracks layer; for a file with all three it zooms to routes — the user may not see their waypoints at all if they're in a different region.

A simple fix is to compute a union bounding box across all newly-added layers. If mapControllerRef.current exposes a multi-layer fit method (or one can be added), calling it after the loop would resolve this. Otherwise, falling back to the first layer rather than the last would be less surprising as a convention.

Comment on lines +956 to +959
if (extension === "gpx") {
layers.push(...parseGpxTextLayers(await file.text(), file.name));
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling (medium confidence): The Tauri loadDroppedVectorPaths path (added in this same PR) wraps GPX parse errors with "Could not read this GPX file. ${detail}" before rethrowing. The browser path here does not — a parse error from parseGpxLayer (e.g. "The GPX file is not valid XML." or "No valid GPX waypoints, routes, or tracks were found.") propagates raw to whatever catch block the caller has.

For parity you could wrap the same way:

Suggested change
if (extension === "gpx") {
layers.push(...parseGpxTextLayers(await file.text(), file.name));
continue;
}
if (extension === "gpx") {
try {
layers.push(...parseGpxTextLayers(await file.text(), file.name));
} catch (error) {
const detail = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Could not read this GPX file. ${detail}`);
}
continue;
}

Comment on lines +210 to +223
function parseGpxTextLayers(text: string, path: string): LoadedVectorLayer[] {
const result = parseGpxLayer(text);
const baseName = pathWithoutExtension(browserSafeFileName(path)) || "GPX";
return [
{ data: result.waypoints, label: "Waypoints" },
{ data: result.tracks, label: "Tracks" },
{ data: result.routes, label: "Routes" },
]
.filter((layer) => layer.data.features.length > 0)
.map((layer) => ({
data: layer.data,
name: `${baseName} ${layer.label}`,
path,
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nits (grouped, low confidence):

  1. The intermediate objects use a label property that has no declared type and is immediately consumed in .map() to produce name. TypeScript infers this fine, but the intent can be made clearer with a typed tuple or a simple const LAYERS with an explicit type annotation.

  2. loadTauriVectorFile keeps its original return type Promise<{ data: FeatureCollection; path: string }> even though LoadedVectorLayer now exists. Structurally it's compatible (the name? field is optional), but updating the annotation would document intent and unify the codebase.

Neither is a correctness issue, just readability.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs / UX

Finding Confidence
fitLayer only zooms to the last GPX layer. Before this PR a GPX import always yielded one layer so the map fit was correct. Now it can yield up to three ([Waypoints, Tracks, Routes] order). The current code zooms only to the last non-empty one, so users who drop a file with all three types end up with the view locked to Routes — waypoints in a different geographic area won't be visible. Raised as an inline comment on DesktopShell.tsx:159. Medium

Performance

Nothing found.

Security

Nothing found.

Quality

Finding Confidence
Browser-path GPX errors propagate unwrapped. loadDroppedVectorFiles (browser drop) lets parse errors from parseGpxTextLayers surface as-is, while the sibling Tauri loadDroppedVectorPaths path added in this same PR wraps them with "Could not read this GPX file. ${detail}". A one-line try/catch brings them in line. Raised inline on tauri-io.ts:956-959. Medium
label intermediate property and loadTauriVectorFile return type. parseGpxTextLayers builds objects with a label field that is immediately consumed in .map() to produce name — this works but leaves TypeScript to infer a transient shape with no declared type. Separately, loadTauriVectorFile retains its old { data, path } return type annotation rather than LoadedVectorLayer; structurally compatible but inconsistent. Grouped as a minor nit inline at tauri-io.ts:210-223. Low
Test plan gap: browser "Add Data" GPX path. The PR adds a parseGpxText branch to loadBrowserVectorFile — the first time GPX is handled explicitly in the browser's "Add Data" dialog flow (previously it fell through to DuckDB Spatial). The test plan only covers the Tauri dialog and drag-and-drop; testing the browser path would confirm this new branch behaves as intended. Low

CLAUDE.md

No CLAUDE.md found in the repository, so no project-specific guidelines to check.


Summary: The logic is sound and the split-layer approach is well-structured. The main actionable concern is the fitLayer viewport issue — it was fine before but becomes an observable regression now that a single GPX drop can produce multiple layers with potentially different extents.

@giswqs
giswqs merged commit a1e2aab into main Jun 3, 2026
6 checks passed
@giswqs
giswqs deleted the feat/gpx-split-dropped-layers branch June 3, 2026 12:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drag and drop gpx?

2 participants