feat: add parcel search to map visualizer

This commit is contained in:
2026-07-20 00:26:24 +01:00
parent 440ee6cc9a
commit 5d19c5f085
3 changed files with 119 additions and 4 deletions
+87 -4
View File
@@ -1,37 +1,52 @@
const DEFAULT_CENTER = [-8.65, 39.55];
const API_CAPABILITIES = "/api/capabilities";
const API_WMS = "/api/wms";
const PARCEL_LAYER_NAME = "xpro:parcelas";
const els = {
status: document.getElementById("status"),
layerList: document.getElementById("layer-list"),
layerCount: document.getElementById("layer-count"),
parcelSearchForm: document.getElementById("parcel-search-form"),
parcelSearchInput: document.getElementById("parcel-search-input"),
parcelSearchStatus: document.getElementById("parcel-search-status"),
};
const osmLayer = new ol.layer.Tile({
source: new ol.source.OSM(),
});
const wmsSource = new ol.source.TileWMS({
// Use one WMS image for the viewport so GeoServer can de-conflict parcel
// labels across the whole map, rather than placing the same label per tile.
const wmsSource = new ol.source.ImageWMS({
url: API_WMS,
params: {
LAYERS: "",
TILED: true,
FORMAT: "image/png",
TRANSPARENT: true,
},
ratio: 1,
crossOrigin: "anonymous",
});
const wmsLayer = new ol.layer.Tile({
const wmsLayer = new ol.layer.Image({
opacity: 0.8,
visible: false,
source: wmsSource,
});
const parcelSearchSource = new ol.source.Vector();
const parcelSearchLayer = new ol.layer.Vector({
source: parcelSearchSource,
style: new ol.style.Style({
stroke: new ol.style.Stroke({ color: "#d9582b", width: 3 }),
fill: new ol.style.Fill({ color: "rgba(255, 201, 82, 0.22)" }),
}),
});
const map = new ol.Map({
target: "map",
layers: [osmLayer, wmsLayer],
layers: [osmLayer, wmsLayer, parcelSearchLayer],
view: new ol.View({
center: ol.proj.fromLonLat(DEFAULT_CENTER),
zoom: 7,
@@ -41,8 +56,67 @@ const map = new ol.Map({
let availableLayers = [];
let selectedLayers = [];
els.parcelSearchForm.addEventListener("submit", searchParcela);
loadLayers();
async function searchParcela(event) {
event.preventDefault();
const parcelNumber = els.parcelSearchInput.value.trim();
if (!parcelNumber) {
updateParcelSearchStatus("Enter a parcel number to search.", "error");
return;
}
updateParcelSearchStatus("Searching for parcel…");
parcelSearchSource.clear();
try {
const params = new URLSearchParams({
service: "WFS",
version: "2.0.0",
request: "GetFeature",
typeNames: PARCEL_LAYER_NAME,
outputFormat: "application/json",
srsName: "EPSG:3857",
CQL_FILTER: `cod_parcela = '${escapeCqlLiteral(parcelNumber)}'`,
});
const response = await fetch(`${API_WMS}?${params}`);
if (!response.ok) {
throw new Error(`Search failed with status ${response.status}.`);
}
const featureCollection = await response.json();
const features = new ol.format.GeoJSON().readFeatures(featureCollection, {
featureProjection: "EPSG:3857",
});
if (!features.length) {
updateParcelSearchStatus(`No parcel found with number “${parcelNumber}”.`, "error");
return;
}
parcelSearchSource.addFeatures(features);
map.getView().fit(parcelSearchSource.getExtent(), {
padding: [64, 64, 64, 64],
duration: 500,
maxZoom: features.length === 1 ? 19 : 16,
});
updateParcelSearchStatus(
features.length === 1
? `Found parcel ${parcelNumber}.`
: `Found ${features.length} parcels numbered ${parcelNumber}.`,
"success",
);
} catch (error) {
console.error(error);
updateParcelSearchStatus(error.message || "Unable to search for that parcel.", "error");
}
}
async function loadLayers() {
updateStatus("Loading WMS layers...");
@@ -198,3 +272,12 @@ function updateStatus(message, tone) {
els.status.textContent = message;
els.status.className = tone ? `status ${tone}` : "status";
}
function updateParcelSearchStatus(message, tone) {
els.parcelSearchStatus.textContent = message;
els.parcelSearchStatus.className = tone ? `status ${tone}` : "status";
}
function escapeCqlLiteral(value) {
return value.replaceAll("'", "''");
}