From 6f081a0400f3ff6f0dc09b80886c40caf73eed99 Mon Sep 17 00:00:00 2001 From: Stefan Forsgren Date: Wed, 25 Oct 2023 14:16:21 +0200 Subject: [PATCH 1/2] Moved wfs part of getFeature to WfsSource --- src/getfeature.js | 81 ++++++++++++++++++++---------------------- src/layer/wfssource.js | 53 ++++++++++++++++++++------- 2 files changed, 79 insertions(+), 55 deletions(-) diff --git a/src/getfeature.js b/src/getfeature.js index 7faea5be9..130443504 100644 --- a/src/getfeature.js +++ b/src/getfeature.js @@ -1,11 +1,22 @@ import EsriJSONFormat from 'ol/format/EsriJSON'; -import GeoJSONFormat from 'ol/format/GeoJSON'; -import replacer from './utils/replacer'; +import WfsSource from './layer/wfssource'; let projectionCode; let projection; const sourceType = {}; +/** + * Fetches features from a layer's source but does not add them to the layer. Supports WFS and AGS_FEATURE altough functionality differs. Mainly used by search, but + * is also exposed as an api function that MultiSelect uses. As q quirky bonus it also support fetching features from WMS layers if there is a WFS service at the same endpoint. + * + * @param {any} id Comma separated list of ids. If specified layer's filter and parameter extent is ignored (even configured map and layer extent is ignored). + * @param {any} layer Layer instance to fetch from + * @param {any} source Array of know sources. Probably the configuration section source. + * @param {any} projCode Projection code for the returned features. Ignored by WFS as map projection is used + * @param {any} proj projection like object for the returned features. Ignored by WFS as map projection is used + * @param {any} extent Extent to fetch inside. Layer configuration extent is honored. + * @returns {Promise} + */ export default function getfeature(id, layer, source, projCode, proj, extent) { projectionCode = projCode; projection = proj; @@ -15,6 +26,7 @@ export default function getfeature(id, layer, source, projCode, proj, extent) { if (type === 'AGS_FEATURE') { return sourceType.AGS_FEATURE(id, layer, serverUrl); } + // Note that this includes WMS which MultiSelect utilizes to make an WFS request to an unknown WFS layer assumed to reside on same place as WMS layer! return sourceType.WFS(id, layer, serverUrl, extent); } @@ -54,47 +66,32 @@ sourceType.AGS_FEATURE = function agsFeature(id, layer, serverUrl) { }; sourceType.WFS = function wfsSourceType(id, layer, serverUrl, extent) { - const geometryName = layer.get('geometryName'); - const format = new GeoJSONFormat({ - geometryName - }); - let queryFilter = ''; - if (!id) { - const filter = replacer.replace(layer.get('filter'), window); - const layerExtent = layer.get('extent'); - let minExtent; - if (extent && layerExtent) { - minExtent = [Math.max(extent[0], layerExtent[0]), - Math.max(extent[1], layerExtent[1]), - Math.min(extent[2], layerExtent[2]), - Math.min(extent[3], layerExtent[3])]; - if (!(minExtent[0] < minExtent[2] && minExtent[1] < minExtent[3])) { - return []; - } - } else if (extent) { - minExtent = extent; - } else if (layerExtent) { - minExtent = layerExtent; - } - if (filter) { - if (minExtent) { - queryFilter = layer.get('geometryName') ? `&CQL_FILTER=${filter} AND BBOX(${layer.get('geometryName')},${minExtent.toString()})` : `&CQL_FILTER=${filter}&BBOX=${minExtent.toString()}`; - } else { - queryFilter = `&CQL_FILTER=${filter}`; - } - } else if (minExtent) { - queryFilter = layer.get('geometryName') ? `&CQL_FILTER=BBOX(${layer.get('geometryName')},${minExtent.toString()})` : `&BBOX=${minExtent.toString()}`; - } + let wfsSource; + const layerType = layer.get('type'); + // Create a temporary WFS source if layer is WMS. + // This is a special case for multiselect which utlizes the fact that Geoserver usually has an WFS endpoint + // at the same place as an WMS endpoint + if (layerType === 'WMS') { + // Create the necessary configuration to create a request to WFS endpoint from a WMS layer + const sourceOpts = { + geometryName: layer.get('geometryName'), + dataProjection: projectionCode, + projectionCode, + loadingstrategy: 'all', + requestMethod: 'GET', + url: serverUrl, + customExtent: layer.get('extent'), + featureType: layer.get('id') + }; + wfsSource = new WfsSource(sourceOpts); } else { - queryFilter = `&featureId=${id}`; + wfsSource = layer.getSource(); } - const url = `${serverUrl}?`; - const data = ['service=WFS', - '&version=1.0.0', - `&request=GetFeature&typeName=${layer.get('name')}`, - '&outputFormat=json', - queryFilter - ].join(''); - return fetch(url + data, { type: 'GET', dataType: 'json' }).then(res => res.json()).then(json => format.readFeatures(json)).catch(error => console.error(error)); + if (id) { + return wfsSource.getFeatureFromSourceByIds(id); + } + // Have to pick up filter from layer as MultiSelect changes filter on layer instead of source + const filter = layer.get('filter'); + return wfsSource.getFeaturesFromSource(extent, filter, true); }; diff --git a/src/layer/wfssource.js b/src/layer/wfssource.js index 4b3835cb9..165481d70 100644 --- a/src/layer/wfssource.js +++ b/src/layer/wfssource.js @@ -54,17 +54,20 @@ class WfsSource extends VectorSource { } /** - * Called by VectorSource + * Called by VectorSource. VectorSource always calls with extent specified. If strategy = 'all' it is an infinite extent. * @param {any} extent */ onLoad(extent, resolution, projection, success, failure) { this._loaderHelper(extent) - .then(f => success(f)) + .then(f => { + super.addFeatures(f); + success(f); + }) .catch(() => failure()); } /** - * Set request method for layer + * Set request method for source * @param {any} method */ setMethod(method) { @@ -72,7 +75,7 @@ class WfsSource extends VectorSource { } /** - * Set filter on layer + * Set filter on source * @param {any} cql */ setFilter(cql) { @@ -81,7 +84,7 @@ class WfsSource extends VectorSource { } /** - * Clear filter on layer + * Clear filter on source */ clearFilter() { this._options.filter = ''; @@ -89,16 +92,18 @@ class WfsSource extends VectorSource { } /** - * Helper to reuse code. Consider it to be private to this class - * @param {any} extent - * @param {any} cql if provided, extent is ignored + * Helper to reuse code. Consider it to be private to this class. + * @param {any} extent Extent to query. If specified the result is limited to the intersection of this parameter and layer's extent configuration. + * @param {any} cql Optional extra cql for this call. + * @param {any} ignoreOriginalFilter true if configured filter should be ignored for this call making parameter cql only filter (if specified) + * @param {any} ids Comma separated list of feature ids. If specified you probably want to call with extent and cql empty and ignoreOriginalFilter = true */ - async _loaderHelper(extent, cql) { + async _loaderHelper(extent, cql, ignoreOriginalFilter, ids) { const serverUrl = this._options.url; // Set up the cql filter as a combination of the layer filter and the temporary cql parameter let cqlfilter = ''; - if (this._options.filter) { + if (this._options.filter && !ignoreOriginalFilter) { cqlfilter = replacer.replace(this._options.filter, window); if (cql) { cqlfilter += ' AND '; @@ -110,7 +115,7 @@ class WfsSource extends VectorSource { // Create the complete CQL query string let queryFilter = ''; - if (this._options.strategy === 'all' || cql || this._options.isTable) { + if (!extent || this._options.isTable) { queryFilter = cqlfilter ? `&CQL_FILTER=${cqlfilter}` : ''; } else { // Extent should be used. Depending if there also is a filter, the queryfilter looks different @@ -129,10 +134,14 @@ class WfsSource extends VectorSource { } // Create the complete URL + // FIXME: rewrite using URL class let url = [`${serverUrl}${serverUrl.indexOf('?') < 0 ? '?' : '&'}service=WFS`, `&version=1.1.0&request=GetFeature&typeName=${this._options.featureType}&outputFormat=application/json`, `&srsname=${this._options.dataProjection}`].join(''); url += queryFilter; + if (ids) { + url += `&FeatureId=${ids}`; + } url = encodeURI(url); // Actually fetch some features @@ -162,7 +171,6 @@ class WfsSource extends VectorSource { f.unset(f.getGeometryName(), true); }); } - super.addFeatures(features); return features; } @@ -172,7 +180,26 @@ class WfsSource extends VectorSource { * @param {any} cql */ async ensureLoaded(cql) { - await this._loaderHelper(null, cql); + const features = await this._loaderHelper(null, cql, false); + super.addFeatures(features); + } + + /** + * Fetches features by id. Extent and filters are ignored. Does NOT add the feature to the layer + * @param {any} ids Comma separated list of ids + */ + async getFeatureFromSourceByIds(ids) { + return this._loaderHelper(null, null, true, ids); + } + + /** + * Fetches features from server without adding them to the source. Honors filter configuration unless ignoreOriginalFilter is specified. + * @param {any} extent Optional extent + * @param {any} cql Optional additional cql filter for this call + * @param {any} ignoreOriginalFilter true if configured cql filter should be ignored for this request + */ + async getFeaturesFromSource(extent, cql, ignoreOriginalFilter) { + return this._loaderHelper(extent, cql, ignoreOriginalFilter); } } From 440641e823f9401f7291a8017227964bb4b712e8 Mon Sep 17 00:00:00 2001 From: Stefan Forsgren Date: Wed, 25 Oct 2023 14:56:10 +0200 Subject: [PATCH 2/2] Create temp source for all layers but WFS --- src/getfeature.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/getfeature.js b/src/getfeature.js index 130443504..dac6da52a 100644 --- a/src/getfeature.js +++ b/src/getfeature.js @@ -71,7 +71,7 @@ sourceType.WFS = function wfsSourceType(id, layer, serverUrl, extent) { // Create a temporary WFS source if layer is WMS. // This is a special case for multiselect which utlizes the fact that Geoserver usually has an WFS endpoint // at the same place as an WMS endpoint - if (layerType === 'WMS') { + if (layerType !== 'WFS') { // Create the necessary configuration to create a request to WFS endpoint from a WMS layer const sourceOpts = { geometryName: layer.get('geometryName'),