feat: indicate clickable map features

This commit is contained in:
2026-07-24 03:51:25 +01:00
parent 2510bc583d
commit bf44014dc1
+60 -15
View File
@@ -111,6 +111,9 @@ let pendingCompassHeading;
let compassAnimationFrame;
let deviceCompassFallbackTimer;
let modalDrag;
let featureHoverRequest;
let featureHoverRequestId = 0;
let featureHoverTimer;
const map = new ol.Map({
target: "map",
@@ -173,6 +176,8 @@ document.addEventListener("keydown", (event) => {
});
window.addEventListener("resize", keepFeatureModalInViewport);
map.on("singleclick", showFeatureInformation);
map.on("pointermove", updateFeatureCursor);
map.getViewport().addEventListener("pointerleave", clearFeatureCursor);
areaZoomInteraction.on("boxend", zoomToSelectedArea);
areaZoomInteraction.on("boxcancel", () => {
// A click or a very short drag is not a selection. Exit on the next frame so
@@ -450,6 +455,7 @@ function setAreaZoomMode(active) {
);
if (active) {
clearFeatureCursor();
closeFeatureModal();
}
}
@@ -655,6 +661,7 @@ function syncSelectedLayers() {
return layerName === "parcelas" || layerName === unqualifiedLayerName(DWELLING_PARCEL_LAYER_NAME);
}),
);
clearFeatureCursor();
}
function isInternalLayer(name) {
@@ -704,6 +711,51 @@ async function showFeatureInformation(event) {
return;
}
try {
const feature = await getFeatureAtCoordinate(event.coordinate);
if (feature) {
openFeatureModal(feature);
}
} catch (error) {
console.error(error);
}
}
function updateFeatureCursor(event) {
if (event.dragging || areaZoomInteraction.getActive()) {
clearFeatureCursor();
return;
}
window.clearTimeout(featureHoverTimer);
const requestId = ++featureHoverRequestId;
const coordinate = [...event.coordinate];
featureHoverTimer = window.setTimeout(async () => {
featureHoverRequest?.abort();
featureHoverRequest = new AbortController();
try {
const feature = await getFeatureAtCoordinate(coordinate, featureHoverRequest.signal);
if (requestId === featureHoverRequestId) {
map.getViewport().style.cursor = feature ? "pointer" : "";
}
} catch (error) {
if (error.name !== "AbortError" && requestId === featureHoverRequestId) {
map.getViewport().style.cursor = "";
}
}
}, 120);
}
function clearFeatureCursor() {
window.clearTimeout(featureHoverTimer);
featureHoverRequestId += 1;
featureHoverRequest?.abort();
featureHoverRequest = undefined;
map.getViewport().style.cursor = "";
}
async function getFeatureAtCoordinate(coordinate, signal) {
const queryLayers = availableLayers
.map((layer) => layer.name)
.filter(
@@ -711,32 +763,25 @@ async function showFeatureInformation(event) {
);
if (!queryLayers.length) {
return;
return null;
}
const url = wmsSource.getFeatureInfoUrl(event.coordinate, map.getView().getResolution(), "EPSG:3857", {
const url = wmsSource.getFeatureInfoUrl(coordinate, map.getView().getResolution(), "EPSG:3857", {
INFO_FORMAT: "application/json",
QUERY_LAYERS: queryLayers.join(","),
FEATURE_COUNT: 1,
});
if (!url) {
return;
return null;
}
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Feature information failed with status ${response.status}.`);
}
const feature = (await response.json()).features?.[0];
if (feature) {
openFeatureModal(feature);
}
} catch (error) {
console.error(error);
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`Feature information failed with status ${response.status}.`);
}
return (await response.json()).features?.[0] || null;
}
function openFeatureModal(feature) {