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