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 //Whether to animate adding markers after adding the MarkerClusterGroup to the map
28 // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
29 animateAddingMarkers
: false,
31 //Increase to increase the distance away that spiderfied markers appear from the center
32 spiderfyDistanceMultiplier
: 1,
34 // 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
35 chunkedLoading
: false,
36 chunkInterval
: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback)
37 chunkDelay
: 50, // at the end of each interval, give n milliseconds back to system/browser
38 chunkProgress
: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator)
40 //Options to pass to the L.Polygon constructor
44 initialize: function (options
) {
45 L
.Util
.setOptions(this, options
);
46 if (!this.options
.iconCreateFunction
) {
47 this.options
.iconCreateFunction
= this._defaultIconCreateFunction
;
50 this._featureGroup
= L
.featureGroup();
51 this._featureGroup
.on(L
.FeatureGroup
.EVENTS
, this._propagateEvent
, this);
53 this._nonPointGroup
= L
.featureGroup();
54 this._nonPointGroup
.on(L
.FeatureGroup
.EVENTS
, this._propagateEvent
, this);
56 this._inZoomAnimation
= 0;
57 this._needsClustering
= [];
58 this._needsRemoving
= []; //Markers removed while we aren't on the map need to be kept track of
59 //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
60 this._currentShownBounds
= null;
65 addLayer: function (layer
) {
67 if (layer
instanceof L
.LayerGroup
) {
69 for (var i
in layer
._layers
) {
70 array
.push(layer
._layers
[i
]);
72 return this.addLayers(array
);
75 //Don't cluster non point data
76 if (!layer
.getLatLng
) {
77 this._nonPointGroup
.addLayer(layer
);
82 this._needsClustering
.push(layer
);
86 if (this.hasLayer(layer
)) {
91 //If we have already clustered we'll need to add this one to a cluster
93 if (this._unspiderfy
) {
97 this._addLayer(layer
, this._maxZoom
);
99 //Work out what is visible
100 var visibleLayer
= layer
,
101 currentZoom
= this._map
.getZoom();
102 if (layer
.__parent
) {
103 while (visibleLayer
.__parent
._zoom
>= currentZoom
) {
104 visibleLayer
= visibleLayer
.__parent
;
108 if (this._currentShownBounds
.contains(visibleLayer
.getLatLng())) {
109 if (this.options
.animateAddingMarkers
) {
110 this._animationAddLayer(layer
, visibleLayer
);
112 this._animationAddLayerNonAnimated(layer
, visibleLayer
);
118 removeLayer: function (layer
) {
120 if (layer
instanceof L
.LayerGroup
)
123 for (var i
in layer
._layers
) {
124 array
.push(layer
._layers
[i
]);
126 return this.removeLayers(array
);
130 if (!layer
.getLatLng
) {
131 this._nonPointGroup
.removeLayer(layer
);
136 if (!this._arraySplice(this._needsClustering
, layer
) && this.hasLayer(layer
)) {
137 this._needsRemoving
.push(layer
);
142 if (!layer
.__parent
) {
146 if (this._unspiderfy
) {
148 this._unspiderfyLayer(layer
);
151 //Remove the marker from clusters
152 this._removeLayer(layer
, true);
154 if (this._featureGroup
.hasLayer(layer
)) {
155 this._featureGroup
.removeLayer(layer
);
156 if (layer
.setOpacity
) {
164 //Takes an array of markers and adds them in bulk
165 addLayers: function (layersArray
) {
166 var fg
= this._featureGroup
,
167 npg
= this._nonPointGroup
,
168 chunked
= this.options
.chunkedLoading
,
169 chunkInterval
= this.options
.chunkInterval
,
170 chunkProgress
= this.options
.chunkProgress
,
175 started
= (new Date()).getTime();
176 var process
= L
.bind(function () {
177 var start
= (new Date()).getTime();
178 for (; offset
< layersArray
.length
; offset
++) {
179 if (chunked
&& offset
% 200 === 0) {
180 // every couple hundred markers, instrument the time elapsed since processing started:
181 var elapsed
= (new Date()).getTime() - start
;
182 if (elapsed
> chunkInterval
) {
183 break; // been working too hard, time to take a break :-)
187 m
= layersArray
[offset
];
189 //Not point data, can't be clustered
195 if (this.hasLayer(m
)) {
199 this._addLayer(m
, this._maxZoom
);
201 //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
203 if (m
.__parent
.getChildCount() === 2) {
204 var markers
= m
.__parent
.getAllChildMarkers(),
205 otherMarker
= markers
[0] === m
? markers
[1] : markers
[0];
206 fg
.removeLayer(otherMarker
);
212 // report progress and time elapsed:
213 chunkProgress(offset
, layersArray
.length
, (new Date()).getTime() - started
);
216 if (offset
=== layersArray
.length
) {
217 //Update the icons of all those visible clusters that were affected
218 this._featureGroup
.eachLayer(function (c
) {
219 if (c
instanceof L
.MarkerCluster
&& c
._iconNeedsUpdate
) {
224 this._topClusterLevel
._recursivelyAddChildrenToMap(null, this._zoom
, this._currentShownBounds
);
226 setTimeout(process
, this.options
.chunkDelay
);
233 for (i
= 0, l
= layersArray
.length
; i
< l
; i
++) {
236 //Not point data, can't be clustered
242 if (this.hasLayer(m
)) {
248 this._needsClustering
= this._needsClustering
.concat(newMarkers
);
253 //Takes an array of markers and removes them in bulk
254 removeLayers: function (layersArray
) {
256 fg
= this._featureGroup
,
257 npg
= this._nonPointGroup
;
260 for (i
= 0, l
= layersArray
.length
; i
< l
; i
++) {
262 this._arraySplice(this._needsClustering
, m
);
268 for (i
= 0, l
= layersArray
.length
; i
< l
; i
++) {
276 this._removeLayer(m
, true, true);
278 if (fg
.hasLayer(m
)) {
286 //Fix up the clusters and markers on the map
287 this._topClusterLevel
._recursivelyAddChildrenToMap(null, this._zoom
, this._currentShownBounds
);
289 fg
.eachLayer(function (c
) {
290 if (c
instanceof L
.MarkerCluster
) {
298 //Removes all layers from the MarkerClusterGroup
299 clearLayers: function () {
300 //Need our own special implementation as the LayerGroup one doesn't work for us
302 //If we aren't on the map (yet), blow away the markers we know of
304 this._needsClustering
= [];
305 delete this._gridClusters
;
306 delete this._gridUnclustered
;
309 if (this._noanimationUnspiderfy
) {
310 this._noanimationUnspiderfy();
313 //Remove all the visible layers
314 this._featureGroup
.clearLayers();
315 this._nonPointGroup
.clearLayers();
317 this.eachLayer(function (marker
) {
318 delete marker
.__parent
;
322 //Reset _topClusterLevel and the DistanceGrids
323 this._generateInitialClusters();
329 //Override FeatureGroup.getBounds as it doesn't work
330 getBounds: function () {
331 var bounds
= new L
.LatLngBounds();
333 if (this._topClusterLevel
) {
334 bounds
.extend(this._topClusterLevel
._bounds
);
337 for (var i
= this._needsClustering
.length
- 1; i
>= 0; i
--) {
338 bounds
.extend(this._needsClustering
[i
].getLatLng());
341 bounds
.extend(this._nonPointGroup
.getBounds());
346 //Overrides LayerGroup.eachLayer
347 eachLayer: function (method
, context
) {
348 var markers
= this._needsClustering
.slice(),
351 if (this._topClusterLevel
) {
352 this._topClusterLevel
.getAllChildMarkers(markers
);
355 for (i
= markers
.length
- 1; i
>= 0; i
--) {
356 method
.call(context
, markers
[i
]);
359 this._nonPointGroup
.eachLayer(method
, context
);
362 //Overrides LayerGroup.getLayers
363 getLayers: function () {
365 this.eachLayer(function (l
) {
371 //Overrides LayerGroup.getLayer, WARNING: Really bad performance
372 getLayer: function (id
) {
375 this.eachLayer(function (l
) {
376 if (L
.stamp(l
) === id
) {
384 //Returns true if the given layer is in this MarkerClusterGroup
385 hasLayer: function (layer
) {
390 var i
, anArray
= this._needsClustering
;
392 for (i
= anArray
.length
- 1; i
>= 0; i
--) {
393 if (anArray
[i
] === layer
) {
398 anArray
= this._needsRemoving
;
399 for (i
= anArray
.length
- 1; i
>= 0; i
--) {
400 if (anArray
[i
] === layer
) {
405 return !!(layer
.__parent
&& layer
.__parent
._group
=== this) || this._nonPointGroup
.hasLayer(layer
);
408 //Zoom down to show the given layer (spiderfying if necessary) then calls the callback
409 zoomToShowLayer: function (layer
, callback
) {
411 var showMarker = function () {
412 if ((layer
._icon
|| layer
.__parent
._icon
) && !this._inZoomAnimation
) {
413 this._map
.off('moveend', showMarker
, this);
414 this.off('animationend', showMarker
, this);
418 } else if (layer
.__parent
._icon
) {
419 var afterSpiderfy = function () {
420 this.off('spiderfied', afterSpiderfy
, this);
424 this.on('spiderfied', afterSpiderfy
, this);
425 layer
.__parent
.spiderfy();
430 if (layer
._icon
&& this._map
.getBounds().contains(layer
.getLatLng())) {
431 //Layer is visible ond on screen, immediate return
433 } else if (layer
.__parent
._zoom
< this._map
.getZoom()) {
434 //Layer should be visible at this zoom level. It must not be on screen so just pan over to it
435 this._map
.on('moveend', showMarker
, this);
436 this._map
.panTo(layer
.getLatLng());
438 var moveStart = function () {
439 this._map
.off('movestart', moveStart
, this);
443 this._map
.on('movestart', moveStart
, this);
444 this._map
.on('moveend', showMarker
, this);
445 this.on('animationend', showMarker
, this);
446 layer
.__parent
.zoomToBounds();
449 //Never started moving, must already be there, probably need clustering however
450 showMarker
.call(this);
455 //Overrides FeatureGroup.onAdd
456 onAdd: function (map
) {
460 if (!isFinite(this._map
.getMaxZoom())) {
461 throw "Map has no maxZoom specified";
464 this._featureGroup
.onAdd(map
);
465 this._nonPointGroup
.onAdd(map
);
467 if (!this._gridClusters
) {
468 this._generateInitialClusters();
471 for (i
= 0, l
= this._needsRemoving
.length
; i
< l
; i
++) {
472 layer
= this._needsRemoving
[i
];
473 this._removeLayer(layer
, true);
475 this._needsRemoving
= [];
477 //Remember the current zoom level and bounds
478 this._zoom
= this._map
.getZoom();
479 this._currentShownBounds
= this._getExpandedVisibleBounds();
481 this._map
.on('zoomend', this._zoomEnd
, this);
482 this._map
.on('moveend', this._moveEnd
, this);
484 if (this._spiderfierOnAdd
) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
485 this._spiderfierOnAdd();
490 //Actually add our markers to the map:
491 l
= this._needsClustering
;
492 this._needsClustering
= [];
496 //Overrides FeatureGroup.onRemove
497 onRemove: function (map
) {
498 map
.off('zoomend', this._zoomEnd
, this);
499 map
.off('moveend', this._moveEnd
, this);
501 this._unbindEvents();
503 //In case we are in a cluster animation
504 this._map
._mapPane
.className
= this._map
._mapPane
.className
.replace(' leaflet-cluster-anim', '');
506 if (this._spiderfierOnRemove
) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
507 this._spiderfierOnRemove();
512 //Clean up all the layers we added to the map
513 this._hideCoverage();
514 this._featureGroup
.onRemove(map
);
515 this._nonPointGroup
.onRemove(map
);
517 this._featureGroup
.clearLayers();
522 getVisibleParent: function (marker
) {
523 var vMarker
= marker
;
524 while (vMarker
&& !vMarker
._icon
) {
525 vMarker
= vMarker
.__parent
;
527 return vMarker
|| null;
530 //Remove the given object from the given array
531 _arraySplice: function (anArray
, obj
) {
532 for (var i
= anArray
.length
- 1; i
>= 0; i
--) {
533 if (anArray
[i
] === obj
) {
534 anArray
.splice(i
, 1);
540 //Internal function for removing a marker from everything.
541 //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
542 _removeLayer: function (marker
, removeFromDistanceGrid
, dontUpdateMap
) {
543 var gridClusters
= this._gridClusters
,
544 gridUnclustered
= this._gridUnclustered
,
545 fg
= this._featureGroup
,
548 //Remove the marker from distance clusters it might be in
549 if (removeFromDistanceGrid
) {
550 for (var z
= this._maxZoom
; z
>= 0; z
--) {
551 if (!gridUnclustered
[z
].removeObject(marker
, map
.project(marker
.getLatLng(), z
))) {
557 //Work our way up the clusters removing them as we go if required
558 var cluster
= marker
.__parent
,
559 markers
= cluster
._markers
,
562 //Remove the marker from the immediate parents marker list
563 this._arraySplice(markers
, marker
);
566 cluster
._childCount
--;
568 if (cluster
._zoom
< 0) {
569 //Top level, do nothing
571 } else if (removeFromDistanceGrid
&& cluster
._childCount
<= 1) { //Cluster no longer required
572 //We need to push the other marker up to the parent
573 otherMarker
= cluster
._markers
[0] === marker
? cluster
._markers
[1] : cluster
._markers
[0];
575 //Update distance grid
576 gridClusters
[cluster
._zoom
].removeObject(cluster
, map
.project(cluster
._cLatLng
, cluster
._zoom
));
577 gridUnclustered
[cluster
._zoom
].addObject(otherMarker
, map
.project(otherMarker
.getLatLng(), cluster
._zoom
));
579 //Move otherMarker up to parent
580 this._arraySplice(cluster
.__parent
._childClusters
, cluster
);
581 cluster
.__parent
._markers
.push(otherMarker
);
582 otherMarker
.__parent
= cluster
.__parent
;
585 //Cluster is currently on the map, need to put the marker on the map instead
586 fg
.removeLayer(cluster
);
587 if (!dontUpdateMap
) {
588 fg
.addLayer(otherMarker
);
592 cluster
._recalculateBounds();
593 if (!dontUpdateMap
|| !cluster
._icon
) {
594 cluster
._updateIcon();
598 cluster
= cluster
.__parent
;
601 delete marker
.__parent
;
604 _isOrIsParent: function (el
, oel
) {
609 oel
= oel
.parentNode
;
614 _propagateEvent: function (e
) {
615 if (e
.layer
instanceof L
.MarkerCluster
) {
616 //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget)
617 if (e
.originalEvent
&& this._isOrIsParent(e
.layer
._icon
, e
.originalEvent
.relatedTarget
)) {
620 e
.type
= 'cluster' + e
.type
;
623 this.fire(e
.type
, e
);
626 //Default functionality
627 _defaultIconCreateFunction: function (cluster
) {
628 var childCount
= cluster
.getChildCount();
630 var c
= ' marker-cluster-';
631 if (childCount
< 10) {
633 } else if (childCount
< 100) {
639 return new L
.DivIcon({ html
: '<div><span>' + childCount
+ '</span></div>', className
: 'marker-cluster' + c
, iconSize
: new L
.Point(40, 40) });
642 _bindEvents: function () {
644 spiderfyOnMaxZoom
= this.options
.spiderfyOnMaxZoom
,
645 showCoverageOnHover
= this.options
.showCoverageOnHover
,
646 zoomToBoundsOnClick
= this.options
.zoomToBoundsOnClick
;
648 //Zoom on cluster click or spiderfy if we are at the lowest level
649 if (spiderfyOnMaxZoom
|| zoomToBoundsOnClick
) {
650 this.on('clusterclick', this._zoomOrSpiderfy
, this);
653 //Show convex hull (boundary) polygon on mouse over
654 if (showCoverageOnHover
) {
655 this.on('clustermouseover', this._showCoverage
, this);
656 this.on('clustermouseout', this._hideCoverage
, this);
657 map
.on('zoomend', this._hideCoverage
, this);
661 _zoomOrSpiderfy: function (e
) {
663 if (map
.getMaxZoom() === map
.getZoom()) {
664 if (this.options
.spiderfyOnMaxZoom
) {
667 } else if (this.options
.zoomToBoundsOnClick
) {
668 e
.layer
.zoomToBounds();
671 // Focus the map again for keyboard users.
672 if (e
.originalEvent
&& e
.originalEvent
.keyCode
=== 13) {
673 map
._container
.focus();
677 _showCoverage: function (e
) {
679 if (this._inZoomAnimation
) {
682 if (this._shownPolygon
) {
683 map
.removeLayer(this._shownPolygon
);
685 if (e
.layer
.getChildCount() > 2 && e
.layer
!== this._spiderfied
) {
686 this._shownPolygon
= new L
.Polygon(e
.layer
.getConvexHull(), this.options
.polygonOptions
);
687 map
.addLayer(this._shownPolygon
);
691 _hideCoverage: function () {
692 if (this._shownPolygon
) {
693 this._map
.removeLayer(this._shownPolygon
);
694 this._shownPolygon
= null;
698 _unbindEvents: function () {
699 var spiderfyOnMaxZoom
= this.options
.spiderfyOnMaxZoom
,
700 showCoverageOnHover
= this.options
.showCoverageOnHover
,
701 zoomToBoundsOnClick
= this.options
.zoomToBoundsOnClick
,
704 if (spiderfyOnMaxZoom
|| zoomToBoundsOnClick
) {
705 this.off('clusterclick', this._zoomOrSpiderfy
, this);
707 if (showCoverageOnHover
) {
708 this.off('clustermouseover', this._showCoverage
, this);
709 this.off('clustermouseout', this._hideCoverage
, this);
710 map
.off('zoomend', this._hideCoverage
, this);
714 _zoomEnd: function () {
715 if (!this._map
) { //May have been removed from the map by a zoomEnd handler
718 this._mergeSplitClusters();
720 this._zoom
= this._map
._zoom
;
721 this._currentShownBounds
= this._getExpandedVisibleBounds();
724 _moveEnd: function () {
725 if (this._inZoomAnimation
) {
729 var newBounds
= this._getExpandedVisibleBounds();
731 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, this._zoom
, newBounds
);
732 this._topClusterLevel
._recursivelyAddChildrenToMap(null, this._map
._zoom
, newBounds
);
734 this._currentShownBounds
= newBounds
;
738 _generateInitialClusters: function () {
739 var maxZoom
= this._map
.getMaxZoom(),
740 radius
= this.options
.maxClusterRadius
,
743 //If we just set maxClusterRadius to a single number, we need to create
744 //a simple function to return that number. Otherwise, we just have to
745 //use the function we've passed in.
746 if (typeof radius
!== "function") {
747 radiusFn = function () { return radius
; };
750 if (this.options
.disableClusteringAtZoom
) {
751 maxZoom
= this.options
.disableClusteringAtZoom
- 1;
753 this._maxZoom
= maxZoom
;
754 this._gridClusters
= {};
755 this._gridUnclustered
= {};
757 //Set up DistanceGrids for each zoom
758 for (var zoom
= maxZoom
; zoom
>= 0; zoom
--) {
759 this._gridClusters
[zoom
] = new L
.DistanceGrid(radiusFn(zoom
));
760 this._gridUnclustered
[zoom
] = new L
.DistanceGrid(radiusFn(zoom
));
763 this._topClusterLevel
= new L
.MarkerCluster(this, -1);
766 //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
767 _addLayer: function (layer
, zoom
) {
768 var gridClusters
= this._gridClusters
,
769 gridUnclustered
= this._gridUnclustered
,
772 if (this.options
.singleMarkerMode
) {
773 layer
.options
.icon
= this.options
.iconCreateFunction({
774 getChildCount: function () {
777 getAllChildMarkers: function () {
783 //Find the lowest zoom level to slot this one in
784 for (; zoom
>= 0; zoom
--) {
785 markerPoint
= this._map
.project(layer
.getLatLng(), zoom
); // calculate pixel position
787 //Try find a cluster close by
788 var closest
= gridClusters
[zoom
].getNearObject(markerPoint
);
790 closest
._addChild(layer
);
791 layer
.__parent
= closest
;
795 //Try find a marker close by to form a new cluster with
796 closest
= gridUnclustered
[zoom
].getNearObject(markerPoint
);
798 var parent
= closest
.__parent
;
800 this._removeLayer(closest
, false);
803 //Create new cluster with these 2 in it
805 var newCluster
= new L
.MarkerCluster(this, zoom
, closest
, layer
);
806 gridClusters
[zoom
].addObject(newCluster
, this._map
.project(newCluster
._cLatLng
, zoom
));
807 closest
.__parent
= newCluster
;
808 layer
.__parent
= newCluster
;
810 //First create any new intermediate parent clusters that don't exist
811 var lastParent
= newCluster
;
812 for (z
= zoom
- 1; z
> parent
._zoom
; z
--) {
813 lastParent
= new L
.MarkerCluster(this, z
, lastParent
);
814 gridClusters
[z
].addObject(lastParent
, this._map
.project(closest
.getLatLng(), z
));
816 parent
._addChild(lastParent
);
818 //Remove closest from this zoom level and any above that it is in, replace with newCluster
819 for (z
= zoom
; z
>= 0; z
--) {
820 if (!gridUnclustered
[z
].removeObject(closest
, this._map
.project(closest
.getLatLng(), z
))) {
828 //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
829 gridUnclustered
[zoom
].addObject(layer
, markerPoint
);
832 //Didn't get in anything, add us to the top
833 this._topClusterLevel
._addChild(layer
);
834 layer
.__parent
= this._topClusterLevel
;
838 //Enqueue code to fire after the marker expand/contract has happened
839 _enqueue: function (fn
) {
840 this._queue
.push(fn
);
841 if (!this._queueTimeout
) {
842 this._queueTimeout
= setTimeout(L
.bind(this._processQueue
, this), 300);
845 _processQueue: function () {
846 for (var i
= 0; i
< this._queue
.length
; i
++) {
847 this._queue
[i
].call(this);
849 this._queue
.length
= 0;
850 clearTimeout(this._queueTimeout
);
851 this._queueTimeout
= null;
854 //Merge and split any existing clusters that are too big or small
855 _mergeSplitClusters: function () {
857 //Incase we are starting to split before the animation finished
858 this._processQueue();
860 if (this._zoom
< this._map
._zoom
&& this._currentShownBounds
.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split
861 this._animationStart();
862 //Remove clusters now off screen
863 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, this._zoom
, this._getExpandedVisibleBounds());
865 this._animationZoomIn(this._zoom
, this._map
._zoom
);
867 } else if (this._zoom
> this._map
._zoom
) { //Zoom out, merge
868 this._animationStart();
870 this._animationZoomOut(this._zoom
, this._map
._zoom
);
876 //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)
877 _getExpandedVisibleBounds: function () {
878 if (!this.options
.removeOutsideVisibleBounds
) {
879 return this._map
.getBounds();
883 bounds
= map
.getBounds(),
884 sw
= bounds
._southWest
,
885 ne
= bounds
._northEast
,
886 latDiff
= L
.Browser
.mobile
? 0 : Math
.abs(sw
.lat
- ne
.lat
),
887 lngDiff
= L
.Browser
.mobile
? 0 : Math
.abs(sw
.lng
- ne
.lng
);
889 return new L
.LatLngBounds(
890 new L
.LatLng(sw
.lat
- latDiff
, sw
.lng
- lngDiff
, true),
891 new L
.LatLng(ne
.lat
+ latDiff
, ne
.lng
+ lngDiff
, true));
894 //Shared animation code
895 _animationAddLayerNonAnimated: function (layer
, newCluster
) {
896 if (newCluster
=== layer
) {
897 this._featureGroup
.addLayer(layer
);
898 } else if (newCluster
._childCount
=== 2) {
899 newCluster
._addToMap();
901 var markers
= newCluster
.getAllChildMarkers();
902 this._featureGroup
.removeLayer(markers
[0]);
903 this._featureGroup
.removeLayer(markers
[1]);
905 newCluster
._updateIcon();
910 L
.MarkerClusterGroup
.include(!L
.DomUtil
.TRANSITION
? {
912 //Non Animated versions of everything
913 _animationStart: function () {
916 _animationZoomIn: function (previousZoomLevel
, newZoomLevel
) {
917 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, previousZoomLevel
);
918 this._topClusterLevel
._recursivelyAddChildrenToMap(null, newZoomLevel
, this._getExpandedVisibleBounds());
920 //We didn't actually animate, but we use this event to mean "clustering animations have finished"
921 this.fire('animationend');
923 _animationZoomOut: function (previousZoomLevel
, newZoomLevel
) {
924 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, previousZoomLevel
);
925 this._topClusterLevel
._recursivelyAddChildrenToMap(null, newZoomLevel
, this._getExpandedVisibleBounds());
927 //We didn't actually animate, but we use this event to mean "clustering animations have finished"
928 this.fire('animationend');
930 _animationAddLayer: function (layer
, newCluster
) {
931 this._animationAddLayerNonAnimated(layer
, newCluster
);
935 //Animated versions here
936 _animationStart: function () {
937 this._map
._mapPane
.className
+= ' leaflet-cluster-anim';
938 this._inZoomAnimation
++;
940 _animationEnd: function () {
942 this._map
._mapPane
.className
= this._map
._mapPane
.className
.replace(' leaflet-cluster-anim', '');
944 this._inZoomAnimation
--;
945 this.fire('animationend');
947 _animationZoomIn: function (previousZoomLevel
, newZoomLevel
) {
948 var bounds
= this._getExpandedVisibleBounds(),
949 fg
= this._featureGroup
,
952 //Add all children of current clusters to map and remove those clusters from map
953 this._topClusterLevel
._recursively(bounds
, previousZoomLevel
, 0, function (c
) {
954 var startPos
= c
._latlng
,
955 markers
= c
._markers
,
958 if (!bounds
.contains(startPos
)) {
962 if (c
._isSingleParent() && previousZoomLevel
+ 1 === newZoomLevel
) { //Immediately add the new child and remove us
964 c
._recursivelyAddChildrenToMap(null, newZoomLevel
, bounds
);
966 //Fade out old cluster
968 c
._recursivelyAddChildrenToMap(startPos
, newZoomLevel
, bounds
);
971 //Remove all markers that aren't visible any more
972 //TODO: Do we actually need to do this on the higher levels too?
973 for (i
= markers
.length
- 1; i
>= 0; i
--) {
975 if (!bounds
.contains(m
._latlng
)) {
985 this._topClusterLevel
._recursivelyBecomeVisible(bounds
, newZoomLevel
);
986 //TODO Maybe? Update markers in _recursivelyBecomeVisible
987 fg
.eachLayer(function (n
) {
988 if (!(n
instanceof L
.MarkerCluster
) && n
._icon
) {
993 //update the positions of the just added clusters/markers
994 this._topClusterLevel
._recursively(bounds
, previousZoomLevel
, newZoomLevel
, function (c
) {
995 c
._recursivelyRestoreChildPositions(newZoomLevel
);
998 //Remove the old clusters and close the zoom animation
999 this._enqueue(function () {
1000 //update the positions of the just added clusters/markers
1001 this._topClusterLevel
._recursively(bounds
, previousZoomLevel
, 0, function (c
) {
1006 this._animationEnd();
1010 _animationZoomOut: function (previousZoomLevel
, newZoomLevel
) {
1011 this._animationZoomOutSingle(this._topClusterLevel
, previousZoomLevel
- 1, newZoomLevel
);
1013 //Need to add markers for those that weren't on the map before but are now
1014 this._topClusterLevel
._recursivelyAddChildrenToMap(null, newZoomLevel
, this._getExpandedVisibleBounds());
1015 //Remove markers that were on the map before but won't be now
1016 this._topClusterLevel
._recursivelyRemoveChildrenFromMap(this._currentShownBounds
, previousZoomLevel
, this._getExpandedVisibleBounds());
1018 _animationZoomOutSingle: function (cluster
, previousZoomLevel
, newZoomLevel
) {
1019 var bounds
= this._getExpandedVisibleBounds();
1021 //Animate all of the markers in the clusters to move to their cluster center point
1022 cluster
._recursivelyAnimateChildrenInAndAddSelfToMap(bounds
, previousZoomLevel
+ 1, newZoomLevel
);
1026 //Update the opacity (If we immediately set it they won't animate)
1027 this._forceLayout();
1028 cluster
._recursivelyBecomeVisible(bounds
, newZoomLevel
);
1030 //TODO: Maybe use the transition timing stuff to make this more reliable
1031 //When the animations are done, tidy up
1032 this._enqueue(function () {
1034 //This cluster stopped being a cluster before the timeout fired
1035 if (cluster
._childCount
=== 1) {
1036 var m
= cluster
._markers
[0];
1037 //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
1038 m
.setLatLng(m
.getLatLng());
1043 cluster
._recursively(bounds
, newZoomLevel
, 0, function (c
) {
1044 c
._recursivelyRemoveChildrenFromMap(bounds
, previousZoomLevel
+ 1);
1050 _animationAddLayer: function (layer
, newCluster
) {
1052 fg
= this._featureGroup
;
1055 if (newCluster
!== layer
) {
1056 if (newCluster
._childCount
> 2) { //Was already a cluster
1058 newCluster
._updateIcon();
1059 this._forceLayout();
1060 this._animationStart();
1062 layer
._setPos(this._map
.latLngToLayerPoint(newCluster
.getLatLng()));
1063 layer
.setOpacity(0);
1065 this._enqueue(function () {
1066 fg
.removeLayer(layer
);
1067 layer
.setOpacity(1);
1072 } else { //Just became a cluster
1073 this._forceLayout();
1075 me
._animationStart();
1076 me
._animationZoomOutSingle(newCluster
, this._map
.getMaxZoom(), this._map
.getZoom());
1081 //Force a browser layout of stuff in the map
1082 // Should apply the current opacity and location to all elements so we can update them again for an animation
1083 _forceLayout: function () {
1084 //In my testing this works, infact offsetWidth of any element seems to work.
1085 //Could loop all this._layers and do this for each _icon if it stops working
1087 L
.Util
.falseFn(document
.body
.offsetWidth
);
1091 L
.markerClusterGroup = function (options
) {
1092 return new L
.MarkerClusterGroup(options
);
1096 L
.MarkerCluster
= L
.Marker
.extend({
1097 initialize: function (group
, zoom
, a
, b
) {
1099 L
.Marker
.prototype.initialize
.call(this, a
? (a
._cLatLng
|| a
.getLatLng()) : new L
.LatLng(0, 0), { icon
: this });
1102 this._group
= group
;
1106 this._childClusters
= [];
1107 this._childCount
= 0;
1108 this._iconNeedsUpdate
= true;
1110 this._bounds
= new L
.LatLngBounds();
1120 //Recursively retrieve all child markers of this cluster
1121 getAllChildMarkers: function (storageArray
) {
1122 storageArray
= storageArray
|| [];
1124 for (var i
= this._childClusters
.length
- 1; i
>= 0; i
--) {
1125 this._childClusters
[i
].getAllChildMarkers(storageArray
);
1128 for (var j
= this._markers
.length
- 1; j
>= 0; j
--) {
1129 storageArray
.push(this._markers
[j
]);
1132 return storageArray
;
1135 //Returns the count of how many child markers we have
1136 getChildCount: function () {
1137 return this._childCount
;
1140 //Zoom to the minimum of showing all of the child markers, or the extents of this cluster
1141 zoomToBounds: function () {
1142 var childClusters
= this._childClusters
.slice(),
1143 map
= this._group
._map
,
1144 boundsZoom
= map
.getBoundsZoom(this._bounds
),
1145 zoom
= this._zoom
+ 1,
1146 mapZoom
= map
.getZoom(),
1149 //calculate how far we need to zoom down to see all of the markers
1150 while (childClusters
.length
> 0 && boundsZoom
> zoom
) {
1152 var newClusters
= [];
1153 for (i
= 0; i
< childClusters
.length
; i
++) {
1154 newClusters
= newClusters
.concat(childClusters
[i
]._childClusters
);
1156 childClusters
= newClusters
;
1159 if (boundsZoom
> zoom
) {
1160 this._group
._map
.setView(this._latlng
, zoom
);
1161 } else if (boundsZoom
<= mapZoom
) { //If fitBounds wouldn't zoom us down, zoom us down instead
1162 this._group
._map
.setView(this._latlng
, mapZoom
+ 1);
1164 this._group
._map
.fitBounds(this._bounds
);
1168 getBounds: function () {
1169 var bounds
= new L
.LatLngBounds();
1170 bounds
.extend(this._bounds
);
1174 _updateIcon: function () {
1175 this._iconNeedsUpdate
= true;
1181 //Cludge for Icon, we pretend to be an icon for performance
1182 createIcon: function () {
1183 if (this._iconNeedsUpdate
) {
1184 this._iconObj
= this._group
.options
.iconCreateFunction(this);
1185 this._iconNeedsUpdate
= false;
1187 return this._iconObj
.createIcon();
1189 createShadow: function () {
1190 return this._iconObj
.createShadow();
1194 _addChild: function (new1
, isNotificationFromChild
) {
1196 this._iconNeedsUpdate
= true;
1197 this._expandBounds(new1
);
1199 if (new1
instanceof L
.MarkerCluster
) {
1200 if (!isNotificationFromChild
) {
1201 this._childClusters
.push(new1
);
1202 new1
.__parent
= this;
1204 this._childCount
+= new1
._childCount
;
1206 if (!isNotificationFromChild
) {
1207 this._markers
.push(new1
);
1212 if (this.__parent
) {
1213 this.__parent
._addChild(new1
, true);
1217 //Expand our bounds and tell our parent to
1218 _expandBounds: function (marker
) {
1220 addedLatLng
= marker
._wLatLng
|| marker
._latlng
;
1222 if (marker
instanceof L
.MarkerCluster
) {
1223 this._bounds
.extend(marker
._bounds
);
1224 addedCount
= marker
._childCount
;
1226 this._bounds
.extend(addedLatLng
);
1230 if (!this._cLatLng
) {
1231 // when clustering, take position of the first point as the cluster center
1232 this._cLatLng
= marker
._cLatLng
|| addedLatLng
;
1235 // when showing clusters, take weighted average of all points as cluster center
1236 var totalCount
= this._childCount
+ addedCount
;
1238 //Calculate weighted latlng for display
1239 if (!this._wLatLng
) {
1240 this._latlng
= this._wLatLng
= new L
.LatLng(addedLatLng
.lat
, addedLatLng
.lng
);
1242 this._wLatLng
.lat
= (addedLatLng
.lat
* addedCount
+ this._wLatLng
.lat
* this._childCount
) / totalCount
;
1243 this._wLatLng
.lng
= (addedLatLng
.lng
* addedCount
+ this._wLatLng
.lng
* this._childCount
) / totalCount
;
1247 //Set our markers position as given and add it to the map
1248 _addToMap: function (startPos
) {
1250 this._backupLatlng
= this._latlng
;
1251 this.setLatLng(startPos
);
1253 this._group
._featureGroup
.addLayer(this);
1256 _recursivelyAnimateChildrenIn: function (bounds
, center
, maxZoom
) {
1257 this._recursively(bounds
, 0, maxZoom
- 1,
1259 var markers
= c
._markers
,
1261 for (i
= markers
.length
- 1; i
>= 0; i
--) {
1264 //Only do it if the icon is still on the map
1272 var childClusters
= c
._childClusters
,
1274 for (j
= childClusters
.length
- 1; j
>= 0; j
--) {
1275 cm
= childClusters
[j
];
1285 _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds
, previousZoomLevel
, newZoomLevel
) {
1286 this._recursively(bounds
, newZoomLevel
, 0,
1288 c
._recursivelyAnimateChildrenIn(bounds
, c
._group
._map
.latLngToLayerPoint(c
.getLatLng()).round(), previousZoomLevel
);
1290 //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
1291 //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
1292 if (c
._isSingleParent() && previousZoomLevel
- 1 === newZoomLevel
) {
1294 c
._recursivelyRemoveChildrenFromMap(bounds
, previousZoomLevel
); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
1304 _recursivelyBecomeVisible: function (bounds
, zoomLevel
) {
1305 this._recursively(bounds
, 0, zoomLevel
, null, function (c
) {
1310 _recursivelyAddChildrenToMap: function (startPos
, zoomLevel
, bounds
) {
1311 this._recursively(bounds
, -1, zoomLevel
,
1313 if (zoomLevel
=== c
._zoom
) {
1317 //Add our child markers at startPos (so they can be animated out)
1318 for (var i
= c
._markers
.length
- 1; i
>= 0; i
--) {
1319 var nm
= c
._markers
[i
];
1321 if (!bounds
.contains(nm
._latlng
)) {
1326 nm
._backupLatlng
= nm
.getLatLng();
1328 nm
.setLatLng(startPos
);
1329 if (nm
.setOpacity
) {
1334 c
._group
._featureGroup
.addLayer(nm
);
1338 c
._addToMap(startPos
);
1343 _recursivelyRestoreChildPositions: function (zoomLevel
) {
1344 //Fix positions of child markers
1345 for (var i
= this._markers
.length
- 1; i
>= 0; i
--) {
1346 var nm
= this._markers
[i
];
1347 if (nm
._backupLatlng
) {
1348 nm
.setLatLng(nm
._backupLatlng
);
1349 delete nm
._backupLatlng
;
1353 if (zoomLevel
- 1 === this._zoom
) {
1354 //Reposition child clusters
1355 for (var j
= this._childClusters
.length
- 1; j
>= 0; j
--) {
1356 this._childClusters
[j
]._restorePosition();
1359 for (var k
= this._childClusters
.length
- 1; k
>= 0; k
--) {
1360 this._childClusters
[k
]._recursivelyRestoreChildPositions(zoomLevel
);
1365 _restorePosition: function () {
1366 if (this._backupLatlng
) {
1367 this.setLatLng(this._backupLatlng
);
1368 delete this._backupLatlng
;
1372 //exceptBounds: If set, don't remove any markers/clusters in it
1373 _recursivelyRemoveChildrenFromMap: function (previousBounds
, zoomLevel
, exceptBounds
) {
1375 this._recursively(previousBounds
, -1, zoomLevel
- 1,
1377 //Remove markers at every level
1378 for (i
= c
._markers
.length
- 1; i
>= 0; i
--) {
1380 if (!exceptBounds
|| !exceptBounds
.contains(m
._latlng
)) {
1381 c
._group
._featureGroup
.removeLayer(m
);
1389 //Remove child clusters at just the bottom level
1390 for (i
= c
._childClusters
.length
- 1; i
>= 0; i
--) {
1391 m
= c
._childClusters
[i
];
1392 if (!exceptBounds
|| !exceptBounds
.contains(m
._latlng
)) {
1393 c
._group
._featureGroup
.removeLayer(m
);
1403 //Run the given functions recursively to this and child clusters
1404 // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
1405 // zoomLevelToStart: zoom level to start running functions (inclusive)
1406 // zoomLevelToStop: zoom level to stop running functions (inclusive)
1407 // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
1408 // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
1409 _recursively: function (boundsToApplyTo
, zoomLevelToStart
, zoomLevelToStop
, runAtEveryLevel
, runAtBottomLevel
) {
1410 var childClusters
= this._childClusters
,
1414 if (zoomLevelToStart
> zoom
) { //Still going down to required depth, just recurse to child clusters
1415 for (i
= childClusters
.length
- 1; i
>= 0; i
--) {
1416 c
= childClusters
[i
];
1417 if (boundsToApplyTo
.intersects(c
._bounds
)) {
1418 c
._recursively(boundsToApplyTo
, zoomLevelToStart
, zoomLevelToStop
, runAtEveryLevel
, runAtBottomLevel
);
1421 } else { //In required depth
1423 if (runAtEveryLevel
) {
1424 runAtEveryLevel(this);
1426 if (runAtBottomLevel
&& this._zoom
=== zoomLevelToStop
) {
1427 runAtBottomLevel(this);
1430 //TODO: This loop is almost the same as above
1431 if (zoomLevelToStop
> zoom
) {
1432 for (i
= childClusters
.length
- 1; i
>= 0; i
--) {
1433 c
= childClusters
[i
];
1434 if (boundsToApplyTo
.intersects(c
._bounds
)) {
1435 c
._recursively(boundsToApplyTo
, zoomLevelToStart
, zoomLevelToStop
, runAtEveryLevel
, runAtBottomLevel
);
1442 _recalculateBounds: function () {
1443 var markers
= this._markers
,
1444 childClusters
= this._childClusters
,
1447 this._bounds
= new L
.LatLngBounds();
1448 delete this._wLatLng
;
1450 for (i
= markers
.length
- 1; i
>= 0; i
--) {
1451 this._expandBounds(markers
[i
]);
1453 for (i
= childClusters
.length
- 1; i
>= 0; i
--) {
1454 this._expandBounds(childClusters
[i
]);
1459 //Returns true if we are the parent of only one cluster and that cluster is the same as us
1460 _isSingleParent: function () {
1461 //Don't need to check this._markers as the rest won't work if there are any
1462 return this._childClusters
.length
> 0 && this._childClusters
[0]._childCount
=== this._childCount
;
1468 L
.DistanceGrid = function (cellSize
) {
1469 this._cellSize
= cellSize
;
1470 this._sqCellSize
= cellSize
* cellSize
;
1472 this._objectPoint
= { };
1475 L
.DistanceGrid
.prototype = {
1477 addObject: function (obj
, point
) {
1478 var x
= this._getCoord(point
.x
),
1479 y
= this._getCoord(point
.y
),
1481 row
= grid
[y
] = grid
[y
] || {},
1482 cell
= row
[x
] = row
[x
] || [],
1483 stamp
= L
.Util
.stamp(obj
);
1485 this._objectPoint
[stamp
] = point
;
1490 updateObject: function (obj
, point
) {
1491 this.removeObject(obj
);
1492 this.addObject(obj
, point
);
1495 //Returns true if the object was found
1496 removeObject: function (obj
, point
) {
1497 var x
= this._getCoord(point
.x
),
1498 y
= this._getCoord(point
.y
),
1500 row
= grid
[y
] = grid
[y
] || {},
1501 cell
= row
[x
] = row
[x
] || [],
1504 delete this._objectPoint
[L
.Util
.stamp(obj
)];
1506 for (i
= 0, len
= cell
.length
; i
< len
; i
++) {
1507 if (cell
[i
] === obj
) {
1521 eachObject: function (fn
, context
) {
1522 var i
, j
, k
, len
, row
, cell
, removed
,
1531 for (k
= 0, len
= cell
.length
; k
< len
; k
++) {
1532 removed
= fn
.call(context
, cell
[k
]);
1542 getNearObject: function (point
) {
1543 var x
= this._getCoord(point
.x
),
1544 y
= this._getCoord(point
.y
),
1545 i
, j
, k
, row
, cell
, len
, obj
, dist
,
1546 objectPoint
= this._objectPoint
,
1547 closestDistSq
= this._sqCellSize
,
1550 for (i
= y
- 1; i
<= y
+ 1; i
++) {
1551 row
= this._grid
[i
];
1554 for (j
= x
- 1; j
<= x
+ 1; j
++) {
1558 for (k
= 0, len
= cell
.length
; k
< len
; k
++) {
1560 dist
= this._sqDist(objectPoint
[L
.Util
.stamp(obj
)], point
);
1561 if (dist
< closestDistSq
) {
1562 closestDistSq
= dist
;
1573 _getCoord: function (x
) {
1574 return Math
.floor(x
/ this._cellSize
);
1577 _sqDist: function (p
, p2
) {
1578 var dx
= p2
.x
- p
.x
,
1580 return dx
* dx
+ dy
* dy
;
1585 /* Copyright (c) 2012 the authors listed at the following URL, and/or
1586 the authors of referenced articles or incorporated external code:
1587 http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256
1589 Permission is hereby granted, free of charge, to any person obtaining
1590 a copy of this software and associated documentation files (the
1591 "Software"), to deal in the Software without restriction, including
1592 without limitation the rights to use, copy, modify, merge, publish,
1593 distribute, sublicense, and/or sell copies of the Software, and to
1594 permit persons to whom the Software is furnished to do so, subject to
1595 the following conditions:
1597 The above copyright notice and this permission notice shall be
1598 included in all copies or substantial portions of the Software.
1600 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1601 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1602 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1603 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1604 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1605 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1606 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1608 Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
1615 * @param {Object} cpt a point to be measured from the baseline
1616 * @param {Array} bl the baseline, as represented by a two-element
1617 * array of latlng objects.
1618 * @returns {Number} an approximate distance measure
1620 getDistant: function (cpt
, bl
) {
1621 var vY
= bl
[1].lat
- bl
[0].lat
,
1622 vX
= bl
[0].lng
- bl
[1].lng
;
1623 return (vX
* (cpt
.lat
- bl
[0].lat
) + vY
* (cpt
.lng
- bl
[0].lng
));
1627 * @param {Array} baseLine a two-element array of latlng objects
1628 * representing the baseline to project from
1629 * @param {Array} latLngs an array of latlng objects
1630 * @returns {Object} the maximum point and all new points to stay
1631 * in consideration for the hull.
1633 findMostDistantPointFromBaseLine: function (baseLine
, latLngs
) {
1639 for (i
= latLngs
.length
- 1; i
>= 0; i
--) {
1641 d
= this.getDistant(pt
, baseLine
);
1655 return { maxPoint
: maxPt
, newPoints
: newPoints
};
1660 * Given a baseline, compute the convex hull of latLngs as an array
1663 * @param {Array} latLngs
1666 buildConvexHull: function (baseLine
, latLngs
) {
1667 var convexHullBaseLines
= [],
1668 t
= this.findMostDistantPointFromBaseLine(baseLine
, latLngs
);
1670 if (t
.maxPoint
) { // if there is still a point "outside" the base line
1671 convexHullBaseLines
=
1672 convexHullBaseLines
.concat(
1673 this.buildConvexHull([baseLine
[0], t
.maxPoint
], t
.newPoints
)
1675 convexHullBaseLines
=
1676 convexHullBaseLines
.concat(
1677 this.buildConvexHull([t
.maxPoint
, baseLine
[1]], t
.newPoints
)
1679 return convexHullBaseLines
;
1680 } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull
1681 return [baseLine
[0]];
1686 * Given an array of latlngs, compute a convex hull as an array
1689 * @param {Array} latLngs
1692 getConvexHull: function (latLngs
) {
1693 // find first baseline
1694 var maxLat
= false, minLat
= false,
1695 maxPt
= null, minPt
= null,
1698 for (i
= latLngs
.length
- 1; i
>= 0; i
--) {
1699 var pt
= latLngs
[i
];
1700 if (maxLat
=== false || pt
.lat
> maxLat
) {
1704 if (minLat
=== false || pt
.lat
< minLat
) {
1709 var ch
= [].concat(this.buildConvexHull([minPt
, maxPt
], latLngs
),
1710 this.buildConvexHull([maxPt
, minPt
], latLngs
));
1716 L
.MarkerCluster
.include({
1717 getConvexHull: function () {
1718 var childMarkers
= this.getAllChildMarkers(),
1722 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
1723 p
= childMarkers
[i
].getLatLng();
1727 return L
.QuickHull
.getConvexHull(points
);
1732 //This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
1733 //Huge thanks to jawj for implementing it first to make my job easy :-)
1735 L
.MarkerCluster
.include({
1738 _circleFootSeparation
: 25, //related to circumference of circle
1739 _circleStartAngle
: Math
.PI
/ 6,
1741 _spiralFootSeparation
: 28, //related to size of spiral (experiment!)
1742 _spiralLengthStart
: 11,
1743 _spiralLengthFactor
: 5,
1745 _circleSpiralSwitchover
: 9, //show spiral instead of circle from this marker count upwards.
1746 // 0 -> always spiral; Infinity -> always circle
1748 spiderfy: function () {
1749 if (this._group
._spiderfied
=== this || this._group
._inZoomAnimation
) {
1753 var childMarkers
= this.getAllChildMarkers(),
1754 group
= this._group
,
1756 center
= map
.latLngToLayerPoint(this._latlng
),
1759 this._group
._unspiderfy();
1760 this._group
._spiderfied
= this;
1762 //TODO Maybe: childMarkers order by distance to center
1764 if (childMarkers
.length
>= this._circleSpiralSwitchover
) {
1765 positions
= this._generatePointsSpiral(childMarkers
.length
, center
);
1767 center
.y
+= 10; //Otherwise circles look wrong
1768 positions
= this._generatePointsCircle(childMarkers
.length
, center
);
1771 this._animationSpiderfy(childMarkers
, positions
);
1774 unspiderfy: function (zoomDetails
) {
1775 /// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param>
1776 if (this._group
._inZoomAnimation
) {
1779 this._animationUnspiderfy(zoomDetails
);
1781 this._group
._spiderfied
= null;
1784 _generatePointsCircle: function (count
, centerPt
) {
1785 var circumference
= this._group
.options
.spiderfyDistanceMultiplier
* this._circleFootSeparation
* (2 + count
),
1786 legLength
= circumference
/ this._2PI
, //radius from circumference
1787 angleStep
= this._2PI
/ count
,
1793 for (i
= count
- 1; i
>= 0; i
--) {
1794 angle
= this._circleStartAngle
+ i
* angleStep
;
1795 res
[i
] = new L
.Point(centerPt
.x
+ legLength
* Math
.cos(angle
), centerPt
.y
+ legLength
* Math
.sin(angle
))._round();
1801 _generatePointsSpiral: function (count
, centerPt
) {
1802 var legLength
= this._group
.options
.spiderfyDistanceMultiplier
* this._spiralLengthStart
,
1803 separation
= this._group
.options
.spiderfyDistanceMultiplier
* this._spiralFootSeparation
,
1804 lengthFactor
= this._group
.options
.spiderfyDistanceMultiplier
* this._spiralLengthFactor
,
1811 for (i
= count
- 1; i
>= 0; i
--) {
1812 angle
+= separation
/ legLength
+ i
* 0.0005;
1813 res
[i
] = new L
.Point(centerPt
.x
+ legLength
* Math
.cos(angle
), centerPt
.y
+ legLength
* Math
.sin(angle
))._round();
1814 legLength
+= this._2PI
* lengthFactor
/ angle
;
1819 _noanimationUnspiderfy: function () {
1820 var group
= this._group
,
1822 fg
= group
._featureGroup
,
1823 childMarkers
= this.getAllChildMarkers(),
1827 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
1828 m
= childMarkers
[i
];
1832 if (m
._preSpiderfyLatlng
) {
1833 m
.setLatLng(m
._preSpiderfyLatlng
);
1834 delete m
._preSpiderfyLatlng
;
1836 if (m
.setZIndexOffset
) {
1837 m
.setZIndexOffset(0);
1841 map
.removeLayer(m
._spiderLeg
);
1842 delete m
._spiderLeg
;
1846 group
._spiderfied
= null;
1850 L
.MarkerCluster
.include(!L
.DomUtil
.TRANSITION
? {
1851 //Non Animated versions of everything
1852 _animationSpiderfy: function (childMarkers
, positions
) {
1853 var group
= this._group
,
1855 fg
= group
._featureGroup
,
1858 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
1859 newPos
= map
.layerPointToLatLng(positions
[i
]);
1860 m
= childMarkers
[i
];
1862 m
._preSpiderfyLatlng
= m
._latlng
;
1863 m
.setLatLng(newPos
);
1864 if (m
.setZIndexOffset
) {
1865 m
.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
1871 leg
= new L
.Polyline([this._latlng
, newPos
], { weight
: 1.5, color
: '#222' });
1875 this.setOpacity(0.3);
1876 group
.fire('spiderfied');
1879 _animationUnspiderfy: function () {
1880 this._noanimationUnspiderfy();
1883 //Animated versions here
1884 SVG_ANIMATION
: (function () {
1885 return document
.createElementNS('http://www.w3.org/2000/svg', 'animate').toString().indexOf('SVGAnimate') > -1;
1888 _animationSpiderfy: function (childMarkers
, positions
) {
1890 group
= this._group
,
1892 fg
= group
._featureGroup
,
1893 thisLayerPos
= map
.latLngToLayerPoint(this._latlng
),
1896 //Add markers to map hidden at our center point
1897 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
1898 m
= childMarkers
[i
];
1900 //If it is a marker, add it now and we'll animate it out
1902 m
.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
1907 m
._setPos(thisLayerPos
);
1909 //Vectors just get immediately added
1914 group
._forceLayout();
1915 group
._animationStart();
1917 var initialLegOpacity
= L
.Path
.SVG
? 0 : 0.3,
1918 xmlns
= L
.Path
.SVG_NS
;
1921 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
1922 newPos
= map
.layerPointToLatLng(positions
[i
]);
1923 m
= childMarkers
[i
];
1925 //Move marker to new position
1926 m
._preSpiderfyLatlng
= m
._latlng
;
1927 m
.setLatLng(newPos
);
1935 leg
= new L
.Polyline([me
._latlng
, newPos
], { weight
: 1.5, color
: '#222', opacity
: initialLegOpacity
});
1939 //Following animations don't work for canvas
1940 if (!L
.Path
.SVG
|| !this.SVG_ANIMATION
) {
1945 //http://stackoverflow.com/questions/5924238/how-do-you-animate-an-svg-path-in-ios
1946 //http://dev.opera.com/articles/view/advanced-svg-animation-techniques/
1949 var length
= leg
._path
.getTotalLength();
1950 leg
._path
.setAttribute("stroke-dasharray", length
+ "," + length
);
1952 var anim
= document
.createElementNS(xmlns
, "animate");
1953 anim
.setAttribute("attributeName", "stroke-dashoffset");
1954 anim
.setAttribute("begin", "indefinite");
1955 anim
.setAttribute("from", length
);
1956 anim
.setAttribute("to", 0);
1957 anim
.setAttribute("dur", 0.25);
1958 leg
._path
.appendChild(anim
);
1959 anim
.beginElement();
1962 anim
= document
.createElementNS(xmlns
, "animate");
1963 anim
.setAttribute("attributeName", "stroke-opacity");
1964 anim
.setAttribute("attributeName", "stroke-opacity");
1965 anim
.setAttribute("begin", "indefinite");
1966 anim
.setAttribute("from", 0);
1967 anim
.setAttribute("to", 0.5);
1968 anim
.setAttribute("dur", 0.25);
1969 leg
._path
.appendChild(anim
);
1970 anim
.beginElement();
1974 //Set the opacity of the spiderLegs back to their correct value
1975 // The animations above override this until they complete.
1976 // If the initial opacity of the spiderlegs isn't 0 then they appear before the animation starts.
1978 this._group
._forceLayout();
1980 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
1981 m
= childMarkers
[i
]._spiderLeg
;
1983 m
.options
.opacity
= 0.5;
1984 m
._path
.setAttribute('stroke-opacity', 0.5);
1988 setTimeout(function () {
1989 group
._animationEnd();
1990 group
.fire('spiderfied');
1994 _animationUnspiderfy: function (zoomDetails
) {
1995 var group
= this._group
,
1997 fg
= group
._featureGroup
,
1998 thisLayerPos
= zoomDetails
? map
._latLngToNewLayerPoint(this._latlng
, zoomDetails
.zoom
, zoomDetails
.center
) : map
.latLngToLayerPoint(this._latlng
),
1999 childMarkers
= this.getAllChildMarkers(),
2000 svg
= L
.Path
.SVG
&& this.SVG_ANIMATION
,
2003 group
._animationStart();
2005 //Make us visible and bring the child markers back in
2007 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
2008 m
= childMarkers
[i
];
2010 //Marker was added to us after we were spidified
2011 if (!m
._preSpiderfyLatlng
) {
2015 //Fix up the location to the real one
2016 m
.setLatLng(m
._preSpiderfyLatlng
);
2017 delete m
._preSpiderfyLatlng
;
2018 //Hack override the location to be our center
2020 m
._setPos(thisLayerPos
);
2026 //Animate the spider legs back in
2028 a
= m
._spiderLeg
._path
.childNodes
[0];
2029 a
.setAttribute('to', a
.getAttribute('from'));
2030 a
.setAttribute('from', 0);
2033 a
= m
._spiderLeg
._path
.childNodes
[1];
2034 a
.setAttribute('from', 0.5);
2035 a
.setAttribute('to', 0);
2036 a
.setAttribute('stroke-opacity', 0);
2039 m
._spiderLeg
._path
.setAttribute('stroke-opacity', 0);
2043 setTimeout(function () {
2044 //If we have only <= one child left then that marker will be shown on the map so don't remove it!
2045 var stillThereChildCount
= 0;
2046 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
2047 m
= childMarkers
[i
];
2049 stillThereChildCount
++;
2054 for (i
= childMarkers
.length
- 1; i
>= 0; i
--) {
2055 m
= childMarkers
[i
];
2057 if (!m
._spiderLeg
) { //Has already been unspiderfied
2064 m
.setZIndexOffset(0);
2067 if (stillThereChildCount
> 1) {
2071 map
.removeLayer(m
._spiderLeg
);
2072 delete m
._spiderLeg
;
2074 group
._animationEnd();
2080 L
.MarkerClusterGroup
.include({
2081 //The MarkerCluster currently spiderfied (if any)
2084 _spiderfierOnAdd: function () {
2085 this._map
.on('click', this._unspiderfyWrapper
, this);
2087 if (this._map
.options
.zoomAnimation
) {
2088 this._map
.on('zoomstart', this._unspiderfyZoomStart
, this);
2090 //Browsers without zoomAnimation or a big zoom don't fire zoomstart
2091 this._map
.on('zoomend', this._noanimationUnspiderfy
, this);
2093 if (L
.Path
.SVG
&& !L
.Browser
.touch
) {
2094 this._map
._initPathRoot();
2095 //Needs to happen in the pageload, not after, or animations don't work in webkit
2096 // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements
2097 //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable
2101 _spiderfierOnRemove: function () {
2102 this._map
.off('click', this._unspiderfyWrapper
, this);
2103 this._map
.off('zoomstart', this._unspiderfyZoomStart
, this);
2104 this._map
.off('zoomanim', this._unspiderfyZoomAnim
, this);
2106 this._unspiderfy(); //Ensure that markers are back where they should be
2110 //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)
2111 //This means we can define the animation they do rather than Markers doing an animation to their actual location
2112 _unspiderfyZoomStart: function () {
2113 if (!this._map
) { //May have been removed from the map by a zoomEnd handler
2117 this._map
.on('zoomanim', this._unspiderfyZoomAnim
, this);
2119 _unspiderfyZoomAnim: function (zoomDetails
) {
2120 //Wait until the first zoomanim after the user has finished touch-zooming before running the animation
2121 if (L
.DomUtil
.hasClass(this._map
._mapPane
, 'leaflet-touching')) {
2125 this._map
.off('zoomanim', this._unspiderfyZoomAnim
, this);
2126 this._unspiderfy(zoomDetails
);
2130 _unspiderfyWrapper: function () {
2131 /// <summary>_unspiderfy but passes no arguments</summary>
2135 _unspiderfy: function (zoomDetails
) {
2136 if (this._spiderfied
) {
2137 this._spiderfied
.unspiderfy(zoomDetails
);
2141 _noanimationUnspiderfy: function () {
2142 if (this._spiderfied
) {
2143 this._spiderfied
._noanimationUnspiderfy();
2147 //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc
2148 _unspiderfyLayer: function (layer
) {
2149 if (layer
._spiderLeg
) {
2150 this._featureGroup
.removeLayer(layer
);
2152 layer
.setOpacity(1);
2153 //Position will be fixed up immediately in _animationUnspiderfy
2154 layer
.setZIndexOffset(0);
2156 this._map
.removeLayer(layer
._spiderLeg
);
2157 delete layer
._spiderLeg
;
2163 }(window
, document
));