jupyter-widgets / ipyleaflet

A Jupyter - Leaflet.js bridge
https://ipyleaflet.readthedocs.io
MIT License
1.49k stars 365 forks source link

Interactive vector tile layers #1213

Closed lopezvoliver closed 2 months ago

lopezvoliver commented 3 months ago

Renderer factory

Vector tile layers can be rendered using either L.canvas.tile or L.svg.tile. Currently, only the canvas option is implemented in ipyleaflet and it's not doing a great job at higher levels of zoom, see for example #1095.

Here a new renderer option was added to VectorTileLayer, which can be either svg or canvas, with the new default being svg.

Here's an example using the ms-buildings from Microsoft Planetary Computer, with the new default (svg):

import ipyleaflet
m = ipyleaflet.Map(center=(41.91867,-88.10602), zoom=15)
url = 'https://planetarycomputer.microsoft.com/api/data/v1/vector/collections/ms-buildings/tilesets/global-footprints/tiles/{z}/{x}/{y}'
layer = ipyleaflet.VectorTileLayer(
    url=url, 
    attribution='Microsoft', 
    max_native_zoom=13,
    max_zoom=20,
    vector_tile_layer_styles={"bingmlbuildings":{
        "fill":True,
        "weight":0.5
    }},    
)
m.add(layer)
m

And here's how it renders using renderer='canvas':

Interactivity

A new interactive option was added with False as default, which enables the user to add listeners to the layer, which include information about the feature. Note that the default renderer='svg' option should be used for interactivity. For example:

m = ipyleaflet.Map()
layer = ipyleaflet.VectorTileLayer(
    url=url_data,  # My vector tile layer 
    interactive=True,  # New interactive option 
    max_native_zoom=13,
    max_zoom=20,
    renderer='svg', # New renderer option. Defaults to 'svg'
    layer_styles=jStyles,   # javascript function given as a string
    feature_id = 'label', # New feature_id option. Here, 'label' is the name of the (numeric) 
    # attribute in my layer that uniquely identifies each feature (see below for more information)
)
m.add(layer)

def handle_click(**kwargs):    
    if ("properties" in kwargs):
        properties = kwargs["properties"]
        options = kwargs["options"]
        print(properties)
        print(options)

layer.on_click(handle_click)  

m

feature_id

This is an optional attribute that is used to construct a simple javascript function to uniquely identify a feature. This is required if you will be updating feature styles through the new set_feature_style and reset_feature_style methods. The javascript function is of the form:

function (feat: any) {return feat.properties[idVar];}

where feat is the feature, and idVar is the name of the (numeric) attribute in the layer to identify a feature. Note that features with the same id will be treated as one when changing style (see the original getFeatureId documentation here).

Updating styles

Two new methods for VectorTileLayer were added: set_feature_style and reset_feature_style. The first one is used to update the style for an individual feature, which is useful for highlighting a feature (e.g., on click or mouseover), while the second one is useful for resetting the style to the default (e.g. to clear the highlight).

Example

Here's a motivating example that demonstrates all of the new features.

m = ipyleaflet.Map()
layer = ipyleaflet.VectorTileLayer(
    url=url_data,  # My vector tile layer 
    interactive=True,  # New interactive option 
    max_native_zoom=13,
    max_zoom=20,
    renderer='svg', # New renderer option. Defaults to 'svg'
    layer_styles=jStyles,   # javascript function given as a string
    feature_id = 'label', # New feature_id option. Here, 'label' is the name of the (numeric) 
    # attribute in my layer that uniquely identifies each feature.
)
m.add(layer)

info_title = "<h4>Field info</h4>"
info_default_value = "Hover over a field" 

info_widget = widgets.HTML(value=info_title + info_default_value)

highlight_style = {
                "weight": 5,
                "color": '#666',
                "dashArray": '',
                "fillOpacity": 0.7,
                "fill": True 
                }

