Statistics
| Branch: | Tag: | Revision:

root / static / js / markerclusterer.js @ 02f2ae43

History | View | Annotate | Download (50.1 kB)

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.14 [May 16, 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
  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.clearInstanceListeners(this.div_);
215
    this.div_.parentNode.removeChild(this.div_);
216
    this.div_ = null;
217
  }
218
};
219

    
220

    
221
/**
222
 * Draws the icon.
223
 */
224
ClusterIcon.prototype.draw = function () {
225
  if (this.visible_) {
226
    var pos = this.getPosFromLatLng_(this.center_);
227
    this.div_.style.top = pos.y + "px";
228
    this.div_.style.left = pos.x + "px";
229
  }
230
};
231

    
232

    
233
/**
234
 * Hides the icon.
235
 */
236
ClusterIcon.prototype.hide = function () {
237
  if (this.div_) {
238
    this.div_.style.display = "none";
239
  }
240
  this.visible_ = false;
241
};
242

    
243

    
244
/**
245
 * Positions and shows the icon.
246
 */
247
ClusterIcon.prototype.show = function () {
248
  if (this.div_) {
249
    var pos = this.getPosFromLatLng_(this.center_);
250
    this.div_.style.cssText = this.createCss(pos);
251
    if (this.cluster_.printable_) {
252
      // (Would like to use "width: inherit;" below, but doesn't work with MSIE)
253
      this.div_.innerHTML = "<img src='" + this.url_ + "'><div style='position: absolute; top: 0px; left: 0px; width: " + this.width_ + "px;'>" + this.sums_.text + "</div>";
254
    } else {
255
      this.div_.innerHTML = this.sums_.text;
256
    }
257
    if (typeof this.sums_.title === "undefined" || this.sums_.title === "") {
258
      this.div_.title = this.cluster_.getMarkerClusterer().getTitle();
259
    } else {
260
      this.div_.title = this.sums_.title;
261
    }
262
    this.div_.style.display = "";
263
  }
264
  this.visible_ = true;
265
};
266

    
267

    
268
/**
269
 * Sets the icon styles to the appropriate element in the styles array.
270
 *
271
 * @param {ClusterIconInfo} sums The icon label text and styles index.
272
 */
273
ClusterIcon.prototype.useStyle = function (sums) {
274
  this.sums_ = sums;
275
  var index = Math.max(0, sums.index - 1);
276
  index = Math.min(this.styles_.length - 1, index);
277
  var style = this.styles_[index];
278
  this.url_ = style.url;
279
  this.height_ = style.height;
280
  this.width_ = style.width;
281
  this.anchor_ = style.anchor;
282
  this.anchorIcon_ = style.anchorIcon || [parseInt(this.height_ / 2, 10), parseInt(this.width_ / 2, 10)];
283
  this.textColor_ = style.textColor || "black";
284
  this.textSize_ = style.textSize || 11;
285
  this.textDecoration_ = style.textDecoration || "none";
286
  this.fontWeight_ = style.fontWeight || "bold";
287
  this.fontStyle_ = style.fontStyle || "normal";
288
  this.fontFamily_ = style.fontFamily || "Arial,sans-serif";
289
  this.backgroundPosition_ = style.backgroundPosition || "0 0";
290
};
291

    
292

    
293
/**
294
 * Sets the position at which to center the icon.
295
 *
296
 * @param {google.maps.LatLng} center The latlng to set as the center.
297
 */
298
ClusterIcon.prototype.setCenter = function (center) {
299
  this.center_ = center;
300
};
301

    
302

    
303
/**
304
 * Creates the cssText style parameter based on the position of the icon.
305
 *
306
 * @param {google.maps.Point} pos The position of the icon.
307
 * @return {string} The CSS style text.
308
 */
309
ClusterIcon.prototype.createCss = function (pos) {
310
  var style = [];
311
  if (!this.cluster_.printable_) {
312
    style.push('background-image:url(' + this.url_ + ');');
313
    style.push('background-position:' + this.backgroundPosition_ + ';');
314
  }
315

    
316
  if (typeof this.anchor_ === 'object') {
317
    if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
318
        this.anchor_[0] < this.height_) {
319
      style.push('height:' + (this.height_ - this.anchor_[0]) +
320
          'px; padding-top:' + this.anchor_[0] + 'px;');
321
    } else {
322
      style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
323
          'px;');
324
    }
325
    if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
326
        this.anchor_[1] < this.width_) {
327
      style.push('width:' + (this.width_ - this.anchor_[1]) +
328
          'px; padding-left:' + this.anchor_[1] + 'px;');
329
    } else {
330
      style.push('width:' + this.width_ + 'px; text-align:center;');
331
    }
332
  } else {
333
    style.push('height:' + this.height_ + 'px; line-height:' +
334
        this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
335
  }
336

    
337
  style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
338
      pos.x + 'px; color:' + this.textColor_ + '; position:absolute; font-size:' +
339
      this.textSize_ + 'px; font-family:' + this.fontFamily_ + '; font-weight:' +
340
      this.fontWeight_ + '; font-style:' + this.fontStyle_ + '; text-decoration:' +
341
      this.textDecoration_ + ';');
342

    
343
  return style.join("");
344
};
345

    
346

    
347
/**
348
 * Returns the position at which to place the DIV depending on the latlng.
349
 *
350
 * @param {google.maps.LatLng} latlng The position in latlng.
351
 * @return {google.maps.Point} The position in pixels.
352
 */
353
ClusterIcon.prototype.getPosFromLatLng_ = function (latlng) {
354
  var pos = this.getProjection().fromLatLngToDivPixel(latlng);
355
  pos.x -= this.anchorIcon_[1];
356
  pos.y -= this.anchorIcon_[0];
357
  return pos;
358
};
359

    
360

    
361
/**
362
 * Creates a single cluster that manages a group of proximate markers.
363
 *  Used internally, do not call this constructor directly.
364
 * @constructor
365
 * @param {MarkerClusterer} mc The <code>MarkerClusterer</code> object with which this
366
 *  cluster is associated.
367
 */
368
function Cluster(mc) {
369
  this.markerClusterer_ = mc;
370
  this.map_ = mc.getMap();
371
  this.gridSize_ = mc.getGridSize();
372
  this.minClusterSize_ = mc.getMinimumClusterSize();
373
  this.averageCenter_ = mc.getAverageCenter();
374
  this.printable_ = mc.getPrintable();
375
  this.markers_ = [];
376
  this.center_ = null;
377
  this.bounds_ = null;
378
  this.clusterIcon_ = new ClusterIcon(this, mc.getStyles());
379
}
380

    
381

    
382
/**
383
 * Returns the number of markers managed by the cluster. You can call this from
384
 * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
385
 * for the <code>MarkerClusterer</code> object.
386
 *
387
 * @return {number} The number of markers in the cluster.
388
 */
