mirror of
https://github.com/stylersnico/librenms.git
synced 2026-07-12 16:06:25 +02:00
Add initial code from https://github.com/danzel/Leaflet/compare/clustered-markers
This commit is contained in:
@@ -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>' });
|
||||
}
|
||||
}});
|
||||
````
|
||||
@@ -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 © 2011 OpenStreetMap contributors, Imagery © 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>
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user