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