markerclustererplus-rails 2.0.0

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