389
Cluster.prototype.getSize = function () {
390
  return this.markers_.length;
391
};
392

    
393

    
394
/**
395
 * Returns the array of markers managed by the cluster. You can call this from
396
 * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
397
 * for the <code>MarkerClusterer</code> object.
398
 *
399
 * @return {Array} The array of markers in the cluster.
400
 */
401
Cluster.prototype.getMarkers = function () {
402
  return this.markers_;
403
};
404

    
405

    
406
/**
407
 * Returns the center of the cluster. You can call this from
408
 * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
409
 * for the <code>MarkerClusterer</code> object.
410
 *
411
 * @return {google.maps.LatLng} The center of the cluster.
412
 */
413
Cluster.prototype.getCenter = function () {
414
  return this.center_;
415
};
416

    
417

    
418
/**
419
 * Returns the map with which the cluster is associated.
420
 *
421
 * @return {google.maps.Map} The map.
422
 * @ignore
423
 */
424
Cluster.prototype.getMap = function () {
425
  return this.map_;
426
};
427

    
428

    
429
/**
430
 * Returns the <code>MarkerClusterer</code> object with which the cluster is associated.
431
 *
432
 * @return {MarkerClusterer} The associated marker clusterer.
433
 * @ignore
434
 */
435
Cluster.prototype.getMarkerClusterer = function () {
436
  return this.markerClusterer_;
437
};
438

    
439

    
440
/**
441
 * Returns the bounds of the cluster.
442
 *
443
 * @return {google.maps.LatLngBounds} the cluster bounds.
444
 * @ignore
445
 */
446
Cluster.prototype.getBounds = function () {
447
  var i;
448
  var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
449
  var markers = this.getMarkers();
450
  for (i = 0; i < markers.length; i++) {
451
    bounds.extend(markers[i].getPosition());
452
  }
453
  return bounds;
454
};
455

    
456

    
457
/**
458
 * Removes the cluster from the map.
459
 *
460
 * @ignore
461
 */
462
Cluster.prototype.remove = function () {
463
  this.clusterIcon_.setMap(null);
464
  this.markers_ = [];
465
  delete this.markers_;
466
};
467

    
468

    
469
/**
470
 * Adds a marker to the cluster.
471
 *
472
 * @param {google.maps.Marker} marker The marker to be added.
473
 * @return {boolean} True if the marker was added.
474
 * @ignore
475
 */
476
Cluster.prototype.addMarker = function (marker) {
477
  var i;
478
  var mCount;
479
  var mz;
480

    
481
  if (this.isMarkerAlreadyAdded_(marker)) {
482
    return false;
483
  }
484

    
485
  if (!this.center_) {
486
    this.center_ = marker.getPosition();
487
    this.calculateBounds_();
488
  } else {
489
    if (this.averageCenter_) {
490
      var l = this.markers_.length + 1;
491
      var lat = (this.center_.lat() * (l - 1) + marker.getPosition().lat()) / l;
492
      var lng = (this.center_.lng() * (l - 1) + marker.getPosition().lng()) / l;
493
      this.center_ = new google.maps.LatLng(lat, lng);
494
      this.calculateBounds_();
495
    }
496
  }
497

    
498
  marker.isAdded = true;
499
  this.markers_.push(marker);
500

    
501
  mCount = this.markers_.length;
502
  mz = this.markerClusterer_.getMaxZoom();
503
  if (mz !== null && this.map_.getZoom() > mz) {
504
    // Zoomed in past max zoom, so show the marker.
505
    if (marker.getMap() !== this.map_) {
506
      marker.setMap(this.map_);
507
    }
508
  } else if (mCount < this.minClusterSize_) {
509
    // Min cluster size not reached so show the marker.
510
    if (marker.getMap() !== this.map_) {
511
      marker.setMap(this.map_);
512
    }
513
  } else if (mCount === this.minClusterSize_) {
514
    // Hide the markers that were showing.
515
    for (i = 0; i < mCount; i++) {
516
      this.markers_[i].setMap(null);
517
    }
518
  } else {
519
    marker.setMap(null);
520
  }
521

    
522
  this.updateIcon_();
523
  return true;
524
};
525

    
526

    
527
/**
528
 * Determines if a marker lies within the cluster's bounds.
529
 *
530
 * @param {google.maps.Marker} marker The marker to check.
531
 * @return {boolean} True if the marker lies in the bounds.
532
 * @ignore
533
 */
534
Cluster.prototype.isMarkerInClusterBounds = function (marker) {
535
  return this.bounds_.contains(marker.getPosition());
536
};
537

    
538

    
539
/**
540
 * Calculates the extended bounds of the cluster with the grid.
541
 */
542
Cluster.prototype.calculateBounds_ = function () {
543
  var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
544
  this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
545
};
546

    
547

    
548
/**
549
 * Updates the cluster icon.
550
 */
551
Cluster.prototype.updateIcon_ = function () {
552
  var mCount = this.markers_.length;
553
  var mz = this.markerClusterer_.getMaxZoom();
554

    
555
  if (mz !== null && this.map_.getZoom() > mz) {
556
    this.clusterIcon_.hide();
557
    return;
558
  }
559

    
560
  if (mCount < this.minClusterSize_) {
561
    // Min cluster size not yet reached.
562
    this.clusterIcon_.hide();
563
    return;
564
  }
565

    
566
  var numStyles = this.markerClusterer_.getStyles().length;
567
  var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
568
  this.clusterIcon_.setCenter(this.center_);
569
  this.clusterIcon_.useStyle(sums);
570
  this.clusterIcon_.show();
571
};
572

    
573

    
574
/**
575
 * Determines if a marker has already been added to the cluster.
576
 *
577
 * @param {google.maps.Marker} marker The marker to check.
578
 * @return {boolean} True if the marker has already been added.
579
 */
