imported latest release of markercluster from google
[myslice.git] / third-party / markerclusterer / markerclusterer-2.0.16.js
1 /*jslint browser: true, confusion: true, sloppy: true, vars: true, nomen: false, plusplus: false, indent: 2 */
2 /*global window,google */
3
4 /**
5  * @name MarkerClustererPlus for Google Maps V3
6  * @version 2.0.16 [October 18, 2012]
7  * @author Gary Little
8  * @fileoverview
9  * The library creates and manages per-zoom-level clusters for large amounts of markers.
10  * <p>
11  * This is an enhanced V3 implementation of the
12  * <a href="http://gmaps-utility-library-dev.googlecode.com/svn/tags/markerclusterer/"
13  * >V2 MarkerClusterer</a> by Xiaoxi Wu. It is based on the
14  * <a href="http://google-maps-utility-library-v3.googlecode.com/svn/tags/markerclusterer/"
15  * >V3 MarkerClusterer</a> port by Luke Mahe. MarkerClustererPlus was created by Gary Little.
16  * <p>
17  * v2.0 release: MarkerClustererPlus v2.0 is backward compatible with MarkerClusterer v1.0. It
18  *  adds support for the <code>ignoreHidden</code>, <code>title</code>, <code>printable</code>,
19  *  <code>batchSizeIE</code>, and <code>calculator</code> properties as well as support for
20  *  four more events. It also allows greater control over the styling of the text that appears
21  *  on the cluster marker. The documentation has been significantly improved and the overall
22  *  code has been simplified and polished. Very large numbers of markers can now be managed
23  *  without causing Javascript timeout errors on Internet Explorer. Note that the name of the
24  *  <code>clusterclick</code> event has been deprecated. The new name is <code>click</code>,
25  *  so please change your application code now.
26  */
27
28 /**
29  * Licensed under the Apache License, Version 2.0 (the "License");
30  * you may not use this file except in compliance with the License.
31  * You may obtain a copy of the License at
32  *
33  *     http://www.apache.org/licenses/LICENSE-2.0
34  *
35  * Unless required by applicable law or agreed to in writing, software
36  * distributed under the License is distributed on an "AS IS" BASIS,
37  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38  * See the License for the specific language governing permissions and
39  * limitations under the License.
40  */
41
42
43 /**
44  * @name ClusterIconStyle
45  * @class This class represents the object for values in the <code>styles</code> array passed
46  *  to the {@link MarkerClusterer} constructor. The element in this array that is used to
47  *  style the cluster icon is determined by calling the <code>calculator</code> function.
48  *
49  * @property {string} url The URL of the cluster icon image file. Required.
50  * @property {number} height The height (in pixels) of the cluster icon. Required.
51  * @property {number} width The width (in pixels) of the cluster icon. Required.
52  * @property {Array} [anchor] The anchor position (in pixels) of the label text to be shown on
53  *  the cluster icon, relative to the top left corner of the icon.
54  *  The format is <code>[yoffset, xoffset]</code>. The <code>yoffset</code> must be positive
55  *  and less than <code>height</code> and the <code>xoffset</code> must be positive and less
56  *  than <code>width</code>. The default is to anchor the label text so that it is centered
57  *  on the icon.
58  * @property {Array} [anchorIcon] The anchor position (in pixels) of the cluster icon. This is the
59  *  spot on the cluster icon that is to be aligned with the cluster position. The format is
60  *  <code>[yoffset, xoffset]</code> where <code>yoffset</code> increases as you go down and
61  *  <code>xoffset</code> increases to the right. The default anchor position is the center of the
62  *  cluster icon.
63  * @property {string} [textColor="black"] The color of the label text shown on the
64  *  cluster icon.
65  * @property {number} [textSize=11] The size (in pixels) of the label text shown on the
66  *  cluster icon.
67  * @property {number} [textDecoration="none"] The value of the CSS <code>text-decoration</code>
68  *  property for the label text shown on the cluster icon.
69  * @property {number} [fontWeight="bold"] The value of the CSS <code>font-weight</code>
70  *  property for the label text shown on the cluster icon.
71  * @property {number} [fontStyle="normal"] The value of the CSS <code>font-style</code>
72  *  property for the label text shown on the cluster icon.
73  * @property {number} [fontFamily="Arial,sans-serif"] The value of the CSS <code>font-family</code>
74  *  property for the label text shown on the cluster icon.
75  * @property {string} [backgroundPosition="0 0"] The position of the cluster icon image
76  *  within the image defined by <code>url</code>. The format is <code>"xpos ypos"</code>
77  *  (the same format as for the CSS <code>background-position</code> property). You must set
78  *  this property appropriately when the image defined by <code>url</code> represents a sprite
79  *  containing multiple images.
80  */
81 /**
82  * @name ClusterIconInfo
83  * @class This class is an object containing general information about a cluster icon. This is
84  *  the object that a <code>calculator</code> function returns.
85  *
86  * @property {string} text The text of the label to be shown on the cluster icon.
87  * @property {number} index The index plus 1 of the element in the <code>styles</code>
88  *  array to be used to style the cluster icon.
89  * @property {string} title The tooltip to display when the mouse moves over the cluster icon.
90  *  If this value is <code>undefined</code> or <code>""</code>, <code>title</code> is set to the
91  *  value of the <code>title</code> property passed to the MarkerClusterer.
92  */
93 /**
94  * A cluster icon.
95  *
96  * @constructor
97  * @extends google.maps.OverlayView
98  * @param {Cluster} cluster The cluster with which the icon is to be associated.
99  * @param {Array} [styles] An array of {@link ClusterIconStyle} defining the cluster icons
100  *  to use for various cluster sizes.
101  * @private
102  */
103 function ClusterIcon(cluster, styles) {
104   cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);
105
106   this.cluster_ = cluster;
107   this.className_ = cluster.getMarkerClusterer().getClusterClass();
108   this.styles_ = styles;
109   this.center_ = null;
110   this.div_ = null;
111   this.sums_ = null;
112   this.visible_ = false;
113
114   this.setMap(cluster.getMap()); // Note: this causes onAdd to be called
115 }
116
117
118 /**
119  * Adds the icon to the DOM.
120  */
121 ClusterIcon.prototype.onAdd = function () {
122   var cClusterIcon = this;
123   var cMouseDownInCluster;
124   var cDraggingMapByCluster;
125
126   this.div_ = document.createElement("div");
127   this.div_.className = this.className_;
128   if (this.visible_) {
129     this.show();
130   }
131
132   this.getPanes().overlayMouseTarget.appendChild(this.div_);
133
134   // Fix for Issue 157
135   this.boundsChangedListener_ = google.maps.event.addListener(this.getMap(), "bounds_changed", function () {
136     cDraggingMapByCluster = cMouseDownInCluster;
137   });
138
139   google.maps.event.addDomListener(this.div_, "mousedown", function () {
140     cMouseDownInCluster = true;
141     cDraggingMapByCluster = false;
142   });
143
144   google.maps.event.addDomListener(this.div_, "click", function (e) {
145     cMouseDownInCluster = false;
146     if (!cDraggingMapByCluster) {
147       var theBounds;
148       var mz;
149       var mc = cClusterIcon.cluster_.getMarkerClusterer();
150       /**
151        * This event is fired when a cluster marker is clicked.
152        * @name MarkerClusterer#click
153        * @param {Cluster} c The cluster that was clicked.
154        * @event
155        */
156       google.maps.event.trigger(mc, "click", cClusterIcon.cluster_);
157       google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name
158
159       // The default click handler follows. Disable it by setting
160       // the zoomOnClick property to false.
161       if (mc.getZoomOnClick()) {
162         // Zoom into the cluster.
163         mz = mc.getMaxZoom();
164         theBounds = cClusterIcon.cluster_.getBounds();
165         mc.getMap().fitBounds(theBounds);
166         // There is a fix for Issue 170 here:
167         setTimeout(function () {
168           mc.getMap().fitBounds(theBounds);
169           // Don't zoom beyond the max zoom level
170           if (mz !== null && (mc.getMap().getZoom() > mz)) {
171             mc.getMap().setZoom(mz + 1);
172           }
173         }, 100);
174       }
175
176       // Prevent event propagation to the map:
177       e.cancelBubble = true;
178       if (e.stopPropagation) {
179         e.stopPropagation();
180       }
181     }
182   });
183
184   google.maps.event.addDomListener(this.div_, "mouseover", function () {
185     var mc = cClusterIcon.cluster_.getMarkerClusterer();
186     /**
187      * This event is fired when the mouse moves over a cluster marker.
188      * @name MarkerClusterer#mouseover
189      * @param {Cluster} c The cluster that the mouse moved over.
190      * @event
191      */
192     google.maps.event.trigger(mc, "mouseover", cClusterIcon.cluster_);
193   });
194
195   google.maps.event.addDomListener(this.div_, "mouseout", function () {
196     var mc = cClusterIcon.cluster_.getMarkerClusterer();
197     /**
198      * This event is fired when the mouse moves out of a cluster marker.
199      * @name MarkerClusterer#mouseout
200      * @param {Cluster} c The cluster that the mouse moved out of.
201      * @event
202      */
203     google.maps.event.trigger(mc, "mouseout", cClusterIcon.cluster_);
204   });
205 };
206
207
208 /**
209  * Removes the icon from the DOM.
210  */
211 ClusterIcon.prototype.onRemove = function () {
212   if (this.div_ && this.div_.parentNode) {
213     this.hide();
214     google.maps.event.removeListener(this.boundsChangedListener_);
215     google.maps.event.clearInstanceListeners(this.div_);
216     this.div_.parentNode.removeChild(this.div_);
217     this.div_ = null;
218   }
219 };
220
221
222 /**
223  * Draws the icon.
224  */
225 ClusterIcon.prototype.draw = function () {
226   if (this.visible_) {
227     var pos = this.getPosFromLatLng_(this.center_);
228     this.div_.style.top = pos.y + "px";
229     this.div_.style.left = pos.x + "px";
230   }
231 };
232
233
234 /**
235  * Hides the icon.
236  */
237 ClusterIcon.prototype.hide = function () {
238   if (this.div_) {
239     this.div_.style.display = "none";
240   }
241   this.visible_ = false;
242 };
243
244
245 /**
246  * Positions and shows the icon.
247  */
248 ClusterIcon.prototype.show = function () {
249   if (this.div_) {
250     var pos = this.getPosFromLatLng_(this.center_);
251     this.div_.style.cssText = this.createCss(pos);
252     if (this.cluster_.printable_) {
253       // (Would like to use "width: inherit;" below, but doesn't work with MSIE)
254       this.div_.innerHTML = "<img src='" + this.url_ + "'><div style='position: absolute; top: 0px; left: 0px; width: " + this.width_ + "px;'>" + this.sums_.text + "</div>";
255     } else {
256       this.div_.innerHTML = this.sums_.text;
257     }
258     if (typeof this.sums_.title === "undefined" || this.sums_.title === "") {
259       this.div_.title = this.cluster_.getMarkerClusterer().getTitle();
260     } else {
261       this.div_.title = this.sums_.title;
262     }
263     this.div_.style.display = "";
264   }
265   this.visible_ = true;
266 };
267
268
269 /**
270  * Sets the icon styles to the appropriate element in the styles array.
271  *
272  * @param {ClusterIconInfo} sums The icon label text and styles index.
273  */
274 ClusterIcon.prototype.useStyle = function (sums) {
275   this.sums_ = sums;
276   var index = Math.max(0, sums.index - 1);
277   index = Math.min(this.styles_.length - 1, index);
278   var style = this.styles_[index];
279   this.url_ = style.url;
280   this.height_ = style.height;
281   this.width_ = style.width;
282   this.anchor_ = style.anchor;
283   this.anchorIcon_ = style.anchorIcon || [parseInt(this.height_ / 2, 10), parseInt(this.width_ / 2, 10)];
284   this.textColor_ = style.textColor || "black";
285   this.textSize_ = style.textSize || 11;
286   this.textDecoration_ = style.textDecoration || "none";
287   this.fontWeight_ = style.fontWeight || "bold";
288   this.fontStyle_ = style.fontStyle || "normal";
289   this.fontFamily_ = style.fontFamily || "Arial,sans-serif";
290   this.backgroundPosition_ = style.backgroundPosition || "0 0";
291 };
292
293
294 /**
295  * Sets the position at which to center the icon.
296  *
297  * @param {google.maps.LatLng} center The latlng to set as the center.
298  */
299 ClusterIcon.prototype.setCenter = function (center) {
300   this.center_ = center;
301 };
302
303
304 /**
305  * Creates the cssText style parameter based on the position of the icon.
306  *
307  * @param {google.maps.Point} pos The position of the icon.
308  * @return {string} The CSS style text.
309  */
310 ClusterIcon.prototype.createCss = function (pos) {
311   var style = [];
312   if (!this.cluster_.printable_) {
313     style.push('background-image:url(' + this.url_ + ');');
314     style.push('background-position:' + this.backgroundPosition_ + ';');
315   }
316
317   if (typeof this.anchor_ === 'object') {
318     if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
319         this.anchor_[0] < this.height_) {
320       style.push('height:' + (this.height_ - this.anchor_[0]) +
321           'px; padding-top:' + this.anchor_[0] + 'px;');
322     } else {
323       style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
324           'px;');
325     }
326     if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
327         this.anchor_[1] < this.width_) {
328       style.push('width:' + (this.width_ - this.anchor_[1]) +
329           'px; padding-left:' + this.anchor_[1] + 'px;');
330     } else {
331       style.push('width:' + this.width_ + 'px; text-align:center;');
332     }
333   } else {
334     style.push('height:' + this.height_ + 'px; line-height:' +
335         this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
336   }
337
338   style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
339       pos.x + 'px; color:' + this.textColor_ + '; position:absolute; font-size:' +
340       this.textSize_ + 'px; font-family:' + this.fontFamily_ + '; font-weight:' +
341       this.fontWeight_ + '; font-style:' + this.fontStyle_ + '; text-decoration:' +
342       this.textDecoration_ + ';');
343
344   return style.join("");
345 };
346
347
348 /**
349  * Returns the position at which to place the DIV depending on the latlng.
350  *
351  * @param {google.maps.LatLng} latlng The position in latlng.
352  * @return {google.maps.Point} The position in pixels.
353  */
354 ClusterIcon.prototype.getPosFromLatLng_ = function (latlng) {
355   var pos = this.getProjection().fromLatLngToDivPixel(latlng);
356   pos.x -= this.anchorIcon_[1];
357   pos.y -= this.anchorIcon_[0];
358   return pos;
359 };
360
361
362 /**
363  * Creates a single cluster that manages a group of proximate markers.
364  *  Used internally, do not call this constructor directly.
365  * @constructor
366  * @param {MarkerClusterer} mc The <code>MarkerClusterer</code> object with which this
367  *  cluster is associated.
368  */
369 function Cluster(mc) {
370   this.markerClusterer_ = mc;
371   this.map_ = mc.getMap();
372   this.gridSize_ = mc.getGridSize();
373   this.minClusterSize_ = mc.getMinimumClusterSize();
374   this.averageCenter_ = mc.getAverageCenter();
375   this.printable_ = mc.getPrintable();
376   this.markers_ = [];
377   this.center_ = null;
378   this.bounds_ = null;
379   this.clusterIcon_ = new ClusterIcon(this, mc.getStyles());
380 }
381
382
383 /**
384  * Returns the number of markers managed by the cluster. You can call this from
385  * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
386  * for the <code>MarkerClusterer</code> object.
387  *
388  * @return {number} The number of markers in the cluster.
389  */
390 Cluster.prototype.getSize = function () {
391   return this.markers_.length;
392 };
393
394
395 /**
396  * Returns the array of markers managed by the cluster. You can call this from
397  * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
398  * for the <code>MarkerClusterer</code> object.
399  *
400  * @return {Array} The array of markers in the cluster.
401  */
402 Cluster.prototype.getMarkers = function () {
403   return this.markers_;
404 };
405
406
407 /**
408  * Returns the center of the cluster. You can call this from
409  * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
410  * for the <code>MarkerClusterer</code> object.
411  *
412  * @return {google.maps.LatLng} The center of the cluster.
413  */
414 Cluster.prototype.getCenter = function () {
415   return this.center_;
416 };
417
418
419 /**
420  * Returns the map with which the cluster is associated.
421  *
422  * @return {google.maps.Map} The map.
423  * @ignore
424  */
425 Cluster.prototype.getMap = function () {
426   return this.map_;
427 };
428
429
430 /**
431  * Returns the <code>MarkerClusterer</code> object with which the cluster is associated.
432  *
433  * @return {MarkerClusterer} The associated marker clusterer.
434  * @ignore
435  */
436 Cluster.prototype.getMarkerClusterer = function () {
437   return this.markerClusterer_;
438 };
439
440
441 /**
442  * Returns the bounds of the cluster.
443  *
444  * @return {google.maps.LatLngBounds} the cluster bounds.
445  * @ignore
446  */
447 Cluster.prototype.getBounds = function () {
448   var i;
449   var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
450   var markers = this.getMarkers();
451   for (i = 0; i < markers.length; i++) {
452     bounds.extend(markers[i].getPosition());
453   }
454   return bounds;
455 };
456
457
458 /**
459  * Removes the cluster from the map.
460  *
461  * @ignore
462  */
463 Cluster.prototype.remove = function () {
464   this.clusterIcon_.setMap(null);
465   this.markers_ = [];
466   delete this.markers_;
467 };
468
469
470 /**
471  * Adds a marker to the cluster.
472  *
473  * @param {google.maps.Marker} marker The marker to be added.
474  * @return {boolean} True if the marker was added.
475  * @ignore
476  */
477 Cluster.prototype.addMarker = function (marker) {
478   var i;
479   var mCount;
480   var mz;
481
482   if (this.isMarkerAlreadyAdded_(marker)) {
483     return false;
484   }
485
486   if (!this.center_) {
487     this.center_ = marker.getPosition();
488     this.calculateBounds_();
489   } else {
490     if (this.averageCenter_) {
491       var l = this.markers_.length + 1;
492       var lat = (this.center_.lat() * (l - 1) + marker.getPosition().lat()) / l;
493       var lng = (this.center_.lng() * (l - 1) + marker.getPosition().lng()) / l;
494       this.center_ = new google.maps.LatLng(lat, lng);
495       this.calculateBounds_();
496     }
497   }
498
499   marker.isAdded = true;
500   this.markers_.push(marker);
501
502   mCount = this.markers_.length;
503   mz = this.markerClusterer_.getMaxZoom();
504   if (mz !== null && this.map_.getZoom() > mz) {
505     // Zoomed in past max zoom, so show the marker.
506     if (marker.getMap() !== this.map_) {
507       marker.setMap(this.map_);
508     }
509   } else if (mCount < this.minClusterSize_) {
510     // Min cluster size not reached so show the marker.
511     if (marker.getMap() !== this.map_) {
512       marker.setMap(this.map_);
513     }
514   } else if (mCount === this.minClusterSize_) {
515     // Hide the markers that were showing.
516     for (i = 0; i < mCount; i++) {
517       this.markers_[i].setMap(null);
518     }
519   } else {
520     marker.setMap(null);
521   }
522
523   this.updateIcon_();
524   return true;
525 };
526
527
528 /**
529  * Determines if a marker lies within the cluster's bounds.
530  *
531  * @param {google.maps.Marker} marker The marker to check.
532  * @return {boolean} True if the marker lies in the bounds.
533  * @ignore
534  */
535 Cluster.prototype.isMarkerInClusterBounds = function (marker) {
536   return this.bounds_.contains(marker.getPosition());
537 };
538
539
540 /**
541  * Calculates the extended bounds of the cluster with the grid.
542  */
543 Cluster.prototype.calculateBounds_ = function () {
544   var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
545   this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
546 };
547
548
549 /**
550  * Updates the cluster icon.
551  */
552 Cluster.prototype.updateIcon_ = function () {
553   var mCount = this.markers_.length;
554   var mz = this.markerClusterer_.getMaxZoom();
555
556   if (mz !== null && this.map_.getZoom() > mz) {
557     this.clusterIcon_.hide();
558     return;
559   }
560
561   if (mCount < this.minClusterSize_) {
562     // Min cluster size not yet reached.
563     this.clusterIcon_.hide();
564     return;
565   }
566
567   var numStyles = this.markerClusterer_.getStyles().length;
568   var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
569   this.clusterIcon_.setCenter(this.center_);
570   this.clusterIcon_.useStyle(sums);
571   this.clusterIcon_.show();
572 };
573
574
575 /**
576  * Determines if a marker has already been added to the cluster.
577  *
578  * @param {google.maps.Marker} marker The marker to check.
579  * @return {boolean} True if the marker has already been added.
580  */
581 Cluster.prototype.isMarkerAlreadyAdded_ = function (marker) {
582   var i;
583   if (this.markers_.indexOf) {
584     return this.markers_.indexOf(marker) !== -1;
585   } else {
586     for (i = 0; i < this.markers_.length; i++) {
587       if (marker === this.markers_[i]) {
588         return true;
589       }
590     }
591   }
592   return false;
593 };
594
595
596 /**
597  * @name MarkerClustererOptions
598  * @class This class represents the optional parameter passed to
599  *  the {@link MarkerClusterer} constructor.
600  * @property {number} [gridSize=60] The grid size of a cluster in pixels. The grid is a square.
601  * @property {number} [maxZoom=null] The maximum zoom level at which clustering is enabled or
602  *  <code>null</code> if clustering is to be enabled at all zoom levels.
603  * @property {boolean} [zoomOnClick=true] Whether to zoom the map when a cluster marker is
604  *  clicked. You may want to set this to <code>false</code> if you have installed a handler
605  *  for the <code>click</code> event and it deals with zooming on its own.
606  * @property {boolean} [averageCenter=false] Whether the position of a cluster marker should be
607  *  the average position of all markers in the cluster. If set to <code>false</code>, the
608  *  cluster marker is positioned at the location of the first marker added to the cluster.
609  * @property {number} [minimumClusterSize=2] The minimum number of markers needed in a cluster
610  *  before the markers are hidden and a cluster marker appears.
611  * @property {boolean} [ignoreHidden=false] Whether to ignore hidden markers in clusters. You
612  *  may want to set this to <code>true</code> to ensure that hidden markers are not included
613  *  in the marker count that appears on a cluster marker (this count is the value of the
614  *  <code>text</code> property of the result returned by the default <code>calculator</code>).
615  *  If set to <code>true</code> and you change the visibility of a marker being clustered, be
616  *  sure to also call <code>MarkerClusterer.repaint()</code>.
617  * @property {boolean} [printable=false] Whether to make the cluster icons printable. Do not
618  *  set to <code>true</code> if the <code>url</code> fields in the <code>styles</code> array
619  *  refer to image sprite files.
620  * @property {string} [title=""] The tooltip to display when the mouse moves over a cluster
621  *  marker. (Alternatively, you can use a custom <code>calculator</code> function to specify a
622  *  different tooltip for each cluster marker.)
623  * @property {function} [calculator=MarkerClusterer.CALCULATOR] The function used to determine
624  *  the text to be displayed on a cluster marker and the index indicating which style to use
625  *  for the cluster marker. The input parameters for the function are (1) the array of markers
626  *  represented by a cluster marker and (2) the number of cluster icon styles. It returns a
627  *  {@link ClusterIconInfo} object. The default <code>calculator</code> returns a
628  *  <code>text</code> property which is the number of markers in the cluster and an
629  *  <code>index</code> property which is one higher than the lowest integer such that
630  *  <code>10^i</code> exceeds the number of markers in the cluster, or the size of the styles
631  *  array, whichever is less. The <code>styles</code> array element used has an index of
632  *  <code>index</code> minus 1. For example, the default <code>calculator</code> returns a
633  *  <code>text</code> value of <code>"125"</code> and an <code>index</code> of <code>3</code>
634  *  for a cluster icon representing 125 markers so the element used in the <code>styles</code>
635  *  array is <code>2</code>. A <code>calculator</code> may also return a <code>title</code>
636  *  property that contains the text of the tooltip to be used for the cluster marker. If
637  *   <code>title</code> is not defined, the tooltip is set to the value of the <code>title</code>
638  *   property for the MarkerClusterer.
639  * @property {string} [clusterClass="cluster"] The name of the CSS class defining general styles
640  *  for the cluster markers. Use this class to define CSS styles that are not set up by the code
641  *  that processes the <code>styles</code> array.
642  * @property {Array} [styles] An array of {@link ClusterIconStyle} elements defining the styles
643  *  of the cluster markers to be used. The element to be used to style a given cluster marker
644  *  is determined by the function defined by the <code>calculator</code> property.
645  *  The default is an array of {@link ClusterIconStyle} elements whose properties are derived
646  *  from the values for <code>imagePath</code>, <code>imageExtension</code>, and
647  *  <code>imageSizes</code>.
648  * @property {number} [batchSize=MarkerClusterer.BATCH_SIZE] Set this property to the
649  *  number of markers to be processed in a single batch when using a browser other than
650  *  Internet Explorer (for Internet Explorer, use the batchSizeIE property instead).
651  * @property {number} [batchSizeIE=MarkerClusterer.BATCH_SIZE_IE] When Internet Explorer is
652  *  being used, markers are processed in several batches with a small delay inserted between
653  *  each batch in an attempt to avoid Javascript timeout errors. Set this property to the
654  *  number of markers to be processed in a single batch; select as high a number as you can
655  *  without causing a timeout error in the browser. This number might need to be as low as 100
656  *  if 15,000 markers are being managed, for example.
657  * @property {string} [imagePath=MarkerClusterer.IMAGE_PATH]
658  *  The full URL of the root name of the group of image files to use for cluster icons.
659  *  The complete file name is of the form <code>imagePath</code>n.<code>imageExtension</code>
660  *  where n is the image file number (1, 2, etc.).
661  * @property {string} [imageExtension=MarkerClusterer.IMAGE_EXTENSION]
662  *  The extension name for the cluster icon image files (e.g., <code>"png"</code> or
663  *  <code>"jpg"</code>).
664  * @property {Array} [imageSizes=MarkerClusterer.IMAGE_SIZES]
665  *  An array of numbers containing the widths of the group of
666  *  <code>imagePath</code>n.<code>imageExtension</code> image files.
667  *  (The images are assumed to be square.)
668  */
669 /**
670  * Creates a MarkerClusterer object with the options specified in {@link MarkerClustererOptions}.
671  * @constructor
672  * @extends google.maps.OverlayView
673  * @param {google.maps.Map} map The Google map to attach to.
674  * @param {Array.<google.maps.Marker>} [opt_markers] The markers to be added to the cluster.
675  * @param {MarkerClustererOptions} [opt_options] The optional parameters.
676  */
677 function MarkerClusterer(map, opt_markers, opt_options) {
678   // MarkerClusterer implements google.maps.OverlayView interface. We use the
679   // extend function to extend MarkerClusterer with google.maps.OverlayView
680   // because it might not always be available when the code is defined so we
681   // look for it at the last possible moment. If it doesn't exist now then
682   // there is no point going ahead :)
683   this.extend(MarkerClusterer, google.maps.OverlayView);
684
685   opt_markers = opt_markers || [];
686   opt_options = opt_options || {};
687
688   this.markers_ = [];
689   this.clusters_ = [];
690   this.listeners_ = [];
691   this.activeMap_ = null;
692   this.ready_ = false;
693
694   this.gridSize_ = opt_options.gridSize || 60;
695   this.minClusterSize_ = opt_options.minimumClusterSize || 2;
696   this.maxZoom_ = opt_options.maxZoom || null;
697   this.styles_ = opt_options.styles || [];
698   this.title_ = opt_options.title || "";
699   this.zoomOnClick_ = true;
700   if (opt_options.zoomOnClick !== undefined) {
701     this.zoomOnClick_ = opt_options.zoomOnClick;
702   }
703   this.averageCenter_ = false;
704   if (opt_options.averageCenter !== undefined) {
705     this.averageCenter_ = opt_options.averageCenter;
706   }
707   this.ignoreHidden_ = false;
708   if (opt_options.ignoreHidden !== undefined) {
709     this.ignoreHidden_ = opt_options.ignoreHidden;
710   }
711   this.printable_ = false;
712   if (opt_options.printable !== undefined) {
713     this.printable_ = opt_options.printable;
714   }
715   this.imagePath_ = opt_options.imagePath || MarkerClusterer.IMAGE_PATH;
716   this.imageExtension_ = opt_options.imageExtension || MarkerClusterer.IMAGE_EXTENSION;
717   this.imageSizes_ = opt_options.imageSizes || MarkerClusterer.IMAGE_SIZES;
718   this.calculator_ = opt_options.calculator || MarkerClusterer.CALCULATOR;
719   this.batchSize_ = opt_options.batchSize || MarkerClusterer.BATCH_SIZE;
720   this.batchSizeIE_ = opt_options.batchSizeIE || MarkerClusterer.BATCH_SIZE_IE;
721   this.clusterClass_ = opt_options.clusterClass || "cluster";
722
723   if (navigator.userAgent.toLowerCase().indexOf("msie") !== -1) {
724     // Try to avoid IE timeout when processing a huge number of markers:
725     this.batchSize_ = this.batchSizeIE_;
726   }
727
728   this.setupStyles_();
729
730   this.addMarkers(opt_markers, true);
731   this.setMap(map); // Note: this causes onAdd to be called
732 }
733
734
735 /**
736  * Implementation of the onAdd interface method.
737  * @ignore
738  */
739 MarkerClusterer.prototype.onAdd = function () {
740   var cMarkerClusterer = this;
741
742   this.activeMap_ = this.getMap();
743   this.ready_ = true;
744
745   this.repaint();
746
747   // Add the map event listeners
748   this.listeners_ = [
749     google.maps.event.addListener(this.getMap(), "zoom_changed", function () {
750       cMarkerClusterer.resetViewport_(false);
751       // Workaround for this Google bug: when map is at level 0 and "-" of
752       // zoom slider is clicked, a "zoom_changed" event is fired even though
753       // the map doesn't zoom out any further. In this situation, no "idle"
754       // event is triggered so the cluster markers that have been removed
755       // do not get redrawn. Same goes for a zoom in at maxZoom.
756       if (this.getZoom() === (this.get("minZoom") || 0) || this.getZoom() === this.get("maxZoom")) {
757         google.maps.event.trigger(this, "idle");
758       }
759     }),
760     google.maps.event.addListener(this.getMap(), "idle", function () {
761       cMarkerClusterer.redraw_();
762     })
763   ];
764 };
765
766
767 /**
768  * Implementation of the onRemove interface method.
769  * Removes map event listeners and all cluster icons from the DOM.
770  * All managed markers are also put back on the map.
771  * @ignore
772  */
773 MarkerClusterer.prototype.onRemove = function () {
774   var i;
775
776   // Put all the managed markers back on the map:
777   for (i = 0; i < this.markers_.length; i++) {
778     if (this.markers_[i].getMap() !== this.activeMap_) {
779       this.markers_[i].setMap(this.activeMap_);
780     }
781   }
782
783   // Remove all clusters:
784   for (i = 0; i < this.clusters_.length; i++) {
785     this.clusters_[i].remove();
786   }
787   this.clusters_ = [];
788
789   // Remove map event listeners:
790   for (i = 0; i < this.listeners_.length; i++) {
791     google.maps.event.removeListener(this.listeners_[i]);
792   }
793   this.listeners_ = [];
794
795   this.activeMap_ = null;
796   this.ready_ = false;
797 };
798
799
800 /**
801  * Implementation of the draw interface method.
802  * @ignore
803  */
804 MarkerClusterer.prototype.draw = function () {};
805
806
807 /**
808  * Sets up the styles object.
809  */
810 MarkerClusterer.prototype.setupStyles_ = function () {
811   var i, size;
812   if (this.styles_.length > 0) {
813     return;
814   }
815
816   for (i = 0; i < this.imageSizes_.length; i++) {
817     size = this.imageSizes_[i];
818     this.styles_.push({
819       url: this.imagePath_ + (i + 1) + "." + this.imageExtension_,
820       height: size,
821       width: size
822     });
823   }
824 };
825
826
827 /**
828  *  Fits the map to the bounds of the markers managed by the clusterer.
829  */
830 MarkerClusterer.prototype.fitMapToMarkers = function () {
831   var i;
832   var markers = this.getMarkers();
833   var bounds = new google.maps.LatLngBounds();
834   for (i = 0; i < markers.length; i++) {
835     bounds.extend(markers[i].getPosition());
836   }
837
838   this.getMap().fitBounds(bounds);
839 };
840
841
842 /**
843  * Returns the value of the <code>gridSize</code> property.
844  *
845  * @return {number} The grid size.
846  */
847 MarkerClusterer.prototype.getGridSize = function () {
848   return this.gridSize_;
849 };
850
851
852 /**
853  * Sets the value of the <code>gridSize</code> property.
854  *
855  * @param {number} gridSize The grid size.
856  */
857 MarkerClusterer.prototype.setGridSize = function (gridSize) {
858   this.gridSize_ = gridSize;
859 };
860
861
862 /**
863  * Returns the value of the <code>minimumClusterSize</code> property.
864  *
865  * @return {number} The minimum cluster size.
866  */
867 MarkerClusterer.prototype.getMinimumClusterSize = function () {
868   return this.minClusterSize_;
869 };
870
871 /**
872  * Sets the value of the <code>minimumClusterSize</code> property.
873  *
874  * @param {number} minimumClusterSize The minimum cluster size.
875  */
876 MarkerClusterer.prototype.setMinimumClusterSize = function (minimumClusterSize) {
877   this.minClusterSize_ = minimumClusterSize;
878 };
879
880
881 /**
882  *  Returns the value of the <code>maxZoom</code> property.
883  *
884  *  @return {number} The maximum zoom level.
885  */
886 MarkerClusterer.prototype.getMaxZoom = function () {
887   return this.maxZoom_;
888 };
889
890
891 /**
892  *  Sets the value of the <code>maxZoom</code> property.
893  *
894  *  @param {number} maxZoom The maximum zoom level.
895  */
896 MarkerClusterer.prototype.setMaxZoom = function (maxZoom) {
897   this.maxZoom_ = maxZoom;
898 };
899
900
901 /**
902  *  Returns the value of the <code>styles</code> property.
903  *
904  *  @return {Array} The array of styles defining the cluster markers to be used.
905  */
906 MarkerClusterer.prototype.getStyles = function () {
907   return this.styles_;
908 };
909
910
911 /**
912  *  Sets the value of the <code>styles</code> property.
913  *
914  *  @param {Array.<ClusterIconStyle>} styles The array of styles to use.
915  */
916 MarkerClusterer.prototype.setStyles = function (styles) {
917   this.styles_ = styles;
918 };
919
920
921 /**
922  * Returns the value of the <code>title</code> property.
923  *
924  * @return {string} The content of the title text.
925  */
926 MarkerClusterer.prototype.getTitle = function () {
927   return this.title_;
928 };
929
930
931 /**
932  *  Sets the value of the <code>title</code> property.
933  *
934  *  @param {string} title The value of the title property.
935  */
936 MarkerClusterer.prototype.setTitle = function (title) {
937   this.title_ = title;
938 };
939
940
941 /**
942  * Returns the value of the <code>zoomOnClick</code> property.
943  *
944  * @return {boolean} True if zoomOnClick property is set.
945  */
946 MarkerClusterer.prototype.getZoomOnClick = function () {
947   return this.zoomOnClick_;
948 };
949
950
951 /**
952  *  Sets the value of the <code>zoomOnClick</code> property.
953  *
954  *  @param {boolean} zoomOnClick The value of the zoomOnClick property.
955  */
956 MarkerClusterer.prototype.setZoomOnClick = function (zoomOnClick) {
957   this.zoomOnClick_ = zoomOnClick;
958 };
959
960
961 /**
962  * Returns the value of the <code>averageCenter</code> property.
963  *
964  * @return {boolean} True if averageCenter property is set.
965  */
966 MarkerClusterer.prototype.getAverageCenter = function () {
967   return this.averageCenter_;
968 };
969
970
971 /**
972  *  Sets the value of the <code>averageCenter</code> property.
973  *
974  *  @param {boolean} averageCenter The value of the averageCenter property.
975  */
976 MarkerClusterer.prototype.setAverageCenter = function (averageCenter) {
977   this.averageCenter_ = averageCenter;
978 };
979
980
981 /**
982  * Returns the value of the <code>ignoreHidden</code> property.
983  *
984  * @return {boolean} True if ignoreHidden property is set.
985  */
986 MarkerClusterer.prototype.getIgnoreHidden = function () {
987   return this.ignoreHidden_;
988 };
989
990
991 /**
992  *  Sets the value of the <code>ignoreHidden</code> property.
993  *
994  *  @param {boolean} ignoreHidden The value of the ignoreHidden property.
995  */
996 MarkerClusterer.prototype.setIgnoreHidden = function (ignoreHidden) {
997   this.ignoreHidden_ = ignoreHidden;
998 };
999
1000
1001 /**
1002  * Returns the value of the <code>imageExtension</code> property.
1003  *
1004  * @return {string} The value of the imageExtension property.
1005  */
1006 MarkerClusterer.prototype.getImageExtension = function () {
1007   return this.imageExtension_;
1008 };
1009
1010
1011 /**
1012  *  Sets the value of the <code>imageExtension</code> property.
1013  *
1014  *  @param {string} imageExtension The value of the imageExtension property.
1015  */
1016 MarkerClusterer.prototype.setImageExtension = function (imageExtension) {
1017   this.imageExtension_ = imageExtension;
1018 };
1019
1020
1021 /**
1022  * Returns the value of the <code>imagePath</code> property.
1023  *
1024  * @return {string} The value of the imagePath property.
1025  */
1026 MarkerClusterer.prototype.getImagePath = function () {
1027   return this.imagePath_;
1028 };
1029
1030
1031 /**
1032  *  Sets the value of the <code>imagePath</code> property.
1033  *
1034  *  @param {string} imagePath The value of the imagePath property.
1035  */
1036 MarkerClusterer.prototype.setImagePath = function (imagePath) {
1037   this.imagePath_ = imagePath;
1038 };
1039
1040
1041 /**
1042  * Returns the value of the <code>imageSizes</code> property.
1043  *
1044  * @return {Array} The value of the imageSizes property.
1045  */
1046 MarkerClusterer.prototype.getImageSizes = function () {
1047   return this.imageSizes_;
1048 };
1049
1050
1051 /**
1052  *  Sets the value of the <code>imageSizes</code> property.
1053  *
1054  *  @param {Array} imageSizes The value of the imageSizes property.
1055  */
1056 MarkerClusterer.prototype.setImageSizes = function (imageSizes) {
1057   this.imageSizes_ = imageSizes;
1058 };
1059
1060
1061 /**
1062  * Returns the value of the <code>calculator</code> property.
1063  *
1064  * @return {function} the value of the calculator property.
1065  */
1066 MarkerClusterer.prototype.getCalculator = function () {
1067   return this.calculator_;
1068 };
1069
1070
1071 /**
1072  * Sets the value of the <code>calculator</code> property.
1073  *
1074  * @param {function(Array.<google.maps.Marker>, number)} calculator The value
1075  *  of the calculator property.
1076  */
1077 MarkerClusterer.prototype.setCalculator = function (calculator) {
1078   this.calculator_ = calculator;
1079 };
1080
1081
1082 /**
1083  * Returns the value of the <code>printable</code> property.
1084  *
1085  * @return {boolean} the value of the printable property.
1086  */
1087 MarkerClusterer.prototype.getPrintable = function () {
1088   return this.printable_;
1089 };
1090
1091
1092 /**
1093  * Sets the value of the <code>printable</code> property.
1094  *
1095  *  @param {boolean} printable The value of the printable property.
1096  */
1097 MarkerClusterer.prototype.setPrintable = function (printable) {
1098   this.printable_ = printable;
1099 };
1100
1101
1102 /**
1103  * Returns the value of the <code>batchSizeIE</code> property.
1104  *
1105  * @return {number} the value of the batchSizeIE property.
1106  */
1107 MarkerClusterer.prototype.getBatchSizeIE = function () {
1108   return this.batchSizeIE_;
1109 };
1110
1111
1112 /**
1113  * Sets the value of the <code>batchSizeIE</code> property.
1114  *
1115  *  @param {number} batchSizeIE The value of the batchSizeIE property.
1116  */
1117 MarkerClusterer.prototype.setBatchSizeIE = function (batchSizeIE) {
1118   this.batchSizeIE_ = batchSizeIE;
1119 };
1120
1121
1122 /**
1123  * Returns the value of the <code>clusterClass</code> property.
1124  *
1125  * @return {string} the value of the clusterClass property.
1126  */
1127 MarkerClusterer.prototype.getClusterClass = function () {
1128   return this.clusterClass_;
1129 };
1130
1131
1132 /**
1133  * Sets the value of the <code>clusterClass</code> property.
1134  *
1135  *  @param {string} clusterClass The value of the clusterClass property.
1136  */
1137 MarkerClusterer.prototype.setClusterClass = function (clusterClass) {
1138   this.clusterClass_ = clusterClass;
1139 };
1140
1141
1142 /**
1143  *  Returns the array of markers managed by the clusterer.
1144  *
1145  *  @return {Array} The array of markers managed by the clusterer.
1146  */
1147 MarkerClusterer.prototype.getMarkers = function () {
1148   return this.markers_;
1149 };
1150
1151
1152 /**
1153  *  Returns the number of markers managed by the clusterer.
1154  *
1155  *  @return {number} The number of markers.
1156  */
1157 MarkerClusterer.prototype.getTotalMarkers = function () {
1158   return this.markers_.length;
1159 };
1160
1161
1162 /**
1163  * Returns the current array of clusters formed by the clusterer.
1164  *
1165  * @return {Array} The array of clusters formed by the clusterer.
1166  */
1167 MarkerClusterer.prototype.getClusters = function () {
1168   return this.clusters_;
1169 };
1170
1171
1172 /**
1173  * Returns the number of clusters formed by the clusterer.
1174  *
1175  * @return {number} The number of clusters formed by the clusterer.
1176  */
1177 MarkerClusterer.prototype.getTotalClusters = function () {
1178   return this.clusters_.length;
1179 };
1180
1181
1182 /**
1183  * Adds a marker to the clusterer. The clusters are redrawn unless
1184  *  <code>opt_nodraw</code> is set to <code>true</code>.
1185  *
1186  * @param {google.maps.Marker} marker The marker to add.
1187  * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
1188  */
1189 MarkerClusterer.prototype.addMarker = function (marker, opt_nodraw) {
1190   this.pushMarkerTo_(marker);
1191   if (!opt_nodraw) {
1192     this.redraw_();
1193   }
1194 };
1195
1196
1197 /**
1198  * Adds an array of markers to the clusterer. The clusters are redrawn unless
1199  *  <code>opt_nodraw</code> is set to <code>true</code>.
1200  *
1201  * @param {Array.<google.maps.Marker>} markers The markers to add.
1202  * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
1203  */
1204 MarkerClusterer.prototype.addMarkers = function (markers, opt_nodraw) {
1205   var i;
1206   for (i = 0; i < markers.length; i++) {
1207     this.pushMarkerTo_(markers[i]);
1208   }
1209   if (!opt_nodraw) {
1210     this.redraw_();
1211   }
1212 };
1213
1214
1215 /**
1216  * Pushes a marker to the clusterer.
1217  *
1218  * @param {google.maps.Marker} marker The marker to add.
1219  */
1220 MarkerClusterer.prototype.pushMarkerTo_ = function (marker) {
1221   // If the marker is draggable add a listener so we can update the clusters on the dragend:
1222   if (marker.getDraggable()) {
1223     var cMarkerClusterer = this;
1224     google.maps.event.addListener(marker, "dragend", function () {
1225       if (cMarkerClusterer.ready_) {
1226         this.isAdded = false;
1227         cMarkerClusterer.repaint();
1228       }
1229     });
1230   }
1231   marker.isAdded = false;
1232   this.markers_.push(marker);
1233 };
1234
1235
1236 /**
1237  * Removes a marker from the cluster.  The clusters are redrawn unless
1238  *  <code>opt_nodraw</code> is set to <code>true</code>. Returns <code>true</code> if the
1239  *  marker was removed from the clusterer.
1240  *
1241  * @param {google.maps.Marker} marker The marker to remove.
1242  * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
1243  * @return {boolean} True if the marker was removed from the clusterer.
1244  */
1245 MarkerClusterer.prototype.removeMarker = function (marker, opt_nodraw) {
1246   var removed = this.removeMarker_(marker);
1247
1248   if (!opt_nodraw && removed) {
1249     this.repaint();
1250   }
1251
1252   return removed;
1253 };
1254
1255
1256 /**
1257  * Removes an array of markers from the cluster. The clusters are redrawn unless
1258  *  <code>opt_nodraw</code> is set to <code>true</code>. Returns <code>true</code> if markers
1259  *  were removed from the clusterer.
1260  *
1261  * @param {Array.<google.maps.Marker>} markers The markers to remove.
1262  * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
1263  * @return {boolean} True if markers were removed from the clusterer.
1264  */
1265 MarkerClusterer.prototype.removeMarkers = function (markers, opt_nodraw) {
1266   var i, r;
1267   var removed = false;
1268
1269   for (i = 0; i < markers.length; i++) {
1270     r = this.removeMarker_(markers[i]);
1271     removed = removed || r;
1272   }
1273
1274   if (!opt_nodraw && removed) {
1275     this.repaint();
1276   }
1277
1278   return removed;
1279 };
1280
1281
1282 /**
1283  * Removes a marker and returns true if removed, false if not.
1284  *
1285  * @param {google.maps.Marker} marker The marker to remove
1286  * @return {boolean} Whether the marker was removed or not
1287  */
1288 MarkerClusterer.prototype.removeMarker_ = function (marker) {
1289   var i;
1290   var index = -1;
1291   if (this.markers_.indexOf) {
1292     index = this.markers_.indexOf(marker);
1293   } else {
1294     for (i = 0; i < this.markers_.length; i++) {
1295       if (marker === this.markers_[i]) {
1296         index = i;
1297         break;
1298       }
1299     }
1300   }
1301
1302   if (index === -1) {
1303     // Marker is not in our list of markers, so do nothing:
1304     return false;
1305   }
1306
1307   marker.setMap(null);
1308   this.markers_.splice(index, 1); // Remove the marker from the list of managed markers
1309   return true;
1310 };
1311
1312
1313 /**
1314  * Removes all clusters and markers from the map and also removes all markers
1315  *  managed by the clusterer.
1316  */
1317 MarkerClusterer.prototype.clearMarkers = function () {
1318   this.resetViewport_(true);
1319   this.markers_ = [];
1320 };
1321
1322
1323 /**
1324  * Recalculates and redraws all the marker clusters from scratch.
1325  *  Call this after changing any properties.
1326  */
1327 MarkerClusterer.prototype.repaint = function () {
1328   var oldClusters = this.clusters_.slice();
1329   this.clusters_ = [];
1330   this.resetViewport_(false);
1331   this.redraw_();
1332
1333   // Remove the old clusters.
1334   // Do it in a timeout to prevent blinking effect.
1335   setTimeout(function () {
1336     var i;
1337     for (i = 0; i < oldClusters.length; i++) {
1338       oldClusters[i].remove();
1339     }
1340   }, 0);
1341 };
1342
1343
1344 /**
1345  * Returns the current bounds extended by the grid size.
1346  *
1347  * @param {google.maps.LatLngBounds} bounds The bounds to extend.
1348  * @return {google.maps.LatLngBounds} The extended bounds.
1349  * @ignore
1350  */
1351 MarkerClusterer.prototype.getExtendedBounds = function (bounds) {
1352   var projection = this.getProjection();
1353
1354   // Turn the bounds into latlng.
1355   var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
1356       bounds.getNorthEast().lng());
1357   var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
1358       bounds.getSouthWest().lng());
1359
1360   // Convert the points to pixels and the extend out by the grid size.
1361   var trPix = projection.fromLatLngToDivPixel(tr);
1362   trPix.x += this.gridSize_;
1363   trPix.y -= this.gridSize_;
1364
1365   var blPix = projection.fromLatLngToDivPixel(bl);
1366   blPix.x -= this.gridSize_;
1367   blPix.y += this.gridSize_;
1368
1369   // Convert the pixel points back to LatLng
1370   var ne = projection.fromDivPixelToLatLng(trPix);
1371   var sw = projection.fromDivPixelToLatLng(blPix);
1372
1373   // Extend the bounds to contain the new bounds.
1374   bounds.extend(ne);
1375   bounds.extend(sw);
1376
1377   return bounds;
1378 };
1379
1380
1381 /**
1382  * Redraws all the clusters.
1383  */
1384 MarkerClusterer.prototype.redraw_ = function () {
1385   this.createClusters_(0);
1386 };
1387
1388
1389 /**
1390  * Removes all clusters from the map. The markers are also removed from the map
1391  *  if <code>opt_hide</code> is set to <code>true</code>.
1392  *
1393  * @param {boolean} [opt_hide] Set to <code>true</code> to also remove the markers
1394  *  from the map.
1395  */
1396 MarkerClusterer.prototype.resetViewport_ = function (opt_hide) {
1397   var i, marker;
1398   // Remove all the clusters
1399   for (i = 0; i < this.clusters_.length; i++) {
1400     this.clusters_[i].remove();
1401   }
1402   this.clusters_ = [];
1403
1404   // Reset the markers to not be added and to be removed from the map.
1405   for (i = 0; i < this.markers_.length; i++) {
1406     marker = this.markers_[i];
1407     marker.isAdded = false;
1408     if (opt_hide) {
1409       marker.setMap(null);
1410     }
1411   }
1412 };
1413
1414
1415 /**
1416  * Calculates the distance between two latlng locations in km.
1417  *
1418  * @param {google.maps.LatLng} p1 The first lat lng point.
1419  * @param {google.maps.LatLng} p2 The second lat lng point.
1420  * @return {number} The distance between the two points in km.
1421  * @see http://www.movable-type.co.uk/scripts/latlong.html
1422 */
1423 MarkerClusterer.prototype.distanceBetweenPoints_ = function (p1, p2) {
1424   var R = 6371; // Radius of the Earth in km
1425   var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1426   var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1427   var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1428     Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1429     Math.sin(dLon / 2) * Math.sin(dLon / 2);
1430   var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1431   var d = R * c;
1432   return d;
1433 };
1434
1435
1436 /**
1437  * Determines if a marker is contained in a bounds.
1438  *
1439  * @param {google.maps.Marker} marker The marker to check.
1440  * @param {google.maps.LatLngBounds} bounds The bounds to check against.
1441  * @return {boolean} True if the marker is in the bounds.
1442  */
1443 MarkerClusterer.prototype.isMarkerInBounds_ = function (marker, bounds) {
1444   return bounds.contains(marker.getPosition());
1445 };
1446
1447
1448 /**
1449  * Adds a marker to a cluster, or creates a new cluster.
1450  *
1451  * @param {google.maps.Marker} marker The marker to add.
1452  */
1453 MarkerClusterer.prototype.addToClosestCluster_ = function (marker) {
1454   var i, d, cluster, center;
1455   var distance = 40000; // Some large number
1456   var clusterToAddTo = null;
1457   for (i = 0; i < this.clusters_.length; i++) {
1458     cluster = this.clusters_[i];
1459     center = cluster.getCenter();
1460     if (center) {
1461       d = this.distanceBetweenPoints_(center, marker.getPosition());
1462       if (d < distance) {
1463         distance = d;
1464         clusterToAddTo = cluster;
1465       }
1466     }
1467   }
1468
1469   if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
1470     clusterToAddTo.addMarker(marker);
1471   } else {
1472     cluster = new Cluster(this);
1473     cluster.addMarker(marker);
1474     this.clusters_.push(cluster);
1475   }
1476 };
1477
1478
1479 /**
1480  * Creates the clusters. This is done in batches to avoid timeout errors
1481  *  in some browsers when there is a huge number of markers.
1482  *
1483  * @param {number} iFirst The index of the first marker in the batch of
1484  *  markers to be added to clusters.
1485  */
1486 MarkerClusterer.prototype.createClusters_ = function (iFirst) {
1487   var i, marker;
1488   var mapBounds;
1489   var cMarkerClusterer = this;
1490   if (!this.ready_) {
1491     return;
1492   }
1493
1494   // Cancel previous batch processing if we're working on the first batch:
1495   if (iFirst === 0) {
1496     /**
1497      * This event is fired when the <code>MarkerClusterer</code> begins
1498      *  clustering markers.
1499      * @name MarkerClusterer#clusteringbegin
1500      * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered.
1501      * @event
1502      */
1503     google.maps.event.trigger(this, "clusteringbegin", this);
1504
1505     if (typeof this.timerRefStatic !== "undefined") {
1506       clearTimeout(this.timerRefStatic);
1507       delete this.timerRefStatic;
1508     }
1509   }
1510
1511   // Get our current map view bounds.
1512   // Create a new bounds object so we don't affect the map.
1513   //
1514   // See Comments 9 & 11 on Issue 3651 relating to this workaround for a Google Maps bug:
1515   if (this.getMap().getZoom() > 3) {
1516     mapBounds = new google.maps.LatLngBounds(this.getMap().getBounds().getSouthWest(),
1517       this.getMap().getBounds().getNorthEast());
1518   } else {
1519     mapBounds = new google.maps.LatLngBounds(new google.maps.LatLng(85.02070771743472, -178.48388434375), new google.maps.LatLng(-85.08136444384544, 178.00048865625));
1520   }
1521   var bounds = this.getExtendedBounds(mapBounds);
1522
1523   var iLast = Math.min(iFirst + this.batchSize_, this.markers_.length);
1524
1525   for (i = iFirst; i < iLast; i++) {
1526     marker = this.markers_[i];
1527     if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
1528       if (!this.ignoreHidden_ || (this.ignoreHidden_ && marker.getVisible())) {
1529         this.addToClosestCluster_(marker);
1530       }
1531     }
1532   }
1533
1534   if (iLast < this.markers_.length) {
1535     this.timerRefStatic = setTimeout(function () {
1536       cMarkerClusterer.createClusters_(iLast);
1537     }, 0);
1538   } else {
1539     delete this.timerRefStatic;
1540
1541     /**
1542      * This event is fired when the <code>MarkerClusterer</code> stops
1543      *  clustering markers.
1544      * @name MarkerClusterer#clusteringend
1545      * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered.
1546      * @event
1547      */
1548     google.maps.event.trigger(this, "clusteringend", this);
1549   }
1550 };
1551
1552
1553 /**
1554  * Extends an object's prototype by another's.
1555  *
1556  * @param {Object} obj1 The object to be extended.
1557  * @param {Object} obj2 The object to extend with.
1558  * @return {Object} The new extended object.
1559  * @ignore
1560  */
1561 MarkerClusterer.prototype.extend = function (obj1, obj2) {
1562   return (function (object) {
1563     var property;
1564     for (property in object.prototype) {
1565       this.prototype[property] = object.prototype[property];
1566     }
1567     return this;
1568   }).apply(obj1, [obj2]);
1569 };
1570
1571
1572 /**
1573  * The default function for determining the label text and style
1574  * for a cluster icon.
1575  *
1576  * @param {Array.<google.maps.Marker>} markers The array of markers represented by the cluster.
1577  * @param {number} numStyles The number of marker styles available.
1578  * @return {ClusterIconInfo} The information resource for the cluster.
1579  * @constant
1580  * @ignore
1581  */
1582 MarkerClusterer.CALCULATOR = function (markers, numStyles) {
1583   var index = 0;
1584   var title = "";
1585   var count = markers.length.toString();
1586
1587   var dv = count;
1588   while (dv !== 0) {
1589     dv = parseInt(dv / 10, 10);
1590     index++;
1591   }
1592
1593   index = Math.min(index, numStyles);
1594   return {
1595     text: count,
1596     index: index,
1597     title: title
1598   };
1599 };
1600
1601
1602 /**
1603  * The number of markers to process in one batch.
1604  *
1605  * @type {number}
1606  * @constant
1607  */
1608 MarkerClusterer.BATCH_SIZE = 2000;
1609
1610
1611 /**
1612  * The number of markers to process in one batch (IE only).
1613  *
1614  * @type {number}
1615  * @constant
1616  */
1617 MarkerClusterer.BATCH_SIZE_IE = 500;
1618
1619
1620 /**
1621  * The default root name for the marker cluster images.
1622  *
1623  * @type {string}
1624  * @constant
1625  */
1626 MarkerClusterer.IMAGE_PATH = "http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclustererplus/images/m";
1627
1628
1629 /**
1630  * The default extension name for the marker cluster images.
1631  *
1632  * @type {string}
1633  * @constant
1634  */
1635 MarkerClusterer.IMAGE_EXTENSION = "png";
1636
1637
1638 /**
1639  * The default array of sizes for the marker cluster images.
1640  *
1641  * @type {Array.<number>}
1642  * @constant
1643  */
1644 MarkerClusterer.IMAGE_SIZES = [53, 56, 66, 78, 90];