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