580
Cluster.prototype.isMarkerAlreadyAdded_ = function (marker) {
581
  var i;
582
  if (this.markers_.indexOf) {
583
    return this.markers_.indexOf(marker) !== -1;
584
  } else {
585
    for (i = 0; i < this.markers_.length; i++) {
586
      if (marker === this.markers_[i]) {
587
        return true;
588
      }
589
    }
590
  }
591
  return false;
592
};
593

    
594

    
595
/**
596
 * @name MarkerClustererOptions
597
 * @class This class represents the optional parameter passed to
598
 *  the {@link MarkerClusterer} constructor.
599
 * @property {number} [gridSize=60] The grid size of a cluster in pixels. The grid is a square.
600
 * @property {number} [maxZoom=null] The maximum zoom level at which clustering is enabled or
601
 *  <code>null</code> if clustering is to be enabled at all zoom levels.
602
 * @property {boolean} [zoomOnClick=true] Whether to zoom the map when a cluster marker is
603
 *  clicked. You may want to set this to <code>false</code> if you have installed a handler
604
 *  for the <code>click</code> event and it deals with zooming on its own.
605
 * @property {boolean} [averageCenter=false] Whether the position of a cluster marker should be
606
 *  the average position of all markers in the cluster. If set to <code>false</code>, the
607
 *  cluster marker is positioned at the location of the first marker added to the cluster.
608
 * @property {number} [minimumClusterSize=2] The minimum number of markers needed in a cluster
609
 *  before the markers are hidden and a cluster marker appears.
610
 * @property {boolean} [ignoreHidden=false] Whether to ignore hidden markers in clusters. You
611
 *  may want to set this to <code>true</code> to ensure that hidden markers are not included
612
 *  in the marker count that appears on a cluster marker (this count is the value of the
613
 *  <code>text</code> property of the result returned by the default <code>calculator</code>).
614
 *  If set to <code>true</code> and you change the visibility of a marker being clustered, be
615
 *  sure to also call <code>MarkerClusterer.repaint()</code>.
616
 * @property {boolean} [printable=false] Whether to make the cluster icons printable. Do not
617
 *  set to <code>true</code> if the <code>url</code> fields in the <code>styles</code> array
618
 *  refer to image sprite files.
619
 * @property {string} [title=""] The tooltip to display when the mouse moves over a cluster
620
 *  marker. (Alternatively, you can use a custom <code>calculator</code> function to specify a
621
 *  different tooltip for each cluster marker.)
622
 * @property {function} [calculator=MarkerClusterer.CALCULATOR] The function used to determine
623
 *  the text to be displayed on a cluster marker and the index indicating which style to use
624
 *  for the cluster marker. The input parameters for the function are (1) the array of markers
625
 *  represented by a cluster marker and (2) the number of cluster icon styles. It returns a
626
 *  {@link ClusterIconInfo} object. The default <code>calculator</code> returns a
627
 *  <code>text</code> property which is the number of markers in the cluster and an
628
 *  <code>index</code> property which is one higher than the lowest integer such that
629
 *  <code>10^i</code> exceeds the number of markers in the cluster, or the size of the styles
630
 *  array, whichever is less. The <code>styles</code> array element used has an index of
631
 *  <code>index</code> minus 1. For example, the default <code>calculator</code> returns a
632
 *  <code>text</code> value of <code>"125"</code> and an <code>index</code> of <code>3</code>
633
 *  for a cluster icon representing 125 markers so the element used in the <code>styles</code>
634
 *  array is <code>2</code>. A <code>calculator</code> may also return a <code>title</code>
635
 *  property that contains the text of the tooltip to be used for the cluster marker. If
636
 *   <code>title</code> is not defined, the tooltip is set to the value of the <code>title</code>
637
 *   property for the MarkerClusterer.
638
 * @property {string} [clusterClass="cluster"] The name of the CSS class defining general styles
639
 *  for the cluster markers. Use this class to define CSS styles that are not set up by the code
640
 *  that processes the <code>styles</code> array.
641
 * @property {Array} [styles] An array of {@link ClusterIconStyle} elements defining the styles
642
 *  of the cluster markers to be used. The element to be used to style a given cluster marker
643
 *  is determined by the function defined by the <code>calculator</code> property.
644
 *  The default is an array of {@link ClusterIconStyle} elements whose properties are derived
645
 *  from the values for <code>imagePath</code>, <code>imageExtension</code>, and
646
 *  <code>imageSizes</code>.
647
 * @property {number} [batchSize=MarkerClusterer.BATCH_SIZE] Set this property to the
648
 *  number of markers to be processed in a single batch when using a browser other than
649
 *  Internet Explorer (for Internet Explorer, use the batchSizeIE property instead).
650
 * @property {number} [batchSizeIE=MarkerClusterer.BATCH_SIZE_IE] When Internet Explorer is
651
 *  being used, markers are processed in several batches with a small delay inserted between
652
 *  each batch in an attempt to avoid Javascript timeout errors. Set this property to the
653
 *  number of markers to be processed in a single batch; select as high a number as you can
654
 *  without causing a timeout error in the browser. This number might need to be as low as 100
655
 *  if 15,000 markers are being managed, for example.
656
 * @property {string} [imagePath=MarkerClusterer.IMAGE_PATH]
657
 *  The full URL of the root name of the group of image files to use for cluster icons.
658
 *  The complete file name is of the form <code>imagePath</code>n.<code>imageExtension</code>
659
 *  where n is the image file number (1, 2, etc.).
660
 * @property {string} [imageExtension=MarkerClusterer.IMAGE_EXTENSION]
661
 *  The extension name for the cluster icon image files (e.g., <code>"png"</code> or
662
 *  <code>"jpg"</code>).
663
 * @property {Array} [imageSizes=MarkerClusterer.IMAGE_SIZES]
664
 *  An array of numbers containing the widths of the group of
665
 *  <code>imagePath</code>n.<code>imageExtension</code> image files.
666
 *  (The images are assumed to be square.)
667
 */
668
/**
669
 * Creates a MarkerClusterer object with the options specified in {@link MarkerClustererOptions}.
670
 * @constructor
671
 * @extends google.maps.OverlayView
672
 * @param {google.maps.Map} map The Google map to attach to.
673
 * @param {Array.<google.maps.Marker>} [opt_markers] The markers to be added to the cluster.
674
 * @param {MarkerClustererOptions} [opt_options] The optional parameters.
675
 */
