Subversion Repositories eFlore/Applications.coel

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1827 aurelien 1
/*
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
5
*/
6
(function (window, document, undefined) {/*
7
 * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
8
 */
9
 
10
L.MarkerClusterGroup = L.FeatureGroup.extend({
11
 
12
	options: {
13
		maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center
14
		iconCreateFunction: null,
15
 
16
		spiderfyOnMaxZoom: true,
17
		showCoverageOnHover: true,
18
		zoomToBoundsOnClick: true,
19
		singleMarkerMode: false,
20
 
21
		disableClusteringAtZoom: null,
22
 
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,
26
 
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,
30
 
31
		//Increase to increase the distance away that spiderfied markers appear from the center
32
		spiderfyDistanceMultiplier: 1,
33
 
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)
39
 
40
		//Options to pass to the L.Polygon constructor
41
		polygonOptions: {}
42
	},
43
 
44
	initialize: function (options) {
45
		L.Util.setOptions(this, options);
46
		if (!this.options.iconCreateFunction) {
47
			this.options.iconCreateFunction = this._defaultIconCreateFunction;
48
		}
49
 
50
		this._featureGroup = L.featureGroup();
51
		this._featureGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
52
 
53
		this._nonPointGroup = L.featureGroup();
54
		this._nonPointGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
55
 
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;
61
 
62
		this._queue = [];
63
	},
64
 
65
	addLayer: function (layer) {
66
 
67
		if (layer instanceof L.LayerGroup) {
68
			var array = [];
69
			for (var i in layer._layers) {
70
				array.push(layer._layers[i]);
71
			}
72
			return this.addLayers(array);
73
		}
74
 
75
		//Don't cluster non point data
76
		if (!layer.getLatLng) {
77
			this._nonPointGroup.addLayer(layer);
78
			return this;
79
		}
80
 
81
		if (!this._map) {
82
			this._needsClustering.push(layer);
83
			return this;
84
		}
85
 
86
		if (this.hasLayer(layer)) {
87
			return this;
88
		}
89
 
90
 
91
		//If we have already clustered we'll need to add this one to a cluster
92
 
93
		if (this._unspiderfy) {
94
			this._unspiderfy();
95
		}
96
 
97
		this._addLayer(layer, this._maxZoom);
98
 
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;
105
			}
106
		}
107
 
108
		if (this._currentShownBounds.contains(visibleLayer.getLatLng())) {
109
			if (this.options.animateAddingMarkers) {
110
				this._animationAddLayer(layer, visibleLayer);
111
			} else {
112
				this._animationAddLayerNonAnimated(layer, visibleLayer);
113
			}
114
		}
115
		return this;
116
	},
117
 
118
	removeLayer: function (layer) {
119
 
120
		if (layer instanceof L.LayerGroup)
121
		{
122
			var array = [];
123
			for (var i in layer._layers) {
124
				array.push(layer._layers[i]);
125
			}
126
			return this.removeLayers(array);
127
		}
128
 
129
		//Non point layers
130
		if (!layer.getLatLng) {
131
			this._nonPointGroup.removeLayer(layer);
132
			return this;
133
		}
134
 
135
		if (!this._map) {
136
			if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) {
137
				this._needsRemoving.push(layer);
138
			}
139
			return this;
140
		}
141
 
142
		if (!layer.__parent) {
143
			return this;
144
		}
145
 
146
		if (this._unspiderfy) {
147
			this._unspiderfy();
148
			this._unspiderfyLayer(layer);
149
		}
150
 
151
		//Remove the marker from clusters
152
		this._removeLayer(layer, true);
153
 
154
		if (this._featureGroup.hasLayer(layer)) {
155
			this._featureGroup.removeLayer(layer);
156
			if (layer.setOpacity) {
157
				layer.setOpacity(1);
158
			}
159
		}
160
 
161
		return this;
162
	},
163
 
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,
171
			newMarkers, i, l, m;
172
 
173
		if (this._map) {
174
			var offset = 0,
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 :-)
184
						}
185
					}
186
 
187
					m = layersArray[offset];
188
 
189
					//Not point data, can't be clustered
190
					if (!m.getLatLng) {
191
						npg.addLayer(m);
192
						continue;
193
					}
194
 
195
					if (this.hasLayer(m)) {
196
						continue;
197
					}
198
 
199
					this._addLayer(m, this._maxZoom);
200
 
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
202
					if (m.__parent) {
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);
207
						}
208
					}
209
				}
210
 
211
				if (chunkProgress) {
212
					// report progress and time elapsed:
213
					chunkProgress(offset, layersArray.length, (new Date()).getTime() - started);
214
				}
215
 
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) {
220
							c._updateIcon();
221
						}
222
					});
223
 
224
					this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
225
				} else {
226
					setTimeout(process, this.options.chunkDelay);
227
				}
228
			}, this);
229
 
230
			process();
231
		} else {
232
			newMarkers = [];
233
			for (i = 0, l = layersArray.length; i < l; i++) {
234
				m = layersArray[i];
235
 
236
				//Not point data, can't be clustered
237
				if (!m.getLatLng) {
238
					npg.addLayer(m);
239
					continue;
240
				}
241
 
242
				if (this.hasLayer(m)) {
243
					continue;
244
				}
245
 
246
				newMarkers.push(m);
247
			}
248
			this._needsClustering = this._needsClustering.concat(newMarkers);
249
		}
250
		return this;
251
	},
252
 
253
	//Takes an array of markers and removes them in bulk
254
	removeLayers: function (layersArray) {
255
		var i, l, m,
256
			fg = this._featureGroup,
257
			npg = this._nonPointGroup;
258
 
259
		if (!this._map) {
260
			for (i = 0, l = layersArray.length; i < l; i++) {
261
				m = layersArray[i];
262
				this._arraySplice(this._needsClustering, m);
263
				npg.removeLayer(m);
264
			}
265
			return this;
266
		}
267
 
268
		for (i = 0, l = layersArray.length; i < l; i++) {
269
			m = layersArray[i];
270
 
271
			if (!m.__parent) {
272
				npg.removeLayer(m);
273
				continue;
274
			}
275
 
276
			this._removeLayer(m, true, true);
277
 
278
			if (fg.hasLayer(m)) {
279
				fg.removeLayer(m);
280
				if (m.setOpacity) {
281
					m.setOpacity(1);
282
				}
283
			}
284
		}
285
 
286
		//Fix up the clusters and markers on the map
287
		this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
288
 
289
		fg.eachLayer(function (c) {
290
			if (c instanceof L.MarkerCluster) {
291
				c._updateIcon();
292
			}
293
		});
294
 
295
		return this;
296
	},
297
 
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
301
 
302
		//If we aren't on the map (yet), blow away the markers we know of
