From 72c794e2524f8ad0f8068d9f0d24a11f853980b1 Mon Sep 17 00:00:00 2001 From: FilipLeitner <44566616+FilipLeitner@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:38:29 +0100 Subject: [PATCH] feat: property to property comparison (#975) * refactor: make sure function filters are parsed properly even as objects * fix: allow property to property comparison filter * fix: properly handle single arg operators in childrenToArgs * test: add test coverage for property to property comaprison filters --- .../function_filter_property_to_property.sld | 61 ++++++++++++++++++ .../function_filter_property_to_property.sld | 62 ++++++++++++++++++ .../function_filter_property_to_property.ts | 64 +++++++++++++++++++ src/SldStyleParser.ts | 25 ++++++-- src/SldStyleParser.v1.0.spec.ts | 24 +++++++ src/SldStyleParser.v1.1.spec.ts | 18 ++++++ 6 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 data/slds/1.0/function_filter_property_to_property.sld create mode 100644 data/slds/1.1/function_filter_property_to_property.sld create mode 100644 data/styles/function_filter_property_to_property.ts diff --git a/data/slds/1.0/function_filter_property_to_property.sld b/data/slds/1.0/function_filter_property_to_property.sld new file mode 100644 index 00000000..09cf1ef8 --- /dev/null +++ b/data/slds/1.0/function_filter_property_to_property.sld @@ -0,0 +1,61 @@ + + + + Function Property to Property + + Function Property to Property + + + Property Comparison Rule + + + + posledni_hodnota + posledni_hodnota_sekundarni + + + value1 + value2 + + + count1 + count2 + + + threshold1 + threshold2 + + + posledni_hodnota + spa1h + + + status + NULL + + + + + + + square + + #FF0000 + + + #000000 + 1 + + + 5 + + + + + + + \ No newline at end of file diff --git a/data/slds/1.1/function_filter_property_to_property.sld b/data/slds/1.1/function_filter_property_to_property.sld new file mode 100644 index 00000000..9b2cffc4 --- /dev/null +++ b/data/slds/1.1/function_filter_property_to_property.sld @@ -0,0 +1,62 @@ + + + + Function Property to Property + + Function Property to Property + + + Property Comparison Rule + + + + posledni_hodnota + posledni_hodnota_sekundarni + + + value1 + value2 + + + count1 + count2 + + + threshold1 + threshold2 + + + posledni_hodnota + spa1h + + + status + NULL + + + + + + + square + + #FF0000 + + + #000000 + 1 + + + 5 + + + + + + + \ No newline at end of file diff --git a/data/styles/function_filter_property_to_property.ts b/data/styles/function_filter_property_to_property.ts new file mode 100644 index 00000000..d4361680 --- /dev/null +++ b/data/styles/function_filter_property_to_property.ts @@ -0,0 +1,64 @@ +import { Style } from 'geostyler-style'; + +const functionFilterPropertyToProperty: Style = { + name: 'Function Property to Property', + rules: [{ + name: 'Property Comparison Rule', + filter: ['&&', + // Basic property to property comparison + ['==', { + name: 'property', + args: ['posledni_hodnota'] + }, { + name: 'property', + args: ['posledni_hodnota_sekundarni'] + }], + // Different comparison operators + ['>', { + name: 'property', + args: ['value1'] + }, { + name: 'property', + args: ['value2'] + }], + ['<', { + name: 'property', + args: ['count1'] + }, { + name: 'property', + args: ['count2'] + }], + ['>=', { + name: 'property', + args: ['threshold1'] + }, { + name: 'property', + args: ['threshold2'] + }], + + [ + '<=', + { + name: 'property', + args: ['posledni_hodnota'] + }, + { + name: 'property', + args: ['spa1h'] + } + ], + // Mixed with property-to-literal + ['!=', 'status', 'NULL'] + ], + symbolizers: [{ + kind: 'Mark', + wellKnownName: 'square', + color: '#FF0000', + radius: 2.5, + strokeColor: '#000000', + strokeWidth: 1 + }] + }] +}; + +export default functionFilterPropertyToProperty; diff --git a/src/SldStyleParser.ts b/src/SldStyleParser.ts index 5399775b..2f813719 100644 --- a/src/SldStyleParser.ts +++ b/src/SldStyleParser.ts @@ -647,7 +647,7 @@ export class SldStyleParser implements StyleParser { let filter: Filter; if (sldOperatorName === 'Function') { - const functionName = sldFilter[0][':@']['@_name']; + const functionName = Array.isArray(sldFilter) ? sldFilter[0][':@']['@_name'] : sldFilter[':@']['@_name']; const tempFunctionName = functionName.charAt(0).toUpperCase() + functionName.slice(1); sldOperatorName = `PropertyIs${tempFunctionName}` as ComparisonType; } @@ -666,11 +666,22 @@ export class SldStyleParser implements StyleParser { const comparisonOperator: ComparisonOperator = COMPARISON_MAP[sldOperatorName] as ComparisonOperator; const filterIsFunction = !!get(sldFilter, 'Function'); let args: any[] = []; - const childrenToArgs = (child: any) => { - if (get([child], '#text') !== undefined) { - return get([child], '#text'); + const childrenToArgs = function (child: any, index: number) { + const propName = get([child], 'PropertyName.#text'); + if (propName !== undefined) { + const isSingleArgOperator = children.length === 1; + // Return property name for the first argument in case second argument is literal + // or isSingleArgOperator eg (PropertyIsNull) + if (isSingleArgOperator || (index === 0 && get([children[1]], 'PropertyName.#text') === undefined)) { + return propName; + } + // ..otherwise + (second argument) return as property function + return { + name: 'property', + args: [propName] + }; } else { - return get([child], 'PropertyName.#text'); + return get([child], '#text'); } }; @@ -1528,7 +1539,7 @@ export class SldStyleParser implements StyleParser { const functionChildren: any = []; if (isGeoStylerFunction(key)) { - functionChildren.unshift(keyResult?.[0]); + functionChildren.unshift(Array.isArray(keyResult) ? keyResult?.[0] : keyResult); } else { functionChildren.unshift({ Literal: [{ @@ -1538,7 +1549,7 @@ export class SldStyleParser implements StyleParser { } if (isGeoStylerFunction(value)) { - functionChildren.push(valueResult?.[0]); + functionChildren.push(Array.isArray(valueResult) ? valueResult?.[0] : valueResult); } else { functionChildren.push({ Literal: [{ diff --git a/src/SldStyleParser.v1.0.spec.ts b/src/SldStyleParser.v1.0.spec.ts index d9023472..923f3a35 100644 --- a/src/SldStyleParser.v1.0.spec.ts +++ b/src/SldStyleParser.v1.0.spec.ts @@ -46,6 +46,7 @@ import unsupported_properties from '../data/styles/unsupported_properties'; import function_markSymbolizer from '../data/styles/function_markSymbolizer'; import function_filter from '../data/styles/function_filter'; import function_nested from '../data/styles/function_nested'; +import functionFilterPropertyToProperty from '../data/styles/function_filter_property_to_property'; it('SldStyleParser is defined', () => { expect(SldStyleParser).toBeDefined(); @@ -242,6 +243,12 @@ describe('SldStyleParser implements StyleParser (reading)', () => { expect(geoStylerStyle).toBeDefined(); expect(geoStylerStyle).toEqual(point_simplepoint_nestedLogicalFilters); }); + it('can read a SLD with nested property-to-property comparison filters', async () => { + const sld = fs.readFileSync('./data/slds/1.0/function_filter_property_to_property.sld', 'utf8'); + const { output: geoStylerStyle } = await styleParser.readStyle(sld); + expect(geoStylerStyle).toBeDefined(); + expect(geoStylerStyle).toEqual(functionFilterPropertyToProperty); + }); it('can read a SLD style with multiple symbolizers in one Rule', async () => { const sld = fs.readFileSync('./data/slds/1.0/multi_simplelineLabel.sld', 'utf8'); const { output: geoStylerStyle } = await styleParser.readStyle(sld); @@ -878,6 +885,23 @@ describe('SldStyleParser implements StyleParser (writing)', () => { const { output: readStyle } = await styleParser.readStyle(sldString!); expect(readStyle).toEqual(point_simplepoint_nestedLogicalFilters); }); + + it('can write a SLD with nested property-to-property comparison filters', async () => { + const { + output: sldString, + errors, + warnings, + unsupportedProperties + } = await styleParser.writeStyle(functionFilterPropertyToProperty); + expect(sldString).toBeDefined(); + expect(errors).toBeUndefined(); + expect(warnings).toBeUndefined(); + expect(unsupportedProperties).toBeUndefined(); + // As string comparison between two XML-Strings is awkward and nonsens + // we read it again and compare the json input with the parser output + const { output: readStyle } = await styleParser.readStyle(sldString!); + expect(readStyle).toEqual(functionFilterPropertyToProperty); + }); // it('can write a SLD style with functionfilters', async () => { // const { // output: sldString, diff --git a/src/SldStyleParser.v1.1.spec.ts b/src/SldStyleParser.v1.1.spec.ts index b213a492..211d3974 100644 --- a/src/SldStyleParser.v1.1.spec.ts +++ b/src/SldStyleParser.v1.1.spec.ts @@ -47,6 +47,7 @@ import unsupported_properties from '../data/styles/unsupported_properties'; import function_markSymbolizer from '../data/styles/function_markSymbolizer'; import function_filter from '../data/styles/function_filter'; import function_nested from '../data/styles/function_nested'; +import functionFilterPropertyToProperty from '../data/styles/function_filter_property_to_property'; it('SldStyleParser is defined', () => { expect(SldStyleParser).toBeDefined(); @@ -296,6 +297,13 @@ describe('SldStyleParser with Symbology Encoding implements StyleParser (reading expect(readResult.output).toEqual(function_nested); }); + it('can read a SLD with nested property-to-property comparisons', async () => { + const sld = fs.readFileSync('./data/slds/1.1/function_filter_property_to_property.sld', 'utf8'); + const { output: geoStylerStyle } = await styleParser.readStyle(sld); + expect(geoStylerStyle).toBeDefined(); + expect(geoStylerStyle).toEqual(functionFilterPropertyToProperty); + }); + describe('#getFilterFromOperatorAndComparison', () => { it('is defined', () => { expect(styleParser.getFilterFromOperatorAndComparison).toBeDefined(); @@ -643,6 +651,16 @@ describe('SldStyleParser with Symbology Encoding implements StyleParser (writing const { output: readStyle} = await styleParser.readStyle(sldString!); expect(readStyle).toEqual(point_simplepoint_nestedLogicalFilters); }); + it('can write a SLD 1.1 with nested property-to-property comparison filters', async () => { + const { + output: sldString + } = await styleParser.writeStyle(functionFilterPropertyToProperty); + expect(sldString).toBeDefined(); + // As string comparison between two XML-Strings is awkward and nonsens + // we read it again and compare the json input with the parser output + const { output: readStyle } = await styleParser.readStyle(sldString!); + expect(readStyle).toEqual(functionFilterPropertyToProperty); + }); // it('can write a SLD 1.1 style with functionfilters', async () => { // const { output: sldString } = await styleParser.writeStyle(point_simplepoint_functionfilter); // expect(sldString).toBeDefined();