476 lines
14 KiB
Vue
476 lines
14 KiB
Vue
//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 (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;
|
|
}
|
|
}
|
|
});
|