//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet

//Huge thanks to jawj for implementing it first to make my job easy :-)



L.MarkerCluster.include({



	_2PI: Math.PI * 2,

	_circleFootSeparation: 25, //related to circumference of circle

	_circleStartAngle: 0,



	_spiralFootSeparation:  28, //related to size of spiral (experiment!)

	_spiralLengthStart: 11,

	_spiralLengthFactor: 5,



	_circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards.

								// 0 -> always spiral; Infinity -> always circle



	spiderfy: function () {

		if (this._group._spiderfied === this || this._group._inZoomAnimation) {

			return;

		}



		var childMarkers = this.getAllChildMarkers(null, true),

			group = this._group,

			map = group._map,

			center = map.latLngToLayerPoint(this._latlng),

			positions;



		this._group._unspiderfy();

		this._group._spiderfied = this;



		//TODO Maybe: childMarkers order by distance to center



		if (this._group.options.spiderfyShapePositions) {

			positions = this._group.options.spiderfyShapePositions(childMarkers.length, center);

		} else if (childMarkers.length >= this._circleSpiralSwitchover) {

			positions = this._generatePointsSpiral(childMarkers.length, center);

		} else {

			center.y += 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons.

			positions = this._generatePointsCircle(childMarkers.length, center);

		}



		this._animationSpiderfy(childMarkers, positions);

	},



	unspiderfy: function (zoomDetails) {

		/// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param>

		if (this._group._inZoomAnimation) {

			return;

		}

		this._animationUnspiderfy(zoomDetails);



		this._group._spiderfied = null;

	},



	_generatePointsCircle: function (count, centerPt) {

		var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),

			legLength = circumference / this._2PI,  //radius from circumference

			angleStep = this._2PI / count,

			res = [],

			i, angle;



		legLength = Math.max(legLength, 35); // Minimum distance to get outside the cluster icon.



		res.length = count;



		for (i = 0; i < count; i++) { // Clockwise, like spiral.

			angle = this._circleStartAngle + i * angleStep;

			res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();

		}



		return res;

	},



	_generatePointsSpiral: function (count, centerPt) {

		var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier,

			legLength = spiderfyDistanceMultiplier * this._spiralLengthStart,

			separation = spiderfyDistanceMultiplier * this._spiralFootSeparation,

			lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI,

			angle = 0,

			res = [],

			i;



		res.length = count;



		// Higher index, closer position to cluster center.

		for (i = count; i >= 0; i--) {

			// Skip the first position, so that we are already farther from center and we avoid

			// being under the default cluster icon (especially important for Circle Markers).

			if (i < count) {

				res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();

			}

			angle += separation / legLength + i * 0.0005;

			legLength += lengthFactor / angle;

		}

		return res;

	},



	_noanimationUnspiderfy: function () {

		var group = this._group,

			map = group._map,

			fg = group._featureGroup,

			childMarkers = this.getAllChildMarkers(null, true),

			m, i;



		group._ignoreMove = true;



		this.setOpacity(1);

		for (i = childMarkers.length - 1; i >= 0; i--) {

			m = childMarkers[i];



			fg.removeLayer(m);



			if (m._preSpiderfyLatlng) {

				m.setLatLng(m._preSpiderfyLatlng);

				delete m._preSpiderfyLatlng;

			}

			if (m.setZIndexOffset) {

				m.setZIndexOffset(0);

			}



			if (m._spiderLeg) {

				map.removeLayer(m._spiderLeg);

				delete m._spiderLeg;

			}

		}



		group.fire('unspiderfied', {

			cluster: this,

			markers: childMarkers

		});

		group._ignoreMove = false;

		group._spiderfied = null;

	}

});



//Non Animated versions of everything

L.MarkerClusterNonAnimated = L.MarkerCluster.extend({

	_animationSpiderfy: function (childMarkers, positions) {

		var group = this._group,

			map = group._map,

			fg = group._featureGroup,

			legOptions = this._group.options.spiderLegPolylineOptions,

			i, m, leg, newPos;



		group._ignoreMove = true;



		// Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.

		// The reverse order trick no longer improves performance on modern browsers.

		for (i = 0; i < childMarkers.length; i++) {

			newPos = map.layerPointToLatLng(positions[i]);

			m = childMarkers[i];



			// Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.

			leg = new L.Polyline([this._latlng, newPos], legOptions);

			map.addLayer(leg);

			m._spiderLeg = leg;



			// Now add the marker.

			m._preSpiderfyLatlng = m._latlng;

			m.setLatLng(newPos);

			if (m.setZIndexOffset) {

				m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING

			}



			fg.addLayer(m);

		}

		this.setOpacity(0.3);



		group._ignoreMove = false;

		group.fire('spiderfied', {

			cluster: this,

			markers: childMarkers

		});

	},



	_animationUnspiderfy: function () {

		this._noanimationUnspiderfy();

	}

});



