2 Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.
3 https://github.com/Leaflet/Leaflet.markercluster
4 (c) 2012-2013, Dave Leaver, smartrak
6 (function (window
, document
, undefined) {/*
7 * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
10 L
.MarkerClusterGroup
= L
.FeatureGroup
.extend({
13 maxClusterRadius
: 80, //A cluster will cover at most this many pixels from its center
14 iconCreateFunction
: null,
16 spiderfyOnMaxZoom
: true,
17 showCoverageOnHover
: true,
18 zoomToBoundsOnClick
: true,
19 singleMarkerMode
: false,
21 disableClusteringAtZoom
: null,
23 // Setting this to false prevents the removal of any clusters outside of the viewpoint, which
24 // is the default behaviour for performance reasons.
25 removeOutsideVisibleBounds
: true,
27 // Set to false to disable all animations (zoom and spiderfy).
28 // If false, option animateAddingMarkers below has no effect.
29 // If L.DomUtil.TRANSITION is falsy, this option has no effect.
32 //Whether to animate adding markers after adding the MarkerClusterGroup to the map
33 // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
34 animateAddingMarkers
: false,
36 //Increase to increase the distance away that spiderfied markers appear from the center
37 spiderfyDistanceMultiplier
: 1,
39 // Make it possible to specify a polyline options on a spider leg
40 spiderLegPolylineOptions
: { weight
: 1.5, color
: '#222', opacity
: 0.5 },
42 // 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
43 chunkedLoading
: false,
44 chunkInterval
: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback)
45 chunkDelay
: 50, // at the end of each interval, give n milliseconds back to system/browser
46 chunkProgress
: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator)
48 //Options to pass to the L.Polygon constructor
52 initialize: function (options
) {
53 L
.Util
.setOptions(this, options
);
54 if (!this.options
.iconCreateFunction
) {
55 this.options
.iconCreateFunction
= this._defaultIconCreateFunction
;
58 this._featureGroup
= L
.featureGroup();
59 this._featureGroup
.on(L
.FeatureGroup
.EVENTS
, this._propagateEvent
, this);
61 this._nonPointGroup
= L
.featureGroup();
62 this._nonPointGroup
.on(L
.FeatureGroup
.EVENTS
, this._propagateEvent
, this);
64 this._inZoomAnimation
= 0;
65 this._needsClustering
= [];
66 this._needsRemoving
= []; //Markers removed while we aren't on the map need to be kept track of
67 //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
68 this._currentShownBounds
= null;
72 // Hook the appropriate animation methods.
73 var animate
= L
.DomUtil
.TRANSITION
&& this.options
.animate
;
74 L
.extend(this, animate
? this._withAnimation
: this._noAnimation
);
75 // Remember which MarkerCluster class to instantiate (animated or not).
76 this._markerCluster
= animate
? L
.MarkerCluster
: L
.MarkerClusterNonAnimated
;
79 addLayer: function (layer
) {
81 if (layer
instanceof L
.LayerGroup
) {
83 for (var i
in layer
._layers
) {
84 array
.push(layer
._layers
[i
]);
86 return this.addLayers(array
);
89 //Don't cluster non point data
90 if (!layer
.getLatLng
) {
91 this._nonPointGroup
.addLayer(layer
);
96 this._needsClustering
.push(layer
);
100 if (this.hasLayer(layer
)) {
105 //If we have already clustered we'll need to add this one to a cluster
107 if (this._unspiderfy
) {
111 this._addLayer(layer
, this._maxZoom
);
113 // Refresh bounds and weighted positions.
114 this._topClusterLevel
._recalculateBounds();
116 //Work out what is visible
117 var visibleLayer
= layer
,
118 currentZoom
= this._map
.getZoom();
119 if (layer
.__parent
) {
120 while (visibleLayer
.__parent
._zoom
>= currentZoom
) {
121 visibleLayer
= visibleLayer
.__parent
;
125 if (this._currentShownBounds
.contains(visibleLayer
.getLatLng())) {
126 if (this.options
.animateAddingMarkers
) {
127 this._animationAddLayer(layer
, visibleLayer
);
129 this._animationAddLayerNonAnimated(layer
, visibleLayer
);
135 removeLayer: function (layer
) {
137 if (layer
instanceof L
.LayerGroup
)
140 for (var i
in layer
._layers
) {
141 array
.push(layer
._layers
[i
]);
143 return this.removeLayers(array
);
147 if (!layer
.getLatLng
) {
148 this._nonPointGroup
.removeLayer(layer
);
153 if (!this._arraySplice(this._needsClustering
, layer
) && this.hasLayer(layer
)) {
154 this._needsRemoving
.push(layer
);
159 if (!layer
.__parent
) {
163 if (this._unspiderfy
) {
165 this._unspiderfyLayer(layer
);
168 //Remove the marker from clusters
169 this._removeLayer(layer
, true);
171 // Refresh bounds and weighted positions.
172 this._topClusterLevel
._recalculateBounds();
174 if (this._featureGroup
.hasLayer(layer
)) {
175 this._featureGroup
.removeLayer(layer
);
176 if (layer
.clusterShow
) {
184 //Takes an array of markers and adds them in bulk
185 addLayers: function (layersArray
) {
186 var fg
= this._featureGroup
,
187 npg
= this._nonPointGroup
,
188 chunked
= this.options
.chunkedLoading
,
189 chunkInterval
= this.options
.chunkInterval
,
190 chunkProgress
= this.options
.chunkProgress
,
195 started
= (new Date()).getTime();
196 var process
= L
.bind(function () {
197 var start
= (new Date()).getTime();
198 for (; offset
< layersArray
.length
; offset
++) {
199 if (chunked
&& offset
% 200 === 0) {
200 // every couple hundred markers, instrument the time elapsed since processing started:
201 var elapsed
= (new Date()).getTime() - start
;
202 if (elapsed
> chunkInterval
) {
203 break; // been working too hard, time to take a break :-)
207 m
= layersArray
[offset
];
209 //Not point data, can't be clustered
215 if (this.hasLayer(m
)) {
219 this._addLayer(m
, this._maxZoom
);
221 //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
223 if (m
.__parent
.getChildCount() === 2) {
224 var markers
= m
.__parent
.getAllChildMarkers(),
225 otherMarker
= markers
[0] === m
? markers
[1] : markers
[0];
226 fg
.removeLayer(otherMarker
);
232 // report progress and time elapsed:
233 chunkProgress(offset
, layersArray
.length
, (new Date()).getTime() - started
);
236 // Completed processing all markers.
237 if (offset
=== layersArray
.length
) {
239 // Refresh bounds and weighted positions.
240 this._topClusterLevel
._recalculateBounds();
242 //Update the icons of all those visible clusters that were affected
243 this._featureGroup
.eachLayer(function (c
) {
244 if (c
instanceof L
.MarkerCluster
&& c
._iconNeedsUpdate
) {
249 this._topClusterLevel
._recursivelyAddChildrenToMap(null, this._zoom
, this._currentShownBounds
);
251 setTimeout(process
, this.options
.chunkDelay
);
258 for (i
= 0, l
= layersArray
.length
; i
< l
; i
++) {
261 //Not point data, can't be clustered
267 if (this.hasLayer(m
)) {
273 this._needsClustering
= this._needsClustering
.concat(newMarkers
);
278 //Takes an array of markers and removes them in bulk
279 removeLayers: function (layersArray
) {
281 fg
= this._featureGroup
,
282 npg
= this._nonPointGroup
;
285 for (i
= 0, l
= layersArray
.length
; i
< l
; i
++) {
287 this._arraySplice(this._needsClustering
, m
);
289 if (this.hasLayer(m
)) {
290 this._needsRemoving
.push(m
);
296 if (this._unspiderfy
) {
298 for (i
= 0, l
= layersArray
.length
; i
< l
; i
++) {
300 this._unspiderfyLayer(m
);
304 for (i
= 0, l
= layersArray
.length
; i
< l
; i
++) {
312 this._removeLayer(m
, true, true);
314 if (fg
.hasLayer(m
)) {
322 // Refresh bounds and weighted positions.
323 this._topClusterLevel
._recalculateBounds();
325 //Fix up the clusters and markers on the map
326 this._topClusterLevel
._recursivelyAddChildrenToMap(null, this._zoom
, this._currentShownBounds
);
328 fg
.eachLayer(function (c
) {
329 if (c
instanceof L
.MarkerCluster
) {
337 //Removes all layers from the MarkerClusterGroup
338 clearLayers: function () {
339 //Need our own special implementation as the LayerGroup one doesn't work for us
341 //If we aren't on the map (yet), blow away the markers we know of
343 this._needsClustering
= [];
344 delete this._gridClusters
;
345 delete this._gridUnclustered
;
348 if (this._noanimationUnspiderfy
) {
349 this._noanimationUnspiderfy();
352 //Remove all the visible layers
353 this._featureGroup
.clearLayers();
354 this._nonPointGroup
.clearLayers();
356 this.eachLayer(function (marker
) {
357 delete marker
.__parent
;
361 //Reset _topClusterLevel and the DistanceGrids
362 this._generateInitialClusters();
368 //Override FeatureGroup.getBounds as it doesn't work
369 getBounds: function () {
370 var bounds
= new L
.LatLngBounds();
372 if (this._topClusterLevel
) {
373 bounds
.extend(this._topClusterLevel
._bounds
);
376 for (var i
= this._needsClustering
.length
- 1; i
>= 0; i
--) {
377 bounds
.extend(this._needsClustering
[i
].getLatLng());
380 bounds
.extend(this._nonPointGroup
.getBounds());
385 //Overrides LayerGroup.eachLayer
386 eachLayer: function (method
, context
) {
387 var markers
= this._needsClustering
.slice(),
390 if (this._topClusterLevel
) {
391 this._topClusterLevel
.getAllChildMarkers(markers
);
394 for (i
= markers
.length
- 1; i
>= 0; i
--) {
395 method
.call(context
, markers
[i
]);
398 this._nonPointGroup
.eachLayer(method
, context
);
401 //Overrides LayerGroup.getLayers
402 getLayers: function () {
404 this.eachLayer(function (l
) {
410 //Overrides LayerGroup.getLayer, WARNING: Really bad performance
411 getLayer: function (id
) {
414 id
= parseInt(id
, 10);
416 this.eachLayer(function (l
) {
417 if (L
.stamp(l
) === id
) {
425 //Returns true if the given layer is in this MarkerClusterGroup
426 hasLayer: function (layer
) {
431 var i
, anArray
= this._needsClustering
;
433 for (i
= anArray
.length
- 1; i
>= 0; i
--) {
434 if (anArray
[i
] === layer
) {
439 anArray
= this._needsRemoving
;
440 for (i
= anArray
.length
- 1; i
>= 0; i
--) {
441 if (anArray
[i
] === layer
) {
446 return !!(layer
.__parent
&& layer
.__parent
._group
=== this) || this._nonPointGroup
.hasLayer(layer
);
449 //Zoom down to show the given layer (spiderfying if necessary) then calls the callback
450 zoomToShowLayer: function (layer
, callback
) {
452 if (typeof callback
!== 'function') {
453 callback = function () {};
456 var showMarker = function () {
457 if ((layer
._icon
|| layer
.__parent
._icon
) && !this._inZoomAnimation
) {
458 this._map
.off('moveend', showMarker
, this);
459 this.off('animationend', showMarker
, this);
463 } else if (layer
.__parent
._icon
) {
464 this.once('spiderfied', callback
, this);
465 layer
.__parent
.spiderfy();
470 if (layer
._icon
&& this._map
.getBounds().contains(layer
.getLatLng())) {
471 //Layer is visible ond on screen, immediate return
473 } else if (layer
.__parent
._zoom
< this._map
.getZoom()) {
474 //Layer should be visible at this zoom level. It must not be on screen so just pan over to it
475 this._map
.on('moveend', showMarker
, this);
476 this._map
.panTo(layer
.getLatLng());
478 var moveStart = function () {
479 this._map
.off('movestart', moveStart
, this);
483 this._map
.on('movestart', moveStart
, this);
484 this._map
.on('moveend', showMarker
, this);
485 this.on('animationend', showMarker
, this);
486 layer
.__parent
.zoomToBounds();
489 //Never started moving, must already be there, probably need clustering however
490 showMarker
.call(this);
495 //Overrides FeatureGroup.onAdd
496 onAdd: function (map
) {
500 if (!isFinite(this._map
.getMaxZoom())) {
501 throw "Map has no maxZoom specified";
504 this._featureGroup
.onAdd(map
);
505 this._nonPointGroup
.onAdd(map
);
507 if (!this._gridClusters
) {
508 this._generateInitialClusters();
511 this._maxLat
= map
.options
.crs
.projection
.MAX_LATITUDE
;
513 for (i
= 0, l
= this._needsRemoving
.length
; i
< l
; i
++) {
514 layer
= this._needsRemoving
[i
];
515 this._removeLayer(layer
, true);
517 this._needsRemoving
= [];
519 //Remember the current zoom level and bounds
520 this._zoom
= this._map
.getZoom();
521 this._currentShownBounds
= this._getExpandedVisibleBounds();
523 this._map
.on('zoomend', this._zoomEnd
, this);
524 this._map
.on('moveend', this._moveEnd
, this);
526 if (this._spiderfierOnAdd
) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
527 this._spiderfierOnAdd();
532 //Actually add our markers to the map:
533 l
= this._needsClustering
;
534 this._needsClustering
= [];
538 //Overrides FeatureGroup.onRemove
539 onRemove: function (map
) {
540 map
.off('zoomend', this._zoomEnd
, this);
541 map
.off('moveend', this._moveEnd
, this);
543 this._unbindEvents();
545 //In case we are in a cluster animation
546 this._map
._mapPane
.className
= this._map
._mapPane
.className
.replace(' leaflet-cluster-anim', '');
548 if (this._spiderfierOnRemove
) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
549 this._spiderfierOnRemove();
554 //Clean up all the layers we added to the map
555 this._hideCoverage();
556 this._featureGroup
.onRemove(map
);
557 this._nonPointGroup
.onRemove(map
);
559 this._featureGroup
.clearLayers();
564 getVisibleParent: function (marker
) {
565 var vMarker
= marker
;
566 while (vMarker
&& !vMarker
._icon
) {
567 vMarker
= vMarker
.__parent
;
569 return vMarker
|| null;
572 //Remove the given object from the given array
573 _arraySplice: function (anArray
, obj
) {
574 for (var i
= anArray
.length
- 1; i
>= 0; i
--) {
575 if (anArray
[i
] === obj
) {
576 anArray
.splice(i
, 1);
583 * Removes a marker from all _gridUnclustered zoom levels, starting at the supplied zoom.
584 * @param marker to be removed from _gridUnclustered.
585 * @param z integer bottom start zoom level (included)
588 _removeFromGridUnclustered: function (marker
, z
) {
590 gridUnclustered
= this._gridUnclustered
;
592 for (; z
>= 0; z
--) {
593 if (!gridUnclustered
[z
].removeObject(marker
, map
.project(marker
.getLatLng(), z
))) {
599 //Internal function for removing a marker from everything.
600 //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
601 _removeLayer: function (marker
, removeFromDistanceGrid
, dontUpdateMap
) {
602 var gridClusters
= this._gridClusters
,
603 gridUnclustered
= this._gridUnclustered
,
604 fg
= this._featureGroup
,
607 //Remove the marker from distance clusters it might be in
608 if (removeFromDistanceGrid
) {
609 this._removeFromGridUnclustered(marker
, this._maxZoom
);
612 //Work our way up the clusters removing them as we go if required
613 var cluster
= marker
.__parent
,
614 markers
= cluster
._markers
,
617 //Remove the marker from the immediate parents marker list
618 this._arraySplice(markers
, marker
);
621 cluster
._childCount
--;
622 cluster
._boundsNeedUpdate
= true;
624 if (cluster
._zoom
< 0) {
625 //Top level, do nothing
627 } else if (removeFromDistanceGrid
&& cluster
._childCount
<= 1) { //Cluster no longer required
628 //We need to push the other marker up to the parent
629 otherMarker
= cluster
._markers
[0] === marker
? cluster
._markers
[1] : cluster
._markers
[0];
631 //Update distance grid
632 gridClusters
[cluster
._zoom
].removeObject(cluster
, map
.project(cluster
._cLatLng
, cluster
._zoom
));
633 gridUnclustered
[cluster
._zoom
].addObject(otherMarker
, map
.project(otherMarker
.getLatLng(), cluster
._zoom
));
635 //Move otherMarker up to parent
636 this._arraySplice(cluster
.__parent
._childClusters
, cluster
);
637 cluster
.__parent
._markers
.push(otherMarker
);
638 otherMarker
.__parent
= cluster
.__parent
;
641 //Cluster is currently on the map, need to put the marker on the map instead
642 fg
.removeLayer(cluster
);
643 if (!dontUpdateMap
) {
644 fg
.addLayer(otherMarker
);
648 if (!dontUpdateMap
|| !cluster
._icon
) {
649 cluster
._updateIcon();
653 cluster
= cluster
.__parent
;
656 delete marker
.__parent
;
659 _isOrIsParent: function (el
, oel
) {
664 oel
= oel
.parentNode
;
669 _propagateEvent: function (e
) {
670 if (e
.layer
instanceof L
.MarkerCluster
) {
671 //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget)
672 if (e
.originalEvent
&& this._isOrIsParent(e
.layer
._icon
, e
.originalEvent
.relatedTarget
)) {
675 e
.type
= 'cluster' + e
.type
;
678 this.fire(e
.type
, e
);
681 //Default functionality
682 _defaultIconCreateFunction: function (cluster
) {
683 var childCount
= cluster
.getChildCount();
685 var c
= ' marker-cluster-';
686 if (childCount
< 10) {
688 } else if (childCount
< 100) {
694 return new L
.DivIcon({ html
: '<div><span>' + childCount
+ '</span></div>', className
: 'marker-cluster' + c
, iconSize
: new L
.Point(40, 40) });
697 _bindEvents: function () {
699 spiderfyOnMaxZoom
= this.options
.spiderfyOnMaxZoom
,
700 showCoverageOnHover
= this.options
.showCoverageOnHover
,
701 zoomToBoundsOnClick
= this.options
.zoomToBoundsOnClick
;
703 //Zoom on cluster click or spiderfy if we are at the lowest level
704 if (spiderfyOnMaxZoom
|| zoomToBoundsOnClick
) {
705 this.on('clusterclick', this._zoomOrSpiderfy
, this);
708 //Show convex hull (boundary) polygon on mouse over
709 if (showCoverageOnHover
) {
710 this.on('clustermouseover', this._showCoverage
, this);
711 this.on('clustermouseout', this._hideCoverage
, this);
712 map
.on('zoomend', this._hideCoverage
, this);
716 _zoomOrSpiderfy: function (e
) {
717 var cluster
= e
.layer
,
718 bottomCluster
= cluster
;
720 while (bottomCluster
._childClusters
.length
=== 1) {
721 bottomCluster
= bottomCluster
._childClusters
[0];
724 if (bottomCluster
._zoom
=== this._maxZoom
&& bottomCluster
._childCount
=== cluster
._childCount
) {
725 // All child markers are contained in a single cluster from this._maxZoom to this cluster.
726 if (this.options
.spiderfyOnMaxZoom
) {
729 } else if (this.options
.zoomToBoundsOnClick
) {
730 cluster
.zoomToBounds();
733 // Focus the map again for keyboard users.
734 if (e
.originalEvent
&& e
.originalEvent
.keyCode
=== 13) {
735 this._map
._container
.focus();
739 _showCoverage: function (e
) {
741 if (this._inZoomAnimation
) {
744 if (this._shownPolygon
) {
745 map
.removeLayer(this._shownPolygon
);
747 if (e
.layer
.getChildCount() > 2 && e
.layer
!== this._spiderfied
) {
748 this._shownPolygon
= new L
.Polygon(e
.layer
.getConvexHull(), this.options
.polygonOptions
);
749 map
.addLayer(this._shownPolygon
);
753 _hideCoverage: function () {
754 if (this._shownPolygon
) {
755 this._map
.removeLayer(this._shownPolygon
);
756 this._shownPolygon
= null;
760 _unbindEvents: function () {
761 var spiderfyOnMaxZoom
= this.options
.spiderfyOnMaxZoom
,
762 showCoverageOnHover
= this.options
.showCoverageOnHover
,
763 zoomToBoundsOnClick
= this.options
.zoomToBoundsOnClick
,
766 if (spiderfyOnMaxZoom
|| zoomToBoundsOnClick
) {
767 this.off('clusterclick', this._zoomOrSpiderfy
, this);
769 if (showCoverageOnHover
) {
770 this.off('clustermouseover', this._showCoverage
, this);
771 this.off('clustermouseout', this._hideCoverage
, this);
772 map
.off('zoomend', this._hideCoverage
, this);
776 _zoomEnd: function () {
777 if (!this._map
) { //May have been removed from the map by a zoomEnd handler
780 this._mergeSplitClusters();
782 this._zoom
= this._map
._zoom
;
783 this._currentShownBounds
= this._getExpandedVisibleBounds();
786 _moveEnd: function () {
787 if (this._inZoomAnimation
) {
791 var newBounds
= this._getExpandedVisibleBounds();
793 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, this._zoom
, newBounds
);
794 this._topClusterLevel
._recursivelyAddChildrenToMap(null, this._map
._zoom
, newBounds
);
796 this._currentShownBounds
= newBounds
;
800 _generateInitialClusters: function () {
801 var maxZoom
= this._map
.getMaxZoom(),
802 radius
= this.options
.maxClusterRadius
,
805 //If we just set maxClusterRadius to a single number, we need to create
806 //a simple function to return that number. Otherwise, we just have to
807 //use the function we've passed in.
808 if (typeof radius
!== "function") {
809 radiusFn = function () { return radius
; };
812 if (this.options
.disableClusteringAtZoom
) {
813 maxZoom
= this.options
.disableClusteringAtZoom
- 1;
815 this._maxZoom
= maxZoom
;
816 this._gridClusters
= {};
817 this._gridUnclustered
= {};
819 //Set up DistanceGrids for each zoom
820 for (var zoom
= maxZoom
; zoom
>= 0; zoom
--) {
821 this._gridClusters
[zoom
] = new L
.DistanceGrid(radiusFn(zoom
));
822 this._gridUnclustered
[zoom
] = new L
.DistanceGrid(radiusFn(zoom
));
825 // Instantiate the appropriate L.MarkerCluster class (animated or not).
826 this._topClusterLevel
= new this._markerCluster(this, -1);
829 //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
830 _addLayer: function (layer
, zoom
) {
831 var gridClusters
= this._gridClusters
,
832 gridUnclustered
= this._gridUnclustered
,
835 if (this.options
.singleMarkerMode
) {
836 this._overrideMarkerIcon(layer
);
839 //Find the lowest zoom level to slot this one in
840 for (; zoom
>= 0; zoom
--) {
841 markerPoint
= this._map
.project(layer
.getLatLng(), zoom
); // calculate pixel position
843 //Try find a cluster close by
844 var closest
= gridClusters
[zoom
].getNearObject(markerPoint
);
846 closest
._addChild(layer
);
847 layer
.__parent
= closest
;
851 //Try find a marker close by to form a new cluster with
852 closest
= gridUnclustered
[zoom
].getNearObject(markerPoint
);
854 var parent
= closest
.__parent
;
856 this._removeLayer(closest
, false);
859 //Create new cluster with these 2 in it
861 var newCluster
= new this._markerCluster(this, zoom
, closest
, layer
);
862 gridClusters
[zoom
].addObject(newCluster
, this._map
.project(newCluster
._cLatLng
, zoom
));
863 closest
.__parent
= newCluster
;
864 layer
.__parent
= newCluster
;
866 //First create any new intermediate parent clusters that don't exist
867 var lastParent
= newCluster
;
868 for (z
= zoom
- 1; z
> parent
._zoom
; z
--) {
869 lastParent
= new this._markerCluster(this, z
, lastParent
);
870 gridClusters
[z
].addObject(lastParent
, this._map
.project(closest
.getLatLng(), z
));
872 parent
._addChild(lastParent
);
874 //Remove closest from this zoom level and any above that it is in, replace with newCluster
875 this._removeFromGridUnclustered(closest
, zoom
);
880 //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
881 gridUnclustered
[zoom
].addObject(layer
, markerPoint
);
884 //Didn't get in anything, add us to the top
885 this._topClusterLevel
._addChild(layer
);
886 layer
.__parent
= this._topClusterLevel
;
890 //Enqueue code to fire after the marker expand/contract has happened
891 _enqueue: function (fn
) {
892 this._queue
.push(fn
);
893 if (!this._queueTimeout
) {
894 this._queueTimeout
= setTimeout(L
.bind(this._processQueue
, this), 300);
897 _processQueue: function () {
898 for (var i
= 0; i
< this._queue
.length
; i
++) {
899 this._queue
[i
].call(this);
901 this._queue
.length
= 0;
902 clearTimeout(this._queueTimeout
);
903 this._queueTimeout
= null;
906 //Merge and split any existing clusters that are too big or small
907 _mergeSplitClusters: function () {
909 //Incase we are starting to split before the animation finished
910 this._processQueue();
912 if (this._zoom
< this._map
._zoom
&& this._currentShownBounds
.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split
913 this._animationStart();
914 //Remove clusters now off screen
915 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, this._zoom
, this._getExpandedVisibleBounds());
917 this._animationZoomIn(this._zoom
, this._map
._zoom
);
919 } else if (this._zoom
> this._map
._zoom
) { //Zoom out, merge
920 this._animationStart();
922 this._animationZoomOut(this._zoom
, this._map
._zoom
);
928 //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)
929 _getExpandedVisibleBounds: function () {
930 if (!this.options
.removeOutsideVisibleBounds
) {
931 return this._mapBoundsInfinite
;
932 } else if (L
.Browser
.mobile
) {
933 return this._checkBoundsMaxLat(this._map
.getBounds());
936 return this._checkBoundsMaxLat(this._map
.getBounds().pad(1)); // Padding expands the bounds by its own dimensions but scaled with the given factor.
940 * Expands the latitude to Infinity (or -Infinity) if the input bounds reach the map projection maximum defined latitude
941 * (in the case of Web/Spherical Mercator, it is 85.0511287798 / see https://en.wikipedia.org/wiki/Web_Mercator#Formulas).
942 * Otherwise, the removeOutsideVisibleBounds option will remove markers beyond that limit, whereas the same markers without
943 * this option (or outside MCG) will have their position floored (ceiled) by the projection and rendered at that limit,
944 * making the user think that MCG "eats" them and never displays them again.
945 * @param bounds L.LatLngBounds
946 * @returns {L.LatLngBounds}
949 _checkBoundsMaxLat: function (bounds
) {
950 var maxLat
= this._maxLat
;
952 if (maxLat
!== undefined) {
953 if (bounds
.getNorth() >= maxLat
) {
954 bounds
._northEast
.lat
= Infinity
;
956 if (bounds
.getSouth() <= -maxLat
) {
957 bounds
._southWest
.lat
= -Infinity
;
964 //Shared animation code
965 _animationAddLayerNonAnimated: function (layer
, newCluster
) {
966 if (newCluster
=== layer
) {
967 this._featureGroup
.addLayer(layer
);
968 } else if (newCluster
._childCount
=== 2) {
969 newCluster
._addToMap();
971 var markers
= newCluster
.getAllChildMarkers();
972 this._featureGroup
.removeLayer(markers
[0]);
973 this._featureGroup
.removeLayer(markers
[1]);
975 newCluster
._updateIcon();
980 * Implements the singleMarkerMode option.
981 * @param layer Marker to re-style using the Clusters iconCreateFunction.
982 * @returns {L.Icon} The newly created icon.
985 _overrideMarkerIcon: function (layer
) {
986 var icon
= layer
.options
.icon
= this.options
.iconCreateFunction({
987 getChildCount: function () {
990 getAllChildMarkers: function () {
999 // Constant bounds used in case option "removeOutsideVisibleBounds" is set to false.
1000 L
.MarkerClusterGroup
.include({
1001 _mapBoundsInfinite
: new L
.LatLngBounds(new L
.LatLng(-Infinity
, -Infinity
), new L
.LatLng(Infinity
, Infinity
))
1004 L
.MarkerClusterGroup
.include({
1006 //Non Animated versions of everything
1007 _animationStart: function () {
1010 _animationZoomIn: function (previousZoomLevel
, newZoomLevel
) {
1011 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, previousZoomLevel
);
1012 this._topClusterLevel
._recursivelyAddChildrenToMap(null, newZoomLevel
, this._getExpandedVisibleBounds());
1014 //We didn't actually animate, but we use this event to mean "clustering animations have finished"
1015 this.fire('animationend');
1017 _animationZoomOut: function (previousZoomLevel
, newZoomLevel
) {
1018 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, previousZoomLevel
);
1019 this._topClusterLevel
._recursivelyAddChildrenToMap(null, newZoomLevel
, this._getExpandedVisibleBounds());
1021 //We didn't actually animate, but we use this event to mean "clustering animations have finished"
1022 this.fire('animationend');
1024 _animationAddLayer: function (layer
, newCluster
) {
1025 this._animationAddLayerNonAnimated(layer
, newCluster
);
1029 //Animated versions here
1030 _animationStart: function () {
1031 this._map
._mapPane
.className
+= ' leaflet-cluster-anim';
1032 this._inZoomAnimation
++;
1034 _animationZoomIn: function (previousZoomLevel
, newZoomLevel
) {
1035 var bounds
= this._getExpandedVisibleBounds(),
1036 fg
= this._featureGroup
,
1039 //Add all children of current clusters to map and remove those clusters from map
1040 this._topClusterLevel
._recursively(bounds
, previousZoomLevel
, 0, function (c
) {
1041 var startPos
= c
._latlng
,
1042 markers
= c
._markers
,
1045 if (!bounds
.contains(startPos
)) {
1049 if (c
._isSingleParent() && previousZoomLevel
+ 1 === newZoomLevel
) { //Immediately add the new child and remove us
1051 c
._recursivelyAddChildrenToMap(null, newZoomLevel
, bounds
);
1053 //Fade out old cluster
1055 c
._recursivelyAddChildrenToMap(startPos
, newZoomLevel
, bounds
);
1058 //Remove all markers that aren't visible any more
1059 //TODO: Do we actually need to do this on the higher levels too?
1060 for (i
= markers
.length
- 1; i
>= 0; i
--) {
1062 if (!bounds
.contains(m
._latlng
)) {
1069 this._forceLayout();
1072 this._topClusterLevel
._recursivelyBecomeVisible(bounds
, newZoomLevel
);
1073 //TODO Maybe? Update markers in _recursivelyBecomeVisible
1074 fg
.eachLayer(function (n
) {
1075 if (!(n
instanceof L
.MarkerCluster
) && n
._icon
) {
1080 //update the positions of the just added clusters/markers
1081 this._topClusterLevel
._recursively(bounds
, previousZoomLevel
, newZoomLevel
, function (c
) {
1082 c
._recursivelyRestoreChildPositions(newZoomLevel
);
1085 //Remove the old clusters and close the zoom animation
1086 this._enqueue(function () {
1087 //update the positions of the just added clusters/markers
1088 this._topClusterLevel
._recursively(bounds
, previousZoomLevel
, 0, function (c
) {
1093 this._animationEnd();
1097 _animationZoomOut: function (previousZoomLevel
, newZoomLevel
) {
1098 this._animationZoomOutSingle(this._topClusterLevel
, previousZoomLevel
- 1, newZoomLevel
);
1100 //Need to add markers for those that weren't on the map before but are now
1101 this._topClusterLevel
._recursivelyAddChildrenToMap(null, newZoomLevel
, this._getExpandedVisibleBounds());
1102 //Remove markers that were on the map before but won't be now
1103 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, previousZoomLevel
, this._getExpandedVisibleBounds());
1105 _animationAddLayer: function (layer
, newCluster
) {
1107 fg
= this._featureGroup
;
1110 if (newCluster
!== layer
) {
1111 if (newCluster
._childCount
> 2) { //Was already a cluster
1113 newCluster
._updateIcon();
1114 this._forceLayout();
1115 this._animationStart();
1117 layer
._setPos(this._map
.latLngToLayerPoint(newCluster
.getLatLng()));
1118 layer
.clusterHide();
1120 this._enqueue(function () {
1121 fg
.removeLayer(layer
);
1122 layer
.clusterShow();
1127 } else { //Just became a cluster
1128 this._forceLayout();
1130 me
._animationStart();
1131 me
._animationZoomOutSingle(newCluster
, this._map
.getMaxZoom(), this._map
.getZoom());
1137 // Private methods for animated versions.
1138 _animationZoomOutSingle: function (cluster
, previousZoomLevel
, newZoomLevel
) {
1139 var bounds
= this._getExpandedVisibleBounds();
1141 //Animate all of the markers in the clusters to move to their cluster center point
1142 cluster
._recursivelyAnimateChildrenInAndAddSelfToMap(bounds
, previousZoomLevel
+ 1, newZoomLevel
);
1146 //Update the opacity (If we immediately set it they won't animate)
1147 this._forceLayout();
1148 cluster
._recursivelyBecomeVisible(bounds
, newZoomLevel
);
1150 //TODO: Maybe use the transition timing stuff to make this more reliable
1151 //When the animations are done, tidy up
1152 this._enqueue(function () {
1154 //This cluster stopped being a cluster before the timeout fired
1155 if (cluster
._childCount
=== 1) {
1156 var m
= cluster
._markers
[0];
1157 //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
1158 m
.setLatLng(m
.getLatLng());
1159 if (m
.clusterShow
) {
1163 cluster
._recursively(bounds
, newZoomLevel
, 0, function (c
) {
1164 c
._recursivelyRemoveChildrenFromMap(bounds
, previousZoomLevel
+ 1);
1171 _animationEnd: function () {
1173 this._map
._mapPane
.className
= this._map
._mapPane
.className
.replace(' leaflet-cluster-anim', '');
1175 this._inZoomAnimation
--;
1176 this.fire('animationend');
1179 //Force a browser layout of stuff in the map
1180 // Should apply the current opacity and location to all elements so we can update them again for an animation
1181 _forceLayout: function () {
1182 //In my testing this works, infact offsetWidth of any element seems to work.
1183 //Could loop all this._layers and do this for each _icon if it stops working
1185 L
.Util
.falseFn(document
.body
.offsetWidth
);
1189 L
.markerClusterGroup = function (options
) {
1190 return new L
.MarkerClusterGroup(options
);
1194 L
.MarkerCluster
= L
.Marker
.extend({
1195 initialize: function (group
, zoom
, a
, b
) {
1197 L
.Marker
.prototype.initialize
.call(this, a
? (a
._cLatLng
|| a
.getLatLng()) : new L
.LatLng(0, 0), { icon
: this });
1200 this._group
= group
;
1204 this._childClusters
= [];
1205 this._childCount
= 0;
1206 this._iconNeedsUpdate
= true;
1207 this._boundsNeedUpdate
= true;
1209 this._bounds
= new L
.LatLngBounds();
1219 //Recursively retrieve all child markers of this cluster
1220 getAllChildMarkers: function (storageArray
) {
1221 storageArray
= storageArray
|| [];
1223 for (var i
= this._childClusters
.length
- 1; i
>= 0; i
--) {
1224 this._childClusters
[i
].getAllChildMarkers(storageArray
);
1227 for (var j
= this._markers
.length
- 1; j
>= 0; j
--) {
1228 storageArray
.push(this._markers
[j
]);
1231 return storageArray
;
1234 //Returns the count of how many child markers we have
1235 getChildCount: function () {
1236 return this._childCount
;
1239 //Zoom to the minimum of showing all of the child markers, or the extents of this cluster
1240 zoomToBounds: function () {
1241 var childClusters
= this._childClusters
.slice(),
1242 map
= this._group
._map
,
1243 boundsZoom
= map
.getBoundsZoom(this._bounds
),
1244 zoom
= this._zoom
+ 1,
1245 mapZoom
= map
.getZoom(),
1248 //calculate how far we need to zoom down to see all of the markers
1249 while (childClusters
.length
> 0 && boundsZoom
> zoom
) {
1251 var newClusters
= [];
1252 for (i
= 0; i
< childClusters
.length
; i
++) {
1253 newClusters
= newClusters
.concat(childClusters
[i
]._childClusters
);
1255 childClusters
= newClusters
;
1258 if (boundsZoom
> zoom
) {
1259 this._group
._map
.setView(this._latlng
, zoom
);
1260 } else if (boundsZoom
<= mapZoom
) { //If fitBounds wouldn't zoom us down, zoom us down instead
1261 this._group
._map
.setView(this._latlng
, mapZoom
+ 1);
1263 this._group
._map
.fitBounds(this._bounds
);
1267 getBounds: function () {
1268 var bounds
= new L
.LatLngBounds();
1269 bounds
.extend(this._bounds
);
1273 _updateIcon: function () {
1274 this._iconNeedsUpdate
= true;
1280 //Cludge for Icon, we pretend to be an icon for performance
1281 createIcon: function () {
1282 if (this._iconNeedsUpdate
) {
1283 this._iconObj
= this._group
.options
.iconCreateFunction(this);
1284 this._iconNeedsUpdate
= false;
1286 return this._iconObj
.createIcon();
1288 createShadow: function () {
1289 return this._iconObj
.createShadow();
1293 _addChild: function (new1
, isNotificationFromChild
) {
1295 this._iconNeedsUpdate
= true;
1297 this._boundsNeedUpdate
= true;
1298 this._setClusterCenter(new1
);
1300 if (new1
instanceof L
.MarkerCluster
) {
1301 if (!isNotificationFromChild
) {
1302 this._childClusters
.push(new1
);
1303 new1
.__parent
= this;
1305 this._childCount
+= new1
._childCount
;
1307 if (!isNotificationFromChild
) {
1308 this._markers
.push(new1
);
1313 if (this.__parent
) {
1314 this.__parent
._addChild(new1
, true);
1319 * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position.
1320 * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet.
1323 _setClusterCenter: function (child
) {
1324 if (!this._cLatLng
) {
1325 // when clustering, take position of the first point as the cluster center
1326 this._cLatLng
= child
._cLatLng
|| child
._latlng
;
1331 * Assigns impossible bounding values so that the next extend entirely determines the new bounds.
1332 * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class.
1333 * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended.
1336 _resetBounds: function () {
1337 var bounds
= this._bounds
;
1339 if (bounds
._southWest
) {
1340 bounds
._southWest
.lat
= Infinity
;
1341 bounds
._southWest
.lng
= Infinity
;
1343 if (bounds
._northEast
) {
1344 bounds
._northEast
.lat
= -Infinity
;
1345 bounds
._northEast
.lng
= -Infinity
;
1349 _recalculateBounds: function () {
1350 var markers
= this._markers
,
1351 childClusters
= this._childClusters
,
1354 totalCount
= this._childCount
,
1355 i
, child
, childLatLng
, childCount
;
1357 // Case where all markers are removed from the map and we are left with just an empty _topClusterLevel.
1358 if (totalCount
=== 0) {
1362 // Reset rather than creating a new object, for performance.
1363 this._resetBounds();
1366 for (i
= 0; i
< markers
.length
; i
++) {
1367 childLatLng
= markers
[i
]._latlng
;
1369 this._bounds
.extend(childLatLng
);
1371 latSum
+= childLatLng
.lat
;
1372 lngSum
+= childLatLng
.lng
;
1376 for (i
= 0; i
< childClusters
.length
; i
++) {
1377 child
= childClusters
[i
];
1379 // Re-compute child bounds and weighted position first if necessary.
1380 if (child
._boundsNeedUpdate
) {
1381 child
._recalculateBounds();
1384 this._bounds
.extend(child
._bounds
);
1386 childLatLng
= child
._wLatLng
;
1387 childCount
= child
._childCount
;
1389 latSum
+= childLatLng
.lat
* childCount
;
1390 lngSum
+= childLatLng
.lng
* childCount
;
1393 this._latlng
= this._wLatLng
= new L
.LatLng(latSum
/ totalCount
, lngSum
/ totalCount
);
1395 // Reset dirty flag.
1396 this._boundsNeedUpdate
= false;
1399 //Set our markers position as given and add it to the map
1400 _addToMap: function (startPos
) {
1402 this._backupLatlng
= this._latlng
;
1403 this.setLatLng(startPos
);
1405 this._group
._featureGroup
.addLayer(this);
1408 _recursivelyAnimateChildrenIn: function (bounds
, center
, maxZoom
) {
1409 this._recursively(bounds
, 0, maxZoom
- 1,
1411 var markers
= c
._markers
,
1413 for (i
= markers
.length
- 1; i
>= 0; i
--) {
1416 //Only do it if the icon is still on the map
1424 var childClusters
= c
._childClusters
,
1426 for (j
= childClusters
.length
- 1; j
>= 0; j
--) {
1427 cm
= childClusters
[j
];
1437 _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds
, previousZoomLevel
, newZoomLevel
) {
1438 this._recursively(bounds
, newZoomLevel
, 0,
1440 c
._recursivelyAnimateChildrenIn(bounds
, c
._group
._map
.latLngToLayerPoint(c
.getLatLng()).round(), previousZoomLevel
);
1442 //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
1443 //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
1444 if (c
._isSingleParent() && previousZoomLevel
- 1 === newZoomLevel
) {
1446 c
._recursivelyRemoveChildrenFromMap(bounds
, previousZoomLevel
); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
1456 _recursivelyBecomeVisible: function (bounds
, zoomLevel
) {
1457 this._recursively(bounds
, 0, zoomLevel
, null, function (c
) {
1462 _recursivelyAddChildrenToMap: function (startPos
, zoomLevel
, bounds
) {
1463 this._recursively(bounds
, -1, zoomLevel
,
1465 if (zoomLevel
=== c
._zoom
) {
1469 //Add our child markers at startPos (so they can be animated out)
1470 for (var i
= c
._markers
.length
- 1; i
>= 0; i
--) {
1471 var nm
= c
._markers
[i
];
1473 if (!bounds
.contains(nm
._latlng
)) {
1478 nm
._backupLatlng
= nm
.getLatLng();
1480 nm
.setLatLng(startPos
);
1481 if (nm
.clusterHide
) {
1486 c
._group
._featureGroup
.addLayer(nm
);
1490 c
._addToMap(startPos
);
1495 _recursivelyRestoreChildPositions: function (zoomLevel
) {
1496 //Fix positions of child markers
1497 for (var i
= this._markers
.length
- 1; i
>= 0; i
--) {
1498 var nm
= this._markers
[i
];
1499 if (nm
._backupLatlng
) {
1500 nm
.setLatLng(nm
._backupLatlng
);
1501 delete nm
._backupLatlng
;
1505 if (zoomLevel
- 1 === this._zoom
) {
1506 //Reposition child clusters
1507 for (var j
= this._childClusters
.length
- 1; j
>= 0; j
--) {
1508 this._childClusters
[j
]._restorePosition();
1511 for (var k
= this._childClusters
.length
- 1; k
>= 0; k
--) {
1512 this._childClusters
[k
]._recursivelyRestoreChildPositions(zoomLevel
);
1517 _restorePosition: function () {
1518 if (this._backupLatlng
) {
1519 this.setLatLng(this._backupLatlng
);
1520 delete this._backupLatlng
;
1524 //exceptBounds: If set, don't remove any markers/clusters in it
1525 _recursivelyRemoveChildrenFromMap: function (previousBounds
, zoomLevel
, exceptBounds
) {
1527 this._recursively(previousBounds
, -1, zoomLevel
- 1,
1529 //Remove markers at every level
1530 for (i
= c
._markers
.length
- 1; i
>= 0; i
--) {
1532 if (!exceptBounds
|| !exceptBounds
.contains(m
._latlng
)) {
1533 c
._group
._featureGroup
.removeLayer(m
);
1534 if (m
.clusterShow
) {
1541 //Remove child clusters at just the bottom level
1542 for (i
= c
._childClusters
.length
- 1; i
>= 0; i
--) {
1543 m
= c
._childClusters
[i
];
1544 if (!exceptBounds
|| !exceptBounds
.contains(m
._latlng
)) {
1545 c
._group
._featureGroup
.removeLayer(m
);
1546 if (m
.clusterShow
) {
1555 //Run the given functions recursively to this and child clusters
1556 // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
1557 // zoomLevelToStart: zoom level to start running functions (inclusive)
1558 // zoomLevelToStop: zoom level to stop running functions (inclusive)
1559 // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
1560 // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
1561 _recursively: function (boundsToApplyTo
, zoomLevelToStart
, zoomLevelToStop
, runAtEveryLevel
, runAtBottomLevel
) {
1562 var childClusters
= this._childClusters
,
1566 if (zoomLevelToStart
> zoom
) { //Still going down to required depth, just recurse to child clusters
1567 for (i
= childClusters
.length
- 1; i
>= 0; i
--) {
1568 c
= childClusters
[i
];
1569 if (boundsToApplyTo
.intersects(c
._bounds
)) {
1570 c
._recursively(boundsToApplyTo
, zoomLevelToStart
, zoomLevelToStop
, runAtEveryLevel
, runAtBottomLevel
);
1573 } else { //In required depth
1575 if (runAtEveryLevel
) {
1576 runAtEveryLevel(this);
1578 if (runAtBottomLevel
&& this._zoom
=== zoomLevelToStop
) {
1579 runAtBottomLevel(this);
1582 //TODO: This loop is almost the same as above
1583 if (zoomLevelToStop
> zoom
) {
1584 for (i
= childClusters
.length
- 1; i
>= 0; i
--) {
1585 c
= childClusters
[i
];
1586 if (boundsToApplyTo
.intersects(c
._bounds
)) {
1587 c
._recursively(boundsToApplyTo
, zoomLevelToStart
, zoomLevelToStop
, runAtEveryLevel
, runAtBottomLevel
);
1594 //Returns true if we are the parent of only one cluster and that cluster is the same as us
1595 _isSingleParent: function () {
1596 //Don't need to check this._markers as the rest won't work if there are any
1597 return this._childClusters
.length
> 0 && this._childClusters
[0]._childCount
=== this._childCount
;
1604 * Extends L.Marker to include two extra methods: clusterHide and clusterShow.
1606 * They work as setOpacity(0) and setOpacity(1) respectively, but
1607 * they will remember the marker's opacity when hiding and showing it again.
1614 clusterHide: function () {
1615 this.options
.opacityWhenUnclustered
= this.options
.opacity
|| 1;
1616 return this.setOpacity(0);
1619 clusterShow: function () {
1620 var ret
= this.setOpacity(this.options
.opacity
|| this.options
.opacityWhenUnclustered
);
1621 delete this.options
.opacityWhenUnclustered
;
1631 L
.DistanceGrid = function (cellSize
) {
1632 this._cellSize
= cellSize
;
1633 this._sqCellSize
= cellSize
* cellSize
;
1635 this._objectPoint
= { };
1638 L
.DistanceGrid
.prototype = {
1640 addObject: function (obj
, point
) {
1641 var x
= this._getCoord(point
.x
),
1642 y
= this._getCoord(point
.y
),
1644 row
= grid
[y
] = grid
[y
] || {},
1645 cell
= row
[x
] = row
[x
] || [],
1646 stamp
= L
.Util
.stamp(obj
);
1648 this._objectPoint
[stamp
] = point
;
1653 updateObject: function (obj
, point
) {
1654 this.removeObject(obj
);
1655 this.addObject(obj
, point
);
1658 //Returns true if the object was found
1659 removeObject: function (obj
, point
) {
1660 var x
= this._getCoord(point
.x
),
1661 y
= this._getCoord(point
.y
),
1663 row
= grid
[y
] = grid
[y
] || {},
1664 cell
= row
[x
] = row
[x
] || [],
1667 delete this._objectPoint
[L
.Util
.stamp(obj
)];
1669 for (i
= 0, len
= cell
.length
; i
< len
; i
++) {
1670 if (cell
[i
] === obj
) {
1684 eachObject: function (fn
, context
) {
1685 var i
, j
, k
, len
, row
, cell
, removed
,
1694 for (k
= 0, len
= cell
.length
; k
< len
; k
++) {
1695 removed
= fn
.call(context
, cell
[k
]);
1705 getNearObject: function (point
) {
1706 var x
= this._getCoord(point
.x
),
1707 y
= this._getCoord(point
.y
),
1708 i
, j
, k
, row
, cell
, len
, obj
, dist
,
1709 objectPoint
= this._objectPoint
,
1710 closestDistSq
= this._sqCellSize
,
1713 for (i
= y
- 1; i
<= y
+ 1; i
++) {
1714 row
= this._grid
[i
];
1717 for (j
= x
- 1; j
<= x
+ 1; j
++) {
1721 for (k
= 0, len
= cell
.length
; k
< len
; k
++) {
1723 dist
= this._sqDist(objectPoint
[L
.Util
.stamp(obj
)], point
);
1724 if (dist
< closestDistSq
) {
1725 closestDistSq
= dist
;
1736 _getCoord: function (x
) {
1737 return Math
.floor(x
/ this._cellSize
);
1740 _sqDist: function (p
, p2
) {
1741 var dx
= p2
.x
- p
.x
,
1743 return dx
* dx
+ dy
* dy
;
1748 /* Copyright (c) 2012 the authors listed at the following URL, and/or
1749 the authors of referenced articles or incorporated external code:
1750 http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256
1752 Permission is hereby granted, free of charge, to any person obtaining
1753 a copy of this software and associated documentation files (the
1754 "Software"), to deal in the Software without restriction, including
1755 without limitation the rights to use, copy, modify, merge, publish,
1756 distribute, sublicense, and/or sell copies of the Software, and to
1757 permit persons to whom the Software is furnished to do so, subject to
1758 the following conditions:
1760 The above copyright notice and this permission notice shall be
1761 included in all copies or substantial portions of the Software.
1763 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1764 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1765 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1766 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1767 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1768 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1769 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1771 Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
1778 * @param {Object} cpt a point to be measured from the baseline
1779 * @param {Array} bl the baseline, as represented by a two-element
1780 * array of latlng objects.
1781 * @returns {Number} an approximate distance measure
1783 getDistant: function (cpt
, bl
) {
1784 var vY
= bl
[1].lat
- bl
[0].lat
,
1785 vX
= bl
[0].lng
- bl
[1].lng
;
1786 return (vX
* (cpt
.lat
- bl
[0].lat
) + vY
* (cpt
.lng
- bl
[0].lng
));
1790 * @param {Array} baseLine a two-element array of latlng objects
1791 * representing the baseline to project from
1792 * @param {Array} latLngs an array of latlng objects
1793 * @returns {Object} the maximum point and all new points to stay
1794 * in consideration for the hull.
1796 findMostDistantPointFromBaseLine: function (baseLine
, latLngs
) {
1802 for (i
= latLngs
.length
- 1; i
>= 0; i
--) {
1804 d
= this.getDistant(pt
, baseLine
);
1818 return { maxPoint
: maxPt
, newPoints
: newPoints
};
1823 * Given a baseline, compute the convex hull of latLngs as an array
1826 * @param {Array} latLngs
1829 buildConvexHull: function (baseLine
, latLngs
) {
1830 var convexHullBaseLines
= [],
1831 t
= this.findMostDistantPointFromBaseLine(baseLine
, latLngs
);
1833 if (t
.maxPoint
) { // if there is still a point "outside" the base line
1834 convexHullBaseLines
=
1835 convexHullBaseLines
.concat(
1836 this.buildConvexHull([baseLine
[0], t
.maxPoint
], t
.newPoints
)
1838 convexHullBaseLines
=
1839 convexHullBaseLines
.concat(
1840 this.buildConvexHull([t
.maxPoint
, baseLine
[1]], t
.newPoints
)
1842 return convexHullBaseLines
;
1843 } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull
1844 return [baseLine
[0]];
1849 * Given an array of latlngs, compute a convex hull as an array
1852 * @param {Array} latLngs
1855 getConvexHull: function (latLngs
) {
1856 // find first baseline
1857 var maxLat
= false, minLat
= false,
1858 maxLng
= false, minLng
= false,
1859 maxLatPt
= null, minLatPt
= null,
1860 maxLngPt
= null, minLngPt
= null,
1861 maxPt
= null, minPt
= null,
1864 for (i
= latLngs
.length
- 1; i
>= 0; i
--) {
1865 var pt
= latLngs
[i
];
1866 if (maxLat
=== false || pt
.lat
> maxLat
) {
1870 if (minLat
=== false || pt
.lat
< minLat
) {
1874 if (maxLng
=== false || pt
.lng
> maxLng
) {
1878 if (minLng
=== false || pt
.lng
< minLng
) {
1884 if (minLat
!== maxLat
) {
1892 var ch
= [].concat(this.buildConvexHull([minPt
, maxPt
], latLngs
),
1893 this.buildConvexHull([maxPt
, minPt
], latLngs
));
1899 L
.MarkerCluster
.include({
1900 getConvexHull: function () {
1901 var childMarkers
= this.getAllChildMarkers(),
1905 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
1906 p
= childMarkers
[i
].getLatLng();
1910 return L
.QuickHull
.getConvexHull(points
);
1915 //This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
1916 //Huge thanks to jawj for implementing it first to make my job easy :-)
1918 L
.MarkerCluster
.include({
1921 _circleFootSeparation
: 25, //related to circumference of circle
1922 _circleStartAngle
: Math
.PI
/ 6,
1924 _spiralFootSeparation
: 28, //related to size of spiral (experiment!)
1925 _spiralLengthStart
: 11,
1926 _spiralLengthFactor
: 5,
1928 _circleSpiralSwitchover
: 9, //show spiral instead of circle from this marker count upwards.
1929 // 0 -> always spiral; Infinity -> always circle
1931 spiderfy: function () {
1932 if (this._group
._spiderfied
=== this || this._group
._inZoomAnimation
) {
1936 var childMarkers
= this.getAllChildMarkers(),
1937 group
= this._group
,
1939 center
= map
.latLngToLayerPoint(this._latlng
),
1942 this._group
._unspiderfy();
1943 this._group
._spiderfied
= this;
1945 //TODO Maybe: childMarkers order by distance to center
1947 if (childMarkers
.length
>= this._circleSpiralSwitchover
) {
1948 positions
= this._generatePointsSpiral(childMarkers
.length
, center
);
1950 center
.y
+= 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons.
1951 positions
= this._generatePointsCircle(childMarkers
.length
, center
);
1954 this._animationSpiderfy(childMarkers
, positions
);
1957 unspiderfy: function (zoomDetails
) {
1958 /// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param>
1959 if (this._group
._inZoomAnimation
) {
1962 this._animationUnspiderfy(zoomDetails
);
1964 this._group
._spiderfied
= null;
1967 _generatePointsCircle: function (count
, centerPt
) {
1968 var circumference
= this._group
.options
.spiderfyDistanceMultiplier
* this._circleFootSeparation
* (2 + count
),
1969 legLength
= circumference
/ this._2PI
, //radius from circumference
1970 angleStep
= this._2PI
/ count
,
1976 for (i
= count
- 1; i
>= 0; i
--) {
1977 angle
= this._circleStartAngle
+ i
* angleStep
;
1978 res
[i
] = new L
.Point(centerPt
.x
+ legLength
* Math
.cos(angle
), centerPt
.y
+ legLength
* Math
.sin(angle
))._round();
1984 _generatePointsSpiral: function (count
, centerPt
) {
1985 var spiderfyDistanceMultiplier
= this._group
.options
.spiderfyDistanceMultiplier
,
1986 legLength
= spiderfyDistanceMultiplier
* this._spiralLengthStart
,
1987 separation
= spiderfyDistanceMultiplier
* this._spiralFootSeparation
,
1988 lengthFactor
= spiderfyDistanceMultiplier
* this._spiralLengthFactor
* this._2PI
,
1995 // Higher index, closer position to cluster center.
1996 for (i
= count
- 1; i
>= 0; i
--) {
1997 angle
+= separation
/ legLength
+ i
* 0.0005;
1998 res
[i
] = new L
.Point(centerPt
.x
+ legLength
* Math
.cos(angle
), centerPt
.y
+ legLength
* Math
.sin(angle
))._round();
1999 legLength
+= lengthFactor
/ angle
;
2004 _noanimationUnspiderfy: function () {
2005 var group
= this._group
,
2007 fg
= group
._featureGroup
,
2008 childMarkers
= this.getAllChildMarkers(),
2012 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
2013 m
= childMarkers
[i
];
2017 if (m
._preSpiderfyLatlng
) {
2018 m
.setLatLng(m
._preSpiderfyLatlng
);
2019 delete m
._preSpiderfyLatlng
;
2021 if (m
.setZIndexOffset
) {
2022 m
.setZIndexOffset(0);
2026 map
.removeLayer(m
._spiderLeg
);
2027 delete m
._spiderLeg
;
2031 group
.fire('unspiderfied', {
2033 markers
: childMarkers
2035 group
._spiderfied
= null;
2039 //Non Animated versions of everything
2040 L
.MarkerClusterNonAnimated
= L
.MarkerCluster
.extend({
2041 _animationSpiderfy: function (childMarkers
, positions
) {
2042 var group
= this._group
,
2044 fg
= group
._featureGroup
,
2045 legOptions
= this._group
.options
.spiderLegPolylineOptions
,
2048 // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
2049 // The reverse order trick no longer improves performance on modern browsers.
2050 for (i
= 0; i
< childMarkers
.length
; i
++) {
2051 newPos
= map
.layerPointToLatLng(positions
[i
]);
2052 m
= childMarkers
[i
];
2054 // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
2055 leg
= new L
.Polyline([this._latlng
, newPos
], legOptions
);
2059 // Now add the marker.
2060 m
._preSpiderfyLatlng
= m
._latlng
;
2061 m
.setLatLng(newPos
);
2062 if (m
.setZIndexOffset
) {
2063 m
.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
2068 this.setOpacity(0.3);
2069 group
.fire('spiderfied', {
2071 markers
: childMarkers
2075 _animationUnspiderfy: function () {
2076 this._noanimationUnspiderfy();
2080 //Animated versions here
2081 L
.MarkerCluster
.include({
2083 _animationSpiderfy: function (childMarkers
, positions
) {
2085 group
= this._group
,
2087 fg
= group
._featureGroup
,
2088 thisLayerLatLng
= this._latlng
,
2089 thisLayerPos
= map
.latLngToLayerPoint(thisLayerLatLng
),
2091 legOptions
= L
.extend({}, this._group
.options
.spiderLegPolylineOptions
), // Copy the options so that we can modify them for animation.
2092 finalLegOpacity
= legOptions
.opacity
,
2093 i
, m
, leg
, legPath
, legLength
, newPos
;
2095 if (finalLegOpacity
=== undefined) {
2096 finalLegOpacity
= L
.MarkerClusterGroup
.prototype.options
.spiderLegPolylineOptions
.opacity
;
2100 // If the initial opacity of the spider leg is not 0 then it appears before the animation starts.
2101 legOptions
.opacity
= 0;
2103 // Add the class for CSS transitions.
2104 legOptions
.className
= (legOptions
.className
|| '') + ' leaflet-cluster-spider-leg';
2106 // Make sure we have a defined opacity.
2107 legOptions
.opacity
= finalLegOpacity
;
2110 // Add markers and spider legs to map, hidden at our center point.
2111 // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
2112 // The reverse order trick no longer improves performance on modern browsers.
2113 for (i
= 0; i
< childMarkers
.length
; i
++) {
2114 m
= childMarkers
[i
];
2116 newPos
= map
.layerPointToLatLng(positions
[i
]);
2118 // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
2119 leg
= new L
.Polyline([thisLayerLatLng
, newPos
], legOptions
);
2123 // Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/
2124 // In our case the transition property is declared in the CSS file.
2126 legPath
= leg
._path
;
2127 legLength
= legPath
.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox.
2128 legPath
.style
.strokeDasharray
= legLength
; // Just 1 length is enough, it will be duplicated.
2129 legPath
.style
.strokeDashoffset
= legLength
;
2132 // If it is a marker, add it now and we'll animate it out
2133 if (m
.setZIndexOffset
) {
2134 m
.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING
2136 if (m
.clusterHide
) {
2140 // Vectors just get immediately added
2144 m
._setPos(thisLayerPos
);
2148 group
._forceLayout();
2149 group
._animationStart();
2151 // Reveal markers and spider legs.
2152 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
2153 newPos
= map
.layerPointToLatLng(positions
[i
]);
2154 m
= childMarkers
[i
];
2156 //Move marker to new position
2157 m
._preSpiderfyLatlng
= m
._latlng
;
2158 m
.setLatLng(newPos
);
2160 if (m
.clusterShow
) {
2164 // Animate leg (animation is actually delegated to CSS transition).
2167 legPath
= leg
._path
;
2168 legPath
.style
.strokeDashoffset
= 0;
2169 //legPath.style.strokeOpacity = finalLegOpacity;
2170 leg
.setStyle({opacity
: finalLegOpacity
});
2173 this.setOpacity(0.3);
2175 setTimeout(function () {
2176 group
._animationEnd();
2177 group
.fire('spiderfied', {
2179 markers
: childMarkers
2184 _animationUnspiderfy: function (zoomDetails
) {
2186 group
= this._group
,
2188 fg
= group
._featureGroup
,
2189 thisLayerPos
= zoomDetails
? map
._latLngToNewLayerPoint(this._latlng
, zoomDetails
.zoom
, zoomDetails
.center
) : map
.latLngToLayerPoint(this._latlng
),
2190 childMarkers
= this.getAllChildMarkers(),
2192 m
, i
, leg
, legPath
, legLength
, nonAnimatable
;
2194 group
._animationStart();
2196 //Make us visible and bring the child markers back in
2198 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
2199 m
= childMarkers
[i
];
2201 //Marker was added to us after we were spiderfied
2202 if (!m
._preSpiderfyLatlng
) {
2206 //Fix up the location to the real one
2207 m
.setLatLng(m
._preSpiderfyLatlng
);
2208 delete m
._preSpiderfyLatlng
;
2210 //Hack override the location to be our center
2211 nonAnimatable
= true;
2213 m
._setPos(thisLayerPos
);
2214 nonAnimatable
= false;
2216 if (m
.clusterHide
) {
2218 nonAnimatable
= false;
2220 if (nonAnimatable
) {
2224 // Animate the spider leg back in (animation is actually delegated to CSS transition).
2227 legPath
= leg
._path
;
2228 legLength
= legPath
.getTotalLength() + 0.1;
2229 legPath
.style
.strokeDashoffset
= legLength
;
2230 leg
.setStyle({opacity
: 0});
2234 setTimeout(function () {
2235 //If we have only <= one child left then that marker will be shown on the map so don't remove it!
2236 var stillThereChildCount
= 0;
2237 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
2238 m
= childMarkers
[i
];
2240 stillThereChildCount
++;
2245 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
2246 m
= childMarkers
[i
];
2248 if (!m
._spiderLeg
) { //Has already been unspiderfied
2252 if (m
.clusterShow
) {
2255 if (m
.setZIndexOffset
) {
2256 m
.setZIndexOffset(0);
2259 if (stillThereChildCount
> 1) {
2263 map
.removeLayer(m
._spiderLeg
);
2264 delete m
._spiderLeg
;
2266 group
._animationEnd();
2267 group
.fire('unspiderfied', {
2269 markers
: childMarkers
2276 L
.MarkerClusterGroup
.include({
2277 //The MarkerCluster currently spiderfied (if any)
2280 _spiderfierOnAdd: function () {
2281 this._map
.on('click', this._unspiderfyWrapper
, this);
2283 if (this._map
.options
.zoomAnimation
) {
2284 this._map
.on('zoomstart', this._unspiderfyZoomStart
, this);
2286 //Browsers without zoomAnimation or a big zoom don't fire zoomstart
2287 this._map
.on('zoomend', this._noanimationUnspiderfy
, this);
2290 _spiderfierOnRemove: function () {
2291 this._map
.off('click', this._unspiderfyWrapper
, this);
2292 this._map
.off('zoomstart', this._unspiderfyZoomStart
, this);
2293 this._map
.off('zoomanim', this._unspiderfyZoomAnim
, this);
2294 this._map
.off('zoomend', this._noanimationUnspiderfy
, this);
2296 //Ensure that markers are back where they should be
2297 // Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane
2298 this._noanimationUnspiderfy();
2301 //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)
2302 //This means we can define the animation they do rather than Markers doing an animation to their actual location
2303 _unspiderfyZoomStart: function () {
2304 if (!this._map
) { //May have been removed from the map by a zoomEnd handler
2308 this._map
.on('zoomanim', this._unspiderfyZoomAnim
, this);
2311 _unspiderfyZoomAnim: function (zoomDetails
) {
2312 //Wait until the first zoomanim after the user has finished touch-zooming before running the animation
2313 if (L
.DomUtil
.hasClass(this._map
._mapPane
, 'leaflet-touching')) {
2317 this._map
.off('zoomanim', this._unspiderfyZoomAnim
, this);
2318 this._unspiderfy(zoomDetails
);
2321 _unspiderfyWrapper: function () {
2322 /// <summary>_unspiderfy but passes no arguments</summary>
2326 _unspiderfy: function (zoomDetails
) {
2327 if (this._spiderfied
) {
2328 this._spiderfied
.unspiderfy(zoomDetails
);
2332 _noanimationUnspiderfy: function () {
2333 if (this._spiderfied
) {
2334 this._spiderfied
._noanimationUnspiderfy();
2338 //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc
2339 _unspiderfyLayer: function (layer
) {
2340 if (layer
._spiderLeg
) {
2341 this._featureGroup
.removeLayer(layer
);
2343 if (layer
.clusterShow
) {
2344 layer
.clusterShow();
2346 //Position will be fixed up immediately in _animationUnspiderfy
2347 if (layer
.setZIndexOffset
) {
2348 layer
.setZIndexOffset(0);
2351 this._map
.removeLayer(layer
._spiderLeg
);
2352 delete layer
._spiderLeg
;
2359 * Adds 1 public method to MCG and 1 to L.Marker to facilitate changing
2360 * markers' icon options and refreshing their icon and their parent clusters
2361 * accordingly (case where their iconCreateFunction uses data of childMarkers
2362 * to make up the cluster icon).
2366 L
.MarkerClusterGroup
.include({
2368 * Updates the icon of all clusters which are parents of the given marker(s).
2369 * In singleMarkerMode, also updates the given marker(s) icon.
2370 * @param layers L.MarkerClusterGroup|L.LayerGroup|Array(L.Marker)|Map(L.Marker)|
2371 * L.MarkerCluster|L.Marker (optional) list of markers (or single marker) whose parent
2372 * clusters need to be updated. If not provided, retrieves all child markers of this.
2373 * @returns {L.MarkerClusterGroup}
2375 refreshClusters: function (layers
) {
2377 layers
= this._topClusterLevel
.getAllChildMarkers();
2378 } else if (layers
instanceof L
.MarkerClusterGroup
) {
2379 layers
= layers
._topClusterLevel
.getAllChildMarkers();
2380 } else if (layers
instanceof L
.LayerGroup
) {
2381 layers
= layers
._layers
;
2382 } else if (layers
instanceof L
.MarkerCluster
) {
2383 layers
= layers
.getAllChildMarkers();
2384 } else if (layers
instanceof L
.Marker
) {
2386 } // else: must be an Array(L.Marker)|Map(L.Marker)
2387 this._flagParentsIconsNeedUpdate(layers
);
2388 this._refreshClustersIcons();
2390 // In case of singleMarkerMode, also re-draw the markers.
2391 if (this.options
.singleMarkerMode
) {
2392 this._refreshSingleMarkerModeMarkers(layers
);
2399 * Simply flags all parent clusters of the given markers as having a "dirty" icon.
2400 * @param layers Array(L.Marker)|Map(L.Marker) list of markers.
2403 _flagParentsIconsNeedUpdate: function (layers
) {
2406 // Assumes layers is an Array or an Object whose prototype is non-enumerable.
2407 for (id
in layers
) {
2408 // Flag parent clusters' icon as "dirty", all the way up.
2409 // Dumb process that flags multiple times upper parents, but still
2410 // much more efficient than trying to be smart and make short lists,
2411 // at least in the case of a hierarchy following a power law:
2412 // http://jsperf.com/flag-nodes-in-power-hierarchy/2
2413 parent
= layers
[id
].__parent
;
2415 parent
._iconNeedsUpdate
= true;
2416 parent
= parent
.__parent
;
2422 * Refreshes the icon of all "dirty" visible clusters.
2423 * Non-visible "dirty" clusters will be updated when they are added to the map.
2426 _refreshClustersIcons: function () {
2427 this._featureGroup
.eachLayer(function (c
) {
2428 if (c
instanceof L
.MarkerCluster
&& c
._iconNeedsUpdate
) {
2435 * Re-draws the icon of the supplied markers.
2436 * To be used in singleMarkerMode only.
2437 * @param layers Array(L.Marker)|Map(L.Marker) list of markers.
2440 _refreshSingleMarkerModeMarkers: function (layers
) {
2443 for (id
in layers
) {
2446 // Make sure we do not override markers that do not belong to THIS group.
2447 if (this.hasLayer(layer
)) {
2448 // Need to re-create the icon first, then re-draw the marker.
2449 layer
.setIcon(this._overrideMarkerIcon(layer
));
2457 * Updates the given options in the marker's icon and refreshes the marker.
2458 * @param options map object of icon options.
2459 * @param directlyRefreshClusters boolean (optional) true to trigger
2460 * MCG.refreshClustersOf() right away with this single marker.
2461 * @returns {L.Marker}
2463 refreshIconOptions: function (options
, directlyRefreshClusters
) {
2464 var icon
= this.options
.icon
;
2466 L
.setOptions(icon
, options
);
2470 // Shortcut to refresh the associated MCG clusters right away.
2471 // To be used when refreshing a single marker.
2472 // Otherwise, better use MCG.refreshClusters() once at the end with
2473 // the list of modified markers.
2474 if (directlyRefreshClusters
&& this.__parent
) {
2475 this.__parent
._group
.refreshClusters(this);
2483 }(window
, document
));