Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VectorTileLayer - How to control vector layer draw order #1232

Open
drclanc-oss opened this issue Oct 3, 2024 · 3 comments
Open

VectorTileLayer - How to control vector layer draw order #1232

drclanc-oss opened this issue Oct 3, 2024 · 3 comments

Comments

@drclanc-oss
Copy link

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
Copy link
Author

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?

@drclanc-oss drclanc-oss changed the title VectorTileLayer seems not to respect layer draw order (zIndex?) VectorTileLayer - How to control vector layer draw order Oct 4, 2024
@lopezvoliver
Copy link
Contributor

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
Copy link
Author

Thanks @lopezvoliver I appreciate your insight.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants