Path: blob/master/core/js/cluster/leaflet.markercluster-src.js
1209 views
/*1Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.2https://github.com/Leaflet/Leaflet.markercluster3(c) 2012-2013, Dave Leaver, smartrak4*/5(function (window, document, undefined) {6/*7* L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within8*/910L.MarkerClusterGroup = L.FeatureGroup.extend({1112options: {13maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center14iconCreateFunction: null,1516spiderfyOnMaxZoom: true,17showCoverageOnHover: true,18zoomToBoundsOnClick: true,19singleMarkerMode: false,2021disableClusteringAtZoom: null,2223// Setting this to false prevents the removal of any clusters outside of the viewpoint, which24// is the default behaviour for performance reasons.25removeOutsideVisibleBounds: true,2627//Whether to animate adding markers after adding the MarkerClusterGroup to the map28// If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.29animateAddingMarkers: false,3031//Increase to increase the distance away that spiderfied markers appear from the center32spiderfyDistanceMultiplier: 1,3334//Options to pass to the L.Polygon constructor35polygonOptions: {}36},3738initialize: function (options) {39L.Util.setOptions(this, options);40if (!this.options.iconCreateFunction) {41this.options.iconCreateFunction = this._defaultIconCreateFunction;42}4344this._featureGroup = L.featureGroup();45this._featureGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);4647this._nonPointGroup = L.featureGroup();48this._nonPointGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);4950this._inZoomAnimation = 0;51this._needsClustering = [];52this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of53//The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move54this._currentShownBounds = null;55},5657addLayer: function (layer) {5859if (layer instanceof L.LayerGroup) {60var array = [];61for (var i in layer._layers) {62array.push(layer._layers[i]);63}64return this.addLayers(array);65}6667//Don't cluster non point data68if (!layer.getLatLng) {69this._nonPointGroup.addLayer(layer);70return this;71}7273if (!this._map) {74this._needsClustering.push(layer);75return this;76}7778if (this.hasLayer(layer)) {79return this;80}818283//If we have already clustered we'll need to add this one to a cluster8485if (this._unspiderfy) {86this._unspiderfy();87}8889this._addLayer(layer, this._maxZoom);9091//Work out what is visible92var visibleLayer = layer,93currentZoom = this._map.getZoom();94if (layer.__parent) {95while (visibleLayer.__parent._zoom >= currentZoom) {96visibleLayer = visibleLayer.__parent;97}98}99100if (this._currentShownBounds.contains(visibleLayer.getLatLng())) {101if (this.options.animateAddingMarkers) {102this._animationAddLayer(layer, visibleLayer);103} else {104this._animationAddLayerNonAnimated(layer, visibleLayer);105}106}107return this;108},109110removeLayer: function (layer) {111112if (layer instanceof L.LayerGroup)113{114var array = [];115for (var i in layer._layers) {116array.push(layer._layers[i]);117}118return this.removeLayers(array);119}120121//Non point layers122if (!layer.getLatLng) {123this._nonPointGroup.removeLayer(layer);124return this;125}126127if (!this._map) {128if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) {129this._needsRemoving.push(layer);130}131return this;132}133134if (!layer.__parent) {135return this;136}137138if (this._unspiderfy) {139this._unspiderfy();140this._unspiderfyLayer(layer);141}142143//Remove the marker from clusters144this._removeLayer(layer, true);145146if (this._featureGroup.hasLayer(layer)) {147this._featureGroup.removeLayer(layer);148if (layer.setOpacity) {149layer.setOpacity(1);150}151}152153return this;154},155156//Takes an array of markers and adds them in bulk157addLayers: function (layersArray) {158var i, l, m,159onMap = this._map,160fg = this._featureGroup,161npg = this._nonPointGroup;162163for (i = 0, l = layersArray.length; i < l; i++) {164m = layersArray[i];165166//Not point data, can't be clustered167if (!m.getLatLng) {168npg.addLayer(m);169continue;170}171172if (this.hasLayer(m)) {173continue;174}175176if (!onMap) {177this._needsClustering.push(m);178continue;179}180181this._addLayer(m, this._maxZoom);182183//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 will184if (m.__parent) {185if (m.__parent.getChildCount() === 2) {186var markers = m.__parent.getAllChildMarkers(),187otherMarker = markers[0] === m ? markers[1] : markers[0];188fg.removeLayer(otherMarker);189}190}191}192193if (onMap) {194//Update the icons of all those visible clusters that were affected195fg.eachLayer(function (c) {196if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) {197c._updateIcon();198}199});200201this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);202}203204return this;205},206207//Takes an array of markers and removes them in bulk208removeLayers: function (layersArray) {209var i, l, m,210fg = this._featureGroup,211npg = this._nonPointGroup;212213if (!this._map) {214for (i = 0, l = layersArray.length; i < l; i++) {215m = layersArray[i];216this._arraySplice(this._needsClustering, m);217npg.removeLayer(m);218}219return this;220}221222for (i = 0, l = layersArray.length; i < l; i++) {223m = layersArray[i];224225if (!m.__parent) {226npg.removeLayer(m);227continue;228}229230this._removeLayer(m, true, true);231232if (fg.hasLayer(m)) {233fg.removeLayer(m);234if (m.setOpacity) {235m.setOpacity(1);236}237}238}239240//Fix up the clusters and markers on the map241this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);242243fg.eachLayer(function (c) {244if (c instanceof L.MarkerCluster) {245c._updateIcon();246}247});248249return this;250},251252//Removes all layers from the MarkerClusterGroup253clearLayers: function () {254//Need our own special implementation as the LayerGroup one doesn't work for us255256//If we aren't on the map (yet), blow away the markers we know of257if (!this._map) {258this._needsClustering = [];259delete this._gridClusters;260delete this._gridUnclustered;261}262263if (this._noanimationUnspiderfy) {264this._noanimationUnspiderfy();265}266267//Remove all the visible layers268this._featureGroup.clearLayers();269this._nonPointGroup.clearLayers();270271this.eachLayer(function (marker) {272delete marker.__parent;273});274275if (this._map) {276//Reset _topClusterLevel and the DistanceGrids277this._generateInitialClusters();278}279280return this;281},282283//Override FeatureGroup.getBounds as it doesn't work284getBounds: function () {285var bounds = new L.LatLngBounds();286if (this._topClusterLevel) {287bounds.extend(this._topClusterLevel._bounds);288} else {289for (var i = this._needsClustering.length - 1; i >= 0; i--) {290bounds.extend(this._needsClustering[i].getLatLng());291}292}293294//TODO: Can remove this isValid test when leaflet 0.6 is released295var nonPointBounds = this._nonPointGroup.getBounds();296if (nonPointBounds.isValid()) {297bounds.extend(nonPointBounds);298}299300return bounds;301},302303//Overrides LayerGroup.eachLayer304eachLayer: function (method, context) {305var markers = this._needsClustering.slice(),306i;307308if (this._topClusterLevel) {309this._topClusterLevel.getAllChildMarkers(markers);310}311312for (i = markers.length - 1; i >= 0; i--) {313method.call(context, markers[i]);314}315316this._nonPointGroup.eachLayer(method, context);317},318319//Returns true if the given layer is in this MarkerClusterGroup320hasLayer: function (layer) {321if (!layer) {322return false;323}324325var i, anArray = this._needsClustering;326327for (i = anArray.length - 1; i >= 0; i--) {328if (anArray[i] === layer) {329return true;330}331}332333anArray = this._needsRemoving;334for (i = anArray.length - 1; i >= 0; i--) {335if (anArray[i] === layer) {336return false;337}338}339340return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer);341},342343//Zoom down to show the given layer (spiderfying if necessary) then calls the callback344zoomToShowLayer: function (layer, callback) {345346var showMarker = function () {347if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) {348this._map.off('moveend', showMarker, this);349this.off('animationend', showMarker, this);350351if (layer._icon) {352callback();353} else if (layer.__parent._icon) {354var afterSpiderfy = function () {355this.off('spiderfied', afterSpiderfy, this);356callback();357};358359this.on('spiderfied', afterSpiderfy, this);360layer.__parent.spiderfy();361}362}363};364365if (layer._icon) {366callback();367} else if (layer.__parent._zoom < this._map.getZoom()) {368//Layer should be visible now but isn't on screen, just pan over to it369this._map.on('moveend', showMarker, this);370if (!layer._icon) {371this._map.panTo(layer.getLatLng());372}373} else {374this._map.on('moveend', showMarker, this);375this.on('animationend', showMarker, this);376this._map.setView(layer.getLatLng(), layer.__parent._zoom + 1);377layer.__parent.zoomToBounds();378}379},380381//Overrides FeatureGroup.onAdd382onAdd: function (map) {383this._map = map;384var i, l, layer;385386if (!isFinite(this._map.getMaxZoom())) {387throw "Map has no maxZoom specified";388}389390this._featureGroup.onAdd(map);391this._nonPointGroup.onAdd(map);392393if (!this._gridClusters) {394this._generateInitialClusters();395}396397for (i = 0, l = this._needsRemoving.length; i < l; i++) {398layer = this._needsRemoving[i];399this._removeLayer(layer, true);400}401this._needsRemoving = [];402403for (i = 0, l = this._needsClustering.length; i < l; i++) {404layer = this._needsClustering[i];405406//If the layer doesn't have a getLatLng then we can't cluster it, so add it to our child featureGroup407if (!layer.getLatLng) {408this._featureGroup.addLayer(layer);409continue;410}411412413if (layer.__parent) {414continue;415}416this._addLayer(layer, this._maxZoom);417}418this._needsClustering = [];419420421this._map.on('zoomend', this._zoomEnd, this);422this._map.on('moveend', this._moveEnd, this);423424if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely425this._spiderfierOnAdd();426}427428this._bindEvents();429430431//Actually add our markers to the map:432433//Remember the current zoom level and bounds434this._zoom = this._map.getZoom();435this._currentShownBounds = this._getExpandedVisibleBounds();436437//Make things appear on the map438this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);439},440441//Overrides FeatureGroup.onRemove442onRemove: function (map) {443map.off('zoomend', this._zoomEnd, this);444map.off('moveend', this._moveEnd, this);445446this._unbindEvents();447448//In case we are in a cluster animation449this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');450451if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely452this._spiderfierOnRemove();453}454455//Clean up all the layers we added to the map456this._featureGroup.onRemove(map);457this._nonPointGroup.onRemove(map);458459this._featureGroup.clearLayers();460461this._map = null;462},463464getVisibleParent: function (marker) {465var vMarker = marker;466while (vMarker !== null && !vMarker._icon) {467vMarker = vMarker.__parent;468}469return vMarker;470},471472//Remove the given object from the given array473_arraySplice: function (anArray, obj) {474for (var i = anArray.length - 1; i >= 0; i--) {475if (anArray[i] === obj) {476anArray.splice(i, 1);477return true;478}479}480},481482//Internal function for removing a marker from everything.483//dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)484_removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) {485var gridClusters = this._gridClusters,486gridUnclustered = this._gridUnclustered,487fg = this._featureGroup,488map = this._map;489490//Remove the marker from distance clusters it might be in491if (removeFromDistanceGrid) {492for (var z = this._maxZoom; z >= 0; z--) {493if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) {494break;495}496}497}498499//Work our way up the clusters removing them as we go if required500var cluster = marker.__parent,501markers = cluster._markers,502otherMarker;503504//Remove the marker from the immediate parents marker list505this._arraySplice(markers, marker);506507while (cluster) {508cluster._childCount--;509510if (cluster._zoom < 0) {511//Top level, do nothing512break;513} else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required514//We need to push the other marker up to the parent515otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0];516517//Update distance grid518gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom));519gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom));520521//Move otherMarker up to parent522this._arraySplice(cluster.__parent._childClusters, cluster);523cluster.__parent._markers.push(otherMarker);524otherMarker.__parent = cluster.__parent;525526if (cluster._icon) {527//Cluster is currently on the map, need to put the marker on the map instead528fg.removeLayer(cluster);529if (!dontUpdateMap) {530fg.addLayer(otherMarker);531}532}533} else {534cluster._recalculateBounds();535if (!dontUpdateMap || !cluster._icon) {536cluster._updateIcon();537}538}539540cluster = cluster.__parent;541}542543delete marker.__parent;544},545546_propagateEvent: function (e) {547if (e.layer instanceof L.MarkerCluster) {548e.type = 'cluster' + e.type;549}550551this.fire(e.type, e);552},553554//Default functionality555_defaultIconCreateFunction: function (cluster) {556var childCount = cluster.getChildCount();557558var c = ' marker-cluster-';559if (childCount < 10) {560c += 'small';561} else if (childCount < 100) {562c += 'medium';563} else {564c += 'large';565}566567return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });568},569570_bindEvents: function () {571var map = this._map,572spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,573showCoverageOnHover = this.options.showCoverageOnHover,574zoomToBoundsOnClick = this.options.zoomToBoundsOnClick;575576//Zoom on cluster click or spiderfy if we are at the lowest level577if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {578this.on('clusterclick', this._zoomOrSpiderfy, this);579}580581//Show convex hull (boundary) polygon on mouse over582if (showCoverageOnHover) {583this.on('clustermouseover', this._hideCoverage, this);584this.on('clustermouseout', this._hideCoverage, this);585map.on('zoomend', this._hideCoverage, this);586map.on('layerremove', this._hideCoverageOnRemove, this);587}588},589590_zoomOrSpiderfy: function (e) {591var map = this._map;592if (map.getMaxZoom() === map.getZoom()) {593if (this.options.spiderfyOnMaxZoom) {594e.layer.spiderfy();595}596} else if (this.options.zoomToBoundsOnClick) {597e.layer.zoomToBounds();598}599},600601_showCoverage: function (e) {602var map = this._map;603if (this._inZoomAnimation) {604return;605}606if (this._shownPolygon) {607map.removeLayer(this._shownPolygon);608}609if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) {610this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions);611map.addLayer(this._shownPolygon);612}613},614615_hideCoverage: function () {616if (this._shownPolygon) {617this._map.removeLayer(this._shownPolygon);618this._shownPolygon = null;619}620},621622_hideCoverageOnRemove: function (e) {623if (e.layer === this) {624this._hideCoverage();625}626},627628_unbindEvents: function () {629var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,630showCoverageOnHover = this.options.showCoverageOnHover,631zoomToBoundsOnClick = this.options.zoomToBoundsOnClick,632map = this._map;633634if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {635this.off('clusterclick', this._zoomOrSpiderfy, this);636}637if (showCoverageOnHover) {638this.off('clustermouseover', this._showCoverage, this);639this.off('clustermouseout', this._hideCoverage, this);640map.off('zoomend', this._hideCoverage, this);641map.off('layerremove', this._hideCoverageOnRemove, this);642}643},644645_zoomEnd: function () {646if (!this._map) { //May have been removed from the map by a zoomEnd handler647return;648}649this._mergeSplitClusters();650651this._zoom = this._map._zoom;652this._currentShownBounds = this._getExpandedVisibleBounds();653},654655_moveEnd: function () {656if (this._inZoomAnimation) {657return;658}659660var newBounds = this._getExpandedVisibleBounds();661662this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds);663this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, newBounds);664665this._currentShownBounds = newBounds;666return;667},668669_generateInitialClusters: function () {670var maxZoom = this._map.getMaxZoom(),671radius = this.options.maxClusterRadius;672673if (this.options.disableClusteringAtZoom) {674maxZoom = this.options.disableClusteringAtZoom - 1;675}676this._maxZoom = maxZoom;677this._gridClusters = {};678this._gridUnclustered = {};679680//Set up DistanceGrids for each zoom681for (var zoom = maxZoom; zoom >= 0; zoom--) {682this._gridClusters[zoom] = new L.DistanceGrid(radius);683this._gridUnclustered[zoom] = new L.DistanceGrid(radius);684}685686this._topClusterLevel = new L.MarkerCluster(this, -1);687},688689//Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)690_addLayer: function (layer, zoom) {691var gridClusters = this._gridClusters,692gridUnclustered = this._gridUnclustered,693markerPoint, z;694695if (this.options.singleMarkerMode) {696layer.options.icon = this.options.iconCreateFunction({697getChildCount: function () {698return 1;699},700getAllChildMarkers: function () {701return [layer];702}703});704}705706//Find the lowest zoom level to slot this one in707for (; zoom >= 0; zoom--) {708markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position709710//Try find a cluster close by711var closest = gridClusters[zoom].getNearObject(markerPoint);712if (closest) {713closest._addChild(layer);714layer.__parent = closest;715return;716}717718//Try find a marker close by to form a new cluster with719closest = gridUnclustered[zoom].getNearObject(markerPoint);720if (closest) {721var parent = closest.__parent;722if (parent) {723this._removeLayer(closest, false);724}725726//Create new cluster with these 2 in it727728var newCluster = new L.MarkerCluster(this, zoom, closest, layer);729gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom));730closest.__parent = newCluster;731layer.__parent = newCluster;732733//First create any new intermediate parent clusters that don't exist734var lastParent = newCluster;735for (z = zoom - 1; z > parent._zoom; z--) {736lastParent = new L.MarkerCluster(this, z, lastParent);737gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z));738}739parent._addChild(lastParent);740741//Remove closest from this zoom level and any above that it is in, replace with newCluster742for (z = zoom; z >= 0; z--) {743if (!gridUnclustered[z].removeObject(closest, this._map.project(closest.getLatLng(), z))) {744break;745}746}747748return;749}750751//Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards752gridUnclustered[zoom].addObject(layer, markerPoint);753}754755//Didn't get in anything, add us to the top756this._topClusterLevel._addChild(layer);757layer.__parent = this._topClusterLevel;758return;759},760761//Merge and split any existing clusters that are too big or small762_mergeSplitClusters: function () {763if (this._zoom < this._map._zoom) { //Zoom in, split764this._animationStart();765//Remove clusters now off screen766this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds());767768this._animationZoomIn(this._zoom, this._map._zoom);769770} else if (this._zoom > this._map._zoom) { //Zoom out, merge771this._animationStart();772773this._animationZoomOut(this._zoom, this._map._zoom);774} else {775this._moveEnd();776}777},778779//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)780_getExpandedVisibleBounds: function () {781if (!this.options.removeOutsideVisibleBounds) {782return this.getBounds();783}784785var map = this._map,786bounds = map.getBounds(),787sw = bounds._southWest,788ne = bounds._northEast,789latDiff = L.Browser.mobile ? 0 : Math.abs(sw.lat - ne.lat),790lngDiff = L.Browser.mobile ? 0 : Math.abs(sw.lng - ne.lng);791792return new L.LatLngBounds(793new L.LatLng(sw.lat - latDiff, sw.lng - lngDiff, true),794new L.LatLng(ne.lat + latDiff, ne.lng + lngDiff, true));795},796797//Shared animation code798_animationAddLayerNonAnimated: function (layer, newCluster) {799if (newCluster === layer) {800this._featureGroup.addLayer(layer);801} else if (newCluster._childCount === 2) {802newCluster._addToMap();803804var markers = newCluster.getAllChildMarkers();805this._featureGroup.removeLayer(markers[0]);806this._featureGroup.removeLayer(markers[1]);807} else {808newCluster._updateIcon();809}810}811});812813L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {814815//Non Animated versions of everything816_animationStart: function () {817//Do nothing...818},819_animationZoomIn: function (previousZoomLevel, newZoomLevel) {820this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);821this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());822},823_animationZoomOut: function (previousZoomLevel, newZoomLevel) {824this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);825this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());826},827_animationAddLayer: function (layer, newCluster) {828this._animationAddLayerNonAnimated(layer, newCluster);829}830} : {831832//Animated versions here833_animationStart: function () {834this._map._mapPane.className += ' leaflet-cluster-anim';835this._inZoomAnimation++;836},837_animationEnd: function () {838if (this._map) {839this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');840}841this._inZoomAnimation--;842this.fire('animationend');843},844_animationZoomIn: function (previousZoomLevel, newZoomLevel) {845var me = this,846bounds = this._getExpandedVisibleBounds(),847fg = this._featureGroup,848i;849850//Add all children of current clusters to map and remove those clusters from map851this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {852var startPos = c._latlng,853markers = c._markers,854m;855856if (!bounds.contains(startPos)) {857startPos = null;858}859860if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us861fg.removeLayer(c);862c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);863} else {864//Fade out old cluster865c.setOpacity(0);866c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds);867}868869//Remove all markers that aren't visible any more870//TODO: Do we actually need to do this on the higher levels too?871for (i = markers.length - 1; i >= 0; i--) {872m = markers[i];873if (!bounds.contains(m._latlng)) {874fg.removeLayer(m);875}876}877878});879880this._forceLayout();881882//Update opacities883me._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel);884//TODO Maybe? Update markers in _recursivelyBecomeVisible885fg.eachLayer(function (n) {886if (!(n instanceof L.MarkerCluster) && n._icon) {887n.setOpacity(1);888}889});890891//update the positions of the just added clusters/markers892me._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {893c._recursivelyRestoreChildPositions(newZoomLevel);894});895896//Remove the old clusters and close the zoom animation897898setTimeout(function () {899//update the positions of the just added clusters/markers900me._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {901fg.removeLayer(c);902c.setOpacity(1);903});904905me._animationEnd();906}, 200);907},908909_animationZoomOut: function (previousZoomLevel, newZoomLevel) {910this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel);911912//Need to add markers for those that weren't on the map before but are now913this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());914//Remove markers that were on the map before but won't be now915this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel, this._getExpandedVisibleBounds());916},917_animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) {918var bounds = this._getExpandedVisibleBounds();919920//Animate all of the markers in the clusters to move to their cluster center point921cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel);922923var me = this;924925//Update the opacity (If we immediately set it they won't animate)926this._forceLayout();927cluster._recursivelyBecomeVisible(bounds, newZoomLevel);928929//TODO: Maybe use the transition timing stuff to make this more reliable930//When the animations are done, tidy up931setTimeout(function () {932933//This cluster stopped being a cluster before the timeout fired934if (cluster._childCount === 1) {935var m = cluster._markers[0];936//If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it937m.setLatLng(m.getLatLng());938m.setOpacity(1);939} else {940cluster._recursively(bounds, newZoomLevel, 0, function (c) {941c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1);942});943}944me._animationEnd();945}, 200);946},947_animationAddLayer: function (layer, newCluster) {948var me = this,949fg = this._featureGroup;950951fg.addLayer(layer);952if (newCluster !== layer) {953if (newCluster._childCount > 2) { //Was already a cluster954955newCluster._updateIcon();956this._forceLayout();957this._animationStart();958959layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));960layer.setOpacity(0);961962setTimeout(function () {963fg.removeLayer(layer);964layer.setOpacity(1);965966me._animationEnd();967}, 200);968969} else { //Just became a cluster970this._forceLayout();971972me._animationStart();973me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom());974}975}976},977978//Force a browser layout of stuff in the map979// Should apply the current opacity and location to all elements so we can update them again for an animation980_forceLayout: function () {981//In my testing this works, infact offsetWidth of any element seems to work.982//Could loop all this._layers and do this for each _icon if it stops working983984L.Util.falseFn(document.body.offsetWidth);985}986});987988L.markerClusterGroup = function (options) {989return new L.MarkerClusterGroup(options);990};991992993L.MarkerCluster = L.Marker.extend({994initialize: function (group, zoom, a, b) {995996L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this });997998999this._group = group;1000this._zoom = zoom;10011002this._markers = [];1003this._childClusters = [];1004this._childCount = 0;1005this._iconNeedsUpdate = true;10061007this._bounds = new L.LatLngBounds();10081009if (a) {1010this._addChild(a);1011}1012if (b) {1013this._addChild(b);1014}1015},10161017//Recursively retrieve all child markers of this cluster1018getAllChildMarkers: function (storageArray) {1019storageArray = storageArray || [];10201021for (var i = this._childClusters.length - 1; i >= 0; i--) {1022this._childClusters[i].getAllChildMarkers(storageArray);1023}10241025for (var j = this._markers.length - 1; j >= 0; j--) {1026storageArray.push(this._markers[j]);1027}10281029return storageArray;1030},10311032//Returns the count of how many child markers we have1033getChildCount: function () {1034return this._childCount;1035},10361037//Zoom to the extents of this cluster1038zoomToBounds: function () {1039this._group._map.fitBounds(this._bounds);1040},10411042getBounds: function () {1043var bounds = new L.LatLngBounds();1044bounds.extend(this._bounds);1045return bounds;1046},10471048_updateIcon: function () {1049this._iconNeedsUpdate = true;1050if (this._icon) {1051this.setIcon(this);1052}1053},10541055//Cludge for Icon, we pretend to be an icon for performance1056createIcon: function () {1057if (this._iconNeedsUpdate) {1058this._iconObj = this._group.options.iconCreateFunction(this);1059this._iconNeedsUpdate = false;1060}1061return this._iconObj.createIcon();1062},1063createShadow: function () {1064return this._iconObj.createShadow();1065},106610671068_addChild: function (new1, isNotificationFromChild) {10691070this._iconNeedsUpdate = true;1071this._expandBounds(new1);10721073if (new1 instanceof L.MarkerCluster) {1074if (!isNotificationFromChild) {1075this._childClusters.push(new1);1076new1.__parent = this;1077}1078this._childCount += new1._childCount;1079} else {1080if (!isNotificationFromChild) {1081this._markers.push(new1);1082}1083this._childCount++;1084}10851086if (this.__parent) {1087this.__parent._addChild(new1, true);1088}1089},10901091//Expand our bounds and tell our parent to1092_expandBounds: function (marker) {1093var addedCount,1094addedLatLng = marker._wLatLng || marker._latlng;10951096if (marker instanceof L.MarkerCluster) {1097this._bounds.extend(marker._bounds);1098addedCount = marker._childCount;1099} else {1100this._bounds.extend(addedLatLng);1101addedCount = 1;1102}11031104if (!this._cLatLng) {1105// when clustering, take position of the first point as the cluster center1106this._cLatLng = marker._cLatLng || addedLatLng;1107}11081109// when showing clusters, take weighted average of all points as cluster center1110var totalCount = this._childCount + addedCount;11111112//Calculate weighted latlng for display1113if (!this._wLatLng) {1114this._latlng = this._wLatLng = new L.LatLng(addedLatLng.lat, addedLatLng.lng);1115} else {1116this._wLatLng.lat = (addedLatLng.lat * addedCount + this._wLatLng.lat * this._childCount) / totalCount;1117this._wLatLng.lng = (addedLatLng.lng * addedCount + this._wLatLng.lng * this._childCount) / totalCount;1118}1119},11201121//Set our markers position as given and add it to the map1122_addToMap: function (startPos) {1123if (startPos) {1124this._backupLatlng = this._latlng;1125this.setLatLng(startPos);1126}1127this._group._featureGroup.addLayer(this);1128},11291130_recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {1131this._recursively(bounds, 0, maxZoom - 1,1132function (c) {1133var markers = c._markers,1134i, m;1135for (i = markers.length - 1; i >= 0; i--) {1136m = markers[i];11371138//Only do it if the icon is still on the map1139if (m._icon) {1140m._setPos(center);1141m.setOpacity(0);1142}1143}1144},1145function (c) {1146var childClusters = c._childClusters,1147j, cm;1148for (j = childClusters.length - 1; j >= 0; j--) {1149cm = childClusters[j];1150if (cm._icon) {1151cm._setPos(center);1152cm.setOpacity(0);1153}1154}1155}1156);1157},11581159_recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, previousZoomLevel, newZoomLevel) {1160this._recursively(bounds, newZoomLevel, 0,1161function (c) {1162c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);11631164//TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.1165//As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate1166if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) {1167c.setOpacity(1);1168c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds1169} else {1170c.setOpacity(0);1171}11721173c._addToMap();1174}1175);1176},11771178_recursivelyBecomeVisible: function (bounds, zoomLevel) {1179this._recursively(bounds, 0, zoomLevel, null, function (c) {1180c.setOpacity(1);1181});1182},11831184_recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {1185this._recursively(bounds, -1, zoomLevel,1186function (c) {1187if (zoomLevel === c._zoom) {1188return;1189}11901191//Add our child markers at startPos (so they can be animated out)1192for (var i = c._markers.length - 1; i >= 0; i--) {1193var nm = c._markers[i];11941195if (!bounds.contains(nm._latlng)) {1196continue;1197}11981199if (startPos) {1200nm._backupLatlng = nm.getLatLng();12011202nm.setLatLng(startPos);1203if (nm.setOpacity) {1204nm.setOpacity(0);1205}1206}12071208c._group._featureGroup.addLayer(nm);1209}1210},1211function (c) {1212c._addToMap(startPos);1213}1214);1215},12161217_recursivelyRestoreChildPositions: function (zoomLevel) {1218//Fix positions of child markers1219for (var i = this._markers.length - 1; i >= 0; i--) {1220var nm = this._markers[i];1221if (nm._backupLatlng) {1222nm.setLatLng(nm._backupLatlng);1223delete nm._backupLatlng;1224}1225}12261227if (zoomLevel - 1 === this._zoom) {1228//Reposition child clusters1229for (var j = this._childClusters.length - 1; j >= 0; j--) {1230this._childClusters[j]._restorePosition();1231}1232} else {1233for (var k = this._childClusters.length - 1; k >= 0; k--) {1234this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);1235}1236}1237},12381239_restorePosition: function () {1240if (this._backupLatlng) {1241this.setLatLng(this._backupLatlng);1242delete this._backupLatlng;1243}1244},12451246//exceptBounds: If set, don't remove any markers/clusters in it1247_recursivelyRemoveChildrenFromMap: function (previousBounds, zoomLevel, exceptBounds) {1248var m, i;1249this._recursively(previousBounds, -1, zoomLevel - 1,1250function (c) {1251//Remove markers at every level1252for (i = c._markers.length - 1; i >= 0; i--) {1253m = c._markers[i];1254if (!exceptBounds || !exceptBounds.contains(m._latlng)) {1255c._group._featureGroup.removeLayer(m);1256if (m.setOpacity) {1257m.setOpacity(1);1258}1259}1260}1261},1262function (c) {1263//Remove child clusters at just the bottom level1264for (i = c._childClusters.length - 1; i >= 0; i--) {1265m = c._childClusters[i];1266if (!exceptBounds || !exceptBounds.contains(m._latlng)) {1267c._group._featureGroup.removeLayer(m);1268if (m.setOpacity) {1269m.setOpacity(1);1270}1271}1272}1273}1274);1275},12761277//Run the given functions recursively to this and child clusters1278// boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to1279// zoomLevelToStart: zoom level to start running functions (inclusive)1280// zoomLevelToStop: zoom level to stop running functions (inclusive)1281// runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level1282// runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level1283_recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) {1284var childClusters = this._childClusters,1285zoom = this._zoom,1286i, c;12871288if (zoomLevelToStart > zoom) { //Still going down to required depth, just recurse to child clusters1289for (i = childClusters.length - 1; i >= 0; i--) {1290c = childClusters[i];1291if (boundsToApplyTo.intersects(c._bounds)) {1292c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);1293}1294}1295} else { //In required depth12961297if (runAtEveryLevel) {1298runAtEveryLevel(this);1299}1300if (runAtBottomLevel && this._zoom === zoomLevelToStop) {1301runAtBottomLevel(this);1302}13031304//TODO: This loop is almost the same as above1305if (zoomLevelToStop > zoom) {1306for (i = childClusters.length - 1; i >= 0; i--) {1307c = childClusters[i];1308if (boundsToApplyTo.intersects(c._bounds)) {1309c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);1310}1311}1312}1313}1314},13151316_recalculateBounds: function () {1317var markers = this._markers,1318childClusters = this._childClusters,1319i;13201321this._bounds = new L.LatLngBounds();1322delete this._wLatLng;13231324for (i = markers.length - 1; i >= 0; i--) {1325this._expandBounds(markers[i]);1326}1327for (i = childClusters.length - 1; i >= 0; i--) {1328this._expandBounds(childClusters[i]);1329}1330},133113321333//Returns true if we are the parent of only one cluster and that cluster is the same as us1334_isSingleParent: function () {1335//Don't need to check this._markers as the rest won't work if there are any1336return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount;1337}1338});1339134013411342L.DistanceGrid = function (cellSize) {1343this._cellSize = cellSize;1344this._sqCellSize = cellSize * cellSize;1345this._grid = {};1346this._objectPoint = { };1347};13481349L.DistanceGrid.prototype = {13501351addObject: function (obj, point) {1352var x = this._getCoord(point.x),1353y = this._getCoord(point.y),1354grid = this._grid,1355row = grid[y] = grid[y] || {},1356cell = row[x] = row[x] || [],1357stamp = L.Util.stamp(obj);13581359this._objectPoint[stamp] = point;13601361cell.push(obj);1362},13631364updateObject: function (obj, point) {1365this.removeObject(obj);1366this.addObject(obj, point);1367},13681369//Returns true if the object was found1370removeObject: function (obj, point) {1371var x = this._getCoord(point.x),1372y = this._getCoord(point.y),1373grid = this._grid,1374row = grid[y] = grid[y] || {},1375cell = row[x] = row[x] || [],1376i, len;13771378delete this._objectPoint[L.Util.stamp(obj)];13791380for (i = 0, len = cell.length; i < len; i++) {1381if (cell[i] === obj) {13821383cell.splice(i, 1);13841385if (len === 1) {1386delete row[x];1387}13881389return true;1390}1391}13921393},13941395eachObject: function (fn, context) {1396var i, j, k, len, row, cell, removed,1397grid = this._grid;13981399for (i in grid) {1400row = grid[i];14011402for (j in row) {1403cell = row[j];14041405for (k = 0, len = cell.length; k < len; k++) {1406removed = fn.call(context, cell[k]);1407if (removed) {1408k--;1409len--;1410}1411}1412}1413}1414},14151416getNearObject: function (point) {1417var x = this._getCoord(point.x),1418y = this._getCoord(point.y),1419i, j, k, row, cell, len, obj, dist,1420objectPoint = this._objectPoint,1421closestDistSq = this._sqCellSize,1422closest = null;14231424for (i = y - 1; i <= y + 1; i++) {1425row = this._grid[i];1426if (row) {14271428for (j = x - 1; j <= x + 1; j++) {1429cell = row[j];1430if (cell) {14311432for (k = 0, len = cell.length; k < len; k++) {1433obj = cell[k];1434dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point);1435if (dist < closestDistSq) {1436closestDistSq = dist;1437closest = obj;1438}1439}1440}1441}1442}1443}1444return closest;1445},14461447_getCoord: function (x) {1448return Math.floor(x / this._cellSize);1449},14501451_sqDist: function (p, p2) {1452var dx = p2.x - p.x,1453dy = p2.y - p.y;1454return dx * dx + dy * dy;1455}1456};145714581459/* Copyright (c) 2012 the authors listed at the following URL, and/or1460the authors of referenced articles or incorporated external code:1461http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=2012041017525614621463Permission is hereby granted, free of charge, to any person obtaining1464a copy of this software and associated documentation files (the1465"Software"), to deal in the Software without restriction, including1466without limitation the rights to use, copy, modify, merge, publish,1467distribute, sublicense, and/or sell copies of the Software, and to1468permit persons to whom the Software is furnished to do so, subject to1469the following conditions:14701471The above copyright notice and this permission notice shall be1472included in all copies or substantial portions of the Software.14731474THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,1475EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF1476MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.1477IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY1478CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,1479TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE1480SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.14811482Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=184341483*/14841485(function () {1486L.QuickHull = {1487getDistant: function (cpt, bl) {1488var vY = bl[1].lat - bl[0].lat,1489vX = bl[0].lng - bl[1].lng;1490return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng));1491},149214931494findMostDistantPointFromBaseLine: function (baseLine, latLngs) {1495var maxD = 0,1496maxPt = null,1497newPoints = [],1498i, pt, d;14991500for (i = latLngs.length - 1; i >= 0; i--) {1501pt = latLngs[i];1502d = this.getDistant(pt, baseLine);15031504if (d > 0) {1505newPoints.push(pt);1506} else {1507continue;1508}15091510if (d > maxD) {1511maxD = d;1512maxPt = pt;1513}15141515}1516return { 'maxPoint': maxPt, 'newPoints': newPoints };1517},15181519buildConvexHull: function (baseLine, latLngs) {1520var convexHullBaseLines = [],1521t = this.findMostDistantPointFromBaseLine(baseLine, latLngs);15221523if (t.maxPoint) { // if there is still a point "outside" the base line1524convexHullBaseLines =1525convexHullBaseLines.concat(1526this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints)1527);1528convexHullBaseLines =1529convexHullBaseLines.concat(1530this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints)1531);1532return convexHullBaseLines;1533} else { // if there is no more point "outside" the base line, the current base line is part of the convex hull1534return [baseLine];1535}1536},15371538getConvexHull: function (latLngs) {1539//find first baseline1540var maxLat = false, minLat = false,1541maxPt = null, minPt = null,1542i;15431544for (i = latLngs.length - 1; i >= 0; i--) {1545var pt = latLngs[i];1546if (maxLat === false || pt.lat > maxLat) {1547maxPt = pt;1548maxLat = pt.lat;1549}1550if (minLat === false || pt.lat < minLat) {1551minPt = pt;1552minLat = pt.lat;1553}1554}1555var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs),1556this.buildConvexHull([maxPt, minPt], latLngs));1557return ch;1558}1559};1560}());15611562L.MarkerCluster.include({1563getConvexHull: function () {1564var childMarkers = this.getAllChildMarkers(),1565points = [],1566hullLatLng = [],1567hull, p, i;15681569for (i = childMarkers.length - 1; i >= 0; i--) {1570p = childMarkers[i].getLatLng();1571points.push(p);1572}15731574hull = L.QuickHull.getConvexHull(points);15751576for (i = hull.length - 1; i >= 0; i--) {1577hullLatLng.push(hull[i][0]);1578}15791580return hullLatLng;1581}1582});15831584//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet1585//Huge thanks to jawj for implementing it first to make my job easy :-)15861587L.MarkerCluster.include({15881589_2PI: Math.PI * 2,1590_circleFootSeparation: 25, //related to circumference of circle1591_circleStartAngle: Math.PI / 6,15921593_spiralFootSeparation: 28, //related to size of spiral (experiment!)1594_spiralLengthStart: 11,1595_spiralLengthFactor: 5,15961597_circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards.1598// 0 -> always spiral; Infinity -> always circle15991600spiderfy: function () {1601if (this._group._spiderfied === this || this._group._inZoomAnimation) {1602return;1603}16041605var childMarkers = this.getAllChildMarkers(),1606group = this._group,1607map = group._map,1608center = map.latLngToLayerPoint(this._latlng),1609positions;16101611this._group._unspiderfy();1612this._group._spiderfied = this;16131614//TODO Maybe: childMarkers order by distance to center16151616if (childMarkers.length >= this._circleSpiralSwitchover) {1617positions = this._generatePointsSpiral(childMarkers.length, center);1618} else {1619center.y += 10; //Otherwise circles look wrong1620positions = this._generatePointsCircle(childMarkers.length, center);1621}16221623this._animationSpiderfy(childMarkers, positions);1624},16251626unspiderfy: function (zoomDetails) {1627/// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param>1628if (this._group._inZoomAnimation) {1629return;1630}1631this._animationUnspiderfy(zoomDetails);16321633this._group._spiderfied = null;1634},16351636_generatePointsCircle: function (count, centerPt) {1637var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),1638legLength = circumference / this._2PI, //radius from circumference1639angleStep = this._2PI / count,1640res = [],1641i, angle;16421643res.length = count;16441645for (i = count - 1; i >= 0; i--) {1646angle = this._circleStartAngle + i * angleStep;1647res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();1648}16491650return res;1651},16521653_generatePointsSpiral: function (count, centerPt) {1654var legLength = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthStart,1655separation = this._group.options.spiderfyDistanceMultiplier * this._spiralFootSeparation,1656lengthFactor = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthFactor,1657angle = 0,1658res = [],1659i;16601661res.length = count;16621663for (i = count - 1; i >= 0; i--) {1664angle += separation / legLength + i * 0.0005;1665res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();1666legLength += this._2PI * lengthFactor / angle;1667}1668return res;1669},16701671_noanimationUnspiderfy: function () {1672var group = this._group,1673map = group._map,1674fg = group._featureGroup,1675childMarkers = this.getAllChildMarkers(),1676m, i;16771678this.setOpacity(1);1679for (i = childMarkers.length - 1; i >= 0; i--) {1680m = childMarkers[i];16811682fg.removeLayer(m);16831684if (m._preSpiderfyLatlng) {1685m.setLatLng(m._preSpiderfyLatlng);1686delete m._preSpiderfyLatlng;1687}1688if (m.setZIndexOffset) {1689m.setZIndexOffset(0);1690}16911692if (m._spiderLeg) {1693map.removeLayer(m._spiderLeg);1694delete m._spiderLeg;1695}1696}1697}1698});16991700L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {1701//Non Animated versions of everything1702_animationSpiderfy: function (childMarkers, positions) {1703var group = this._group,1704map = group._map,1705fg = group._featureGroup,1706i, m, leg, newPos;17071708for (i = childMarkers.length - 1; i >= 0; i--) {1709newPos = map.layerPointToLatLng(positions[i]);1710m = childMarkers[i];17111712m._preSpiderfyLatlng = m._latlng;1713m.setLatLng(newPos);1714if (m.setZIndexOffset) {1715m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING1716}17171718fg.addLayer(m);171917201721leg = new L.Polyline([this._latlng, newPos], { weight: 1.5, color: '#222' });1722map.addLayer(leg);1723m._spiderLeg = leg;1724}1725this.setOpacity(0);1726group.fire('spiderfied');1727},17281729_animationUnspiderfy: function () {1730this._noanimationUnspiderfy();1731}1732} : {1733//Animated versions here1734SVG_ANIMATION: (function () {1735return document.createElementNS('http://www.w3.org/2000/svg', 'animate').toString().indexOf('SVGAnimate') > -1;1736}()),17371738_animationSpiderfy: function (childMarkers, positions) {1739var me = this,1740group = this._group,1741map = group._map,1742fg = group._featureGroup,1743thisLayerPos = map.latLngToLayerPoint(this._latlng),1744i, m, leg, newPos;17451746//Add markers to map hidden at our center point1747for (i = childMarkers.length - 1; i >= 0; i--) {1748m = childMarkers[i];17491750//If it is a marker, add it now and we'll animate it out1751if (m.setOpacity) {1752m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING1753m.setOpacity(0);17541755fg.addLayer(m);17561757m._setPos(thisLayerPos);1758} else {1759//Vectors just get immediately added1760fg.addLayer(m);1761}1762}17631764group._forceLayout();1765group._animationStart();17661767var initialLegOpacity = L.Path.SVG ? 0 : 0,1768xmlns = L.Path.SVG_NS;176917701771for (i = childMarkers.length - 1; i >= 0; i--) {1772newPos = map.layerPointToLatLng(positions[i]);1773m = childMarkers[i];17741775//Move marker to new position1776m._preSpiderfyLatlng = m._latlng;1777m.setLatLng(newPos);17781779if (m.setOpacity) {1780m.setOpacity(1);1781}178217831784//Add Legs.1785leg = new L.Polyline([me._latlng, newPos], { weight: 1.5, color: '#222', opacity: 0 });1786map.addLayer(leg);1787m._spiderLeg = leg;17881789//Following animations don't work for canvas1790if (!L.Path.SVG || !this.SVG_ANIMATION) {1791continue;1792}17931794//How this works:1795//http://stackoverflow.com/questions/5924238/how-do-you-animate-an-svg-path-in-ios1796//http://dev.opera.com/articles/view/advanced-svg-animation-techniques/17971798//Animate length1799var length = leg._path.getTotalLength();1800leg._path.setAttribute("stroke-dasharray", length + "," + length);18011802var anim = document.createElementNS(xmlns, "animate");1803anim.setAttribute("attributeName", "stroke-dashoffset");1804anim.setAttribute("begin", "indefinite");1805anim.setAttribute("from", length);1806anim.setAttribute("to", 0);1807anim.setAttribute("dur", 0.25);1808leg._path.appendChild(anim);1809anim.beginElement();18101811//Animate opacity1812anim = document.createElementNS(xmlns, "animate");1813anim.setAttribute("attributeName", "stroke-opacity");1814anim.setAttribute("attributeName", "stroke-opacity");1815anim.setAttribute("begin", "indefinite");1816anim.setAttribute("from", 0);1817anim.setAttribute("to", 0);1818anim.setAttribute("dur", 0.25);1819leg._path.appendChild(anim);1820anim.beginElement();1821}1822me.setOpacity(0);18231824//Set the opacity of the spiderLegs back to their correct value1825// The animations above override this until they complete.1826// If the initial opacity of the spiderlegs isn't 0 then they appear before the animation starts.1827if (L.Path.SVG) {1828this._group._forceLayout();18291830for (i = childMarkers.length - 1; i >= 0; i--) {1831m = childMarkers[i]._spiderLeg;18321833m.options.opacity = 0;1834m._path.setAttribute('stroke-opacity', 0);1835}1836}18371838setTimeout(function () {1839group._animationEnd();1840group.fire('spiderfied');1841}, 200);1842},18431844_animationUnspiderfy: function (zoomDetails) {1845var group = this._group,1846map = group._map,1847fg = group._featureGroup,1848thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng),1849childMarkers = this.getAllChildMarkers(),1850svg = L.Path.SVG && this.SVG_ANIMATION,1851m, i, a;18521853group._animationStart();18541855//Make us visible and bring the child markers back in1856this.setOpacity(1);1857for (i = childMarkers.length - 1; i >= 0; i--) {1858m = childMarkers[i];18591860//Marker was added to us after we were spidified1861if (!m._preSpiderfyLatlng) {1862continue;1863}18641865//Fix up the location to the real one1866m.setLatLng(m._preSpiderfyLatlng);1867delete m._preSpiderfyLatlng;1868//Hack override the location to be our center1869if (m.setOpacity) {1870m._setPos(thisLayerPos);1871m.setOpacity(0);1872} else {1873fg.removeLayer(m);1874}18751876//Animate the spider legs back in1877if (svg) {1878a = m._spiderLeg._path.childNodes[0];1879a.setAttribute('to', a.getAttribute('from'));1880a.setAttribute('from', 0);1881a.beginElement();18821883a = m._spiderLeg._path.childNodes[1];1884a.setAttribute('from', 0);1885a.setAttribute('to', 0);1886a.setAttribute('stroke-opacity', 0);1887a.beginElement();18881889m._spiderLeg._path.setAttribute('stroke-opacity', 0);1890}1891}18921893setTimeout(function () {1894//If we have only <= one child left then that marker will be shown on the map so don't remove it!1895var stillThereChildCount = 0;1896for (i = childMarkers.length - 1; i >= 0; i--) {1897m = childMarkers[i];1898if (m._spiderLeg) {1899stillThereChildCount++;1900}1901}190219031904for (i = childMarkers.length - 1; i >= 0; i--) {1905m = childMarkers[i];19061907if (!m._spiderLeg) { //Has already been unspiderfied1908continue;1909}191019111912if (m.setOpacity) {1913m.setOpacity(1);1914m.setZIndexOffset(0);1915}19161917if (stillThereChildCount > 1) {1918fg.removeLayer(m);1919}19201921map.removeLayer(m._spiderLeg);1922delete m._spiderLeg;1923}1924group._animationEnd();1925}, 200);1926}1927});192819291930L.MarkerClusterGroup.include({1931//The MarkerCluster currently spiderfied (if any)1932_spiderfied: null,19331934_spiderfierOnAdd: function () {1935this._map.on('click', this._unspiderfyWrapper, this);19361937if (this._map.options.zoomAnimation) {1938this._map.on('zoomstart', this._unspiderfyZoomStart, this);1939} else {1940//Browsers without zoomAnimation don't fire zoomstart1941this._map.on('zoomend', this._unspiderfyWrapper, this);1942}19431944if (L.Path.SVG && !L.Browser.touch) {1945this._map._initPathRoot();1946//Needs to happen in the pageload, not after, or animations don't work in webkit1947// http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements1948//Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable1949}1950},19511952_spiderfierOnRemove: function () {1953this._map.off('click', this._unspiderfyWrapper, this);1954this._map.off('zoomstart', this._unspiderfyZoomStart, this);1955this._map.off('zoomanim', this._unspiderfyZoomAnim, this);19561957this._unspiderfy(); //Ensure that markers are back where they should be1958},195919601961//On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)1962//This means we can define the animation they do rather than Markers doing an animation to their actual location1963_unspiderfyZoomStart: function () {1964if (!this._map) { //May have been removed from the map by a zoomEnd handler1965return;1966}19671968this._map.on('zoomanim', this._unspiderfyZoomAnim, this);1969},1970_unspiderfyZoomAnim: function (zoomDetails) {1971//Wait until the first zoomanim after the user has finished touch-zooming before running the animation1972if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) {1973return;1974}19751976this._map.off('zoomanim', this._unspiderfyZoomAnim, this);1977this._unspiderfy(zoomDetails);1978},197919801981_unspiderfyWrapper: function () {1982/// <summary>_unspiderfy but passes no arguments</summary>1983this._unspiderfy();1984},19851986_unspiderfy: function (zoomDetails) {1987if (this._spiderfied) {1988this._spiderfied.unspiderfy(zoomDetails);1989}1990},19911992_noanimationUnspiderfy: function () {1993if (this._spiderfied) {1994this._spiderfied._noanimationUnspiderfy();1995}1996},19971998//If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc1999_unspiderfyLayer: function (layer) {2000if (layer._spiderLeg) {2001this._featureGroup.removeLayer(layer);20022003layer.setOpacity(1);2004//Position will be fixed up immediately in _animationUnspiderfy2005layer.setZIndexOffset(0);20062007this._map.removeLayer(layer._spiderLeg);2008delete layer._spiderLeg;2009}2010}2011});201220132014}(window, document));20152016