source: binary-improvements/webserver/leaflet/markercluster/leaflet.markercluster-src.js@ 316

Last change on this file since 316 was 315, checked in by alloc, 7 years ago

Fixes

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