//Animated versions here

L.MarkerCluster.include({



	_animationSpiderfy: function (childMarkers, positions) {

		var me = this,

			group = this._group,

			map = group._map,

			fg = group._featureGroup,

			thisLayerLatLng = this._latlng,

			thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng),

			svg = L.Path.SVG,

			legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation.

			finalLegOpacity = legOptions.opacity,

			i, m, leg, legPath, legLength, newPos;



		if (finalLegOpacity === undefined) {

			finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity;

		}



		if (svg) {

			// If the initial opacity of the spider leg is not 0 then it appears before the animation starts.

			legOptions.opacity = 0;



			// Add the class for CSS transitions.

			legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg';

		} else {

			// Make sure we have a defined opacity.

			legOptions.opacity = finalLegOpacity;

		}



		group._ignoreMove = true;



		// Add markers and spider legs to map, hidden at our center point.

		// Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.

		// The reverse order trick no longer improves performance on modern browsers.

		for (i = 0; i < childMarkers.length; i++) {

			m = childMarkers[i];



			newPos = map.layerPointToLatLng(positions[i]);



			// Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.

			leg = new L.Polyline([thisLayerLatLng, newPos], legOptions);

			map.addLayer(leg);

			m._spiderLeg = leg;



			// Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/

			// In our case the transition property is declared in the CSS file.

			if (svg) {

				legPath = leg._path;

				legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox.

				legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated.

				legPath.style.strokeDashoffset = legLength;

			}



			// If it is a marker, add it now and we'll animate it out

			if (m.setZIndexOffset) {

				m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING

			}

			if (m.clusterHide) {

				m.clusterHide();

			}

			

			// Vectors just get immediately added

			fg.addLayer(m);



			if (m._setPos) {

				m._setPos(thisLayerPos);

			}

		}



		group._forceLayout();

		group._animationStart();



		// Reveal markers and spider legs.

		for (i = childMarkers.length - 1; i >= 0; i--) {

			newPos = map.layerPointToLatLng(positions[i]);

			m = childMarkers[i];



			//Move marker to new position

			m._preSpiderfyLatlng = m._latlng;

			m.setLatLng(newPos);

			

			if (m.clusterShow) {

				m.clusterShow();

			}



			// Animate leg (animation is actually delegated to CSS transition).

			if (svg) {

				leg = m._spiderLeg;

				legPath = leg._path;

				legPath.style.strokeDashoffset = 0;

				//legPath.style.strokeOpacity = finalLegOpacity;

				leg.setStyle({opacity: finalLegOpacity});

			}

		}

		this.setOpacity(0.3);



		group._ignoreMove = false;



		setTimeout(function () {

			group._animationEnd();

			group.fire('spiderfied', {

				cluster: me,

				markers: childMarkers

			});

		}, 200);

	},



	_animationUnspiderfy: function (zoomDetails) {

		var me = this,

			group = this._group,

			map = group._map,

			fg = group._featureGroup,

			thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng),

			childMarkers = this.getAllChildMarkers(null, true),

			svg = L.Path.SVG,

			m, i, leg, legPath, legLength, nonAnimatable;



		group._ignoreMove = true;

		group._animationStart();



		//Make us visible and bring the child markers back in

		this.setOpacity(1);

		for (i = childMarkers.length - 1; i >= 0; i--) {

			m = childMarkers[i];



			//Marker was added to us after we were spiderfied

			if (!m._preSpiderfyLatlng) {

				continue;

			}



			//Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll

			m.closePopup();



			//Fix up the location to the real one

			m.setLatLng(m._preSpiderfyLatlng);

			delete m._preSpiderfyLatlng;



			//Hack override the location to be our center

			nonAnimatable = true;

			if (m._setPos) {

				m._setPos(thisLayerPos);

				nonAnimatable = false;

			}

			if (m.clusterHide) {

				m.clusterHide();

				nonAnimatable = false;

			}

			if (nonAnimatable) {

				fg.removeLayer(m);

			}



			// Animate the spider leg back in (animation is actually delegated to CSS transition).

			if (svg) {

				leg = m._spiderLeg;

				legPath = leg._path;

				legLength = legPath.getTotalLength() + 0.1;

				legPath.style.strokeDashoffset = legLength;

				leg.setStyle({opacity: 0});

			}

		}



		group._ignoreMove = false;



		setTimeout(function () {

			//If we have only <= one child left then that marker will be shown on the map so don't remove it!

			var stillThereChildCount = 0;

			for (i = childMarkers.length - 1; i >= 0; i--) {

				m = childMarkers[i];

				if (m._spiderLeg) {

					stillThereChildCount++;

				}

			}





			for (i = childMarkers.length - 1; i >= 0; i--) {

				m = childMarkers[i];



				if (!m._spiderLeg) { //Has already been unspiderfied

					continue;

				}



				if (m.clusterShow) {

					m.clusterShow();

				}

				if (m.setZIndexOffset) {

					m.setZIndexOffset(0);

				}



				if (stillThereChildCount > 1) {

					fg.removeLayer(m);

				}



				map.removeLayer(m._spiderLeg);

				delete m._spiderLeg;

			}

			group._animationEnd();

			group.fire('unspiderfied', {

				cluster: me,

				markers: childMarkers

			});

		}, 200);

	}

});





