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