676
function MarkerClusterer(map, opt_markers, opt_options) {
677
  // MarkerClusterer implements google.maps.OverlayView interface. We use the
678
  // extend function to extend MarkerClusterer with google.maps.OverlayView
679
  // because it might not always be available when the code is defined so we
680
  // look for it at the last possible moment. If it doesn't exist now then
681
  // there is no point going ahead :)
682
  this.extend(MarkerClusterer, google.maps.OverlayView);
683

    
684
  opt_markers = opt_markers || [];
685
  opt_options = opt_options || {};
686

    
687
  this.markers_ = [];
688
  this.clusters_ = [];
689
  this.listeners_ = [];
690
  this.activeMap_ = null;
691
  this.ready_ = false;
692

    
693
  this.gridSize_ = opt_options.gridSize || 60;
694
  this.minClusterSize_ = opt_options.minimumClusterSize || 2;
695
  this.maxZoom_ = opt_options.maxZoom || null;
696
  this.styles_ = opt_options.styles || [];
697
  this.title_ = opt_options.title || "";
698
  this.zoomOnClick_ = true;
699
  if (opt_options.zoomOnClick !== undefined) {
700
    this.zoomOnClick_ = opt_options.zoomOnClick;
701
  }
702
  this.averageCenter_ = false;
703
  if (opt_options.averageCenter !== undefined) {
704
    this.averageCenter_ = opt_options.averageCenter;
705
  }
706
  this.ignoreHidden_ = false;
707
  if (opt_options.ignoreHidden !== undefined) {
708
    this.ignoreHidden_ = opt_options.ignoreHidden;
709
  }
710
  this.printable_ = false;
711
  if (opt_options.printable !== undefined) {
712
    this.printable_ = opt_options.printable;
713
  }
714
  this.imagePath_ = opt_options.imagePath || MarkerClusterer.IMAGE_PATH;
715
  this.imageExtension_ = opt_options.imageExtension || MarkerClusterer.IMAGE_EXTENSION;
716
  this.imageSizes_ = opt_options.imageSizes || MarkerClusterer.IMAGE_SIZES;
717
  this.calculator_ = opt_options.calculator || MarkerClusterer.CALCULATOR;
718
  this.batchSize_ = opt_options.batchSize || MarkerClusterer.BATCH_SIZE;
719
  this.batchSizeIE_ = opt_options.batchSizeIE || MarkerClusterer.BATCH_SIZE_IE;
720
  this.clusterClass_ = opt_options.clusterClass || "cluster";
721

    
722
  if (navigator.userAgent.toLowerCase().indexOf("msie") !== -1) {
723
    // Try to avoid IE timeout when processing a huge number of markers:
724
    this.batchSize_ = this.batchSizeIE_;
725
  }
726

    
727
  this.setupStyles_();
728

    
729
  this.addMarkers(opt_markers, true);
730
  this.setMap(map); // Note: this causes onAdd to be called
731
}
732

    
733

    
734
/**
735
 * Implementation of the onAdd interface method.
736
 * @ignore
737
 */
738
MarkerClusterer.prototype.onAdd = function () {
739
  var cMarkerClusterer = this;
740

    
741
  this.activeMap_ = this.getMap();
742
  this.ready_ = true;
743

    
744
  this.repaint();
745

    
746
  // Add the map event listeners
747
  this.listeners_ = [
748
    google.maps.event.addListener(this.getMap(), "zoom_changed", function () {
749
      cMarkerClusterer.resetViewport_(false);
750
      // Workaround for this Google bug: when map is at level 0 and "-" of
751
      // zoom slider is clicked, a "zoom_changed" event is fired even though
752
      // the map doesn't zoom out any further. In this situation, no "idle"
753
      // event is triggered so the cluster markers that have been removed
754
      // do not get redrawn. Same goes for a zoom in at maxZoom.
755
      if (this.getZoom() === (this.get("minZoom") || 0) || this.getZoom() === this.get("maxZoom")) {
756
        google.maps.event.trigger(this, "idle");
757
      }
758
    }),
759
    google.maps.event.addListener(this.getMap(), "idle", function () {
760
      cMarkerClusterer.redraw_();
761
    })
762
  ];
763
};
764

    
765

    
766
/**
767
 * Implementation of the onRemove interface method.
768
 * Removes map event listeners and all cluster icons from the DOM.
769
 * All managed markers are also put back on the map.
770
 * @ignore
771
 */
772
MarkerClusterer.prototype.onRemove = function () {
773
  var i;
774

    
775
  // Put all the managed markers back on the map:
776
  for (i = 0; i < this.markers_.length; i++) {
777
    this.markers_[i].setMap(this.activeMap_);
778
  }
779

    
780
  // Remove all clusters:
781
  for (i = 0; i < this.clusters_.length; i++) {
782
    this.clusters_[i].remove();
783
  }
784
  this.clusters_ = [];
785

    
786
  // Remove map event listeners:
787
  for (i = 0; i < this.listeners_.length; i++) {
788
    google.maps.event.removeListener(this.listeners_[i]);
789
  }
790
  this.listeners_ = [];
791

    
792
  this.activeMap_ = null;
793
  this.ready_ = false;
794
};
795

    
796

    
797
/**
798
 * Implementation of the draw interface method.
799
 * @ignore
800
 */
801
MarkerClusterer.prototype.draw = function () {};
802

    
803

    
804
/**
805
 * Sets up the styles object.
806
 */
807
MarkerClusterer.prototype.setupStyles_ = function () {
808
  var i, size;
809
  if (this.styles_.length > 0) {
810
    return;
811
  }
812

    
813
  for (i = 0; i < this.imageSizes_.length; i++) {
814
    size = this.imageSizes_[i];
815
    this.styles_.push({
816
      url: this.imagePath_ + (i + 1) + "." + this.imageExtension_,
817
      height: size,
818
      width: size
819
    });
820
  }
821
};
822

    
823

    
824
/**
825
 *  Fits the map to the bounds of the markers managed by the clusterer.
826
 */
827
MarkerClusterer.prototype.fitMapToMarkers = function () {
828
  var i;
829
  var markers = this.getMarkers();
830
  var bounds = new google.maps.LatLngBounds();
831
  for (i = 0; i < markers.length; i++) {
832
    bounds.extend(markers[i].getPosition());
833
  }
834

    
835
  this.getMap().fitBounds(bounds);
836
};
837

    
838

    
839
/**
840
 * Returns the value of the <code>gridSize</code> property.
841
 *
842
 * @return {number} The grid size.
843
 */
844
MarkerClusterer.prototype.getGridSize = function () {
845
  return this.gridSize_;
846
};
847

    
848

    
849
/**
850
 * Sets the value of the <code>gridSize</code> property.
851
 *
852
 * @param {number} gridSize The grid size.
853
 */
854
MarkerClusterer.prototype.setGridSize = function (gridSize) {
855
  this.gridSize_ = gridSize;
856
};
857

    
858

    
859
/**
860
 * Returns the value of the <code>minimumClusterSize</code> property.
861
 *
862
 * @return {number} The minimum cluster size.
863
 */
864
MarkerClusterer.prototype.getMinimumClusterSize = function () {
865
  return this.minClusterSize_;
866
};
867

    
868
/**
869
 * Sets the value of the <code>minimumClusterSize</code> property.
870
 *
871
 * @param {number} minimumClusterSize The minimum cluster size.
872
 */
