'},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)
}
var yfield = self.model.fields.get(field);
- var y = doc.getFieldValueUnrendered(yfield);
+ var y = parseFloat(doc.getFieldValueUnrendered(yfield));
if (self.state.attributes.graphType == 'bars') {
points.push([y, x]);
diff --git a/docs/src/widget.pager.html b/docs/src/widget.pager.html
index 71ab94da..e223a41f 100644
--- a/docs/src/widget.pager.html
+++ b/docs/src/widget.pager.html
@@ -142,7 +142,7 @@ my.Pager = Backbone.View.extend({
<div class="pagination"> \
<ul class="pagination"> \
<li class="prev action-pagination-update"><a href="" class="btn btn-default">«</a></li> \
- <li class="page-range"><a><label for="from">From</label><input name="from" type="text" value="{{from}}" /> – <label for="to">To</label><input name="to" type="text" value="{{to}}" /> </a></li> \
+ <li class="page-range"><a><label for="from">From</label><input id="from" name="from" type="text" value="{{from}}" /> – <label for="to">To</label><input id="to" name="to" type="text" value="{{to}}" /> </a></li> \
<li class="next action-pagination-update"><a href="" class="btn btn-default">»</a></li> \
</ul> \
</div> \
diff --git a/docs/src/widget.queryeditor.html b/docs/src/widget.queryeditor.html
index 37a42d30..3ce92817 100644
--- a/docs/src/widget.queryeditor.html
+++ b/docs/src/widget.queryeditor.html
@@ -145,8 +145,8 @@ my.QueryEditor = Backbone.View.extend({
<div class="input-group-addon"> \
<i class="glyphicon glyphicon-search"></i> \
</div> \
- <label>Search</label> \
- <input class="form-control search-query" type="text" name="q" value="{{q}}" placeholder="Search data ..."> \
+ <label for="q">Search</label> \
+ <input class="form-control search-query" type="text" id="q" name="q" value="{{q}}" placeholder="Search data ..."> \
</div> \
</div> \
<button type="submit" class="btn btn-default">Go »</button> \
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 05dfc922..76df040e 100644
--- a/docs/tutorial-views.markdown
+++ b/docs/tutorial-views.markdown
@@ -230,9 +230,6 @@ library and the Recline Map view:
-
@@ -275,7 +272,7 @@ map.render();
### Creating a Timeline
-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
diff --git a/src/view.flot.js b/src/view.flot.js
index cf75265c..d145c3b5 100644
--- a/src/view.flot.js
+++ b/src/view.flot.js
@@ -343,7 +343,7 @@ my.Flot = Backbone.View.extend({
}
var yfield = self.model.fields.get(field);
- var y = doc.getFieldValueUnrendered(yfield);
+ var y = parseFloat(doc.getFieldValueUnrendered(yfield));
if (self.state.attributes.graphType == 'bars') {
points.push([y, x]);
diff --git a/src/view.slickgrid.js b/src/view.slickgrid.js
index 5e86b4bf..aed90744 100644
--- a/src/view.slickgrid.js
+++ b/src/view.slickgrid.js
@@ -161,7 +161,12 @@ my.SlickGrid = Backbone.View.extend({
}
function sanitizeFieldName(name) {
- var sanitized = $(name).text();
+ var sanitized;
+ try{
+ sanitized = $(name).text();
+ } catch(e){
+ sanitized = '';
+ }
return (name !== sanitized && sanitized !== '') ? sanitized : name;
}
diff --git a/test/base.js b/test/base.js
index eb629e25..93ebd69a 100644
--- a/test/base.js
+++ b/test/base.js
@@ -6,18 +6,19 @@ var Fixture = {
{id: 'x'},
{id: 'y'},
{id: 'z'},
+ {id: 'p'},
{id: 'country'},
{id: 'title'},
{id: 'lat'},
{id: 'lon'}
];
var documents = [
- {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', title: 'first', lat:52.56, lon:13.40},
- {id: 1, date: '2011-02-02', x: 2, y: 4, z: 24, country: 'UK', title: 'second', lat:54.97, lon:-1.60},
- {id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, country: 'US', title: 'third', lat:40.00, lon:-75.5},
- {id: 3, date: '2011-04-04', x: 4, y: 8, z: 6, country: 'UK', title: 'fourth', lat:57.27, lon:-6.20},
- {id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, country: 'UK', title: 'fifth', lat:51.58, lon:0},
- {id: 5, date: '2011-06-02', x: 6, y: 12, z: 18, country: 'DE', title: 'sixth', lat:51.04, lon:7.9}
+ {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, p: '4%', country: 'DE', title: 'first', lat:52.56, lon:13.40},
+ {id: 1, date: '2011-02-02', x: 2, y: 4, z: 24, p: '14%',country: 'UK', title: 'second', lat:54.97, lon:-1.60},
+ {id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, p: '43%', country: 'US', title: 'third', lat:40.00, lon:-75.5},
+ {id: 3, date: '2011-04-04', x: 4, y: 8, z: 6, p: '21%', country: 'UK', title: 'fourth', lat:57.27, lon:-6.20},
+ {id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, p: '29%', country: 'UK', title: 'fifth', lat:51.58, lon:0},
+ {id: 5, date: '2011-06-02', x: 6, y: 12, z: 18, p: '87%', country: 'DE', title: 'sixth', lat:51.04, lon:7.9}
];
var dataset = new recline.Model.Dataset({records: documents, fields: fields});
return dataset;
diff --git a/test/view.flot.test.js b/test/view.flot.test.js
index fb458181..37fa786a 100644
--- a/test/view.flot.test.js
+++ b/test/view.flot.test.js
@@ -55,6 +55,27 @@ test('dates in graph view', function () {
view.remove();
});
+test('percentages in graph view', function () {
+ var dataset = Fixture.getDataset();
+ var view = new recline.View.Flot({
+ model: dataset,
+ state: {
+ 'graphType': 'lines',
+ 'group': 'country',
+ 'series': ['p']
+ }
+ });
+ view.render();
+ $('.fixtures').append(view.el);
+ view.redraw();
+ var graphData = view.plot.getData()[0].data;
+ $.each(graphData, function(i, p){
+ var percentage = p[1];
+ equal($.isNumeric(percentage), true);
+ });
+ view.remove();
+});
+
test('FlotControls basics', function () {
var dataset = Fixture.getDataset();
var view = new recline.View.FlotControls({
diff --git a/test/view.slickgrid.test.js b/test/view.slickgrid.test.js
index 223edf7f..2f79d193 100644
--- a/test/view.slickgrid.test.js
+++ b/test/view.slickgrid.test.js
@@ -27,7 +27,7 @@ test('state', function () {
var view = new recline.View.SlickGrid({
model: dataset,
state: {
- hiddenColumns:['x','lat','title'],
+ hiddenColumns:['x','lat','title','p'],
columnsOrder:['lon','id','z','date', 'y', 'country'],
columnsWidth:[
{column:'id',width: 250}
@@ -71,7 +71,7 @@ test('editable', function () {
var view = new recline.View.SlickGrid({
model: dataset,
state: {
- hiddenColumns:['x','lat','title'],
+ hiddenColumns:['x','lat','title','p'],
columnsOrder:['lon','id','z','date', 'y', 'country'],
columnsWidth:[
{column:'id',width: 250}
@@ -108,7 +108,7 @@ test('delete-row' , function(){
var view = new recline.View.SlickGrid({
model: dataset,
state: {
- hiddenColumns:['x','lat','title'],
+ hiddenColumns:['x','lat','title','p'],
columnsOrder:['lon','id','z','date', 'y', 'country'],
columnsWidth:[
{column:'id',width: 250}
@@ -154,7 +154,7 @@ test('delete-row-with-row-reorder-activated' , function(){
var view = new recline.View.SlickGrid({
model: dataset,
state: {
- hiddenColumns:['x','lat','title'],
+ hiddenColumns:['x','lat','title','p'],
columnsOrder:['lon','id','z','date', 'y', 'country'],
columnsWidth:[
{column:'id',width: 250}
@@ -197,7 +197,7 @@ var dataset = Fixture.getDataset();
var view = new recline.View.SlickGrid({
model: dataset,
state: {
- hiddenColumns:['x','lat','title'],
+ hiddenColumns:['x','lat','title','p'],
columnsOrder:['lon','id','z','date', 'y', 'country'],
columnsWidth:[
{column:'id',width: 250}
@@ -226,7 +226,7 @@ test('update', function() {
var view = new recline.View.SlickGrid({
model: dataset,
state: {
- hiddenColumns:['x','lat','title'],
+ hiddenColumns:['x','lat','title','p'],
columnsOrder:['lon','id','z','date', 'y', 'country'],
columnsWidth:[
{column:'id',width: 250}
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:"