@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
309 lines (308 loc) • 16.2 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { and, or, between, during, equalTo, greaterThan, greaterThanOrEqualTo, isNull, lessThan, lessThanOrEqualTo, like, not, notEqualTo, contains, intersects, within } from 'ol/format/filter.js';
import { isDate, isString, isStringNumeric, typeGroupFromAttributeType } from '../../models/xmlTypes.js';
import { filterOperatorValues, filterOperators, operatorsByAttributeTypeGroup, isListFilterOperator } from '../../models/filter.js';
import { BaseFilterCondition } from '../filter/basefilter.js';
import Geometry from 'ol/geom/Geometry.js';
import DrawingFeature from '../drawing/drawingFeature.js';
import GML3 from 'ol/format/GML3.js';
/**
* Define the supported operators for WfsFilter: WFS supports all available operators
*/
const wfsOperatorsStrList = [...filterOperatorValues];
export function isWfsOperator(value) {
return wfsOperatorsStrList.includes(value);
}
const BeginningOfTime = '0001-01-01';
const EndOfTime = '9999-12-31';
class WfsFilterCondition extends BaseFilterCondition {
operator;
/**
* Is this a condition that can be displayed in a simple layer tree filter?
*/
get isSimpleCondition() {
return this.propertyTypeGroup !== 'geometry' && !isListFilterOperator(this.operator);
}
/**
* If the filter value is a geometry, retrieves the OpenLayers Geometry object, otherwise an empty geometry.
*/
get olGeometry() {
if (this.propertyTypeGroup === 'geometry') {
// Simplify circle geometries to polygons with no more than 100 vertices
return DrawingFeature.olFeatureFromGeoJson(JSON.parse(this.value), 100).getGeometry();
}
return new Geometry();
}
/**
* If the filter value is a geometry, retrieves the gml node, otherwise an empty string.
*/
get gmlGeometry() {
const format = new GML3();
const gmlNode = format.writeGeometryNode(this.olGeometry, { decimals: 2 });
return new XMLSerializer().serializeToString(gmlNode.childNodes[0]);
}
/**
* If the filter value is a list, returns the parsed list of values, otherwise an empty array.
*/
get valueList() {
if (isListFilterOperator(this.operator)) {
return (JSON.parse(this.value).items ?? []);
}
return [];
}
constructor(property, operator, value, value2, propertyType) {
if (!isWfsOperator(operator)) {
throw new Error(`Invalid WFS operator: ${operator}`);
}
let propertyTypeGroup = undefined;
if (propertyType) {
propertyTypeGroup = typeGroupFromAttributeType(propertyType);
}
super(property, operator, value, value2, propertyType, propertyTypeGroup);
this.operator = operator;
}
static get supportedOperators() {
return filterOperators.filter((op) => isWfsOperator(op.operator));
}
/**
* Returns the supported operators for the given attribute type.
*/
static supportedOperatorsByAttributeType(attributeType) {
try {
// Check to which type group (string, numbers, date) the attribute type belongs
const operatorsSupportingTypeGroup = operatorsByAttributeTypeGroup[typeGroupFromAttributeType(attributeType)];
// Filter list of operators
return WfsFilterCondition.supportedOperators.filter((op) => operatorsSupportingTypeGroup.includes(op.operator));
}
catch (err) {
console.error('Error while getting attribute type group', err);
return [];
}
}
toOpenLayersFilter() {
if (!this.propertyType) {
throw new Error('The type of the property should never be null !');
}
const wildcard = '*';
const singleChar = '.';
const escapeChar = '!';
const wildcardPattern = `${wildcard}${this.value}${wildcard}`;
const isNumericStringPropertyFilter = isString(this.propertyType) && isStringNumeric(this.value);
switch (this.operator) {
case 'eq':
if (isNumericStringPropertyFilter) {
// See https://mapserver-users.osgeo.narkive.com/P0EVA6Qr/wfs-filter-creates-a-query-using-a-number-instead-of-text
return like(this.property, wildcardPattern);
}
return equalTo(this.property, this.value);
case 'neq':
if (isNumericStringPropertyFilter) {
// See https://mapserver-users.osgeo.narkive.com/P0EVA6Qr/wfs-filter-creates-a-query-using-a-number-instead-of-text
return not(like(this.property, wildcardPattern));
}
else if (isDate(this.propertyType)) {
// mapserver does not support notEqualTo for date values, see notes in https://www.mapserver.org/ogc/filter_encoding.html#tests
return not(equalTo(this.property, this.value));
}
return notEqualTo(this.property, this.value);
case 'gt':
return greaterThan(this.property, Number(this.value));
case 'gte':
return greaterThanOrEqualTo(this.property, Number(this.value));
case 'lt':
return lessThan(this.property, Number(this.value));
case 'lte':
return lessThanOrEqualTo(this.property, Number(this.value));
case 'like':
return like(this.property, wildcardPattern, wildcard, singleChar, escapeChar, false);
case 'nlike':
return not(like(this.property, wildcardPattern, wildcard, singleChar, escapeChar, false));
case 'in':
if (this.valueList.length > 1) {
return or(...this.valueList.map((value) => equalTo(this.property, value)));
}
return equalTo(this.property, this.valueList[0]);
case 'nin':
if (this.valueList.length > 1) {
return not(or(...this.valueList.map((value) => equalTo(this.property, value))));
}
return notEqualTo(this.property, this.valueList[0]);
case 'before':
return during(this.property, BeginningOfTime, this.value);
case 'after':
return during(this.property, this.value, EndOfTime);
case 'between':
if (isDate(this.propertyType)) {
return during(this.property, this.value, this.value2);
}
return between(this.property, Number(this.value), Number(this.value2));
case 'nbetween':
if (isDate(this.propertyType)) {
return and(during(this.property, BeginningOfTime, this.value), during(this.property, this.value2, EndOfTime));
}
return not(between(this.property, Number(this.value), Number(this.value2)));
case 'nul':
return isNull(this.property);
case 'nnul':
if (isDate(this.propertyType)) {
return notEqualTo(this.property, '');
}
return not(isNull(this.property));
case 'contains':
return contains(this.property, this.olGeometry);
case 'intersects':
return intersects(this.property, this.olGeometry);
case 'within':
return within(this.property, this.olGeometry);
default:
throw new Error('Unknown filter operator');
}
}
/**
*
* @returns a simple filter string that can be used in a WMS GetMap request, does not provide any XML namespace (xmlns attributes)
*/
toWmsGetMapFilter() {
if (!this.propertyType) {
throw new Error('The type of the property should never be null !');
}
const wildCard = WfsFilterCondition.simpleLikeFilterGenerateWildCard(this.value);
switch (this.operator) {
case 'eq':
if (this.propertyType === 'string' && isStringNumeric(this.value)) {
// See https://mapserver-users.osgeo.narkive.com/P0EVA6Qr/wfs-filter-creates-a-query-using-a-number-instead-of-text
return WfsFilterCondition.simpleLikeFilter(this.property, this.value);
}
else {
return WfsFilterCondition.simpleEqFilter(this.property, this.value);
}
case 'neq':
if (this.propertyType === 'string' && isStringNumeric(this.value)) {
// See https://mapserver-users.osgeo.narkive.com/P0EVA6Qr/wfs-filter-creates-a-query-using-a-number-instead-of-text
return WfsFilterCondition.simpleNlikeFilter(this.property, this.value);
}
else {
return WfsFilterCondition.simpleNeqFilter(this.property, this.value);
}
case 'gt':
return WfsFilterCondition.simpleGtFilter(this.property, this.value);
case 'gte':
return WfsFilterCondition.simpleGteFilter(this.property, this.value);
case 'lt':
return WfsFilterCondition.simpleLtFilter(this.property, this.value);
case 'lte':
return WfsFilterCondition.simpleLteFilter(this.property, this.value);
case 'like':
return WfsFilterCondition.simpleLikeFilter(this.property, wildCard + this.value + wildCard, wildCard);
case 'nlike':
return WfsFilterCondition.simpleNlikeFilter(this.property, wildCard + this.value + wildCard, wildCard);
case 'in':
return WfsFilterCondition.simpleInFilter(this.property, this.valueList);
case 'nin':
return WfsFilterCondition.simpleNinFilter(this.property, this.valueList);
case 'before':
return WfsFilterCondition.simpleLteFilter(this.property, this.value);
case 'after':
return WfsFilterCondition.simpleGteFilter(this.property, this.value);
case 'between':
return WfsFilterCondition.simpleBetweenFilter(this.property, this.value, this.value2);
case 'nbetween':
return WfsFilterCondition.simpleNbetweenFilter(this.property, this.value, this.value2);
case 'nul':
return WfsFilterCondition.simpleEqFilter(this.property, '');
case 'nnul':
return WfsFilterCondition.simpleNeqFilter(this.property, '');
case 'contains':
return WfsFilterCondition.simpleContainsFilter(this.property, this.gmlGeometry);
case 'intersects':
return WfsFilterCondition.simpleIntersectsFilter(this.property, this.gmlGeometry);
case 'within':
return WfsFilterCondition.simpleWithinFilter(this.property, this.gmlGeometry);
default:
throw new Error('Unknown filter operator');
}
}
static simpleEqFilter(name, value) {
return `<Filter><PropertyIsEqualTo><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsEqualTo></Filter>`;
}
static simpleNeqFilter(name, value) {
return `<Filter><Not><PropertyIsEqualTo><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsEqualTo></Not></Filter>`;
}
static simpleLtFilter(name, value) {
return `<Filter><PropertyIsLessThan><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsLessThan></Filter>`;
}
static simpleLteFilter(name, value) {
return `<Filter><PropertyIsLessThanOrEqualTo><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsLessThanOrEqualTo></Filter>`;
}
static simpleGtFilter(name, value) {
return `<Filter><PropertyIsGreaterThan><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsGreaterThan></Filter>`;
}
static simpleGteFilter(name, value) {
return `<Filter><PropertyIsGreaterThanOrEqualTo><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsGreaterThanOrEqualTo></Filter>`;
}
static simpleBetweenFilter(name, value, value2) {
return `<Filter><And><PropertyIsGreaterThanOrEqualTo><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsGreaterThanOrEqualTo><PropertyIsLessThanOrEqualTo><PropertyName>${name}</PropertyName><Literal>${value2}</Literal></PropertyIsLessThanOrEqualTo></And></Filter>`;
}
static simpleNbetweenFilter(name, value, value2) {
return `<Filter><Not><And><PropertyIsGreaterThanOrEqualTo><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsGreaterThanOrEqualTo><PropertyIsLessThanOrEqualTo><PropertyName>${name}</PropertyName><Literal>${value2}</Literal></PropertyIsLessThanOrEqualTo></And></Not></Filter>`;
}
static simpleInnerLikeFilter(name, value, wildCard) {
if (!wildCard) {
wildCard = WfsFilterCondition.simpleLikeFilterGenerateWildCard(value);
}
return `<PropertyIsLike wildCard="${wildCard}" singleChar="." escapeChar="!" matchCase="false"><PropertyName>${name}</PropertyName><Literal>${value}</Literal></PropertyIsLike>`;
}
static simpleLikeFilter(name, value, wildCard) {
return '<Filter>' + this.simpleInnerLikeFilter(name, value, wildCard) + '</Filter>';
}
static simpleNlikeFilter(name, value, wildCard) {
return '<Filter><Not>' + this.simpleInnerLikeFilter(name, value, wildCard) + '</Not></Filter>';
}
static simpleInFilter(name, valueList) {
const innerFilters = valueList.map((value) => WfsFilterCondition.simpleEqFilter(name, value));
return WfsFilterCondition.buildNestedExpression(innerFilters, 'or');
}
static simpleNinFilter(name, valueList) {
const nestedFilter = WfsFilterCondition.simpleInFilter(name, valueList);
return nestedFilter.replace('<Filter>', '<Filter><Not>').replace('</Filter>', '</Not></Filter>');
}
static simpleContainsFilter(name, value) {
return `<Filter><Contains><PropertyName>${name}</PropertyName>${value}</Contains></Filter>`;
}
static simpleIntersectsFilter(name, value) {
return `<Filter><Intersects><PropertyName>${name}</PropertyName>${value}</Intersects></Filter>`;
}
static simpleWithinFilter(name, value) {
return `<Filter><Within><PropertyName>${name}</PropertyName>${value}</Within></Filter>`;
}
// Here is a quickfix for map.bs.ch not accepting * in url params
static simpleLikeFilterGenerateWildCard(value) {
return 'WABCDEFGIJKLMNOPQRSTUVXYZ'.split('').find((c) => !value.includes(c)) ?? '*';
}
static buildNestedExpression(filterStrings, logicalOperator) {
const tag = logicalOperator.charAt(0).toUpperCase() + logicalOperator.slice(1).toLowerCase();
filterStrings = filterStrings
.filter((s) => s !== '')
.map((s) => s.replace('<Filter>', '').replace('</Filter>', ''));
if (filterStrings.length === 0)
return '';
if (filterStrings.length === 1 && logicalOperator === 'not')
return `<Filter><${tag}>${filterStrings[0]}</${tag}></Filter>`;
if (filterStrings.length === 1)
return `<Filter>${filterStrings[0]}</Filter>`;
if (filterStrings.length === 2)
return `<Filter><${tag}>${filterStrings[0]}${filterStrings[1]}</${tag}></Filter>`;
// Split into pairs and recursively build nested structure
const pairs = [];
for (let i = 0; i < filterStrings.length; i += 2) {
if (i + 1 < filterStrings.length) {
pairs.push(`<${tag}>${filterStrings[i]}${filterStrings[i + 1]}</${tag}>`);
}
else {
pairs.push(filterStrings[i]);
}
}
return WfsFilterCondition.buildNestedExpression(pairs, logicalOperator);
}
}
export default WfsFilterCondition;