873
MarkerClusterer.prototype.setMinimumClusterSize = function (minimumClusterSize) {
874
  this.minClusterSize_ = minimumClusterSize;
875
};
876

    
877

    
878
/**
879
 *  Returns the value of the <code>maxZoom</code> property.
880
 *
881
 *  @return {number} The maximum zoom level.
882
 */
883
MarkerClusterer.prototype.getMaxZoom = function () {
884
  return this.maxZoom_;
885
};
886

    
887

    
888
/**
889
 *  Sets the value of the <code>maxZoom</code> property.
890
 *
891
 *  @param {number} maxZoom The maximum zoom level.
892
 */
893
MarkerClusterer.prototype.setMaxZoom = function (maxZoom) {
894
  this.maxZoom_ = maxZoom;
895
};
896

    
897

    
898
/**
899
 *  Returns the value of the <code>styles</code> property.
900
 *
901
 *  @return {Array} The array of styles defining the cluster markers to be used.
902
 */
903
MarkerClusterer.prototype.getStyles = function () {
904
  return this.styles_;
905
};
906

    
907

    
908
/**
909
 *  Sets the value of the <code>styles</code> property.
910
 *
911
 *  @param {Array.<ClusterIconStyle>} styles The array of styles to use.
912
 */
913
MarkerClusterer.prototype.setStyles = function (styles) {
914
  this.styles_ = styles;
915
};
916

    
917

    
918
/**
919
 * Returns the value of the <code>title</code> property.
920
 *
921
 * @return {string} The content of the title text.
922
 */
923
MarkerClusterer.prototype.getTitle = function () {
924
  return this.title_;
925
};
926

    
927

    
928
/**
929
 *  Sets the value of the <code>title</code> property.
930
 *
931
 *  @param {string} title The value of the title property.
932
 */
933
MarkerClusterer.prototype.setTitle = function (title) {
934
  this.title_ = title;
935
};
936

    
937

    
938
/**
939
 * Returns the value of the <code>zoomOnClick</code> property.
940
 *
941
 * @return {boolean} True if zoomOnClick property is set.
942
 */
943
MarkerClusterer.prototype.getZoomOnClick = function () {
944
  return this.zoomOnClick_;
945
};
946

    
947

    
948
/**
949
 *  Sets the value of the <code>zoomOnClick</code> property.
950
 *
951
 *  @param {boolean} zoomOnClick The value of the zoomOnClick property.
952
 */
953
MarkerClusterer.prototype.setZoomOnClick = function (zoomOnClick) {
954
  this.zoomOnClick_ = zoomOnClick;
955
};
956

    
957

    
958
/**
959
 * Returns the value of the <code>averageCenter</code> property.
960
 *
961
 * @return {boolean} True if averageCenter property is set.
962
 */
963
MarkerClusterer.prototype.getAverageCenter = function () {
964
  return this.averageCenter_;
965
};
966

    
967

    
968
/**
969
 *  Sets the value of the <code>averageCenter</code> property.
970
 *
971
 *  @param {boolean} averageCenter The value of the averageCenter property.
972
 */
973
MarkerClusterer.prototype.setAverageCenter = function (averageCenter) {
974
  this.averageCenter_ = averageCenter;
975
};
976

    
977

    
978
/**
979
 * Returns the value of the <code>ignoreHidden</code> property.
980
 *
981
 * @return {boolean} True if ignoreHidden property is set.
982
 */
983
MarkerClusterer.prototype.getIgnoreHidden = function () {
984
  return this.ignoreHidden_;
985
};
986

    
987

    
988
/**
989
 *  Sets the value of the <code>ignoreHidden</code> property.
990
 *
991
 *  @param {boolean} ignoreHidden The value of the ignoreHidden property.
992
 */
993
MarkerClusterer.prototype.setIgnoreHidden = function (ignoreHidden) {
994
  this.ignoreHidden_ = ignoreHidden;
995
};
996

    
997

    
998
/**
999
 * Returns the value of the <code>imageExtension</code> property.
1000
 *
1001
 * @return {string} The value of the imageExtension property.
1002
 */
1003
MarkerClusterer.prototype.getImageExtension = function () {
1004
  return this.imageExtension_;
1005
};
1006

    
1007

    
1008
/**
1009
 *  Sets the value of the <code>imageExtension</code> property.
1010
 *
1011
 *  @param {string} imageExtension The value of the imageExtension property.
1012
 */
1013
MarkerClusterer.prototype.setImageExtension = function (imageExtension) {
1014
  this.imageExtension_ = imageExtension;
1015
};
1016

    
1017

    
1018
/**
1019
 * Returns the value of the <code>imagePath</code> property.
1020
 *
1021
 * @return {string} The value of the imagePath property.
1022
 */
1023
MarkerClusterer.prototype.getImagePath = function () {
1024
  return this.imagePath_;
1025
};
1026

    
1027

    
1028
/**
1029
 *  Sets the value of the <code>imagePath</code> property.
1030
 *
1031
 *  @param {string} imagePath The value of the imagePath property.
1032
 */
1033
MarkerClusterer.prototype.setImagePath = function (imagePath) {
1034
  this.imagePath_ = imagePath;
1035
};
1036

    
1037

    
1038
/**
1039
 * Returns the value of the <code>imageSizes</code> property.
1040
 *
1041
 * @return {Array} The value of the imageSizes property.
1042
 */
1043
MarkerClusterer.prototype.getImageSizes = function () {
1044
  return this.imageSizes_;
1045
};
1046

    
1047

    
1048
/**
1049
 *  Sets the value of the <code>imageSizes</code> property.
1050
 *
1051
 *  @param {Array} imageSizes The value of the imageSizes property.
1052
 */
1053
MarkerClusterer.prototype.setImageSizes = function (imageSizes) {
1054
  this.imageSizes_ = imageSizes;
1055
};
1056

    
1057

    
1058
/**
1059
 * Returns the value of the <code>calculator</code> property.
1060
 *
1061
 * @return {function} the value of the calculator property.
1062
 */
1063
MarkerClusterer.prototype.getCalculator = function () {
1064
  return this.calculator_;
1065
};
1066

    
1067

    
1068
/**
1069
 * Sets the value of the <code>calculator</code> property.
1070
 *
1071
 * @param {function(Array.<google.maps.Marker>, number)} calculator The value
1072
 *  of the calculator property.
1073
 */
