Show clicked hiking trail info in a docked panel, not a popup
A map-anchored Popup on a trail click routinely overlapped the very line it described. Follows the same click -> docked panel pattern gpx.js already uses for a track line click (TrackProfilePanel) rather than inventing a new one: a fixed #trail-panel docks to the right edge on desktop and the bottom edge on narrow viewports (this file's first @media breakpoint), so the trail stays fully visible either way. routeHtml()'s own markup is reused unchanged for the panel body — .trail-panel__body's padding matches .maplibregl-popup-content's own (15px 10px) so the header's canceling negative margin still lines up. The popup-identity staleness guard for the async Overpass lookup is replaced with a plain request token, since there's no longer a fresh Popup instance to key off of.
This commit is contained in:
1 parent
eada464fb3
commit
9edc2d9534
3 files changed
+102
-34
No files matched your search
+35
-34
@@ -1,4 +1,3 @@
|
||||
import { Popup } from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
||||
import { WALKING_ICON } from "/js/icons.js";
|
||||
import { firstSymbolLayerId } from "/js/radar.js";
|
||||
import { escapeHtml } from "/js/gpx.js";
|
||||
@@ -101,11 +100,15 @@ export class HikingTrailsControl {
|
||||
|
||||
map.on("click", (event) => this._handleClick(event));
|
||||
|
||||
this._panel = document.getElementById("trail-panel");
|
||||
this._panelBody = this._panel.querySelector(".trail-panel__body");
|
||||
this._panel.querySelector(".trail-panel__close").addEventListener("click", () => this._hidePanel());
|
||||
|
||||
// setStyle() (switching Online/Offline) tears down all custom sources/
|
||||
// layers — put whatever was showing back, same as every other
|
||||
// Overlays control. The highlight is transient (tied to whichever
|
||||
// popup's still open, if any) — simpler to just drop it here than try
|
||||
// to carry it across a style swap.
|
||||
// trail is showing in the panel, if any) — simpler to just drop it
|
||||
// here than try to carry it across a style swap.
|
||||
map.on("style.load", () => {
|
||||
if (this._active) this._addOnlineLayer();
|
||||
for (const code of this._selectedCountries) this._addOfflineLayer(code);
|
||||
@@ -191,33 +194,33 @@ export class HikingTrailsControl {
|
||||
|
||||
// The local vector layers have real features to hit-test directly — no
|
||||
// network round-trip, so this resolves instantly. A genuine miss (empty
|
||||
// result) shows no popup at all rather than a "not found" message, same
|
||||
// as clicking empty space anywhere else on the map.
|
||||
// result) just closes an already-open panel rather than showing a "not
|
||||
// found" message, same as clicking empty space anywhere else on the map.
|
||||
_handleOfflineClick(event) {
|
||||
const layers = [...this._selectedCountries].map(offlineLayerId).filter((id) => this._map.getLayer(id));
|
||||
if (layers.length === 0) return;
|
||||
|
||||
const features = this._map.queryRenderedFeatures(event.point, { layers });
|
||||
if (features.length === 0) return;
|
||||
if (features.length === 0) {
|
||||
this._hidePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
const matched = features.filter((feature, i) => features.findIndex((f) => f.properties.id === feature.properties.id) === i);
|
||||
const html = matched.map((feature) => routeHtml(feature.properties)).join("<hr>");
|
||||
this._setHighlight(matched.map((feature) => ({ geometry: feature.geometry, color: feature.properties.color })));
|
||||
const popup = new Popup({ closeButton: true, maxWidth: "260px" }).setLngLat(event.lngLat).setHTML(html).addTo(this._map);
|
||||
this._trackPopup(popup);
|
||||
this._showPanel(html);
|
||||
}
|
||||
|
||||
// MapLibre's own closeOnClick (default true) means a click on a *new*
|
||||
// trail both opens its popup and, in the same event dispatch, closes
|
||||
// whichever popup was already open — firing *that* popup's "close"
|
||||
// *after* this method already set the highlight for the new one. Without
|
||||
// this guard, the stale popup's close handler would wipe the highlight
|
||||
// that was just set a moment earlier for the popup replacing it.
|
||||
_trackPopup(popup) {
|
||||
this._activePopup = popup;
|
||||
popup.on("close", () => {
|
||||
if (this._activePopup === popup) this._clearHighlight();
|
||||
});
|
||||
_showPanel(html) {
|
||||
this._panelBody.innerHTML = html;
|
||||
this._panel.hidden = false;
|
||||
}
|
||||
|
||||
_hidePanel() {
|
||||
if (this._panel.hidden) return;
|
||||
this._panel.hidden = true;
|
||||
this._clearHighlight();
|
||||
}
|
||||
|
||||
// The raster tiles have no queryable features (they're pixels), so
|
||||
@@ -227,30 +230,28 @@ export class HikingTrailsControl {
|
||||
// pixel-thick) rendered line.
|
||||
async _handleOnlineClick(event) {
|
||||
const { lng, lat } = event.lngLat;
|
||||
const popup = new Popup({ closeButton: true, maxWidth: "260px" })
|
||||
.setLngLat(event.lngLat)
|
||||
.setHTML(`<p class="gpx-popup__category">Looking for trails…</p>`)
|
||||
.addTo(this._map);
|
||||
this._trackPopup(popup);
|
||||
this._showPanel(`<p class="gpx-popup__category">Looking for trails…</p>`);
|
||||
// A newer click's request replaces this one while Overpass is still
|
||||
// answering — same "in-flight response arrives after it's already
|
||||
// stale" guard the old Popup-identity check gave, just keyed off a
|
||||
// fresh token each click instead of a fresh Popup instance.
|
||||
const request = (this._onlineRequest = {});
|
||||
|
||||
let relations;
|
||||
try {
|
||||
relations = await fetchNearbyRoutes(lng, lat);
|
||||
} catch {
|
||||
popup.setHTML(`<p class="gpx-popup__category">Could not reach Overpass.</p>`);
|
||||
if (this._onlineRequest === request) this._panelBody.innerHTML = `<p class="gpx-popup__category">Could not reach Overpass.</p>`;
|
||||
return;
|
||||
}
|
||||
if (this._onlineRequest !== request) return;
|
||||
|
||||
if (relations.length === 0) {
|
||||
popup.setHTML(`<p class="gpx-popup__category">No hiking trail found here.</p>`);
|
||||
this._panelBody.innerHTML = `<p class="gpx-popup__category">No hiking trail found here.</p>`;
|
||||
return;
|
||||
}
|
||||
popup.setHTML(relations.map((relation) => routeHtml(overpassRouteInfo(relation))).join("<hr>"));
|
||||
// A newer click may have replaced this popup while Overpass was still
|
||||
// answering — don't paint a highlight for a selection that's already
|
||||
// stale.
|
||||
if (this._activePopup === popup)
|
||||
this._setHighlight(relations.map((relation) => ({ geometry: routeGeometry(relation), color: normalizedTrailColor(relation.tags?.colour) })));
|
||||
this._panelBody.innerHTML = relations.map((relation) => routeHtml(overpassRouteInfo(relation))).join("<hr>");
|
||||
this._setHighlight(relations.map((relation) => ({ geometry: routeGeometry(relation), color: normalizedTrailColor(relation.tags?.colour) })));
|
||||
}
|
||||
|
||||
_addOnlineLayer() {
|
||||
@@ -389,8 +390,8 @@ function routeGeometry(relation) {
|
||||
|
||||
// Adapts a raw Overpass relation into the same shape offline GeoJSON
|
||||
// features' properties already carry (lib/maptool/hiking_trails.rb's own
|
||||
// ROUTE_TAGS + distance_meters + id) — one popup builder below serves
|
||||
// both instead of two near-identical ones.
|
||||
// ROUTE_TAGS + distance_meters + id) — one HTML builder (routeHtml) below
|
||||
// serves both instead of two near-identical ones.
|
||||
function overpassRouteInfo(relation) {
|
||||
const tags = relation.tags || {};
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user