def highlight_feature(**kwargs):    
    if ("properties" in kwargs):
        properties = kwargs["properties"]
        options = kwargs["options"]
        feature_id = properties["label"]
        fill_color = options["fillColor"]
        highlight_style["fillColor"] = fill_color
        layer.set_feature_style(
            id=feature_id,
            layer_style=highlight_style,
        )
        info_html = info_title
        for k,v in properties.items():
            info_html += "<b>"+ k + "</b>" + ": " + str(v) + "<br />"
        info_widget.value = info_html

def clear_highlight(**kwargs):
    if layer.feature_style: 
        layer.reset_feature_style(layer.feature_style["id"])
        info_widget.value = info_title + info_default_value

layer.on_mouseover(highlight_feature)
layer.on_mouseout(clear_highlight)

widget_control = ipyleaflet.WidgetControl(widget=info_widget, position='topright')
m.add(widget_control)

m
lopezvoliver commented 3 months ago

@martinRenou could you please suggest someone for review?

martinRenou commented 2 months ago

Thanks! I'll have a look

martinRenou commented 2 months ago

Here's an example using the ms-buildings from Microsoft Planetary Computer, with the new default (svg):

@lopezvoliver I was curious to run this example, but it seems I'm getting 404s on this https://planetarycomputer.microsoft.com url when LeafletJS tries to download the tiles. Is there anything particular to do to allow this example to work?

lopezvoliver commented 2 months ago

Here's an example using the ms-buildings from Microsoft Planetary Computer, with the new default (svg):

@lopezvoliver I was curious to run this example, but it seems I'm getting 404s on this https://planetarycomputer.microsoft.com url when LeafletJS tries to download the tiles. Is there anything particular to do to allow this example to work?

I just checked and it should work as is. Are you able to access the following tile directly?

https://planetarycomputer.microsoft.com/api/data/v1/vector/collections/ms-buildings/tilesets/global-footprints/tiles/13/2090/3043

This layer is only available until z=13, so the maxNativeZoom (added in #1206) is set to 13.

lopezvoliver commented 2 months ago

Here's a description of the recent five commits:

  1. As suggested, I used feature_style as a traitlets property so that we can add observe its change in javascript and call the setFeatureStyle method. The user can call the set_feature_style method with an id and a layer_style dictionary. The user can also call the reset_feature_style method with an id, which updates the same feature_style traitlets property but with a reset value set to True. At this time, only one id should be used at a time, as is the intended use case of the original setFeatureStyle and resetFeatureStyle methods in VectorGrid.

  2. I realized that in #1210 one small issue persisted: when initializing a VectorTileLayer with visible=False, this wasn't reflected initially. This commit fixes this issue by setting opacity=0 after initializing the vector tile layer with visible=False.

3, 4, and 5: Each of these commits is a decision to rename vector_tile_layer_styles, renderer_factory, and get_feature_id (the latter two are additions in this pull request) to a different name in python (layer_styles, renderer, and feature_id, respectively). The reason for this is that because all three are converted to functions in javascript, they shouldn't be synced to the python model. Instead, they are used to construct the javascript counterparts VectorTileLayerStyles, rendererFactory, and getFeatureId.

I will update the original PR comment to reflect these changes where needed.

martinRenou commented 2 months ago

This layer is only available until z=13, so the maxNativeZoom (added in https://github.com/jupyter-widgets/ipyleaflet/pull/1206) is set to 13.

Ah right I was not using the main branch! Thanks for the clarification.

Here's a description of the recent five commits:

Is it ready for another review?

Happy to make a release of your recent PRs after this one is merged

lopezvoliver commented 2 months ago

Is it ready for another review?

Yes, it's ready for review. (Edit: I just pushed one last commit -- I figured out a way to keep vector_tile_layer_styles as input for backwards compatibility)

Happy to make a release of your recent PRs after this one is merged

That would be great, thanks!

martinRenou commented 2 months ago

The CI would deserve some love, let's do that separately