Skip to content

Commit

Permalink
Add filterType source param and use QGIS EXP_FILTER when param is…
Browse files Browse the repository at this point in the history
… `qgis` (#1536)

* Separate filter creation, add `filterType` param

* Support QGIS expression filters; `EXP_FILTER`

* Support `getFeature` on QGIS Server layers

* Remove duplicate statement

* Missing semicolon

* Support QGIS's `EXP_FILTER` in offline wfs layer

* Fix for QGIS filters when not using extent

* Fixed linter errors

* Fixed spelling error

* Removed default that is already set on the source

* Moved source defaults back to wfs.js

* Returned standard queries using BBOX parameter

* Add layer names to ids for QGIS support

* Indentation fix

* Revert "Support QGIS's `EXP_FILTER` in offline wfs layer"

This reverts commit 9d529ca.

* Revert "Missing semicolon"

This reverts commit a63e0b9.

* Revert "Remove duplicate statement"

This reverts commit 1c486ba.

* Mark _createQueryFilter() as private

* Fix incorrect comment

* Revert "Support `getFeature` on QGIS Server layers"

This reverts commit c4394a7.

* Fix incorrect function call

* Allow filterType on layer level

* Revert "Allow filterType on layer level"

This reverts commit 6c5055b.
  • Loading branch information
MattiasSp authored Apr 9, 2024
1 parent 8930b3c commit d71858d
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 42 deletions.
2 changes: 1 addition & 1 deletion src/getfeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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.
* is also exposed as an api function that MultiSelect uses. As a quirky bonus it also supports 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
Expand Down
4 changes: 3 additions & 1 deletion src/layer/wfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export default function wfs(layerOptions, viewer) {
const wfsDefault = {
layerType: 'vector'
};
const sourceDefault = {};
const sourceDefault = {
filterType: 'cql'
};
const wfsOptions = Object.assign({}, wfsDefault, layerOptions);
const sourceOptions = Object.assign({}, sourceDefault, viewer.getMapSource()[layerOptions.sourceName]);
sourceOptions.featureType = wfsOptions.id;
Expand Down
160 changes: 120 additions & 40 deletions src/layer/wfssource.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ class WfsSource extends VectorSource {

/**
* Set filter on source
* @param {any} cql
* @param {any} filter
*/
setFilter(cql) {
this._options.filter = cql;
setFilter(filter) {
this._options.filter = filter;
this.refresh();
}

Expand All @@ -92,55 +92,135 @@ class WfsSource extends VectorSource {
}

/**
* 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
* Generate a wfs query filter according to the wfs layer source's `filterType` (`cql`|`qgis`).
* If specified, an extra filter is combined with any layer filter already present.
* Consider this function 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} extraFilter Optional extra filter for this call with syntax matching the source's `filterType` (`cql`|`qgis`). Will be combined with any configured layer filter unless ignoreOriginalFilter is true
* @param {any} ignoreOriginalFilter true if configured layer filter should be ignored for this call, making parameter extraFilter the only filter (if specified)
*/
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 = '';
_createQueryFilter(extent, extraFilter, ignoreOriginalFilter) {
let layerFilter = '';
let queryFilter = '';
// Add layer filter unless `ignoreOriginalFilter` is set
if (this._options.filter && !ignoreOriginalFilter) {
cqlfilter = replacer.replace(this._options.filter, window);
if (cql) {
cqlfilter += ' AND ';
}
}
if (cql) {
cqlfilter += `${replacer.replace(cql, window)}`;
layerFilter = replacer.replace(this._options.filter, window);
}

// Create the complete CQL query string
let queryFilter = '';
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
// Prepare extent if used
let requestExtent;
if (extent && !this._options.isTable) {
// Combine the layer's extent with the extent used for this function call
const ext = getIntersection(this._options.customExtent, extent) || extent;
let requestExtent;
// Reproject it if the layer's data projection differs from that of the map
if (this._options.dataProjection !== this._options.projectionCode) {
requestExtent = transformExtent(ext, this._options.projectionCode, this._options.dataProjection);
} else {
requestExtent = ext;
}
if (cqlfilter) {
queryFilter = `&CQL_FILTER=${cqlfilter} AND BBOX(${this._options.geometryName},${requestExtent.join(',')},'${this._options.dataProjection}')`;
} else {
// If extent is used but no filters are set, just return the BBOX parameter.
if (!layerFilter && !extraFilter) {
queryFilter = `&BBOX=${requestExtent.join(',')},${this._options.dataProjection}`;
return queryFilter;
}
}

// Integrate provided layer filters, `extraFilter` and extent into a single query filter
// Both QGIS and GeoServer treats the WFS parameters `BBOX` and `CQL_FILTER`/`EXP_FILTER` as mutually exclusive,
// so instead of BBOX we use the vendor filters also for the extent filtering.
switch (this._options.filterType) {
case 'cql': {
// Set up the filter as a combination of the layer filter and the extraFilter parameter
let cqlfilter = layerFilter;
if (layerFilter && extraFilter) {
cqlfilter += ' AND ';
}
if (extraFilter) {
cqlfilter += `${replacer.replace(extraFilter, window)}`;
}

// If using extent, and the layer is not a geometryless table, integrate it into the query filter
if (extent && !this._options.isTable) {
if (cqlfilter) {
cqlfilter += ' AND ';
}
cqlfilter += `BBOX(${this._options.geometryName},${requestExtent.join(',')},'${this._options.dataProjection}')`;
}

// Create the complete CQL query string
if (cqlfilter) {
queryFilter = `&CQL_FILTER=${cqlfilter}`;
}
break;
}
case 'qgis': {
// Set up the filter as a combination of the layer filter and the extraFilter parameter
let qgisFilter = layerFilter;
if (layerFilter && extraFilter) {
qgisFilter += ' AND ';
}
if (extraFilter) {
qgisFilter += `${replacer.replace(extraFilter, window)}`;
}

// If using extent, and the layer is not a geometryless table, integrate it into the query filter
if (extent && !this._options.isTable) {
if (qgisFilter) {
qgisFilter += ' AND ';
}
const wktBbox = `POLYGON ((${requestExtent[0]} ${requestExtent[3]},${requestExtent[2]} ${requestExtent[3]},${requestExtent[2]} ${requestExtent[1]},${requestExtent[0]} ${requestExtent[1]},${requestExtent[0]} ${requestExtent[3]}))`;
qgisFilter += `intersects_bbox(@geometry,geom_from_wkt('${wktBbox},${this._options.dataProjection}'))`;
}

// Create the complete QGIS EXP_FILTER query string
if (qgisFilter) {
queryFilter = `&EXP_FILTER=${qgisFilter}`;
}
break;
}
default: break;
}
return queryFilter;
}

/**
* 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} extraFilter Optional extra filter for this call with syntax matching the source's `filterType` (`cql`|`qgis`)
* @param {any} ignoreOriginalFilter true if configured layer filter should be ignored for this call making parameter `extraFilter` the only filter (if specified)
* @param {any} ids Comma separated list of feature ids. If specified, extent and other filters will be ignored.
*/
async _loaderHelper(extent, extraFilter, ignoreOriginalFilter, ids) {
const serverUrl = this._options.url;

// 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;
// Add FeatureId parameter if there are ids requested.
// FeatureId is incompatible with BBOX and CQL_FILTER (will override QGIS's EXP_FILTER) so they should not be used together.
// QGIS Server expects feature type name to be prepended while GeoServer handles both with and without.
if (ids || ids === 0) {
url += `&FeatureId=${ids}`;
switch (this._options.filterType) {
case 'qgis': {
const idArray = ids.toString().split(','); // Split to array
idArray.map(id => {
// Prepend the layername using id if needed (in case the name is using double underscore notation)
if (!id.toString().startsWith(`${this._options.featureType}.`)) {
return `${this._options.featureType}.${id}`;
}
return id;
}); // Join to a comma separated string again
url += `&FeatureId=${idArray.join(',')}`;
break;
}
default: {
url += `&FeatureId=${ids}`;
}
}
} else { // If there are no ids requested, append the query filter
url += this._createQueryFilter(extent, extraFilter, ignoreOriginalFilter);
}
url = encodeURI(url);

Expand Down Expand Up @@ -175,12 +255,12 @@ class WfsSource extends VectorSource {
}

/**
* Makes a call to the server with the provided cql filter and adds all matching records to the layer.
* Makes a call to the server with the provided query filter and adds all matching records to the layer.
* If the layer has a filter it is honoured.
* @param {any} cql
* @param {any} filter
*/
async ensureLoaded(cql) {
const features = await this._loaderHelper(null, cql, false);
async ensureLoaded(filter) {
const features = await this._loaderHelper(null, filter, false);
super.addFeatures(features);
}

Expand All @@ -195,11 +275,11 @@ class WfsSource extends VectorSource {
/**
* 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
* @param {any} filter Optional additional filter for this call with syntax matching the source's `filterType` (`cql`|`qgis`)
* @param {any} ignoreOriginalFilter true if configured layer filter should be ignored for this request
*/
async getFeaturesFromSource(extent, cql, ignoreOriginalFilter) {
return this._loaderHelper(extent, cql, ignoreOriginalFilter);
async getFeaturesFromSource(extent, filter, ignoreOriginalFilter) {
return this._loaderHelper(extent, filter, ignoreOriginalFilter);
}
}

Expand Down

0 comments on commit d71858d

Please sign in to comment.