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"; }