diff --git a/docs/developer-guide/LayerFilter.md b/docs/developer-guide/LayerFilter.md
index a31dfacbad..00ad710cf7 100644
--- a/docs/developer-guide/LayerFilter.md
+++ b/docs/developer-guide/LayerFilter.md
@@ -91,7 +91,7 @@ The `cql` format is a JSON object that has this shape:
```
!!! Note:
- MapStore actually supports only a subset of CQL, that is the one used by GeoServer. In particular can not parse WKT geometries yet.
+ MapStore actually supports only a subset of CQL, that is the one used by GeoServer.
### `mapstore-query-panel` format
diff --git a/web/client/utils/ogc/Filter/CQL/__tests__/parser-test.js b/web/client/utils/ogc/Filter/CQL/__tests__/parser-test.js
index 5adf180c40..500ab7c8b3 100644
--- a/web/client/utils/ogc/Filter/CQL/__tests__/parser-test.js
+++ b/web/client/utils/ogc/Filter/CQL/__tests__/parser-test.js
@@ -1,72 +1,64 @@
import expect from 'expect';
-import {get} from 'lodash';
-import parser from '../parser';
+import { get } from 'lodash';
+import {read, functionOperator} from '../parser';
const COMPARISON_TESTS = [
// numeric
{
cql: "PROP = 1",
expected: {
- property: "PROP",
- type: "=",
- value: 1
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 1}],
+ type: "="
}
},
{
cql: "PROP <> 1",
expected: {
- property: "PROP",
- type: "<>",
- value: 1
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 1}],
+ type: "<>"
}
},
{
cql: "PROP < 1",
expected: {
- property: "PROP",
- type: "<",
- value: 1
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 1}],
+ type: "<"
}
},
{
cql: "PROP <= 1",
expected: {
- property: "PROP",
- type: "<=",
- value: 1
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 1}],
+ type: "<="
}
},
{
cql: "PROP > 1",
expected: {
- property: "PROP",
- type: ">",
- value: 1
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 1}],
+ type: ">"
}
},
{
cql: "PROP >= 1",
expected: {
- property: "PROP",
- type: ">=",
- value: 1
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 1}],
+ type: ">="
}
},
// string
{
cql: "PROP = 'a'",
expected: {
- property: "PROP",
- type: "=",
- value: 'a'
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 'a'}],
+ type: "="
}
},
{
cql: "PROP like 'a'",
expected: {
- property: "PROP",
- type: "like",
- value: 'a'
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 'a'}],
+ type: "like"
}
},
{
@@ -76,12 +68,12 @@ const COMPARISON_TESTS = [
}
}
];
+
const VARIANTS = [{
cql: "\"PROP\" = 'a'",
expected: {
- property: "PROP",
- type: "=",
- value: 'a'
+ args: [{type: "property", name: "PROP"}, {type: "literal", value: 'a'}],
+ type: "="
}
}];
@@ -91,12 +83,12 @@ const LOGICAL = [
cql: "PROP1 like 'a' and PROP2 < 1",
expected: {
"type": "and",
- "filters[0].property": "PROP1",
+ "filters[0].args[0].name": "PROP1",
"filters[0].type": "like",
- "filters[0].value": "a",
- "filters[1].property": "PROP2",
+ "filters[0].args[1].value": "a",
+ "filters[1].args[0].name": "PROP2",
"filters[1].type": "<",
- "filters[1].value": 1
+ "filters[1].args[1].value": 1
}
},
// or
@@ -104,12 +96,12 @@ const LOGICAL = [
cql: "PROP1 like 'a' or PROP2 < 1",
expected: {
"type": "or",
- "filters[0].property": "PROP1",
+ "filters[0].args[0].name": "PROP1",
"filters[0].type": "like",
- "filters[0].value": "a",
- "filters[1].property": "PROP2",
+ "filters[0].args[1].value": "a",
+ "filters[1].args[0].name": "PROP2",
"filters[1].type": "<",
- "filters[1].value": 1
+ "filters[1].args[1].value": 1
}
},
// not
@@ -117,9 +109,9 @@ const LOGICAL = [
cql: "NOT X < 12",
expected: {
"type": "not",
- "filters[0].property": "X",
+ "filters[0].args[0].name": "X",
"filters[0].type": "<",
- "filters[0].value": 12
+ "filters[0].args[1].value": 12
}
},
// complex filter
@@ -129,21 +121,299 @@ const LOGICAL = [
"type": "and",
"filters[0].type": "and",
"filters[0].filters[0].type": "like",
- "filters[0].filters[0].property": "PROP1",
- "filters[0].filters[0].value": "a",
+ "filters[0].filters[0].args[0].name": "PROP1",
+ "filters[0].filters[0].args[1].value": "a",
"filters[0].filters[1].type": "<",
- "filters[0].filters[1].property": "PROP2",
- "filters[0].filters[1].value": 1,
+ "filters[0].filters[1].args[0].name": "PROP2",
+ "filters[0].filters[1].args[1].value": 1,
"filters[1].type": "or",
"filters[1].filters[0].type": "<>",
- "filters[1].filters[0].property": "PROP1",
- "filters[1].filters[0].value": "a",
+ "filters[1].filters[0].args[0].name": "PROP1",
+ "filters[1].filters[0].args[1].value": "a",
"filters[1].filters[1].type": "not",
- "filters[1].filters[1].filters[0].property": "PROP2"
+ "filters[1].filters[1].filters[0].args[0].name": "PROP2"
}
}
];
+const WKT_TESTS = [
+ // POINT
+ {
+ cql: "INTERSECTS(PROP1, POINT(1 2))",
+ expected: {
+ type: "INTERSECTS",
+ args: [{
+ type: "property",
+ name: "PROP1"
+ }, {
+ type: "Point",
+ coordinates: [1, 2]
+ }]
+ }
+ },
+ // MULTIPOINT
+ {
+ cql: "INTERSECTS(PROP1, MULTIPOINT(1 2, 3 4))",
+ expected: {
+ type: "INTERSECTS",
+ args: [{type: "property", name: "PROP1"},
+ {
+ type: "MultiPoint",
+ coordinates: [[1, 2], [3, 4]]
+ }]
+ }
+ },
+ // LINESTRING
+ {
+ cql: "INTERSECTS(PROP1, LINESTRING(1 2, 3 4))",
+ expected: {
+ type: "INTERSECTS",
+ args: [{
+ type: "property",
+ name: "PROP1"
+ }, {
+ type: "LineString",
+ coordinates: [[1, 2], [3, 4]]
+ }]
+ }
+ },
+ // MULTILINESTRING
+ {
+ cql: "INTERSECTS(PROP1, MULTILINESTRING((1 2, 3 4), (5 6, 7 8)))",
+ expected: {
+ type: "INTERSECTS",
+ args: [ {type: "property", name: "PROP1"}, {
+ type: "MultiLineString",
+ coordinates: [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
+ }]
+ }
+ },
+ // POLYGON
+ {
+ cql: "INTERSECTS(PROP1, POLYGON((1 2, 3 4, 5 6, 1 2)))",
+ expected: {
+ type: "INTERSECTS",
+ args: [
+ {type: "property", name: "PROP1"}, {
+ type: "Polygon",
+ coordinates: [[[1, 2], [3, 4], [5, 6], [1, 2]]]
+ }]
+ }
+ },
+ // MULTIPOLYGON
+ {
+ cql: "INTERSECTS(PROP1, MULTIPOLYGON(((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 10, 11 12, 7 8))))",
+ expected: {
+ type: "INTERSECTS",
+ args: [
+ {type: "property", name: "PROP1"},
+ {
+ type: "MultiPolygon",
+ coordinates: [
+ [[[1, 2], [3, 4], [5, 6], [1, 2]]],
+ [[[7, 8], [9, 10], [11, 12], [7, 8]]]
+ ]
+ }]
+ }
+ }
+];
+const FUNCTION_TESTS = [
+ {
+ cql: "func('text')",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [{type: 'literal', value: 'text'}]
+ }
+ },
+ // multiple strings
+ {
+ cql: "func('Hello', ' ', 'World')",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [
+ {type: 'literal', value: 'Hello'},
+ {type: 'literal', value: ' '},
+ {type: 'literal', value: 'World'}
+ ]
+ }
+ },
+ // mixed args
+ {
+ cql: "func('abcdef', 2, 4)",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [
+ {type: 'literal', value: 'abcdef'},
+ {type: 'literal', value: 2},
+ {type: 'literal', value: 4}
+ ]
+ }
+ },
+ // Property Names
+ {
+ cql: "func( propertyName )",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [{type: 'property', name: 'propertyName'}]
+ }
+ },
+ // with double quotes
+ {
+ cql: 'func("firstName" , lastName )',
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [
+ {type: 'property', name: 'firstName'},
+ {type: 'property', name: 'lastName'}
+ ]
+ }
+ },
+ // mixing numbers, property names and strings
+ {
+ cql: 'substring("address", 1, \'test\')',
+ expected: {
+ type: functionOperator,
+ name: "substring",
+ args: [
+ {type: 'property', name: 'address'},
+ {type: 'literal', value: 1},
+ {type: 'literal', value: 'test'}]
+ }
+ },
+ {
+ cql: 'func(propertyName, \'oldValue\', \'newValue\')',
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [
+ {type: 'property', name: 'propertyName'},
+ {type: 'literal', value: 'oldValue'},
+ {type: 'literal', value: 'newValue'}
+ ]
+ }
+ },
+
+ // Numbers
+ {
+ cql: "func(-5)",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [{type: 'literal', value: -5}]
+ }
+ },
+ {
+ cql: "func(3.14159)",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [{type: 'literal', value: 3.14159}]
+ }
+ },
+ // nested functions
+ {
+ cql: "func(func2('text'))",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [{
+ type: functionOperator,
+ name: "func2",
+ args: [{type: 'literal', value: 'text'}]
+ }]
+ }
+ },
+ {
+ cql: "func(func2('text1'), func3('text2'))",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [
+ {
+ type: functionOperator,
+ name: "func2",
+ args: [{type: 'literal', value: 'text1'}]
+ },
+ {
+ type: functionOperator,
+ name: "func3",
+ args: [{type: 'literal', value: 'text2'}]
+ }
+ ]
+ }
+ },
+ // nested functions with property names
+ {
+ cql: "func(func2(propertyName))",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [{
+ type: functionOperator,
+ name: "func2",
+ args: [{type: 'property', name: 'propertyName'}]
+ }]
+ }
+ },
+ // nested functions with multiple arguments
+ {
+ cql: "func(func2(propertyName, 'text1'), func3('text2', 2))",
+ expected: {
+ type: functionOperator,
+ name: "func",
+ args: [
+ {
+ type: functionOperator,
+ name: "func2",
+ args: [
+ {type: 'property', name: 'propertyName'},
+ {type: 'literal', value: 'text1'}
+ ]
+ }, {
+ type: functionOperator,
+ name: "func3",
+ args: [
+ {type: 'literal', value: 'text2'},
+ {type: 'literal', value: 2}
+ ]
+ }
+ ]
+ }
+ },
+ // Existing functions
+
+ // jsonArrayContains
+ {
+ cql: "jsonArrayContains(\"properties\", 'key', 'value')",
+ expected: {
+ "type": functionOperator,
+ "name": "jsonArrayContains",
+ "args": [
+ {type: 'property', name: "properties"},
+ {type: 'literal', value: "key"},
+ {type: 'literal', value: "value"}
+ ]
+
+ }
+ }, {
+ cql: "jsonPointer(\"properties\", 'key')",
+ expected: {
+ "type": functionOperator,
+ "name": "jsonPointer",
+ "args": [
+ {type: 'property', name: "properties"},
+ {type: 'literal', value: "key"}
+ ]
+ }
+ }
+
+];
+
const REAL_WORLD = [
// real world example
{
@@ -152,31 +422,246 @@ const REAL_WORLD = [
"type": "and",
"filters[0].type": "and",
"filters[0].filters[0].filters[0].type": "<=",
- "filters[1].property": "TPINCID"
+ "filters[1].args[0].type": "property",
+ "filters[1].args[0].name": "TPINCID",
+ "filters[1].type": "=",
+ "filters[1].args[1].value": "1"
+
+ }
+ }, {
+ cql: 'func1() = false',
+ expected: {
+ "type": "=",
+ "args": [{
+ "type": functionOperator,
+ "name": "func1",
+ "args": []
+ }, {
+ "type": "literal",
+ "value": false
+ }]
+ }
+ },
+ {
+ cql: "jsonArrayContains(\"property1\", 'key', 'value') = true",
+ expected: {
+ "type": "=",
+ "args": [{
+ "type": functionOperator,
+ "name": "jsonArrayContains",
+ "args": [
+ {type: 'property', name: "property1"},
+ {type: 'literal', value: "key"},
+ {type: 'literal', value: "value"}
+ ]
+ }, {
+ "type": "literal",
+ "value": true
+ }]
+ }
+ },
+
+ {
+ cql: "INTERSECTS(PROP1, POINT(1 2)) AND PROP2 < 1",
+ expected: {
+ "type": "and",
+ "filters": [{
+ "type": "INTERSECTS",
+ "args": [
+ {type: "property", name: "PROP1"},
+ {
+ "type": "Point",
+ "coordinates": [1, 2]
+ }]
+ }, {
+ "type": "<",
+ "args": [{type: "property", name: "PROP2"}, {type: "literal", value: 1}]
+ }]
+
+ }
+ },
+ {
+ cql: "a = 1 AND b = 2",
+ expected: {
+ "type": "and",
+ "filters": [{
+ "type": "=",
+ "args": [{type: 'property', name: "a"}, {type: 'literal', value: 1}]
+ }, {
+ "type": "=",
+ "args": [{type: 'property', name: "b"}, {type: 'literal', value: 2}]
+ }]
+ }
+ }, {
+ cql: "f1(a) AND f2(b)",
+ expected: {
+ "type": "and",
+ "filters": [{
+ "type": functionOperator,
+ "name": "f1",
+ "args": [{type: 'property', name: "a"}]
+ }, {
+ "type": functionOperator,
+ "name": "f2",
+ "args": [{type: 'property', name: "b"}]
+ }]
+ }
+ },
+ {
+ cql: "jsonArrayContains(\"property1\", 'key', 'value') = false AND jsonPointer(\"property2\", 'key') = 'value')",
+ expected: {
+ "type": "and",
+ filters: [{
+ "type": "=",
+ "args": [{
+ "type": functionOperator,
+ "name": "jsonArrayContains",
+ "args": [
+ {type: 'property', name: "property1"},
+ {type: 'literal', value: "key"},
+ {type: 'literal', value: "value"}
+ ]
+ }, {
+ "type": "literal",
+ "value": false
+ }]
+ }, {
+ "type": "=",
+ "args": [{
+ "type": functionOperator,
+ "name": "jsonPointer",
+ "args": [
+ {type: 'property', name: "property2"},
+ {type: 'literal', value: "key"}
+ ]
+ }, {
+ "type": "literal",
+ "value": "value"
+ }]
+ }]
+ }
+ },
+ {
+ cql: "(jsonArrayContains(\"property1\", 'key', 'value') = false) AND (jsonPointer(\"property2\", 'key') = 'value'))",
+ expected: {
+ "type": "and",
+ filters: [{
+ "type": "=",
+ "args": [{
+ "type": functionOperator,
+ "name": "jsonArrayContains",
+ "args": [
+ {type: 'property', name: "property1"},
+ {type: 'literal', value: "key"},
+ {type: 'literal', value: "value"}
+ ]
+ }, {
+ "type": "literal",
+ "value": false
+ }]
+ }, {
+ "type": "=",
+ "args": [{
+ "type": functionOperator,
+ "name": "jsonPointer",
+ "args": [
+ {type: 'property', name: "property2"},
+ {type: 'literal', value: "key"}
+ ]
+ }, {
+ "type": "literal",
+ "value": "value"
+ }]
+ }]
+ }
+ },
+ {
+ // mixed functions and operators
+ cql: `jsonArrayContains(\"property1\", 'key', 'value') = false AND jsonPointer(\"property2\", 'key') = 'value' AND INTERSECTS(geom, POINT(1 2)) AND property3 = 'value'`,
+ expected: {
+ type: "and",
+ filters: [{
+ "type": "and",
+ filters: [{
+ type: "and",
+ filters: [{
+ "type": "=",
+ "args": [{
+ "type": functionOperator,
+ "name": "jsonArrayContains",
+ "args": [
+ {type: 'property', name: "property1"},
+ {type: 'literal', value: "key"},
+ {type: 'literal', value: "value"}
+ ]
+ }, {
+ "type": "literal",
+ "value": false
+ }]
+ }, {
+ "type": "=",
+ "args": [{
+ "type": functionOperator,
+ "name": "jsonPointer",
+ "args": [
+ {type: 'property', name: "property2"},
+ {type: 'literal', value: "key"}
+ ]
+ }, {
+ "type": "literal",
+ "value": "value"
+ }]
+ }]
+ }, {
+ "type": "INTERSECTS",
+ "args": [
+ {type: "property", name: "geom"},
+ {
+ "type": "Point",
+ "coordinates": [1, 2]
+ }]
+ }]
+ }, {
+ "type": "=",
+ "args": [{type: 'property', name: "property3"}, {type: 'literal', value: "value"}]
+ }]
}
}
];
const testRules = rules => rules.map(({ cql, expected }) => {
- const res = parser.read(cql);
- Object.keys(expected).map(k =>
- expect(get(res, k)).toBe(expected[k])
- );
+ it(`testing ${cql}`, () => {
+ try {
+ const res = read(cql);
+ Object.keys(expected).map(k => {
+ expect(get(res, k)).toEqual(expected[k]);
+ });
+ } catch (e) {
+ throw e;
+ }
+ });
});
describe('cql parser', () => {
- it('test simple comparison', () => {
+ describe('test simple comparison', () => {
testRules(COMPARISON_TESTS);
});
- it('test logical operators', () => {
+ describe('test logical operators', () => {
testRules(LOGICAL);
});
- it('test variants of operators', () => {
+ describe('test variants of operators', () => {
testRules(VARIANTS);
});
- it('test more real world examples', () => {
+ describe('test wkt parsing', () => {
+ testRules(WKT_TESTS);
+ });
+ describe('test function parsing', () => {
+ testRules(FUNCTION_TESTS);
+ });
+ describe('test more real world examples', () => {
testRules(REAL_WORLD);
});
+
});
diff --git a/web/client/utils/ogc/Filter/CQL/parser.js b/web/client/utils/ogc/Filter/CQL/parser.js
index a6d6b37aa1..aeb392d7c4 100644
--- a/web/client/utils/ogc/Filter/CQL/parser.js
+++ b/web/client/utils/ogc/Filter/CQL/parser.js
@@ -5,29 +5,31 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
-const fromWKT = () => {
- throw new Error("WKT parsing for CQL filter not supported yet");
-}; // TODO: use wkt-parser
-const spatialOperators = {
+import {toGeoJSON} from '../../WKT';
+
+export const spatialOperators = {
INTERSECTS: "INTERSECTS",
BBOX: "BBOX",
CONTAINS: "CONTAINS",
DWITHIN: "DWITHIN",
WITHIN: "WITHIN"
};
-const patterns = {
+
+export const functionOperator = "func";
+export const patterns = {
INCLUDE: /^INCLUDE$/,
PROPERTY: /^"?[_a-zA-Z"]\w*"?/,
COMPARISON: /^(=|<>|<=|<|>=|>|LIKE)/i,
IS_NULL: /^IS NULL/i,
COMMA: /^,/,
LOGICAL: /^(AND|OR)/i,
- VALUE: /^('([^']|'')*'|-?\d+(\.\d*)?|\.\d+)/,
+ VALUE: /^('([^']|'')*'|-?\d+(\.\d*)?|\.\d+|true|false)/i,
LPAREN: /^\(/,
RPAREN: /^\)/,
SPATIAL: /^(BBOX|INTERSECTS|DWITHIN|WITHIN|CONTAINS)/i,
NOT: /^NOT/i,
BETWEEN: /^BETWEEN/i,
+ FUNCTION: /^[_a-zA-Z][_a-zA-Z1-9]*(?=\()/,
GEOMETRY: (text) => {
let type = /^(POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)/.exec(text);
if (type) {
@@ -57,18 +59,19 @@ const patterns = {
};
const follows = {
INCLUDE: ['END'],
- LPAREN: ['GEOMETRY', 'SPATIAL', 'PROPERTY', 'VALUE', 'LPAREN'],
- RPAREN: ['NOT', 'LOGICAL', 'END', 'RPAREN'],
- PROPERTY: ['COMPARISON', 'BETWEEN', 'COMMA', 'IS_NULL'],
+ LPAREN: ['GEOMETRY', 'SPATIAL', 'FUNCTION', 'PROPERTY', 'VALUE', 'LPAREN', 'RPAREN', 'NOT'],
+ RPAREN: ['NOT', 'LOGICAL', 'END', 'RPAREN', 'COMMA', 'COMPARISON', 'BETWEEN', 'IS_NULL'],
+ PROPERTY: ['COMPARISON', 'BETWEEN', 'COMMA', 'IS_NULL', 'RPAREN'],
BETWEEN: ['VALUE'],
IS_NULL: ['END'],
- COMPARISON: ['VALUE'],
- COMMA: ['GEOMETRY', 'VALUE', 'PROPERTY'],
+ COMPARISON: ['VALUE', 'FUNCTION'],
+ COMMA: ['GEOMETRY', 'FUNCTION', 'VALUE', 'PROPERTY'],
VALUE: ['LOGICAL', 'COMMA', 'RPAREN', 'END'],
SPATIAL: ['LPAREN'],
- LOGICAL: ['NOT', 'VALUE', 'SPATIAL', 'PROPERTY', 'LPAREN'],
+ LOGICAL: ['NOT', 'VALUE', 'SPATIAL', 'FUNCTION', 'PROPERTY', 'LPAREN'],
NOT: ['PROPERTY', 'LPAREN'],
- GEOMETRY: ['COMMA', 'RPAREN']
+ GEOMETRY: ['COMMA', 'RPAREN'],
+ FUNCTION: ['LPAREN', 'FUNCTION', 'VALUE', 'PROPERTY']
};
@@ -141,7 +144,7 @@ const nextToken = (text, tokens) => {
const tokenize = (text) => {
let results = [];
let token;
- const expect = ["INCLUDE", "NOT", "GEOMETRY", "SPATIAL", "PROPERTY", "LPAREN"];
+ const expect = ["INCLUDE", "NOT", "GEOMETRY", "SPATIAL", "FUNCTION", "PROPERTY", "LPAREN"];
let text2 = text;
let expect2 = expect;
do {
@@ -161,13 +164,16 @@ const tokenize = (text) => {
const buildAst = (tokens) => {
let operatorStack = [];
let postfix = [];
-
while (tokens.length) {
let tok = tokens.shift();
switch (tok.type) {
case "PROPERTY":
case "GEOMETRY":
case "VALUE":
+ if (operatorStack.length > 0 &&
+ operatorStack?.[operatorStack.length - 2]?.type === "FUNCTION") {
+ operatorStack[operatorStack.length - 2].count++;
+ }
postfix.push(tok);
break;
case "COMPARISON":
@@ -182,12 +188,26 @@ const buildAst = (tokens) => {
) {
postfix.push(operatorStack.pop());
}
-
+ if (operatorStack.length > 0 &&
+ operatorStack?.[operatorStack.length - 2]?.type === "FUNCTION") {
+ operatorStack[operatorStack.length - 2].count++;
+ }
operatorStack.push(tok);
break;
case "SPATIAL":
+ case "FUNCTION":
case "NOT":
+ if (operatorStack.length > 0 &&
+ operatorStack?.[operatorStack.length - 2]?.type === "FUNCTION") {
+ operatorStack[operatorStack.length - 2].count++;
+ }
+ operatorStack.push(tok);
+ break;
case "LPAREN":
+ if (operatorStack.length > 0 &&
+ operatorStack?.[operatorStack.length - 1]?.type === "FUNCTION") {
+ operatorStack[operatorStack.length - 1].count = 0;
+ }
operatorStack.push(tok);
break;
case "RPAREN":
@@ -202,6 +222,12 @@ const buildAst = (tokens) => {
operatorStack[operatorStack.length - 1].type === "SPATIAL") {
postfix.push(operatorStack.pop());
}
+
+ if (operatorStack.length > 0 &&
+ operatorStack[operatorStack.length - 1].type === "FUNCTION") {
+ let funcTok = operatorStack.pop();
+ postfix.push(funcTok);
+ }
break;
case "COMMA":
case "END":
@@ -237,39 +263,68 @@ const buildAst = (tokens) => {
let min = buildTree();
const property = buildTree();
return ({
- property,
- lowerBoundary: min,
- upperBoundary: max,
+ args: [property, min, max],
type: operators.BETWEEN
});
}
case "COMPARISON": {
- let value = buildTree();
- const property = buildTree();
+ const arg2 = buildTree();
+ const arg1 = buildTree();
return ({
- property,
- value: value,
+ args: [arg1, arg2],
type: operators[tok.text.toUpperCase()]
});
}
case "IS_NULL": {
const property = buildTree();
return ({
- property,
+ args: [property],
type: operators[tok.text.toUpperCase()]
});
}
case "VALUE":
let match = tok.text.match(/^'(.*)'$/);
if (match) {
- return match[1].replace(/''/g, "'");
+ return {
+ type: 'literal',
+ value: match[1].replace(/''/g, "'")
+ };
}
- return Number(tok.text);
+ if (tok.text.toLowerCase() === "true" || tok.text.toLowerCase() === "false") {
+ return {
+ type: 'literal',
+ value: tok.text.toLowerCase() === "true"
+ };
+ }
+ return {
+ type: 'literal',
+ value: Number(tok.text)
+ };
+ case "PROPERTY":
+ return ({
+ type: "property",
+ name: tok.text
+ });
+
case "INCLUDE": {
return ({
type: cql.INCLUDE
});
}
+ case "FUNCTION": {
+ let name = tok.text.replace(/\($/, "");
+ let args = [];
+ for ( let i = 0; i < tok.count; i++) {
+ args.push(buildTree());
+ }
+
+ return ({
+ type: functionOperator,
+ name,
+ args: args.reverse()
+ });
+ }
+
case "SPATIAL":
switch (tok.text.toUpperCase()) {
case "BBOX": {
@@ -290,8 +345,7 @@ const buildAst = (tokens) => {
const property = buildTree();
return ({
type: spatialOperators.INTERSECTS,
- property,
- value
+ args: [property, value]
});
}
case "WITHIN": {
@@ -299,8 +353,7 @@ const buildAst = (tokens) => {
let property = buildTree();
return ({
type: spatialOperators.WITHIN,
- property,
- value
+ args: [property, value]
});
}
case "CONTAINS": {
@@ -308,8 +361,7 @@ const buildAst = (tokens) => {
const property = buildTree();
return ({
type: spatialOperators.CONTAINS,
- property,
- value
+ args: [property, value]
});
}
case "DWITHIN": {
@@ -327,7 +379,8 @@ const buildAst = (tokens) => {
return null;
}
case "GEOMETRY":
- return fromWKT(tok.text);
+ // WKT to convert in GeoJSON.
+ return toGeoJSON(tok.text);
default:
return tok.text;
}
@@ -344,33 +397,42 @@ const buildAst = (tokens) => {
return result;
};
-module.exports = {
- /**
- * Parse a CQL filter. returns an object representation of the filter.
- * For the moment this parser doesn't support WKT parsing.
- * Example:
- * ```
- * const cqlFilter = "property1 = 'value1' AND property2 = 'value2'";
- * const obj = read(cqlFilter);
- * console.log(obj);
- * // obj looks like this
- * {
- * type: "and",
- * filters: [{
- * type: "=",
- * property: "property1",
- * value: "value1"
- * },{
- * type: "=",
- * property: "property1",
- * value: "value2"
- * }]
- * }
- * ```
- * @memberof utils.ogc.Filter.CQL.parser
- * @name read
- * @param cqlFilter the cql_filter o parse
- * @return a javascript representation of the filter.
- */
- read: (text) => buildAst(tokenize(text))
-};
+
+
+/**
+ * Parse a CQL filter. returns an AST object representation of the filter.
+ * Example:
+ * ```
+ * const cqlFilter = "property1 = 'value1' AND property2 = 'value2'";
+ * const obj = read(cqlFilter);
+ * console.log(obj);
+ * // obj looks like this
+ * {
+ * type: "and",
+ * filters: [{
+ * type: "=",
+ * args[{type: "property", name: "property1"}, {type: "literal", value: "value1"}]
+ * },{
+ * type: "=",
+ * property: "property1",
+ * args[{type: "property", name: "property1"}, {type: "literal", value: "value1"}]
+ * }, {
+ * type: "func",
+ * name: "func1",
+ * args: [{
+ * type: "property",
+ * name: "property5"
+ * }, {
+ * type: "literal",
+ * value: "value5"
+ * }]
+ * }]]
+ * }
+ * ```
+ * @memberof utils.ogc.Filter.CQL.parser
+ * @name read
+ * @param cqlFilter the cql_filter o parse
+ * @return a javascript representation of the filter.
+ */
+export const read = (text) => buildAst(tokenize(text));
+
diff --git a/web/client/utils/ogc/Filter/FilterBuilder.js b/web/client/utils/ogc/Filter/FilterBuilder.js
index e49ca146bb..fd94b803e6 100644
--- a/web/client/utils/ogc/Filter/FilterBuilder.js
+++ b/web/client/utils/ogc/Filter/FilterBuilder.js
@@ -5,7 +5,7 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
-const {logical, spatial, comparison, literal, propertyName, valueReference, distance, lower, upper} = require('./operators');
+const {logical, spatial, comparison, literal, propertyName, valueReference, distance, lower, upper, func} = require('./operators');
const {filter, fidFilter} = require('./filter');
const {processOGCGeometry} = require("../GML");
// const isValidXML = (value, {filterNS, gmlNS}) => value.indexOf(`<${filterNS}:` === 0) || value.indexOf(`<${gmlNS}:`) === 0;
@@ -21,7 +21,7 @@ const {processOGCGeometry} = require("../GML");
* filter(
* and(
* property("P1").equals("v1"),
- * proprety("the_geom").intersects(geoJSONGeometry)
+ * property("the_geom").intersects(geoJSONGeometry)
* )
* ),
* {srsName="EPSG:4326"} // 3rd for query is optional
@@ -92,11 +92,27 @@ module.exports = function({filterNS = "ogc", gmlVersion, wfsVersion = "1.1.0"} =
};
const propName = wfsVersion.indexOf("2.") === 0 ? valueReference : propertyName;
return {
+
+ operations: Object.entries({
+ ...spatial,
+ ...comparison,
+ ...logical,
+ // override between to avoid to explicit lower and upper boundaries as arguments
+ between: (ns, prop, ...args) => comparison.between(ns, prop, lower(ns, args[0]), upper(ns, args[1])),
+ func
+ }).reduce((acc, [key, fun]) => {
+ acc[key] = fun.bind(null, filterNS);
+ return acc;
+ }, {}),
+ geometry: getGeom,
filter: filter.bind(null, filterNS),
fidFilter: fidFilter.bind(null, filterNS),
and: logical.and.bind(null, filterNS),
or: logical.or.bind(null, filterNS),
not: logical.not.bind(null, filterNS),
+ func: func.bind(null, filterNS),
+ literal: getValue,
+ propertyName: propName.bind(null, filterNS),
property: function(name) {
return {
equalTo: (value) => comparison.equal(filterNS, propName(filterNS, name), getValue(value)),
diff --git a/web/client/utils/ogc/Filter/__tests__/fromObject-test.js b/web/client/utils/ogc/Filter/__tests__/fromObject-test.js
index 0835b7e785..908119cbf2 100644
--- a/web/client/utils/ogc/Filter/__tests__/fromObject-test.js
+++ b/web/client/utils/ogc/Filter/__tests__/fromObject-test.js
@@ -106,6 +106,70 @@ const CQL_SPECIFIC = [{
expected: ""
}];
+const FUNCTIONS = [
+ // empty
+ {
+ cql: "func1() = false",
+ expected: ''
+ + ''
+ + ''
+ + 'false'
+ + ''
+ },
+ // simple
+ {
+ cql: "func1('arg1') = false",
+ expected: ''
+ + ''
+ + 'arg1'
+ + ''
+ + 'false'
+ + ''
+ },
+ // multiple args
+ {
+ cql: "func1('arg1', 'arg2') = false",
+ expected: ''
+ + ''
+ + 'arg1'
+ + 'arg2'
+ + ''
+ + 'false'
+ + ''
+ },
+ // property
+ {
+ cql: "func1(PROP) = false",
+ expected: ''
+ + ''
+ + 'PROP'
+ + ''
+ + 'false'
+ + ''
+ },
+
+ // nested
+ {
+ cql: "jsonArrayContains(\"property1\", 'key', 'value') = false",
+ expected: ''
+ + ''
+ + 'property1'
+ + 'key'
+ + 'value'
+ + ''
+ + 'false'
+ + ''
+ }, {
+ cql: "jsonPointer(\"property2\", 'key') = 'value')",
+ expected: ''
+ + ''
+ + 'property2'
+ + 'key'
+ + ''
+ + 'value'
+ + ''
+ }];
+
const REAL_WORLD = [
// real world example
{
@@ -133,6 +197,41 @@ const REAL_WORLD = [
+ '1'
+ ''
+ ''
+ }, {
+ cql: "jsonArrayContains(\"property1\", 'key', 'value') = false AND jsonPointer(\"property2\", 'key') = 'value')",
+ expected:
+ ''
+ + ''
+ + ''
+ + 'property1'
+ + 'key'
+ + 'value'
+ + ''
+ + 'false'
+ + ''
+ + ''
+ + ''
+ + 'property2'
+ + 'key'
+ + ''
+ + 'value'
+ + ''
+ + ''
+
+ },
+ // geometry example
+ {
+ cql: "INTERSECTS(GEOMETRY, POLYGON((0 0, 0 10, 10 10, 10 0, 0 0)))",
+ expected: ''
+ + 'GEOMETRY'
+ + ''
+ + ''
+ + ''
+ + '0 0 0 10 10 10 10 0 0 0'
+ + ''
+ + ''
+ + ''
+ + ''
}
];
const testRules = (rules, toOGCFilter) => rules.map(({ cql, expected }) => {
@@ -156,6 +255,10 @@ describe('Convert CQL filter to OGC Filter', () => {
testRules(CQL_SPECIFIC, toOGCFilter);
});
+ it('functions', () => {
+ const toOGCFilter = fromObject(filterBuilder({ gmlVersion: "3.1.1" }));
+ testRules(FUNCTIONS, toOGCFilter);
+ });
it('more real world examples', () => {
const toOGCFilter = fromObject(filterBuilder({ gmlVersion: "3.1.1" }));
testRules(REAL_WORLD, toOGCFilter);
diff --git a/web/client/utils/ogc/Filter/fromObject.js b/web/client/utils/ogc/Filter/fromObject.js
index ceb03fc888..44be8a2bea 100644
--- a/web/client/utils/ogc/Filter/fromObject.js
+++ b/web/client/utils/ogc/Filter/fromObject.js
@@ -1,22 +1,24 @@
-import {includes, isNil} from 'lodash';
+import {includes} from 'lodash';
const logical = ["and", "or", "not"];
const cql = ["include"];
const operators = {
- '=': "equalTo",
- "<>": "notEqualTo",
+ '=': "equal",
+ "<>": "notEqual",
"><": "between",
- '<': "lessThen",
- '<=': "lessThenOrEqualTo",
- '>': "greaterThen",
- '>=': "greaterThenOrEqualTo",
+ '<': "less",
+ '<=': "lessOrEqual",
+ '>': "greater",
+ '>=': "greaterOrEqual",
'like': "like",
'ilike': "ilike"
// TODO: support unary operators like isNull
// TODO: support geometry operations
};
+const spatial = ["intersects", "within", "bbox", "dwithin", "contains"];
+const geometryTypes = ["Point", "LineString", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon", "GeometryCollection"];
/**
* Returns a function that convert objects coming from CQL/parser.js --> read function
- * into ogc filter
+ * into XML OGC filter
* @param {object} filterBuilder The FilterBuilder instance to use for this conversion.
* @example
* const cqlFilter = "property = 'value";
@@ -26,16 +28,41 @@ const operators = {
* const ogcFilter = toOgcFiler(filterObject);
* // ogcFilter --> "propertyvalue"
*/
-const fromObject = (filterBuilder = {}) => ({type, filters = [], value, property, lowerBoundary, upperBoundary }) => {
+const fromObject = (filterBuilder = {}) => ({type, filters = [], args = [], name, value, ...rest }) => {
+ if (type === "literal") {
+ return filterBuilder.literal(value);
+ }
+ if (type === "property") {
+ return filterBuilder.propertyName(name);
+ }
if (includes(logical, type)) {
return filterBuilder[type](
...filters.map(fromObject(filterBuilder))
);
}
if (includes(cql, type)) {
- return "";
+ return ""; // TODO: implement in filterBuilder as empty filter
+ }
+ if (includes(Object.keys(operators), type)) {
+ return filterBuilder.operations[operators[type]](...args.map(fromObject(filterBuilder)));
+ }
+ if (includes(filterBuilder.operators, type)) {
+ return filterBuilder.operations[type](...args.map(fromObject(filterBuilder)));
+ }
+ if (includes(geometryTypes, type)) {
+ return filterBuilder.geometry({
+ type,
+ ...rest
+ });
+ }
+
+ if (includes(spatial, type.toLowerCase())) {
+ return filterBuilder.operations[type.toLowerCase()](name, ...args.map(fromObject(filterBuilder)));
+ }
+ if (typeof filterBuilder[type] === "function") {
+ return filterBuilder[type](name, ...args.map(fromObject(filterBuilder)));
}
- return filterBuilder.property(property)[operators[type]](isNil(value) ? lowerBoundary : value, upperBoundary);
+ throw new Error(`Filter type ${type} not supported`);
};
export default fromObject;
diff --git a/web/client/utils/ogc/Filter/operators.js b/web/client/utils/ogc/Filter/operators.js
index 810ab8161d..14e8341f26 100644
--- a/web/client/utils/ogc/Filter/operators.js
+++ b/web/client/utils/ogc/Filter/operators.js
@@ -36,6 +36,14 @@ const valueReference = (ns, name) => `<${ns}:ValueReference>${name}${ns}:Value
const literal = (ns, value) => `<${ns}:Literal>${value}${ns}:Literal>`;
const lower = (ns, value) => `<${ns}:LowerBoundary>${value}${ns}:LowerBoundary>`;
const upper = (ns, value) => `<${ns}:UpperBoundary>${value}${ns}:UpperBoundary>`;
+
+/**
+ * This function is used to apply multiple operations to the same content.
+ * @param {string} ns namespace
+ * @param {function} op operation function
+ * @param {string|Array} content content
+ * @returns the operation result
+ */
const multiop = (ns, op, content) => op(ns, Array.isArray(content) ? content.join("") : content);
const logical = {
and: (ns, content, ...other) => other && other.length > 0 ? multiop(ns, ogcLogicalOperators.AND, [content, ...other]) : multiop(ns, ogcLogicalOperators.AND, content),
@@ -44,6 +52,7 @@ const logical = {
nor: (ns, content, ...other) => other && other.length > 0 ? multiop(ns, ogcLogicalOperators.NOR, [content, ...other]) : multiop(ns, ogcLogicalOperators.NOR, content)
};
+const ogcFunc = (ns, name, content = []) => `<${ns}:Function name="${name}">${Array.isArray(content) ? content.join("") : content}${ns}:Function>`;
const spatial = {
intersects: (ns, ...args) => multiop(ns, ogcSpatialOperators.INTERSECTS, args),
@@ -65,6 +74,7 @@ const comparison = {
ilike: (ns, ...args) => multiop(ns, ogcComparisonOperators.ilike, args),
isNull: (ns, ...args) => multiop(ns, ogcComparisonOperators.isNull, args)
};
+const func = (ns, name, ...args) => ogcFunc(ns, name, args);
module.exports = {
ogcComparisonOperators,
@@ -78,5 +88,6 @@ module.exports = {
spatial,
comparison,
lower,
- upper
+ upper,
+ func
};
diff --git a/web/client/utils/ogc/WFS/RequestBuilder.js b/web/client/utils/ogc/WFS/RequestBuilder.js
index cfc185e46d..aac43be6a4 100644
--- a/web/client/utils/ogc/WFS/RequestBuilder.js
+++ b/web/client/utils/ogc/WFS/RequestBuilder.js
@@ -49,7 +49,7 @@ const getStaticAttributesWFS2 = (ver) => 'service="WFS" version="' + ver + '" '
* filter(
* and(
* property("P1").equals("v1"),
- * proprety("the_geom").intersects(geoJSONGeometry)
+ * property("the_geom").intersects(geoJSONGeometry)
* )
* ),
* {srsName="EPSG:4326"} // 3rd for query is optional
diff --git a/web/client/utils/ogc/WKT/__tests__/toGeoJSON-test.js b/web/client/utils/ogc/WKT/__tests__/toGeoJSON-test.js
new file mode 100644
index 0000000000..bdbe0d53a6
--- /dev/null
+++ b/web/client/utils/ogc/WKT/__tests__/toGeoJSON-test.js
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2023, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import expect from 'expect';
+import toGeoJSON from '../toGeoJSON';
+const WKT_TESTS = [{
+ wkt: 'POINT(30 10)',
+ geojson: {
+ type: 'Point',
+ coordinates: [30, 10]
+ }
+}, {
+ wkt: 'LINESTRING(30 10, 10 30, 40 40)',
+ geojson: {
+ type: 'LineString',
+ coordinates: [[30, 10], [10, 30], [40, 40]]
+ }
+}, {
+ wkt: 'POLYGON((30 10, 40 40, 20 40, 10 20, 30 10))',
+ geojson: {
+ type: 'Polygon',
+ coordinates: [[[30, 10], [40, 40], [20, 40], [10, 20], [30, 10]]]
+ }
+},
+// MULTIPOINT HAS 2 EQUIVALENT FORMS
+{
+ wkt: 'MULTIPOINT((10 40), (40 30), (20 20), (30 10))',
+ geojson: {
+ type: 'MultiPoint',
+ coordinates: [[10, 40], [40, 30], [20, 20], [30, 10]]
+ }
+},
+{
+ wkt: 'MULTIPOINT(10 40, 40 30, 20 20, 30 10)',
+ geojson: {
+ type: 'MultiPoint',
+ coordinates: [[10, 40], [40, 30], [20, 20], [30, 10]]
+ }
+},
+{
+
+ wkt: 'MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))',
+ geojson: {
+ type: 'MultiLineString',
+ coordinates: [[[10, 10], [20, 20], [10, 40]], [[40, 40], [30, 30], [40, 20], [30, 10]]]
+ }
+},
+{
+ wkt: 'MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))',
+ geojson: {
+ type: 'MultiPolygon',
+ coordinates: [
+ [[[30, 20], [45, 40], [10, 40], [30, 20]]],
+ [[[15, 5], [40, 10], [10, 20], [5, 10], [15, 5]
+ ]]
+ ]
+ }
+}, {
+ wkt: 'GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))',
+ geojson: {
+ type: 'GeometryCollection',
+ geometries: [{
+ type: 'Point',
+ coordinates: [4, 6]
+ }, {
+ type: 'LineString',
+ coordinates: [[4, 6], [7, 10]]
+ }]
+ }
+}];
+describe('WKT convertion to geoJSON (toGeoJSON)', function() {
+ WKT_TESTS.forEach((test) => {
+ it(test.wkt, () => {
+ expect(toGeoJSON(test.wkt)).toEqual(test.geojson);
+ });
+ });
+});
diff --git a/web/client/utils/ogc/WKT/index.js b/web/client/utils/ogc/WKT/index.js
new file mode 100644
index 0000000000..075e85df7f
--- /dev/null
+++ b/web/client/utils/ogc/WKT/index.js
@@ -0,0 +1 @@
+export {default as toGeoJSON} from './toGeoJSON';
diff --git a/web/client/utils/ogc/WKT/toGeoJSON.js b/web/client/utils/ogc/WKT/toGeoJSON.js
new file mode 100644
index 0000000000..8095854b4f
--- /dev/null
+++ b/web/client/utils/ogc/WKT/toGeoJSON.js
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2023, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * Convert a WKT sub-string of geometries to a geoJSON Point
+ * @private
+ * @param {string} coordinates coordinates string
+ * @returns {object} geoJSON point
+ */
+const parsePoint = (coordinates) => {
+ const [x, y] = coordinates.split(' ').map(parseFloat);
+ return {
+ type: 'Point',
+ coordinates: [x, y]
+ };
+};
+
+/**
+ * Convert a WKT sub-string of geometries to a geoJSON LineString
+ * @private
+ * @param {string} coordinates coordinates string
+ * @returns {object} geoJSON LineString
+ */
+const parseLineString = (coordinates) => {
+ const points = coordinates.split(',').map(point => {
+ const [x, y] = point.trim().split(' ').map(parseFloat);
+ return [x, y];
+ });
+ return {
+ type: 'LineString',
+ coordinates: points
+ };
+};
+
+/**
+ * Convert a WKT sub-string of geometries to a geoJSON Polygon
+ * @private
+ * @param {string} coordinates coordinates string
+ * @returns {object} geoJSON Polygon
+ */
+const parsePolygon = (coordinates) => {
+ const rings = coordinates.split('),').map(ring => {
+ const points = ring.replace('(', '').trim().split(',').map(point => {
+ const [x, y] = point.trim().split(' ').map(parseFloat);
+ return [x, y];
+ });
+ return points;
+ });
+ return {
+ type: 'Polygon',
+ coordinates: rings
+ };
+};
+
+/**
+ * Convert a WKT sub-string of geometries to a geoJSON MultiPoint
+ * @private
+ * @param {string} coordinates coordinates string
+ * @returns {object} geoJSON MultiPoint
+ */
+const parseMultiPoint = (coordinates) => {
+ const points = coordinates.split(',').map(point => {
+ const [x, y] = point
+ /*
+ MultiPoint can have these forms. Remove the parenthesis to support both,
+ given that we are parsing one single point.
+ MULTIPOINT ((10 40), (40 30), (20 20), (30 10))
+ MULTIPOINT (10 40, 40 30, 20 20, 30 10)
+ */
+ .replace('(', '').replace(')', '')
+ .trim().split(' ').map(parseFloat);
+ return [x, y];
+ });
+ return {
+ type: 'MultiPoint',
+ coordinates: points
+ };
+};
+
+/**
+ * Convert a WKT sub-string of geometries to a geoJSON MultiLineString
+ * @private
+ * @param {string} coordinates coordinates string
+ * @returns {object} geoJSON MultiLineString
+ */
+const parseMultiLineString = (coordinates) => {
+ const lines = coordinates.split('),').map(line => {
+ const points = line.replace('(', '').trim().split(',').map(point => {
+ const [x, y] = point.trim().split(' ').map(parseFloat);
+ return [x, y];
+ });
+ return points;
+ });
+ return {
+ type: 'MultiLineString',
+ coordinates: lines
+ };
+};
+
+/**
+ * Convert a WKT sub-string of geometries to a geoJSON MultiPolygon
+ * @private
+ * @param {string} coordinates coordinates string
+ * @returns {object} geoJSON MultiPolygon
+ */
+const parseMultiPolygon = (coordinates) => {
+ const polygons = coordinates.split(')),').map(polygon => {
+ const rings = polygon.replace('(', '').trim().split('),').map(ring => {
+ const points = ring.replace('(', '').trim().split(',').map(point => {
+ const [x, y] = point.trim().split(' ').map(parseFloat);
+ return [x, y];
+ });
+ return points;
+ });
+ return rings;
+ });
+ return {
+ type: 'MultiPolygon',
+ coordinates: polygons
+ };
+};
+let toGeoJSON;
+
+/**
+ * Convert a WKT sub-string of geometries to a geoJSON GeometryCollection
+ * @private
+ * @param {string} coordinates coordinates string
+ * @returns {object} geoJSON GeometryCollection
+ */
+const parseGeometryCollection = (coordinates) => {
+ const geometries = coordinates.split('),').map(geometry => {
+ const type = geometry.substring(0, geometry.indexOf('(')).trim().toUpperCase();
+ const coords = geometry.substring(geometry.indexOf('(') + 1).trim();
+ return toGeoJSON(`${type}(${coords})`);
+ });
+ return {
+ type: 'GeometryCollection',
+ geometries: geometries
+ };
+};
+
+toGeoJSON = (rawWkt) => {
+ // Remove any leading or trailing white spaces from the WKT
+ let wkt = rawWkt.trim();
+
+ // Determine the geometry type based on the initial keyword
+ const type = wkt.substring(0, wkt.indexOf('(')).trim().toUpperCase();
+
+ // Extract the coordinates from the inner part of the WKT
+ const coordinates = wkt.substring(wkt.indexOf('(') + 1, wkt.lastIndexOf(')')).trim();
+
+ // Parse the coordinates based on the geometry type
+ let result;
+ switch (type) {
+ case 'POINT':
+ result = parsePoint(coordinates);
+ break;
+ case 'LINESTRING':
+ result = parseLineString(coordinates);
+ break;
+ case 'POLYGON':
+ result = parsePolygon(coordinates);
+ break;
+ // Add support for additional geometry types here
+ case 'MULTIPOINT':
+ result = parseMultiPoint(coordinates);
+ break;
+ case 'MULTILINESTRING':
+ result = parseMultiLineString(coordinates);
+ break;
+ case 'MULTIPOLYGON':
+ result = parseMultiPolygon(coordinates);
+ break;
+ case 'GEOMETRYCOLLECTION':
+ result = parseGeometryCollection(coordinates);
+ break;
+ default:
+ throw new Error(`Not supported geometry: ${type}`);
+ }
+
+ return result;
+};
+/**
+ * Convert a WKT sub-string of geometries to a geoJSON geometry
+ * @name toGeoJSON
+ * @memberof utils.ogc.Filter.WKT
+ * @param {string} wkt the wkt string
+ * @return {object} the geoJSON geometry
+ */
+export default toGeoJSON;