I have a map which has a filter applied to a layer. We have a function refreshFilter(filterValue) which clears and then re-adds the filter with the new value.
e.g.
refreshFilter(filterValue) {
removeLayer('ourLayer');
addLayer({
'id': 'ourLayer',
'type': 'line',
'filter': ['all', ['==', 'sex', filterValue]]
}, 'marker');
}
This works as expected, and the features I see rendered from the GeoJson match the sex provided.
My problem comes when I want to return from the map, the features which match my filter.
I updated the refreshFilter function to return the features which were visible using queryRenderedFeatures but this always returns the opposite of what I'm expecting. So if pass in male I visually see the features which match sex=='male' (expected) but the queryRenderedFeatures function returns features which match sex=='female' (unexpected).
Simplified version
refreshFilter(filterValue) {
removeLayer('ourLayer');
addLayer({
'id': 'ourLayer',
'type': 'line',
'filter': ['all', ['==', 'sex', filterValue]]
}, 'marker');
return queryRenderedFeatures({layers: ['ourLayer']});
}
It seems to me that the call to queryRenderedFeatures hasn't waited for the layers to be changed and has queried the previous set of features to work out what to return.
Am I doing something wrong here? Any pointers greatly appreciated.
As an update I hacked a setTimeout round the queryRenderedFeatures call and this works. This would suggest that the update through addLayer is async. Having looked over the API I can't see a callback or anything to use to ensure the new filter has been applied.
Is these a recommended approach to handle this kind of situation?
Can you please create a minimal self-contained example that demonstrates the issue?
@jfirebaugh Ok, fixed it, here's the JSBin with the access token removed:
https://jsbin.com/binawol/edit?html,js,console,output
If you click the button it will output the values I pass into the filter property of addLayer and then after that, the properties.DEV_STATUS value from the features which queryRenderedFeatures returns.
Click it a few times to get the idea, you will notice that the features returned from queryRenderedFeatures are always one step the filters passed to addLayer.
You can wrap the console log part of the click handler in a setTimeout like this:
setTimeout(function() {
var features = map.queryRenderedFeatures({ layers: ['sites-outline'] }).map(function(feat) {
return feat.properties && feat.properties.DEV_STATUS;
});
console.log(features);
}, 500);
This then returns the expected results from the queryRenderedFeatures function.
Hope that makes sense, shout for more info,
@dougajmcdonald thank you for providing the example. The reason the results of your queryRenderedFeatures lags 'behind' the most recent layer settings is that adding a layer (or using map.setFilter to modify a layer's filter) is an asynchronous operation. We are discussing ways to improve the API of async methods like this, but in the meantime, you can check map.loaded() to determine whether the operation is complete. A common pattern for doing this without resorting to setTimeout is:
map.addLayer(...) // make your change
map.on('render', afterChangeComplete); // warning: this fires many times per second!
function afterChangeComplete () {
if (!map.loaded()) { return } // still not loaded; bail out.
// now that the map is loaded, it's safe to query the features:
map.queryRenderedFeatures(...);
map.off('render', afterChangeComplete); // remove this handler now that we're done.
}
update: As @dougajmcdonald notes below, be careful to use a named function (afterChangeComplete above), not .bind() or an anonymous function (function () {} or () => {}), for the render event handler you attach. Otherwise, the map.off(...) line will fail to remove the handler, as it won't refer to the same instance that was attached.
@dougajmcdonald I'm closing this issue, but if the above suggestion does not help, please feel free to contact Mapbox Support or reopen it!
Thanks for the info, this is what I suspected.
As a work around for my use case could I just query the source with the map bounds passed in? Or would I see the same kind of issue?
@anandthakker I just wanted to add one more note to this issue.
I got caught out by using either es6 fat arrow functions OR es5 bind calls when setting up a handler for the render event from inside an es6 class
If you use the following:
map.on('render', () => this.afterChangeComplete);
map.off('render', () => this.afterChangeComplete);
OR
map.on('render', this.afterChangeComplete.bind(this));
map.off('render', this.afterChangeComplete.bind(this));
Then the handler won't be removed as JS will create an anonomous function under the hood and the off call won't remove it.
@anandthakker I've been working with queryRenderedFeature to iterate through an array of previously undefined intersect point coordinates upon a current layer on the map. The returned feature array is intermittent and often incomplete and varies with zoom level. Am i experiencing the same issue as above. Heres an example bl.ock of whats going on. Sorry if this is a dead issue but I'm not sure where to turn. Thanks.
Thanks to you all, Hope this helps:
// debounce from loadsh/debounce
let debouncedOnRender = debounce(()=>{
console.log("debounced render");
func();
map.off('render', debouncedOnRender);
},100);
map.on('render', debouncedOnRender);
Most helpful comment
Thanks to you all, Hope this helps: