Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map Filter search by resource layer feature #10827

Merged
merged 76 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
11d1b5c
includes geometries on feature props in getPopupData, re #10816
whatisgalen Apr 24, 2024
bc4bf3a
adds translation for map popup map-filter search, re #10816
whatisgalen Apr 24, 2024
a99651f
creates fn in map-filter to take in a feature, interrogate type, set …
whatisgalen Apr 24, 2024
1eebc61
adds link in popup footer to search near this feature, re #10816
whatisgalen Apr 24, 2024
074c511
passes in resourceid as arg to filterByFeatureGeom, re #10816
whatisgalen Apr 24, 2024
1d01240
takes in resourceid as arg in filterByFeatureGeom, re #10816
whatisgalen Apr 24, 2024
31ef333
rm for loop in geom.features, assumes 1 feature, re #10816
whatisgalen Apr 24, 2024
cf05766
simplify buffer assignment in filterByFeatureGeom, re #10816
whatisgalen Apr 24, 2024
54281be
makes filter feature geom on map static, un-editable, re #10816
whatisgalen Apr 24, 2024
0a2f425
alternates map-filter payload with featureid and resourceid to avoid …
whatisgalen Apr 24, 2024
5ab58d6
moves geoshape_query build logic into own method in map_filter, re #1…
whatisgalen Apr 24, 2024
1b1a573
imports other dsl builder methods into map_filter, re #10816
whatisgalen Apr 24, 2024
2922ce5
creates 2nd control flow for payload of featureid, resourceid into ma…
whatisgalen Apr 24, 2024
ea387b5
refactors standard spatial_query logic to use helper method create_ge…
whatisgalen Apr 24, 2024
f075998
creates alternate flow if no resourceid supplied to filterByGeom, sim…
whatisgalen Apr 25, 2024
1c4d836
moves duplicative code into helper method, alters search_query in pla…
whatisgalen Apr 25, 2024
f5181b1
refactors method signature to reflect inplace alteration, re #10816
whatisgalen Apr 25, 2024
2a9266d
writes test for spatial filter by featureid, resourceid, re #10816
whatisgalen Apr 27, 2024
4095957
ensure pre-assigned featureids not overwritten in datatype transform …
whatisgalen Apr 27, 2024
3c9878f
checks for geometries.length in map_popup before passing args, re #10816
whatisgalen Apr 27, 2024
805693e
minor cleanup of logic in filterByFeatureGeom, re #10816
whatisgalen Apr 27, 2024
dfb65e3
fixes polygon format in search_test, re #10816
whatisgalen Apr 28, 2024
cb3140d
minor tweak to search tests, re #10816
whatisgalen Apr 29, 2024
f9fac10
moves popup feature handling into map popup provider utils for easier…
whatisgalen Apr 30, 2024
37a5d45
refactors mapFilter filterByFeatureGeom, expecting map popup provider…
whatisgalen Apr 30, 2024
4dc361a
indent child comment ko blocks to improve readability, re #10816
whatisgalen Apr 30, 2024
57d6742
cleanup map popup provider method, re #10816
whatisgalen Apr 30, 2024
56b61de
minor optimize in featureid assignment, re #10787
whatisgalen May 2, 2024
dcbe236
changes transltn to match functionality descrip, re #10816
whatisgalen May 10, 2024
6afe8ca
migrates visibility logic of filter by feature to map popup provider,…
whatisgalen May 10, 2024
f8e4f48
include showFilterByFeature on feature props in map vm, re #10816
whatisgalen May 10, 2024
eab6138
rm unneeded draw.setup in mapfilter.clear, re #10816
whatisgalen May 10, 2024
9dda454
camelcase correction in method name sendFeatureToMapFilter, re #10816
whatisgalen May 10, 2024
a6de407
enforce consistent param name, re #10816
whatisgalen May 10, 2024
24a7299
accomodates single feature map query on restoreState, re #10816
whatisgalen May 10, 2024
1674617
enables feature geom editing if map popup feature not from resource-l…
whatisgalen May 10, 2024
820a911
grabs the correct feature based on id match in feature-based spatial …
whatisgalen May 11, 2024
387752c
move spatial feature tests into own module, re #10816
whatisgalen May 11, 2024
89e64d1
moves up mutate of search_results_obj earlier, adds buffered geom if …
whatisgalen May 11, 2024
ae5b220
includes commented implementation of turfjs polygon simplification, r…
whatisgalen May 11, 2024
2fb80c9
includes documentation for popup provider method, re #10816
whatisgalen May 11, 2024
bcab5d0
pass in idx of corresponding geom.feature to sendFeatureToMapFilter, …
whatisgalen May 13, 2024
91b1e98
rm unused commented code, move to docs, re #10816
whatisgalen May 13, 2024
8868cbd
selects geometries.feature corresponding to the feature clicked in po…
whatisgalen May 13, 2024
fcbfcd8
change default buffer behavior in filterByFeatureGeom, re #10816
whatisgalen May 13, 2024
cecdc2c
adds check for feature-filter geom match before access, query mod, re…
whatisgalen May 13, 2024
ddc814c
clarifies method docs in map-popup-provider, re #10816
whatisgalen May 13, 2024
6b67f99
rm refs to featureid, resourceid in filter by feature, re #10816
whatisgalen Jun 7, 2024
53918a1
alter trans text for filter by feature, re #10816
whatisgalen Jun 7, 2024
e13e233
merges conflict for latest linting styling, re #10816
whatisgalen Jun 11, 2024
f30d648
Merge branch 'archesproject-dev/7.6.x' into 10816_popup_map_filter
whatisgalen Jun 18, 2024
e1ea2f3
rm refs to popupFeatureObject index, re #10816
whatisgalen Jun 18, 2024
f8b0ba5
merge conflict from latest in dev/7.6.x
whatisgalen Aug 14, 2024
f18555c
find specific feature in multi-feature geom tiles, re #10816
whatisgalen Aug 15, 2024
52aad84
run black against map_filter.py
whatisgalen Aug 15, 2024
9782f7f
run black on spatial_search_tests.py
whatisgalen Aug 15, 2024
5f6717d
fix merge conflict with latest, clean up logic, re #10816
whatisgalen Aug 22, 2024
ede08dd
rm unneeded test for feature-less map-filter req, refactor get_respon…
whatisgalen Aug 22, 2024
ae1adfb
refactor get_response_json into utils, change kwarg names in search_t…
whatisgalen Aug 22, 2024
aa2fba0
clear filter geoms every time filterByFeatureGeom called, re #10816
whatisgalen Aug 22, 2024
84a16ac
fix merge conflict against latest dev/7.6.x
whatisgalen Aug 29, 2024
5672e47
restore setting feature_geom onto query_object.map_filter, re #10816
whatisgalen Aug 30, 2024
43a3e4f
Merge branch 'archesproject-dev/7.6.x' into 10816_popup_map_filter
whatisgalen Sep 18, 2024
dc23575
Update arches/app/media/js/utils/map-popup-provider.js
whatisgalen Sep 19, 2024
d85c4d2
Update arches/app/media/js/utils/map-popup-provider.js
whatisgalen Sep 19, 2024
dea8266
fix merge conflict, re #10816
whatisgalen Sep 20, 2024
3d84a0f
Merge branch '10816_popup_map_filter' of https://github.com/archespro…
whatisgalen Sep 20, 2024
0b735eb
update get_response_json helper method in tests, re #10816
whatisgalen Sep 20, 2024
43348ae
fix typo in const, add null check for foundFeature, re #10816
whatisgalen Sep 20, 2024
75acb85
update trans text for filterByFeature, re #10816
whatisgalen Sep 21, 2024
54d79d5
change filterByFeature icon to fa-search, re #10816
whatisgalen Sep 21, 2024
d0da3dd
duplicate featureid match find logic in the show method, re #10816
whatisgalen Sep 21, 2024
9b67cbc
add check for undefined featureid on popupfeature, refactor duplicate…
whatisgalen Sep 22, 2024
a90dfaa
fix "this" reference error, re #10816
apeters Sep 23, 2024
98e92dd
nit, re #10816
apeters Sep 23, 2024
3665ef0
only show filter by feature button on the seach page, re #10816
apeters Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions arches/app/datatypes/core/geojson_feature_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,11 @@ def check_geojson_value(self, value):
feature["geometry"]
)
for new_feature in new_collection["features"]:
new_feature["id"] = (
geojson["id"] if "id" in geojson else str(uuid.uuid4())
)
new_feature["id"] = geojson.get("id", str(uuid.uuid4()))
features = features + new_collection["features"]
else:
# keep the feature id if it exists, or generate a fresh one.
feature["id"] = (
feature["id"] if "id" in feature else str(uuid.uuid4())
)
feature["id"] = feature.get("id", str(uuid.uuid4()))
features.append(feature)
geojson["features"] = features
return geojson
Expand Down
34 changes: 33 additions & 1 deletion arches/app/media/js/utils/map-popup-provider.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
define(['arches',
define([
'arches',
'knockout',
'templates/views/components/map-popup.htm'
], function(arches, ko, popupTemplate) {
Expand Down Expand Up @@ -40,6 +41,37 @@ define(['arches',
return features;
},

/**
* This method enables custom logic for how the feature in the popup should be handled and/or mutated en route to the mapFilter.
* @param popupFeatureObject - the javascript object of the feature and its associated contexts (e.g. mapCard).
* @required @method mapCard.filterByFeatureGeom()
* @required @send argument: @param feature - a geojson feature object
*/
sendFeatureToMapFilter: function(popupFeatureObject)
{
let foundFeature = null;
whatisgalen marked this conversation as resolved.
Show resolved Hide resolved
const strippedFeatureId = popupFeatureObject.feature.properties.featureid.replace(/-/g,"");
for (let geometry of popupFeatureObject.geometries()) {
if (geometry.geom && Array.isArray(geometry.geom.features)) {
foundFeature = geometry.geom.features.find(feature => feature.id.replace(/-/g, "") === strippedFeatureId);
if (foundFeature)
break;
}
}
if (foundFeature)
popupFeatureObject.mapCard.filterByFeatureGeom(foundFeature);
},

/**
* Determines whether to show the button for Filter By Feature
* @param popupFeatureObject - the javascript object of the feature and its associated contexts (e.g. mapCard).
* @returns {boolean} - whether to show "Filter by Feature" on map popup
* typically dependent on at least 1 feature with a geometry and/or a featureid/resourceid combo
*/
showFilterByFeature: function(popupFeatureObject) {
return (ko.unwrap(popupFeatureObject.geometries) || []).length > 0;
},

};
return provider;
});
5 changes: 4 additions & 1 deletion arches/app/media/js/viewmodels/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,17 @@ define([
var data = feature.properties;
var id = data.resourceinstanceid;
data.showEditButton = self.canEdit;
const descriptionProperties = ['displayname', 'graph_name', 'map_popup'];
data.sendFeatureToMapFilter = mapPopupProvider.sendFeatureToMapFilter;
data.showFilterByFeature = mapPopupProvider.showFilterByFeature;
const descriptionProperties = ['displayname', 'graph_name', 'map_popup', 'geometries'];
if (id) {
if (!self.resourceLookup[id]){
data = _.defaults(data, {
'loading': true,
'displayname': '',
'graph_name': '',
'map_popup': '',
'geometries': [],
'feature': feature,
});
if (data.permissions) {
Expand Down
12 changes: 12 additions & 0 deletions arches/app/media/js/views/components/search/map-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,18 @@ define([
}
}, this);

this.filterByFeatureGeom = function(feature) {
if (feature.geometry.type == 'Point' && this.buffer() == 0) { this.buffer(25); }
self.searchGeometries.removeAll();
this.draw.deleteAll();
this.draw.set({
bferguso marked this conversation as resolved.
Show resolved Hide resolved
"type": "FeatureCollection",
"features": [feature]
});
self.searchGeometries([feature]);
self.updateFilter();
};

var updateSearchResultPointLayer = function() {
var pointSource = self.map().getSource('search-results-points');
var agg = ko.unwrap(self.searchAggregations);
Expand Down
116 changes: 70 additions & 46 deletions arches/app/search/components/map_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@
from django.utils.translation import gettext as _
from arches.app.models.system_settings import settings
from arches.app.utils.betterJSONSerializer import JSONSerializer, JSONDeserializer
from arches.app.search.elasticsearch_dsl_builder import Bool, Nested, Terms, GeoShape
from arches.app.search.elasticsearch_dsl_builder import (
Bool,
Match,
Query,
Nested,
Term,
Terms,
GeoShape,
)
from arches.app.search.components.base import BaseSearchFilter
from arches.app.search.search_engine_factory import SearchEngineFactory
from arches.app.search.mappings import RESOURCES_INDEX

logger = logging.getLogger(__name__)

Expand All @@ -27,61 +37,30 @@ def append_dsl(self, search_query_object, **kwargs):
permitted_nodegroups = kwargs.get("permitted_nodegroups")
include_provisional = kwargs.get("include_provisional")
search_query = Bool()
querystring_params = kwargs.get("querystring", "")
querystring_params = kwargs.get("querystring", "{}")
spatial_filter = JSONDeserializer().deserialize(querystring_params)
if "features" in spatial_filter:
if len(spatial_filter["features"]) > 0:
feature_geom = spatial_filter["features"][0]["geometry"]
feature_properties = {}
if "properties" in spatial_filter["features"][0]:
feature_properties = spatial_filter["features"][0]["properties"]
buffer = {"width": 0, "unit": "ft"}
if "buffer" in feature_properties:
buffer = feature_properties["buffer"]
search_buffer = _buffer(feature_geom, buffer["width"], buffer["unit"])
feature_geom = JSONDeserializer().deserialize(search_buffer.geojson)
geoshape = GeoShape(
field="geometries.geom.features.geometry",
type=feature_geom["type"],
coordinates=feature_geom["coordinates"],
)

invert_spatial_search = False
if "inverted" in feature_properties:
invert_spatial_search = feature_properties["inverted"]

spatial_query = Bool()
if invert_spatial_search is True:
spatial_query.must_not(geoshape)
else:
spatial_query.filter(geoshape)

# get the nodegroup_ids that the user has permission to search
spatial_query.filter(
Terms(field="geometries.nodegroup_id", terms=permitted_nodegroups)
buffered_feature_geom = add_geoshape_query_to_search_query(
feature_geom,
feature_properties,
permitted_nodegroups,
include_provisional,
search_query,
)
search_query_object["query"].add_query(search_query)

if include_provisional is False:
spatial_query.filter(
Terms(field="geometries.provisional", terms=["false"])
)

elif include_provisional == "only provisional":
spatial_query.filter(
Terms(field="geometries.provisional", terms=["true"])
)

search_query.filter(Nested(path="geometries", query=spatial_query))

search_query_object["query"].add_query(search_query)

if self.componentname not in search_query_object:
search_query_object[self.componentname] = {}

try:
search_query_object[self.componentname]["search_buffer"] = feature_geom
except NameError:
logger.info(_("Feature geometry is not defined"))
# Add the buffered feature geometry to the search query object
if self.componentname not in search_query_object:
search_query_object[self.componentname] = {}
search_query_object[self.componentname][
"search_buffer"
] = buffered_feature_geom


def _buffer(geojson, width=0, unit="ft"):
Expand Down Expand Up @@ -111,3 +90,48 @@ def _buffer(geojson, width=0, unit="ft"):
res = cursor.fetchone()
geom = GEOSGeometry(res[0], srid=4326)
return geom


def add_geoshape_query_to_search_query(
feature_geom,
feature_properties,
permitted_nodegroups,
include_provisional,
search_query,
):

buffer = {"width": 0, "unit": "ft"}
if "buffer" in feature_properties:
buffer = feature_properties["buffer"]
# feature_geom = spatial_filter["features"][0]["geometry"]
search_buffer = _buffer(feature_geom, int(buffer["width"]), buffer["unit"])
feature_geom = JSONDeserializer().deserialize(search_buffer.geojson)
geoshape = GeoShape(
field="geometries.geom.features.geometry",
type=feature_geom["type"],
coordinates=feature_geom["coordinates"],
)
invert_spatial_search = False
if "inverted" in feature_properties:
invert_spatial_search = feature_properties["inverted"]

spatial_query = Bool()
if invert_spatial_search is True:
spatial_query.must_not(geoshape)
else:
spatial_query.filter(geoshape)

# get the nodegroup_ids that the user has permission to search
spatial_query.filter(
Terms(field="geometries.nodegroup_id", terms=permitted_nodegroups)
)

if include_provisional is False:
spatial_query.filter(Terms(field="geometries.provisional", terms=["false"]))

elif include_provisional == "only provisional":
spatial_query.filter(Terms(field="geometries.provisional", terms=["true"]))

search_query.filter(Nested(path="geometries", query=spatial_query))

return feature_geom
1 change: 1 addition & 0 deletions arches/app/templates/javascript.htm
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@
map-add-line='{% trans "Add line" as mapAddLine %} "{{ mapAddLine|escapejs }}"'
map-add-polygon='{% trans "Add polygon" as mapAddPolygon %} "{{ mapAddPolygon|escapejs }}"'
map-select-drawing='{% trans "Select drawing" as mapSelectDrawing %} "{{ mapSelectDrawing|escapejs }}"'
filter-by-feature='{% trans "Add Feature to Map Filter" as filterByFeature %} "{{ filterByFeature|escapejs }}"'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently only one map geometry or feature can be used as a geometry filter. Perhaps this should read "Filter by Map Feature" instead? The term "Add" is somewhat confusing since it will clear any existing geometry filters.

related-instance-map-sources='{% trans "Related instance map sources" as relatedInstanceMapSources %} "{{ relatedInstanceMapSources|escapejs }}"'
related-instance-map-source-layers='{% trans "Related instance map source layers (optional)" as relatedInstanceMapSourceLayers %} "{{ relatedInstanceMapSourceLayers|escapejs }}"'
intersection-layer-configuration='{% trans "Intersection layer configuration" as intersectionLayerConfiguration %} "{{ intersectionLayerConfiguration|escapejs }}"'
Expand Down
118 changes: 61 additions & 57 deletions arches/app/templates/views/components/map-popup.htm
Original file line number Diff line number Diff line change
Expand Up @@ -9,65 +9,69 @@
<!--/ko-->

<!-- ko if: !loading() -->
<!-- ko foreach: popupFeatures -->
<!-- ko if: active -->
<!-- ko if: displayname -->
<div class="hover-feature-title-bar">
{% block title %}
<div style="display: flex;">
<div data-bind="visible: $parent.popupFeatures.length > 1, click: function(){$parent.advanceFeature('left')}" class="hover-feature-nav-left"><i class="fa fa-angle-left"></i></div>
<div data-bind="visible: $parent.popupFeatures.length > 1, click: function(){$parent.advanceFeature('right')}" class="hover-feature-nav-right"><i class="fa fa-angle-right"></i></div>
<div class="hover-feature-title" data-bind="text: displayname"></div>
</div>
{% endblock title %}
</div>
<!--/ko-->
<div class="hover-feature-body">
{% block body %}
<div class="hover-feature" data-bind="html: map_popup"></div>
<!-- ko if: resourceinstanceid -->
<div class="hover-feature-metadata-block">
<div class="hover-feature-metadata">
<span data-bind="text: $root.translations.resourceModel"></span>
<span data-bind="text: graph_name"></span>
<!-- ko foreach: popupFeatures -->
<!-- ko if: active -->
<!-- ko if: displayname -->
<div class="hover-feature-title-bar">
{% block title %}
<div style="display: flex;">
<div data-bind="visible: $parent.popupFeatures.length > 1, click: function(){$parent.advanceFeature('left')}" class="hover-feature-nav-left"><i class="fa fa-angle-left"></i></div>
<div data-bind="visible: $parent.popupFeatures.length > 1, click: function(){$parent.advanceFeature('right')}" class="hover-feature-nav-right"><i class="fa fa-angle-right"></i></div>
<div class="hover-feature-title" data-bind="text: displayname"></div>
</div>
{% endblock title %}
</div>
<div class="hover-feature-metadata">
<span data-bind="text: $root.translations.idString"></span>
<span data-bind="text: resourceinstanceid"></span>
<!--/ko-->
<div class="hover-feature-body">
{% block body %}
<div class="hover-feature" data-bind="html: map_popup"></div>
<!-- ko if: resourceinstanceid -->
<div class="hover-feature-metadata-block">
<div class="hover-feature-metadata">
<span data-bind="text: $root.translations.resourceModel"></span>
<span data-bind="text: graph_name"></span>
</div>
<div class="hover-feature-metadata">
<span data-bind="text: $root.translations.idString"></span>
<span data-bind="text: resourceinstanceid"></span>
</div>
</div>
<!--/ko-->
{% endblock body %}
</div>
<div class="hover-feature-footer">
<div>
{% block footer %}
<!-- ko if: resourceinstanceid -->
<a data-bind="click: function () {
window.open(reportURL + ko.unwrap(resourceinstanceid));
}" href="javascript:void(0)">
<i class="ion-document-text"></i>
<span data-bind="text: $root.translations.report"></span>
</a>
<!--/ko-->
<!-- ko if: showEditButton -->
<a data-bind="click: function () {
window.open(editURL + ko.unwrap(resourceinstanceid));
}" href="javascript:void(0)">
<i class="ion-ios-refresh-empty"></i>
<span data-bind="text: $root.translations.edit"></span>
</a>
<!--/ko-->
<a data-bind="click: function() {sendFeatureToMapFilter($data);}, visible: showFilterByFeature($data)" href="#">
<i class="fa fa-map-pin"></i>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using a funnel icon instead? (ion-filter icon) Or a magnifying icon, which is used as search elsewhere (fa fa-search)?

<span data-bind="text: $root.translations.filterByFeature"></span>
</a>
</div>
<!-- ko if: $parent.popupFeatures.length > 1 -->
<div style="display: flex; flex-direction: row; width: 60px; justify-content: space-evenly; font-weight: 500;">
<div class="hover-feature-instance-counter" data-bind="text: 1 + $parent.popupFeatures.findIndex(feature => feature.active());"></div>
<span data-bind="text: $root.translations.of"></span>
<div style="padding-left: 3px;" data-bind="text: $parent.popupFeatures.length"></div>
</div>
<!--/ko-->
{% endblock footer %}
</div>
</div>
<!--/ko-->
{% endblock body %}
</div>
<div class="hover-feature-footer">
<div>
{% block footer %}
<!-- ko if: resourceinstanceid -->
<a data-bind="click: function () {
window.open(reportURL + ko.unwrap(resourceinstanceid));
}" href="javascript:void(0)">
<i class="ion-document-text"></i>
<span data-bind="text: $root.translations.report"></span>
</a>
<!--/ko-->
<!-- ko if: showEditButton -->
<a data-bind="click: function () {
window.open(editURL + ko.unwrap(resourceinstanceid));
}" href="javascript:void(0)">
<i class="ion-ios-refresh-empty"></i>
<span data-bind="text: $root.translations.edit"></span>
</a>
<!--/ko-->
</div>
<!-- ko if: $parent.popupFeatures.length > 1 -->
<div style="display: flex; flex-direction: row; width: 60px; justify-content: space-evenly; font-weight: 500;">
<div class="hover-feature-instance-counter" data-bind="text: 1 + $parent.popupFeatures.findIndex(feature => feature.active());"></div>
<span data-bind="text: $root.translations.of"></span>
<div style="padding-left: 3px;" data-bind="text: $parent.popupFeatures.length"></div>
</div>
<!--/ko-->
{% endblock footer %}
</div>
<!--/ko-->
<!--/ko-->
<!--/ko-->
Loading