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