├── .gitignore ├── .babelrc ├── assets ├── img_strategy_clock1.png ├── img_strategy_clock2.png ├── img_strategy_clock3.png ├── img_strategy_spiral1.png ├── img_strategy_spiral2.png ├── img_strategy_spiral3.png ├── img_strategy_concentric1.png ├── img_strategy_concentric2.png ├── img_strategy_concentric3.png ├── img_strategy_one-circle1.png ├── img_strategy_one-circle2.png ├── img_strategy_one-circle3.png ├── img_strategy_original1.png ├── img_strategy_original2.png ├── img_strategy_original3.png ├── img_strategy_clock-concentric1.png ├── img_strategy_clock-concentric2.png └── img_strategy_clock-concentric3.png ├── banner ├── LICENSE ├── package.json ├── src ├── markerclustergroup.options.js └── markercluster.strategies.js ├── README.md ├── dist ├── leaflet-markercluster.placementstrategies.js └── leaflet-markercluster.placementstrategies.src.js └── demo └── random-data.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/env", { "loose": true }]] 3 | } 4 | -------------------------------------------------------------------------------- /assets/img_strategy_clock1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_clock1.png -------------------------------------------------------------------------------- /assets/img_strategy_clock2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_clock2.png -------------------------------------------------------------------------------- /assets/img_strategy_clock3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_clock3.png -------------------------------------------------------------------------------- /assets/img_strategy_spiral1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_spiral1.png -------------------------------------------------------------------------------- /assets/img_strategy_spiral2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_spiral2.png -------------------------------------------------------------------------------- /assets/img_strategy_spiral3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_spiral3.png -------------------------------------------------------------------------------- /assets/img_strategy_concentric1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_concentric1.png -------------------------------------------------------------------------------- /assets/img_strategy_concentric2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_concentric2.png -------------------------------------------------------------------------------- /assets/img_strategy_concentric3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_concentric3.png -------------------------------------------------------------------------------- /assets/img_strategy_one-circle1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_one-circle1.png -------------------------------------------------------------------------------- /assets/img_strategy_one-circle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_one-circle2.png -------------------------------------------------------------------------------- /assets/img_strategy_one-circle3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_one-circle3.png -------------------------------------------------------------------------------- /assets/img_strategy_original1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_original1.png -------------------------------------------------------------------------------- /assets/img_strategy_original2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_original2.png -------------------------------------------------------------------------------- /assets/img_strategy_original3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_original3.png -------------------------------------------------------------------------------- /assets/img_strategy_clock-concentric1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_clock-concentric1.png -------------------------------------------------------------------------------- /assets/img_strategy_clock-concentric2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_clock-concentric2.png -------------------------------------------------------------------------------- /assets/img_strategy_clock-concentric3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/HEAD/assets/img_strategy_clock-concentric3.png -------------------------------------------------------------------------------- /banner: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | markerCluster placement-strategies subplugin for leaflet.markercluster 4 | https://github.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies 5 | Adam Mertel | univie 6 | */ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Adam Mertel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet.markercluster.placementstrategies", 3 | "version": "0.2.1", 4 | "description": "leaflet.markercluster subplugin that defines more strategies to place clustered markers", 5 | "keywords": [ 6 | "leaflet", 7 | "markercluster", 8 | "placement", 9 | "clustering" 10 | ], 11 | "author": "Adam Mertel, University of Vienna", 12 | "contributors": [ 13 | "kuuup-at-work" 14 | ], 15 | "license": "ISC", 16 | "main": "dist/leaflet-markercluster.placementstrategies.js", 17 | "devDependencies": { 18 | "@babel/cli": "^7.10.5", 19 | "@babel/core": "^7.11.1", 20 | "@babel/preset-env": "^7.11.0", 21 | "babel-plugin-minify-mangle-names": "^0.5.0", 22 | "babel-plugin-transform-remove-console": "^6.9.4", 23 | "concat-cli": "^4.0.0", 24 | "mkdirp": "^1.0.4", 25 | "npm-run-all": "^4.1.5", 26 | "onchange": "^7.0.2" 27 | }, 28 | "files": [ 29 | "dist", 30 | "src", 31 | "demo" 32 | ], 33 | "scripts": { 34 | "banners": "run-s banner-js-src banner-js", 35 | "banner-js-src": "concat-cli -f banner dist/leaflet-markercluster.placementstrategies.src.js -o dist/leaflet-markercluster.placementstrategies.src.js", 36 | "banner-js": "concat-cli -f banner dist/leaflet-markercluster.placementstrategies.js -o dist/leaflet-markercluster.placementstrategies.js", 37 | "create-dist-folder": "mkdirp dist", 38 | "babel": "babel src --out-file dist/leaflet-markercluster.placementstrategies.src.js", 39 | "minify": "babel --plugins=transform-remove-console,minify-mangle-names dist/leaflet-markercluster.placementstrategies.src.js --out-file dist/leaflet-markercluster.placementstrategies.js --minified", 40 | "onchange": "onchange src/**/*.js -- npm run build", 41 | "build": "run-s create-dist-folder babel minify banners", 42 | "start": "run-p create-dist-folder onchange build" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/markerclustergroup.options.js: -------------------------------------------------------------------------------- 1 | /*global L:true*/ 2 | 3 | L.MarkerClusterGroup.include({ 4 | options: { 5 | maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center 6 | iconCreateFunction: null, 7 | clusterPane: L.Marker.prototype.options.pane, 8 | 9 | spiderfyOnMaxZoom: true, 10 | showCoverageOnHover: true, 11 | zoomToBoundsOnClick: true, 12 | singleMarkerMode: false, 13 | 14 | disableClusteringAtZoom: null, 15 | 16 | // extra className that will be assigned to spiderfied child markers 17 | spiderfiedClassName: false, 18 | 19 | // Setting this to false prevents the removal of any clusters outside of the viewpoint, which 20 | // is the default behaviour for performance reasons. 21 | removeOutsideVisibleBounds: true, 22 | 23 | // Method of cluster elements placements 24 | // 'default' - one-circle strategy up to 8 elements (could be changed), then spiral strategy 25 | // 'spiral' - snail/spiral placement 26 | // 'one-circle' - put all the elements into one circle 27 | // 'concentric' - elements are placed automatically into concentric circles, there is a maximum of 4 circles 28 | // 'clock' - fills circles around the cluster marker in the style of clocks 29 | // 'clock-concentric' - in case of one circle, elements are places based on the concentric style, more circles are in clock style 30 | elementsPlacementStrategy: "clock-concentric", 31 | 32 | // Options that are valid for placement strategies 'concentric', 'clock' and 'clock-concentric' 33 | // Number of elements in the first circle 34 | firstCircleElements: 10, 35 | // multiplicator of elements number for the next circle 36 | elementsMultiplier: 1.5, 37 | // Value to be added to each new circle 38 | spiderfyDistanceSurplus: 30, 39 | // will draw additional helping circles 40 | helpingCircles: true, 41 | 42 | // Possibility to specify helpingCircle style 43 | clockHelpingCircleOptions: { 44 | color: "grey", 45 | dashArray: "5", 46 | fillOpacity: 0, 47 | opacity: 0.5, 48 | weight: 3 49 | }, 50 | 51 | // Set to false to disable all animations (zoom and spiderfy). 52 | // If false, option animateAddingMarkers below has no effect. 53 | // If L.DomUtil.TRANSITION is falsy, this option has no effect. 54 | animate: false, 55 | 56 | // Whether to animate adding markers after adding the MarkerClusterGroup to the map 57 | // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. 58 | animateAddingMarkers: false, 59 | 60 | // Increase to increase the distance away that spiderfied markers appear from the center 61 | spiderfyDistanceMultiplier: 1, 62 | 63 | // Make it possible to specify a polyline options on a spider leg 64 | spiderLegPolylineOptions: { weight: 1.5, color: "#222", opacity: 0.5 }, 65 | 66 | // 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 67 | chunkedLoading: false, 68 | chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) 69 | chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser 70 | chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) 71 | 72 | // Options to pass to the L.Polygon constructor 73 | polygonOptions: {} 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leaflet.MarkerCluster.PlacementStrategies 2 | 3 | **subplugin for the [Leaflet.MarkerCluster](https://github.com/Leaflet/Leaflet.markercluster) that implements new possibilities how to place clustered chidren markers** 4 | 5 | ## Demo: 6 | 7 | [random data demo](https://adammertel.github.io/Leaflet.MarkerCluster.PlacementStrategies/demo/random-data.html) 8 | 9 | ## How to use: 10 | 11 | 1. include Leaflet and Leaflet.MarkerCluster libraries (cdnjs, ungkg, ...) or npm install these libraries with `npm install leaflet leaflet.markercluster` 12 | 13 | 2. download and include built [leaflet-markercluster.placementstrategies.js](https://github.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/blob/master/dist/leaflet-markercluster.placementstrategies.js) or [leaflet-markercluster.placementstrategies.src.js](https://github.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/blob/master/dist/leaflet-markercluster.placementstrategies.src.js) file from dist folder or npm install `npm install leaflet.markercluster.placementstrategies` 14 | 15 | 3. create L.markerClusterGroup instance, add markers, and define placement strategy and other options. We recommend to hide spiderLegs by `spiderLegPolylineOptions: {weight: 0}` 16 | 17 | ```js 18 | var markers = L.markerClusterGroup({ 19 | spiderLegPolylineOptions: { weight: 0 }, 20 | clockHelpingCircleOptions: { 21 | weight: 0.7, 22 | opacity: 1, 23 | color: "black", 24 | fillOpacity: 0, 25 | dashArray: "10 5", 26 | }, 27 | 28 | elementsPlacementStrategy: "clock", 29 | helpingCircles: true, 30 | 31 | spiderfyDistanceSurplus: 25, 32 | spiderfyDistanceMultiplier: 1, 33 | 34 | elementsMultiplier: 1.4, 35 | firstCircleElements: 8, 36 | }); 37 | 38 | for (var i = 0; i < 10000; i++) { 39 | var circle = L.circleMarker([Math.random() * 30, Math.random() * 30], { 40 | fillOpacity: 0.7, 41 | radius: 8, 42 | fillColor: "red", 43 | color: "black", 44 | }); 45 | markers.addLayer(circle); 46 | } 47 | 48 | map.addLayer(markers); 49 | ``` 50 | 51 | ### React-leaflet 52 | 53 | This sub-plugin can be easily used also with react-leaflet. 54 | 55 | 1. Install leaflet, react-leaflet, leaflet.markercluster, react-leaflet-markercluster 56 | `npm install leaflet react-leaflet leaflet.markercluster react-leaflet-markercluster` 57 | 2. Install leaflet.markercluster.placementstrategies 58 | `npm install leaflet.markercluster.placementstrategies` 59 | 3. Import libraries and create component 60 | 61 | ```tsx 62 | import L from "leaflet"; 63 | 64 | import { 65 | Map, 66 | Marker, 67 | TileLayer, 68 | LayersControl, 69 | LayerGroup, 70 | } from "react-leaflet"; 71 | 72 | import MarkerClusterGroup from "react-leaflet-markercluster"; 73 | import "leaflet.markercluster.placementstrategies"; 74 | 75 | import "./../node_modules/leaflet/dist/leaflet.css"; 76 | 77 | export const MapComponent: React.FC = ({ center, zoom, points }) => { 78 | return ( 79 | 80 | 81 | 82 | 86 | 87 | 88 | 89 | { 103 | return divIcon({ 104 | html: `
${cluster.getChildCount()}
`, 105 | className: "marker-cluster", 106 | iconSize: [20, 20], 107 | }); 108 | }} 109 | > 110 | {points.map((point, ii) => { 111 | return ( 112 | 113 | 114 |
{point.info}
115 |
116 |
117 | ); 118 | })} 119 |
120 |
121 |
122 | ); 123 | }; 124 | ``` 125 | 126 | ## How to build: 127 | 128 | 1. install npm modules `npm install` 129 | 2. run build command `npm run build` 130 | 131 | `npm start` watches changes in js files and builds 132 | 133 | ## Placement Strategies 134 | 135 | - **default** - one-circle strategy (up to 8 elements\*, else spiral strategy) 136 | - **spiral** - snail/spiral placement 137 | 138 | 139 | 140 | 141 | 142 | - **one-circle** - put all the elements into one circle 143 | 144 | 145 | 146 | 147 | 148 | - **concentric** - elements are placed automatically into concentric circles, there is a maximum of 4 circles 149 | 150 | 151 | 152 | 153 | 154 | - **clock** - fills circles around the cluster marker in the style of clocks 155 | 156 | 157 | 158 | 159 | 160 | - **clock-concentric** - in the case of one circle, elements are places based on the concentric style, more circles are dislocated in the clock style 161 | 162 | 163 | 164 | 165 | 166 | - **original-locations** - elements are placed at their original locations 167 | 168 | 169 | 170 | 171 | 172 | \*_can be changed - \_circleSpiralSwitchover variable in the original markerCluster code_ 173 | 174 | ## Helping Circles 175 | 176 | the new type geometry called "helpingCircle" to make the cluster more visually-consistent (not supported for **origin-locations** strategy and **spiral** strategy) 177 | 178 | ## Options 179 | 180 | - **elementsPlacementStrategy** (default value 'clock-concentric') - defines the strategy for placing markers in cluster, see above 181 | - **spiderfiedClassName** (default value false) - a classname value for spiderfied markers, usefull for styling... 182 | 183 | ### Options that are valid for placement strategies 'concentric', 'clock' and 'clock-concentric' 184 | 185 | - **firstCircleElements** (default value **10**) - the number of elements in the first circle 186 | - **elementsMultiplier** (default value **1.5**) - the multiplicator of elements number for the next circle 187 | - **spiderfyDistanceSurplus** (default value **30**) - the value to be added to each new circle distance value 188 | - **helpingCircles** (default value **true**) - switch drawing helping circles on 189 | - **helpingCircleOptions** (default value **{ fillOpacity: 0, color: 'grey', weight: 0.6 }** ) - the style object for helpingCircle element 190 | 191 | ## Notes: 192 | 193 | - this subplugin was not tested with the animations turned on (`animation` and `animateAddingMarkers` variables) 194 | - `circleMarkers` should be preferred to markers 195 | - use with `L.SVG` renderer if possible (`L.Canvas` renderer has technical issues with some visual properties, see [#6](https://github.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies/issues/6)) 196 | -------------------------------------------------------------------------------- /dist/leaflet-markercluster.placementstrategies.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | markerCluster placement-strategies subplugin for leaflet.markercluster 4 | https://github.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies 5 | Adam Mertel | univie 6 | */ 7 | "use strict";/*global L:true*/L.MarkerClusterGroup.include({_noanimationUnspiderfy:function a(){if(this._spiderfied){//this._spiderfied._noanimationUnspiderfy(); 8 | this._spiderfied.unspiderfy()}}});L.MarkerCluster.include({spiderfy:function a(){var b=this._group;var c=b.options;if(b._spiderfied===this||b._inZoomAnimation){return}var d=this.getAllChildMarkers();var e=b._map;var f=e.latLngToLayerPoint(this._latlng);var g=[];// add options.spiderfiedClassName to the spiderfied markers 9 | if(c.spiderfiedClassName){for(var h in d){var i=d[h];// marker 10 | if(i.getIcon){var j=i.getIcon();if(j){if(j.options.className){if(!j.options.className.includes(c.spiderfiedClassName)){j.options.className+=" "+c.spiderfiedClassName}}else{j.options.className=c.spiderfiedClassName}}//circleMarker 11 | }else if(d[h].setStyle){var k=d[h].options.className;d[h].setStyle({className:k+" "+c.spiderfiedClassName})}}}b._unspiderfy();b._spiderfied=this;this._clockHelpingGeometries=[];// TODO Maybe: childMarkers order by distance to center 12 | // applies chosen placement strategy 13 | switch(c.elementsPlacementStrategy){case"default":if(d.length>=this._circleSpiralSwitchover){g=this._generatePointsSpiral(d.length,f)}else{g=this._generatePointsCircle(d.length,f)}break;case"spiral":g=this._generatePointsSpiral(d.length,f);break;case"one-circle":g=this._generatePointsCircle(d.length,f);break;case"concentric":g=this._generatePointsConcentricCircles(d.length,f);break;case"clock":g=this._generatePointsClocksCircles(d.length,f,false);break;case"clock-concentric":g=this._generatePointsClocksCircles(d.length,f,true);break;case"original-locations":g=this._getOriginalLocations(d,b._map);break;default:}this._animationSpiderfy(d,g)},unspiderfy:function a(b){// remove _supportiveGeometries from map 14 | this._removeClockHelpingCircles();/// Argument from zoomanim if being called in a zoom animation or null otherwise 15 | if(this._group._inZoomAnimation){return}this._animationUnspiderfy(b);this._group._spiderfied=null},_generatePointsCircle:function a(b,c){var d=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+b),e=d/this._2PI,//radius from circumference 16 | f=this._2PI/b,g=[];var h,j;g.length=b;for(h=b-1;h>=0;h--){j=this._circleStartAngle+h*f;g[h]=new L.Point(c.x+e*Math.cos(j),c.y+e*Math.sin(j))._round()}this._createHelpingCircle(c,e);return g},_generatePointsSpiral:function a(b,c){var d=this._group.options.spiderfyDistanceMultiplier,e=d*this._spiralFootSeparation,f=d*this._spiralLengthFactor*this._2PI,g=[];var h,j=0;var k=d*this._spiralLengthStart;g.length=b;// Higher index, closer position to cluster center. 17 | for(h=b-1;h>=0;h--){j+=e/k+h*0.0005;g[h]=new L.Point(c.x+k*Math.cos(j),c.y+k*Math.sin(j))._round();k+=f/j}return g},// auxiliary method - returns placement of vertex of given regular n-side polygon 18 | _regularPolygonVertexPlacement:function a(b,c,d,e){var f=this._2PI/c;var g=f*b;// in case of two vertices, right-left placement is more estetic 19 | if(c!==2){g-=1.6}return new L.Point(d.x+Math.cos(g)*e,d.y+Math.sin(g)*e)._round()},// clock strategy placement. 20 | // regularFirstCicle parameter - true if first elements in the first circle are placed regularly 21 | _generatePointsClocksCircles:function a(b,c,d){var e=[];var f=this._group.options;var g=f.firstCircleElements;var h=this._circleFootSeparation*1.5,// offset of the first circle 22 | j=f.spiderfyDistanceMultiplier,// multiplier of the offset for a next circle 23 | k=f.spiderfyDistanceSurplus,// multiplier of the offset for a next circle 24 | l=f.elementsMultiplier;// multiplier of number of elements in the next circle 25 | var m=1,n=g,o=h,p=0;this._createHelpingCircle(c,o);// iterating elements 26 | for(var q=1;q<=b;q++){var r=q-p;// position of current element in this circle 27 | // changing the circle 28 | if(r>n){m+=1;p+=n;r=q-p;// position of current element in this circle 29 | n=Math.floor(n*l);o=(k+o)*j;this._createHelpingCircle(c,o)}if(d&&m===1){e[q-1]=this._regularPolygonVertexPlacement(r-1,Math.min(g,b),c,o)}else{e[q-1]=this._regularPolygonVertexPlacement(r-1,n,c,o)}}return e},// method for creating and storing helping circles for clock/concentric circles strategy 30 | _createHelpingCircle:function a(b,c){var d=this._group;var e=d.options;if(e.helpingCircles){var f={radius:c};// keeping without fill if it is not defined 31 | if(!e.clockHelpingCircleOptions.fill){e.clockHelpingCircleOptions.fillColor="none"}L.extend(f,e.clockHelpingCircleOptions);var g=new L.CircleMarker(d._map.layerPointToLatLng(b),f);d._featureGroup.addLayer(g);this._clockHelpingGeometries.push(g)}},// concentric circles strategy placement. 32 | // divide elements of cluster into concentric zones based on elementsMultiplier and firstCircleElements parameters 33 | _generatePointsConcentricCircles:function a(b,d){var e=this;var f=this._group.options;var g=[];var h=f.firstCircleElements,j=this._circleFootSeparation*1.5,// offset of the first circle 34 | k=f.spiderfyDistanceMultiplier,// multiplier of the offset for a next circle 35 | c=f.elementsMultiplier,// multiplier of number of elements in the next circle 36 | l=f.spiderfyDistanceSurplus,// multiplier of the offset for a next circle 37 | m=Math.round(h*c);// number of elements in the second circle 38 | var n=[{distance:j,noElements:0},{distance:(l+j)*k,noElements:0},{distance:(2*l+j)*k*k,noElements:0},{distance:(3*l+j)*k*k*k,noElements:0}];// number of points in the second circle 39 | if(b>h){n[1].noElements=m;if(hh+Math.round(h*c)){n[2].noElements=Math.round(h*c)}if(b>h+2*Math.round(h*c)){n[2].noElements=Math.round(h*c*c)}if(b>h+Math.round(h*c)+Math.round(h*c*c)){n[2].noElements=Math.round(h*c)}if(b>h+2*Math.round(h*c)+Math.round(h*c*c)){n[2].noElements=Math.round(h*c*c)}// number of points in the first circle 41 | n[0].noElements=Math.min(b-n[1].noElements-n[2].noElements,h);// number of points in the fourth circle 42 | n[3].noElements=Math.max(b-n[0].noElements-n[1].noElements-n[2].noElements,0);var o=0;// number of elements in the finished circles 43 | var p=n[0];// curretly driven circle 44 | // iterating elements 45 | for(var q=1;q<=b;q++){// changing to the new circle 46 | if(n[1].noElements>0){if(q>n[0].noElements){p=n[1];o=n[0].noElements}if(q>n[0].noElements+n[1].noElements&&n[2].noElements>0){p=n[2];o=n[0].noElements+n[1].noElements}if(q>n[0].noElements+n[1].noElements+n[2].noElements&&n[3].noElements>0){p=n[3];o=n[0].noElements-n[1].noElements-n[2].noElements}}g[q-1]=this._regularPolygonVertexPlacement(q-o,p.noElements,d,p.distance)}n.filter(function(a){return a.noElements}).map(function(a){return e._createHelpingCircle(d,a.distance)});return g},_removeClockHelpingCircles:function a(b){if(this._group.options.helpingCircles){for(var c in this._clockHelpingGeometries){var d=this._group._featureGroup;d.removeLayer(this._clockHelpingGeometries[c])}}},_getOriginalLocations:function a(b,c){var d=[];b.forEach(function(a){d.push(c.latLngToLayerPoint(a.getLatLng()))});return d}});"use strict";/*global L:true*/L.MarkerClusterGroup.include({options:{maxClusterRadius:80,//A cluster will cover at most this many pixels from its center 47 | 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 48 | spiderfiedClassName:false,// Setting this to false prevents the removal of any clusters outside of the viewpoint, which 49 | // is the default behaviour for performance reasons. 50 | removeOutsideVisibleBounds:true,// Method of cluster elements placements 51 | // 'default' - one-circle strategy up to 8 elements (could be changed), then spiral strategy 52 | // 'spiral' - snail/spiral placement 53 | // 'one-circle' - put all the elements into one circle 54 | // 'concentric' - elements are placed automatically into concentric circles, there is a maximum of 4 circles 55 | // 'clock' - fills circles around the cluster marker in the style of clocks 56 | // 'clock-concentric' - in case of one circle, elements are places based on the concentric style, more circles are in clock style 57 | elementsPlacementStrategy:"clock-concentric",// Options that are valid for placement strategies 'concentric', 'clock' and 'clock-concentric' 58 | // Number of elements in the first circle 59 | firstCircleElements:10,// multiplicator of elements number for the next circle 60 | elementsMultiplier:1.5,// Value to be added to each new circle 61 | spiderfyDistanceSurplus:30,// will draw additional helping circles 62 | helpingCircles:true,// Possibility to specify helpingCircle style 63 | clockHelpingCircleOptions:{color:"grey",dashArray:"5",fillOpacity:0,opacity:0.5,weight:3},// Set to false to disable all animations (zoom and spiderfy). 64 | // If false, option animateAddingMarkers below has no effect. 65 | // If L.DomUtil.TRANSITION is falsy, this option has no effect. 66 | animate:false,// Whether to animate adding markers after adding the MarkerClusterGroup to the map 67 | // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. 68 | animateAddingMarkers:false,// Increase to increase the distance away that spiderfied markers appear from the center 69 | spiderfyDistanceMultiplier:1,// Make it possible to specify a polyline options on a spider leg 70 | 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 71 | chunkedLoading:false,chunkInterval:200,// process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) 72 | chunkDelay:50,// at the end of each interval, give n milliseconds back to system/browser 73 | chunkProgress:null,// progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) 74 | // Options to pass to the L.Polygon constructor 75 | polygonOptions:{}}}); 76 | -------------------------------------------------------------------------------- /src/markercluster.strategies.js: -------------------------------------------------------------------------------- 1 | /*global L:true*/ 2 | 3 | L.MarkerClusterGroup.include({ 4 | _noanimationUnspiderfy: function () { 5 | if (this._spiderfied) { 6 | //this._spiderfied._noanimationUnspiderfy(); 7 | this._spiderfied.unspiderfy(); 8 | } 9 | }, 10 | }); 11 | 12 | L.MarkerCluster.include({ 13 | spiderfy: function () { 14 | const group = this._group; 15 | const options = group.options; 16 | 17 | if (group._spiderfied === this || group._inZoomAnimation) { 18 | return; 19 | } 20 | 21 | const childMarkers = this.getAllChildMarkers(); 22 | const map = group._map; 23 | const center = map.latLngToLayerPoint(this._latlng); 24 | let positions = []; 25 | 26 | // add options.spiderfiedClassName to the spiderfied markers 27 | if (options.spiderfiedClassName) { 28 | for (var chmi in childMarkers) { 29 | const marker = childMarkers[chmi]; 30 | 31 | // marker 32 | if (marker.getIcon) { 33 | const icon = marker.getIcon(); 34 | if (icon) { 35 | if (icon.options.className) { 36 | if ( 37 | !icon.options.className.includes(options.spiderfiedClassName) 38 | ) { 39 | icon.options.className += " " + options.spiderfiedClassName; 40 | } 41 | } else { 42 | icon.options.className = options.spiderfiedClassName; 43 | } 44 | } 45 | 46 | //circleMarker 47 | } else if (childMarkers[chmi].setStyle) { 48 | const classNames = childMarkers[chmi].options.className; 49 | childMarkers[chmi].setStyle({ 50 | className: classNames + " " + options.spiderfiedClassName, 51 | }); 52 | } 53 | } 54 | } 55 | 56 | group._unspiderfy(); 57 | group._spiderfied = this; 58 | 59 | this._clockHelpingGeometries = []; 60 | 61 | // TODO Maybe: childMarkers order by distance to center 62 | 63 | // applies chosen placement strategy 64 | switch (options.elementsPlacementStrategy) { 65 | case "default": 66 | if (childMarkers.length >= this._circleSpiralSwitchover) { 67 | positions = this._generatePointsSpiral(childMarkers.length, center); 68 | } else { 69 | positions = this._generatePointsCircle(childMarkers.length, center); 70 | } 71 | break; 72 | 73 | case "spiral": 74 | positions = this._generatePointsSpiral(childMarkers.length, center); 75 | break; 76 | 77 | case "one-circle": 78 | positions = this._generatePointsCircle(childMarkers.length, center); 79 | break; 80 | 81 | case "concentric": 82 | positions = this._generatePointsConcentricCircles( 83 | childMarkers.length, 84 | center 85 | ); 86 | break; 87 | 88 | case "clock": 89 | positions = this._generatePointsClocksCircles( 90 | childMarkers.length, 91 | center, 92 | false 93 | ); 94 | break; 95 | 96 | case "clock-concentric": 97 | positions = this._generatePointsClocksCircles( 98 | childMarkers.length, 99 | center, 100 | true 101 | ); 102 | break; 103 | 104 | case "original-locations": 105 | positions = this._getOriginalLocations(childMarkers, group._map); 106 | break; 107 | 108 | default: 109 | console.log( 110 | '!!unknown placement strategy value. Allowed strategy names are : "default", "spiral", "one-circle", "concentric", "clock", "clock-concentric" and "original-locations" ' 111 | ); 112 | } 113 | 114 | this._animationSpiderfy(childMarkers, positions); 115 | }, 116 | 117 | unspiderfy: function (zoomDetails) { 118 | // remove _supportiveGeometries from map 119 | this._removeClockHelpingCircles(); 120 | 121 | /// Argument from zoomanim if being called in a zoom animation or null otherwise 122 | if (this._group._inZoomAnimation) { 123 | return; 124 | } 125 | this._animationUnspiderfy(zoomDetails); 126 | 127 | this._group._spiderfied = null; 128 | }, 129 | 130 | _generatePointsCircle: function (count, centerPt) { 131 | const circumference = 132 | this._group.options.spiderfyDistanceMultiplier * 133 | this._circleFootSeparation * 134 | (2 + count), 135 | legLength = circumference / this._2PI, //radius from circumference 136 | angleStep = this._2PI / count, 137 | res = []; 138 | 139 | let i, angle; 140 | 141 | res.length = count; 142 | 143 | for (i = count - 1; i >= 0; i--) { 144 | angle = this._circleStartAngle + i * angleStep; 145 | res[i] = new L.Point( 146 | centerPt.x + legLength * Math.cos(angle), 147 | centerPt.y + legLength * Math.sin(angle) 148 | )._round(); 149 | } 150 | 151 | this._createHelpingCircle(centerPt, legLength); 152 | 153 | return res; 154 | }, 155 | 156 | _generatePointsSpiral: function (count, centerPt) { 157 | const spiderfyDistanceMultiplier = this._group.options 158 | .spiderfyDistanceMultiplier, 159 | separation = spiderfyDistanceMultiplier * this._spiralFootSeparation, 160 | lengthFactor = 161 | spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI, 162 | res = []; 163 | let i, 164 | angle = 0; 165 | let legLength = spiderfyDistanceMultiplier * this._spiralLengthStart; 166 | 167 | res.length = count; 168 | 169 | // Higher index, closer position to cluster center. 170 | for (i = count - 1; i >= 0; i--) { 171 | angle += separation / legLength + i * 0.0005; 172 | res[i] = new L.Point( 173 | centerPt.x + legLength * Math.cos(angle), 174 | centerPt.y + legLength * Math.sin(angle) 175 | )._round(); 176 | legLength += lengthFactor / angle; 177 | } 178 | return res; 179 | }, 180 | 181 | // auxiliary method - returns placement of vertex of given regular n-side polygon 182 | _regularPolygonVertexPlacement: function ( 183 | vertexNo, 184 | totalVertices, 185 | centerPt, 186 | distanceFromCenter 187 | ) { 188 | const deltaAngle = this._2PI / totalVertices; 189 | let thisAngle = deltaAngle * vertexNo; 190 | 191 | // in case of two vertices, right-left placement is more estetic 192 | if (totalVertices !== 2) { 193 | thisAngle -= 1.6; 194 | } 195 | 196 | return new L.Point( 197 | centerPt.x + Math.cos(thisAngle) * distanceFromCenter, 198 | centerPt.y + Math.sin(thisAngle) * distanceFromCenter 199 | )._round(); 200 | }, 201 | 202 | // clock strategy placement. 203 | // regularFirstCicle parameter - true if first elements in the first circle are placed regularly 204 | _generatePointsClocksCircles: function (count, centerPt, regularFirstCircle) { 205 | const res = []; 206 | const options = this._group.options; 207 | const fce = options.firstCircleElements; 208 | 209 | const baseDistance = this._circleFootSeparation * 1.5, // offset of the first circle 210 | dm = options.spiderfyDistanceMultiplier, // multiplier of the offset for a next circle 211 | distanceSurplus = options.spiderfyDistanceSurplus, // multiplier of the offset for a next circle 212 | elementsMultiplier = options.elementsMultiplier; // multiplier of number of elements in the next circle 213 | 214 | let iCircleNumber = 1, 215 | iCircleNoElements = fce, 216 | iCircleDistance = baseDistance, 217 | elementsInPreviousCircles = 0; 218 | 219 | this._createHelpingCircle(centerPt, iCircleDistance); 220 | 221 | // iterating elements 222 | for (var i = 1; i <= count; i++) { 223 | var elementOrder = i - elementsInPreviousCircles; // position of current element in this circle 224 | 225 | // changing the circle 226 | if (elementOrder > iCircleNoElements) { 227 | iCircleNumber += 1; 228 | elementsInPreviousCircles += iCircleNoElements; 229 | elementOrder = i - elementsInPreviousCircles; // position of current element in this circle 230 | 231 | iCircleNoElements = Math.floor(iCircleNoElements * elementsMultiplier); 232 | iCircleDistance = (distanceSurplus + iCircleDistance) * dm; 233 | 234 | this._createHelpingCircle(centerPt, iCircleDistance); 235 | } 236 | 237 | if (regularFirstCircle && iCircleNumber === 1) { 238 | res[i - 1] = this._regularPolygonVertexPlacement( 239 | elementOrder - 1, 240 | Math.min(fce, count), 241 | centerPt, 242 | iCircleDistance 243 | ); 244 | } else { 245 | res[i - 1] = this._regularPolygonVertexPlacement( 246 | elementOrder - 1, 247 | iCircleNoElements, 248 | centerPt, 249 | iCircleDistance 250 | ); 251 | } 252 | } 253 | 254 | return res; 255 | }, 256 | 257 | // method for creating and storing helping circles for clock/concentric circles strategy 258 | _createHelpingCircle: function (center, radius) { 259 | const group = this._group; 260 | const options = group.options; 261 | 262 | if (options.helpingCircles) { 263 | const clockCircleStyle = { radius: radius }; 264 | 265 | // keeping without fill if it is not defined 266 | if (!options.clockHelpingCircleOptions.fill) { 267 | options.clockHelpingCircleOptions.fillColor = "none"; 268 | } 269 | L.extend(clockCircleStyle, options.clockHelpingCircleOptions); 270 | 271 | const clockCircle = new L.CircleMarker( 272 | group._map.layerPointToLatLng(center), 273 | clockCircleStyle 274 | ); 275 | group._featureGroup.addLayer(clockCircle); 276 | this._clockHelpingGeometries.push(clockCircle); 277 | } 278 | }, 279 | 280 | // concentric circles strategy placement. 281 | // divide elements of cluster into concentric zones based on elementsMultiplier and firstCircleElements parameters 282 | _generatePointsConcentricCircles: function (count, centerPt) { 283 | const options = this._group.options; 284 | const res = []; 285 | 286 | const fce = options.firstCircleElements, 287 | baseDistance = this._circleFootSeparation * 1.5, // offset of the first circle 288 | dm = options.spiderfyDistanceMultiplier, // multiplier of the offset for a next circle 289 | elementsMultiplier = options.elementsMultiplier, // multiplier of number of elements in the next circle 290 | distanceSurplus = options.spiderfyDistanceSurplus, // multiplier of the offset for a next circle 291 | secondCircleElements = Math.round(fce * elementsMultiplier); // number of elements in the second circle 292 | 293 | var circles = [ 294 | { 295 | distance: baseDistance, 296 | noElements: 0, 297 | }, 298 | { 299 | distance: (distanceSurplus + baseDistance) * dm, 300 | noElements: 0, 301 | }, 302 | { 303 | distance: (2 * distanceSurplus + baseDistance) * dm * dm, 304 | noElements: 0, 305 | }, 306 | { 307 | distance: (3 * distanceSurplus + baseDistance) * dm * dm * dm, 308 | noElements: 0, 309 | }, 310 | ]; 311 | 312 | // number of points in the second circle 313 | if (count > fce) { 314 | circles[1].noElements = secondCircleElements; 315 | 316 | if ( 317 | (fce < count && count < 2 * fce) || 318 | (fce + secondCircleElements < count && 319 | count < 2 * fce + secondCircleElements) 320 | ) { 321 | circles[1].noElements = fce; 322 | } 323 | } 324 | 325 | // number of points in the third circle 326 | if (count > fce + Math.round(fce * elementsMultiplier)) { 327 | circles[2].noElements = Math.round(fce * elementsMultiplier); 328 | } 329 | if (count > fce + 2 * Math.round(fce * elementsMultiplier)) { 330 | circles[2].noElements = Math.round( 331 | fce * elementsMultiplier * elementsMultiplier 332 | ); 333 | } 334 | if ( 335 | count > 336 | fce + 337 | Math.round(fce * elementsMultiplier) + 338 | Math.round(fce * elementsMultiplier * elementsMultiplier) 339 | ) { 340 | circles[2].noElements = Math.round(fce * elementsMultiplier); 341 | } 342 | if ( 343 | count > 344 | fce + 345 | 2 * Math.round(fce * elementsMultiplier) + 346 | Math.round(fce * elementsMultiplier * elementsMultiplier) 347 | ) { 348 | circles[2].noElements = Math.round( 349 | fce * elementsMultiplier * elementsMultiplier 350 | ); 351 | } 352 | 353 | // number of points in the first circle 354 | circles[0].noElements = Math.min( 355 | count - circles[1].noElements - circles[2].noElements, 356 | fce 357 | ); 358 | 359 | // number of points in the fourth circle 360 | circles[3].noElements = Math.max( 361 | count - 362 | circles[0].noElements - 363 | circles[1].noElements - 364 | circles[2].noElements, 365 | 0 366 | ); 367 | 368 | let prevCirclesEls = 0; // number of elements in the finished circles 369 | let iCircle = circles[0]; // curretly driven circle 370 | 371 | // iterating elements 372 | for (var i = 1; i <= count; i++) { 373 | // changing to the new circle 374 | if (circles[1].noElements > 0) { 375 | if (i > circles[0].noElements) { 376 | iCircle = circles[1]; 377 | prevCirclesEls = circles[0].noElements; 378 | } 379 | if ( 380 | i > circles[0].noElements + circles[1].noElements && 381 | circles[2].noElements > 0 382 | ) { 383 | iCircle = circles[2]; 384 | prevCirclesEls = circles[0].noElements + circles[1].noElements; 385 | } 386 | if ( 387 | i > 388 | circles[0].noElements + 389 | circles[1].noElements + 390 | circles[2].noElements && 391 | circles[3].noElements > 0 392 | ) { 393 | iCircle = circles[3]; 394 | prevCirclesEls = 395 | circles[0].noElements - 396 | circles[1].noElements - 397 | circles[2].noElements; 398 | } 399 | } 400 | 401 | res[i - 1] = this._regularPolygonVertexPlacement( 402 | i - prevCirclesEls, 403 | iCircle.noElements, 404 | centerPt, 405 | iCircle.distance 406 | ); 407 | } 408 | 409 | circles 410 | .filter((c) => c.noElements) 411 | .map((c) => this._createHelpingCircle(centerPt, c.distance)); 412 | 413 | return res; 414 | }, 415 | 416 | _removeClockHelpingCircles: function (fg) { 417 | if (this._group.options.helpingCircles) { 418 | for (var hg in this._clockHelpingGeometries) { 419 | const featureGroup = this._group._featureGroup; 420 | featureGroup.removeLayer(this._clockHelpingGeometries[hg]); 421 | } 422 | } 423 | }, 424 | 425 | _getOriginalLocations: function (childMarkers, map) { 426 | var res = []; 427 | 428 | childMarkers.forEach(function (marker) { 429 | res.push(map.latLngToLayerPoint(marker.getLatLng())); 430 | }); 431 | 432 | return res; 433 | }, 434 | }); 435 | -------------------------------------------------------------------------------- /demo/random-data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 22 | 25 | 26 | 31 | 36 | 41 | 42 | 43 | 44 | 45 | 98 | 99 | 100 |
101 | 102 |
103 |

GENERAL

104 |

elementsPlacementStrategy:

105 | 114 | 115 |

opacity of base map:

116 | 123 | 124 |

base map:

125 | 130 | 131 |

shapes:

132 | 136 | 137 |

MARKERCLUSTER

138 |

showCoverageOnHover:

139 | 143 |

spiderfyOnMaxZoom:

144 | 148 |

zoomToBoundsOnClick:

149 | 153 |

maxClusterRadius:

154 | 166 | 167 |

ELEMENTS

168 | 169 |

spiderfyDistanceSurplus:

170 | 180 | 181 |

distanceMultiplier:

182 | 189 | 190 |

firstCircleElements:

191 | 197 | 198 |

elementsMultiplier:

199 | 209 | 210 |

HELPING CIRCLE

211 | 212 |

helpingCircles :

213 | 217 | 218 |

color:

219 | 226 | 227 |

stroke weight:

228 | 249 | 250 |

stroke opacity:

251 | 263 | 264 |

stroke style:

265 | 288 | 289 |

Data: random points

290 |
291 | 437 | 438 | 439 | -------------------------------------------------------------------------------- /dist/leaflet-markercluster.placementstrategies.src.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | markerCluster placement-strategies subplugin for leaflet.markercluster 4 | https://github.com/adammertel/Leaflet.MarkerCluster.PlacementStrategies 5 | Adam Mertel | univie 6 | */ 7 | "use strict"; 8 | 9 | /*global L:true*/ 10 | L.MarkerClusterGroup.include({ 11 | _noanimationUnspiderfy: function _noanimationUnspiderfy() { 12 | if (this._spiderfied) { 13 | //this._spiderfied._noanimationUnspiderfy(); 14 | this._spiderfied.unspiderfy(); 15 | } 16 | } 17 | }); 18 | L.MarkerCluster.include({ 19 | spiderfy: function spiderfy() { 20 | var group = this._group; 21 | var options = group.options; 22 | 23 | if (group._spiderfied === this || group._inZoomAnimation) { 24 | return; 25 | } 26 | 27 | var childMarkers = this.getAllChildMarkers(); 28 | var map = group._map; 29 | var center = map.latLngToLayerPoint(this._latlng); 30 | var positions = []; // add options.spiderfiedClassName to the spiderfied markers 31 | 32 | if (options.spiderfiedClassName) { 33 | for (var chmi in childMarkers) { 34 | var marker = childMarkers[chmi]; // marker 35 | 36 | if (marker.getIcon) { 37 | var icon = marker.getIcon(); 38 | 39 | if (icon) { 40 | if (icon.options.className) { 41 | if (!icon.options.className.includes(options.spiderfiedClassName)) { 42 | icon.options.className += " " + options.spiderfiedClassName; 43 | } 44 | } else { 45 | icon.options.className = options.spiderfiedClassName; 46 | } 47 | } //circleMarker 48 | 49 | } else if (childMarkers[chmi].setStyle) { 50 | var classNames = childMarkers[chmi].options.className; 51 | childMarkers[chmi].setStyle({ 52 | className: classNames + " " + options.spiderfiedClassName 53 | }); 54 | } 55 | } 56 | } 57 | 58 | group._unspiderfy(); 59 | 60 | group._spiderfied = this; 61 | this._clockHelpingGeometries = []; // TODO Maybe: childMarkers order by distance to center 62 | // applies chosen placement strategy 63 | 64 | switch (options.elementsPlacementStrategy) { 65 | case "default": 66 | if (childMarkers.length >= this._circleSpiralSwitchover) { 67 | positions = this._generatePointsSpiral(childMarkers.length, center); 68 | } else { 69 | positions = this._generatePointsCircle(childMarkers.length, center); 70 | } 71 | 72 | break; 73 | 74 | case "spiral": 75 | positions = this._generatePointsSpiral(childMarkers.length, center); 76 | break; 77 | 78 | case "one-circle": 79 | positions = this._generatePointsCircle(childMarkers.length, center); 80 | break; 81 | 82 | case "concentric": 83 | positions = this._generatePointsConcentricCircles(childMarkers.length, center); 84 | break; 85 | 86 | case "clock": 87 | positions = this._generatePointsClocksCircles(childMarkers.length, center, false); 88 | break; 89 | 90 | case "clock-concentric": 91 | positions = this._generatePointsClocksCircles(childMarkers.length, center, true); 92 | break; 93 | 94 | case "original-locations": 95 | positions = this._getOriginalLocations(childMarkers, group._map); 96 | break; 97 | 98 | default: 99 | console.log('!!unknown placement strategy value. Allowed strategy names are : "default", "spiral", "one-circle", "concentric", "clock", "clock-concentric" and "original-locations" '); 100 | } 101 | 102 | this._animationSpiderfy(childMarkers, positions); 103 | }, 104 | unspiderfy: function unspiderfy(zoomDetails) { 105 | // remove _supportiveGeometries from map 106 | this._removeClockHelpingCircles(); /// Argument from zoomanim if being called in a zoom animation or null otherwise 107 | 108 | 109 | if (this._group._inZoomAnimation) { 110 | return; 111 | } 112 | 113 | this._animationUnspiderfy(zoomDetails); 114 | 115 | this._group._spiderfied = null; 116 | }, 117 | _generatePointsCircle: function _generatePointsCircle(count, centerPt) { 118 | var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), 119 | legLength = circumference / this._2PI, 120 | //radius from circumference 121 | angleStep = this._2PI / count, 122 | res = []; 123 | var i, angle; 124 | res.length = count; 125 | 126 | for (i = count - 1; i >= 0; i--) { 127 | angle = this._circleStartAngle + i * angleStep; 128 | res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); 129 | } 130 | 131 | this._createHelpingCircle(centerPt, legLength); 132 | 133 | return res; 134 | }, 135 | _generatePointsSpiral: function _generatePointsSpiral(count, centerPt) { 136 | var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier, 137 | separation = spiderfyDistanceMultiplier * this._spiralFootSeparation, 138 | lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI, 139 | res = []; 140 | var i, 141 | angle = 0; 142 | var legLength = spiderfyDistanceMultiplier * this._spiralLengthStart; 143 | res.length = count; // Higher index, closer position to cluster center. 144 | 145 | for (i = count - 1; i >= 0; i--) { 146 | angle += separation / legLength + i * 0.0005; 147 | res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); 148 | legLength += lengthFactor / angle; 149 | } 150 | 151 | return res; 152 | }, 153 | // auxiliary method - returns placement of vertex of given regular n-side polygon 154 | _regularPolygonVertexPlacement: function _regularPolygonVertexPlacement(vertexNo, totalVertices, centerPt, distanceFromCenter) { 155 | var deltaAngle = this._2PI / totalVertices; 156 | var thisAngle = deltaAngle * vertexNo; // in case of two vertices, right-left placement is more estetic 157 | 158 | if (totalVertices !== 2) { 159 | thisAngle -= 1.6; 160 | } 161 | 162 | return new L.Point(centerPt.x + Math.cos(thisAngle) * distanceFromCenter, centerPt.y + Math.sin(thisAngle) * distanceFromCenter)._round(); 163 | }, 164 | // clock strategy placement. 165 | // regularFirstCicle parameter - true if first elements in the first circle are placed regularly 166 | _generatePointsClocksCircles: function _generatePointsClocksCircles(count, centerPt, regularFirstCircle) { 167 | var res = []; 168 | var options = this._group.options; 169 | var fce = options.firstCircleElements; 170 | var baseDistance = this._circleFootSeparation * 1.5, 171 | // offset of the first circle 172 | dm = options.spiderfyDistanceMultiplier, 173 | // multiplier of the offset for a next circle 174 | distanceSurplus = options.spiderfyDistanceSurplus, 175 | // multiplier of the offset for a next circle 176 | elementsMultiplier = options.elementsMultiplier; // multiplier of number of elements in the next circle 177 | 178 | var iCircleNumber = 1, 179 | iCircleNoElements = fce, 180 | iCircleDistance = baseDistance, 181 | elementsInPreviousCircles = 0; 182 | 183 | this._createHelpingCircle(centerPt, iCircleDistance); // iterating elements 184 | 185 | 186 | for (var i = 1; i <= count; i++) { 187 | var elementOrder = i - elementsInPreviousCircles; // position of current element in this circle 188 | // changing the circle 189 | 190 | if (elementOrder > iCircleNoElements) { 191 | iCircleNumber += 1; 192 | elementsInPreviousCircles += iCircleNoElements; 193 | elementOrder = i - elementsInPreviousCircles; // position of current element in this circle 194 | 195 | iCircleNoElements = Math.floor(iCircleNoElements * elementsMultiplier); 196 | iCircleDistance = (distanceSurplus + iCircleDistance) * dm; 197 | 198 | this._createHelpingCircle(centerPt, iCircleDistance); 199 | } 200 | 201 | if (regularFirstCircle && iCircleNumber === 1) { 202 | res[i - 1] = this._regularPolygonVertexPlacement(elementOrder - 1, Math.min(fce, count), centerPt, iCircleDistance); 203 | } else { 204 | res[i - 1] = this._regularPolygonVertexPlacement(elementOrder - 1, iCircleNoElements, centerPt, iCircleDistance); 205 | } 206 | } 207 | 208 | return res; 209 | }, 210 | // method for creating and storing helping circles for clock/concentric circles strategy 211 | _createHelpingCircle: function _createHelpingCircle(center, radius) { 212 | var group = this._group; 213 | var options = group.options; 214 | 215 | if (options.helpingCircles) { 216 | var clockCircleStyle = { 217 | radius: radius 218 | }; // keeping without fill if it is not defined 219 | 220 | if (!options.clockHelpingCircleOptions.fill) { 221 | options.clockHelpingCircleOptions.fillColor = "none"; 222 | } 223 | 224 | L.extend(clockCircleStyle, options.clockHelpingCircleOptions); 225 | var clockCircle = new L.CircleMarker(group._map.layerPointToLatLng(center), clockCircleStyle); 226 | 227 | group._featureGroup.addLayer(clockCircle); 228 | 229 | this._clockHelpingGeometries.push(clockCircle); 230 | } 231 | }, 232 | // concentric circles strategy placement. 233 | // divide elements of cluster into concentric zones based on elementsMultiplier and firstCircleElements parameters 234 | _generatePointsConcentricCircles: function _generatePointsConcentricCircles(count, centerPt) { 235 | var _this = this; 236 | 237 | var options = this._group.options; 238 | var res = []; 239 | var fce = options.firstCircleElements, 240 | baseDistance = this._circleFootSeparation * 1.5, 241 | // offset of the first circle 242 | dm = options.spiderfyDistanceMultiplier, 243 | // multiplier of the offset for a next circle 244 | elementsMultiplier = options.elementsMultiplier, 245 | // multiplier of number of elements in the next circle 246 | distanceSurplus = options.spiderfyDistanceSurplus, 247 | // multiplier of the offset for a next circle 248 | secondCircleElements = Math.round(fce * elementsMultiplier); // number of elements in the second circle 249 | 250 | var circles = [{ 251 | distance: baseDistance, 252 | noElements: 0 253 | }, { 254 | distance: (distanceSurplus + baseDistance) * dm, 255 | noElements: 0 256 | }, { 257 | distance: (2 * distanceSurplus + baseDistance) * dm * dm, 258 | noElements: 0 259 | }, { 260 | distance: (3 * distanceSurplus + baseDistance) * dm * dm * dm, 261 | noElements: 0 262 | }]; // number of points in the second circle 263 | 264 | if (count > fce) { 265 | circles[1].noElements = secondCircleElements; 266 | 267 | if (fce < count && count < 2 * fce || fce + secondCircleElements < count && count < 2 * fce + secondCircleElements) { 268 | circles[1].noElements = fce; 269 | } 270 | } // number of points in the third circle 271 | 272 | 273 | if (count > fce + Math.round(fce * elementsMultiplier)) { 274 | circles[2].noElements = Math.round(fce * elementsMultiplier); 275 | } 276 | 277 | if (count > fce + 2 * Math.round(fce * elementsMultiplier)) { 278 | circles[2].noElements = Math.round(fce * elementsMultiplier * elementsMultiplier); 279 | } 280 | 281 | if (count > fce + Math.round(fce * elementsMultiplier) + Math.round(fce * elementsMultiplier * elementsMultiplier)) { 282 | circles[2].noElements = Math.round(fce * elementsMultiplier); 283 | } 284 | 285 | if (count > fce + 2 * Math.round(fce * elementsMultiplier) + Math.round(fce * elementsMultiplier * elementsMultiplier)) { 286 | circles[2].noElements = Math.round(fce * elementsMultiplier * elementsMultiplier); 287 | } // number of points in the first circle 288 | 289 | 290 | circles[0].noElements = Math.min(count - circles[1].noElements - circles[2].noElements, fce); // number of points in the fourth circle 291 | 292 | circles[3].noElements = Math.max(count - circles[0].noElements - circles[1].noElements - circles[2].noElements, 0); 293 | var prevCirclesEls = 0; // number of elements in the finished circles 294 | 295 | var iCircle = circles[0]; // curretly driven circle 296 | // iterating elements 297 | 298 | for (var i = 1; i <= count; i++) { 299 | // changing to the new circle 300 | if (circles[1].noElements > 0) { 301 | if (i > circles[0].noElements) { 302 | iCircle = circles[1]; 303 | prevCirclesEls = circles[0].noElements; 304 | } 305 | 306 | if (i > circles[0].noElements + circles[1].noElements && circles[2].noElements > 0) { 307 | iCircle = circles[2]; 308 | prevCirclesEls = circles[0].noElements + circles[1].noElements; 309 | } 310 | 311 | if (i > circles[0].noElements + circles[1].noElements + circles[2].noElements && circles[3].noElements > 0) { 312 | iCircle = circles[3]; 313 | prevCirclesEls = circles[0].noElements - circles[1].noElements - circles[2].noElements; 314 | } 315 | } 316 | 317 | res[i - 1] = this._regularPolygonVertexPlacement(i - prevCirclesEls, iCircle.noElements, centerPt, iCircle.distance); 318 | } 319 | 320 | circles.filter(function (c) { 321 | return c.noElements; 322 | }).map(function (c) { 323 | return _this._createHelpingCircle(centerPt, c.distance); 324 | }); 325 | return res; 326 | }, 327 | _removeClockHelpingCircles: function _removeClockHelpingCircles(fg) { 328 | if (this._group.options.helpingCircles) { 329 | for (var hg in this._clockHelpingGeometries) { 330 | var featureGroup = this._group._featureGroup; 331 | featureGroup.removeLayer(this._clockHelpingGeometries[hg]); 332 | } 333 | } 334 | }, 335 | _getOriginalLocations: function _getOriginalLocations(childMarkers, map) { 336 | var res = []; 337 | childMarkers.forEach(function (marker) { 338 | res.push(map.latLngToLayerPoint(marker.getLatLng())); 339 | }); 340 | return res; 341 | } 342 | }); 343 | "use strict"; 344 | 345 | /*global L:true*/ 346 | L.MarkerClusterGroup.include({ 347 | options: { 348 | maxClusterRadius: 80, 349 | //A cluster will cover at most this many pixels from its center 350 | iconCreateFunction: null, 351 | clusterPane: L.Marker.prototype.options.pane, 352 | spiderfyOnMaxZoom: true, 353 | showCoverageOnHover: true, 354 | zoomToBoundsOnClick: true, 355 | singleMarkerMode: false, 356 | disableClusteringAtZoom: null, 357 | // extra className that will be assigned to spiderfied child markers 358 | spiderfiedClassName: false, 359 | // Setting this to false prevents the removal of any clusters outside of the viewpoint, which 360 | // is the default behaviour for performance reasons. 361 | removeOutsideVisibleBounds: true, 362 | // Method of cluster elements placements 363 | // 'default' - one-circle strategy up to 8 elements (could be changed), then spiral strategy 364 | // 'spiral' - snail/spiral placement 365 | // 'one-circle' - put all the elements into one circle 366 | // 'concentric' - elements are placed automatically into concentric circles, there is a maximum of 4 circles 367 | // 'clock' - fills circles around the cluster marker in the style of clocks 368 | // 'clock-concentric' - in case of one circle, elements are places based on the concentric style, more circles are in clock style 369 | elementsPlacementStrategy: "clock-concentric", 370 | // Options that are valid for placement strategies 'concentric', 'clock' and 'clock-concentric' 371 | // Number of elements in the first circle 372 | firstCircleElements: 10, 373 | // multiplicator of elements number for the next circle 374 | elementsMultiplier: 1.5, 375 | // Value to be added to each new circle 376 | spiderfyDistanceSurplus: 30, 377 | // will draw additional helping circles 378 | helpingCircles: true, 379 | // Possibility to specify helpingCircle style 380 | clockHelpingCircleOptions: { 381 | color: "grey", 382 | dashArray: "5", 383 | fillOpacity: 0, 384 | opacity: 0.5, 385 | weight: 3 386 | }, 387 | // Set to false to disable all animations (zoom and spiderfy). 388 | // If false, option animateAddingMarkers below has no effect. 389 | // If L.DomUtil.TRANSITION is falsy, this option has no effect. 390 | animate: false, 391 | // Whether to animate adding markers after adding the MarkerClusterGroup to the map 392 | // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. 393 | animateAddingMarkers: false, 394 | // Increase to increase the distance away that spiderfied markers appear from the center 395 | spiderfyDistanceMultiplier: 1, 396 | // Make it possible to specify a polyline options on a spider leg 397 | spiderLegPolylineOptions: { 398 | weight: 1.5, 399 | color: "#222", 400 | opacity: 0.5 401 | }, 402 | // 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 403 | chunkedLoading: false, 404 | chunkInterval: 200, 405 | // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) 406 | chunkDelay: 50, 407 | // at the end of each interval, give n milliseconds back to system/browser 408 | chunkProgress: null, 409 | // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) 410 | // Options to pass to the L.Polygon constructor 411 | polygonOptions: {} 412 | } 413 | }); 414 | --------------------------------------------------------------------------------