@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
449 lines (448 loc) • 18.9 kB
JavaScript
// 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;