Leaflet + Supercluster: L.marker's autoPan not working

Viewed 983

I'm using the mapbox supercluster library to cluster a fairly large amount of point data and plotting it with leaflet inside a shiny app. At maxZoom the points are shown and are getting draggable for the user in case the location is wrong. Mapbox supercluster uses a static kdbush spatial index, so each time a L.marker is moved, the cluster will be reloaded (not a problem in my case, because supercluster is extremely fast).

However, it is only possible to drag an L.marker used with supercluster inside the visible map bounds. This makes sense, because the L.marker's are generated on the fly for only the visible map bounds. If the L.marker are defined with options {draggable:true,autoPan:true} an error occurs in case L.marker is dragged outside of the visible map extent:

Uncaught TypeError: t is null
  leaflet 1.3.3/dist/leaflet.js:5
  leaflet 1.3.3/dist/leaflet.js:5
  at https://unpkg.com/leaflet@1.3.3/dist/leaflet.js:5
  _adjustPan https://unpkg.com/leaflet@1.3.3/dist/leaflet.js:5
  _adjustPan self-hosted:891
  _adjustPan self-hosted:844

or in my shiny app I get the following error:

Uncaught TypeError: Cannot set property '_leaflet_pos' of null
at Object.Lt [as setPosition] (eval at <anonymous> (jquery.min.js:2), <anonymous>:5:9959)
at e._adjustPan (eval at <anonymous> (jquery.min.js:2), <anonymous>:5:71149)

It would be really cool to have some sort of autopan for L.marker's used with supercluster. I know the user could just zoom out to drag the point to the intended location. This is not really an option in my case, because there are quickly too many points in the visible map extent which will slow down map panning (that's why I use the clustering in the first place).

I tried to center the leaflet map during the drag event using panTo

layer.on('drag',function(e){
//console.log('marker dragstart event');
var position=e.target.getLatLng();
map.panTo(new L.latLng(position.lat,position.lng));
});

but it seems that this somehow stops the drag event.

My question is: Is there a way to get an autopan for L.marker used with supercluster?

I set up a minimal reproducible example below. This example plots a single point (var marker) as well as three points (var markers) with supercluster. The single point can be dragged outside the visible map extent whereas the cluster points cannot.

var map = L.map('map').setView([51.505, -1.09], 9);

L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);

var marker = new L.marker([51.505, -1.09],{
    draggable: true,
    autoPan:true
}).bindPopup('autopan working').addTo(map);

// Empty Layer Group that will receive the clusters data on the fly.
var markers = L.geoJson(null, {
  pointToLayer: createClusterIcon,
  onEachFeature: onEachFeature
}).addTo(map);

var clusterData = {
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties":{
        "id":0
      },
      "geometry": {
        "type": "Point",
        "coordinates": [-1.0,51.5]
      }
    },
    {
     "type": "Feature",
     "properties":{
        "id":1
      },
      "geometry": {
        "type": "Point",
        "coordinates": [-1.1,51.6]
      }
    },
    {
    "type": "Feature",
    "properties":{
        "id":2
      },
     "geometry": {
        "type": "Point",
        "coordinates": [-0.9,51.4]
     }
    }
    ]
}
        
function onEachFeature(f, layer) {          
    //add drag event
    layer.on('drag',function(e){
    //console.log('marker dragstart event');
    //var position=e.target.getLatLng();
    //map.panTo(new L.latLng(position.lat,position.lng));
    });
  layer.on('dragend',function(e){
    //console.log('marker dragend event');
    var changedPos = e.target.getLatLng();
    //console.log('new location '+changedPos);
    //load cluster again
    clusterData.features[f.properties.id].geometry.coordinates[1]=changedPos.lat;
    clusterData.features[f.properties.id].geometry.coordinates[0]=changedPos.lng;
    index.load(clusterData.features);
    update();
  });
}

     
// Update the displayed clusters after user pan / zoom.
map.on('moveend', update);

function update() {
    if (!ready) return;
    var bounds = map.getBounds();
    var bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
    var zoom = map.getZoom();
    var clusters = index.getClusters(bbox, zoom);
    //console.log('clusters '+clusters);
    markers.clearLayers();
    markers.addData(clusters);
}


// Zoom to expand the cluster clicked by user.
markers.on('click', function(e) {
    //console.log('check data' + e.layer.feature.properties.cluster_id);
  var clusterId = e.layer.feature.properties.cluster_id;
  var center = e.latlng;
  var expansionZoom;
  if (clusterId) {
    expansionZoom = index.getClusterExpansionZoom(clusterId);
    map.flyTo(center, expansionZoom);
    } 
});


var ready = false;
//load data
const index = new Supercluster({
    radius: 150,
    maxZoom:10
});
index.load(clusterData.features);
ready= true;
update();


function createClusterIcon(feature, latlng) {
    if (!feature.properties.cluster){ 
        return L.marker(latlng,{draggable:true,autoPan:true}); //add autoPan:true
    }
  
  var count = feature.properties.point_count;
  var size =
    count < 100 ? 'small' :
    count < 1000 ? 'medium' : 'large';
  var icon = L.divIcon({
    html: '<div><span>' + feature.properties.point_count_abbreviated + '</span></div>',
    className: 'marker-cluster marker-cluster-' + size,
    iconSize: L.point(40, 40)
  });
  return L.marker(latlng, {
    icon: icon
  });
}
#map {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
}
<script src="https://unpkg.com/supercluster@7.1.0/dist/supercluster.min.js"></script>
<link href="https://cdn.rawgit.com/mapbox/supercluster/v4.0.1/demo/cluster.css" rel="stylesheet"/>
<script src="https://unpkg.com/leaflet@1.3.3/dist/leaflet.js"></script>
<link href="https://unpkg.com/leaflet@1.3.3/dist/leaflet.css" rel="stylesheet"/>
<div id="map"></div>

0 Answers
Related