L.MarkerClusterGroup.include({

	//The MarkerCluster currently spiderfied (if any)

	_spiderfied: null,



	unspiderfy: function () {

		this._unspiderfy.apply(this, arguments);

	},



	_spiderfierOnAdd: function () {

		this._map.on('click', this._unspiderfyWrapper, this);



		if (this._map.options.zoomAnimation) {

			this._map.on('zoomstart', this._unspiderfyZoomStart, this);

		}

		//Browsers without zoomAnimation or a big zoom don't fire zoomstart

		this._map.on('zoomend', this._noanimationUnspiderfy, this);



		if (!L.Browser.touch) {

			this._map.getRenderer(this);

			//Needs to happen in the pageload, not after, or animations don't work in webkit

			//  http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements

			//Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable

		}

	},



	_spiderfierOnRemove: function () {

		this._map.off('click', this._unspiderfyWrapper, this);

		this._map.off('zoomstart', this._unspiderfyZoomStart, this);

		this._map.off('zoomanim', this._unspiderfyZoomAnim, this);

		this._map.off('zoomend', this._noanimationUnspiderfy, this);



		//Ensure that markers are back where they should be

		// Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane

		this._noanimationUnspiderfy();

	},



	//On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)

	//This means we can define the animation they do rather than Markers doing an animation to their actual location

	_unspiderfyZoomStart: function () {

		if (!this._map) { //May have been removed from the map by a zoomEnd handler

			return;

		}



		this._map.on('zoomanim', this._unspiderfyZoomAnim, this);

	},



	_unspiderfyZoomAnim: function (zoomDetails) {

		//Wait until the first zoomanim after the user has finished touch-zooming before running the animation

		if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) {

			return;

		}



		this._map.off('zoomanim', this._unspiderfyZoomAnim, this);

		this._unspiderfy(zoomDetails);

	},



	_unspiderfyWrapper: function () {

		/// <summary>_unspiderfy but passes no arguments</summary>

		this._unspiderfy();

	},



	_unspiderfy: function (zoomDetails) {

		if (this._spiderfied) {

			this._spiderfied.unspiderfy(zoomDetails);

		}

	},



	_noanimationUnspiderfy: function () {

		if (this._spiderfied) {

			this._spiderfied._noanimationUnspiderfy();

		}

	},



	//If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc

	_unspiderfyLayer: function (layer) {

		if (layer._spiderLeg) {

			this._featureGroup.removeLayer(layer);



			if (layer.clusterShow) {

				layer.clusterShow();

			}

				//Position will be fixed up immediately in _animationUnspiderfy

			if (layer.setZIndexOffset) {

				layer.setZIndexOffset(0);

			}



			this._map.removeLayer(layer._spiderLeg);

			delete layer._spiderLeg;

		}

	}

});

