Skip to content

Commit

Permalink
feat: property to property comparison (#975)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
FilipLeitner authored Jan 14, 2025
1 parent 853221d commit 72c794e
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 7 deletions.
61 changes: 61 additions & 0 deletions data/slds/1.0/function_filter_property_to_property.sld
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<StyledLayerDescriptor version="1.0.0"
xmlns="http://www.opengis.net/sld"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/sld StyledLayerDescriptor.xsd">
<NamedLayer>
<Name>Function Property to Property</Name>
<UserStyle>
<Name>Function Property to Property</Name>
<FeatureTypeStyle>
<Rule>
<Name>Property Comparison Rule</Name>
<ogc:Filter>
<ogc:And>
<ogc:PropertyIsEqualTo>
<ogc:PropertyName>posledni_hodnota</ogc:PropertyName>
<ogc:PropertyName>posledni_hodnota_sekundarni</ogc:PropertyName>
</ogc:PropertyIsEqualTo>
<ogc:PropertyIsGreaterThan>
<ogc:PropertyName>value1</ogc:PropertyName>
<ogc:PropertyName>value2</ogc:PropertyName>
</ogc:PropertyIsGreaterThan>
<ogc:PropertyIsLessThan>
<ogc:PropertyName>count1</ogc:PropertyName>
<ogc:PropertyName>count2</ogc:PropertyName>
</ogc:PropertyIsLessThan>
<ogc:PropertyIsGreaterThanOrEqualTo>
<ogc:PropertyName>threshold1</ogc:PropertyName>
<ogc:PropertyName>threshold2</ogc:PropertyName>
</ogc:PropertyIsGreaterThanOrEqualTo>
<ogc:Function name="lessThanOrEqualTo">
<ogc:PropertyName>posledni_hodnota</ogc:PropertyName>
<ogc:PropertyName>spa1h</ogc:PropertyName>
</ogc:Function>
<ogc:PropertyIsNotEqualTo>
<ogc:PropertyName>status</ogc:PropertyName>
<ogc:Literal>NULL</ogc:Literal>
</ogc:PropertyIsNotEqualTo>
</ogc:And>
</ogc:Filter>
<PointSymbolizer>
<Graphic>
<Mark>
<WellKnownName>square</WellKnownName>
<Fill>
<CssParameter name="fill">#FF0000</CssParameter>
</Fill>
<Stroke>
<CssParameter name="stroke">#000000</CssParameter>
<CssParameter name="stroke-width">1</CssParameter>
</Stroke>
</Mark>
<Size>5</Size>
</Graphic>
</PointSymbolizer>
</Rule>
</FeatureTypeStyle>
</UserStyle>
</NamedLayer>
</StyledLayerDescriptor>
62 changes: 62 additions & 0 deletions data/slds/1.1/function_filter_property_to_property.sld
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<StyledLayerDescriptor version="1.1.0" xsi:schemaLocation="http://www.opengis.net/sld StyledLayerDescriptor.xsd"
xmlns="http://www.opengis.net/sld"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:se="http://www.opengis.net/se">
<NamedLayer>
<se:Name>Function Property to Property</se:Name>
<UserStyle>
<se:Name>Function Property to Property</se:Name>
<se:FeatureTypeStyle>
<se:Rule>
<se:Name>Property Comparison Rule</se:Name>
<Filter xmlns="http://www.opengis.net/ogc">
<And>
<PropertyIsEqualTo>
<PropertyName>posledni_hodnota</PropertyName>
<PropertyName>posledni_hodnota_sekundarni</PropertyName>
</PropertyIsEqualTo>
<PropertyIsGreaterThan>
<PropertyName>value1</PropertyName>
<PropertyName>value2</PropertyName>
</PropertyIsGreaterThan>
<PropertyIsLessThan>
<PropertyName>count1</PropertyName>
<PropertyName>count2</PropertyName>
</PropertyIsLessThan>
<PropertyIsGreaterThanOrEqualTo>
<PropertyName>threshold1</PropertyName>
<PropertyName>threshold2</PropertyName>
</PropertyIsGreaterThanOrEqualTo>
<Function name="lessThanOrEqualTo">
<PropertyName>posledni_hodnota</PropertyName>
<PropertyName>spa1h</PropertyName>
</Function>
<PropertyIsNotEqualTo>
<PropertyName>status</PropertyName>
<Literal>NULL</Literal>
</PropertyIsNotEqualTo>
</And>
</Filter>
<se:PointSymbolizer>
<se:Graphic>
<se:Mark>
<se:WellKnownName>square</se:WellKnownName>
<se:Fill>
<se:SvgParameter name="fill">#FF0000</se:SvgParameter>
</se:Fill>
<se:Stroke>
<se:SvgParameter name="stroke">#000000</se:SvgParameter>
<se:SvgParameter name="stroke-width">1</se:SvgParameter>
</se:Stroke>
</se:Mark>
<se:Size>5</se:Size>
</se:Graphic>
</se:PointSymbolizer>
</se:Rule>
</se:FeatureTypeStyle>
</UserStyle>
</NamedLayer>
</StyledLayerDescriptor>
64 changes: 64 additions & 0 deletions data/styles/function_filter_property_to_property.ts
Original file line number Diff line number Diff line change
@@ -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;
25 changes: 18 additions & 7 deletions src/SldStyleParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ export class SldStyleParser implements StyleParser<string> {
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;
}
Expand All @@ -666,11 +666,22 @@ export class SldStyleParser implements StyleParser<string> {
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;

Check failure on line 672 in src/SldStyleParser.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'children' was used before it was defined

Check failure on line 672 in src/SldStyleParser.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

'children' was used before it was defined

Check failure on line 672 in src/SldStyleParser.ts

View workflow job for this annotation

GitHub Actions / Release

'children' was used before it was defined

Check failure on line 672 in src/SldStyleParser.ts

View workflow job for this annotation

GitHub Actions / Release

'children' was used before it was defined
// 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)) {

Check failure on line 675 in src/SldStyleParser.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'children' was used before it was defined

Check failure on line 675 in src/SldStyleParser.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

'children' was used before it was defined

Check failure on line 675 in src/SldStyleParser.ts

View workflow job for this annotation

GitHub Actions / Release

'children' was used before it was defined

Check failure on line 675 in src/SldStyleParser.ts

View workflow job for this annotation

GitHub Actions / Release

'children' was used before it was defined
return propName;
}
// ..otherwise + (second argument) return as property function
return {
name: 'property',
args: [propName]
};
} else {
return get([child], 'PropertyName.#text');
return get([child], '#text');
}
};

Expand Down Expand Up @@ -1528,7 +1539,7 @@ export class SldStyleParser implements StyleParser<string> {
const functionChildren: any = [];

if (isGeoStylerFunction(key)) {
functionChildren.unshift(keyResult?.[0]);
functionChildren.unshift(Array.isArray(keyResult) ? keyResult?.[0] : keyResult);
} else {
functionChildren.unshift({
Literal: [{
Expand All @@ -1538,7 +1549,7 @@ export class SldStyleParser implements StyleParser<string> {
}

if (isGeoStylerFunction(value)) {
functionChildren.push(valueResult?.[0]);
functionChildren.push(Array.isArray(valueResult) ? valueResult?.[0] : valueResult);
} else {
functionChildren.push({
Literal: [{
Expand Down
24 changes: 24 additions & 0 deletions src/SldStyleParser.v1.0.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions src/SldStyleParser.v1.1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 72c794e

Please sign in to comment.