UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

449 lines (448 loc) 18.9 kB
// SPDX-License-Identifier: Apache-2.0 import { Image as ImageLayer } from 'ol/layer.js'; import ImageWMS from 'ol/source/ImageWMS.js'; import WMSCapabilities from 'ol/format/WMSCapabilities.js'; import WMSGetFeatureInfo from 'ol/format/WMSGetFeatureInfo.js'; import GeoJSON from 'ol/format/GeoJSON.js'; import SelectionParam from '../../models/selectionparam.js'; import Exception from 'jsts/java/lang/Exception.js'; export default class WmsClient { map; ogcServer; context; resolutionTolerance = 5; geoJson = new GeoJSON(); wmsGetFeatureInfo = new WMSGetFeatureInfo(); capabilityPromise = null; capabilities = null; get state() { return this.context.stateManager.state; } get audienceExcludedPaths() { return (this.context.configManager.Config.oauth?.issuer.audienceExcludedPaths ?? this.context.configManager.Config.gmfauth?.audienceExcludedPaths ?? []); } // The Id of this dictionary if an unique ID that allow the differenciantion of server queries. // For example, a combination of server URL and ImageType could be used. // Each element of this dictionary will generate 1 WMS server query layers = []; olayer; // Independent layers are layers that need to be queried alone // (not combine to other WMS layers in the same query) // The treeItemId will be used as key for this dictionary independentLayers = {}; basemapLayers = {}; constructor(ogcServer, map, context) { this.ogcServer = ogcServer; this.map = map; this.context = context; } removeAllBasemapLayers() { for (const basmapLayer of Object.values(this.basemapLayers)) { this.map.removeLayer(basmapLayer.olayer); } this.basemapLayers = {}; } addLayer(layerWms) { this.addLayerInternal(layerWms); this.manageLayerOptions(layerWms); } addLayerInternal(layerWms) { this.layers.push(layerWms); if (!this.olayer) { // Create a new ol layer and add it to the right server this.olayer = new ImageLayer(); // Set zindex for this new layer // (The bigger the order is, the deeper in the map it should be displayed.) // (order is the inverse of z-index) this.olayer.setZIndex(-layerWms.order); this.map.addLayer(this.olayer); } const source = this.createImageWMSSource(); this.olayer.setSource(source); } createImageWMSSource(layerList = this.layers) { const url = this.ogcServer.url; const imageType = this.ogcServer.imageType; const orderedLayers = layerList.slice().sort((l1, l2) => { return l2.order - l1.order; }); const orderedLayerNames = this.getOpenLayerLayerNames(orderedLayers); const requestedUrl = new URL(url); const shouldExclude = this.audienceExcludedPaths.some((pattern) => new RegExp(pattern).test(requestedUrl.pathname)); const crossOrigin = this.state.oauth.audience?.includes(requestedUrl.hostname) && !shouldExclude ? 'use-credentials' : 'anonymous'; const source = new ImageWMS({ url: url, params: { LAYERS: orderedLayerNames, FORMAT: imageType }, crossOrigin: crossOrigin }); // We intercept the event in order to set an error icon if the WMS query has an error // Otherwise we do no see anything on the client. source.on('imageloaderror', () => { for (const layerWms of layerList) { this.context.layerManager.setError(layerWms, 'Image cannot be loaded from WMS Server'); } }); source.on('imageloadend', () => { for (const layerWms of layerList) { this.context.layerManager.unsetError(layerWms); } }); return source; } addBasemapLayer(layerWms) { const source = this.createImageWMSSource([layerWms]); const olayer = new ImageLayer({ source: source, opacity: layerWms.opacity }); // For basemap, set a minimal number (arbitrary defined to less than -5000) olayer.setZIndex(-5000 - layerWms.order); this.basemapLayers[layerWms.treeItemId] = { layerWms: layerWms, olayer: olayer }; this.map.addLayer(olayer); } removeLayer(layerWms) { if (this.layerExists(layerWms)) { if (layerWms.treeItemId in this.independentLayers) { const olayer = this.independentLayers[layerWms.treeItemId].olayer; delete this.independentLayers[layerWms.treeItemId]; this.map.removeLayer(olayer); } else if (this.layerInStandardLayers(layerWms)) { // Get existing ol layer for this server and remove the wms layer from the source this.layers = this.layers.filter((l) => l.treeItemId !== layerWms.treeItemId); if (this.layers.length > 0) { // There are still layers in the list. => We update the layer source const source = this.createImageWMSSource(); this.olayer.setSource(source); } else if (this.olayer) { // No more layer here. // => We simply remove the whole layer this.map.removeLayer(this.olayer); delete this.olayer; } } else { console.warn('Nothing to remove !'); } } else { console.error(`Cannot remove WMS-Layer ${layerWms.name} from the map: it does not exist!`); } } layerExists(layerWms) { return (this.layerInStandardLayers(layerWms) || this.layerIsIndependentLayer(layerWms) || this.layerIsBasemapLayer(layerWms)); } layerInStandardLayers(layerWms) { return this.layers.some((l) => l.treeItemId === layerWms.treeItemId); } layerIsIndependentLayer(layerWms) { return layerWms.treeItemId in this.independentLayers; } layerIsBasemapLayer(layerWms) { return layerWms.treeItemId in this.basemapLayers; } getOLayer(layerWms) { if (layerWms.treeItemId in this.independentLayers) { return this.independentLayers[layerWms.treeItemId].olayer; } if (layerWms.treeItemId in this.basemapLayers) { return this.basemapLayers[layerWms.treeItemId].olayer; } if (this.layerInStandardLayers(layerWms)) { return this.olayer; } return null; } changeOpacity(layerWms) { this.manageLayerOptions(layerWms); } changeFilter(layerWms) { this.manageLayerOptions(layerWms); } changeTimeRestriction(layerWms) { this.manageLayerOptions(layerWms); } prepareSwipe(layerWms) { this.manageLayerOptions(layerWms); } manageLayerOptions(layerWms) { if (!this.layerExists(layerWms)) { throw new Error('Cannot change filter for this layer: it does not exist'); } const isBasemapLayer = layerWms.treeItemId in this.basemapLayers; const isLayerIndependent = layerWms.treeItemId in this.independentLayers; // TODO SMS: if all layers share the same opacity, then mustBeIndependent can be false even if isTransparent is true. // This will be changed in the future. const mustBeIndependent = layerWms.hasFilter || layerWms.hasTimeRestriction || layerWms.isTransparent || layerWms.swiped !== 'no'; if (!isBasemapLayer) { if (isLayerIndependent && !mustBeIndependent) { const olayer = this.independentLayers[layerWms.treeItemId].olayer; // We delete the layer from the transparent layers delete this.independentLayers[layerWms.treeItemId]; this.map.removeLayer(olayer); // And add it to the normal layer again this.addLayerInternal(layerWms); } else if (!isLayerIndependent && mustBeIndependent) { this.makeLayerIndependent(layerWms); } } const olayer = this.getOLayer(layerWms); if (!olayer) { throw new Exception('The layer must exist at this state!'); } if (layerWms.hasValidOpacity) { olayer.setOpacity(layerWms.opacity); } this.updateLayerFilter(layerWms, olayer); this.updateTimeRestriction(layerWms, olayer); } makeLayerIndependent(layerWms) { if (layerWms.treeItemId in this.independentLayers) { // The layer is already independent. => nothing to do here. } else if (this.layerInStandardLayers(layerWms)) { // First, we remove the layer from the default layer this.removeLayer(layerWms); // Then, we create a new layer const source = this.createImageWMSSource([layerWms]); const olayer = new ImageLayer({ source: source, opacity: layerWms.opacity }); this.independentLayers[layerWms.treeItemId] = { layerWms: layerWms, olayer: olayer }; this.map.addLayer(olayer); } else { throw new Error('A layer can be made independent only if it has already been added to the map.'); } } updateLayerFilter(layerWms, olayer) { const source = olayer.getSource(); if (layerWms.hasFilter) { const filterStr = this.buildFilterQuery(layerWms); source.updateParams({ FILTER: filterStr }); } else { // If present, remove the filter parameter const params = source.getParams(); if (params.FILTER) { delete params.FILTER; source.updateParams(params); } } } updateTimeRestriction(layerWms, olayer) { const source = olayer.getSource(); if (layerWms.hasTimeRestriction) { olayer.getSource().updateParams({ TIME: layerWms.timeRestriction }); } else { // If present, remove the time parameter const params = source.getParams(); if (params.TIME) { delete params.TIME; source.updateParams(params); } } } selectFeatures(extent) { if (this.layers.length === 0 && !this.independentLayers) { return; } const selectionParams = []; if (this.ogcServer.wfsSupport && this.layers.length > 0) { selectionParams.push(new SelectionParam(this.ogcServer, this.layers, this.state.projection, extent, this.olayer)); } else { // This will need a WMS GetFeatureInfo. // In this case multiple layers are not allowed, and we have to create 1 selection param per layer for (const layer of this.layers) { selectionParams.push(new SelectionParam(this.ogcServer, [layer], this.state.projection, extent, this.olayer)); } } for (const key in this.independentLayers) { const indepLayer = this.independentLayers[key]; selectionParams.push(new SelectionParam(this.ogcServer, [indepLayer.layerWms], this.state.projection, extent, indepLayer.olayer)); } this.state.selection.selectionParameters.push(...selectionParams); } /** * Selects features based on the specified query and prepares selection parameters. */ selectFeaturesByQuery(query) { const selectionParams = []; selectionParams.push(new SelectionParam(this.ogcServer, this.layers, this.state.projection, undefined, this.olayer, query)); for (const key in this.independentLayers) { const indepLayer = this.independentLayers[key]; selectionParams.push(new SelectionParam(this.ogcServer, [indepLayer.layerWms], this.state.projection, undefined, indepLayer.olayer, query)); } this.state.selection.selectionParameters.push(...selectionParams); } async getFeatureInfo(selectionParam) { const urlsAndLayerNames = this.getFeatureInfoUrl(selectionParam); const promises = Object.keys(urlsAndLayerNames).map((url) => fetch(url) .then((r) => r.text()) .then((response) => this.handleGetFeatureInfoResponse(response, url, urlsAndLayerNames))); return (await Promise.all(promises)).flat(); } getFeatureInfoUrl(param) { /* Url-layerName (feature id) objects. */ const urlsAndLayerNames = {}; const currentResolution = this.state.position.resolution; if (!currentResolution) { console.log('WMSClient called before resolution is set.'); return urlsAndLayerNames; } param.layers.forEach((layer) => { const olLayer = param.oLayer ?? this.getOLayer(layer); if (!layer.queryable || !olLayer || !layer.isVisibleAtResolution(currentResolution)) { return; } // Layer is queryable through WMS and has an OL layer. const coordinate = param.selectionBox ? [(param.selectionBox[0] + param.selectionBox[2]) / 2, (param.selectionBox[1] + param.selectionBox[3]) / 2] : []; const url = olLayer .getSource() ?.getFeatureInfoUrl(coordinate, (olLayer.getMapInternal()?.getView().getResolution() ?? currentResolution) + this.resolutionTolerance, this.state.projection, { INFO_FORMAT: layer.wmsInfoFormat, FEATURE_COUNT: 300 }); if (url !== undefined) { urlsAndLayerNames[url] = layer.name; } else throw new Error(`Unable to construct GetFeatureInfo URL for layer ${layer.name}`); }); return urlsAndLayerNames; } handleGetFeatureInfoResponse(response, url, urlsAndLayerNames) { const infoFormat = URL.parse(url)?.searchParams.get('INFO_FORMAT'); let features; if (infoFormat && (infoFormat.includes('application/geo+json') || infoFormat.includes('application/geojson'))) { features = this.geoJson.readFeatures(response, { dataProjection: this.state.projection, featureProjection: this.state.projection }); } else { features = this.wmsGetFeatureInfo.readFeatures(response, { dataProjection: this.state.projection, featureProjection: this.state.projection }); } // Set the feature id with the layer name. features.forEach((feature) => { if (!feature.getId()) { feature.setId(urlsAndLayerNames[url]); } }); return features; } refreshZIndexes() { // Recalculate source for Layers if (this.layers.length > 0) { // TODO: openLayer zindex = -order of the last layer in this.layers? not consistent: to improve? for (const layerWms of this.layers) { const zindex = -layerWms.order; this.olayer.setZIndex(zindex); } const source = this.createImageWMSSource(this.layers); this.olayer.setSource(source); } // Manage independant layers for (const obj of Object.values(this.independentLayers)) { const zindex = -obj.layerWms.order; obj.olayer.setZIndex(zindex); } } async getWmsCapabilities(params) { if (this.capabilityPromise !== null) { return this.capabilityPromise; } if (this.capabilities !== null) { // Capabilities were already loaded return Promise.resolve(this.capabilities); } this.capabilityPromise = (async () => { // Capabilities were not loaded yet. if (params) { params.delete('REQUEST'); params.delete('SERVICE'); params.delete('VERSION'); } else { params = new URLSearchParams(); } params.append('REQUEST', 'GetCapabilities'); params.append('SERVICE', 'WMS'); params.append('VERSION', '1.3.0'); const response = await fetch(`${this.ogcServer.url}?${params}`); const result = await response.text(); // Create new WMS Layer from Capabilities const parser = new WMSCapabilities(); this.capabilities = parser.read(result); return this.capabilities; })(); return this.capabilityPromise; } } export class WmsClientQgis extends WmsClient { /** QGIS-server does not filter on a WMS layer made from multiple underlying WFS queryLayers * Solution: directly query the queryLayers */ getOpenLayerLayerNames(layerList) { const hasFilter = layerList.some((layerWms) => layerWms.hasFilter); if (hasFilter) { const layerNames = layerList.flatMap((l) => l.queryLayers?.split(',')); return layerNames; } else { const layerNames = layerList.map((l) => l.layers ?? l.name); return layerNames; } } /** MapServer wants 1 filter per underlying WFS queryLayer, all included in parentheses * QGIS-server wants 1 filter per underlying WFS queryLayer, each in its own in parentheses * * MapServer: (<filter>...</filter><filter>...</filter>) * QGIS-server: (<filter>...</filter>)(<filter>...</filter>) */ buildFilterQuery(layerWms) { let filterStr = ''; if (layerWms.hasFilter) { const filter = layerWms.filter; const nbQuerylayers = layerWms.queryLayers.split(',').length; filterStr = ('(' + filter.toWmsGetMapFilter() + ')').repeat(nbQuerylayers); } return filterStr; } } export class WmsClientMapServer extends WmsClient { getOpenLayerLayerNames(layerList) { return layerList.map((l) => l.layers); } buildFilterQuery(layerWms) { let filterStr = ''; if (layerWms.hasFilter) { const filter = layerWms.filter; const nbQuerylayers = layerWms.queryLayers.split(',').length; filterStr = '(' + filter.toWmsGetMapFilter().repeat(nbQuerylayers) + ')'; } return filterStr; } } export const WmsClientDefault = WmsClientQgis; export const WmsClientGeoServer = WmsClientMapServer;