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();