UNPKG

leaflet.markercluster.placementstrategies

Version:

leaflet.markercluster subplugin that defines more strategies to place clustered markers

414 lines (349 loc) 16.1 kB
/* markerCluster placement-strategies subplugin for leaflet.markercluster https://github.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies Adam Mertel | univie */ "use strict"; /*global L:true*/ L.MarkerClusterGroup.include({ _noanimationUnspiderfy: function _noanimationUnspiderfy() { if (this._spiderfied) { //this._spiderfied._noanimationUnspiderfy(); this._spiderfied.unspiderfy(); } } }); L.MarkerCluster.include({ spiderfy: function spiderfy() { var group = this._group; var options = group.options; if (group._spiderfied === this || group._inZoomAnimation) { return; } var childMarkers = this.getAllChildMarkers(); var map = group._map; var center = map.latLngToLayerPoint(this._latlng); var positions = []; // add options.spiderfiedClassName to the spiderfied markers if (options.spiderfiedClassName) { for (var chmi in childMarkers) { var marker = childMarkers[chmi]; // marker if (marker.getIcon) { var icon = marker.getIcon(); if (icon) { if (icon.options.className) { if (!icon.options.className.includes(options.spiderfiedClassName)) { icon.options.className += " " + options.spiderfiedClassName; } } else { icon.options.className = options.spiderfiedClassName; } } //circleMarker } else if (childMarkers[chmi].setStyle) { var classNames = childMarkers[chmi].options.className; childMarkers[chmi].setStyle({ className: classNames + " " + options.spiderfiedClassName }); } } } group._unspiderfy(); group._spiderfied = this; this._clockHelpingGeometries = []; // TODO Maybe: childMarkers order by distance to center // applies chosen placement strategy switch (options.elementsPlacementStrategy) { case "default": if (childMarkers.length >= this._circleSpiralSwitchover) { positions = this._generatePointsSpiral(childMarkers.length, center); } else { positions = this._generatePointsCircle(childMarkers.length, center); } break; case "spiral": positions = this._generatePointsSpiral(childMarkers.length, center); break; case "one-circle": positions = this._generatePointsCircle(childMarkers.length, center); break; case "concentric": positions = this._generatePointsConcentricCircles(childMarkers.length, center); break; case "clock": positions = this._generatePointsClocksCircles(childMarkers.length, center, false); break; case "clock-concentric": positions = this._generatePointsClocksCircles(childMarkers.length, center, true); break; case "original-locations": positions = this._getOriginalLocations(childMarkers, group._map); break; default: console.log('!!unknown placement strategy value. Allowed strategy names are : "default", "spiral", "one-circle", "concentric", "clock", "clock-concentric" and "original-locations" '); } this._animationSpiderfy(childMarkers, positions); }, unspiderfy: function unspiderfy(zoomDetails) { // remove _supportiveGeometries from map this._removeClockHelpingCircles(); /// <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 _generatePointsCircle(count, centerPt) { var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), legLength = circumference / this._2PI, //radius from circumference angleStep = this._2PI / count, res = []; var i, angle; res.length = count; for (i = count - 1; i >= 0; i--) { angle = this._circleStartAngle + i * angleStep; res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); } this._createHelpingCircle(centerPt, legLength); return res; }, _generatePointsSpiral: function _generatePointsSpiral(count, centerPt) { var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier, separation = spiderfyDistanceMultiplier * this._spiralFootSeparation, lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI, res = []; var i, angle = 0; var legLength = spiderfyDistanceMultiplier * this._spiralLengthStart; res.length = count; // Higher index, closer position to cluster center. for (i = count - 1; i >= 0; i--) { angle += separation / legLength + i * 0.0005; res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); legLength += lengthFactor / angle; } return res; }, // auxiliary method - returns placement of vertex of given regular n-side polygon _regularPolygonVertexPlacement: function _regularPolygonVertexPlacement(vertexNo, totalVertices, centerPt, distanceFromCenter) { var deltaAngle = this._2PI / totalVertices; var thisAngle = deltaAngle * vertexNo; // in case of two vertices, right-left placement is more estetic if (totalVertices !== 2) { thisAngle -= 1.6; } return new L.Point(centerPt.x + Math.cos(thisAngle) * distanceFromCenter, centerPt.y + Math.sin(thisAngle) * distanceFromCenter)._round(); }, // clock strategy placement. // regularFirstCicle parameter - true if first elements in the first circle are placed regularly _generatePointsClocksCircles: function _generatePointsClocksCircles(count, centerPt, regularFirstCircle) { var res = []; var options = this._group.options; var fce = options.firstCircleElements; var baseDistance = this._circleFootSeparation * 1.5, // offset of the first circle dm = options.spiderfyDistanceMultiplier, // multiplier of the offset for a next circle distanceSurplus = options.spiderfyDistanceSurplus, // multiplier of the offset for a next circle elementsMultiplier = options.elementsMultiplier; // multiplier of number of elements in the next circle var iCircleNumber = 1, iCircleNoElements = fce, iCircleDistance = baseDistance, elementsInPreviousCircles = 0; this._createHelpingCircle(centerPt, iCircleDistance); // iterating elements for (var i = 1; i <= count; i++) { var elementOrder = i - elementsInPreviousCircles; // position of current element in this circle // changing the circle if (elementOrder > iCircleNoElements) { iCircleNumber += 1; elementsInPreviousCircles += iCircleNoElements; elementOrder = i - elementsInPreviousCircles; // position of current element in this circle iCircleNoElements = Math.floor(iCircleNoElements * elementsMultiplier); iCircleDistance = (distanceSurplus + iCircleDistance) * dm; this._createHelpingCircle(centerPt, iCircleDistance); } if (regularFirstCircle && iCircleNumber === 1) { res[i - 1] = this._regularPolygonVertexPlacement(elementOrder - 1, Math.min(fce, count), centerPt, iCircleDistance); } else { res[i - 1] = this._regularPolygonVertexPlacement(elementOrder - 1, iCircleNoElements, centerPt, iCircleDistance); } } return res; }, // method for creating and storing helping circles for clock/concentric circles strategy _createHelpingCircle: function _createHelpingCircle(center, radius) { var group = this._group; var options = group.options; if (options.helpingCircles) { var clockCircleStyle = { radius: radius }; // keeping without fill if it is not defined if (!options.clockHelpingCircleOptions.fill) { options.clockHelpingCircleOptions.fillColor = "none"; } L.extend(clockCircleStyle, options.clockHelpingCircleOptions); var clockCircle = new L.CircleMarker(group._map.layerPointToLatLng(center), clockCircleStyle); group._featureGroup.addLayer(clockCircle); this._clockHelpingGeometries.push(clockCircle); } }, // concentric circles strategy placement. // divide elements of cluster into concentric zones based on elementsMultiplier and firstCircleElements parameters _generatePointsConcentricCircles: function _generatePointsConcentricCircles(count, centerPt) { var _this = this; var options = this._group.options; var res = []; var fce = options.firstCircleElements, baseDistance = this._circleFootSeparation * 1.5, // offset of the first circle dm = options.spiderfyDistanceMultiplier, // multiplier of the offset for a next circle elementsMultiplier = options.elementsMultiplier, // multiplier of number of elements in the next circle distanceSurplus = options.spiderfyDistanceSurplus, // multiplier of the offset for a next circle secondCircleElements = Math.round(fce * elementsMultiplier); // number of elements in the second circle var circles = [{ distance: baseDistance, noElements: 0 }, { distance: (distanceSurplus + baseDistance) * dm, noElements: 0 }, { distance: (2 * distanceSurplus + baseDistance) * dm * dm, noElements: 0 }, { distance: (3 * distanceSurplus + baseDistance) * dm * dm * dm, noElements: 0 }]; // number of points in the second circle if (count > fce) { circles[1].noElements = secondCircleElements; if (fce < count && count < 2 * fce || fce + secondCircleElements < count && count < 2 * fce + secondCircleElements) { circles[1].noElements = fce; } } // number of points in the third circle if (count > fce + Math.round(fce * elementsMultiplier)) { circles[2].noElements = Math.round(fce * elementsMultiplier); } if (count > fce + 2 * Math.round(fce * elementsMultiplier)) { circles[2].noElements = Math.round(fce * elementsMultiplier * elementsMultiplier); } if (count > fce + Math.round(fce * elementsMultiplier) + Math.round(fce * elementsMultiplier * elementsMultiplier)) { circles[2].noElements = Math.round(fce * elementsMultiplier); } if (count > fce + 2 * Math.round(fce * elementsMultiplier) + Math.round(fce * elementsMultiplier * elementsMultiplier)) { circles[2].noElements = Math.round(fce * elementsMultiplier * elementsMultiplier); } // number of points in the first circle circles[0].noElements = Math.min(count - circles[1].noElements - circles[2].noElements, fce); // number of points in the fourth circle circles[3].noElements = Math.max(count - circles[0].noElements - circles[1].noElements - circles[2].noElements, 0); var prevCirclesEls = 0; // number of elements in the finished circles var iCircle = circles[0]; // curretly driven circle // iterating elements for (var i = 1; i <= count; i++) { // changing to the new circle if (circles[1].noElements > 0) { if (i > circles[0].noElements) { iCircle = circles[1]; prevCirclesEls = circles[0].noElements; } if (i > circles[0].noElements + circles[1].noElements && circles[2].noElements > 0) { iCircle = circles[2]; prevCirclesEls = circles[0].noElements + circles[1].noElements; } if (i > circles[0].noElements + circles[1].noElements + circles[2].noElements && circles[3].noElements > 0) { iCircle = circles[3]; prevCirclesEls = circles[0].noElements - circles[1].noElements - circles[2].noElements; } } res[i - 1] = this._regularPolygonVertexPlacement(i - prevCirclesEls, iCircle.noElements, centerPt, iCircle.distance); } circles.filter(function (c) { return c.noElements; }).map(function (c) { return _this._createHelpingCircle(centerPt, c.distance); }); return res; }, _removeClockHelpingCircles: function _removeClockHelpingCircles(fg) { if (this._group.options.helpingCircles) { for (var hg in this._clockHelpingGeometries) { var featureGroup = this._group._featureGroup; featureGroup.removeLayer(this._clockHelpingGeometries[hg]); } } }, _getOriginalLocations: function _getOriginalLocations(childMarkers, map) { var res = []; childMarkers.forEach(function (marker) { res.push(map.latLngToLayerPoint(marker.getLatLng())); }); return res; } }); "use strict"; /*global L:true*/ L.MarkerClusterGroup.include({ options: { maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center iconCreateFunction: null, clusterPane: L.Marker.prototype.options.pane, spiderfyOnMaxZoom: true, showCoverageOnHover: true, zoomToBoundsOnClick: true, singleMarkerMode: false, disableClusteringAtZoom: null, // extra className that will be assigned to spiderfied child markers spiderfiedClassName: false, // Setting this to false prevents the removal of any clusters outside of the viewpoint, which // is the default behaviour for performance reasons. removeOutsideVisibleBounds: true, // Method of cluster elements placements // 'default' - one-circle strategy up to 8 elements (could be changed), then spiral strategy // 'spiral' - snail/spiral placement // 'one-circle' - put all the elements into one circle // 'concentric' - elements are placed automatically into concentric circles, there is a maximum of 4 circles // 'clock' - fills circles around the cluster marker in the style of clocks // 'clock-concentric' - in case of one circle, elements are places based on the concentric style, more circles are in clock style elementsPlacementStrategy: "clock-concentric", // Options that are valid for placement strategies 'concentric', 'clock' and 'clock-concentric' // Number of elements in the first circle firstCircleElements: 10, // multiplicator of elements number for the next circle elementsMultiplier: 1.5, // Value to be added to each new circle spiderfyDistanceSurplus: 30, // will draw additional helping circles helpingCircles: true, // Possibility to specify helpingCircle style clockHelpingCircleOptions: { color: "grey", dashArray: "5", fillOpacity: 0, opacity: 0.5, weight: 3 }, // Set to false to disable all animations (zoom and spiderfy). // If false, option animateAddingMarkers below has no effect. // If L.DomUtil.TRANSITION is falsy, this option has no effect. animate: false, // Whether to animate adding markers after adding the MarkerClusterGroup to the map // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. animateAddingMarkers: false, // Increase to increase the distance away that spiderfied markers appear from the center spiderfyDistanceMultiplier: 1, // Make it possible to specify a polyline options on a spider leg spiderLegPolylineOptions: { weight: 1.5, color: "#222", opacity: 0.5 }, // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts chunkedLoading: false, chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) // Options to pass to the L.Polygon constructor polygonOptions: {} } });