emilhe / dash-leaflet

MIT License
214 stars 40 forks source link

Add a query parameter to enable modifying url of VectorTileLayer to filter features #263

Open zifanw9 opened 3 weeks ago

zifanw9 commented 3 weeks ago

Why is this feature needed?

Why is current version not suitable for the needs?

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.

  1. 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.

  1. 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
zifanw9 commented 3 weeks ago

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

zifanw9 commented 2 weeks ago

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

zifanw9 commented 2 weeks ago

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:

  1. 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;}
    }
  2. I open the map in browser and apply some filtering on the layer (defining mininclusion and maxinclusion)
  3. I zoom out or zoom in the map so that the zoom level changed
  4. 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)
  5. 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