source: extensions/piwigo-openstreetmap/leaflet/leaflet.markercluster-src.js @ 30089

Last change on this file since 30089 was 24677, checked in by ddtddt, 11 years ago

[extensions] - piwigo-openstreetmap - add file for translate

File size: 53.6 KB
Line 
1/*
2 Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.
3 https://github.com/Leaflet/Leaflet.markercluster
4 (c) 2012-2013, Dave Leaver, smartrak
5*/
6(function (window, document, undefined) {
7/*
8 * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within
9 */
10
11L.MarkerClusterGroup = L.FeatureGroup.extend({
12
13        options: {
14                maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center
15                iconCreateFunction: null,
16
17                spiderfyOnMaxZoom: true,
18                showCoverageOnHover: true,
19                zoomToBoundsOnClick: true,
20                singleMarkerMode: false,
21
22                disableClusteringAtZoom: null,
23
24                // Setting this to false prevents the removal of any clusters outside of the viewpoint, which
25                // is the default behaviour for performance reasons.
26                removeOutsideVisibleBounds: true,
27
28                //Whether to animate adding markers after adding the MarkerClusterGroup to the map
29                // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
30                animateAddingMarkers: false,
31
32                //Increase to increase the distance away that spiderfied markers appear from the center
33                spiderfyDistanceMultiplier: 1,
34
35                //Options to pass to the L.Polygon constructor
36                polygonOptions: {}
37        },
38
39        initialize: function (options) {
40                L.Util.setOptions(this, options);
41                if (!this.options.iconCreateFunction) {
42                        this.options.iconCreateFunction = this._defaultIconCreateFunction;
43                }
44
45                this._featureGroup = L.featureGroup();
46                this._featureGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
47
48                this._nonPointGroup = L.featureGroup();
49                this._nonPointGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
50
51                this._inZoomAnimation = 0;
52                this._needsClustering = [];
53                this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of
54                //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
55                this._currentShownBounds = null;
56        },
57
58        addLayer: function (layer) {
59
60                if (layer instanceof L.LayerGroup) {
61                        var array = [];
62                        for (var i in layer._layers) {
63                                array.push(layer._layers[i]);
64                        }
65                        return this.addLayers(array);
66                }
67
68                //Don't cluster non point data
69                if (!layer.getLatLng) {
70                        this._nonPointGroup.addLayer(layer);
71                        return this;
72                }
73
74                if (!this._map) {
75                        this._needsClustering.push(layer);
76                        return this;
77                }
78
79                if (this.hasLayer(layer)) {
80                        return this;
81                }
82
83
84                //If we have already clustered we'll need to add this one to a cluster
85
86                if (this._unspiderfy) {
87                        this._unspiderfy();
88                }
89
90                this._addLayer(layer, this._maxZoom);
91
92                //Work out what is visible
93                var visibleLayer = layer,
94                        currentZoom = this._map.getZoom();
95                if (layer.__parent) {
96                        while (visibleLayer.__parent._zoom >= currentZoom) {
97                                visibleLayer = visibleLayer.__parent;
98                        }
99                }
100
101                if (this._currentShownBounds.contains(visibleLayer.getLatLng())) {
102                        if (this.options.animateAddingMarkers) {
103                                this._animationAddLayer(layer, visibleLayer);
104                        } else {
105                                this._animationAddLayerNonAnimated(layer, visibleLayer);
106                        }
107                }
108                return this;
109        },
110
111        removeLayer: function (layer) {
112
113                //Non point layers
114                if (!layer.getLatLng) {
115                        this._nonPointGroup.removeLayer(layer);
116                        return this;
117                }
118
119                if (!this._map) {
120                        if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) {
121                                this._needsRemoving.push(layer);
122                        }
123                        return this;
124                }
125
126                if (!layer.__parent) {
127                        return this;
128                }
129
130                if (this._unspiderfy) {
131                        this._unspiderfy();
132                        this._unspiderfyLayer(layer);
133                }
134
135                //Remove the marker from clusters
136                this._removeLayer(layer, true);
137
138                if (this._featureGroup.hasLayer(layer)) {
139                        this._featureGroup.removeLayer(layer);
140                        if (layer.setOpacity) {
141                                layer.setOpacity(1);
142                        }
143                }
144
145                return this;
146        },
147
148        //Takes an array of markers and adds them in bulk
149        addLayers: function (layersArray) {
150                var i, l, m,
151                        onMap = this._map,
152                        fg = this._featureGroup,
153                        npg = this._nonPointGroup;
154
155                for (i = 0, l = layersArray.length; i < l; i++) {
156                        m = layersArray[i];
157
158                        //Not point data, can't be clustered
159                        if (!m.getLatLng) {
160                                npg.addLayer(m);
161                                continue;
162                        }
163
164                        if (this.hasLayer(m)) {
165                                continue;
166                        }
167
168                        if (!onMap) {
169                                this._needsClustering.push(m);
170                                continue;
171                        }
172
173                        this._addLayer(m, this._maxZoom);
174
175                        //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
176                        if (m.__parent) {
177                                if (m.__parent.getChildCount() === 2) {
178                                        var markers = m.__parent.getAllChildMarkers(),
179                                                otherMarker = markers[0] === m ? markers[1] : markers[0];
180                                        fg.removeLayer(otherMarker);
181                                }
182                        }
183                }
184
185                if (onMap) {
186                        //Update the icons of all those visible clusters that were affected
187                        fg.eachLayer(function (c) {
188                                if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) {
189                                        c._updateIcon();
190                                }
191                        });
192
193                        this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
194                }
195
196                return this;
197        },
198
199        //Takes an array of markers and removes them in bulk
200        removeLayers: function (layersArray) {
201                var i, l, m,
202                        fg = this._featureGroup,
203                        npg = this._nonPointGroup;
204
205                if (!this._map) {
206                        for (i = 0, l = layersArray.length; i < l; i++) {
207                                m = layersArray[i];
208                                this._arraySplice(this._needsClustering, m);
209                                npg.removeLayer(m);
210                        }
211                        return this;
212                }
213
214                for (i = 0, l = layersArray.length; i < l; i++) {
215                        m = layersArray[i];
216
217                        if (!m.__parent) {
218                                npg.removeLayer(m);
219                                continue;
220                        }
221
222                        this._removeLayer(m, true, true);
223
224                        if (fg.hasLayer(m)) {
225                                fg.removeLayer(m);
226                                if (m.setOpacity) {
227                                        m.setOpacity(1);
228                                }
229                        }
230                }
231
232                //Fix up the clusters and markers on the map
233                this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
234
235                fg.eachLayer(function (c) {
236                        if (c instanceof L.MarkerCluster) {
237                                c._updateIcon();
238                        }
239                });
240
241                return this;
242        },
243
244        //Removes all layers from the MarkerClusterGroup
245        clearLayers: function () {
246                //Need our own special implementation as the LayerGroup one doesn't work for us
247
248                //If we aren't on the map (yet), blow away the markers we know of
249                if (!this._map) {
250                        this._needsClustering = [];
251                        delete this._gridClusters;
252                        delete this._gridUnclustered;
253                }
254
255                if (this._noanimationUnspiderfy) {
256                        this._noanimationUnspiderfy();
257                }
258
259                //Remove all the visible layers
260                this._featureGroup.clearLayers();
261                this._nonPointGroup.clearLayers();
262
263                this.eachLayer(function (marker) {
264                        delete marker.__parent;
265                });
266
267                if (this._map) {
268                        //Reset _topClusterLevel and the DistanceGrids
269                        this._generateInitialClusters();
270                }
271
272                return this;
273        },
274
275        //Override FeatureGroup.getBounds as it doesn't work
276        getBounds: function () {
277                var bounds = new L.LatLngBounds();
278                if (this._topClusterLevel) {
279                        bounds.extend(this._topClusterLevel._bounds);
280                } else {
281                        for (var i = this._needsClustering.length - 1; i >= 0; i--) {
282                                bounds.extend(this._needsClustering[i].getLatLng());
283                        }
284                }
285
286                //TODO: Can remove this isValid test when leaflet 0.6 is released
287                var nonPointBounds = this._nonPointGroup.getBounds();
288                if (nonPointBounds.isValid()) {
289                        bounds.extend(nonPointBounds);
290                }
291
292                return bounds;
293        },
294
295        //Overrides LayerGroup.eachLayer
296        eachLayer: function (method, context) {
297                var markers = this._needsClustering.slice(),
298                    i;
299
300                if (this._topClusterLevel) {
301                        this._topClusterLevel.getAllChildMarkers(markers);
302                }
303
304                for (i = markers.length - 1; i >= 0; i--) {
305                        method.call(context, markers[i]);
306                }
307
308                this._nonPointGroup.eachLayer(method, context);
309        },
310
311        //Returns true if the given layer is in this MarkerClusterGroup
312        hasLayer: function (layer) {
313                if (!layer) {
314                        return false;
315                }
316
317                var i, anArray = this._needsClustering;
318
319                for (i = anArray.length - 1; i >= 0; i--) {
320                        if (anArray[i] === layer) {
321                                return true;
322                        }
323                }
324
325                anArray = this._needsRemoving;
326                for (i = anArray.length - 1; i >= 0; i--) {
327                        if (anArray[i] === layer) {
328                                return false;
329                        }
330                }
331
332                return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer);
333        },
334
335        //Zoom down to show the given layer (spiderfying if necessary) then calls the callback
336        zoomToShowLayer: function (layer, callback) {
337
338                var showMarker = function () {
339                        if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) {
340                                this._map.off('moveend', showMarker, this);
341                                this.off('animationend', showMarker, this);
342
343                                if (layer._icon) {
344                                        callback();
345                                } else if (layer.__parent._icon) {
346                                        var afterSpiderfy = function () {
347                                                this.off('spiderfied', afterSpiderfy, this);
348                                                callback();
349                                        };
350
351                                        this.on('spiderfied', afterSpiderfy, this);
352                                        layer.__parent.spiderfy();
353                                }
354                        }
355                };
356
357                if (layer._icon) {
358                        callback();
359                } else if (layer.__parent._zoom < this._map.getZoom()) {
360                        //Layer should be visible now but isn't on screen, just pan over to it
361                        this._map.on('moveend', showMarker, this);
362                        if (!layer._icon) {
363                                this._map.panTo(layer.getLatLng());
364                        }
365                } else {
366                        this._map.on('moveend', showMarker, this);
367                        this.on('animationend', showMarker, this);
368                        this._map.setView(layer.getLatLng(), layer.__parent._zoom + 1);
369                        layer.__parent.zoomToBounds();
370                }
371        },
372
373        //Overrides FeatureGroup.onAdd
374        onAdd: function (map) {
375                this._map = map;
376                var i, l, layer;
377
378                this._featureGroup.onAdd(map);
379                this._nonPointGroup.onAdd(map);
380
381                if (!this._gridClusters) {
382                        this._generateInitialClusters();
383                }
384
385                for (i = 0, l = this._needsRemoving.length; i < l; i++) {
386                        layer = this._needsRemoving[i];
387                        this._removeLayer(layer);
388                }
389                this._needsRemoving = [];
390
391                for (i = 0, l = this._needsClustering.length; i < l; i++) {
392                        layer = this._needsClustering[i];
393
394                        //If the layer doesn't have a getLatLng then we can't cluster it, so add it to our child featureGroup
395                        if (!layer.getLatLng) {
396                                this._featureGroup.addLayer(layer);
397                                continue;
398                        }
399
400
401                        if (layer.__parent) {
402                                continue;
403                        }
404                        this._addLayer(layer, this._maxZoom);
405                }
406                this._needsClustering = [];
407
408
409                this._map.on('zoomend', this._zoomEnd, this);
410                this._map.on('moveend', this._moveEnd, this);
411
412                if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
413                        this._spiderfierOnAdd();
414                }
415
416                this._bindEvents();
417
418
419                //Actually add our markers to the map:
420
421                //Remember the current zoom level and bounds
422                this._zoom = this._map.getZoom();
423                this._currentShownBounds = this._getExpandedVisibleBounds();
424
425                //Make things appear on the map
426                this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds);
427        },
428
429        //Overrides FeatureGroup.onRemove
430        onRemove: function (map) {
431                map.off('zoomend', this._zoomEnd, this);
432                map.off('moveend', this._moveEnd, this);
433
434                this._unbindEvents();
435
436                //In case we are in a cluster animation
437                this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
438
439                if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
440                        this._spiderfierOnRemove();
441                }
442
443                //Clean up all the layers we added to the map
444                this._featureGroup.onRemove(map);
445                this._nonPointGroup.onRemove(map);
446
447                this._map = null;
448        },
449
450        getVisibleParent: function (marker) {
451                var vMarker = marker;
452                while (vMarker !== null && !vMarker._icon) {
453                        vMarker = vMarker.__parent;
454                }
455                return vMarker;
456        },
457
458        //Remove the given object from the given array
459        _arraySplice: function (anArray, obj) {
460                for (var i = anArray.length - 1; i >= 0; i--) {
461                        if (anArray[i] === obj) {
462                                anArray.splice(i, 1);
463                                return true;
464                        }
465                }
466        },
467
468        //Internal function for removing a marker from everything.
469        //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
470        _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) {
471                var gridClusters = this._gridClusters,
472                        gridUnclustered = this._gridUnclustered,
473                        fg = this._featureGroup,
474                        map = this._map;
475
476                //Remove the marker from distance clusters it might be in
477                if (removeFromDistanceGrid) {
478                        for (var z = this._maxZoom; z >= 0; z--) {
479                                if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) {
480                                        break;
481                                }
482                        }
483                }
484
485                //Work our way up the clusters removing them as we go if required
486                var cluster = marker.__parent,
487                        markers = cluster._markers,
488                        otherMarker;
489
490                //Remove the marker from the immediate parents marker list
491                this._arraySplice(markers, marker);
492
493                while (cluster) {
494                        cluster._childCount--;
495
496                        if (cluster._zoom < 0) {
497                                //Top level, do nothing
498                                break;
499                        } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required
500                                //We need to push the other marker up to the parent
501                                otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0];
502
503                                //Update distance grid
504                                gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom));
505                                gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom));
506
507                                //Move otherMarker up to parent
508                                this._arraySplice(cluster.__parent._childClusters, cluster);
509                                cluster.__parent._markers.push(otherMarker);
510                                otherMarker.__parent = cluster.__parent;
511
512                                if (cluster._icon) {
513                                        //Cluster is currently on the map, need to put the marker on the map instead
514                                        fg.removeLayer(cluster);
515                                        if (!dontUpdateMap) {
516                                                fg.addLayer(otherMarker);
517                                        }
518                                }
519                        } else {
520                                cluster._recalculateBounds();
521                                if (!dontUpdateMap || !cluster._icon) {
522                                        cluster._updateIcon();
523                                }
524                        }
525
526                        cluster = cluster.__parent;
527                }
528
529                delete marker.__parent;
530        },
531
532        _propagateEvent: function (e) {
533                if (e.layer instanceof L.MarkerCluster) {
534                        e.type = 'cluster' + e.type;
535                }
536
537                this.fire(e.type, e);
538        },
539
540        //Default functionality
541        _defaultIconCreateFunction: function (cluster) {
542                var childCount = cluster.getChildCount();
543
544                var c = ' marker-cluster-';
545                if (childCount < 10) {
546                        c += 'small';
547                } else if (childCount < 100) {
548                        c += 'medium';
549                } else {
550                        c += 'large';
551                }
552
553                return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
554        },
555
556        _bindEvents: function () {
557                var map = this._map,
558                    spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
559                    showCoverageOnHover = this.options.showCoverageOnHover,
560                    zoomToBoundsOnClick = this.options.zoomToBoundsOnClick;
561
562                //Zoom on cluster click or spiderfy if we are at the lowest level
563                if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
564                        this.on('clusterclick', this._zoomOrSpiderfy, this);
565                }
566
567                //Show convex hull (boundary) polygon on mouse over
568                if (showCoverageOnHover) {
569                        this.on('clustermouseover', this._showCoverage, this);
570                        this.on('clustermouseout', this._hideCoverage, this);
571                        map.on('zoomend', this._hideCoverage, this);
572                        map.on('layerremove', this._hideCoverageOnRemove, this);
573                }
574        },
575
576        _zoomOrSpiderfy: function (e) {
577                var map = this._map;
578                if (map.getMaxZoom() === map.getZoom()) {
579                        if (this.options.spiderfyOnMaxZoom) {
580                                e.layer.spiderfy();
581                        }
582                } else if (this.options.zoomToBoundsOnClick) {
583                        e.layer.zoomToBounds();
584                }
585        },
586
587        _showCoverage: function (e) {
588                var map = this._map;
589                if (this._inZoomAnimation) {
590                        return;
591                }
592                if (this._shownPolygon) {
593                        map.removeLayer(this._shownPolygon);
594                }
595                if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) {
596                        this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions);
597                        map.addLayer(this._shownPolygon);
598                }
599        },
600
601        _hideCoverage: function () {
602                if (this._shownPolygon) {
603                        this._map.removeLayer(this._shownPolygon);
604                        this._shownPolygon = null;
605                }
606        },
607
608        _hideCoverageOnRemove: function (e) {
609                if (e.layer === this) {
610                        this._hideCoverage();
611                }
612        },
613
614        _unbindEvents: function () {
615                var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom,
616                        showCoverageOnHover = this.options.showCoverageOnHover,
617                        zoomToBoundsOnClick = this.options.zoomToBoundsOnClick,
618                        map = this._map;
619
620                if (spiderfyOnMaxZoom || zoomToBoundsOnClick) {
621                        this.off('clusterclick', this._zoomOrSpiderfy, this);
622                }
623                if (showCoverageOnHover) {
624                        this.off('clustermouseover', this._showCoverage, this);
625                        this.off('clustermouseout', this._hideCoverage, this);
626                        map.off('zoomend', this._hideCoverage, this);
627                        map.off('layerremove', this._hideCoverageOnRemove, this);
628                }
629        },
630
631        _zoomEnd: function () {
632                if (!this._map) { //May have been removed from the map by a zoomEnd handler
633                        return;
634                }
635                this._mergeSplitClusters();
636
637                this._zoom = this._map._zoom;
638                this._currentShownBounds = this._getExpandedVisibleBounds();
639        },
640
641        _moveEnd: function () {
642                if (this._inZoomAnimation) {
643                        return;
644                }
645
646                var newBounds = this._getExpandedVisibleBounds();
647
648                this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds);
649                this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, newBounds);
650
651                this._currentShownBounds = newBounds;
652                return;
653        },
654
655        _generateInitialClusters: function () {
656                var maxZoom = this._map.getMaxZoom(),
657                        radius = this.options.maxClusterRadius;
658
659                if (this.options.disableClusteringAtZoom) {
660                        maxZoom = this.options.disableClusteringAtZoom - 1;
661                }
662                this._maxZoom = maxZoom;
663                this._gridClusters = {};
664                this._gridUnclustered = {};
665
666                //Set up DistanceGrids for each zoom
667                for (var zoom = maxZoom; zoom >= 0; zoom--) {
668                        this._gridClusters[zoom] = new L.DistanceGrid(radius);
669                        this._gridUnclustered[zoom] = new L.DistanceGrid(radius);
670                }
671
672                this._topClusterLevel = new L.MarkerCluster(this, -1);
673        },
674
675        //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
676        _addLayer: function (layer, zoom) {
677                var gridClusters = this._gridClusters,
678                    gridUnclustered = this._gridUnclustered,
679                    markerPoint, z;
680
681                if (this.options.singleMarkerMode) {
682                        layer.options.icon = this.options.iconCreateFunction({
683                                getChildCount: function () {
684                                        return 1;
685                                },
686                                getAllChildMarkers: function () {
687                                        return [layer];
688                                }
689                        });
690                }
691
692                //Find the lowest zoom level to slot this one in
693                for (; zoom >= 0; zoom--) {
694                        markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position
695
696                        //Try find a cluster close by
697                        var closest = gridClusters[zoom].getNearObject(markerPoint);
698                        if (closest) {
699                                closest._addChild(layer);
700                                layer.__parent = closest;
701                                return;
702                        }
703
704                        //Try find a marker close by to form a new cluster with
705                        closest = gridUnclustered[zoom].getNearObject(markerPoint);
706                        if (closest) {
707                                var parent = closest.__parent;
708                                if (parent) {
709                                        this._removeLayer(closest, false);
710                                }
711
712                                //Create new cluster with these 2 in it
713
714                                var newCluster = new L.MarkerCluster(this, zoom, closest, layer);
715                                gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom));
716                                closest.__parent = newCluster;
717                                layer.__parent = newCluster;
718
719                                //First create any new intermediate parent clusters that don't exist
720                                var lastParent = newCluster;
721                                for (z = zoom - 1; z > parent._zoom; z--) {
722                                        lastParent = new L.MarkerCluster(this, z, lastParent);
723                                        gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z));
724                                }
725                                parent._addChild(lastParent);
726
727                                //Remove closest from this zoom level and any above that it is in, replace with newCluster
728                                for (z = zoom; z >= 0; z--) {
729                                        if (!gridUnclustered[z].removeObject(closest, this._map.project(closest.getLatLng(), z))) {
730                                                break;
731                                        }
732                                }
733
734                                return;
735                        }
736
737                        //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
738                        gridUnclustered[zoom].addObject(layer, markerPoint);
739                }
740
741                //Didn't get in anything, add us to the top
742                this._topClusterLevel._addChild(layer);
743                layer.__parent = this._topClusterLevel;
744                return;
745        },
746
747        //Merge and split any existing clusters that are too big or small
748        _mergeSplitClusters: function () {
749                if (this._zoom < this._map._zoom) { //Zoom in, split
750                        this._animationStart();
751                        //Remove clusters now off screen
752                        this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds());
753
754                        this._animationZoomIn(this._zoom, this._map._zoom);
755
756                } else if (this._zoom > this._map._zoom) { //Zoom out, merge
757                        this._animationStart();
758
759                        this._animationZoomOut(this._zoom, this._map._zoom);
760                } else {
761                        this._moveEnd();
762                }
763        },
764
765        //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)
766        _getExpandedVisibleBounds: function () {
767                if (!this.options.removeOutsideVisibleBounds) {
768                        return this.getBounds();
769                }
770
771                var map = this._map,
772                        bounds = map.getBounds(),
773                        sw = bounds._southWest,
774                        ne = bounds._northEast,
775                        latDiff = L.Browser.mobile ? 0 : Math.abs(sw.lat - ne.lat),
776                        lngDiff = L.Browser.mobile ? 0 : Math.abs(sw.lng - ne.lng);
777
778                return new L.LatLngBounds(
779                        new L.LatLng(sw.lat - latDiff, sw.lng - lngDiff, true),
780                        new L.LatLng(ne.lat + latDiff, ne.lng + lngDiff, true));
781        },
782
783        //Shared animation code
784        _animationAddLayerNonAnimated: function (layer, newCluster) {
785                if (newCluster === layer) {
786                        this._featureGroup.addLayer(layer);
787                } else if (newCluster._childCount === 2) {
788                        newCluster._addToMap();
789
790                        var markers = newCluster.getAllChildMarkers();
791                        this._featureGroup.removeLayer(markers[0]);
792                        this._featureGroup.removeLayer(markers[1]);
793                } else {
794                        newCluster._updateIcon();
795                }
796        }
797});
798
799L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? {
800
801        //Non Animated versions of everything
802        _animationStart: function () {
803                //Do nothing...
804        },
805        _animationZoomIn: function (previousZoomLevel, newZoomLevel) {
806                this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
807                this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
808        },
809        _animationZoomOut: function (previousZoomLevel, newZoomLevel) {
810                this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel);
811                this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
812        },
813        _animationAddLayer: function (layer, newCluster) {
814                this._animationAddLayerNonAnimated(layer, newCluster);
815        }
816} : {
817
818        //Animated versions here
819        _animationStart: function () {
820                this._map._mapPane.className += ' leaflet-cluster-anim';
821                this._inZoomAnimation++;
822        },
823        _animationEnd: function () {
824                if (this._map) {
825                        this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', '');
826                }
827                this._inZoomAnimation--;
828                this.fire('animationend');
829        },
830        _animationZoomIn: function (previousZoomLevel, newZoomLevel) {
831                var me = this,
832                    bounds = this._getExpandedVisibleBounds(),
833                        fg = this._featureGroup,
834                    i;
835
836                //Add all children of current clusters to map and remove those clusters from map
837                this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
838                        var startPos = c._latlng,
839                                markers = c._markers,
840                                m;
841
842                        if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us
843                                fg.removeLayer(c);
844                                c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds);
845                        } else {
846                                //Fade out old cluster
847                                c.setOpacity(0);
848                                c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds);
849                        }
850
851                        //Remove all markers that aren't visible any more
852                        //TODO: Do we actually need to do this on the higher levels too?
853                        for (i = markers.length - 1; i >= 0; i--) {
854                                m = markers[i];
855                                if (!bounds.contains(m._latlng)) {
856                                        fg.removeLayer(m);
857                                }
858                        }
859
860                });
861
862                this._forceLayout();
863
864                //Update opacities
865                me._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel);
866                //TODO Maybe? Update markers in _recursivelyBecomeVisible
867                fg.eachLayer(function (n) {
868                        if (!(n instanceof L.MarkerCluster) && n._icon) {
869                                n.setOpacity(1);
870                        }
871                });
872
873                //update the positions of the just added clusters/markers
874                me._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) {
875                        c._recursivelyRestoreChildPositions(newZoomLevel);
876                });
877
878                //Remove the old clusters and close the zoom animation
879
880                setTimeout(function () {
881                        //update the positions of the just added clusters/markers
882                        me._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) {
883                                fg.removeLayer(c);
884                                c.setOpacity(1);
885                        });
886
887                        me._animationEnd();
888                }, 200);
889        },
890
891        _animationZoomOut: function (previousZoomLevel, newZoomLevel) {
892                this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel);
893
894                //Need to add markers for those that weren't on the map before but are now
895                this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds());
896                //Remove markers that were on the map before but won't be now
897                this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel, this._getExpandedVisibleBounds());
898        },
899        _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) {
900                var bounds = this._getExpandedVisibleBounds();
901
902                //Animate all of the markers in the clusters to move to their cluster center point
903                cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel);
904
905                var me = this;
906
907                //Update the opacity (If we immediately set it they won't animate)
908                this._forceLayout();
909                cluster._recursivelyBecomeVisible(bounds, newZoomLevel);
910
911                //TODO: Maybe use the transition timing stuff to make this more reliable
912                //When the animations are done, tidy up
913                setTimeout(function () {
914
915                        //This cluster stopped being a cluster before the timeout fired
916                        if (cluster._childCount === 1) {
917                                var m = cluster._markers[0];
918                                //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
919                                m.setLatLng(m.getLatLng());
920                                m.setOpacity(1);
921                        } else {
922                                cluster._recursively(bounds, newZoomLevel, 0, function (c) {
923                                        c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1);
924                                });
925                        }
926                        me._animationEnd();
927                }, 200);
928        },
929        _animationAddLayer: function (layer, newCluster) {
930                var me = this,
931                        fg = this._featureGroup;
932
933                fg.addLayer(layer);
934                if (newCluster !== layer) {
935                        if (newCluster._childCount > 2) { //Was already a cluster
936
937                                newCluster._updateIcon();
938                                this._forceLayout();
939                                this._animationStart();
940
941                                layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng()));
942                                layer.setOpacity(0);
943
944                                setTimeout(function () {
945                                        fg.removeLayer(layer);
946                                        layer.setOpacity(1);
947
948                                        me._animationEnd();
949                                }, 200);
950
951                        } else { //Just became a cluster
952                                this._forceLayout();
953
954                                me._animationStart();
955                                me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom());
956                        }
957                }
958        },
959
960        //Force a browser layout of stuff in the map
961        // Should apply the current opacity and location to all elements so we can update them again for an animation
962        _forceLayout: function () {
963                //In my testing this works, infact offsetWidth of any element seems to work.
964                //Could loop all this._layers and do this for each _icon if it stops working
965
966                L.Util.falseFn(document.body.offsetWidth);
967        }
968});
969
970L.markerClusterGroup = function (options) {
971        return new L.MarkerClusterGroup(options);
972};
973
974
975L.MarkerCluster = L.Marker.extend({
976        initialize: function (group, zoom, a, b) {
977
978                L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this });
979
980
981                this._group = group;
982                this._zoom = zoom;
983
984                this._markers = [];
985                this._childClusters = [];
986                this._childCount = 0;
987                this._iconNeedsUpdate = true;
988
989                this._bounds = new L.LatLngBounds();
990
991                if (a) {
992                        this._addChild(a);
993                }
994                if (b) {
995                        this._addChild(b);
996                }
997        },
998
999        //Recursively retrieve all child markers of this cluster
1000        getAllChildMarkers: function (storageArray) {
1001                storageArray = storageArray || [];
1002
1003                for (var i = this._childClusters.length - 1; i >= 0; i--) {
1004                        this._childClusters[i].getAllChildMarkers(storageArray);
1005                }
1006
1007                for (var j = this._markers.length - 1; j >= 0; j--) {
1008                        storageArray.push(this._markers[j]);
1009                }
1010
1011                return storageArray;
1012        },
1013
1014        //Returns the count of how many child markers we have
1015        getChildCount: function () {
1016                return this._childCount;
1017        },
1018
1019        //Zoom to the extents of this cluster
1020        zoomToBounds: function () {
1021                this._group._map.fitBounds(this._bounds);
1022        },
1023
1024        getBounds: function () {
1025                var bounds = new L.LatLngBounds();
1026                bounds.extend(this._bounds);
1027                return bounds;
1028        },
1029
1030        _updateIcon: function () {
1031                this._iconNeedsUpdate = true;
1032                if (this._icon) {
1033                        this.setIcon(this);
1034                }
1035        },
1036
1037        //Cludge for Icon, we pretend to be an icon for performance
1038        createIcon: function () {
1039                if (this._iconNeedsUpdate) {
1040                        this._iconObj = this._group.options.iconCreateFunction(this);
1041                        this._iconNeedsUpdate = false;
1042                }
1043                return this._iconObj.createIcon();
1044        },
1045        createShadow: function () {
1046                return this._iconObj.createShadow();
1047        },
1048
1049
1050        _addChild: function (new1, isNotificationFromChild) {
1051
1052                this._iconNeedsUpdate = true;
1053                this._expandBounds(new1);
1054
1055                if (new1 instanceof L.MarkerCluster) {
1056                        if (!isNotificationFromChild) {
1057                                this._childClusters.push(new1);
1058                                new1.__parent = this;
1059                        }
1060                        this._childCount += new1._childCount;
1061                } else {
1062                        if (!isNotificationFromChild) {
1063                                this._markers.push(new1);
1064                        }
1065                        this._childCount++;
1066                }
1067
1068                if (this.__parent) {
1069                        this.__parent._addChild(new1, true);
1070                }
1071        },
1072
1073        //Expand our bounds and tell our parent to
1074        _expandBounds: function (marker) {
1075                var addedCount,
1076                    addedLatLng = marker._wLatLng || marker._latlng;
1077
1078                if (marker instanceof L.MarkerCluster) {
1079                        this._bounds.extend(marker._bounds);
1080                        addedCount = marker._childCount;
1081                } else {
1082                        this._bounds.extend(addedLatLng);
1083                        addedCount = 1;
1084                }
1085
1086                if (!this._cLatLng) {
1087                        // when clustering, take position of the first point as the cluster center
1088                        this._cLatLng = marker._cLatLng || addedLatLng;
1089                }
1090
1091                // when showing clusters, take weighted average of all points as cluster center
1092                var totalCount = this._childCount + addedCount;
1093
1094                //Calculate weighted latlng for display
1095                if (!this._wLatLng) {
1096                        this._latlng = this._wLatLng = new L.LatLng(addedLatLng.lat, addedLatLng.lng);
1097                } else {
1098                        this._wLatLng.lat = (addedLatLng.lat * addedCount + this._wLatLng.lat * this._childCount) / totalCount;
1099                        this._wLatLng.lng = (addedLatLng.lng * addedCount + this._wLatLng.lng * this._childCount) / totalCount;
1100                }
1101        },
1102
1103        //Set our markers position as given and add it to the map
1104        _addToMap: function (startPos) {
1105                if (startPos) {
1106                        this._backupLatlng = this._latlng;
1107                        this.setLatLng(startPos);
1108                }
1109                this._group._featureGroup.addLayer(this);
1110        },
1111
1112        _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {
1113                this._recursively(bounds, 0, maxZoom - 1,
1114                        function (c) {
1115                                var markers = c._markers,
1116                                        i, m;
1117                                for (i = markers.length - 1; i >= 0; i--) {
1118                                        m = markers[i];
1119
1120                                        //Only do it if the icon is still on the map
1121                                        if (m._icon) {
1122                                                m._setPos(center);
1123                                                m.setOpacity(0);
1124                                        }
1125                                }
1126                        },
1127                        function (c) {
1128                                var childClusters = c._childClusters,
1129                                        j, cm;
1130                                for (j = childClusters.length - 1; j >= 0; j--) {
1131                                        cm = childClusters[j];
1132                                        if (cm._icon) {
1133                                                cm._setPos(center);
1134                                                cm.setOpacity(0);
1135                                        }
1136                                }
1137                        }
1138                );
1139        },
1140
1141        _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, previousZoomLevel, newZoomLevel) {
1142                this._recursively(bounds, newZoomLevel, 0,
1143                        function (c) {
1144                                c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);
1145
1146                                //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
1147                                //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
1148                                if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) {
1149                                        c.setOpacity(1);
1150                                        c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
1151                                } else {
1152                                        c.setOpacity(0);
1153                                }
1154
1155                                c._addToMap();
1156                        }
1157                );
1158        },
1159
1160        _recursivelyBecomeVisible: function (bounds, zoomLevel) {
1161                this._recursively(bounds, 0, zoomLevel, null, function (c) {
1162                        c.setOpacity(1);
1163                });
1164        },
1165
1166        _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {
1167                this._recursively(bounds, -1, zoomLevel,
1168                        function (c) {
1169                                if (zoomLevel === c._zoom) {
1170                                        return;
1171                                }
1172
1173                                //Add our child markers at startPos (so they can be animated out)
1174                                for (var i = c._markers.length - 1; i >= 0; i--) {
1175                                        var nm = c._markers[i];
1176
1177                                        if (!bounds.contains(nm._latlng)) {
1178                                                continue;
1179                                        }
1180
1181                                        if (startPos) {
1182                                                nm._backupLatlng = nm.getLatLng();
1183
1184                                                nm.setLatLng(startPos);
1185                                                if (nm.setOpacity) {
1186                                                        nm.setOpacity(0);
1187                                                }
1188                                        }
1189
1190                                        c._group._featureGroup.addLayer(nm);
1191                                }
1192                        },
1193                        function (c) {
1194                                c._addToMap(startPos);
1195                        }
1196                );
1197        },
1198
1199        _recursivelyRestoreChildPositions: function (zoomLevel) {
1200                //Fix positions of child markers
1201                for (var i = this._markers.length - 1; i >= 0; i--) {
1202                        var nm = this._markers[i];
1203                        if (nm._backupLatlng) {
1204                                nm.setLatLng(nm._backupLatlng);
1205                                delete nm._backupLatlng;
1206                        }
1207                }
1208
1209                if (zoomLevel - 1 === this._zoom) {
1210                        //Reposition child clusters
1211                        for (var j = this._childClusters.length - 1; j >= 0; j--) {
1212                                this._childClusters[j]._restorePosition();
1213                        }
1214                } else {
1215                        for (var k = this._childClusters.length - 1; k >= 0; k--) {
1216                                this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);
1217                        }
1218                }
1219        },
1220
1221        _restorePosition: function () {
1222                if (this._backupLatlng) {
1223                        this.setLatLng(this._backupLatlng);
1224                        delete this._backupLatlng;
1225                }
1226        },
1227
1228        //exceptBounds: If set, don't remove any markers/clusters in it
1229        _recursivelyRemoveChildrenFromMap: function (previousBounds, zoomLevel, exceptBounds) {
1230                var m, i;
1231                this._recursively(previousBounds, -1, zoomLevel - 1,
1232                        function (c) {
1233                                //Remove markers at every level
1234                                for (i = c._markers.length - 1; i >= 0; i--) {
1235                                        m = c._markers[i];
1236                                        if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1237                                                c._group._featureGroup.removeLayer(m);
1238                                                if (m.setOpacity) {
1239                                                        m.setOpacity(1);
1240                                                }
1241                                        }
1242                                }
1243                        },
1244                        function (c) {
1245                                //Remove child clusters at just the bottom level
1246                                for (i = c._childClusters.length - 1; i >= 0; i--) {
1247                                        m = c._childClusters[i];
1248                                        if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
1249                                                c._group._featureGroup.removeLayer(m);
1250                                                if (m.setOpacity) {
1251                                                        m.setOpacity(1);
1252                                                }
1253                                        }
1254                                }
1255                        }
1256                );
1257        },
1258
1259        //Run the given functions recursively to this and child clusters
1260        // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
1261        // zoomLevelToStart: zoom level to start running functions (inclusive)
1262        // zoomLevelToStop: zoom level to stop running functions (inclusive)
1263        // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
1264        // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
1265        _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) {
1266                var childClusters = this._childClusters,
1267                    zoom = this._zoom,
1268                        i, c;
1269
1270                if (zoomLevelToStart > zoom) { //Still going down to required depth, just recurse to child clusters
1271                        for (i = childClusters.length - 1; i >= 0; i--) {
1272                                c = childClusters[i];
1273                                if (boundsToApplyTo.intersects(c._bounds)) {
1274                                        c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
1275                                }
1276                        }
1277                } else { //In required depth
1278
1279                        if (runAtEveryLevel) {
1280                                runAtEveryLevel(this);
1281                        }
1282                        if (runAtBottomLevel && this._zoom === zoomLevelToStop) {
1283                                runAtBottomLevel(this);
1284                        }
1285
1286                        //TODO: This loop is almost the same as above
1287                        if (zoomLevelToStop > zoom) {
1288                                for (i = childClusters.length - 1; i >= 0; i--) {
1289                                        c = childClusters[i];
1290                                        if (boundsToApplyTo.intersects(c._bounds)) {
1291                                                c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
1292                                        }
1293                                }
1294                        }
1295                }
1296        },
1297
1298        _recalculateBounds: function () {
1299                var markers = this._markers,
1300                        childClusters = this._childClusters,
1301                        i;
1302
1303                this._bounds = new L.LatLngBounds();
1304                delete this._wLatLng;
1305
1306                for (i = markers.length - 1; i >= 0; i--) {
1307                        this._expandBounds(markers[i]);
1308                }
1309                for (i = childClusters.length - 1; i >= 0; i--) {
1310                        this._expandBounds(childClusters[i]);
1311                }
1312        },
1313
1314
1315        //Returns true if we are the parent of only one cluster and that cluster is the same as us
1316        _isSingleParent: function () {
1317                //Don't need to check this._markers as the rest won't work if there are any
1318                return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount;
1319        }
1320});
1321
1322
1323
1324L.DistanceGrid = function (cellSize) {
1325        this._cellSize = cellSize;
1326        this._sqCellSize = cellSize * cellSize;
1327        this._grid = {};
1328        this._objectPoint = { };
1329};
1330
1331L.DistanceGrid.prototype = {
1332
1333        addObject: function (obj, point) {
1334                var x = this._getCoord(point.x),
1335                    y = this._getCoord(point.y),
1336                    grid = this._grid,
1337                    row = grid[y] = grid[y] || {},
1338                    cell = row[x] = row[x] || [],
1339                    stamp = L.Util.stamp(obj);
1340
1341                this._objectPoint[stamp] = point;
1342
1343                cell.push(obj);
1344        },
1345
1346        updateObject: function (obj, point) {
1347                this.removeObject(obj);
1348                this.addObject(obj, point);
1349        },
1350
1351        //Returns true if the object was found
1352        removeObject: function (obj, point) {
1353                var x = this._getCoord(point.x),
1354                    y = this._getCoord(point.y),
1355                    grid = this._grid,
1356                    row = grid[y] = grid[y] || {},
1357                    cell = row[x] = row[x] || [],
1358                    i, len;
1359
1360                delete this._objectPoint[L.Util.stamp(obj)];
1361
1362                for (i = 0, len = cell.length; i < len; i++) {
1363                        if (cell[i] === obj) {
1364
1365                                cell.splice(i, 1);
1366
1367                                if (len === 1) {
1368                                        delete row[x];
1369                                }
1370
1371                                return true;
1372                        }
1373                }
1374
1375        },
1376
1377        eachObject: function (fn, context) {
1378                var i, j, k, len, row, cell, removed,
1379                    grid = this._grid;
1380
1381                for (i in grid) {
1382                        row = grid[i];
1383
1384                        for (j in row) {
1385                                cell = row[j];
1386
1387                                for (k = 0, len = cell.length; k < len; k++) {
1388                                        removed = fn.call(context, cell[k]);
1389                                        if (removed) {
1390                                                k--;
1391                                                len--;
1392                                        }
1393                                }
1394                        }
1395                }
1396        },
1397
1398        getNearObject: function (point) {
1399                var x = this._getCoord(point.x),
1400                    y = this._getCoord(point.y),
1401                    i, j, k, row, cell, len, obj, dist,
1402                    objectPoint = this._objectPoint,
1403                    closestDistSq = this._sqCellSize,
1404                    closest = null;
1405
1406                for (i = y - 1; i <= y + 1; i++) {
1407                        row = this._grid[i];
1408                        if (row) {
1409
1410                                for (j = x - 1; j <= x + 1; j++) {
1411                                        cell = row[j];
1412                                        if (cell) {
1413
1414                                                for (k = 0, len = cell.length; k < len; k++) {
1415                                                        obj = cell[k];
1416                                                        dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point);
1417                                                        if (dist < closestDistSq) {
1418                                                                closestDistSq = dist;
1419                                                                closest = obj;
1420                                                        }
1421                                                }
1422                                        }
1423                                }
1424                        }
1425                }
1426                return closest;
1427        },
1428
1429        _getCoord: function (x) {
1430                return Math.floor(x / this._cellSize);
1431        },
1432
1433        _sqDist: function (p, p2) {
1434                var dx = p2.x - p.x,
1435                    dy = p2.y - p.y;
1436                return dx * dx + dy * dy;
1437        }
1438};
1439
1440
1441/* Copyright (c) 2012 the authors listed at the following URL, and/or
1442the authors of referenced articles or incorporated external code:
1443http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256
1444
1445Permission is hereby granted, free of charge, to any person obtaining
1446a copy of this software and associated documentation files (the
1447"Software"), to deal in the Software without restriction, including
1448without limitation the rights to use, copy, modify, merge, publish,
1449distribute, sublicense, and/or sell copies of the Software, and to
1450permit persons to whom the Software is furnished to do so, subject to
1451the following conditions:
1452
1453The above copyright notice and this permission notice shall be
1454included in all copies or substantial portions of the Software.
1455
1456THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1457EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1458MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1459IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1460CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1461TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1462SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1463
1464Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
1465*/
1466
1467(function () {
1468        L.QuickHull = {
1469                getDistant: function (cpt, bl) {
1470                        var vY = bl[1].lat - bl[0].lat,
1471                                vX = bl[0].lng - bl[1].lng;
1472                        return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng));
1473                },
1474
1475
1476                findMostDistantPointFromBaseLine: function (baseLine, latLngs) {
1477                        var maxD = 0,
1478                                maxPt = null,
1479                                newPoints = [],
1480                                i, pt, d;
1481
1482                        for (i = latLngs.length - 1; i >= 0; i--) {
1483                                pt = latLngs[i];
1484                                d = this.getDistant(pt, baseLine);
1485
1486                                if (d > 0) {
1487                                        newPoints.push(pt);
1488                                } else {
1489                                        continue;
1490                                }
1491
1492                                if (d > maxD) {
1493                                        maxD = d;
1494                                        maxPt = pt;
1495                                }
1496
1497                        }
1498                        return { 'maxPoint': maxPt, 'newPoints': newPoints };
1499                },
1500
1501                buildConvexHull: function (baseLine, latLngs) {
1502                        var convexHullBaseLines = [],
1503                                t = this.findMostDistantPointFromBaseLine(baseLine, latLngs);
1504
1505                        if (t.maxPoint) { // if there is still a point "outside" the base line
1506                                convexHullBaseLines =
1507                                        convexHullBaseLines.concat(
1508                                                this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints)
1509                                        );
1510                                convexHullBaseLines =
1511                                        convexHullBaseLines.concat(
1512                                                this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints)
1513                                        );
1514                                return convexHullBaseLines;
1515                        } else {  // if there is no more point "outside" the base line, the current base line is part of the convex hull
1516                                return [baseLine];
1517                        }
1518                },
1519
1520                getConvexHull: function (latLngs) {
1521                        //find first baseline
1522                        var maxLat = false, minLat = false,
1523                                maxPt = null, minPt = null,
1524                                i;
1525
1526                        for (i = latLngs.length - 1; i >= 0; i--) {
1527                                var pt = latLngs[i];
1528                                if (maxLat === false || pt.lat > maxLat) {
1529                                        maxPt = pt;
1530                                        maxLat = pt.lat;
1531                                }
1532                                if (minLat === false || pt.lat < minLat) {
1533                                        minPt = pt;
1534                                        minLat = pt.lat;
1535                                }
1536                        }
1537                        var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs),
1538                                                                this.buildConvexHull([maxPt, minPt], latLngs));
1539                        return ch;
1540                }
1541        };
1542}());
1543
1544L.MarkerCluster.include({
1545        getConvexHull: function () {
1546                var childMarkers = this.getAllChildMarkers(),
1547                        points = [],
1548                        hullLatLng = [],
1549                        hull, p, i;
1550
1551                for (i = childMarkers.length - 1; i >= 0; i--) {
1552                        p = childMarkers[i].getLatLng();
1553                        points.push(p);
1554                }
1555
1556                hull = L.QuickHull.getConvexHull(points);
1557
1558                for (i = hull.length - 1; i >= 0; i--) {
1559                        hullLatLng.push(hull[i][0]);
1560                }
1561
1562                return hullLatLng;
1563        }
1564});
1565
1566//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
1567//Huge thanks to jawj for implementing it first to make my job easy :-)
1568
1569L.MarkerCluster.include({
1570
1571        _2PI: Math.PI * 2,
1572        _circleFootSeparation: 25, //related to circumference of circle
1573        _circleStartAngle: Math.PI / 6,
1574
1575        _spiralFootSeparation:  28, //related to size of spiral (experiment!)
1576        _spiralLengthStart: 11,
1577        _spiralLengthFactor: 5,
1578
1579        _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards.
1580                                                                // 0 -> always spiral; Infinity -> always circle
1581
1582        spiderfy: function () {
1583                if (this._group._spiderfied === this || this._group._inZoomAnimation) {
1584                        return;
1585                }
1586
1587                var childMarkers = this.getAllChildMarkers(),
1588                        group = this._group,
1589                        map = group._map,
1590                        center = map.latLngToLayerPoint(this._latlng),
1591                        positions;
1592
1593                this._group._unspiderfy();
1594                this._group._spiderfied = this;
1595
1596                //TODO Maybe: childMarkers order by distance to center
1597
1598                if (childMarkers.length >= this._circleSpiralSwitchover) {
1599                        positions = this._generatePointsSpiral(childMarkers.length, center);
1600                } else {
1601                        center.y += 10; //Otherwise circles look wrong
1602                        positions = this._generatePointsCircle(childMarkers.length, center);
1603                }
1604
1605                this._animationSpiderfy(childMarkers, positions);
1606        },
1607
1608        unspiderfy: function (zoomDetails) {
1609                /// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param>
1610                if (this._group._inZoomAnimation) {
1611                        return;
1612                }
1613                this._animationUnspiderfy(zoomDetails);
1614
1615                this._group._spiderfied = null;
1616        },
1617
1618        _generatePointsCircle: function (count, centerPt) {
1619                var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),
1620                        legLength = circumference / this._2PI,  //radius from circumference
1621                        angleStep = this._2PI / count,
1622                        res = [],
1623                        i, angle;
1624
1625                res.length = count;
1626
1627                for (i = count - 1; i >= 0; i--) {
1628                        angle = this._circleStartAngle + i * angleStep;
1629                        res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
1630                }
1631
1632                return res;
1633        },
1634
1635        _generatePointsSpiral: function (count, centerPt) {
1636                var legLength = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthStart,
1637                        separation = this._group.options.spiderfyDistanceMultiplier * this._spiralFootSeparation,
1638                        lengthFactor = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthFactor,
1639                        angle = 0,
1640                        res = [],
1641                        i;
1642
1643                res.length = count;
1644
1645                for (i = count - 1; i >= 0; i--) {
1646                        angle += separation / legLength + i * 0.0005;
1647                        res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round();
1648                        legLength += this._2PI * lengthFactor / angle;
1649                }
1650                return res;
1651        },
1652
1653        _noanimationUnspiderfy: function () {
1654                var group = this._group,
1655                        map = group._map,
1656                        fg = group._featureGroup,
1657                        childMarkers = this.getAllChildMarkers(),
1658                        m, i;
1659
1660                this.setOpacity(1);
1661                for (i = childMarkers.length - 1; i >= 0; i--) {
1662                        m = childMarkers[i];
1663
1664                        fg.removeLayer(m);
1665
1666                        if (m._preSpiderfyLatlng) {
1667                                m.setLatLng(m._preSpiderfyLatlng);
1668                                delete m._preSpiderfyLatlng;
1669                        }
1670                        m.setZIndexOffset(0);
1671
1672                        if (m._spiderLeg) {
1673                                map.removeLayer(m._spiderLeg);
1674                                delete m._spiderLeg;
1675                        }
1676                }
1677        }
1678});
1679
1680L.MarkerCluster.include(!L.DomUtil.TRANSITION ? {
1681        //Non Animated versions of everything
1682        _animationSpiderfy: function (childMarkers, positions) {
1683                var group = this._group,
1684                        map = group._map,
1685                        fg = group._featureGroup,
1686                        i, m, leg, newPos;
1687
1688                for (i = childMarkers.length - 1; i >= 0; i--) {
1689                        newPos = map.layerPointToLatLng(positions[i]);
1690                        m = childMarkers[i];
1691
1692                        m._preSpiderfyLatlng = m._latlng;
1693                        m.setLatLng(newPos);
1694                        m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
1695
1696                        fg.addLayer(m);
1697
1698
1699                        leg = new L.Polyline([this._latlng, newPos], { weight: 1.5, color: '#222' });
1700                        map.addLayer(leg);
1701                        m._spiderLeg = leg;
1702                }
1703                this.setOpacity(0.3);
1704                group.fire('spiderfied');
1705        },
1706
1707        _animationUnspiderfy: function () {
1708                this._noanimationUnspiderfy();
1709        }
1710} : {
1711        //Animated versions here
1712        SVG_ANIMATION: (function () {
1713                return document.createElementNS('http://www.w3.org/2000/svg', 'animate').toString().indexOf('SVGAnimate') > -1;
1714        }()),
1715
1716        _animationSpiderfy: function (childMarkers, positions) {
1717                var me = this,
1718                        group = this._group,
1719                        map = group._map,
1720                        fg = group._featureGroup,
1721                        thisLayerPos = map.latLngToLayerPoint(this._latlng),
1722                        i, m, leg, newPos;
1723
1724                //Add markers to map hidden at our center point
1725                for (i = childMarkers.length - 1; i >= 0; i--) {
1726                        m = childMarkers[i];
1727
1728                        m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING
1729                        m.setOpacity(0);
1730
1731                        fg.addLayer(m);
1732
1733                        m._setPos(thisLayerPos);
1734                }
1735
1736                group._forceLayout();
1737                group._animationStart();
1738
1739                var initialLegOpacity = L.Path.SVG ? 0 : 0.3,
1740                        xmlns = L.Path.SVG_NS;
1741
1742
1743                for (i = childMarkers.length - 1; i >= 0; i--) {
1744                        newPos = map.layerPointToLatLng(positions[i]);
1745                        m = childMarkers[i];
1746
1747                        //Move marker to new position
1748                        m._preSpiderfyLatlng = m._latlng;
1749                        m.setLatLng(newPos);
1750                        m.setOpacity(1);
1751
1752
1753                        //Add Legs.
1754                        leg = new L.Polyline([me._latlng, newPos], { weight: 1.5, color: '#222', opacity: initialLegOpacity });
1755                        map.addLayer(leg);
1756                        m._spiderLeg = leg;
1757
1758                        //Following animations don't work for canvas
1759                        if (!L.Path.SVG || !this.SVG_ANIMATION) {
1760                                continue;
1761                        }
1762
1763                        //How this works:
1764                        //http://stackoverflow.com/questions/5924238/how-do-you-animate-an-svg-path-in-ios
1765                        //http://dev.opera.com/articles/view/advanced-svg-animation-techniques/
1766
1767                        //Animate length
1768                        var length = leg._path.getTotalLength();
1769                        leg._path.setAttribute("stroke-dasharray", length + "," + length);
1770
1771                        var anim = document.createElementNS(xmlns, "animate");
1772                        anim.setAttribute("attributeName", "stroke-dashoffset");
1773                        anim.setAttribute("begin", "indefinite");
1774                        anim.setAttribute("from", length);
1775                        anim.setAttribute("to", 0);
1776                        anim.setAttribute("dur", 0.25);
1777                        leg._path.appendChild(anim);
1778                        anim.beginElement();
1779
1780                        //Animate opacity
1781                        anim = document.createElementNS(xmlns, "animate");
1782                        anim.setAttribute("attributeName", "stroke-opacity");
1783                        anim.setAttribute("attributeName", "stroke-opacity");
1784                        anim.setAttribute("begin", "indefinite");
1785                        anim.setAttribute("from", 0);
1786                        anim.setAttribute("to", 0.5);
1787                        anim.setAttribute("dur", 0.25);
1788                        leg._path.appendChild(anim);
1789                        anim.beginElement();
1790                }
1791                me.setOpacity(0.3);
1792
1793                //Set the opacity of the spiderLegs back to their correct value
1794                // The animations above override this until they complete.
1795                // If the initial opacity of the spiderlegs isn't 0 then they appear before the animation starts.
1796                if (L.Path.SVG) {
1797                        this._group._forceLayout();
1798
1799                        for (i = childMarkers.length - 1; i >= 0; i--) {
1800                                m = childMarkers[i]._spiderLeg;
1801
1802                                m.options.opacity = 0.5;
1803                                m._path.setAttribute('stroke-opacity', 0.5);
1804                        }
1805                }
1806
1807                setTimeout(function () {
1808                        group._animationEnd();
1809                        group.fire('spiderfied');
1810                }, 200);
1811        },
1812
1813        _animationUnspiderfy: function (zoomDetails) {
1814                var group = this._group,
1815                        map = group._map,
1816                        fg = group._featureGroup,
1817                        thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng),
1818                        childMarkers = this.getAllChildMarkers(),
1819                        svg = L.Path.SVG && this.SVG_ANIMATION,
1820                        m, i, a;
1821
1822                group._animationStart();
1823
1824                //Make us visible and bring the child markers back in
1825                this.setOpacity(1);
1826                for (i = childMarkers.length - 1; i >= 0; i--) {
1827                        m = childMarkers[i];
1828
1829                        //Marker was added to us after we were spidified
1830                        if (!m._preSpiderfyLatlng) {
1831                                continue;
1832                        }
1833
1834                        //Fix up the location to the real one
1835                        m.setLatLng(m._preSpiderfyLatlng);
1836                        delete m._preSpiderfyLatlng;
1837                        //Hack override the location to be our center
1838                        m._setPos(thisLayerPos);
1839
1840                        m.setOpacity(0);
1841
1842                        //Animate the spider legs back in
1843                        if (svg) {
1844                                a = m._spiderLeg._path.childNodes[0];
1845                                a.setAttribute('to', a.getAttribute('from'));
1846                                a.setAttribute('from', 0);
1847                                a.beginElement();
1848
1849                                a = m._spiderLeg._path.childNodes[1];
1850                                a.setAttribute('from', 0.5);
1851                                a.setAttribute('to', 0);
1852                                a.setAttribute('stroke-opacity', 0);
1853                                a.beginElement();
1854
1855                                m._spiderLeg._path.setAttribute('stroke-opacity', 0);
1856                        }
1857                }
1858
1859                setTimeout(function () {
1860                        //If we have only <= one child left then that marker will be shown on the map so don't remove it!
1861                        var stillThereChildCount = 0;
1862                        for (i = childMarkers.length - 1; i >= 0; i--) {
1863                                m = childMarkers[i];
1864                                if (m._spiderLeg) {
1865                                        stillThereChildCount++;
1866                                }
1867                        }
1868
1869
1870                        for (i = childMarkers.length - 1; i >= 0; i--) {
1871                                m = childMarkers[i];
1872
1873                                if (!m._spiderLeg) { //Has already been unspiderfied
1874                                        continue;
1875                                }
1876
1877
1878                                m.setOpacity(1);
1879                                m.setZIndexOffset(0);
1880
1881                                if (stillThereChildCount > 1) {
1882                                        fg.removeLayer(m);
1883                                }
1884
1885                                map.removeLayer(m._spiderLeg);
1886                                delete m._spiderLeg;
1887                        }
1888                        group._animationEnd();
1889                }, 200);
1890        }
1891});
1892
1893
1894L.MarkerClusterGroup.include({
1895        //The MarkerCluster currently spiderfied (if any)
1896        _spiderfied: null,
1897
1898        _spiderfierOnAdd: function () {
1899                this._map.on('click', this._unspiderfyWrapper, this);
1900
1901                if (this._map.options.zoomAnimation) {
1902                        this._map.on('zoomstart', this._unspiderfyZoomStart, this);
1903                } else {
1904                        //Browsers without zoomAnimation don't fire zoomstart
1905                        this._map.on('zoomend', this._unspiderfyWrapper, this);
1906                }
1907
1908                if (L.Path.SVG && !L.Browser.touch) {
1909                        this._map._initPathRoot();
1910                        //Needs to happen in the pageload, not after, or animations don't work in webkit
1911                        //  http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements
1912                        //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable
1913                }
1914        },
1915
1916        _spiderfierOnRemove: function () {
1917                this._map.off('click', this._unspiderfyWrapper, this);
1918                this._map.off('zoomstart', this._unspiderfyZoomStart, this);
1919                this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
1920
1921                this._unspiderfy(); //Ensure that markers are back where they should be
1922        },
1923
1924
1925        //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)
1926        //This means we can define the animation they do rather than Markers doing an animation to their actual location
1927        _unspiderfyZoomStart: function () {
1928                if (!this._map) { //May have been removed from the map by a zoomEnd handler
1929                        return;
1930                }
1931
1932                this._map.on('zoomanim', this._unspiderfyZoomAnim, this);
1933        },
1934        _unspiderfyZoomAnim: function (zoomDetails) {
1935                //Wait until the first zoomanim after the user has finished touch-zooming before running the animation
1936                if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) {
1937                        return;
1938                }
1939
1940                this._map.off('zoomanim', this._unspiderfyZoomAnim, this);
1941                this._unspiderfy(zoomDetails);
1942        },
1943
1944
1945        _unspiderfyWrapper: function () {
1946                /// <summary>_unspiderfy but passes no arguments</summary>
1947                this._unspiderfy();
1948        },
1949
1950        _unspiderfy: function (zoomDetails) {
1951                if (this._spiderfied) {
1952                        this._spiderfied.unspiderfy(zoomDetails);
1953                }
1954        },
1955
1956        _noanimationUnspiderfy: function () {
1957                if (this._spiderfied) {
1958                        this._spiderfied._noanimationUnspiderfy();
1959                }
1960        },
1961
1962        //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc
1963        _unspiderfyLayer: function (layer) {
1964                if (layer._spiderLeg) {
1965                        this._featureGroup.removeLayer(layer);
1966
1967                        layer.setOpacity(1);
1968                        //Position will be fixed up immediately in _animationUnspiderfy
1969                        layer.setZIndexOffset(0);
1970
1971                        this._map.removeLayer(layer._spiderLeg);
1972                        delete layer._spiderLeg;
1973                }
1974        }
1975});
1976
1977
1978}(window, document));
Note: See TracBrowser for help on using the repository browser.