ipyleaflet
ipyleaflet copied to clipboard
VectorTileLayer - How to control vector layer draw order
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!
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.
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.
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?
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
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:
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.
Thanks @lopezvoliver I appreciate your insight.