/*
* L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
*/
L.MarkerClusterGroup = L.FeatureGroup.extend({
options: {
maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center
iconCreateFunction: null,
spiderfyOnMaxZoom: true,
showCoverageOnHover: true,
zoomToBoundsOnClick: true,
singleMarkerMode: false,
disableClusteringAtZoom: null,
//Whether to animate adding markers after adding the MarkerClusterGroup to the map
// If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
animateAddingMarkers: false,
//Options to pass to the L.Polygon constructor
polygonOptions: {}
},
initialize: function (options) {
L.Util.setOptions(this, options);
if (!this.options.iconCreateFunction) {
this.options.iconCreateFunction = this._defaultIconCreateFunction;
}
L.FeatureGroup.prototype.initialize.call(this, []);
this._inZoomAnimation = 0;
this._needsClustering = [];
//The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
this._currentShownBounds = null;
},
addLayer: function (layer) {
if (layer instanceof L.LayerGroup) {
var array = [];
for (var i in layer._layers) {
if (layer._layers.hasOwnProperty(i)) {
array.push(layer._layers[i]);
}
}
return this.addLayers(array);
}
if (!this._map) {
this._needsClustering.push(layer);
return this;
}
if (this.hasLayer(layer)) {
return this;
}
//If we have already clustered we'll need to add this one to a cluster
if (this._unspiderfy) {
this._unspiderfy();
}
this._addLayer(layer, this._maxZoom);
//Work out what is visible
var visibleLayer = layer,
currentZoom = this._map.getZoom();
if (layer.__parent) {
while (visibleLayer.__parent._zoom >= currentZoom) {
visibleLayer = visibleLayer.__parent;
}
}
if (this._currentShownBounds.contains(visibleLayer.getLatLng())) {
if (this.options.animateAddingMarkers) {
this._animationAddLayer(layer, visibleLayer);
} else {
this._animationAddLayerNonAnimated(layer, visibleLayer);
}
}
return this;
},
removeLayer: function (layer) {
if (!this._map) {
this._arraySplice(this._needsClustering, layer);
return this;
}
if (!layer.__parent) {
return this;
}
if (this._unspiderfy) {
this._unspiderfy();
this._unspiderfyLayer(layer);
}
//Remove the marker from clusters
this._removeLayer(layer, true);
if (layer._icon) {
L.FeatureGroup.prototype.removeLayer.call(this, layer);
layer.setOpacity(1);
}
return this;
},
//Takes an array of markers and adds them in bulk
addLayers: function (layersArray) {
if (!this._map) {
this._needsClustering = this._needsClustering.concat(layersArray);
return this;
}
for (var i = 0, l = layersArray.length; i < l; i++) {
var m = layersArray[i];
if (this.hasLayer(m)) {
continue;
}
this._addLayer(m, this._maxZoom);
//If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will
if (m.__parent) {
if (m.__parent.getChildCount() === 2) {
var markers = m.__parent.getAllChildMarkers(),
otherMarker = markers[0] === m ? markers[1] : markers[0];
L.FeatureGroup.prototype.removeLayer.call(this, otherMarker);
}
}
}
this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
return this;
},
//Takes an array of markers and removes them in bulk
removeLayers: function (layersArray) {
var i, l, m;
if (!this._map) {
for (i = 0, l = layersArray.length; i < l; i++) {
this._arraySplice(this._needsClustering, layersArray[i]);
}
return this;
}
for (i = 0, l = layersArray.length; i < l; i++) {
m = layersArray[i];
this._removeLayer(m, true, true);
if (m._icon) {
L.FeatureGroup.prototype.removeLayer.call(this, m);
m.setOpacity(1);
}
}
//Fix up the clusters and markers on the map
this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
for (i in this._layers) {
if (this._layers.hasOwnProperty(i)) {
m = this._layers[i];
if (m instanceof L.MarkerCluster) {
m._updateIcon();
}
}
}
return this;
},
//Removes all layers from the MarkerClusterGroup
clearLayers: function () {
//Need our own special implementation as the LayerGroup one doesn't work for us
//If we aren't on the map (yet), blow away the markers we know of
if (!this._map) {
this._needsClustering = [];
delete this._gridClusters;
delete this._gridUnclustered;
}
if (this._unspiderfy) {
this._unspiderfy();
}
//Remove all the visible layers
for (var i in this._layers) {
if (this._layers.hasOwnProperty(i)) {
L.FeatureGroup.prototype.removeLayer.call(this, this._layers[i]);
}
}
if (this._map) {
//Reset _topClusterLevel and the DistanceGrids
this._generateInitialClusters();
}
return this;
},
//Override FeatureGroup.getBounds as it doesn't work
getBounds: function () {
var bounds = new L.LatLngBounds();
if (this._topClusterLevel) {
bounds.extend(this._topClusterLevel._bounds);
} else {
for (var i = this._needsClustering.length - 1; i >= 0; i--) {
bounds.extend(this._needsClustering[i].getLatLng());
}
}
return bounds;
},
//Returns true if the given layer is in this MarkerClusterGroup
hasLayer: function (layer) {
if (this._needsClustering.length > 0) {
var anArray = this._needsClustering;
for (var i = anArray.length - 1; i >= 0; i--) {
if (anArray[i] === layer) {
return true;
}
}
}
return !!(layer.__parent && layer.__parent._group === this);
},
//Zoom down to show the given layer (spiderfying if necessary) then calls the callback
zoomToShowLayer: function (layer, callback) {
var showMarker = function () {
if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) {
this._map.off('moveend', showMarker, this);
this.off('animationend', showMarker, this);
if (layer._icon) {
callback();
} else if (layer.__parent._icon) {
var afterSpiderfy = function () {
this.off('spiderfied', afterSpiderfy, this);
callback();
};
this.on('spiderfied', afterSpiderfy, this);
layer.__parent.spiderfy();
}
}
};
if (layer._icon) {
callback();
} else if (layer.__parent._zoom < this._map.getZoom()) {
//Layer should be visible now but isn't on screen, just pan over to it
this._map.on('moveend', showMarker, this);
if (!layer._icon) {
this._map.panTo(layer.getLatLng());
}
} else {
this._map.on('moveend', showMarker, this);
this.on('animationend', showMarker, this);
this._map.setView(layer.getLatLng(), layer.__parent._zoom + 1);
layer.__parent.zoomToBounds();
}
},
//Overrides FeatureGroup.onAdd
onAdd: function (map) {
L.FeatureGroup.prototype.onAdd.call(this, map);
if (!this._gridClusters) {
this._generateInitialClusters();
}
for (var i = 0, l = this._needsClustering.length; i < l; i++) {
var layer = this._needsClustering[i];
if (layer.__parent) {
continue;
}
this._addLayer(layer, this._maxZoom);
}
this._needsClustering = [];
this._map.on('zoomend', this._zoomEnd, this);
this._map.on('moveend', this._moveEnd, this);
if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
this._spiderfierOnAdd();
}
this._bindEvents();
//Actually add our markers to the map:
//Remember the current zoom level and bounds
this._zoom = this._map.getZoom();
this._currentShownBounds = this._getExpandedVisibleBounds();
//Make things appear on the map
this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
},
//Overrides FeatureGroup.onRemove
onRemove: function (map) {
this._map.off('zoomend', this._zoomEnd, this);
this._map.off('moveend', this._moveEnd, this);
//In case we are in a cluster animation
this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
this._spiderfierOnRemove();
}
L.FeatureGroup.prototype.onRemove.call(this, map);
},
//Remove the given object from the given array
_arraySplice: function (anArray, obj) {
for (var i = anArray.length - 1; i >= 0; i--) {
if (anArray[i] === obj) {
anArray.splice(i, 1);
return;
}
}
},
//Internal function for removing a marker from everything.
//dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
_removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) {
var gridClusters = this._gridClusters,
gridUnclustered = this._gridUnclustered,
map = this._map;
//Remove the marker from distance clusters it might be in
if (removeFromDistanceGrid) {
for (var z = this._maxZoom; z >= 0; z--) {
if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) {
break;
}
}
}
//Work our way up the clusters removing them as we go if required
var cluster = marker.__parent,
markers = cluster._markers,
otherMarker;
//Remove the marker from the immediate parents marker list
this._arraySplice(markers, marker);
while (cluster) {
cluster._childCount--;
if (cluster._zoom < 0) {
//Top level, do nothing
break;
} else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required
//We need to push the other marker up to the parent
otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0];
//Update distance grid
gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom));
gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom));
//Move otherMarker up to parent
this._arraySplice(cluster.__parent._childClusters, cluster);
cluster.__parent._markers.push(otherMarker);
otherMarker.__parent = cluster.__parent;
if (cluster._icon) {
//Cluster is currently on the map, need to put the marker on the map instead
L.FeatureGroup.prototype.removeLayer.call(this, cluster);
if (!dontUpdateMap) {
L.FeatureGroup.prototype.addLayer.call(this, otherMarker);
}
}
} else {
cluster._recalculateBounds();
if (!dontUpdateMap || !cluster._icon) {
cluster._updateIcon();
}
}
cluster = cluster.__parent;
}
delete marker.__parent;
},
//Overrides FeatureGroup._propagateEvent
_propagateEvent: function (e) {
if (e.target instanceof L.MarkerCluster) {
e.type = 'cluster' + e.type;
}
L.FeatureGroup.prototype._propagateEvent.call(this, e);
},
//Default functionality
_defaultIconCreateFunction: function (cluster) {
var childCount = cluster.getChildCount();
var c = ' marker-cluster-';
if (childCount < 10) {
c += 'small';
} else if (childCount < 100) {
c += 'medium';
} else {
c += 'large';
}
return new L.DivIcon({ html: '
' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
},
_bindEvents: function () {
var shownPolygon = null,
map = this._map,
spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
showCoverageOnHover = this.options.showCoverageOnHover,
zoomToBoundsOnClick = this.options.zoomToBoundsOnClick;
//Zoom on cluster click or spiderfy if we are at the lowest level
if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
this.on('clusterclick', function (a) {
if (map.getMaxZoom() === map.getZoom()) {
if (spiderfyOnMaxZoom) {
a.layer.spiderfy();
}
} else if (zoomToBoundsOnClick) {
a.layer.zoomToBounds();
}
}, this);
}
//Show convex hull (boundary) polygon on mouse over
if (showCoverageOnHover) {
this.on('clustermouseover', function (a) {
if (this._inZoomAnimation) {
return;
}
if (shownPolygon) {
map.removeLayer(shownPolygon);
}
if (a.layer.getChildCount() > 2) {
shownPolygon = new L.Polygon(a.layer.getConvexHull(), this.options.polygonOptions);
map.addLayer(shownPolygon);
}
}, this);
this.on('clustermouseout', function () {
if (shownPolygon) {
map.removeLayer(shownPolygon);
shownPolygon = null;
}
}, this);
map.on('zoomend', function () {
if (shownPolygon) {
map.removeLayer(shownPolygon);
shownPolygon = null;
}
}, this);
map.on('layerremove', function (opt) {
if (shownPolygon && opt.layer === this) {
map.removeLayer(shownPolygon);
shownPolygon = null;
}
}, this);
}
},
_zoomEnd: function () {
if (!this._map) { //May have been removed from the map by a zoomEnd handler
return;
}
this._mergeSplitClusters();
this._zoom = this._map._zoom;
this._currentShownBounds = this._getExpandedVisibleBounds();
},
_moveEnd: function () {
if (this._inZoomAnimation) {
return;
}
var newBounds = this._getExpandedVisibleBounds();
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds);
this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, newBounds);
this._currentShownBounds = newBounds;
return;
},
_generateInitialClusters: function () {
var maxZoom = this._map.getMaxZoom(),
radius = this.options.maxClusterRadius;
if (this.options.disableClusteringAtZoom) {
maxZoom = this.options.disableClusteringAtZoom - 1;
}
this._maxZoom = maxZoom;
this._gridClusters = {};
this._gridUnclustered = {};
//Set up DistanceGrids for each zoom
for (var zoom = maxZoom; zoom >= 0; zoom--) {
this._gridClusters[zoom] = new L.DistanceGrid(radius);
this._gridUnclustered[zoom] = new L.DistanceGrid(radius);
}
this._topClusterLevel = new L.MarkerCluster(this, -1);
},
//Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
_addLayer: function (layer, zoom) {
var gridClusters = this._gridClusters,
gridUnclustered = this._gridUnclustered,
markerPoint, z;
if (this.options.singleMarkerMode) {
layer.options.icon = this.options.iconCreateFunction({
getChildCount: function () {
return 1;
},
getAllChildMarkers: function () {
return [layer];
}
});
}
//Find the lowest zoom level to slot this one in
for (; zoom >= 0; zoom--) {
markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position
//Try find a cluster close by
var closest = gridClusters[zoom].getNearObject(markerPoint);
if (closest) {
closest._addChild(layer);
layer.__parent = closest;
return;
}
//Try find a marker close by to form a new cluster with
closest = gridUnclustered[zoom].getNearObject(markerPoint);
if (closest) {
var parent = closest.__parent;
if (parent) {
this._removeLayer(closest, false);
}
//Create new cluster with these 2 in it
var newCluster = new L.MarkerCluster(this, zoom, closest, layer);
gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom));
closest.__parent = newCluster;
layer.__parent = newCluster;
//First create any new intermediate parent clusters that don't exist
var lastParent = newCluster;
for (z = zoom - 1; z > parent._zoom; z--) {
lastParent = new L.MarkerCluster(this, z, lastParent);
gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z));
}
parent._addChild(lastParent);
//Remove closest from this zoom level and any above that it is in, replace with newCluster
for (z = zoom; z >= 0; z--) {
if (!gridUnclustered[z].removeObject(closest, this._map.project(closest.getLatLng(), z))) {
break;
}
}
return;
}
//Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
gridUnclustered[zoom].addObject(layer, markerPoint);
}
//Didn't get in anything, add us to the top
this._topClusterLevel._addChild(layer);
layer.__parent = this._topClusterLevel;
return;
},
//Merge and split any existing clusters that are too big or small
_mergeSplitClusters: function () {
if (this._zoom < this._map._zoom) { //Zoom in, split
this._animationStart();
//Remove clusters now off screen
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds());
this._animationZoomIn(this._zoom, this._map._zoom);
} else if (this._zoom > this._map._zoom) { //Zoom out, merge
this._animationStart();
this._animationZoomOut(this._zoom, this._map._zoom);
} else {
this._moveEnd();
}
},
//Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan)
_getExpandedVisibleBounds: function () {
var map = this._map,
bounds = map.getBounds(),
sw = bounds._southWest,
ne = bounds._northEast,
latDiff = L.Browser.mobile ? 0 : Math.abs(sw.lat - ne.lat),
lngDiff = L.Browser.mobile ? 0 : Math.abs(sw.lng - ne.lng);
return new L.LatLngBounds(
new L.LatLng(sw.lat - latDiff, sw.lng - lngDiff, true),
new L.LatLng(ne.lat + latDiff, ne.lng + lngDiff, true));
},
//Shared animation code
_animationAddLayerNonAnimated: function (layer, newCluster) {
if (newCluster === layer) {
L.FeatureGroup.prototype.addLayer.call(this, layer);
} else if (newCluster._childCount === 2) {
newCluster._addToMap();
var markers = newCluster.getAllChildMarkers();
L.FeatureGroup.prototype.removeLayer.call(this, markers[0]);
L.FeatureGroup.prototype.removeLayer.call(this, markers[1]);
} else {
newCluster._updateIcon();
}
}
});
L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
//Non Animated versions of everything
_animationStart: function () {
//Do nothing...
},
_animationZoomIn: function (previousZoomLevel, newZoomLevel) {
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
},
_animationZoomOut: function (previousZoomLevel, newZoomLevel) {
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
},
_animationAddLayer: function (layer, newCluster) {
this._animationAddLayerNonAnimated(layer, newCluster);
}
} : {
//Animated versions here
_animationStart: function () {
this._map._mapPane.className += ' leaflet-cluster-anim';
this._inZoomAnimation++;
},
_animationEnd: function () {
if (this._map) {
this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
}
this._inZoomAnimation--;
this.fire('animationend');
},
_animationZoomIn: function (previousZoomLevel, newZoomLevel) {
var me = this,
bounds = this._getExpandedVisibleBounds(),
i;
//Add all children of current clusters to map and remove those clusters from map
this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
var startPos = c._latlng,
markers = c._markers,
m;
if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us
L.FeatureGroup.prototype.removeLayer.call(me, c);
c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);
} else {
//Fade out old cluster
c.setOpacity(0);
c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds);
}
//Remove all markers that aren't visible any more
//TODO: Do we actually need to do this on the higher levels too?
for (i = markers.length - 1; i >= 0; i--) {
m = markers[i];
if (!bounds.contains(m._latlng)) {
L.FeatureGroup.prototype.removeLayer.call(me, m);
}
}
});
this._forceLayout();
var j, n;
//Update opacities
me._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel);
//TODO Maybe? Update markers in _recursivelyBecomeVisible
for (j in me._layers) {
if (me._layers.hasOwnProperty(j)) {
n = me._layers[j];
if (!(n instanceof L.MarkerCluster) && n._icon) {
n.setOpacity(1);
}
}
}
//update the positions of the just added clusters/markers
me._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {
c._recursivelyRestoreChildPositions(newZoomLevel);
});
//Remove the old clusters and close the zoom animation
setTimeout(function () {
//update the positions of the just added clusters/markers
me._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
L.FeatureGroup.prototype.removeLayer.call(me, c);
c.setOpacity(1);
});
me._animationEnd();
}, 250);
},
_animationZoomOut: function (previousZoomLevel, newZoomLevel) {
this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel);
//Need to add markers for those that weren't on the map before but are now
this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
//Remove markers that were on the map before but won't be now
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel, this._getExpandedVisibleBounds());
},
_animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) {
var bounds = this._getExpandedVisibleBounds();
//Animate all of the markers in the clusters to move to their cluster center point
cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel);
var me = this;
//Update the opacity (If we immediately set it they won't animate)
this._forceLayout();
cluster._recursivelyBecomeVisible(bounds, newZoomLevel);
//TODO: Maybe use the transition timing stuff to make this more reliable
//When the animations are done, tidy up
setTimeout(function () {
//This cluster stopped being a cluster before the timeout fired
if (cluster._childCount === 1) {
var m = cluster._markers[0];
//If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it
m.setLatLng(m.getLatLng());
m.setOpacity(1);
return;
}
cluster._recursively(bounds, newZoomLevel, 0, function (c) {
c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1);
});
me._animationEnd();
}, 250);
},
_animationAddLayer: function (layer, newCluster) {
var me = this;
L.FeatureGroup.prototype.addLayer.call(this, layer);
if (newCluster !== layer) {
if (newCluster._childCount > 2) { //Was already a cluster
newCluster._updateIcon();
this._forceLayout();
this._animationStart();
layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));
layer.setOpacity(0);
setTimeout(function () {
L.FeatureGroup.prototype.removeLayer.call(me, layer);
layer.setOpacity(1);
me._animationEnd();
}, 250);
} else { //Just became a cluster
this._forceLayout();
me._animationStart();
me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom());
}
}
},
//Force a browser layout of stuff in the map
// Should apply the current opacity and location to all elements so we can update them again for an animation
_forceLayout: function () {
//In my testing this works, infact offsetWidth of any element seems to work.
//Could loop all this._layers and do this for each _icon if it stops working
L.Util.falseFn(document.body.offsetWidth);
}
});