Somesh
Somesh

Reputation: 87

Mapbox GL clustering with Vector Tile source

I am checking if we can do clustering at mapbox end from a Vector tile source, I have tried doing clustering at backend (POSTGIS) but it took too much time for big data table (1M records).

Or I should look for any other solution.

Upvotes: 2

Views: 2476

Answers (2)

Danysan
Danysan

Reputation: 343

There is an issue on MapBox GL JS requesting to integrate clustering in the VectorTileSource but it's closed and there seems to be no intention to implement it in the forseeable future. There seems to be no issue about this on MapLibre GL JS so it won't come there either. So front-end clustering on vector tiles is not an option.

As you said real-time server side clustering struggles on big datasets (especially at low zoom). My solution is to use ahead-of-time clustering while generating a set of vector tiles or a pmtiles file. I did this by dumping my dataset in a GeoJSON file and using Tippecanoe taking inspiration from the instructions frm its cookbook:

tippecanoe -zg -o ne_10m_populated_places.mbtiles -r1 --cluster-distance=10 --accumulate-attribute=POP_MAX:sum ne_10m_populated_places.geojson

-zg: Automatically choose a maxzoom that should be sufficient to clearly distinguish the features and the detail within each feature

-r1: Do not automatically drop a fraction of points at low zoom levels, since clustering will be used instead

--cluster-distance=10: Cluster together features that are closer than about 10 pixels from each other

--accumulate-attribute=POP_MAX:sum: Sum the POP_MAX (population) attribute in features that are clustered together. Other attributes will be arbitrarily taken from the first feature in the cluster.

Once you do that, you can simply use your tiles with a VectorTileSource and using it with the same layers from the official clustering example (here there is a real-life example I implemented).

Another alternative, discussed here is to create materialized views on the DB for each zoom level, serve them with a tool such as pg_tileserv or Martin and then create on VectorTileSource for each zoom level, but the Tippecanoe solution seems much more clean to me.

Upvotes: 1

Patrick Leonard
Patrick Leonard

Reputation: 412

You can cluster a large number of points on the frontend with Mapbox GL JS. This post includes an example with 400,000 points from a GeoJSON file and a similar approach should work for 1M. There is example code on the Mapbox website here that I've reproduced below:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Create and style clusters</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src="https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.css" rel="stylesheet" />
<style>
	body { margin: 0; padding: 0; }
	#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>

<script>
	mapboxgl.accessToken = 'pk.eyJ1IjoicGxtYXBib3giLCJhIjoiY2s2MThyOG9vMDEydjNrcGplNzZiMXJ2NyJ9.KJn6ZtoogD-QMm80LonTzA';
    var map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/dark-v10',
        center: [-103.59179687498357, 40.66995747013945],
        zoom: 3
    });

    map.on('load', function() {
        // Add a new source from our GeoJSON data and
        // set the 'cluster' option to true. GL-JS will
        // add the point_count property to your source data.
        map.addSource('earthquakes', {
            type: 'geojson',
            // Point to GeoJSON data. This example visualizes all M1.0+ earthquakes
            // from 12/22/15 to 1/21/16 as logged by USGS' Earthquake hazards program.
            data:
                'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson',
            cluster: true,
            clusterMaxZoom: 14, // Max zoom to cluster points on
            clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
        });

        map.addLayer({
            id: 'clusters',
            type: 'circle',
            source: 'earthquakes',
            filter: ['has', 'point_count'],
            paint: {
                // Use step expressions (https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions-step)
                // with three steps to implement three types of circles:
                //   * Blue, 20px circles when point count is less than 100
                //   * Yellow, 30px circles when point count is between 100 and 750
                //   * Pink, 40px circles when point count is greater than or equal to 750
                'circle-color': [
                    'step',
                    ['get', 'point_count'],
                    '#51bbd6',
                    100,
                    '#f1f075',
                    750,
                    '#f28cb1'
                ],
                'circle-radius': [
                    'step',
                    ['get', 'point_count'],
                    20,
                    100,
                    30,
                    750,
                    40
                ]
            }
        });

        map.addLayer({
            id: 'cluster-count',
            type: 'symbol',
            source: 'earthquakes',
            filter: ['has', 'point_count'],
            layout: {
                'text-field': '{point_count_abbreviated}',
                'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
                'text-size': 12
            }
        });

        map.addLayer({
            id: 'unclustered-point',
            type: 'circle',
            source: 'earthquakes',
            filter: ['!', ['has', 'point_count']],
            paint: {
                'circle-color': '#11b4da',
                'circle-radius': 4,
                'circle-stroke-width': 1,
                'circle-stroke-color': '#fff'
            }
        });

        // inspect a cluster on click
        map.on('click', 'clusters', function(e) {
            var features = map.queryRenderedFeatures(e.point, {
                layers: ['clusters']
            });
            var clusterId = features[0].properties.cluster_id;
            map.getSource('earthquakes').getClusterExpansionZoom(
                clusterId,
                function(err, zoom) {
                    if (err) return;

                    map.easeTo({
                        center: features[0].geometry.coordinates,
                        zoom: zoom
                    });
                }
            );
        });
        
        // When a click event occurs on a feature in
        // the unclustered-point layer, open a popup at
        // the location of the feature, with
        // description HTML from its properties.
        map.on('click', 'unclustered-point', function(e) {

            var coordinates = e.features[0].geometry.coordinates.slice();
            var mag = e.features[0].properties.mag;
            var tsunami;
            
            if (e.features[0].properties.tsunami === 1) {
                tsunami = 'yes'
            } else {
                tsunami = 'no'
            }

            // Ensure that if the map is zoomed out such that
            // multiple copies of the feature are visible, the
            // popup appears over the copy being pointed to.
            while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
                coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
            }

            new mapboxgl.Popup()
                .setLngLat(coordinates)
                .setHTML("magnitude: " + mag + "<br>Was there a tsunami?: " + tsunami)
                .addTo(map);
        });

        map.on('mouseenter', 'clusters', function() {
            map.getCanvas().style.cursor = 'pointer';
        });
        map.on('mouseleave', 'clusters', function() {
            map.getCanvas().style.cursor = '';
        });
    });
</script>

</body>
</html>

Upvotes: -1

Related Questions