From a7efe19a1532dba65685526be9b3b8e75da236db Mon Sep 17 00:00:00 2001
From: Brandon Amos
Date: Mon, 23 Feb 2015 18:54:15 -0500
Subject: [PATCH 1/7] Fix link to @aliounedia's profile in README.
Found with https://github.com/bamos/girl
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index c295aedc..14551b79 100755
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ See CONTRIBUTING.md.
* [AdriĆ Mercader](http://amercader.net/)
* [Dominik Moritz](https://github.com/domoritz)
* [Friedrich Lindenberg](http://pudo.org/)
-* [Alioune Dia](http://https://github.com/aliounedia)
+* [Alioune Dia](https://github.com/aliounedia)
* [kielni](https://github.com/kielni)
* And [many more](https://github.com/okfn/recline/graphs/contributors)
From bafe84060cd0ec4ccc9c7ad30c65f80c974c86da Mon Sep 17 00:00:00 2001
From: Matt Fullerton
Date: Sat, 21 Mar 2015 10:38:42 +0100
Subject: [PATCH 2/7] [docs/demos] Fixes #400, Typos
---
_includes/views-list.html | 12 +-
docs/tutorial-multiview.markdown | 239 +++++++++++++++++++++++++++++++
docs/tutorial-views.markdown | 8 +-
docs/tutorials.html | 5 +
4 files changed, 254 insertions(+), 10 deletions(-)
create mode 100644 docs/tutorial-multiview.markdown
diff --git a/_includes/views-list.html b/_includes/views-list.html
index 36bb87ca..9cb68d3b 100644
--- a/_includes/views-list.html
+++ b/_includes/views-list.html
@@ -1,7 +1,7 @@
-* Grid (simple)
-* Grid (SlickGrid)
-* Map
+* Grid (simple)
+* Grid (SlickGrid)
+* Map
* Choropleth Map
-* Graph
-* Timeline
-* Multiview (combines views)
\ No newline at end of file
+* Graph
+* Timeline
+* Multiview (combines views)
\ No newline at end of file
diff --git a/docs/tutorial-multiview.markdown b/docs/tutorial-multiview.markdown
new file mode 100644
index 00000000..13df5205
--- /dev/null
+++ b/docs/tutorial-multiview.markdown
@@ -0,0 +1,239 @@
+---
+layout: container
+title: Library - Multiview Tutorial
+recline-deps: true
+root: ../
+---
+
+
+
+ Multiview Tutorial
+
+ This tutorial will quickly get you started with Recline Multiview
+
+
+
+
+The full source code along with all dependencies for the tutorial can be found at this GitHub repository. Do not try to assemble a working example from the code snippets in this page! See it in action via GitHub Pages.
+ The code is almost identical to that used for the site demo, with the advantage of the code being separated out of the main recline website. You can also see Multiview in action in many CKAN-based data catalogs (resource views) and in the OKFN Labs DataDeck
+
+### Multiview
+
+Multiview, as its name suggests, combines multiple [Recline views](views.html) into one visualization. The different views are synced to one dataset. The [technical documentation for Multiview](src/view.multiview.html) details the nuts and bolts.
+
+When building a Multiview from scratch, it is advised to start by getting each individual view to work satisfactorily before combining into a Multiview, to aid debugging.
+
+### Preparing your page
+
+Before writing any code with Recline, you need to do the following preparation steps on your page:
+
+* [Download ReclineJS]({{page.root}}download.html) (downloading the master, developer code is recommended as this example is based on that and all dependencies should be available) or the all-in-one demo code.
+* Include the Multiview CSS as well as the CSS for each view in the head section of your document, as well as any 3rd party CSS for each view e.g.:
+ {% highlight html %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endhighlight %}
+
+* Include the relevant Javascript files somewhere on the page (preferably before body close tag). You will need to include any necessary Javascript dependencies for each view as well, e.g.:
+ {% highlight html %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endhighlight %}
+
+### Creating a Dataset
+
+Here's the function to create an example dataset we are going to work with:
+
+{% highlight javascript %}
+function createDemoDataset() {
+ var dataset = new recline.Model.Dataset({
+ records: [
+ {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', sometext: 'first', lat:52.56, lon:13.40},
+ {id: 1, date: '2011-02-02', x: 2, y: 4, z: 24, country: 'UK', sometext: 'second', lat:54.97, lon:-1.60},
+ {id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, country: 'US', sometext: 'third', lat:40.00, lon:-75.5},
+ {id: 3, date: '2011-04-04', x: 4, y: 8, z: 6, country: 'UK', sometext: 'fourth', lat:57.27, lon:-6.20},
+ {id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, country: 'UK', sometext: 'fifth', lat:51.58, lon:0},
+ {id: 5, date: '2011-06-02', x: 6, y: 12, z: 18, country: 'DE', sometext: 'sixth', lat:51.04, lon:7.9}
+ ],
+ // let's be really explicit about fields
+ // Plus take opportunity to set date to be a date field and set some labels
+ fields: [
+ {id: 'id'},
+ {id: 'date', type: 'date'},
+ {id: 'x', type: 'number'},
+ {id: 'y', type: 'number'},
+ {id: 'z', type: 'number'},
+ {id: 'country', 'label': 'Country'},
+ {id: 'sometext', 'label': 'Some text'},
+ {id: 'lat'},
+ {id: 'lon'}
+ ]
+ });
+ return dataset;
+}
+{% endhighlight %}
+
+In this data we have 6 documents / rows. Each document is a javascript object
+containing keys and values (note that all values here are 'simple' but there is
+no reason you cannot have objects as values allowing you to nest data.
+
+### Setting up the Multiview
+
+To create a Multiview, we first create each view that we want to include, and include these in an array. A function to do everything for SlickGrid, Graph and Map views is shown below:
+
+{% highlight javascript %}
+var createMultiView = function(dataset, state) {
+ // remove existing multiview if present
+ var reload = false;
+ if (window.multiView) {
+ window.multiView.remove();
+ window.multiView = null;
+ reload = true;
+ }
+
+ var $el = $('');
+ $el.appendTo(window.explorerDiv);
+
+ // customize the subviews for the MultiView
+ var views = [
+ {
+ id: 'grid',
+ label: 'Grid',
+ view: new recline.View.SlickGrid({
+ model: dataset,
+ state: {
+ gridOptions: {
+ editable: true,
+ // Enable support for row add
+ enabledAddRow: true,
+ // Enable support for row delete
+ enabledDelRow: true,
+ // Enable support for row ReOrder
+ enableReOrderRow:true,
+ autoEdit: false,
+ enableCellNavigation: true
+ },
+ columnsEditor: [
+ { column: 'date', editor: Slick.Editors.Date },
+ { column: 'sometext', editor: Slick.Editors.Text }
+ ]
+ }
+ })
+ },
+ {
+ id: 'graph',
+ label: 'Graph',
+ view: new recline.View.Graph({
+ model: dataset
+
+ })
+ },
+ {
+ id: 'map',
+ label: 'Map',
+ view: new recline.View.Map({
+ model: dataset
+ })
+ }
+ ];
+
+ var multiView = new recline.View.MultiView({
+ model: dataset,
+ el: $el,
+ state: state,
+ views: views
+ });
+ return multiView;
+}
+{% endhighlight %}
+
+To tie it all together:
+{% highlight javascript %}
+jQuery(function($) {
+ window.multiView = null;
+ window.explorerDiv = $('.data-explorer-here');
+
+ // create the demo dataset
+ var dataset = createDemoDataset();
+ // now create the multiview
+ // this is rather more elaborate than the minimum as we configure the
+ // MultiView in various ways (see function below)
+ window.multiview = createMultiView(dataset);
+
+ // last, we'll demonstrate binding to changes in the dataset
+ // this will print out a summary of each change onto the page in the
+ // changelog section
+ dataset.records.bind('all', function(name, obj) {
+ var $info = $('');
+ $info.html(name + ': ' + JSON.stringify(obj.toJSON()));
+ $('.changelog').append($info);
+ $('.changelog').show();
+ });
+});
+{% endhighlight %}
+
+The HTML is very simple:
+{% highlight html %}
+
+
+
+
+
Changes
+
+
+
+
+
+{% endhighlight %}
diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown
index 8f850c30..05dfc922 100644
--- a/docs/tutorial-views.markdown
+++ b/docs/tutorial-views.markdown
@@ -21,15 +21,15 @@ Views display Recline datasets in different ways. This page covers the interesti
Before writing any code with Recline, you need to do the following preparation steps on your page:
-1. [Download ReclineJS]({{page.root}}download.html) and relevant dependencies.
-2. Include the relevant CSS in the head section of your document:
+* [Download ReclineJS]({{page.root}}download.html) and relevant dependencies.
+* Include the relevant CSS in the head section of your document:
{% highlight html %}
{% endhighlight %}
-3. Include the relevant Javascript files somewhere on the page (preferably before body close tag):
+* Include the relevant Javascript files somewhere on the page (preferably before body close tag):
{% highlight html %}
@@ -50,7 +50,7 @@ You're now ready to start working with Recline.
### Creating a Dataset
-Here's some example data We are going to work with:
+Here's some example data we are going to work with:
{% highlight javascript %}
{% include data.js %}
diff --git a/docs/tutorials.html b/docs/tutorials.html
index 6ef1e832..35dae7df 100644
--- a/docs/tutorials.html
+++ b/docs/tutorials.html
@@ -53,6 +53,11 @@ root: ../
'},events:{"click .js-remove-filter":"onRemoveFilter","click .js-add-filter":"onAddFilterShow","submit form.js-edit":"onTermFiltersUpdate","submit form.js-add":"onAddFilter"},initialize:function(){_.bindAll(this,"render");this.listenTo(this.model.fields,"all",this.render);this.listenTo(this.model.queryState,"change change:filters:new-blank",this.render);this.render()},render:function(){var self=this;var tmplData=$.extend(true,{},this.model.queryState.toJSON());tmplData.filters=_.map(tmplData.filters,function(filter,idx){filter.id=idx;return filter});tmplData.fields=this.model.fields.toJSON();tmplData.filterRender=function(){return Mustache.render(self.filterTemplates.term,this)};var out=Mustache.render(this.template,tmplData);this.$el.html(out)},updateFilter:function(input){var self=this;var filters=self.model.queryState.get("filters");var $input=$(input);var filterIndex=parseInt($input.attr("data-filter-id"),10);var value=$input.val();filters[filterIndex].term=value},onAddFilterShow:function(e){e.preventDefault();var $target=$(e.target);$target.hide();this.$el.find("form.js-add").show()},onAddFilter:function(e){e.preventDefault();var $target=$(e.target);$target.hide();var field=$target.find("select.fields").val();this.model.queryState.addFilter({type:"term",field:field})},onRemoveFilter:function(e){e.preventDefault();var $target=$(e.target);var filterId=$target.attr("data-filter-id");this.model.queryState.removeFilter(filterId)},onTermFiltersUpdate:function(e){var self=this;e.preventDefault();var filters=self.model.queryState.get("filters");var $form=$(e.target);_.each($form.find("input"),function(input){self.updateFilter(input)});self.model.queryState.set({filters:filters,from:0});self.model.queryState.trigger("change")}})})(jQuery,recline.View);
\ No newline at end of file
diff --git a/docs/src/view.flot.html b/docs/src/view.flot.html
index 30e442fc..a8b69629 100644
--- a/docs/src/view.flot.html
+++ b/docs/src/view.flot.html
@@ -622,7 +622,7 @@ could have a lot of values and so we limit to max 15 (we assume)
-The source code along with all dependencies for the map part of the tutorial can be found at this GitHub repository. See it in action via GitHub Pages.
+The source code along with all dependencies for the timeline part of the tutorial can be found at this GitHub repository. See it in action via GitHub Pages.
Now, let's create a timeline for this dataset using the date information which is
From b1e768320528376e3575f19f20791f54fe2c213b Mon Sep 17 00:00:00 2001
From: Matt Fullerton
Date: Sat, 2 May 2015 20:24:22 +0200
Subject: [PATCH 7/7] Upgrade leaflet.markercluster
---
_includes/recline-deps.html | 3 -
docs/tutorial-multiview.markdown | 3 -
docs/tutorial-views.markdown | 3 -
.../MarkerCluster.Default.css | 22 +
.../MarkerCluster.Default.ie.css | 22 -
.../leaflet.markercluster/MarkerCluster.css | 8 +-
.../leaflet.markercluster-src.js | 1054 ++++++++++++-----
.../leaflet.markercluster.js | 8 +-
8 files changed, 805 insertions(+), 318 deletions(-)
delete mode 100644 vendor/leaflet.markercluster/MarkerCluster.Default.ie.css
diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html
index 2b8cd735..0c16f808 100644
--- a/_includes/recline-deps.html
+++ b/_includes/recline-deps.html
@@ -4,9 +4,6 @@
-
diff --git a/docs/tutorial-multiview.markdown b/docs/tutorial-multiview.markdown
index 13df5205..3b069793 100644
--- a/docs/tutorial-multiview.markdown
+++ b/docs/tutorial-multiview.markdown
@@ -40,9 +40,6 @@ Before writing any code with Recline, you need to do the following preparation s
-
diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown
index e5f28ac1..76df040e 100644
--- a/docs/tutorial-views.markdown
+++ b/docs/tutorial-views.markdown
@@ -230,9 +230,6 @@ library and the Recline Map view:
-
diff --git a/vendor/leaflet.markercluster/MarkerCluster.Default.css b/vendor/leaflet.markercluster/MarkerCluster.Default.css
index 90558dd6..bbc8c9fb 100644
--- a/vendor/leaflet.markercluster/MarkerCluster.Default.css
+++ b/vendor/leaflet.markercluster/MarkerCluster.Default.css
@@ -19,6 +19,28 @@
background-color: rgba(241, 128, 23, 0.6);
}
+ /* IE 6-8 fallback colors */
+.leaflet-oldie .marker-cluster-small {
+ background-color: rgb(181, 226, 140);
+ }
+.leaflet-oldie .marker-cluster-small div {
+ background-color: rgb(110, 204, 57);
+ }
+
+.leaflet-oldie .marker-cluster-medium {
+ background-color: rgb(241, 211, 87);
+ }
+.leaflet-oldie .marker-cluster-medium div {
+ background-color: rgb(240, 194, 12);
+ }
+
+.leaflet-oldie .marker-cluster-large {
+ background-color: rgb(253, 156, 115);
+ }
+.leaflet-oldie .marker-cluster-large div {
+ background-color: rgb(241, 128, 23);
+}
+
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
diff --git a/vendor/leaflet.markercluster/MarkerCluster.Default.ie.css b/vendor/leaflet.markercluster/MarkerCluster.Default.ie.css
deleted file mode 100644
index 1d0de51d..00000000
--- a/vendor/leaflet.markercluster/MarkerCluster.Default.ie.css
+++ /dev/null
@@ -1,22 +0,0 @@
- /* 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
index a915c1a4..00b0eddb 100644
--- a/vendor/leaflet.markercluster/MarkerCluster.css
+++ b/vendor/leaflet.markercluster/MarkerCluster.css
@@ -1,6 +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;
+ -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
+ -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
+ -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
+ transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
diff --git a/vendor/leaflet.markercluster/leaflet.markercluster-src.js b/vendor/leaflet.markercluster/leaflet.markercluster-src.js
index e2959410..40433162 100644
--- a/vendor/leaflet.markercluster/leaflet.markercluster-src.js
+++ b/vendor/leaflet.markercluster/leaflet.markercluster-src.js
@@ -1,12 +1,9 @@
/*
- 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
+ Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.
+ https://github.com/Leaflet/Leaflet.markercluster
+ (c) 2012-2013, Dave Leaver, smartrak
*/
-(function (window, undefined) {
-
-
-/*
+(function (window, document, undefined) {/*
* L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
*/
@@ -23,11 +20,25 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
disableClusteringAtZoom: null,
- skipDuplicateAddTesting: false,
+ // Setting this to false prevents the removal of any clusters outside of the viewpoint, which
+ // is the default behaviour for performance reasons.
+ removeOutsideVisibleBounds: true,
//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
+ animateAddingMarkers: false,
+
+ //Increase to increase the distance away that spiderfied markers appear from the center
+ spiderfyDistanceMultiplier: 1,
+
+ // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts
+ chunkedLoading: false,
+ chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback)
+ chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser
+ chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator)
+
+ //Options to pass to the L.Polygon constructor
+ polygonOptions: {}
},
initialize: function (options) {
@@ -36,34 +47,35 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
this.options.iconCreateFunction = this._defaultIconCreateFunction;
}
- L.FeatureGroup.prototype.initialize.call(this, []);
+ this._featureGroup = L.featureGroup();
+ this._featureGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
+
+ this._nonPointGroup = L.featureGroup();
+ this._nonPointGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
this._inZoomAnimation = 0;
this._needsClustering = [];
+ this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of
//The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
this._currentShownBounds = null;
+
+ this._queue = [];
},
addLayer: function (layer) {
if (layer instanceof L.LayerGroup) {
+ var array = [];
for (var i in layer._layers) {
- if (layer._layers.hasOwnProperty(i)) {
- this.addLayer(layer._layers[i]);
- }
+ array.push(layer._layers[i]);
}
- return this;
+ return this.addLayers(array);
}
- if (this.options.singleMarkerMode) {
- layer.options.icon = this.options.iconCreateFunction({
- getChildCount: function () {
- return 1;
- },
- getAllChildMarkers: function () {
- return [layer];
- }
- });
+ //Don't cluster non point data
+ if (!layer.getLatLng) {
+ this._nonPointGroup.addLayer(layer);
+ return this;
}
if (!this._map) {
@@ -71,10 +83,11 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
return this;
}
- if (!this.options.skipDuplicateAddTesting && this.hasLayer(layer)) {
+ if (this.hasLayer(layer)) {
return this;
}
+
//If we have already clustered we'll need to add this one to a cluster
if (this._unspiderfy) {
@@ -92,15 +105,40 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
}
}
- if (this.options.animateAddingMarkers) {
- this._animationAddLayer(layer, visibleLayer);
- } else {
- this._animationAddLayerNonAnimated(layer, visibleLayer);
+ if (this._currentShownBounds.contains(visibleLayer.getLatLng())) {
+ if (this.options.animateAddingMarkers) {
+ this._animationAddLayer(layer, visibleLayer);
+ } else {
+ this._animationAddLayerNonAnimated(layer, visibleLayer);
+ }
}
return this;
},
removeLayer: function (layer) {
+
+ if (layer instanceof L.LayerGroup)
+ {
+ var array = [];
+ for (var i in layer._layers) {
+ array.push(layer._layers[i]);
+ }
+ return this.removeLayers(array);
+ }
+
+ //Non point layers
+ if (!layer.getLatLng) {
+ this._nonPointGroup.removeLayer(layer);
+ return this;
+ }
+
+ if (!this._map) {
+ if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) {
+ this._needsRemoving.push(layer);
+ }
+ return this;
+ }
+
if (!layer.__parent) {
return this;
}
@@ -113,52 +151,261 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
//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]);
+ if (this._featureGroup.hasLayer(layer)) {
+ this._featureGroup.removeLayer(layer);
+ if (layer.setOpacity) {
+ layer.setOpacity(1);
}
}
- //Reset _topClusterLevel and the DistanceGrids
- this._generateInitialClusters();
+ return this;
+ },
+
+ //Takes an array of markers and adds them in bulk
+ addLayers: function (layersArray) {
+ var fg = this._featureGroup,
+ npg = this._nonPointGroup,
+ chunked = this.options.chunkedLoading,
+ chunkInterval = this.options.chunkInterval,
+ chunkProgress = this.options.chunkProgress,
+ newMarkers, i, l, m;
+
+ if (this._map) {
+ var offset = 0,
+ started = (new Date()).getTime();
+ var process = L.bind(function () {
+ var start = (new Date()).getTime();
+ for (; offset < layersArray.length; offset++) {
+ if (chunked && offset % 200 === 0) {
+ // every couple hundred markers, instrument the time elapsed since processing started:
+ var elapsed = (new Date()).getTime() - start;
+ if (elapsed > chunkInterval) {
+ break; // been working too hard, time to take a break :-)
+ }
+ }
+
+ m = layersArray[offset];
+
+ //Not point data, can't be clustered
+ if (!m.getLatLng) {
+ npg.addLayer(m);
+ continue;
+ }
+
+ if (this.hasLayer(m)) {
+ continue;
+ }
+
+ this._addLayer(m, this._maxZoom);
+
+ //If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will
+ if (m.__parent) {
+ if (m.__parent.getChildCount() === 2) {
+ var markers = m.__parent.getAllChildMarkers(),
+ otherMarker = markers[0] === m ? markers[1] : markers[0];
+ fg.removeLayer(otherMarker);
+ }
+ }
+ }
+
+ if (chunkProgress) {
+ // report progress and time elapsed:
+ chunkProgress(offset, layersArray.length, (new Date()).getTime() - started);
+ }
+
+ if (offset === layersArray.length) {
+ //Update the icons of all those visible clusters that were affected
+ this._featureGroup.eachLayer(function (c) {
+ if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) {
+ c._updateIcon();
+ }
+ });
+
+ this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
+ } else {
+ setTimeout(process, this.options.chunkDelay);
+ }
+ }, this);
+
+ process();
+ } else {
+ newMarkers = [];
+ for (i = 0, l = layersArray.length; i < l; i++) {
+ m = layersArray[i];
+
+ //Not point data, can't be clustered
+ if (!m.getLatLng) {
+ npg.addLayer(m);
+ continue;
+ }
+
+ if (this.hasLayer(m)) {
+ continue;
+ }
+
+ newMarkers.push(m);
+ }
+ this._needsClustering = this._needsClustering.concat(newMarkers);
+ }
+ return this;
+ },
+
+ //Takes an array of markers and removes them in bulk
+ removeLayers: function (layersArray) {
+ var i, l, m,
+ fg = this._featureGroup,
+ npg = this._nonPointGroup;
+
+ if (!this._map) {
+ for (i = 0, l = layersArray.length; i < l; i++) {
+ m = layersArray[i];
+ this._arraySplice(this._needsClustering, m);
+ npg.removeLayer(m);
+ }
+ return this;
+ }
+
+ for (i = 0, l = layersArray.length; i < l; i++) {
+ m = layersArray[i];
+
+ if (!m.__parent) {
+ npg.removeLayer(m);
+ continue;
+ }
+
+ this._removeLayer(m, true, true);
+
+ if (fg.hasLayer(m)) {
+ fg.removeLayer(m);
+ if (m.setOpacity) {
+ m.setOpacity(1);
+ }
+ }
+ }
+
+ //Fix up the clusters and markers on the map
+ this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
+
+ fg.eachLayer(function (c) {
+ if (c instanceof L.MarkerCluster) {
+ c._updateIcon();
+ }
+ });
return this;
},
- hasLayer: function (layer) {
- var res = false;
+ //Removes all layers from the MarkerClusterGroup
+ clearLayers: function () {
+ //Need our own special implementation as the LayerGroup one doesn't work for us
- 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;
+ //If we aren't on the map (yet), blow away the markers we know of
+ if (!this._map) {
+ this._needsClustering = [];
+ delete this._gridClusters;
+ delete this._gridUnclustered;
+ }
+
+ if (this._noanimationUnspiderfy) {
+ this._noanimationUnspiderfy();
+ }
+
+ //Remove all the visible layers
+ this._featureGroup.clearLayers();
+ this._nonPointGroup.clearLayers();
+
+ this.eachLayer(function (marker) {
+ delete marker.__parent;
+ });
+
+ if (this._map) {
+ //Reset _topClusterLevel and the DistanceGrids
+ this._generateInitialClusters();
+ }
+
+ return this;
},
+ //Override FeatureGroup.getBounds as it doesn't work
+ getBounds: function () {
+ var bounds = new L.LatLngBounds();
+
+ if (this._topClusterLevel) {
+ bounds.extend(this._topClusterLevel._bounds);
+ }
+
+ for (var i = this._needsClustering.length - 1; i >= 0; i--) {
+ bounds.extend(this._needsClustering[i].getLatLng());
+ }
+
+ bounds.extend(this._nonPointGroup.getBounds());
+
+ return bounds;
+ },
+
+ //Overrides LayerGroup.eachLayer
+ eachLayer: function (method, context) {
+ var markers = this._needsClustering.slice(),
+ i;
+
+ if (this._topClusterLevel) {
+ this._topClusterLevel.getAllChildMarkers(markers);
+ }
+
+ for (i = markers.length - 1; i >= 0; i--) {
+ method.call(context, markers[i]);
+ }
+
+ this._nonPointGroup.eachLayer(method, context);
+ },
+
+ //Overrides LayerGroup.getLayers
+ getLayers: function () {
+ var layers = [];
+ this.eachLayer(function (l) {
+ layers.push(l);
+ });
+ return layers;
+ },
+
+ //Overrides LayerGroup.getLayer, WARNING: Really bad performance
+ getLayer: function (id) {
+ var result = null;
+
+ this.eachLayer(function (l) {
+ if (L.stamp(l) === id) {
+ result = l;
+ }
+ });
+
+ return result;
+ },
+
+ //Returns true if the given layer is in this MarkerClusterGroup
+ hasLayer: function (layer) {
+ if (!layer) {
+ return false;
+ }
+
+ var i, anArray = this._needsClustering;
+
+ for (i = anArray.length - 1; i >= 0; i--) {
+ if (anArray[i] === layer) {
+ return true;
+ }
+ }
+
+ anArray = this._needsRemoving;
+ for (i = anArray.length - 1; i >= 0; i--) {
+ if (anArray[i] === layer) {
+ return false;
+ }
+ }
+
+ return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer);
+ },
+
+ //Zoom down to show the given layer (spiderfying if necessary) then calls the callback
zoomToShowLayer: function (layer, callback) {
var showMarker = function () {
@@ -180,28 +427,56 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
}
};
- if ((layer._icon || layer.__parent._icon) && this._map.getBounds().contains(layer.__parent._latlng)) {
- //Layer or cluster is already visible
- showMarker.call(this);
+ if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) {
+ //Layer is visible ond on screen, immediate return
+ callback();
+ } else if (layer.__parent._zoom < this._map.getZoom()) {
+ //Layer should be visible at this zoom level. It must not be on screen so just pan over to it
+ this._map.on('moveend', showMarker, this);
+ this._map.panTo(layer.getLatLng());
} else {
+ var moveStart = function () {
+ this._map.off('movestart', moveStart, this);
+ moveStart = null;
+ };
+
+ this._map.on('movestart', moveStart, this);
this._map.on('moveend', showMarker, this);
this.on('animationend', showMarker, this);
layer.__parent.zoomToBounds();
+
+ if (moveStart) {
+ //Never started moving, must already be there, probably need clustering however
+ showMarker.call(this);
+ }
}
},
//Overrides FeatureGroup.onAdd
onAdd: function (map) {
- L.FeatureGroup.prototype.onAdd.call(this, map);
+ this._map = map;
+ var i, l, layer;
+
+ if (!isFinite(this._map.getMaxZoom())) {
+ throw "Map has no maxZoom specified";
+ }
+
+ this._featureGroup.onAdd(map);
+ this._nonPointGroup.onAdd(map);
if (!this._gridClusters) {
this._generateInitialClusters();
}
- for (var i = 0, l = this._needsClustering.length; i < l; i++) {
- this._addLayer(this._needsClustering[i], this._maxZoom);
+ for (i = 0, l = this._needsRemoving.length; i < l; i++) {
+ layer = this._needsRemoving[i];
+ this._removeLayer(layer, true);
}
- this._needsClustering = [];
+ this._needsRemoving = [];
+
+ //Remember the current zoom level and bounds
+ this._zoom = this._map.getZoom();
+ this._currentShownBounds = this._getExpandedVisibleBounds();
this._map.on('zoomend', this._zoomEnd, this);
this._map.on('moveend', this._moveEnd, this);
@@ -212,21 +487,18 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
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);
+ l = this._needsClustering;
+ this._needsClustering = [];
+ this.addLayers(l);
},
//Overrides FeatureGroup.onRemove
onRemove: function (map) {
- this._map.off('zoomend', this._zoomEnd, this);
- this._map.off('moveend', this._moveEnd, this);
+ map.off('zoomend', this._zoomEnd, this);
+ map.off('moveend', this._moveEnd, this);
+
+ this._unbindEvents();
//In case we are in a cluster animation
this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
@@ -235,23 +507,42 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
this._spiderfierOnRemove();
}
- L.FeatureGroup.prototype.onRemove.call(this, map);
+
+
+ //Clean up all the layers we added to the map
+ this._hideCoverage();
+ this._featureGroup.onRemove(map);
+ this._nonPointGroup.onRemove(map);
+
+ this._featureGroup.clearLayers();
+
+ this._map = null;
},
+ getVisibleParent: function (marker) {
+ var vMarker = marker;
+ while (vMarker && !vMarker._icon) {
+ vMarker = vMarker.__parent;
+ }
+ return vMarker || null;
+ },
//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;
+ return true;
}
}
},
- _removeLayer: function (marker, removeFromDistanceGrid) {
+ //Internal function for removing a marker from everything.
+ //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
+ _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) {
var gridClusters = this._gridClusters,
gridUnclustered = this._gridUnclustered,
+ fg = this._featureGroup,
map = this._map;
//Remove the marker from distance clusters it might be in
@@ -292,24 +583,44 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
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);
+ fg.removeLayer(cluster);
+ if (!dontUpdateMap) {
+ fg.addLayer(otherMarker);
+ }
}
} else {
cluster._recalculateBounds();
- cluster._updateIcon();
+ if (!dontUpdateMap || !cluster._icon) {
+ cluster._updateIcon();
+ }
}
cluster = cluster.__parent;
}
+
+ delete marker.__parent;
+ },
+
+ _isOrIsParent: function (el, oel) {
+ while (oel) {
+ if (el === oel) {
+ return true;
+ }
+ oel = oel.parentNode;
+ }
+ return false;
},
- //Overrides FeatureGroup._propagateEvent
_propagateEvent: function (e) {
- if (e.target instanceof L.MarkerCluster) {
+ if (e.layer instanceof L.MarkerCluster) {
+ //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget)
+ if (e.originalEvent && this._isOrIsParent(e.layer._icon, e.originalEvent.relatedTarget)) {
+ return;
+ }
e.type = 'cluster' + e.type;
}
- L.FeatureGroup.prototype._propagateEvent.call(this, e);
+
+ this.fire(e.type, e);
},
//Default functionality
@@ -329,58 +640,74 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
},
_bindEvents: function () {
- var shownPolygon = null,
- map = this._map,
-
- spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
- showCoverageOnHover = this.options.showCoverageOnHover,
- zoomToBoundsOnClick = this.options.zoomToBoundsOnClick;
+ var 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);
+ this.on('clusterclick', this._zoomOrSpiderfy, 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);
+ this.on('clustermouseover', this._showCoverage, this);
+ this.on('clustermouseout', this._hideCoverage, this);
+ map.on('zoomend', this._hideCoverage, this);
+ }
+ },
+
+ _zoomOrSpiderfy: function (e) {
+ var map = this._map;
+ if (map.getMaxZoom() === map.getZoom()) {
+ if (this.options.spiderfyOnMaxZoom) {
+ e.layer.spiderfy();
+ }
+ } else if (this.options.zoomToBoundsOnClick) {
+ e.layer.zoomToBounds();
+ }
+
+ // Focus the map again for keyboard users.
+ if (e.originalEvent && e.originalEvent.keyCode === 13) {
+ map._container.focus();
+ }
+ },
+
+ _showCoverage: function (e) {
+ var map = this._map;
+ if (this._inZoomAnimation) {
+ return;
+ }
+ if (this._shownPolygon) {
+ map.removeLayer(this._shownPolygon);
+ }
+ if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) {
+ this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions);
+ map.addLayer(this._shownPolygon);
+ }
+ },
+
+ _hideCoverage: function () {
+ if (this._shownPolygon) {
+ this._map.removeLayer(this._shownPolygon);
+ this._shownPolygon = null;
+ }
+ },
+
+ _unbindEvents: function () {
+ var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
+ showCoverageOnHover = this.options.showCoverageOnHover,
+ zoomToBoundsOnClick = this.options.zoomToBoundsOnClick,
+ map = this._map;
+
+ if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
+ this.off('clusterclick', this._zoomOrSpiderfy, this);
+ }
+ if (showCoverageOnHover) {
+ this.off('clustermouseover', this._showCoverage, this);
+ this.off('clustermouseout', this._hideCoverage, this);
+ map.off('zoomend', this._hideCoverage, this);
}
},
@@ -402,7 +729,7 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
var newBounds = this._getExpandedVisibleBounds();
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds);
- this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, newBounds);
+ this._topClusterLevel._recursivelyAddChildrenToMap(null, this._map._zoom, newBounds);
this._currentShownBounds = newBounds;
return;
@@ -410,7 +737,15 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
_generateInitialClusters: function () {
var maxZoom = this._map.getMaxZoom(),
- radius = this.options.maxClusterRadius;
+ radius = this.options.maxClusterRadius,
+ radiusFn = radius;
+
+ //If we just set maxClusterRadius to a single number, we need to create
+ //a simple function to return that number. Otherwise, we just have to
+ //use the function we've passed in.
+ if (typeof radius !== "function") {
+ radiusFn = function () { return radius; };
+ }
if (this.options.disableClusteringAtZoom) {
maxZoom = this.options.disableClusteringAtZoom - 1;
@@ -418,11 +753,11 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
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._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom));
+ this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom));
}
this._topClusterLevel = new L.MarkerCluster(this, -1);
@@ -434,6 +769,17 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
gridUnclustered = this._gridUnclustered,
markerPoint, z;
+ if (this.options.singleMarkerMode) {
+ layer.options.icon = this.options.iconCreateFunction({
+ getChildCount: function () {
+ return 1;
+ },
+ getAllChildMarkers: function () {
+ return [layer];
+ }
+ });
+ }
+
//Find the lowest zoom level to slot this one in
for (; zoom >= 0; zoom--) {
markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position
@@ -449,10 +795,10 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
//Try find a marker close by to form a new cluster with
closest = gridUnclustered[zoom].getNearObject(markerPoint);
if (closest) {
- if (closest.__parent) {
+ var parent = closest.__parent;
+ if (parent) {
this._removeLayer(closest, false);
}
- var parent = closest.__parent || this._topClusterLevel;
//Create new cluster with these 2 in it
@@ -478,17 +824,40 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
return;
}
-
+
//Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
gridUnclustered[zoom].addObject(layer, markerPoint);
}
+ //Didn't get in anything, add us to the top
+ this._topClusterLevel._addChild(layer);
+ layer.__parent = this._topClusterLevel;
return;
},
+ //Enqueue code to fire after the marker expand/contract has happened
+ _enqueue: function (fn) {
+ this._queue.push(fn);
+ if (!this._queueTimeout) {
+ this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300);
+ }
+ },
+ _processQueue: function () {
+ for (var i = 0; i < this._queue.length; i++) {
+ this._queue[i].call(this);
+ }
+ this._queue.length = 0;
+ clearTimeout(this._queueTimeout);
+ this._queueTimeout = null;
+ },
+
//Merge and split any existing clusters that are too big or small
_mergeSplitClusters: function () {
- if (this._zoom < this._map._zoom) { //Zoom in, split
+
+ //Incase we are starting to split before the animation finished
+ this._processQueue();
+
+ if (this._zoom < this._map._zoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split
this._animationStart();
//Remove clusters now off screen
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds());
@@ -503,29 +872,35 @@ L.MarkerClusterGroup = L.FeatureGroup.extend({
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));
+ if (!this.options.removeOutsideVisibleBounds) {
+ return this._map.getBounds();
+ }
- return new L.LatLngBounds(sw, ne);
+ var map = this._map,
+ bounds = map.getBounds(),
+ sw = bounds._southWest,
+ ne = bounds._northEast,
+ latDiff = L.Browser.mobile ? 0 : Math.abs(sw.lat - ne.lat),
+ lngDiff = L.Browser.mobile ? 0 : Math.abs(sw.lng - ne.lng);
+
+ return new L.LatLngBounds(
+ new L.LatLng(sw.lat - latDiff, sw.lng - lngDiff, true),
+ new L.LatLng(ne.lat + latDiff, ne.lng + lngDiff, true));
},
//Shared animation code
_animationAddLayerNonAnimated: function (layer, newCluster) {
if (newCluster === layer) {
- L.FeatureGroup.prototype.addLayer.call(this, layer);
+ this._featureGroup.addLayer(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]);
+ this._featureGroup.removeLayer(markers[0]);
+ this._featureGroup.removeLayer(markers[1]);
} else {
newCluster._updateIcon();
}
@@ -541,10 +916,16 @@ L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
_animationZoomIn: function (previousZoomLevel, newZoomLevel) {
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
+
+ //We didn't actually animate, but we use this event to mean "clustering animations have finished"
+ this.fire('animationend');
},
_animationZoomOut: function (previousZoomLevel, newZoomLevel) {
this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
+
+ //We didn't actually animate, but we use this event to mean "clustering animations have finished"
+ this.fire('animationend');
},
_animationAddLayer: function (layer, newCluster) {
this._animationAddLayerNonAnimated(layer, newCluster);
@@ -564,8 +945,8 @@ L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
this.fire('animationend');
},
_animationZoomIn: function (previousZoomLevel, newZoomLevel) {
- var me = this,
- bounds = this._getExpandedVisibleBounds(),
+ var bounds = this._getExpandedVisibleBounds(),
+ fg = this._featureGroup,
i;
//Add all children of current clusters to map and remove those clusters from map
@@ -574,8 +955,12 @@ L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
markers = c._markers,
m;
+ if (!bounds.contains(startPos)) {
+ startPos = null;
+ }
+
if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us
- L.FeatureGroup.prototype.removeLayer.call(me, c);
+ fg.removeLayer(c);
c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);
} else {
//Fade out old cluster
@@ -588,77 +973,85 @@ L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
for (i = markers.length - 1; i >= 0; i--) {
m = markers[i];
if (!bounds.contains(m._latlng)) {
- L.FeatureGroup.prototype.removeLayer.call(me, m);
+ fg.removeLayer(m);
}
}
});
this._forceLayout();
- var j, n;
//Update opacities
- me._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel);
+ this._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);
- }
+ fg.eachLayer(function (n) {
+ 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) {
+ this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {
c._recursivelyRestoreChildPositions(newZoomLevel);
});
//Remove the old clusters and close the zoom animation
-
- setTimeout(function () {
+ this._enqueue(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);
+ this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
+ fg.removeLayer(c);
+ c.setOpacity(1);
});
- me._animationEnd();
- }, 250);
+ this._animationEnd();
+ });
},
_animationZoomOut: function (previousZoomLevel, newZoomLevel) {
- this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel, newZoomLevel);
+ this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel);
//Need to add markers for those that weren't on the map before but are now
this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
+ //Remove markers that were on the map before but won't be now
+ this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel, this._getExpandedVisibleBounds());
},
- _animationZoomOutSingle: function (marker, previousZoomLevel, newZoomLevel) {
+ _animationZoomOutSingle: function (cluster, 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);
+ cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel);
var me = this;
//Update the opacity (If we immediately set it they won't animate)
this._forceLayout();
- marker._recursivelyBecomeVisible(bounds, newZoomLevel);
+ cluster._recursivelyBecomeVisible(bounds, newZoomLevel);
//TODO: Maybe use the transition timing stuff to make this more reliable
//When the animations are done, tidy up
- setTimeout(function () {
+ this._enqueue(function () {
- marker._recursively(bounds, newZoomLevel, 0, function (c) {
- c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel);
- });
+ //This cluster stopped being a cluster before the timeout fired
+ if (cluster._childCount === 1) {
+ var m = cluster._markers[0];
+ //If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it
+ m.setLatLng(m.getLatLng());
+ if (m.setOpacity) {
+ m.setOpacity(1);
+ }
+ } else {
+ cluster._recursively(bounds, newZoomLevel, 0, function (c) {
+ c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1);
+ });
+ }
me._animationEnd();
- }, 250);
+ });
},
_animationAddLayer: function (layer, newCluster) {
- var me = this;
+ var me = this,
+ fg = this._featureGroup;
- L.FeatureGroup.prototype.addLayer.call(this, layer);
+ fg.addLayer(layer);
if (newCluster !== layer) {
if (newCluster._childCount > 2) { //Was already a cluster
@@ -669,12 +1062,12 @@ L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));
layer.setOpacity(0);
- setTimeout(function () {
- L.FeatureGroup.prototype.removeLayer.call(me, layer);
+ this._enqueue(function () {
+ fg.removeLayer(layer);
layer.setOpacity(1);
me._animationEnd();
- }, 250);
+ });
} else { //Just became a cluster
this._forceLayout();
@@ -695,6 +1088,10 @@ L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
}
});
+L.markerClusterGroup = function (options) {
+ return new L.MarkerClusterGroup(options);
+};
+
L.MarkerCluster = L.Marker.extend({
initialize: function (group, zoom, a, b) {
@@ -740,11 +1137,39 @@ L.MarkerCluster = L.Marker.extend({
return this._childCount;
},
- //Zoom to the extents of this cluster
+ //Zoom to the minimum of showing all of the child markers, or the extents of this cluster
zoomToBounds: function () {
- this._group._map.fitBounds(this._bounds);
+ var childClusters = this._childClusters.slice(),
+ map = this._group._map,
+ boundsZoom = map.getBoundsZoom(this._bounds),
+ zoom = this._zoom + 1,
+ mapZoom = map.getZoom(),
+ i;
+
+ //calculate how far we need to zoom down to see all of the markers
+ while (childClusters.length > 0 && boundsZoom > zoom) {
+ zoom++;
+ var newClusters = [];
+ for (i = 0; i < childClusters.length; i++) {
+ newClusters = newClusters.concat(childClusters[i]._childClusters);
+ }
+ childClusters = newClusters;
+ }
+
+ if (boundsZoom > zoom) {
+ this._group._map.setView(this._latlng, zoom);
+ } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead
+ this._group._map.setView(this._latlng, mapZoom + 1);
+ } else {
+ this._group._map.fitBounds(this._bounds);
+ }
},
+ getBounds: function () {
+ var bounds = new L.LatLngBounds();
+ bounds.extend(this._bounds);
+ return bounds;
+ },
_updateIcon: function () {
this._iconNeedsUpdate = true;
@@ -825,11 +1250,11 @@ L.MarkerCluster = L.Marker.extend({
this._backupLatlng = this._latlng;
this.setLatLng(startPos);
}
- L.FeatureGroup.prototype.addLayer.call(this._group, this);
+ this._group._featureGroup.addLayer(this);
},
-
- _recursivelyAnimateChildrenIn: function (bounds, center, depth) {
- this._recursively(bounds, 0, depth - 1,
+
+ _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {
+ this._recursively(bounds, 0, maxZoom - 1,
function (c) {
var markers = c._markers,
i, m;
@@ -883,7 +1308,7 @@ L.MarkerCluster = L.Marker.extend({
},
_recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {
- this._recursively(bounds, 0, zoomLevel,
+ this._recursively(bounds, -1, zoomLevel,
function (c) {
if (zoomLevel === c._zoom) {
return;
@@ -901,10 +1326,12 @@ L.MarkerCluster = L.Marker.extend({
nm._backupLatlng = nm.getLatLng();
nm.setLatLng(startPos);
- nm.setOpacity(0);
+ if (nm.setOpacity) {
+ nm.setOpacity(0);
+ }
}
- L.FeatureGroup.prototype.addLayer.call(c._group, nm);
+ c._group._featureGroup.addLayer(nm);
}
},
function (c) {
@@ -951,8 +1378,10 @@ L.MarkerCluster = L.Marker.extend({
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);
+ c._group._featureGroup.removeLayer(m);
+ if (m.setOpacity) {
+ m.setOpacity(1);
+ }
}
}
},
@@ -961,8 +1390,10 @@ L.MarkerCluster = L.Marker.extend({
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);
+ c._group._featureGroup.removeLayer(m);
+ if (m.setOpacity) {
+ m.setOpacity(1);
+ }
}
}
}
@@ -1092,20 +1523,16 @@ L.DistanceGrid.prototype = {
grid = this._grid;
for (i in grid) {
- if (grid.hasOwnProperty(i)) {
- row = grid[i];
+ row = grid[i];
- for (j in row) {
- if (row.hasOwnProperty(j)) {
- cell = row[j];
+ for (j in row) {
+ cell = row[j];
- for (k = 0, len = cell.length; k < len; k++) {
- removed = fn.call(context, cell[k]);
- if (removed) {
- k--;
- len--;
- }
- }
+ for (k = 0, len = cell.length; k < len; k++) {
+ removed = fn.call(context, cell[k]);
+ if (removed) {
+ k--;
+ len--;
}
}
}
@@ -1183,13 +1610,26 @@ Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=1843
(function () {
L.QuickHull = {
+
+ /*
+ * @param {Object} cpt a point to be measured from the baseline
+ * @param {Array} bl the baseline, as represented by a two-element
+ * array of latlng objects.
+ * @returns {Number} an approximate distance measure
+ */
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));
},
-
+ /*
+ * @param {Array} baseLine a two-element array of latlng objects
+ * representing the baseline to project from
+ * @param {Array} latLngs an array of latlng objects
+ * @returns {Object} the maximum point and all new points to stay
+ * in consideration for the hull.
+ */
findMostDistantPointFromBaseLine: function (baseLine, latLngs) {
var maxD = 0,
maxPt = null,
@@ -1210,11 +1650,19 @@ Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=1843
maxD = d;
maxPt = pt;
}
-
}
- return { 'maxPoint': maxPt, 'newPoints': newPoints };
+
+ return { maxPoint: maxPt, newPoints: newPoints };
},
+
+ /*
+ * Given a baseline, compute the convex hull of latLngs as an array
+ * of latLngs.
+ *
+ * @param {Array} latLngs
+ * @returns {Array}
+ */
buildConvexHull: function (baseLine, latLngs) {
var convexHullBaseLines = [],
t = this.findMostDistantPointFromBaseLine(baseLine, latLngs);
@@ -1230,12 +1678,19 @@ Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=1843
);
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];
+ return [baseLine[0]];
}
},
+ /*
+ * Given an array of latlngs, compute a convex hull as an array
+ * of latlngs
+ *
+ * @param {Array} latLngs
+ * @returns {Array}
+ */
getConvexHull: function (latLngs) {
- //find first baseline
+ // find first baseline
var maxLat = false, minLat = false,
maxPt = null, minPt = null,
i;
@@ -1262,24 +1717,18 @@ L.MarkerCluster.include({
getConvexHull: function () {
var childMarkers = this.getAllChildMarkers(),
points = [],
- hullLatLng = [],
- hull, p, i;
+ 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;
+ return L.QuickHull.getConvexHull(points);
}
});
+
//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 :-)
@@ -1333,7 +1782,7 @@ L.MarkerCluster.include({
},
_generatePointsCircle: function (count, centerPt) {
- var circumference = this._circleFootSeparation * (2 + count),
+ var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),
legLength = circumference / this._2PI, //radius from circumference
angleStep = this._2PI / count,
res = [],
@@ -1350,7 +1799,9 @@ L.MarkerCluster.include({
},
_generatePointsSpiral: function (count, centerPt) {
- var legLength = this._spiralLengthStart,
+ var legLength = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthStart,
+ separation = this._group.options.spiderfyDistanceMultiplier * this._spiralFootSeparation,
+ lengthFactor = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthFactor,
angle = 0,
res = [],
i;
@@ -1358,11 +1809,41 @@ L.MarkerCluster.include({
res.length = count;
for (i = count - 1; i >= 0; i--) {
- angle += this._spiralFootSeparation / legLength + i * 0.0005;
+ angle += separation / legLength + i * 0.0005;
res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
- legLength += this._2PI * this._spiralLengthFactor / angle;
+ legLength += this._2PI * lengthFactor / angle;
}
return res;
+ },
+
+ _noanimationUnspiderfy: function () {
+ var group = this._group,
+ map = group._map,
+ fg = group._featureGroup,
+ childMarkers = this.getAllChildMarkers(),
+ m, i;
+
+ this.setOpacity(1);
+ for (i = childMarkers.length - 1; i >= 0; i--) {
+ m = childMarkers[i];
+
+ fg.removeLayer(m);
+
+ if (m._preSpiderfyLatlng) {
+ m.setLatLng(m._preSpiderfyLatlng);
+ delete m._preSpiderfyLatlng;
+ }
+ if (m.setZIndexOffset) {
+ m.setZIndexOffset(0);
+ }
+
+ if (m._spiderLeg) {
+ map.removeLayer(m._spiderLeg);
+ delete m._spiderLeg;
+ }
+ }
+
+ group._spiderfied = null;
}
});
@@ -1371,6 +1852,7 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
_animationSpiderfy: function (childMarkers, positions) {
var group = this._group,
map = group._map,
+ fg = group._featureGroup,
i, m, leg, newPos;
for (i = childMarkers.length - 1; i >= 0; i--) {
@@ -1379,9 +1861,11 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
m._preSpiderfyLatlng = m._latlng;
m.setLatLng(newPos);
- m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
+ if (m.setZIndexOffset) {
+ m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
+ }
- L.FeatureGroup.prototype.addLayer.call(group, m);
+ fg.addLayer(m);
leg = new L.Polyline([this._latlng, newPos], { weight: 1.5, color: '#222' });
@@ -1393,31 +1877,19 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
},
_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;
- }
+ this._noanimationUnspiderfy();
}
} : {
//Animated versions here
+ SVG_ANIMATION: (function () {
+ return document.createElementNS('http://www.w3.org/2000/svg', 'animate').toString().indexOf('SVGAnimate') > -1;
+ }()),
+
_animationSpiderfy: function (childMarkers, positions) {
var me = this,
group = this._group,
map = group._map,
+ fg = group._featureGroup,
thisLayerPos = map.latLngToLayerPoint(this._latlng),
i, m, leg, newPos;
@@ -1425,18 +1897,24 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
for (i = childMarkers.length - 1; i >= 0; i--) {
m = childMarkers[i];
- m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
- m.setOpacity(0);
+ //If it is a marker, add it now and we'll animate it out
+ if (m.setOpacity) {
+ m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
+ m.setOpacity(0);
+
+ fg.addLayer(m);
- L.FeatureGroup.prototype.addLayer.call(group, m);
-
- m._setPos(thisLayerPos);
+ m._setPos(thisLayerPos);
+ } else {
+ //Vectors just get immediately added
+ fg.addLayer(m);
+ }
}
group._forceLayout();
group._animationStart();
- var initialLegOpacity = L.Browser.svg ? 0 : 0.3,
+ var initialLegOpacity = L.Path.SVG ? 0 : 0.3,
xmlns = L.Path.SVG_NS;
@@ -1447,7 +1925,10 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
//Move marker to new position
m._preSpiderfyLatlng = m._latlng;
m.setLatLng(newPos);
- m.setOpacity(1);
+
+ if (m.setOpacity) {
+ m.setOpacity(1);
+ }
//Add Legs.
@@ -1456,7 +1937,7 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
m._spiderLeg = leg;
//Following animations don't work for canvas
- if (!L.Browser.svg) {
+ if (!L.Path.SVG || !this.SVG_ANIMATION) {
continue;
}
@@ -1493,7 +1974,7 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
//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) {
+ if (L.Path.SVG) {
this._group._forceLayout();
for (i = childMarkers.length - 1; i >= 0; i--) {
@@ -1507,31 +1988,40 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
setTimeout(function () {
group._animationEnd();
group.fire('spiderfied');
- }, 250);
+ }, 200);
},
_animationUnspiderfy: function (zoomDetails) {
var group = this._group,
map = group._map,
+ fg = group._featureGroup,
thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng),
childMarkers = this.getAllChildMarkers(),
- svg = L.Browser.svg,
+ svg = L.Path.SVG && this.SVG_ANIMATION,
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];
+ //Marker was added to us after we were spidified
+ if (!m._preSpiderfyLatlng) {
+ continue;
+ }
+
//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);
+ if (m.setOpacity) {
+ m._setPos(thisLayerPos);
+ m.setOpacity(0);
+ } else {
+ fg.removeLayer(m);
+ }
//Animate the spider legs back in
if (svg) {
@@ -1569,18 +2059,20 @@ L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
}
- m.setOpacity(1);
- m.setZIndexOffset(0);
+ if (m.setOpacity) {
+ m.setOpacity(1);
+ m.setZIndexOffset(0);
+ }
if (stillThereChildCount > 1) {
- L.FeatureGroup.prototype.removeLayer.call(group, m);
+ fg.removeLayer(m);
}
map.removeLayer(m._spiderLeg);
delete m._spiderLeg;
}
group._animationEnd();
- }, 250);
+ }, 200);
}
});
@@ -1594,12 +2086,11 @@ L.MarkerClusterGroup.include({
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);
}
+ //Browsers without zoomAnimation or a big zoom don't fire zoomstart
+ this._map.on('zoomend', this._noanimationUnspiderfy, this);
- if (L.Browser.svg && !L.Browser.touch) {
+ if (L.Path.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
@@ -1647,10 +2138,16 @@ L.MarkerClusterGroup.include({
}
},
+ _noanimationUnspiderfy: function () {
+ if (this._spiderfied) {
+ this._spiderfied._noanimationUnspiderfy();
+ }
+ },
+
//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);
+ this._featureGroup.removeLayer(layer);
layer.setOpacity(1);
//Position will be fixed up immediately in _animationUnspiderfy
@@ -1663,5 +2160,4 @@ L.MarkerClusterGroup.include({
});
-
-}(this));
\ No newline at end of file
+}(window, document));
\ No newline at end of file
diff --git a/vendor/leaflet.markercluster/leaflet.markercluster.js b/vendor/leaflet.markercluster/leaflet.markercluster.js
index 24d2b7ab..e7532116 100644
--- a/vendor/leaflet.markercluster/leaflet.markercluster.js
+++ b/vendor/leaflet.markercluster/leaflet.markercluster.js
@@ -1,6 +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
+ Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.
+ https://github.com/Leaflet/Leaflet.markercluster
+ (c) 2012-2013, Dave Leaver, smartrak
*/
-(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:"