/**
 * @name MarkerManager v3
 * @version 1.0
 * @copyright (c) 2007 Google Inc.
 * @author Doug Ricket, Bjorn Brala (port to v3), others,
 *
 * @fileoverview Marker manager is an interface between the map and the user,
 * designed to manage adding and removing many points when the viewport changes.
 * <br /><br />
 * <b>How it Works</b>:<br/> 
 * The MarkerManager places its markers onto a grid, similar to the map tiles.
 * When the user moves the viewport, it computes which grid cells have
 * entered or left the viewport, and shows or hides all the markers in those
 * cells.
 * (If the users scrolls the viewport beyond the markers that are loaded,
 * no markers will be visible until the <code>EVENT_moveend</code> 
 * triggers an update.)
 * In practical consequences, this allows 10,000 markers to be distributed over
 * a large area, and as long as only 100-200 are visible in any given viewport,
 * the user will see good performance corresponding to the 100 visible markers,
 * rather than poor performance corresponding to the total 10,000 markers.
 * Note that some code is optimized for speed over space,
 * with the goal of accommodating thousands of markers.
 */

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */

/**
 * @name MarkerManagerOptions
 * @class This class represents optional arguments to the {@link MarkerManager}
 *     constructor.
 * @property {Number} maxZoom Sets the maximum zoom level monitored by a
 *     marker manager. If not given, the manager assumes the maximum map zoom
 *     level. This value is also used when markers are added to the manager
 *     without the optional {@link maxZoom} parameter.
 * @property {Number} borderPadding Specifies, in pixels, the extra padding
 *     outside the map's current viewport monitored by a manager. Markers that
 *     fall within this padding are added to the map, even if they are not fully
 *     visible.
 * @property {Boolean} trackMarkers=false Indicates whether or not a marker
 *     manager should track markers' movements. If you wish to move managed
 *     markers using the {@link setPoint}/{@link setLatLng} methods, 
 *     this option should be set to {@link true}.
 */