1074
MarkerClusterer.prototype.setCalculator = function (calculator) {
1075
  this.calculator_ = calculator;
1076
};
1077

    
1078

    
1079
/**
1080
 * Returns the value of the <code>printable</code> property.
1081
 *
1082
 * @return {boolean} the value of the printable property.
1083
 */
1084
MarkerClusterer.prototype.getPrintable = function () {
1085
  return this.printable_;
1086
};
1087

    
1088

    
1089
/**
1090
 * Sets the value of the <code>printable</code> property.
1091
 *
1092
 *  @param {boolean} printable The value of the printable property.
1093
 */
1094
MarkerClusterer.prototype.setPrintable = function (printable) {
1095
  this.printable_ = printable;
1096
};
1097

    
1098

    
1099
/**
1100
 * Returns the value of the <code>batchSizeIE</code> property.
1101
 *
1102
 * @return {number} the value of the batchSizeIE property.
1103
 */
1104
MarkerClusterer.prototype.getBatchSizeIE = function () {
1105
  return this.batchSizeIE_;
1106
};
1107

    
1108

    
1109
/**
1110
 * Sets the value of the <code>batchSizeIE</code> property.
1111
 *
1112
 *  @param {number} batchSizeIE The value of the batchSizeIE property.
1113
 */
1114
MarkerClusterer.prototype.setBatchSizeIE = function (batchSizeIE) {
1115
  this.batchSizeIE_ = batchSizeIE;
1116
};
1117

    
1118

    
1119
/**
1120
 * Returns the value of the <code>clusterClass</code> property.
1121
 *
1122
 * @return {string} the value of the clusterClass property.
1123
 */
1124
MarkerClusterer.prototype.getClusterClass = function () {
1125
  return this.clusterClass_;
1126
};
1127

    
1128

    
1129
/**
1130
 * Sets the value of the <code>clusterClass</code> property.
1131
 *
1132
 *  @param {string} clusterClass The value of the clusterClass property.
1133
 */
1134
MarkerClusterer.prototype.setClusterClass = function (clusterClass) {
1135
  this.clusterClass_ = clusterClass;
1136
};
1137

    
1138

    
1139
/**
1140
 *  Returns the array of markers managed by the clusterer.
1141
 *
1142
 *  @return {Array} The array of markers managed by the clusterer.
1143
 */
1144
MarkerClusterer.prototype.getMarkers = function () {
1145
  return this.markers_;
1146
};
1147

    
1148

    
1149
/**
1150
 *  Returns the number of markers managed by the clusterer.
1151
 *
1152
 *  @return {number} The number of markers.
1153
 */
1154
MarkerClusterer.prototype.getTotalMarkers = function () {
1155
  return this.markers_.length;
1156
};
1157

    
1158

    
1159
/**
1160
 * Returns the current array of clusters formed by the clusterer.
1161
 *
1162
 * @return {Array} The array of clusters formed by the clusterer.
1163
 */
1164
MarkerClusterer.prototype.getClusters = function () {
1165
  return this.clusters_;
1166
};
1167

    
1168

    
1169
/**
1170
 * Returns the number of clusters formed by the clusterer.
1171
 *
1172
 * @return {number} The number of clusters formed by the clusterer.
1173
 */
1174
MarkerClusterer.prototype.getTotalClusters = function () {
1175
  return this.clusters_.length;
1176
};
1177

    
1178

    
1179
/**
1180
 * Adds a marker to the clusterer. The clusters are redrawn unless
1181
 *  <code>opt_nodraw</code> is set to <code>true</code>.
1182
 *
1183
 * @param {google.maps.Marker} marker The marker to add.
1184
 * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
1185
 */
1186
MarkerClusterer.prototype.addMarker = function (marker, opt_nodraw) {
1187
  this.pushMarkerTo_(marker);
1188
  if (!opt_nodraw) {
1189
    this.redraw_();
1190
  }
1191
};
1192

    
1193

    
1194
/**
1195
 * Adds an array of markers to the clusterer. The clusters are redrawn unless
1196
 *  <code>opt_nodraw</code> is set to <code>true</code>.
1197
 *
1198
 * @param {Array.<google.maps.Marker>} markers The markers to add.
1199
 * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
1200
 */
1201
MarkerClusterer.prototype.addMarkers = function (markers, opt_nodraw) {
1202
  var i;
1203
  for (i = 0; i < markers.length; i++) {
1204
    this.pushMarkerTo_(markers[i]);
1205
  }
1206
  if (!opt_nodraw) {
1207
    this.redraw_();
1208
  }
1209
};
1210

    
1211

    
1212
/**
1213
 * Pushes a marker to the clusterer.
1214
 *
1215
 * @param {google.maps.Marker} marker The marker to add.
1216
 */
1217
MarkerClusterer.prototype.pushMarkerTo_ = function (marker) {
1218
  // If the marker is draggable add a listener so we can update the clusters on the dragend:
1219
  if (marker.getDraggable()) {
1220
    var cMarkerClusterer = this;
1221
    google.maps.event.addListener(marker, "dragend", function () {
1222
      if (cMarkerClusterer.ready_) {
1223
        this.isAdded = false;
1224
        cMarkerClusterer.repaint();
1225
      }
1226
    });
1227
  }
1228
  marker.isAdded = false;
1229
  this.markers_.push(marker);
1230
};
1231

    
1232

    
1233
/**
1234
 * Removes a marker from the cluster.  The clusters are redrawn unless
1235
 *  <code>opt_nodraw</code> is set to <code>true</code>. Returns <code>true</code> if the
1236
 *  marker was removed from the clusterer.
1237
 *
1238
 * @param {google.maps.Marker} marker The marker to remove.
1239
 * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
1240
 * @return {boolean} True if the marker was removed from the clusterer.
1241
 */
1242
MarkerClusterer.prototype.removeMarker = function (marker, opt_nodraw) {
1243
  var removed = this.removeMarker_(marker);
1244

    
1245
  if (!opt_nodraw && removed) {
1246
    this.repaint();
1247
  }
1248

    
1249
  return removed;
1250
};
1251

    
1252

    
1253
/**
1254
 * Removes an array of markers from the cluster. The clusters are redrawn unless
1255
 *  <code>opt_nodraw</code> is set to <code>true</code>. Returns <code>true</code> if markers
1256
 *  were removed from the clusterer.
1257
 *
1258
 * @param {Array.<google.maps.Marker>} markers The markers to remove.
1259
 * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
1260
 * @return {boolean} True if markers were removed from the clusterer.
1261
 */
