Skip to content

Commit

Permalink
WFS Feature / Zoom to filtered results. (#3701)
Browse files Browse the repository at this point in the history
* WFS Feature / Zoom to filtered results.

    When indexing features, add 4 fields to store the coordinate of the
    bounding box of each features. On client side use min/max aggregations
    to retrieve the bounding box of all features matching filter.

    When ES support for geo_bounds aggregation will be implemented for geo_shape type
    See elastic/elasticsearch#7574
    The following would be better probably:

    ```
     "viewport" : {
       "geo_bounds" : {
         "field" : "location",
         "wrap_longitude" : true
       }
     }
    ```

* WFS Feature / Zoom to filtered results / Zoom when applying filters too.

* WFS Feature / Clean up sort issue. Do not zoom to on empty aggs.

* WFS / Only use regex filter on string type.

* WFS / Only use regex filter on string type.

* WFS / Zoom to point with a buffer.

* WFS / Fix heatmap index requests
  • Loading branch information
fxprunayre authored May 14, 2019
1 parent 603781d commit 24ee908
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 24 deletions.
12 changes: 12 additions & 0 deletions es/config/records.json
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,18 @@
"location": {
"type": "geo_point"
},
"bbox_xmin": {
"type": "double"
},
"bbox_xmax": {
"type": "double"
},
"bbox_ymin": {
"type": "double"
},
"bbox_ymax": {
"type": "double"
},
"harvesterId": {
"type": "keyword"
},
Expand Down
13 changes: 10 additions & 3 deletions web-ui/src/main/resources/catalog/components/index/IndexRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,12 @@
field.isTree ? FACET_TREE_ROWS : ROWS
}
};
// if (!field.isDateTime) {
if (field.idxName.match(/^ft_.*_s$/)) {
// ignore empty strings
// include/exclude settings as they can only be applied to string fields
facetParams[field.idxName].terms['exclude'] = '';
}
}
}
else {
Expand Down Expand Up @@ -494,6 +500,7 @@
var fields = [];
for (var fieldId in response.aggregations) {
if (fieldId.indexOf('_stats') > 0) break;
if (fieldId.indexOf('bbox_') === 0) continue;
var respAgg = response.aggregations[fieldId];
var reqAgg = requestParam.aggs[fieldId];

Expand Down Expand Up @@ -1012,15 +1019,15 @@
fieldsQ.push('+*' + v + '*');
});
}
if (this.initialParams.filter) {
fieldsQ.push(this.initialParams.filter);
}

// Search for all if no filter defined
if (fieldsQ.length === 0) {
fieldsQ.push('*:*');
}

if (this.initialParams.filter != '') {
fieldsQ.push(this.initialParams.filter);
}

var filter = fieldsQ.join(' ');
qParam += filter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
},
facets: true,
stats: true,
excludedFields: ['geom', 'the_geom', 'ms_geometry',
'msgeometry', 'id_s', '_version_', 'featuretypeid', 'doctype']
excludedFields: [
'geom', 'the_geom', 'ms_geometry', 'msgeometry',
'bbox_xmin', 'bbox_ymin', 'bbox_xmax', 'bbox_ymax',
'id_s', '_version_', 'featuretypeid', 'doctype']
};
}]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
}, {
match_phrase: {
featureTypeId: {
query: featureType
query: encodeURIComponent(featureType)
}
}
}, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@
var indexObject, extentFilter;
scope.filterGeometry = undefined;

// Extent of current features matching the filter.
scope.featureExtent = undefined;

