Skip to content

Commit

Permalink
fix: allow percent sign in getFeature filter (#1877)
Browse files Browse the repository at this point in the history
* Moved wfs part of getFeature to WfsSource

* Create temp source for all layers but WFS

---------

Co-authored-by: Stefan Forsgren <stefan.forsgren@xlent.se>
  • Loading branch information
steff-o and Stefan Forsgren authored Oct 31, 2023
1 parent dd11ef5 commit 0ade01c
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 55 deletions.
81 changes: 39 additions & 42 deletions src/getfeature.js
Original file line number Diff line number Diff line change
@@ -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<any[]>}
*/
export default function getfeature(id, layer, source, projCode, proj, extent) {
projectionCode = projCode;
projection = proj;
Expand All @@ -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);
}

Expand Down Expand Up @@ -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 !== 'WFS') {
// 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);
};
53 changes: 40 additions & 13 deletions src/layer/wfssource.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,25 +54,28 @@ 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) {
this._options.requestMethod = method;
}

/**
* Set filter on layer
* Set filter on source
* @param {any} cql
*/
setFilter(cql) {
Expand All @@ -81,24 +84,26 @@ class WfsSource extends VectorSource {
}

/**
* Clear filter on layer
* Clear filter on source
*/
clearFilter() {
this._options.filter = '';
this.refresh();
}

/**
* 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 ';
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -162,7 +171,6 @@ class WfsSource extends VectorSource {
f.unset(f.getGeometryName(), true);
});
}
super.addFeatures(features);
return features;
}

Expand All @@ -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);
}
}

Expand Down

0 comments on commit 0ade01c

Please sign in to comment.