UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

309 lines (308 loc) 16.2 kB
// 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;