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