Home | History | Annotate | Download | only in bench
      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