UNPKG

81.2 kBJavaScriptView Raw
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