diff --git a/docs/architecture.adoc b/docs/architecture.adoc index ca04485..de1fcde 100644 --- a/docs/architecture.adoc +++ b/docs/architecture.adoc @@ -139,7 +139,20 @@ back to the app's actual default is worth having: a "Home" button `home-button` MapLibre control — see `docs/third-party-notices.adoc`) sits top-left on the map, pinned to `CENTER`/`ZOOM` explicitly rather than its own fallback (the view at construction time, which would just -be wherever a loaded permalink happened to point). +be wherever a loaded permalink happened to point). It resets to the +app's default view, not the visitor's own location — that's +`GeolocateControl`, a separate button at bottom-right. + +That Home button is the top of a small top-left stack — Home, zoom +in/out (`NavigationControl`, `showCompass: false` since this map never +rotates), and a "Base map" button — matching the National Park +Service's own map control layout (grouped together on the map itself, +rather than split between the map and a side panel). The last of +those, `MapLayersButton` (`public/js/map.js`), is a second entry point +into the exact same Online/Offline panel `LayerToggleControl` already +manages in the page's own sidebar — both call the same +`LayerToggleControl#toggle()`, so there's one source of truth for the +panel's open/closed state either way. == Weather radar overlay diff --git a/public/js/map.js b/public/js/map.js index 0d8fa9f..cf9591b 100644 --- a/public/js/map.js +++ b/public/js/map.js @@ -127,6 +127,13 @@ class LayerToggleControl { if (open) notifySidePanelOpen(this._panel); } + // Public wrapper so a second entry point (MapLayersButton, a map corner + // control) can open/close the same panel without reaching into a + // "private" method by convention only. + toggle() { + this._togglePanel(); + } + setActive(name) { this._panel.querySelectorAll(".basemap-panel__option").forEach((button) => { button.classList.toggle("active", button.dataset.style === name); @@ -142,6 +149,38 @@ class LayerToggleControl { } } +// A small map-corner button (grouped with Home/zoom, see below) that's a +// second entry point to the same panel LayerToggleControl already manages +// in the page's own sidebar — matching the National Park Service's own +// map control layout, which keeps zoom/home/base-map together on the map +// rather than split between the map and a separate side rail. +class MapLayersButton { + constructor(layerToggle) { + this._layerToggle = layerToggle; + } + + onAdd() { + this._container = document.createElement("div"); + this._container.className = "maplibregl-ctrl maplibregl-ctrl-group"; + const button = document.createElement("button"); + button.type = "button"; + button.title = "Base map"; + button.setAttribute("aria-label", "Base map"); + button.innerHTML = LAYERS_ICON; + button.addEventListener("click", () => this._layerToggle.toggle()); + this._container.appendChild(button); + return this._container; + } + + onRemove() { + this._container.remove(); + } + + getDefaultPosition() { + return "top-left"; + } +} + const map = new MaplibreMap({ container: "map", style: ONLINE_STYLE, @@ -155,7 +194,6 @@ const map = new MaplibreMap({ hash: "map", }); -map.addControl(new NavigationControl(), "bottom-right"); map.addControl( new GeolocateControl({ positionOptions: { enableHighAccuracy: true }, @@ -164,19 +202,34 @@ map.addControl( }), "bottom-right", ); + +// A compact top-left stack — Home, zoom, base map — matching the National +// Park Service's own map control layout. Added in this order since +// MapLibre stacks same-position controls in add order. +// // homePosition/zoom pinned to the app's own default rather than left to the // button's own fallback (current center/zoom at construction) — now that // the URL hash can restore any view on load (see the Hash option above), // "current view" and "the default view" are no longer the same thing, and // a home button that resets to wherever you happened to load the page -// wouldn't be a home button. +// wouldn't be a home button. This is *not* "go to my location" — that's +// GeolocateControl, above. map.addControl(new HomeButton({ homePosition: CENTER, zoom: ZOOM }), "top-left"); +// showCompass: false — this map never rotates, so a bearing-reset button +// (NavigationControl's default third button) would just be dead weight. +map.addControl(new NavigationControl({ showCompass: false }), "top-left"); const sidebar = document.getElementById("sidebar"); const layerToggle = new LayerToggleControl(applyStyle); layerToggle.mount(sidebar); layerToggle.setActive("online"); +// The sidebar already has a "Base map" icon (opens the same panel) — +// this is a second entry point for it, grouped with Home/zoom on the map +// itself rather than off in the page's own side rail, matching NPS's +// layout. Added after mount() since it reuses that panel/toggle state. +const mapLayersButton = new MapLayersButton(layerToggle); +map.addControl(mapLayersButton, "top-left"); const profilePanel = new TrackProfilePanel(map); profilePanel.mount(); @@ -202,6 +255,7 @@ fetch("/api/offline-tiles") applyStyle("offline"); } else { layerToggle.remove(); + map.removeControl(mapLayersButton); } }) .catch(() => {