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