This commit is contained in:
danzel
2012-07-11 14:13:46 +12:00
parent b500f4c6cc
commit 383d961cf7
16 changed files with 8240 additions and 1 deletions
+25 -1
View File
@@ -1,4 +1,28 @@
Leaflet.markercluster
=====================
Provides Marker Clustering functionality for Leaflet
Provides Marker Clustering functionality for Leaflet
##Using the plugin
See the included example for usage.
Create a new MarkerClusterGroup, add your markers to it, then add it to the map
````
var markers = new L.MarkerClusterGroup();
markers.addLayer(new L.Marker(getRandomLatLng(map)));
map.addLayer(markers);
````
For a more complete example see example/marker-clustering.html
###Customising the Clustered Marker
As an option to MarkerClusterGroup you can provide your own function for creating the Icon for the clustered markers.
The default implementation changes color at bounds of 10 and 100, but more advanced uses may require customising this.
````
var markers = new L.MarkerClusterGroup({ options: {
iconCreateFunction: function(childCount) {
return new L.DivIcon({ html: '<b>' + childCount + '</b>' });
}
}});
````
+115
View File
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<head>
<title>Leaflet debug page</title>
<link rel="stylesheet" href="../lib/leaflet-dist/leaflet.css" />
<!--[if lte IE 8]><link rel="stylesheet" href="../lib/leaflet-dist/leaflet.ie.css" /><![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="screen.css" />
<script src="../lib/leaflet-dist/leaflet-src.js"></script>
<link rel="stylesheet" href="../src/MarkerCluster.css" />
<script src="../src/MarkerClusterGroup.js"></script>
<script src="../src/MarkerCluster.js"></script>
<style type="text/css">
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
.marker-cluster {
text-align: center;
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}
</style>
</head>
<body>
<div id="map"></div>
<button id="populate">Populate with 10 markers</button>
<script type="text/javascript">
var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png',
cloudmadeAttribution = 'Map data &copy; 2011 OpenStreetMap contributors, Imagery &copy; 2011 CloudMade',
cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution}),
latlng = new L.LatLng(50.5, 30.51);
var map = new L.Map('map', {center: latlng, zoom: 15, layers: [cloudmade]});
var markers = new L.MarkerClusterGroup();
function populate() {
for (var i = 0; i < 100; i++) {
markers.addLayer(new L.Marker(getRandomLatLng(map)));
}
return false;
}
function populateRandomVector() {
for (var i = 0, latlngs = [], len = 20; i < len; i++) {
latlngs.push(getRandomLatLng(map));
}
var path = new L.Polyline(latlngs);
map.addLayer(path);
}
function getRandomLatLng(map) {
var bounds = map.getBounds(),
southWest = bounds.getSouthWest(),
northEast = bounds.getNorthEast(),
lngSpan = northEast.lng - southWest.lng,
latSpan = northEast.lat - southWest.lat;
return new L.LatLng(
southWest.lat + latSpan * Math.random(),
southWest.lng + lngSpan * Math.random());
}
//markers.bindPopup("<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec odio. Quisque volutpat mattis eros. Nullam malesuada erat ut turpis. Suspendisse urna nibh, viverra non, semper suscipit, posuere a, pede.</p><p>Donec nec justo eget felis facilisis fermentum. Aliquam porttitor mauris sit amet orci. Aenean dignissim pellentesque.</p>");
markers.on('click', function (a) {
if (a.layer instanceof L.MarkerCluster) {
console.log('cluster ' + a.layer.getAllChildMarkers().length);
} else {
console.log('marker ' + a.layer);
}
});
populate();
populateRandomVector();
map.addLayer(markers);
L.DomUtil.get('populate').onclick = populate;
</script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
#map {
width: 800px;
height: 600px;
border: 1px solid #ccc;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

