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