1 /** 2 * @name MarkerManager v3 3 * @version 1.0 4 * @copyright (c) 2007 Google Inc. 5 * @author Doug Ricket, Bjorn Brala (port to v3), others, 6 * 7 * @fileoverview Marker manager is an interface between the map and the user, 8 * designed to manage adding and removing many points when the viewport changes. 9 * <br /><br /> 10 * <b>How it Works</b>:<br/> 11 * The MarkerManager places its markers onto a grid, similar to the map tiles. 12 * When the user moves the viewport, it computes which grid cells have 13 * entered or left the viewport, and shows or hides all the markers in those 14 * cells. 15 * (If the users scrolls the viewport beyond the markers that are loaded, 16 * no markers will be visible until the <code>EVENT_moveend</code> 17 * triggers an update.) 18 * In practical consequences, this allows 10,000 markers to be distributed over 19 * a large area, and as long as only 100-200 are visible in any given viewport, 20 * the user will see good performance corresponding to the 100 visible markers, 21 * rather than poor performance corresponding to the total 10,000 markers. 22 * Note that some code is optimized for speed over space, 23 * with the goal of accommodating thousands of markers. 24 */ 25 26 /* 27 * Licensed under the Apache License, Version 2.0 (the "License"); 28 * you may not use this file except in compliance with the License. 29 * You may obtain a copy of the License at 30 * 31 * http://www.apache.org/licenses/LICENSE-2.0 32 * 33 * Unless required by applicable law or agreed to in writing, software 34 * distributed under the License is distributed on an "AS IS" BASIS, 35 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 36 * See the License for the specific language governing permissions and 37 * limitations under the License. 38 */ 39 40 /** 41 * @name MarkerManagerOptions 42 * @class This class represents optional arguments to the {@link MarkerManager} 43 * constructor. 44 * @property {Number} maxZoom Sets the maximum zoom level monitored by a 45 * marker manager. If not given, the manager assumes the maximum map zoom 46 * level. This value is also used when markers are added to the manager 47 * without the optional {@link maxZoom} parameter. 48 * @property {Number} borderPadding Specifies, in pixels, the extra padding 49 * outside the map's current viewport monitored by a manager. Markers that 50 * fall within this padding are added to the map, even if they are not fully 51 * visible. 52 * @property {Boolean} trackMarkers=false Indicates whether or not a marker 53 * manager should track markers' movements. If you wish to move managed 54 * markers using the {@link setPoint}/{@link setLatLng} methods, 55 * this option should be set to {@link true}. 56 */ 57 58 /** 59 * Creates a new MarkerManager that will show/hide markers on a map. 60 * 61 * Events: 62 * @event changed (Parameters: shown bounds, shown markers) Notify listeners when the state of what is displayed changes. 63 * @event loaded MarkerManager has succesfully been initialized. 64 * 65 * @constructor 66 * @param {Map} map The map to manage. 67 * @param {Object} opt_opts A container for optional arguments: 68 * {Number} maxZoom The maximum zoom level for which to create tiles. 69 * {Number} borderPadding The width in pixels beyond the map border, 70 * where markers should be display. 71 * {Boolean} trackMarkers Whether or not this manager should track marker 72 * movements. 73 */ 74 function MarkerManager(map, opt_opts) { 75 var me = this; 76 me.map_ = map; 77 me.mapZoom_ = map.getZoom(); 78 79 me.projectionHelper_ = new ProjectionHelperOverlay(map); 80 google.maps.event.addListener(me.projectionHelper_, 'ready', function () { 81 me.projection_ = this.getProjection(); 82 me.initialize(map, opt_opts); 83 }); 84 } 85 86 87 MarkerManager.prototype.initialize = function (map, opt_opts) { 88 var me = this; 89 90 opt_opts = opt_opts || {}; 91 me.tileSize_ = MarkerManager.DEFAULT_TILE_SIZE_; 92 93 var mapTypes = map.mapTypes; 94 95 // Find max zoom level 96 var mapMaxZoom = 1; 97 for (var sType in mapTypes ) { 98 if (typeof map.mapTypes.get(sType) === 'object' && typeof map.mapTypes.get(sType).maxZoom === 'number') { 99 var mapTypeMaxZoom = map.mapTypes.get(sType).maxZoom; 100 if (mapTypeMaxZoom > mapMaxZoom) { 101 mapMaxZoom = mapTypeMaxZoom; 102 } 103 } 104 } 105 106 me.maxZoom_ = opt_opts.maxZoom || 19; 107 108 me.trackMarkers_ = opt_opts.trackMarkers; 109 me.show_ = opt_opts.show || true; 110 111 var padding; 112 if (typeof opt_opts.borderPadding === 'number') { 113 padding = opt_opts.borderPadding; 114 } else { 115 padding = MarkerManager.DEFAULT_BORDER_PADDING_; 116 } 117 // The padding in pixels beyond the viewport, where we will pre-load markers. 118 me.swPadding_ = new google.maps.Size(-padding, padding); 119 me.nePadding_ = new google.maps.Size(padding, -padding); 120 me.borderPadding_ = padding; 121 122 me.gridWidth_ = {}; 123 124 me.grid_ = {}; 125 me.grid_[me.maxZoom_] = {}; 126 me.numMarkers_ = {}; 127 me.numMarkers_[me.maxZoom_] = 0; 128 129 130 google.maps.event.addListener(map, 'dragend', function () { 131 me.onMapMoveEnd_(); 132 }); 133 google.maps.event.addListener(map, 'zoom_changed', function () { 134 me.onMapMoveEnd_(); 135 }); 136 137 138 139 /** 140 * This closure provide easy access to the map. 141 * They are used as callbacks, not as methods. 142 * @param GMarker marker Marker to be removed from the map 143 * @private 144 */ 145 me.removeOverlay_ = function (marker) { 146 marker.setMap(null); 147 me.shownMarkers_--; 148 }; 149 150 /** 151 * This closure provide easy access to the map. 152 * They are used as callbacks, not as methods. 153 * @param GMarker marker Marker to be added to the map 154 * @private 155 */ 156 me.addOverlay_ = function (marker) { 157 if (me.show_) { 158 marker.setMap(me.map_); 159 me.shownMarkers_++; 160 } 161 }; 162 163 me.resetManager_(); 164 me.shownMarkers_ = 0; 165 166 me.shownBounds_ = me.getMapGridBounds_(); 167 168 google.maps.event.trigger(me, 'loaded'); 169 170 }; 171 172 /** 173 * Default tile size used for deviding the map into a grid. 174 */ 175 MarkerManager.DEFAULT_TILE_SIZE_ = 1024; 176 177 /* 178 * How much extra space to show around the map border so 179 * dragging doesn't result in an empty place. 180 */ 181 MarkerManager.DEFAULT_BORDER_PADDING_ = 100; 182 183 /** 184 * Default tilesize of single tile world. 185 */ 186 MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE = 256; 187 188 189 /** 190 * Initializes MarkerManager arrays for all zoom levels 191 * Called by constructor and by clearAllMarkers 192 */ 193 MarkerManager.prototype.resetManager_ = function () { 194 var mapWidth = MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE; 195 for (var zoom = 0; zoom <= this.maxZoom_; ++zoom) { 196 this.grid_[zoom] = {}; 197 this.numMarkers_[zoom] = 0; 198 this.gridWidth_[zoom] = Math.ceil(mapWidth / this.tileSize_); 199 mapWidth <<= 1; 200 } 201 202 }; 203 204 /** 205 * Removes all markers in the manager, and 206 * removes any visible markers from the map. 207 */ 208 MarkerManager.prototype.clearMarkers = function () { 209 this.processAll_(this.shownBounds_, this.removeOverlay_); 210 this.resetManager_(); 211 }; 212 213 214 /** 215 * Gets the tile coordinate for a given latlng point. 216 * 217 * @param {LatLng} latlng The geographical point. 218 * @param {Number} zoom The zoom level. 219 * @param {google.maps.Size} padding The padding used to shift the pixel coordinate. 220 * Used for expanding a bounds to include an extra padding 221 * of pixels surrounding the bounds. 222 * @return {GPoint} The point in tile coordinates. 223 * 224 */ 225 MarkerManager.prototype.getTilePoint_ = function (latlng, zoom, padding) { 226 227 var pixelPoint = this.projectionHelper_.LatLngToPixel(latlng, zoom); 228 229 var point = new google.maps.Point( 230 Math.floor((pixelPoint.x + padding.width) / this.tileSize_), 231 Math.floor((pixelPoint.y + padding.height) / this.tileSize_) 232 ); 233 234 return point; 235 }; 236 237 238 /** 239 * Finds the appropriate place to add the marker to the grid. 240 * Optimized for speed; does not actually add the marker to the map. 241 * Designed for batch-processing thousands of markers. 242 * 243 * @param {Marker} marker The marker to add. 244 * @param {Number} minZoom The minimum zoom for displaying the marker. 245 * @param {Number} maxZoom The maximum zoom for displaying the marker. 246 */ 247 MarkerManager.prototype.addMarkerBatch_ = function (marker, minZoom, maxZoom) { 248 var me = this; 249 250 var mPoint = marker.getPosition(); 251 marker.MarkerManager_minZoom = minZoom; 252 253 254 // Tracking markers is expensive, so we do this only if the 255 // user explicitly requested it when creating marker manager. 256 if (this.trackMarkers_) { 257 google.maps.event.addListener(marker, 'changed', function (a, b, c) { 258 me.onMarkerMoved_(a, b, c); 259 }); 260 } 261 262 var gridPoint = this.getTilePoint_(mPoint, maxZoom, new google.maps.Size(0, 0, 0, 0)); 263 264 for (var zoom = maxZoom; zoom >= minZoom; zoom--) { 265 var cell = this.getGridCellCreate_(gridPoint.x, gridPoint.y, zoom); 266 cell.push(marker); 267 268 gridPoint.x = gridPoint.x >> 1; 269 gridPoint.y = gridPoint.y >> 1; 270 } 271 }; 272 273 274 /** 275 * Returns whether or not the given point is visible in the shown bounds. This 276 * is a helper method that takes care of the corner case, when shownBounds have 277 * negative minX value. 278 * 279 * @param {Point} point a point on a grid. 280 * @return {Boolean} Whether or not the given point is visible in the currently 281 * shown bounds. 282 */ 283 MarkerManager.prototype.isGridPointVisible_ = function (point) { 284 var vertical = this.shownBounds_.minY <= point.y && 285 point.y <= this.shownBounds_.maxY; 286 var minX = this.shownBounds_.minX; 287 var horizontal = minX <= point.x && point.x <= this.shownBounds_.maxX; 288 if (!horizontal && minX < 0) { 289 // Shifts the negative part of the rectangle. As point.x is always less 290 // than grid width, only test shifted minX .. 0 part of the shown bounds. 291 var width = this.gridWidth_[this.shownBounds_.z]; 292 horizontal = minX + width <= point.x && point.x <= width - 1; 293 } 294 return vertical && horizontal; 295 }; 296 297 298 /** 299 * Reacts to a notification from a marker that it has moved to a new location. 300 * It scans the grid all all zoom levels and moves the marker from the old grid 301 * location to a new grid location. 302 * 303 * @param {Marker} marker The marker that moved. 304 * @param {LatLng} oldPoint The old position of the marker. 305 * @param {LatLng} newPoint The new position of the marker. 306 */ 307 MarkerManager.prototype.onMarkerMoved_ = function (marker, oldPoint, newPoint) { 308 // NOTE: We do not know the minimum or maximum zoom the marker was 309 // added at, so we start at the absolute maximum. Whenever we successfully 310 // remove a marker at a given zoom, we add it at the new grid coordinates. 311 var zoom = this.maxZoom_; 312 var changed = false; 313 var oldGrid = this.getTilePoint_(oldPoint, zoom, new google.maps.Size(0, 0, 0, 0)); 314 var newGrid = this.getTilePoint_(newPoint, zoom, new google.maps.Size(0, 0, 0, 0)); 315 while (zoom >= 0 && (oldGrid.x !== newGrid.x || oldGrid.y !== newGrid.y)) { 316 var cell = this.getGridCellNoCreate_(oldGrid.x, oldGrid.y, zoom); 317 if (cell) { 318 if (this.removeFromArray_(cell, marker)) { 319 this.getGridCellCreate_(newGrid.x, newGrid.y, zoom).push(marker); 320 } 321 } 322 // For the current zoom we also need to update the map. Markers that no 323 // longer are visible are removed from the map. Markers that moved into 324 // the shown bounds are added to the map. This also lets us keep the count 325 // of visible markers up to date. 326 if (zoom === this.mapZoom_) { 327 if (this.isGridPointVisible_(oldGrid)) { 328 if (!this.isGridPointVisible_(newGrid)) { 329 this.removeOverlay_(marker); 330 changed = true; 331 } 332 } else { 333 if (this.isGridPointVisible_(newGrid)) { 334 this.addOverlay_(marker); 335 changed = true; 336 } 337 } 338 } 339 oldGrid.x = oldGrid.x >> 1; 340 oldGrid.y = oldGrid.y >> 1; 341 newGrid.x = newGrid.x >> 1; 342 newGrid.y = newGrid.y >> 1; 343 --zoom; 344 } 345 if (changed) { 346 this.notifyListeners_(); 347 } 348 }; 349 350 351 /** 352 * Removes marker from the manager and from the map 353 * (if it's currently visible). 354 * @param {GMarker} marker The marker to delete. 355 */ 356 MarkerManager.prototype.removeMarker = function (marker) { 357 var zoom = this.maxZoom_; 358 var changed = false; 359 var point = marker.getPosition(); 360 var grid = this.getTilePoint_(point, zoom, new google.maps.Size(0, 0, 0, 0)); 361 while (zoom >= 0) { 362 var cell = this.getGridCellNoCreate_(grid.x, grid.y, zoom); 363 364 if (cell) { 365 this.removeFromArray_(cell, marker); 366 } 367 // For the current zoom we also need to update the map. Markers that no 368 // longer are visible are removed from the map. This also lets us keep the count 369 // of visible markers up to date. 370 if (zoom === this.mapZoom_) { 371 if (this.isGridPointVisible_(grid)) { 372 this.removeOverlay_(marker); 373 changed = true; 374 } 375 } 376 grid.x = grid.x >> 1; 377 grid.y = grid.y >> 1; 378 --zoom; 379 } 380 if (changed) { 381 this.notifyListeners_(); 382 } 383 this.numMarkers_[marker.MarkerManager_minZoom]--; 384 }; 385 386 387 /** 388 * Add many markers at once. 389 * Does not actually update the map, just the internal grid. 390 * 391 * @param {Array of Marker} markers The markers to add. 392 * @param {Number} minZoom The minimum zoom level to display the markers. 393 * @param {Number} opt_maxZoom The maximum zoom level to display the markers. 394 */ 395 MarkerManager.prototype.addMarkers = function (markers, minZoom, opt_maxZoom) { 396 var maxZoom = this.getOptMaxZoom_(opt_maxZoom); 397 for (var i = markers.length - 1; i >= 0; i--) { 398 this.addMarkerBatch_(markers[i], minZoom, maxZoom); 399 } 400 401 this.numMarkers_[minZoom] += markers.length; 402 }; 403 404 405 /** 406 * Returns the value of the optional maximum zoom. This method is defined so 407 * that we have just one place where optional maximum zoom is calculated. 408 * 409 * @param {Number} opt_maxZoom The optinal maximum zoom. 410 * @return The maximum zoom. 411 */ 412 MarkerManager.prototype.getOptMaxZoom_ = function (opt_maxZoom) { 413 return opt_maxZoom || this.maxZoom_; 414 }; 415 416 417 /** 418 * Calculates the total number of markers potentially visible at a given 419 * zoom level. 420 * 421 * @param {Number} zoom The zoom level to check. 422 */ 423 MarkerManager.prototype.getMarkerCount = function (zoom) { 424 var total = 0; 425 for (var z = 0; z <= zoom; z++) { 426 total += this.numMarkers_[z]; 427 } 428 return total; 429 }; 430 431 /** 432 * Returns a marker given latitude, longitude and zoom. If the marker does not 433 * exist, the method will return a new marker. If a new marker is created, 434 * it will NOT be added to the manager. 435 * 436 * @param {Number} lat - the latitude of a marker. 437 * @param {Number} lng - the longitude of a marker. 438 * @param {Number} zoom - the zoom level 439 * @return {GMarker} marker - the marker found at lat and lng 440 */ 441 MarkerManager.prototype.getMarker = function (lat, lng, zoom) { 442 var mPoint = new google.maps.LatLng(lat, lng); 443 var gridPoint = this.getTilePoint_(mPoint, zoom, new google.maps.Size(0, 0, 0, 0)); 444 445 var marker = new google.maps.Marker({position: mPoint}); 446 447 var cellArray = this.getGridCellNoCreate_(gridPoint.x, gridPoint.y, zoom); 448 if (cellArray !== undefined) { 449 for (var i = 0; i < cellArray.length; i++) 450 { 451 if (lat === cellArray[i].getLatLng().lat() && lng === cellArray[i].getLatLng().lng()) { 452 marker = cellArray[i]; 453 } 454 } 455 } 456 return marker; 457 }; 458 459 /** 460 * Add a single marker to the map. 461 * 462 * @param {Marker} marker The marker to add. 463 * @param {Number} minZoom The minimum zoom level to display the marker. 464 * @param {Number} opt_maxZoom The maximum zoom level to display the marker. 465 */ 466 MarkerManager.prototype.addMarker = function (marker, minZoom, opt_maxZoom) { 467 var maxZoom = this.getOptMaxZoom_(opt_maxZoom); 468 this.addMarkerBatch_(marker, minZoom, maxZoom); 469 var gridPoint = this.getTilePoint_(marker.getPosition(), this.mapZoom_, new google.maps.Size(0, 0, 0, 0)); 470 if (this.isGridPointVisible_(gridPoint) && 471 minZoom <= this.shownBounds_.z && 472 this.shownBounds_.z <= maxZoom) { 473 this.addOverlay_(marker); 474 this.notifyListeners_(); 475 } 476 this.numMarkers_[minZoom]++; 477 }; 478 479 480 /** 481 * Helper class to create a bounds of INT ranges. 482 * @param bounds Array.<Object.<string, number>> Bounds object. 483 * @constructor 484 */ 485 function GridBounds(bounds) { 486 // [sw, ne] 487 488 this.minX = Math.min(bounds[0].x, bounds[1].x); 489 this.maxX = Math.max(bounds[0].x, bounds[1].x); 490 this.minY = Math.min(bounds[0].y, bounds[1].y); 491 this.maxY = Math.max(bounds[0].y, bounds[1].y); 492 493 } 494 495 /** 496 * Returns true if this bounds equal the given bounds. 497 * @param {GridBounds} gridBounds GridBounds The bounds to test. 498 * @return {Boolean} This Bounds equals the given GridBounds. 499 */ 500 GridBounds.prototype.equals = function (gridBounds) { 501 if (this.maxX === gridBounds.maxX && this.maxY === gridBounds.maxY && this.minX === gridBounds.minX && this.minY === gridBounds.minY) { 502 return true; 503 } else { 504 return false; 505 } 506 }; 507 508 /** 509 * Returns true if this bounds (inclusively) contains the given point. 510 * @param {Point} point The point to test. 511 * @return {Boolean} This Bounds contains the given Point. 512 */ 513 GridBounds.prototype.containsPoint = function (point) { 514 var outer = this; 515 return (outer.minX <= point.x && outer.maxX >= point.x && outer.minY <= point.y && outer.maxY >= point.y); 516 }; 517 518 /** 519 * Get a cell in the grid, creating it first if necessary. 520 * 521 * Optimization candidate 522 * 523 * @param {Number} x The x coordinate of the cell. 524 * @param {Number} y The y coordinate of the cell. 525 * @param {Number} z The z coordinate of the cell. 526 * @return {Array} The cell in the array. 527 */ 528 MarkerManager.prototype.getGridCellCreate_ = function (x, y, z) { 529 var grid = this.grid_[z]; 530 if (x < 0) { 531 x += this.gridWidth_[z]; 532 } 533 var gridCol = grid[x]; 534 if (!gridCol) { 535 gridCol = grid[x] = []; 536 return (gridCol[y] = []); 537 } 538 var gridCell = gridCol[y]; 539 if (!gridCell) { 540 return (gridCol[y] = []); 541 } 542 return gridCell; 543 }; 544 545 546 /** 547 * Get a cell in the grid, returning undefined if it does not exist. 548 * 549 * NOTE: Optimized for speed -- otherwise could combine with getGridCellCreate_. 550 * 551 * @param {Number} x The x coordinate of the cell. 552 * @param {Number} y The y coordinate of the cell. 553 * @param {Number} z The z coordinate of the cell. 554 * @return {Array} The cell in the array. 555 */ 556 MarkerManager.prototype.getGridCellNoCreate_ = function (x, y, z) { 557 var grid = this.grid_[z]; 558 559 if (x < 0) { 560 x += this.gridWidth_[z]; 561 } 562 var gridCol = grid[x]; 563 return gridCol ? gridCol[y] : undefined; 564 }; 565 566 567 /** 568 * Turns at geographical bounds into a grid-space bounds. 569 * 570 * @param {LatLngBounds} bounds The geographical bounds. 571 * @param {Number} zoom The zoom level of the bounds. 572 * @param {google.maps.Size} swPadding The padding in pixels to extend beyond the 573 * given bounds. 574 * @param {google.maps.Size} nePadding The padding in pixels to extend beyond the 575 * given bounds. 576 * @return {GridBounds} The bounds in grid space. 577 */ 578 MarkerManager.prototype.getGridBounds_ = function (bounds, zoom, swPadding, nePadding) { 579 zoom = Math.min(zoom, this.maxZoom_); 580 581 var bl = bounds.getSouthWest(); 582 var tr = bounds.getNorthEast(); 583 var sw = this.getTilePoint_(bl, zoom, swPadding); 584 585 var ne = this.getTilePoint_(tr, zoom, nePadding); 586 var gw = this.gridWidth_[zoom]; 587 588 // Crossing the prime meridian requires correction of bounds. 589 if (tr.lng() < bl.lng() || ne.x < sw.x) { 590 sw.x -= gw; 591 } 592 if (ne.x - sw.x + 1 >= gw) { 593 // Computed grid bounds are larger than the world; truncate. 594 sw.x = 0; 595 ne.x = gw - 1; 596 } 597 598 var gridBounds = new GridBounds([sw, ne]); 599 gridBounds.z = zoom; 600 601 return gridBounds; 602 }; 603 604 605 /** 606 * Gets the grid-space bounds for the current map viewport. 607 * 608 * @return {Bounds} The bounds in grid space. 609 */ 610 MarkerManager.prototype.getMapGridBounds_ = function () { 611 return this.getGridBounds_(this.map_.getBounds(), this.mapZoom_, this.swPadding_, this.nePadding_); 612 }; 613 614 615 /** 616 * Event listener for map:movend. 617 * NOTE: Use a timeout so that the user is not blocked 618 * from moving the map. 619 * 620 * Removed this because a a lack of a scopy override/callback function on events. 621 */ 622 MarkerManager.prototype.onMapMoveEnd_ = function () { 623 this.objectSetTimeout_(this, this.updateMarkers_, 0); 624 }; 625 626 627 /** 628 * Call a function or evaluate an expression after a specified number of 629 * milliseconds. 630 * 631 * Equivalent to the standard window.setTimeout function, but the given 632 * function executes as a method of this instance. So the function passed to 633 * objectSetTimeout can contain references to this. 634 * objectSetTimeout(this, function () { alert(this.x) }, 1000); 635 * 636 * @param {Object} object The target object. 637 * @param {Function} command The command to run. 638 * @param {Number} milliseconds The delay. 639 * @return {Boolean} Success. 640 */ 641 MarkerManager.prototype.objectSetTimeout_ = function (object, command, milliseconds) { 642 return window.setTimeout(function () { 643 command.call(object); 644 }, milliseconds); 645 }; 646 647 648 /** 649 * Is this layer visible? 650 * 651 * Returns visibility setting 652 * 653 * @return {Boolean} Visible 654 */ 655 MarkerManager.prototype.visible = function () { 656 return this.show_ ? true : false; 657 }; 658 659 660 /** 661 * Returns true if the manager is hidden. 662 * Otherwise returns false. 663 * @return {Boolean} Hidden 664 */ 665 MarkerManager.prototype.isHidden = function () { 666 return !this.show_; 667 }; 668 669 670 /** 671 * Shows the manager if it's currently hidden. 672 */ 673 MarkerManager.prototype.show = function () { 674 this.show_ = true; 675 this.refresh(); 676 }; 677 678 679 /** 680 * Hides the manager if it's currently visible 681 */ 682 MarkerManager.prototype.hide = function () { 683 this.show_ = false; 684 this.refresh(); 685 }; 686 687 688 /** 689 * Toggles the visibility of the manager. 690 */ 691 MarkerManager.prototype.toggle = function () { 692 this.show_ = !this.show_; 693 this.refresh(); 694 }; 695 696 697 /** 698 * Refresh forces the marker-manager into a good state. 699 * <ol> 700 * <li>If never before initialized, shows all the markers.</li> 701 * <li>If previously initialized, removes and re-adds all markers.</li> 702 * </ol> 703 */ 704 MarkerManager.prototype.refresh = function () { 705 if (this.shownMarkers_ > 0) { 706 this.processAll_(this.shownBounds_, this.removeOverlay_); 707 } 708 // An extra check on this.show_ to increase performance (no need to processAll_) 709 if (this.show_) { 710 this.processAll_(this.shownBounds_, this.addOverlay_); 711 } 712 this.notifyListeners_(); 713 }; 714 715 716 /** 717 * After the viewport may have changed, add or remove markers as needed. 718 */ 719 MarkerManager.prototype.updateMarkers_ = function () { 720 this.mapZoom_ = this.map_.getZoom(); 721 var newBounds = this.getMapGridBounds_(); 722 723 // If the move does not include new grid sections, 724 // we have no work to do: 725 if (newBounds.equals(this.shownBounds_) && newBounds.z === this.shownBounds_.z) { 726 return; 727 } 728 729 if (newBounds.z !== this.shownBounds_.z) { 730 this.processAll_(this.shownBounds_, this.removeOverlay_); 731 if (this.show_) { // performance 732 this.processAll_(newBounds, this.addOverlay_); 733 } 734 } else { 735 // Remove markers: 736 this.rectangleDiff_(this.shownBounds_, newBounds, this.removeCellMarkers_); 737 738 // Add markers: 739 if (this.show_) { // performance 740 this.rectangleDiff_(newBounds, this.shownBounds_, this.addCellMarkers_); 741 } 742 } 743 this.shownBounds_ = newBounds; 744 745 this.notifyListeners_(); 746 }; 747 748 749 /** 750 * Notify listeners when the state of what is displayed changes. 751 */ 752 MarkerManager.prototype.notifyListeners_ = function () { 753 google.maps.event.trigger(this, 'changed', this.shownBounds_, this.shownMarkers_); 754 }; 755 756 757 /** 758 * Process all markers in the bounds provided, using a callback. 759 * 760 * @param {Bounds} bounds The bounds in grid space. 761 * @param {Function} callback The function to call for each marker. 762 */ 763 MarkerManager.prototype.processAll_ = function (bounds, callback) { 764 for (var x = bounds.minX; x <= bounds.maxX; x++) { 765 for (var y = bounds.minY; y <= bounds.maxY; y++) { 766 this.processCellMarkers_(x, y, bounds.z, callback); 767 } 768 } 769 }; 770 771 772 /** 773 * Process all markers in the grid cell, using a callback. 774 * 775 * @param {Number} x The x coordinate of the cell. 776 * @param {Number} y The y coordinate of the cell. 777 * @param {Number} z The z coordinate of the cell. 778 * @param {Function} callback The function to call for each marker. 779 */ 780 MarkerManager.prototype.processCellMarkers_ = function (x, y, z, callback) { 781 var cell = this.getGridCellNoCreate_(x, y, z); 782 if (cell) { 783 for (var i = cell.length - 1; i >= 0; i--) { 784 callback(cell[i]); 785 } 786 } 787 }; 788 789 790 /** 791 * Remove all markers in a grid cell. 792 * 793 * @param {Number} x The x coordinate of the cell. 794 * @param {Number} y The y coordinate of the cell. 795 * @param {Number} z The z coordinate of the cell. 796 */ 797 MarkerManager.prototype.removeCellMarkers_ = function (x, y, z) { 798 this.processCellMarkers_(x, y, z, this.removeOverlay_); 799 }; 800 801 802 /** 803 * Add all markers in a grid cell. 804 * 805 * @param {Number} x The x coordinate of the cell. 806 * @param {Number} y The y coordinate of the cell. 807 * @param {Number} z The z coordinate of the cell. 808 */ 809 MarkerManager.prototype.addCellMarkers_ = function (x, y, z) { 810 this.processCellMarkers_(x, y, z, this.addOverlay_); 811 }; 812 813 814 /** 815 * Use the rectangleDiffCoords_ function to process all grid cells 816 * that are in bounds1 but not bounds2, using a callback, and using 817 * the current MarkerManager object as the instance. 818 * 819 * Pass the z parameter to the callback in addition to x and y. 820 * 821 * @param {Bounds} bounds1 The bounds of all points we may process. 822 * @param {Bounds} bounds2 The bounds of points to exclude. 823 * @param {Function} callback The callback function to call 824 * for each grid coordinate (x, y, z). 825 */ 826 MarkerManager.prototype.rectangleDiff_ = function (bounds1, bounds2, callback) { 827 var me = this; 828 me.rectangleDiffCoords_(bounds1, bounds2, function (x, y) { 829 callback.apply(me, [x, y, bounds1.z]); 830 }); 831 }; 832 833 834 /** 835 * Calls the function for all points in bounds1, not in bounds2 836 * 837 * @param {Bounds} bounds1 The bounds of all points we may process. 838 * @param {Bounds} bounds2 The bounds of points to exclude. 839 * @param {Function} callback The callback function to call 840 * for each grid coordinate. 841 */ 842 MarkerManager.prototype.rectangleDiffCoords_ = function (bounds1, bounds2, callback) { 843 var minX1 = bounds1.minX; 844 var minY1 = bounds1.minY; 845 var maxX1 = bounds1.maxX; 846 var maxY1 = bounds1.maxY; 847 var minX2 = bounds2.minX; 848 var minY2 = bounds2.minY; 849 var maxX2 = bounds2.maxX; 850 var maxY2 = bounds2.maxY; 851 852 var x, y; 853 for (x = minX1; x <= maxX1; x++) { // All x in R1 854 // All above: 855 for (y = minY1; y <= maxY1 && y < minY2; y++) { // y in R1 above R2 856 callback(x, y); 857 } 858 // All below: 859 for (y = Math.max(maxY2 + 1, minY1); // y in R1 below R2 860 y <= maxY1; y++) { 861 callback(x, y); 862 } 863 } 864 865 for (y = Math.max(minY1, minY2); 866 y <= Math.min(maxY1, maxY2); y++) { // All y in R2 and in R1 867 // Strictly left: 868 for (x = Math.min(maxX1 + 1, minX2) - 1; 869 x >= minX1; x--) { // x in R1 left of R2 870 callback(x, y); 871 } 872 // Strictly right: 873 for (x = Math.max(minX1, maxX2 + 1); // x in R1 right of R2 874 x <= maxX1; x++) { 875 callback(x, y); 876 } 877 } 878 }; 879 880 881 /** 882 * Removes value from array. O(N). 883 * 884 * @param {Array} array The array to modify. 885 * @param {any} value The value to remove. 886 * @param {Boolean} opt_notype Flag to disable type checking in equality. 887 * @return {Number} The number of instances of value that were removed. 888 */ 889 MarkerManager.prototype.removeFromArray_ = function (array, value, opt_notype) { 890 var shift = 0; 891 for (var i = 0; i < array.length; ++i) { 892 if (array[i] === value || (opt_notype && array[i] === value)) { 893 array.splice(i--, 1); 894 shift++; 895 } 896 } 897 return shift; 898 }; 899 900 901 902 903 904 905 906 /** 907 * Projection overlay helper. Helps in calculating 908 * that markers get into the right grid. 909 * @constructor 910 * @param {Map} map The map to manage. 911 **/ 912 function ProjectionHelperOverlay(map) { 913 914 this.setMap(map); 915 916 var TILEFACTOR = 8; 917 var TILESIDE = 1 << TILEFACTOR; 918 var RADIUS = 7; 919 920 this._map = map; 921 this._zoom = -1; 922 this._X0 = 923 this._Y0 = 924 this._X1 = 925 this._Y1 = -1; 926 927 928 } 929 if (typeof(google) != 'undefined' && google.maps) { // make sure it exists -- amalo 930 ProjectionHelperOverlay.prototype = new google.maps.OverlayView(); 931 } 932 933 /** 934 * Helper function to convert Lng to X 935 * @private 936 * @param {float} lng 937 **/ 938 ProjectionHelperOverlay.prototype.LngToX_ = function (lng) { 939 return (1 + lng / 180); 940 }; 941 942 /** 943 * Helper function to convert Lat to Y 944 * @private 945 * @param {float} lat 946 **/ 947 ProjectionHelperOverlay.prototype.LatToY_ = function (lat) { 948 var sinofphi = Math.sin(lat * Math.PI / 180); 949 return (1 - 0.5 / Math.PI * Math.log((1 + sinofphi) / (1 - sinofphi))); 950 }; 951 952 /** 953 * Old school LatLngToPixel 954 * @param {LatLng} latlng google.maps.LatLng object 955 * @param {Number} zoom Zoom level 956 * @return {position} {x: pixelPositionX, y: pixelPositionY} 957 **/ 958 ProjectionHelperOverlay.prototype.LatLngToPixel = function (latlng, zoom) { 959 var map = this._map; 960 var div = this.getProjection().fromLatLngToDivPixel(latlng); 961 var abs = {x: ~~(0.5 + this.LngToX_(latlng.lng()) * (2 << (zoom + 6))), y: ~~(0.5 + this.LatToY_(latlng.lat()) * (2 << (zoom + 6)))}; 962 return abs; 963 }; 964 965 966 /** 967 * Draw function only triggers a ready event for 968 * MarkerManager to know projection can proceed to 969 * initialize. 970 */ 971 ProjectionHelperOverlay.prototype.draw = function () { 972 if (!this.ready) { 973 this.ready = true; 974 google.maps.event.trigger(this, 'ready'); 975 } 976 }; 977