303
		if (!this._map) {
304
			this._needsClustering = [];
305
			delete this._gridClusters;
306
			delete this._gridUnclustered;
307
		}
308
 
309
		if (this._noanimationUnspiderfy) {
310
			this._noanimationUnspiderfy();
311
		}
312
 
313
		//Remove all the visible layers
314
		this._featureGroup.clearLayers();
315
		this._nonPointGroup.clearLayers();
316
 
317
		this.eachLayer(function (marker) {
318
			delete marker.__parent;
319
		});
320
 
321
		if (this._map) {
322
			//Reset _topClusterLevel and the DistanceGrids
323
			this._generateInitialClusters();
324
		}
325
 
326
		return this;
327
	},
328
 
329
	//Override FeatureGroup.getBounds as it doesn't work
330
	getBounds: function () {
331
		var bounds = new L.LatLngBounds();
332
 
333
		if (this._topClusterLevel) {
334
			bounds.extend(this._topClusterLevel._bounds);
335
		}
336
 
337
		for (var i = this._needsClustering.length - 1; i >= 0; i--) {
338
			bounds.extend(this._needsClustering[i].getLatLng());
339
		}
340
 
341
		bounds.extend(this._nonPointGroup.getBounds());
342
 
343
		return bounds;
344
	},
345
 
346
	//Overrides LayerGroup.eachLayer
347
	eachLayer: function (method, context) {
348
		var markers = this._needsClustering.slice(),
349
			i;
350
 
351
		if (this._topClusterLevel) {
352
			this._topClusterLevel.getAllChildMarkers(markers);
353
		}
354
 
355
		for (i = markers.length - 1; i >= 0; i--) {
356
			method.call(context, markers[i]);
357
		}
358
 
359
		this._nonPointGroup.eachLayer(method, context);
360
	},
361
 
362
	//Overrides LayerGroup.getLayers
363
	getLayers: function () {
364
		var layers = [];
365
		this.eachLayer(function (l) {
366
			layers.push(l);
367
		});
368
		return layers;
369
	},
370
 
371
	//Overrides LayerGroup.getLayer, WARNING: Really bad performance
372
	getLayer: function (id) {
373
		var result = null;
374
 
375
		this.eachLayer(function (l) {
376
			if (L.stamp(l) === id) {
377
				result = l;
378
			}
379
		});
380
 
381
		return result;
382
	},
383
 
384
	//Returns true if the given layer is in this MarkerClusterGroup
385
	hasLayer: function (layer) {
386
		if (!layer) {
387
			return false;
388
		}
389
 
390
		var i, anArray = this._needsClustering;
391
 
392
		for (i = anArray.length - 1; i >= 0; i--) {
393
			if (anArray[i] === layer) {
394
				return true;
395
			}
396
		}
397
 
398
		anArray = this._needsRemoving;
399
		for (i = anArray.length - 1; i >= 0; i--) {
400
			if (anArray[i] === layer) {
401
				return false;
402
			}
403
		}
404
 
405
		return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer);
406
	},
407
 
408
	//Zoom down to show the given layer (spiderfying if necessary) then calls the callback
409
	zoomToShowLayer: function (layer, callback) {
410
 
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);
415
 
416
				if (layer._icon) {
417
					callback();
418
				} else if (layer.__parent._icon) {
419
					var afterSpiderfy = function () {
420
						this.off('spiderfied', afterSpiderfy, this);
421
						callback();
422
					};
423
 
424
					this.on('spiderfied', afterSpiderfy, this);
425
					layer.__parent.spiderfy();
426
				}
427
			}
428
		};
429
 
430
		if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) {
431
			//Layer is visible ond on screen, immediate return
432
			callback();
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());
437
		} else {
438
			var moveStart = function () {
439
				this._map.off('movestart', moveStart, this);
440
				moveStart = null;
441
			};
442
 
443
			this._map.on('movestart', moveStart, this);
444
			this._map.on('moveend', showMarker, this);
445
			this.on('animationend', showMarker, this);
446
			layer.__parent.zoomToBounds();
447
 
448
			if (moveStart) {
449
				//Never started moving, must already be there, probably need clustering however
450
				showMarker.call(this);
451
			}
452
		}
453
	},
454
 
455
	//Overrides FeatureGroup.onAdd
456
	onAdd: function (map) {
457
		this._map = map;
458
		var i, l, layer;
459
 
460
		if (!isFinite(this._map.getMaxZoom())) {
461
			throw "Map has no maxZoom specified";
462
		}
463
 
464
		this._featureGroup.onAdd(map);
465
		this._nonPointGroup.onAdd(map);
466
 
467
		if (!this._gridClusters) {
468
			this._generateInitialClusters();
469
		}
470
 
471
		for (i = 0, l = this._needsRemoving.length; i < l; i++) {
472
			layer = this._needsRemoving[i];
473
			this._removeLayer(layer, true);
474
		}
475
		this._needsRemoving = [];
476
 
477
		//Remember the current zoom level and bounds
478
		this._zoom = this._map.getZoom();
479
		this._currentShownBounds = this._getExpandedVisibleBounds();
480
 
481
		this._map.on('zoomend', this._zoomEnd, this);
482
		this._map.on('moveend', this._moveEnd, this);
483
 
484
		if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
485
			this._spiderfierOnAdd();
486
		}
487
 
488
		this._bindEvents();
489
 
490
		//Actually add our markers to the map:
491
		l = this._needsClustering;
492
		this._needsClustering = [];
493
		this.addLayers(l);
494
	},
495
 
496
	//Overrides FeatureGroup.onRemove
497
	onRemove: function (map) {
498
		map.off('zoomend', this._zoomEnd, this);
499
		map.off('moveend', this._moveEnd, this);
500
 
501
		this._unbindEvents();
502
 
503
		//In case we are in a cluster animation
504
		this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
505
 
506
		if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
507
			this._spiderfierOnRemove();
508
		}
509
 
510
 
511
 
512
		//Clean up all the layers we added to the map
513
		this._hideCoverage();
514
		this._featureGroup.onRemove(map);
515
		this._nonPointGroup.onRemove(map);
516
 
517
		this._featureGroup.clearLayers();
518
 
519
		this._map = null;
520
	},
521
 
522
	getVisibleParent: function (marker) {
523
		var vMarker = marker;
524
		while (vMarker && !vMarker._icon) {
525
			vMarker = vMarker.__parent;
526
		}
527
		return vMarker || null;
528
	},
529
 
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);
535
				return true;
536
			}
537
		}
538
	},
539
 
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,
546
			map = this._map;
547
 
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))) {
552
					break;
553
				}
554
			}
555
		}
556
 
557
		//Work our way up the clusters removing them as we go if required
