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