1// JavaScript Document 2 3/* 4 Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. 5 https://github.com/Leaflet/Leaflet.markercluster 6 (c) 2012-2013, Dave Leaver, smartrak 7*/ 8(function (window, document, undefined) {/* 9 * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within 10 */ 11 12L.MarkerClusterGroup = L.FeatureGroup.extend({ 13 14 options: { 15 // maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center 16 iconCreateFunction: null, 17 18 spiderfyOnMaxZoom: true, 19 showCoverageOnHover: true, 20 zoomToBoundsOnClick: true, 21 singleMarkerMode: false, 22 23 disableClusteringAtZoom: null, 24 25 // Setting this to false prevents the removal of any clusters outside of the viewpoint, which 26 // is the default behaviour for performance reasons. 27 removeOutsideVisibleBounds: true, 28 29 //Whether to animate adding markers after adding the MarkerClusterGroup to the map 30 // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. 31 animateAddingMarkers: false, 32 33 //Increase to increase the distance away that spiderfied markers appear from the center 34 spiderfyDistanceMultiplier: 1, 35 36 // 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 37 chunkedLoading: false, 38 chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) 39 chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser 40 chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) 41 42 //Options to pass to the L.Polygon constructor 43 polygonOptions: {} 44 }, 45 46 initialize: function (options) { 47 L.Util.setOptions(this, options); 48 if (!this.options.iconCreateFunction) { 49 this.options.iconCreateFunction = this._defaultIconCreateFunction; 50 } 51 52 this._featureGroup = L.featureGroup(); 53 this._featureGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); 54 55 this._nonPointGroup = L.featureGroup(); 56 this._nonPointGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); 57 58 this._inZoomAnimation = 0; 59 this._needsClustering = []; 60 this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of 61 //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move 62 this._currentShownBounds = null; 63 64 this._queue = []; 65 }, 66 67 addLayer: function (layer) { 68 69 if (layer instanceof L.LayerGroup) { 70 var array = []; 71 for (var i in layer._layers) { 72 array.push(layer._layers[i]); 73 } 74 return this.addLayers(array); 75 } 76 77 //Don't cluster non point data 78 if (!layer.getLatLng) { 79 this._nonPointGroup.addLayer(layer); 80 return this; 81 } 82 83 if (!this._map) { 84 this._needsClustering.push(layer); 85 return this; 86 } 87 88 if (this.hasLayer(layer)) { 89 return this; 90 } 91 92 93 //If we have already clustered we'll need to add this one to a cluster 94 95 if (this._unspiderfy) { 96 this._unspiderfy(); 97 } 98 99 this._addLayer(layer, this._maxZoom); 100 101 //Work out what is visible 102 var visibleLayer = layer, 103 currentZoom = this._map.getZoom(); 104 if (layer.__parent) { 105 while (visibleLayer.__parent._zoom >= currentZoom) { 106 visibleLayer = visibleLayer.__parent; 107 } 108 } 109 110 if (this._currentShownBounds.contains(visibleLayer.getLatLng())) { 111 if (this.options.animateAddingMarkers) { 112 this._animationAddLayer(layer, visibleLayer); 113 } else { 114 this._animationAddLayerNonAnimated(layer, visibleLayer); 115 } 116 } 117 return this; 118 }, 119 120 removeLayer: function (layer) { 121 122 if (layer instanceof L.LayerGroup) 123 { 124 var array = []; 125 for (var i in layer._layers) { 126 array.push(layer._layers[i]); 127 } 128 return this.removeLayers(array); 129 } 130 131 //Non point layers 132 if (!layer.getLatLng) { 133 this._nonPointGroup.removeLayer(layer); 134 return this; 135 } 136 137 if (!this._map) { 138 if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) { 139 this._needsRemoving.push(layer); 140 } 141 return this; 142 } 143 144 if (!layer.__parent) { 145 return this; 146 } 147 148 if (this._unspiderfy) { 149 this._unspiderfy(); 150 this._unspiderfyLayer(layer); 151 } 152
153 //Remove the marker from clusters 154 this._removeLayer(layer, true); 155 156 if (this._featureGroup.hasLayer(layer)) { 157 this._featureGroup.removeLayer(layer); 158 if (layer.setOpacity) { 159 layer.setOpacity(1); 160 } 161 } 162 163 return this; 164 }, 165 166 //Takes an array of markers and adds them in bulk 167 addLayers: function (layersArray) { 168 var fg = this._featureGroup, 169 npg = this._nonPointGroup, 170 chunked = this.options.chunkedLoading, 171 chunkInterval = this.options.chunkInterval, 172 chunkProgress = this.options.chunkProgress, 173 newMarkers, i, l, m; 174 175 if (this._map) { 176 var offset = 0, 177 started = (new Date()).getTime(); 178 var process = L.bind(function () { 179 var start = (new Date()).getTime(); 180 for (; offset < layersArray.length; offset++) { 181 if (chunked && offset % 200 === 0) { 182 // every couple hundred markers, instrument the time elapsed since processing started: 183 var elapsed = (new Date()).getTime() - start; 184 if (elapsed > chunkInterval) { 185 break; // been working too hard, time to take a break :-) 186 } 187 } 188 189 m = layersArray[offset]; 190 191 //Not point data, can't be clustered 192 if (!m.getLatLng) { 193 npg.addLayer(m); 194 continue; 195 } 196 197 if (this.hasLayer(m)) { 198 continue; 199 } 200 201 this._addLayer(m, this._maxZoom); 202 203 //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 204 if (m.__parent) { 205 if (m.__parent.getChildCount() === 2) { 206 var markers = m.__parent.getAllChildMarkers(), 207 otherMarker = markers[0] === m ? markers[1] : markers[0]; 208 fg.removeLayer(otherMarker); 209 } 210 } 211 } 212 213 if (chunkProgress) { 214 // report progress and time elapsed: 215 chunkProgress(offset, layersArray.length, (new Date()).getTime() - started); 216 } 217 218 if (offset === layersArray.length) { 219 //Update the icons of all those visible clusters that were affected 220 this._featureGroup.eachLayer(function (c) { 221 if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) { 222 c._updateIcon(); 223 } 224 }); 225 226 this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); 227 } else { 228 setTimeout(process, this.options.chunkDelay); 229 } 230 }, this); 231 232 process(); 233 } else { 234 newMarkers = []; 235 for (i = 0, l = layersArray.length; i < l; i++) { 236 m = layersArray[i]; 237 238 //Not point data, can't be clustered 239 if (!m.getLatLng) { 240 npg.addLayer(m); 241 continue; 242 } 243 244 if (this.hasLayer(m)) { 245 continue; 246 } 247 248 newMarkers.push(m); 249 } 250 this._needsClustering = this._needsClustering.concat(newMarkers); 251 } 252 return this; 253 }, 254 255 //Takes an array of markers and removes them in bulk 256 removeLayers: function (layersArray) { 257 var i, l, m, 258 fg = this._featureGroup, 259 npg = this._nonPointGroup; 260 261 if (!this._map) { 262 for (i = 0, l = layersArray.length; i < l; i++) { 263 m = layersArray[i]; 264 this._arraySplice(this._needsClustering, m); 265 npg.removeLayer(m); 266 } 267 return this; 268 } 269 270 for (i = 0, l = layersArray.length; i < l; i++) { 271 m = layersArray[i]; 272 273 if (!m.__parent) { 274 npg.removeLayer(m); 275 continue; 276 } 277 278 this._removeLayer(m, true, true); 279 280 if (fg.hasLayer(m)) { 281 fg.removeLayer(m); 282 if (m.setOpacity) { 283 m.setOpacity(1); 284 } 285 } 286 } 287 288 //Fix up the clusters and markers on the map 289 this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); 290 291 fg.eachLayer(function (c) { 292 if (c instanceof L.MarkerCluster) { 293 c._updateIcon(); 294 } 295 }); 296 297 return this; 298 }, 299 300 //Removes all layers from the MarkerClusterGroup 301 clearLayers: function () { 302 //Need our own special implementation as the LayerGroup one doesn't work for us 303 304 //If we aren't on the map (yet), blow away the markers we know of 305 if (!this._map) { 306 this._needsClustering = []; 307 delete this._gridClusters; 308 delete this._gridUnclustered; 309 } 310 311 if (this._noanimationUnspiderfy) { 312 this._noanimationUnspiderfy(); 313 } 314 315 //Remove all the visible layers 316 this._featureGroup.clearLayers(); 317 this._nonPointGroup.clearLayers(); 318 319 this.eachLayer(function (marker) { 320 delete marker.__parent; 321 }); 322 323 if (this._map) { 324 //Reset _topClusterLevel and the DistanceGrids 325 this._generateInitialClusters(); 326 } 327 328 return this; 329 }, 330 331 //Override FeatureGroup.getBounds as it doesn't work 332 getBounds: function () { 333 var bounds = new L.LatLngBounds(); 334 335 if (this._topClusterLevel) { 336 bounds.extend(this._topClusterLevel._bounds); 337 } 338 339 for (var i = this._needsClustering.length - 1; i >= 0; i--) { 340 bounds.extend(this._needsClustering[i].getLatLng()); 341 } 342 343 bounds.extend(this._nonPointGroup.getBounds()); 344 345 return bounds; 346 }, 347 348 //Overrides LayerGroup.eachLayer 349 eachLayer: function (method, context) { 350 var markers = this._needsClustering.slice(), 351 i; 352 353 if (this._topClusterLevel) { 354 this._topClusterLevel.getAllChildMarkers(markers); 355 } 356 357 for (i = markers.length - 1; i >= 0; i--) { 358 method.call(context, markers[i]); 359 } 360 361 this._nonPointGroup.eachLayer(method, context); 362 }, 363 364 //Overrides LayerGroup.getLayers 365 getLayers: function () { 366 var layers = []; 367 this.eachLayer(function (l) { 368 layers.push(l); 369 }); 370 return layers; 371 }, 372 373 //Overrides LayerGroup.getLayer, WARNING: Really bad performance 374 getLayer: function (id) { 375 var result = null; 376 377 this.eachLayer(function (l) { 378 if (L.stamp(l) === id) { 379 result = l; 380 } 381 }); 382 383 return result; 384 }, 385 386 //Returns true if the given layer is in this MarkerClusterGroup 387 hasLayer: function (layer) { 388 if (!layer) { 389 return false;
390 } 391 392 var i, anArray = this._needsClustering; 393 394 for (i = anArray.length - 1; i >= 0; i--) { 395 if (anArray[i] === layer) { 396 return true; 397 } 398 } 399 400 anArray = this._needsRemoving; 401 for (i = anArray.length - 1; i >= 0; i--) { 402 if (anArray[i] === layer) { 403 return false; 404 } 405 } 406 407 return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer); 408 }, 409 410 //Zoom down to show the given layer (spiderfying if necessary) then calls the callback 411 zoomToShowLayer: function (layer, callback) { 412 413 var showMarker = function () { 414 if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) { 415 this._map.off('moveend', showMarker, this); 416 this.off('animationend', showMarker, this); 417 418 if (layer._icon) { 419 callback(); 420 } else if (layer.__parent._icon) { 421 var afterSpiderfy = function () { 422 this.off('spiderfied', afterSpiderfy, this); 423 callback(); 424 }; 425 426 this.on('spiderfied', afterSpiderfy, this); 427 layer.__parent.spiderfy(); 428 } 429 } 430 }; 431 432 if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) { 433 //Layer is visible ond on screen, immediate return 434 callback(); 435 } else if (layer.__parent._zoom < this._map.getZoom()) { 436 //Layer should be visible at this zoom level. It must not be on screen so just pan over to it 437 this._map.on('moveend', showMarker, this); 438 this._map.panTo(layer.getLatLng()); 439 } else { 440 var moveStart = function () { 441 this._map.off('movestart', moveStart, this); 442 moveStart = null; 443 }; 444 445 this._map.on('movestart', moveStart, this); 446 this._map.on('moveend', showMarker, this); 447 this.on('animationend', showMarker, this); 448 layer.__parent.zoomToBounds(); 449 450 if (moveStart) { 451 //Never started moving, must already be there, probably need clustering however 452 showMarker.call(this); 453 } 454 } 455 }, 456 457 //Overrides FeatureGroup.onAdd 458 onAdd: function (map) { 459 this._map = map; 460 var i, l, layer; 461 462 if (!isFinite(this._map.getMaxZoom())) { 463 throw "Map has no maxZoom specified"; 464 } 465 466 this._featureGroup.onAdd(map); 467 this._nonPointGroup.onAdd(map); 468 469 if (!this._gridClusters) { 470 this._generateInitialClusters(); 471 } 472 473 for (i = 0, l = this._needsRemoving.length; i < l; i++) { 474 layer = this._needsRemoving[i]; 475 this._removeLayer(layer, true); 476 } 477 this._needsRemoving = []; 478 479 //Remember the current zoom level and bounds 480 this._zoom = this._map.getZoom(); 481 this._currentShownBounds = this._getExpandedVisibleBounds(); 482 483 this._map.on('zoomend', this._zoomEnd, this); 484 this._map.on('moveend', this._moveEnd, this); 485 486 if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely 487 this._spiderfierOnAdd(); 488 } 489 490 this._bindEvents(); 491 492 //Actually add our markers to the map: 493 l = this._needsClustering; 494 this._needsClustering = []; 495 this.addLayers(l); 496 }, 497 498 //Overrides FeatureGroup.onRemove 499 onRemove: function (map) { 500 map.off('zoomend', this._zoomEnd, this); 501 map.off('moveend', this._moveEnd, this); 502 503 this._unbindEvents(); 504 505 //In case we are in a cluster animation 506 this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); 507 508 if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely 509 this._spiderfierOnRemove(); 510 } 511 512 513 514 //Clean up all the layers we added to the map 515 this._hideCoverage(); 516 this._featureGroup.onRemove(map); 517 this._nonPointGroup.onRemove(map); 518 519 this._featureGroup.clearLayers(); 520 521 this._map = null; 522 }, 523 524 getVisibleParent: function (marker) { 525 var vMarker = marker; 526 while (vMarker && !vMarker._icon) { 527 vMarker = vMarker.__parent; 528 } 529 return vMarker || null; 530 }, 531 532 //Remove the given object from the given array 533 _arraySplice: function (anArray, obj) { 534 for (var i = anArray.length - 1; i >= 0; i--) { 535 if (anArray[i] === obj) { 536 anArray.splice(i, 1); 537 return true; 538 } 539 } 540 }, 541 542 //Internal function for removing a marker from everything. 543 //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions) 544 _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) { 545 var gridClusters = this._gridClusters, 546 gridUnclustered = this._gridUnclustered, 547 fg = this._featureGroup, 548 map = this._map; 549
550 //Remove the marker from distance clusters it might be in 551 if (removeFromDistanceGrid) { 552 for (var z = this._maxZoom; z >= 0; z--) { 553 if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) { 554 break; 555 } 556 } 557 } 558 559 //Work our way up the clusters removing them as we go if required 560 var cluster = marker.__parent, 561 markers = cluster._markers, 562 otherMarker; 563 564 //Remove the marker from the immediate parents marker list 565 this._arraySplice(markers, marker); 566 567 while (cluster) { 568 cluster._childCount--; 569 570 if (cluster._zoom < 0) { 571 //Top level, do nothing 572 break; 573 } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required 574 //We need to push the other marker up to the parent 575 otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0]; 576 577 //Update distance grid 578 gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom)); 579 gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom)); 580 581 //Move otherMarker up to parent 582 this._arraySplice(cluster.__parent._childClusters, cluster); 583 cluster.__parent._markers.push(otherMarker); 584 otherMarker.__parent = cluster.__parent; 585 586 if (cluster._icon) { 587 //Cluster is currently on the map, need to put the marker on the map instead 588 fg.removeLayer(cluster); 589 if (!dontUpdateMap) { 590 fg.addLayer(otherMarker); 591 } 592 } 593 } else { 594 cluster._recalculateBounds(); 595 if (!dontUpdateMap || !cluster._icon) { 596 cluster._updateIcon(); 597 } 598 } 599 600 cluster = cluster.__parent; 601 } 602 603 delete marker.__parent; 604 }, 605 606 _isOrIsParent: function (el, oel) { 607 while (oel) { 608 if (el === oel) { 609 return true; 610 } 611 oel = oel.parentNode; 612 } 613 return false; 614 }, 615 616 _propagateEvent: function (e) { 617 if (e.layer instanceof L.MarkerCluster) { 618 //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget) 619 if (e.originalEvent && this._isOrIsParent(e.layer._icon, e.originalEvent.relatedTarget)) { 620 return; 621 } 622 e.type = 'cluster' + e.type; 623 } 624 625 this.fire(e.type, e); 626 }, 627 628 //Default functionality 629 _defaultIconCreateFunction: function (cluster) { 630 var childCount = cluster.getChildCount(); 631 632 var c = ' marker-cluster-'; 633 if (childCount < 10) { 634 c += 'small'; 635 } else if (childCount < 100) { 636 c += 'medium'; 637 } else { 638 c += 'large'; 639 } 640 641 return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); 642 }, 643 644 _bindEvents: function () { 645 var map = this._map, 646 spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, 647 showCoverageOnHover = this.options.showCoverageOnHover, 648 zoomToBoundsOnClick = this.options.zoomToBoundsOnClick; 649 650 //Zoom on cluster click or spiderfy if we are at the lowest level 651 if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { 652 this.on('clusterclick', this._zoomOrSpiderfy, this); 653 } 654 655 //Show convex hull (boundary) polygon on mouse over 656 if (showCoverageOnHover) { 657 this.on('clustermouseover', this._showCoverage, this); 658 this.on('clustermouseout', this._hideCoverage, this); 659 map.on('zoomend', this._hideCoverage, this); 660 } 661 }, 662 663 _zoomOrSpiderfy: function (e) { 664 var map = this._map; 665 if (map.getMaxZoom() === map.getZoom()) { 666 if (this.options.spiderfyOnMaxZoom) { 667 e.layer.spiderfy(); 668 } 669 } else if (this.options.zoomToBoundsOnClick) { 670 e.layer.zoomToBounds(); 671 } 672 673 // Focus the map again for keyboard users. 674 if (e.originalEvent && e.originalEvent.keyCode === 13) { 675 map._container.focus(); 676 } 677 }, 678 679 _showCoverage: function (e) { 680 var map = this._map; 681 if (this._inZoomAnimation) { 682 return; 683 } 684 if (this._shownPolygon) { 685 map.removeLayer(this._shownPolygon); 686 } 687 if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) { 688 this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions); 689 map.addLayer(this._shownPolygon); 690 } 691 }, 692 693 _hideCoverage: function () { 694 if (this._shownPolygon) { 695 this._map.removeLayer(this._shownPolygon); 696 this._shownPolygon = null; 697 } 698 }, 699 700 _unbindEvents: function () { 701 var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, 702 showCoverageOnHover = this.options.showCoverageOnHover, 703 zoomToBoundsOnClick = this.options.zoomToBoundsOnClick, 704 map = this._map; 705 706 if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { 707 this.off('clusterclick', this._zoomOrSpiderfy, this); 708 } 709 if (showCoverageOnHover) { 710 this.off('clustermouseover', this._showCoverage, this); 711 this.off('clustermouseout', this._hideCoverage, this); 712 map.off('zoomend', this._hideCoverage, this); 713 } 714 }, 715 716 _zoomEnd: function () { 717 if (!this._map) { //May have been removed from the map by a zoomEnd handler 718 return; 719 }
720 this._mergeSplitClusters(); 721 722 this._zoom = this._map._zoom; 723 this._currentShownBounds = this._getExpandedVisibleBounds(); 724 }, 725 726 _moveEnd: function () { 727 if (this._inZoomAnimation) { 728 return; 729 } 730 731 var newBounds = this._getExpandedVisibleBounds(); 732 733 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds); 734 this._topClusterLevel._recursivelyAddChildrenToMap(null, this._map._zoom, newBounds); 735 736 this._currentShownBounds = newBounds; 737 return; 738 }, 739 740 _generateInitialClusters: function () { 741 var maxZoom = this._map.getMaxZoom(), 742 radius = this.options.maxClusterRadius, 743 radiusFn = radius; 744 745 //If we just set maxClusterRadius to a single number, we need to create 746 //a simple function to return that number. Otherwise, we just have to 747 //use the function we've passed in. 748 if (typeof radius !== "function") { 749 radiusFn = function () { return radius; }; 750 } 751 752 if (this.options.disableClusteringAtZoom) { 753 maxZoom = this.options.disableClusteringAtZoom - 1; 754 } 755 this._maxZoom = maxZoom; 756 this._gridClusters = {}; 757 this._gridUnclustered = {}; 758 759 //Set up DistanceGrids for each zoom 760 for (var zoom = maxZoom; zoom >= 0; zoom--) { 761 this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom)); 762 this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom)); 763 } 764 765 this._topClusterLevel = new L.MarkerCluster(this, -1); 766 }, 767 768 //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom) 769 _addLayer: function (layer, zoom) { 770 var gridClusters = this._gridClusters, 771 gridUnclustered = this._gridUnclustered, 772 markerPoint, z; 773 774 if (this.options.singleMarkerMode) { 775 layer.options.icon = this.options.iconCreateFunction({ 776 getChildCount: function () { 777 return 1; 778 }, 779 getAllChildMarkers: function () { 780 return [layer]; 781 } 782 }); 783 } 784 785 //Find the lowest zoom level to slot this one in 786 for (; zoom >= 0; zoom--) { 787 markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position 788 789 //Try find a cluster close by 790 var closest = gridClusters[zoom].getNearObject(markerPoint); 791 if (closest) { 792 closest._addChild(layer); 793 layer.__parent = closest; 794 return; 795 } 796 797 //Try find a marker close by to form a new cluster with 798 closest = gridUnclustered[zoom].getNearObject(markerPoint); 799 if (closest) { 800 var parent = closest.__parent; 801 if (parent) { 802 this._removeLayer(closest, false); 803 } 804 805 //Create new cluster with these 2 in it 806 807 var newCluster = new L.MarkerCluster(this, zoom, closest, layer); 808 gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom)); 809 closest.__parent = newCluster; 810 layer.__parent = newCluster; 811 812 //First create any new intermediate parent clusters that don't exist 813 var lastParent = newCluster; 814 for (z = zoom - 1; z > parent._zoom; z--) { 815 lastParent = new L.MarkerCluster(this, z, lastParent); 816 gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z)); 817 } 818 parent._addChild(lastParent); 819 820 //Remove closest from this zoom level and any above that it is in, replace with newCluster 821 for (z = zoom; z >= 0; z--) { 822 if (!gridUnclustered[z].removeObject(closest, this._map.project(closest.getLatLng(), z))) { 823 break; 824 } 825 } 826 827 return; 828 } 829 830 //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards 831 gridUnclustered[zoom].addObject(layer, markerPoint); 832 } 833 834 //Didn't get in anything, add us to the top 835 this._topClusterLevel._addChild(layer); 836 layer.__parent = this._topClusterLevel; 837 return; 838 }, 839 840 //Enqueue code to fire after the marker expand/contract has happened 841 _enqueue: function (fn) { 842 this._queue.push(fn); 843 if (!this._queueTimeout) { 844 this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300); 845 } 846 }, 847 _processQueue: function () { 848 for (var i = 0; i < this._queue.length; i++) { 849 this._queue[i].call(this); 850 } 851 this._queue.length = 0; 852 clearTimeout(this._queueTimeout); 853 this._queueTimeout = null; 854 }, 855
856 //Merge and split any existing clusters that are too big or small 857 _mergeSplitClusters: function () { 858 859 //Incase we are starting to split before the animation finished 860 this._processQueue(); 861 862 if (this._zoom < this._map._zoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split 863 this._animationStart(); 864 //Remove clusters now off screen 865 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds()); 866 867 this._animationZoomIn(this._zoom, this._map._zoom); 868 869 } else if (this._zoom > this._map._zoom) { //Zoom out, merge 870 this._animationStart(); 871 872 this._animationZoomOut(this._zoom, this._map._zoom); 873 } else { 874 this._moveEnd(); 875 } 876 }, 877 878 //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) 879 _getExpandedVisibleBounds: function () { 880 if (!this.options.removeOutsideVisibleBounds) { 881 return this.getBounds(); 882 } 883 884 var map = this._map, 885 bounds = map.getBounds(), 886 sw = bounds._southWest, 887 ne = bounds._northEast, 888 latDiff = L.Browser.mobile ? 0 : Math.abs(sw.lat - ne.lat), 889 lngDiff = L.Browser.mobile ? 0 : Math.abs(sw.lng - ne.lng); 890 891 return new L.LatLngBounds( 892 new L.LatLng(sw.lat - latDiff, sw.lng - lngDiff, true), 893 new L.LatLng(ne.lat + latDiff, ne.lng + lngDiff, true)); 894 }, 895 896 //Shared animation code 897 _animationAddLayerNonAnimated: function (layer, newCluster) { 898 if (newCluster === layer) { 899 this._featureGroup.addLayer(layer); 900 } else if (newCluster._childCount === 2) { 901 newCluster._addToMap(); 902 903 var markers = newCluster.getAllChildMarkers(); 904 this._featureGroup.removeLayer(markers[0]); 905 this._featureGroup.removeLayer(markers[1]); 906 } else { 907 newCluster._updateIcon(); 908 } 909 } 910}); 911 912L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? { 913 914 //Non Animated versions of everything 915 _animationStart: function () { 916 //Do nothing... 917 }, 918 _animationZoomIn: function (previousZoomLevel, newZoomLevel) { 919 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel); 920 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 921 922 //We didn't actually animate, but we use this event to mean "clustering animations have finished" 923 this.fire('animationend'); 924 }, 925 _animationZoomOut: function (previousZoomLevel, newZoomLevel) { 926 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel); 927 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 928 929 //We didn't actually animate, but we use this event to mean "clustering animations have finished" 930 this.fire('animationend'); 931 }, 932 _animationAddLayer: function (layer, newCluster) { 933 this._animationAddLayerNonAnimated(layer, newCluster); 934 } 935} : { 936 937 //Animated versions here 938 _animationStart: function () { 939 this._map._mapPane.className += ' leaflet-cluster-anim'; 940 this._inZoomAnimation++; 941 }, 942 _animationEnd: function () { 943 if (this._map) { 944 this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); 945 } 946 this._inZoomAnimation--; 947 this.fire('animationend'); 948 }, 949 _animationZoomIn: function (previousZoomLevel, newZoomLevel) { 950 var bounds = this._getExpandedVisibleBounds(), 951 fg = this._featureGroup, 952 i; 953 954 //Add all children of current clusters to map and remove those clusters from map 955 this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) { 956 var startPos = c._latlng, 957 markers = c._markers, 958 m; 959 960 if (!bounds.contains(startPos)) { 961 startPos = null; 962 } 963 964 if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us 965 fg.removeLayer(c); 966 c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds); 967 } else { 968 //Fade out old cluster 969 c.setOpacity(0); 970 c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds); 971 } 972 973 //Remove all markers that aren't visible any more 974 //TODO: Do we actually need to do this on the higher levels too? 975 for (i = markers.length - 1; i >= 0; i--) { 976 m = markers[i]; 977 if (!bounds.contains(m._latlng)) { 978 fg.removeLayer(m); 979 } 980 } 981 982 }); 983 984 this._forceLayout(); 985 986 //Update opacities 987 this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel); 988 //TODO Maybe? Update markers in _recursivelyBecomeVisible 989 fg.eachLayer(function (n) { 990 if (!(n instanceof L.MarkerCluster) && n._icon) { 991 n.setOpacity(1); 992 } 993 }); 994 995 //update the positions of the just added clusters/markers 996 this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) { 997 c._recursivelyRestoreChildPositions(newZoomLevel); 998 }); 999 1000 //Remove the old clusters and close the zoom animation 1001 this._enqueue(function () { 1002 //update the positions of the just added clusters/markers 1003 this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) { 1004 fg.removeLayer(c); 1005 c.setOpacity(1); 1006 }); 1007 1008 this._animationEnd(); 1009 }); 1010 }, 1011 1012 _animationZoomOut: function (previousZoomLevel, newZoomLevel) { 1013 this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZo
1013omLevel); 1014 1015 //Need to add markers for those that weren't on the map before but are now 1016 this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 1017 //Remove markers that were on the map before but won't be now 1018 this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel, this._getExpandedVisibleBounds()); 1019 }, 1020 _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) { 1021 var bounds = this._getExpandedVisibleBounds(); 1022 1023 //Animate all of the markers in the clusters to move to their cluster center point 1024 cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel); 1025 1026 var me = this; 1027 1028 //Update the opacity (If we immediately set it they won't animate) 1029 this._forceLayout(); 1030 cluster._recursivelyBecomeVisible(bounds, newZoomLevel); 1031 1032 //TODO: Maybe use the transition timing stuff to make this more reliable 1033 //When the animations are done, tidy up 1034 this._enqueue(function () { 1035 1036 //This cluster stopped being a cluster before the timeout fired 1037 if (cluster._childCount === 1) { 1038 var m = cluster._markers[0]; 1039 //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 1040 m.setLatLng(m.getLatLng()); 1041 if (m.setOpacity) { 1042 m.setOpacity(1); 1043 } 1044 } else { 1045 cluster._recursively(bounds, newZoomLevel, 0, function (c) { 1046 c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1); 1047 }); 1048 } 1049 me._animationEnd(); 1050 }); 1051 }, 1052 _animationAddLayer: function (layer, newCluster) { 1053 var me = this, 1054 fg = this._featureGroup; 1055 1056 fg.addLayer(layer); 1057 if (newCluster !== layer) { 1058 if (newCluster._childCount > 2) { //Was already a cluster 1059 1060 newCluster._updateIcon(); 1061 this._forceLayout(); 1062 this._animationStart(); 1063 1064 layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng())); 1065 layer.setOpacity(0); 1066 1067 this._enqueue(function () { 1068 fg.removeLayer(layer); 1069 layer.setOpacity(1); 1070 1071 me._animationEnd(); 1072 }); 1073 1074 } else { //Just became a cluster 1075 this._forceLayout(); 1076 1077 me._animationStart(); 1078 me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom()); 1079 } 1080 } 1081 }, 1082 1083 //Force a browser layout of stuff in the map 1084 // Should apply the current opacity and location to all elements so we can update them again for an animation 1085 _forceLayout: function () { 1086 //In my testing this works, infact offsetWidth of any element seems to work. 1087 //Could loop all this._layers and do this for each _icon if it stops working 1088 1089 L.Util.falseFn(document.body.offsetWidth); 1090 } 1091}); 1092 1093L.markerClusterGroup = function (options) { 1094 return new L.MarkerClusterGroup(options); 1095}; 1096 1097 1098L.MarkerCluster = L.Marker.extend({ 1099 initialize: function (group, zoom, a, b) { 1100 1101 L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this }); 1102 1103 1104 this._group = group; 1105 this._zoom = zoom; 1106 1107 this._markers = []; 1108 this._childClusters = []; 1109 this._childCount = 0; 1110 this._iconNeedsUpdate = true; 1111 1112 this._bounds = new L.LatLngBounds(); 1113 1114 if (a) { 1115 this._addChild(a); 1116 } 1117 if (b) { 1118 this._addChild(b); 1119 } 1120 }, 1121 1122 //Recursively retrieve all child markers of this cluster 1123 getAllChildMarkers: function (storageArray) { 1124 storageArray = storageArray || []; 1125 1126 for (var i = this._childClusters.length - 1; i >= 0; i--) { 1127 this._childClusters[i].getAllChildMarkers(storageArray); 1128 } 1129 1130 for (var j = this._markers.length - 1; j >= 0; j--) { 1131 storageArray.push(this._markers[j]); 1132 } 1133 1134 return storageArray; 1135 }, 1136 1137 //Returns the count of how many child markers we have 1138 getChildCount: function () { 1139 return this._childCount; 1140 }, 1141 1142 //Zoom to the minimum of showing all of the child markers, or the extents of this cluster 1143 zoomToBounds: function () { 1144 var childClusters = this._childClusters.slice(), 1145 map = this._group._map, 1146 boundsZoom = map.getBoundsZoom(this._bounds), 1147 zoom = this._zoom + 1, 1148 mapZoom = map.getZoom(), 1149 i; 1150 1151 //calculate how far we need to zoom down to see all of the markers 1152 while (childClusters.length > 0 && boundsZoom > zoom) { 1153 zoom++; 1154 var newClusters = []; 1155 for (i = 0; i < childClusters.length; i++) { 1156 newClusters = newClusters.concat(childClusters[i]._childClusters); 1157 } 1158 childClusters = newClusters; 1159 } 1160 1161 if (boundsZoom > zoom) {
1162 this._group._map.setView(this._latlng, zoom); 1163 } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead 1164 this._group._map.setView(this._latlng, mapZoom + 1); 1165 } else { 1166 this._group._map.fitBounds(this._bounds); 1167 } 1168 }, 1169 1170 getBounds: function () { 1171 var bounds = new L.LatLngBounds(); 1172 bounds.extend(this._bounds); 1173 return bounds; 1174 }, 1175 1176 _updateIcon: function () { 1177 this._iconNeedsUpdate = true; 1178 if (this._icon) { 1179 this.setIcon(this); 1180 } 1181 }, 1182 1183 //Cludge for Icon, we pretend to be an icon for performance 1184 createIcon: function () { 1185 if (this._iconNeedsUpdate) { 1186 this._iconObj = this._group.options.iconCreateFunction(this); 1187 this._iconNeedsUpdate = false; 1188 } 1189 return this._iconObj.createIcon(); 1190 }, 1191 createShadow: function () { 1192 return this._iconObj.createShadow(); 1193 }, 1194 1195 1196 _addChild: function (new1, isNotificationFromChild) { 1197 1198 this._iconNeedsUpdate = true; 1199 this._expandBounds(new1); 1200 1201 if (new1 instanceof L.MarkerCluster) { 1202 if (!isNotificationFromChild) { 1203 this._childClusters.push(new1); 1204 new1.__parent = this; 1205 } 1206 this._childCount += new1._childCount; 1207 } else { 1208 if (!isNotificationFromChild) { 1209 this._markers.push(new1); 1210 } 1211 this._childCount++; 1212 } 1213 1214 if (this.__parent) { 1215 this.__parent._addChild(new1, true); 1216 } 1217 }, 1218 1219 //Expand our bounds and tell our parent to 1220 _expandBounds: function (marker) { 1221 var addedCount, 1222 addedLatLng = marker._wLatLng || marker._latlng; 1223 1224 if (marker instanceof L.MarkerCluster) { 1225 this._bounds.extend(marker._bounds); 1226 addedCount = marker._childCount; 1227 } else { 1228 this._bounds.extend(addedLatLng); 1229 addedCount = 1; 1230 } 1231 1232 if (!this._cLatLng) { 1233 // when clustering, take position of the first point as the cluster center 1234 this._cLatLng = marker._cLatLng || addedLatLng; 1235 } 1236 1237 // when showing clusters, take weighted average of all points as cluster center 1238 var totalCount = this._childCount + addedCount; 1239 1240 //Calculate weighted latlng for display 1241 if (!this._wLatLng) { 1242 this._latlng = this._wLatLng = new L.LatLng(addedLatLng.lat, addedLatLng.lng); 1243 } else { 1244 this._wLatLng.lat = (addedLatLng.lat * addedCount + this._wLatLng.lat * this._childCount) / totalCount; 1245 this._wLatLng.lng = (addedLatLng.lng * addedCount + this._wLatLng.lng * this._childCount) / totalCount; 1246 } 1247 }, 1248 1249 //Set our markers position as given and add it to the map 1250 _addToMap: function (startPos) { 1251 if (startPos) { 1252 this._backupLatlng = this._latlng; 1253 this.setLatLng(startPos); 1254 } 1255 this._group._featureGroup.addLayer(this); 1256 }, 1257 1258 _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) { 1259 this._recursively(bounds, 0, maxZoom - 1, 1260 function (c) { 1261 var markers = c._markers, 1262 i, m; 1263 for (i = markers.length - 1; i >= 0; i--) { 1264 m = markers[i]; 1265 1266 //Only do it if the icon is still on the map 1267 if (m._icon) { 1268 m._setPos(center); 1269 m.setOpacity(0); 1270 } 1271 } 1272 }, 1273 function (c) { 1274 var childClusters = c._childClusters, 1275 j, cm; 1276 for (j = childClusters.length - 1; j >= 0; j--) { 1277 cm = childClusters[j]; 1278 if (cm._icon) { 1279 cm._setPos(center); 1280 cm.setOpacity(0); 1281 } 1282 } 1283 } 1284 ); 1285 }, 1286 1287 _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, previousZoomLevel, newZoomLevel) { 1288 this._recursively(bounds, newZoomLevel, 0, 1289 function (c) { 1290 c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel); 1291 1292 //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be. 1293 //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate 1294 if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) { 1295 c.setOpacity(1); 1296 c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds 1297 } else { 1298 c.setOpacity(0); 1299 } 1300 1301 c._addToMap(); 1302 } 1303 ); 1304 }, 1305 1306 _recursivelyBecomeVisible: function (bounds, zoomLevel) { 1307 this._recursively(bounds, 0, zoomLevel, null, function (c) { 1308 c.setOpacity(1); 1309 }); 1310 }, 1311 1312 _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) { 1313 this._recursively(bounds, -1, zoomLevel, 1314 function (c) { 1315 if (zoomLevel === c._zoom) { 1316 return; 1317 } 1318 1319 //Add our child markers at startPos (so they can be animated out) 1320 for (var i = c._markers.length - 1; i >= 0; i--) { 1321 var nm = c._markers[i]; 1322 1323 if (!bounds.contains(nm._latlng)) { 1324 continue; 1325 } 1326 1327 if (startPos) { 1328 nm._backupLatlng = nm.getLatLng(); 1329 1330 nm.setLatLng(startPos); 1331 if (nm.setOpacity) { 1332 nm.setOpacity(0); 1333 } 1334 } 1335 1336 c._group._featureGroup.addLayer(nm); 1337 } 1338 }, 1339 function (c) { 1340 c._addToMap(startPos); 1341 } 1342 ); 1343 }, 1344 1345 _recursivelyRestoreChildPositions: function (zoomLevel) { 1346 //Fix positions of child markers 1347 for (var i = this._markers.length - 1; i >= 0; i--) { 1348 var nm = this._markers[i]; 1349 if (nm._backupLatlng) { 1350 nm.setLatLng(nm._backupLatlng); 1351 delete nm._backupLatlng; 1352 } 1353 } 1354 1355 if (zoomLevel - 1 === this._zoom) { 1356 //Reposition child clusters 1357 for (var j = this._childClusters.length - 1; j >= 0; j--) { 1358 this._childClusters[j]._restorePosition(); 1359 } 1360 } else { 1361 for (var k = this._childClusters.length - 1; k >= 0; k--) { 1362 this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel); 1363 } 1364 } 1365 }, 1366 1367 _restorePosition: function () { 1368 if (this._backupLatlng) { 1369 this.setLatLng(this._backupLatlng); 1370 delete this._backupLatlng; 1371 } 1372 }, 1373 1374 //exceptBounds: If set, don't remove any markers/clusters in it 1375 _recursivelyRemoveChildrenFromMap: function (previousBounds, zoomLevel, exceptBounds) { 1376 var m, i; 1377 this._recursively(previousBounds, -1, zoomLevel - 1, 1378 function (c) { 1379 //Remove markers at every level 1380 for (i = c._markers.length - 1; i >= 0; i--) { 1381 m = c._markers[i]; 1382 if (!exceptBounds || !exceptBounds.contains(m._latlng)) { 1383 c._group._featureGroup.removeLayer(m); 1384 if (m.setOpacity) { 1385 m.setOpacity(1); 1386 } 1387 } 1388 } 1389 }, 1390 function (c) { 1391 //Remove child clusters at just the bottom level 1392 for (i = c._childClusters.length - 1; i >= 0; i--) { 1393 m = c._childClusters[i]; 1394 if (!exceptBounds || !exceptBounds.contains(m._latlng)) { 1395 c._group._featureGroup.removeLayer(m); 1396 if (m.setOpacity) { 1397 m.setOpacity(1); 1398 } 1399 } 1400 } 1401 } 1402 ); 1403 }, 1404 1405 //Run the given functions recursively to this and child clusters 1406 // boundsToApplyTo: a L.LatLngBounds representing the bounds of what cluster
1406s to recurse in to 1407 // zoomLevelToStart: zoom level to start running functions (inclusive) 1408 // zoomLevelToStop: zoom level to stop running functions (inclusive) 1409 // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level 1410 // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level 1411 _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) { 1412 var childClusters = this._childClusters, 1413 zoom = this._zoom, 1414 i, c; 1415 1416 if (zoomLevelToStart > zoom) { //Still going down to required depth, just recurse to child clusters 1417 for (i = childClusters.length - 1; i >= 0; i--) { 1418 c = childClusters[i]; 1419 if (boundsToApplyTo.intersects(c._bounds)) { 1420 c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); 1421 } 1422 } 1423 } else { //In required depth 1424 1425 if (runAtEveryLevel) { 1426 runAtEveryLevel(this); 1427 } 1428 if (runAtBottomLevel && this._zoom === zoomLevelToStop) { 1429 runAtBottomLevel(this); 1430 } 1431 1432 //TODO: This loop is almost the same as above 1433 if (zoomLevelToStop > zoom) { 1434 for (i = childClusters.length - 1; i >= 0; i--) { 1435 c = childClusters[i]; 1436 if (boundsToApplyTo.intersects(c._bounds)) { 1437 c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); 1438 } 1439 } 1440 } 1441 } 1442 }, 1443 1444 _recalculateBounds: function () { 1445 var markers = this._markers, 1446 childClusters = this._childClusters, 1447 i; 1448 1449 this._bounds = new L.LatLngBounds(); 1450 delete this._wLatLng; 1451 1452 for (i = markers.length - 1; i >= 0; i--) { 1453 this._expandBounds(markers[i]); 1454 } 1455 for (i = childClusters.length - 1; i >= 0; i--) { 1456 this._expandBounds(childClusters[i]); 1457 } 1458 }, 1459 1460 1461 //Returns true if we are the parent of only one cluster and that cluster is the same as us 1462 _isSingleParent: function () { 1463 //Don't need to check this._markers as the rest won't work if there are any 1464 return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount; 1465 } 1466}); 1467 1468 1469 1470L.DistanceGrid = function (cellSize) { 1471 this._cellSize = cellSize; 1472 this._sqCellSize = cellSize * cellSize; 1473 this._grid = {}; 1474 this._objectPoint = { }; 1475}; 1476 1477L.DistanceGrid.prototype = { 1478 1479 addObject: function (obj, point) { 1480 var x = this._getCoord(point.x), 1481 y = this._getCoord(point.y), 1482 grid = this._grid, 1483 row = grid[y] = grid[y] || {}, 1484 cell = row[x] = row[x] || [], 1485 stamp = L.Util.stamp(obj); 1486 1487 this._objectPoint[stamp] = point; 1488 1489 cell.push(obj); 1490 }, 1491 1492 updateObject: function (obj, point) { 1493 this.removeObject(obj); 1494 this.addObject(obj, point); 1495 }, 1496 1497 //Returns true if the object was found 1498 removeObject: function (obj, point) { 1499 var x = this._getCoord(point.x), 1500 y = this._getCoord(point.y), 1501 grid = this._grid, 1502 row = grid[y] = grid[y] || {}, 1503 cell = row[x] = row[x] || [], 1504 i, len; 1505 1506 delete this._objectPoint[L.Util.stamp(obj)]; 1507 1508 for (i = 0, len = cell.length; i < len; i++) { 1509 if (cell[i] === obj) { 1510 1511 cell.splice(i, 1); 1512 1513 if (len === 1) { 1514 delete row[x]; 1515 } 1516 1517 return true; 1518 } 1519 } 1520 1521 }, 1522 1523 eachObject: function (fn, context) { 1524 var i, j, k, len, row, cell, removed, 1525 grid = this._grid; 1526 1527 for (i in grid) { 1528 row = grid[i]; 1529 1530 for (j in row) { 1531 cell = row[j]; 1532 1533 for (k = 0, len = cell.length; k < len; k++) { 1534 removed = fn.call(context, cell[k]); 1535 if (removed) { 1536 k--; 1537 len--; 1538 } 1539 } 1540 } 1541 } 1542 }, 1543 1544 getNearObject: function (point) { 1545 var x = this._getCoord(point.x), 1546 y = this._getCoord(point.y), 1547 i, j, k, row, cell, len, obj, dist, 1548 objectPoint = this._objectPoint, 1549 closestDistSq = this._sqCellSize, 1550 closest = null; 1551 1552 for (i = y - 1; i <= y + 1; i++) { 1553 row = this._grid[i]; 1554 if (row) { 1555 1556 for (j = x - 1; j <= x + 1; j++) { 1557 cell = row[j]; 1558 if (cell) { 1559 1560 for (k = 0, len = cell.length; k < len; k++) { 1561 obj = cell[k]; 1562 dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point); 1563 if (dist < closestDistSq) { 1564 closestDistSq = dist; 1565 closest = obj; 1566 } 1567 } 1568 } 1569 } 1570 } 1571 } 1572 return closest; 1573 }, 1574 1575 _getCoord: function (x) { 1576 return Math.floor(x / this._cellSize); 1577 }, 1578 1579 _sqDist: function (p, p2) { 1580 var dx = p2.x - p.x, 1581 dy = p2.y - p.y; 1582 return dx * dx + dy * dy; 1583 } 1584}; 1585 1586 1587/* Copyright (c) 2012 the authors listed at the following URL, and/or
1588the authors of referenced articles or incorporated external code: 1589http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256 1590 1591Permission is hereby granted, free of charge, to any person obtaining 1592a copy of this software and associated documentation files (the 1593"Software"), to deal in the Software without restriction, including 1594without limitation the rights to use, copy, modify, merge, publish, 1595distribute, sublicense, and/or sell copies of the Software, and to 1596permit persons to whom the Software is furnished to do so, subject to 1597the following conditions: 1598 1599The above copyright notice and this permission notice shall be 1600included in all copies or substantial portions of the Software. 1601 1602THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 1603EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 1604MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 1605IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 1606CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 1607TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 1608SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 1609 1610Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434 1611*/ 1612 1613(function () { 1614 L.QuickHull = { 1615 1616 /* 1617 * @param {Object} cpt a point to be measured from the baseline 1618 * @param {Array} bl the baseline, as represented by a two-element 1619 * array of latlng objects. 1620 * @returns {Number} an approximate distance measure 1621 */ 1622 getDistant: function (cpt, bl) { 1623 var vY = bl[1].lat - bl[0].lat, 1624 vX = bl[0].lng - bl[1].lng; 1625 return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng)); 1626 }, 1627 1628 /* 1629 * @param {Array} baseLine a two-element array of latlng objects 1630 * representing the baseline to project from 1631 * @param {Array} latLngs an array of latlng objects 1632 * @returns {Object} the maximum point and all new points to stay 1633 * in consideration for the hull. 1634 */ 1635 findMostDistantPointFromBaseLine: function (baseLine, latLngs) { 1636 var maxD = 0, 1637 maxPt = null, 1638 newPoints = [], 1639 i, pt, d; 1640 1641 for (i = latLngs.length - 1; i >= 0; i--) { 1642 pt = latLngs[i]; 1643 d = this.getDistant(pt, baseLine); 1644 1645 if (d > 0) { 1646 newPoints.push(pt); 1647 } else { 1648 continue; 1649 } 1650 1651 if (d > maxD) { 1652 maxD = d; 1653 maxPt = pt; 1654 } 1655 } 1656 1657 return { maxPoint: maxPt, newPoints: newPoints }; 1658 }, 1659 1660 1661 /* 1662 * Given a baseline, compute the convex hull of latLngs as an array 1663 * of latLngs. 1664 * 1665 * @param {Array} latLngs 1666 * @returns {Array} 1667 */ 1668 buildConvexHull: function (baseLine, latLngs) { 1669 var convexHullBaseLines = [], 1670 t = this.findMostDistantPointFromBaseLine(baseLine, latLngs); 1671 1672 if (t.maxPoint) { // if there is still a point "outside" the base line 1673 convexHullBaseLines = 1674 convexHullBaseLines.concat( 1675 this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints) 1676 ); 1677 convexHullBaseLines = 1678 convexHullBaseLines.concat( 1679 this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints) 1680 ); 1681 return convexHullBaseLines; 1682 } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull 1683 return [baseLine[0]]; 1684 } 1685 }, 1686 1687 /* 1688 * Given an array of latlngs, compute a convex hull as an array 1689 * of latlngs 1690 * 1691 * @param {Array} latLngs 1692 * @returns {Array} 1693 */ 1694 getConvexHull: function (latLngs) { 1695 // find first baseline 1696 var maxLat = false, minLat = false, 1697 maxPt = null, minPt = null, 1698 i; 1699 1700 for (i = latLngs.length - 1; i >= 0; i--) { 1701 var pt = latLngs[i]; 1702 if (maxLat === false || pt.lat > maxLat) { 1703 maxPt = pt; 1704 maxLat = pt.lat; 1705 } 1706 if (minLat === false || pt.lat < minLat) { 1707 minPt = pt; 1708 minLat = pt.lat; 1709 } 1710 } 1711 var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs), 1712 this.buildConvexHull([maxPt, minPt], latLngs)); 1713 return ch; 1714 } 1715 }; 1716}()); 1717 1718L.MarkerCluster.include({ 1719 getConvexHull: function () { 1720 var childMarkers = this.getAllChildMarkers(), 1721 points = [], 1722 p, i; 1723 1724 for (i = childMarkers.length - 1; i >= 0; i--) { 1725 p = childMarkers[i].getLatLng(); 1726 points.push(p); 1727 } 1728 1729 return L.QuickHull.getConvexHull(points); 1730 } 1731}); 1732 1733 1734//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet 1735//Huge thanks to jawj for implementing it first to make my job easy :-) 1736 1737L.MarkerCluster.include({ 1738 1739 _2PI: Math.PI * 2, 1740 _circleFootSeparation: 25, //related to circumference of circle 1741 _circleStartAngle: Math.PI / 6, 1742 1743 _spiralFootSeparation: 28, //related to size of spiral (experiment!) 1744 _spiralLengthStart: 11, 1745 _spiralLengthFactor: 5, 1746 1747 _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards. 1748 // 0 -> always spiral; Infinity -> always circle 1749 1750 spiderfy: function () { 1751 if (this._group._spiderfied === this || this._group._inZoomAnimation) { 1752 return; 1753 } 1754 1755 var childMarkers = this.getAllChildMarkers(), 1756 group = this._group, 1757 map = group._map, 1758 center = map.latLngToLayerPoint(this._latlng), 1759 positions; 1760 1761 this._group._unspiderfy(); 1762 this._group._spiderfied = this; 1763 1764 //TODO Maybe: childMarkers order by distance to center 1765 1766 if (childMarkers.length >= this._circleSpiralSwitchover) { 1767 positions = this._generatePointsSpiral(childMarkers.length, center); 1768 } else { 1769 center.y += 10; //Otherwise circles look wrong 1770 positions = this._generatePointsCircle(childMarkers.length, center); 1771 } 1772 1773 this._animationSpiderfy(childMarkers, positions); 1774 }, 1775 1776 unspiderfy: function (zoomDetails) { 1777 /// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param> 1778 if (this._group._inZoomAnimation) { 1779 return; 1780 } 1781 this._animationUnspiderfy(zoomDetails); 1782 1783 this._group._spiderfied = null; 1784 }, 1785 1786 _generatePointsCircle: function (count, centerPt) { 1787 var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),
1788 legLength = circumference / this._2PI, //radius from circumference 1789 angleStep = this._2PI / count, 1790 res = [], 1791 i, angle; 1792 1793 res.length = count; 1794 1795 for (i = count - 1; i >= 0; i--) { 1796 angle = this._circleStartAngle + i * angleStep; 1797 res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); 1798 } 1799 1800 return res; 1801 }, 1802 1803 _generatePointsSpiral: function (count, centerPt) { 1804 var legLength = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthStart, 1805 separation = this._group.options.spiderfyDistanceMultiplier * this._spiralFootSeparation, 1806 lengthFactor = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthFactor, 1807 angle = 0, 1808 res = [], 1809 i; 1810 1811 res.length = count; 1812 1813 for (i = count - 1; i >= 0; i--) { 1814 angle += separation / legLength + i * 0.0005; 1815 res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); 1816 legLength += this._2PI * lengthFactor / angle; 1817 } 1818 return res; 1819 }, 1820 1821 _noanimationUnspiderfy: function () { 1822 var group = this._group, 1823 map = group._map, 1824 fg = group._featureGroup, 1825 childMarkers = this.getAllChildMarkers(), 1826 m, i; 1827 1828 this.setOpacity(1); 1829 for (i = childMarkers.length - 1; i >= 0; i--) { 1830 m = childMarkers[i]; 1831 1832 fg.removeLayer(m); 1833 1834 if (m._preSpiderfyLatlng) { 1835 m.setLatLng(m._preSpiderfyLatlng); 1836 delete m._preSpiderfyLatlng; 1837 } 1838 if (m.setZIndexOffset) { 1839 m.setZIndexOffset(0); 1840 } 1841 1842 if (m._spiderLeg) { 1843 map.removeLayer(m._spiderLeg); 1844 delete m._spiderLeg; 1845 } 1846 } 1847 1848 group._spiderfied = null; 1849 } 1850}); 1851 1852L.MarkerCluster.include(!L.DomUtil.TRANSITION ? { 1853 //Non Animated versions of everything 1854 _animationSpiderfy: function (childMarkers, positions) { 1855 var group = this._group, 1856 map = group._map, 1857 fg = group._featureGroup, 1858 i, m, leg, newPos; 1859 1860 for (i = childMarkers.length - 1; i >= 0; i--) { 1861 newPos = map.layerPointToLatLng(positions[i]); 1862 m = childMarkers[i]; 1863 1864 m._preSpiderfyLatlng = m._latlng; 1865 m.setLatLng(newPos); 1866 if (m.setZIndexOffset) { 1867 m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING 1868 } 1869 1870 fg.addLayer(m); 1871 1872 1873 leg = new L.Polyline([this._latlng, newPos], { weight: 1.5, color: '#222' }); 1874 map.addLayer(leg); 1875 m._spiderLeg = leg; 1876 } 1877 this.setOpacity(0.3); 1878 group.fire('spiderfied'); 1879 }, 1880 1881 _animationUnspiderfy: function () { 1882 this._noanimationUnspiderfy(); 1883 } 1884} : { 1885 //Animated versions here 1886 SVG_ANIMATION: (function () { 1887 return document.createElementNS('http://www.w3.org/2000/svg', 'animate').toString().indexOf('SVGAnimate') > -1; 1888 }()), 1889 1890 _animationSpiderfy: function (childMarkers, positions) { 1891 var me = this, 1892 group = this._group, 1893 map = group._map, 1894 fg = group._featureGroup, 1895 thisLayerPos = map.latLngToLayerPoint(this._latlng), 1896 i, m, leg, newPos; 1897 1898 //Add markers to map hidden at our center point 1899 for (i = childMarkers.length - 1; i >= 0; i--) { 1900 m = childMarkers[i]; 1901 1902 //If it is a marker, add it now and we'll animate it out 1903 if (m.setOpacity) { 1904 m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING 1905 m.setOpacity(0); 1906 1907 fg.addLayer(m); 1908 1909 m._setPos(thisLayerPos); 1910 } else { 1911 //Vectors just get immediately added 1912 fg.addLayer(m); 1913 } 1914 } 1915 1916 group._forceLayout(); 1917 group._animationStart(); 1918 1919 var initialLegOpacity = L.Path.SVG ? 0 : 0.3, 1920 xmlns = L.Path.SVG_NS; 1921 1922 1923 for (i = childMarkers.length - 1; i >= 0; i--) { 1924 newPos = map.layerPointToLatLng(positions[i]); 1925 m = childMarkers[i]; 1926 1927 //Move marker to new position 1928 m._preSpiderfyLatlng = m._latlng; 1929 m.setLatLng(newPos); 1930 1931 if (m.setOpacity) { 1932 m.setOpacity(1); 1933 } 1934 1935 1936 //Add Legs. 1937 leg = new L.Polyline([me._latlng, newPos], { weight: 1.5, color: '#222', opacity: initialLegOpacity }); 1938 map.addLayer(leg); 1939 m._spiderLeg = leg; 1940 1941 //Following animations don't work for canvas 1942 if (!L.Path.SVG || !this.SVG_ANIMATION) { 1943 continue; 1944 } 1945 1946 //How this works: 1947 //http://stackoverflow.com/questions/5924238/how-do-you-animate-an-svg-path-in-ios 1948 //http://dev.opera.com/articles/view/advanced-svg-animation-techniques/ 1949 1950 //Animate length 1951 var length = leg._path.getTotalLength(); 1952 leg._path.setAttribute("stroke-dasharray", length + "," + length); 1953 1954 var anim = document.createElementNS(xmlns, "animate"); 1955 anim.setAttribute("attributeName", "stroke-dashoffset"); 1956 anim.setAttribute("begin", "indefinite"); 1957 anim.setAttribute("from", length); 1958 anim.setAttribute("to", 0); 1959 anim.setAttribute("dur", 0.25); 1960 leg._path.appendChild(anim); 1961 anim.beginElement(); 1962 1963 //Animate opacity 1964 anim = document.createElementNS(xmlns, "animate"); 1965 anim.setAttribute("attributeName", "stroke-opacity"); 1966 anim.setAttribute("attributeName", "stroke-opacity"); 1967 anim.setAttribute("begin", "indefinite"); 1968 anim.setAttribute("from", 0); 1969 anim.setAttribute("to", 0.5); 1970 anim.setAttribute("dur", 0.25); 1971 leg._path.appendChild(anim); 1972 anim.beginElement(); 1973 } 1974 me.setOpacity(0.3); 1975 1976 //Set the opacity of the spiderLegs back to their correct value 1977 // The animations above override this until they complete. 1978 // If the initial opacity of the spiderlegs isn't 0 then they appear before the animation starts. 1979 if (L.Path.SVG) { 1980 this._group._forceLayout(); 1981 1982 for (i = childMarkers.length - 1; i >= 0; i--) { 1983 m = childMarkers[i]._spiderLeg; 1984 1985 m.options.opacity = 0.5; 1986 m._path.setAttribute('stroke-opacity', 0.5); 1987 } 1988 } 1989 1990 setTimeout(function () { 1991 group._animationEnd(); 1992 group.fire('spiderfied'); 1993 }, 200); 1994 }, 1995 1996 _animationUnspiderfy: function (zoomDetails) { 1997 var group = this._group, 1998 map = group._map, 1999 fg = group._featureGroup, 2000 thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng), 2001 childMarkers = this.getAllChildMarkers(), 2002 svg = L.Path.SVG && this.SVG_ANIMATION, 2003 m, i, a; 2004 2005 group._animationStart(); 2006 2007 //Make us visible and bring the child markers back in 2008 this.setOpacity(1); 2009 for (i = childMarkers.length - 1; i >= 0; i--) { 2010 m = childMarkers[i]; 2011 2012 //Marker was added to us after we were spidified 2013 if (!m._preSpiderfyLatlng) { 2014 continue; 2015 } 2016 2017 //Fix up the location to the real one 2018 m.setLatLng(m._preSpiderfyLatlng); 2019 delete m._preSpiderfyLatlng; 2020 //Hack override the location to be our center 2021 if (m.setOpacity) { 2022 m._setPos(thisLayerPos); 2023 m.setOpacity(0); 2024 } else { 2025 fg.removeLayer(m); 2026 } 2027 2028 //Animate the spider legs back in 2029 if (svg) { 2030 a = m._spiderLeg._path.childNodes[0]; 2031 a.setAttribute('to', a.getAttribute('from')); 2032 a.setAttribute('from', 0); 2033 a.beginElement(); 2034 2035 a = m._spiderLeg._path.childNodes[1]; 2036 a.setAttribute('from', 0.5); 2037 a.setAttribute('to', 0); 2038 a.setAttribute('stroke-opacity', 0); 2039 a.beginElement(); 2040 2041 m._spiderLeg._path.setAttribute('stroke-opacity', 0); 2042 } 2043 } 2044 2045 setTimeout(function () { 2046 //If we have only <= one child left then that marker will be shown on the map so don't remove it! 2047 var stillThereChildCount = 0; 2048 for (i = childMarkers.length - 1; i >= 0; i--) { 2049 m = childMarkers[i]; 2050 if (m._spiderLeg) { 2051 stillThereChildCount++; 2052 } 2053 } 2054 2055 2056 for (i = childMarkers.length - 1; i >= 0; i--) { 2057 m = childMarkers[i]; 2058 2059 if (!m._spiderLeg) { //Has already been unspiderfied 2060 continue; 2061 } 2062 2063 2064 if (m.setOpacity) { 2065 m.setOpacity(1); 2066 m.setZIndexOffset(0); 2067 } 2068 2069 if (stillThereChildCount > 1) { 2070 fg.removeLayer(m); 2071 } 2072 2073 map.removeLayer(m._spiderLeg); 2074 delete m._spiderLeg; 2075 } 2076 group._animationEnd(); 2077 }, 200); 2078 } 2079}); 2080 2081 2082L.MarkerClusterGroup.include({ 2083 //The MarkerCluster currently spiderfied (if any) 2084 _spiderfied: null, 2085 2086 _spiderfierOnAdd: function () { 2087 this._map.on('click', this._unspiderfyWrapper, this); 2088 2089 if (this._map.options.zoomAnimation) { 2090 this._map.on('zoomstart', this._unspiderfyZoomStart, this); 2091 } 2092 //Browsers without zoomAnimation or a big zoom don't fire zoomstart 2093 this._map.on('zoomend', this._noanimationUnspiderfy, this); 2094 2095 if (L.Path.SVG && !L.Browser.touch) { 2096 this._map._initPathRoot(); 2097 //Needs to happen in the pageload, not after, or animations don't work in webkit 2098 // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements 2099 //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable 2100 } 2101 }, 2102 2103 _spiderfierOnRemove: function () { 2104 this._map.off('click', this._unspiderfyWrapper, this); 2105 this._map.off('zoomstart', this._unspiderfyZoomStart, this); 2106 this._map.off('zoomanim', this._unspiderfyZoomAnim, this); 2107 2108 this._unspiderfy(); //Ensure that markers are back where they should be 2109 }, 2110 2111 2112 //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated) 2113 //This means we can define the animation they do rather than Markers doing an animation to their actual location 2114 _unspiderfyZoomStart: function () { 2115 if (!this._map) { //May have been removed from the map by a zoomEnd handler 2116 return; 2117 } 2118 2119 this._map.on('zoomanim', this._unspiderfyZoomAnim, this); 2120 }, 2121 _unspiderfyZoomAnim: function (zoomDetails) { 2122 //Wait until the first zoomanim after the user has finished touch-zooming before running the animation 2123 if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) { 2124 return; 2125 } 2126 2127 this._map.off('zoomanim', this._unspiderfyZoomAnim, this); 2128 this._unspiderfy(zoomDetails); 2129 }, 2130 2131 2132 _unspiderfyWrapper: function () { 2133 /// <summary>_unspiderfy but passes no arguments</summary> 2134 this._unspiderfy(); 2135 }, 2136 2137 _unspiderfy: function (zoomDetails) { 2138 if (this._spiderfied) { 2139 this._spiderfied.unspiderfy(zoomDetails); 2140 } 2141 }, 2142 2143 _noanimationUnspiderfy: function () { 2144 if (this._spiderfied) { 2145 this._spiderfied._noanimationUnspiderfy(); 2146 } 2147 }, 2148 2149 //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc 2150 _unspiderfyLayer: function (layer) { 2151 if (layer._spiderLeg) { 2152 this._featureGroup.removeLayer(layer); 2153 2154 layer.setOpacity(1); 2155 //Position will be fixed up immediately in _animationUnspiderfy 2156 layer.setZIndexOffset(0); 2157 2158 this._map.removeLayer(layer._spiderLeg); 2159 delete layer._spiderLeg; 2160 } 2161 } 2162}); 2163 2164 2165}(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.