558
		var cluster = marker.__parent,
559
			markers = cluster._markers,
560
			otherMarker;
561
 
562
		//Remove the marker from the immediate parents marker list
563
		this._arraySplice(markers, marker);
564
 
565
		while (cluster) {
566
			cluster._childCount--;
567
 
568
			if (cluster._zoom < 0) {
569
				//Top level, do nothing
570
				break;
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];
574
 
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));
578
 
579
				//Move otherMarker up to parent
580
				this._arraySplice(cluster.__parent._childClusters, cluster);
581
				cluster.__parent._markers.push(otherMarker);
582
				otherMarker.__parent = cluster.__parent;
583
 
584
				if (cluster._icon) {
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);
589
					}
590
				}
591
			} else {
592
				cluster._recalculateBounds();
593
				if (!dontUpdateMap || !cluster._icon) {
594
					cluster._updateIcon();
595
				}
596
			}
597
 
598
			cluster = cluster.__parent;
599
		}
600
 
601
		delete marker.__parent;
602
	},
603
 
604
	_isOrIsParent: function (el, oel) {
605
		while (oel) {
606
			if (el === oel) {
607
				return true;
608
			}
609
			oel = oel.parentNode;
610
		}
611
		return false;
612
	},
613
 
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)) {
618
				return;
619
			}
620
			e.type = 'cluster' + e.type;
621
		}
622
 
623
		this.fire(e.type, e);
624
	},
625
 
626
	//Default functionality
627
	_defaultIconCreateFunction: function (cluster) {
628
		var childCount = cluster.getChildCount();
629
 
630
		var c = ' marker-cluster-';
631
		if (childCount < 10) {
632
			c += 'small';
633
		} else if (childCount < 100) {
634
			c += 'medium';
635
		} else {
636
			c += 'large';
637
		}
638
 
639
		return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
640
	},
641
 
642
	_bindEvents: function () {
643
		var map = this._map,
644
		    spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
645
		    showCoverageOnHover = this.options.showCoverageOnHover,
646
		    zoomToBoundsOnClick = this.options.zoomToBoundsOnClick;
647
 
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);
651
		}
652
 
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);
658
		}
659
	},
660
 
661
	_zoomOrSpiderfy: function (e) {
662
		var map = this._map;
663
		if (map.getMaxZoom() === map.getZoom()) {
664
			if (this.options.spiderfyOnMaxZoom) {
665
				e.layer.spiderfy();
666
			}
667
		} else if (this.options.zoomToBoundsOnClick) {
668
			e.layer.zoomToBounds();
669
		}
670
 
671
		// Focus the map again for keyboard users.
672
		if (e.originalEvent && e.originalEvent.keyCode === 13) {
673
			map._container.focus();
674
		}
675
	},
676
 
677
	_showCoverage: function (e) {
678
		var map = this._map;
679
		if (this._inZoomAnimation) {
680
			return;
681
		}
682
		if (this._shownPolygon) {
683
			map.removeLayer(this._shownPolygon);
684
		}
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);
688
		}
689
	},
690
 
691
	_hideCoverage: function () {
692
		if (this._shownPolygon) {
693
			this._map.removeLayer(this._shownPolygon);
694
			this._shownPolygon = null;
695
		}
696
	},
697
 
698
	_unbindEvents: function () {
699
		var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
700
			showCoverageOnHover = this.options.showCoverageOnHover,
701
			zoomToBoundsOnClick = this.options.zoomToBoundsOnClick,
702
			map = this._map;
703
 
704
		if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
705
			this.off('clusterclick', this._zoomOrSpiderfy, this);
706
		}
707
		if (showCoverageOnHover) {
708
			this.off('clustermouseover', this._showCoverage, this);
709
			this.off('clustermouseout', this._hideCoverage, this);
710
			map.off('zoomend', this._hideCoverage, this);
711
		}
712
	},
713
 
714
	_zoomEnd: function () {
715
		if (!this._map) { //May have been removed from the map by a zoomEnd handler
716
			return;
717
		}
718
		this._mergeSplitClusters();
719
 
720
		this._zoom = this._map._zoom;
721
		this._currentShownBounds = this._getExpandedVisibleBounds();
722
	},
723
 
724
	_moveEnd: function () {
725
		if (this._inZoomAnimation) {
726
			return;
727
		}
728
 
729
		var newBounds = this._getExpandedVisibleBounds();
730
 
731
		this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds);
732
		this._topClusterLevel._recursivelyAddChildrenToMap(null, this._map._zoom, newBounds);
733
 
734
		this._currentShownBounds = newBounds;
735
		return;
736
	},
737
 
738
	_generateInitialClusters: function () {
739
		var maxZoom = this._map.getMaxZoom(),
740
			radius = this.options.maxClusterRadius,
741
			radiusFn = radius;
742
 
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; };
748
		}
749
 
750
		if (this.options.disableClusteringAtZoom) {
751
			maxZoom = this.options.disableClusteringAtZoom - 1;
752
		}
753
		this._maxZoom = maxZoom;
754
		this._gridClusters = {};
755
		this._gridUnclustered = {};
756
 
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));
761
		}
762
 
763
		this._topClusterLevel = new L.MarkerCluster(this, -1);
764
	},
765
 
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,
770
		    markerPoint, z;
771
 
772
		if (this.options.singleMarkerMode) {
773
			layer.options.icon = this.options.iconCreateFunction({
774
				getChildCount: function () {
775
					return 1;
776
				},
777
				getAllChildMarkers: function () {
778
					return [layer];
779
				}
780
			});
781
		}
782
 
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
786
 
787
			//Try find a cluster close by
788
			var closest = gridClusters[zoom].getNearObject(markerPoint);
789
			if (closest) {
790
				closest._addChild(layer);
791
				layer.__parent = closest;
792
				return;
793
			}
794
 
795
			//Try find a marker close by to form a new cluster with
796
			closest = gridUnclustered[zoom].getNearObject(markerPoint);
797
			if (closest) {
798
				var parent = closest.__parent;
799
				if (parent) {
800
					this._removeLayer(closest, false);
801
				}
802
 
803
				//Create new cluster with these 2 in it
804
 
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;
809
 
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));
815
				}
816
				parent._addChild(lastParent);
817
 
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))) {
821
						break;
822
					}
823
				}
824
 
825
				return;
826
			}
827
 
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);
830
		}
831
 
832
		//Didn't get in anything, add us to the top
833
		this._topClusterLevel._addChild(layer);
834
		layer.__parent = this._topClusterLevel;
835
		return;
836
	},
837
 
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);
843
		}
844
	},
