PageRenderTime 81ms CodeModel.GetById 16ms app.highlight 54ms RepoModel.GetById 1ms app.codeStats 0ms

/Artem.GoogleMap.Extensions/Scripts/markermanager.js

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