PageSourceSearch

https://tuulikartta.info/node_modules/leaflet.markercluster/dist/leaflet.markercluster-src.js

js tuulikartta.info collected 2026-10-08 19:44:47 UTC 81,160 bytes, 2,718 lines download raw bytes

vendor: 81,160 bytes, lines 1-2718
1/*
2 * Leaflet.markercluster 1.5.3+master.e5124b2,
3 * Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.
4 * https://github.com/Leaflet/Leaflet.markercluster
5 * (c) 2012-2017, Dave Leaver, smartrak
6 */
7(function (global, factory) {
8	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
9	typeof define === 'function' && define.amd ? define(['exports'], factory) :
10	(global = global || self, factory((global.Leaflet = global.Leaflet || {}, global.Leaflet.markercluster = {})));
11}(this, function (exports) { 'use strict';
12
13	/*
14	 * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
15	 */
16
17	var MarkerClusterGroup = L.MarkerClusterGroup = L.FeatureGroup.extend({
18
19		options: {
20			maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center
21			iconCreateFunction: null,
22			clusterPane: L.Marker.prototype.options.pane,
23
24			spiderfyOnEveryZoom: false,
25			spiderfyOnMaxZoom: true,
26			showCoverageOnHover: true,
27			zoomToBoundsOnClick: true,
28			singleMarkerMode: false,
29
30			disableClusteringAtZoom: null,
31
32			// Setting this to false prevents the removal of any clusters outside of the viewpoint, which
33			// is the default behaviour for performance reasons.
34			removeOutsideVisibleBounds: true,
35
36			// Set to false to disable all animations (zoom and spiderfy).
37			// If false, option animateAddingMarkers below has no effect.
38			// If L.DomUtil.TRANSITION is falsy, this option has no effect.
39			animate: true,
40
41			//Whether to animate adding markers after adding the MarkerClusterGroup to the map
42			// If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
43			animateAddingMarkers: false,
44
45			// Make it possible to provide custom function to calculate spiderfy shape positions
46			spiderfyShapePositions: null,
47
48			//Increase to increase the distance away that spiderfied markers appear from the center
49			spiderfyDistanceMultiplier: 1,
50
51			// Make it possible to specify a polyline options on a spider leg
52			spiderLegPolylineOptions: { weight: 1.5, color: '#222', opacity: 0.5 },
53
54			// 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
55			chunkedLoading: false,
56			chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback)
57			chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser
58			chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator)
59
60			//Options to pass to the L.Polygon constructor
61			polygonOptions: {}
62		},
63
64		initialize: function (options) {
65			L.Util.setOptions(this, options);
66			if (!this.options.iconCreateFunction) {
67				this.options.iconCreateFunction = this._defaultIconCreateFunction;
68			}
69
70			this._featureGroup = L.featureGroup();
71			this._featureGroup.addEventParent(this);
72
73			this._nonPointGroup = L.featureGroup();
74			this._nonPointGroup.addEventParent(this);
75
76			this._inZoomAnimation = 0;
77			this._needsClustering = [];
78			this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of
79			//The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
80			this._currentShownBounds = null;
81
82			this._queue = [];
83
84			this._childMarkerEventHandlers = {
85				'dragstart': this._childMarkerDragStart,
86				'move': this._childMarkerMoved,
87				'dragend': this._childMarkerDragEnd,
88			};
89
90			// Hook the appropriate animation methods.
91			var animate = L.DomUtil.TRANSITION && this.options.animate;
92			L.extend(this, animate ? this._withAnimation : this._noAnimation);
93			// Remember which MarkerCluster class to instantiate (animated or not).
94			this._markerCluster = animate ? L.MarkerCluster : L.MarkerClusterNonAnimated;
95		},
96
97		addLayer: function (layer) {
98
99			if (layer instanceof L.LayerGroup) {
100				return this.addLayers([layer]);
101			}
102
103			//Don't cluster non point data
104			if (!layer.getLatLng) {
105				this._nonPointGroup.addLayer(layer);
106				this.fire('layeradd', { layer: layer });
107				return this;
108			}
109
110			if (!this._map) {
111				this._needsClustering.push(layer);
112				this.fire('layeradd', { layer: layer });
113				return this;
114			}
115
116			if (this.hasLayer(layer)) {
117				return this;
118			}
119
120
121			//If we have already clustered we'll need to add this one to a cluster
122
123			if (this._unspiderfy) {
124				this._unspiderfy();
125			}
126
127			this._addLayer(layer, this._maxZoom);
128			this.fire('layeradd', { layer: layer });
129
130			// Refresh bounds and weighted positions.
131			this._topClusterLevel._recalculateBounds();
132
133			this._refreshClustersIcons();
134
135			//Work out what is visible
136			var visibleLayer = layer,
137			    currentZoom = this._zoom;
138			if (layer.__parent) {
139				while (visibleLayer.__parent._zoom >= currentZoom) {
140					visibleLayer = visibleLayer.__parent;
141				}
142			}
143
144			if (this._currentShownBounds.contains(visibleLayer.getLatLng())) {
145				if (this.options.animateAddingMarkers) {
146					this._animationAddLayer(layer, visibleLayer);
147				} else {
148					this._animationAddLayerNonAnimated(layer, visibleLayer);
149				}
150			}
151			return this;
152		},
153
154		removeLayer: function (layer) {
155
156			if (layer instanceof L.LayerGroup) {
157				return this.removeLayers([layer]);
158			}
159
160			//Non point layers
161			if (!layer.getLatLng) {
162				this._nonPointGroup.removeLayer(layer);
163				this.fire('layerremove', { layer: layer });
164				return this;
165			}
166
167			if (!this._map) {
168				if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) {
169					this._needsRemoving.push({ layer: layer, latlng: layer._latlng });
170				}
171				this.fire('layerremove', { layer: layer });
172				return this;
173			}
174
175			if (!layer.__parent) {
176				return this;
177			}
178
179			if (this._unspiderfy) {
180				this._unspiderfy();
181				this._unspiderfyLayer(layer);
182			}
183
184			//Remove the marker from clusters
185			this._removeLayer(layer, true);
186			this.fire('layerremove', { layer: layer });
187
188			// Refresh bounds and weighted positions.
189			this._topClusterLevel._recalculateBounds();
190
191			this._refreshClustersIcons();
192
193			layer.off(this._childMarkerEventHandlers, this);
194
195			if (this._featureGroup.hasLayer(layer)) {
196				this._featureGroup.removeLayer(layer);
197				if (layer.clusterShow) {
198					layer.clusterShow();
199				}
200			}
201
202			return this;
203		},
204
205		//Takes an array of markers and adds them in bulk
206		addLayers: function (layersArray, skipLayerAddEvent) {
207			if (!L.Util.isArray(layersArray)) {
208				return this.addLayer(layersArray);
209			}
210
211			var fg = this._featureGroup,
212			    npg = this._nonPointGroup,
213			    chunked = this.options.chunkedLoading,
214			    chunkInterval = this.options.chunkInterval,
215			    chunkProgress = this.options.chunkProgress,
216			    l = layersArray.length,
217			    offset = 0,
218			    originalArray = true,
219			    m;
220
221			if (this._map) {
222				var started = (new Date()).getTime();
223				var process = L.bind(function () {
224					var start = (new Date()).getTime();
225
226					// Make sure to unspiderfy before starting to add some layers
227					if (this._map && this._unspiderfy) {
228						this._unspiderfy();
229					}
230
231					for (; offset < l; offset++) {
232						if (chunked && offset % 200 === 0) {
233							// every couple hundred markers, instrument the time elapsed since processing started:
234							var elapsed = (new Date()).getTime() - start;
235							if (elapsed > chunkInterval) {
236								break; // been working too hard, time to take a break :-)
237							}
238						}
239
240						m = layersArray[offset];
241
242						// Group of layers, append children to layersArray and skip.
243						// Side effects:
244						// - Total increases, so chunkProgress ratio jumps backward.
245						// - Groups are not included in this group, only their non-group child layers (hasLayer).
246						// Changing array length while looping does not affect performance in current browsers:
247						// http://jsperf.com/for-loop-changing-length/6
248						if (m instanceof L.LayerGroup) {
249							if (originalArray) {
250								layersArray = layersArray.slice();
251								originalArray = false;
252							}
253							this._extractNonGroupLayers(m, layersArray);
254							l = layersArray.length;
255							continue;
256						}
257
258						//Not point data, can't be clustered
259						if (!m.getLatLng) {
260							npg.addLayer(m);
261							if (!skipLayerAddEvent) {
262								this.fire('layeradd', { layer: m });
263							}
264							continue;
265						}
266
267						if (this.hasLayer(m)) {
268							continue;
269						}
270
271						this._addLayer(m, this._maxZoom);
272						if (!skipLayerAddEvent) {
273							this.fire('layeradd', { layer: m });
274						}
275
276						//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
277						if (m.__parent) {
278							if (m.__parent.getChildCount() === 2) {
279								var markers = m.__parent.getAllChildMarkers(),
280								    otherMarker = markers[0] === m ? markers[1] : markers[0];
281								fg.removeLayer(otherMarker);
282							}
283						}
284					}
285
286					if (chunkProgress) {
287						// report progress and time elapsed:
288						chunkProgress(offset, l, (new Date()).getTime() - started);
289					}
290
291					// Completed processing all markers.
292					if (offset === l) {
293
294						// Refresh bounds and weighted positions.
295						this._topClusterLevel._recalculateBounds();
296
297						this._refreshClustersIcons();
298
299						this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
300					} else {
301						setTimeout(process, this.options.chunkDelay);
302					}
303				}, this);
304
305				process();
306			} else {
307				var needsClustering = this._needsClustering;
308
309				for (; offset < l; offset++) {
310					m = layersArray[offset];
311
312					// Group of layers, append children to layersArray and skip.
313					if (m instanceof L.LayerGroup) {
314						if (originalArray) {
315							layersArray = layersArray.slice();
316							originalArray = false;
317						}
318						this._extractNonGroupLayers(m, layersArray);
319						l = layersArray.length;
320						continue;
321					}
322
323					//Not point data, can't be clustered
324					if (!m.getLatLng) {
325						npg.addLayer(m);
326						continue;
327					}
328
329					if (this.hasLayer(m)) {
330						continue;
331					}
332
333					needsClustering.push(m);
334				}
335			}
336			return this;
337		},
338
339		//Takes an array of markers and removes them in bulk
340		removeLayers: function (layersArray) {
341			var i, m,
342			    l = layersArray.length,
343			    fg = this._featureGroup,
344			    npg = this._nonPointGroup,
345			    originalArray = true;
346
347			if (!this._map) {
348				for (i = 0; i < l; i++) {
349					m = layersArray[i];
350
351					// Group of layers, append children to layersArray and skip.
352					if (m instanceof L.LayerGroup) {
353						if (originalArray) {
354							layersArray = layersArray.slice();
355							originalArray = false;
356						}
357						this._extractNonGroupLayers(m, layersArray);
358						l = layersArray.length;
359						continue;
360					}
361
362					this._arraySplice(this._needsClustering, m);
363					npg.removeLayer(m);
364					if (this.hasLayer(m)) {
365						this._needsRemoving.push({ layer: m, latlng: m._latlng });
366					}
367					this.fire('layerremove', { layer: m });
368				}
369				return this;
370			}
371
372			if (this._unspiderfy) {
373				this._unspiderfy();
374
375				// Work on a copy of the array, so that next loop is not affected.
376				var layersArray2 = layersArray.slice(),
377				    l2 = l;
378				for (i = 0; i < l2; i++) {
379					m = layersArray2[i];
380
381					// Group of layers, append children to layersArray and skip.
382					if (m instanceof L.LayerGroup) {
383						this._extractNonGroupLayers(m, layersArray2);
384						l2 = layersArray2.length;
385						continue;
386					}
387
388					this._unspiderfyLayer(m);
389				}
390			}
391
392			for (i = 0; i < l; i++) {
393				m = layersArray[i];
394
395				// Group of layers, append children to layersArray and skip.
396				if (m instanceof L.LayerGroup) {
397					if (originalArray) {
398						layersArray = layersArray.slice();
399						originalArray = false;
400					}
401					this._extractNonGroupLayers(m, layersArray);
402					l = layersArray.length;
403					continue;
404				}
405
406				if (!m.__parent) {
407					npg.removeLayer(m);
408					this.fire('layerremove', { layer: m });
409					continue;
410				}
411
412				this._removeLayer(m, true, true);
413				this.fire('layerremove', { layer: m });
414
415				if (fg.hasLayer(m)) {
416					fg.removeLayer(m);
417					if (m.clusterShow) {
418						m.clusterShow();
419					}
420				}
421			}
422
423			// Refresh bounds and weighted positions.
424			this._topClusterLevel._recalculateBounds();
425
426			this._refreshClustersIcons();
427
428			//Fix up the clusters and markers on the map
429			this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
430
431			return this;
432		},
433
434		//Removes all layers from the MarkerClusterGroup
435		clearLayers: function () {
436			//Need our own special implementation as the LayerGroup one doesn't work for us
437
438			//If we aren't on the map (yet), blow away the markers we know of
439			if (!this._map) {
440				this._needsClustering = [];
441				this._needsRemoving = [];
442				delete this._gridClusters;
443				delete this._gridUnclustered;
444			}
445
446			if (this._noanimationUnspiderfy) {
447				this._noanimationUnspiderfy();
448			}
449
450			//Remove all the visible layers
451			this._featureGroup.clearLayers();
452			this._nonPointGroup.clearLayers();
453
454			this.eachLayer(function (marker) {
455				marker.off(this._childMarkerEventHandlers, this);
456				delete marker.__parent;
457			}, this);
458
459			if (this._map) {
460				//Reset _topClusterLevel and the DistanceGrids
461				this._generateInitialClusters();
462			}
463
464			return this;
465		},
466
467		//Override FeatureGroup.getBounds as it doesn't work
468		getBounds: function () {
469			var bounds = new L.LatLngBounds();
470
471			if (this._topClusterLevel) {
472				bounds.extend(this._topClusterLevel._bounds);
473			}
474
475			for (var i = this._needsClustering.length - 1; i >= 0; i--) {
476				bounds.extend(this._needsClustering[i].getLatLng());
477			}
478
479			bounds.extend(this._nonPointGroup.getBounds());
480
481			return bounds;
482		},
483
484		//Overrides LayerGroup.eachLayer
485		eachLayer: function (method, context) {
486			var markers = this._needsClustering.slice(),
487				needsRemoving = this._needsRemoving,
488				thisNeedsRemoving, i, j;
489
490			if (this._topClusterLevel) {
491				this._topClusterLevel.getAllChildMarkers(markers);
492			}
493
494			for (i = markers.length - 1; i >= 0; i--) {
495				thisNeedsRemoving = true;
496
497				for (j = needsRemoving.length - 1; j >= 0; j--) {
498					if (needsRemoving[j].layer === markers[i]) {
499						thisNeedsRemoving = false;
500						break;
501					}
502				}
503
504				if (thisNeedsRemoving) {
505					method.call(context, markers[i]);
506				}
507			}
508
509			this._nonPointGroup.eachLayer(method, context);
510		},
511
512		//Overrides LayerGroup.getLayers
513		getLayers: function () {
514			var layers = [];
515			this.eachLayer(function (l) {
516				layers.push(l);
517			});
518			return layers;
519		},
520
521		//Overrides LayerGroup.getLayer, WARNING: Really bad performance
522		getLayer: function (id) {
523			var result = null;
524
525			id = parseInt(id, 10);
526
527			this.eachLayer(function (l) {
528				if (L.stamp(l) === id) {
529					result = l;
530				}
531			});
532
533			return result;
534		},
535
536		//Returns true if the given layer is in this MarkerClusterGroup
537		hasLayer: function (layer) {
538			if (!layer) {
539				return false;
540			}
541
542			var i, anArray = this._needsClustering;
543
544			for (i = anArray.length - 1; i >= 0; i--) {
545				if (anArray[i] === layer) {
546					return true;
547				}
548			}
549
550			anArray = this._needsRemoving;
551			for (i = anArray.length - 1; i >= 0; i--) {
552				if (anArray[i].layer === layer) {
553					return false;
554				}
555			}
556
557			return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer);
558		},
559
560		//Zoom down to show the given layer (spiderfying if necessary) then calls the callback
561		zoomToShowLayer: function (layer, callback) {
562
563			var map = this._map;
564
565			if (typeof callback !== 'function') {
566				callback = function () {};
567			}
568
569			var showMarker = function () {
570				// Assumes that map.hasLayer checks for direct appearance on map, not recursively calling
571				// hasLayer on Layer Groups that are on map (typically not calling this MarkerClusterGroup.hasLayer, which would always return true)
572				if ((map.hasLayer(layer) || map.hasLayer(layer.__parent)) && !this._inZoomAnimation) {
573					this._map.off('moveend', showMarker, this);
574					this.off('animationend', showMarker, this);
575
576					if (map.hasLayer(layer)) {
577						callback();
578					} else if (layer.__parent._icon) {
579						this.once('spiderfied', callback, this);
580						layer.__parent.spiderfy();
581					}
582				}
583			};
584
585			if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) {
586				//Layer is visible ond on screen, immediate return
587				callback();
588			} else if (layer.__parent._zoom < Math.round(this._map._zoom)) {
589				//Layer should be visible at this zoom level. It must not be on screen so just pan over to it
590				this._map.on('moveend', showMarker, this);
591				this._map.panTo(layer.getLatLng());
592			} else {
593				this._map.on('moveend', showMarker, this);
594				this.on('animationend', showMarker, this);
595				layer.__parent.zoomToBounds();
596			}
597		},
598
599		//Overrides FeatureGroup.onAdd
600		onAdd: function (map) {
601			this._map = map;
602			var i, l, layer;
603
604			if (!isFinite(this._map.getMaxZoom())) {
605				throw "Map has no maxZoom specified";
606			}
607
608			this._featureGroup.addTo(map);
609			this._nonPointGroup.addTo(map);
610
611			if (!this._gridClusters) {
612				this._generateInitialClusters();
613			}
614
615			this._maxLat = map.options.crs.projection.MAX_LATITUDE;
616
617			//Restore all the positions as they are in the MCG before removing them
618			for (i = 0, l = this._needsRemoving.length; i < l; i++) {
619				layer = this._needsRemoving[i];
620				layer.newlatlng = layer.layer._latlng;
621				layer.layer._latlng = layer.latlng;
622			}
623			//Remove them, then restore their new positions
624			for (i = 0, l = this._needsRemoving.length; i < l; i++) {
625				layer = this._needsRemoving[i];
626				this._removeLayer(layer.layer, true);
627				layer.layer._latlng = layer.newlatlng;
628			}
629			this._needsRemoving = [];
630
631			//Remember the current zoom level and bounds
632			this._zoom = Math.round(this._map._zoom);
633			this._currentShownBounds = this._getExpandedVisibleBounds();
634
635			this._map.on('zoomend', this._zoomEnd, this);
636			this._map.on('moveend', this._moveEnd, this);
637
638			if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
639				this._spiderfierOnAdd();
640			}
641
642			this._bindEvents();
643
644			//Actually add our markers to the map:
645			l = this._needsClustering;
646			this._needsClustering = [];
647			this.addLayers(l, true);
648		},
649
650		//Overrides FeatureGroup.onRemove
651		onRemove: function (map) {
652			map.off('zoomend', this._zoomEnd, this);
653			map.off('moveend', this._moveEnd, this);
654
655			this._unbindEvents();
656
657			//In case we are in a cluster animation
658			this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
659
660			if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
661				this._spiderfierOnRemove();
662			}
663
664			delete this._maxLat;
665
666			//Clean up all the layers we added to the map
667			this._hideCoverage();
668			this._featureGroup.remove();
669			this._nonPointGroup.remove();
670
671			this._featureGroup.clearLayers();
672
673			this._map = null;
674		},
675
676		getVisibleParent: function (marker) {
677			var vMarker = marker;
678			while (vMarker && !vMarker._icon) {
679				vMarker = vMarker.__parent;
680			}
681			return vMarker || null;
682		},
683
684		//Remove the given object from the given array
685		_arraySplice: function (anArray, obj) {
686			for (var i = anArray.length - 1; i >= 0; i--) {
687				if (anArray[i] === obj) {
688					anArray.splice(i, 1);
689					return true;
690				}
691			}
692		},
693
694		/**
695		 * Removes a marker from all _gridUnclustered zoom levels, starting at the supplied zoom.
696		 * @param marker to be removed from _gridUnclustered.
697		 * @param z integer bottom start zoom level (included)
698		 * @private
699		 */
700		_removeFromGridUnclustered: function (marker, z) {
701			var map = this._map,
702			    gridUnclustered = this._gridUnclustered,
703				minZoom = Math.floor(this._map.getMinZoom());
704
705			for (; z >= minZoom; z--) {
706				if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) {
707					break;
708				}
709			}
710		},
711
712		_childMarkerDragStart: function (e) {
713			e.target.__dragStart = e.target._latlng;
714		},
715
716		_childMarkerMoved: function (e) {
717			if (!this._ignoreMove && !e.target.__dragStart) {
718				var isPopupOpen = e.target._popup && e.target._popup.isOpen();
719
720				this._moveChild(e.target, e.oldLatLng, e.latlng);
721
722				if (isPopupOpen) {
723					e.target.openPopup();
724				}
725			}
726		},
727
728		_moveChild: function (layer, from, to) {
729			layer._latlng = from;
730			this.removeLayer(layer);
731
732			layer._latlng = to;
733			this.addLayer(layer);
734		},
735
736		_childMarkerDragEnd: function (e) {
737			var dragStart = e.target.__dragStart;
738			delete e.target.__dragStart;
739			if (dragStart) {
740				this._moveChild(e.target, dragStart, e.target._latlng);
741			}		
742		},
743
744
745		//Internal function for removing a marker from everything.
746		//dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
747		_removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) {
748			var gridClusters = this._gridClusters,
749				gridUnclustered = this._gridUnclustered,
750				fg = this._featureGroup,
751				map = this._map,
752				minZoom = Math.floor(this._map.getMinZoom());
753
754			//Remove the marker from distance clusters it might be in
755			if (removeFromDistanceGrid) {
756				this._removeFromGridUnclustered(marker, this._maxZoom);
757			}
758
759			//Work our way up the clusters removing them as we go if required
760			var cluster = marker.__parent,
761				markers = cluster._markers,
762				otherMarker;
763
764			//Remove the marker from the immediate parents marker list
765			this._arraySplice(markers, marker);
766
767			while (cluster) {
768				cluster._childCount--;
769				cluster._boundsNeedUpdate = true;
770
771				if (cluster._zoom < minZoom) {
772					//Top level, do nothing
773					break;
774				} else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required
775					//We need to push the other marker up to the parent
776					otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0];
777
778					//Update distance grid
779					gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom));
780					gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom));
781
782					//Move otherMarker up to parent
783					this._arraySplice(cluster.__parent._childClusters, cluster);
784					cluster.__parent._markers.push(otherMarker);
785					otherMarker.__parent = cluster.__parent;
786
787					if (cluster._icon) {
788						//Cluster is currently on the map, need to put the marker on the map instead
789						fg.removeLayer(cluster);
790						if (!dontUpdateMap) {
791							fg.addLayer(otherMarker);
792						}
793					}
794				} else {
795					cluster._iconNeedsUpdate = true;
796				}
797
798				cluster = cluster.__parent;
799			}
800
801			delete marker.__parent;
802		},
803
804		_isOrIsParent: function (el, oel) {
805			while (oel) {
806				if (el === oel) {
807					return true;
808				}
809				oel = oel.parentNode;
810			}
811			return false;
812		},
813
814		//Override L.Evented.fire
815		fire: function (type, data, propagate) {
816			if (data && data.layer instanceof L.MarkerCluster) {
817				//Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget)
818				if (data.originalEvent && this._isOrIsParent(data.layer._icon, data.originalEvent.relatedTarget)) {
819					return;
820				}
821				type = 'cluster' + type;
822			}
823
824			L.FeatureGroup.prototype.fire.call(this, type, data, propagate);
825		},
826
827		//Override L.Evented.listens
828		listens: function (type, propagate) {
829			return L.FeatureGroup.prototype.listens.call(this, type, propagate) || L.FeatureGroup.prototype.listens.call(this, 'cluster' + type, propagate);
830		},
831
832		//Default functionality
833		_defaultIconCreateFunction: function (cluster) {
834			var childCount = cluster.getChildCount();
835
836			var c = ' marker-cluster-';
837			if (childCount < 10) {
838				c += 'small';
839			} else if (childCount < 100) {
840				c += 'medium';
841			} else {
842				c += 'large';
843			}
844
845			return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
846		},
847
848		_bindEvents: function () {
849			var map = this._map,
850			    spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
851			    showCoverageOnHover = this.options.showCoverageOnHover,
852			    zoomToBoundsOnClick = this.options.zoomToBoundsOnClick,
853			    spiderfyOnEveryZoom = this.options.spiderfyOnEveryZoom;
854
855			//Zoom on cluster click or spiderfy if we are at the lowest level
856			if (spiderfyOnMaxZoom || zoomToBoundsOnClick || spiderfyOnEveryZoom) {
857				this.on('clusterclick clusterkeypress', this._zoomOrSpiderfy, this);
858			}
859
860			//Show convex hull (boundary) polygon on mouse over
861			if (showCoverageOnHover) {
862				this.on('clustermouseover', this._showCoverage, this);
863				this.on('clustermouseout', this._hideCoverage, this);
864				map.on('zoomend', this._hideCoverage, this);
865			}
866		},
867
868		_zoomOrSpiderfy: function (e) {
869			var cluster = e.layer,
870			    bottomCluster = cluster;
871
872			if (e.type === 'clusterkeypress' && e.originalEvent && e.originalEvent.keyCode !== 13) {
873				return;
874			}
875
876			while (bottomCluster._childClusters.length === 1) {
877				bottomCluster = bottomCluster._childClusters[0];
878			}
879
880			if (bottomCluster._zoom === this._maxZoom &&
881				bottomCluster._childCount === cluster._childCount &&
882				this.options.spiderfyOnMaxZoom) {
883
884				// All child markers are contained in a single cluster from this._maxZoom to this cluster.
885				cluster.spiderfy();
886			} else if (this.options.zoomToBoundsOnClick) {
887				cluster.zoomToBounds();
888			}
889
890			if (this.options.spiderfyOnEveryZoom) {
891				cluster.spiderfy();
892			}
893
894			// Focus the map again for keyboard users.
895			if (e.originalEvent && e.originalEvent.keyCode === 13) {
896				this._map._container.focus();
897			}
898		},
899
900		_showCoverage: function (e) {
901			var map = this._map;
902			if (this._inZoomAnimation) {
903				return;
904			}
905			if (this._shownPolygon) {
906				map.removeLayer(this._shownPolygon);
907			}
908			if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) {
909				this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions);
910				map.addLayer(this._shownPolygon);
911			}
912		},
913
914		_hideCoverage: function () {
915			if (this._shownPolygon) {
916				this._map.removeLayer(this._shownPolygon);
917				this._shownPolygon = null;
918			}
919		},
920
921		_unbindEvents: function () {
922			var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
923				showCoverageOnHover = this.options.showCoverageOnHover,
924				zoomToBoundsOnClick = this.options.zoomToBoundsOnClick,
925				spiderfyOnEveryZoom = this.options.spiderfyOnEveryZoom,
926				map = this._map;
927
928			if (spiderfyOnMaxZoom || zoomToBoundsOnClick || spiderfyOnEveryZoom) {
929				this.off('clusterclick clusterkeypress', this._zoomOrSpiderfy, this);
930			}
931			if (showCoverageOnHover) {
932				this.off('clustermouseover', this._showCoverage, this);
933				this.off('clustermouseout', this._hideCoverage, this);
934				map.off('zoomend', this._hideCoverage, this);
935			}
936		},
937
938		_zoomEnd: function () {
939			if (!this._map) { //May have been removed from the map by a zoomEnd handler
940				return;
941			}
942			this._mergeSplitClusters();
943
944			this._zoom = Math.round(this._map._zoom);
945			this._currentShownBounds = this._getExpandedVisibleBounds();
946		},
947
948		_moveEnd: function () {
949			if (this._inZoomAnimation) {
950				return;
951			}
952
953			var newBounds = this._getExpandedVisibleBounds();
954
955			this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, newBounds);
956			this._topClusterLevel._recursivelyAddChildrenToMap(null, Math.round(this._map._zoom), newBounds);
957
958			this._currentShownBounds = newBounds;
959			return;
960		},
961
962		_generateInitialClusters: function () {
963			var maxZoom = Math.ceil(this._map.getMaxZoom()),
964				minZoom = Math.floor(this._map.getMinZoom()),
965				radius = this.options.maxClusterRadius,
966				radiusFn = radius;
967
968			//If we just set maxClusterRadius to a single number, we need to create
969			//a simple function to return that number. Otherwise, we just have to
970			//use the function we've passed in.
971			if (typeof radius !== "function") {
972				radiusFn = function () { return radius; };
973			}
974
975			if (this.options.disableClusteringAtZoom !== null) {
976				maxZoom = this.options.disableClusteringAtZoom - 1;
977			}
978			this._maxZoom = maxZoom;
979			this._gridClusters = {};
980			this._gridUnclustered = {};
981
982			//Set up DistanceGrids for each zoom
983			for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
984				this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom));
985				this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom));
986			}
987
988			// Instantiate the appropriate L.MarkerCluster class (animated or not).
989			this._topClusterLevel = new this._markerCluster(this, minZoom - 1);
990		},
991
992		//Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
993		_addLayer: function (layer, zoom) {
994			var gridClusters = this._gridClusters,
995			    gridUnclustered = this._gridUnclustered,
996				minZoom = Math.floor(this._map.getMinZoom()),
997			    markerPoint, z;
998
999			if (this.options.singleMarkerMode) {
1000				this._overrideMarkerIcon(layer);
1001			}
1002
1003			layer.on(this._childMarkerEventHandlers, this);
1004
1005			//Find the lowest zoom level to slot this one in
1006			for (; zoom >= minZoom; zoom--) {
1007				markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position
1008
1009				//Try find a cluster close by
1010				var closest = gridClusters[zoom].getNearObject(markerPoint);
1011				if (closest) {
1012					closest._addChild(layer);
1013					layer.__parent = closest;
1014					return;
1015				}
1016
1017				//Try find a marker close by to form a new cluster with
1018				closest = gridUnclustered[zoom].getNearObject(markerPoint);
1019				if (closest) {
1020					var parent = closest.__parent;
1021					if (parent) {
1022						this._removeLayer(closest, false);
1023					}
1024
1025					//Create new cluster with these 2 in it
1026
1027					var newCluster = new this._markerCluster(this, zoom, closest, layer);
1028					gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom));
1029					closest.__parent = newCluster;
1030					layer.__parent = newCluster;
1031
1032					//First create any new intermediate parent clusters that don't exist
1033					var lastParent = newCluster;
1034					for (z = zoom - 1; z > parent._zoom; z--) {
1035						lastParent = new this._markerCluster(this, z, lastParent);
1036						gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z));
1037					}
1038					parent._addChild(lastParent);
1039
1040					//Remove closest from this zoom level and any above that it is in, replace with newCluster
1041					this._removeFromGridUnclustered(closest, zoom);
1042
1043					return;
1044				}
1045
1046				//Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
1047				gridUnclustered[zoom].addObject(layer, markerPoint);
1048			}
1049
1050			//Didn't get in anything, add us to the top
1051			this._topClusterLevel._addChild(layer);
1052			layer.__parent = this._topClusterLevel;
1053			return;
1054		},
1055
1056		/**
1057		 * Refreshes the icon of all "dirty" visible clusters.
1058		 * Non-visible "dirty" clusters will be updated when they are added to the map.
1059		 * @private
1060		 */
1061		_refreshClustersIcons: function () {
1062			this._featureGroup.eachLayer(function (c) {
1063				if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) {
1064					c._updateIcon();
1065				}
1066			});
1067		},
1068
1069		//Enqueue code to fire after the marker expand/contract has happened
1070		_enqueue: function (fn) {
1071			this._queue.push(fn);
1072			if (!this._queueTimeout) {
1073				this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300);
1074			}
1075		},
1076		_processQueue: function () {
1077			for (var i = 0; i < this._queue.length; i++) {
1078				this._queue[i].call(this);
1079			}
1080			this._queue.length = 0;
1081			clearTimeout(this._queueTimeout);
1082			this._queueTimeout = null;
1083		},
1084
1085		//Merge and split any existing clusters that are too big or small
1086		_mergeSplitClusters: function () {
1087			var mapZoom = Math.round(this._map._zoom);
1088
1089			//In case we are starting to split before the animation finished
1090			this._processQueue();
1091
1092			if (this._zoom < mapZoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split
1093				this._animationStart();
1094				//Remove clusters now off screen
1095				this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, this._getExpandedVisibleBounds());
1096
1097				this._animationZoomIn(this._zoom, mapZoom);
1098
1099			} else if (this._zoom > mapZoom) { //Zoom out, merge
1100				this._animationStart();
1101
1102				this._animationZoomOut(this._zoom, mapZoom);
1103			} else {
1104				this._moveEnd();
1105			}
1106		},
1107
1108		//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)
1109		_getExpandedVisibleBounds: function () {
1110			if (!this.options.removeOutsideVisibleBounds) {
1111				return this._mapBoundsInfinite;
1112			} else if (L.Browser.mobile) {
1113				return this._checkBoundsMaxLat(this._map.getBounds());
1114			}
1115
1116			return this._checkBoundsMaxLat(this._map.getBounds().pad(1)); // Padding expands the bounds by its own dimensions but scaled with the given factor.
1117		},
1118
1119		/**
1120		 * Expands the latitude to Infinity (or -Infinity) if the input bounds reach the map projection maximum defined latitude
1121		 * (in the case of Web/Spherical Mercator, it is 85.0511287798 / see https://en.wikipedia.org/wiki/Web_Mercator#Formulas).
1122		 * Otherwise, the removeOutsideVisibleBounds option will remove markers beyond that limit, whereas the same markers without
1123		 * this option (or outside MCG) will have their position floored (ceiled) by the projection and rendered at that limit,
1124		 * making the user think that MCG "eats" them and never displays them again.
1125		 * @param bounds L.LatLngBounds
1126		 * @returns {L.LatLngBounds}
1127		 * @private
1128		 */
1129		_checkBoundsMaxLat: function (bounds) {
1130			var maxLat = this._maxLat;
1131
1132			if (maxLat !== undefined) {
1133				if (bounds.getNorth() >= maxLat) {
1134					bounds._northEast.lat = Infinity;
1135				}
1136				if (bounds.getSouth() <= -maxLat) {
1137					bounds._southWest.lat = -Infinity;
1138				}
1139			}
1140
1141			return bounds;
1142		},
1143
1144		//Shared animation code
1145		_animationAddLayerNonAnimated: function (layer, newCluster) {
1146			if (newCluster === layer) {
1147				this._featureGroup.addLayer(layer);
1148			} else if (newCluster._childCount === 2) {
1149				newCluster._addToMap();
1150
1151				var markers = newCluster.getAllChildMarkers();
1152				this._featureGroup.removeLayer(markers[0]);
1153				this._featureGroup.removeLayer(markers[1]);
1154			} else {
1155				newCluster._updateIcon();
1156			}
1157		},
1158
1159		/**
1160		 * Extracts individual (i.e. non-group) layers from a Layer Group.
1161		 * @param group to extract layers from.
1162		 * @param output {Array} in which to store the extracted layers.
1163		 * @returns {*|Array}
1164		 * @private
1165		 */
1166		_extractNonGroupLayers: function (group, output) {
1167			var layers = group.getLayers(),
1168			    i = 0,
1169			    layer;
1170
1171			output = output || [];
1172
1173			for (; i < layers.length; i++) {
1174				layer = layers[i];
1175
1176				if (layer instanceof L.LayerGroup) {
1177					this._extractNonGroupLayers(layer, output);
1178					continue;
1179				}
1180
1181				output.push(layer);
1182			}
1183
1184			return output;
1185		},
1186
1187		/**
1188		 * Implements the singleMarkerMode option.
1189		 * @param layer Marker to re-style using the Clusters iconCreateFunction.
1190		 * @returns {L.Icon} The newly created icon.
1191		 * @private
1192		 */
1193		_overrideMarkerIcon: function (layer) {
1194			var icon = layer.options.icon = this.options.iconCreateFunction({
1195				getChildCount: function () {
1196					return 1;
1197				},
1198				getAllChildMarkers: function () {
1199					return [layer];
1200				}
1201			});
1202
1203			return icon;
1204		}
1205	});
1206
1207	// Constant bounds used in case option "removeOutsideVisibleBounds" is set to false.
1208	L.MarkerClusterGroup.include({
1209		_mapBoundsInfinite: new L.LatLngBounds(new L.LatLng(-Infinity, -Infinity), new L.LatLng(Infinity, Infinity))
1210	});
1211
1212	L.MarkerClusterGroup.include({
1213		_noAnimation: {
1214			//Non Animated versions of everything
1215			_animationStart: function () {
1216				//Do nothing...
1217			},
1218			_animationZoomIn: function (previousZoomLevel, newZoomLevel) {
1219				this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel);
1220				this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
1221
1222				//We didn't actually animate, but we use this event to mean "clustering animations have finished"
1223				this.fire('animationend');
1224			},
1225			_animationZoomOut: function (previousZoomLevel, newZoomLevel) {
1226				this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel);
1227				this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
1228
1229				//We didn't actually animate, but we use this event to mean "clustering animations have finished"
1230				this.fire('animationend');
1231			},
1232			_animationAddLayer: function (layer, newCluster) {
1233				this._animationAddLayerNonAnimated(layer, newCluster);
1234			}
1235		},
1236
1237		_withAnimation: {
1238			//Animated versions here
1239			_animationStart: function () {
1240				this._map._mapPane.className += ' leaflet-cluster-anim';
1241				this._inZoomAnimation++;
1242			},
1243
1244			_animationZoomIn: function (previousZoomLevel, newZoomLevel) {
1245				var bounds = this._getExpandedVisibleBounds(),
1246				    fg = this._featureGroup,
1247					minZoom = Math.floor(this._map.getMinZoom()),
1248				    i;
1249
1250				this._ignoreMove = true;
1251
1252				//Add all children of current clusters to map and remove those clusters from map
1253				this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) {
1254					var startPos = c._latlng,
1255					    markers  = c._markers,
1256					    m;
1257
1258					if (!bounds.contains(startPos)) {
1259						startPos = null;
1260					}
1261
1262					if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us
1263						fg.removeLayer(c);
1264						c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);
1265					} else {
1266						//Fade out old cluster
1267						c.clusterHide();
1268						c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds);
1269					}
1270
1271					//Remove all markers that aren't visible any more
1272					//TODO: Do we actually need to do this on the higher levels too?
1273					for (i = markers.length - 1; i >= 0; i--) {
1274						m = markers[i];
1275						if (!bounds.contains(m._latlng)) {
1276							fg.removeLayer(m);
1277						}
1278					}
1279
1280				});
1281
1282				this._forceLayout();
1283
1284				//Update opacities
1285				this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel);
1286				//TODO Maybe? Update markers in _recursivelyBecomeVisible
1287				fg.eachLayer(function (n) {
1288					if (!(n instanceof L.MarkerCluster) && n._icon) {
1289						n.clusterShow();
1290					}
1291				});
1292
1293				//update the positions of the just added clusters/markers
1294				this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {
1295					c._recursivelyRestoreChildPositions(newZoomLevel);
1296				});
1297
1298				this._ignoreMove = false;
1299
1300				//Remove the old clusters and close the zoom animation
1301				this._enqueue(function () {
1302					//update the positions of the just added clusters/markers
1303					this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) {
1304						fg.removeLayer(c);
1305						c.clusterShow();
1306					});
1307
1308					this._animationEnd();
1309				});
1310			},
1311
1312			_animationZoomOut: function (previousZoomLevel, newZoomLevel) {
1313				this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel);
1314
1315				//Need to add markers for those that weren't on the map before but are now
1316				this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
1317				//Remove markers that were on the map before but won't be now
1318				this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel, this._getExpandedVisibleBounds());
1319			},
1320
1321			_animationAddLayer: function (layer, newCluster) {
1322				var me = this,
1323				    fg = this._featureGroup;
1324
1325				fg.addLayer(layer);
1326				if (newCluster !== layer) {
1327					if (newCluster._childCount > 2) { //Was already a cluster
1328
1329						newCluster._updateIcon();
1330						this._forceLayout();
1331						this._animationStart();
1332
1333						layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));
1334						layer.clusterHide();
1335
1336						this._enqueue(function () {
1337							fg.removeLayer(layer);
1338							layer.clusterShow();
1339
1340							me._animationEnd();
1341						});
1342
1343					} else { //Just became a cluster
1344						this._forceLayout();
1345
1346						me._animationStart();
1347						me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._zoom);
1348					}
1349				}
1350			}
1351		},
1352
1353		// Private methods for animated versions.
1354		_animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) {
1355			var bounds = this._getExpandedVisibleBounds(),
1356				minZoom = Math.floor(this._map.getMinZoom());
1357
1358			//Animate all of the markers in the clusters to move to their cluster center point
1359			cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, minZoom, previousZoomLevel + 1, newZoomLevel);
1360
1361			var me = this;
1362
1363			//Update the opacity (If we immediately set it they won't animate)
1364			this._forceLayout();
1365			cluster._recursivelyBecomeVisible(bounds, newZoomLevel);
1366
1367			//TODO: Maybe use the transition timing stuff to make this more reliable
1368			//When the animations are done, tidy up
1369			this._enqueue(function () {
1370
1371				//This cluster stopped being a cluster before the timeout fired
1372				if (cluster._childCount === 1) {
1373					var m = cluster._markers[0];
1374					//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
1375					this._ignoreMove = true;
1376					m.setLatLng(m.getLatLng());
1377					this._ignoreMove = false;
1378					if (m.clusterShow) {
1379						m.clusterShow();
1380					}
1381				} else {
1382					cluster._recursively(bounds, newZoomLevel, minZoom, function (c) {
1383						c._recursivelyRemoveChildrenFromMap(bounds, minZoom, previousZoomLevel + 1);
1384					});
1385				}
1386				me._animationEnd();
1387			});
1388		},
1389
1390		_animationEnd: function () {
1391			if (this._map) {
1392				this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
1393			}
1394			this._inZoomAnimation--;
1395			this.fire('animationend');
1396		},
1397
1398		//Force a browser layout of stuff in the map
1399		// Should apply the current opacity and location to all elements so we can update them again for an animation
1400		_forceLayout: function () {
1401			//In my testing this works, infact offsetWidth of any element seems to work.
1402			//Could loop all this._layers and do this for each _icon if it stops working
1403
1404			L.Util.falseFn(document.body.offsetWidth);
1405		}
1406	});
1407
1408	L.markerClusterGroup = function (options) {
1409		return new L.MarkerClusterGroup(options);
1410	};
1411
1412	var MarkerCluster = L.MarkerCluster = L.Marker.extend({
1413		options: L.Icon.prototype.options,
1414
1415		initialize: function (group, zoom, a, b) {
1416
1417			L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0),
1418	            { icon: this, pane: group.options.clusterPane });
1419
1420			this._group = group;
1421			this._zoom = zoom;
1422
1423			this._markers = [];
1424			this._childClusters = [];
1425			this._childCount = 0;
1426			this._iconNeedsUpdate = true;
1427			this._boundsNeedUpdate = true;
1428
1429			this._bounds = new L.LatLngBounds();
1430
1431			if (a) {
1432				this._addChild(a);
1433			}
1434			if (b) {
1435				this._addChild(b);
1436			}
1437		},
1438
1439		//Recursively retrieve all child markers of this cluster
1440		getAllChildMarkers: function (storageArray, ignoreDraggedMarker) {
1441			storageArray = storageArray || [];
1442
1443			for (var i = this._childClusters.length - 1; i >= 0; i--) {
1444				this._childClusters[i].getAllChildMarkers(storageArray, ignoreDraggedMarker);
1445			}
1446
1447			for (var j = this._markers.length - 1; j >= 0; j--) {
1448				if (ignoreDraggedMarker && this._markers[j].__dragStart) {
1449					continue;
1450				}
1451				storageArray.push(this._markers[j]);
1452			}
1453
1454			return storageArray;
1455		},
1456
1457		//Returns the count of how many child markers we have
1458		getChildCount: function () {
1459			return this._childCount;
1460		},
1461
1462		//Zoom to the minimum of showing all of the child markers, or the extents of this cluster
1463		zoomToBounds: function (fitBoundsOptions) {
1464			var childClusters = this._childClusters.slice(),
1465				map = this._group._map,
1466				boundsZoom = map.getBoundsZoom(this._bounds),
1467				zoom = this._zoom + 1,
1468				mapZoom = map.getZoom(),
1469				i;
1470
1471			//calculate how far we need to zoom down to see all of the markers
1472			while (childClusters.length > 0 && boundsZoom > zoom) {
1473				zoom++;
1474				var newClusters = [];
1475				for (i = 0; i < childClusters.length; i++) {
1476					newClusters = newClusters.concat(childClusters[i]._childClusters);
1477				}
1478				childClusters = newClusters;
1479			}
1480
1481			if (boundsZoom > zoom) {
1482				this._group._map.setView(this._latlng, zoom);
1483			} else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead
1484				this._group._map.setView(this._latlng, mapZoom + 1);
1485			} else {
1486				this._group._map.fitBounds(this._bounds, fitBoundsOptions);
1487			}
1488		},
1489
1490		getBounds: function () {
1491			var bounds = new L.LatLngBounds();
1492			bounds.extend(this._bounds);
1493			return bounds;
1494		},
1495
1496		_updateIcon: function () {
1497			this._iconNeedsUpdate = true;
1498			if (this._icon) {
1499				this.setIcon(this);
1500			}
1501		},
1502
1503		//Cludge for Icon, we pretend to be an icon for performance
1504		createIcon: function () {
1505			if (this._iconNeedsUpdate) {
1506				this._iconObj = this._group.options.iconCreateFunction(this);
1507				this._iconNeedsUpdate = false;
1508			}
1509			return this._iconObj.createIcon();
1510		},
1511		createShadow: function () {
1512			return this._iconObj.createShadow();
1513		},
1514
1515
1516		_addChild: function (new1, isNotificationFromChild) {
1517
1518			this._iconNeedsUpdate = true;
1519
1520			this._boundsNeedUpdate = true;
1521			this._setClusterCenter(new1);
1522
1523			if (new1 instanceof L.MarkerCluster) {
1524				if (!isNotificationFromChild) {
1525					this._childClusters.push(new1);
1526					new1.__parent = this;
1527				}
1528				this._childCount += new1._childCount;
1529			} else {
1530				if (!isNotificationFromChild) {
1531					this._markers.push(new1);
1532				}
1533				this._childCount++;
1534			}
1535
1536			if (this.__parent) {
1537				this.__parent._addChild(new1, true);
1538			}
1539		},
1540
1541		/**
1542		 * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position.
1543		 * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet.
1544		 * @private
1545		 */
1546		_setClusterCenter: function (child) {
1547			if (!this._cLatLng) {
1548				// when clustering, take position of the first point as the cluster center
1549				this._cLatLng = child._cLatLng || child._latlng;
1550			}
1551		},
1552
1553		/**
1554		 * Assigns impossible bounding values so that the next extend entirely determines the new bounds.
1555		 * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class.
1556		 * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended.
1557		 * @private
1558		 */
1559		_resetBounds: function () {
1560			var bounds = this._bounds;
1561
1562			if (bounds._southWest) {
1563				bounds._southWest.lat = Infinity;
1564				bounds._southWest.lng = Infinity;
1565			}
1566			if (bounds._northEast) {
1567				bounds._northEast.lat = -Infinity;
1568				bounds._northEast.lng = -Infinity;
1569			}
1570		},
1571
1572		_recalculateBounds: function () {
1573			var markers = this._markers,
1574			    childClusters = this._childClusters,
1575			    latSum = 0,
1576			    lngSum = 0,
1577			    totalCount = this._childCount,
1578			    i, child, childLatLng, childCount;
1579
1580			// Case where all markers are removed from the map and we are left with just an empty _topClusterLevel.
1581			if (totalCount === 0) {
1582				return;
1583			}
1584
1585			// Reset rather than creating a new object, for performance.
1586			this._resetBounds();
1587
1588			// Child markers.
1589			for (i = 0; i < markers.length; i++) {
1590				childLatLng = markers[i]._latlng;
1591
1592				this._bounds.extend(childLatLng);
1593
1594				latSum += childLatLng.lat;
1595				lngSum += childLatLng.lng;
1596			}
1597
1598			// Child clusters.
1599			for (i = 0; i < childClusters.length; i++) {
1600				child = childClusters[i];
1601
1602				// Re-compute child bounds and weighted position first if necessary.
1603				if (child._boundsNeedUpdate) {
1604					child._recalculateBounds();
1605				}
1606
1607				this._bounds.extend(child._bounds);
1608
1609				childLatLng = child._wLatLng;
1610				childCount = child._childCount;
1611
1612				latSum += childLatLng.lat * childCount;
1613				lngSum += childLatLng.lng * childCount;
1614			}
1615
1616			this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount);
1617
1618			// Reset dirty flag.
1619			this._boundsNeedUpdate = false;
1620		},
1621
1622		//Set our markers position as given and add it to the map
1623		_addToMap: function (startPos) {
1624			if (startPos) {
1625				this._backupLatlng = this._latlng;
1626				this.setLatLng(startPos);
1627			}
1628			this._group._featureGroup.addLayer(this);
1629		},
1630
1631		_recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {
1632			this._recursively(bounds, this._group._map.getMinZoom(), maxZoom - 1,
1633				function (c) {
1634					var markers = c._markers,
1635						i, m;
1636					for (i = markers.length - 1; i >= 0; i--) {
1637						m = markers[i];
1638
1639						//Only do it if the icon is still on the map
1640						if (m._icon) {
1641							m._setPos(center);
1642							m.clusterHide();
1643						}
1644					}
1645				},
1646				function (c) {
1647					var childClusters = c._childClusters,
1648						j, cm;
1649					for (j = childClusters.length - 1; j >= 0; j--) {
1650						cm = childClusters[j];
1651						if (cm._icon) {
1652							cm._setPos(center);
1653							cm.clusterHide();
1654						}
1655					}
1656				}
1657			);
1658		},
1659
1660		_recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, mapMinZoom, previousZoomLevel, newZoomLevel) {
1661			this._recursively(bounds, newZoomLevel, mapMinZoom,
1662				function (c) {
1663					c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);
1664
1665					//TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
1666					//As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
1667					if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) {
1668						c.clusterShow();
1669						c._recursivelyRemoveChildrenFromMap(bounds, mapMinZoom, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
1670					} else {
1671						c.clusterHide();
1672					}
1673
1674					c._addToMap();
1675				}
1676			);
1677		},
1678
1679		_recursivelyBecomeVisible: function (bounds, zoomLevel) {
1680			this._recursively(bounds, this._group._map.getMinZoom(), zoomLevel, null, function (c) {
1681				c.clusterShow();
1682			});
1683		},
1684
1685		_recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {
1686			this._recursively(bounds, this._group._map.getMinZoom() - 1, zoomLevel,
1687				function (c) {
1688					if (zoomLevel === c._zoom) {
1689						return;
1690					}
1691
1692					//Add our child markers at startPos (so they can be animated out)
1693					for (var i = c._markers.length - 1; i >= 0; i--) {
1694						var nm = c._markers[i];
1695
1696						if (!bounds.contains(nm._latlng)) {
1697							continue;
1698						}
1699
1700						if (startPos) {
1701							nm._backupLatlng = nm.getLatLng();
1702
1703							nm.setLatLng(startPos);
1704							if (nm.clusterHide) {
1705								nm.clusterHide();
1706							}
1707						}
1708
1709						c._group._featureGroup.addLayer(nm);
1710					}
1711				},
1712				function (c) {
1713					c._addToMap(startPos);
1714				}
1715			);
1716		},
1717
1718		_recursivelyRestoreChildPositions: function (zoomLevel) {
1719			//Fix positions of child markers
1720			for (var i = this._markers.length - 1; i >= 0; i--) {
1721				var nm = this._markers[i];
1722				if (nm._backupLatlng) {
1723					nm.setLatLng(nm._backupLatlng);
1724					delete nm._backupLatlng;
1725				}
1726			}
1727
1728			if (zoomLevel - 1 === this._zoom) {
1729				//Reposition child clusters
1730				for (var j = this._childClusters.length - 1; j >= 0; j--) {
1731					this._childClusters[j]._restorePosition();
1732				}
1733			} else {
1734				for (var k = this._childClusters.length - 1; k >= 0; k--) {
1735					this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);
1736				}
1737			}
1738		},
1739
1740		_restorePosition: function () {
1741			if (this._backupLatlng) {
1742				this.setLatLng(this._backupLatlng);
1743				delete this._backupLatlng;
1744			}
1745		},
1746
1747		//exceptBounds: If set, don't remove any markers/clusters in it
1748		_recursivelyRemoveChildrenFromMap: function (previousBounds, mapMinZoom, zoomLevel, exceptBounds) {
1749			var m, i;
1750			this._recursively(previousBounds, mapMinZoom - 1, zoomLevel - 1,
1751				function (c) {
1752					//Remove markers at every level
1753					for (i = c._markers.length - 1; i >= 0; i--) {
1754						m = c._markers[i];
1755						if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1756							c._group._featureGroup.removeLayer(m);
1757							if (m.clusterShow) {
1758								m.clusterShow();
1759							}
1760						}
1761					}
1762				},
1763				function (c) {
1764					//Remove child clusters at just the bottom level
1765					for (i = c._childClusters.length - 1; i >= 0; i--) {
1766						m = c._childClusters[i];
1767						if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1768							c._group._featureGroup.removeLayer(m);
1769							if (m.clusterShow) {
1770								m.clusterShow();
1771							}
1772						}
1773					}
1774				}
1775			);
1776		},
1777
1778		//Run the given functions recursively to this and child clusters
1779		// boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
1780		// zoomLevelToStart: zoom level to start running functions (inclusive)
1781		// zoomLevelToStop: zoom level to stop running functions (inclusive)
1782		// runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
1783		// runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
1784		_recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) {
1785			var childClusters = this._childClusters,
1786			    zoom = this._zoom,
1787			    i, c;
1788
1789			if (zoomLevelToStart <= zoom) {
1790				if (runAtEveryLevel) {
1791					runAtEveryLevel(this);
1792				}
1793				if (runAtBottomLevel && zoom === zoomLevelToStop) {
1794					runAtBottomLevel(this);
1795				}
1796			}
1797
1798			if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) {
1799				for (i = childClusters.length - 1; i >= 0; i--) {
1800					c = childClusters[i];
1801					if (c._boundsNeedUpdate) {
1802						c._recalculateBounds();
1803					}
1804					if (boundsToApplyTo.intersects(c._bounds)) {
1805						c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
1806					}
1807				}
1808			}
1809		},
1810
1811		//Returns true if we are the parent of only one cluster and that cluster is the same as us
1812		_isSingleParent: function () {
1813			//Don't need to check this._markers as the rest won't work if there are any
1814			return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount;
1815		}
1816	});
1817
1818	/*
1819	* Extends L.Marker to include two extra methods: clusterHide and clusterShow.
1820	* 
1821	* They work as setOpacity(0) and setOpacity(1) respectively, but
1822	* don't overwrite the options.opacity
1823	* 
1824	*/
1825
1826	L.Marker.include({
1827		clusterHide: function () {
1828			var backup = this.options.opacity;
1829			this.setOpacity(0);
1830			this.options.opacity = backup;
1831			return this;
1832		},
1833		
1834		clusterShow: function () {
1835			return this.setOpacity(this.options.opacity);
1836		}
1837	});
1838
1839	L.DistanceGrid = function (cellSize) {
1840		this._cellSize = cellSize;
1841		this._sqCellSize = cellSize * cellSize;
1842		this._grid = {};
1843		this._objectPoint = { };
1844	};
1845
1846	L.DistanceGrid.prototype = {
1847
1848		addObject: function (obj, point) {
1849			var x = this._getCoord(point.x),
1850			    y = this._getCoord(point.y),
1851			    grid = this._grid,
1852			    row = grid[y] = grid[y] || {},
1853			    cell = row[x] = row[x] || [],
1854			    stamp = L.Util.stamp(obj);
1855
1856			this._objectPoint[stamp] = point;
1857
1858			cell.push(obj);
1859		},
1860
1861		updateObject: function (obj, point) {
1862			this.removeObject(obj);
1863			this.addObject(obj, point);
1864		},
1865
1866		//Returns true if the object was found
1867		removeObject: function (obj, point) {
1868			var x = this._getCoord(point.x),
1869			    y = this._getCoord(point.y),
1870			    grid = this._grid,
1871			    row = grid[y] = grid[y] || {},
1872			    cell = row[x] = row[x] || [],
1873			    i, len;
1874
1875			delete this._objectPoint[L.Util.stamp(obj)];
1876
1877			for (i = 0, len = cell.length; i < len; i++) {
1878				if (cell[i] === obj) {
1879
1880					cell.splice(i, 1);
1881
1882					if (len === 1) {
1883						delete row[x];
1884					}
1885
1886					return true;
1887				}
1888			}
1889
1890		},
1891
1892		eachObject: function (fn, context) {
1893			var i, j, k, len, row, cell, removed,
1894			    grid = this._grid;
1895
1896			for (i in grid) {
1897				row = grid[i];
1898
1899				for (j in row) {
1900					cell = row[j];
1901
1902					for (k = 0, len = cell.length; k < len; k++) {
1903						removed = fn.call(context, cell[k]);
1904						if (removed) {
1905							k--;
1906							len--;
1907						}
1908					}
1909				}
1910			}
1911		},
1912
1913		getNearObject: function (point) {
1914			var x = this._getCoord(point.x),
1915			    y = this._getCoord(point.y),
1916			    i, j, k, row, cell, len, obj, dist,
1917			    objectPoint = this._objectPoint,
1918			    closestDistSq = this._sqCellSize,
1919			    closest = null;
1920
1921			for (i = y - 1; i <= y + 1; i++) {
1922				row = this._grid[i];
1923				if (row) {
1924
1925					for (j = x - 1; j <= x + 1; j++) {
1926						cell = row[j];
1927						if (cell) {
1928
1929							for (k = 0, len = cell.length; k < len; k++) {
1930								obj = cell[k];
1931								dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point);
1932								if (dist < closestDistSq ||
1933									dist <= closestDistSq && closest === null) {
1934									closestDistSq = dist;
1935									closest = obj;
1936								}
1937							}
1938						}
1939					}
1940				}
1941			}
1942			return closest;
1943		},
1944
1945		_getCoord: function (x) {
1946			var coord = Math.floor(x / this._cellSize);
1947			return isFinite(coord) ? coord : x;
1948		},
1949
1950		_sqDist: function (p, p2) {
1951			var dx = p2.x - p.x,
1952			    dy = p2.y - p.y;
1953			return dx * dx + dy * dy;
1954		}
1955	};
1956
1957	/* Copyright (c) 2012 the authors listed at the following URL, and/or
1958	the authors of referenced articles or incorporated external code:
1959	http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256
1960
1961	Permission is hereby granted, free of charge, to any person obtaining
1962	a copy of this software and associated documentation files (the
1963	"Software"), to deal in the Software without restriction, including
1964	without limitation the rights to use, copy, modify, merge, publish,
1965	distribute, sublicense, and/or sell copies of the Software, and to
1966	permit persons to whom the Software is furnished to do so, subject to
1967	the following conditions:
1968
1969	The above copyright notice and this permission notice shall be
1970	included in all copies or substantial portions of the Software.
1971
1972	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1973	EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1974	MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1975	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1976	CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1977	TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1978	SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1979
1980	Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
1981	*/
1982
1983	(function () {
1984		L.QuickHull = {
1985
1986			/*
1987			 * @param {Object} cpt a point to be measured from the baseline
1988			 * @param {Array} bl the baseline, as represented by a two-element
1989			 *   array of latlng objects.
1990			 * @returns {Number} an approximate distance measure
1991			 */
1992			getDistant: function (cpt, bl) {
1993				var vY = bl[1].lat - bl[0].lat,
1994					vX = bl[0].lng - bl[1].lng;
1995				return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng));
1996			},
1997
1998			/*
1999			 * @param {Array} baseLine a two-element array of latlng objects
2000			 *   representing the baseline to project from
2001			 * @param {Array} latLngs an array of latlng objects
2002			 * @returns {Object} the maximum point and all new points to stay
2003			 *   in consideration for the hull.
2004			 */
2005			findMostDistantPointFromBaseLine: function (baseLine, latLngs) {
2006				var maxD = 0,
2007					maxPt = null,
2008					newPoints = [],
2009					i, pt, d;
2010
2011				for (i = latLngs.length - 1; i >= 0; i--) {
2012					pt = latLngs[i];
2013					d = this.getDistant(pt, baseLine);
2014
2015					if (d > 0) {
2016						newPoints.push(pt);
2017					} else {
2018						continue;
2019					}
2020
2021					if (d > maxD) {
2022						maxD = d;
2023						maxPt = pt;
2024					}
2025				}
2026
2027				return { maxPoint: maxPt, newPoints: newPoints };
2028			},
2029
2030
2031			/*
2032			 * Given a baseline, compute the convex hull of latLngs as an array
2033			 * of latLngs.
2034			 *
2035			 * @param {Array} latLngs
2036			 * @returns {Array}
2037			 */
2038			buildConvexHull: function (baseLine, latLngs) {
2039				var convexHullBaseLines = [],
2040					t = this.findMostDistantPointFromBaseLine(baseLine, latLngs);
2041
2042				if (t.maxPoint) { // if there is still a point "outside" the base line
2043					convexHullBaseLines =
2044						convexHullBaseLines.concat(
2045							this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints)
2046						);
2047					convexHullBaseLines =
2048						convexHullBaseLines.concat(
2049							this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints)
2050						);
2051					return convexHullBaseLines;
2052				} else {  // if there is no more point "outside" the base line, the current base line is part of the convex hull
2053					return [baseLine[0]];
2054				}
2055			},
2056
2057			/*
2058			 * Given an array of latlngs, compute a convex hull as an array
2059			 * of latlngs
2060			 *
2061			 * @param {Array} latLngs
2062			 * @returns {Array}
2063			 */
2064			getConvexHull: function (latLngs) {
2065				// find first baseline
2066				var maxLat = false, minLat = false,
2067					maxLng = false, minLng = false,
2068					maxLatPt = null, minLatPt = null,
2069					maxLngPt = null, minLngPt = null,
2070					maxPt = null, minPt = null,
2071					i;
2072
2073				for (i = latLngs.length - 1; i >= 0; i--) {
2074					var pt = latLngs[i];
2075					if (maxLat === false || pt.lat > maxLat) {
2076						maxLatPt = pt;
2077						maxLat = pt.lat;
2078					}
2079					if (minLat === false || pt.lat < minLat) {
2080						minLatPt = pt;
2081						minLat = pt.lat;
2082					}
2083					if (maxLng === false || pt.lng > maxLng) {
2084						maxLngPt = pt;
2085						maxLng = pt.lng;
2086					}
2087					if (minLng === false || pt.lng < minLng) {
2088						minLngPt = pt;
2089						minLng = pt.lng;
2090					}
2091				}
2092				
2093				if (minLat !== maxLat) {
2094					minPt = minLatPt;
2095					maxPt = maxLatPt;
2096				} else {
2097					minPt = minLngPt;
2098					maxPt = maxLngPt;
2099				}
2100
2101				var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs),
2102									this.buildConvexHull([maxPt, minPt], latLngs));
2103				return ch;
2104			}
2105		};
2106	}());
2107
2108	L.MarkerCluster.include({
2109		getConvexHull: function () {
2110			var childMarkers = this.getAllChildMarkers(),
2111				points = [],
2112				p, i;
2113
2114			for (i = childMarkers.length - 1; i >= 0; i--) {
2115				p = childMarkers[i].getLatLng();
2116				points.push(p);
2117			}
2118
2119			return L.QuickHull.getConvexHull(points);
2120		}
2121	});
2122
2123	//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
2124	//Huge thanks to jawj for implementing it first to make my job easy :-)
2125
2126	L.MarkerCluster.include({
2127
2128		_2PI: Math.PI * 2,
2129		_circleFootSeparation: 25, //related to circumference of circle
2130		_circleStartAngle: 0,
2131
2132		_spiralFootSeparation:  28, //related to size of spiral (experiment!)
2133		_spiralLengthStart: 11,
2134		_spiralLengthFactor: 5,
2135
2136		_circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards.
2137									// 0 -> always spiral; Infinity -> always circle
2138
2139		spiderfy: function () {
2140			if (this._group._spiderfied === this || this._group._inZoomAnimation) {
2141				return;
2142			}
2143
2144			var childMarkers = this.getAllChildMarkers(null, true),
2145				group = this._group,
2146				map = group._map,
2147				center = map.latLngToLayerPoint(this._latlng),
2148				positions;
2149
2150			this._group._unspiderfy();
2151			this._group._spiderfied = this;
2152
2153			//TODO Maybe: childMarkers order by distance to center
2154
2155			if (this._group.options.spiderfyShapePositions) {
2156				positions = this._group.options.spiderfyShapePositions(childMarkers.length, center);
2157			} else if (childMarkers.length >= this._circleSpiralSwitchover) {
2158				positions = this._generatePointsSpiral(childMarkers.length, center);
2159			} else {
2160				center.y += 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons.
2161				positions = this._generatePointsCircle(childMarkers.length, center);
2162			}
2163
2164			this._animationSpiderfy(childMarkers, positions);
2165		},
2166
2167		unspiderfy: function (zoomDetails) {
2168			/// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param>
2169			if (this._group._inZoomAnimation) {
2170				return;
2171			}
2172			this._animationUnspiderfy(zoomDetails);
2173
2174			this._group._spiderfied = null;
2175		},
2176
2177		_generatePointsCircle: function (count, centerPt) {
2178			var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),
2179				legLength = circumference / this._2PI,  //radius from circumference
2180				angleStep = this._2PI / count,
2181				res = [],
2182				i, angle;
2183
2184			legLength = Math.max(legLength, 35); // Minimum distance to get outside the cluster icon.
2185
2186			res.length = count;
2187
2188			for (i = 0; i < count; i++) { // Clockwise, like spiral.
2189				angle = this._circleStartAngle + i * angleStep;
2190				res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
2191			}
2192
2193			return res;
2194		},
2195
2196		_generatePointsSpiral: function (count, centerPt) {
2197			var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier,
2198				legLength = spiderfyDistanceMultiplier * this._spiralLengthStart,
2199				separation = spiderfyDistanceMultiplier * this._spiralFootSeparation,
2200				lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI,
2201				angle = 0,
2202				res = [],
2203				i;
2204
2205			res.length = count;
2206
2207			// Higher index, closer position to cluster center.
2208			for (i = count; i >= 0; i--) {
2209				// Skip the first position, so that we are already farther from center and we avoid
2210				// being under the default cluster icon (especially important for Circle Markers).
2211				if (i < count) {
2212					res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
2213				}
2214				angle += separation / legLength + i * 0.0005;
2215				legLength += lengthFactor / angle;
2216			}
2217			return res;
2218		},
2219
2220		_noanimationUnspiderfy: function () {
2221			var group = this._group,
2222				map = group._map,
2223				fg = group._featureGroup,
2224				childMarkers = this.getAllChildMarkers(null, true),
2225				m, i;
2226
2227			group._ignoreMove = true;
2228
2229			this.setOpacity(1);
2230			for (i = childMarkers.length - 1; i >= 0; i--) {
2231				m = childMarkers[i];
2232
2233				fg.removeLayer(m);
2234
2235				if (m._preSpiderfyLatlng) {
2236					m.setLatLng(m._preSpiderfyLatlng);
2237					delete m._preSpiderfyLatlng;
2238				}
2239				if (m.setZIndexOffset) {
2240					m.setZIndexOffset(0);
2241				}
2242
2243				if (m._spiderLeg) {
2244					map.removeLayer(m._spiderLeg);
2245					delete m._spiderLeg;
2246				}
2247			}
2248
2249			group.fire('unspiderfied', {
2250				cluster: this,
2251				markers: childMarkers
2252			});
2253			group._ignoreMove = false;
2254			group._spiderfied = null;
2255		}
2256	});
2257
2258	//Non Animated versions of everything
2259	L.MarkerClusterNonAnimated = L.MarkerCluster.extend({
2260		_animationSpiderfy: function (childMarkers, positions) {
2261			var group = this._group,
2262				map = group._map,
2263				fg = group._featureGroup,
2264				legOptions = this._group.options.spiderLegPolylineOptions,
2265				i, m, leg, newPos;
2266
2267			group._ignoreMove = true;
2268
2269			// Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
2270			// The reverse order trick no longer improves performance on modern browsers.
2271			for (i = 0; i < childMarkers.length; i++) {
2272				newPos = map.layerPointToLatLng(positions[i]);
2273				m = childMarkers[i];
2274
2275				// Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
2276				leg = new L.Polyline([this._latlng, newPos], legOptions);
2277				map.addLayer(leg);
2278				m._spiderLeg = leg;
2279
2280				// Now add the marker.
2281				m._preSpiderfyLatlng = m._latlng;
2282				m.setLatLng(newPos);
2283				if (m.setZIndexOffset) {
2284					m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
2285				}
2286
2287				fg.addLayer(m);
2288			}
2289			this.setOpacity(0.3);
2290
2291			group._ignoreMove = false;
2292			group.fire('spiderfied', {
2293				cluster: this,
2294				markers: childMarkers
2295			});
2296		},
2297
2298		_animationUnspiderfy: function () {
2299			this._noanimationUnspiderfy();
2300		}
2301	});
2302
2303	//Animated versions here
2304	L.MarkerCluster.include({
2305
2306		_animationSpiderfy: function (childMarkers, positions) {
2307			var me = this,
2308				group = this._group,
2309				map = group._map,
2310				fg = group._featureGroup,
2311				thisLayerLatLng = this._latlng,
2312				thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng),
2313				svg = L.Path.SVG,
2314				legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation.
2315				finalLegOpacity = legOptions.opacity,
2316				i, m, leg, legPath, legLength, newPos;
2317
2318			if (finalLegOpacity === undefined) {
2319				finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity;
2320			}
2321
2322			if (svg) {
2323				// If the initial opacity of the spider leg is not 0 then it appears before the animation starts.
2324				legOptions.opacity = 0;
2325
2326				// Add the class for CSS transitions.
2327				legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg';
2328			} else {
2329				// Make sure we have a defined opacity.
2330				legOptions.opacity = finalLegOpacity;
2331			}
2332
2333			group._ignoreMove = true;
2334
2335			// Add markers and spider legs to map, hidden at our center point.
2336			// Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
2337			// The reverse order trick no longer improves performance on modern browsers.
2338			for (i = 0; i < childMarkers.length; i++) {
2339				m = childMarkers[i];
2340
2341				newPos = map.layerPointToLatLng(positions[i]);
2342
2343				// Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
2344				leg = new L.Polyline([thisLayerLatLng, newPos], legOptions);
2345				map.addLayer(leg);
2346				m._spiderLeg = leg;
2347
2348				// Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/
2349				// In our case the transition property is declared in the CSS file.
2350				if (svg) {
2351					legPath = leg._path;
2352					legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox.
2353					legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated.
2354					legPath.style.strokeDashoffset = legLength;
2355				}
2356
2357				// If it is a marker, add it now and we'll animate it out
2358				if (m.setZIndexOffset) {
2359					m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING
2360				}
2361				if (m.clusterHide) {
2362					m.clusterHide();
2363				}
2364				
2365				// Vectors just get immediately added
2366				fg.addLayer(m);
2367
2368				if (m._setPos) {
2369					m._setPos(thisLayerPos);
2370				}
2371			}
2372
2373			group._forceLayout();
2374			group._animationStart();
2375
2376			// Reveal markers and spider legs.
2377			for (i = childMarkers.length - 1; i >= 0; i--) {
2378				newPos = map.layerPointToLatLng(positions[i]);
2379				m = childMarkers[i];
2380
2381				//Move marker to new position
2382				m._preSpiderfyLatlng = m._latlng;
2383				m.setLatLng(newPos);
2384				
2385				if (m.clusterShow) {
2386					m.clusterShow();
2387				}
2388
2389				// Animate leg (animation is actually delegated to CSS transition).
2390				if (svg) {
2391					leg = m._spiderLeg;
2392					legPath = leg._path;
2393					legPath.style.strokeDashoffset = 0;
2394					//legPath.style.strokeOpacity = finalLegOpacity;
2395					leg.setStyle({opacity: finalLegOpacity});
2396				}
2397			}
2398			this.setOpacity(0.3);
2399
2400			group._ignoreMove = false;
2401
2402			setTimeout(function () {
2403				group._animationEnd();
2404				group.fire('spiderfied', {
2405					cluster: me,
2406					markers: childMarkers
2407				});
2408			}, 200);
2409		},
2410
2411		_animationUnspiderfy: function (zoomDetails) {
2412			var me = this,
2413				group = this._group,
2414				map = group._map,
2415				fg = group._featureGroup,
2416				thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng),
2417				childMarkers = this.getAllChildMarkers(null, true),
2418				svg = L.Path.SVG,
2419				m, i, leg, legPath, legLength, nonAnimatable;
2420
2421			group._ignoreMove = true;
2422			group._animationStart();
2423
2424			//Make us visible and bring the child markers back in
2425			this.setOpacity(1);
2426			for (i = childMarkers.length - 1; i >= 0; i--) {
2427				m = childMarkers[i];
2428
2429				//Marker was added to us after we were spiderfied
2430				if (!m._preSpiderfyLatlng) {
2431					continue;
2432				}
2433
2434				//Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll
2435				m.closePopup();
2436
2437				//Fix up the location to the real one
2438				m.setLatLng(m._preSpiderfyLatlng);
2439				delete m._preSpiderfyLatlng;
2440
2441				//Hack override the location to be our center
2442				nonAnimatable = true;
2443				if (m._setPos) {
2444					m._setPos(thisLayerPos);
2445					nonAnimatable = false;
2446				}
2447				if (m.clusterHide) {
2448					m.clusterHide();
2449					nonAnimatable = false;
2450				}
2451				if (nonAnimatable) {
2452					fg.removeLayer(m);
2453				}
2454
2455				// Animate the spider leg back in (animation is actually delegated to CSS transition).
2456				if (svg) {
2457					leg = m._spiderLeg;
2458					legPath = leg._path;
2459					legLength = legPath.getTotalLength() + 0.1;
2460					legPath.style.strokeDashoffset = legLength;
2461					leg.setStyle({opacity: 0});
2462				}
2463			}
2464
2465			group._ignoreMove = false;
2466
2467			setTimeout(function () {
2468				//If we have only <= one child left then that marker will be shown on the map so don't remove it!
2469				var stillThereChildCount = 0;
2470				for (i = childMarkers.length - 1; i >= 0; i--) {
2471					m = childMarkers[i];
2472					if (m._spiderLeg) {
2473						stillThereChildCount++;
2474					}
2475				}
2476
2477
2478				for (i = childMarkers.length - 1; i >= 0; i--) {
2479					m = childMarkers[i];
2480
2481					if (!m._spiderLeg) { //Has already been unspiderfied
2482						continue;
2483					}
2484
2485					if (m.clusterShow) {
2486						m.clusterShow();
2487					}
2488					if (m.setZIndexOffset) {
2489						m.setZIndexOffset(0);
2490					}
2491
2492					if (stillThereChildCount > 1) {
2493						fg.removeLayer(m);
2494					}
2495
2496					map.removeLayer(m._spiderLeg);
2497					delete m._spiderLeg;
2498				}
2499				group._animationEnd();
2500				group.fire('unspiderfied', {
2501					cluster: me,
2502					markers: childMarkers
2503				});
2504			}, 200);
2505		}
2506	});
2507
2508
2509	L.MarkerClusterGroup.include({
2510		//The MarkerCluster currently spiderfied (if any)
2511		_spiderfied: null,
2512
2513		unspiderfy: function () {
2514			this._unspiderfy.apply(this, arguments);
2515		},
2516
2517		_spiderfierOnAdd: function () {
2518			this._map.on('click', this._unspiderfyWrapper, this);
2519
2520			if (this._map.options.zoomAnimation) {
2521				this._map.on('zoomstart', this._unspiderfyZoomStart, this);
2522			}
2523			//Browsers without zoomAnimation or a big zoom don't fire zoomstart
2524			this._map.on('zoomend', this._noanimationUnspiderfy, this);
2525
2526			if (!L.Browser.touch) {
2527				this._map.getRenderer(this);
2528				//Needs to happen in the pageload, not after, or animations don't work in webkit
2529				//  http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements
2530				//Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable
2531			}
2532		},
2533
2534		_spiderfierOnRemove: function () {
2535			this._map.off('click', this._unspiderfyWrapper, this);
2536			this._map.off('zoomstart', this._unspiderfyZoomStart, this);
2537			this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
2538			this._map.off('zoomend', this._noanimationUnspiderfy, this);
2539
2540			//Ensure that markers are back where they should be
2541			// Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane
2542			this._noanimationUnspiderfy();
2543		},
2544
2545		//On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)
2546		//This means we can define the animation they do rather than Markers doing an animation to their actual location
2547		_unspiderfyZoomStart: function () {
2548			if (!this._map) { //May have been removed from the map by a zoomEnd handler
2549				return;
2550			}
2551
2552			this._map.on('zoomanim', this._unspiderfyZoomAnim, this);
2553		},
2554
2555		_unspiderfyZoomAnim: function (zoomDetails) {
2556			//Wait until the first zoomanim after the user has finished touch-zooming before running the animation
2557			if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) {
2558				return;
2559			}
2560
2561			this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
2562			this._unspiderfy(zoomDetails);
2563		},
2564
2565		_unspiderfyWrapper: function () {
2566			/// <summary>_unspiderfy but passes no arguments</summary>
2567			this._unspiderfy();
2568		},
2569
2570		_unspiderfy: function (zoomDetails) {
2571			if (this._spiderfied) {
2572				this._spiderfied.unspiderfy(zoomDetails);
2573			}
2574		},
2575
2576		_noanimationUnspiderfy: function () {
2577			if (this._spiderfied) {
2578				this._spiderfied._noanimationUnspiderfy();
2579			}
2580		},
2581
2582		//If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc
2583		_unspiderfyLayer: function (layer) {
2584			if (layer._spiderLeg) {
2585				this._featureGroup.removeLayer(layer);
2586
2587				if (layer.clusterShow) {
2588					layer.clusterShow();
2589				}
2590					//Position will be fixed up immediately in _animationUnspiderfy
2591				if (layer.setZIndexOffset) {
2592					layer.setZIndexOffset(0);
2593				}
2594
2595				this._map.removeLayer(layer._spiderLeg);
2596				delete layer._spiderLeg;
2597			}
2598		}
2599	});
2600
2601	/**
2602	 * Adds 1 public method to MCG and 1 to L.Marker to facilitate changing
2603	 * markers' icon options and refreshing their icon and their parent clusters
2604	 * accordingly (case where their iconCreateFunction uses data of childMarkers
2605	 * to make up the cluster icon).
2606	 */
2607
2608
2609	L.MarkerClusterGroup.include({
2610		/**
2611		 * Updates the icon of all clusters which are parents of the given marker(s).
2612		 * In singleMarkerMode, also updates the given marker(s) icon.
2613		 * @param layers L.MarkerClusterGroup|L.LayerGroup|Array(L.Marker)|Map(L.Marker)|
2614		 * L.MarkerCluster|L.Marker (optional) list of markers (or single marker) whose parent
2615		 * clusters need to be updated. If not provided, retrieves all child markers of this.
2616		 * @returns {L.MarkerClusterGroup}
2617		 */
2618		refreshClusters: function (layers) {
2619			if (!layers) {
2620				layers = this._topClusterLevel.getAllChildMarkers();
2621			} else if (layers instanceof L.MarkerClusterGroup) {
2622				layers = layers._topClusterLevel.getAllChildMarkers();
2623			} else if (layers instanceof L.LayerGroup) {
2624				layers = layers._layers;
2625			} else if (layers instanceof L.MarkerCluster) {
2626				layers = layers.getAllChildMarkers();
2627			} else if (layers instanceof L.Marker) {
2628				layers = [layers];
2629			} // else: must be an Array(L.Marker)|Map(L.Marker)
2630			this._flagParentsIconsNeedUpdate(layers);
2631			this._refreshClustersIcons();
2632
2633			// In case of singleMarkerMode, also re-draw the markers.
2634			if (this.options.singleMarkerMode) {
2635				this._refreshSingleMarkerModeMarkers(layers);
2636			}
2637
2638			return this;
2639		},
2640
2641		/**
2642		 * Simply flags all parent clusters of the given markers as having a "dirty" icon.
2643		 * @param layers Array(L.Marker)|Map(L.Marker) list of markers.
2644		 * @private
2645		 */
2646		_flagParentsIconsNeedUpdate: function (layers) {
2647			var id, parent;
2648
2649			// Assumes layers is an Array or an Object whose prototype is non-enumerable.
2650			for (id in layers) {
2651				// Flag parent clusters' icon as "dirty", all the way up.
2652				// Dumb process that flags multiple times upper parents, but still
2653				// much more efficient than trying to be smart and make short lists,
2654				// at least in the case of a hierarchy following a power law:
2655				// http://jsperf.com/flag-nodes-in-power-hierarchy/2
2656				parent = layers[id].__parent;
2657				while (parent) {
2658					parent._iconNeedsUpdate = true;
2659					parent = parent.__parent;
2660				}
2661			}
2662		},
2663
2664		/**
2665		 * Re-draws the icon of the supplied markers.
2666		 * To be used in singleMarkerMode only.
2667		 * @param layers Array(L.Marker)|Map(L.Marker) list of markers.
2668		 * @private
2669		 */
2670		_refreshSingleMarkerModeMarkers: function (layers) {
2671			var id, layer;
2672
2673			for (id in layers) {
2674				layer = layers[id];
2675
2676				// Make sure we do not override markers that do not belong to THIS group.
2677				if (this.hasLayer(layer)) {
2678					// Need to re-create the icon first, then re-draw the marker.
2679					layer.setIcon(this._overrideMarkerIcon(layer));
2680				}
2681			}
2682		}
2683	});
2684
2685	L.Marker.include({
2686		/**
2687		 * Updates the given options in the marker's icon and refreshes the marker.
2688		 * @param options map object of icon options.
2689		 * @param directlyRefreshClusters boolean (optional) true to trigger
2690		 * MCG.refreshClustersOf() right away with this single marker.
2691		 * @returns {L.Marker}
2692		 */
2693		refreshIconOptions: function (options, directlyRefreshClusters) {
2694			var icon = this.options.icon;
2695
2696			L.setOptions(icon, options);
2697
2698			this.setIcon(icon);
2699
2700			// Shortcut to refresh the associated MCG clusters right away.
2701			// To be used when refreshing a single marker.
2702			// Otherwise, better use MCG.refreshClusters() once at the end with
2703			// the list of modified markers.
2704			if (directlyRefreshClusters && this.__parent) {
2705				this.__parent._group.refreshClusters(this);
2706			}
2707
2708			return this;
2709		}
2710	});
2711
2712	exports.MarkerClusterGroup = MarkerClusterGroup;
2713	exports.MarkerCluster = MarkerCluster;
2714
2715	Object.defineProperty(exports, '__esModule', { value: true });
2716
2717}));
2718//# sourceMappingURL=leaflet.markercluster-src.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.