From e3c286f7aebe59a14d22d4a145635a2a4ea35d0a Mon Sep 17 00:00:00 2001 From: tariqksoliman Date: Tue, 27 May 2025 17:42:28 -0700 Subject: [PATCH] #693 Support default/preset filters for vector layers --- API/Backend/Geodatasets/routes/geodatasets.js | 35 +++- .../src/metaconfigs/layer-vector-config.json | 76 ++++++++ src/essence/Ancillary/LocalFilterer.js | 4 +- .../Basics/Layers_/Filtering/Filtering.css | 7 +- .../Basics/Layers_/Filtering/Filtering.js | 173 ++++++++++++------ src/essence/Tools/Layers/LayersTool.js | 2 + 6 files changed, 238 insertions(+), 59 deletions(-) diff --git a/API/Backend/Geodatasets/routes/geodatasets.js b/API/Backend/Geodatasets/routes/geodatasets.js index 94234add1..764580316 100644 --- a/API/Backend/Geodatasets/routes/geodatasets.js +++ b/API/Backend/Geodatasets/routes/geodatasets.js @@ -71,7 +71,7 @@ function get(reqtype, req, res, next) { const filterSplit = req.query.filters.split(","); filters = []; filterSplit.forEach((f) => { - if (f === "OR" || f === "AND" || f === "NOT") { + if (f === "OR" || f === "AND" || f === "NOT_AND" || f === "NOT_OR") { filters.push({ isGroup: true, op: f, @@ -257,9 +257,17 @@ function get(reqtype, req, res, next) { ) { filterSQL.push( `${ - currentGroupOp == "NOT" ? "NOT " : "" + currentGroupOp == "NOT_AND" || currentGroupOp == "NOT_OR" + ? "NOT " + : "" }(${currentGroup.join( - ` ${currentGroupOp == "NOT" ? "AND" : f.op} ` + ` ${ + currentGroupOp == "NOT_AND" + ? "AND" + : currentGroupOp == "NOT_OR" + ? "OR" + : currentGroupOp + } ` )})` ); currentGroup = []; @@ -286,6 +294,12 @@ function get(reqtype, req, res, next) { case "<": op = "<"; break; + case ">=": + op = ">="; + break; + case "<=": + op = "<="; + break; case "in": op = "IN"; break; @@ -294,6 +308,9 @@ function get(reqtype, req, res, next) { case "endswith": op = "LIKE"; break; + case "!=": + op = "!="; + break; case "=": default: break; @@ -342,9 +359,17 @@ function get(reqtype, req, res, next) { // Final group if (currentGroup.length > 0) { filterSQL.push( - `${currentGroupOp == "NOT" ? "NOT " : ""}(${currentGroup.join( + `${ + currentGroupOp == "NOT_AND" || currentGroupOp == "NOT_OR" + ? "NOT " + : "" + }(${currentGroup.join( ` ${ - currentGroupOp === "NOT" ? "AND" : currentGroupOp || "AND" + currentGroupOp === "NOT_AND" + ? "AND" + : currentGroupOp === "NOT_OR" + ? "OR" + : currentGroupOp || "AND" } ` )})` ); diff --git a/configure/src/metaconfigs/layer-vector-config.json b/configure/src/metaconfigs/layer-vector-config.json index b6555e478..96937c6ad 100644 --- a/configure/src/metaconfigs/layer-vector-config.json +++ b/configure/src/metaconfigs/layer-vector-config.json @@ -555,6 +555,82 @@ } ] }, + { + "name": "Filter", + "rows": [ + { + "name": "Filters", + "components": [ + { + "field": "variables.initialFilters", + "name": "Initial Filters", + "description": "Configures the layer's filters for displaying an initial custom subset of the data.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "type", + "name": "Type of Property", + "description": "If not a Group, whether the property's value should be treated as a string or as a number.", + "type": "dropdown", + "width": 2, + "options": ["string", "number"] + }, + { + "field": "key", + "name": "Property", + "description": "If not a Group, a field name from the properties object of each feature of this layer to be to construct a filter. Supports dot.notation for nested properties.", + "type": "text", + "width": 4 + }, + { + "field": "op", + "name": "Operator", + "description": "If not a Group, the operator to use between the 'Property' and 'Value'.", + "type": "dropdown", + "width": 2, + "options": [ + "=", + "!=", + ",", + "<", + ">", + "<=", + ">=", + "contains", + "beginswith", + "endswith" + ] + }, + { + "field": "value", + "name": "Value", + "description": "If not a Group, a value for the equation to operate on.", + "type": "text", + "width": 4 + }, + { + "field": "isGroup", + "name": "Is A Group", + "description": "A group contains all property-value rows beneath this row and up until the next Group row or up until the end. Groups themselves are always ANDed together but enables member rows within them to abide by a different operator. If this entry 'Is A Group', then the type, property, operator and values fields are ignored.", + "type": "switch", + "width": 6, + "defaultChecked": false + }, + { + "field": "groupOp", + "name": "Group Operator", + "description": "If 'Is A Group', the operator to use for members within the group. 'AND' ands all the group members together. 'OR' ors all the group members together. 'NOT_AND' ands all the group members together and then negates the evaluation. 'NOT_OR' ors all the group members together and then negates the evaluation.", + "type": "dropdown", + "width": 6, + "options": ["AND", "OR", "NOT_AND", "NOT_OR"] + } + ] + } + ] + } + ] + }, { "name": "Interface", "rows": [ diff --git a/src/essence/Ancillary/LocalFilterer.js b/src/essence/Ancillary/LocalFilterer.js index d706a7a3c..9b1653d0c 100644 --- a/src/essence/Ancillary/LocalFilterer.js +++ b/src/essence/Ancillary/LocalFilterer.js @@ -318,8 +318,10 @@ const LocalFilterer = { let result if (group.op === 'OR') { result = group.matches.some(Boolean) - } else if (group.op === 'NOT') { + } else if (group.op === 'NOT_AND') { result = !group.matches.every(Boolean) + } else if (group.op === 'NOT_OR') { + result = !group.matches.some(Boolean) } else { // default to AND result = group.matches.every(Boolean) diff --git a/src/essence/Basics/Layers_/Filtering/Filtering.css b/src/essence/Basics/Layers_/Filtering/Filtering.css index 6d3dcc9f0..a3768b135 100644 --- a/src/essence/Basics/Layers_/Filtering/Filtering.css +++ b/src/essence/Basics/Layers_/Filtering/Filtering.css @@ -168,7 +168,7 @@ font-size: 13px; } .layersTool_filtering_group_operator { - width: 190px; + width: 231px; text-align: center; } @@ -257,7 +257,10 @@ .layersTool_filtering_group_operator_select.op_or { background: var(--color-c2); } -.layersTool_filtering_group_operator_select.op_not { +.layersTool_filtering_group_operator_select.op_not_and { + background: var(--color-orange2); +} +.layersTool_filtering_group_operator_select.op_not_or { background: var(--color-p4); } .layersTool_filtering_group_operator_select .dropy__title span { diff --git a/src/essence/Basics/Layers_/Filtering/Filtering.js b/src/essence/Basics/Layers_/Filtering/Filtering.js index aab9b59bb..4e2ed25d5 100644 --- a/src/essence/Basics/Layers_/Filtering/Filtering.js +++ b/src/essence/Basics/Layers_/Filtering/Filtering.js @@ -23,6 +23,46 @@ const Filtering = { filters: {}, current: {}, mapSpatialLayer: null, + initialize: function () { + Object.keys(L_.layers.data).forEach((layerName) => { + const layerObj = L_.layers.data[layerName] + + if (layerObj == null || layerObj.type != 'vector') return + + let shouldInitiallySubmit = false + + let initialFilterValues = [] + if ( + Filtering.filters[layerName] == null && + layerObj?.variables?.initialFilters && + layerObj.variables.initialFilters.length > 0 + ) { + initialFilterValues = layerObj.variables.initialFilters + initialFilterValues.forEach((f, idx) => { + f.id = idx + if (f.isGroup === true) { + if (f.groupOp != null) f.op = f.groupOp + if (f.key != null) delete f.key + if (f.value != null) delete f.value + if (f.type != null) delete f.type + } else { + f.op = f.op || '=' + } + }) + + Filtering.filters[layerName] = Filtering.filters[layerName] || { + spatial: { + center: null, + radius: 0, + }, + values: initialFilterValues || [], + geojson: null, + } + + Filtering.submit(layerName) + } + }) + }, make: async function (container, layerName) { const layerObj = L_.layers.data[layerName] @@ -34,7 +74,6 @@ const Filtering = { radius: 0, }, values: [], - groups: [], geojson: null, } Filtering.current = { @@ -387,43 +426,7 @@ const Filtering = { // Submit $(`#layersTool_filtering_submit`).on('click', async () => { - // Update the desired order of values - const valuesOrder = [] - $('#layerTool_filtering_filters_list > li').each(function () { - const idx = $(this).attr('idx') - if (idx !== undefined) { - valuesOrder.push(parseInt(idx)) - } - }) - Filtering.filters[layerName].valuesOrder = valuesOrder - - Filtering.setSubmitButtonState(true) - $(`#layersTool_filtering_submit_loading`).addClass('active') - if (Filtering.current.type === 'vector') { - if (Filtering.current.needsToQueryGeodataset) { - GeodatasetFilterer.filter( - layerName, - Filtering.filters[layerName] - ) - } else { - LocalFilterer.filter( - layerName, - Filtering.filters[layerName] - ) - } - } else if (Filtering.current.type === 'query') { - await ESFilterer.filter( - layerName, - Filtering.filters[layerName], - Filtering.getConfig() - ) - } - - $(`#layersTool_filtering_submit_loading`).removeClass('active') - Filtering.setSubmitButtonState(false) - - if (Filtering.mapSpatialLayer) - Filtering.mapSpatialLayer.bringToFront() + Filtering.submit(layerName, true) }) // Clear @@ -522,14 +525,15 @@ const Filtering = { layerName )}_${id}` - const ops = ['AND', 'OR', 'NOT'] + const ops = ['AND', 'OR', 'NOT_AND', 'NOT_OR'] const opId = Math.max(ops.indexOf(options.op), 0) $(elmId).html( Dropy.construct( [ - `
Match All (AND)
`, - `
Match Any (OR)
`, - `
Match Inverse (NOT AND)
`, + `
All Must Match (AND)
`, + `
Any May Match (OR)
`, + `
Not All May Match (NOT AND)
`, + `
None Must Match (NOT OR)
`, ], 'op', opId, @@ -542,18 +546,27 @@ const Filtering = { switch (newOp) { case 'AND': $(elmId).removeClass('op_or') - $(elmId).removeClass('op_not') + $(elmId).removeClass('op_not_and') + $(elmId).removeClass('op_not_or') $(elmId).addClass('op_and') break case 'OR': $(elmId).removeClass('op_and') - $(elmId).removeClass('op_not') + $(elmId).removeClass('op_not_and') + $(elmId).removeClass('op_not_or') $(elmId).addClass('op_or') break - case 'NOT': + case 'NOT_AND': $(elmId).removeClass('op_and') $(elmId).removeClass('op_or') - $(elmId).addClass('op_not') + $(elmId).removeClass('op_not_or') + $(elmId).addClass('op_not_and') + break + case 'NOT_OR': + $(elmId).removeClass('op_and') + $(elmId).removeClass('op_or') + $(elmId).removeClass('op_not_and') + $(elmId).addClass('op_not_or') break default: break @@ -663,14 +676,14 @@ const Filtering = { }) $(elmId).on('blur', function (event) { - const property = - Filtering.filters[layerName].aggs[event.value || $(this).val()] + const val = event.value || $(this).val() + const property = Filtering.filters[layerName].aggs[val] if (property) { if ( Filtering.filters[layerName].values[id] && - Filtering.filters[layerName].values[id].key !== event.value + Filtering.filters[layerName].values[id].key !== val ) { - Filtering.filters[layerName].values[id].key = event.value + Filtering.filters[layerName].values[id].key = val Filtering.filters[layerName].values[id].type = property.type Filtering.updateValuesAutoComplete(id, layerName) Filtering.setSubmitButtonState(true) @@ -688,15 +701,29 @@ const Filtering = { layerName )}_${id}` - const ops = ['=', ',', '<', '>', 'contains', 'beginswith', 'endswith'] + const ops = [ + '=', + '!=', + ',', + '<', + '>', + '<=', + '>=', + 'contains', + 'beginswith', + 'endswith', + ] const opId = Math.max(ops.indexOf(options.op), 0) $(elmId).html( Dropy.construct( [ ``, + `
!=
`, `
in
`, ``, ``, + ``, + ``, ``, ``, ``, @@ -714,6 +741,50 @@ const Filtering = { // Value AutoComplete Filtering.updateValuesAutoComplete(id, layerName) }, + submit: async function (layerName, updateValuesOrder) { + const layerObj = L_.layers.data[layerName] + + // Update the desired order of values + if (updateValuesOrder) { + const valuesOrder = [] + $('#layerTool_filtering_filters_list > li').each(function () { + const idx = $(this).attr('idx') + if (idx !== undefined) { + valuesOrder.push(parseInt(idx)) + } + }) + Filtering.filters[layerName].valuesOrder = valuesOrder + } + + Filtering.setSubmitButtonState(true) + $(`#layersTool_filtering_submit_loading`).addClass('active') + if (layerObj.type === 'vector') { + // needsToQueryGeodataset (but pulled out so submit could be called standalone) + if ( + layerObj?.url.startsWith('geodatasets:') && + layerObj?.variables?.dynamicExtent === true && + layerObj?.variables?.getFeaturePropertiesOnClick === true + ) { + GeodatasetFilterer.filter( + layerName, + Filtering.filters[layerName] + ) + } else { + LocalFilterer.filter(layerName, Filtering.filters[layerName]) + } + } else if (layerObj.type === 'query') { + await ESFilterer.filter( + layerName, + Filtering.filters[layerName], + Filtering.getConfig() + ) + } + + $(`#layersTool_filtering_submit_loading`).removeClass('active') + Filtering.setSubmitButtonState(false) + + if (Filtering.mapSpatialLayer) Filtering.mapSpatialLayer.bringToFront() + }, makeFilterListSortable: function () { const listToSort = document.getElementById( 'layerTool_filtering_filters_list' diff --git a/src/essence/Tools/Layers/LayersTool.js b/src/essence/Tools/Layers/LayersTool.js index f526161ba..5add9b059 100644 --- a/src/essence/Tools/Layers/LayersTool.js +++ b/src/essence/Tools/Layers/LayersTool.js @@ -118,6 +118,8 @@ var LayersTool = { LayersTool.make(null, true) LayersTool.destroy() } + + Filtering.initialize() }, make: function (t, fromInit) { this.MMGISInterface = new interfaceWithMMGIS(fromInit)