diff --git a/README.md b/README.md
index dd13e2f7..284a5b67 100755
--- a/README.md
+++ b/README.md
@@ -28,10 +28,10 @@ For small bugfixes or enhancements:
For larger changes:
* Cleanup your code and affected code parts
-* Run the tests from test/index.html in different browsers (at least Chrome and FF)
+* Run the tests from `/test/index.html` in different browsers (at least Chrome and FF)
* Update the documentation and tutorials where necessary
-* Update _layouts/recline-deps.html if you change required files (e.g. leaflet libraries)
-* Try to build the demos in /demos with jekyll and then check out the demos/multiview/ which utilizes most aspects of Recline
+* Update `/_includes/recline-deps.html` if you change required files (e.g. leaflet libraries)
+* Try to build the demos in `/demos/` with jekyll and then check out the `/demos/multiview/` which utilizes most aspects of Recline
## Changelog
@@ -42,6 +42,8 @@ For larger changes:
Possible breaking changes
+* Updated Leaflet to latest version 0.4.4 #220
+* Added marker clustering in map view to handle a large number of markers
* Dataset.restore method removed (not used internally except from Multiview.restore)
* Views no longer call render in initialize but must be called client code
diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html
index ad926765..1a72adb7 100644
--- a/_includes/recline-deps.html
+++ b/_includes/recline-deps.html
@@ -2,6 +2,11 @@
+
+
+
@@ -22,6 +27,7 @@
+
diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown
index 32865631..9fbdfe74 100644
--- a/docs/tutorial-views.markdown
+++ b/docs/tutorial-views.markdown
@@ -193,10 +193,16 @@ library and the Recline Map view:
+
+
+
+
{% endhighlight %}
diff --git a/download.markdown b/download.markdown
index efcabc9e..db8a7e67 100644
--- a/download.markdown
+++ b/download.markdown
@@ -79,6 +79,7 @@ Optional dependencies:
* [Mustache.js](https://github.com/janl/mustache.js/) >= 0.5.0-dev (required for all views)
* [JQuery Flot](http://code.google.com/p/flot/) >= 0.7 (required for for graph view)
* [Leaflet](http://leaflet.cloudmade.com/) >= 0.4.4 (required for map view)
+* [Leaflet.markercluster](https://github.com/danzel/Leaflet.markercluster) as of 2012-09-12 (required for marker clustering)
* [Verite Timeline](https://github.com/VeriteCo/Timeline/) as of 2012-05-02 (required for the timeline view)
* [Bootstrap](http://twitter.github.com/bootstrap/) >= v2.0 (default option for CSS and UI JS but you can use your own)
diff --git a/src/view.map.js b/src/view.map.js
index c74b2cf0..b34066a1 100644
--- a/src/view.map.js
+++ b/src/view.map.js
@@ -53,12 +53,22 @@ my.Map = Backbone.View.extend({
geomField: null,
lonField: null,
latField: null,
- autoZoom: true
+ autoZoom: true,
+ cluster: false
},
options.state
);
this.state = new recline.Model.ObjectState(stateData);
+ this._clusterOptions = {
+ zoomToBoundsOnClick: true,
+ //disableClusteringAtZoom: 10,
+ maxClusterRadius: 80,
+ singleMarkerMode: false,
+ skipDuplicateAddTesting: true,
+ animateAddingMarkers: false
+ };
+
// Listen to changes in the fields
this.model.fields.bind('change', function() {
self._setupGeometryField();
@@ -82,6 +92,9 @@ my.Map = Backbone.View.extend({
self.state.set(self.menu.state.toJSON());
self.redraw();
});
+ this.state.bind('change', function() {
+ self.redraw();
+ });
this.elSidebar = this.menu.el;
},
@@ -93,7 +106,7 @@ my.Map = Backbone.View.extend({
// ### infobox
//
// Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes.
- //
+ //
// Users should override this function to customize behaviour i.e.
//
// view = new View({...});
@@ -146,14 +159,34 @@ my.Map = Backbone.View.extend({
}
if (this._geomReady() && this.mapReady){
- if (action == 'reset' || action == 'refresh'){
+ // removing ad re-adding the layer enables faster bulk loading
+ this.map.removeLayer(this.features);
+ this.map.removeLayer(this.markers);
+
+ var countBefore = 0;
+ this.features.eachLayer(function(){countBefore++;});
+
+ if (action == 'refresh' || action == 'reset') {
this.features.clearLayers();
+ // recreate cluster group because of issues with clearLayer
+ this.map.removeLayer(this.markers);
+ this.markers = new L.MarkerClusterGroup(this._clusterOptions);
this._add(this.model.records.models);
} else if (action == 'add' && doc){
this._add(doc);
} else if (action == 'remove' && doc){
this._remove(doc);
}
+
+ // enable clustering if there is a large number of markers
+ var countAfter = 0;
+ this.features.eachLayer(function(){countAfter++;});
+ var sizeIncreased = countAfter - countBefore > 0;
+ if (!this.state.get('cluster') && countAfter > 64 && sizeIncreased) {
+ this.state.set({cluster: true});
+ return;
+ }
+
if (this.state.get('autoZoom')){
if (this.visible){
this._zoomToFeatures();
@@ -161,6 +194,11 @@ my.Map = Backbone.View.extend({
this._zoomPending = true;
}
}
+ if (this.state.get('cluster')) {
+ this.map.addLayer(this.markers);
+ } else {
+ this.map.addLayer(this.features);
+ }
}
},
@@ -216,7 +254,6 @@ my.Map = Backbone.View.extend({
try {
self.features.addData(feature);
-
} catch (except) {
wrongSoFar += 1;
var msg = 'Wrong geometry value';
@@ -235,7 +272,7 @@ my.Map = Backbone.View.extend({
});
},
- // Private: Remove one or n features to the map
+ // Private: Remove one or n features from the map
//
_remove: function(docs){
@@ -344,7 +381,7 @@ my.Map = Backbone.View.extend({
//
_zoomToFeatures: function(){
var bounds = this.features.getBounds();
- if (bounds.getNorthEast()){
+ if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){
this.map.fitBounds(bounds);
} else {
this.map.setView([0, 0], 2);
@@ -357,6 +394,7 @@ my.Map = Backbone.View.extend({
// on [OpenStreetMap](http://openstreetmap.org).
//
_setupMap: function(){
+ var self = this;
this.map = new L.Map(this.$map.get(0));
var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
@@ -364,12 +402,16 @@ my.Map = Backbone.View.extend({
var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
this.map.addLayer(bg);
- this.features = new L.GeoJSON(null,
- {onEachFeature: function(feature,layer) {
- layer.bindPopup(feature.properties.popupContent);
+ this.markers = new L.MarkerClusterGroup(this._clusterOptions);
+
+ this.features = new L.GeoJSON(null,{
+ pointToLayer: function (feature, latlng) {
+ var marker = new L.marker(latlng);
+ marker.bindPopup(feature.properties.popupContent);
+ self.markers.addLayer(marker);
+ return marker;
}
- });
- this.map.addLayer(this.features);
+ });
this.map.setView([0, 0], 2);
@@ -442,19 +484,23 @@ my.MapMenu = Backbone.View.extend({
\
\
\
+ \
\
\
\
\
-',
+ ',
// Define here events for UI elements
events: {
'click .editor-update-map': 'onEditorSubmit',
'change .editor-field-type': 'onFieldTypeChange',
- 'click #editor-auto-zoom': 'onAutoZoomChange'
+ 'click #editor-auto-zoom': 'onAutoZoomChange',
+ 'click #editor-cluster': 'onClusteringChange'
},
initialize: function(options) {
@@ -487,10 +533,14 @@ my.MapMenu = Backbone.View.extend({
}
if (this.state.get('autoZoom')) {
this.el.find('#editor-auto-zoom').attr('checked', 'checked');
- }
- else {
+ } else {
this.el.find('#editor-auto-zoom').removeAttr('checked');
}
+ if (this.state.get('cluster')) {
+ this.el.find('#editor-cluster').attr('checked', 'checked');
+ } else {
+ this.el.find('#editor-cluster').removeAttr('checked');
+ }
return this;
},
@@ -541,6 +591,10 @@ my.MapMenu = Backbone.View.extend({
this.state.set({autoZoom: !this.state.get('autoZoom')});
},
+ onClusteringChange: function(e){
+ this.state.set({cluster: !this.state.get('cluster')});
+ },
+
// Private: Helper function to select an option from a select list
//
_selectOption: function(id,value){
diff --git a/test/index.html b/test/index.html
index aa786ada..d0c688ba 100644
--- a/test/index.html
+++ b/test/index.html
@@ -15,6 +15,7 @@
+
diff --git a/test/view.map.test.js b/test/view.map.test.js
index 698c22e6..3d8f038f 100644
--- a/test/view.map.test.js
+++ b/test/view.map.test.js
@@ -53,7 +53,8 @@ test('_setupGeometryField', function () {
geomField: null,
lonField: 'lon',
latField: 'lat',
- autoZoom: true
+ autoZoom: true,
+ cluster: false
};
deepEqual(view.state.toJSON(), exp);
deepEqual(view.menu.state.toJSON(), exp);
@@ -129,6 +130,30 @@ test('_getGeometryFromRecord non-GeoJSON', function () {
});
});
+test('many markers', function () {
+ var data = [];
+ for (var i = 0; i<1000; i++) {
+ data.push({ id: i, lon: 13+3*i, lat: 52+i/10});
+ }
+ var fields = [
+ {id: 'id'},
+ {id: 'lat'},
+ {id: 'lon'}
+ ];
+
+ var dataset = new recline.Model.Dataset({records: data, fields: fields});
+ var view = new recline.View.Map({
+ model: dataset
+ });
+ $('.fixtures').append(view.el);
+ view.render();
+
+ dataset.query();
+
+ equal(view.state.get('cluster'), true);
+ view.remove();
+});
+
test('Popup', function () {
var dataset = GeoJSONFixture.getDataset();
var view = new recline.View.Map({
diff --git a/vendor/leaflet.markercluster/MarkerCluster.Default.css b/vendor/leaflet.markercluster/MarkerCluster.Default.css
new file mode 100644
index 00000000..90558dd6
--- /dev/null
+++ b/vendor/leaflet.markercluster/MarkerCluster.Default.css
@@ -0,0 +1,38 @@
+.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 {
+ 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;
+ }
\ No newline at end of file
diff --git a/vendor/leaflet.markercluster/MarkerCluster.Default.ie.css b/vendor/leaflet.markercluster/MarkerCluster.Default.ie.css
new file mode 100644
index 00000000..1d0de51d
--- /dev/null
+++ b/vendor/leaflet.markercluster/MarkerCluster.Default.ie.css
@@ -0,0 +1,22 @@
+ /* IE 6-8 fallback colors */
+.marker-cluster-small {
+ background-color: rgb(181, 226, 140);
+ }
+.marker-cluster-small div {
+ background-color: rgb(110, 204, 57);
+ }
+
+.marker-cluster-medium {
+ background-color: rgb(241, 211, 87);
+ }
+.marker-cluster-medium div {
+ background-color: rgb(240, 194, 12);
+ }
+
+.marker-cluster-large {
+ background-color: rgb(253, 156, 115);
+ }
+.marker-cluster-large div {
+ background-color: rgb(241, 128, 23);
+}
+
diff --git a/vendor/leaflet.markercluster/MarkerCluster.css b/vendor/leaflet.markercluster/MarkerCluster.css
new file mode 100644
index 00000000..a915c1a4
--- /dev/null
+++ b/vendor/leaflet.markercluster/MarkerCluster.css
@@ -0,0 +1,6 @@
+.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
+ -webkit-transition: -webkit-transform 0.25s ease-out, opacity 0.25s ease-in;
+ -moz-transition: -moz-transform 0.25s ease-out, opacity 0.25s ease-in;
+ -o-transition: -o-transform 0.25s ease-out, opacity 0.25s ease-in;
+ transition: transform 0.25s ease-out, opacity 0.25s ease-in;
+ }
diff --git a/vendor/leaflet.markercluster/leaflet.markercluster-src.js b/vendor/leaflet.markercluster/leaflet.markercluster-src.js
new file mode 100644
index 00000000..e2959410
--- /dev/null
+++ b/vendor/leaflet.markercluster/leaflet.markercluster-src.js
@@ -0,0 +1,1667 @@
+/*
+ Copyright (c) 2012, Smartrak, David Leaver
+ Leaflet.markercluster is an open-source JavaScript library for Marker Clustering on leaflet powered maps.
+ https://github.com/danzel/Leaflet.markercluster
+*/
+(function (window, undefined) {
+
+
+/*
+ * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
+ */
+
+L.MarkerClusterGroup = L.FeatureGroup.extend({
+
+ options: {
+ maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center
+ iconCreateFunction: null,
+
+ spiderfyOnMaxZoom: true,
+ showCoverageOnHover: true,
+ zoomToBoundsOnClick: true,
+ singleMarkerMode: false,
+
+ disableClusteringAtZoom: null,
+
+ skipDuplicateAddTesting: false,
+
+ //Whether to animate adding markers after adding the MarkerClusterGroup to the map
+ // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
+ animateAddingMarkers: false
+ },
+
+ initialize: function (options) {
+ L.Util.setOptions(this, options);
+ if (!this.options.iconCreateFunction) {
+ this.options.iconCreateFunction = this._defaultIconCreateFunction;
+ }
+
+ L.FeatureGroup.prototype.initialize.call(this, []);
+
+ this._inZoomAnimation = 0;
+ this._needsClustering = [];
+ //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
+ this._currentShownBounds = null;
+ },
+
+ addLayer: function (layer) {
+
+ if (layer instanceof L.LayerGroup) {
+ for (var i in layer._layers) {
+ if (layer._layers.hasOwnProperty(i)) {
+ this.addLayer(layer._layers[i]);
+ }
+ }
+ return this;
+ }
+
+ if (this.options.singleMarkerMode) {
+ layer.options.icon = this.options.iconCreateFunction({
+ getChildCount: function () {
+ return 1;
+ },
+ getAllChildMarkers: function () {
+ return [layer];
+ }
+ });
+ }
+
+ if (!this._map) {
+ this._needsClustering.push(layer);
+ return this;
+ }
+
+ if (!this.options.skipDuplicateAddTesting && this.hasLayer(layer)) {
+ return this;
+ }
+
+ //If we have already clustered we'll need to add this one to a cluster
+
+ if (this._unspiderfy) {
+ this._unspiderfy();
+ }
+
+ this._addLayer(layer, this._maxZoom);
+
+ //Work out what is visible
+ var visibleLayer = layer,
+ currentZoom = this._map.getZoom();
+ if (layer.__parent) {
+ while (visibleLayer.__parent._zoom >= currentZoom) {
+ visibleLayer = visibleLayer.__parent;
+ }
+ }
+
+ if (this.options.animateAddingMarkers) {
+ this._animationAddLayer(layer, visibleLayer);
+ } else {
+ this._animationAddLayerNonAnimated(layer, visibleLayer);
+ }
+ return this;
+ },
+
+ removeLayer: function (layer) {
+ if (!layer.__parent) {
+ return this;
+ }
+
+ if (this._unspiderfy) {
+ this._unspiderfy();
+ this._unspiderfyLayer(layer);
+ }
+
+ //Remove the marker from clusters
+ this._removeLayer(layer, true);
+
+ if (layer._icon) {
+ L.FeatureGroup.prototype.removeLayer.call(this, layer);
+ }
+ return this;
+ },
+
+ clearLayers: function () {
+ //Need our own special implementation as the LayerGroup one doesn't work for us
+
+ //If we aren't on the map yet, just blow away the markers we know of
+ if (!this._map) {
+ this._needsClustering = [];
+ return this;
+ }
+
+ if (this._unspiderfy) {
+ this._unspiderfy();
+ }
+
+ //Remove all the visible layers
+ for (var i in this._layers) {
+ if (this._layers.hasOwnProperty(i)) {
+ L.FeatureGroup.prototype.removeLayer.call(this, this._layers[i]);
+ }
+ }
+
+ //Reset _topClusterLevel and the DistanceGrids
+ this._generateInitialClusters();
+
+ return this;
+ },
+
+ hasLayer: function (layer) {
+ var res = false;
+
+ this._topClusterLevel._recursively(new L.LatLngBounds([layer.getLatLng()]), 0, this._map.getMaxZoom() + 1,
+ function (cluster) {
+ for (var i = cluster._markers.length - 1; i >= 0 && !res; i--) {
+ if (cluster._markers[i] === layer) {
+ res = true;
+ }
+ }
+ }, null);
+ return res;
+ },
+
+ zoomToShowLayer: function (layer, callback) {
+
+ var showMarker = function () {
+ if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) {
+ this._map.off('moveend', showMarker, this);
+ this.off('animationend', showMarker, this);
+
+ if (layer._icon) {
+ callback();
+ } else if (layer.__parent._icon) {
+ var afterSpiderfy = function () {
+ this.off('spiderfied', afterSpiderfy, this);
+ callback();
+ };
+
+ this.on('spiderfied', afterSpiderfy, this);
+ layer.__parent.spiderfy();
+ }
+ }
+ };
+
+ if ((layer._icon || layer.__parent._icon) && this._map.getBounds().contains(layer.__parent._latlng)) {
+ //Layer or cluster is already visible
+ showMarker.call(this);
+ } else {
+ this._map.on('moveend', showMarker, this);
+ this.on('animationend', showMarker, this);
+ layer.__parent.zoomToBounds();
+ }
+ },
+
+ //Overrides FeatureGroup.onAdd
+ onAdd: function (map) {
+ L.FeatureGroup.prototype.onAdd.call(this, map);
+
+ if (!this._gridClusters) {
+ this._generateInitialClusters();
+ }
+
+ for (var i = 0, l = this._needsClustering.length; i < l; i++) {
+ this._addLayer(this._needsClustering[i], this._maxZoom);
+ }
+ this._needsClustering = [];
+
+ this._map.on('zoomend', this._zoomEnd, this);
+ this._map.on('moveend', this._moveEnd, this);
+
+ if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
+ this._spiderfierOnAdd();
+ }
+
+ this._bindEvents();
+
+
+ //Actually add our markers to the map:
+
+ //Remember the current zoom level and bounds
+ this._zoom = this._map.getZoom();
+ this._currentShownBounds = this._getExpandedVisibleBounds();
+
+ //Make things appear on the map
+ this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
+ },
+
+ //Overrides FeatureGroup.onRemove
+ onRemove: function (map) {
+ this._map.off('zoomend', this._zoomEnd, this);
+ this._map.off('moveend', this._moveEnd, this);
+
+ //In case we are in a cluster animation
+ this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
+
+ if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
+ this._spiderfierOnRemove();
+ }
+
+ L.FeatureGroup.prototype.onRemove.call(this, map);
+ },
+
+
+ //Remove the given object from the given array
+ _arraySplice: function (anArray, obj) {
+ for (var i = anArray.length - 1; i >= 0; i--) {
+ if (anArray[i] === obj) {
+ anArray.splice(i, 1);
+ return;
+ }
+ }
+ },
+
+ _removeLayer: function (marker, removeFromDistanceGrid) {
+ var gridClusters = this._gridClusters,
+ gridUnclustered = this._gridUnclustered,
+ map = this._map;
+
+ //Remove the marker from distance clusters it might be in
+ if (removeFromDistanceGrid) {
+ for (var z = this._maxZoom; z >= 0; z--) {
+ if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) {
+ break;
+ }
+ }
+ }
+
+ //Work our way up the clusters removing them as we go if required
+ var cluster = marker.__parent,
+ markers = cluster._markers,
+ otherMarker;
+
+ //Remove the marker from the immediate parents marker list
+ this._arraySplice(markers, marker);
+
+ while (cluster) {
+ cluster._childCount--;
+
+ if (cluster._zoom < 0) {
+ //Top level, do nothing
+ break;
+ } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required
+ //We need to push the other marker up to the parent
+ otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0];
+
+ //Update distance grid
+ gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom));
+ gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom));
+
+ //Move otherMarker up to parent
+ this._arraySplice(cluster.__parent._childClusters, cluster);
+ cluster.__parent._markers.push(otherMarker);
+ otherMarker.__parent = cluster.__parent;
+
+ if (cluster._icon) {
+ //Cluster is currently on the map, need to put the marker on the map instead
+ L.FeatureGroup.prototype.removeLayer.call(this, cluster);
+ L.FeatureGroup.prototype.addLayer.call(this, otherMarker);
+ }
+ } else {
+ cluster._recalculateBounds();
+ cluster._updateIcon();
+ }
+
+ cluster = cluster.__parent;
+ }
+ },
+
+ //Overrides FeatureGroup._propagateEvent
+ _propagateEvent: function (e) {
+ if (e.target instanceof L.MarkerCluster) {
+ e.type = 'cluster' + e.type;
+ }
+ L.FeatureGroup.prototype._propagateEvent.call(this, e);
+ },
+
+ //Default functionality
+ _defaultIconCreateFunction: function (cluster) {
+ var childCount = cluster.getChildCount();
+
+ var c = ' marker-cluster-';
+ if (childCount < 10) {
+ c += 'small';
+ } else if (childCount < 100) {
+ c += 'medium';
+ } else {
+ c += 'large';
+ }
+
+ return new L.DivIcon({ html: '
' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
+ },
+
+ _bindEvents: function () {
+ var shownPolygon = null,
+ map = this._map,
+
+ spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
+ showCoverageOnHover = this.options.showCoverageOnHover,
+ zoomToBoundsOnClick = this.options.zoomToBoundsOnClick;
+
+ //Zoom on cluster click or spiderfy if we are at the lowest level
+ if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
+ this.on('clusterclick', function (a) {
+ if (map.getMaxZoom() === map.getZoom()) {
+ if (spiderfyOnMaxZoom) {
+ a.layer.spiderfy();
+ }
+ } else if (zoomToBoundsOnClick) {
+ a.layer.zoomToBounds();
+ }
+ }, this);
+ }
+
+ //Show convex hull (boundary) polygon on mouse over
+ if (showCoverageOnHover) {
+ this.on('clustermouseover', function (a) {
+ if (this._inZoomAnimation) {
+ return;
+ }
+ if (shownPolygon) {
+ map.removeLayer(shownPolygon);
+ }
+ if (a.layer.getChildCount() > 2) {
+ shownPolygon = new L.Polygon(a.layer.getConvexHull());
+ map.addLayer(shownPolygon);
+ }
+ }, this);
+ this.on('clustermouseout', function () {
+ if (shownPolygon) {
+ map.removeLayer(shownPolygon);
+ shownPolygon = null;
+ }
+ }, this);
+ map.on('zoomend', function () {
+ if (shownPolygon) {
+ map.removeLayer(shownPolygon);
+ shownPolygon = null;
+ }
+ }, this);
+ map.on('layerremove', function (opt) {
+ if (shownPolygon && opt.layer === this) {
+ map.removeLayer(shownPolygon);
+ shownPolygon = null;
+ }
+ }, this);
+ }
+ },
+
+ _zoomEnd: function () {
+ if (!this._map) { //May have been removed from the map by a zoomEnd handler
+ return;
+ }
+ this._mergeSplitClusters();
+
+ this._zoom = this._map._zoom;
+ this._currentShownBounds = this._getExpandedVisibleBounds();
+ },
+
+ _moveEnd: function () {
+ if (this._inZoomAnimation) {
+ return;
+ }
+
+ var newBounds = this._getExpandedVisibleBounds();
+
+ this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds);
+ this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, newBounds);
+
+ this._currentShownBounds = newBounds;
+ return;
+ },
+
+ _generateInitialClusters: function () {
+ var maxZoom = this._map.getMaxZoom(),
+ radius = this.options.maxClusterRadius;
+
+ if (this.options.disableClusteringAtZoom) {
+ maxZoom = this.options.disableClusteringAtZoom - 1;
+ }
+ this._maxZoom = maxZoom;
+ this._gridClusters = {};
+ this._gridUnclustered = {};
+
+ //Set up DistanceGrids for each zoom
+ for (var zoom = maxZoom; zoom >= 0; zoom--) {
+ this._gridClusters[zoom] = new L.DistanceGrid(radius);
+ this._gridUnclustered[zoom] = new L.DistanceGrid(radius);
+ }
+
+ this._topClusterLevel = new L.MarkerCluster(this, -1);
+ },
+
+ //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
+ _addLayer: function (layer, zoom) {
+ var gridClusters = this._gridClusters,
+ gridUnclustered = this._gridUnclustered,
+ markerPoint, z;
+
+ //Find the lowest zoom level to slot this one in
+ for (; zoom >= 0; zoom--) {
+ markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position
+
+ //Try find a cluster close by
+ var closest = gridClusters[zoom].getNearObject(markerPoint);
+ if (closest) {
+ closest._addChild(layer);
+ layer.__parent = closest;
+ return;
+ }
+
+ //Try find a marker close by to form a new cluster with
+ closest = gridUnclustered[zoom].getNearObject(markerPoint);
+ if (closest) {
+ if (closest.__parent) {
+ this._removeLayer(closest, false);
+ }
+ var parent = closest.__parent || this._topClusterLevel;
+
+ //Create new cluster with these 2 in it
+
+ var newCluster = new L.MarkerCluster(this, zoom, closest, layer);
+ gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom));
+ closest.__parent = newCluster;
+ layer.__parent = newCluster;
+
+ //First create any new intermediate parent clusters that don't exist
+ var lastParent = newCluster;
+ for (z = zoom - 1; z > parent._zoom; z--) {
+ lastParent = new L.MarkerCluster(this, z, lastParent);
+ gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z));
+ }
+ parent._addChild(lastParent);
+
+ //Remove closest from this zoom level and any above that it is in, replace with newCluster
+ for (z = zoom; z >= 0; z--) {
+ if (!gridUnclustered[z].removeObject(closest, this._map.project(closest.getLatLng(), z))) {
+ break;
+ }
+ }
+
+ return;
+ }
+
+ //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
+ gridUnclustered[zoom].addObject(layer, markerPoint);
+ }
+
+ return;
+ },
+
+ //Merge and split any existing clusters that are too big or small
+ _mergeSplitClusters: function () {
+ if (this._zoom < this._map._zoom) { //Zoom in, split
+ this._animationStart();
+ //Remove clusters now off screen
+ this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds());
+
+ this._animationZoomIn(this._zoom, this._map._zoom);
+
+ } else if (this._zoom > this._map._zoom) { //Zoom out, merge
+ this._animationStart();
+
+ this._animationZoomOut(this._zoom, this._map._zoom);
+ } else {
+ this._moveEnd();
+ }
+ },
+
+ //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 = L.Browser.mobile ? 0 : Math.abs(bounds.max.x - bounds.min.x),
+ height = L.Browser.mobile ? 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);
+ },
+
+ //Shared animation code
+ _animationAddLayerNonAnimated: function (layer, newCluster) {
+ if (newCluster === layer) {
+ L.FeatureGroup.prototype.addLayer.call(this, layer);
+ } else if (newCluster._childCount === 2) {
+ newCluster._addToMap();
+
+ var markers = newCluster.getAllChildMarkers();
+ L.FeatureGroup.prototype.removeLayer.call(this, markers[0]);
+ L.FeatureGroup.prototype.removeLayer.call(this, markers[1]);
+ } else {
+ newCluster._updateIcon();
+ }
+ }
+});
+
+L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
+
+ //Non Animated versions of everything
+ _animationStart: function () {
+ //Do nothing...
+ },
+ _animationZoomIn: function (previousZoomLevel, newZoomLevel) {
+ this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
+ this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
+ },
+ _animationZoomOut: function (previousZoomLevel, newZoomLevel) {
+ this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
+ this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
+ },
+ _animationAddLayer: function (layer, newCluster) {
+ this._animationAddLayerNonAnimated(layer, newCluster);
+ }
+} : {
+
+ //Animated versions here
+ _animationStart: function () {
+ this._map._mapPane.className += ' leaflet-cluster-anim';
+ this._inZoomAnimation++;
+ },
+ _animationEnd: function () {
+ if (this._map) {
+ this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
+ }
+ this._inZoomAnimation--;
+ this.fire('animationend');
+ },
+ _animationZoomIn: function (previousZoomLevel, newZoomLevel) {
+ var me = this,
+ bounds = this._getExpandedVisibleBounds(),
+ i;
+
+ //Add all children of current clusters to map and remove those clusters from map
+ this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
+ var startPos = c._latlng,
+ markers = c._markers,
+ m;
+
+ if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us
+ L.FeatureGroup.prototype.removeLayer.call(me, c);
+ c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);
+ } else {
+ //Fade out old cluster
+ c.setOpacity(0);
+ c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds);
+ }
+
+ //Remove all markers that aren't visible any more
+ //TODO: Do we actually need to do this on the higher levels too?
+ for (i = markers.length - 1; i >= 0; i--) {
+ m = markers[i];
+ if (!bounds.contains(m._latlng)) {
+ L.FeatureGroup.prototype.removeLayer.call(me, m);
+ }
+ }
+
+ });
+
+ this._forceLayout();
+ var j, n;
+
+ //Update opacities
+ me._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel);
+ //TODO Maybe? Update markers in _recursivelyBecomeVisible
+ for (j in me._layers) {
+ if (me._layers.hasOwnProperty(j)) {
+ n = me._layers[j];
+
+ if (!(n instanceof L.MarkerCluster) && n._icon) {
+ n.setOpacity(1);
+ }
+ }
+ }
+
+ //update the positions of the just added clusters/markers
+ me._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {
+ c._recursivelyRestoreChildPositions(newZoomLevel);
+ });
+
+ //Remove the old clusters and close the zoom animation
+
+ setTimeout(function () {
+ //update the positions of the just added clusters/markers
+ me._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
+ L.FeatureGroup.prototype.removeLayer.call(me, c);
+ });
+
+ me._animationEnd();
+ }, 250);
+ },
+
+ _animationZoomOut: function (previousZoomLevel, newZoomLevel) {
+ this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel, newZoomLevel);
+
+ //Need to add markers for those that weren't on the map before but are now
+ this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
+ },
+ _animationZoomOutSingle: function (marker, previousZoomLevel, newZoomLevel) {
+ var bounds = this._getExpandedVisibleBounds();
+
+ //Animate all of the markers in the clusters to move to their cluster center point
+ marker._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel, newZoomLevel);
+
+ var me = this;
+
+ //Update the opacity (If we immediately set it they won't animate)
+ this._forceLayout();
+ marker._recursivelyBecomeVisible(bounds, newZoomLevel);
+
+ //TODO: Maybe use the transition timing stuff to make this more reliable
+ //When the animations are done, tidy up
+ setTimeout(function () {
+
+ marker._recursively(bounds, newZoomLevel, 0, function (c) {
+ c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel);
+ });
+ me._animationEnd();
+ }, 250);
+ },
+ _animationAddLayer: function (layer, newCluster) {
+ var me = this;
+
+ L.FeatureGroup.prototype.addLayer.call(this, layer);
+ if (newCluster !== layer) {
+ if (newCluster._childCount > 2) { //Was already a cluster
+
+ newCluster._updateIcon();
+ this._forceLayout();
+ this._animationStart();
+
+ layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));
+ layer.setOpacity(0);
+
+ setTimeout(function () {
+ L.FeatureGroup.prototype.removeLayer.call(me, layer);
+ layer.setOpacity(1);
+
+ me._animationEnd();
+ }, 250);
+
+ } else { //Just became a cluster
+ this._forceLayout();
+
+ me._animationStart();
+ me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom());
+ }
+ }
+ },
+
+ //Force a browser layout of stuff in the map
+ // Should apply the current opacity and location to all elements so we can update them again for an animation
+ _forceLayout: function () {
+ //In my testing this works, infact offsetWidth of any element seems to work.
+ //Could loop all this._layers and do this for each _icon if it stops working
+
+ L.Util.falseFn(document.body.offsetWidth);
+ }
+});
+
+
+L.MarkerCluster = L.Marker.extend({
+ initialize: function (group, zoom, a, b) {
+
+ L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this });
+
+
+ this._group = group;
+ this._zoom = zoom;
+
+ this._markers = [];
+ this._childClusters = [];
+ this._childCount = 0;
+ this._iconNeedsUpdate = true;
+
+ this._bounds = new L.LatLngBounds();
+
+ if (a) {
+ this._addChild(a);
+ }
+ if (b) {
+ this._addChild(b);
+ }
+ },
+
+ //Recursively retrieve all child markers of this cluster
+ getAllChildMarkers: function (storageArray) {
+ storageArray = storageArray || [];
+
+ for (var i = this._childClusters.length - 1; i >= 0; i--) {
+ this._childClusters[i].getAllChildMarkers(storageArray);
+ }
+
+ for (var j = this._markers.length - 1; j >= 0; j--) {
+ storageArray.push(this._markers[j]);
+ }
+
+ return storageArray;
+ },
+
+ //Returns the count of how many child markers we have
+ getChildCount: function () {
+ return this._childCount;
+ },
+
+ //Zoom to the extents of this cluster
+ zoomToBounds: function () {
+ this._group._map.fitBounds(this._bounds);
+ },
+
+
+ _updateIcon: function () {
+ this._iconNeedsUpdate = true;
+ if (this._icon) {
+ this.setIcon(this);
+ }
+ },
+
+ //Cludge for Icon, we pretend to be an icon for performance
+ createIcon: function () {
+ if (this._iconNeedsUpdate) {
+ this._iconObj = this._group.options.iconCreateFunction(this);
+ this._iconNeedsUpdate = false;
+ }
+ return this._iconObj.createIcon();
+ },
+ createShadow: function () {
+ return this._iconObj.createShadow();
+ },
+
+
+ _addChild: function (new1, isNotificationFromChild) {
+
+ this._iconNeedsUpdate = true;
+ this._expandBounds(new1);
+
+ if (new1 instanceof L.MarkerCluster) {
+ if (!isNotificationFromChild) {
+ this._childClusters.push(new1);
+ new1.__parent = this;
+ }
+ this._childCount += new1._childCount;
+ } else {
+ if (!isNotificationFromChild) {
+ this._markers.push(new1);
+ }
+ this._childCount++;
+ }
+
+ if (this.__parent) {
+ this.__parent._addChild(new1, true);
+ }
+ },
+
+ //Expand our bounds and tell our parent to
+ _expandBounds: function (marker) {
+ var addedCount,
+ addedLatLng = marker._wLatLng || marker._latlng;
+
+ if (marker instanceof L.MarkerCluster) {
+ this._bounds.extend(marker._bounds);
+ addedCount = marker._childCount;
+ } else {
+ this._bounds.extend(addedLatLng);
+ addedCount = 1;
+ }
+
+ if (!this._cLatLng) {
+ // when clustering, take position of the first point as the cluster center
+ this._cLatLng = marker._cLatLng || addedLatLng;
+ }
+
+ // when showing clusters, take weighted average of all points as cluster center
+ var totalCount = this._childCount + addedCount;
+
+ //Calculate weighted latlng for display
+ if (!this._wLatLng) {
+ this._latlng = this._wLatLng = new L.LatLng(addedLatLng.lat, addedLatLng.lng);
+ } else {
+ this._wLatLng.lat = (addedLatLng.lat * addedCount + this._wLatLng.lat * this._childCount) / totalCount;
+ this._wLatLng.lng = (addedLatLng.lng * addedCount + this._wLatLng.lng * this._childCount) / totalCount;
+ }
+ },
+
+ //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 (bounds, center, depth) {
+ this._recursively(bounds, 0, depth - 1,
+ function (c) {
+ var markers = c._markers,
+ i, m;
+ for (i = markers.length - 1; i >= 0; i--) {
+ m = markers[i];
+
+ //Only do it if the icon is still on the map
+ if (m._icon) {
+ m._setPos(center);
+ m.setOpacity(0);
+ }
+ }
+ },
+ function (c) {
+ var childClusters = c._childClusters,
+ j, cm;
+ for (j = childClusters.length - 1; j >= 0; j--) {
+ cm = childClusters[j];
+ if (cm._icon) {
+ cm._setPos(center);
+ cm.setOpacity(0);
+ }
+ }
+ }
+ );
+ },
+
+ _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, previousZoomLevel, newZoomLevel) {
+ this._recursively(bounds, newZoomLevel, 0,
+ function (c) {
+ c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);
+
+ //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
+ //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
+ if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) {
+ c.setOpacity(1);
+ c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
+ } else {
+ c.setOpacity(0);
+ }
+
+ c._addToMap();
+ }
+ );
+ },
+
+ _recursivelyBecomeVisible: function (bounds, zoomLevel) {
+ this._recursively(bounds, 0, zoomLevel, null, function (c) {
+ c.setOpacity(1);
+ });
+ },
+
+ _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {
+ this._recursively(bounds, 0, zoomLevel,
+ function (c) {
+ if (zoomLevel === c._zoom) {
+ return;
+ }
+
+ //Add our child markers at startPos (so they can be animated out)
+ for (var i = c._markers.length - 1; i >= 0; i--) {
+ var nm = c._markers[i];
+
+ if (!bounds.contains(nm._latlng)) {
+ continue;
+ }
+
+ if (startPos) {
+ nm._backupLatlng = nm.getLatLng();
+
+ nm.setLatLng(startPos);
+ nm.setOpacity(0);
+ }
+
+ L.FeatureGroup.prototype.addLayer.call(c._group, nm);
+ }
+ },
+ function (c) {
+ c._addToMap(startPos);
+ }
+ );
+ },
+
+ _recursivelyRestoreChildPositions: function (zoomLevel) {
+ //Fix positions of child markers
+ for (var i = this._markers.length - 1; i >= 0; i--) {
+ var nm = this._markers[i];
+ if (nm._backupLatlng) {
+ nm.setLatLng(nm._backupLatlng);
+ delete nm._backupLatlng;
+ }
+ }
+
+ if (zoomLevel - 1 === this._zoom) {
+ //Reposition child clusters
+ for (var j = this._childClusters.length - 1; j >= 0; j--) {
+ this._childClusters[j]._restorePosition();
+ }
+ } else {
+ for (var k = this._childClusters.length - 1; k >= 0; k--) {
+ this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);
+ }
+ }
+ },
+
+ _restorePosition: function () {
+ if (this._backupLatlng) {
+ this.setLatLng(this._backupLatlng);
+ delete this._backupLatlng;
+ }
+ },
+
+ //exceptBounds: If set, don't remove any markers/clusters in it
+ _recursivelyRemoveChildrenFromMap: function (previousBounds, zoomLevel, exceptBounds) {
+ var m, i;
+ this._recursively(previousBounds, -1, zoomLevel - 1,
+ function (c) {
+ //Remove markers at every level
+ for (i = c._markers.length - 1; i >= 0; i--) {
+ m = c._markers[i];
+ if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
+ L.FeatureGroup.prototype.removeLayer.call(c._group, m);
+ m.setOpacity(1);
+ }
+ }
+ },
+ function (c) {
+ //Remove child clusters at just the bottom level
+ for (i = c._childClusters.length - 1; i >= 0; i--) {
+ m = c._childClusters[i];
+ if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
+ L.FeatureGroup.prototype.removeLayer.call(c._group, m);
+ m.setOpacity(1);
+ }
+ }
+ }
+ );
+ },
+
+ //Run the given functions recursively to this and child clusters
+ // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
+ // zoomLevelToStart: zoom level to start running functions (inclusive)
+ // zoomLevelToStop: zoom level to stop running functions (inclusive)
+ // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
+ // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
+ _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) {
+ var childClusters = this._childClusters,
+ zoom = this._zoom,
+ i, c;
+
+ if (zoomLevelToStart > zoom) { //Still going down to required depth, just recurse to child clusters
+ for (i = childClusters.length - 1; i >= 0; i--) {
+ c = childClusters[i];
+ if (boundsToApplyTo.intersects(c._bounds)) {
+ c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
+ }
+ }
+ } else { //In required depth
+
+ if (runAtEveryLevel) {
+ runAtEveryLevel(this);
+ }
+ if (runAtBottomLevel && this._zoom === zoomLevelToStop) {
+ runAtBottomLevel(this);
+ }
+
+ //TODO: This loop is almost the same as above
+ if (zoomLevelToStop > zoom) {
+ for (i = childClusters.length - 1; i >= 0; i--) {
+ c = childClusters[i];
+ if (boundsToApplyTo.intersects(c._bounds)) {
+ c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
+ }
+ }
+ }
+ }
+ },
+
+ _recalculateBounds: function () {
+ var markers = this._markers,
+ childClusters = this._childClusters,
+ i;
+
+ this._bounds = new L.LatLngBounds();
+ delete this._wLatLng;
+
+ for (i = markers.length - 1; i >= 0; i--) {
+ this._expandBounds(markers[i]);
+ }
+ for (i = childClusters.length - 1; i >= 0; i--) {
+ this._expandBounds(childClusters[i]);
+ }
+ },
+
+
+ //Returns true if we are the parent of only one cluster and that cluster is the same as us
+ _isSingleParent: function () {
+ //Don't need to check this._markers as the rest won't work if there are any
+ return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount;
+ }
+});
+
+
+
+L.DistanceGrid = function (cellSize) {
+ this._cellSize = cellSize;
+ this._sqCellSize = cellSize * cellSize;
+ this._grid = {};
+ this._objectPoint = { };
+};
+
+L.DistanceGrid.prototype = {
+
+ addObject: function (obj, point) {
+ var x = this._getCoord(point.x),
+ y = this._getCoord(point.y),
+ grid = this._grid,
+ row = grid[y] = grid[y] || {},
+ cell = row[x] = row[x] || [],
+ stamp = L.Util.stamp(obj);
+
+ this._objectPoint[stamp] = point;
+
+ cell.push(obj);
+ },
+
+ updateObject: function (obj, point) {
+ this.removeObject(obj);
+ this.addObject(obj, point);
+ },
+
+ //Returns true if the object was found
+ removeObject: function (obj, point) {
+ var x = this._getCoord(point.x),
+ y = this._getCoord(point.y),
+ grid = this._grid,
+ row = grid[y] = grid[y] || {},
+ cell = row[x] = row[x] || [],
+ i, len;
+
+ delete this._objectPoint[L.Util.stamp(obj)];
+
+ for (i = 0, len = cell.length; i < len; i++) {
+ if (cell[i] === obj) {
+
+ cell.splice(i, 1);
+
+ if (len === 1) {
+ delete row[x];
+ }
+
+ return true;
+ }
+ }
+
+ },
+
+ eachObject: function (fn, context) {
+ var i, j, k, len, row, cell, removed,
+ grid = this._grid;
+
+ for (i in grid) {
+ if (grid.hasOwnProperty(i)) {
+ row = grid[i];
+
+ for (j in row) {
+ if (row.hasOwnProperty(j)) {
+ cell = row[j];
+
+ for (k = 0, len = cell.length; k < len; k++) {
+ removed = fn.call(context, cell[k]);
+ if (removed) {
+ k--;
+ len--;
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ getNearObject: function (point) {
+ var x = this._getCoord(point.x),
+ y = this._getCoord(point.y),
+ i, j, k, row, cell, len, obj, dist,
+ objectPoint = this._objectPoint,
+ closestDistSq = this._sqCellSize,
+ closest = null;
+
+ for (i = y - 1; i <= y + 1; i++) {
+ row = this._grid[i];
+ if (row) {
+
+ for (j = x - 1; j <= x + 1; j++) {
+ cell = row[j];
+ if (cell) {
+
+ for (k = 0, len = cell.length; k < len; k++) {
+ obj = cell[k];
+ dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point);
+ if (dist < closestDistSq) {
+ closestDistSq = dist;
+ closest = obj;
+ }
+ }
+ }
+ }
+ }
+ }
+ return closest;
+ },
+
+ _getCoord: function (x) {
+ return Math.floor(x / this._cellSize);
+ },
+
+ _sqDist: function (p, p2) {
+ var dx = p2.x - p.x,
+ dy = p2.y - p.y;
+ return dx * dx + dy * dy;
+ }
+};
+
+
+/* Copyright (c) 2012 the authors listed at the following URL, and/or
+the authors of referenced articles or incorporated external code:
+http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
+*/
+
+(function () {
+ L.QuickHull = {
+ getDistant: function (cpt, bl) {
+ var vY = bl[1].lat - bl[0].lat,
+ vX = bl[0].lng - bl[1].lng;
+ return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng));
+ },
+
+
+ findMostDistantPointFromBaseLine: function (baseLine, latLngs) {
+ var maxD = 0,
+ maxPt = null,
+ newPoints = [],
+ i, pt, d;
+
+ for (i = latLngs.length - 1; i >= 0; i--) {
+ pt = latLngs[i];
+ d = this.getDistant(pt, baseLine);
+
+ if (d > 0) {
+ newPoints.push(pt);
+ } else {
+ continue;
+ }
+
+ if (d > maxD) {
+ maxD = d;
+ maxPt = pt;
+ }
+
+ }
+ return { 'maxPoint': maxPt, 'newPoints': newPoints };
+ },
+
+ buildConvexHull: function (baseLine, latLngs) {
+ var convexHullBaseLines = [],
+ t = this.findMostDistantPointFromBaseLine(baseLine, latLngs);
+
+ if (t.maxPoint) { // if there is still a point "outside" the base line
+ convexHullBaseLines =
+ convexHullBaseLines.concat(
+ this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints)
+ );
+ convexHullBaseLines =
+ convexHullBaseLines.concat(
+ this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints)
+ );
+ return convexHullBaseLines;
+ } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull
+ return [baseLine];
+ }
+ },
+
+ getConvexHull: function (latLngs) {
+ //find first baseline
+ var maxLat = false, minLat = false,
+ maxPt = null, minPt = null,
+ i;
+
+ for (i = latLngs.length - 1; i >= 0; i--) {
+ var pt = latLngs[i];
+ if (maxLat === false || pt.lat > maxLat) {
+ maxPt = pt;
+ maxLat = pt.lat;
+ }
+ if (minLat === false || pt.lat < minLat) {
+ minPt = pt;
+ minLat = pt.lat;
+ }
+ }
+ var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs),
+ this.buildConvexHull([maxPt, minPt], latLngs));
+ return ch;
+ }
+ };
+}());
+
+L.MarkerCluster.include({
+ getConvexHull: function () {
+ var childMarkers = this.getAllChildMarkers(),
+ points = [],
+ hullLatLng = [],
+ hull, p, i;
+
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ p = childMarkers[i].getLatLng();
+ points.push(p);
+ }
+
+ hull = L.QuickHull.getConvexHull(points);
+
+ for (i = hull.length - 1; i >= 0; i--) {
+ hullLatLng.push(hull[i][0]);
+ }
+
+ return hullLatLng;
+ }
+});
+
+//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
+//Huge thanks to jawj for implementing it first to make my job easy :-)
+
+L.MarkerCluster.include({
+
+ _2PI: Math.PI * 2,
+ _circleFootSeparation: 25, //related to circumference of circle
+ _circleStartAngle: Math.PI / 6,
+
+ _spiralFootSeparation: 28, //related to size of spiral (experiment!)
+ _spiralLengthStart: 11,
+ _spiralLengthFactor: 5,
+
+ _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards.
+ // 0 -> always spiral; Infinity -> always circle
+
+ spiderfy: function () {
+ if (this._group._spiderfied === this || this._group._inZoomAnimation) {
+ return;
+ }
+
+ var childMarkers = this.getAllChildMarkers(),
+ group = this._group,
+ map = group._map,
+ center = map.latLngToLayerPoint(this._latlng),
+ positions;
+
+ this._group._unspiderfy();
+ this._group._spiderfied = this;
+
+ //TODO Maybe: childMarkers order by distance to center
+
+ if (childMarkers.length >= this._circleSpiralSwitchover) {
+ positions = this._generatePointsSpiral(childMarkers.length, center);
+ } else {
+ center.y += 10; //Otherwise circles look wrong
+ positions = this._generatePointsCircle(childMarkers.length, center);
+ }
+
+ this._animationSpiderfy(childMarkers, positions);
+ },
+
+ unspiderfy: function (zoomDetails) {
+ /// Argument from zoomanim if being called in a zoom animation or null otherwise
+ if (this._group._inZoomAnimation) {
+ return;
+ }
+ this._animationUnspiderfy(zoomDetails);
+
+ this._group._spiderfied = null;
+ },
+
+ _generatePointsCircle: function (count, centerPt) {
+ var circumference = this._circleFootSeparation * (2 + count),
+ legLength = circumference / this._2PI, //radius from circumference
+ angleStep = this._2PI / count,
+ res = [],
+ i, angle;
+
+ res.length = count;
+
+ for (i = count - 1; i >= 0; i--) {
+ angle = this._circleStartAngle + i * angleStep;
+ res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
+ }
+
+ return res;
+ },
+
+ _generatePointsSpiral: function (count, centerPt) {
+ var legLength = this._spiralLengthStart,
+ angle = 0,
+ res = [],
+ i;
+
+ res.length = count;
+
+ for (i = count - 1; i >= 0; i--) {
+ angle += this._spiralFootSeparation / legLength + i * 0.0005;
+ res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
+ legLength += this._2PI * this._spiralLengthFactor / angle;
+ }
+ return res;
+ }
+});
+
+L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
+ //Non Animated versions of everything
+ _animationSpiderfy: function (childMarkers, positions) {
+ var group = this._group,
+ map = group._map,
+ i, m, leg, newPos;
+
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ newPos = map.layerPointToLatLng(positions[i]);
+ m = childMarkers[i];
+
+ m._preSpiderfyLatlng = m._latlng;
+ m.setLatLng(newPos);
+ m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
+
+ L.FeatureGroup.prototype.addLayer.call(group, m);
+
+
+ leg = new L.Polyline([this._latlng, newPos], { weight: 1.5, color: '#222' });
+ map.addLayer(leg);
+ m._spiderLeg = leg;
+ }
+ this.setOpacity(0.3);
+ group.fire('spiderfied');
+ },
+
+ _animationUnspiderfy: function () {
+ var group = this._group,
+ map = group._map,
+ childMarkers = this.getAllChildMarkers(),
+ m, i;
+
+ this.setOpacity(1);
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ m = childMarkers[i];
+
+ L.FeatureGroup.prototype.removeLayer.call(group, m);
+
+ m.setLatLng(m._preSpiderfyLatlng);
+ delete m._preSpiderfyLatlng;
+ m.setZIndexOffset(0);
+
+ map.removeLayer(m._spiderLeg);
+ delete m._spiderLeg;
+ }
+ }
+} : {
+ //Animated versions here
+ _animationSpiderfy: function (childMarkers, positions) {
+ var me = this,
+ group = this._group,
+ map = group._map,
+ thisLayerPos = map.latLngToLayerPoint(this._latlng),
+ i, m, leg, newPos;
+
+ //Add markers to map hidden at our center point
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ m = childMarkers[i];
+
+ m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
+ m.setOpacity(0);
+
+ L.FeatureGroup.prototype.addLayer.call(group, m);
+
+ m._setPos(thisLayerPos);
+ }
+
+ group._forceLayout();
+ group._animationStart();
+
+ var initialLegOpacity = L.Browser.svg ? 0 : 0.3,
+ xmlns = L.Path.SVG_NS;
+
+
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ newPos = map.layerPointToLatLng(positions[i]);
+ m = childMarkers[i];
+
+ //Move marker to new position
+ m._preSpiderfyLatlng = m._latlng;
+ m.setLatLng(newPos);
+ m.setOpacity(1);
+
+
+ //Add Legs.
+ leg = new L.Polyline([me._latlng, newPos], { weight: 1.5, color: '#222', opacity: initialLegOpacity });
+ map.addLayer(leg);
+ m._spiderLeg = leg;
+
+ //Following animations don't work for canvas
+ if (!L.Browser.svg) {
+ continue;
+ }
+
+ //How this works:
+ //http://stackoverflow.com/questions/5924238/how-do-you-animate-an-svg-path-in-ios
+ //http://dev.opera.com/articles/view/advanced-svg-animation-techniques/
+
+ //Animate length
+ var length = leg._path.getTotalLength();
+ leg._path.setAttribute("stroke-dasharray", length + "," + length);
+
+ var anim = document.createElementNS(xmlns, "animate");
+ anim.setAttribute("attributeName", "stroke-dashoffset");
+ anim.setAttribute("begin", "indefinite");
+ anim.setAttribute("from", length);
+ anim.setAttribute("to", 0);
+ anim.setAttribute("dur", 0.25);
+ leg._path.appendChild(anim);
+ anim.beginElement();
+
+ //Animate opacity
+ anim = document.createElementNS(xmlns, "animate");
+ anim.setAttribute("attributeName", "stroke-opacity");
+ anim.setAttribute("attributeName", "stroke-opacity");
+ anim.setAttribute("begin", "indefinite");
+ anim.setAttribute("from", 0);
+ anim.setAttribute("to", 0.5);
+ anim.setAttribute("dur", 0.25);
+ leg._path.appendChild(anim);
+ anim.beginElement();
+ }
+ me.setOpacity(0.3);
+
+ //Set the opacity of the spiderLegs back to their correct value
+ // The animations above override this until they complete.
+ // If the initial opacity of the spiderlegs isn't 0 then they appear before the animation starts.
+ if (L.Browser.svg) {
+ this._group._forceLayout();
+
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ m = childMarkers[i]._spiderLeg;
+
+ m.options.opacity = 0.5;
+ m._path.setAttribute('stroke-opacity', 0.5);
+ }
+ }
+
+ setTimeout(function () {
+ group._animationEnd();
+ group.fire('spiderfied');
+ }, 250);
+ },
+
+ _animationUnspiderfy: function (zoomDetails) {
+ var group = this._group,
+ map = group._map,
+ thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng),
+ childMarkers = this.getAllChildMarkers(),
+ svg = L.Browser.svg,
+ m, i, a;
+
+ group._animationStart();
+
+ //Make us visible and bring the child markers back in
+ this.setOpacity(1);
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ m = childMarkers[i];
+
+ //Fix up the location to the real one
+ m.setLatLng(m._preSpiderfyLatlng);
+ delete m._preSpiderfyLatlng;
+ //Hack override the location to be our center
+ m._setPos(thisLayerPos);
+
+ m.setOpacity(0);
+
+ //Animate the spider legs back in
+ if (svg) {
+ a = m._spiderLeg._path.childNodes[0];
+ a.setAttribute('to', a.getAttribute('from'));
+ a.setAttribute('from', 0);
+ a.beginElement();
+
+ a = m._spiderLeg._path.childNodes[1];
+ a.setAttribute('from', 0.5);
+ a.setAttribute('to', 0);
+ a.setAttribute('stroke-opacity', 0);
+ a.beginElement();
+
+ m._spiderLeg._path.setAttribute('stroke-opacity', 0);
+ }
+ }
+
+ setTimeout(function () {
+ //If we have only <= one child left then that marker will be shown on the map so don't remove it!
+ var stillThereChildCount = 0;
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ m = childMarkers[i];
+ if (m._spiderLeg) {
+ stillThereChildCount++;
+ }
+ }
+
+
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ m = childMarkers[i];
+
+ if (!m._spiderLeg) { //Has already been unspiderfied
+ continue;
+ }
+
+
+ m.setOpacity(1);
+ m.setZIndexOffset(0);
+
+ if (stillThereChildCount > 1) {
+ L.FeatureGroup.prototype.removeLayer.call(group, m);
+ }
+
+ map.removeLayer(m._spiderLeg);
+ delete m._spiderLeg;
+ }
+ group._animationEnd();
+ }, 250);
+ }
+});
+
+
+L.MarkerClusterGroup.include({
+ //The MarkerCluster currently spiderfied (if any)
+ _spiderfied: null,
+
+ _spiderfierOnAdd: function () {
+ this._map.on('click', this._unspiderfyWrapper, this);
+
+ if (this._map.options.zoomAnimation) {
+ this._map.on('zoomstart', this._unspiderfyZoomStart, this);
+ } else {
+ //Browsers without zoomAnimation don't fire zoomstart
+ this._map.on('zoomend', this._unspiderfyWrapper, this);
+ }
+
+ if (L.Browser.svg && !L.Browser.touch) {
+ this._map._initPathRoot();
+ //Needs to happen in the pageload, not after, or animations don't work in webkit
+ // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements
+ //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable
+ }
+ },
+
+ _spiderfierOnRemove: function () {
+ this._map.off('click', this._unspiderfyWrapper, this);
+ this._map.off('zoomstart', this._unspiderfyZoomStart, this);
+ this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
+
+ this._unspiderfy(); //Ensure that markers are back where they should be
+ },
+
+
+ //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)
+ //This means we can define the animation they do rather than Markers doing an animation to their actual location
+ _unspiderfyZoomStart: function () {
+ if (!this._map) { //May have been removed from the map by a zoomEnd handler
+ return;
+ }
+
+ this._map.on('zoomanim', this._unspiderfyZoomAnim, this);
+ },
+ _unspiderfyZoomAnim: function (zoomDetails) {
+ //Wait until the first zoomanim after the user has finished touch-zooming before running the animation
+ if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) {
+ return;
+ }
+
+ this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
+ this._unspiderfy(zoomDetails);
+ },
+
+
+ _unspiderfyWrapper: function () {
+ /// _unspiderfy but passes no arguments
+ this._unspiderfy();
+ },
+
+ _unspiderfy: function (zoomDetails) {
+ if (this._spiderfied) {
+ this._spiderfied.unspiderfy(zoomDetails);
+ }
+ },
+
+ //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc
+ _unspiderfyLayer: function (layer) {
+ if (layer._spiderLeg) {
+ L.FeatureGroup.prototype.removeLayer.call(this, layer);
+
+ layer.setOpacity(1);
+ //Position will be fixed up immediately in _animationUnspiderfy
+ layer.setZIndexOffset(0);
+
+ this._map.removeLayer(layer._spiderLeg);
+ delete layer._spiderLeg;
+ }
+ }
+});
+
+
+
+}(this));
\ No newline at end of file
diff --git a/vendor/leaflet.markercluster/leaflet.markercluster.js b/vendor/leaflet.markercluster/leaflet.markercluster.js
new file mode 100644
index 00000000..24d2b7ab
--- /dev/null
+++ b/vendor/leaflet.markercluster/leaflet.markercluster.js
@@ -0,0 +1,6 @@
+/*
+ Copyright (c) 2012, Smartrak, David Leaver
+ Leaflet.markercluster is an open-source JavaScript library for Marker Clustering on leaflet powered maps.
+ https://github.com/danzel/Leaflet.markercluster
+*/
+(function(e,t){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,skipDuplicateAddTesting:!1,animateAddingMarkers:!1},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),L.FeatureGroup.prototype.initialize.call(this,[]),this._inZoomAnimation=0,this._needsClustering=[],this._currentShownBounds=null},addLayer:function(e){if(e instanceof L.LayerGroup){for(var t in e._layers)e._layers.hasOwnProperty(t)&&this.addLayer(e._layers[t]);return this}this.options.singleMarkerMode&&(e.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[e]}}));if(!this._map)return this._needsClustering.push(e),this;if(!this.options.skipDuplicateAddTesting&&this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom);var n=e,r=this._map.getZoom();if(e.__parent)while(n.__parent._zoom>=r)n=n.__parent;return this.options.animateAddingMarkers?this._animationAddLayer(e,n):this._animationAddLayerNonAnimated(e,n),this},removeLayer:function(e){return e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),e._icon&&L.FeatureGroup.prototype.removeLayer.call(this,e),this):this},clearLayers:function(){if(!this._map)return this._needsClustering=[],this;this._unspiderfy&&this._unspiderfy();for(var e in this._layers)this._layers.hasOwnProperty(e)&&L.FeatureGroup.prototype.removeLayer.call(this,this._layers[e]);return this._generateInitialClusters(),this},hasLayer:function(e){var t=!1;return this._topClusterLevel._recursively(new L.LatLngBounds([e.getLatLng()]),0,this._map.getMaxZoom()+1,function(n){for(var r=n._markers.length-1;r>=0&&!t;r--)n._markers[r]===e&&(t=!0)},null),t},zoomToShowLayer:function(e,t){var n=function(){if((e._icon||e.__parent._icon)&&!this._inZoomAnimation){this._map.off("moveend",n,this),this.off("animationend",n,this);if(e._icon)t();else if(e.__parent._icon){var r=function(){this.off("spiderfied",r,this),t()};this.on("spiderfied",r,this),e.__parent.spiderfy()}}};(e._icon||e.__parent._icon)&&this._map.getBounds().contains(e.__parent._latlng)?n.call(this):(this._map.on("moveend",n,this),this.on("animationend",n,this),e.__parent.zoomToBounds())},onAdd:function(e){L.FeatureGroup.prototype.onAdd.call(this,e),this._gridClusters||this._generateInitialClusters();for(var t=0,n=this._needsClustering.length;t=0;n--)if(e[n]===t){e.splice(n,1);return}},_removeLayer:function(e,t){var n=this._gridClusters,r=this._gridUnclustered,i=this._map;if(t)for(var s=this._maxZoom;s>=0;s--)if(!r[s].removeObject(e,i.project(e.getLatLng(),s)))break;var o=e.__parent,u=o._markers,a;this._arraySplice(u,e);while(o){o._childCount--;if(o._zoom<0)break;t&&o._childCount<=1?(a=o._markers[0]===e?o._markers[1]:o._markers[0],n[o._zoom].removeObject(o,i.project(o._cLatLng,o._zoom)),r[o._zoom].addObject(a,i.project(a.getLatLng(),o._zoom)),this._arraySplice(o.__parent._childClusters,o),o.__parent._markers.push(a),a.__parent=o.__parent,o._icon&&(L.FeatureGroup.prototype.removeLayer.call(this,o),L.FeatureGroup.prototype.addLayer.call(this,a))):(o._recalculateBounds(),o._updateIcon()),o=o.__parent}},_propagateEvent:function(e){e.target instanceof L.MarkerCluster&&(e.type="cluster"+e.type),L.FeatureGroup.prototype._propagateEvent.call(this,e)},_defaultIconCreateFunction:function(e){var t=e.getChildCount(),n=" marker-cluster-";return t<10?n+="small":t<100?n+="medium":n+="large",new L.DivIcon({html:"