845
	_processQueue: function () {
846
		for (var i = 0; i < this._queue.length; i++) {
847
			this._queue[i].call(this);
848
		}
849
		this._queue.length = 0;
850
		clearTimeout(this._queueTimeout);
851
		this._queueTimeout = null;
852
	},
853
 
854
	//Merge and split any existing clusters that are too big or small
855
	_mergeSplitClusters: function () {
856
 
857
		//Incase we are starting to split before the animation finished
858
		this._processQueue();
859
 
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());
864
 
865
			this._animationZoomIn(this._zoom, this._map._zoom);
866
 
867
		} else if (this._zoom > this._map._zoom) { //Zoom out, merge
868
			this._animationStart();
869
 
870
			this._animationZoomOut(this._zoom, this._map._zoom);
871
		} else {
872
			this._moveEnd();
873
		}
874
	},
875
 
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.getBounds();
880
		}
881
 
882
		var map = this._map,
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);
888
 
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));
892
	},
893
 
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();
900
 
901
			var markers = newCluster.getAllChildMarkers();
902
			this._featureGroup.removeLayer(markers[0]);
903
			this._featureGroup.removeLayer(markers[1]);
904
		} else {
905
			newCluster._updateIcon();
906
		}
907
	}
908
});
909
 
910
L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
911
 
912
	//Non Animated versions of everything
913
	_animationStart: function () {
914
		//Do nothing...
915
	},
916
	_animationZoomIn: function (previousZoomLevel, newZoomLevel) {
917
		this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
918
		this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
919
 
920
		//We didn't actually animate, but we use this event to mean "clustering animations have finished"
921
		this.fire('animationend');
922
	},
923
	_animationZoomOut: function (previousZoomLevel, newZoomLevel) {
924
		this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
925
		this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
926
 
927
		//We didn't actually animate, but we use this event to mean "clustering animations have finished"
928
		this.fire('animationend');
929
	},
930
	_animationAddLayer: function (layer, newCluster) {
931
		this._animationAddLayerNonAnimated(layer, newCluster);
932
	}
933
} : {
934
 
935
	//Animated versions here
936
	_animationStart: function () {
937
		this._map._mapPane.className += ' leaflet-cluster-anim';
938
		this._inZoomAnimation++;
939
	},
940
	_animationEnd: function () {
941
		if (this._map) {
942
			this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
943
		}
944
		this._inZoomAnimation--;
945
		this.fire('animationend');
946
	},
947
	_animationZoomIn: function (previousZoomLevel, newZoomLevel) {
948
		var bounds = this._getExpandedVisibleBounds(),
949
		    fg = this._featureGroup,
950
		    i;
951
 
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,
956
				m;
957
 
958
			if (!bounds.contains(startPos)) {
959
				startPos = null;
960
			}
961
 
962
			if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us
963
				fg.removeLayer(c);
964
				c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);
965
			} else {
966
				//Fade out old cluster
967
				c.setOpacity(0);
968
				c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds);
969
			}
970
 
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--) {
974
				m = markers[i];
975
				if (!bounds.contains(m._latlng)) {
976
					fg.removeLayer(m);
977
				}
978
			}
979
 
980
		});
981
 
982
		this._forceLayout();
983
 
984
		//Update opacities
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) {
989
				n.setOpacity(1);
990
			}
991
		});
992
 
993
		//update the positions of the just added clusters/markers
994
		this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {
995
			c._recursivelyRestoreChildPositions(newZoomLevel);
996
		});
997
 
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) {
1002
				fg.removeLayer(c);
1003
				c.setOpacity(1);
1004
			});
1005
 
1006
			this._animationEnd();
1007
		});
1008
	},
1009
 
1010
	_animationZoomOut: function (previousZoomLevel, newZoomLevel) {
1011
		this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel);
1012
 
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());
1017
	},
1018
	_animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) {
1019
		var bounds = this._getExpandedVisibleBounds();
1020
 
1021
		//Animate all of the markers in the clusters to move to their cluster center point
1022
		cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel);
1023
 
1024
		var me = this;
1025
 
1026
		//Update the opacity (If we immediately set it they won't animate)
1027
		this._forceLayout();
1028
		cluster._recursivelyBecomeVisible(bounds, newZoomLevel);
1029
 
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 () {
1033
 
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());
1039
				if (m.setOpacity) {
1040
					m.setOpacity(1);
1041
				}
1042
			} else {
1043
				cluster._recursively(bounds, newZoomLevel, 0, function (c) {
1044
					c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1);
1045
				});
1046
			}
1047
			me._animationEnd();
1048
		});
1049
	},
1050
	_animationAddLayer: function (layer, newCluster) {
1051
		var me = this,
1052
			fg = this._featureGroup;
1053
 
1054
		fg.addLayer(layer);
1055
		if (newCluster !== layer) {
1056
			if (newCluster._childCount > 2) { //Was already a cluster
1057
 
1058
				newCluster._updateIcon();
1059
				this._forceLayout();
1060
				this._animationStart();
1061
 
1062
				layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));
1063
				layer.setOpacity(0);
1064
 
1065
				this._enqueue(function () {
1066
					fg.removeLayer(layer);
1067
					layer.setOpacity(1);
1068
 
1069
					me._animationEnd();
1070
				});
1071
 
1072
			} else { //Just became a cluster
1073
				this._forceLayout();
1074
 
1075
				me._animationStart();
1076
				me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom());
1077
			}
1078
		}
1079
	},
1080
 
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
1086
 
1087
		L.Util.falseFn(document.body.offsetWidth);
1088
	}
1089
});
1090
 
1091
L.markerClusterGroup = function (options) {
1092
	return new L.MarkerClusterGroup(options);
1093
};
1094
 
1095
 
1096
L.MarkerCluster = L.Marker.extend({
1097
	initialize: function (group, zoom, a, b) {
1098
 
1099
		L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this });
1100
 
1101
 
1102
		this._group = group;
1103
		this._zoom = zoom;
1104
 
1105
		this._markers = [];
1106
		this._childClusters = [];
1107
		this._childCount = 0;
1108
		this._iconNeedsUpdate = true;
1109
 
1110
		this._bounds = new L.LatLngBounds();
1111
 
1112
		if (a) {
1113
			this._addChild(a);
1114
		}
1115
		if (b) {
1116
			this._addChild(b);
1117
		}
1118
	},
1119
 
1120
	//Recursively retrieve all child markers of this cluster
1121
	getAllChildMarkers: function (storageArray) {
1122
		storageArray = storageArray || [];
1123
 
1124
		for (var i = this._childClusters.length - 1; i >= 0; i--) {
1125
			this._childClusters[i].getAllChildMarkers(storageArray);
1126
		}
1127
 
1128
		for (var j = this._markers.length - 1; j >= 0; j--) {
1129
			storageArray.push(this._markers[j]);
1130
		}
1131
 
1132
		return storageArray;
1133
	},
