From d807f13a9325724c6a39226fa4ff04a49da5c58a Mon Sep 17 00:00:00 2001 From: Benjamin Gerber Date: Mon, 16 Nov 2020 06:15:35 +0100 Subject: [PATCH] Add featureTypesBbox option on WFS writeGetFeature Add a possibility to provide one specific bbox per feature type on WFS writeGetFeature. This option results to one query node per featureTypesBbox item. One query node, for one feature type, will have a specific bbox filter and every query node will share the same others filters (if a filter option is defined). --- doc/errors/index.md | 4 +- src/ol/format/WFS.js | 94 +++++++++++++++++++++++---------- test/spec/ol/format/wfs.test.js | 53 +++++++++++++++++++ 3 files changed, 122 insertions(+), 29 deletions(-) diff --git a/doc/errors/index.md b/doc/errors/index.md index 646a6a33b5..de2df3826b 100644 --- a/doc/errors/index.md +++ b/doc/errors/index.md @@ -47,7 +47,7 @@ The default `geometryFunction` can only handle `ol/geom/Point` geometries. ### 11 -`options.featureTypes` should be an Array. +`options.featureTypes` must be an Array. ### 12 @@ -252,4 +252,4 @@ A layer can only be added to the map once. Use either `layer.setMap()` or `map.a ### 68 -Data from this source can only be rendered if it has a projection compatible with the view projection. \ No newline at end of file +Data from this source can only be rendered if it has a projection compatible with the view projection. diff --git a/src/ol/format/WFS.js b/src/ol/format/WFS.js index cf539b7479..f30390fa21 100644 --- a/src/ol/format/WFS.js +++ b/src/ol/format/WFS.js @@ -19,7 +19,7 @@ import { pushParseAndPop, pushSerializeAndPop, } from '../xml.js'; -import {and as andFilter, bbox as bboxFilter} from './filter.js'; +import {and as andFilterFn, bbox as bboxFilterFn} from './filter.js'; import {assert} from '../asserts.js'; import {assign} from '../obj.js'; import {get as getProjection} from '../proj.js'; @@ -129,7 +129,9 @@ const TRANSACTION_SERIALIZERS = { * @typedef {Object} WriteGetFeatureOptions * @property {string} featureNS The namespace URI used for features. * @property {string} featurePrefix The prefix for the feature namespace. - * @property {Array} featureTypes The feature type names. + * @property {Array} featureTypes The feature type names or FeatureType objects to + * define a unique bbox filter per feature type name (in this case, options `bbox` and `geometryName` are + * ignored.). * @property {string} [srsName] SRS name. No srsName attribute will be set on * geometries when this is not provided. * @property {string} [handle] Handle. @@ -151,6 +153,13 @@ const TRANSACTION_SERIALIZERS = { * E.g. `hits` only includes the `numberOfFeatures` attribute in the response and no features. */ +/** + * @typedef {Object} FeatureType + * @property {!string} name The feature type name. + * @property {!import("../extent.js").Extent} bbox Extent to use for the BBOX filter. + * @property {!string} geometryName Geometry name to use in the BBOX filter. + */ + /** * @typedef {Object} WriteTransactionOptions * @property {string} featureNS The namespace URI used for features. @@ -472,7 +481,6 @@ class WFS extends XMLFeature { const node = createElementNS(WFSNS[this.version_], 'GetFeature'); node.setAttribute('service', 'WFS'); node.setAttribute('version', this.version_); - let filter; if (options.handle) { node.setAttribute('handle', options.handle); } @@ -494,21 +502,6 @@ class WFS extends XMLFeature { if (options.viewParams !== undefined) { node.setAttribute('viewParams', options.viewParams); } - filter = options.filter; - if (options.bbox) { - assert(options.geometryName, 12); // `options.geometryName` must also be provided when `options.bbox` is set - const bbox = bboxFilter( - /** @type {string} */ (options.geometryName), - options.bbox, - options.srsName - ); - if (filter) { - // if bbox and filter are both set, combine the two into a single filter - filter = andFilter(filter, bbox); - } else { - filter = bbox; - } - } node.setAttributeNS( XML_SCHEMA_INSTANCE_URI, 'xsi:schemaLocation', @@ -523,20 +516,67 @@ class WFS extends XMLFeature { 'srsName': options.srsName, 'featureNS': options.featureNS ? options.featureNS : this.featureNS_, 'featurePrefix': options.featurePrefix, - 'geometryName': options.geometryName, - 'filter': filter, 'propertyNames': options.propertyNames ? options.propertyNames : [], }); - - assert(Array.isArray(options.featureTypes), 11); // `options.featureTypes` should be an Array - writeGetFeature( - node, - /** @type {!Array} */ (options.featureTypes), - [context] - ); + assert(Array.isArray(options.featureTypes), 11); // `options.featureTypes` must be an Array + if (typeof options.featureTypes[0] === 'string') { + let filter = options.filter; + if (options.bbox) { + assert(options.geometryName, 12); // `options.geometryName` must also be provided when `options.bbox` is set + filter = this.combineBboxAndFilter( + options.geometryName, + options.bbox, + options.srsName, + filter + ); + } + assign(context, { + 'geometryName': options.geometryName, + 'filter': filter, + }); + writeGetFeature( + node, + /** @type {!Array} */ (options.featureTypes), + [context] + ); + } else { + // Write one query node per element in featuresType. + options.featureTypes.forEach((/** @type {FeatureType} */ featureType) => { + const completeFilter = this.combineBboxAndFilter( + featureType.geometryName, + featureType.bbox, + options.srsName, + options.filter + ); + assign(context, { + 'geometryName': featureType.geometryName, + 'filter': completeFilter, + }); + writeGetFeature(node, [featureType.name], [context]); + }); + } return node; } + /** + * Create a bbox filter and combine it with another optional filter. + * + * @param {!string} geometryName Geometry name to use. + * @param {!import("../extent.js").Extent} extent Extent. + * @param {string=} opt_srsName SRS name. No srsName attribute will be + * set on geometries when this is not provided. + * @param {import("./filter/Filter.js").default=} opt_filter Filter condition. + * @return {import("./filter/Filter.js").default} The filter. + */ + combineBboxAndFilter(geometryName, extent, opt_srsName, opt_filter) { + const bboxFilter = bboxFilterFn(geometryName, extent, opt_srsName); + if (opt_filter) { + // if bbox and filter are both set, combine the two into a single filter + return andFilterFn(opt_filter, bboxFilter); + } + return bboxFilter; + } + /** * Encode format as WFS `Transaction` and return the Node. * diff --git a/test/spec/ol/format/wfs.test.js b/test/spec/ol/format/wfs.test.js index 45be23a0ac..e2a22cf55e 100644 --- a/test/spec/ol/format/wfs.test.js +++ b/test/spec/ol/format/wfs.test.js @@ -316,6 +316,59 @@ describe('ol.format.WFS', function () { expect(serialized.firstElementChild).to.xmleql(parse(text)); }); + it('creates one BBOX filter per feature type', function () { + const textQuery1 = + '' + + ' ' + + ' ' + + ' the_geom_1' + + ' ' + + ' 1 2' + + ' 3 4' + + ' ' + + ' ' + + ' ' + + ''; + const textQuery2 = + '' + + ' ' + + ' ' + + ' the_geom_2' + + ' ' + + ' 5 6' + + ' 7 8' + + ' ' + + ' ' + + ' ' + + ''; + const serialized = new WFS().writeGetFeature({ + srsName: 'urn:ogc:def:crs:EPSG::4326', + featureNS: 'http://www.openplans.org/topp', + featurePrefix: 'topp', + featureTypes: [ + { + name: 'states_1', + geometryName: 'the_geom_1', + bbox: [1, 2, 3, 4], + }, + { + name: 'states_2', + geometryName: 'the_geom_2', + bbox: [5, 6, 7, 8], + }, + ], + }); + expect(serialized.children.length).to.equal(2); + expect(serialized.firstElementChild).to.xmleql(parse(textQuery1)); + expect(serialized.lastElementChild).to.xmleql(parse(textQuery2)); + }); + it('creates a property filter', function () { const text = '