/* * 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); } });