• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
74function 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
87MarkerManager.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 */
175MarkerManager.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 */
181MarkerManager.DEFAULT_BORDER_PADDING_ = 100;
182
183/**
184 *  Default tilesize of single tile world.
185 */
186MarkerManager.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 */
193MarkerManager.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 */
208MarkerManager.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 */
225MarkerManager.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 */
247MarkerManager.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 */
283MarkerManager.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 */
307MarkerManager.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 */
356MarkerManager.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 */
395MarkerManager.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 */
412MarkerManager.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 */
423MarkerManager.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 */
441MarkerManager.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 */
466MarkerManager.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 */
485function 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 */
500GridBounds.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 */
513GridBounds.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 */
528MarkerManager.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 */
556MarkerManager.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 */
578MarkerManager.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 */
610MarkerManager.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 */
622MarkerManager.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 */
641MarkerManager.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 */
655MarkerManager.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 */
665MarkerManager.prototype.isHidden = function () {
666  return !this.show_;
667};
668
669
670/**
671 * Shows the manager if it's currently hidden.
672 */
673MarkerManager.prototype.show = function () {
674  this.show_ = true;
675  this.refresh();
676};
677
678
679/**
680 * Hides the manager if it's currently visible
681 */
682MarkerManager.prototype.hide = function () {
683  this.show_ = false;
684  this.refresh();
685};
686
687
688/**
689 * Toggles the visibility of the manager.
690 */
691MarkerManager.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 */
704MarkerManager.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 */
719MarkerManager.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 */
752MarkerManager.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 */
763MarkerManager.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 */
780MarkerManager.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 */
797MarkerManager.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 */
809MarkerManager.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 */
826MarkerManager.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 */
842MarkerManager.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 */
889MarkerManager.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**/
912function 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}
929if (typeof(google) != 'undefined' && google.maps) { // make sure it exists -- amalo
930ProjectionHelperOverlay.prototype = new google.maps.OverlayView();
931}
932
933/**
934 *  Helper function to convert Lng to X
935 *  @private
936 *  @param {float} lng
937 **/
938ProjectionHelperOverlay.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 **/
947ProjectionHelperOverlay.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**/
958ProjectionHelperOverlay.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 */
971ProjectionHelperOverlay.prototype.draw = function () {
972  if (!this.ready) {
973    this.ready = true;
974    google.maps.event.trigger(this, 'ready');
975  }
976};
977