1

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?

LBes
  • 3,366
  • 1
  • 32
  • 66
  • @stdob-- for performance issues (I've tried deck.gl before) I would like to keep everything working with mapbox only. – LBes Sep 20 '18 at 18:16
  • @stdob-- sure give a few minutes and I'll update with a js fiddle and notify you in the comments – LBes Sep 20 '18 at 18:39
  • @stdob-- just added the code and geojson in my question. – LBes Sep 20 '18 at 19:00
  • I do not really understand what you are trying to achieve, but you have at least a problem with the scope of variables `layer` and `radiusPX` - https://jsfiddle.net/L69d7kpv/ – stdob-- Sep 20 '18 at 21:41
  • @stdob-- to be more clear: I want to be able to create the data that is going to be used for the extrusion once (or at least when I want) for all of the geospatial data. And then just update the radius of the extrusion, without having to re-create the whole extrusion data (because this is too computationally heavy when you have a huge dataset) – LBes Sep 21 '18 at 14:59
  • @stdob-- and I've just tried your code again and I think it does not do that. – LBes Sep 21 '18 at 15:12
  • Sorry, a lot of work - I can not help you now :( – stdob-- Sep 21 '18 at 15:28
  • @stdob-- no problem. Hope you manage. Lemme know when you have a moment to take a look at it :) – LBes Sep 21 '18 at 15:39
  • Here is an example of how it can work - 100,000 points on a small area, plus clustering. And at the moment you can not specify the radius for objects with height for extrusion, only recalculate each time for visible objects. https://jsfiddle.net/upgm54s3/ – stdob-- Sep 22 '18 at 14:44
  • @stdob-- thanks for this but unfortunately this doesn't solve my problem. Using queryRenderedFeature is not possible for me as I have 2 Million data points. The problem is that this takes, even if I only show extrusion on high zoom values, way too long. Hence why I asked whether it would be possible to actually just compute the height for all data points once, and then only dynamically change the radius. – LBes Sep 24 '18 at 08:41
  • May tiling on the server side help you... – stdob-- Sep 24 '18 at 09:06
  • @stdob-- so there is no way to just change the radius without redoing all computations like you do in the example you gave? I think the bottleneck is having to recompute and reset all the source data of the extrusion layer. Isn't there a way to just get the extrusion layer data and modify for each the radius? – LBes Sep 24 '18 at 09:07

0 Answers0