jupyter-widgets / ipyleaflet

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

VectorTileLayer - How to control vector layer draw order #1232

Open drclanc-oss opened 1 month ago

drclanc-oss commented 1 month ago

I am trying to make an ocean map layer, including bathymetry, from this Esri vector tile service. I am able to style the various bathymetry depths by providing a JavaScript string to the vector_tile_layer_styles param on the VectorTileLayer constructor and it works well.

import ipyleaflet
from ipyleaflet import Map, VectorTileLayer

ipyleaflet.__version__

'0.19.2'

world_tiles_url = "https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer/tile/{z}/{y}/{x}.pbf"

jstyle='''{
    "Bathymetry": function(properties, zoom){
        var color = "blue";
        var fill = true;

        if(properties._symbol==0){
            color = "#d6f4ff";
        }
        if(properties._symbol==1){
            color = "#ccf0ff";
        }
        if(properties._symbol==2){
            color = "#c2ecff";
        }
        if(properties._symbol==3){
            color = "#b8e6ff";
        }
        if(properties._symbol==4){
            color = "#ade0ff";
        }
        if(properties._symbol==5){
            color = "#a3d9ff";
        }

        return {
            color: color,
            fill: fill,
            opacity: 1,
            fillOpacity: 1
        }
    },
    //"Marine area": {"fillOpacity": 1, "color": "#e7f9ff", "fill": true},
}
'''

bathymetry = VectorTileLayer(
    url=world_tiles_url,
    vector_tile_layer_styles=jstyle,
    base=True,
    name="Bathymetry Basemap"
)

m = Map(center=[45, -45], zoom=5)
[m.remove(lyr) for lyr in m.layers]
m.add(bathymetry)
m

This looks pretty good!

image

However, the water areas nearest the coastlines is not filled. This is because the required features are in the "Marine area" layer; when I uncomment the "Marine area" style in my jstyle above those areas are indeed filled, but the features cover up my bathymetry.

image

When I look at these vector tiles using Esri's style editor the layers draw in the correct order: https://www.arcgis.com/apps/vtseditor/en/#/7dc6cea0b1764a1f9af2e679f642f0f5/layers

Can I control the layer draw order somehow? From everything I've read, this is controlled in the vector tiles themselves via the zIndex. In this case I want the Marine area layer to draw underneath the Bathymetry layer instead of on top of it.

drclanc-oss commented 1 month ago

Perhaps I was wrong about zIndex being encoded directly in the vector tiles.

import httpx
import mapbox_vector_tile

tile = httpx.get("https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer/tile/8/128/125.pbf", verify=False)

decoded_data = mapbox_vector_tile.decode(tile.content)
decoded_data["Bathymetry"]

That gives:

{'extent': 1048576,
 'version': 2,
 'features': [{'geometry': {'type': 'Polygon',
    'coordinates': [[[1064960, 1064960],
      [1064960, -16384],
      [-16384, -16384],
      [-16384, 1064960],
      [1064960, 1064960]]]},
   'properties': {'_symbol': 0},
   'id': 0,
   'type': 'Feature'},
  {'geometry': {'type': 'Polygon',
    'coordinates': [[[1064960, 1064960],
      [1064960, -16384],
      [-16384, -16384],
      [-16384, 1064960],
      [1064960, 1064960]]]},
   'properties': {'_symbol': 1},
   'id': 0,
   'type': 'Feature'},
  {'geometry': {'type': 'Polygon',
    'coordinates': [[[1064960, 1064960],
      [1064960, -16384],
      [-16384, -16384],
      [-16384, 1064960],
      [1064960, 1064960]]]},
   'properties': {'_symbol': 2},
   'id': 0,
   'type': 'Feature'},
  {'geometry': {'type': 'Polygon',
    'coordinates': [[[-16384, -16384],
      [-16384, 1064960],
      [1064960, 1064960],
      [1064960, -16384],
      [-16384, -16384]]]},
   'properties': {'_symbol': 3},
   'id': 0,
   'type': 'Feature'},
  {'geometry': {'type': 'Polygon',
    'coordinates': [[[1064960, 1064960],
      [1064960, 244623],
      [1038487, 245318],
      [1031516, 236286],
      [1023680, 230237],
      [1022483, 173782],
      [1031516, 166810],
      [1037506, 159048],
      ...
      ...
      ...
      [613505, 65588],
      [621037, 58609],
      [626230, 53006],
      [631833, 47813],
      [638823, 40268],
      [673563, 39614]]]},
   'properties': {'_symbol': 4},
   'id': 0,
   'type': 'Feature'}],
 'type': 'FeatureCollection'}

There doesn't appear to be a zIndex or anything else to indicate layer draw order. How do we control which layers draw on top and which on bottom?

lopezvoliver commented 1 month ago

Hi @drclanc-oss . Unfortunately, I don't think there is a way to control the order.

However, you can try adding multiple leaflet layers with different vectorTileLayerOptions:

from ipyleaflet import Map, VectorTileLayer
world_tiles_url = "https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer/tile/{z}/{y}/{x}.pbf"

bathymetry_style='''{
    "Graticule/label":[],
    "Graticule":[],
    "Marine area":[],
    "Bathymetry": function(properties, zoom, geometryDimension){
        L.Path.mergeOptions({stroke:false});  // HACK 
        var color;

        # ... your code to assign color based on the property

        return {
            fillColor: color,
            fill: true,
            weight: 0,
            opacity: 1,
            fillOpacity: 1
        }
    }
}
'''

marine_area_style='''{
    "Marine area": {"fillOpacity": 1, "color": "#e7f9ff", "fill": true},
}
'''

bathymetry = VectorTileLayer(
    url=world_tiles_url,
    vector_tile_layer_styles=bathymetry_style,
    name="Bathymetry Basemap"
)
marine_areas = VectorTileLayer(
    url=world_tiles_url,
    vector_tile_layer_styles=marine_area_style,
    name="Marine areas"
)

m = Map(center=[30, -65], zoom=4)
[m.remove(lyr) for lyr in m.layers]
m.add(marine_areas)
m.add(bathymetry)
bathymetry.redraw()  # Hack.

m

image

I think this is what you are looking for, right?

However, there is one problem with your vector tile layer: it seems there are extra Line features that are not contained in any "layer", i.e. they are not coming from either of Graticule/label, Graticule, Marine area, or Bathymetry, so even if I set all of these to an empty style ([]), you can still see those features being drawn with the default L.path options:

image

In leaflet.js, you could update L.path directly (see this jsfiddle example) -- but in ipyleaflet I don't think there is a way to do this.

In the code I shared above, I did it in a hacky way.. I updated L.path inside the function for the Bathymetry layer. However, because some of these extra features are drawn before this update, we have to redraw the layer. I tested this on vscode and the update to L.path seems to persist within the same notebook (until you close it/reopen it).. so keep that in mind if you rely on the default style.

drclanc-oss commented 2 weeks ago

Thanks @lopezvoliver I appreciate your insight.