Search code examples
asynchronousfirebase-realtime-databasemapbox-gl

Add source to existing layer in mapbox GL


The title pretty much says what I intend to do.

I am using Firebase as backend for markers on a map and use the on('child_added') method to monitor these. For every node on a specific location in the database, the on('child_added') will fire once.

This also applies to new nodes being created, hence this is perfect for asynchronously adding new markers to the map as they are added to the database.

In order display these on a map, mapbox GL requires me to transform the data to geojson, create a source and then add this source to a layer. The code below shows this and it actually displays the markers on the map.

markersRef.on('child_added', function(childSnapshot) { //fires once for every child node
    var currentKey = childSnapshot.key; //the key of current child
    var entry = childSnapshot.val(); //the value of current child

    //creates a geojson object from child
    var geojson = {
        "type": "FeatureCollection",
        "features": [{
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [entry.position.long, entry.position.lat]
            }
        }],
        "properties": {
            title: entry.title,
            text: entry.text
        }
    };

    //creates a source with the geojson object from above
    map.addSource(currentKey, { //currentKey is the name of this source
        "type": "geojson",
        "data": geojson,
        cluster: true //clusters the points in this source
    });

    //adds the source defined above to a layer that will be displayed on a map
    map.addLayer({
        "id": currentKey, // Sets id as current child's key 
        "source": currentKey, // The source layer defined above
    });
});

The problem is that the markers will be in individual sources, making them appear on different layers. Therefore, I cannot cluster them or e.g. search across them.

What I look for is a way to add a source to an existing layer. This would enable me to create a layer outside the on('child_added') method and then add the sources to this layer.

I have looked at the mapbox GL docs but I cannot find anything in there that will enable me to do this. It seems very limited in this respect compared to mapbox js.

I see this as a pretty important feature and don't understand why this is not possible. I hope some of you have a workaround or a way to achieve asynchronously adding markers to a map in mapbox GL.


Solution

  • As the documentation at https://www.mapbox.com/mapbox-gl-js/api/#geojsonsource states: A GeoJSON data object or a URL to one. The latter is preferable in the case of large GeoJSON files.

    What happens here is that geojson sources loaded via a url are loaded using a background thread worker so they do not affect the main thread, basically always load your data via url or a mapbox style to offload all JSON parsing and layer loading to another thread. Thus anytime you have a change event fired from your firebase monitoring you can simply reload the url you are using to initially load the source.

    In addition, the founder of Leaflet and amazing Mapbox Developer Vladimir Agafonkin discusses this here: https://github.com/mapbox/mapbox-gl-js/issues/2289, and it is essentially what they do in their real-time example: https://www.mapbox.com/mapbox-gl-js/example/live-geojson/.

    Furthermore, here is an example with socket.io I use client side:

    const url = {server url that retrieves geojson},
          socket = {setup all your socket initiation, etc};
    
          socket.on('msg', function(data) {
    
                if (data) {
                    //Here is where you can manipulate the JSON object returned from the socket server
                    console.log("Message received is: %s", JSON.stringify(data));
                    if(data.fetch){
                        map.getSource('stuff').setData(url)
                    }
    
                } else {
                    console.log("Message received is empty: so it is %s", JSON.stringify(data));
                }
    
            });
    
    
            map.on('load', function(feature) {
    
                map.addSource('events', {
                    type: 'stuff',
                    data: url
                });
                map.addLayer({
                    "id": "events",
                    "type": "symbol",
                    "source": "events",
                    "layout": {
                        "icon-image": "{icon}"
                    }
                });
            });