1134
 
1135
	//Returns the count of how many child markers we have
1136
	getChildCount: function () {
1137
		return this._childCount;
1138
	},
1139
 
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(),
1147
			i;
1148
 
1149
		//calculate how far we need to zoom down to see all of the markers
1150
		while (childClusters.length > 0 && boundsZoom > zoom) {
1151
			zoom++;
1152
			var newClusters = [];
1153
			for (i = 0; i < childClusters.length; i++) {
1154
				newClusters = newClusters.concat(childClusters[i]._childClusters);
1155
			}
1156
			childClusters = newClusters;
1157
		}
1158
 
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);
1163
		} else {
1164
			this._group._map.fitBounds(this._bounds);
1165
		}
1166
	},
1167
 
1168
	getBounds: function () {
1169
		var bounds = new L.LatLngBounds();
1170
		bounds.extend(this._bounds);
1171
		return bounds;
1172
	},
1173
 
1174
	_updateIcon: function () {
1175
		this._iconNeedsUpdate = true;
1176
		if (this._icon) {
1177
			this.setIcon(this);
1178
		}
1179
	},
1180
 
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;
1186
		}
1187
		return this._iconObj.createIcon();
1188
	},
1189
	createShadow: function () {
1190
		return this._iconObj.createShadow();
1191
	},
1192
 
1193
 
1194
	_addChild: function (new1, isNotificationFromChild) {
1195
 
1196
		this._iconNeedsUpdate = true;
1197
		this._expandBounds(new1);
1198
 
1199
		if (new1 instanceof L.MarkerCluster) {
1200
			if (!isNotificationFromChild) {
1201
				this._childClusters.push(new1);
1202
				new1.__parent = this;
1203
			}
1204
			this._childCount += new1._childCount;
1205
		} else {
1206
			if (!isNotificationFromChild) {
1207
				this._markers.push(new1);
1208
			}
1209
			this._childCount++;
1210
		}
1211
 
1212
		if (this.__parent) {
1213
			this.__parent._addChild(new1, true);
1214
		}
1215
	},
1216
 
1217
	//Expand our bounds and tell our parent to
1218
	_expandBounds: function (marker) {
1219
		var addedCount,
1220
		    addedLatLng = marker._wLatLng || marker._latlng;
1221
 
1222
		if (marker instanceof L.MarkerCluster) {
1223
			this._bounds.extend(marker._bounds);
1224
			addedCount = marker._childCount;
1225
		} else {
1226
			this._bounds.extend(addedLatLng);
1227
			addedCount = 1;
1228
		}
1229
 
1230
		if (!this._cLatLng) {
1231
			// when clustering, take position of the first point as the cluster center
1232
			this._cLatLng = marker._cLatLng || addedLatLng;
1233
		}
1234
 
1235
		// when showing clusters, take weighted average of all points as cluster center
1236
		var totalCount = this._childCount + addedCount;
1237
 
1238
		//Calculate weighted latlng for display
1239
		if (!this._wLatLng) {
1240
			this._latlng = this._wLatLng = new L.LatLng(addedLatLng.lat, addedLatLng.lng);
1241
		} else {
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;
1244
		}
1245
	},
1246
 
1247
	//Set our markers position as given and add it to the map
1248
	_addToMap: function (startPos) {
1249
		if (startPos) {
1250
			this._backupLatlng = this._latlng;
1251
			this.setLatLng(startPos);
1252
		}
1253
		this._group._featureGroup.addLayer(this);
1254
	},
1255
 
1256
	_recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {
1257
		this._recursively(bounds, 0, maxZoom - 1,
1258
			function (c) {
1259
				var markers = c._markers,
1260
					i, m;
1261
				for (i = markers.length - 1; i >= 0; i--) {
1262
					m = markers[i];
1263
 
1264
					//Only do it if the icon is still on the map
1265
					if (m._icon) {
1266
						m._setPos(center);
1267
						m.setOpacity(0);
1268
					}
1269
				}
1270
			},
1271
			function (c) {
1272
				var childClusters = c._childClusters,
1273
					j, cm;
1274
				for (j = childClusters.length - 1; j >= 0; j--) {
1275
					cm = childClusters[j];
1276
					if (cm._icon) {
1277
						cm._setPos(center);
1278
						cm.setOpacity(0);
1279
					}
1280
				}
1281
			}
1282
		);
1283
	},
1284
 
1285
	_recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, previousZoomLevel, newZoomLevel) {
1286
		this._recursively(bounds, newZoomLevel, 0,
1287
			function (c) {
1288
				c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);
1289
 
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) {
1293
					c.setOpacity(1);
1294
					c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
1295
				} else {
1296
					c.setOpacity(0);
1297
				}
1298
 
1299
				c._addToMap();
1300
			}
1301
		);
1302
	},
1303
 
1304
	_recursivelyBecomeVisible: function (bounds, zoomLevel) {
1305
		this._recursively(bounds, 0, zoomLevel, null, function (c) {
1306
			c.setOpacity(1);
1307
		});
1308
	},
1309
 
1310
	_recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {
1311
		this._recursively(bounds, -1, zoomLevel,
1312
			function (c) {
1313
				if (zoomLevel === c._zoom) {
1314
					return;
1315
				}
1316
 
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];
1320
 
1321
					if (!bounds.contains(nm._latlng)) {
1322
						continue;
1323
					}
1324
 
1325
					if (startPos) {
1326
						nm._backupLatlng = nm.getLatLng();
1327
 
1328
						nm.setLatLng(startPos);
1329
						if (nm.setOpacity) {
1330
							nm.setOpacity(0);
1331
						}
1332
					}
1333
 
1334
					c._group._featureGroup.addLayer(nm);
1335
				}
1336
			},
1337
			function (c) {
1338
				c._addToMap(startPos);
1339
			}
1340
		);
1341
	},
1342
 
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;
1350
			}
1351
		}
1352
 
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();
1357
			}
1358
		} else {
1359
			for (var k = this._childClusters.length - 1; k >= 0; k--) {
1360
				this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);
1361
			}
1362
		}
1363
	},
1364
 
1365
	_restorePosition: function () {
1366
		if (this._backupLatlng) {
1367
			this.setLatLng(this._backupLatlng);
1368
			delete this._backupLatlng;
1369
		}
1370
	},
1371
 
1372
	//exceptBounds: If set, don't remove any markers/clusters in it
