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