File diff suppressed because it is too large Load Diff
+368
View File
@@ -0,0 +1,368 @@
/* required styles */
.leaflet-map-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-pane,
.leaflet-overlay-pane,
.leaflet-shadow-pane,
.leaflet-marker-pane,
.leaflet-popup-pane,
.leaflet-overlay-pane svg,
.leaflet-zoom-box,
.leaflet-image-layer { /* TODO optimize classes */
position: absolute;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
.leaflet-clickable {
cursor: pointer;
}
.leaflet-dragging, .leaflet-dragging .leaflet-clickable {
cursor: move;
}
.leaflet-container img {
/* map is broken in FF if you have max-width: 100% on tiles */
max-width: none !important;
}
.leaflet-container img.leaflet-image-layer {
/* stupid Android 2 doesn't understand "max-width: none" properly */
max-width: 15000px !important;
}
.leaflet-tile-pane { z-index: 2; }
.leaflet-objects-pane { z-index: 3; }
.leaflet-overlay-pane { z-index: 4; }
.leaflet-shadow-pane { z-index: 5; }
.leaflet-marker-pane { z-index: 6; }
.leaflet-popup-pane { z-index: 7; }
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
}
/* Leaflet controls */
.leaflet-control {
position: relative;
z-index: 7;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
.leaflet-control-zoom {
-moz-border-radius: 7px;
-webkit-border-radius: 7px;
border-radius: 7px;
}
.leaflet-control-zoom {
padding: 5px;
background: rgba(0, 0, 0, 0.25);
}
.leaflet-control-zoom a {
background-color: rgba(255, 255, 255, 0.75);
}
.leaflet-control-zoom a, .leaflet-control-layers a {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-control-zoom a {
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
width: 19px;
height: 19px;
}
.leaflet-control-zoom a:hover {
background-color: #fff;
}
.leaflet-touch .leaflet-control-zoom a {
width: 27px;
height: 27px;
}
.leaflet-control-zoom-in {
background-image: url(images/zoom-in.png);
margin-bottom: 5px;
}
.leaflet-control-zoom-out {
background-image: url(images/zoom-out.png);
}
.leaflet-control-layers {
box-shadow: 0 1px 7px #999;
background: #f8f8f9;
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
border-radius: 8px;
}
.leaflet-control-layers a {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-touch .leaflet-control-layers a {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
color: #333;
background: #fff;
}
.leaflet-control-layers input {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
.leaflet-container .leaflet-control-attribution {
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px #bbb;
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
color: black;
line-height: 1;
font-size: 10px;
padding-bottom: 2px;
text-shadow: 1px 1px 1px #fff;
background-color: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:nth-child(2) {
border-top: 2px solid #777;
padding-top: 1px;
border-bottom: none;
margin-top: -2px;
}
.leaflet-touch .leaflet-control-attribution, .leaflet-touch .leaflet-control-layers {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers {
border: 5px solid #bbb;
}
/* Zoom and fade animations */
.leaflet-fade-anim .leaflet-tile, .leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-tile-loaded, .leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0.25,0.1,0.25,0.75);
-moz-transition: -moz-transform 0.25s cubic-bezier(0.25,0.1,0.25,0.75);
-o-transition: -o-transform 0.25s cubic-bezier(0.25,0.1,0.25,0.75);
transition: transform 0.25s cubic-bezier(0.25,0.1,0.25,0.75);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile,
.leaflet-touching .leaflet-zoom-animated {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* Popup layout */
.leaflet-popup {
position: absolute;
text-align: center;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
}
.leaflet-popup-content {
margin: 14px 20px;
}
.leaflet-popup-tip-container {
margin: 0 auto;
width: 40px;
height: 16px;
position: relative;
overflow: hidden;
}
.leaflet-popup-tip {
width: 15px;
height: 15px;
padding: 1px;
margin: -8px auto 0;
-moz-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-close-button {
position: absolute;
top: 8px;
right: 8px;
width: 10px;
height: 10px;
overflow: hidden;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
/* Visual appearance */
.leaflet-container {
background: #ddd;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #05f;
background: white;
opacity: 0.5;
}
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
.leaflet-editing-icon {
border-radius: 2px;
}
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
background: white;
box-shadow: 0 3px 10px #888;
-moz-box-shadow: 0 3px 10px #888;
-webkit-box-shadow: 0 3px 14px #999;
}
.leaflet-popup-content-wrapper {
-moz-border-radius: 20px;
-webkit-border-radius: 20px;
border-radius: 20px;
}
.leaflet-popup-content {
font: 12px/1.4 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.leaflet-popup-close-button {
background: white url(images/popup-close.png);
}
+44
View File
@@ -0,0 +1,44 @@
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
.leaflet-control {
display: inline;
}
.leaflet-popup-tip {
width: 21px;
_width: 27px;
margin: 0 auto;
_margin-top: -3px;
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
}
.leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
border: 1px solid #bbb;
}
.leaflet-control-zoom {
filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#3F000000',EndColorStr='#3F000000');
}
.leaflet-control-zoom a {
background-color: #eee;
}
.leaflet-control-zoom a:hover {
background-color: #fff;
}
.leaflet-control-layers-toggle {
}
.leaflet-control-attribution, .leaflet-control-layers {
background: white;
}
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.25s ease-out;
-moz-transition: -moz-transform 0.25s ease-out;
-o-transition: -o-transform 0.25s ease-out;
transition: transform 0.25s ease-out;
}
+184
View File
@@ -0,0 +1,184 @@
L.MarkerCluster = L.Marker.extend({
initialize: function (group, a, b) {
this._group = group;
this._markers = [];
this._childClusters = [];
this._childCount = 0;
this._bounds = new L.LatLngBounds();
this._addChild(a);
if (b) {
this._addChild(b);
}
},
//Recursively retrieve all child markers of this cluster
getAllChildMarkers: function (storageArray) {
storageArray = storageArray || [];
for (var i = 0; i < this._childClusters.length; i++) {
this._childClusters[i].getAllChildMarkers(storageArray);
}
for (var j = 0; j < this._markers.length; j++) {
storageArray.push(this._markers[j]);
}
return storageArray;
},
_baseInit: function () {
L.Marker.prototype.initialize.call(this, this._latlng, { icon: this._group.options.iconCreateFunction(this._childCount) });
},
_addChild: function (new1) {
if (new1 instanceof L.MarkerCluster) {
this._childClusters.push(new1);
this._childCount += new1._childCount;
} else {
this._markers.push(new1);
this._childCount++;
}
this._expandBounds(new1);
},
//TODO: Replace with L.LatLngBounds
_expandBounds: function (marker) {
if (marker instanceof L.MarkerCluster) {
this._bounds.extend(marker._bounds);
} else {
this._bounds.extend(marker.getLatLng());
}
this._latlng = this._bounds.getCenter();
},
//Set our markers position as given and add it to the map
_addToMap: function (startPos) {
if (startPos) {
this._backupLatlng = this._latlng;
this.setLatLng(startPos);
}
L.FeatureGroup.prototype.addLayer.call(this._group, this);
},
_recursivelyAnimateChildrenIn: function (center, depth) {
var markers = this._markers,
markersLength = markers.length,
childClusters = this._childClusters,
childClustersLength = childClusters.length;
for (var i = 0; i < markersLength; i++) {
var m = markers[i];
//Only do it if the icon is still on the map
if (m._icon) {
m._setPos(center);
//TODO Scale them down as they move? Fade them as they move?
}
}
if (depth === 1) {
for (var j = 0; j < childClustersLength; j++) {
var cm = childClusters[j];
if (cm._icon) {
cm._setPos(center);
}
}
} else {
for (var k = 0; k < childClustersLength; k++) {
childClusters[k]._recursivelyAnimateChildrenIn(center, depth - 1);
}
}
},
_recursivelyAddChildrenToMap: function (startPos, depth, bounds) {
if (depth === 0) {
this._addToMap(startPos);
return;
}
//Add our child markers at startPos (so they can be animated out)
for (var i = 0; i < this._markers.length; i++) {
var nm = this._markers[i];
if (!bounds.contains(nm._latlng)) {
continue;
}
if (startPos) {
nm._backupLatlng = nm.getLatLng();
nm.setLatLng(startPos);
}
L.FeatureGroup.prototype.addLayer.call(this._group, nm);
}
//Recurse down to child clusters
for (var k = 0; k < this._childClusters.length; k++) {
var cc = this._childClusters[k];
if (bounds.intersects(cc._bounds)) {
cc._recursivelyAddChildrenToMap(startPos, depth - 1, bounds);
}
}
},
_recursivelyRestoreChildPositions: function (depth) {
//Fix positions of child markers
for (var i = 0; i < this._markers.length; i++) {
var nm = this._markers[i];
if (nm._backupLatlng) {
nm.setLatLng(nm._backupLatlng);
delete nm._backupLatlng;
}
}
if (depth === 1) {
//Reposition child clusters
for (var j = 0; j < this._childClusters.length; j++) {
this._childClusters[j]._restorePosition();
}
} else {
for (var k = 0; k < this._childClusters.length; k++) {
this._childClusters[k]._recursivelyRestoreChildPositions(depth - 1);
}
}
},
_restorePosition: function () {
if (this._backupLatlng) {
this.setLatLng(this._backupLatlng);
delete this._backupLatlng;
}
},
_recursivelyRemoveChildrenFromMap: function (depth) {
//markers
for (var i = 0; i < this._markers.length; i++) {
//TODO: animate removing
L.FeatureGroup.prototype.removeLayer.call(this._group, this._markers[i]);
}
if (depth === 1) {
//child clusters
for (var j = 0; j < this._childClusters.length; j++) {
//TODO: animate removing
L.FeatureGroup.prototype.removeLayer.call(this._group, this._childClusters[j]);
}
} else {
var childClusters = this._childClusters,
childClustersLength = childClusters.length;
for (var k = 0; k < childClustersLength; k++) {
childClusters[k]._recursivelyRemoveChildrenFromMap(depth - 1);
}
}
}
});
+385
View File
@@ -0,0 +1,385 @@
/*
* L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
*/
L.MarkerClusterGroup = L.FeatureGroup.extend({
options: {
//distanceToCluster: 10, //Any points closer than this will probably get put in to a cluster
maxClusterRadius: 60, //A cluster will cover at most this many pixels from its center
iconCreateFunction: function (childCount) {
var c = ' marker-cluster-';
if (childCount < 10) {
c += 'small';
} else if (childCount < 100) {
c += 'medium';
} else {
c += 'large';
}
return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', elementType: 'span', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
}
},
initialize: function (options) {
L.Util.setOptions(this, options);
L.FeatureGroup.prototype.initialize.call(this, []);
this._needsClustering = [];
this._markersAndClustersAtZoom = {};
},
_sqDist: function (p1, p2) {
var dx = p2.x - p1.x,
dy = p2.y - p1.y;
return dx * dx + dy * dy;
},
_zoomEnd: function () {
this._animationStart();
this._mergeSplitClusters();
this._zoom = this._map._zoom;
},
_moveEnd: function () {
if (this._inZoomAnimation) {
return;
}
var l, i,
layers = this._layers,
bounds = this._getExpandedVisibleBounds(),
highestLevel = this._markersAndClustersAtZoom[this._highestZoom],
depth = this._zoom - this._highestZoom,
highestLevelClusters = highestLevel.clusters,
highestLevelUnclustered = highestLevel.unclustered;
//Remove visible layers that are no longer visible
for (i in layers) {
l = layers[i];
if (!bounds.contains(l.getLatLng())) {
L.FeatureGroup.prototype.removeLayer.call(this, l);
}
}
//Re-Check everyone for being in the viewport
//Do the clusters (and their child unclustered ones) recursively for performance
for (i = 0; i < highestLevelClusters.length; i++) {
l = highestLevelClusters[i];
if (bounds.contains(l.getLatLng())) {
l._recursivelyAddChildrenToMap(null, depth, bounds);
}
}
//Do the markers at this level too
for (i = 0; i < highestLevelUnclustered.length; i++) {
l = highestLevelUnclustered[i];
if (bounds.contains(l.getLatLng())) {
L.FeatureGroup.prototype.addLayer.call(this, l);
}
}
},
_generateInitialClusters: function () {
var res = this._cluster(this._needsClustering, [], this._map.getZoom());
this._markersAndClustersAtZoom[this._map._zoom] = res;
//Remember the highest zoom level we've seen and the current zoom level
this._highestZoom = this._zoom = this._map._zoom;
//Make things appear on the map
for (var i = 0; i < res.clusters.length; i++) {
res.clusters[i]._addToMap();
}
for (var j = 0; j < res.unclustered.length; j++) {
L.FeatureGroup.prototype.addLayer.call(this, res.unclustered[j]);
}
},
//Merge and split any existing clusters that are too big or small
_mergeSplitClusters: function () {
var map = this._map,
newState,
depth = Math.abs(this._map._zoom - this._zoom);
if (this._zoom < this._map._zoom) { //Zoom in, split
var startingClusters = this._markersAndClustersAtZoom[this._zoom].clusters;
while (this._zoom < this._map._zoom) { //Split each intermediate layer if required
var currentClusters = this._markersAndClustersAtZoom[this._zoom].clusters;
this._zoom++;
newState = this._markersAndClustersAtZoom[this._zoom];
if (!newState) { //If we don't have clusters for the new level, calculate them
newState = { 'clusters': [], 'unclustered': [] };
for (var i = 0; i < currentClusters.length; i++) {
var newClusters;
if (currentClusters[i]._childClusters.length > 0) {
//Child clusters should always be 0 as we haven't calculated clusters for this level
throw 'something is wrong, childClusters length should be 0: ' + currentClusters[i]._childClusters.length;
} else {
newClusters = this._cluster(currentClusters[i]._markers, [], this._zoom);
}
currentClusters[i]._childClusters = newClusters.clusters;
currentClusters[i]._markers = newClusters.unclustered;
newState.clusters = newState.clusters.concat(newClusters.clusters);
newState.unclustered = newState.unclustered.concat(newClusters.unclustered);
}
this._markersAndClustersAtZoom[this._zoom] = newState;
}
}
this._animationZoomIn(startingClusters, depth);
} else if (this._zoom > this._map._zoom) { //Zoom out, merge
this._highestZoom = Math.min(this._highestZoom, this._map._zoom);
//Ensure all of the intermediate zoom levels are generated
var now;
while (this._zoom > this._map._zoom) {
now = this._markersAndClustersAtZoom[this._zoom];
newState = this._markersAndClustersAtZoom[this._zoom - 1];
this._zoom--;
if (!newState) {
newState = this._cluster(now.clusters.concat(now.unclustered), [], this._zoom);
this._markersAndClustersAtZoom[this._zoom] = newState;
}
}
this._animationZoomOut(newState.clusters, depth);
}
},
addLayer: function (layer) {
this._needsClustering.push(layer);
//TODO: If we have already clustered we'll need to add this one to a cluster
//Should be able to use _cluster with the current clusters and just this layer
return this;
},
removeLayer: function (layer) {
//TODO Hrm....
//Will need to got through each cluster and find the marker, removing it as required, possibly turning its parent from a cluster into an individual marker.
//Or the easy version: Just recluster everything!
return this;
},
onAdd: function (map) {
L.FeatureGroup.prototype.onAdd.call(this, map); // LayerGroup
this._generateInitialClusters();
this._map.on('zoomend', this._zoomEnd, this);
this._map.on('moveend', this._moveEnd, this);
},
//Takes a list of objects that have a 'getLatLng()' function (Marker / MarkerCluster)
//Performs clustering on them (using a greedy algorithm) and returns those clusters.
//Returns { clusters: [], unclustered: [] }
_cluster: function (points, existingClusters, zoom) {
var clusterRadiusSqrd = this.options.maxClusterRadius * this.options.maxClusterRadius,
clusters = existingClusters,
unclustered = [],
center = this._map.getCenter(),
i, j, c;
//Calculate pixel positions
for (i = 0; i < clusters.length; i++) {
c = clusters[i];
c._projCenter = this._map.project(c.getLatLng(), zoom);
}
for (i = 0; i < points.length; i++) {
var p2 = points[i];
p2._projCenter = this._map.project(p2.getLatLng(), zoom);
}
//go through each point
for (i = 0; i < points.length; i++) {
var point = points[i];
var used = false;
//try add it to an existing cluster
for (j = 0; j < clusters.length; j++) {
c = clusters[j];
if (this._sqDist(point._projCenter, c._projCenter) <= clusterRadiusSqrd) {
c._addChild(point);
c._projCenter = this._map.project(c.getLatLng(), zoom);
used = true;
break;
}
}
//otherwise, look through all of the markers we haven't managed to cluster and see if we should form a cluster with them
if (!used) {
for (j = 0 ; j < unclustered.length; j++) {
if (this._sqDist(point._projCenter, unclustered[j]._projCenter) <= clusterRadiusSqrd) {
//Create a new cluster with these 2
var newCluster = new L.MarkerCluster(this, point, unclustered[j]);
clusters.push(newCluster);
newCluster._projCenter = this._map.project(newCluster.getLatLng(), zoom);
unclustered.splice(j, 1);
used = true;
break;
}
}
if (!used) {
unclustered.push(point);
}
}
}
//Any clusters that did not end up being a child of a new cluster, make them a child of a new cluster
for (i = unclustered.length - 1; i >= 0; i--) {
c = unclustered[i];
delete c._projCenter;
if (c instanceof L.MarkerCluster) {
var nc = new L.MarkerCluster(this, c);
clusters.push(nc);
unclustered.splice(i, 1);
}
}
//Remove the _projCenter temp variable from clusters
for (i = 0; i < clusters.length; i++) {
delete clusters[i]._projCenter;
clusters[i]._baseInit();
}
return { 'clusters': clusters, 'unclustered': unclustered };
},
//Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan)
_getExpandedVisibleBounds: function () {
var map = this._map,
bounds = map.getPixelBounds(),
width = 0,//Math.abs(bounds.max.x - bounds.min.x),
height = 0,//Math.abs(bounds.max.y - bounds.min.y),
sw = map.unproject(new L.Point(bounds.min.x - width, bounds.min.y - height)),
ne = map.unproject(new L.Point(bounds.max.x + width, bounds.max.y + height));
return new L.LatLngBounds(sw, ne);
}
});
L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
//Non Animated versions of everything
_animationStart: function () {
//Do nothing...
},
_animationZoomIn: function (startingClusters, depth) {
var bounds = this._getExpandedVisibleBounds();
//Add all children of current clusters to map and remove those clusters from map
for (var j = 0; j < startingClusters.length; j++) {
var c = startingClusters[j];
//Remove old cluster
L.FeatureGroup.prototype.removeLayer.call(this, c); //TODO Animate
c._recursivelyAddChildrenToMap(null, depth, bounds);
}
},
_animationZoomOut: function (newClusters, depth) {
var bounds = this._getExpandedVisibleBounds();
//Just add new and remove old from the map
for (var j = 0; j < newClusters.length; j++) {
var cl = newClusters[j];
if (bounds.contains(cl._latlng)) {
cl._addToMap();
}
cl._recursivelyRemoveChildrenFromMap(depth);
}
}
} : {
//Animated versions here
_animationStart: function () {
this._map._mapPane.className += ' leaflet-cluster-anim';
},
_animationZoomIn: function (startingClusters, depth) {
var map = this._map,
bounds = this._getExpandedVisibleBounds();
//Add all children of current clusters to map and remove those clusters from map
for (var j = 0; j < startingClusters.length; j++) {
var c = startingClusters[j];
var startPos = c.getLatLng();
//Remove old cluster
L.FeatureGroup.prototype.removeLayer.call(this, c); //TODO Animate
c._recursivelyAddChildrenToMap(startPos, depth, bounds);
}
this._inZoomAnimation = true;
var me = this;
//Start up a function to update the positions of the just added clusters/markers
//This must happen after otherwise they don't get animated
setTimeout(function () {
for (var j = 0; j < startingClusters.length; j++) {
startingClusters[j]._recursivelyRestoreChildPositions(depth);
}
setTimeout(function () {
map._mapPane.className = map._mapPane.className.replace(' leaflet-cluster-anim', '');
me._inZoomAnimation = false;
}, 250);
}, 0);
},
_animationZoomOut: function (newClusters, depth) {
var map = this._map,
bounds = this._getExpandedVisibleBounds();
//Animate all of the markers in the clusters to move to their cluster center point
for (var i = 0; i < newClusters.length; i++) {
var c = newClusters[i];
c._recursivelyAnimateChildrenIn(this._map.latLngToLayerPoint(c.getLatLng()).round(), depth);
}
this._inZoomAnimation = true;
var me = this;
//TODO: Use the transition timing stuff to make this more reliable
setTimeout(function () {
map._mapPane.className = map._mapPane.className.replace(' leaflet-cluster-anim', '');
for (var j = 0; j < newClusters.length; j++) {
var cl = newClusters[j];
if (bounds.contains(cl._latlng)) {
cl._addToMap();
}
cl._recursivelyRemoveChildrenFromMap(depth);
}
me._inZoomAnimation = false;
}, 250);
}
});