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