Compare commits
2
Commits
c0686d0c5a
...
5d19c5f085
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d19c5f085 | ||
|
|
440ee6cc9a |
@@ -1,37 +1,52 @@
|
|||||||
const DEFAULT_CENTER = [-8.65, 39.55];
|
const DEFAULT_CENTER = [-8.65, 39.55];
|
||||||
const API_CAPABILITIES = "/api/capabilities";
|
const API_CAPABILITIES = "/api/capabilities";
|
||||||
const API_WMS = "/api/wms";
|
const API_WMS = "/api/wms";
|
||||||
|
const PARCEL_LAYER_NAME = "xpro:parcelas";
|
||||||
|
|
||||||
const els = {
|
const els = {
|
||||||
status: document.getElementById("status"),
|
status: document.getElementById("status"),
|
||||||
layerList: document.getElementById("layer-list"),
|
layerList: document.getElementById("layer-list"),
|
||||||
layerCount: document.getElementById("layer-count"),
|
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({
|
const osmLayer = new ol.layer.Tile({
|
||||||
source: new ol.source.OSM(),
|
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,
|
url: API_WMS,
|
||||||
params: {
|
params: {
|
||||||
LAYERS: "",
|
LAYERS: "",
|
||||||
TILED: true,
|
|
||||||
FORMAT: "image/png",
|
FORMAT: "image/png",
|
||||||
TRANSPARENT: true,
|
TRANSPARENT: true,
|
||||||
},
|
},
|
||||||
|
ratio: 1,
|
||||||
crossOrigin: "anonymous",
|
crossOrigin: "anonymous",
|
||||||
});
|
});
|
||||||
|
|
||||||
const wmsLayer = new ol.layer.Tile({
|
const wmsLayer = new ol.layer.Image({
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
visible: false,
|
visible: false,
|
||||||
source: wmsSource,
|
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({
|
const map = new ol.Map({
|
||||||
target: "map",
|
target: "map",
|
||||||
layers: [osmLayer, wmsLayer],
|
layers: [osmLayer, wmsLayer, parcelSearchLayer],
|
||||||
view: new ol.View({
|
view: new ol.View({
|
||||||
center: ol.proj.fromLonLat(DEFAULT_CENTER),
|
center: ol.proj.fromLonLat(DEFAULT_CENTER),
|
||||||
zoom: 7,
|
zoom: 7,
|
||||||
@@ -41,8 +56,67 @@ const map = new ol.Map({
|
|||||||
let availableLayers = [];
|
let availableLayers = [];
|
||||||
let selectedLayers = [];
|
let selectedLayers = [];
|
||||||
|
|
||||||
|
els.parcelSearchForm.addEventListener("submit", searchParcela);
|
||||||
|
|
||||||
loadLayers();
|
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() {
|
async function loadLayers() {
|
||||||
updateStatus("Loading WMS layers...");
|
updateStatus("Loading WMS layers...");
|
||||||
|
|
||||||
@@ -198,3 +272,12 @@ function updateStatus(message, tone) {
|
|||||||
els.status.textContent = message;
|
els.status.textContent = message;
|
||||||
els.status.className = tone ? `status ${tone}` : "status";
|
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("'", "''");
|
||||||
|
}
|
||||||
|
|||||||
+4
-2
@@ -7,9 +7,11 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
PORT: ${APP_PORT:-4173}
|
PORT: ${APP_PORT:-4173}
|
||||||
WMS_ENDPOINT: ${WMS_ENDPOINT:-https://xpro-viz.jfig.net/geoserver/xpro/wms}
|
WMS_ENDPOINT: ${DOCKER_WMS_ENDPOINT:-${WMS_ENDPOINT:-https://xpro-viz.jfig.net/geoserver/xpro/wms}}
|
||||||
ALLOWED_WMS_ENDPOINTS: ${ALLOWED_WMS_ENDPOINTS:-https://xpro-viz.jfig.net/geoserver/xpro/wms}
|
ALLOWED_WMS_ENDPOINTS: ${DOCKER_ALLOWED_WMS_ENDPOINTS:-${ALLOWED_WMS_ENDPOINTS:-https://xpro-viz.jfig.net/geoserver/xpro/wms}}
|
||||||
GEOSERVER_USERNAME: ${GEOSERVER_USERNAME:-admin}
|
GEOSERVER_USERNAME: ${GEOSERVER_USERNAME:-admin}
|
||||||
GEOSERVER_PASSWORD: ${GEOSERVER_PASSWORD}
|
GEOSERVER_PASSWORD: ${GEOSERVER_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-4173}:${APP_PORT:-4173}"
|
- "${APP_PORT:-4173}:${APP_PORT:-4173}"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|||||||
+22
@@ -14,6 +14,28 @@
|
|||||||
<h1>XPro Map Visualizer</h1>
|
<h1>XPro Map Visualizer</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>Find parcel</h2>
|
||||||
|
</div>
|
||||||
|
<form id="parcel-search-form" class="parcel-search-form">
|
||||||
|
<label for="parcel-search-input">
|
||||||
|
<span>Parcel number</span>
|
||||||
|
<input
|
||||||
|
id="parcel-search-input"
|
||||||
|
name="parcel"
|
||||||
|
type="search"
|
||||||
|
inputmode="text"
|
||||||
|
placeholder="e.g. 57 or 2669.1"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button type="submit">Find on map</button>
|
||||||
|
</form>
|
||||||
|
<p id="parcel-search-status" class="status" aria-live="polite"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h2>Layers</h2>
|
<h2>Layers</h2>
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js"
|
"start": "node --env-file-if-exists=.env server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ol": "10.6.1"
|
"ol": "10.6.1"
|
||||||
|
|||||||
+10
@@ -83,6 +83,16 @@ form.panel {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.parcel-search-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcel-search-form button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user