diff --git a/app.js b/app.js index 3d537e9..d77b2a7 100644 --- a/app.js +++ b/app.js @@ -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("'", "''"); +} diff --git a/index.html b/index.html index acf7d86..80973bb 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,28 @@

XPro Map Visualizer

+
+
+

Find parcel

+
+
+ + +
+

+
+

Layers

diff --git a/styles.css b/styles.css index 2094f2c..876fc67 100644 --- a/styles.css +++ b/styles.css @@ -83,6 +83,16 @@ form.panel { gap: 14px; } +.parcel-search-form { + display: grid; + gap: 12px; + margin-top: 16px; +} + +.parcel-search-form button { + width: 100%; +} + label { display: flex; flex-direction: column;