1262
MarkerClusterer.prototype.removeMarkers = function (markers, opt_nodraw) {
1263
  var i, r;
1264
  var removed = false;
1265

    
1266
  for (i = 0; i < markers.length; i++) {
1267
    r = this.removeMarker_(markers[i]);
1268
    removed = removed || r;
1269
  }
1270

    
1271
  if (!opt_nodraw && removed) {
1272
    this.repaint();
1273
  }
1274

    
1275
  return removed;
1276
};
1277

    
1278

    
1279
/**
1280
 * Removes a marker and returns true if removed, false if not.
1281
 *
1282
 * @param {google.maps.Marker} marker The marker to remove
1283
 * @return {boolean} Whether the marker was removed or not
1284
 */
1285
MarkerClusterer.prototype.removeMarker_ = function (marker) {
1286
  var i;
1287
  var index = -1;
1288
  if (this.markers_.indexOf) {
1289
    index = this.markers_.indexOf(marker);
1290
  } else {
1291
    for (i = 0; i < this.markers_.length; i++) {
1292
      if (marker === this.markers_[i]) {
1293
        index = i;
1294
        break;
1295
      }
1296
    }
1297
  }
1298

    
1299
  if (index === -1) {
1300
    // Marker is not in our list of markers, so do nothing:
1301
    return false;
1302
  }
1303

    
1304
  marker.setMap(null);
1305
  this.markers_.splice(index, 1); // Remove the marker from the list of managed markers
1306
  return true;
1307
};
1308

    
1309

    
1310
/**
1311
 * Removes all clusters and markers from the map and also removes all markers
1312
 *  managed by the clusterer.
1313
 */
1314
MarkerClusterer.prototype.clearMarkers = function () {
1315
  this.resetViewport_(true);
1316
  this.markers_ = [];
1317
};
1318

    
1319

    
1320
/**
1321
 * Recalculates and redraws all the marker clusters from scratch.
1322
 *  Call this after changing any properties.
1323
 */
1324
MarkerClusterer.prototype.repaint = function () {
1325
  var oldClusters = this.clusters_.slice();
1326
  this.clusters_ = [];
1327
  this.resetViewport_(false);
1328
  this.redraw_();
1329

    
1330
  // Remove the old clusters.
1331
  // Do it in a timeout to prevent blinking effect.
1332
  setTimeout(function () {
1333
    var i;
1334
    for (i = 0; i < oldClusters.length; i++) {
1335
      oldClusters[i].remove();
1336
    }
1337
  }, 0);
1338
};
1339

    
1340

    
1341
/**
1342
 * Returns the current bounds extended by the grid size.
1343
 *
1344
 * @param {google.maps.LatLngBounds} bounds The bounds to extend.
1345
 * @return {google.maps.LatLngBounds} The extended bounds.
1346
 * @ignore
1347
 */
1348
MarkerClusterer.prototype.getExtendedBounds = function (bounds) {
1349
  var projection = this.getProjection();
1350

    
1351
  // Turn the bounds into latlng.
1352
  var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
1353
      bounds.getNorthEast().lng());
1354
  var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
1355
      bounds.getSouthWest().lng());
1356

    
1357
  // Convert the points to pixels and the extend out by the grid size.
1358
  var trPix = projection.fromLatLngToDivPixel(tr);
1359
  trPix.x += this.gridSize_;
1360
  trPix.y -= this.gridSize_;
1361

    
1362
  var blPix = projection.fromLatLngToDivPixel(bl);
1363
  blPix.x -= this.gridSize_;
1364
  blPix.y += this.gridSize_;
1365

    
1366
  // Convert the pixel points back to LatLng
1367
  var ne = projection.fromDivPixelToLatLng(trPix);
1368
  var sw = projection.fromDivPixelToLatLng(blPix);
1369

    
1370
  // Extend the bounds to contain the new bounds.
1371
  bounds.extend(ne);
1372
  bounds.extend(sw);
1373

    
1374
  return bounds;
1375
};
1376

    
1377

    
1378
/**
1379
 * Redraws all the clusters.
1380
 */
1381
MarkerClusterer.prototype.redraw_ = function () {
1382
  this.createClusters_(0);
1383
};
1384

    
1385

    
1386
/**
1387
 * Removes all clusters from the map. The markers are also removed from the map
1388
 *  if <code>opt_hide</code> is set to <code>true</code>.
1389
 *
1390
 * @param {boolean} [opt_hide] Set to <code>true</code> to also remove the markers
1391
 *  from the map.
1392
 */
1393
MarkerClusterer.prototype.resetViewport_ = function (opt_hide) {
1394
  var i, marker;
1395
  // Remove all the clusters
1396
  for (i = 0; i < this.clusters_.length; i++) {
1397
    this.clusters_[i].remove();
1398
  }
1399
  this.clusters_ = [];
1400

    
1401
  // Reset the markers to not be added and to be removed from the map.
1402
  for (i = 0; i < this.markers_.length; i++) {
1403
    marker = this.markers_[i];
1404
    marker.isAdded = false;
1405
    if (opt_hide) {
1406
      marker.setMap(null);
1407
    }
1408
  }
1409
};
1410

    
1411

    
1412
/**
1413
 * Calculates the distance between two latlng locations in km.
1414
 *
1415
 * @param {google.maps.LatLng} p1 The first lat lng point.
1416
 * @param {google.maps.LatLng} p2 The second lat lng point.
1417
 * @return {number} The distance between the two points in km.
1418
 * @see http://www.movable-type.co.uk/scripts/latlong.html
1419
*/
1420
MarkerClusterer.prototype.distanceBetweenPoints_ = function (p1, p2) {
1421
  var R = 6371; // Radius of the Earth in km
1422
  var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1423
  var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1424
  var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1425
    Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1426
    Math.sin(dLon / 2) * Math.sin(dLon / 2);
1427
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1428
  var d = R * c;
1429
  return d;
1430
};
1431

    
1432

    
1433
/**
1434
 * Determines if a marker is contained in a bounds.
1435
 *
1436
 * @param {google.maps.Marker} marker The marker to check.
1437
 * @param {google.maps.LatLngBounds} bounds The bounds to check against.
1438
 * @return {boolean} True if the marker is in the bounds.
1439
 */
1440
MarkerClusterer.prototype.isMarkerInBounds_ = function (marker, bounds) {
1441
  return bounds.contains(marker.getPosition());
1442
};
1443

    
1444

    
1445
/**
1446
 * Adds a marker to a cluster, or creates a new cluster.
1447
 *
1448
 * @param {google.maps.Marker} marker The marker to add.
1449
 */
