feat: indicate clickable map features
This commit is contained in:
@@ -111,6 +111,9 @@ let pendingCompassHeading;
|
|||||||
let compassAnimationFrame;
|
let compassAnimationFrame;
|
||||||
let deviceCompassFallbackTimer;
|
let deviceCompassFallbackTimer;
|
||||||
let modalDrag;
|
let modalDrag;
|
||||||
|
let featureHoverRequest;
|
||||||
|
let featureHoverRequestId = 0;
|
||||||
|
let featureHoverTimer;
|
||||||
|
|
||||||
const map = new ol.Map({
|
const map = new ol.Map({
|
||||||
target: "map",
|
target: "map",
|
||||||
@@ -173,6 +176,8 @@ document.addEventListener("keydown", (event) => {
|
|||||||
});
|
});
|
||||||
window.addEventListener("resize", keepFeatureModalInViewport);
|
window.addEventListener("resize", keepFeatureModalInViewport);
|
||||||
map.on("singleclick", showFeatureInformation);
|
map.on("singleclick", showFeatureInformation);
|
||||||
|
map.on("pointermove", updateFeatureCursor);
|
||||||
|
map.getViewport().addEventListener("pointerleave", clearFeatureCursor);
|
||||||
areaZoomInteraction.on("boxend", zoomToSelectedArea);
|
areaZoomInteraction.on("boxend", zoomToSelectedArea);
|
||||||
areaZoomInteraction.on("boxcancel", () => {
|
areaZoomInteraction.on("boxcancel", () => {
|
||||||
// A click or a very short drag is not a selection. Exit on the next frame so
|
// 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) {
|
if (active) {
|
||||||
|
clearFeatureCursor();
|
||||||
closeFeatureModal();
|
closeFeatureModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,6 +661,7 @@ function syncSelectedLayers() {
|
|||||||
return layerName === "parcelas" || layerName === unqualifiedLayerName(DWELLING_PARCEL_LAYER_NAME);
|
return layerName === "parcelas" || layerName === unqualifiedLayerName(DWELLING_PARCEL_LAYER_NAME);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
clearFeatureCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInternalLayer(name) {
|
function isInternalLayer(name) {
|
||||||
@@ -704,6 +711,51 @@ async function showFeatureInformation(event) {
|
|||||||
return;
|
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
|
const queryLayers = availableLayers
|
||||||
.map((layer) => layer.name)
|
.map((layer) => layer.name)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -711,32 +763,25 @@ async function showFeatureInformation(event) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!queryLayers.length) {
|
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",
|
INFO_FORMAT: "application/json",
|
||||||
QUERY_LAYERS: queryLayers.join(","),
|
QUERY_LAYERS: queryLayers.join(","),
|
||||||
FEATURE_COUNT: 1,
|
FEATURE_COUNT: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const response = await fetch(url, { signal });
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Feature information failed with status ${response.status}.`);
|
throw new Error(`Feature information failed with status ${response.status}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const feature = (await response.json()).features?.[0];
|
return (await response.json()).features?.[0] || null;
|
||||||
if (feature) {
|
|
||||||
openFeatureModal(feature);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFeatureModal(feature) {
|
function openFeatureModal(feature) {
|
||||||
|
|||||||
Reference in New Issue
Block a user