PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.39
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.39
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / assets / libraries / markerclusterer / markerclusterer.js

markerclusterer.js in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.39, at includes/assets/libraries/markerclusterer/markerclusterer.js

1,321 lines 34.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // ==ClosureCompiler==
2 // @compilation_level ADVANCED_OPTIMIZATIONS
3 // @externs_url http://closure-compiler.googlecode.com/svn/trunk/contrib/externs/maps/google_maps_api_v3_3.js
4 // ==/ClosureCompiler==
5
6 /**
7 * @name MarkerClusterer for Google Maps v3
8 * @version version 1.0.3
9 * @author Luke Mahe
10 * @fileoverview
11 * The library creates and manages per-zoom-level clusters for large amounts of
12 * markers.
13 */
14
15 /**
16 * @license
17 * Licensed under the Apache License, Version 2.0 (the "License");
18 * you may not use this file except in compliance with the License.
19 * You may obtain a copy of the License at
20 *
21 * http://www.apache.org/licenses/LICENSE-2.0
22 *
23 * Unless required by applicable law or agreed to in writing, software
24 * distributed under the License is distributed on an "AS IS" BASIS,
25 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26 * See the License for the specific language governing permissions and
27 * limitations under the License.
28 */
29
30
31 /**
32 * A Marker Clusterer that clusters markers.
33 *
34 * @param {google.maps.Map} map The Google map to attach to.
35 * @param {Array.<google.maps.Marker>=} opt_markers Optional markers to add to
36 * the cluster.
37 * @param {Object=} opt_options support the following options:
38 * 'gridSize': (number) The grid size of a cluster in pixels.
39 * 'maxZoom': (number) The maximum zoom level that a marker can be part of a
40 * cluster.
41 * 'zoomOnClick': (boolean) Whether the default behaviour of clicking on a
42 * cluster is to zoom into it.
43 * 'imagePath': (string) The base URL where the images representing
44 * clusters will be found. The full URL will be:
45 * {imagePath}[1-5].{imageExtension}
46 * Default: '../images/m'.
47 * 'imageExtension': (string) The suffix for images URL representing
48 * clusters will be found. See _imagePath_ for details.
49 * Default: 'png'.
50 * 'averageCenter': (boolean) Whether the center of each cluster should be
51 * the average of all markers in the cluster.
52 * 'minimumClusterSize': (number) The minimum number of markers to be in a
53 * cluster before the markers are hidden and a count
54 * is shown.
55 * 'styles': (object) An object that has style properties:
56 * 'url': (string) The image url.
57 * 'height': (number) The image height.
58 * 'width': (number) The image width.
59 * 'anchor': (Array) The anchor position of the label text.
60 * 'textColor': (string) The text color.
61 * 'textSize': (number) The text size.
62 * 'backgroundPosition': (string) The position of the background x, y.
63 * @constructor
64 * @extends google.maps.OverlayView
65 */
66 function MarkerClusterer(map, opt_markers, opt_options) {
67 // MarkerClusterer implements google.maps.OverlayView interface. We use the
68 // extend function to extend MarkerClusterer with google.maps.OverlayView
69 // because it might not always be available when the code is defined so we
70 // look for it at the last possible moment. If it doesn't exist now then
71 // there is no point going ahead :)
72 this.extend(MarkerClusterer, google.maps.OverlayView);
73 this.map_ = map;
74
75 /**
76 * @type {Array.<google.maps.Marker>}
77 * @private
78 */
79 this.markers_ = [];
80
81 /**
82 * @type {Array.<Cluster>}
83 */
84 this.clusters_ = [];
85
86 this.sizes = [53, 56, 66, 78, 90];
87
88 /**
89 * @private
90 */
91 this.styles_ = [];
92
93 /**
94 * @type {boolean}
95 * @private
96 */
97 this.ready_ = false;
98
99 var options = opt_options || {};
100
101 /**
102 * @type {number}
103 * @private
104 */
105 this.gridSize_ = options['gridSize'] || 60;
106
107 /**
108 * @private
109 */
110 this.minClusterSize_ = options['minimumClusterSize'] || 2;
111
112
113 /**
114 * @type {?number}
115 * @private
116 */
117 this.maxZoom_ = options['maxZoom'] || null;
118
119 this.styles_ = options['styles'] || [];
120
121 /**
122 * @type {string}
123 * @private
124 */
125 this.imagePath_ = options['imagePath'] ||
126 this.MARKER_CLUSTER_IMAGE_PATH_;
127
128 /**
129 * @type {string}
130 * @private
131 */
132 this.imageExtension_ = options['imageExtension'] ||
133 this.MARKER_CLUSTER_IMAGE_EXTENSION_;
134
135 /**
136 * @type {boolean}
137 * @private
138 */
139 this.zoomOnClick_ = true;
140
141 if (options['zoomOnClick'] != undefined) {
142 this.zoomOnClick_ = options['zoomOnClick'];
143 }
144
145 /**
146 * @type {boolean}
147 * @private
148 */
149 this.averageCenter_ = false;
150
151 if (options['averageCenter'] != undefined) {
152 this.averageCenter_ = options['averageCenter'];
153 }
154
155 this.setupStyles_();
156
157 this.setMap(map);
158
159 /**
160 * @type {number}
161 * @private
162 */
163 this.prevZoom_ = this.map_.getZoom();
164
165 // Add the map event listeners
166 var that = this;
167 google.maps.event.addListener(this.map_, 'zoom_changed', function() {
168 // Determines map type and prevent illegal zoom levels
169 var zoom = that.map_.getZoom();
170 var minZoom = that.map_.minZoom || 0;
171 var maxZoom = Math.min(that.map_.maxZoom || 100,
172 that.map_.mapTypes[that.map_.getMapTypeId()].maxZoom);
173 zoom = Math.min(Math.max(zoom,minZoom),maxZoom);
174
175 if (that.prevZoom_ != zoom) {
176 that.prevZoom_ = zoom;
177 that.resetViewport();
178 }
179 });
180
181 google.maps.event.addListener(this.map_, 'idle', function() {
182 that.redraw();
183 });
184
185 // Finally, add the markers
186 if (opt_markers && (opt_markers.length || Object.keys(opt_markers).length)) {
187 this.addMarkers(opt_markers, false);
188 }
189 }
190
191
192 /**
193 * The marker cluster image path.
194 *
195 * @type {string}
196 * @private
197 */
198 MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m';
199
200
201 /**
202 * The marker cluster image path.
203 *
204 * @type {string}
205 * @private
206 */
207 MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png';
208
209
210 /**
211 * Extends a objects prototype by anothers.
212 *
213 * @param {Object} obj1 The object to be extended.
214 * @param {Object} obj2 The object to extend with.
215 * @return {Object} The new extended object.
216 * @ignore
217 */
218 MarkerClusterer.prototype.extend = function(obj1, obj2) {
219 return (function(object) {
220 for (var property in object.prototype) {
221 this.prototype[property] = object.prototype[property];
222 }
223 return this;
224 }).apply(obj1, [obj2]);
225 };
226
227
228 /**
229 * Implementaion of the interface method.
230 * @ignore
231 */
232 MarkerClusterer.prototype.onAdd = function() {
233 this.setReady_(true);
234 };
235
236 /**
237 * Implementaion of the interface method.
238 * @ignore
239 */
240 MarkerClusterer.prototype.draw = function() {};
241
242 /**
243 * Sets up the styles object.
244 *
245 * @private
246 */
247 MarkerClusterer.prototype.setupStyles_ = function() {
248 if (this.styles_.length) {
249 return;
250 }
251
252 for (var i = 0, size; size = this.sizes[i]; i++) {
253 this.styles_.push({
254 url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_,
255 height: size,
256 width: size
257 });
258 }
259 };
260
261 /**
262 * Fit the map to the bounds of the markers in the clusterer.
263 */
264 MarkerClusterer.prototype.fitMapToMarkers = function() {
265 var markers = this.getMarkers();
266 var bounds = new google.maps.LatLngBounds();
267 for (var i = 0, marker; marker = markers[i]; i++) {
268 bounds.extend(marker.getPosition());
269 }
270
271 this.map_.fitBounds(bounds);
272 };
273
274
275 /**
276 * Sets the styles.
277 *
278 * @param {Object} styles The style to set.
279 */
280 MarkerClusterer.prototype.setStyles = function(styles) {
281 this.styles_ = styles;
282 };
283
284
285 /**
286 * Gets the styles.
287 *
288 * @return {Object} The styles object.
289 */
290 MarkerClusterer.prototype.getStyles = function() {
291 return this.styles_;
292 };
293
294
295 /**
296 * Whether zoom on click is set.
297 *
298 * @return {boolean} True if zoomOnClick_ is set.
299 */
300 MarkerClusterer.prototype.isZoomOnClick = function() {
301 return this.zoomOnClick_;
302 };
303
304 /**
305 * Whether average center is set.
306 *
307 * @return {boolean} True if averageCenter_ is set.
308 */
309 MarkerClusterer.prototype.isAverageCenter = function() {
310 return this.averageCenter_;
311 };
312
313
314 /**
315 * Returns the array of markers in the clusterer.
316 *
317 * @return {Array.<google.maps.Marker>} The markers.
318 */
319 MarkerClusterer.prototype.getMarkers = function() {
320 return this.markers_;
321 };
322
323
324 /**
325 * Returns the number of markers in the clusterer
326 *
327 * @return {Number} The number of markers.
328 */
329 MarkerClusterer.prototype.getTotalMarkers = function() {
330 return this.markers_.length;
331 };
332
333
334 /**
335 * Sets the max zoom for the clusterer.
336 *
337 * @param {number} maxZoom The max zoom level.
338 */
339 MarkerClusterer.prototype.setMaxZoom = function(maxZoom) {
340 this.maxZoom_ = maxZoom;
341 };
342
343
344 /**
345 * Gets the max zoom for the clusterer.
346 *
347 * @return {number} The max zoom level.
348 */
349 MarkerClusterer.prototype.getMaxZoom = function() {
350 return this.maxZoom_;
351 };
352
353
354 /**
355 * The function for calculating the cluster icon image.
356 *
357 * @param {Array.<google.maps.Marker>} markers The markers in the clusterer.
358 * @param {number} numStyles The number of styles available.
359 * @return {Object} A object properties: 'text' (string) and 'index' (number).
360 * @private
361 */
362 MarkerClusterer.prototype.calculator_ = function(markers, numStyles) {
363 var index = 0;
364 var count = markers.length;
365 var dv = count;
366 while (dv !== 0) {
367 dv = parseInt(dv / 10, 10);
368 index++;
369 }
370
371 index = Math.min(index, numStyles);
372 return {
373 text: count,
374 index: index
375 };
376 };
377
378
379 /**
380 * Set the calculator function.
381 *
382 * @param {function(Array, number)} calculator The function to set as the
383 * calculator. The function should return a object properties:
384 * 'text' (string) and 'index' (number).
385 *
386 */
387 MarkerClusterer.prototype.setCalculator = function(calculator) {
388 this.calculator_ = calculator;
389 };
390
391
392 /**
393 * Get the calculator function.
394 *
395 * @return {function(Array, number)} the calculator function.
396 */
397 MarkerClusterer.prototype.getCalculator = function() {
398 return this.calculator_;
399 };
400
401
402 /**
403 * Add an array of markers to the clusterer.
404 *
405 * @param {Array.<google.maps.Marker>} markers The markers to add.
406 * @param {boolean=} opt_nodraw Whether to redraw the clusters.
407 */
408 MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) {
409 if (markers.length) {
410 for (var i = 0, marker; marker = markers[i]; i++) {
411 this.pushMarkerTo_(marker);
412 }
413 } else if (Object.keys(markers).length) {
414 for (var marker in markers) {
415 this.pushMarkerTo_(markers[marker]);
416 }
417 }
418 if (!opt_nodraw) {
419 this.redraw();
420 }
421 };
422
423
424 /**
425 * Pushes a marker to the clusterer.
426 *
427 * @param {google.maps.Marker} marker The marker to add.
428 * @private
429 */
430 MarkerClusterer.prototype.pushMarkerTo_ = function(marker) {
431 marker.isAdded = false;
432 if (marker['draggable']) {
433 // If the marker is draggable add a listener so we update the clusters on
434 // the drag end.
435 var that = this;
436 google.maps.event.addListener(marker, 'dragend', function() {
437 marker.isAdded = false;
438 that.repaint();
439 });
440 }
441 this.markers_.push(marker);
442 };
443
444
445 /**
446 * Adds a marker to the clusterer and redraws if needed.
447 *
448 * @param {google.maps.Marker} marker The marker to add.
449 * @param {boolean=} opt_nodraw Whether to redraw the clusters.
450 */
451 MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) {
452 this.pushMarkerTo_(marker);
453 if (!opt_nodraw) {
454 this.redraw();
455 }
456 };
457
458
459 /**
460 * Removes a marker and returns true if removed, false if not
461 *
462 * @param {google.maps.Marker} marker The marker to remove
463 * @return {boolean} Whether the marker was removed or not
464 * @private
465 */
466 MarkerClusterer.prototype.removeMarker_ = function(marker) {
467 var index = -1;
468 if (this.markers_.indexOf) {
469 index = this.markers_.indexOf(marker);
470 } else {
471 for (var i = 0, m; m = this.markers_[i]; i++) {
472 if (m == marker) {
473 index = i;
474 break;
475 }
476 }
477 }
478
479 if (index == -1) {
480 // Marker is not in our list of markers.
481 return false;
482 }
483
484 marker.setMap(null);
485
486 this.markers_.splice(index, 1);
487
488 return true;
489 };
490
491
492 /**
493 * Remove a marker from the cluster.
494 *
495 * @param {google.maps.Marker} marker The marker to remove.
496 * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
497 * @return {boolean} True if the marker was removed.
498 */
499 MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) {
500 var removed = this.removeMarker_(marker);
501
502 if (!opt_nodraw && removed) {
503 this.resetViewport();
504 this.redraw();
505 return true;
506 } else {
507 return false;
508 }
509 };
510
511
512 /**
513 * Removes an array of markers from the cluster.
514 *
515 * @param {Array.<google.maps.Marker>} markers The markers to remove.
516 * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
517 */
518 MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) {
519 // create a local copy of markers if required
520 // (removeMarker_ modifies the getMarkers() array in place)
521 var markersCopy = markers === this.getMarkers() ? markers.slice() : markers;
522 var removed = false;
523
524 for (var i = 0, marker; marker = markersCopy[i]; i++) {
525 var r = this.removeMarker_(marker);
526 removed = removed || r;
527 }
528
529 if (!opt_nodraw && removed) {
530 this.resetViewport();
531 this.redraw();
532 return true;
533 }
534 };
535
536
537 /**
538 * Sets the clusterer's ready state.
539 *
540 * @param {boolean} ready The state.
541 * @private
542 */
543 MarkerClusterer.prototype.setReady_ = function(ready) {
544 if (!this.ready_) {
545 this.ready_ = ready;
546 this.createClusters_();
547 }
548 };
549
550
551 /**
552 * Returns the number of clusters in the clusterer.
553 *
554 * @return {number} The number of clusters.
555 */
556 MarkerClusterer.prototype.getTotalClusters = function() {
557 return this.clusters_.length;
558 };
559
560
561 /**
562 * Returns the google map that the clusterer is associated with.
563 *
564 * @return {google.maps.Map} The map.
565 */
566 MarkerClusterer.prototype.getMap = function() {
567 return this.map_;
568 };
569
570
571 /**
572 * Sets the google map that the clusterer is associated with.
573 *
574 * @param {google.maps.Map} map The map.
575 */
576 MarkerClusterer.prototype.setMap = function(map) {
577 this.map_ = map;
578 };
579
580
581 /**
582 * Returns the size of the grid.
583 *
584 * @return {number} The grid size.
585 */
586 MarkerClusterer.prototype.getGridSize = function() {
587 return this.gridSize_;
588 };
589
590
591 /**
592 * Sets the size of the grid.
593 *
594 * @param {number} size The grid size.
595 */
596 MarkerClusterer.prototype.setGridSize = function(size) {
597 this.gridSize_ = size;
598 };
599
600
601 /**
602 * Returns the min cluster size.
603 *
604 * @return {number} The grid size.
605 */
606 MarkerClusterer.prototype.getMinClusterSize = function() {
607 return this.minClusterSize_;
608 };
609
610 /**
611 * Sets the min cluster size.
612 *
613 * @param {number} size The grid size.
614 */
615 MarkerClusterer.prototype.setMinClusterSize = function(size) {
616 this.minClusterSize_ = size;
617 };
618
619
620 /**
621 * Extends a bounds object by the grid size.
622 *
623 * @param {google.maps.LatLngBounds} bounds The bounds to extend.
624 * @return {google.maps.LatLngBounds} The extended bounds.
625 */
626 MarkerClusterer.prototype.getExtendedBounds = function(bounds) {
627 var projection = this.getProjection();
628
629 // Turn the bounds into latlng.
630 var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
631 bounds.getNorthEast().lng());
632 var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
633 bounds.getSouthWest().lng());
634
635 // Convert the points to pixels and the extend out by the grid size.
636 var trPix = projection.fromLatLngToDivPixel(tr);
637 trPix.x += this.gridSize_;
638 trPix.y -= this.gridSize_;
639
640 var blPix = projection.fromLatLngToDivPixel(bl);
641 blPix.x -= this.gridSize_;
642 blPix.y += this.gridSize_;
643
644 // Convert the pixel points back to LatLng
645 var ne = projection.fromDivPixelToLatLng(trPix);
646 var sw = projection.fromDivPixelToLatLng(blPix);
647
648 // Extend the bounds to contain the new bounds.
649 bounds.extend(ne);
650 bounds.extend(sw);
651
652 return bounds;
653 };
654
655
656 /**
657 * Determins if a marker is contained in a bounds.
658 *
659 * @param {google.maps.Marker} marker The marker to check.
660 * @param {google.maps.LatLngBounds} bounds The bounds to check against.
661 * @return {boolean} True if the marker is in the bounds.
662 * @private
663 */
664 MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) {
665 return bounds.contains(marker.getPosition());
666 };
667
668
669 /**
670 * Clears all clusters and markers from the clusterer.
671 */
672 MarkerClusterer.prototype.clearMarkers = function() {
673 this.resetViewport(true);
674
675 // Set the markers a empty array.
676 this.markers_ = [];
677 };
678
679
680 /**
681 * Clears all existing clusters and recreates them.
682 * @param {boolean} opt_hide To also hide the marker.
683 */
684 MarkerClusterer.prototype.resetViewport = function(opt_hide) {
685 // Remove all the clusters
686 for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
687 cluster.remove();
688 }
689
690 // Reset the markers to not be added and to be invisible.
691 for (var i = 0, marker; marker = this.markers_[i]; i++) {
692 marker.isAdded = false;
693 if (opt_hide) {
694 marker.setMap(null);
695 }
696 }
697
698 this.clusters_ = [];
699 };
700
701 /**
702 *
703 */
704 MarkerClusterer.prototype.repaint = function() {
705 var oldClusters = this.clusters_.slice();
706 this.clusters_.length = 0;
707 this.resetViewport();
708 this.redraw();
709
710 // Remove the old clusters.
711 // Do it in a timeout so the other clusters have been drawn first.
712 window.setTimeout(function() {
713 for (var i = 0, cluster; cluster = oldClusters[i]; i++) {
714 cluster.remove();
715 }
716 }, 0);
717 };
718
719
720 /**
721 * Redraws the clusters.
722 */
723 MarkerClusterer.prototype.redraw = function() {
724 this.createClusters_();
725 };
726
727
728 /**
729 * Calculates the distance between two latlng locations in km.
730 * @see http://www.movable-type.co.uk/scripts/latlong.html
731 *
732 * @param {google.maps.LatLng} p1 The first lat lng point.
733 * @param {google.maps.LatLng} p2 The second lat lng point.
734 * @return {number} The distance between the two points in km.
735 * @private
736 */
737 MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) {
738 if (!p1 || !p2) {
739 return 0;
740 }
741
742 var R = 6371; // Radius of the Earth in km
743 var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
744 var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
745 var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
746 Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
747 Math.sin(dLon / 2) * Math.sin(dLon / 2);
748 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
749 var d = R * c;
750 return d;
751 };
752
753
754 /**
755 * Add a marker to a cluster, or creates a new cluster.
756 *
757 * @param {google.maps.Marker} marker The marker to add.
758 * @private
759 */
760 MarkerClusterer.prototype.addToClosestCluster_ = function(marker) {
761 var distance = 40000; // Some large number
762 var clusterToAddTo = null;
763 var pos = marker.getPosition();
764 for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
765 var center = cluster.getCenter();
766 if (center) {
767 var d = this.distanceBetweenPoints_(center, marker.getPosition());
768 if (d < distance) {
769 distance = d;
770 clusterToAddTo = cluster;
771 }
772 }
773 }
774
775 if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
776 clusterToAddTo.addMarker(marker);
777 } else {
778 var cluster = new Cluster(this);
779 cluster.addMarker(marker);
780 this.clusters_.push(cluster);
781 }
782 };
783
784
785 /**
786 * Creates the clusters.
787 *
788 * @private
789 */
790 MarkerClusterer.prototype.createClusters_ = function() {
791 if (!this.ready_) {
792 return;
793 }
794
795 // Get our current map view bounds.
796 // Create a new bounds object so we don't affect the map.
797 var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(),
798 this.map_.getBounds().getNorthEast());
799 var bounds = this.getExtendedBounds(mapBounds);
800
801 for (var i = 0, marker; marker = this.markers_[i]; i++) {
802 if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
803 this.addToClosestCluster_(marker);
804 }
805 }
806 };
807
808
809 /**
810 * A cluster that contains markers.
811 *
812 * @param {MarkerClusterer} markerClusterer The markerclusterer that this
813 * cluster is associated with.
814 * @constructor
815 * @ignore
816 */
817 function Cluster(markerClusterer) {
818 this.markerClusterer_ = markerClusterer;
819 this.map_ = markerClusterer.getMap();
820 this.gridSize_ = markerClusterer.getGridSize();
821 this.minClusterSize_ = markerClusterer.getMinClusterSize();
822 this.averageCenter_ = markerClusterer.isAverageCenter();
823 this.center_ = null;
824 this.markers_ = [];
825 this.bounds_ = null;
826 this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(),
827 markerClusterer.getGridSize());
828 }
829
830 /**
831 * Determins if a marker is already added to the cluster.
832 *
833 * @param {google.maps.Marker} marker The marker to check.
834 * @return {boolean} True if the marker is already added.
835 */
836 Cluster.prototype.isMarkerAlreadyAdded = function(marker) {
837 if (this.markers_.indexOf) {
838 return this.markers_.indexOf(marker) != -1;
839 } else {
840 for (var i = 0, m; m = this.markers_[i]; i++) {
841 if (m == marker) {
842 return true;
843 }
844 }
845 }
846 return false;
847 };
848
849
850 /**
851 * Add a marker the cluster.
852 *
853 * @param {google.maps.Marker} marker The marker to add.
854 * @return {boolean} True if the marker was added.
855 */
856 Cluster.prototype.addMarker = function(marker) {
857 if (this.isMarkerAlreadyAdded(marker)) {
858 return false;
859 }
860
861 if (!this.center_) {
862 this.center_ = marker.getPosition();
863 this.calculateBounds_();
864 } else {
865 if (this.averageCenter_) {
866 var l = this.markers_.length + 1;
867 var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l;
868 var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l;
869 this.center_ = new google.maps.LatLng(lat, lng);
870 this.calculateBounds_();
871 }
872 }
873
874 marker.isAdded = true;
875 this.markers_.push(marker);
876
877 var len = this.markers_.length;
878 if (len < this.minClusterSize_ && marker.getMap() != this.map_) {
879 // Min cluster size not reached so show the marker.
880 marker.setMap(this.map_);
881 }
882
883 if (len == this.minClusterSize_) {
884 // Hide the markers that were showing.
885 for (var i = 0; i < len; i++) {
886 this.markers_[i].setMap(null);
887 }
888 }
889
890 if (len >= this.minClusterSize_) {
891 marker.setMap(null);
892 }
893
894 this.updateIcon();
895 return true;
896 };
897
898
899 /**
900 * Returns the marker clusterer that the cluster is associated with.
901 *
902 * @return {MarkerClusterer} The associated marker clusterer.
903 */
904 Cluster.prototype.getMarkerClusterer = function() {
905 return this.markerClusterer_;
906 };
907
908
909 /**
910 * Returns the bounds of the cluster.
911 *
912 * @return {google.maps.LatLngBounds} the cluster bounds.
913 */
914 Cluster.prototype.getBounds = function() {
915 var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
916 var markers = this.getMarkers();
917 for (var i = 0, marker; marker = markers[i]; i++) {
918 bounds.extend(marker.getPosition());
919 }
920 return bounds;
921 };
922
923
924 /**
925 * Removes the cluster
926 */
927 Cluster.prototype.remove = function() {
928 this.clusterIcon_.remove();
929 this.markers_.length = 0;
930 delete this.markers_;
931 };
932
933
934 /**
935 * Returns the number of markers in the cluster.
936 *
937 * @return {number} The number of markers in the cluster.
938 */
939 Cluster.prototype.getSize = function() {
940 return this.markers_.length;
941 };
942
943
944 /**
945 * Returns a list of the markers in the cluster.
946 *
947 * @return {Array.<google.maps.Marker>} The markers in the cluster.
948 */
949 Cluster.prototype.getMarkers = function() {
950 return this.markers_;
951 };
952
953
954 /**
955 * Returns the center of the cluster.
956 *
957 * @return {google.maps.LatLng} The cluster center.
958 */
959 Cluster.prototype.getCenter = function() {
960 return this.center_;
961 };
962
963
964 /**
965 * Calculated the extended bounds of the cluster with the grid.
966 *
967 * @private
968 */
969 Cluster.prototype.calculateBounds_ = function() {
970 var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
971 this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
972 };
973
974
975 /**
976 * Determines if a marker lies in the clusters bounds.
977 *
978 * @param {google.maps.Marker} marker The marker to check.
979 * @return {boolean} True if the marker lies in the bounds.
980 */
981 Cluster.prototype.isMarkerInClusterBounds = function(marker) {
982 return this.bounds_.contains(marker.getPosition());
983 };
984
985
986 /**
987 * Returns the map that the cluster is associated with.
988 *
989 * @return {google.maps.Map} The map.
990 */
991 Cluster.prototype.getMap = function() {
992 return this.map_;
993 };
994
995
996 /**
997 * Updates the cluster icon
998 */
999 Cluster.prototype.updateIcon = function() {
1000 var zoom = this.map_.getZoom();
1001 var mz = this.markerClusterer_.getMaxZoom();
1002
1003 if (mz && zoom > mz) {
1004 // The zoom is greater than our max zoom so show all the markers in cluster.
1005 for (var i = 0, marker; marker = this.markers_[i]; i++) {
1006 marker.setMap(this.map_);
1007 }
1008 return;
1009 }
1010
1011 if (this.markers_.length < this.minClusterSize_) {
1012 // Min cluster size not yet reached.
1013 this.clusterIcon_.hide();
1014 return;
1015 }
1016
1017 var numStyles = this.markerClusterer_.getStyles().length;
1018 var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
1019 this.clusterIcon_.setCenter(this.center_);
1020 this.clusterIcon_.setSums(sums);
1021 this.clusterIcon_.show();
1022 };
1023
1024
1025 /**
1026 * A cluster icon
1027 *
1028 * @param {Cluster} cluster The cluster to be associated with.
1029 * @param {Object} styles An object that has style properties:
1030 * 'url': (string) The image url.
1031 * 'height': (number) The image height.
1032 * 'width': (number) The image width.
1033 * 'anchor': (Array) The anchor position of the label text.
1034 * 'textColor': (string) The text color.
1035 * 'textSize': (number) The text size.
1036 * 'backgroundPosition: (string) The background postition x, y.
1037 * @param {number=} opt_padding Optional padding to apply to the cluster icon.
1038 * @constructor
1039 * @extends google.maps.OverlayView
1040 * @ignore
1041 */
1042 function ClusterIcon(cluster, styles, opt_padding) {
1043 cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);
1044
1045 this.styles_ = styles;
1046 this.padding_ = opt_padding || 0;
1047 this.cluster_ = cluster;
1048 this.center_ = null;
1049 this.map_ = cluster.getMap();
1050 this.div_ = null;
1051 this.sums_ = null;
1052 this.visible_ = false;
1053
1054 this.setMap(this.map_);
1055 }
1056
1057
1058 /**
1059 * Triggers the clusterclick event and zoom's if the option is set.
1060 */
1061 ClusterIcon.prototype.triggerClusterClick = function() {
1062 var markerClusterer = this.cluster_.getMarkerClusterer();
1063
1064 // Trigger the clusterclick event.
1065 google.maps.event.trigger(markerClusterer.map_, 'clusterclick', this.cluster_);
1066
1067 if (markerClusterer.isZoomOnClick()) {
1068 // Zoom into the cluster.
1069 this.map_.fitBounds(this.cluster_.getBounds());
1070 }
1071 };
1072
1073
1074 /**
1075 * Adding the cluster icon to the dom.
1076 * @ignore
1077 */
1078 ClusterIcon.prototype.onAdd = function() {
1079 this.div_ = document.createElement('DIV');
1080 if (this.visible_) {
1081 var pos = this.getPosFromLatLng_(this.center_);
1082 this.div_.style.cssText = this.createCss(pos);
1083 this.div_.innerHTML = this.sums_.text;
1084 }
1085
1086 var panes = this.getPanes();
1087 panes.overlayMouseTarget.appendChild(this.div_);
1088
1089 var that = this;
1090 google.maps.event.addDomListener(this.div_, 'click', function() {
1091 that.triggerClusterClick();
1092 });
1093 };
1094
1095
1096 /**
1097 * Returns the position to place the div dending on the latlng.
1098 *
1099 * @param {google.maps.LatLng} latlng The position in latlng.
1100 * @return {google.maps.Point} The position in pixels.
1101 * @private
1102 */
1103 ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) {
1104 var pos = this.getProjection().fromLatLngToDivPixel(latlng);
1105 pos.x -= parseInt(this.width_ / 2, 10);
1106 pos.y -= parseInt(this.height_ / 2, 10);
1107 return pos;
1108 };
1109
1110
1111 /**
1112 * Draw the icon.
1113 * @ignore
1114 */
1115 ClusterIcon.prototype.draw = function() {
1116 if (this.visible_) {
1117 var pos = this.getPosFromLatLng_(this.center_);
1118 this.div_.style.top = pos.y + 'px';
1119 this.div_.style.left = pos.x + 'px';
1120 this.div_.style.zIndex = google.maps.Marker.MAX_ZINDEX + 1;
1121 }
1122 };
1123
1124
1125 /**
1126 * Hide the icon.
1127 */
1128 ClusterIcon.prototype.hide = function() {
1129 if (this.div_) {
1130 this.div_.style.display = 'none';
1131 }
1132 this.visible_ = false;
1133 };
1134
1135
1136 /**
1137 * Position and show the icon.
1138 */
1139 ClusterIcon.prototype.show = function() {
1140 if (this.div_) {
1141 var pos = this.getPosFromLatLng_(this.center_);
1142 this.div_.style.cssText = this.createCss(pos);
1143 this.div_.style.display = '';
1144 }
1145 this.visible_ = true;
1146 };
1147
1148
1149 /**
1150 * Remove the icon from the map
1151 */
1152 ClusterIcon.prototype.remove = function() {
1153 this.setMap(null);
1154 };
1155
1156
1157 /**
1158 * Implementation of the onRemove interface.
1159 * @ignore
1160 */
1161 ClusterIcon.prototype.onRemove = function() {
1162 if (this.div_ && this.div_.parentNode) {
1163 this.hide();
1164 this.div_.parentNode.removeChild(this.div_);
1165 this.div_ = null;
1166 }
1167 };
1168
1169
1170 /**
1171 * Set the sums of the icon.
1172 *
1173 * @param {Object} sums The sums containing:
1174 * 'text': (string) The text to display in the icon.
1175 * 'index': (number) The style index of the icon.
1176 */
1177 ClusterIcon.prototype.setSums = function(sums) {
1178 this.sums_ = sums;
1179 this.text_ = sums.text;
1180 this.index_ = sums.index;
1181 if (this.div_) {
1182 this.div_.innerHTML = sums.text;
1183 }
1184
1185 this.useStyle();
1186 };
1187
1188
1189 /**
1190 * Sets the icon to the the styles.
1191 */
1192 ClusterIcon.prototype.useStyle = function() {
1193 var index = Math.max(0, this.sums_.index - 1);
1194 index = Math.min(this.styles_.length - 1, index);
1195 var style = this.styles_[index];
1196 this.url_ = style['url'];
1197 this.height_ = style['height'];
1198 this.width_ = style['width'];
1199 this.textColor_ = style['textColor'];
1200 this.anchor_ = style['anchor'];
1201 this.textSize_ = style['textSize'];
1202 this.backgroundPosition_ = style['backgroundPosition'];
1203 };
1204
1205
1206 /**
1207 * Sets the center of the icon.
1208 *
1209 * @param {google.maps.LatLng} center The latlng to set as the center.
1210 */
1211 ClusterIcon.prototype.setCenter = function(center) {
1212 this.center_ = center;
1213 };
1214
1215
1216 /**
1217 * Create the css text based on the position of the icon.
1218 *
1219 * @param {google.maps.Point} pos The position.
1220 * @return {string} The css style text.
1221 */
1222 ClusterIcon.prototype.createCss = function(pos) {
1223 var style = [];
1224 style.push('background-image:url(' + this.url_ + ');');
1225 var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0';
1226 style.push('background-position:' + backgroundPosition + ';');
1227
1228 if (typeof this.anchor_ === 'object') {
1229 if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
1230 this.anchor_[0] < this.height_) {
1231 style.push('height:' + (this.height_ - this.anchor_[0]) +
1232 'px; padding-top:' + this.anchor_[0] + 'px;');
1233 } else {
1234 style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
1235 'px;');
1236 }
1237 if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
1238 this.anchor_[1] < this.width_) {
1239 style.push('width:' + (this.width_ - this.anchor_[1]) +
1240 'px; padding-left:' + this.anchor_[1] + 'px;');
1241 } else {
1242 style.push('width:' + this.width_ + 'px; text-align:center;');
1243 }
1244 } else {
1245 style.push('height:' + this.height_ + 'px; line-height:' +
1246 this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
1247 }
1248
1249 var txtColor = this.textColor_ ? this.textColor_ : 'black';
1250 var txtSize = this.textSize_ ? this.textSize_ : 11;
1251
1252 style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
1253 pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' +
1254 txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold');
1255 return style.join('');
1256 };
1257
1258
1259 // Export Symbols for Closure
1260 // If you are not going to compile with closure then you can remove the
1261 // code below.
1262 var window = window || {};
1263 window['MarkerClusterer'] = MarkerClusterer;
1264 MarkerClusterer.prototype['addMarker'] = MarkerClusterer.prototype.addMarker;
1265 MarkerClusterer.prototype['addMarkers'] = MarkerClusterer.prototype.addMarkers;
1266 MarkerClusterer.prototype['clearMarkers'] =
1267 MarkerClusterer.prototype.clearMarkers;
1268 MarkerClusterer.prototype['fitMapToMarkers'] =
1269 MarkerClusterer.prototype.fitMapToMarkers;
1270 MarkerClusterer.prototype['getCalculator'] =
1271 MarkerClusterer.prototype.getCalculator;
1272 MarkerClusterer.prototype['getGridSize'] =
1273 MarkerClusterer.prototype.getGridSize;
1274 MarkerClusterer.prototype['getExtendedBounds'] =
1275 MarkerClusterer.prototype.getExtendedBounds;
1276 MarkerClusterer.prototype['getMap'] = MarkerClusterer.prototype.getMap;
1277 MarkerClusterer.prototype['getMarkers'] = MarkerClusterer.prototype.getMarkers;
1278 MarkerClusterer.prototype['getMaxZoom'] = MarkerClusterer.prototype.getMaxZoom;
1279 MarkerClusterer.prototype['getStyles'] = MarkerClusterer.prototype.getStyles;
1280 MarkerClusterer.prototype['getTotalClusters'] =
1281 MarkerClusterer.prototype.getTotalClusters;
1282 MarkerClusterer.prototype['getTotalMarkers'] =
1283 MarkerClusterer.prototype.getTotalMarkers;
1284 MarkerClusterer.prototype['redraw'] = MarkerClusterer.prototype.redraw;
1285 MarkerClusterer.prototype['removeMarker'] =
1286 MarkerClusterer.prototype.removeMarker;
1287 MarkerClusterer.prototype['removeMarkers'] =
1288 MarkerClusterer.prototype.removeMarkers;
1289 MarkerClusterer.prototype['resetViewport'] =
1290 MarkerClusterer.prototype.resetViewport;
1291 MarkerClusterer.prototype['repaint'] =
1292 MarkerClusterer.prototype.repaint;
1293 MarkerClusterer.prototype['setCalculator'] =
1294 MarkerClusterer.prototype.setCalculator;
1295 MarkerClusterer.prototype['setGridSize'] =
1296 MarkerClusterer.prototype.setGridSize;
1297 MarkerClusterer.prototype['setMaxZoom'] =
1298 MarkerClusterer.prototype.setMaxZoom;
1299 MarkerClusterer.prototype['onAdd'] = MarkerClusterer.prototype.onAdd;
1300 MarkerClusterer.prototype['draw'] = MarkerClusterer.prototype.draw;
1301
1302 Cluster.prototype['getCenter'] = Cluster.prototype.getCenter;
1303 Cluster.prototype['getSize'] = Cluster.prototype.getSize;
1304 Cluster.prototype['getMarkers'] = Cluster.prototype.getMarkers;
1305
1306 ClusterIcon.prototype['onAdd'] = ClusterIcon.prototype.onAdd;
1307 ClusterIcon.prototype['draw'] = ClusterIcon.prototype.draw;
1308 ClusterIcon.prototype['onRemove'] = ClusterIcon.prototype.onRemove;
1309
1310 Object.keys = Object.keys || function(o) {
1311 var result = [];
1312 for(var name in o) {
1313 if (o.hasOwnProperty(name))
1314 result.push(name);
1315 }
1316 return result;
1317 };
1318
1319 if (typeof module == 'object') {
1320 module.exports = MarkerClusterer;
1321 }