feat: add area zoom map control

This commit is contained in:
2026-07-22 09:44:25 +01:00
parent 93ac14941e
commit ff8d55f1c0
2 changed files with 133 additions and 0 deletions
+78
View File
@@ -72,12 +72,21 @@ const parcelSearchLayer = new ol.layer.Vector({
}),
});
const areaZoomInteraction = new ol.interaction.DragBox({
className: "ol-area-zoom-box",
condition: ol.events.condition.primaryAction,
minArea: 16,
});
areaZoomInteraction.setActive(false);
let areaZoomButton;
const map = new ol.Map({
target: "map",
controls: ol.control
.defaults.defaults({ rotate: false })
.extend([
createVisibleLayersExtentControl(),
createAreaZoomControl(),
new ol.control.Rotate({
autoHide: true,
label: createNorthPointerIcon(),
@@ -90,6 +99,7 @@ const map = new ol.Map({
zoom: 7,
}),
});
map.addInteraction(areaZoomInteraction);
let availableLayers = [];
let selectedLayers = [];
@@ -108,12 +118,25 @@ document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
if (!els.featureModal.hidden) {
closeFeatureModal();
} else if (areaZoomInteraction.getActive()) {
setAreaZoomMode(false);
} else if (!els.appShell.classList.contains("sidebar-is-collapsed")) {
setSidebarCollapsed(true);
}
}
});
map.on("singleclick", showFeatureInformation);
areaZoomInteraction.on("boxend", zoomToSelectedArea);
areaZoomInteraction.on("boxcancel", () => {
// A click or a very short drag is not a selection. Exit on the next frame so
// cancelling a drag while disabling the interaction cannot recursively
// trigger another cancellation.
requestAnimationFrame(() => {
if (areaZoomInteraction.getActive()) {
setAreaZoomMode(false);
}
});
});
loadLayers();
@@ -150,6 +173,57 @@ function createVisibleLayersExtentControl() {
return new ol.control.Control({ element });
}
function createAreaZoomControl() {
const element = document.createElement("div");
element.className = "ol-area-zoom ol-unselectable ol-control";
const button = document.createElement("button");
button.type = "button";
button.title = "Zoom to area";
button.setAttribute("aria-label", "Zoom to area");
button.setAttribute("aria-pressed", "false");
const icon = document.createElement("img");
icon.className = "area-zoom-icon";
icon.src = "./assets/icons/area-zoom.svg";
icon.alt = "";
button.append(icon);
button.addEventListener("click", () => setAreaZoomMode(!areaZoomInteraction.getActive()));
element.append(button);
areaZoomButton = button;
return new ol.control.Control({ element });
}
function setAreaZoomMode(active) {
areaZoomInteraction.setActive(active);
map.getViewport().classList.toggle("area-zoom-is-active", active);
areaZoomButton?.setAttribute("aria-pressed", String(active));
areaZoomButton?.setAttribute(
"title",
active ? "Exit area zoom (Esc)" : "Zoom to area",
);
if (active) {
closeFeatureModal();
}
}
function zoomToSelectedArea() {
const extent = areaZoomInteraction.getGeometry()?.getExtent();
if (extent && extent.every(Number.isFinite)) {
map.getView().fit(extent, {
padding: [64, 64, 64, 64],
duration: 500,
maxZoom: 19,
});
}
setAreaZoomMode(false);
}
async function searchParcela(event) {
event.preventDefault();
@@ -328,6 +402,10 @@ function syncSelectedLayers() {
}
async function showFeatureInformation(event) {
if (areaZoomInteraction.getActive()) {
return;
}
const queryLayers = availableLayers
.map((layer) => layer.name)
.filter(
+55
View File
@@ -442,6 +442,56 @@ button:disabled {
font-size: 1.3rem;
}
.map-wrap .ol-area-zoom {
top: 140px;
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-area-zoom button {
display: grid;
width: 2.0625em;
height: 2.0625em;
margin: 0;
padding: 0;
place-items: center;
background: linear-gradient(135deg, var(--accent) 0%, #2b9a79 100%);
color: #fffefb;
font-size: 1.3rem;
line-height: 1;
}
.map-wrap .ol-area-zoom button:hover,
.map-wrap .ol-area-zoom button:focus,
.map-wrap .ol-area-zoom button[aria-pressed="true"] {
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;
}
.area-zoom-icon {
display: block;
width: 1.2em;
height: 1.2em;
filter: brightness(0) invert(1);
}
.map-wrap .ol-viewport.area-zoom-is-active {
cursor: crosshair;
}
.map-wrap .ol-area-zoom-box {
border-color: var(--accent);
background: rgba(31, 122, 98, 0.16);
}
/* OpenLayers hides this control while the view is north-up. */
.map-wrap .ol-rotate {
top: auto;
@@ -497,6 +547,11 @@ button:disabled {
right: 12px;
}
.map-wrap .ol-area-zoom {
top: 136px;
right: 12px;
}
.map-wrap .ol-rotate {
right: 12px;
bottom: 12px;