1373
	_recursivelyRemoveChildrenFromMap: function (previousBounds, zoomLevel, exceptBounds) {
1374
		var m, i;
1375
		this._recursively(previousBounds, -1, zoomLevel - 1,
1376
			function (c) {
1377
				//Remove markers at every level
1378
				for (i = c._markers.length - 1; i >= 0; i--) {
1379
					m = c._markers[i];
1380
					if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1381
						c._group._featureGroup.removeLayer(m);
1382
						if (m.setOpacity) {
1383
							m.setOpacity(1);
1384
						}
1385
					}
1386
				}
1387
			},
1388
			function (c) {
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);
1394
						if (m.setOpacity) {
1395
							m.setOpacity(1);
1396
						}
1397
					}
1398
				}
1399
			}
1400
		);
1401
	},
1402
 
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,
1411
		    zoom = this._zoom,
1412
			i, c;
1413
 
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);
1419
				}
1420
			}
1421
		} else { //In required depth
1422
 
1423
			if (runAtEveryLevel) {
1424
				runAtEveryLevel(this);
1425
			}
1426
			if (runAtBottomLevel && this._zoom === zoomLevelToStop) {
1427
				runAtBottomLevel(this);
1428
			}
1429
 
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);
1436
					}
1437
				}
1438
			}
1439
		}
1440
	},
1441
 
1442
	_recalculateBounds: function () {
1443
		var markers = this._markers,
1444
			childClusters = this._childClusters,
1445
			i;
1446
 
1447
		this._bounds = new L.LatLngBounds();
1448
		delete this._wLatLng;
1449
 
1450
		for (i = markers.length - 1; i >= 0; i--) {
1451
			this._expandBounds(markers[i]);
1452
		}
1453
		for (i = childClusters.length - 1; i >= 0; i--) {
1454
			this._expandBounds(childClusters[i]);
1455
		}
1456
	},
1457
 
1458
 
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;
1463
	}
1464
});
1465
 
1466
 
1467
 
1468
L.DistanceGrid = function (cellSize) {
1469
	this._cellSize = cellSize;
1470
	this._sqCellSize = cellSize * cellSize;
1471
	this._grid = {};
1472
	this._objectPoint = { };
1473
};
1474
 
1475
L.DistanceGrid.prototype = {
1476
 
1477
	addObject: function (obj, point) {
1478
		var x = this._getCoord(point.x),
1479
		    y = this._getCoord(point.y),
1480
		    grid = this._grid,
1481
		    row = grid[y] = grid[y] || {},
1482
		    cell = row[x] = row[x] || [],
1483
		    stamp = L.Util.stamp(obj);
1484
 
1485
		this._objectPoint[stamp] = point;
1486
 
1487
		cell.push(obj);
1488
	},
1489
 
1490
	updateObject: function (obj, point) {
1491
		this.removeObject(obj);
1492
		this.addObject(obj, point);
1493
	},
1494
 
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),
1499
		    grid = this._grid,
1500
		    row = grid[y] = grid[y] || {},
1501
		    cell = row[x] = row[x] || [],
1502
		    i, len;
1503
 
1504
		delete this._objectPoint[L.Util.stamp(obj)];
1505
 
1506
		for (i = 0, len = cell.length; i < len; i++) {
1507
			if (cell[i] === obj) {
1508
 
1509
				cell.splice(i, 1);
1510
 
1511
				if (len === 1) {
1512
					delete row[x];
1513
				}
1514
 
1515
				return true;
1516
			}
1517
		}
1518
 
1519
	},
1520
 
1521
	eachObject: function (fn, context) {
1522
		var i, j, k, len, row, cell, removed,
1523
		    grid = this._grid;
1524
 
1525
		for (i in grid) {
1526
			row = grid[i];
1527
 
1528
			for (j in row) {
1529
				cell = row[j];
1530
 
1531
				for (k = 0, len = cell.length; k < len; k++) {
1532
					removed = fn.call(context, cell[k]);
1533
					if (removed) {
1534
						k--;
1535
						len--;
1536
					}
1537
				}
1538
			}
1539
		}
1540
	},
1541
 
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,
1548
		    closest = null;
1549
 
1550
		for (i = y - 1; i <= y + 1; i++) {
1551
			row = this._grid[i];
1552
			if (row) {
1553
 
1554
				for (j = x - 1; j <= x + 1; j++) {
1555
					cell = row[j];
1556
					if (cell) {
1557
 
1558
						for (k = 0, len = cell.length; k < len; k++) {
1559
							obj = cell[k];
1560
							dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point);
1561
							if (dist < closestDistSq) {
1562
								closestDistSq = dist;
1563
								closest = obj;
1564
							}
1565
						}
1566
					}
1567
				}
1568
			}
1569
		}
1570
		return closest;
1571
	},
1572
 
1573
	_getCoord: function (x) {
1574
		return Math.floor(x / this._cellSize);
1575
	},
1576
 
1577
	_sqDist: function (p, p2) {
1578
		var dx = p2.x - p.x,
1579
		    dy = p2.y - p.y;
1580
		return dx * dx + dy * dy;
1581
	}
1582
};
1583
 
1584
 
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
1588
 
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:
1596
 
1597
The above copyright notice and this permission notice shall be
1598
included in all copies or substantial portions of the Software.
1599
 
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.
1607
 
1608
Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
1609
*/
1610
 
1611
(function () {
1612
	L.QuickHull = {
1613
 
1614
		/*
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
1619
		 */
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));
1624
		},
1625
 
1626
		/*
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.
1632
		 */
1633
		findMostDistantPointFromBaseLine: function (baseLine, latLngs) {
1634
			var maxD = 0,
1635
				maxPt = null,
1636
				newPoints = [],
1637
				i, pt, d;
1638
 
1639
			for (i = latLngs.length - 1; i >= 0; i--) {
1640
				pt = latLngs[i];
1641
				d = this.getDistant(pt, baseLine);
1642
 
1643
				if (d > 0) {
1644
					newPoints.push(pt);
1645
				} else {
1646
					continue;
1647
				}
1648
 
1649
				if (d > maxD) {
1650
					maxD = d;
1651
					maxPt = pt;
1652
				}
1653
			}
1654
 
1655
			return { maxPoint: maxPt, newPoints: newPoints };
1656
		},
1657
 
1658
 
1659
		/*
1660
		 * Given a baseline, compute the convex hull of latLngs as an array
1661
		 * of latLngs.
1662
		 *
1663
		 * @param {Array} latLngs
1664
		 * @returns {Array}
1665
		 */
1666
		buildConvexHull: function (baseLine, latLngs) {
1667
			var convexHullBaseLines = [],
1668
				t = this.findMostDistantPointFromBaseLine(baseLine, latLngs);
1669
 
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)
1674
					);
1675
				convexHullBaseLines =
1676
					convexHullBaseLines.concat(
1677
						this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints)
1678
					);
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]];
1682
			}
1683
		},
