Surface GPX/FIT parse errors instead of silently rendering nothing
CI / check (push) Successful in 44s
CI / deploy (push) Successful in 17s

_addLayers handed MapLibre a URL and let it fetch the GeoJSON
internally — a 422 (a file REXML/Fit can't parse) just meant the
layer never appeared, with nothing telling the user why. It now
fetches the file itself first, alerts with the actual error and
un-checks the box on failure.
This commit is contained in:
randy committed 2026-09-07 14:10:46 +02:00
1 parent a0476f1204
commit 11fc14824e
2 files changed
+41 -7

No files matched your search

+7 -1
View File
@@ -189,7 +189,13 @@ equivalent, so it never produces `Point` features. MapLibre's
`geometry-type` expression treats `MultiLineString` the same as `geometry-type` expression treats `MultiLineString` the same as
`LineString`, so a single GeoJSON source per file with a `line` layer `LineString`, so a single GeoJSON source per file with a `line` layer
and a `circle` layer (each filtered by geometry type) is enough to and a `circle` layer (each filtered by geometry type) is enough to
render both. render both. Checking a file fetches its GeoJSON itself
(`public/js/gpx.js`'s `_addLayers`) and hands the parsed result to
`addSource` — rather than passing MapLibre a URL and letting it fetch
internally — specifically so a `422` (a file REXML or `Fit` can't
parse) surfaces as an alert and un-checks the box instead of the layer
just silently never appearing, which is what a `data: url` source does
on a failed fetch.
FIT parsing:: No gem was added for this — the one Ruby option FIT parsing:: No gem was added for this — the one Ruby option
(`fit-parser`, tjwallace/fit) hasn't been touched since 2022 and pulls (`fit-parser`, tjwallace/fit) hasn't been touched since 2022 and pulls
+34 -6
View File
@@ -206,7 +206,7 @@ export class GpxPanelControl {
const checkbox = document.createElement("input"); const checkbox = document.createElement("input");
checkbox.type = "checkbox"; checkbox.type = "checkbox";
checkbox.checked = this._visible.has(file.path); checkbox.checked = this._visible.has(file.path);
checkbox.addEventListener("change", () => this._toggleFile(file.path, checkbox.checked)); checkbox.addEventListener("change", () => this._toggleFile(file.path, checkbox.checked, checkbox));
const name = document.createElement("span"); const name = document.createElement("span");
name.className = "gpx-tree__file-name"; name.className = "gpx-tree__file-name";
name.textContent = file.name; name.textContent = file.name;
@@ -269,21 +269,48 @@ export class GpxPanelControl {
}); });
} }
_toggleFile(path, visible) { async _toggleFile(path, visible, checkbox) {
if (visible) { if (visible) {
this._visible.add(path); this._visible.add(path);
this._addLayers(path); const ok = await this._addLayers(path);
if (!ok) {
// Loading failed (see _addLayers) — don't leave the box checked
// for a file that isn't actually showing anything.
this._visible.delete(path);
if (checkbox) checkbox.checked = false;
}
} else { } else {
this._visible.delete(path); this._visible.delete(path);
this._removeLayers(path); this._removeLayers(path);
} }
} }
_addLayers(path) { // Fetches the file's GeoJSON itself (rather than handing maplibre a URL
// to fetch internally) so a parse failure — a real GPX a device/app
// wrote in some way REXML doesn't like, say — surfaces as an alert
// instead of the layer just silently never appearing. Returns whether it
// succeeded.
async _addLayers(path) {
const id = sourceId(path); const id = sourceId(path);
if (this._map.getSource(id)) return; if (this._map.getSource(id)) return true;
this._map.addSource(id, { type: "geojson", data: gpxFileUrl(path) }); let geojson;
try {
const response = await fetch(gpxFileUrl(path));
if (!response.ok) {
const { error } = await response.json().catch(() => ({}));
window.alert(error || `Could not load "${path}".`);
return false;
}
geojson = await response.json();
} catch (err) {
window.alert(`Could not load "${path}": ${err.message}`);
return false;
}
if (this._map.getSource(id)) return true; // added concurrently (e.g. a style.load race) while this was fetching
this._map.addSource(id, { type: "geojson", data: geojson });
this._map.addLayer({ this._map.addLayer({
id: `${id}-line`, id: `${id}-line`,
type: "line", type: "line",
@@ -307,6 +334,7 @@ export class GpxPanelControl {
this._registerPointInteractivity(`${id}-points`); this._registerPointInteractivity(`${id}-points`);
this._registerLineInteractivity(`${id}-line`); this._registerLineInteractivity(`${id}-line`);
return true;
} }
// Registered once per layer id, ever — map.on(type, layerId, fn) is a // Registered once per layer id, ever — map.on(type, layerId, fn) is a