2 * Leaflet.markercluster 1.4.1+master.37ab9a2,
3 * Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.
4 * https://github.com/Leaflet/Leaflet.markercluster
5 * (c) 2012-2017, Dave Leaver, smartrak
7 (function (global, factory) {
8 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
9 typeof define === 'function' && define.amd ? define(['exports'], factory) :
10 (factory((global.Leaflet = global.Leaflet || {}, global.Leaflet.markercluster = global.Leaflet.markercluster || {})));
11 }(this, (function (exports) { 'use strict';
14 * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
17 var MarkerClusterGroup = L.MarkerClusterGroup = L.FeatureGroup.extend({
20 maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center
21 iconCreateFunction: null,
22 clusterPane: L.Marker.prototype.options.pane,
24 spiderfyOnMaxZoom: true,
25 showCoverageOnHover: true,
26 zoomToBoundsOnClick: true,
27 singleMarkerMode: false,
29 disableClusteringAtZoom: null,
31 // Setting this to false prevents the removal of any clusters outside of the viewpoint, which
32 // is the default behaviour for performance reasons.
33 removeOutsideVisibleBounds: true,
35 // Set to false to disable all animations (zoom and spiderfy).
36 // If false, option animateAddingMarkers below has no effect.
37 // If L.DomUtil.TRANSITION is falsy, this option has no effect.
40 //Whether to animate adding markers after adding the MarkerClusterGroup to the map
41 // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
42 animateAddingMarkers: false,
44 //Increase to increase the distance away that spiderfied markers appear from the center
45 spiderfyDistanceMultiplier: 1,
47 // Make it possible to specify a polyline options on a spider leg
48 spiderLegPolylineOptions: { weight: 1.5, color: '#222', opacity: 0.5 },
50 // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts
51 chunkedLoading: false,
52 chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback)
53 chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser
54 chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator)
56 //Options to pass to the L.Polygon constructor
60 initialize: function (options) {
61 L.Util.setOptions(this, options);
62 if (!this.options.iconCreateFunction) {
63 this.options.iconCreateFunction = this._defaultIconCreateFunction;
66 this._featureGroup = L.featureGroup();
67 this._featureGroup.addEventParent(this);
69 this._nonPointGroup = L.featureGroup();
70 this._nonPointGroup.addEventParent(this);
72 this._inZoomAnimation = 0;
73 this._needsClustering = [];
74 this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of
75 //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
76 this._currentShownBounds = null;
80 this._childMarkerEventHandlers = {
81 'dragstart': this._childMarkerDragStart,
82 'move': this._childMarkerMoved,
83 'dragend': this._childMarkerDragEnd,
86 // Hook the appropriate animation methods.
87 var animate = L.DomUtil.TRANSITION && this.options.animate;
88 L.extend(this, animate ? this._withAnimation : this._noAnimation);
89 // Remember which MarkerCluster class to instantiate (animated or not).
90 this._markerCluster = animate ? L.MarkerCluster : L.MarkerClusterNonAnimated;
93 addLayer: function (layer) {
95 if (layer instanceof L.LayerGroup) {
96 return this.addLayers([layer]);
99 //Don't cluster non point data
100 if (!layer.getLatLng) {
101 this._nonPointGroup.addLayer(layer);
102 this.fire('layeradd', { layer: layer });
107 this._needsClustering.push(layer);
108 this.fire('layeradd', { layer: layer });
112 if (this.hasLayer(layer)) {
117 //If we have already clustered we'll need to add this one to a cluster
119 if (this._unspiderfy) {
123 this._addLayer(layer, this._maxZoom);
124 this.fire('layeradd', { layer: layer });
126 // Refresh bounds and weighted positions.
127 this._topClusterLevel._recalculateBounds();
129 this._refreshClustersIcons();
131 //Work out what is visible
132 var visibleLayer = layer,
133 currentZoom = this._zoom;
134 if (layer.__parent) {
135 while (visibleLayer.__parent._zoom >= currentZoom) {
136 visibleLayer = visibleLayer.__parent;
140 if (this._currentShownBounds.contains(visibleLayer.getLatLng())) {
141 if (this.options.animateAddingMarkers) {
142 this._animationAddLayer(layer, visibleLayer);
144 this._animationAddLayerNonAnimated(layer, visibleLayer);
150 removeLayer: function (layer) {
152 if (layer instanceof L.LayerGroup) {
153 return this.removeLayers([layer]);
157 if (!layer.getLatLng) {
158 this._nonPointGroup.removeLayer(layer);
159 this.fire('layerremove', { layer: layer });
164 if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) {
165 this._needsRemoving.push({ layer: layer, latlng: layer._latlng });
167 this.fire('layerremove', { layer: layer });
171 if (!layer.__parent) {
175 if (this._unspiderfy) {
177 this._unspiderfyLayer(layer);
180 //Remove the marker from clusters
181 this._removeLayer(layer, true);
182 this.fire('layerremove', { layer: layer });
184 // Refresh bounds and weighted positions.
185 this._topClusterLevel._recalculateBounds();
187 this._refreshClustersIcons();
189 layer.off(this._childMarkerEventHandlers, this);
191 if (this._featureGroup.hasLayer(layer)) {
192 this._featureGroup.removeLayer(layer);
193 if (layer.clusterShow) {
201 //Takes an array of markers and adds them in bulk
202 addLayers: function (layersArray, skipLayerAddEvent) {
203 if (!L.Util.isArray(layersArray)) {
204 return this.addLayer(layersArray);
207 var fg = this._featureGroup,
208 npg = this._nonPointGroup,
209 chunked = this.options.chunkedLoading,
210 chunkInterval = this.options.chunkInterval,
211 chunkProgress = this.options.chunkProgress,
212 l = layersArray.length,
214 originalArray = true,
218 var started = (new Date()).getTime();
219 var process = L.bind(function () {
220 var start = (new Date()).getTime();
221 for (; offset < l; offset++) {
222 if (chunked && offset % 200 === 0) {
223 // every couple hundred markers, instrument the time elapsed since processing started:
224 var elapsed = (new Date()).getTime() - start;
225 if (elapsed > chunkInterval) {
226 break; // been working too hard, time to take a break :-)
230 m = layersArray[offset];
232 // Group of layers, append children to layersArray and skip.
234 // - Total increases, so chunkProgress ratio jumps backward.
235 // - Groups are not included in this group, only their non-group child layers (hasLayer).
236 // Changing array length while looping does not affect performance in current browsers:
237 // http://jsperf.com/for-loop-changing-length/6
238 if (m instanceof L.LayerGroup) {
240 layersArray = layersArray.slice();
241 originalArray = false;
243 this._extractNonGroupLayers(m, layersArray);
244 l = layersArray.length;
248 //Not point data, can't be clustered
251 if (!skipLayerAddEvent) {
252 this.fire('layeradd', { layer: m });
257 if (this.hasLayer(m)) {
261 this._addLayer(m, this._maxZoom);
262 if (!skipLayerAddEvent) {
263 this.fire('layeradd', { layer: m });
266 //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
268 if (m.__parent.getChildCount() === 2) {
269 var markers = m.__parent.getAllChildMarkers(),
270 otherMarker = markers[0] === m ? markers[1] : markers[0];
271 fg.removeLayer(otherMarker);
277 // report progress and time elapsed:
278 chunkProgress(offset, l, (new Date()).getTime() - started);
281 // Completed processing all markers.
284 // Refresh bounds and weighted positions.
285 this._topClusterLevel._recalculateBounds();
287 this._refreshClustersIcons();
289 this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
291 setTimeout(process, this.options.chunkDelay);
297 var needsClustering = this._needsClustering;
299 for (; offset < l; offset++) {
300 m = layersArray[offset];
302 // Group of layers, append children to layersArray and skip.
303 if (m instanceof L.LayerGroup) {
305 layersArray = layersArray.slice();
306 originalArray = false;
308 this._extractNonGroupLayers(m, layersArray);
309 l = layersArray.length;
313 //Not point data, can't be clustered
319 if (this.hasLayer(m)) {
323 needsClustering.push(m);
329 //Takes an array of markers and removes them in bulk
330 removeLayers: function (layersArray) {
332 l = layersArray.length,
333 fg = this._featureGroup,
334 npg = this._nonPointGroup,
335 originalArray = true;
338 for (i = 0; i < l; i++) {
341 // Group of layers, append children to layersArray and skip.
342 if (m instanceof L.LayerGroup) {
344 layersArray = layersArray.slice();
345 originalArray = false;
347 this._extractNonGroupLayers(m, layersArray);
348 l = layersArray.length;
352 this._arraySplice(this._needsClustering, m);
354 if (this.hasLayer(m)) {
355 this._needsRemoving.push({ layer: m, latlng: m._latlng });
357 this.fire('layerremove', { layer: m });
362 if (this._unspiderfy) {
365 // Work on a copy of the array, so that next loop is not affected.
366 var layersArray2 = layersArray.slice(),
368 for (i = 0; i < l2; i++) {
371 // Group of layers, append children to layersArray and skip.
372 if (m instanceof L.LayerGroup) {
373 this._extractNonGroupLayers(m, layersArray2);
374 l2 = layersArray2.length;
378 this._unspiderfyLayer(m);
382 for (i = 0; i < l; i++) {
385 // Group of layers, append children to layersArray and skip.
386 if (m instanceof L.LayerGroup) {
388 layersArray = layersArray.slice();
389 originalArray = false;
391 this._extractNonGroupLayers(m, layersArray);
392 l = layersArray.length;
398 this.fire('layerremove', { layer: m });
402 this._removeLayer(m, true, true);
403 this.fire('layerremove', { layer: m });
405 if (fg.hasLayer(m)) {
413 // Refresh bounds and weighted positions.
414 this._topClusterLevel._recalculateBounds();
416 this._refreshClustersIcons();
418 //Fix up the clusters and markers on the map
419 this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
424 //Removes all layers from the MarkerClusterGroup
425 clearLayers: function () {
426 //Need our own special implementation as the LayerGroup one doesn't work for us
428 //If we aren't on the map (yet), blow away the markers we know of
430 this._needsClustering = [];
431 this._needsRemoving = [];
432 delete this._gridClusters;
433 delete this._gridUnclustered;
436 if (this._noanimationUnspiderfy) {
437 this._noanimationUnspiderfy();
440 //Remove all the visible layers
441 this._featureGroup.clearLayers();
442 this._nonPointGroup.clearLayers();
444 this.eachLayer(function (marker) {
445 marker.off(this._childMarkerEventHandlers, this);
446 delete marker.__parent;
450 //Reset _topClusterLevel and the DistanceGrids
451 this._generateInitialClusters();
457 //Override FeatureGroup.getBounds as it doesn't work
458 getBounds: function () {
459 var bounds = new L.LatLngBounds();
461 if (this._topClusterLevel) {
462 bounds.extend(this._topClusterLevel._bounds);
465 for (var i = this._needsClustering.length - 1; i >= 0; i--) {
466 bounds.extend(this._needsClustering[i].getLatLng());
469 bounds.extend(this._nonPointGroup.getBounds());
474 //Overrides LayerGroup.eachLayer
475 eachLayer: function (method, context) {
476 var markers = this._needsClustering.slice(),
477 needsRemoving = this._needsRemoving,
478 thisNeedsRemoving, i, j;
480 if (this._topClusterLevel) {
481 this._topClusterLevel.getAllChildMarkers(markers);
484 for (i = markers.length - 1; i >= 0; i--) {
485 thisNeedsRemoving = true;
487 for (j = needsRemoving.length - 1; j >= 0; j--) {
488 if (needsRemoving[j].layer === markers[i]) {
489 thisNeedsRemoving = false;
494 if (thisNeedsRemoving) {
495 method.call(context, markers[i]);
499 this._nonPointGroup.eachLayer(method, context);
502 //Overrides LayerGroup.getLayers
503 getLayers: function () {
505 this.eachLayer(function (l) {
511 //Overrides LayerGroup.getLayer, WARNING: Really bad performance
512 getLayer: function (id) {
515 id = parseInt(id, 10);
517 this.eachLayer(function (l) {
518 if (L.stamp(l) === id) {
526 //Returns true if the given layer is in this MarkerClusterGroup
527 hasLayer: function (layer) {
532 var i, anArray = this._needsClustering;
534 for (i = anArray.length - 1; i >= 0; i--) {
535 if (anArray[i] === layer) {
540 anArray = this._needsRemoving;
541 for (i = anArray.length - 1; i >= 0; i--) {
542 if (anArray[i].layer === layer) {
547 return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer);
550 //Zoom down to show the given layer (spiderfying if necessary) then calls the callback
551 zoomToShowLayer: function (layer, callback) {
553 if (typeof callback !== 'function') {
554 callback = function () {};
557 var showMarker = function () {
558 if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) {
559 this._map.off('moveend', showMarker, this);
560 this.off('animationend', showMarker, this);
564 } else if (layer.__parent._icon) {
565 this.once('spiderfied', callback, this);
566 layer.__parent.spiderfy();
571 if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) {
572 //Layer is visible ond on screen, immediate return
574 } else if (layer.__parent._zoom < Math.round(this._map._zoom)) {
575 //Layer should be visible at this zoom level. It must not be on screen so just pan over to it
576 this._map.on('moveend', showMarker, this);
577 this._map.panTo(layer.getLatLng());
579 this._map.on('moveend', showMarker, this);
580 this.on('animationend', showMarker, this);
581 layer.__parent.zoomToBounds();
585 //Overrides FeatureGroup.onAdd
586 onAdd: function (map) {
590 if (!isFinite(this._map.getMaxZoom())) {
591 throw "Map has no maxZoom specified";
594 this._featureGroup.addTo(map);
595 this._nonPointGroup.addTo(map);
597 if (!this._gridClusters) {
598 this._generateInitialClusters();
601 this._maxLat = map.options.crs.projection.MAX_LATITUDE;
603 //Restore all the positions as they are in the MCG before removing them
604 for (i = 0, l = this._needsRemoving.length; i < l; i++) {
605 layer = this._needsRemoving[i];
606 layer.newlatlng = layer.layer._latlng;
607 layer.layer._latlng = layer.latlng;
609 //Remove them, then restore their new positions
610 for (i = 0, l = this._needsRemoving.length; i < l; i++) {
611 layer = this._needsRemoving[i];
612 this._removeLayer(layer.layer, true);
613 layer.layer._latlng = layer.newlatlng;
615 this._needsRemoving = [];
617 //Remember the current zoom level and bounds
618 this._zoom = Math.round(this._map._zoom);
619 this._currentShownBounds = this._getExpandedVisibleBounds();
621 this._map.on('zoomend', this._zoomEnd, this);
622 this._map.on('moveend', this._moveEnd, this);
624 if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
625 this._spiderfierOnAdd();
630 //Actually add our markers to the map:
631 l = this._needsClustering;
632 this._needsClustering = [];
633 this.addLayers(l, true);
636 //Overrides FeatureGroup.onRemove
637 onRemove: function (map) {
638 map.off('zoomend', this._zoomEnd, this);
639 map.off('moveend', this._moveEnd, this);
641 this._unbindEvents();
643 //In case we are in a cluster animation
644 this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
646 if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
647 this._spiderfierOnRemove();
652 //Clean up all the layers we added to the map
653 this._hideCoverage();
654 this._featureGroup.remove();
655 this._nonPointGroup.remove();
657 this._featureGroup.clearLayers();
662 getVisibleParent: function (marker) {
663 var vMarker = marker;
664 while (vMarker && !vMarker._icon) {
665 vMarker = vMarker.__parent;
667 return vMarker || null;
670 //Remove the given object from the given array
671 _arraySplice: function (anArray, obj) {
672 for (var i = anArray.length - 1; i >= 0; i--) {
673 if (anArray[i] === obj) {
674 anArray.splice(i, 1);
681 * Removes a marker from all _gridUnclustered zoom levels, starting at the supplied zoom.
682 * @param marker to be removed from _gridUnclustered.
683 * @param z integer bottom start zoom level (included)
686 _removeFromGridUnclustered: function (marker, z) {
688 gridUnclustered = this._gridUnclustered,
689 minZoom = Math.floor(this._map.getMinZoom());
691 for (; z >= minZoom; z--) {
692 if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) {
698 _childMarkerDragStart: function (e) {
699 e.target.__dragStart = e.target._latlng;
702 _childMarkerMoved: function (e) {
703 if (!this._ignoreMove && !e.target.__dragStart) {
704 var isPopupOpen = e.target._popup && e.target._popup.isOpen();
706 this._moveChild(e.target, e.oldLatLng, e.latlng);
709 e.target.openPopup();
714 _moveChild: function (layer, from, to) {
715 layer._latlng = from;
716 this.removeLayer(layer);
719 this.addLayer(layer);
722 _childMarkerDragEnd: function (e) {
723 var dragStart = e.target.__dragStart;
724 delete e.target.__dragStart;
726 this._moveChild(e.target, dragStart, e.target._latlng);
731 //Internal function for removing a marker from everything.
732 //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
733 _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) {
734 var gridClusters = this._gridClusters,
735 gridUnclustered = this._gridUnclustered,
736 fg = this._featureGroup,
738 minZoom = Math.floor(this._map.getMinZoom());
740 //Remove the marker from distance clusters it might be in
741 if (removeFromDistanceGrid) {
742 this._removeFromGridUnclustered(marker, this._maxZoom);
745 //Work our way up the clusters removing them as we go if required
746 var cluster = marker.__parent,
747 markers = cluster._markers,
750 //Remove the marker from the immediate parents marker list
751 this._arraySplice(markers, marker);
754 cluster._childCount--;
755 cluster._boundsNeedUpdate = true;
757 if (cluster._zoom < minZoom) {
758 //Top level, do nothing
760 } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required
761 //We need to push the other marker up to the parent
762 otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0];
764 //Update distance grid
765 gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom));
766 gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom));
768 //Move otherMarker up to parent
769 this._arraySplice(cluster.__parent._childClusters, cluster);
770 cluster.__parent._markers.push(otherMarker);
771 otherMarker.__parent = cluster.__parent;
774 //Cluster is currently on the map, need to put the marker on the map instead
775 fg.removeLayer(cluster);
776 if (!dontUpdateMap) {
777 fg.addLayer(otherMarker);
781 cluster._iconNeedsUpdate = true;
784 cluster = cluster.__parent;
787 delete marker.__parent;
790 _isOrIsParent: function (el, oel) {
795 oel = oel.parentNode;
800 //Override L.Evented.fire
801 fire: function (type, data, propagate) {
802 if (data && data.layer instanceof L.MarkerCluster) {
803 //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget)
804 if (data.originalEvent && this._isOrIsParent(data.layer._icon, data.originalEvent.relatedTarget)) {
807 type = 'cluster' + type;
810 L.FeatureGroup.prototype.fire.call(this, type, data, propagate);
813 //Override L.Evented.listens
814 listens: function (type, propagate) {
815 return L.FeatureGroup.prototype.listens.call(this, type, propagate) || L.FeatureGroup.prototype.listens.call(this, 'cluster' + type, propagate);
818 //Default functionality
819 _defaultIconCreateFunction: function (cluster) {
820 var childCount = cluster.getChildCount();
822 var c = ' marker-cluster-';
823 if (childCount < 10) {
825 } else if (childCount < 100) {
831 return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
834 _bindEvents: function () {
836 spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
837 showCoverageOnHover = this.options.showCoverageOnHover,
838 zoomToBoundsOnClick = this.options.zoomToBoundsOnClick;
840 //Zoom on cluster click or spiderfy if we are at the lowest level
841 if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
842 this.on('clusterclick', this._zoomOrSpiderfy, this);
845 //Show convex hull (boundary) polygon on mouse over
846 if (showCoverageOnHover) {
847 this.on('clustermouseover', this._showCoverage, this);
848 this.on('clustermouseout', this._hideCoverage, this);
849 map.on('zoomend', this._hideCoverage, this);
853 _zoomOrSpiderfy: function (e) {
854 var cluster = e.layer,
855 bottomCluster = cluster;
857 while (bottomCluster._childClusters.length === 1) {
858 bottomCluster = bottomCluster._childClusters[0];
861 if (bottomCluster._zoom === this._maxZoom &&
862 bottomCluster._childCount === cluster._childCount &&
863 this.options.spiderfyOnMaxZoom) {
865 // All child markers are contained in a single cluster from this._maxZoom to this cluster.
867 } else if (this.options.zoomToBoundsOnClick) {
868 cluster.zoomToBounds();
871 // Focus the map again for keyboard users.
872 if (e.originalEvent && e.originalEvent.keyCode === 13) {
873 this._map._container.focus();
877 _showCoverage: function (e) {
879 if (this._inZoomAnimation) {
882 if (this._shownPolygon) {
883 map.removeLayer(this._shownPolygon);
885 if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) {
886 this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions);
887 map.addLayer(this._shownPolygon);
891 _hideCoverage: function () {
892 if (this._shownPolygon) {
893 this._map.removeLayer(this._shownPolygon);
894 this._shownPolygon = null;
898 _unbindEvents: function () {
899 var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
900 showCoverageOnHover = this.options.showCoverageOnHover,
901 zoomToBoundsOnClick = this.options.zoomToBoundsOnClick,
904 if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
905 this.off('clusterclick', this._zoomOrSpiderfy, this);
907 if (showCoverageOnHover) {
908 this.off('clustermouseover', this._showCoverage, this);
909 this.off('clustermouseout', this._hideCoverage, this);
910 map.off('zoomend', this._hideCoverage, this);
914 _zoomEnd: function () {
915 if (!this._map) { //May have been removed from the map by a zoomEnd handler
918 this._mergeSplitClusters();
920 this._zoom = Math.round(this._map._zoom);
921 this._currentShownBounds = this._getExpandedVisibleBounds();
924 _moveEnd: function () {
925 if (this._inZoomAnimation) {
929 var newBounds = this._getExpandedVisibleBounds();
931 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, newBounds);
932 this._topClusterLevel._recursivelyAddChildrenToMap(null, Math.round(this._map._zoom), newBounds);
934 this._currentShownBounds = newBounds;
938 _generateInitialClusters: function () {
939 var maxZoom = Math.ceil(this._map.getMaxZoom()),
940 minZoom = Math.floor(this._map.getMinZoom()),
941 radius = this.options.maxClusterRadius,
944 //If we just set maxClusterRadius to a single number, we need to create
945 //a simple function to return that number. Otherwise, we just have to
946 //use the function we've passed in.
947 if (typeof radius !== "function") {
948 radiusFn = function () { return radius; };
951 if (this.options.disableClusteringAtZoom !== null) {
952 maxZoom = this.options.disableClusteringAtZoom - 1;
954 this._maxZoom = maxZoom;
955 this._gridClusters = {};
956 this._gridUnclustered = {};
958 //Set up DistanceGrids for each zoom
959 for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
960 this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom));
961 this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom));
964 // Instantiate the appropriate L.MarkerCluster class (animated or not).
965 this._topClusterLevel = new this._markerCluster(this, minZoom - 1);
968 //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
969 _addLayer: function (layer, zoom) {
970 var gridClusters = this._gridClusters,
971 gridUnclustered = this._gridUnclustered,
972 minZoom = Math.floor(this._map.getMinZoom()),
975 if (this.options.singleMarkerMode) {
976 this._overrideMarkerIcon(layer);
979 layer.on(this._childMarkerEventHandlers, this);
981 //Find the lowest zoom level to slot this one in
982 for (; zoom >= minZoom; zoom--) {
983 markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position
985 //Try find a cluster close by
986 var closest = gridClusters[zoom].getNearObject(markerPoint);
988 closest._addChild(layer);
989 layer.__parent = closest;
993 //Try find a marker close by to form a new cluster with
994 closest = gridUnclustered[zoom].getNearObject(markerPoint);
996 var parent = closest.__parent;
998 this._removeLayer(closest, false);
1001 //Create new cluster with these 2 in it
1003 var newCluster = new this._markerCluster(this, zoom, closest, layer);
1004 gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom));
1005 closest.__parent = newCluster;
1006 layer.__parent = newCluster;
1008 //First create any new intermediate parent clusters that don't exist
1009 var lastParent = newCluster;
1010 for (z = zoom - 1; z > parent._zoom; z--) {
1011 lastParent = new this._markerCluster(this, z, lastParent);
1012 gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z));
1014 parent._addChild(lastParent);
1016 //Remove closest from this zoom level and any above that it is in, replace with newCluster
1017 this._removeFromGridUnclustered(closest, zoom);
1022 //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
1023 gridUnclustered[zoom].addObject(layer, markerPoint);
1026 //Didn't get in anything, add us to the top
1027 this._topClusterLevel._addChild(layer);
1028 layer.__parent = this._topClusterLevel;
1033 * Refreshes the icon of all "dirty" visible clusters.
1034 * Non-visible "dirty" clusters will be updated when they are added to the map.
1037 _refreshClustersIcons: function () {
1038 this._featureGroup.eachLayer(function (c) {
1039 if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) {
1045 //Enqueue code to fire after the marker expand/contract has happened
1046 _enqueue: function (fn) {
1047 this._queue.push(fn);
1048 if (!this._queueTimeout) {
1049 this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300);
1052 _processQueue: function () {
1053 for (var i = 0; i < this._queue.length; i++) {
1054 this._queue[i].call(this);
1056 this._queue.length = 0;
1057 clearTimeout(this._queueTimeout);
1058 this._queueTimeout = null;
1061 //Merge and split any existing clusters that are too big or small
1062 _mergeSplitClusters: function () {
1063 var mapZoom = Math.round(this._map._zoom);
1065 //In case we are starting to split before the animation finished
1066 this._processQueue();
1068 if (this._zoom < mapZoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split
1069 this._animationStart();
1070 //Remove clusters now off screen
1071 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, this._getExpandedVisibleBounds());
1073 this._animationZoomIn(this._zoom, mapZoom);
1075 } else if (this._zoom > mapZoom) { //Zoom out, merge
1076 this._animationStart();
1078 this._animationZoomOut(this._zoom, mapZoom);
1084 //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)
1085 _getExpandedVisibleBounds: function () {
1086 if (!this.options.removeOutsideVisibleBounds) {
1087 return this._mapBoundsInfinite;
1088 } else if (L.Browser.mobile) {
1089 return this._checkBoundsMaxLat(this._map.getBounds());
1092 return this._checkBoundsMaxLat(this._map.getBounds().pad(1)); // Padding expands the bounds by its own dimensions but scaled with the given factor.
1096 * Expands the latitude to Infinity (or -Infinity) if the input bounds reach the map projection maximum defined latitude
1097 * (in the case of Web/Spherical Mercator, it is 85.0511287798 / see https://en.wikipedia.org/wiki/Web_Mercator#Formulas).
1098 * Otherwise, the removeOutsideVisibleBounds option will remove markers beyond that limit, whereas the same markers without
1099 * this option (or outside MCG) will have their position floored (ceiled) by the projection and rendered at that limit,
1100 * making the user think that MCG "eats" them and never displays them again.
1101 * @param bounds L.LatLngBounds
1102 * @returns {L.LatLngBounds}
1105 _checkBoundsMaxLat: function (bounds) {
1106 var maxLat = this._maxLat;
1108 if (maxLat !== undefined) {
1109 if (bounds.getNorth() >= maxLat) {
1110 bounds._northEast.lat = Infinity;
1112 if (bounds.getSouth() <= -maxLat) {
1113 bounds._southWest.lat = -Infinity;
1120 //Shared animation code
1121 _animationAddLayerNonAnimated: function (layer, newCluster) {
1122 if (newCluster === layer) {
1123 this._featureGroup.addLayer(layer);
1124 } else if (newCluster._childCount === 2) {
1125 newCluster._addToMap();
1127 var markers = newCluster.getAllChildMarkers();
1128 this._featureGroup.removeLayer(markers[0]);
1129 this._featureGroup.removeLayer(markers[1]);
1131 newCluster._updateIcon();
1136 * Extracts individual (i.e. non-group) layers from a Layer Group.
1137 * @param group to extract layers from.
1138 * @param output {Array} in which to store the extracted layers.
1139 * @returns {*|Array}
1142 _extractNonGroupLayers: function (group, output) {
1143 var layers = group.getLayers(),
1147 output = output || [];
1149 for (; i < layers.length; i++) {
1152 if (layer instanceof L.LayerGroup) {
1153 this._extractNonGroupLayers(layer, output);
1164 * Implements the singleMarkerMode option.
1165 * @param layer Marker to re-style using the Clusters iconCreateFunction.
1166 * @returns {L.Icon} The newly created icon.
1169 _overrideMarkerIcon: function (layer) {
1170 var icon = layer.options.icon = this.options.iconCreateFunction({
1171 getChildCount: function () {
1174 getAllChildMarkers: function () {
1183 // Constant bounds used in case option "removeOutsideVisibleBounds" is set to false.
1184 L.MarkerClusterGroup.include({
1185 _mapBoundsInfinite: new L.LatLngBounds(new L.LatLng(-Infinity, -Infinity), new L.LatLng(Infinity, Infinity))
1188 L.MarkerClusterGroup.include({
1190 //Non Animated versions of everything
1191 _animationStart: function () {
1194 _animationZoomIn: function (previousZoomLevel, newZoomLevel) {
1195 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel);
1196 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
1198 //We didn't actually animate, but we use this event to mean "clustering animations have finished"
1199 this.fire('animationend');
1201 _animationZoomOut: function (previousZoomLevel, newZoomLevel) {
1202 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel);
1203 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
1205 //We didn't actually animate, but we use this event to mean "clustering animations have finished"
1206 this.fire('animationend');
1208 _animationAddLayer: function (layer, newCluster) {
1209 this._animationAddLayerNonAnimated(layer, newCluster);
1214 //Animated versions here
1215 _animationStart: function () {
1216 this._map._mapPane.className += ' leaflet-cluster-anim';
1217 this._inZoomAnimation++;
1220 _animationZoomIn: function (previousZoomLevel, newZoomLevel) {
1221 var bounds = this._getExpandedVisibleBounds(),
1222 fg = this._featureGroup,
1223 minZoom = Math.floor(this._map.getMinZoom()),
1226 this._ignoreMove = true;
1228 //Add all children of current clusters to map and remove those clusters from map
1229 this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) {
1230 var startPos = c._latlng,
1231 markers = c._markers,
1234 if (!bounds.contains(startPos)) {
1238 if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us
1240 c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);
1242 //Fade out old cluster
1244 c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds);
1247 //Remove all markers that aren't visible any more
1248 //TODO: Do we actually need to do this on the higher levels too?
1249 for (i = markers.length - 1; i >= 0; i--) {
1251 if (!bounds.contains(m._latlng)) {
1258 this._forceLayout();
1261 this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel);
1262 //TODO Maybe? Update markers in _recursivelyBecomeVisible
1263 fg.eachLayer(function (n) {
1264 if (!(n instanceof L.MarkerCluster) && n._icon) {
1269 //update the positions of the just added clusters/markers
1270 this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {
1271 c._recursivelyRestoreChildPositions(newZoomLevel);
1274 this._ignoreMove = false;
1276 //Remove the old clusters and close the zoom animation
1277 this._enqueue(function () {
1278 //update the positions of the just added clusters/markers
1279 this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) {
1284 this._animationEnd();
1288 _animationZoomOut: function (previousZoomLevel, newZoomLevel) {
1289 this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel);
1291 //Need to add markers for those that weren't on the map before but are now
1292 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
1293 //Remove markers that were on the map before but won't be now
1294 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel, this._getExpandedVisibleBounds());
1297 _animationAddLayer: function (layer, newCluster) {
1299 fg = this._featureGroup;
1302 if (newCluster !== layer) {
1303 if (newCluster._childCount > 2) { //Was already a cluster
1305 newCluster._updateIcon();
1306 this._forceLayout();
1307 this._animationStart();
1309 layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));
1310 layer.clusterHide();
1312 this._enqueue(function () {
1313 fg.removeLayer(layer);
1314 layer.clusterShow();
1319 } else { //Just became a cluster
1320 this._forceLayout();
1322 me._animationStart();
1323 me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._zoom);
1329 // Private methods for animated versions.
1330 _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) {
1331 var bounds = this._getExpandedVisibleBounds(),
1332 minZoom = Math.floor(this._map.getMinZoom());
1334 //Animate all of the markers in the clusters to move to their cluster center point
1335 cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, minZoom, previousZoomLevel + 1, newZoomLevel);
1339 //Update the opacity (If we immediately set it they won't animate)
1340 this._forceLayout();
1341 cluster._recursivelyBecomeVisible(bounds, newZoomLevel);
1343 //TODO: Maybe use the transition timing stuff to make this more reliable
1344 //When the animations are done, tidy up
1345 this._enqueue(function () {
1347 //This cluster stopped being a cluster before the timeout fired
1348 if (cluster._childCount === 1) {
1349 var m = cluster._markers[0];
1350 //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
1351 this._ignoreMove = true;
1352 m.setLatLng(m.getLatLng());
1353 this._ignoreMove = false;
1354 if (m.clusterShow) {
1358 cluster._recursively(bounds, newZoomLevel, minZoom, function (c) {
1359 c._recursivelyRemoveChildrenFromMap(bounds, minZoom, previousZoomLevel + 1);
1366 _animationEnd: function () {
1368 this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
1370 this._inZoomAnimation--;
1371 this.fire('animationend');
1374 //Force a browser layout of stuff in the map
1375 // Should apply the current opacity and location to all elements so we can update them again for an animation
1376 _forceLayout: function () {
1377 //In my testing this works, infact offsetWidth of any element seems to work.
1378 //Could loop all this._layers and do this for each _icon if it stops working
1380 L.Util.falseFn(document.body.offsetWidth);
1384 L.markerClusterGroup = function (options) {
1385 return new L.MarkerClusterGroup(options);
1388 var MarkerCluster = L.MarkerCluster = L.Marker.extend({
1389 options: L.Icon.prototype.options,
1391 initialize: function (group, zoom, a, b) {
1393 L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0),
1394 { icon: this, pane: group.options.clusterPane });
1396 this._group = group;
1400 this._childClusters = [];
1401 this._childCount = 0;
1402 this._iconNeedsUpdate = true;
1403 this._boundsNeedUpdate = true;
1405 this._bounds = new L.LatLngBounds();
1415 //Recursively retrieve all child markers of this cluster
1416 getAllChildMarkers: function (storageArray, ignoreDraggedMarker) {
1417 storageArray = storageArray || [];
1419 for (var i = this._childClusters.length - 1; i >= 0; i--) {
1420 this._childClusters[i].getAllChildMarkers(storageArray);
1423 for (var j = this._markers.length - 1; j >= 0; j--) {
1424 if (ignoreDraggedMarker && this._markers[j].__dragStart) {
1427 storageArray.push(this._markers[j]);
1430 return storageArray;
1433 //Returns the count of how many child markers we have
1434 getChildCount: function () {
1435 return this._childCount;
1438 //Zoom to the minimum of showing all of the child markers, or the extents of this cluster
1439 zoomToBounds: function (fitBoundsOptions) {
1440 var childClusters = this._childClusters.slice(),
1441 map = this._group._map,
1442 boundsZoom = map.getBoundsZoom(this._bounds),
1443 zoom = this._zoom + 1,
1444 mapZoom = map.getZoom(),
1447 //calculate how far we need to zoom down to see all of the markers
1448 while (childClusters.length > 0 && boundsZoom > zoom) {
1450 var newClusters = [];
1451 for (i = 0; i < childClusters.length; i++) {
1452 newClusters = newClusters.concat(childClusters[i]._childClusters);
1454 childClusters = newClusters;
1457 if (boundsZoom > zoom) {
1458 this._group._map.setView(this._latlng, zoom);
1459 } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead
1460 this._group._map.setView(this._latlng, mapZoom + 1);
1462 this._group._map.fitBounds(this._bounds, fitBoundsOptions);
1466 getBounds: function () {
1467 var bounds = new L.LatLngBounds();
1468 bounds.extend(this._bounds);
1472 _updateIcon: function () {
1473 this._iconNeedsUpdate = true;
1479 //Cludge for Icon, we pretend to be an icon for performance
1480 createIcon: function () {
1481 if (this._iconNeedsUpdate) {
1482 this._iconObj = this._group.options.iconCreateFunction(this);
1483 this._iconNeedsUpdate = false;
1485 return this._iconObj.createIcon();
1487 createShadow: function () {
1488 return this._iconObj.createShadow();
1492 _addChild: function (new1, isNotificationFromChild) {
1494 this._iconNeedsUpdate = true;
1496 this._boundsNeedUpdate = true;
1497 this._setClusterCenter(new1);
1499 if (new1 instanceof L.MarkerCluster) {
1500 if (!isNotificationFromChild) {
1501 this._childClusters.push(new1);
1502 new1.__parent = this;
1504 this._childCount += new1._childCount;
1506 if (!isNotificationFromChild) {
1507 this._markers.push(new1);
1512 if (this.__parent) {
1513 this.__parent._addChild(new1, true);
1518 * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position.
1519 * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet.
1522 _setClusterCenter: function (child) {
1523 if (!this._cLatLng) {
1524 // when clustering, take position of the first point as the cluster center
1525 this._cLatLng = child._cLatLng || child._latlng;
1530 * Assigns impossible bounding values so that the next extend entirely determines the new bounds.
1531 * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class.
1532 * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended.
1535 _resetBounds: function () {
1536 var bounds = this._bounds;
1538 if (bounds._southWest) {
1539 bounds._southWest.lat = Infinity;
1540 bounds._southWest.lng = Infinity;
1542 if (bounds._northEast) {
1543 bounds._northEast.lat = -Infinity;
1544 bounds._northEast.lng = -Infinity;
1548 _recalculateBounds: function () {
1549 var markers = this._markers,
1550 childClusters = this._childClusters,
1553 totalCount = this._childCount,
1554 i, child, childLatLng, childCount;
1556 // Case where all markers are removed from the map and we are left with just an empty _topClusterLevel.
1557 if (totalCount === 0) {
1561 // Reset rather than creating a new object, for performance.
1562 this._resetBounds();
1565 for (i = 0; i < markers.length; i++) {
1566 childLatLng = markers[i]._latlng;
1568 this._bounds.extend(childLatLng);
1570 latSum += childLatLng.lat;
1571 lngSum += childLatLng.lng;
1575 for (i = 0; i < childClusters.length; i++) {
1576 child = childClusters[i];
1578 // Re-compute child bounds and weighted position first if necessary.
1579 if (child._boundsNeedUpdate) {
1580 child._recalculateBounds();
1583 this._bounds.extend(child._bounds);
1585 childLatLng = child._wLatLng;
1586 childCount = child._childCount;
1588 latSum += childLatLng.lat * childCount;
1589 lngSum += childLatLng.lng * childCount;
1592 this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount);
1594 // Reset dirty flag.
1595 this._boundsNeedUpdate = false;
1598 //Set our markers position as given and add it to the map
1599 _addToMap: function (startPos) {
1601 this._backupLatlng = this._latlng;
1602 this.setLatLng(startPos);
1604 this._group._featureGroup.addLayer(this);
1607 _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {
1608 this._recursively(bounds, this._group._map.getMinZoom(), maxZoom - 1,
1610 var markers = c._markers,
1612 for (i = markers.length - 1; i >= 0; i--) {
1615 //Only do it if the icon is still on the map
1623 var childClusters = c._childClusters,
1625 for (j = childClusters.length - 1; j >= 0; j--) {
1626 cm = childClusters[j];
1636 _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, mapMinZoom, previousZoomLevel, newZoomLevel) {
1637 this._recursively(bounds, newZoomLevel, mapMinZoom,
1639 c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);
1641 //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
1642 //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
1643 if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) {
1645 c._recursivelyRemoveChildrenFromMap(bounds, mapMinZoom, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
1655 _recursivelyBecomeVisible: function (bounds, zoomLevel) {
1656 this._recursively(bounds, this._group._map.getMinZoom(), zoomLevel, null, function (c) {
1661 _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {
1662 this._recursively(bounds, this._group._map.getMinZoom() - 1, zoomLevel,
1664 if (zoomLevel === c._zoom) {
1668 //Add our child markers at startPos (so they can be animated out)
1669 for (var i = c._markers.length - 1; i >= 0; i--) {
1670 var nm = c._markers[i];
1672 if (!bounds.contains(nm._latlng)) {
1677 nm._backupLatlng = nm.getLatLng();
1679 nm.setLatLng(startPos);
1680 if (nm.clusterHide) {
1685 c._group._featureGroup.addLayer(nm);
1689 c._addToMap(startPos);
1694 _recursivelyRestoreChildPositions: function (zoomLevel) {
1695 //Fix positions of child markers
1696 for (var i = this._markers.length - 1; i >= 0; i--) {
1697 var nm = this._markers[i];
1698 if (nm._backupLatlng) {
1699 nm.setLatLng(nm._backupLatlng);
1700 delete nm._backupLatlng;
1704 if (zoomLevel - 1 === this._zoom) {
1705 //Reposition child clusters
1706 for (var j = this._childClusters.length - 1; j >= 0; j--) {
1707 this._childClusters[j]._restorePosition();
1710 for (var k = this._childClusters.length - 1; k >= 0; k--) {
1711 this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);
1716 _restorePosition: function () {
1717 if (this._backupLatlng) {
1718 this.setLatLng(this._backupLatlng);
1719 delete this._backupLatlng;
1723 //exceptBounds: If set, don't remove any markers/clusters in it
1724 _recursivelyRemoveChildrenFromMap: function (previousBounds, mapMinZoom, zoomLevel, exceptBounds) {
1726 this._recursively(previousBounds, mapMinZoom - 1, zoomLevel - 1,
1728 //Remove markers at every level
1729 for (i = c._markers.length - 1; i >= 0; i--) {
1731 if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1732 c._group._featureGroup.removeLayer(m);
1733 if (m.clusterShow) {
1740 //Remove child clusters at just the bottom level
1741 for (i = c._childClusters.length - 1; i >= 0; i--) {
1742 m = c._childClusters[i];
1743 if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1744 c._group._featureGroup.removeLayer(m);
1745 if (m.clusterShow) {
1754 //Run the given functions recursively to this and child clusters
1755 // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
1756 // zoomLevelToStart: zoom level to start running functions (inclusive)
1757 // zoomLevelToStop: zoom level to stop running functions (inclusive)
1758 // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
1759 // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
1760 _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) {
1761 var childClusters = this._childClusters,
1765 if (zoomLevelToStart <= zoom) {
1766 if (runAtEveryLevel) {
1767 runAtEveryLevel(this);
1769 if (runAtBottomLevel && zoom === zoomLevelToStop) {
1770 runAtBottomLevel(this);
1774 if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) {
1775 for (i = childClusters.length - 1; i >= 0; i--) {
1776 c = childClusters[i];
1777 if (c._boundsNeedUpdate) {
1778 c._recalculateBounds();
1780 if (boundsToApplyTo.intersects(c._bounds)) {
1781 c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
1787 //Returns true if we are the parent of only one cluster and that cluster is the same as us
1788 _isSingleParent: function () {
1789 //Don't need to check this._markers as the rest won't work if there are any
1790 return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount;
1795 * Extends L.Marker to include two extra methods: clusterHide and clusterShow.
1797 * They work as setOpacity(0) and setOpacity(1) respectively, but
1798 * don't overwrite the options.opacity
1803 clusterHide: function () {
1804 var backup = this.options.opacity;
1806 this.options.opacity = backup;
1810 clusterShow: function () {
1811 return this.setOpacity(this.options.opacity);
1815 L.DistanceGrid = function (cellSize) {
1816 this._cellSize = cellSize;
1817 this._sqCellSize = cellSize * cellSize;
1819 this._objectPoint = { };
1822 L.DistanceGrid.prototype = {
1824 addObject: function (obj, point) {
1825 var x = this._getCoord(point.x),
1826 y = this._getCoord(point.y),
1828 row = grid[y] = grid[y] || {},
1829 cell = row[x] = row[x] || [],
1830 stamp = L.Util.stamp(obj);
1832 this._objectPoint[stamp] = point;
1837 updateObject: function (obj, point) {
1838 this.removeObject(obj);
1839 this.addObject(obj, point);
1842 //Returns true if the object was found
1843 removeObject: function (obj, point) {
1844 var x = this._getCoord(point.x),
1845 y = this._getCoord(point.y),
1847 row = grid[y] = grid[y] || {},
1848 cell = row[x] = row[x] || [],
1851 delete this._objectPoint[L.Util.stamp(obj)];
1853 for (i = 0, len = cell.length; i < len; i++) {
1854 if (cell[i] === obj) {
1868 eachObject: function (fn, context) {
1869 var i, j, k, len, row, cell, removed,
1878 for (k = 0, len = cell.length; k < len; k++) {
1879 removed = fn.call(context, cell[k]);
1889 getNearObject: function (point) {
1890 var x = this._getCoord(point.x),
1891 y = this._getCoord(point.y),
1892 i, j, k, row, cell, len, obj, dist,
1893 objectPoint = this._objectPoint,
1894 closestDistSq = this._sqCellSize,
1897 for (i = y - 1; i <= y + 1; i++) {
1898 row = this._grid[i];
1901 for (j = x - 1; j <= x + 1; j++) {
1905 for (k = 0, len = cell.length; k < len; k++) {
1907 dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point);
1908 if (dist < closestDistSq ||
1909 dist <= closestDistSq && closest === null) {
1910 closestDistSq = dist;
1921 _getCoord: function (x) {
1922 var coord = Math.floor(x / this._cellSize);
1923 return isFinite(coord) ? coord : x;
1926 _sqDist: function (p, p2) {
1927 var dx = p2.x - p.x,
1929 return dx * dx + dy * dy;
1933 /* Copyright (c) 2012 the authors listed at the following URL, and/or
1934 the authors of referenced articles or incorporated external code:
1935 http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256
1937 Permission is hereby granted, free of charge, to any person obtaining
1938 a copy of this software and associated documentation files (the
1939 "Software"), to deal in the Software without restriction, including
1940 without limitation the rights to use, copy, modify, merge, publish,
1941 distribute, sublicense, and/or sell copies of the Software, and to
1942 permit persons to whom the Software is furnished to do so, subject to
1943 the following conditions:
1945 The above copyright notice and this permission notice shall be
1946 included in all copies or substantial portions of the Software.
1948 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1949 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1950 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1951 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1952 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1953 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1954 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1956 Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
1963 * @param {Object} cpt a point to be measured from the baseline
1964 * @param {Array} bl the baseline, as represented by a two-element
1965 * array of latlng objects.
1966 * @returns {Number} an approximate distance measure
1968 getDistant: function (cpt, bl) {
1969 var vY = bl[1].lat - bl[0].lat,
1970 vX = bl[0].lng - bl[1].lng;
1971 return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng));
1975 * @param {Array} baseLine a two-element array of latlng objects
1976 * representing the baseline to project from
1977 * @param {Array} latLngs an array of latlng objects
1978 * @returns {Object} the maximum point and all new points to stay
1979 * in consideration for the hull.
1981 findMostDistantPointFromBaseLine: function (baseLine, latLngs) {
1987 for (i = latLngs.length - 1; i >= 0; i--) {
1989 d = this.getDistant(pt, baseLine);
2003 return { maxPoint: maxPt, newPoints: newPoints };
2008 * Given a baseline, compute the convex hull of latLngs as an array
2011 * @param {Array} latLngs
2014 buildConvexHull: function (baseLine, latLngs) {
2015 var convexHullBaseLines = [],
2016 t = this.findMostDistantPointFromBaseLine(baseLine, latLngs);
2018 if (t.maxPoint) { // if there is still a point "outside" the base line
2019 convexHullBaseLines =
2020 convexHullBaseLines.concat(
2021 this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints)
2023 convexHullBaseLines =
2024 convexHullBaseLines.concat(
2025 this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints)
2027 return convexHullBaseLines;
2028 } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull
2029 return [baseLine[0]];
2034 * Given an array of latlngs, compute a convex hull as an array
2037 * @param {Array} latLngs
2040 getConvexHull: function (latLngs) {
2041 // find first baseline
2042 var maxLat = false, minLat = false,
2043 maxLng = false, minLng = false,
2044 maxLatPt = null, minLatPt = null,
2045 maxLngPt = null, minLngPt = null,
2046 maxPt = null, minPt = null,
2049 for (i = latLngs.length - 1; i >= 0; i--) {
2050 var pt = latLngs[i];
2051 if (maxLat === false || pt.lat > maxLat) {
2055 if (minLat === false || pt.lat < minLat) {
2059 if (maxLng === false || pt.lng > maxLng) {
2063 if (minLng === false || pt.lng < minLng) {
2069 if (minLat !== maxLat) {
2077 var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs),
2078 this.buildConvexHull([maxPt, minPt], latLngs));
2084 L.MarkerCluster.include({
2085 getConvexHull: function () {
2086 var childMarkers = this.getAllChildMarkers(),
2090 for (i = childMarkers.length - 1; i >= 0; i--) {
2091 p = childMarkers[i].getLatLng();
2095 return L.QuickHull.getConvexHull(points);
2099 //This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
2100 //Huge thanks to jawj for implementing it first to make my job easy :-)
2102 L.MarkerCluster.include({
2105 _circleFootSeparation: 25, //related to circumference of circle
2106 _circleStartAngle: 0,
2108 _spiralFootSeparation: 28, //related to size of spiral (experiment!)
2109 _spiralLengthStart: 11,
2110 _spiralLengthFactor: 5,
2112 _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards.
2113 // 0 -> always spiral; Infinity -> always circle
2115 spiderfy: function () {
2116 if (this._group._spiderfied === this || this._group._inZoomAnimation) {
2120 var childMarkers = this.getAllChildMarkers(null, true),
2121 group = this._group,
2123 center = map.latLngToLayerPoint(this._latlng),
2126 this._group._unspiderfy();
2127 this._group._spiderfied = this;
2129 //TODO Maybe: childMarkers order by distance to center
2131 if (childMarkers.length >= this._circleSpiralSwitchover) {
2132 positions = this._generatePointsSpiral(childMarkers.length, center);
2134 center.y += 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons.
2135 positions = this._generatePointsCircle(childMarkers.length, center);
2138 this._animationSpiderfy(childMarkers, positions);
2141 unspiderfy: function (zoomDetails) {
2142 /// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param>
2143 if (this._group._inZoomAnimation) {
2146 this._animationUnspiderfy(zoomDetails);
2148 this._group._spiderfied = null;
2151 _generatePointsCircle: function (count, centerPt) {
2152 var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),
2153 legLength = circumference / this._2PI, //radius from circumference
2154 angleStep = this._2PI / count,
2158 legLength = Math.max(legLength, 35); // Minimum distance to get outside the cluster icon.
2162 for (i = 0; i < count; i++) { // Clockwise, like spiral.
2163 angle = this._circleStartAngle + i * angleStep;
2164 res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
2170 _generatePointsSpiral: function (count, centerPt) {
2171 var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier,
2172 legLength = spiderfyDistanceMultiplier * this._spiralLengthStart,
2173 separation = spiderfyDistanceMultiplier * this._spiralFootSeparation,
2174 lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI,
2181 // Higher index, closer position to cluster center.
2182 for (i = count; i >= 0; i--) {
2183 // Skip the first position, so that we are already farther from center and we avoid
2184 // being under the default cluster icon (especially important for Circle Markers).
2186 res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
2188 angle += separation / legLength + i * 0.0005;
2189 legLength += lengthFactor / angle;
2194 _noanimationUnspiderfy: function () {
2195 var group = this._group,
2197 fg = group._featureGroup,
2198 childMarkers = this.getAllChildMarkers(null, true),
2201 group._ignoreMove = true;
2204 for (i = childMarkers.length - 1; i >= 0; i--) {
2205 m = childMarkers[i];
2209 if (m._preSpiderfyLatlng) {
2210 m.setLatLng(m._preSpiderfyLatlng);
2211 delete m._preSpiderfyLatlng;
2213 if (m.setZIndexOffset) {
2214 m.setZIndexOffset(0);
2218 map.removeLayer(m._spiderLeg);
2219 delete m._spiderLeg;
2223 group.fire('unspiderfied', {
2225 markers: childMarkers
2227 group._ignoreMove = false;
2228 group._spiderfied = null;
2232 //Non Animated versions of everything
2233 L.MarkerClusterNonAnimated = L.MarkerCluster.extend({
2234 _animationSpiderfy: function (childMarkers, positions) {
2235 var group = this._group,
2237 fg = group._featureGroup,
2238 legOptions = this._group.options.spiderLegPolylineOptions,
2241 group._ignoreMove = true;
2243 // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
2244 // The reverse order trick no longer improves performance on modern browsers.
2245 for (i = 0; i < childMarkers.length; i++) {
2246 newPos = map.layerPointToLatLng(positions[i]);
2247 m = childMarkers[i];
2249 // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
2250 leg = new L.Polyline([this._latlng, newPos], legOptions);
2254 // Now add the marker.
2255 m._preSpiderfyLatlng = m._latlng;
2256 m.setLatLng(newPos);
2257 if (m.setZIndexOffset) {
2258 m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
2263 this.setOpacity(0.3);
2265 group._ignoreMove = false;
2266 group.fire('spiderfied', {
2268 markers: childMarkers
2272 _animationUnspiderfy: function () {
2273 this._noanimationUnspiderfy();
2277 //Animated versions here
2278 L.MarkerCluster.include({
2280 _animationSpiderfy: function (childMarkers, positions) {
2282 group = this._group,
2284 fg = group._featureGroup,
2285 thisLayerLatLng = this._latlng,
2286 thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng),
2288 legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation.
2289 finalLegOpacity = legOptions.opacity,
2290 i, m, leg, legPath, legLength, newPos;
2292 if (finalLegOpacity === undefined) {
2293 finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity;
2297 // If the initial opacity of the spider leg is not 0 then it appears before the animation starts.
2298 legOptions.opacity = 0;
2300 // Add the class for CSS transitions.
2301 legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg';
2303 // Make sure we have a defined opacity.
2304 legOptions.opacity = finalLegOpacity;
2307 group._ignoreMove = true;
2309 // Add markers and spider legs to map, hidden at our center point.
2310 // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
2311 // The reverse order trick no longer improves performance on modern browsers.
2312 for (i = 0; i < childMarkers.length; i++) {
2313 m = childMarkers[i];
2315 newPos = map.layerPointToLatLng(positions[i]);
2317 // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
2318 leg = new L.Polyline([thisLayerLatLng, newPos], legOptions);
2322 // Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/
2323 // In our case the transition property is declared in the CSS file.
2325 legPath = leg._path;
2326 legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox.
2327 legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated.
2328 legPath.style.strokeDashoffset = legLength;
2331 // If it is a marker, add it now and we'll animate it out
2332 if (m.setZIndexOffset) {
2333 m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING
2335 if (m.clusterHide) {
2339 // Vectors just get immediately added
2343 m._setPos(thisLayerPos);
2347 group._forceLayout();
2348 group._animationStart();
2350 // Reveal markers and spider legs.
2351 for (i = childMarkers.length - 1; i >= 0; i--) {
2352 newPos = map.layerPointToLatLng(positions[i]);
2353 m = childMarkers[i];
2355 //Move marker to new position
2356 m._preSpiderfyLatlng = m._latlng;
2357 m.setLatLng(newPos);
2359 if (m.clusterShow) {
2363 // Animate leg (animation is actually delegated to CSS transition).
2366 legPath = leg._path;
2367 legPath.style.strokeDashoffset = 0;
2368 //legPath.style.strokeOpacity = finalLegOpacity;
2369 leg.setStyle({opacity: finalLegOpacity});
2372 this.setOpacity(0.3);
2374 group._ignoreMove = false;
2376 setTimeout(function () {
2377 group._animationEnd();
2378 group.fire('spiderfied', {
2380 markers: childMarkers
2385 _animationUnspiderfy: function (zoomDetails) {
2387 group = this._group,
2389 fg = group._featureGroup,
2390 thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng),
2391 childMarkers = this.getAllChildMarkers(null, true),
2393 m, i, leg, legPath, legLength, nonAnimatable;
2395 group._ignoreMove = true;
2396 group._animationStart();
2398 //Make us visible and bring the child markers back in
2400 for (i = childMarkers.length - 1; i >= 0; i--) {
2401 m = childMarkers[i];
2403 //Marker was added to us after we were spiderfied
2404 if (!m._preSpiderfyLatlng) {
2408 //Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll
2411 //Fix up the location to the real one
2412 m.setLatLng(m._preSpiderfyLatlng);
2413 delete m._preSpiderfyLatlng;
2415 //Hack override the location to be our center
2416 nonAnimatable = true;
2418 m._setPos(thisLayerPos);
2419 nonAnimatable = false;
2421 if (m.clusterHide) {
2423 nonAnimatable = false;
2425 if (nonAnimatable) {
2429 // Animate the spider leg back in (animation is actually delegated to CSS transition).
2432 legPath = leg._path;
2433 legLength = legPath.getTotalLength() + 0.1;
2434 legPath.style.strokeDashoffset = legLength;
2435 leg.setStyle({opacity: 0});
2439 group._ignoreMove = false;
2441 setTimeout(function () {
2442 //If we have only <= one child left then that marker will be shown on the map so don't remove it!
2443 var stillThereChildCount = 0;
2444 for (i = childMarkers.length - 1; i >= 0; i--) {
2445 m = childMarkers[i];
2447 stillThereChildCount++;
2452 for (i = childMarkers.length - 1; i >= 0; i--) {
2453 m = childMarkers[i];
2455 if (!m._spiderLeg) { //Has already been unspiderfied
2459 if (m.clusterShow) {
2462 if (m.setZIndexOffset) {
2463 m.setZIndexOffset(0);
2466 if (stillThereChildCount > 1) {
2470 map.removeLayer(m._spiderLeg);
2471 delete m._spiderLeg;
2473 group._animationEnd();
2474 group.fire('unspiderfied', {
2476 markers: childMarkers
2483 L.MarkerClusterGroup.include({
2484 //The MarkerCluster currently spiderfied (if any)
2487 unspiderfy: function () {
2488 this._unspiderfy.apply(this, arguments);
2491 _spiderfierOnAdd: function () {
2492 this._map.on('click', this._unspiderfyWrapper, this);
2494 if (this._map.options.zoomAnimation) {
2495 this._map.on('zoomstart', this._unspiderfyZoomStart, this);
2497 //Browsers without zoomAnimation or a big zoom don't fire zoomstart
2498 this._map.on('zoomend', this._noanimationUnspiderfy, this);
2500 if (!L.Browser.touch) {
2501 this._map.getRenderer(this);
2502 //Needs to happen in the pageload, not after, or animations don't work in webkit
2503 // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements
2504 //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable
2508 _spiderfierOnRemove: function () {
2509 this._map.off('click', this._unspiderfyWrapper, this);
2510 this._map.off('zoomstart', this._unspiderfyZoomStart, this);
2511 this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
2512 this._map.off('zoomend', this._noanimationUnspiderfy, this);
2514 //Ensure that markers are back where they should be
2515 // Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane
2516 this._noanimationUnspiderfy();
2519 //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)
2520 //This means we can define the animation they do rather than Markers doing an animation to their actual location
2521 _unspiderfyZoomStart: function () {
2522 if (!this._map) { //May have been removed from the map by a zoomEnd handler
2526 this._map.on('zoomanim', this._unspiderfyZoomAnim, this);
2529 _unspiderfyZoomAnim: function (zoomDetails) {
2530 //Wait until the first zoomanim after the user has finished touch-zooming before running the animation
2531 if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) {
2535 this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
2536 this._unspiderfy(zoomDetails);
2539 _unspiderfyWrapper: function () {
2540 /// <summary>_unspiderfy but passes no arguments</summary>
2544 _unspiderfy: function (zoomDetails) {
2545 if (this._spiderfied) {
2546 this._spiderfied.unspiderfy(zoomDetails);
2550 _noanimationUnspiderfy: function () {
2551 if (this._spiderfied) {
2552 this._spiderfied._noanimationUnspiderfy();
2556 //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc
2557 _unspiderfyLayer: function (layer) {
2558 if (layer._spiderLeg) {
2559 this._featureGroup.removeLayer(layer);
2561 if (layer.clusterShow) {
2562 layer.clusterShow();
2564 //Position will be fixed up immediately in _animationUnspiderfy
2565 if (layer.setZIndexOffset) {
2566 layer.setZIndexOffset(0);
2569 this._map.removeLayer(layer._spiderLeg);
2570 delete layer._spiderLeg;
2576 * Adds 1 public method to MCG and 1 to L.Marker to facilitate changing
2577 * markers' icon options and refreshing their icon and their parent clusters
2578 * accordingly (case where their iconCreateFunction uses data of childMarkers
2579 * to make up the cluster icon).
2583 L.MarkerClusterGroup.include({
2585 * Updates the icon of all clusters which are parents of the given marker(s).
2586 * In singleMarkerMode, also updates the given marker(s) icon.
2587 * @param layers L.MarkerClusterGroup|L.LayerGroup|Array(L.Marker)|Map(L.Marker)|
2588 * L.MarkerCluster|L.Marker (optional) list of markers (or single marker) whose parent
2589 * clusters need to be updated. If not provided, retrieves all child markers of this.
2590 * @returns {L.MarkerClusterGroup}
2592 refreshClusters: function (layers) {
2594 layers = this._topClusterLevel.getAllChildMarkers();
2595 } else if (layers instanceof L.MarkerClusterGroup) {
2596 layers = layers._topClusterLevel.getAllChildMarkers();
2597 } else if (layers instanceof L.LayerGroup) {
2598 layers = layers._layers;
2599 } else if (layers instanceof L.MarkerCluster) {
2600 layers = layers.getAllChildMarkers();
2601 } else if (layers instanceof L.Marker) {
2603 } // else: must be an Array(L.Marker)|Map(L.Marker)
2604 this._flagParentsIconsNeedUpdate(layers);
2605 this._refreshClustersIcons();
2607 // In case of singleMarkerMode, also re-draw the markers.
2608 if (this.options.singleMarkerMode) {
2609 this._refreshSingleMarkerModeMarkers(layers);
2616 * Simply flags all parent clusters of the given markers as having a "dirty" icon.
2617 * @param layers Array(L.Marker)|Map(L.Marker) list of markers.
2620 _flagParentsIconsNeedUpdate: function (layers) {
2623 // Assumes layers is an Array or an Object whose prototype is non-enumerable.
2624 for (id in layers) {
2625 // Flag parent clusters' icon as "dirty", all the way up.
2626 // Dumb process that flags multiple times upper parents, but still
2627 // much more efficient than trying to be smart and make short lists,
2628 // at least in the case of a hierarchy following a power law:
2629 // http://jsperf.com/flag-nodes-in-power-hierarchy/2
2630 parent = layers[id].__parent;
2632 parent._iconNeedsUpdate = true;
2633 parent = parent.__parent;
2639 * Re-draws the icon of the supplied markers.
2640 * To be used in singleMarkerMode only.
2641 * @param layers Array(L.Marker)|Map(L.Marker) list of markers.
2644 _refreshSingleMarkerModeMarkers: function (layers) {
2647 for (id in layers) {
2650 // Make sure we do not override markers that do not belong to THIS group.
2651 if (this.hasLayer(layer)) {
2652 // Need to re-create the icon first, then re-draw the marker.
2653 layer.setIcon(this._overrideMarkerIcon(layer));
2661 * Updates the given options in the marker's icon and refreshes the marker.
2662 * @param options map object of icon options.
2663 * @param directlyRefreshClusters boolean (optional) true to trigger
2664 * MCG.refreshClustersOf() right away with this single marker.
2665 * @returns {L.Marker}
2667 refreshIconOptions: function (options, directlyRefreshClusters) {
2668 var icon = this.options.icon;
2670 L.setOptions(icon, options);
2674 // Shortcut to refresh the associated MCG clusters right away.
2675 // To be used when refreshing a single marker.
2676 // Otherwise, better use MCG.refreshClusters() once at the end with
2677 // the list of modified markers.
2678 if (directlyRefreshClusters && this.__parent) {
2679 this.__parent._group.refreshClusters(this);
2686 exports.MarkerClusterGroup = MarkerClusterGroup;
2687 exports.MarkerCluster = MarkerCluster;
2690 //# sourceMappingURL=leaflet.markercluster-src.js.map