1684
 
1685
		/*
1686
		 * Given an array of latlngs, compute a convex hull as an array
1687
		 * of latlngs
1688
		 *
1689
		 * @param {Array} latLngs
1690
		 * @returns {Array}
1691
		 */
1692
		getConvexHull: function (latLngs) {
1693
			// find first baseline
1694
			var maxLat = false, minLat = false,
1695
				maxPt = null, minPt = null,
1696
				i;
1697
 
1698
			for (i = latLngs.length - 1; i >= 0; i--) {
1699
				var pt = latLngs[i];
1700
				if (maxLat === false || pt.lat > maxLat) {
1701
					maxPt = pt;
1702
					maxLat = pt.lat;
1703
				}
1704
				if (minLat === false || pt.lat < minLat) {
1705
					minPt = pt;
1706
					minLat = pt.lat;
1707
				}
1708
			}
1709
			var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs),
1710
								this.buildConvexHull([maxPt, minPt], latLngs));
1711
			return ch;
1712
		}
1713
	};
1714
}());
1715
 
1716
L.MarkerCluster.include({
1717
	getConvexHull: function () {
1718
		var childMarkers = this.getAllChildMarkers(),
1719
			points = [],
1720
			p, i;
1721
 
1722
		for (i = childMarkers.length - 1; i >= 0; i--) {
1723
			p = childMarkers[i].getLatLng();
1724
			points.push(p);
1725
		}
1726
 
1727
		return L.QuickHull.getConvexHull(points);
1728
	}
1729
});
1730
 
1731
 
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 :-)
1734
 
1735
L.MarkerCluster.include({
1736
 
1737
	_2PI: Math.PI * 2,
1738
	_circleFootSeparation: 25, //related to circumference of circle
1739
	_circleStartAngle: Math.PI / 6,
1740
 
1741
	_spiralFootSeparation:  28, //related to size of spiral (experiment!)
1742
	_spiralLengthStart: 11,
1743
	_spiralLengthFactor: 5,
1744
 
1745
	_circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards.
1746
								// 0 -> always spiral; Infinity -> always circle
1747
 
1748
	spiderfy: function () {
1749
		if (this._group._spiderfied === this || this._group._inZoomAnimation) {
1750
			return;
1751
		}
1752
 
1753
		var childMarkers = this.getAllChildMarkers(),
1754
			group = this._group,
1755
			map = group._map,
1756
			center = map.latLngToLayerPoint(this._latlng),
1757
			positions;
1758
 
1759
		this._group._unspiderfy();
1760
		this._group._spiderfied = this;
1761
 
1762
		//TODO Maybe: childMarkers order by distance to center
1763
 
1764
		if (childMarkers.length >= this._circleSpiralSwitchover) {
1765
			positions = this._generatePointsSpiral(childMarkers.length, center);
1766
		} else {
1767
			center.y += 10; //Otherwise circles look wrong
1768
			positions = this._generatePointsCircle(childMarkers.length, center);
1769
		}
1770
 
1771
		this._animationSpiderfy(childMarkers, positions);
1772
	},
1773
 
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) {
1777
			return;
1778
		}
1779
		this._animationUnspiderfy(zoomDetails);
1780
 
1781
		this._group._spiderfied = null;
1782
	},
1783
 
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,
1788
			res = [],
1789
			i, angle;
1790
 
1791
		res.length = count;
1792
 
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();
1796
		}
1797
 
1798
		return res;
1799
	},
1800
 
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,
1805
			angle = 0,
1806
			res = [],
1807
			i;
1808
 
1809
		res.length = count;
1810
 
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;
1815
		}
1816
		return res;
1817
	},
1818
 
1819
	_noanimationUnspiderfy: function () {
1820
		var group = this._group,
1821
			map = group._map,
1822
			fg = group._featureGroup,
1823
			childMarkers = this.getAllChildMarkers(),
1824
			m, i;
1825
 
1826
		this.setOpacity(1);
1827
		for (i = childMarkers.length - 1; i >= 0; i--) {
1828
			m = childMarkers[i];
1829
 
1830
			fg.removeLayer(m);
1831
 
1832
			if (m._preSpiderfyLatlng) {
1833
				m.setLatLng(m._preSpiderfyLatlng);
1834
				delete m._preSpiderfyLatlng;
1835
			}
1836
			if (m.setZIndexOffset) {
1837
				m.setZIndexOffset(0);
1838
			}
1839
 
1840
			if (m._spiderLeg) {
1841
				map.removeLayer(m._spiderLeg);
1842
				delete m._spiderLeg;
1843
			}
1844
		}
1845
 
1846
		group._spiderfied = null;
1847
	}
1848
});
1849
 
1850
L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
1851
	//Non Animated versions of everything
1852
	_animationSpiderfy: function (childMarkers, positions) {
1853
		var group = this._group,
1854
			map = group._map,
1855
			fg = group._featureGroup,
1856
			i, m, leg, newPos;
1857
 
1858
		for (i = childMarkers.length - 1; i >= 0; i--) {
1859
			newPos = map.layerPointToLatLng(positions[i]);
1860
			m = childMarkers[i];
1861
 
1862
			m._preSpiderfyLatlng = m._latlng;
1863
			m.setLatLng(newPos);
1864
			if (m.setZIndexOffset) {
1865
				m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
1866
			}
1867
 
1868
			fg.addLayer(m);
1869
 
1870
 
1871
			leg = new L.Polyline([this._latlng, newPos], { weight: 1.5, color: '#222' });
1872
			map.addLayer(leg);
1873
			m._spiderLeg = leg;
1874
		}
1875
		this.setOpacity(0.3);
1876
		group.fire('spiderfied');
1877
	},
1878
 
1879
	_animationUnspiderfy: function () {
1880
		this._noanimationUnspiderfy();
1881
	}
