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