diff --git a/app.js b/app.js index 1d9db1b..78f764b 100644 --- a/app.js +++ b/app.js @@ -72,6 +72,7 @@ const map = new ol.Map({ controls: ol.control .defaults.defaults({ rotate: false }) .extend([ + createVisibleLayersExtentControl(), new ol.control.Rotate({ autoHide: true, label: createNorthPointerIcon(), @@ -118,6 +119,21 @@ function createNorthPointerIcon() { return icon; } +function createVisibleLayersExtentControl() { + const element = document.createElement("div"); + element.className = "ol-visible-layers-extent ol-unselectable ol-control"; + + const button = document.createElement("button"); + button.type = "button"; + button.title = "Zoom to visible layers"; + button.setAttribute("aria-label", "Zoom to visible layers"); + button.textContent = "⤢"; + button.addEventListener("click", () => fitToVisibleLayers()); + + element.append(button); + return new ol.control.Control({ element }); +} + async function searchParcela(event) { event.preventDefault(); @@ -296,6 +312,34 @@ function fitToLayerExtent(layer) { }); } +function fitToVisibleLayers() { + const visibleLayers = availableLayers.filter((layer) => selectedLayers.includes(layer.name)); + const projectedExtents = visibleLayers + .filter((layer) => layer.extent && layer.extent.every(Number.isFinite)) + .map((layer) => ol.proj.transformExtent(layer.extent, "EPSG:4326", "EPSG:3857")); + + if (!projectedExtents.length) { + updateStatus("Select a layer with a published extent to zoom to it.", "error"); + return; + } + + const combinedExtent = projectedExtents.reduce( + (combined, extent) => [ + Math.min(combined[0], extent[0]), + Math.min(combined[1], extent[1]), + Math.max(combined[2], extent[2]), + Math.max(combined[3], extent[3]), + ], + [Infinity, Infinity, -Infinity, -Infinity], + ); + + map.getView().fit(combinedExtent, { + padding: [64, 64, 64, 64], + duration: 500, + maxZoom: 18, + }); +} + function childElementsByName(node, localName) { return [...(node?.children || [])].filter((child) => child.localName === localName); } diff --git a/styles.css b/styles.css index b2adfe2..1fc57ca 100644 --- a/styles.css +++ b/styles.css @@ -312,6 +312,54 @@ button:disabled { top: 16px; right: 16px; left: auto; + overflow: hidden; + background: var(--panel); + border: 1px solid var(--panel-border); + border-radius: 14px; + backdrop-filter: blur(16px); + box-shadow: var(--shadow); +} + +.map-wrap .ol-zoom button, +.map-wrap .ol-visible-layers-extent button { + width: 2.0625em; + height: 2.0625em; + margin: 0; + background: linear-gradient(135deg, var(--accent) 0%, #2b9a79 100%); + color: #fffefb; + font-size: 1rem; + line-height: 1; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.24); +} + +.map-wrap .ol-zoom button + button { + border-top: 1px solid rgba(255, 255, 255, 0.35); +} + +.map-wrap .ol-zoom button:hover, +.map-wrap .ol-zoom button:focus, +.map-wrap .ol-visible-layers-extent button:hover, +.map-wrap .ol-visible-layers-extent button:focus { + background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%); + color: #fff; + outline: 2px solid rgba(31, 122, 98, 0.35); + outline-offset: -2px; +} + +.map-wrap .ol-visible-layers-extent { + top: 96px; + right: 16px; + left: auto; + overflow: hidden; + background: var(--panel); + border: 1px solid var(--panel-border); + border-radius: 14px; + backdrop-filter: blur(16px); + box-shadow: var(--shadow); +} + +.map-wrap .ol-visible-layers-extent button { + font-size: 1.3rem; } /* OpenLayers hides this control while the view is north-up. */ @@ -364,6 +412,11 @@ button:disabled { right: 12px; } + .map-wrap .ol-visible-layers-extent { + top: 92px; + right: 12px; + } + .map-wrap .ol-rotate { right: 12px; bottom: 12px;