Turn the GPX category legend into a clickable Unigrid filter matrix
Legend items are now toggle buttons (icon grid, not a color-swatch list) that hide/show that category's waypoints on the map. Filtering re-slices each file's source data rather than using a layer filter, since MapLibre clusters a GeoJSON source's full data regardless of what a layer filter hides.
This commit is contained in:
1 parent
bc9ea58614
commit
53b0e8961b
3 files changed
+90
-13
No files matched your search
+48
-9
@@ -43,6 +43,7 @@ export class GpxPanelControl {
|
||||
this._visible = new Set(); // file paths currently shown on the map
|
||||
this._interactiveLayers = new Set(); // point layer ids with click/hover wired up
|
||||
this._pointsByPath = new Map(); // path -> its waypoints FeatureCollection, for the category legend
|
||||
this._hiddenCategories = new Set(); // category names toggled off in the legend
|
||||
this._openDownloadMenu = null; // the one gpx-tree__download-menu currently open, if any
|
||||
|
||||
// setStyle() (switching the Online/Offline base map) tears down all
|
||||
@@ -545,6 +546,7 @@ export class GpxPanelControl {
|
||||
|
||||
if (points.features.length > 0) this._pointsByPath.set(path, points);
|
||||
this._updateLegend();
|
||||
this._applyCategoryFilter();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -713,32 +715,69 @@ export class GpxPanelControl {
|
||||
|
||||
// Rebuilt from whichever visible files actually carry categorized
|
||||
// waypoints (a park4night/OsmAnd POI export) — a plain GPS track has none
|
||||
// and the legend just stays hidden.
|
||||
// and the legend just stays hidden. Doubles as a filter: each item is a
|
||||
// toggle button (Unigrid-style icon grid) that hides/shows that category
|
||||
// on the map, not just a read-only color key.
|
||||
_updateLegend() {
|
||||
const categories = new Map(); // category name -> color
|
||||
const categories = new Map(); // category name -> { color, icon }
|
||||
for (const points of this._pointsByPath.values()) {
|
||||
for (const feature of points.features) {
|
||||
const category = feature.properties.category;
|
||||
if (category && !categories.has(category)) categories.set(category, feature.properties.color || TRACK_COLOR);
|
||||
if (category && !categories.has(category)) {
|
||||
categories.set(category, { color: feature.properties.color || TRACK_COLOR, icon: feature.properties.icon });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._legendContainer.innerHTML = "";
|
||||
this._legendContainer.hidden = categories.size === 0;
|
||||
[...categories].sort(([a], [b]) => a.localeCompare(b)).forEach(([category, color]) => {
|
||||
const item = document.createElement("div");
|
||||
[...categories].sort(([a], [b]) => a.localeCompare(b)).forEach(([category, { color, icon }]) => {
|
||||
const item = document.createElement("button");
|
||||
item.type = "button";
|
||||
item.className = "gpx-legend__item";
|
||||
const swatch = document.createElement("span");
|
||||
swatch.className = "gpx-legend__swatch";
|
||||
swatch.style.background = color; // a malformed/malicious value from an uploaded file is simply ignored by the CSSOM, never parsed as markup
|
||||
item.classList.toggle("gpx-legend__item--hidden", this._hiddenCategories.has(category));
|
||||
item.title = category;
|
||||
item.setAttribute("aria-pressed", String(!this._hiddenCategories.has(category)));
|
||||
|
||||
const glyph = document.createElement("span");
|
||||
if (icon && POI_ICONS[icon]) {
|
||||
glyph.className = "gpx-legend__icon";
|
||||
glyph.innerHTML = POI_ICONS[icon];
|
||||
} else {
|
||||
glyph.className = "gpx-legend__swatch";
|
||||
glyph.style.background = color; // a malformed/malicious value from an uploaded file is simply ignored by the CSSOM, never parsed as markup
|
||||
}
|
||||
const label = document.createElement("span");
|
||||
label.className = "gpx-legend__label";
|
||||
label.textContent = category;
|
||||
item.appendChild(swatch);
|
||||
item.appendChild(glyph);
|
||||
item.appendChild(label);
|
||||
|
||||
item.addEventListener("click", () => {
|
||||
if (!this._hiddenCategories.delete(category)) this._hiddenCategories.add(category);
|
||||
this._updateLegend();
|
||||
this._applyCategoryFilter();
|
||||
});
|
||||
|
||||
this._legendContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Re-clusters each visible file's points around only the categories still
|
||||
// toggled on — a plain layer `filter` can't do this: MapLibre clusters a
|
||||
// GeoJSON source's *entire* data set regardless of what a layer filter
|
||||
// hides, so a cluster's count/expansion would stay wrong. Cheap to redo
|
||||
// in full: _pointsByPath already holds each file's complete features in
|
||||
// memory, this is just re-slicing it, no re-parsing or network involved.
|
||||
_applyCategoryFilter() {
|
||||
for (const [path, points] of this._pointsByPath) {
|
||||
const source = this._map.getSource(pointsSourceId(path));
|
||||
if (!source) continue;
|
||||
const visible = points.features.filter((f) => !this._hiddenCategories.has(f.properties.category));
|
||||
source.setData({ type: "FeatureCollection", features: visible });
|
||||
}
|
||||
}
|
||||
|
||||
async _uploadFiles(fileList, folder) {
|
||||
// .zip: Garmin's bulk/FIT export format — the server expands it into
|
||||
// its .gpx/.fit/.geojson entries, so it's accepted here even though the
|
||||
|
||||
Reference in new issue
Block a user