I want to dynamically adjust the radius of circle-based extrusions with Mapbox based on the zoom level.
I have used for a toy dataset the solution provided by @stdob-- here and for which the JS Fiddle is available here.
The problem with that solution is that it is computationally very expensive and with my real dataset (more than a million point) this is not a viable solution. I therefore thought about using queryRenderedFeatures()
as suggested in the comments of the previous SO posts. However even that is not giving me a good enough interactive visualization.
Instead, I therefore wanted to initially load all of my dataset and layers (including the 3D extrusions) and then on map-zoom events only recompute the radius that is going to be used for the 3D extrusions.
Here is the code I used:
Here is simple geojson file to reproduce the error with
{"type": "FeatureCollection", "features": [{"id": 1, "type": "Feature", "properties": {"x": 1.0, "group": 1, "my_property": 217}, "geometry": {"type": "Point", "coordinates": [8.539961, 47.37347]}}, {"id": 2, "type": "Feature", "properties": {"x": 2.0, "group": 1, "my_property": 520}, "geometry": {"type": "Point", "coordinates": [8.517961, 47.37520]}}]}
the following code:
HTML:
<html>
<head>
<meta charset='utf-8' />
<title>Display buildings in 3D</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.48.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.48.0/mapbox-gl.css' rel='stylesheet' />
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<script src="https://unpkg.com/supercluster@4.1.1/dist/supercluster.min.js"></script>
</head>
<body>
<div id='map'></div>
<script>
</script>
</body>
</html>
CSS:
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
JS:
mapboxgl.accessToken = 'pk.eyJ1IjoibG9ubmliZXNhbmNvbiIsImEiOiJjamxjaWNpOHQwMHV0M3FwaHhneGhvY2l2In0.7GxI8W_dnTKITNF4hEvZeQ';
var map = new mapboxgl.Map({
style: 'mapbox://styles/mapbox/light-v9',
center:[8.538961, 47.37247],
zoom: 10,
pitch: 20,
bearing: 0,
container: 'map'
});
var url = "REPLACE WITH GEOJSON LOCATION"
//
var zoom_level_3D_bars = 14
var radius_zoom_d = 10
var map_zoom = 10
map.on('load', function() {
// Insert the layer beneath any symbol layer.
var layers = map.getStyle().layers;
var labelLayerId;
for (var i = 0; i < layers.length; i++) {
if (layers[i].type === 'symbol' && layers[i].layout['text-field']) {
labelLayerId = layers[i].id;
break;
}
}
map.addSource("data", {
type: "geojson",
data: url,
});
map.addLayer({
'id': 'extrusion',
'type': 'fill-extrusion',
'minzoom': zoom_level_3D_bars,
"source": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
},
'source': 'data',
'paint': {
'fill-extrusion-height': ['/', ['number', ['get', 'my_property'],0], 10],
'fill-extrusion-base': 0,
'fill-extrusion-opacity': 0.5
}
});
map.addLayer({
'id': 'population',
'type': 'circle',
'source': 'data',
'paint': {
'circle-color': {
'property': 'group',
'type': 'categorical',
stops: [
[1, 'rgba(252,141,98,1)'],
[2, 'rgba(102,194,165,1)'],
[3, 'rgba(102,194,165,1)'],
[4, 'rgba(102,194,165,1)'],
[5, 'rgba(102,194,165,1)'],
[6, 'rgba(102,194,165,1)'],
//'4', '#3bb2d0',
/* other 'rgba(102,194,165,0.1)'*/
]
},
}
});
map.on('data', function() {
//if (!firstTower) updateTower();
//});
//console.log("Initialize")
//initializeTower();
})
map.on('zoom', function() {
map_zoom = map.getZoom();
if(map.isSourceLoaded('data') == false){
return
}
if(map_zoom < zoom_level_3D_bars){
map.setPaintProperty('population', 'circle-radius', radius_zoom_d);
if(map.getPaintProperty('population','circle-opacity') != 1){
map.setPaintProperty('population', 'circle-opacity', 1)
}
}
radius_zoom_d = 10 - (map_zoom/2)
if(map_zoom >= zoom_level_3D_bars){
opacity_point = 0
console.log("Update tower bc zoom = "+map_zoom)
if(map.getPaintProperty('population','circle-opacity') != 0){
map.setPaintProperty('population', 'circle-opacity', 0)
}
updateTower();
}
})
function updateTower() {
var radiusPX = false;
var layer = map.getLayer('population')
if (layer.paint) radiusPX = map.getLayer('population').paint.get('circle-radius').evaluate();
if (radiusPX === false) return;
var data = {
"type": "FeatureCollection",
"features": []
}
//HERE IS THE PART where I would like to change the radius without having to take
// all the querySourceFeatures or queryRenderedFeatures for performance issues
//But I don't know how to just go through the dataset of the layer extrusion
}
map.on('data', function(e) {
// if (e.sourceId !== 'total') return
if (e.sourceId !== 'data') return
if (e.isSourceLoaded !== true) return
initializeTower()
})
//map.on('sourcedata', sourceCallback);
function initializeTower(){
if (layer.paint) radiusPX = map.getLayer('population').paint.get('circle-radius').evaluate();
if (radiusPX === false) return;
var nb_of_objects = 0
var data = {
"type": "FeatureCollection",
"features": []
}
map.querySourceFeatures('data').forEach(function(f) {
var object = turf.centerOfMass(f)
var center = object.geometry.coordinates
var xy = map.project(center)
xy.x += radiusPX;
var LL = map.unproject(xy)
LL = turf.point([LL.lng, LL.lat])
//var radius = turf.distance(center, LL, {
// units: 'meters'
//}) + 0.00000001
var radius = radius_zoom_d ;
var options = {
steps: 16,
units: 'meters',
properties: object.properties
};
data.features.push(turf.circle(center, radius, options))
nb_of_objects +=1
})
console.log("Finished preparing data for "+nb_of_objects+" objects")
map.getSource('extrusion').setData(data);
}
});
The first issue I have is that it triggers a ReferenceError: layer is not defined
on the line if (layer.paint) radiusPX = map.getLayer('my_initial_2D_layer').paint.get('circle-radius').evaluate();
. This is probably due to the layer's style not being rendered yet, but it seems from the documentation and few Mapbox Questions on SO and on their GitHub that there is no way to check for that.
If I comment this line, this triggers later on in the code a Cannot read property 'setData' of undefined
on the line map.getSource('extrusion').setData(data);
and also that it prints it processed 0 objects which is quite problematic. I get the output from my console.log()
.
Finished preparing data for 0 objects
The second issue that I have is that I don't know how I could later modify the data of this extrusion layer. It seems that there is no function to get the data for my extrusion layer in order to just change its radius (as it seems that this cannot be done dynamically in the layer style).
Would anyone know how to proceed?