Skip to content

Commit 0bdfc05

Browse files
#694 Increase Querying Capabilities of LayersTool filters (#695)
* #694 Filter ORs p1 * #694 Increase Querying Capabilities of LayersTool filters
1 parent f7e72d5 commit 0bdfc05

File tree

6 files changed

+556
-104
lines changed

6 files changed

+556
-104
lines changed

API/Backend/Geodatasets/routes/geodatasets.js

Lines changed: 111 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,20 @@ function get(reqtype, req, res, next) {
7171
const filterSplit = req.query.filters.split(",");
7272
filters = [];
7373
filterSplit.forEach((f) => {
74-
const fSplit = f.split("+");
75-
filters.push({
76-
key: fSplit[0],
77-
op: fSplit[1],
78-
type: fSplit[2],
79-
value: fSplit[3],
80-
});
74+
if (f === "OR" || f === "AND" || f === "NOT") {
75+
filters.push({
76+
isGroup: true,
77+
op: f,
78+
});
79+
} else {
80+
const fSplit = f.split("+");
81+
filters.push({
82+
key: fSplit[0],
83+
op: fSplit[1],
84+
type: fSplit[2],
85+
value: fSplit[3],
86+
});
87+
}
8188
});
8289
}
8390
if (req.query.spatialFilter != null) {
@@ -238,68 +245,114 @@ function get(reqtype, req, res, next) {
238245
// Filters
239246
if (filters != null && filters.length > 0) {
240247
let filterSQL = [];
241-
filters.forEach((f, i) => {
242-
let fkey = f.key;
243-
let derivedKey = false;
244-
if (fkey === "Latitude (Centroid)") {
245-
fkey = `ST_Y(ST_Centroid(geom))`;
246-
derivedKey = true;
247-
} else if (fkey === "Longitude (Centroid)") {
248-
fkey = `ST_X(ST_Centroid(geom))`;
249-
derivedKey = true;
250-
}
248+
let currentGroupOp = null;
249+
let currentGroup = [];
251250

252-
replacements[`filter_key_${i}`] = fkey;
253-
replacements[`filter_value_${i}`] = f.value;
254-
let op = "=";
255-
switch (f.op) {
256-
case ">":
257-
op = ">";
258-
break;
259-
case "<":
260-
op = "<";
261-
break;
262-
case "in":
263-
op = "IN";
264-
break;
265-
case "=":
266-
default:
267-
break;
268-
}
269-
let value = "";
270-
if (op === "IN") {
271-
const valueSplit = f.value.split("$");
272-
const values = [];
273-
valueSplit.forEach((v) => {
274-
replacements[`filter_value_${i}_${v}`] = v;
275-
values.push(`:filter_value_${i}_${v}`);
276-
});
277-
value = `(${values.join(",")})`;
251+
filters.forEach((f, i) => {
252+
if (f.isGroup === true) {
253+
if (
254+
currentGroupOp != null &&
255+
currentGroupOp != f.op &&
256+
currentGroup.length > 0
257+
) {
258+
filterSQL.push(
259+
`${
260+
currentGroupOp == "NOT" ? "NOT " : ""
261+
}(${currentGroup.join(
262+
` ${currentGroupOp == "NOT" ? "AND" : f.op} `
263+
)})`
264+
);
265+
currentGroup = [];
266+
}
267+
currentGroupOp = f.op;
278268
} else {
269+
let fkey = f.key;
270+
let derivedKey = false;
271+
if (fkey === "Latitude (Centroid)") {
272+
fkey = `ST_Y(ST_Centroid(geom))`;
273+
derivedKey = true;
274+
} else if (fkey === "Longitude (Centroid)") {
275+
fkey = `ST_X(ST_Centroid(geom))`;
276+
derivedKey = true;
277+
}
278+
279+
replacements[`filter_key_${i}`] = fkey;
279280
replacements[`filter_value_${i}`] = f.value;
280-
value = `:filter_value_${i}`;
281-
}
282-
if (f.type === "number") {
283-
filterSQL.push(
284-
`${
281+
let op = "=";
282+
switch (f.op) {
283+
case ">":
284+
op = ">";
285+
break;
286+
case "<":
287+
op = "<";
288+
break;
289+
case "in":
290+
op = "IN";
291+
break;
292+
case "contains":
293+
case "beginswith":
294+
case "endswith":
295+
op = "LIKE";
296+
break;
297+
case "=":
298+
default:
299+
break;
300+
}
301+
let value = "";
302+
if (op === "IN") {
303+
const valueSplit = f.value.split("$");
304+
const values = [];
305+
valueSplit.forEach((v) => {
306+
replacements[`filter_value_${i}_${v}`] = v;
307+
values.push(`:filter_value_${i}_${v}`);
308+
});
309+
value = `(${values.join(",")})`;
310+
} else if (op === "LIKE") {
311+
if (f.op == "contains")
312+
replacements[`filter_value_${i}`] = `%${f.value}%`;
313+
else if (f.op == "beginswith")
314+
replacements[`filter_value_${i}`] = `${f.value}%`;
315+
else if (f.op == "endswith")
316+
replacements[`filter_value_${i}`] = `%${f.value}`;
317+
318+
value = `:filter_value_${i}`;
319+
} else {
320+
replacements[`filter_value_${i}`] = f.value;
321+
value = `:filter_value_${i}`;
322+
}
323+
if (f.type === "number" && op !== "LIKE") {
324+
const q1 = `${
285325
derivedKey === true
286326
? `${fkey}`
287327
: `(properties->>:filter_key_${i})`
288-
}::FLOAT ${op} ${value}`
289-
);
290-
} else {
291-
filterSQL.push(
292-
`${
328+
}::FLOAT ${op} ${value}`;
329+
if (currentGroupOp == null) filterSQL.push(q1);
330+
else currentGroup.push(q1);
331+
} else {
332+
const q2 = `${
293333
derivedKey === true
294334
? `${fkey}`
295335
: `properties->>:filter_key_${i}`
296-
} ${op} ${value}`
297-
);
336+
} ${op} ${value}`;
337+
if (currentGroupOp == null) filterSQL.push(q2);
338+
else currentGroup.push(q2);
339+
}
298340
}
299341
});
300-
q += `${
301-
q.indexOf(" WHERE ") === -1 ? " WHERE " : " AND "
302-
}${filterSQL.join(` AND `)}`;
342+
// Final group
343+
if (currentGroup.length > 0) {
344+
filterSQL.push(
345+
`${currentGroupOp == "NOT" ? "NOT " : ""}(${currentGroup.join(
346+
` ${
347+
currentGroupOp === "NOT" ? "AND" : currentGroupOp || "AND"
348+
} `
349+
)})`
350+
);
351+
}
352+
if (filterSQL.length > 0)
353+
q += `${
354+
q.indexOf(" WHERE ") === -1 ? " WHERE " : " AND "
355+
}${filterSQL.join(` AND `)}`;
303356
}
304357

305358
if (

public/helps/LayersTool-Filtering.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,44 @@ These queries allow you to filter features by property-value equivalencies and r
1010

1111
- The `Property` field auto-completes and must be one of the auto-completed values.
1212
- The `Operator` dropdown contains the following operators:
13-
- `=` - The feature's `Property` must equal `Value` to remain visible.
14-
- `in` - The feature's `Property` must equal _at least one of_ of the _comma-separated_ entries of `Value` to remain visible.
15-
- `<` - The feature's `Property` must be less than `Value`. If performed on a textual `Property`, the comparison becomes alphabetical.
16-
- `>` - The feature's `Property` must be greater than `Value`. If performed on a textual `Property`, the comparison becomes alphabetical.
13+
- `=` - _Equals._ The feature's `Property` must equal `Value` to remain visible.
14+
- `in` - _In List._ The feature's `Property` must equal _at least one of_ of the _comma-separated_ entries of `Value` to remain visible.
15+
- `<` - _Less Than._ The feature's `Property` must be less than `Value`. If performed on a textual `Property`, the comparison becomes alphabetical.
16+
- `>` - _Greater Than._ The feature's `Property` must be greater than `Value`. If performed on a textual `Property`, the comparison becomes alphabetical.
17+
- `[...]` - _Contains._ The feature's `Property` must contain `Value`. If performed on a numeric `Property`, its value is first parsed into a string.
18+
- `[...` - _Begins With._ The feature's `Property` must begin with `Value`. If performed on a numeric `Property`, its value is first parsed into a string.
19+
- `...]` - _Ends With._ The feature's `Property` must end with `Value`. If performed on a numeric `Property`, its value is first parsed into a string.
1720

1821
#### Add +
1922

20-
By default one property-value row is provide. To add more, click the top-right "Add +" button. Use a property-value row's right-most "X" to then remove the row.
23+
By default, one property-value row is provide. To add more, click the top-right "Add +" button. Use a property-value row's right-most "X" to then remove the row.
2124

22-
#### Logical Groupings (Parentheticals)
25+
#### Group +
2326

24-
In favoring simplicity, not all boolean logic queries are possible. There is no **NOT** operator. All property-value rows are **AND**ed together with the following exception:
27+
By default, all property-value rows are ANDed together. Adding Operator Groups and dragging to rearrange rows enables modifying this behavior. Each Group is still ANDed together with the other groups. Each Group ends when another Group starts. Groups cannot be nested.
2528

26-
- If there exists multiple instances of the same `Property`, only the `<` and `>` are **AND**ed together and the other operations are **OR**ed together. (See example below)
27-
28-
Rows of the same `Property` are color coded to better visually track this function. Row order has zero bearing on the derived parenthetical groupings.
29+
- `Match All (AND)` - All property-value rows beneath this row and up until the next Group row or up until the end are ANDed together - they must all be true for a feature to match the query.
30+
- `Match Any (OR)` - All property-value rows beneath this row and up until the next Group row or up until the end are ORed together - at least one match must be true for a feature to match the query.
31+
- `Match Inverse (NOT AND)` - All property-value rows beneath this row and up until the next Group row or up until the end are ANDed together and then negated - all matches must be false for a feature to match the query.
2932

3033
#### Example
3134

3235
```json
36+
group OR
3337
sol > 10
3438
sol < 20
3539
sol = 25
36-
drive = 0
40+
group AND
41+
site = 3
42+
drive = 1
43+
group NOT AND
44+
pose = 0
3745
```
3846

3947
_becomes:_
4048

4149
```
42-
((sol > 10 AND sol < 20) OR sol = 25) AND drive = 0
50+
(sol > 10 OR sol < 20 OR sol = 25) AND (site = 3 && drive = 1) AND NOT (pose = 0)
4351
```
4452

4553
### Spatial Queries

src/essence/Ancillary/LocalFilterer.js

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ const LocalFilterer = {
126126
// Perform the per row match
127127
for (let i = 0; i < filter.values.length; i++) {
128128
const v = filter.values[i]
129-
if (v && v.key != null) {
129+
if (v && v.isGroup === true) {
130+
} else if (v && v.key != null) {
130131
let featureValue =
131132
v.key === 'geometry.type'
132133
? feature.geometry.type
@@ -178,6 +179,34 @@ const LocalFilterer = {
178179
v.matches = true
179180
else v.matches = false
180181
break
182+
183+
case 'contains':
184+
if (
185+
String(featureValue).indexOf(
186+
String(filterValue)
187+
) != -1
188+
)
189+
v.matches = true
190+
else v.matches = false
191+
break
192+
case 'beginswith':
193+
if (
194+
String(featureValue).startsWith(
195+
String(filterValue)
196+
)
197+
)
198+
v.matches = true
199+
else v.matches = false
200+
break
201+
case 'endswith':
202+
if (
203+
String(featureValue).endsWith(
204+
String(filterValue)
205+
)
206+
)
207+
v.matches = true
208+
else v.matches = false
209+
break
181210
default:
182211
break
183212
}
@@ -188,6 +217,7 @@ const LocalFilterer = {
188217
}
189218
}
190219

220+
/*
191221
// Now group together all matching keys and process
192222
// Filter values with the same key are ORed together if = and ANDed if not
193223
// i.e. sol = 50, sol = 51 becomes sol == 50 OR sol == 51
@@ -235,6 +265,71 @@ const LocalFilterer = {
235265
236266
// If all are true
237267
return matches.filter(Boolean).length === matches.length
268+
*/
269+
270+
let fvalues = JSON.parse(JSON.stringify(filter.values))
271+
fvalues = fvalues.filter(Boolean)
272+
273+
if (filter.valuesOrder) {
274+
fvalues.sort((a, b) => {
275+
return (
276+
filter.valuesOrder.indexOf(a.id) -
277+
filter.valuesOrder.indexOf(b.id)
278+
)
279+
})
280+
}
281+
return LocalFilterer.evaluateMatchGroupings(fvalues)
282+
},
283+
evaluateMatchGroupings(conditions) {
284+
const groups = []
285+
let currentOp = 'AND' // Default to AND if no op is provided
286+
let currentGroup = []
287+
let negateNextGroup = false
288+
289+
for (let i = 0; i < conditions.length; i++) {
290+
const item = conditions[i]
291+
292+
if (item.isGroup && item.op) {
293+
// Save the current group before switching op
294+
if (currentGroup.length > 0) {
295+
groups.push({
296+
op: currentOp,
297+
matches: currentGroup,
298+
})
299+
currentGroup = []
300+
}
301+
302+
currentOp = item.op
303+
} else if ('matches' in item) {
304+
currentGroup.push(item.matches)
305+
}
306+
}
307+
308+
// Push the final group
309+
if (currentGroup.length > 0) {
310+
groups.push({
311+
op: currentOp,
312+
matches: currentGroup,
313+
})
314+
}
315+
316+
// Evaluate each group
317+
const evaluatedGroups = groups.map((group) => {
318+
let result
319+
if (group.op === 'OR') {
320+
result = group.matches.some(Boolean)
321+
} else if (group.op === 'NOT') {
322+
result = !group.matches.every(Boolean)
323+
} else {
324+
// default to AND
325+
result = group.matches.every(Boolean)
326+
}
327+
328+
return result
329+
})
330+
331+
// Final result: AND all evaluated group results
332+
return evaluatedGroups.every(Boolean)
238333
},
239334
}
240335

0 commit comments

Comments
 (0)