Subversion Repositories wpShopGermany4

Rev

Details | Last modification | View Log | RSS feed

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