Search code examples
leafletleaflet.markercluster

Leaflet Clustering Marker Groups with grouped Circles


I'm trying to cluster some Markers that have Circles grouped with them.

The clustering works. But the Circles don't appear once zoomed in enough to display the Markers instead of clusters.

Example code:

const lMap = L.map('map').setView([51.508, -0.128], 11);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
  maxZoom: 19,
  attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(lMap);
const mainItems = [L.marker([51.508, -0.128]), L.marker([51.5, -0.14]), L.marker([51.51, -0.1])];
const subItems = [L.circle([51.508, -0.128], {
  color: '#38f',
  fillColor: '#38f',
  fillOpacity: 0.2,
  radius: 60
}), L.circle([51.5, -0.14], {
  color: '#38f',
  fillColor: '#38f',
  fillOpacity: 0.2,
  radius: 40
}), L.circle([51.51, -0.1], {
  color: '#38f',
  fillColor: '#38f',
  fillOpacity: 0.2,
  radius: 50
})];
const mainGroup = L.featureGroup(mainItems);
const subGroup = L.featureGroup.subGroup(mainGroup, subItems);

const mainCluster = L.markerClusterGroup();
mainCluster.addLayer(mainGroup);
lMap.addLayer(mainCluster);
lMap.addLayer(subGroup);

If I remove the clustering, then Circles do appear:

// const mainCluster = L.markerClusterGroup();
// mainCluster.addLayer(mainGroup);
lMap.addLayer(mainGroup);
lMap.addLayer(subGroup);

Is what I'm attempting possible? What is the correct way to do it?


Solution

  • While not explicit, Leaflet.markercluster has only partial support of Layer Groups:

    Groups are not actually added into the MarkerClusterGroup, only their non-group child layers.

    As such, when you add subGroup to the map, and it adds the subItems into mainGroup, this does not transitively result into adding them into mainCluster Marker Cluster Group (MCG).

    IIUC, you would like to "decorate" your Markers with some Circles, but you cannot directly add them into the MCG, otherwise they would increase the count and trigger spiderfication; and you still want them to be shown or hidden accordingly with the Marker they decorate?

    In that case, you have several workarounds, one of them being to have directly a custom icon that contains its own decoration, as suggested by RobertHardy's answer.

    Another possible solution, closer to your original intention of using actual Circles for decoration, would be to listen to your Markers events being added or removed from the map (being handled by MCG for (un)clustering):

    map.on('layeradd', (e) => {
        mainItems.forEach((marker, index) => {
            if (e.layer === marker) {
                // Assumes subItems are in same order as mainItems
                map.addLayer(subItems[index]);
            }
        });
    });
    
    map.on('layerremove', (e) => {
        mainItems.forEach((marker, index) => {
            if (e.layer === marker) {
                // Assumes subItems are in same order as mainItems
                map.removeLayer(subItems[index]);
            }
        });
    });
    

    Live demo: https://plnkr.co/edit/8XfhHuh3YU3ZlgtK?preview


    Another solution, somehow more generic and robust, could be to create a special Marker that "knows" that it should come along with an associated decoration Layer when it is unclustered, and collects it back when clustered:

    // Variation of Leaflet Marker that comes along with an
    // associated "decorationLayer" whenever it is added to the map,
    // and goes away with it when removed.
    // Typically useful when added to a MarkerClusterGroup,
    // so that the decoration does not count, but still follows
    // the Marker (un)clustering.
    // Limitation: when spiderfied, decoration remains on original coordinates.
    // Could be improved with custom setLagLng method?
    L.Marker.Decorated = L.Marker.extend({
        options: {
            decorationLayer: null, // Could even be a Layer Group!
        },
        onAdd(map) {
            L.Marker.prototype.onAdd.call(this, map);
    
            // If decoration has been provided, add it to the map as well
            this.options.decorationLayer?.addTo(map);
        },
        onRemove(map) {
            L.Marker.prototype.onRemove.call(this, map);
    
            // If decoration has been provided, remove it from the map as well
            this.options.decorationLayer?.remove();
        },
    });
    
    const mainItems = [
        new L.Marker.Decorated([51.508, -0.128], {
            decorationLayer: L.circle([51.508, -0.128], {
                color: '#38f',
                fillColor: '#38f',
                fillOpacity: 0.2,
                radius: 500,
            }),
        }),
        // Etc.
    ];
    

    Live demo: https://plnkr.co/edit/Utr7WC110oIzaATl?preview