Add a query parameter to enable modifying url of VectorTileLayer to filter features
Why is this feature needed?
- Allow users to interactively filter features in vector tile layer, given that hideout property is not introduced to vector tile layer yet
Why is current version not suitable for the needs?
- Line 100 of VectorTileLayer.ts uses layer.setUrl(url) method, but this method is not available and will throw error
- Joachim, the author of Leaflet.VectorTileLayer suggests
- URL template string can have an extra {q} in addition to {x}, {y}, {z}
- If we initialize a const URLSearchParams with a name such as q, and pass this const directly as an element in options when initializing VectorTileLayer, then we will be able to update the layer by modifying the key/value in the const q
- To make the update take effect immediately, use layer.redraw() method
With the new changes, how to do the filter then?
The vector tile server needs to support CQL in order to perform filtering by passing different URL. Here I use TiPG for testing the functionality.
- Initialize the layer within the map container
dl.VectorTileLayer(url="https://<TileServerEndpointURL>/collections/<SCHEMA.TABLE>/tiles/WebMercatorQuad/{z}/{x}/{y}?{q}",
id="vectortile",
query={
"properties": "res,comind,geometry",
"filter-lang": "cql2-text",
"filter": "res IS NULL AND comind IS NULL"
})
Note in the above code, the url ends with ?{q}, this format must be strictly followed if you want to perform filtering. If you do not expect to use filtering functionality, you can choose to specify the url without ?{q} at the end.
Additionally, to perform filtering, the query parameter needs to be specified. This parameter assumes a dictionary-like object. In this example, "properties": "res,comind,geometry" means to select those three columns from the PostGIS table. "filter-lang": "cql2-text" specifies the filter language, and "filter": "res IS NULL AND comind IS NULL" is like a SQL where clause that selects only features with both res and comind fields having null values. Note that those specific query parameters are tile server dependent and please consult the documentation of the tile server that you are using. I also want to mention that TiPG seems to assume column names to be all lower case.
- Create a callback function to update the query parameter of VectorTileLayer
@callback(
Output("vectortile","query"),
[Input("map","zoom")],
State("vectortile","query")
)
def updatebuildinglayer(zoomlevel,current_url):
expected_url = {
"properties": "res,comind,geometry",
"filter-lang": "cql2-text",
"filter": "res = '0'"
}
if expected_url != current_url:
return expected_url
return dash.no_update
Known issue that should be resolved If there are two vector tile layers (corresponding to two PostGIS tables) applied with this query parameter, the queries will mess up. Currently it seems like only one vector tile layer with query parameter can be added to the map at once
Update: the issue seems to be resolved in my third commit, but I am not sure if the code follows the best practice and whether it will lead to other unseen issues
I just made one more commit to add setStyle method, which can update the style dictionary or function used for a vector tile layer.
I give an example of changing the attribute to display in a callback function.
clientside_callback(
'''
function(zoomlevel) {
let displayattribute = "avgdistnearbyhydrant";
if (zoomlevel > 16) {
displayattribute = "maxdistnearbyhydrant";
}
const style_function = function(feature,layername,zoomlevel,context) {
let style_template = {
fillOpacity: 1,
stroke: false,
radius: 5,
fillColor: "#808080"
};
const _value = feature.properties[displayattribute];
if (_value!=null) {
if (_value <= 500) { style_template.fillColor="#ffffb2"; }
else { style_template.fillColor="#bd0026"; }
}
if (zoomlevel <= 11) { style_template.radius=1; }
else if (zoomlevel <= 12) { style_template.radius=2; }
else if (zoomlevel <= 13) { style_template.radius=3; }
else if (zoomlevel <= 14) { style_template.radius=4; }
return style_template;
};
style_function.hideout = {"displayattribute": displayattribute};
return style_function;
}
''',
Output("hydrantMain","style"),
Input("homepagemap","zoom"),
prevent_initial_call=True,
)
Maybe using this method to un-display certain features (e.g., set stroke and fill to false) in lieu of url filter that I wrote earlier in this PR is better, since setStyle does not require re-transmission of updated tiles.
Ideally it will be great if we could have a real hideout property in the context, but I do not know how to update the properties stored in the context
I just made 5th commit to resolve a bug related to update the style of the layer to perform filtering.
Previously layer is not redrawn after layer.setStyle method is applied, and I suggest that the layer style typically is updated automatically.
I discover that there is a case in which the above assumption is not true:
- Assuming that I have a callback function that update the vector tile layer's style function to perform some filtering based on user input min and max values. Within the style_function like the one I give in my previous comment, the followings thing is needed
const no_style = {
stroke : false,
fill : false
};
if (_value!=null){
if (_value >= mininclusion && _value <= maxinclusion) {}
else {return no_style;}
}
- I open the map in browser and apply some filtering on the layer (defining mininclusion and maxinclusion)
- I zoom out or zoom in the map so that the zoom level changed
- I reset/cancel the filter or change the filter with a different set of mininclusion/maxinclusion values (particularly the new filter values imply more features should be shown; if less features should be shown, a redraw is not necessary)
- The layer is not updated, and a redraw is required in this case
In my 5th commit, I add a property to track the zoom value when the style is changed, and redraw the layer if the zoom values is different between current and previous zoom. If the zoom level is the same between two style updates, I have not observed any issue so far.
Redraw probably should be avoided when possible for vector tile layer since it will re-request tiles from the tile server. Update the style of a vector tile layer itself does not seem to make duplicate requests of tiles, although new tiles will be requested when panning the map or zoom in and out of the map
Hi @emilhe and @zifanw9, I am also looking for vector tile functionality and was brought here from #84. I wonder if we can merge this feature? I am happy to do some testing with this PR if that is needed
Hi @dmragar, I am happy to see that you are interested in this feature. While I am unable to do resolve conflicts or merge the PR, I can give some suggestions:
- If your code is not already heavily relying on dash, I recommend to look into mapbox-gl-js library which works really well for vector tiles (you can dynamically set filter, update style, update opacity, etc; it also does not have memory leaking issue). It is billed mostly on page loads and tile hosting. The monthly free amount on page loads and tile hosting should be sufficient for small projects (virtually free then), and you do not need to worry about tile server hosting. Honestly, TiPG that I mention in this PR requires both tile server and database; it costs a lot of money, and TiPG is not as fast as mapbox-hosted tilesets or some other vector tile servers like Martin tile server
- If you do not want to use mapbox but is still open to options other than dash, you can dig into PMTiles; it also requires Javascript rather than Python knowledge. The benefit though is it eliminates the need of tile server or PG or mapbox-hosting, but you need S3 (probably CloudFront as well)
- If you want to stick with dash, one option is to clone my fork, build it, and copy it as a dependency to your dash app. This will be the quickest way to prototype ideas
Hi @dmragar, I am happy to see that you are interested in this feature. While I am unable to do resolve conflicts or merge the PR, I can give some suggestions:
- If your code is not already heavily relying on dash, I recommend to look into mapbox-gl-js library which works really well for vector tiles (you can dynamically set filter, update style, update opacity, etc; it also does not have memory leaking issue). It is billed mostly on page loads and tile hosting. The monthly free amount on page loads and tile hosting should be sufficient for small projects (virtually free then), and you do not need to worry about tile server hosting. Honestly, TiPG that I mention in this PR requires both tile server and database; it costs a lot of money, and TiPG is not as fast as mapbox-hosted tilesets or some other vector tile servers like Martin tile server
- If you do not want to use mapbox but is still open to options other than dash, you can dig into PMTiles; it also requires Javascript rather than Python knowledge. The benefit though is it eliminates the need of tile server or PG or mapbox-hosting, but you need S3 (probably CloudFront as well)
- If you want to stick with dash, one option is to clone my fork, build it, and copy it as a dependency to your dash app. This will be the quickest way to prototype ideas
Thanks @zifanw9, I appreciate this detailed reply. The PMTiles option is intriguing, and could fit our use case. We have a few existing Dash apps where a vector tile option would prevent have to do a rewrite with a different framework, hence my interest in this PR.