this._objectsOnMap[i];
115 |
116 | if (o.data._leafletMarker !== m) {
117 | if (o.bounds.minLat >= cbounds.minLat &&
118 | o.bounds.maxLat <= cbounds.maxLat &&
119 | o.bounds.minLng >= cbounds.minLng &&
120 | o.bounds.maxLng <= cbounds.maxLng) {
121 | filteredBounds.push(o.bounds);
122 | }
123 | }
124 | }
125 |
126 | // Filter the markers
127 | if (filteredBounds.length > 0) {
128 | var newMarkersArea = [];
129 | var ll = filteredBounds.length;
130 | for (i = 0, l = markersArea.length; i < l; ++i) {
131 | var markerPos = markersArea[i].position;
132 | var isFiltered = false;
133 | for (var j = 0; j < ll; ++j) {
134 | var currentFilteredBounds = filteredBounds[j];
135 | if (markerPos.lat >= currentFilteredBounds.minLat &&
136 | markerPos.lat <= currentFilteredBounds.maxLat &&
137 | markerPos.lng >= currentFilteredBounds.minLng &&
138 | markerPos.lng <= currentFilteredBounds.maxLng) {
139 | isFiltered = true;
140 | break;
141 | }
142 | }
143 | if (!isFiltered) {
144 | newMarkersArea.push(markersArea[i]);
145 | }
146 | }
147 | markersArea = newMarkersArea;
148 | }
149 |
150 | // TODO use an option registered somewhere
151 | if (markersArea.length < 200 || zoomLevelAfter >= this._map.getMaxZoom()) {
152 |
153 | // Send an event for the LeafletSpiderfier
154 | this._map.fire('overlappingmarkers', {
155 | cluster: this,
156 | markers: markersArea,
157 | center: m.getLatLng(),
158 | marker: m
159 | });
160 |
161 | } else {
162 | zoomLevelAfter++;
163 | }
164 |
165 | this._map.setView(m.getLatLng(), zoomLevelAfter);
166 | } else {
167 | this._map.fitBounds(bounds);
168 | }
169 |
170 | }
171 | });
172 |
173 | return m;
174 | },
175 |
176 | BuildLeafletClusterIcon: function(cluster: PruneCluster.Cluster): L.Icon {
177 | var c = 'prunecluster prunecluster-';
178 | var iconSize = 38;
179 | var maxPopulation = this.Cluster.GetPopulation();
180 |
181 | if (cluster.population < Math.max(10, maxPopulation*0.01)) {
182 | c += 'small';
183 | } else if (cluster.population < Math.max(100, maxPopulation * 0.05)) {
184 | c += 'medium';
185 | iconSize = 40;
186 | } else {
187 | c += 'large';
188 | iconSize = 44;
189 | }
190 |
191 | return new L.DivIcon({
192 | html: "" + cluster.population + "
",
193 | className: c,
194 | iconSize: L.point(iconSize, iconSize)
195 | });
196 | },
197 |
198 | BuildLeafletMarker: function (marker: PruneCluster.Marker, position: L.LatLng): L.Marker {
199 | var m = new L.Marker(position);
200 | this.PrepareLeafletMarker(m, marker.data, marker.category);
201 | return m;
202 | },
203 |
204 | PrepareLeafletMarker: (marker: L.Marker, data: any, category: number) => {
205 | if (data.icon) {
206 | if (typeof data.icon === 'function') {
207 | marker.setIcon(data.icon(data, category));
208 | } else {
209 | marker.setIcon(data.icon);
210 | }
211 | }
212 |
213 | if (data.popup) {
214 | var content = typeof data.popup === 'function' ? data.popup(data, category) : data.popup;
215 | if (marker.getPopup()) {
216 | marker.setPopupContent(content, data.popupOptions);
217 | } else {
218 | marker.bindPopup(content, data.popupOptions);
219 | }
220 | }
221 | },
222 |
223 | onAdd: function(map: L.Map) {
224 | this._map = map;
225 | map.on('movestart', this._moveStart, this);
226 | map.on('moveend', this._moveEnd, this);
227 | map.on('zoomend', this._zoomStart, this);
228 | map.on('zoomend', this._zoomEnd, this);
229 | this.ProcessView();
230 |
231 | map.addLayer(this.spiderfier);
232 | },
233 |
234 | onRemove: function(map: L.Map) {
235 |
236 | map.off('movestart', this._moveStart, this);
237 | map.off('moveend', this._moveEnd, this);
238 | map.off('zoomend', this._zoomStart, this);
239 | map.off('zoomend', this._zoomEnd, this);
240 |
241 | for (var i = 0, l = this._objectsOnMap.length; i < l; ++i) {
242 | map.removeLayer(this._objectsOnMap[i].data._leafletMarker);
243 | }
244 |
245 | this._objectsOnMap = [];
246 | this.Cluster.ResetClusters();
247 |
248 | map.removeLayer(this.spiderfier);
249 |
250 | this._map = null;
251 | },
252 |
253 | _moveStart: function() {
254 | this._moveInProgress = true;
255 | },
256 |
257 | _moveEnd: function(e) {
258 | this._moveInProgress = false;
259 | this._hardMove = e.hard;
260 | this.ProcessView();
261 | },
262 |
263 | _zoomStart: function() {
264 | this._zoomInProgress = true;
265 | },
266 |
267 | _zoomEnd: function() {
268 | this._zoomInProgress = false;
269 | this.ProcessView();
270 | },
271 |
272 | ProcessView: function () {
273 | // Don't do anything during the map manipulation
274 | if (!this._map || this._zoomInProgress || this._moveInProgress) {
275 | return;
276 | }
277 |
278 | var map = this._map,
279 | bounds = map.getBounds(),
280 | zoom = Math.floor(map.getZoom()),
281 | marginRatio = this.clusterMargin / this.Cluster.Size,
282 | resetIcons = this._resetIcons;
283 |
284 | var southWest = bounds.getSouthWest(),
285 | northEast = bounds.getNorthEast();
286 |
287 | // First step : Compute the clusters
288 | var clusters: PruneCluster.Cluster[] = this.Cluster.ProcessView({
289 | minLat: southWest.lat,
290 | minLng: southWest.lng,
291 | maxLat: northEast.lat,
292 | maxLng: northEast.lng
293 | });
294 |
295 | var objectsOnMap: PruneCluster.Cluster[] = this._objectsOnMap,
296 | newObjectsOnMap: PruneCluster.Cluster[] = [],
297 | markersOnMap: PruneCluster.LeafletMarker[] = new Array(objectsOnMap.length);
298 |
299 | // Second step : By default, all the leaflet markers should be removed
300 | for (var i = 0, l = objectsOnMap.length; i < l; ++i) {
301 | var marker = (objectsOnMap[i].data)._leafletMarker;
302 | markersOnMap[i] = marker;
303 | marker._removeFromMap = true;
304 | }
305 |
306 | var clusterCreationList: PruneCluster.Cluster[] = [];
307 | var clusterCreationListPopOne: PruneCluster.Cluster[] = [];
308 |
309 | var opacityUpdateList = [];
310 |
311 | // Third step : anti collapsing system
312 | // => merge collapsing cluster using a sweep and prune algorithm
313 | var workingList: PruneCluster.Cluster[] = [];
314 |
315 | for (i = 0, l = clusters.length; i < l; ++i) {
316 | var icluster = clusters[i],
317 | iclusterData = icluster.data;
318 |
319 | var latMargin = (icluster.bounds.maxLat - icluster.bounds.minLat) * marginRatio,
320 | lngMargin = (icluster.bounds.maxLng - icluster.bounds.minLng) * marginRatio;
321 |
322 | for (var j = 0, ll = workingList.length; j < ll; ++j) {
323 | var c = workingList[j];
324 | if (c.bounds.maxLng < icluster.bounds.minLng) {
325 | workingList.splice(j, 1);
326 | --j;
327 | --ll;
328 | continue;
329 | }
330 |
331 | var oldMaxLng = c.averagePosition.lng + lngMargin,
332 | oldMinLat = c.averagePosition.lat - latMargin,
333 | oldMaxLat = c.averagePosition.lat + latMargin,
334 | newMinLng = icluster.averagePosition.lng - lngMargin,
335 | newMinLat = icluster.averagePosition.lat - latMargin,
336 | newMaxLat = icluster.averagePosition.lat + latMargin;
337 |
338 | // Collapsing detected
339 | if (oldMaxLng > newMinLng && oldMaxLat > newMinLat && oldMinLat < newMaxLat) {
340 | iclusterData._leafletCollision = true;
341 | c.ApplyCluster(icluster);
342 | break;
343 | }
344 | }
345 |
346 | // If the object is not in collision, we keep it in the process
347 | if (!iclusterData._leafletCollision) {
348 | workingList.push(icluster);
349 | }
350 |
351 | }
352 |
353 | // Fourth step : update the already existing leaflet markers and create
354 | // a list of required new leaflet markers
355 | clusters.forEach((cluster: PruneCluster.Cluster) => {
356 | var m = undefined;
357 | var data = cluster.data;
358 |
359 | // Ignore collapsing clusters detected by the previous step
360 | if (data._leafletCollision) {
361 | // Reset these clusters
362 | data._leafletCollision = false;
363 | data._leafletOldPopulation = 0;
364 | data._leafletOldHashCode = 0;
365 | return;
366 | }
367 |
368 | var position = new L.LatLng(cluster.averagePosition.lat, cluster.averagePosition.lng);
369 |
370 | // If the cluster is already attached to a leaflet marker
371 | var oldMarker = data._leafletMarker;
372 | if (oldMarker) {
373 |
374 | // If it's a single marker and it doesn't have changed
375 | if (cluster.population === 1 && data._leafletOldPopulation === 1 && cluster.hashCode === oldMarker._hashCode) {
376 | // Update if the zoom level has changed or if we need to reset the icon
377 | if (resetIcons || oldMarker._zoomLevel !== zoom || cluster.lastMarker.data.forceIconRedraw) {
378 | this.PrepareLeafletMarker(
379 | oldMarker,
380 | cluster.lastMarker.data,
381 | cluster.lastMarker.category);
382 | if (cluster.lastMarker.data.forceIconRedraw) {
383 | cluster.lastMarker.data.forceIconRedraw = false;
384 | }
385 | }
386 | // Update the position
387 | oldMarker.setLatLng(position);
388 | m = oldMarker;
389 |
390 | // If it's a cluster marker on the same position
391 | } else if (cluster.population > 1 && data._leafletOldPopulation > 1 && (oldMarker._zoomLevel === zoom ||
392 | data._leafletPosition.equals(position))) {
393 |
394 | // Update the position
395 | oldMarker.setLatLng(position);
396 |
397 | // Update the icon if the population of his content has changed or if we need to reset the icon
398 | if (resetIcons || cluster.population != data._leafletOldPopulation ||
399 | cluster.hashCode !== data._leafletOldHashCode) {
400 | var boundsCopy = {};
401 | L.Util.extend(boundsCopy, cluster.bounds);
402 | (oldMarker)._leafletClusterBounds = boundsCopy;
403 | oldMarker.setIcon(this.BuildLeafletClusterIcon(cluster));
404 | }
405 |
406 | data._leafletOldPopulation = cluster.population;
407 | data._leafletOldHashCode = cluster.hashCode;
408 | m = oldMarker;
409 | }
410 |
411 | }
412 |
413 | // If a leaflet marker is unfound,
414 | // register it in the creation waiting list
415 | if (!m) {
416 | // Clusters with a single marker are placed at the beginning
417 | // of the cluster creation list, to recycle them in priority
418 | if (cluster.population === 1) {
419 | clusterCreationListPopOne.push(cluster);
420 | } else {
421 | clusterCreationList.push(cluster);
422 | }
423 |
424 | data._leafletPosition = position;
425 | data._leafletOldPopulation = cluster.population;
426 | data._leafletOldHashCode = cluster.hashCode;
427 | } else {
428 | // The leafet marker is used, we don't need to remove it anymore
429 | m._removeFromMap = false;
430 | newObjectsOnMap.push(cluster);
431 |
432 | // Update the properties
433 | m._zoomLevel = zoom;
434 | m._hashCode = cluster.hashCode;
435 | m._population = cluster.population;
436 | data._leafletMarker = m;
437 | data._leafletPosition = position;
438 | }
439 |
440 | });
441 |
442 | // Fifth step : recycle leaflet markers using a sweep and prune algorithm
443 | // The purpose of this step is to make smooth transition when a cluster or a marker
444 | // is moving on the map and its grid cell changes
445 | clusterCreationList = clusterCreationListPopOne.concat(clusterCreationList);
446 |
447 | for (i = 0, l = objectsOnMap.length; i < l; ++i) {
448 | icluster = objectsOnMap[i];
449 | var idata = icluster.data;
450 | marker = idata._leafletMarker;
451 |
452 | // We do not recycle markers already in use
453 | if (idata._leafletMarker._removeFromMap) {
454 |
455 | // If the sweep and prune algorithm doesn't find anything,
456 | // the leaflet marker can't be recycled and it will be removed
457 | var remove = true;
458 |
459 | // Recycle marker only with the same zoom level
460 | if (marker._zoomLevel === zoom) {
461 | var pa = icluster.averagePosition;
462 |
463 | latMargin = (icluster.bounds.maxLat - icluster.bounds.minLat) * marginRatio,
464 | lngMargin = (icluster.bounds.maxLng - icluster.bounds.minLng) * marginRatio;
465 |
466 | for (j = 0, ll = clusterCreationList.length; j < ll; ++j) {
467 | var jcluster = clusterCreationList[j],
468 | jdata = jcluster.data;
469 |
470 |
471 | // If luckily it's the same single marker
472 | if (marker._population === 1 && jcluster.population === 1 &&
473 | marker._hashCode === jcluster.hashCode) {
474 |
475 | // I we need to reset the icon
476 | if (resetIcons || jcluster.lastMarker.data.forceIconRedraw) {
477 | this.PrepareLeafletMarker(
478 | marker,
479 | jcluster.lastMarker.data,
480 | jcluster.lastMarker.category);
481 |
482 | if (jcluster.lastMarker.data.forceIconRedraw) {
483 | jcluster.lastMarker.data.forceIconRedraw = false;
484 | }
485 | }
486 |
487 | // Update the position
488 | marker.setLatLng(jdata._leafletPosition);
489 | remove = false;
490 |
491 | } else {
492 |
493 | var pb = jcluster.averagePosition;
494 | var oldMinLng = pa.lng - lngMargin,
495 | newMaxLng = pb.lng + lngMargin;
496 |
497 | oldMaxLng = pa.lng + lngMargin;
498 | oldMinLat = pa.lat - latMargin;
499 | oldMaxLat = pa.lat + latMargin;
500 | newMinLng = pb.lng - lngMargin;
501 | newMinLat = pb.lat - latMargin;
502 | newMaxLat = pb.lat + latMargin;
503 |
504 | // If it's a cluster marker
505 | // and if a collapsing leaflet marker is found, it may be recycled
506 | if ((marker._population > 1 && jcluster.population > 1) &&
507 | (oldMaxLng > newMinLng && oldMinLng < newMaxLng && oldMaxLat > newMinLat && oldMinLat < newMaxLat)) {
508 | // Update everything
509 | marker.setLatLng(jdata._leafletPosition);
510 | marker.setIcon(this.BuildLeafletClusterIcon(jcluster));
511 | var poisson = {};
512 | L.Util.extend(poisson, jcluster.bounds);
513 | (marker)._leafletClusterBounds = poisson;
514 | jdata._leafletOldPopulation = jcluster.population;
515 | jdata._leafletOldHashCode = jcluster.hashCode;
516 | marker._population = jcluster.population;
517 | remove = false;
518 | }
519 | }
520 |
521 | // If the leaflet marker is recycled
522 | if (!remove) {
523 |
524 | // Register the new marker
525 | jdata._leafletMarker = marker;
526 | marker._removeFromMap = false;
527 | newObjectsOnMap.push(jcluster);
528 |
529 | // Remove it from the sweep and prune working list
530 | clusterCreationList.splice(j, 1);
531 | --j;
532 | --ll;
533 |
534 | break;
535 | }
536 | }
537 | }
538 |
539 | // If sadly the leaflet marker can't be recycled
540 | if (remove) {
541 | if (!marker._removeFromMap) console.error("wtf");
542 | }
543 | }
544 | }
545 |
546 | // Sixth step : Create the new leaflet markers
547 | for (i = 0, l = clusterCreationList.length; i < l; ++i) {
548 | icluster = clusterCreationList[i],
549 | idata = icluster.data;
550 |
551 | var iposition = idata._leafletPosition;
552 |
553 | var creationMarker: any;
554 | if (icluster.population === 1) {
555 | creationMarker = this.BuildLeafletMarker(icluster.lastMarker, iposition);
556 | } else {
557 | creationMarker = this.BuildLeafletCluster(icluster, iposition);
558 | }
559 |
560 | creationMarker.addTo(map);
561 |
562 | // Fading in transition
563 | // (disabled by default with no-anim)
564 | // if(creationMarker._icon) L.DomUtil.addClass(creationMarker._icon, "no-anim");
565 | creationMarker.setOpacity(0);
566 | opacityUpdateList.push(creationMarker);
567 |
568 | idata._leafletMarker = creationMarker;
569 | creationMarker._zoomLevel = zoom;
570 | creationMarker._hashCode = icluster.hashCode;
571 | creationMarker._population = icluster.population;
572 |
573 | newObjectsOnMap.push(icluster);
574 | }
575 |
576 | // Start the fading in transition
577 | window.setTimeout(() => {
578 | for (i = 0, l = opacityUpdateList.length; i < l; ++i) {
579 | var m = opacityUpdateList[i];
580 | if(m._icon) L.DomUtil.addClass(m._icon, "prunecluster-anim");
581 | if(m._shadow) L.DomUtil.addClass(m._shadow, "prunecluster-anim");
582 | m.setOpacity(1);
583 | }
584 | }, 1);
585 |
586 | // Remove the remaining unused markers
587 | if (this._hardMove) {
588 | for (i = 0, l = markersOnMap.length; i < l; ++i) {
589 | marker = markersOnMap[i];
590 | if (marker._removeFromMap) {
591 | map.removeLayer(marker);
592 | }
593 | }
594 | } else {
595 | if (this._removeTimeoutId !== 0) {
596 | window.clearTimeout(this._removeTimeoutId);
597 | for (i = 0, l = this._markersRemoveListTimeout.length; i < l; ++i) {
598 | map.removeLayer(this._markersRemoveListTimeout[i]);
599 | }
600 | }
601 |
602 | var toRemove = [];
603 | for (i = 0, l = markersOnMap.length; i < l; ++i) {
604 | marker = markersOnMap[i];
605 | if (marker._removeFromMap) {
606 | marker.setOpacity(0);
607 | toRemove.push(marker);
608 | }
609 | }
610 | if (toRemove.length > 0) {
611 | this._removeTimeoutId = window.setTimeout(() => {
612 | for (i = 0, l = toRemove.length; i < l; ++i) {
613 | map.removeLayer(toRemove[i]);
614 | }
615 | this._removeTimeoutId = 0;
616 | }, 300);
617 | }
618 | this._markersRemoveListTimeout = toRemove;
619 | }
620 |
621 | this._objectsOnMap = newObjectsOnMap;
622 | this._hardMove = false;
623 | this._resetIcons = false;
624 | },
625 |
626 | FitBounds: function(withFiltered: boolean = true) {
627 | var bounds: PruneCluster.Bounds = this.Cluster.ComputeGlobalBounds(withFiltered);
628 | if (bounds) {
629 | this._map.fitBounds(new L.LatLngBounds(
630 | new L.LatLng(bounds.minLat, bounds.maxLng),
631 | new L.LatLng(bounds.maxLat, bounds.minLng)));
632 | }
633 | },
634 |
635 | GetMarkers: function() {
636 | return this.Cluster.GetMarkers();
637 | },
638 |
639 | RedrawIcons: function (processView: boolean = true) {
640 | this._resetIcons = true;
641 | if (processView) {
642 | this.ProcessView();
643 | }
644 | }
645 | });
646 |
--------------------------------------------------------------------------------
/LeafletSpiderfier.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // Based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet and
4 | // https://github.com/Leaflet/Leaflet.markercluster because it works very perfectly
5 |
6 |
7 | var PruneClusterLeafletSpiderfier = ((L).Layer ? (L).Layer : L.Class).extend({
8 | _2PI: Math.PI * 2,
9 | _circleFootSeparation: 25, //related to circumference of circle
10 | _circleStartAngle: Math.PI / 6,
11 |
12 | _spiralFootSeparation: 28, //related to size of spiral (experiment!)
13 | _spiralLengthStart: 11,
14 | _spiralLengthFactor: 5,
15 |
16 | _spiralCountTrigger: 8,
17 |
18 | spiderfyDistanceMultiplier: 1,
19 |
20 | initialize: function(cluster: PruneCluster.LeafletAdapter) {
21 | this._cluster = cluster;
22 | this._currentMarkers = [];
23 |
24 | this._multiLines = !!L.multiPolyline;
25 | this._lines = this._multiLines ?
26 | L.multiPolyline([], { weight: 1.5, color: '#222' }) :
27 | L.polyline([], { weight: 1.5, color: '#222' });
28 | },
29 |
30 | onAdd: function(map: L.Map) {
31 | this._map = map;
32 |
33 | this._map.on('overlappingmarkers', this.Spiderfy, this);
34 |
35 | this._map.on('click', this.Unspiderfy, this);
36 | this._map.on('zoomend', this.Unspiderfy, this);
37 | },
38 |
39 | Spiderfy: function (data) {
40 | // Ignore events from other PruneCluster instances
41 | if (data.cluster !== this._cluster) {
42 | return;
43 | }
44 |
45 | this.Unspiderfy();
46 | var markers = data.markers.filter(function(marker) {
47 | return !marker.filtered;
48 | });
49 |
50 | this._currentCenter = data.center;
51 |
52 | var centerPoint = this._map.latLngToLayerPoint(data.center);
53 |
54 | var points: L.Point[];
55 | if (markers.length >= this._spiralCountTrigger) {
56 | points = this._generatePointsSpiral(markers.length, centerPoint);
57 | } else {
58 | if (this._multiLines) { // if multilines, leaflet < 0.8
59 | centerPoint.y += 10; // center fix
60 | }
61 | points = this._generatePointsCircle(markers.length, centerPoint);
62 | }
63 |
64 | var polylines: L.LatLng[][] = [];
65 |
66 |
67 | var leafletMarkers: L.Marker[] = [];
68 | var projectedPoints: L.LatLng[] = [];
69 |
70 | for (var i = 0, l = points.length; i < l; ++i) {
71 | var pos = this._map.layerPointToLatLng(points[i]);
72 | var m = this._cluster.BuildLeafletMarker(markers[i], data.center);
73 | m.setZIndexOffset(5000);
74 | m.setOpacity(0);
75 |
76 | // polylines.push([data.center, pos]);
77 |
78 | this._currentMarkers.push(m);
79 | this._map.addLayer(m);
80 |
81 | leafletMarkers.push(m);
82 | projectedPoints.push(pos);
83 | }
84 |
85 | window.setTimeout(() => {
86 | for (i = 0, l = points.length; i < l; ++i) {
87 | leafletMarkers[i].setLatLng(projectedPoints[i])
88 | .setOpacity(1);
89 | }
90 |
91 | var startTime = +new Date();
92 |
93 | var interval = 42, duration = 290;
94 | var anim = window.setInterval(() => {
95 |
96 | polylines = [];
97 |
98 | var now = +new Date();
99 | var d = now - startTime;
100 | if (d >= duration) {
101 | window.clearInterval(anim);
102 | stepRatio = 1.0;
103 | } else {
104 | var stepRatio = d / duration;
105 | }
106 |
107 | var center = data.center;
108 |
109 | for (i = 0, l = points.length; i < l; ++i) {
110 | var p = projectedPoints[i],
111 | diffLat = p.lat - center.lat,
112 | diffLng = p.lng - center.lng;
113 |
114 | polylines.push([center, new L.LatLng(center.lat + diffLat * stepRatio, center.lng + diffLng * stepRatio)]);
115 | }
116 |
117 | this._lines.setLatLngs(polylines);
118 |
119 | }, interval);
120 | }, 1);
121 |
122 | this._lines.setLatLngs(polylines);
123 | this._map.addLayer(this._lines);
124 |
125 | if (data.marker) {
126 | this._clusterMarker = data.marker.setOpacity(0.3);
127 | }
128 | },
129 |
130 | _generatePointsCircle: function(count: number, centerPt: L.Point): L.Point[] {
131 | var circumference = this.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count),
132 | legLength = circumference / this._2PI, //radius from circumference
133 | angleStep = this._2PI / count,
134 | res = [],
135 | i,
136 | angle;
137 |
138 | res.length = count;
139 |
140 | for (i = count - 1; i >= 0; i--) {
141 | angle = this._circleStartAngle + i * angleStep;
142 | res[i] = new L.Point(
143 | Math.round(centerPt.x + legLength * Math.cos(angle)),
144 | Math.round(centerPt.y + legLength * Math.sin(angle)));
145 | }
146 |
147 | return res;
148 | },
149 |
150 | _generatePointsSpiral: function(count: number, centerPt: L.Point): L.Point[] {
151 | var legLength = this.spiderfyDistanceMultiplier * this._spiralLengthStart,
152 | separation = this.spiderfyDistanceMultiplier * this._spiralFootSeparation,
153 | lengthFactor = this.spiderfyDistanceMultiplier * this._spiralLengthFactor,
154 | angle = 0,
155 | res = [],
156 | i;
157 |
158 | res.length = count;
159 |
160 | for (i = count - 1; i >= 0; i--) {
161 | angle += separation / legLength + i * 0.0005;
162 | res[i] = new L.Point(
163 | Math.round(centerPt.x + legLength * Math.cos(angle)),
164 | Math.round(centerPt.y + legLength * Math.sin(angle)));
165 | legLength += this._2PI * lengthFactor / angle;
166 | }
167 | return res;
168 | },
169 |
170 | Unspiderfy: function() {
171 | for (var i = 0, l = this._currentMarkers.length; i < l; ++i) {
172 | this._currentMarkers[i].setLatLng(this._currentCenter).setOpacity(0);
173 | }
174 |
175 | var map = this._map;
176 | var markers = this._currentMarkers;
177 | window.setTimeout(() => {
178 | for (i = 0, l = markers.length; i < l; ++i) {
179 | map.removeLayer(markers[i]);
180 | }
181 |
182 | }, 300);
183 |
184 | this._currentMarkers = [];
185 |
186 | this._map.removeLayer(this._lines);
187 | if (this._clusterMarker) {
188 | this._clusterMarker.setOpacity(1);
189 | }
190 | },
191 |
192 | onRemove: function(map: L.Map) {
193 | this.Unspiderfy();
194 | map.off('overlappingmarkers', this.Spiderfy, this);
195 | map.off('click', this.Unspiderfy, this);
196 | map.off('zoomend', this.Unspiderfy, this);
197 | }
198 | });
199 |
--------------------------------------------------------------------------------
/LeafletStyleSheet.css:
--------------------------------------------------------------------------------
1 | .prunecluster {
2 | font-size: 12px;
3 | border-radius: 20px;
4 | transition: all 0.3s linear;
5 | }
6 | .leaflet-marker-icon.prunecluster-anim,
7 | .leaflet-marker-shadow.prunecluster-anim,
8 | .leaflet-markercluster-icon.prunecluster-anim {
9 | transition: all 0.3s linear;
10 | }
11 |
12 | .leaflet-zoom-anim .leaflet-zoom-animated.leaflet-marker-icon,
13 | .leaflet-zoom-anim .leaflet-zoom-animated.leaflet-marker-shadow,
14 | .leaflet-zoom-anim .leaflet-zoom-animated.leaflet-markercluster-icon {
15 | transition: transform 0.25s cubic-bezier(0,0,0.25,1);
16 | }
17 | .prunecluster div {
18 | width: 30px;
19 | height: 30px;
20 | text-align: center;
21 | margin-left: 5px;
22 | margin-top: 5px;
23 | border-radius: 50%;
24 | }
25 | .prunecluster div span {
26 | line-height: 30px;
27 | }
28 |
29 | .prunecluster-small {
30 | background-color: #b5e28c;
31 | background-color: rgba(181, 226, 140, 0.6);
32 | }
33 |
34 | .prunecluster-small div {
35 | width: 28px;
36 | height: 28px;
37 | background-color: #6ecc39;
38 | background-color: rgba(110, 204, 57, 0.6);
39 | }
40 |
41 | .prunecluster-small div span {
42 | line-height: 28px;
43 | }
44 |
45 | .prunecluster-medium {
46 | background-color: #f1d357;
47 | background-color: rgba(241, 211, 87, 0.6);
48 | }
49 |
50 | .prunecluster-medium div {
51 | background-color: #f0c20c;
52 | background-color: rgba(240, 194, 12, 0.6);
53 | }
54 |
55 | .prunecluster-large {
56 | background-color: #fd9c73;
57 | background-color: rgba(253, 156, 115, 0.6);
58 | }
59 |
60 | .prunecluster-large div {
61 | width: 34px;
62 | height: 34px;
63 | background-color: #f18017;
64 | background-color: rgba(241, 128, 23, 0.6);
65 | }
66 |
67 | .prunecluster-large div span {
68 | line-height: 34px;
69 | }
70 |
--------------------------------------------------------------------------------
/PruneCluster.ts:
--------------------------------------------------------------------------------
1 | namespace PruneCluster {
2 |
3 | // The position is the real position of the object
4 | // using a standard coordinate system, as WGS 84
5 | export interface Position {
6 | lat: number;
7 | lng: number;
8 | }
9 |
10 | // The point is a project position on the client display
11 | export class Point {
12 | x: number;
13 | y: number;
14 | }
15 |
16 | export interface Bounds {
17 | minLat: number;
18 | maxLat: number;
19 | minLng: number;
20 | maxLng: number;
21 | }
22 |
23 | export class ClusterObject {
24 | // Map position of the object
25 | public position: Position;
26 |
27 | // An attached javascript object, storing user data
28 | public data: any;
29 |
30 | // An hashCode identifing the object
31 | public hashCode: number;
32 | }
33 |
34 | // Hidden variable counting the number of created hashcode
35 | var hashCodeCounter: number = 1;
36 |
37 | // Number.MAX_SAFE_INTEGER
38 | var maxHashCodeValue = Math.pow(2, 53) - 1;
39 |
40 | export class Marker extends ClusterObject {
41 |
42 | // The category of the Marker, ideally a number between 0 and 7
43 | // can also be a string
44 | public category: number;
45 |
46 | // The weight of a Marker can influence the cluster icon or the cluster position
47 | public weight: number;
48 |
49 | // If filtered is true, the marker is not included in the clustering
50 | // With some datasets, it's faster to keep the markers inside PruneCluster and to
51 | // use the filtering feature. With some other datasets, it's better to remove the
52 | // markers
53 | public filtered: boolean;
54 |
55 | constructor(lat: number, lng: number, data: {} = {},
56 | category?: number, weight: number = 1, filtered: boolean = false) {
57 | super();
58 | this.data = data;
59 | this.position = { lat: +lat, lng: +lng };
60 | this.weight = weight;
61 | this.category = category;
62 | this.filtered = filtered;
63 |
64 | // The hashCode is used to identify the Cluster object
65 | this.hashCode = hashCodeCounter++;
66 | }
67 |
68 | public Move(lat: number, lng: number) {
69 | this.position.lat = +lat;
70 | this.position.lng = +lng;
71 | }
72 |
73 | // Apply the data object
74 | public SetData(data: any) {
75 | for (var key in data) {
76 | this.data[key] = data[key];
77 | }
78 | }
79 | }
80 |
81 | export class Cluster extends ClusterObject {
82 | // Cluster area
83 | public bounds: Bounds;
84 |
85 | // Number of markers clustered
86 | public population: number;
87 |
88 | // Average position of the cluster,
89 | // taking into account the cluster weight
90 | public averagePosition: Position;
91 |
92 | // Statistics table
93 | // The key is the category and the value is the sum
94 | // of the weights
95 | public stats: number[];
96 |
97 | // The total weight of the cluster
98 | public totalWeight: number;
99 |
100 | // The last marker added in the cluster
101 | // Usefull when the cluster contains only one marker
102 | public lastMarker: Marker;
103 |
104 | // If enabled, the cluster contains a list of his marker
105 | // It implies a performance cost, but you can use it
106 | // for building the icon, if your dataset is not too big
107 | public static ENABLE_MARKERS_LIST: boolean = false;
108 |
109 | // The list of markers in the cluster
110 | private _clusterMarkers: Marker[];
111 |
112 | constructor(marker?: Marker) {
113 | super();
114 |
115 | // Create a stats table optimized for categories between 0 and 7
116 | this.stats = [0, 0, 0, 0, 0, 0, 0, 0];
117 | this.data = {};
118 |
119 |
120 | // You can provide a marker directly in the constructor
121 | // It's like using AddMarker, but a bit faster
122 | if (!marker) {
123 | this.hashCode = 1;
124 | if (Cluster.ENABLE_MARKERS_LIST) {
125 | this._clusterMarkers = [];
126 | }
127 | return;
128 | }
129 |
130 | if (Cluster.ENABLE_MARKERS_LIST) {
131 | this._clusterMarkers = [marker];
132 | }
133 |
134 | this.lastMarker = marker;
135 |
136 | this.hashCode = 31 + marker.hashCode;
137 |
138 | this.population = 1;
139 |
140 | if (marker.category !== undefined) {
141 | this.stats[marker.category] = 1;
142 | }
143 |
144 | this.totalWeight = marker.weight;
145 |
146 | this.position = {
147 | lat: marker.position.lat,
148 | lng: marker.position.lng
149 | };
150 |
151 | this.averagePosition = {
152 | lat: marker.position.lat,
153 | lng: marker.position.lng
154 | };
155 |
156 | }
157 |
158 | public AddMarker(marker: Marker) {
159 |
160 | if (Cluster.ENABLE_MARKERS_LIST) {
161 | this._clusterMarkers.push(marker);
162 | }
163 |
164 | var h = this.hashCode;
165 | h = ((h << 5) - h) + marker.hashCode;
166 | if (h >= maxHashCodeValue) {
167 | this.hashCode = h % maxHashCodeValue;
168 | } else {
169 | this.hashCode = h;
170 | }
171 |
172 | this.lastMarker = marker;
173 |
174 | // Compute the weighted arithmetic mean
175 | var weight = marker.weight,
176 | currentTotalWeight = this.totalWeight,
177 | newWeight = weight + currentTotalWeight;
178 |
179 | this.averagePosition.lat =
180 | (this.averagePosition.lat * currentTotalWeight +
181 | marker.position.lat * weight) / newWeight;
182 |
183 | this.averagePosition.lng =
184 | (this.averagePosition.lng * currentTotalWeight +
185 | marker.position.lng * weight) / newWeight;
186 |
187 | ++this.population;
188 | this.totalWeight = newWeight;
189 |
190 | // Update the statistics if needed
191 | if (marker.category !== undefined) {
192 | this.stats[marker.category] = (this.stats[marker.category] + 1) || 1;
193 | }
194 | }
195 |
196 | public Reset() {
197 | this.hashCode = 1;
198 | this.lastMarker = undefined;
199 | this.population = 0;
200 | this.totalWeight = 0;
201 | this.stats = [0, 0, 0, 0, 0, 0, 0, 0];
202 |
203 | if (Cluster.ENABLE_MARKERS_LIST) {
204 | this._clusterMarkers = [];
205 | }
206 | }
207 |
208 | // Compute the bounds
209 | // Settle the cluster to the projected grid
210 | public ComputeBounds(cluster: PruneCluster.PruneCluster) {
211 |
212 | var proj = cluster.Project(this.position.lat, this.position.lng);
213 |
214 | var size = cluster.Size;
215 |
216 | // Compute the position of the cluster
217 | var nbX = Math.floor(proj.x / size),
218 | nbY = Math.floor(proj.y / size),
219 | startX = nbX * size,
220 | startY = nbY * size;
221 |
222 | // Project it to lat/lng values
223 | var a = cluster.UnProject(startX, startY),
224 | b = cluster.UnProject(startX + size, startY + size);
225 |
226 | this.bounds = {
227 | minLat: b.lat,
228 | maxLat: a.lat,
229 | minLng: a.lng,
230 | maxLng: b.lng
231 | };
232 | }
233 |
234 | public GetClusterMarkers() {
235 | return this._clusterMarkers;
236 | }
237 |
238 | public ApplyCluster(newCluster: Cluster) {
239 |
240 | this.hashCode = this.hashCode * 41 + newCluster.hashCode * 43;
241 | if (this.hashCode > maxHashCodeValue) {
242 | this.hashCode = this.hashCode = maxHashCodeValue;
243 | }
244 |
245 | var weight = newCluster.totalWeight,
246 | currentTotalWeight = this.totalWeight,
247 | newWeight = weight + currentTotalWeight;
248 |
249 | this.averagePosition.lat =
250 | (this.averagePosition.lat * currentTotalWeight +
251 | newCluster.averagePosition.lat * weight) / newWeight;
252 |
253 | this.averagePosition.lng =
254 | (this.averagePosition.lng * currentTotalWeight +
255 | newCluster.averagePosition.lng * weight) / newWeight;
256 |
257 | this.population += newCluster.population;
258 | this.totalWeight = newWeight;
259 |
260 | // Merge the bounds
261 | this.bounds.minLat = Math.min(this.bounds.minLat, newCluster.bounds.minLat);
262 | this.bounds.minLng = Math.min(this.bounds.minLng, newCluster.bounds.minLng);
263 | this.bounds.maxLat = Math.max(this.bounds.maxLat, newCluster.bounds.maxLat);
264 | this.bounds.maxLng = Math.max(this.bounds.maxLng, newCluster.bounds.maxLng);
265 |
266 | // Merge the statistics
267 | for (var category in newCluster.stats) {
268 | if (newCluster.stats.hasOwnProperty(category)) {
269 | if (this.stats.hasOwnProperty(category)) {
270 | this.stats[category] += newCluster.stats[category];
271 | } else {
272 | this.stats[category] = newCluster.stats[category];
273 | }
274 | }
275 | }
276 |
277 | // Merge the clusters lists
278 | if (Cluster.ENABLE_MARKERS_LIST) {
279 | this._clusterMarkers = this._clusterMarkers.concat(newCluster.GetClusterMarkers());
280 | }
281 | }
282 | }
283 |
284 | function checkPositionInsideBounds(a: Position, b: Bounds): boolean {
285 | return (a.lat >= b.minLat && a.lat <= b.maxLat) &&
286 | a.lng >= b.minLng && a.lng <= b.maxLng;
287 | }
288 |
289 | function insertionSort(list: ClusterObject[]) {
290 | for (var i: number = 1,
291 | j: number,
292 | tmp: ClusterObject,
293 | tmpLng: number,
294 | length = list.length; i < length; ++i) {
295 | tmp = list[i];
296 | tmpLng = tmp.position.lng;
297 | for (j = i - 1; j >= 0 && list[j].position.lng > tmpLng; --j) {
298 | list[j + 1] = list[j];
299 | }
300 | list[j + 1] = tmp;
301 | }
302 | }
303 |
304 | // PruneCluster must work on a sorted collection
305 | // the insertion sort is preferred for its stability and its performances
306 | // on sorted or almost sorted collections.
307 | //
308 | // However the insertion sort's worst case is extreme and we should avoid it.
309 | function shouldUseInsertionSort(total: number, nbChanges: number): boolean {
310 | if (nbChanges > 300) {
311 | return false;
312 | } else {
313 | return (nbChanges / total) < 0.2;
314 | }
315 | }
316 |
317 | export class PruneCluster {
318 | private _markers: Marker[] = [];
319 |
320 | // Represent the number of marker added or deleted since the last sort
321 | private _nbChanges: number = 0;
322 |
323 | private _clusters: Cluster[] = [];
324 |
325 | // Cluster size in (in pixels)
326 | public Size: number = 166;
327 |
328 | // View padding (extended size of the view)
329 | public ViewPadding: number = 0.2;
330 |
331 | // These methods should be defined by the user
332 | public Project: (lat: number, lng: number) => Point;
333 | public UnProject: (x: number, y: number) => Position;
334 |
335 | public RegisterMarker(marker: Marker) {
336 | if ((marker)._removeFlag) {
337 | delete (marker)._removeFlag;
338 | }
339 | this._markers.push(marker);
340 | this._nbChanges += 1;
341 | }
342 |
343 | public RegisterMarkers(markers: Marker[]) {
344 | markers.forEach((marker: Marker) => {
345 | this.RegisterMarker(marker);
346 | });
347 | }
348 |
349 | private _sortMarkers() {
350 | var markers = this._markers,
351 | length = markers.length;
352 |
353 | if (this._nbChanges && !shouldUseInsertionSort(length, this._nbChanges)) {
354 | // native (n log n) sort
355 | this._markers.sort((a: Marker, b: Marker) => a.position.lng - b.position.lng);
356 | } else {
357 | insertionSort(markers); // faster for almost-sorted arrays
358 | }
359 |
360 | // Now the list is sorted, we can reset the counter
361 | this._nbChanges = 0;
362 | }
363 |
364 | private _sortClusters() {
365 | // Insertion sort because the list is often almost sorted
366 | // and we want to have a stable list of clusters
367 | insertionSort(this._clusters);
368 | }
369 |
370 | private _indexLowerBoundLng(lng: number): number {
371 | // Inspired by std::lower_bound
372 |
373 | // It's a binary search algorithm
374 | var markers = this._markers,
375 | it,
376 | step,
377 | first = 0,
378 | count = markers.length;
379 |
380 | while (count > 0) {
381 | step = Math.floor(count / 2);
382 | it = first + step;
383 | if (markers[it].position.lng < lng) {
384 | first = ++it;
385 | count -= step + 1;
386 | } else {
387 | count = step;
388 | }
389 | }
390 |
391 | return first;
392 | }
393 |
394 | private _resetClusterViews() {
395 | // Reset all the clusters
396 | for (var i = 0, l = this._clusters.length; i < l; ++i) {
397 | var cluster = this._clusters[i];
398 | cluster.Reset();
399 |
400 | // The projection changes in accordance with the view's zoom level
401 | // (at least with Leaflet.js)
402 | cluster.ComputeBounds(this);
403 | }
404 | }
405 |
406 | public ProcessView(bounds: Bounds): Cluster[] {
407 |
408 | // Compute the extended bounds of the view
409 | var heightBuffer = Math.abs(bounds.maxLat - bounds.minLat) * this.ViewPadding,
410 | widthBuffer = Math.abs(bounds.maxLng - bounds.minLng) * this.ViewPadding;
411 |
412 | var extendedBounds: Bounds = {
413 | minLat: bounds.minLat - heightBuffer - heightBuffer,
414 | maxLat: bounds.maxLat + heightBuffer + heightBuffer,
415 | minLng: bounds.minLng - widthBuffer - widthBuffer,
416 | maxLng: bounds.maxLng + widthBuffer + widthBuffer
417 | };
418 |
419 | // We keep the list of all markers sorted
420 | // It's faster to keep the list sorted so we can use
421 | // a insertion sort algorithm which is faster for sorted lists
422 | this._sortMarkers();
423 |
424 | // Reset the cluster for the new view
425 | this._resetClusterViews();
426 |
427 | // Binary search for the first interesting marker
428 | var firstIndex = this._indexLowerBoundLng(extendedBounds.minLng);
429 |
430 | // Just some shortcuts
431 | var markers = this._markers,
432 | clusters = this._clusters;
433 |
434 |
435 | var workingClusterList = clusters.slice(0);
436 |
437 | // For every markers in the list
438 | for (var i = firstIndex, l = markers.length; i < l; ++i) {
439 |
440 | var marker = markers[i],
441 | markerPosition = marker.position;
442 |
443 | // If the marker longitute is higher than the view longitude,
444 | // we can stop to iterate
445 | if (markerPosition.lng > extendedBounds.maxLng) {
446 | break;
447 | }
448 |
449 |
450 | // If the marker is inside the view and is not filtered
451 | if (markerPosition.lat > extendedBounds.minLat &&
452 | markerPosition.lat < extendedBounds.maxLat &&
453 | !marker.filtered) {
454 |
455 | var clusterFound = false, cluster: Cluster;
456 |
457 | // For every active cluster
458 | for (var j = 0, ll = workingClusterList.length; j < ll; ++j) {
459 | cluster = workingClusterList[j];
460 |
461 | // If the cluster is far away the current marker
462 | // we can remove it from the list of active clusters
463 | // because we will never reach it again
464 | if (cluster.bounds.maxLng < marker.position.lng) {
465 | workingClusterList.splice(j, 1);
466 | --j;
467 | --ll;
468 | continue;
469 | }
470 |
471 | if (checkPositionInsideBounds(markerPosition, cluster.bounds)) {
472 | cluster.AddMarker(marker);
473 | // We found a marker, we don't need to go further
474 | clusterFound = true;
475 | break;
476 | }
477 | }
478 |
479 |
480 | // If the marker doesn't fit in any cluster,
481 | // we must create a brand new cluster.
482 | if (!clusterFound) {
483 | cluster = new Cluster(marker);
484 | cluster.ComputeBounds(this);
485 | clusters.push(cluster);
486 | workingClusterList.push(cluster);
487 | }
488 | }
489 | }
490 |
491 |
492 | // Time to remove empty clusters
493 | var newClustersList: Cluster[] = [];
494 | for (i = 0, l = clusters.length; i < l; ++i) {
495 | cluster = clusters[i];
496 | if (cluster.population > 0) {
497 | newClustersList.push(cluster);
498 | }
499 | }
500 |
501 | this._clusters = newClustersList;
502 |
503 | // We keep the list of markers sorted, it's faster
504 | this._sortClusters();
505 |
506 | return this._clusters;
507 | }
508 |
509 | public RemoveMarkers(markers?: Marker[]) {
510 |
511 | // if markers are undefined, remove all
512 | if (!markers) {
513 | this._markers = [];
514 | return;
515 | }
516 |
517 | // Mark the markers to be deleted
518 | for (var i = 0, l = markers.length; i < l; ++i) {
519 | (markers[i])._removeFlag = true;
520 | }
521 |
522 | // Create a new list without the marked markers
523 | var newMarkersList = [];
524 | for (i = 0, l = this._markers.length; i < l; ++i) {
525 | if (!(this._markers[i])._removeFlag) {
526 | newMarkersList.push(this._markers[i]);
527 | }
528 | else{
529 | delete (this._markers[i])._removeFlag;
530 | }
531 | }
532 |
533 | this._markers = newMarkersList;
534 | }
535 |
536 | // This method is a bit slow ( O(n)) because it's not worth to make
537 | // system which will slow down all the clusters just to have
538 | // this one fast
539 | public FindMarkersInArea(area: Bounds): Marker[] {
540 | var aMinLat = area.minLat,
541 | aMaxLat = area.maxLat,
542 | aMinLng = area.minLng,
543 | aMaxLng = area.maxLng,
544 |
545 | markers = this._markers,
546 |
547 | result = [];
548 |
549 | var firstIndex = this._indexLowerBoundLng(aMinLng);
550 |
551 | for (var i = firstIndex, l = markers.length; i < l; ++i) {
552 | var pos = markers[i].position;
553 |
554 | if (pos.lng > aMaxLng) {
555 | break;
556 | }
557 |
558 | if (pos.lat >= aMinLat && pos.lat <= aMaxLat &&
559 | pos.lng >= aMinLng) {
560 |
561 | result.push(markers[i]);
562 | }
563 | }
564 |
565 | return result;
566 | }
567 |
568 | // Compute the bounds of the list of markers
569 | // It's slow O(n)
570 | public ComputeBounds(markers: Marker[], withFiltered: boolean = true): Bounds {
571 |
572 | if (!markers || !markers.length) {
573 | return null;
574 | }
575 |
576 | var rMinLat = Number.MAX_VALUE,
577 | rMaxLat = -Number.MAX_VALUE,
578 | rMinLng = Number.MAX_VALUE,
579 | rMaxLng = -Number.MAX_VALUE;
580 |
581 | for (var i = 0, l = markers.length; i < l; ++i) {
582 | if (!withFiltered && markers[i].filtered) {
583 | continue;
584 | }
585 | var pos = markers[i].position;
586 |
587 | if (pos.lat < rMinLat) rMinLat = pos.lat;
588 | if (pos.lat > rMaxLat) rMaxLat = pos.lat;
589 | if (pos.lng < rMinLng) rMinLng = pos.lng;
590 | if (pos.lng > rMaxLng) rMaxLng = pos.lng;
591 | }
592 |
593 | return {
594 | minLat: rMinLat,
595 | maxLat: rMaxLat,
596 | minLng: rMinLng,
597 | maxLng: rMaxLng
598 | };
599 | }
600 |
601 | public FindMarkersBoundsInArea(area: Bounds): Bounds {
602 | return this.ComputeBounds(this.FindMarkersInArea(area));
603 | }
604 |
605 | public ComputeGlobalBounds(withFiltered: boolean = true): Bounds {
606 | return this.ComputeBounds(this._markers, withFiltered);
607 | }
608 |
609 | public GetMarkers(): Marker[] {
610 | return this._markers;
611 | }
612 |
613 | public GetPopulation(): number {
614 | return this._markers.length;
615 | }
616 |
617 | public ResetClusters() {
618 | this._clusters = [];
619 | }
620 |
621 | }
622 | }
623 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | ============
3 |
4 | PruneCluster is a fast and realtime marker clustering library.
5 |
6 | *Example 1:* [150 000 randomly moving markers](http://sintef-9012.github.io/PruneCluster/examples/random.150000.html).
7 |
8 | 
9 | *Example 2: Realtime clusters of tweets.*
10 |
11 | It's working with [Leaflet](http://leafletjs.com/) as an alternative to [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster).
12 |
13 |
14 | *The library is designed for large datasets or live situations.* The memory consumption is kept low and the library is fast on mobile devices, thanks to a new algorithm inspired by collision detection in physical engines.
15 |
16 |
17 |
18 | ### Features
19 |
20 | #### Realtime
21 | The clusters can be updated in realtime. It's perfect for live datasets or datasets you want to filter at runtime.
22 |
23 | #### Fast
24 |
25 | Number of markers|First step|Update (low zoom level)|Update (high zoom level)
26 | ---------|------------------|------------------------|------------------
27 | [100](http://sintef-9012.github.io/PruneCluster/examples/random.100.html)|instant|instant|instant
28 | [1 000](http://sintef-9012.github.io/PruneCluster/examples/random.1000.html)|instant|instant|instant
29 | [10 000](http://sintef-9012.github.io/PruneCluster/examples/random.10000.html)|14ms|3ms|2ms
30 | [60 000](http://sintef-9012.github.io/PruneCluster/examples/random.60000.html)|70ms|23ms|9ms
31 | [150 000](http://sintef-9012.github.io/PruneCluster/examples/random.150000.html)|220ms|60ms|20ms
32 | [1 000 000](http://sintef-9012.github.io/PruneCluster/examples/random.1000000.html)|1.9s|400ms|135ms
33 |
34 | These values are tested with random positions, on a recent laptop, using Chrome 38. One half of markers is moving randomly and the other half is static. It is also fast enough for mobile devices.
35 |
36 | If you prefer real world data, the [50k Leaflet.markercluster example](http://sintef-9012.github.io/PruneCluster/examples/realworld.50000.html) is computed in 60ms *([original](http://sintef-9012.github.io/Leaflet.markercluster/example/marker-clustering-realworld.50000.html))*.
37 |
38 | #### Weight
39 | You can specify the weight of each marker.
40 |
41 | For example, you may want to add more importance to a marker representing an incident, than a marker representing a tweet.
42 |
43 | #### Categories
44 |
45 | You can specify a category for the markers. Then a small object representing the number of markers for each category is attached to the clusters. This way, you can create cluster icons adapted to their content.
46 |
47 | [](http://sintef-9012.github.io/PruneCluster/examples/random.10000-categories.html) [](http://sintef-9012.github.io/PruneCluster/examples/random.10000-categories-2.html)
48 |
49 | #### Dynamic cluster size
50 |
51 | The size of a cluster can be adjusted on the fly *([Example](http://sintef-9012.github.io/PruneCluster/examples/random.10000-size.html))*
52 |
53 | #### Filtering
54 | The markers can be filtered easily with no performance cost.
55 |
56 |
57 | ### Usage
58 |
59 | #### Classic Way
60 | ```html
61 |
62 |
65 |
66 |
67 |
70 |
71 | ```
72 |
73 | #### Webpack & NPM
74 |
75 | `npm install exports-loader prunecluster`
76 |
77 | ```javascript
78 | import { PruneCluster, PruneClusterForLeaflet } from 'exports-loader?PruneCluster,PruneClusterForLeaflet!prunecluster/dist/PruneCluster.js'
79 |
80 | ```
81 |
82 | #### Example
83 |
84 | ```javascript
85 | var pruneCluster = new PruneClusterForLeaflet();
86 |
87 | ...
88 | var marker = new PruneCluster.Marker(59.8717, 11.1909);
89 | pruneCluster.RegisterMarker(marker);
90 | ...
91 |
92 | leafletMap.addLayer(pruneCluster);
93 | ```
94 |
95 | ### PruneClusterForLeaflet constructor
96 |
97 | ```javascript
98 | PruneClusterForLeaflet([size](#set-the-clustering-size), margin);
99 | ```
100 |
101 | You can specify the size and margin which affect when your clusters and markers will be merged.
102 |
103 | size defaults to 120 and margin to 20.
104 |
105 | #### Update a position
106 | ```javascript
107 | marker.Move(lat, lng);
108 | ```
109 |
110 | #### Deletions
111 | ```javascript
112 | // Remove all the markers
113 | pruneCluster.RemoveMarkers();
114 |
115 | // Remove a list of markers
116 | pruneCluster.RemoveMarkers([markerA,markerB,...]);
117 | ```
118 |
119 | #### Set the category
120 | The category can be a number or a string, but in order to minimize the performance cost, it is recommended to use numbers between 0 and 7.
121 | ```javascript
122 | marker.category = 5;
123 | ```
124 |
125 | #### Set the weight
126 | ```javascript
127 | marker.weight = 4;
128 | ```
129 |
130 | #### Filtering
131 | ```javascript
132 | marker.filtered = true|false;
133 | ```
134 |
135 | #### Set the clustering size
136 | You can specify a number indicating the area of the cluster. Higher number means more markers "merged". *([Example](http://sintef-9012.github.io/PruneCluster/examples/random.10000-size.html))*
137 | ```javascript
138 | pruneCluster.Cluster.Size = 87;
139 | ```
140 |
141 | #### Apply the changes
142 |
143 | **Must be called when ANY changes are made.**
144 |
145 | ```javascript
146 | pruneCluster.ProcessView();
147 | ```
148 |
149 | #### Add custom data to marker object
150 |
151 | Each marker has a data object where you can specify your data.
152 | ```javascript
153 | marker.data.name = 'Roger';
154 | marker.data.ID = '76ez';
155 | ```
156 |
157 | #### Setting up a Leaflet icon or a Leaflet popup
158 |
159 | You can attach to the markers an icon object and a popup content
160 | ```javascript
161 | marker.data.icon = L.icon(...); // See http://leafletjs.com/reference.html#icon
162 | marker.data.popup = 'Popup content';
163 | ```
164 |
165 | #### Faster leaflet icons
166 |
167 | If you have a lot of markers, you can create the icons and popups on the fly in order to improve their performance.
168 |
169 | ```javascript
170 | function createIcon(data, category) {
171 | return L.icon(...);
172 | }
173 |
174 | ...
175 |
176 | marker.data.icon = createIcon;
177 | ```
178 |
179 | You can also override the PreapareLeafletMarker method. You can apply listeners to the markers here.
180 |
181 | ```javascript
182 | pruneCluster.PrepareLeafletMarker = function(leafletMarker, data) {
183 | leafletMarker.setIcon(/*... */); // See http://leafletjs.com/reference.html#icon
184 | //listeners can be applied to markers in this function
185 | leafletMarker.on('click', function(){
186 | //do click event logic here
187 | });
188 | // A popup can already be attached to the marker
189 | // bindPopup can override it, but it's faster to update the content instead
190 | if (leafletMarker.getPopup()) {
191 | leafletMarker.setPopupContent(data.name);
192 | } else {
193 | leafletMarker.bindPopup(data.name);
194 | }
195 | };
196 | ```
197 |
198 | #### Setting up a custom cluster icon
199 | ```javascript
200 | pruneCluster.BuildLeafletClusterIcon = function(cluster) {
201 | var population = cluster.population, // the number of markers inside the cluster
202 | stats = cluster.stats; // if you have categories on your markers
203 |
204 | // If you want list of markers inside the cluster
205 | // (you must enable the option using PruneCluster.Cluster.ENABLE_MARKERS_LIST = true)
206 | var markers = cluster.GetClusterMarkers()
207 |
208 | ...
209 |
210 | return icon; // L.Icon object (See http://leafletjs.com/reference.html#icon);
211 | };
212 | ```
213 |
214 | #### Listening to events on a cluster
215 |
216 | To listen to events on the cluster, you will need to override the ```BuildLeafletCluster``` method. A click event is already specified on m, but you can add other events like mouseover, mouseout, etc. Any events that a Leaflet marker supports, the cluster also supports, since it is just a modified marker. A full list of events can be found [here](http://leafletjs.com/reference.html#marker-click).
217 |
218 | Below is an example of how to implement mouseover and mousedown for the cluster, but any events can be used in place of those.
219 | ```javascript
220 | pruneCluster.BuildLeafletCluster = function(cluster, position) {
221 | var m = new L.Marker(position, {
222 | icon: pruneCluster.BuildLeafletClusterIcon(cluster)
223 | });
224 |
225 | m.on('click', function() {
226 | // Compute the cluster bounds (it's slow : O(n))
227 | var markersArea = pruneCluster.Cluster.FindMarkersInArea(cluster.bounds);
228 | var b = pruneCluster.Cluster.ComputeBounds(markersArea);
229 |
230 | if (b) {
231 | var bounds = new L.LatLngBounds(
232 | new L.LatLng(b.minLat, b.maxLng),
233 | new L.LatLng(b.maxLat, b.minLng));
234 |
235 | var zoomLevelBefore = pruneCluster._map.getZoom();
236 | var zoomLevelAfter = pruneCluster._map.getBoundsZoom(bounds, false, new L.Point(20, 20, null));
237 |
238 | // If the zoom level doesn't change
239 | if (zoomLevelAfter === zoomLevelBefore) {
240 | // Send an event for the LeafletSpiderfier
241 | pruneCluster._map.fire('overlappingmarkers', {
242 | cluster: pruneCluster,
243 | markers: markersArea,
244 | center: m.getLatLng(),
245 | marker: m
246 | });
247 |
248 | pruneCluster._map.setView(position, zoomLevelAfter);
249 | }
250 | else {
251 | pruneCluster._map.fitBounds(bounds);
252 | }
253 | }
254 | });
255 | m.on('mouseover', function() {
256 | //do mouseover stuff here
257 | });
258 | m.on('mouseout', function() {
259 | //do mouseout stuff here
260 | });
261 |
262 | return m;
263 | };
264 | };
265 | ```
266 |
267 | #### Redraw the icons
268 |
269 | Marker icon redrawing with a flag:
270 |
271 | ```javascript
272 | marker.data.forceIconRedraw = true;
273 |
274 | ...
275 |
276 | pruneCluster.ProcessView();
277 | ```
278 |
279 | Redraw all the icons:
280 | ```javascript
281 | pruneCluster.RedrawIcons();
282 | ```
283 |
284 | ### Acknowledgements
285 |
286 | This library was developed in context of the BRIDGE project. It is now supported by the community and we thank [the contributors](https://github.com/SINTEF-9012/PruneCluster/graphs/contributors).
287 |
288 | ### Licence
289 |
290 | The source code of this library is licensed under the MIT License.
291 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PruneCluster",
3 | "dependencies": {
4 | "leaflet": "0.7 - 2"
5 | },
6 | "devDependencies": {
7 | "leaflet-dist": "~0.7.2"
8 | },
9 | "ignore": [
10 | "*.ts",
11 | "*.md",
12 | "package.json",
13 | "meteor",
14 | "examples",
15 | "Gruntfile.js",
16 | "LICENSE",
17 | "bower.json"
18 | ],
19 | "license": "MIT",
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/SINTEF-9012/PruneCluster.git"
23 | },
24 | "main": ["./dist/LeafletStyleSheet.css", "./dist/PruneCluster.js"]
25 | }
26 |
--------------------------------------------------------------------------------
/dist/LeafletStyleSheet.css:
--------------------------------------------------------------------------------
1 | .prunecluster {
2 | font-size: 12px;
3 | border-radius: 20px;
4 | transition: all 0.3s linear;
5 | }
6 | .leaflet-marker-icon.prunecluster-anim,
7 | .leaflet-marker-shadow.prunecluster-anim,
8 | .leaflet-markercluster-icon.prunecluster-anim {
9 | transition: all 0.3s linear;
10 | }
11 |
12 | .leaflet-zoom-anim .leaflet-zoom-animated.leaflet-marker-icon,
13 | .leaflet-zoom-anim .leaflet-zoom-animated.leaflet-marker-shadow,
14 | .leaflet-zoom-anim .leaflet-zoom-animated.leaflet-markercluster-icon {
15 | transition: transform 0.25s cubic-bezier(0,0,0.25,1);
16 | }
17 | .prunecluster div {
18 | width: 30px;
19 | height: 30px;
20 | text-align: center;
21 | margin-left: 5px;
22 | margin-top: 5px;
23 | border-radius: 50%;
24 | }
25 | .prunecluster div span {
26 | line-height: 30px;
27 | }
28 |
29 | .prunecluster-small {
30 | background-color: #b5e28c;
31 | background-color: rgba(181, 226, 140, 0.6);
32 | }
33 |
34 | .prunecluster-small div {
35 | width: 28px;
36 | height: 28px;
37 | background-color: #6ecc39;
38 | background-color: rgba(110, 204, 57, 0.6);
39 | }
40 |
41 | .prunecluster-small div span {
42 | line-height: 28px;
43 | }
44 |
45 | .prunecluster-medium {
46 | background-color: #f1d357;
47 | background-color: rgba(241, 211, 87, 0.6);
48 | }
49 |
50 | .prunecluster-medium div {
51 | background-color: #f0c20c;
52 | background-color: rgba(240, 194, 12, 0.6);
53 | }
54 |
55 | .prunecluster-large {
56 | background-color: #fd9c73;
57 | background-color: rgba(253, 156, 115, 0.6);
58 | }
59 |
60 | .prunecluster-large div {
61 | width: 34px;
62 | height: 34px;
63 | background-color: #f18017;
64 | background-color: rgba(241, 128, 23, 0.6);
65 | }
66 |
67 | .prunecluster-large div span {
68 | line-height: 34px;
69 | }
70 |
--------------------------------------------------------------------------------
/dist/PruneCluster.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare namespace PruneCluster {
3 | interface Position {
4 | lat: number;
5 | lng: number;
6 | }
7 | class Point {
8 | x: number;
9 | y: number;
10 | }
11 | interface Bounds {
12 | minLat: number;
13 | maxLat: number;
14 | minLng: number;
15 | maxLng: number;
16 | }
17 | class ClusterObject {
18 | position: Position;
19 | data: any;
20 | hashCode: number;
21 | }
22 | class Marker extends ClusterObject {
23 | category: number;
24 | weight: number;
25 | filtered: boolean;
26 | constructor(lat: number, lng: number, data?: {}, category?: number, weight?: number, filtered?: boolean);
27 | Move(lat: number, lng: number): void;
28 | SetData(data: any): void;
29 | }
30 | class Cluster extends ClusterObject {
31 | bounds: Bounds;
32 | population: number;
33 | averagePosition: Position;
34 | stats: number[];
35 | totalWeight: number;
36 | lastMarker: Marker;
37 | static ENABLE_MARKERS_LIST: boolean;
38 | private _clusterMarkers;
39 | constructor(marker?: Marker);
40 | AddMarker(marker: Marker): void;
41 | Reset(): void;
42 | ComputeBounds(cluster: PruneCluster.PruneCluster): void;
43 | GetClusterMarkers(): Marker[];
44 | ApplyCluster(newCluster: Cluster): void;
45 | }
46 | class PruneCluster {
47 | private _markers;
48 | private _nbChanges;
49 | private _clusters;
50 | Size: number;
51 | ViewPadding: number;
52 | Project: (lat: number, lng: number) => Point;
53 | UnProject: (x: number, y: number) => Position;
54 | RegisterMarker(marker: Marker): void;
55 | RegisterMarkers(markers: Marker[]): void;
56 | private _sortMarkers();
57 | private _sortClusters();
58 | private _indexLowerBoundLng(lng);
59 | private _resetClusterViews();
60 | ProcessView(bounds: Bounds): Cluster[];
61 | RemoveMarkers(markers?: Marker[]): void;
62 | FindMarkersInArea(area: Bounds): Marker[];
63 | ComputeBounds(markers: Marker[], withFiltered?: boolean): Bounds;
64 | FindMarkersBoundsInArea(area: Bounds): Bounds;
65 | ComputeGlobalBounds(withFiltered?: boolean): Bounds;
66 | GetMarkers(): Marker[];
67 | GetPopulation(): number;
68 | ResetClusters(): void;
69 | }
70 | }
71 | declare namespace PruneCluster {
72 | class LeafletAdapter implements L.ILayer {
73 | Cluster: PruneCluster.PruneCluster;
74 | onAdd: (map: L.Map) => void;
75 | onRemove: (map: L.Map) => void;
76 | RegisterMarker: (marker: Marker) => void;
77 | RegisterMarkers: (markers: Marker[]) => void;
78 | RemoveMarkers: (markers: Marker[]) => void;
79 | ProcessView: () => void;
80 | FitBounds: (withFiltered?: boolean) => void;
81 | GetMarkers: () => Marker[];
82 | RedrawIcons: (processView?: boolean) => void;
83 | BuildLeafletCluster: (cluster: Cluster, position: L.LatLng) => L.ILayer;
84 | BuildLeafletClusterIcon: (cluster: Cluster) => L.Icon;
85 | BuildLeafletMarker: (marker: Marker, position: L.LatLng) => L.Marker;
86 | PrepareLeafletMarker: (marker: L.Marker, data: {}, category: number) => void;
87 | }
88 | interface LeafletMarker extends L.Marker {
89 | _population?: number;
90 | _hashCode?: number;
91 | _zoomLevel?: number;
92 | _removeFromMap?: boolean;
93 | }
94 | interface ILeafletAdapterData {
95 | _leafletMarker?: LeafletMarker;
96 | _leafletCollision?: boolean;
97 | _leafletOldPopulation?: number;
98 | _leafletOldHashCode?: number;
99 | _leafletPosition?: L.LatLng;
100 | }
101 | }
102 | declare var PruneClusterForLeaflet: any;
103 | declare var PruneClusterLeafletSpiderfier: any;
104 |
--------------------------------------------------------------------------------
/dist/PruneCluster.js:
--------------------------------------------------------------------------------
1 | var __extends = (this && this.__extends) || (function () {
2 | var extendStatics = Object.setPrototypeOf ||
3 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
4 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
5 | return function (d, b) {
6 | extendStatics(d, b);
7 | function __() { this.constructor = d; }
8 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
9 | };
10 | })();
11 | var PruneCluster;
12 | (function (PruneCluster_1) {
13 | var Point = (function () {
14 | function Point() {
15 | }
16 | return Point;
17 | }());
18 | PruneCluster_1.Point = Point;
19 | var ClusterObject = (function () {
20 | function ClusterObject() {
21 | }
22 | return ClusterObject;
23 | }());
24 | PruneCluster_1.ClusterObject = ClusterObject;
25 | var hashCodeCounter = 1;
26 | var maxHashCodeValue = Math.pow(2, 53) - 1;
27 | var Marker = (function (_super) {
28 | __extends(Marker, _super);
29 | function Marker(lat, lng, data, category, weight, filtered) {
30 | if (data === void 0) { data = {}; }
31 | if (weight === void 0) { weight = 1; }
32 | if (filtered === void 0) { filtered = false; }
33 | var _this = _super.call(this) || this;
34 | _this.data = data;
35 | _this.position = { lat: +lat, lng: +lng };
36 | _this.weight = weight;
37 | _this.category = category;
38 | _this.filtered = filtered;
39 | _this.hashCode = hashCodeCounter++;
40 | return _this;
41 | }
42 | Marker.prototype.Move = function (lat, lng) {
43 | this.position.lat = +lat;
44 | this.position.lng = +lng;
45 | };
46 | Marker.prototype.SetData = function (data) {
47 | for (var key in data) {
48 | this.data[key] = data[key];
49 | }
50 | };
51 | return Marker;
52 | }(ClusterObject));
53 | PruneCluster_1.Marker = Marker;
54 | var Cluster = (function (_super) {
55 | __extends(Cluster, _super);
56 | function Cluster(marker) {
57 | var _this = _super.call(this) || this;
58 | _this.stats = [0, 0, 0, 0, 0, 0, 0, 0];
59 | _this.data = {};
60 | if (!marker) {
61 | _this.hashCode = 1;
62 | if (Cluster.ENABLE_MARKERS_LIST) {
63 | _this._clusterMarkers = [];
64 | }
65 | return _this;
66 | }
67 | if (Cluster.ENABLE_MARKERS_LIST) {
68 | _this._clusterMarkers = [marker];
69 | }
70 | _this.lastMarker = marker;
71 | _this.hashCode = 31 + marker.hashCode;
72 | _this.population = 1;
73 | if (marker.category !== undefined) {
74 | _this.stats[marker.category] = 1;
75 | }
76 | _this.totalWeight = marker.weight;
77 | _this.position = {
78 | lat: marker.position.lat,
79 | lng: marker.position.lng
80 | };
81 | _this.averagePosition = {
82 | lat: marker.position.lat,
83 | lng: marker.position.lng
84 | };
85 | return _this;
86 | }
87 | Cluster.prototype.AddMarker = function (marker) {
88 | if (Cluster.ENABLE_MARKERS_LIST) {
89 | this._clusterMarkers.push(marker);
90 | }
91 | var h = this.hashCode;
92 | h = ((h << 5) - h) + marker.hashCode;
93 | if (h >= maxHashCodeValue) {
94 | this.hashCode = h % maxHashCodeValue;
95 | }
96 | else {
97 | this.hashCode = h;
98 | }
99 | this.lastMarker = marker;
100 | var weight = marker.weight, currentTotalWeight = this.totalWeight, newWeight = weight + currentTotalWeight;
101 | this.averagePosition.lat =
102 | (this.averagePosition.lat * currentTotalWeight +
103 | marker.position.lat * weight) / newWeight;
104 | this.averagePosition.lng =
105 | (this.averagePosition.lng * currentTotalWeight +
106 | marker.position.lng * weight) / newWeight;
107 | ++this.population;
108 | this.totalWeight = newWeight;
109 | if (marker.category !== undefined) {
110 | this.stats[marker.category] = (this.stats[marker.category] + 1) || 1;
111 | }
112 | };
113 | Cluster.prototype.Reset = function () {
114 | this.hashCode = 1;
115 | this.lastMarker = undefined;
116 | this.population = 0;
117 | this.totalWeight = 0;
118 | this.stats = [0, 0, 0, 0, 0, 0, 0, 0];
119 | if (Cluster.ENABLE_MARKERS_LIST) {
120 | this._clusterMarkers = [];
121 | }
122 | };
123 | Cluster.prototype.ComputeBounds = function (cluster) {
124 | var proj = cluster.Project(this.position.lat, this.position.lng);
125 | var size = cluster.Size;
126 | var nbX = Math.floor(proj.x / size), nbY = Math.floor(proj.y / size), startX = nbX * size, startY = nbY * size;
127 | var a = cluster.UnProject(startX, startY), b = cluster.UnProject(startX + size, startY + size);
128 | this.bounds = {
129 | minLat: b.lat,
130 | maxLat: a.lat,
131 | minLng: a.lng,
132 | maxLng: b.lng
133 | };
134 | };
135 | Cluster.prototype.GetClusterMarkers = function () {
136 | return this._clusterMarkers;
137 | };
138 | Cluster.prototype.ApplyCluster = function (newCluster) {
139 | this.hashCode = this.hashCode * 41 + newCluster.hashCode * 43;
140 | if (this.hashCode > maxHashCodeValue) {
141 | this.hashCode = this.hashCode = maxHashCodeValue;
142 | }
143 | var weight = newCluster.totalWeight, currentTotalWeight = this.totalWeight, newWeight = weight + currentTotalWeight;
144 | this.averagePosition.lat =
145 | (this.averagePosition.lat * currentTotalWeight +
146 | newCluster.averagePosition.lat * weight) / newWeight;
147 | this.averagePosition.lng =
148 | (this.averagePosition.lng * currentTotalWeight +
149 | newCluster.averagePosition.lng * weight) / newWeight;
150 | this.population += newCluster.population;
151 | this.totalWeight = newWeight;
152 | this.bounds.minLat = Math.min(this.bounds.minLat, newCluster.bounds.minLat);
153 | this.bounds.minLng = Math.min(this.bounds.minLng, newCluster.bounds.minLng);
154 | this.bounds.maxLat = Math.max(this.bounds.maxLat, newCluster.bounds.maxLat);
155 | this.bounds.maxLng = Math.max(this.bounds.maxLng, newCluster.bounds.maxLng);
156 | for (var category in newCluster.stats) {
157 | if (newCluster.stats.hasOwnProperty(category)) {
158 | if (this.stats.hasOwnProperty(category)) {
159 | this.stats[category] += newCluster.stats[category];
160 | }
161 | else {
162 | this.stats[category] = newCluster.stats[category];
163 | }
164 | }
165 | }
166 | if (Cluster.ENABLE_MARKERS_LIST) {
167 | this._clusterMarkers = this._clusterMarkers.concat(newCluster.GetClusterMarkers());
168 | }
169 | };
170 | Cluster.ENABLE_MARKERS_LIST = false;
171 | return Cluster;
172 | }(ClusterObject));
173 | PruneCluster_1.Cluster = Cluster;
174 | function checkPositionInsideBounds(a, b) {
175 | return (a.lat >= b.minLat && a.lat <= b.maxLat) &&
176 | a.lng >= b.minLng && a.lng <= b.maxLng;
177 | }
178 | function insertionSort(list) {
179 | for (var i = 1, j, tmp, tmpLng, length = list.length; i < length; ++i) {
180 | tmp = list[i];
181 | tmpLng = tmp.position.lng;
182 | for (j = i - 1; j >= 0 && list[j].position.lng > tmpLng; --j) {
183 | list[j + 1] = list[j];
184 | }
185 | list[j + 1] = tmp;
186 | }
187 | }
188 | function shouldUseInsertionSort(total, nbChanges) {
189 | if (nbChanges > 300) {
190 | return false;
191 | }
192 | else {
193 | return (nbChanges / total) < 0.2;
194 | }
195 | }
196 | var PruneCluster = (function () {
197 | function PruneCluster() {
198 | this._markers = [];
199 | this._nbChanges = 0;
200 | this._clusters = [];
201 | this.Size = 166;
202 | this.ViewPadding = 0.2;
203 | }
204 | PruneCluster.prototype.RegisterMarker = function (marker) {
205 | if (marker._removeFlag) {
206 | delete marker._removeFlag;
207 | }
208 | this._markers.push(marker);
209 | this._nbChanges += 1;
210 | };
211 | PruneCluster.prototype.RegisterMarkers = function (markers) {
212 | var _this = this;
213 | markers.forEach(function (marker) {
214 | _this.RegisterMarker(marker);
215 | });
216 | };
217 | PruneCluster.prototype._sortMarkers = function () {
218 | var markers = this._markers, length = markers.length;
219 | if (this._nbChanges && !shouldUseInsertionSort(length, this._nbChanges)) {
220 | this._markers.sort(function (a, b) { return a.position.lng - b.position.lng; });
221 | }
222 | else {
223 | insertionSort(markers);
224 | }
225 | this._nbChanges = 0;
226 | };
227 | PruneCluster.prototype._sortClusters = function () {
228 | insertionSort(this._clusters);
229 | };
230 | PruneCluster.prototype._indexLowerBoundLng = function (lng) {
231 | var markers = this._markers, it, step, first = 0, count = markers.length;
232 | while (count > 0) {
233 | step = Math.floor(count / 2);
234 | it = first + step;
235 | if (markers[it].position.lng < lng) {
236 | first = ++it;
237 | count -= step + 1;
238 | }
239 | else {
240 | count = step;
241 | }
242 | }
243 | return first;
244 | };
245 | PruneCluster.prototype._resetClusterViews = function () {
246 | for (var i = 0, l = this._clusters.length; i < l; ++i) {
247 | var cluster = this._clusters[i];
248 | cluster.Reset();
249 | cluster.ComputeBounds(this);
250 | }
251 | };
252 | PruneCluster.prototype.ProcessView = function (bounds) {
253 | var heightBuffer = Math.abs(bounds.maxLat - bounds.minLat) * this.ViewPadding, widthBuffer = Math.abs(bounds.maxLng - bounds.minLng) * this.ViewPadding;
254 | var extendedBounds = {
255 | minLat: bounds.minLat - heightBuffer - heightBuffer,
256 | maxLat: bounds.maxLat + heightBuffer + heightBuffer,
257 | minLng: bounds.minLng - widthBuffer - widthBuffer,
258 | maxLng: bounds.maxLng + widthBuffer + widthBuffer
259 | };
260 | this._sortMarkers();
261 | this._resetClusterViews();
262 | var firstIndex = this._indexLowerBoundLng(extendedBounds.minLng);
263 | var markers = this._markers, clusters = this._clusters;
264 | var workingClusterList = clusters.slice(0);
265 | for (var i = firstIndex, l = markers.length; i < l; ++i) {
266 | var marker = markers[i], markerPosition = marker.position;
267 | if (markerPosition.lng > extendedBounds.maxLng) {
268 | break;
269 | }
270 | if (markerPosition.lat > extendedBounds.minLat &&
271 | markerPosition.lat < extendedBounds.maxLat &&
272 | !marker.filtered) {
273 | var clusterFound = false, cluster;
274 | for (var j = 0, ll = workingClusterList.length; j < ll; ++j) {
275 | cluster = workingClusterList[j];
276 | if (cluster.bounds.maxLng < marker.position.lng) {
277 | workingClusterList.splice(j, 1);
278 | --j;
279 | --ll;
280 | continue;
281 | }
282 | if (checkPositionInsideBounds(markerPosition, cluster.bounds)) {
283 | cluster.AddMarker(marker);
284 | clusterFound = true;
285 | break;
286 | }
287 | }
288 | if (!clusterFound) {
289 | cluster = new Cluster(marker);
290 | cluster.ComputeBounds(this);
291 | clusters.push(cluster);
292 | workingClusterList.push(cluster);
293 | }
294 | }
295 | }
296 | var newClustersList = [];
297 | for (i = 0, l = clusters.length; i < l; ++i) {
298 | cluster = clusters[i];
299 | if (cluster.population > 0) {
300 | newClustersList.push(cluster);
301 | }
302 | }
303 | this._clusters = newClustersList;
304 | this._sortClusters();
305 | return this._clusters;
306 | };
307 | PruneCluster.prototype.RemoveMarkers = function (markers) {
308 | if (!markers) {
309 | this._markers = [];
310 | return;
311 | }
312 | for (var i = 0, l = markers.length; i < l; ++i) {
313 | markers[i]._removeFlag = true;
314 | }
315 | var newMarkersList = [];
316 | for (i = 0, l = this._markers.length; i < l; ++i) {
317 | if (!this._markers[i]._removeFlag) {
318 | newMarkersList.push(this._markers[i]);
319 | }
320 | else {
321 | delete this._markers[i]._removeFlag;
322 | }
323 | }
324 | this._markers = newMarkersList;
325 | };
326 | PruneCluster.prototype.FindMarkersInArea = function (area) {
327 | var aMinLat = area.minLat, aMaxLat = area.maxLat, aMinLng = area.minLng, aMaxLng = area.maxLng, markers = this._markers, result = [];
328 | var firstIndex = this._indexLowerBoundLng(aMinLng);
329 | for (var i = firstIndex, l = markers.length; i < l; ++i) {
330 | var pos = markers[i].position;
331 | if (pos.lng > aMaxLng) {
332 | break;
333 | }
334 | if (pos.lat >= aMinLat && pos.lat <= aMaxLat &&
335 | pos.lng >= aMinLng) {
336 | result.push(markers[i]);
337 | }
338 | }
339 | return result;
340 | };
341 | PruneCluster.prototype.ComputeBounds = function (markers, withFiltered) {
342 | if (withFiltered === void 0) { withFiltered = true; }
343 | if (!markers || !markers.length) {
344 | return null;
345 | }
346 | var rMinLat = Number.MAX_VALUE, rMaxLat = -Number.MAX_VALUE, rMinLng = Number.MAX_VALUE, rMaxLng = -Number.MAX_VALUE;
347 | for (var i = 0, l = markers.length; i < l; ++i) {
348 | if (!withFiltered && markers[i].filtered) {
349 | continue;
350 | }
351 | var pos = markers[i].position;
352 | if (pos.lat < rMinLat)
353 | rMinLat = pos.lat;
354 | if (pos.lat > rMaxLat)
355 | rMaxLat = pos.lat;
356 | if (pos.lng < rMinLng)
357 | rMinLng = pos.lng;
358 | if (pos.lng > rMaxLng)
359 | rMaxLng = pos.lng;
360 | }
361 | return {
362 | minLat: rMinLat,
363 | maxLat: rMaxLat,
364 | minLng: rMinLng,
365 | maxLng: rMaxLng
366 | };
367 | };
368 | PruneCluster.prototype.FindMarkersBoundsInArea = function (area) {
369 | return this.ComputeBounds(this.FindMarkersInArea(area));
370 | };
371 | PruneCluster.prototype.ComputeGlobalBounds = function (withFiltered) {
372 | if (withFiltered === void 0) { withFiltered = true; }
373 | return this.ComputeBounds(this._markers, withFiltered);
374 | };
375 | PruneCluster.prototype.GetMarkers = function () {
376 | return this._markers;
377 | };
378 | PruneCluster.prototype.GetPopulation = function () {
379 | return this._markers.length;
380 | };
381 | PruneCluster.prototype.ResetClusters = function () {
382 | this._clusters = [];
383 | };
384 | return PruneCluster;
385 | }());
386 | PruneCluster_1.PruneCluster = PruneCluster;
387 | })(PruneCluster || (PruneCluster = {}));
388 | var PruneCluster;
389 | (function (PruneCluster) {
390 | })(PruneCluster || (PruneCluster = {}));
391 | var PruneClusterForLeaflet = (L.Layer ? L.Layer : L.Class).extend({
392 | initialize: function (size, clusterMargin) {
393 | var _this = this;
394 | if (size === void 0) { size = 120; }
395 | if (clusterMargin === void 0) { clusterMargin = 20; }
396 | this.Cluster = new PruneCluster.PruneCluster();
397 | this.Cluster.Size = size;
398 | this.clusterMargin = Math.min(clusterMargin, size / 4);
399 | this.Cluster.Project = function (lat, lng) {
400 | return _this._map.project(new L.LatLng(lat, lng), Math.floor(_this._map.getZoom()));
401 | };
402 | this.Cluster.UnProject = function (x, y) {
403 | return _this._map.unproject(new L.Point(x, y), Math.floor(_this._map.getZoom()));
404 | };
405 | this._objectsOnMap = [];
406 | this.spiderfier = new PruneClusterLeafletSpiderfier(this);
407 | this._hardMove = false;
408 | this._resetIcons = false;
409 | this._removeTimeoutId = 0;
410 | this._markersRemoveListTimeout = [];
411 | },
412 | RegisterMarker: function (marker) {
413 | this.Cluster.RegisterMarker(marker);
414 | },
415 | RegisterMarkers: function (markers) {
416 | this.Cluster.RegisterMarkers(markers);
417 | },
418 | RemoveMarkers: function (markers) {
419 | this.Cluster.RemoveMarkers(markers);
420 | },
421 | BuildLeafletCluster: function (cluster, position) {
422 | var _this = this;
423 | var m = new L.Marker(position, {
424 | icon: this.BuildLeafletClusterIcon(cluster)
425 | });
426 | m._leafletClusterBounds = cluster.bounds;
427 | m.on('click', function () {
428 | var cbounds = m._leafletClusterBounds;
429 | var markersArea = _this.Cluster.FindMarkersInArea(cbounds);
430 | var b = _this.Cluster.ComputeBounds(markersArea);
431 | if (b) {
432 | var bounds = new L.LatLngBounds(new L.LatLng(b.minLat, b.maxLng), new L.LatLng(b.maxLat, b.minLng));
433 | var zoomLevelBefore = _this._map.getZoom(), zoomLevelAfter = _this._map.getBoundsZoom(bounds, false, new L.Point(20, 20));
434 | if (zoomLevelAfter === zoomLevelBefore) {
435 | var filteredBounds = [];
436 | for (var i = 0, l = _this._objectsOnMap.length; i < l; ++i) {
437 | var o = _this._objectsOnMap[i];
438 | if (o.data._leafletMarker !== m) {
439 | if (o.bounds.minLat >= cbounds.minLat &&
440 | o.bounds.maxLat <= cbounds.maxLat &&
441 | o.bounds.minLng >= cbounds.minLng &&
442 | o.bounds.maxLng <= cbounds.maxLng) {
443 | filteredBounds.push(o.bounds);
444 | }
445 | }
446 | }
447 | if (filteredBounds.length > 0) {
448 | var newMarkersArea = [];
449 | var ll = filteredBounds.length;
450 | for (i = 0, l = markersArea.length; i < l; ++i) {
451 | var markerPos = markersArea[i].position;
452 | var isFiltered = false;
453 | for (var j = 0; j < ll; ++j) {
454 | var currentFilteredBounds = filteredBounds[j];
455 | if (markerPos.lat >= currentFilteredBounds.minLat &&
456 | markerPos.lat <= currentFilteredBounds.maxLat &&
457 | markerPos.lng >= currentFilteredBounds.minLng &&
458 | markerPos.lng <= currentFilteredBounds.maxLng) {
459 | isFiltered = true;
460 | break;
461 | }
462 | }
463 | if (!isFiltered) {
464 | newMarkersArea.push(markersArea[i]);
465 | }
466 | }
467 | markersArea = newMarkersArea;
468 | }
469 | if (markersArea.length < 200 || zoomLevelAfter >= _this._map.getMaxZoom()) {
470 | _this._map.fire('overlappingmarkers', {
471 | cluster: _this,
472 | markers: markersArea,
473 | center: m.getLatLng(),
474 | marker: m
475 | });
476 | }
477 | else {
478 | zoomLevelAfter++;
479 | }
480 | _this._map.setView(m.getLatLng(), zoomLevelAfter);
481 | }
482 | else {
483 | _this._map.fitBounds(bounds);
484 | }
485 | }
486 | });
487 | return m;
488 | },
489 | BuildLeafletClusterIcon: function (cluster) {
490 | var c = 'prunecluster prunecluster-';
491 | var iconSize = 38;
492 | var maxPopulation = this.Cluster.GetPopulation();
493 | if (cluster.population < Math.max(10, maxPopulation * 0.01)) {
494 | c += 'small';
495 | }
496 | else if (cluster.population < Math.max(100, maxPopulation * 0.05)) {
497 | c += 'medium';
498 | iconSize = 40;
499 | }
500 | else {
501 | c += 'large';
502 | iconSize = 44;
503 | }
504 | return new L.DivIcon({
505 | html: "" + cluster.population + "
",
506 | className: c,
507 | iconSize: L.point(iconSize, iconSize)
508 | });
509 | },
510 | BuildLeafletMarker: function (marker, position) {
511 | var m = new L.Marker(position);
512 | this.PrepareLeafletMarker(m, marker.data, marker.category);
513 | return m;
514 | },
515 | PrepareLeafletMarker: function (marker, data, category) {
516 | if (data.icon) {
517 | if (typeof data.icon === 'function') {
518 | marker.setIcon(data.icon(data, category));
519 | }
520 | else {
521 | marker.setIcon(data.icon);
522 | }
523 | }
524 | if (data.popup) {
525 | var content = typeof data.popup === 'function' ? data.popup(data, category) : data.popup;
526 | if (marker.getPopup()) {
527 | marker.setPopupContent(content, data.popupOptions);
528 | }
529 | else {
530 | marker.bindPopup(content, data.popupOptions);
531 | }
532 | }
533 | },
534 | onAdd: function (map) {
535 | this._map = map;
536 | map.on('movestart', this._moveStart, this);
537 | map.on('moveend', this._moveEnd, this);
538 | map.on('zoomend', this._zoomStart, this);
539 | map.on('zoomend', this._zoomEnd, this);
540 | this.ProcessView();
541 | map.addLayer(this.spiderfier);
542 | },
543 | onRemove: function (map) {
544 | map.off('movestart', this._moveStart, this);
545 | map.off('moveend', this._moveEnd, this);
546 | map.off('zoomend', this._zoomStart, this);
547 | map.off('zoomend', this._zoomEnd, this);
548 | for (var i = 0, l = this._objectsOnMap.length; i < l; ++i) {
549 | map.removeLayer(this._objectsOnMap[i].data._leafletMarker);
550 | }
551 | this._objectsOnMap = [];
552 | this.Cluster.ResetClusters();
553 | map.removeLayer(this.spiderfier);
554 | this._map = null;
555 | },
556 | _moveStart: function () {
557 | this._moveInProgress = true;
558 | },
559 | _moveEnd: function (e) {
560 | this._moveInProgress = false;
561 | this._hardMove = e.hard;
562 | this.ProcessView();
563 | },
564 | _zoomStart: function () {
565 | this._zoomInProgress = true;
566 | },
567 | _zoomEnd: function () {
568 | this._zoomInProgress = false;
569 | this.ProcessView();
570 | },
571 | ProcessView: function () {
572 | var _this = this;
573 | if (!this._map || this._zoomInProgress || this._moveInProgress) {
574 | return;
575 | }
576 | var map = this._map, bounds = map.getBounds(), zoom = Math.floor(map.getZoom()), marginRatio = this.clusterMargin / this.Cluster.Size, resetIcons = this._resetIcons;
577 | var southWest = bounds.getSouthWest(), northEast = bounds.getNorthEast();
578 | var clusters = this.Cluster.ProcessView({
579 | minLat: southWest.lat,
580 | minLng: southWest.lng,
581 | maxLat: northEast.lat,
582 | maxLng: northEast.lng
583 | });
584 | var objectsOnMap = this._objectsOnMap, newObjectsOnMap = [], markersOnMap = new Array(objectsOnMap.length);
585 | for (var i = 0, l = objectsOnMap.length; i < l; ++i) {
586 | var marker = objectsOnMap[i].data._leafletMarker;
587 | markersOnMap[i] = marker;
588 | marker._removeFromMap = true;
589 | }
590 | var clusterCreationList = [];
591 | var clusterCreationListPopOne = [];
592 | var opacityUpdateList = [];
593 | var workingList = [];
594 | for (i = 0, l = clusters.length; i < l; ++i) {
595 | var icluster = clusters[i], iclusterData = icluster.data;
596 | var latMargin = (icluster.bounds.maxLat - icluster.bounds.minLat) * marginRatio, lngMargin = (icluster.bounds.maxLng - icluster.bounds.minLng) * marginRatio;
597 | for (var j = 0, ll = workingList.length; j < ll; ++j) {
598 | var c = workingList[j];
599 | if (c.bounds.maxLng < icluster.bounds.minLng) {
600 | workingList.splice(j, 1);
601 | --j;
602 | --ll;
603 | continue;
604 | }
605 | var oldMaxLng = c.averagePosition.lng + lngMargin, oldMinLat = c.averagePosition.lat - latMargin, oldMaxLat = c.averagePosition.lat + latMargin, newMinLng = icluster.averagePosition.lng - lngMargin, newMinLat = icluster.averagePosition.lat - latMargin, newMaxLat = icluster.averagePosition.lat + latMargin;
606 | if (oldMaxLng > newMinLng && oldMaxLat > newMinLat && oldMinLat < newMaxLat) {
607 | iclusterData._leafletCollision = true;
608 | c.ApplyCluster(icluster);
609 | break;
610 | }
611 | }
612 | if (!iclusterData._leafletCollision) {
613 | workingList.push(icluster);
614 | }
615 | }
616 | clusters.forEach(function (cluster) {
617 | var m = undefined;
618 | var data = cluster.data;
619 | if (data._leafletCollision) {
620 | data._leafletCollision = false;
621 | data._leafletOldPopulation = 0;
622 | data._leafletOldHashCode = 0;
623 | return;
624 | }
625 | var position = new L.LatLng(cluster.averagePosition.lat, cluster.averagePosition.lng);
626 | var oldMarker = data._leafletMarker;
627 | if (oldMarker) {
628 | if (cluster.population === 1 && data._leafletOldPopulation === 1 && cluster.hashCode === oldMarker._hashCode) {
629 | if (resetIcons || oldMarker._zoomLevel !== zoom || cluster.lastMarker.data.forceIconRedraw) {
630 | _this.PrepareLeafletMarker(oldMarker, cluster.lastMarker.data, cluster.lastMarker.category);
631 | if (cluster.lastMarker.data.forceIconRedraw) {
632 | cluster.lastMarker.data.forceIconRedraw = false;
633 | }
634 | }
635 | oldMarker.setLatLng(position);
636 | m = oldMarker;
637 | }
638 | else if (cluster.population > 1 && data._leafletOldPopulation > 1 && (oldMarker._zoomLevel === zoom ||
639 | data._leafletPosition.equals(position))) {
640 | oldMarker.setLatLng(position);
641 | if (resetIcons || cluster.population != data._leafletOldPopulation ||
642 | cluster.hashCode !== data._leafletOldHashCode) {
643 | var boundsCopy = {};
644 | L.Util.extend(boundsCopy, cluster.bounds);
645 | oldMarker._leafletClusterBounds = boundsCopy;
646 | oldMarker.setIcon(_this.BuildLeafletClusterIcon(cluster));
647 | }
648 | data._leafletOldPopulation = cluster.population;
649 | data._leafletOldHashCode = cluster.hashCode;
650 | m = oldMarker;
651 | }
652 | }
653 | if (!m) {
654 | if (cluster.population === 1) {
655 | clusterCreationListPopOne.push(cluster);
656 | }
657 | else {
658 | clusterCreationList.push(cluster);
659 | }
660 | data._leafletPosition = position;
661 | data._leafletOldPopulation = cluster.population;
662 | data._leafletOldHashCode = cluster.hashCode;
663 | }
664 | else {
665 | m._removeFromMap = false;
666 | newObjectsOnMap.push(cluster);
667 | m._zoomLevel = zoom;
668 | m._hashCode = cluster.hashCode;
669 | m._population = cluster.population;
670 | data._leafletMarker = m;
671 | data._leafletPosition = position;
672 | }
673 | });
674 | clusterCreationList = clusterCreationListPopOne.concat(clusterCreationList);
675 | for (i = 0, l = objectsOnMap.length; i < l; ++i) {
676 | icluster = objectsOnMap[i];
677 | var idata = icluster.data;
678 | marker = idata._leafletMarker;
679 | if (idata._leafletMarker._removeFromMap) {
680 | var remove = true;
681 | if (marker._zoomLevel === zoom) {
682 | var pa = icluster.averagePosition;
683 | latMargin = (icluster.bounds.maxLat - icluster.bounds.minLat) * marginRatio,
684 | lngMargin = (icluster.bounds.maxLng - icluster.bounds.minLng) * marginRatio;
685 | for (j = 0, ll = clusterCreationList.length; j < ll; ++j) {
686 | var jcluster = clusterCreationList[j], jdata = jcluster.data;
687 | if (marker._population === 1 && jcluster.population === 1 &&
688 | marker._hashCode === jcluster.hashCode) {
689 | if (resetIcons || jcluster.lastMarker.data.forceIconRedraw) {
690 | this.PrepareLeafletMarker(marker, jcluster.lastMarker.data, jcluster.lastMarker.category);
691 | if (jcluster.lastMarker.data.forceIconRedraw) {
692 | jcluster.lastMarker.data.forceIconRedraw = false;
693 | }
694 | }
695 | marker.setLatLng(jdata._leafletPosition);
696 | remove = false;
697 | }
698 | else {
699 | var pb = jcluster.averagePosition;
700 | var oldMinLng = pa.lng - lngMargin, newMaxLng = pb.lng + lngMargin;
701 | oldMaxLng = pa.lng + lngMargin;
702 | oldMinLat = pa.lat - latMargin;
703 | oldMaxLat = pa.lat + latMargin;
704 | newMinLng = pb.lng - lngMargin;
705 | newMinLat = pb.lat - latMargin;
706 | newMaxLat = pb.lat + latMargin;
707 | if ((marker._population > 1 && jcluster.population > 1) &&
708 | (oldMaxLng > newMinLng && oldMinLng < newMaxLng && oldMaxLat > newMinLat && oldMinLat < newMaxLat)) {
709 | marker.setLatLng(jdata._leafletPosition);
710 | marker.setIcon(this.BuildLeafletClusterIcon(jcluster));
711 | var poisson = {};
712 | L.Util.extend(poisson, jcluster.bounds);
713 | marker._leafletClusterBounds = poisson;
714 | jdata._leafletOldPopulation = jcluster.population;
715 | jdata._leafletOldHashCode = jcluster.hashCode;
716 | marker._population = jcluster.population;
717 | remove = false;
718 | }
719 | }
720 | if (!remove) {
721 | jdata._leafletMarker = marker;
722 | marker._removeFromMap = false;
723 | newObjectsOnMap.push(jcluster);
724 | clusterCreationList.splice(j, 1);
725 | --j;
726 | --ll;
727 | break;
728 | }
729 | }
730 | }
731 | if (remove) {
732 | if (!marker._removeFromMap)
733 | console.error("wtf");
734 | }
735 | }
736 | }
737 | for (i = 0, l = clusterCreationList.length; i < l; ++i) {
738 | icluster = clusterCreationList[i],
739 | idata = icluster.data;
740 | var iposition = idata._leafletPosition;
741 | var creationMarker;
742 | if (icluster.population === 1) {
743 | creationMarker = this.BuildLeafletMarker(icluster.lastMarker, iposition);
744 | }
745 | else {
746 | creationMarker = this.BuildLeafletCluster(icluster, iposition);
747 | }
748 | creationMarker.addTo(map);
749 | creationMarker.setOpacity(0);
750 | opacityUpdateList.push(creationMarker);
751 | idata._leafletMarker = creationMarker;
752 | creationMarker._zoomLevel = zoom;
753 | creationMarker._hashCode = icluster.hashCode;
754 | creationMarker._population = icluster.population;
755 | newObjectsOnMap.push(icluster);
756 | }
757 | window.setTimeout(function () {
758 | for (i = 0, l = opacityUpdateList.length; i < l; ++i) {
759 | var m = opacityUpdateList[i];
760 | if (m._icon)
761 | L.DomUtil.addClass(m._icon, "prunecluster-anim");
762 | if (m._shadow)
763 | L.DomUtil.addClass(m._shadow, "prunecluster-anim");
764 | m.setOpacity(1);
765 | }
766 | }, 1);
767 | if (this._hardMove) {
768 | for (i = 0, l = markersOnMap.length; i < l; ++i) {
769 | marker = markersOnMap[i];
770 | if (marker._removeFromMap) {
771 | map.removeLayer(marker);
772 | }
773 | }
774 | }
775 | else {
776 | if (this._removeTimeoutId !== 0) {
777 | window.clearTimeout(this._removeTimeoutId);
778 | for (i = 0, l = this._markersRemoveListTimeout.length; i < l; ++i) {
779 | map.removeLayer(this._markersRemoveListTimeout[i]);
780 | }
781 | }
782 | var toRemove = [];
783 | for (i = 0, l = markersOnMap.length; i < l; ++i) {
784 | marker = markersOnMap[i];
785 | if (marker._removeFromMap) {
786 | marker.setOpacity(0);
787 | toRemove.push(marker);
788 | }
789 | }
790 | if (toRemove.length > 0) {
791 | this._removeTimeoutId = window.setTimeout(function () {
792 | for (i = 0, l = toRemove.length; i < l; ++i) {
793 | map.removeLayer(toRemove[i]);
794 | }
795 | _this._removeTimeoutId = 0;
796 | }, 300);
797 | }
798 | this._markersRemoveListTimeout = toRemove;
799 | }
800 | this._objectsOnMap = newObjectsOnMap;
801 | this._hardMove = false;
802 | this._resetIcons = false;
803 | },
804 | FitBounds: function (withFiltered) {
805 | if (withFiltered === void 0) { withFiltered = true; }
806 | var bounds = this.Cluster.ComputeGlobalBounds(withFiltered);
807 | if (bounds) {
808 | this._map.fitBounds(new L.LatLngBounds(new L.LatLng(bounds.minLat, bounds.maxLng), new L.LatLng(bounds.maxLat, bounds.minLng)));
809 | }
810 | },
811 | GetMarkers: function () {
812 | return this.Cluster.GetMarkers();
813 | },
814 | RedrawIcons: function (processView) {
815 | if (processView === void 0) { processView = true; }
816 | this._resetIcons = true;
817 | if (processView) {
818 | this.ProcessView();
819 | }
820 | }
821 | });
822 | var PruneClusterLeafletSpiderfier = (L.Layer ? L.Layer : L.Class).extend({
823 | _2PI: Math.PI * 2,
824 | _circleFootSeparation: 25,
825 | _circleStartAngle: Math.PI / 6,
826 | _spiralFootSeparation: 28,
827 | _spiralLengthStart: 11,
828 | _spiralLengthFactor: 5,
829 | _spiralCountTrigger: 8,
830 | spiderfyDistanceMultiplier: 1,
831 | initialize: function (cluster) {
832 | this._cluster = cluster;
833 | this._currentMarkers = [];
834 | this._multiLines = !!L.multiPolyline;
835 | this._lines = this._multiLines ?
836 | L.multiPolyline([], { weight: 1.5, color: '#222' }) :
837 | L.polyline([], { weight: 1.5, color: '#222' });
838 | },
839 | onAdd: function (map) {
840 | this._map = map;
841 | this._map.on('overlappingmarkers', this.Spiderfy, this);
842 | this._map.on('click', this.Unspiderfy, this);
843 | this._map.on('zoomend', this.Unspiderfy, this);
844 | },
845 | Spiderfy: function (data) {
846 | var _this = this;
847 | if (data.cluster !== this._cluster) {
848 | return;
849 | }
850 | this.Unspiderfy();
851 | var markers = data.markers.filter(function (marker) {
852 | return !marker.filtered;
853 | });
854 | this._currentCenter = data.center;
855 | var centerPoint = this._map.latLngToLayerPoint(data.center);
856 | var points;
857 | if (markers.length >= this._spiralCountTrigger) {
858 | points = this._generatePointsSpiral(markers.length, centerPoint);
859 | }
860 | else {
861 | if (this._multiLines) {
862 | centerPoint.y += 10;
863 | }
864 | points = this._generatePointsCircle(markers.length, centerPoint);
865 | }
866 | var polylines = [];
867 | var leafletMarkers = [];
868 | var projectedPoints = [];
869 | for (var i = 0, l = points.length; i < l; ++i) {
870 | var pos = this._map.layerPointToLatLng(points[i]);
871 | var m = this._cluster.BuildLeafletMarker(markers[i], data.center);
872 | m.setZIndexOffset(5000);
873 | m.setOpacity(0);
874 | this._currentMarkers.push(m);
875 | this._map.addLayer(m);
876 | leafletMarkers.push(m);
877 | projectedPoints.push(pos);
878 | }
879 | window.setTimeout(function () {
880 | for (i = 0, l = points.length; i < l; ++i) {
881 | leafletMarkers[i].setLatLng(projectedPoints[i])
882 | .setOpacity(1);
883 | }
884 | var startTime = +new Date();
885 | var interval = 42, duration = 290;
886 | var anim = window.setInterval(function () {
887 | polylines = [];
888 | var now = +new Date();
889 | var d = now - startTime;
890 | if (d >= duration) {
891 | window.clearInterval(anim);
892 | stepRatio = 1.0;
893 | }
894 | else {
895 | var stepRatio = d / duration;
896 | }
897 | var center = data.center;
898 | for (i = 0, l = points.length; i < l; ++i) {
899 | var p = projectedPoints[i], diffLat = p.lat - center.lat, diffLng = p.lng - center.lng;
900 | polylines.push([center, new L.LatLng(center.lat + diffLat * stepRatio, center.lng + diffLng * stepRatio)]);
901 | }
902 | _this._lines.setLatLngs(polylines);
903 | }, interval);
904 | }, 1);
905 | this._lines.setLatLngs(polylines);
906 | this._map.addLayer(this._lines);
907 | if (data.marker) {
908 | this._clusterMarker = data.marker.setOpacity(0.3);
909 | }
910 | },
911 | _generatePointsCircle: function (count, centerPt) {
912 | var circumference = this.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), legLength = circumference / this._2PI, angleStep = this._2PI / count, res = [], i, angle;
913 | res.length = count;
914 | for (i = count - 1; i >= 0; i--) {
915 | angle = this._circleStartAngle + i * angleStep;
916 | res[i] = new L.Point(Math.round(centerPt.x + legLength * Math.cos(angle)), Math.round(centerPt.y + legLength * Math.sin(angle)));
917 | }
918 | return res;
919 | },
920 | _generatePointsSpiral: function (count, centerPt) {
921 | var legLength = this.spiderfyDistanceMultiplier * this._spiralLengthStart, separation = this.spiderfyDistanceMultiplier * this._spiralFootSeparation, lengthFactor = this.spiderfyDistanceMultiplier * this._spiralLengthFactor, angle = 0, res = [], i;
922 | res.length = count;
923 | for (i = count - 1; i >= 0; i--) {
924 | angle += separation / legLength + i * 0.0005;
925 | res[i] = new L.Point(Math.round(centerPt.x + legLength * Math.cos(angle)), Math.round(centerPt.y + legLength * Math.sin(angle)));
926 | legLength += this._2PI * lengthFactor / angle;
927 | }
928 | return res;
929 | },
930 | Unspiderfy: function () {
931 | var _this = this;
932 | for (var i = 0, l = this._currentMarkers.length; i < l; ++i) {
933 | this._currentMarkers[i].setLatLng(this._currentCenter).setOpacity(0);
934 | }
935 | var markers = this._currentMarkers;
936 | window.setTimeout(function () {
937 | for (i = 0, l = markers.length; i < l; ++i) {
938 | _this._map.removeLayer(markers[i]);
939 | }
940 | }, 300);
941 | this._currentMarkers = [];
942 | this._map.removeLayer(this._lines);
943 | if (this._clusterMarker) {
944 | this._clusterMarker.setOpacity(1);
945 | }
946 | },
947 | onRemove: function (map) {
948 | this.Unspiderfy();
949 | map.off('overlappingmarkers', this.Spiderfy, this);
950 | map.off('click', this.Unspiderfy, this);
951 | map.off('zoomend', this.Unspiderfy, this);
952 | }
953 | });
954 |
--------------------------------------------------------------------------------
/examples/airplane.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SINTEF-9012/PruneCluster/f96bf4ee2c5025d099ef9e0ebdbe0425a0d4c748/examples/airplane.png
--------------------------------------------------------------------------------
/examples/bug-107.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Bug 107
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/examples/bug-11.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Switch
18 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/examples/bug-18.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Switch
18 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/examples/bug-23.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/examples/bug-87.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/examples/examples.css:
--------------------------------------------------------------------------------
1 | @import url('../dist/LeafletStyleSheet.css');
2 |
3 | html, body, #map {
4 | width:100%;
5 | height:100%;
6 | margin: 0;
7 | padding: 0;
8 | font-family: sans-serif;
9 | }
10 |
11 | div#size, a#delete {
12 | position: absolute;
13 | right: 1em;
14 | top: 1em;
15 | background: white;
16 | color: black;
17 | padding: 0.4em;
18 | border-radius: 4px;
19 | z-index: 500;
20 | }
21 |
--------------------------------------------------------------------------------
/examples/helicopter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SINTEF-9012/PruneCluster/f96bf4ee2c5025d099ef9e0ebdbe0425a0d4c748/examples/helicopter.png
--------------------------------------------------------------------------------
/examples/json example/diwalipretty.json.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SINTEF-9012/PruneCluster/f96bf4ee2c5025d099ef9e0ebdbe0425a0d4c748/examples/json example/diwalipretty.json.zip
--------------------------------------------------------------------------------
/examples/json example/heatmap.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Heatmap with Leaflet
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Cluster size: 160
21 |
90 |
91 |
--------------------------------------------------------------------------------
/examples/json example/instructions.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf600
2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;}
3 | {\colortbl;\red255\green255\blue255;}
4 | {\*\expandedcolortbl;\csgray\c100000;}
5 | \margl1440\margr1440\vieww10800\viewh8400\viewkind0
6 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
7 |
8 | \f0\fs24 \cf0 Instructions for using this demo:\
9 | \
10 | 1. You need to unzip the json file for the demo html to work\
11 | 2. This json file is massive and has over 3,000,000 features in it. It takes my MacBook about 1 minute to load the web page - but once loaded, it works seamlessly. \
12 | 3. If you\'92d like to switch it out for your own file, edit the html accordingly. The code is quite simple :) and smaller json files will work far more quickly than my massive one.\
13 | 4. In case you\'92re curious, the json file included is the complete history of all \'93nodes\'94 mapped in Open Street Maps until 2015 in Nepal. Enjoy!}
--------------------------------------------------------------------------------
/examples/moving.1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/examples/moving.10.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/examples/random.100-icons.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/examples/random.100-popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/examples/random.100.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/examples/random.1000.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/examples/random.10000-categories-2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/examples/random.10000-categories.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/examples/random.10000-control.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/examples/random.10000-delete.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Delete
20 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/examples/random.10000-filtering.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Filtering:
20 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/examples/random.10000-fractionalzoom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Zoom:
20 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/examples/random.10000-getclustermarkers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/examples/random.10000-reseticons.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Reset icons
20 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/examples/random.10000-size.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Cluster size: 160
20 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/examples/random.10000-spiderfier.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/examples/random.10000.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/examples/random.1000000.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/examples/random.150000.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/examples/random.60000.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/examples/realworld.50000-categories.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/examples/realworld.50000.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PruneCluster - Realworld 50k
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/meteor/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Packaging [PruneCluster](https://github.com/SINTEF-9012/PruneCluster) for [Meteor.js](http://meteor.com).
4 |
5 |
6 | # Meteor
7 |
8 | If you're new to Meteor, here's what the excitement is all about -
9 | [watch the first two minutes](https://www.youtube.com/watch?v=fsi0aJ9yr2o); you'll be hooked by 1:28.
10 |
11 | That screencast is from 2012. In the meantime, Meteor has become a mature JavaScript-everywhere web
12 | development framework. Read more at [Why Meteor](http://www.meteorpedia.com/read/Why_Meteor).
13 |
14 |
15 | # Issues
16 |
17 | If you encounter an issue while using this package, please CC @mordka when you file it in this repo.
18 |
19 |
20 | # DONE
21 |
22 |
23 | * Instantiation test
24 |
25 |
26 | # TODO
27 |
28 | * Merge with upstream
--------------------------------------------------------------------------------
/meteor/package.js:
--------------------------------------------------------------------------------
1 | // package metadata file for Meteor.js
2 | 'use strict';
3 |
4 | var packageName = 'prunecluster:prunecluster';
5 | var where = 'client';
6 | var packageJson = JSON.parse(Npm.require("fs").readFileSync('package.json'));
7 |
8 | Package.describe({
9 | name: packageName,
10 | version: packageJson.version,
11 | summary: 'PruneCluster: add fast, realtime marker clustering to Leaflet, with low memory footprint',
12 | git: 'https://github.com/SINTEF-9012/PruneCluster.git'
13 | });
14 |
15 | Package.onUse(function(api) {
16 | api.versionsFrom(['METEOR@0.9.0', 'METEOR@1.0']);
17 | api.use(["bevanhunt:leaflet@1.0.3"]);
18 | api.export("PruneCluster",where);
19 | api.export("PruneClusterForLeaflet",where);
20 | api.export("PruneClusterLeafletSpiderfier",where);
21 | api.addFiles(['dist/LeafletStyleSheet.css',
22 | 'dist/PruneCluster.js',
23 | 'dist/PruneCluster.js.map',
24 | 'dist/PruneCluster.min.js'
25 | ],where,{bare: true});
26 | });
27 |
28 | Package.onTest(function (api) {
29 | api.use(packageName, where);
30 | api.use('tinytest', where);
31 |
32 | api.addFiles('meteor/test.js', where);
33 | });
34 |
--------------------------------------------------------------------------------
/meteor/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Tinytest.add('PruneCluster.is', function (test) {
4 | var pruneCluster = new PruneClusterForLeaflet();
5 | var pruneMarker = new PruneCluster.Marker(0,0);
6 | test.isNotUndefined(pruneCluster, 'Cluster Instantiation OK');
7 | test.equal(pruneMarker.position,{ lat: 0, lng: 0 }, 'Marker Instantiation OK');
8 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sintef/prune-cluster",
3 | "version": "2.1.0",
4 | "homepage": "https://github.com/SINTEF-9012/PruneCluster",
5 | "description": "Fast and realtime marker clustering for Leaflet",
6 | "keywords": [
7 | "map",
8 | "clustering",
9 | "leaflet",
10 | "marker",
11 | "realtime"
12 | ],
13 | "license": "MIT",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/SINTEF-9012/PruneCluster.git"
17 | },
18 | "main": "./dist/PruneCluster.js",
19 | "devDependencies": {
20 | "grunt": "^1.0.1",
21 | "grunt-contrib-clean": "~1.1.0",
22 | "grunt-contrib-copy": "~1.0.0",
23 | "grunt-contrib-uglify": "~3.0.1",
24 | "grunt-exec": "^2.0.0",
25 | "grunt-ts": "~6.0.0-beta.16",
26 | "load-grunt-tasks": "~3.5.2",
27 | "spacejam": "^1.1.1",
28 | "time-grunt": "~1.4.0",
29 | "typescript": "^2.4.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tsd.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "v4",
3 | "repo": "borisyankov/DefinitelyTyped",
4 | "ref": "master",
5 | "path": "typings",
6 | "bundle": "typings/tsd.d.ts",
7 | "installed": {
8 | "leaflet/leaflet.d.ts": {
9 | "commit": "b57c33fec0be3cba8f5e9cec642db873565923d0"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------