feat: show feature information on map click

This commit is contained in:
2026-07-22 07:21:03 +01:00
parent ec922c22ff
commit c60840e4c1
3 changed files with 199 additions and 3 deletions
+92 -3
View File
@@ -2,6 +2,7 @@ const DEFAULT_CENTER = [-8.65, 39.55];
const API_CAPABILITIES = "/api/capabilities";
const API_WMS = "/api/wms";
const PARCEL_LAYER_NAME = "xpro:parcelas";
const FEATURE_INFO_LAYER_NAMES = new Set(["parcelas", "predios"]);
const els = {
appShell: document.querySelector(".app-shell"),
@@ -15,6 +16,10 @@ const els = {
parcelSearchInput: document.getElementById("parcel-search-input"),
parcelSearchStatus: document.getElementById("parcel-search-status"),
backgroundSelect: document.getElementById("background-select"),
featureModal: document.getElementById("feature-modal"),
featureModalClose: document.getElementById("feature-modal-close"),
featureModalKind: document.getElementById("feature-modal-kind"),
featureArea: document.getElementById("feature-area"),
};
const softBasemapLayer = new ol.layer.Tile({
@@ -93,11 +98,22 @@ els.parcelSearchForm.addEventListener("submit", searchParcela);
els.backgroundSelect.addEventListener("change", syncBackground);
els.sidebarOpen.addEventListener("click", () => setSidebarCollapsed(false));
els.sidebarClose.addEventListener("click", () => setSidebarCollapsed(true));
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !els.appShell.classList.contains("sidebar-is-collapsed")) {
setSidebarCollapsed(true);
els.featureModalClose.addEventListener("click", closeFeatureModal);
els.featureModal.addEventListener("click", (event) => {
if (event.target.matches("[data-feature-modal-close]")) {
closeFeatureModal();
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
if (!els.featureModal.hidden) {
closeFeatureModal();
} else if (!els.appShell.classList.contains("sidebar-is-collapsed")) {
setSidebarCollapsed(true);
}
}
});
map.on("singleclick", showFeatureInformation);
loadLayers();
@@ -299,6 +315,79 @@ function syncSelectedLayers() {
});
}
async function showFeatureInformation(event) {
const queryLayers = availableLayers
.map((layer) => layer.name)
.filter(
(name) => selectedLayers.includes(name) && FEATURE_INFO_LAYER_NAMES.has(unqualifiedLayerName(name)),
);
if (!queryLayers.length) {
return;
}
const url = wmsSource.getFeatureInfoUrl(event.coordinate, map.getView().getResolution(), "EPSG:3857", {
INFO_FORMAT: "application/json",
QUERY_LAYERS: queryLayers.join(","),
FEATURE_COUNT: 1,
});
if (!url) {
return;
}
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);
}
}
function openFeatureModal(feature) {
const layerName = unqualifiedLayerName(feature.id?.split(".")[0] || "");
const isParcel = layerName === "parcelas" || feature.properties?.cod_parcela != null;
els.featureModalKind.textContent = isParcel ? "Parcel" : "Building";
els.featureArea.textContent = formatFeatureArea(feature);
els.featureModal.hidden = false;
els.featureModalClose.focus();
}
function unqualifiedLayerName(name) {
return name.split(":").at(-1);
}
function closeFeatureModal() {
els.featureModal.hidden = true;
}
function formatFeatureArea(feature) {
const properties = feature.properties || {};
const areaEntry = Object.entries(properties).find(([name, value]) =>
value != null && /(^|[_\s])area([_\s]|$)|shape_area/i.test(name),
);
const area = Number(areaEntry?.[1]);
if (Number.isFinite(area) && area >= 0) {
return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(area)} m²`;
}
const geometry = new ol.format.GeoJSON().readGeometry(feature.geometry, {
featureProjection: "EPSG:3857",
});
const calculatedArea = geometry?.getArea?.();
return Number.isFinite(calculatedArea)
? `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(calculatedArea)} m²`
: "Area unavailable";
}
function fitToLayerExtent(layer) {
if (!layer?.extent || layer.extent.some((value) => Number.isNaN(value))) {
return;