1450
MarkerClusterer.prototype.addToClosestCluster_ = function (marker) {
1451
  var i, d, cluster, center;
1452
  var distance = 40000; // Some large number
1453
  var clusterToAddTo = null;
1454
  for (i = 0; i < this.clusters_.length; i++) {
1455
    cluster = this.clusters_[i];
1456
    center = cluster.getCenter();
1457
    if (center) {
1458
      d = this.distanceBetweenPoints_(center, marker.getPosition());
1459
      if (d < distance) {
1460
        distance = d;
1461
        clusterToAddTo = cluster;
1462
      }
1463
    }
1464
  }
1465

    
1466
  if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
1467
    clusterToAddTo.addMarker(marker);
1468
  } else {
1469
    cluster = new Cluster(this);
1470
    cluster.addMarker(marker);
1471
    this.clusters_.push(cluster);
1472
  }
1473
};
1474

    
1475

    
1476
/**
1477
 * Creates the clusters. This is done in batches to avoid timeout errors
1478
 *  in some browsers when there is a huge number of markers.
1479
 *
1480
 * @param {number} iFirst The index of the first marker in the batch of
1481
 *  markers to be added to clusters.
1482
 */
1483
MarkerClusterer.prototype.createClusters_ = function (iFirst) {
1484
  var i, marker;
1485
  var mapBounds;
1486
  var cMarkerClusterer = this;
1487
  if (!this.ready_) {
1488
    return;
1489
  }
1490

    
1491
  // Cancel previous batch processing if we're working on the first batch:
1492
  if (iFirst === 0) {
1493
    /**
1494
     * This event is fired when the <code>MarkerClusterer</code> begins
1495
     *  clustering markers.
1496
     * @name MarkerClusterer#clusteringbegin
1497
     * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered.
1498
     * @event
1499
     */
1500
    google.maps.event.trigger(this, "clusteringbegin", this);
1501

    
1502
    if (typeof this.timerRefStatic !== "undefined") {
1503
      clearTimeout(this.timerRefStatic);
1504
      delete this.timerRefStatic;
1505
    }
1506
  }
1507

    
1508
  // Get our current map view bounds.
1509
  // Create a new bounds object so we don't affect the map.
1510
  //
1511
  // See Comments 9 & 11 on Issue 3651 relating to this workaround for a Google Maps bug:
1512
  if (this.getMap().getZoom() > 3) {
1513
    mapBounds = new google.maps.LatLngBounds(this.getMap().getBounds().getSouthWest(),
1514
      this.getMap().getBounds().getNorthEast());
1515
  } else {
1516
    mapBounds = new google.maps.LatLngBounds(new google.maps.LatLng(85.02070771743472, -178.48388434375), new google.maps.LatLng(-85.08136444384544, 178.00048865625));
1517
  }
1518
  var bounds = this.getExtendedBounds(mapBounds);
1519

    
1520
  var iLast = Math.min(iFirst + this.batchSize_, this.markers_.length);
1521

    
1522
  for (i = iFirst; i < iLast; i++) {
1523
    marker = this.markers_[i];
1524
    if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
1525
      if (!this.ignoreHidden_ || (this.ignoreHidden_ && marker.getVisible())) {
1526
        this.addToClosestCluster_(marker);
1527
      }
1528
    }
1529
  }
1530

    
1531
  if (iLast < this.markers_.length) {
1532
    this.timerRefStatic = setTimeout(function () {
1533
      cMarkerClusterer.createClusters_(iLast);
1534
    }, 0);
1535
  } else {
1536
    delete this.timerRefStatic;
1537

    
1538
    /**
1539
     * This event is fired when the <code>MarkerClusterer</code> stops
1540
     *  clustering markers.
1541
     * @name MarkerClusterer#clusteringend
1542
     * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered.
1543
     * @event
1544
     */
1545
    google.maps.event.trigger(this, "clusteringend", this);
1546
  }
1547
};
1548

    
1549

    
1550
/**
1551
 * Extends an object's prototype by another's.
1552
 *
1553
 * @param {Object} obj1 The object to be extended.
1554
 * @param {Object} obj2 The object to extend with.
1555
 * @return {Object} The new extended object.
1556
 * @ignore
1557
 */
1558
MarkerClusterer.prototype.extend = function (obj1, obj2) {
1559
  return (function (object) {
1560
    var property;
1561
    for (property in object.prototype) {
1562
      this.prototype[property] = object.prototype[property];
1563
    }
1564
    return this;
1565
  }).apply(obj1, [obj2]);
1566
};
1567

    
1568

    
1569
/**
1570
 * The default function for determining the label text and style
1571
 * for a cluster icon.
1572
 *
1573
 * @param {Array.<google.maps.Marker>} markers The array of markers represented by the cluster.
1574
 * @param {number} numStyles The number of marker styles available.
1575
 * @return {ClusterIconInfo} The information resource for the cluster.
1576
 * @constant
1577
 * @ignore
1578
 */
1579
MarkerClusterer.CALCULATOR = function (markers, numStyles) {
1580
  var index = 0;
1581
  var title = "";
1582
  var count = markers.length.toString();
1583

    
1584
  var dv = count;
1585
  while (dv !== 0) {
1586
    dv = parseInt(dv / 10, 10);
1587
    index++;
1588
  }
1589

    
1590
  index = Math.min(index, numStyles);
1591
  return {
1592
    text: count,
1593
    index: index,
1594
    title: title
1595
  };
1596
};
1597

    
1598

    
1599
/**
1600
 * The number of markers to process in one batch.
1601
 *
1602
 * @type {number}
1603
 * @constant
1604
 */
1605
MarkerClusterer.BATCH_SIZE = 2000;
1606

    
1607

    
1608
/**
1609
 * The number of markers to process in one batch (IE only).
1610
 *
1611
 * @type {number}
1612
 * @constant
1613
 */
1614
MarkerClusterer.BATCH_SIZE_IE = 500;
1615

    
1616

    
1617
/**
1618
 * The default root name for the marker cluster images.
1619
 *
1620
 * @type {string}
1621
 * @constant
1622
 */
1623
MarkerClusterer.IMAGE_PATH = "http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclustererplus/images/m";
1624

    
1625

    
1626
/**
1627
 * The default extension name for the marker cluster images.
1628
 *
1629
 * @type {string}
1630
 * @constant
1631
 */
1632
MarkerClusterer.IMAGE_EXTENSION = "png";
1633

    
1634

    
1635
/**
1636
 * The default array of sizes for the marker cluster images.
1637
 *
1638
 * @type {Array.<number>}
1639
 * @constant
1640
 */
1641
MarkerClusterer.IMAGE_SIZES = [53, 56, 66, 78, 90];