1882
} : {
1883
	//Animated versions here
1884
	SVG_ANIMATION: (function () {
1885
		return document.createElementNS('http://www.w3.org/2000/svg', 'animate').toString().indexOf('SVGAnimate') > -1;
1886
	}()),
1887
 
1888
	_animationSpiderfy: function (childMarkers, positions) {
1889
		var me = this,
1890
			group = this._group,
1891
			map = group._map,
1892
			fg = group._featureGroup,
1893
			thisLayerPos = map.latLngToLayerPoint(this._latlng),
1894
			i, m, leg, newPos;
1895
 
1896
		//Add markers to map hidden at our center point
1897
		for (i = childMarkers.length - 1; i >= 0; i--) {
1898
			m = childMarkers[i];
1899
 
1900
			//If it is a marker, add it now and we'll animate it out
1901
			if (m.setOpacity) {
1902
				m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
1903
				m.setOpacity(0);
1904
 
1905
				fg.addLayer(m);
1906
 
1907
				m._setPos(thisLayerPos);
1908
			} else {
1909
				//Vectors just get immediately added
1910
				fg.addLayer(m);
1911
			}
1912
		}
1913
 
1914
		group._forceLayout();
1915
		group._animationStart();
1916
 
1917
		var initialLegOpacity = L.Path.SVG ? 0 : 0.3,
1918
			xmlns = L.Path.SVG_NS;
1919
 
1920
 
1921
		for (i = childMarkers.length - 1; i >= 0; i--) {
1922
			newPos = map.layerPointToLatLng(positions[i]);
1923
			m = childMarkers[i];
1924
 
1925
			//Move marker to new position
1926
			m._preSpiderfyLatlng = m._latlng;
1927
			m.setLatLng(newPos);
1928
 
1929
			if (m.setOpacity) {
1930
				m.setOpacity(1);
1931
			}
1932
 
1933
 
1934
			//Add Legs.
1935
			leg = new L.Polyline([me._latlng, newPos], { weight: 1.5, color: '#222', opacity: initialLegOpacity });
1936
			map.addLayer(leg);
1937
			m._spiderLeg = leg;
1938
 
1939
			//Following animations don't work for canvas
1940
			if (!L.Path.SVG || !this.SVG_ANIMATION) {
1941
				continue;
1942
			}
1943
 
1944
			//How this works:
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/
1947
 
1948
			//Animate length
1949
			var length = leg._path.getTotalLength();
1950
			leg._path.setAttribute("stroke-dasharray", length + "," + length);
1951
 
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();
1960
 
1961
			//Animate opacity
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();
1971
		}
1972
		me.setOpacity(0.3);
1973
 
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.
1977
		if (L.Path.SVG) {
1978
			this._group._forceLayout();
1979
 
1980
			for (i = childMarkers.length - 1; i >= 0; i--) {
1981
				m = childMarkers[i]._spiderLeg;
1982
 
1983
				m.options.opacity = 0.5;
1984
				m._path.setAttribute('stroke-opacity', 0.5);
1985
			}
1986
		}
1987
 
1988
		setTimeout(function () {
1989
			group._animationEnd();
1990
			group.fire('spiderfied');
1991
		}, 200);
1992
	},
1993
 
1994
	_animationUnspiderfy: function (zoomDetails) {
1995
		var group = this._group,
1996
			map = group._map,
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,
2001
			m, i, a;
2002
 
2003
		group._animationStart();
2004
 
2005
		//Make us visible and bring the child markers back in
2006
		this.setOpacity(1);
2007
		for (i = childMarkers.length - 1; i >= 0; i--) {
2008
			m = childMarkers[i];
2009
 
2010
			//Marker was added to us after we were spidified
2011
			if (!m._preSpiderfyLatlng) {
2012
				continue;
2013
			}
2014
 
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
2019
			if (m.setOpacity) {
2020
				m._setPos(thisLayerPos);
2021
				m.setOpacity(0);
2022
			} else {
2023
				fg.removeLayer(m);
2024
			}
2025
 
2026
			//Animate the spider legs back in
2027
			if (svg) {
2028
				a = m._spiderLeg._path.childNodes[0];
2029
				a.setAttribute('to', a.getAttribute('from'));
2030
				a.setAttribute('from', 0);
2031
				a.beginElement();
2032
 
2033
				a = m._spiderLeg._path.childNodes[1];
2034
				a.setAttribute('from', 0.5);
2035
				a.setAttribute('to', 0);
2036
				a.setAttribute('stroke-opacity', 0);
2037
				a.beginElement();
2038
 
2039
				m._spiderLeg._path.setAttribute('stroke-opacity', 0);
2040
			}
2041
		}
2042
 
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];
2048
				if (m._spiderLeg) {
2049
					stillThereChildCount++;
2050
				}
2051
			}
2052
 
2053
 
2054
			for (i = childMarkers.length - 1; i >= 0; i--) {
2055
				m = childMarkers[i];
2056
 
2057
				if (!m._spiderLeg) { //Has already been unspiderfied
2058
					continue;
2059
				}
2060
 
2061
 
2062
				if (m.setOpacity) {
2063
					m.setOpacity(1);
2064
					m.setZIndexOffset(0);
2065
				}
2066
 
2067
				if (stillThereChildCount > 1) {
2068
					fg.removeLayer(m);
2069
				}
2070
 
2071
				map.removeLayer(m._spiderLeg);
2072
				delete m._spiderLeg;
2073
			}
2074
			group._animationEnd();
2075
		}, 200);
2076
	}
2077
});
2078
 
2079
 
2080
L.MarkerClusterGroup.include({
2081
	//The MarkerCluster currently spiderfied (if any)
2082
	_spiderfied: null,
2083
 
2084
	_spiderfierOnAdd: function () {
2085
		this._map.on('click', this._unspiderfyWrapper, this);
2086
 
2087
		if (this._map.options.zoomAnimation) {
2088
			this._map.on('zoomstart', this._unspiderfyZoomStart, this);
2089
		}
2090
		//Browsers without zoomAnimation or a big zoom don't fire zoomstart
2091
		this._map.on('zoomend', this._noanimationUnspiderfy, this);
2092
 
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
2098
		}
2099
	},
2100
 
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);
2105
 
2106
		this._unspiderfy(); //Ensure that markers are back where they should be
2107
	},
2108
 
2109
 
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
2114
			return;
2115
		}
2116
 
2117
		this._map.on('zoomanim', this._unspiderfyZoomAnim, this);
2118
	},
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')) {
2122
			return;
2123
		}
2124
 
2125
		this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
2126
		this._unspiderfy(zoomDetails);
2127
	},
2128
 
2129
 
2130
	_unspiderfyWrapper: function () {
2131
		/// <summary>_unspiderfy but passes no arguments</summary>
2132
		this._unspiderfy();
2133
	},
2134
 
2135
	_unspiderfy: function (zoomDetails) {
2136
		if (this._spiderfied) {
2137
			this._spiderfied.unspiderfy(zoomDetails);
2138
		}
2139
	},
2140
 
2141
	_noanimationUnspiderfy: function () {
2142
		if (this._spiderfied) {
2143
			this._spiderfied._noanimationUnspiderfy();
2144
		}
2145
	},
2146
 
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);
2151
 
2152
			layer.setOpacity(1);
2153
			//Position will be fixed up immediately in _animationUnspiderfy
2154
			layer.setZIndexOffset(0);
2155
 
2156
			this._map.removeLayer(layer._spiderLeg);
2157
			delete layer._spiderLeg;
2158
		}
2159
	}
2160
});
2161
 
2162
 
2163
}(window, document));