/**
* Init the directive when the scope.layer has changed.
* If the layer is given through the isolate scope object, the init
Expand Down Expand Up @@ -398,28 +401,35 @@
var filter = scope.facetFilters[facetName];
if (!filter) { return; }

// make the filter case insensitive, ie : abc => [aA][bB][cC]
filter = filter.replace(/./g, function(match) {
return '[' + match.toLowerCase() + match.toUpperCase() + ']';
});
// Regex filter can only be apply to string type.
if (facetName.match(/^ft_.*_s$/)) {
// make the filter case insensitive, ie : abc => [aA][bB][cC]
// only alpha regex
var lettersRegexOnly = /^[A-Za-z\u00C0-\u017F]+$/;
filter = filter.replace(/./g, function (match) {
var upperMatch = scope.accentify(match).toUpperCase();
var lowerMatch = scope.accentify(match).toLowerCase();
return lettersRegexOnly.test(match) ? '[' + lowerMatch + upperMatch + ']': match;
});

aggs[facetName] = {
terms: {
include: '.*' + filter + '.*'
}
};
aggs[facetName] = {
terms: {
include: '.*' + filter + '.*'
}
};
}
});

addBboxAggregation(aggs);

//indexObject is only available if Elastic is configured
if (indexObject) {
indexObject.searchWithFacets({
params: scope.output,
geometry: scope.filterGeometry
}, aggs).
then(function(resp) {
indexObject.pushState();
scope.fields = resp.facets;
scope.count = resp.count;
searchResponseHandler(resp);
angular.forEach(scope.fields, function(f) {
if (expandedFields.indexOf(f.name) >= 0) {
f.expanded = true;
Expand All @@ -429,6 +439,72 @@
}
};


// Compute bbox of returned object
// At some point we may be able to use geo_bounds aggregation
// when it is supported for geo_shape type
// See https://github.com/elastic/elasticsearch/issues/7574
// Eg.
// "viewport" : {
// "geo_bounds" : {
// "field" : "location",
// "wrap_longitude" : true
// }
// }
function addBboxAggregation(aggs) {
aggs['bbox_xmin'] = {'min': {'field': 'bbox_xmin'}};
aggs['bbox_ymin'] = {'min': {'field': 'bbox_ymin'}};
aggs['bbox_xmax'] = {'max': {'field': 'bbox_xmax'}};
aggs['bbox_ymax'] = {'max': {'field': 'bbox_ymax'}};
};

function setFeatureExtent(agg) {
scope.autoZoomToExtent = true;
if (scope.autoZoomToExtent
&& agg.bbox_xmin.value && agg.bbox_ymin.value
&& agg.bbox_xmax.value && agg.bbox_ymax.value) {
var isPoint = agg.bbox_xmin.value === agg.bbox_xmax.value
&& agg.bbox_ymin.value === agg.bbox_ymax.value,
radius = .05,
extent = [agg.bbox_xmin.value, agg.bbox_ymin.value,
agg.bbox_xmax.value, agg.bbox_ymax.value];

if (isPoint) {
var point = new ol.geom.Point([agg.bbox_xmin.value, agg.bbox_ymin.value]);
extent = new ol.extent.buffer(point.getExtent(), radius);
}
scope.featureExtent = ol.extent.applyTransform(extent,
ol.proj.getTransform("EPSG:4326", scope.map.getView().getProjection()));
}
};

scope.zoomToResults = function () {
scope.map.getView().fit(scope.featureExtent, scope.map.getSize());
};

scope.$watch('featureExtent', function(n, o) {
if (n && n !== o) {
scope.zoomToResults();
}
});


scope.accentify = function(str) {
var searchStr = str.toLocaleLowerCase()
var accents = {
a: 'àáâãäåæa',
c: 'çc',
e: 'èéêëæe',
i: 'ìíîïi',
n: 'ñn',
o: 'òóôõöøo',
s: 'ßs',
u: 'ùúûüu',
y: 'ÿy'
}
return accents.hasOwnProperty(searchStr) ? accents[searchStr] : str
}

scope.getMore = function(field) {
indexObject.getFacetMoreResults(field).then(function(response) {
field.values = response.facets[0].values;
Expand All @@ -453,15 +529,63 @@
scope.layer.set('esConfig', null);
scope.$broadcast('FiltersChanged');

// reset text search in facets
scope.facetFilters = {};

var aggs = {};
addBboxAggregation(aggs);

// load all facet and fill ui structure for the list
return indexObject.searchWithFacets({}).
return indexObject.searchWithFacets({}, aggs).
then(function(resp) {
indexObject.pushState();
scope.fields = resp.facets;
scope.count = resp.count;
});
searchResponseHandler(resp);
});
};

function searchResponseHandler(resp) {
indexObject.pushState();
scope.count = resp.count;
scope.fields = resp.facets;
scope.sortAggregation();
resp.indexData.aggregations &&
setFeatureExtent(resp.indexData.aggregations);
};

/**
* Each aggregations are sorted based as defined in the application profil config
* and the query is ordered based on this config.
*
* The values of each aggregations are sorted, checked first.
*/
scope.sortAggregation = function() {
// Disable sorting of aggregations by alpha order and based on expansion
// Order comes from application profile
// scope.fields.sort(function (a, b) {
// var aChecked = !!scope.output[a.name];
// var bChecked = !!scope.output[b.name];
// var aLabel = a.label;
// var bLabel = b.label;
// if ((aChecked && bChecked) || (!aChecked && !bChecked)) {
// return aLabel.localeCompare(bLabel);
// }
// return (aChecked === bChecked) ? 0 : aChecked ? -1 : 1;
// });

scope.fields.forEach(function (facette) {
facette.values.sort(function (a, b) {
var aChecked = scope.isFacetSelected(facette.name, a.value);
var bChecked = scope.isFacetSelected(facette.name, b.value);
if ((aChecked && bChecked) || (!aChecked && !bChecked)) {
if (gnSearchSettings.facetOrdering === 'alphabetical') {
return a.value.localeCompare(b.value);
}
return b.count - a.count;
}
return (aChecked === bChecked) ? 0 : aChecked ? -1 : 1;
})
})
}

/**
* alter form values & resend a search in case there are initial
* filters loaded from the context. This must only happen once
Expand All @@ -484,13 +608,17 @@
initialFilters.geometry[0][1];
}

var aggs = {};
addBboxAggregation(aggs);

// resend a search with initial filters to alter the facets
return indexObject.searchWithFacets({
params: initialFilters.qParams,
geometry: initialFilters.geometry
}).then(function(resp) {
}, aggs).then(function(resp) {
indexObject.pushState();
scope.fields = resp.facets;
scope.sortAggregation();
scope.count = resp.count;

// look for date graph fields; call onUpdateDate to refresh them
Expand Down Expand Up @@ -520,6 +648,8 @@
scope.previousFilterState.params = angular.merge({}, scope.output);
scope.previousFilterState.geometry = scope.ctrl.searchGeometry;

scope.zoomToResults();

var defer = $q.defer();
var sldConfig = wfsFilterService.createSLDConfig(scope.output);
var layer = scope.layer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.joda.time.DateTimeZone;
import org.joda.time.format.ISODateTimeFormat;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.geometry.BoundingBox;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -317,6 +318,15 @@ public void indexFeatures(Exchange exchange) throws Exception {
} else {
report.setPointOnlyForGeomsFalse();
}

// Populate bbox coordinates to be able to compute
// global bbox of search results
final BoundingBox bbox = feature.getBounds();
rootNode.put("bbox_xmin", bbox.getMinX());
rootNode.put("bbox_ymin", bbox.getMinY());
rootNode.put("bbox_xmax", bbox.getMaxX());
rootNode.put("bbox_ymax", bbox.getMaxY());

} else {
String value = attributeValue.toString();
rootNode.put(getDocumentFieldName(attributeName),
Expand Down

0 comments on commit 24ee908

Please sign in to comment.