Initial XPro map visualizer
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
const DEFAULT_CENTER = [-8.65, 39.55];
|
||||
const API_CAPABILITIES = "/api/capabilities";
|
||||
const API_WMS = "/api/wms";
|
||||
|
||||
const els = {
|
||||
status: document.getElementById("status"),
|
||||
layerList: document.getElementById("layer-list"),
|
||||
layerCount: document.getElementById("layer-count"),
|
||||
};
|
||||
|
||||
const osmLayer = new ol.layer.Tile({
|
||||
source: new ol.source.OSM(),
|
||||
});
|
||||
|
||||
const wmsSource = new ol.source.TileWMS({
|
||||
url: API_WMS,
|
||||
params: {
|
||||
LAYERS: "",
|
||||
TILED: true,
|
||||
FORMAT: "image/png",
|
||||
TRANSPARENT: true,
|
||||
},
|
||||
crossOrigin: "anonymous",
|
||||
});
|
||||
|
||||
const wmsLayer = new ol.layer.Tile({
|
||||
opacity: 0.8,
|
||||
visible: false,
|
||||
source: wmsSource,
|
||||
});
|
||||
|
||||
const map = new ol.Map({
|
||||
target: "map",
|
||||
layers: [osmLayer, wmsLayer],
|
||||
view: new ol.View({
|
||||
center: ol.proj.fromLonLat(DEFAULT_CENTER),
|
||||
zoom: 7,
|
||||
}),
|
||||
});
|
||||
|
||||
let availableLayers = [];
|
||||
let selectedLayers = [];
|
||||
|
||||
loadLayers();
|
||||
|
||||
async function loadLayers() {
|
||||
updateStatus("Loading WMS layers...");
|
||||
|
||||
try {
|
||||
const capabilities = await loadCapabilities();
|
||||
availableLayers = extractLayers(capabilities);
|
||||
selectedLayers = availableLayers.map((layer) => layer.name);
|
||||
|
||||
renderLayerList();
|
||||
syncSelectedLayers();
|
||||
fitToLayerExtent(availableLayers[0]);
|
||||
|
||||
updateStatus(`Showing ${availableLayers.length} layer(s) from GeoServer.`, "success");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
updateStatus(error.message || "Unable to load the WMS service.", "error");
|
||||
availableLayers = [];
|
||||
selectedLayers = [];
|
||||
renderLayerList();
|
||||
syncSelectedLayers();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCapabilities() {
|
||||
const url = new URL(API_CAPABILITIES, window.location.origin);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GeoServer connection failed with status ${response.status}.`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
function extractLayers(xmlText) {
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(xmlText, "text/xml");
|
||||
const capabilityNode = firstChildByName(xml.documentElement, "Capability");
|
||||
const rootLayerNode = firstChildByName(capabilityNode, "Layer");
|
||||
const nodes = collectNamedLayers(rootLayerNode);
|
||||
|
||||
return nodes
|
||||
.map((layerNode) => {
|
||||
const name = textFromChild(layerNode, "Name");
|
||||
const title = textFromChild(layerNode, "Title") || name;
|
||||
const bbox = firstChildByName(layerNode, "EX_GeographicBoundingBox");
|
||||
const extent = bbox
|
||||
? [
|
||||
Number(textFromChild(bbox, "westBoundLongitude")),
|
||||
Number(textFromChild(bbox, "southBoundLatitude")),
|
||||
Number(textFromChild(bbox, "eastBoundLongitude")),
|
||||
Number(textFromChild(bbox, "northBoundLatitude")),
|
||||
]
|
||||
: null;
|
||||
|
||||
return { name, title, extent };
|
||||
})
|
||||
.filter((layer) => layer.name);
|
||||
}
|
||||
|
||||
function renderLayerList() {
|
||||
els.layerCount.textContent = String(availableLayers.length);
|
||||
|
||||
if (!availableLayers.length) {
|
||||
els.layerList.className = "layer-list empty";
|
||||
els.layerList.textContent = "No published layers were returned by the service.";
|
||||
return;
|
||||
}
|
||||
|
||||
els.layerList.className = "layer-list";
|
||||
els.layerList.replaceChildren();
|
||||
|
||||
availableLayers.forEach((layer) => {
|
||||
const label = document.createElement("label");
|
||||
label.className = "layer-item";
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = selectedLayers.includes(layer.name);
|
||||
checkbox.addEventListener("change", () => {
|
||||
selectedLayers = checkbox.checked
|
||||
? [...new Set([...selectedLayers, layer.name])]
|
||||
: selectedLayers.filter((name) => name !== layer.name);
|
||||
syncSelectedLayers();
|
||||
if (checkbox.checked) {
|
||||
fitToLayerExtent(layer);
|
||||
}
|
||||
});
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "layer-meta";
|
||||
|
||||
const title = document.createElement("span");
|
||||
title.className = "layer-title";
|
||||
title.textContent = layer.title;
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "layer-name";
|
||||
name.textContent = layer.name;
|
||||
|
||||
meta.append(title, name);
|
||||
label.append(checkbox, meta);
|
||||
els.layerList.append(label);
|
||||
});
|
||||
}
|
||||
|
||||
function syncSelectedLayers() {
|
||||
const orderedSelectedLayers = availableLayers
|
||||
.map((layer) => layer.name)
|
||||
.filter((name) => selectedLayers.includes(name));
|
||||
|
||||
wmsLayer.setVisible(selectedLayers.length > 0);
|
||||
wmsSource.updateParams({
|
||||
LAYERS: [...orderedSelectedLayers].reverse().join(","),
|
||||
_: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
function fitToLayerExtent(layer) {
|
||||
if (!layer?.extent || layer.extent.some((value) => Number.isNaN(value))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectedExtent = ol.proj.transformExtent(layer.extent, "EPSG:4326", "EPSG:3857");
|
||||
map.getView().fit(projectedExtent, {
|
||||
padding: [40, 40, 40, 40],
|
||||
duration: 500,
|
||||
maxZoom: 18,
|
||||
});
|
||||
}
|
||||
|
||||
function childElementsByName(node, localName) {
|
||||
return [...(node?.children || [])].filter((child) => child.localName === localName);
|
||||
}
|
||||
|
||||
function firstChildByName(node, localName) {
|
||||
return childElementsByName(node, localName)[0] || null;
|
||||
}
|
||||
|
||||
function textFromChild(node, localName) {
|
||||
return firstChildByName(node, localName)?.textContent?.trim() || "";
|
||||
}
|
||||
|
||||
function collectNamedLayers(node) {
|
||||
return childElementsByName(node, "Layer").flatMap((layerNode) => {
|
||||
const children = collectNamedLayers(layerNode);
|
||||
return textFromChild(layerNode, "Name") ? [layerNode, ...children] : children;
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatus(message, tone) {
|
||||
els.status.textContent = message;
|
||||
els.status.className = tone ? `status ${tone}` : "status";
|
||||
}
|
||||
Reference in New Issue
Block a user