/* Copyright (c) 2015-present The Open Source Geospatial Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* A utility class for converting ExtJS filters to OGC compliant filters
*
* @class GeoExt.util.OGCFilter
*/
Ext.define('GeoExt.util.OGCFilter', {
statics: {
/**
* The WFS 1.0.0 GetFeature XML body template
*/
wfs100GetFeatureXmlTpl:
'<wfs:GetFeature service="WFS" version="1.0.0"' +
' outputFormat="JSON"' +
' xmlns:wfs="http://www.opengis.net/wfs"' +
' xmlns="http://www.opengis.net/ogc"' +
' xmlns:gml="http://www.opengis.net/gml"' +
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
' xsi:schemaLocation="http://www.opengis.net/wfs' +
' http://schemas.opengis.net/wfs/1.0.0/WFS-basic.xsd">' +
'<wfs:Query typeName="{0}">{1}' +
'</wfs:Query>' +
'</wfs:GetFeature>',
/**
* The WFS 1.1.0 GetFeature XML body template
*/
wfs110GetFeatureXmlTpl:
'<wfs:GetFeature service="WFS" version="1.1.0"' +
' outputFormat="JSON"' +
' xmlns:wfs="http://www.opengis.net/wfs"' +
' xmlns="http://www.opengis.net/ogc"' +
' xmlns:gml="http://www.opengis.net/gml"' +
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
' xsi:schemaLocation="http://www.opengis.net/wfs' +
' http://schemas.opengis.net/wfs/1.0.0/WFS-basic.xsd">' +
'<wfs:Query typeName="{0}">{1}' +
'</wfs:Query>' +
'</wfs:GetFeature>',
/**
* The WFS 2.0.0 GetFeature XML body template
*/
wfs200GetFeatureXmlTpl:
'<wfs:GetFeature service="WFS" version="2.0.0" ' +
'xmlns:wfs="http://www.opengis.net/wfs/2.0" ' +
'xmlns:fes="http://www.opengis.net/fes/2.0" ' +
'xmlns:gml="http://www.opengis.net/gml/3.2" ' +
'xmlns:sf="http://www.openplans.org/spearfish" ' +
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
'xsi:schemaLocation="http://www.opengis.net/wfs/2.0 ' +
'http://schemas.opengis.net/wfs/2.0/wfs.xsd ' +
'http://www.opengis.net/gml/3.2 ' +
'http://schemas.opengis.net/gml/3.2.1/gml.xsd">' +
'<wfs:Query typeName="{0}">{1}' +
'</wfs:Query>' +
'</wfs:GetFeature>',
/**
* The template for spatial filters used in WFS 1.x.0 queries
*/
spatialFilterWfs1xXmlTpl:
'<{0}>' + '<PropertyName>{1}</PropertyName>' + '{2}' + '</{0}>',
/**
* The template for spatial filters used in WFS 2.0.0 queries
*/
spatialFilterWfs2xXmlTpl:
'<fes:{0}>' +
'<fes:ValueReference>{1}</fes:ValueReference>' +
'{2}' +
'</fes:{0}>',
/**
* The template for spatial bbox filters used in WFS 1.x.0 queries
*/
spatialFilterBBoxTpl:
'<BBOX>' +
' <PropertyName>{0}</PropertyName>' +
' <gml:Envelope' +
' xmlns:gml="http://www.opengis.net/gml" srsName="{1}">' +
' <gml:lowerCorner>{2} {3}</gml:lowerCorner>' +
' <gml:upperCorner>{4} {5}</gml:upperCorner>' +
' </gml:Envelope>' +
'</BBOX>',
/**
* Template string for GML 3.2.1 polygon
*/
gml32PolygonTpl:
'<gml:Polygon gml:id="P1" ' +
'srsName="urn:ogc:def:crs:{0}" srsDimension="2">' +
'<gml:exterior>' +
'<gml:LinearRing>' +
'<gml:posList>{1}</gml:posList>' +
'</gml:LinearRing>' +
'</gml:exterior>' +
'</gml:Polygon>',
/**
* Template string for GML 3.2.1 linestring
*/
gml32LineStringTpl:
'<gml:LineString gml:id="L1" ' +
'srsName="urn:ogc:def:crs:{0}" srsDimension="2">' +
'<gml:posList>{1}</gml:posList>' +
'</gml:LineString>',
/**
* Template string for GML 3.2.1 point
*/
gml32PointTpl:
'<gml:Point gml:id="Pt1" ' +
'srsName="urn:ogc:def:crs:{0}" srsDimension="2">' +
'<gml:pos>{1}</gml:pos>' +
'</gml:Point>',
/**
* The start element for a FE filter instance in version 2.0
* as string value
*/
filter20StartElementStr:
'<fes:Filter ' +
'xsi:schemaLocation="http://www.opengis.net/fes/2.0 ' +
'http://schemas.opengis.net/filter/2.0/filterAll.xsd ' +
'http://www.opengis.net/gml/3.2 ' +
'http://schemas.opengis.net/gml/3.2.1/gml.xsd" ' +
'xmlns:fes="http://www.opengis.net/fes/2.0" ' +
'xmlns:gml="http://www.opengis.net/gml/3.2" ' +
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">',
/**
* The list of supported topological and spatial filter operators
*/
topologicalOrSpatialFilterOperators: [
'intersect',
'within',
'contains',
'equals',
'disjoint',
'crosses',
'touches',
'overlaps',
'bbox',
],
/**
* Given an array of ExtJS grid-filters, this method will return an OGC
* compliant filter which can be used for WMS requests
* @param {Array<Ext.util.Filter>} filters array containing all
* `Ext.util.Filter` that should be converted
* @param {string} combinator The combinator used for combining multiple
* filters. Can be 'and' or 'or'
* @return {string} The OGC Filter XML
*/
getOgcWmsFilterFromExtJsFilter: function (filters, combinator) {
return GeoExt.util.OGCFilter.getOgcFilterFromExtJsFilter(
filters,
'wms',
combinator,
);
},
/**
* Given an array of ExtJS grid-filters, this method will return an OGC
* compliant filter which can be used for WFS requests
* @param {Array<Ext.util.Filter>} filters array containing all
* `Ext.util.Filter` that should be converted
* @param {string} combinator The combinator used for combining multiple
* filters. Can be 'and' or 'or'
* @param {string} wfsVersion The WFS version to use, either `1.0.0`,
* `1.1.0` or `2.0.0`
* @return {string} The OGC Filter XML
*/
getOgcWfsFilterFromExtJsFilter: function (filters, combinator, wfsVersion) {
return GeoExt.util.OGCFilter.getOgcFilterFromExtJsFilter(
filters,
'wfs',
combinator,
wfsVersion,
);
},
/**
* Given an ExtJS grid-filter, this method will return an OGC compliant
* filter which can be used for WMS or WFS queries
* @param {Array<Ext.util.Filter>} filters array containing all
* `Ext.util.Filter` that should be converted
* @param {string} type The OGC type we will be using, can be
* `wms` or `wfs`
* @param {string} combinator The combinator used for combining multiple
* filters. Can be 'and' or 'or'
* @param {string} wfsVersion The WFS version to use, either `1.0.0`,
* `1.1.0` or `2.0.0`
* @return {string} The OGC Filter as XML String
*/
getOgcFilterFromExtJsFilter: function (
filters,
type,
combinator,
wfsVersion,
) {
if (!Ext.isDefined(filters) || !Ext.isArray(filters)) {
Ext.Logger.error(
'Invalid filter argument given to ' +
'GeoExt.util.OGCFilter. You need to pass an array of ' +
'"Ext.util.Filter"',
);
return;
}
if (Ext.isEmpty(filters)) {
return null;
}
let omitNamespaces = false;
// filters for WMS layers need to omit the namespaces
if (!Ext.isEmpty(type) && type.toLowerCase() === 'wms') {
omitNamespaces = true;
}
const ogcFilters = [];
const ogcUtil = GeoExt.util.OGCFilter;
let filterBody;
Ext.each(filters, function (filter) {
filterBody = ogcUtil.getOgcFilterBodyFromExtJsFilterObject(
filter,
wfsVersion,
);
if (filterBody) {
ogcFilters.push(filterBody);
}
});
return ogcUtil.combineFilters(
ogcFilters,
combinator,
omitNamespaces,
wfsVersion,
);
},
/**
* Converts given ExtJS grid-filter to an OGC compliant filter
* body content.
* @param {Ext.util.Filter} filter Instance of
* `Ext.util.Filter` which should be converted to OGC filter
* @param {string} wfsVersion The WFS version to use, either `1.0.0`,
* `1.1.0` or `2.0.0`
* @return {string} The OGC Filter body as XML String
*/
getOgcFilterBodyFromExtJsFilterObject: function (filter, wfsVersion) {
if (!Ext.isDefined(filter)) {
Ext.Logger.error(
'Invalid filter argument given to ' +
'GeoExt.util.OGCFilter. You need to pass an instance of ' +
'"Ext.util.Filter"',
);
return;
}
const property = filter.getProperty();
const operator = filter.getOperator();
let value = filter.getValue();
let srsName;
if (filter.type === 'spatial') {
srsName = filter.srsName;
}
if (
Ext.isEmpty(property) ||
Ext.isEmpty(operator) ||
Ext.isEmpty(value)
) {
Ext.Logger.warn(
'Skipping a filter as some values ' + 'seem to be undefined',
);
return;
}
if (filter.isDateValue) {
if (filter.getDateFormat) {
value = Ext.Date.format(filter.getValue(), filter.getDateFormat());
} else {
value = Ext.Date.format(filter.getValue(), 'Y-m-d');
}
}
return GeoExt.util.OGCFilter.getOgcFilter(
property,
operator,
value,
wfsVersion,
srsName,
);
},
/**
* Returns a GetFeature XML body containing the filters
* which can be used to directly request the features
* @param {Array<Ext.util.Filter>} filters array containing all
* `Ext.util.Filter` that should be converted
* @param {string} combinator The combinator used for combining multiple
* filters. Can be 'and' or 'or'
* @param {string} wfsVersion The WFS version to use, either `1.0.0`,
* `1.1.0` or `2.0.0`
* @param {string} typeName The featuretype name to be used
* @return {string} the GetFeature XML body as string
*/
buildWfsGetFeatureWithFilter: function (
filters,
combinator,
wfsVersion,
typeName,
) {
const filter = GeoExt.util.OGCFilter.getOgcWfsFilterFromExtJsFilter(
filters,
combinator,
wfsVersion,
);
let tpl = GeoExt.util.OGCFilter.wfs100GetFeatureXmlTpl;
if (wfsVersion && wfsVersion === '1.1.0') {
tpl = GeoExt.util.OGCFilter.wfs110GetFeatureXmlTpl;
} else if (wfsVersion && wfsVersion === '2.0.0') {
tpl = GeoExt.util.OGCFilter.wfs200GetFeatureXmlTpl;
}
return Ext.String.format(tpl, typeName, filter);
},
/**
* Returns an OGC filter for the given parameters.
* @param {string} property The property to filter on
* @param {string} operator The operator to use
* @param {*} value The value for the filter
* @param {string} wfsVersion The WFS version to use, either `1.0.0`,
* `1.1.0` or `2.0.0`
* @param {string} srsName The code for the projection
* @return {string} The OGC filter.
*/
getOgcFilter: function (property, operator, value, wfsVersion, srsName) {
if (
Ext.isEmpty(property) ||
Ext.isEmpty(operator) ||
Ext.isEmpty(value)
) {
Ext.Logger.error(
'Invalid argument given to method ' +
'`getOgcFilter`. You need to supply property, ' +
'operator and value.',
);
return;
}
let ogcFilterType;
let closingTag;
let propName = 'PropertyName';
const isWfs20 = !Ext.isEmpty(wfsVersion) && wfsVersion === '2.0.0';
if (isWfs20) {
propName = 'fes:ValueReference';
}
// always replace surrounding quotes
if (!(value instanceof ol.geom.Geometry)) {
value = value.toString().replace(/(^['])/g, '');
value = value.toString().replace(/([']$)/g, '');
}
const wfsPrefix = isWfs20 ? 'fes:' : '';
switch (operator) {
case '==':
case '=':
case 'eq':
ogcFilterType = wfsPrefix + 'PropertyIsEqualTo';
break;
case '!==':
case '!=':
case 'ne':
ogcFilterType = wfsPrefix + 'PropertyIsNotEqualTo';
break;
case 'lt':
case '<':
ogcFilterType = wfsPrefix + 'PropertyIsLessThan';
break;
case 'lte':
case '<=':
ogcFilterType = wfsPrefix + 'PropertyIsLessThanOrEqualTo';
break;
case 'gt':
case '>':
ogcFilterType = wfsPrefix + 'PropertyIsGreaterThan';
break;
case 'gte':
case '>=':
ogcFilterType = wfsPrefix + 'PropertyIsGreaterThanOrEqualTo';
break;
case 'like':
value = '*' + value + '*';
const likeFilterTpl =
'<{0}PropertyIsLike wildCard="*" singleChar="."' +
' escape="!" matchCase="false">' +
'<' +
propName +
'>' +
property +
'</' +
propName +
'>' +
'<{0}Literal>' +
value +
'</{0}Literal>' +
'</{0}PropertyIsLike>';
return Ext.String.format(likeFilterTpl, wfsPrefix);
case 'in':
ogcFilterType = wfsPrefix + 'Or';
let values = value;
if (!Ext.isArray(value)) {
// cleanup brackets and quotes
value = value.replace(/([()'])/g, '');
values = value.split(',');
}
let filters = '';
Ext.each(values || value, function (val) {
filters +=
'<' +
wfsPrefix +
'PropertyIsEqualTo>' +
'<' +
propName +
'>' +
property +
'</' +
propName +
'>' +
'<' +
wfsPrefix +
'Literal>' +
val +
'</' +
wfsPrefix +
'Literal>' +
'</' +
wfsPrefix +
'PropertyIsEqualTo>';
});
ogcFilterType = '<' + ogcFilterType + '>';
let inFilter;
closingTag = Ext.String.insert(ogcFilterType, '/', 1);
// only use an Or filter when there are multiple values
if (values.length > 1) {
inFilter = ogcFilterType + filters + closingTag;
} else {
inFilter = filters;
}
return inFilter;
case 'intersect':
case 'within':
case 'contains':
case 'equals':
case 'disjoint':
case 'crosses':
case 'touches':
case 'overlaps':
switch (operator) {
case 'equals':
ogcFilterType = 'Equals';
break;
case 'contains':
ogcFilterType = 'Contains';
break;
case 'within':
ogcFilterType = 'Within';
break;
case 'disjoint':
ogcFilterType = 'Disjoint';
break;
case 'touches':
ogcFilterType = 'Touches';
break;
case 'crosses':
ogcFilterType = 'Crosses';
break;
case 'overlaps':
ogcFilterType = 'Overlaps';
break;
case 'intersect':
ogcFilterType = 'Intersects';
break;
default:
Ext.Logger.warn(
'Method `getOgcFilter` could not ' +
'handle the given topological operator: ' +
operator,
);
return;
}
const gmlElement = GeoExt.util.OGCFilter.getGmlElementForGeometry(
value,
srsName,
wfsVersion,
);
const spatialTpl =
wfsVersion !== '2.0.0'
? GeoExt.util.OGCFilter.spatialFilterWfs1xXmlTpl
: GeoExt.util.OGCFilter.spatialFilterWfs2xXmlTpl;
return Ext.String.format(
spatialTpl,
ogcFilterType,
property,
gmlElement,
);
case 'bbox':
value = value.getExtent();
const llx = value[0];
const lly = value[1];
const urx = value[2];
const ury = value[3];
return Ext.String.format(
GeoExt.util.OGCFilter.spatialFilterBBoxTpl,
property,
srsName,
llx,
lly,
urx,
ury,
);
default:
Ext.Logger.warn(
'Method `getOgcFilter` could not ' +
'handle the given operator: ' +
operator,
);
return;
}
ogcFilterType = '<' + ogcFilterType + '>';
closingTag = Ext.String.insert(ogcFilterType, '/', 1);
const literalStr = isWfs20
? '<fes:Literal>{2}</fes:Literal>'
: '<Literal>{2}</Literal>';
const tpl =
'' +
'{0}' +
'<' +
propName +
'>{1}</' +
propName +
'>' +
literalStr +
'{3}';
const filter = Ext.String.format(
tpl,
ogcFilterType,
property,
value,
closingTag,
);
return filter;
},
/**
* Returns a serialized geometry in GML3 format
* @param {ol.geometry.Geometry} geometry The geometry to serialize
* @param {string} srsName The epsg code to use to serialization
* @param {string} wfsVersion The WFS version to use (WFS 2.0.0
* requires gml prefix for geometries)
* @return {string} The serialized geometry in GML3 format
*/
getGmlElementForGeometry: function (geometry, srsName, wfsVersion) {
if (wfsVersion === '2.0.0') {
// supported geometries: Point, LineString and Polygon
// in case of multigeometries, the first one is used.
const geometryType = geometry.getType();
const staticMe = GeoExt.util.OGCFilter;
const isMulti = geometryType.indexOf('Multi') > -1;
switch (geometryType) {
case 'Polygon':
case 'MultiPolygon':
let coordsPoly = geometry.getCoordinates()[0];
if (isMulti) {
coordsPoly = coordsPoly[0];
}
return Ext.String.format(
staticMe.gml32PolygonTpl,
srsName,
staticMe.flattenCoordinates(coordsPoly),
);
case 'LineString':
case 'MultiLineString':
let coordsLine = geometry.getCoordinates();
if (isMulti) {
coordsLine = coordsLine[0];
}
return Ext.String.format(
staticMe.gml32LineStringTpl,
srsName,
staticMe.flattenCoordinates(coordsLine),
);
case 'Point':
case 'MultiPoint':
let coordsPt = geometry.getCoordinates();
if (isMulti) {
coordsPt = coordsPt[0];
}
return Ext.String.format(
staticMe.gml32PointTpl,
srsName,
staticMe.flattenCoordinates(coordsPt),
);
default:
return '';
}
} else {
const format = new ol.format.GML3({
srsName: srsName,
});
const geometryNode = format.writeGeometryNode(geometry, {
dataProjection: srsName,
});
if (!geometryNode) {
Ext.Logger.warn('Could not serialize geometry');
return null;
}
const childNodes = geometryNode.children || geometryNode.childNodes;
const serializer = new XMLSerializer();
const geomNode = childNodes[0];
const serializedValue = serializer.serializeToString(geomNode);
return serializedValue;
}
},
/**
* Reduce an ol.Coordinate array to a string of whitespace
* separated coordinate values
* @param {Array<ol.Coordinate>} coordArray An array of
* coordinates
* @return {string} Concatenated array of coordinates
*/
flattenCoordinates: function (coordArray) {
return Ext.Array.map(coordArray, function (cp) {
return cp.join(' ');
}).join(' ');
},
/**
* Combines the passed filter bodies with an `<And>` or `<Or>` and
* returns them. E.g. created with
* GeoExt.util.OGCFilter.getOgcFilterBodyFromExtJsFilterObject
*
* @param {Array} filterBodies The filter bodies to join.
* @param {string} combinator The combinator to use, should be
* either `And` (the default) or `Or`.
* @param {string} wfsVersion The WFS version to use, either `1.0.0`,
* `1.1.0` or `2.0.0`
* @return {string} And/Or combined OGC filter bodies.
*/
combineFilterBodies: function (filterBodies, combinator, wfsVersion) {
if (
!Ext.isDefined(filterBodies) ||
!Ext.isArray(filterBodies) ||
filterBodies.length === 0
) {
Ext.Logger.error(
'Invalid "filterBodies" argument given to ' +
'GeoExt.util.OGCFilter. You need to pass an array of ' +
'OGC filter bodies as XML string',
);
return;
}
const combineWith = combinator || 'And';
const isWfs20 = !Ext.isEmpty(wfsVersion) && wfsVersion === '2.0.0';
const wfsPrefix = isWfs20 ? 'fes:' : '';
let ogcFilterType = wfsPrefix + combineWith;
const openingTag = (ogcFilterType = '<' + ogcFilterType + '>');
const closingTag = Ext.String.insert(openingTag, '/', 1);
let combinedFilterBodies = '';
// only use an And/Or filter when there are multiple filter bodies
if (filterBodies.length > 1) {
Ext.each(filterBodies, function (filterBody) {
combinedFilterBodies += filterBody;
});
combinedFilterBodies = openingTag + combinedFilterBodies + closingTag;
} else {
combinedFilterBodies = filterBodies[0];
}
return combinedFilterBodies;
},
/**
* Combines the passed filters with an `<And>` or `<Or>` and
* returns them.
*
* @param {Array} filters The filters to join.
* @param {string} combinator The combinator to use, should be
* either `And` (the default) or `Or`.
* @param {boolean} omitNamespaces Indicates if namespaces
* should be omitted in filters, which is useful for WMS
* @param {string} wfsVersion The WFS version to use, either `1.0.0`,
* `1.1.0` or `2.0.0`
* @return {string} An combined OGC filter with the passed filters.
*/
combineFilters: function (filters, combinator, omitNamespaces, wfsVersion) {
const staticMe = GeoExt.util.OGCFilter;
const defaultCombineWith = 'And';
const combineWith = combinator || defaultCombineWith;
const numFilters = filters.length;
const parts = [];
const ns = omitNamespaces ? '' : 'ogc';
let omitNamespaceFromWfsVersion = !wfsVersion || wfsVersion === '1.0.0';
if (
!Ext.isEmpty(wfsVersion) &&
wfsVersion === '2.0.0' &&
!omitNamespaces
) {
parts.push(staticMe.filter20StartElementStr);
} else {
parts.push(
'<Filter' +
(omitNamespaces
? ''
: ' xmlns="http://www.opengis.net/' +
ns +
'"' +
' xmlns:gml="http://www.opengis.net/gml"') +
'>',
);
omitNamespaceFromWfsVersion = true;
}
parts.push();
if (numFilters > 1) {
parts.push(
'<' +
(omitNamespaces || omitNamespaceFromWfsVersion ? '' : 'fes:') +
combineWith +
'>',
);
}
Ext.each(filters, function (filter) {
parts.push(filter);
});
if (numFilters > 1) {
parts.push(
'</' +
(omitNamespaces || omitNamespaceFromWfsVersion ? '' : 'fes:') +
combineWith +
'>',
);
}
parts.push(
'</' +
(omitNamespaces || omitNamespaceFromWfsVersion ? '' : 'fes:') +
'Filter>',
);
return parts.join('');
},
/**
* Create an instance of {Ext.util.Filter} that contains the required
* information on spatial filter, e.g. operator and geometry
*
* @param {string} operator The spatial / toplogical operator
* @param {string} typeName The name of geometry field
* @param {ol.geom.Geometry} value The geometry to use for filtering
* @param {string} srsName The EPSG code of the geometry
*
* @return {Ext.util.Filter} A 'spatial' {Ext.util.Filter}
*/
createSpatialFilter: function (operator, typeName, value, srsName) {
if (
!Ext.Array.contains(
GeoExt.util.OGCFilter.topologicalOrSpatialFilterOperators,
operator,
)
) {
return null;
}
// construct an instance of Filter
return new Ext.util.Filter({
type: 'spatial',
srsName: srsName,
operator: operator,
property: typeName,
value: value,
});
},
},
});