/**
 * Creates a new MarkerManager that will show/hide markers on a map.
 *
 * Events:
 * @event changed (Parameters: shown bounds, shown markers) Notify listeners when the state of what is displayed changes.
 * @event loaded MarkerManager has succesfully been initialized.
 *
 * @constructor
 * @param {Map} map The map to manage.
 * @param {Object} opt_opts A container for optional arguments:
 *   {Number} maxZoom The maximum zoom level for which to create tiles.
 *   {Number} borderPadding The width in pixels beyond the map border,
 *                   where markers should be display.
 *   {Boolean} trackMarkers Whether or not this manager should track marker
 *                   movements.
 */
 
 function MarkerManager(map,opt_opts){var me=this;me.map_=map;me.mapZoom_=map.getZoom();me.projectionHelper_=new ProjectionHelperOverlay(map);google.maps.event.addListener(me.projectionHelper_,'ready',function(){me.projection_=this.getProjection();me.initialize(map,opt_opts)})}MarkerManager.prototype.initialize=function(map,opt_opts){var me=this;opt_opts=opt_opts||{};me.tileSize_=MarkerManager.DEFAULT_TILE_SIZE_;var mapTypes=map.mapTypes;var mapMaxZoom=1;for(var sType in mapTypes){if(typeof map.mapTypes.get(sType)==='object'&&typeof map.mapTypes.get(sType).maxZoom==='number'){var mapTypeMaxZoom=map.mapTypes.get(sType).maxZoom;if(mapTypeMaxZoom>mapMaxZoom){mapMaxZoom=mapTypeMaxZoom}}}me.maxZoom_=opt_opts.maxZoom||19;me.trackMarkers_=opt_opts.trackMarkers;me.show_=opt_opts.show||true;var padding;if(typeof opt_opts.borderPadding==='number'){padding=opt_opts.borderPadding}else{padding=MarkerManager.DEFAULT_BORDER_PADDING_}me.swPadding_=new google.maps.Size(-padding,padding);me.nePadding_=new google.maps.Size(padding,-padding);me.borderPadding_=padding;me.gridWidth_={};me.grid_={};me.grid_[me.maxZoom_]={};me.numMarkers_={};me.numMarkers_[me.maxZoom_]=0;google.maps.event.addListener(map,'dragend',function(){me.onMapMoveEnd_()});google.maps.event.addListener(map,'zoom_changed',function(){me.onMapMoveEnd_()});me.removeOverlay_=function(marker){marker.setMap(null);me.shownMarkers_--};me.addOverlay_=function(marker){if(me.show_){marker.setMap(me.map_);me.shownMarkers_++}};me.resetManager_();me.shownMarkers_=0;me.shownBounds_=me.getMapGridBounds_();google.maps.event.trigger(me,'loaded')};MarkerManager.DEFAULT_TILE_SIZE_=1024;MarkerManager.DEFAULT_BORDER_PADDING_=100;MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE=256;MarkerManager.prototype.resetManager_=function(){var mapWidth=MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE;for(var zoom=0;zoom<=this.maxZoom_;++zoom){this.grid_[zoom]={};this.numMarkers_[zoom]=0;this.gridWidth_[zoom]=Math.ceil(mapWidth/this.tileSize_);mapWidth<<=1}};MarkerManager.prototype.clearMarkers=function(){this.processAll_(this.shownBounds_,this.removeOverlay_);this.resetManager_()};MarkerManager.prototype.getTilePoint_=function(latlng,zoom,padding){var pixelPoint=this.projectionHelper_.LatLngToPixel(latlng,zoom);var point=new google.maps.Point(Math.floor((pixelPoint.x+padding.width)/this.tileSize_),Math.floor((pixelPoint.y+padding.height)/this.tileSize_));return point};MarkerManager.prototype.addMarkerBatch_=function(marker,minZoom,maxZoom){var me=this;var mPoint=marker.getPosition();marker.MarkerManager_minZoom=minZoom;if(this.trackMarkers_){google.maps.event.addListener(marker,'changed',function(a,b,c){me.onMarkerMoved_(a,b,c)})}var gridPoint=this.getTilePoint_(mPoint,maxZoom,new google.maps.Size(0,0,0,0));for(var zoom=maxZoom;zoom>=minZoom;zoom--){var cell=this.getGridCellCreate_(gridPoint.x,gridPoint.y,zoom);cell.push(marker);gridPoint.x=gridPoint.x>>1;gridPoint.y=gridPoint.y>>1}};MarkerManager.prototype.isGridPointVisible_=function(point){var vertical=this.shownBounds_.minY<=point.y&&point.y<=this.shownBounds_.maxY;var minX=this.shownBounds_.minX;var horizontal=minX<=point.x&&point.x<=this.shownBounds_.maxX;if(!horizontal&&minX<0){var width=this.gridWidth_[this.shownBounds_.z];horizontal=minX+width<=point.x&&point.x<=width-1}return vertical&&horizontal};MarkerManager.prototype.onMarkerMoved_=function(marker,oldPoint,newPoint){var zoom=this.maxZoom_;var changed=false;var oldGrid=this.getTilePoint_(oldPoint,zoom,new google.maps.Size(0,0,0,0));var newGrid=this.getTilePoint_(newPoint,zoom,new google.maps.Size(0,0,0,0));while(zoom>=0&&(oldGrid.x!==newGrid.x||oldGrid.y!==newGrid.y)){var cell=this.getGridCellNoCreate_(oldGrid.x,oldGrid.y,zoom);if(cell){if(this.removeFromArray_(cell,marker)){this.getGridCellCreate_(newGrid.x,newGrid.y,zoom).push(marker)}}if(zoom===this.mapZoom_){if(this.isGridPointVisible_(oldGrid)){if(!this.isGridPointVisible_(newGrid)){this.removeOverlay_(marker);changed=true}}else{if(this.isGridPointVisible_(newGrid)){this.addOverlay_(marker);changed=true}}}oldGrid.x=oldGrid.x>>1;oldGrid.y=oldGrid.y>>1;newGrid.x=newGrid.x>>1;newGrid.y=newGrid.y>>1;--zoom}if(changed){this.notifyListeners_()}};MarkerManager.prototype.removeMarker=function(marker){var zoom=this.maxZoom_;var changed=false;var point=marker.getPosition();var grid=this.getTilePoint_(point,zoom,new google.maps.Size(0,0,0,0));while(zoom>=0){var cell=this.getGridCellNoCreate_(grid.x,grid.y,zoom);if(cell){this.removeFromArray_(cell,marker)}if(zoom===this.mapZoom_){if(this.isGridPointVisible_(grid)){this.removeOverlay_(marker);changed=true}}grid.x=grid.x>>1;grid.y=grid.y>>1;--zoom}if(changed){this.notifyListeners_()}this.numMarkers_[marker.MarkerManager_minZoom]--};MarkerManager.prototype.addMarkers=function(markers,minZoom,opt_maxZoom){var maxZoom=this.getOptMaxZoom_(opt_maxZoom);for(var i=markers.length-1;i>=0;i--){this.addMarkerBatch_(markers[i],minZoom,maxZoom)}this.numMarkers_[minZoom]+=markers.length};MarkerManager.prototype.getOptMaxZoom_=function(opt_maxZoom){return opt_maxZoom||this.maxZoom_};MarkerManager.prototype.getMarkerCount=function(zoom){var total=0;for(var z=0;z<=zoom;z++){total+=this.numMarkers_[z]}return total};MarkerManager.prototype.getMarker=function(lat,lng,zoom){var mPoint=new google.maps.LatLng(lat,lng);var gridPoint=this.getTilePoint_(mPoint,zoom,new google.maps.Size(0,0,0,0));var marker=new google.maps.Marker({position:mPoint});var cellArray=this.getGridCellNoCreate_(gridPoint.x,gridPoint.y,zoom);if(cellArray!==undefined){for(var i=0;i<cellArray.length;i++){if(lat===cellArray[i].getLatLng().lat()&&lng===cellArray[i].getLatLng().lng()){marker=cellArray[i]}}}return marker};MarkerManager.prototype.addMarker=function(marker,minZoom,opt_maxZoom){var maxZoom=this.getOptMaxZoom_(opt_maxZoom);this.addMarkerBatch_(marker,minZoom,maxZoom);var gridPoint=this.getTilePoint_(marker.getPosition(),this.mapZoom_,new google.maps.Size(0,0,0,0));if(this.isGridPointVisible_(gridPoint)&&minZoom<=this.shownBounds_.z&&this.shownBounds_.z<=maxZoom){this.addOverlay_(marker);this.notifyListeners_()}this.numMarkers_[minZoom]++};function GridBounds(bounds){this.minX=Math.min(bounds[0].x,bounds[1].x);this.maxX=Math.max(bounds[0].x,bounds[1].x);this.minY=Math.min(bounds[0].y,bounds[1].y);this.maxY=Math.max(bounds[0].y,bounds[1].y)}GridBounds.prototype.equals=function(gridBounds){if(this.maxX===gridBounds.maxX&&this.maxY===gridBounds.maxY&&this.minX===gridBounds.minX&&this.minY===gridBounds.minY){return true}else{return false}};GridBounds.prototype.containsPoint=function(point){var outer=this;return(outer.minX<=point.x&&outer.maxX>=point.x&&outer.minY<=point.y&&outer.maxY>=point.y)};MarkerManager.prototype.getGridCellCreate_=function(x,y,z){var grid=this.grid_[z];if(x<0){x+=this.gridWidth_[z]}var gridCol=grid[x];if(!gridCol){gridCol=grid[x]=[];return(gridCol[y]=[])}var gridCell=gridCol[y];if(!gridCell){return(gridCol[y]=[])}return gridCell};MarkerManager.prototype.getGridCellNoCreate_=function(x,y,z){var grid=this.grid_[z];if(x<0){x+=this.gridWidth_[z]}var gridCol=grid[x];return gridCol?gridCol[y]:undefined};MarkerManager.prototype.getGridBounds_=function(bounds,zoom,swPadding,nePadding){zoom=Math.min(zoom,this.maxZoom_);var bl=bounds.getSouthWest();var tr=bounds.getNorthEast();var sw=this.getTilePoint_(bl,zoom,swPadding);var ne=this.getTilePoint_(tr,zoom,nePadding);var gw=this.gridWidth_[zoom];if(tr.lng()<bl.lng()||ne.x<sw.x){sw.x-=gw}if(ne.x-sw.x+1>=gw){sw.x=0;ne.x=gw-1}var gridBounds=new GridBounds([sw,ne]);gridBounds.z=zoom;return gridBounds};MarkerManager.prototype.getMapGridBounds_=function(){return this.getGridBounds_(this.map_.getBounds(),this.mapZoom_,this.swPadding_,this.nePadding_)};MarkerManager.prototype.onMapMoveEnd_=function(){this.objectSetTimeout_(this,this.updateMarkers_,0)};MarkerManager.prototype.objectSetTimeout_=function(object,command,milliseconds){return window.setTimeout(function(){command.call(object)},milliseconds)};MarkerManager.prototype.visible=function(){return this.show_?true:false};MarkerManager.prototype.isHidden=function(){return!this.show_};MarkerManager.prototype.show=function(){this.show_=true;this.refresh()};MarkerManager.prototype.hide=function(){this.show_=false;this.refresh()};MarkerManager.prototype.toggle=function(){this.show_=!this.show_;this.refresh()};MarkerManager.prototype.refresh=function(){if(this.shownMarkers_>0){this.processAll_(this.shownBounds_,this.removeOverlay_)}if(this.show_){this.processAll_(this.shownBounds_,this.addOverlay_)}this.notifyListeners_()};MarkerManager.prototype.updateMarkers_=function(){this.mapZoom_=this.map_.getZoom();var newBounds=this.getMapGridBounds_();if(newBounds.equals(this.shownBounds_)&&newBounds.z===this.shownBounds_.z){return}if(newBounds.z!==this.shownBounds_.z){this.processAll_(this.shownBounds_,this.removeOverlay_);if(this.show_){this.processAll_(newBounds,this.addOverlay_)}}else{this.rectangleDiff_(this.shownBounds_,newBounds,this.removeCellMarkers_);if(this.show_){this.rectangleDiff_(newBounds,this.shownBounds_,this.addCellMarkers_)}}this.shownBounds_=newBounds;this.notifyListeners_()};MarkerManager.prototype.notifyListeners_=function(){google.maps.event.trigger(this,'changed',this.shownBounds_,this.shownMarkers_)};MarkerManager.prototype.processAll_=function(bounds,callback){for(var x=bounds.minX;x<=bounds.maxX;x++){for(var y=bounds.minY;y<=bounds.maxY;y++){this.processCellMarkers_(x,y,bounds.z,callback)}}};MarkerManager.prototype.processCellMarkers_=function(x,y,z,callback){var cell=this.getGridCellNoCreate_(x,y,z);if(cell){for(var i=cell.length-1;i>=0;i--){callback(cell[i])}}};MarkerManager.prototype.removeCellMarkers_=function(x,y,z){this.processCellMarkers_(x,y,z,this.removeOverlay_)};MarkerManager.prototype.addCellMarkers_=function(x,y,z){this.processCellMarkers_(x,y,z,this.addOverlay_)};MarkerManager.prototype.rectangleDiff_=function(bounds1,bounds2,callback){var me=this;me.rectangleDiffCoords_(bounds1,bounds2,function(x,y){callback.apply(me,[x,y,bounds1.z])})};MarkerManager.prototype.rectangleDiffCoords_=function(bounds1,bounds2,callback){var minX1=bounds1.minX;var minY1=bounds1.minY;var maxX1=bounds1.maxX;var maxY1=bounds1.maxY;var minX2=bounds2.minX;var minY2=bounds2.minY;var maxX2=bounds2.maxX;var maxY2=bounds2.maxY;var x,y;for(x=minX1;x<=maxX1;x++){for(y=minY1;y<=maxY1&&y<minY2;y++){callback(x,y)}for(y=Math.max(maxY2+1,minY1);y<=maxY1;y++){callback(x,y)}}for(y=Math.max(minY1,minY2);y<=Math.min(maxY1,maxY2);y++){for(x=Math.min(maxX1+1,minX2)-1;x>=minX1;x--){callback(x,y)}for(x=Math.max(minX1,maxX2+1);x<=maxX1;x++){callback(x,y)}}};MarkerManager.prototype.removeFromArray_=function(array,value,opt_notype){var shift=0;for(var i=0;i<array.length;++i){if(array[i]===value||(opt_notype&&array[i]===value)){array.splice(i--,1);shift++}}return shift};function ProjectionHelperOverlay(map){this.setMap(map);var TILEFACTOR=8;var TILESIDE=1<<TILEFACTOR;var RADIUS=7;this._map=map;this._zoom=-1;this._X0=this._Y0=this._X1=this._Y1=-1}ProjectionHelperOverlay.prototype=new google.maps.OverlayView();ProjectionHelperOverlay.prototype.LngToX_=function(lng){return(1+lng/180)};ProjectionHelperOverlay.prototype.LatToY_=function(lat){var sinofphi=Math.sin(lat*Math.PI/180);return(1-0.5/Math.PI*Math.log((1+sinofphi)/(1-sinofphi)))};ProjectionHelperOverlay.prototype.LatLngToPixel=function(latlng,zoom){var map=this._map;var div=this.getProjection().fromLatLngToDivPixel(latlng);var abs={x:~~(0.5+this.LngToX_(latlng.lng())*(2<<(zoom+6))),y:~~(0.5+this.LatToY_(latlng.lat())*(2<<(zoom+6)))};return abs};ProjectionHelperOverlay.prototype.draw=function(){if(!this.ready){this.ready=true;google.maps.event.trigger(this,'ready')}};
