diff --git a/public/css/style.css b/public/css/style.css index 5d92969..3271029 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -397,6 +397,68 @@ html, body { visibility: visible; } +/* Docked detail panel for a clicked hiking trail — replaces a map-anchored + Popup (which routinely covered up the very trail line it described) with + a fixed side panel, same "click info moves off the map" idea + track_profile.js already established for clicking a GPX track line + (TrackProfilePanel). .trail-panel__body gets the exact padding + .maplibregl-popup-content used (15px 10px) so routeHtml()'s own output — + built for that popup, including its header's canceling negative margin — + renders here unchanged; no separate panel-flavored template needed. */ +.trail-panel { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 300px; + max-width: 90vw; + background: #fff; + border-left: 1px solid #ddd; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15); + z-index: 10; +} + +.trail-panel__body { + height: 100%; + box-sizing: border-box; + overflow-y: auto; + padding: 15px 10px; + font-size: 13px; +} + +.trail-panel__close { + position: absolute; + top: 6px; + right: 6px; + border: none; + border-radius: 50%; + background: rgba(255, 255, 255, 0.85); + font-size: 18px; + line-height: 1; + padding: 2px 7px; + cursor: pointer; +} + +/* First breakpoint in this file. Below it the same panel docks to the + bottom edge instead of the right one — a fixed-width side panel would + otherwise eat most of a phone-width map, the exact "popup dominates the + screen" problem this panel was meant to fix in the first place. */ +@media (max-width: 640px) { + .trail-panel { + top: auto; + left: 0; + right: 0; + bottom: 0; + width: auto; + max-width: none; + max-height: 50vh; + border-left: none; + border-top: 1px solid #ddd; + border-radius: 8px 8px 0 0; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15); + } +} + .offline-data-panel__body { padding: 8px 12px; } diff --git a/public/js/hiking_trails.js b/public/js/hiking_trails.js index df9226b..455c159 100644 --- a/public/js/hiking_trails.js +++ b/public/js/hiking_trails.js @@ -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("
Looking for trails…
`) - .addTo(this._map); - this._trackPopup(popup); + this._showPanel(`Looking for trails…
`); + // 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(`Could not reach Overpass.
`); + if (this._onlineRequest === request) this._panelBody.innerHTML = `Could not reach Overpass.
`; return; } + if (this._onlineRequest !== request) return; if (relations.length === 0) { - popup.setHTML(`No hiking trail found here.
`); + this._panelBody.innerHTML = `No hiking trail found here.
`; return; } - popup.setHTML(relations.map((relation) => routeHtml(overpassRouteInfo(relation))).join("