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