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