${item.label}
diff --git a/examples/jsm/OGCClientHelper.js b/examples/jsm/OGCClientHelper.js
new file mode 100644
index 0000000000..e1a03be85d
--- /dev/null
+++ b/examples/jsm/OGCClientHelper.js
@@ -0,0 +1,299 @@
+// @ts-check
+import { WmtsEndpoint, WmsEndpoint, WfsEndpoint } from '@camptocamp/ogc-client';
+import { Extent, WFSSource, WMSSource, WMTSSource } from 'itowns';
+
+/** @typedef {WmtsEndpoint | WmsEndpoint | WfsEndpoint} Endpoint */
+/** @typedef {import('@camptocamp/ogc-client').BoundingBox} BoundingBox */
+/** @typedef {import('@camptocamp/ogc-client').WfsFeatureTypeSummary} WfsFeatureType */
+/** @typedef {import('@camptocamp/ogc-client').WmsLayerFull} WmsLayer */
+/** @typedef {import('@camptocamp/ogc-client').WmtsLayer} WmtsLayer */
+/** @typedef {WmtsLayer['matrixSets'][number]['limits']} MatrixSetLimits */
+/** @typedef {WmtsLayer | WmsLayer | WfsFeatureType} LayerDescriptor */
+/**
+ * @typedef {object} LayerSource
+ * @property {WMTSSource | WMSSource | WFSSource} source
+ * @property {'color' | 'elevation'} layerType
+ */
+
+const SUPPORTED_CRS = ['EPSG:3857', 'EPSG:4326'];
+const RASTER_FORMATS = ['image/png', 'image/jpeg'];
+const VECTOR_FORMATS = ['application/json', 'application/geojson'];
+
+function isCrsSupported(/** @type {string} */ crs) {
+ return SUPPORTED_CRS.includes(crs);
+}
+
+function findCompatibleMatrixSet(/** @type {WmtsLayer} */ layer) {
+ return layer.matrixSets.find(matrixSet => isCrsSupported(matrixSet.crs));
+}
+
+function tileMatrixSetLimits(/** @type {MatrixSetLimits} */ limits) {
+ return Object.fromEntries(
+ limits.map(({ tileMatrix, ...bounds }) => [tileMatrix, bounds]),
+ );
+}
+
+function bboxToExtent(/** @type {BoundingBox} */ bbox, /** @type {string} */ crs) {
+ const [west, south, east, north] = bbox;
+ return new Extent(crs).setFromExtent({ west, south, east, north });
+}
+
+function getWMTSLayerCrs(/** @type {WmtsLayer} */ layer) {
+ return findCompatibleMatrixSet(layer)?.crs;
+}
+
+function getWMSLayerCrs(/** @type {WmsLayer} */ layer) {
+ return layer.availableCrs.find(crs => isCrsSupported(crs));
+}
+
+function getWFSLayerCrs(/** @type {WfsFeatureType} */ featureType) {
+ return isCrsSupported(featureType.defaultCrs) ? featureType.defaultCrs
+ : featureType.otherCrs.find(crs => isCrsSupported(crs));
+}
+
+
+function selectRasterFormat(/** @type {string[] | undefined} */ formats) {
+ if (!formats) { return; }
+ return RASTER_FORMATS.find(f => formats.includes(f));
+}
+
+function selectVectorFormat(/** @type {string[] | undefined} */ formats) {
+ if (!formats) { return; }
+ return VECTOR_FORMATS.find(f => formats.includes(f));
+}
+
+/**
+ * Returns a globe-compatible CRS from a layer descriptor.
+ *
+ * @param {LayerDescriptor} layer - A WMS, WFS or WMTS layer descriptor.
+ * @returns {string | undefined} A compatible CRS, or `undefined` if none is
+ * available.
+ */
+export function getLayerCrs(layer) {
+ if ('matrixSets' in layer) { // WMTS layer
+ return getWMTSLayerCrs(layer);
+ }
+ if ('availableCrs' in layer) { // WMS layer
+ return getWMSLayerCrs(layer);
+ }
+ if ('defaultCrs' in layer) { // WFS layer
+ return getWFSLayerCrs(layer);
+ }
+ return;
+}
+
+/**
+ * Zoom levels covered by a WMTS layer, or `undefined` for any other layer.
+ *
+ * @param {LayerDescriptor} layer - A WMS, WFS or WMTS layer descriptor.
+ * @returns {{ min: number, max: number } | undefined}
+ */
+export function getZoom(layer) {
+ if (!('matrixSets' in layer)) {
+ return;
+ }
+
+ const limits = findCompatibleMatrixSet(layer)?.limits;
+ if (!limits?.length) {
+ return;
+ }
+
+ // Note: This is fragile but we are severely limited by the lack of support
+ // for tilegrids. See WMTSEndpoint#getOpenLayersTileGrid.
+ const zooms = limits.map(limit => Number(limit.tileMatrix));
+ return { min: Math.min(...zooms), max: Math.max(...zooms) };
+}
+
+/**
+ * Fetch and parse the capabilities of an OGC service from an URL and the type
+ * of service. Supported services include WMS, WFS and WMTS.
+ *
+ * @param {string} url
+ * @param {'wmts' | 'wms' | 'wfs'} type
+ * @returns {Promise
}
+ */
+export function endpointFromUrl(url, type) {
+ switch (type) {
+ case 'wmts':
+ return new WmtsEndpoint(url).isReady();
+ case 'wms':
+ return new WmsEndpoint(url).isReady();
+ case 'wfs':
+ return new WfsEndpoint(url).isReady();
+ default:
+ throw new Error(`Unsupported OGC service type: ${type}`);
+ }
+}
+
+/**
+ * List renderable layers advertised by an OGC service.
+ *
+ * @param {Endpoint} endpoint - The OGC endpoint to list layers from.
+ * @returns {LayerDescriptor[]} - A list of layer descriptors.
+ */
+export function listLayers(endpoint) {
+ if (endpoint instanceof WmtsEndpoint) {
+ return endpoint.getLayers();
+ }
+
+ if (endpoint instanceof WmsEndpoint) {
+ return endpoint.getFlattenedLayers()
+ // Layers without a name are group headers, they cannot be rendered
+ .filter(layer => layer.name)
+ .map(layer => endpoint.getLayerByName(layer.name));
+ }
+
+ if (endpoint instanceof WfsEndpoint) {
+ return endpoint.getFeatureTypes()
+ .map(f => endpoint.getFeatureTypeSummary(f.name));
+ }
+
+ throw new Error('Unsupported OGC endpoint');
+}
+
+/**
+ * @param {WmtsEndpoint} endpoint
+ * @param {string} name
+ * @returns {LayerSource}
+ */
+function wmtsSource(endpoint, name) {
+ const layer = endpoint.getLayerByName(name);
+ if (!layer) {
+ throw new Error(`WMTS layer "${name}" not found in capabilities`);
+ }
+
+ const url = endpoint.getServiceInfo()?.getTileUrls?.kvp ??
+ layer.resourceLinks?.find(link => link.encoding === 'KVP')?.url;
+ if (!url) {
+ throw new Error(`No KVP GetTile URL found for WMTS layer "${name}"`);
+ }
+
+ const matrixSet = findCompatibleMatrixSet(layer);
+ if (!matrixSet) {
+ throw new Error(`No globe matrixSet for WMTS layer "${name}"`);
+ }
+
+ const format = selectRasterFormat(layer.resourceLinks?.map(link => link.format));
+ if (!format) {
+ throw new Error(`Image format not supported for WMTS layer "${name}"! ${layer.resourceLinks?.map(link => link.format).join(', ')}`);
+ }
+
+ return {
+ source: new WMTSSource({
+ url,
+ name: layer.name,
+ crs: matrixSet.crs,
+ tileMatrixSet: matrixSet.identifier,
+ format,
+ tileMatrixSetLimits: matrixSet.limits?.length
+ ? tileMatrixSetLimits(matrixSet.limits)
+ : undefined,
+ }),
+ layerType: 'color',
+ };
+}
+
+/**
+ * @param {WmsEndpoint} endpoint
+ * @param {string} name
+ * @returns {LayerSource}
+ */
+function wmsSource(endpoint, name) {
+ const layer = endpoint.getLayerByName(name);
+ if (!layer) {
+ throw new Error(`WMS layer "${name}" not found in capabilities`);
+ }
+
+ const url = endpoint.getOperationUrl('GetMap') ??
+ endpoint.getCapabilitiesUrl();
+
+ const crs = getWMSLayerCrs(layer);
+ if (!crs) {
+ throw new Error(`No globe-compatible CRS for WMS layer "${name}"`);
+ }
+
+ const format = selectRasterFormat(endpoint.getServiceInfo()?.outputFormats);
+ if (!format) {
+ throw new Error(`No image output format for WMS layer "${name}"`);
+ }
+
+ const bbox = layer.boundingBoxes?.[crs];
+ if (!bbox) {
+ throw new Error(`No bounding box for WMS layer "${name}"`);
+ }
+
+ return {
+ source: new WMSSource({
+ url,
+ name: layer.name,
+ crs,
+ format,
+ extent: bboxToExtent(bbox, crs),
+ version: endpoint.getVersion(),
+ }),
+ layerType: 'color',
+ };
+}
+
+/**
+ * @param {WfsEndpoint} endpoint
+ * @param {string} name
+ * @returns {LayerSource}
+ */
+function wfsSource(endpoint, name) {
+ const featureType = endpoint.getFeatureTypeSummary(name);
+ if (!featureType) {
+ throw new Error(`WFS feature type "${name}" not found in capabilities`);
+ }
+
+ const url = endpoint.getOperationUrl('GetFeature') ??
+ endpoint.getCapabilitiesUrl();
+
+ const crs = getWFSLayerCrs(featureType);
+ if (!crs) {
+ throw new Error(`No globe CRS for WFS feature type "${name}"`);
+ }
+
+ const format = selectVectorFormat(featureType.outputFormats);
+ if (!format) {
+ throw new Error(`Feature type "${name}" does not advertise JSON output format`);
+ }
+
+ // bounding box is always lat/lon
+ const extent = featureType.boundingBox ?
+ bboxToExtent(featureType.boundingBox, 'EPSG:4326') :
+ undefined;
+
+ return {
+ source: new WFSSource({
+ url,
+ typeName: featureType.name,
+ crs,
+ format,
+ version: endpoint.getVersion(),
+ extent,
+ }),
+ layerType: 'color',
+ };
+}
+
+/**
+ * Build the iTowns source feeding a layer of a service, together with the kind
+ * of layer it should be attached to.
+ *
+ * @param {Endpoint} endpoint
+ * @param {string} layerName
+ * @returns {LayerSource}
+ */
+export function sourceFromEndpoint(endpoint, layerName) {
+ if (endpoint instanceof WmtsEndpoint) {
+ return wmtsSource(endpoint, layerName);
+ }
+ if (endpoint instanceof WmsEndpoint) {
+ return wmsSource(endpoint, layerName);
+ }
+ if (endpoint instanceof WfsEndpoint) {
+ return wfsSource(endpoint, layerName);
+ }
+ throw new Error('Unsupported OGC endpoint');
+}
diff --git a/examples/source_ogc_client.html b/examples/source_ogc_client.html
new file mode 100644
index 0000000000..e0a1c3b5b5
--- /dev/null
+++ b/examples/source_ogc_client.html
@@ -0,0 +1,104 @@
+
+
+ Itowns - OGC Client layer browser
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/source_ogc_client.js b/examples/source_ogc_client.js
new file mode 100644
index 0000000000..58b294c081
--- /dev/null
+++ b/examples/source_ogc_client.js
@@ -0,0 +1,284 @@
+// @ts-check
+import * as itowns from 'itowns';
+import {
+ endpointFromUrl,
+ listLayers,
+ sourceFromEndpoint,
+ getLayerCrs,
+ getZoom,
+} from './jsm/OGCClientHelper.js';
+
+/** @typedef {import('./jsm/OGCClientHelper.js').Endpoint} Endpoint */
+/** @typedef {import('./jsm/OGCClientHelper.js').LayerDescriptor} LayerDescriptor */
+
+/**
+ * @template {keyof HTMLElementTagNameMap} T
+ * @param {T} tag
+ * @param {Partial} [props]
+ * @param {...(Node | string)} children
+ * @returns {HTMLElementTagNameMap[T]}
+ */
+function el(tag, props = {}, ...children) {
+ const element = Object.assign(document.createElement(tag), props);
+ element.append(...children);
+ return element;
+}
+
+
+// ---- OGC client wrapper ----
+
+/**
+ * @param {itowns.GlobeView} view
+ * @param {object} props
+ * @param {Endpoint} props.endpoint
+ * @param {string} props.name
+ * @returns {Promise}
+ */
+async function addLayer(view, { endpoint, name }) {
+ const layerId = `${name}_${crypto.randomUUID()}`;
+ const { source } = sourceFromEndpoint(endpoint, name);
+
+ /** @type {itowns.ColorLayer} */
+ const layer = new itowns.ColorLayer(layerId, { source, name });
+
+ await view.addLayer(layer);
+ return layer;
+}
+
+// ---- UI components ----
+
+/**
+ * @param {object} props
+ * @param {LayerDescriptor} props.layer
+ * @returns {HTMLLIElement}
+ */
+function ogcLayerItem({ layer }) {
+ const crs = getLayerCrs(layer);
+ const zoom = getZoom(layer);
+
+ const title = 'title' in layer ? layer.title : undefined;
+ let subtitle = 'unsupported CRS';
+ if (crs) {
+ subtitle = zoom ? `${crs}, zoom ${zoom.min}-${zoom.max}` : crs;
+ }
+
+ const checkbox = el('input', { type: 'checkbox', value: layer.name, disabled: !crs });
+ const label = el('label', { title },
+ checkbox,
+ el('span', {},
+ layer.name ?? '',
+ el('small', { textContent: subtitle }),
+ ),
+ );
+
+ return el('li', {}, label);
+}
+
+/**
+ * @param {object} props
+ * @param {itowns.ColorLayer} props.layer
+ * @param {(direction: -1 | 1) => void} props.onMove
+ * @param {() => void} props.onRemove
+ * @param {(opacity: number) => void} props.onOpacity
+ * @returns {HTMLLIElement}
+ */
+function itownsLayerItem({ layer, onMove, onRemove, onOpacity }) {
+ const upBtn = el('button', { type: 'button', textContent: '\u25B2', title: 'Move up' });
+ const downBtn = el('button', { type: 'button', textContent: '\u25BC', title: 'Move down' });
+ const removeBtn = el('button', { type: 'button', textContent: '\u2715', title: 'Remove' });
+
+ const label = layer.name;
+ const children = [
+ upBtn,
+ downBtn,
+ el('span', { textContent: label }),
+ ];
+
+ if ('isColorLayer' in layer) {
+ const slider = el('input', {
+ type: 'range', min: '0', max: '1', step: '0.05', value: '1', title: 'opacity',
+ });
+ slider.addEventListener('input', () => onOpacity(slider.valueAsNumber));
+ children.push(slider);
+ }
+
+ children.push(removeBtn);
+ const item = el('li', {}, ...children);
+
+ /**
+ * @param {-1 | 1} direction
+ */
+ function move(/** @type {-1 | 1} */ direction) {
+ const sibling = direction < 0 ?
+ item.previousElementSibling : item.nextElementSibling;
+ if (!sibling) {
+ return;
+ }
+
+ if (direction < 0) {
+ sibling.before(item);
+ } else {
+ sibling.after(item);
+ }
+ onMove(direction);
+ }
+
+ upBtn.addEventListener('click', () => move(-1));
+ downBtn.addEventListener('click', () => move(1));
+ removeBtn.addEventListener('click', () => {
+ item.remove();
+ onRemove();
+ });
+
+ return item;
+}
+
+
+// ---- State ----
+
+const viewerDiv = /** @type {HTMLDivElement} */ (document.getElementById('viewerDiv'));
+const state = {
+ /** @type {Endpoint | null} */
+ endpoint: null,
+ /** @type {itowns.GlobeView} */
+ view: new itowns.GlobeView(viewerDiv, {
+ coord: new itowns.Coordinates('EPSG:4326', 2.351323, 48.856712),
+ }),
+};
+
+
+// ---- DOM events ----
+
+const toolbox = /** @type {HTMLFormElement} */ (document.getElementById('toolbox'));
+const ogcURL = /** @type {HTMLInputElement} */ (document.getElementById('ogc-url'));
+const ogcSelect = /** @type {HTMLSelectElement} */ (document.getElementById('ogc-type'));
+const ogcStatus = /** @type {HTMLOutputElement} */ (document.getElementById('ogc-status'));
+const layerPicker = /** @type {HTMLFieldSetElement} */ (document.getElementById('layer-picker'));
+const layerPickerCount = /** @type {HTMLOutputElement} */ (document.getElementById('layer-count'));
+const layerPickerSearch = /** @type {HTMLInputElement} */ (document.getElementById('layer-search'));
+const pickerEl = /** @type {HTMLUListElement} */ (document.getElementById('layer-list'));
+const layerAddButton = /** @type {HTMLButtonElement} */ (document.getElementById('layer-add'));
+const activeLayers = /** @type {HTMLFieldSetElement} */ (document.getElementById('active'));
+const activeLayersList = /** @type {HTMLOListElement} */ (document.getElementById('active-list'));
+const activeLayersCount = /** @type {HTMLOutputElement} */ (document.getElementById('active-count'));
+
+function getCheckedNames() {
+ return Array.from(
+ /** @type {NodeListOf} */ (pickerEl.querySelectorAll('input:checked')),
+ input => input.value,
+ );
+}
+
+function updateActiveList() {
+ const items = activeLayersList.children;
+
+ activeLayers.hidden = items.length === 0;
+ activeLayersCount.value = items.length.toString();
+
+ for (let i = 0; i < items.length; i++) {
+ const buttons = items[i].getElementsByTagName('button');
+ buttons[0].disabled = i === 0;
+ buttons[1].disabled = i === items.length - 1;
+ }
+}
+
+ogcURL.value = 'https://data.geopf.fr/wmts';
+const types = ['wmts', 'wms', 'wfs'];
+
+const params = new URLSearchParams(window.location.search);
+const url = params.get('url');
+if (url) {
+ ogcURL.value = url;
+}
+const type = params.get('type');
+if (type && types.includes(type)) {
+ ogcSelect.value = type;
+}
+
+toolbox.addEventListener('submit', async (event) => {
+ event.preventDefault();
+
+ const type = /** @type {'wmts' | 'wms' | 'wfs'} */ (ogcSelect.value);
+
+ ogcStatus.textContent = 'Connecting...';
+ layerPicker.hidden = true;
+ pickerEl.replaceChildren();
+ layerPickerSearch.value = '';
+
+ try {
+ const endpoint = await endpointFromUrl(ogcURL.value.trim(), type);
+ state.endpoint = endpoint;
+ ogcStatus.textContent = endpoint.getServiceInfo()?.title;
+
+ const layers = listLayers(endpoint);
+ const supported = layers.filter(layer => getLayerCrs(layer) !== undefined);
+ pickerEl.replaceChildren(...layers.map(
+ layer => ogcLayerItem({ layer }),
+ ));
+ layerPickerCount.value = `${supported.length}/${layers.length}`;
+ layerPicker.hidden = false;
+ } catch (err) {
+ ogcStatus.textContent = `Error: ${err instanceof Error ? err.message : err}`;
+ }
+});
+
+layerPickerSearch.addEventListener('input', () => {
+ const query = layerPickerSearch.value.toLowerCase().trim();
+ for (const item of pickerEl.children) {
+ if (!(item instanceof HTMLElement)) {
+ continue;
+ }
+ const label = item.firstElementChild;
+ if (!(label instanceof HTMLLabelElement)) {
+ continue;
+ }
+ const content = `${label.textContent} ${label.title}`.toLowerCase();
+ item.hidden = query !== '' && !content.includes(query);
+ }
+});
+
+layerAddButton.addEventListener('click', async () => {
+ const { endpoint, view } = state;
+ if (!endpoint) {
+ return;
+ }
+
+ for (const name of getCheckedNames()) {
+ try {
+ const layer = await addLayer(view, { endpoint, name });
+ activeLayersList.append(itownsLayerItem({
+ layer,
+ onMove(direction) {
+ if (layer instanceof itowns.ColorLayer) {
+ if (direction < 0) {
+ itowns.ColorLayersOrdering.moveLayerDown(state.view, layer.id);
+ } else {
+ itowns.ColorLayersOrdering.moveLayerUp(state.view, layer.id);
+ }
+ }
+ updateActiveList();
+ },
+ onRemove() {
+ state.view.removeLayer(layer.id);
+ state.view.notifyChange();
+ updateActiveList();
+ },
+ onOpacity(opacity) {
+ if (layer instanceof itowns.ColorLayer) {
+ layer.opacity = opacity;
+ state.view.notifyChange(layer);
+ }
+ },
+ }));
+ updateActiveList();
+ } catch (err) {
+ console.error(err instanceof Error ? err.message : err);
+ }
+ }
+
+ for (const checkbox of /** @type {NodeListOf} */ (
+ pickerEl.querySelectorAll('input:checked')
+ )) {
+ checkbox.checked = false;
+ }
+});
diff --git a/package-lock.json b/package-lock.json
index e28a9dc9ad..b2d479d07a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"@babel/preset-env": "^7.29.0",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.28.6",
+ "@camptocamp/ogc-client": "^1.3.0",
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.1.0",
"@types/three": "^0.182.0",
@@ -1765,6 +1766,48 @@
"node": ">=18"
}
},
+ "node_modules/@camptocamp/ogc-client": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@camptocamp/ogc-client/-/ogc-client-1.3.0.tgz",
+ "integrity": "sha512-E1g11pBbVyWRudlWMVJrHtSfwK6nvCP0bLy8+RyCWLjlEEg92BICKep1Eg/daS/0d2Gjxlpnr1/aXowvvnosMA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@rgrove/parse-xml": "^4.1.0",
+ "node-fetch": "^3.3.1"
+ },
+ "peerDependencies": {
+ "ol": ">5.x",
+ "proj4": ">2.8"
+ },
+ "peerDependenciesMeta": {
+ "ol": {
+ "optional": true
+ },
+ "proj4": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@camptocamp/ogc-client/node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
"node_modules/@conventional-changelog/git-client": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.6.0.tgz",
@@ -3113,6 +3156,16 @@
}
}
},
+ "node_modules/@rgrove/parse-xml": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.2.3.tgz",
+ "integrity": "sha512-Jhlb+0zYez1T1yXUQs3F1qAtFuJljBVNdy9TKmLDauAXkxsOXopKYOyQ5Wm6SvP3fycav0GviX4Y15WWhGetMw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@simple-libs/child-process-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz",
@@ -6065,6 +6118,16 @@
"integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==",
"license": "Apache-2.0"
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -7030,6 +7093,30 @@
}
}
},
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
@@ -7223,6 +7310,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -9286,6 +9386,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -12255,6 +12376,16 @@
"minimalistic-assert": "^1.0.0"
}
},
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/web-worker": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
diff --git a/package.json b/package.json
index f8e72e6216..e40929f7fa 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
"@babel/preset-env": "^7.29.0",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.28.6",
+ "@camptocamp/ogc-client": "^1.3.0",
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.1.0",
"@types/three": "^0.182.0",
diff --git a/packages/Main/src/Core/Style.js b/packages/Main/src/Core/Style.js
index bc7860148c..5c57952730 100644
--- a/packages/Main/src/Core/Style.js
+++ b/packages/Main/src/Core/Style.js
@@ -533,8 +533,8 @@ class Style extends EventDispatcher {
@private */
_initFill(params) {
this._defineCategoryProperty('fill');
- defineStyleProperty(this, 'fill', 'color', params.color);
- defineStyleProperty(this, 'fill', 'opacity', params.opacity, 1.0);
+ defineStyleProperty(this, 'fill', 'color', params.color, '#ff0000');
+ defineStyleProperty(this, 'fill', 'opacity', params.opacity, 0.5);
defineStyleProperty(this, 'fill', 'pattern', params.pattern);
defineStyleProperty(this, 'fill', 'base_altitude', params.base_altitude, baseAltitudeDefault);
@@ -585,7 +585,7 @@ class Style extends EventDispatcher {
@private */
_initStroke(params) {
this._defineCategoryProperty('stroke');
- defineStyleProperty(this, 'stroke', 'color', params.color);
+ defineStyleProperty(this, 'stroke', 'color', params.color, '#ff0000');
defineStyleProperty(this, 'stroke', 'opacity', params.opacity, 1.0);
defineStyleProperty(this, 'stroke', 'width', params.width, 1.0);
defineStyleProperty(this, 'stroke', 'dasharray', params.dasharray, []);
@@ -597,7 +597,7 @@ class Style extends EventDispatcher {
@private */
_initPoint(params) {
this._defineCategoryProperty('point');
- defineStyleProperty(this, 'point', 'color', params.color);
+ defineStyleProperty(this, 'point', 'color', params.color, '#ff0000');
defineStyleProperty(this, 'point', 'line', params.line);
defineStyleProperty(this, 'point', 'opacity', params.opacity, 1.0);
defineStyleProperty(this, 'point', 'radius', params.radius, 2.0);
diff --git a/packages/Main/src/Source/WMSSource.js b/packages/Main/src/Source/WMSSource.js
index 8cb42b1e3a..d7f39b8bb3 100644
--- a/packages/Main/src/Source/WMSSource.js
+++ b/packages/Main/src/Source/WMSSource.js
@@ -127,7 +127,7 @@ class WMSSource extends Source {
this.width = source.width || source.height || 256;
this.height = source.height || source.width || 256;
this.version = source.version || '1.3.0';
- this.transparent = source.transparent || false;
+ this.transparent = source.transparent || true;
this.bboxDigits = source.bboxDigits;
if (source.axisOrder) {