/*
* LabeledMarker Class, v1.1
*
* Copyright 2007 Mike Purvis (http://uwmike.com)
* 
* 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.
*
* This class extends the Maps API's standard GMarker class with the ability
* to support markers with textual labels. Please see articles here:
*
*       http://googlemapsbook.com/2007/01/22/extending-gmarker/
*       http://googlemapsbook.com/2007/03/06/clickable-labeledmarker/
*/

/**
 * Constructor for LabeledMarker, which picks up on strings from the GMarker
 * options array, and then calls the GMarker constructor.
 *
 * @param {GLatLng} latlng
 * @param {GMarkerOptions} Named optional arguments:
 *   opt_opts.labelText {String} text to place in the overlay div.
 *   opt_opts.labelClass {String} class to use for the overlay div.
 *     (default "LabeledMarker_markerLabel")
 *   opt_opts.labelOffset {GSize} label offset, the x- and y-distance between
 *     the marker's latlng and the upper-left corner of the text div.
 */
function LabeledMarker(latlng, opt_opts){
  this.latlng_ = latlng;
  this.opts_ = opt_opts;

  this.labelText_ = opt_opts.labelText || "";
  this.labelClass_ = opt_opts.labelClass || "LabeledMarker_markerLabel";
  this.labelOffset_ = opt_opts.labelOffset || new GSize(0, 0);
  
  this.clickable_ = opt_opts.clickable || true;
  this.title_ = opt_opts.title || "";
  this.labelVisibility_  = true;
   
  if (opt_opts.draggable) {
  	// This version of LabeledMarker doesn't support dragging.
  	opt_opts.draggable = false;
  }
  
  GMarker.apply(this, arguments);
}


// It's a limitation of JavaScript inheritance that we can't conveniently
// inherit from GMarker without having to run its constructor. In order for 
// the constructor to run, it requires some dummy GLatLng.
LabeledMarker.prototype = new GMarker(new GLatLng(0, 0));

/**
 * Is called by GMap2's addOverlay method. Creates the text div and adds it
 * to the relevant parent div.
 *
 * @param {GMap2} map the map that has had this labeledmarker added to it.
 */
LabeledMarker.prototype.initialize = function(map) {
  // Do the GMarker constructor first.
  GMarker.prototype.initialize.apply(this, arguments);
  
  this.map_ = map;
  this.div_ = document.createElement("div");
  this.div_.className = this.labelClass_;
  this.div_.innerHTML = this.labelText_;
  this.div_.style.position = "absolute";
  this.div_.style.cursor = "pointer";
  this.div_.title = this.title_;
  
  map.getPane(G_MAP_MARKER_PANE).appendChild(this.div_);

  if (this.clickable_) {
    /**
     * Creates a closure for passing events through to the source marker
     * This is located in here to avoid cluttering the global namespace.
     * The downside is that the local variables from initialize() continue
     * to occupy space on the stack.
     *
     * @param {Object} object to receive event trigger.
     * @param {GEventListener} event to be triggered.
     */
    function newEventPassthru(obj, event) {
      return function() { 
        GEvent.trigger(obj, event);
      };
    }
  
    // Pass through events fired on the text div to the marker.
    var eventPassthrus = ['click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'];
    for(var i = 0; i < eventPassthrus.length; i++) {
      var name = eventPassthrus[i];
      GEvent.addDomListener(this.div_, name, newEventPassthru(this, name));
    }
  }
}

/**
 * Move the text div based on current projection and zoom level, call the redraw()
 * handler in GMarker.
 *
 * @param {Boolean} force will be true when pixel coordinates need to be recomputed.
 */
LabeledMarker.prototype.redraw = function(force) {
  GMarker.prototype.redraw.apply(this, arguments);
  
  // Calculate the DIV coordinates of two opposite corners of our bounds to
  // get the size and position of our rectangle
  var p = this.map_.fromLatLngToDivPixel(this.latlng_);
  var z = GOverlay.getZIndex(this.latlng_.lat());
  
  // Now position our div based on the div coordinates of our bounds
  this.div_.style.left = (p.x + this.labelOffset_.width) + "px";
  this.div_.style.top = (p.y + this.labelOffset_.height) + "px";
  this.div_.style.zIndex = z; // in front of the marker
}

/**
 * Remove the text div from the map pane, destroy event passthrus, and calls the
 * default remove() handler in GMarker.
 */
 LabeledMarker.prototype.remove = function() {
  GEvent.clearInstanceListeners(this.div_);
  if (this.div_.outerHTML) {
    this.div_.outerHTML = ""; //prevent pseudo-leak in IE
  }
  if (this.div_.parentNode) {
    this.div_.parentNode.removeChild(this.div_);
  }
  this.div_ = null;
  GMarker.prototype.remove.apply(this, arguments);
}

/**
 * Return a copy of this overlay, for the parent Map to duplicate itself in full. This
 * is part of the Overlay interface and is used, for example, to copy everything in the 
 * main view into the mini-map.
 */
LabeledMarker.prototype.copy = function() {
  return new LabeledMarker(this.latlng_, this.opt_opts_);
}


/**
 * Shows the marker, and shows label if it wasn't hidden. Note that this function 
 * triggers the event GMarker.visibilitychanged in case the marker is currently hidden.
 */
LabeledMarker.prototype.show = function() {
  GMarker.prototype.show.apply(this, arguments);
  if (this.labelVisibility_) {
    this.showLabel();
  } else {
    this.hideLabel();
  }
}


/**
 * Hides the marker and label if it is currently visible. Note that this function 
 * triggers the event GMarker.visibilitychanged in case the marker is currently visible.
 */
LabeledMarker.prototype.hide = function() {
  GMarker.prototype.hide.apply(this, arguments);
  this.hideLabel();
}


/**
 * Sets the visibility of the label, which will be respected during show/hides.
 * If marker is visible when set, it will show or hide label appropriately.
 */
LabeledMarker.prototype.setLabelVisibility = function(visibility) {
  this.labelVisibility_ = visibility;
  if (!this.isHidden()) { // Marker showing, make visible change
    if (this.labelVisibility_) {
      this.showLabel();
    } else {
      this.hideLabel();
    }
  }
}


/**
 * Returns whether label visibility is set on.
 * @return {Boolean}  
 */
LabeledMarker.prototype.getLabelVisibility = function() {
  return this.labelVisibility_;
}


/**
 * Hides the label of the marker.
 */
LabeledMarker.prototype.hideLabel = function() {
  this.div_.style.visibility = 'hidden';
}


/**
 * Shows the label of the marker.
 */
LabeledMarker.prototype.showLabel = function() {
  this.div_.style.visibility = 'visible';
}
/* 
 * MarkerManager, v1.0
 * Copyright (c) 2007 Google Inc.
 *
 * 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. 
 *
 *
 * Author: Doug Ricket, others
 * 
 * Marker manager is an interface between the map and the user, designed
 * to manage adding and removing many points when the viewport changes.
 *
 *
 * Algorithm: The MM places its markers onto a grid, similar to the map tiles.
 * When the user moves the viewport, the MM 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 EVENT_moveend 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.
 *
 */



/**
 * Creates a new MarkerManager that will show/hide markers on a map.
 *
 * @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.projection_ = map.getCurrentMapType().getProjection();

  opt_opts = opt_opts || {};
  me.tileSize_ = MarkerManager.DEFAULT_TILE_SIZE_;
  
  var maxZoom = MarkerManager.DEFAULT_MAX_ZOOM_;
  if(opt_opts.maxZoom != undefined) {
    maxZoom = opt_opts.maxZoom;
  }
  me.maxZoom_ = maxZoom;

  me.trackMarkers_ = opt_opts.trackMarkers;

  var padding;
  if (typeof opt_opts.borderPadding == "number") {
    padding = opt_opts.borderPadding;
  } else {
    padding = MarkerManager.DEFAULT_BORDER_PADDING_;
  }
  // The padding in pixels beyond the viewport, where we will pre-load markers.
  me.swPadding_ = new GSize(-padding, padding);
  me.nePadding_ = new GSize(padding, -padding);
  me.borderPadding_ = padding;

  me.gridWidth_ = [];

  me.grid_ = [];
  me.grid_[maxZoom] = [];
  me.numMarkers_ = [];
  me.numMarkers_[maxZoom] = 0;

  GEvent.bind(map, "moveend", me, me.onMapMoveEnd_);

  // NOTE: These two closures provide easy access to the map.
  // They are used as callbacks, not as methods.
  me.removeOverlay_ = function(marker) {
    map.removeOverlay(marker);
    me.shownMarkers_--;
  };
  me.addOverlay_ = function(marker) {
    map.addOverlay(marker);
    me.shownMarkers_++;
  };

  me.resetManager_();
  me.shownMarkers_ = 0;

  me.shownBounds_ = me.getMapGridBounds_();
};

// Static constants:
MarkerManager.DEFAULT_TILE_SIZE_ = 1024;
MarkerManager.DEFAULT_MAX_ZOOM_ = 17;
MarkerManager.DEFAULT_BORDER_PADDING_ = 100;
MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE = 256;


/**
 * Initializes MarkerManager arrays for all zoom levels
 * Called by constructor and by clearAllMarkers
 */ 
MarkerManager.prototype.resetManager_ = function() {
  var me = this;
  var mapWidth = MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE;
  for (var zoom = 0; zoom <= me.maxZoom_; ++zoom) {
    me.grid_[zoom] = [];
    me.numMarkers_[zoom] = 0;
    me.gridWidth_[zoom] = Math.ceil(mapWidth/me.tileSize_);
    mapWidth <<= 1;
  }
};

/**
 * Removes all currently displayed markers
 * and calls resetManager to clear arrays
 */
MarkerManager.prototype.clearMarkers = function() {
  var me = this;
  me.processAll_(me.shownBounds_, me.removeOverlay_);
  me.resetManager_();
};


/**
 * Gets the tile coordinate for a given latlng point.
 *
 * @param {LatLng} latlng The geographical point.
 * @param {Number} zoom The zoom level.
 * @param {GSize} padding The padding used to shift the pixel coordinate.
 *               Used for expanding a bounds to include an extra padding
 *               of pixels surrounding the bounds.
 * @return {GPoint} The point in tile coordinates.
 *
 */
MarkerManager.prototype.getTilePoint_ = function(latlng, zoom, padding) {
  var pixelPoint = this.projection_.fromLatLngToPixel(latlng, zoom);
  return new GPoint(
      Math.floor((pixelPoint.x + padding.width) / this.tileSize_),
      Math.floor((pixelPoint.y + padding.height) / this.tileSize_));
};


/**
 * Finds the appropriate place to add the marker to the grid.
 * Optimized for speed; does not actually add the marker to the map.
 * Designed for batch-processing thousands of markers.
 *
 * @param {Marker} marker The marker to add.
 * @param {Number} minZoom The minimum zoom for displaying the marker.
 * @param {Number} maxZoom The maximum zoom for displaying the marker.
 */
MarkerManager.prototype.addMarkerBatch_ = function(marker, minZoom, maxZoom) {
  var mPoint = marker.getPoint();
  // Tracking markers is expensive, so we do this only if the
  // user explicitly requested it when creating marker manager.
  if (this.trackMarkers_) {
    GEvent.bind(marker, "changed", this, this.onMarkerMoved_);
  }

  var gridPoint = this.getTilePoint_(mPoint, maxZoom, GSize.ZERO);

  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;
  }
};


/**
 * Returns whether or not the given point is visible in the shown bounds. This
 * is a helper method that takes care of the corner case, when shownBounds have
 * negative minX value.
 *
 * @param {Point} point a point on a grid.
 * @return {Boolean} Whether or not the given point is visible in the currently
 * shown bounds.
 */
MarkerManager.prototype.isGridPointVisible_ = function(point) {
  var me = this;
  var vertical = me.shownBounds_.minY <= point.y &&
      point.y <= me.shownBounds_.maxY;
  var minX = me.shownBounds_.minX;
  var horizontal = minX <= point.x && point.x <= me.shownBounds_.maxX;
  if (!horizontal && minX < 0) {
    // Shifts the negative part of the rectangle. As point.x is always less
    // than grid width, only test shifted minX .. 0 part of the shown bounds.
    var width = me.gridWidth_[me.shownBounds_.z];
    horizontal = minX + width <= point.x && point.x <= width - 1;
  }
  return vertical && horizontal;
}


/**
 * Reacts to a notification from a marker that it has moved to a new location.
 * It scans the grid all all zoom levels and moves the marker from the old grid
 * location to a new grid location.
 *
 * @param {Marker} marker The marker that moved.
 * @param {LatLng} oldPoint The old position of the marker.
 * @param {LatLng} newPoint The new position of the marker.
 */
MarkerManager.prototype.onMarkerMoved_ = function(marker, oldPoint, newPoint) {
  // NOTE: We do not know the minimum or maximum zoom the marker was
  // added at, so we start at the absolute maximum. Whenever we successfully
  // remove a marker at a given zoom, we add it at the new grid coordinates.
  var me = this;
  var zoom = me.maxZoom_;
  var changed = false;
  var oldGrid = me.getTilePoint_(oldPoint, zoom, GSize.ZERO);
  var newGrid = me.getTilePoint_(newPoint, zoom, GSize.ZERO);
  while (zoom >= 0 && (oldGrid.x != newGrid.x || oldGrid.y != newGrid.y)) {
    var cell = me.getGridCellNoCreate_(oldGrid.x, oldGrid.y, zoom);
    if (cell) {
      if (me.removeFromArray(cell, marker)) {
        me.getGridCellCreate_(newGrid.x, newGrid.y, zoom).push(marker);
      }
    }
    // For the current zoom we also need to update the map. Markers that no
    // longer are visible are removed from the map. Markers that moved into
    // the shown bounds are added to the map. This also lets us keep the count
    // of visible markers up to date.
    if (zoom == me.mapZoom_) {
      if (me.isGridPointVisible_(oldGrid)) {
        if (!me.isGridPointVisible_(newGrid)) {
          me.removeOverlay_(marker);
          changed = true;
        }
      } else {
        if (me.isGridPointVisible_(newGrid)) {
          me.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) {
    me.notifyListeners_();
  }
};


/**
 * Searches at every zoom level to find grid cell
 * that marker would be in, removes from that array if found.
 * Also removes marker with removeOverlay if visible.
 * @param {GMarker} marker The marker to delete.
 */
MarkerManager.prototype.removeMarker = function(marker) {
  var me = this;
  var zoom = me.maxZoom_;
  var changed = false;
  var point = marker.getPoint();
  var grid = me.getTilePoint_(point, zoom, GSize.ZERO);
  while (zoom >= 0) {
    var cell = me.getGridCellNoCreate_(grid.x, grid.y, zoom);

    if (cell) {
      me.removeFromArray(cell, marker);
    }
    // For the current zoom we also need to update the map. Markers that no
    // longer are visible are removed from the map. This also lets us keep the count
    // of visible markers up to date.
    if (zoom == me.mapZoom_) {
      if (me.isGridPointVisible_(grid)) {
          me.removeOverlay_(marker);
          changed = true;
      } 
    }
    grid.x = grid.x >> 1;
    grid.y = grid.y >> 1;
    --zoom;
  }
  if (changed) {
    me.notifyListeners_();
  }
};


/**
 * Add many markers at once.
 * Does not actually update the map, just the internal grid.
 *
 * @param {Array of Marker} markers The markers to add.
 * @param {Number} minZoom The minimum zoom level to display the markers.
 * @param {Number} opt_maxZoom The maximum zoom level to display the markers.
 */
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;
};


/**
 * Returns the value of the optional maximum zoom. This method is defined so
 * that we have just one place where optional maximum zoom is calculated.
 *
 * @param {Number} opt_maxZoom The optinal maximum zoom.
 * @return The maximum zoom.
 */
MarkerManager.prototype.getOptMaxZoom_ = function(opt_maxZoom) {
  return opt_maxZoom != undefined ? opt_maxZoom : this.maxZoom_;
}


/**
 * Calculates the total number of markers potentially visible at a given
 * zoom level.
 *
 * @param {Number} zoom The zoom level to check.
 */
MarkerManager.prototype.getMarkerCount = function(zoom) {
  var total = 0;
  for (var z = 0; z <= zoom; z++) {
    total += this.numMarkers_[z];
  }
  return total;
};


/**
 * Add a single marker to the map.
 *
 * @param {Marker} marker The marker to add.
 * @param {Number} minZoom The minimum zoom level to display the marker.
 * @param {Number} opt_maxZoom The maximum zoom level to display the marker.
 */
MarkerManager.prototype.addMarker = function(marker, minZoom, opt_maxZoom) {
  var me = this;
  var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
  me.addMarkerBatch_(marker, minZoom, maxZoom);
  var gridPoint = me.getTilePoint_(marker.getPoint(), me.mapZoom_, GSize.ZERO);
  if(me.isGridPointVisible_(gridPoint) && 
     minZoom <= me.shownBounds_.z &&
     me.shownBounds_.z <= maxZoom ) {
    me.addOverlay_(marker);
    me.notifyListeners_();
  }
  this.numMarkers_[minZoom]++;
};

/**
 * Returns true if this bounds (inclusively) contains the given point.
 * @param {Point} point  The point to test.
 * @return {Boolean} This Bounds contains the given Point.
 */
GBounds.prototype.containsPoint = function(point) {
  var outer = this;
  return (outer.minX <= point.x &&
          outer.maxX >= point.x &&
          outer.minY <= point.y &&
          outer.maxY >= point.y);
}

/**
 * Get a cell in the grid, creating it first if necessary.
 *
 * Optimization candidate
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @return {Array} The cell in the array.
 */
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;
};


/**
 * Get a cell in the grid, returning undefined if it does not exist.
 *
 * NOTE: Optimized for speed -- otherwise could combine with getGridCellCreate_.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @return {Array} The cell in the array.
 */
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;
};


/**
 * Turns at geographical bounds into a grid-space bounds.
 *
 * @param {LatLngBounds} bounds The geographical bounds.
 * @param {Number} zoom The zoom level of the bounds.
 * @param {GSize} swPadding The padding in pixels to extend beyond the
 * given bounds.
 * @param {GSize} nePadding The padding in pixels to extend beyond the
 * given bounds.
 * @return {GBounds} The bounds in grid space.
 */
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];
  
  // Crossing the prime meridian requires correction of bounds.
  if (tr.lng() < bl.lng() || ne.x < sw.x) {
    sw.x -= gw;
  }
  if (ne.x - sw.x  + 1 >= gw) {
    // Computed grid bounds are larger than the world; truncate.
    sw.x = 0;
    ne.x = gw - 1;
  }
  var gridBounds = new GBounds([sw, ne]);
  gridBounds.z = zoom;
  return gridBounds;
};


/**
 * Gets the grid-space bounds for the current map viewport.
 *
 * @return {Bounds} The bounds in grid space.
 */
MarkerManager.prototype.getMapGridBounds_ = function() {
  var me = this;
  return me.getGridBounds_(me.map_.getBounds(), me.mapZoom_,
                           me.swPadding_, me.nePadding_);
};


/**
 * Event listener for map:movend.
 * NOTE: Use a timeout so that the user is not blocked
 * from moving the map.
 *
 */
MarkerManager.prototype.onMapMoveEnd_ = function() {
  var me = this;
  me.objectSetTimeout_(this, this.updateMarkers_, 0);
};


/**
 * Call a function or evaluate an expression after a specified number of
 * milliseconds.
 *
 * Equivalent to the standard window.setTimeout function, but the given
 * function executes as a method of this instance. So the function passed to
 * objectSetTimeout can contain references to this.
 *    objectSetTimeout(this, function() { alert(this.x) }, 1000);
 *
 * @param {Object} object  The target object.
 * @param {Function} command  The command to run.
 * @param {Number} milliseconds  The delay.
 * @return {Boolean}  Success.
 */
MarkerManager.prototype.objectSetTimeout_ = function(object, command, milliseconds) {
  return window.setTimeout(function() {
    command.call(object);
  }, milliseconds);
};


/**
 * Refresh forces the marker-manager into a good state.
 * <ol>
 *   <li>If never before initialized, shows all the markers.</li>
 *   <li>If previously initialized, removes and re-adds all markers.</li>
 * </ol>
 */
MarkerManager.prototype.refresh = function() {
  var me = this;
  if (me.shownMarkers_ > 0) {
    me.processAll_(me.shownBounds_, me.removeOverlay_);
  }
  me.processAll_(me.shownBounds_, me.addOverlay_);
  me.notifyListeners_();
};


/**
 * After the viewport may have changed, add or remove markers as needed.
 */
MarkerManager.prototype.updateMarkers_ = function() {
  var me = this;
  me.mapZoom_ = this.map_.getZoom();
  var newBounds = me.getMapGridBounds_();
  
  // If the move does not include new grid sections,
  // we have no work to do:
  if (newBounds.equals(me.shownBounds_) && newBounds.z == me.shownBounds_.z) {
    return;
  }

  if (newBounds.z != me.shownBounds_.z) {
    me.processAll_(me.shownBounds_, me.removeOverlay_);
    me.processAll_(newBounds, me.addOverlay_);
  } else {
    // Remove markers:
    me.rectangleDiff_(me.shownBounds_, newBounds, me.removeCellMarkers_);

    // Add markers:
    me.rectangleDiff_(newBounds, me.shownBounds_, me.addCellMarkers_);
  }
  me.shownBounds_ = newBounds;

  me.notifyListeners_();
};


/**
 * Notify listeners when the state of what is displayed changes.
 */
MarkerManager.prototype.notifyListeners_ = function() {
  GEvent.trigger(this, "changed", this.shownBounds_, this.shownMarkers_);
};


/**
 * Process all markers in the bounds provided, using a callback.
 *
 * @param {Bounds} bounds The bounds in grid space.
 * @param {Function} callback The function to call for each marker.
 */
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);
    }
  }
};


/**
 * Process all markers in the grid cell, using a callback.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @param {Function} callback The function to call for each marker.
 */
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]);
    }
  }
};


/**
 * Remove all markers in a grid cell.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 */
MarkerManager.prototype.removeCellMarkers_ = function(x, y, z) {
  this.processCellMarkers_(x, y, z, this.removeOverlay_);
};


/**
 * Add all markers in a grid cell.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 */
MarkerManager.prototype.addCellMarkers_ = function(x, y, z) {
  this.processCellMarkers_(x, y, z, this.addOverlay_);
};


/**
 * Use the rectangleDiffCoords function to process all grid cells
 * that are in bounds1 but not bounds2, using a callback, and using
 * the current MarkerManager object as the instance.
 *
 * Pass the z parameter to the callback in addition to x and y.
 *
 * @param {Bounds} bounds1 The bounds of all points we may process.
 * @param {Bounds} bounds2 The bounds of points to exclude.
 * @param {Function} callback The callback function to call
 *                   for each grid coordinate (x, y, z).
 */
MarkerManager.prototype.rectangleDiff_ = function(bounds1, bounds2, callback) {
  var me = this;
  me.rectangleDiffCoords(bounds1, bounds2, function(x, y) {
    callback.apply(me, [x, y, bounds1.z]);
  });
};


/**
 * Calls the function for all points in bounds1, not in bounds2
 *
 * @param {Bounds} bounds1 The bounds of all points we may process.
 * @param {Bounds} bounds2 The bounds of points to exclude.
 * @param {Function} callback The callback function to call
 *                   for each grid coordinate.
 */
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;

  for (var x = minX1; x <= maxX1; x++) {  // All x in R1
    // All above:
    for (var y = minY1; y <= maxY1 && y < minY2; y++) {  // y in R1 above R2
      callback(x, y);
    }
    // All below:
    for (var y = Math.max(maxY2 + 1, minY1);  // y in R1 below R2
         y <= maxY1; y++) {
      callback(x, y);
    }
  }

  for (var y = Math.max(minY1, minY2);
       y <= Math.min(maxY1, maxY2); y++) {  // All y in R2 and in R1
    // Strictly left:
    for (var x = Math.min(maxX1 + 1, minX2) - 1;
         x >= minX1; x--) {  // x in R1 left of R2
      callback(x, y);
    }
    // Strictly right:
    for (var x = Math.max(minX1, maxX2 + 1);  // x in R1 right of R2
         x <= maxX1; x++) {
      callback(x, y);
    }
  }
};


/**
 * Removes value from array. O(N).
 *
 * @param {Array} array  The array to modify.
 * @param {any} value  The value to remove.
 * @param {Boolean} opt_notype  Flag to disable type checking in equality.
 * @return {Number}  The number of instances of value that were removed.
 */
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;
};
var LocationService = function() {
    LocationService.initializeBase(this);
    this._timeout = 0;
    this._userContext = null;
    this._succeeded = null;
    this._failed = null;
}
LocationService.prototype = {
    _get_path: function() {
        var p = this.get_path();
        if (p) return p;
        else return LocationService._staticInstance.get_path();
    },
    GetLocationDetails: function(d, succeededCallback, failedCallback, userContext) {
        /// <param name="d" type="Object">System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[Venue, App_Code.reuytndm, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]</param>
        /// <param name="succeededCallback" type="Function" optional="true" mayBeNull="true"></param>
        /// <param name="failedCallback" type="Function" optional="true" mayBeNull="true"></param>
        /// <param name="userContext" optional="true" mayBeNull="true"></param>
        return this._invoke(this._get_path(), 'GetLocationDetails', false, { d: d }, succeededCallback, failedCallback, userContext);
    } 
}
LocationService.registerClass('LocationService', Sys.Net.WebServiceProxy);
LocationService._staticInstance = new LocationService();
LocationService.set_path = function(value) {
    LocationService._staticInstance.set_path(value);
}
LocationService.get_path = function() {
    /// <value type="String" mayBeNull="true">The service url.</value>
    return LocationService._staticInstance.get_path();
}
LocationService.set_timeout = function(value) {
    LocationService._staticInstance.set_timeout(value);
}
LocationService.get_timeout = function() {
    /// <value type="Number">The service timeout.</value>
    return LocationService._staticInstance.get_timeout();
}
LocationService.set_defaultUserContext = function(value) {
    LocationService._staticInstance.set_defaultUserContext(value);
}
LocationService.get_defaultUserContext = function() {
    /// <value mayBeNull="true">The service default user context.</value>
    return LocationService._staticInstance.get_defaultUserContext();
}
LocationService.set_defaultSucceededCallback = function(value) {
    LocationService._staticInstance.set_defaultSucceededCallback(value);
}
LocationService.get_defaultSucceededCallback = function() {
    /// <value type="Function" mayBeNull="true">The service default succeeded callback.</value>
    return LocationService._staticInstance.get_defaultSucceededCallback();
}
LocationService.set_defaultFailedCallback = function(value) {
    LocationService._staticInstance.set_defaultFailedCallback(value);
}
LocationService.get_defaultFailedCallback = function() {
    /// <value type="Function" mayBeNull="true">The service default failed callback.</value>
    return LocationService._staticInstance.get_defaultFailedCallback();
}
LocationService.set_path("/turboeats/Services/LocationService.asmx");
LocationService.GetLocationDetails = function(d, onSuccess, onFailed, userContext) {
    /// <param name="d" type="Object">System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[Venue, App_Code.reuytndm, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]</param>
    /// <param name="succeededCallback" type="Function" optional="true" mayBeNull="true"></param>
    /// <param name="failedCallback" type="Function" optional="true" mayBeNull="true"></param>
    /// <param name="userContext" optional="true" mayBeNull="true"></param>
    LocationService._staticInstance.GetLocationDetails(d, onSuccess, onFailed, userContext);
}
var gtc = Sys.Net.WebServiceProxy._generateTypedConstructor;
if (typeof (Venue) === 'undefined') {
    var Venue = gtc("Venue");
    Venue.registerClass('Venue');
}
var centerLatitude =  41.880929; 
var centerLongitude = -87.942523;
var Map, manager, marker;
function ReceiveCoupons() {
    $('#ea').val($('#ctl00_Body_ContentPlaceHolder_Email_TextBox').val());
    $('#ccoptin').submit();
}  
function BeginSearch(){
    $('#NotFound').hide();
    if($('#aspnetForm').validate().element('#ctl00_Body_ContentPlaceHolder_Zip_TextBox') && $('#aspnetForm').validate().element('#ctl00_Body_ContentPlaceHolder_Address_TextBox')){
        $('#SubmitLink').hide();
        $('#Searching').show();
        $('#ctl00_Body_ContentPlaceHolder_Address_TextBox').attr('disabled', true); 
        $('#ctl00_Body_ContentPlaceHolder_Zip_TextBox').attr('disabled', true);
        __doPostBack('ctl00$Body_ContentPlaceHolder$Search_LinkButton', '');
    }else{
        $('#NotFound').show();
        $('#ctl00_Body_ContentPlaceHolder_Error_Label').text('Valid Address and ZIP code is required.'); 
    }
}
function BackToSearch(){
    $('#inst').hide();
    $('#Locations').hide();
    $('#SubmitLink').show();
    $('#Searching').hide();
    $('#ctl00_Body_ContentPlaceHolder_Error_Label').text(''); 
    $('#NotFound').hide();
    $('#ctl00_Body_ContentPlaceHolder_Address_TextBox').attr('disabled', false); 
    $('#ctl00_Body_ContentPlaceHolder_Zip_TextBox').attr('disabled', false); 
    $('.Panel').animate({right: '0px'}, 'slow'); 
    Map.clearOverlays(); 
    GUnload();
    batch = [];  
}
function BackToRestaurants(){
    $('#StepThree .Menu').hide();
    $('#StepThree .MenuItems').hide();
    $('#StepThree .ItemDetails').hide();
    $('.Panel').animate({right: '955px'}, 'slow');
    $('#Locations').show();
    $('#inst').text("What do you have a taste for? Choose a restaurant by clicking “view menu” on the right.");
}
function BackToMenu(){
    $('#StepThree .Items li').removeClass('Selected');
    $('#Locations').hide();
    $('#StepThree .Menu').show();
    $('#StepThree .ItemDetails').hide();
    $('.Panel').animate({right: '1910px'}, 'slow');
    $('#StepFour').hide();
    $('#inst').text("Select which category you’d like to order from. (Yes, you can order from more than one category.) Make your selection by clicking on your desired item. Type in any special instructions and click “add to cart.” Once you’ve added an item to your cart, you can either add more items by going through the same process or you can checkout by clicking the “checkout” button in the top right corner.");
}
function AddToCart(){
    $('#AddToCartButton').hide();
    $('#AddingToCartButton').show();
    __doPostBack('ctl00$Body_ContentPlaceHolder$AddToCart_LinkButton','');
}
function ViewMenu(ID,item){ 
    $('#StepThree .MenuItems').hide();
    $('#StepThree .ItemDetails').hide();
    $('#ctl00_Body_ContentPlaceHolder_LocationID_HiddenField').val(ID);   
    $('#StepThree .LocationTitle').text($(item).attr('alt'));
    __doPostBack('ctl00$Body_ContentPlaceHolder$ViewMenu_LinkButton','');
}

function AddToFavorites(ID, item) {
    $('#ctl00_Body_ContentPlaceHolder_LocationID_HiddenField').val(ID);
    __doPostBack('ctl00$Body_ContentPlaceHolder$AddToFavorites_LinkButton', '');
    $(item).attr({ "src": "App_Themes/Default/images/favorite.gif" });
    $(item).attr({ "class": "FavoriteLocation" });   
}

function ViewMenuItems(ID,item){
    $('#ctl00_Body_ContentPlaceHolder_MenuCategoryID_HiddenField').val(ID);   
    __doPostBack('ctl00$Body_ContentPlaceHolder$ViewMenuItems_LinkButton','');
    $('#StepThree .MenuItems').fadeIn('slow');
    $('#StepThree .Items li').removeClass('Selected');
    $(item).parent().addClass('Selected');
    $('#StepThree .MenuItems .ItemTitle').text($(item).text());
    $('#StepThree .ItemDetails').fadeOut('slow');
}
function ViewMenuItemDetails(ID,item){
    $('#ctl00_Body_ContentPlaceHolder_Item_HiddenField').val(ID);   
    __doPostBack('ctl00$Body_ContentPlaceHolder$ViewItemDetails_LinkButton','');
    $('#StepThree .MenuItems li').removeClass('Selected');     
    $(item).parent().addClass('Selected');
    $('#StepThree .ItemDetails .ItemDetailsTitle').text($(item).text());
    $('#StepThree .ItemDetails').fadeIn('slow');
     
}
function HomeEndRequestHandler(sender, args)
{
    $('#StepThree .ItemDetails .Details .i').Watermark("Insert Special Instructions Here (i.e. no pickles)");
    $('#StepFour .Cart .i').Watermark("Insert Special Instructions Here (i.e. no pickles)");

    if($('#ctl00_Body_ContentPlaceHolder_Error_HiddenField').val() == 1){
        $('#NotFound').show();
        $('#ctl00_Body_ContentPlaceHolder_Address_TextBox').attr('disabled', false); 
        $('#ctl00_Body_ContentPlaceHolder_Zip_TextBox').attr('disabled', false); 
        $('#SubmitLink').show();
        $('#Searching').hide();
    }else{
        switch ($('#Action_HiddenField').val()){
            case "ctl00_Body_ContentPlaceHolder_Search_LinkButton":
                window.location = "#Locations";
                DisplayLocations();
                $('#inst').text("What do you have a taste for? Choose a restaurant by clicking “view menu” on the right.");
                break;
            case "ctl00_Body_ContentPlaceHolder_ViewMenu_LinkButton":
                 $('#Locations').hide();
                 $('#StepThree .Menu').show();
                 window.location = "#Menu";
                 $(".Panel").animate({ right: '1910px' }, "slow");
                 $('#inst').text("Select which category you’d like order from. (Yes, you can order from more than one category.) Make your selection by clicking on your desired item. Type in any special instructions and click “add to cart.” Once you’ve added an item to your cart, you can either add more items by going through the same process or you can checkout by clicking the “checkout” button in the top right corner.");
                break;
            case "ctl00_Body_ContentPlaceHolder_AddToCart_LinkButton":
                ShowMessage("Item(s) added to your cart.");
                $("#AddToCartButton").show();
                $("#AddingToCartButton").hide();
                $("#CartActions").show();
                break;
            case "ctl00_Body_ContentPlaceHolder_ViewCart_LinkButton":
                ViewCart();
                $("#BackToRestaurantsButton").hide();
                $('#StepThree .LocationTitle').text($('#ctl00_Body_ContentPlaceHolder_LocationTitle_HiddenField').val());
                $('#ctl00_Body_ContentPlaceHolder_LocationID_HiddenField').val($('#ctl00_Body_ContentPlaceHolder_CartLocationID_HiddenField').val());
                $('#inst').text("");
                break;
            case "ctl00_Body_ContentPlaceHolder_UpdateCart_LinkButton":
                    ShowMessage("Your cart has been updated.");
                break;
            default:
                break;
         }   
         
         if ($('#Action_HiddenField').val().match("Delete_LinkButton")) {
            ShowMessage("Your cart has been updated.");
         }
         
        
    }
}
function DisplayLocations(){
    $('#NotFound').hide();
    Map = new GMap($get("Map"));
    Map.addControl(new GSmallMapControl());
    Map.setCenter(new GLatLng($('#ctl00_Body_ContentPlaceHolder_SourceLat_HiddenField').val(), $('#ctl00_Body_ContentPlaceHolder_SourceLong_HiddenField').val()), 12);
    Map.addControl(new GMapTypeControl());
    var e;
    batch = [];
    e = new Venue();
    e.Title = "Home";
    e.VenueID = 0;
    e.Icon = "home.png";
    e.Latitude = $('#ctl00_Body_ContentPlaceHolder_SourceLat_HiddenField').val();
    e.Longitude = $('#ctl00_Body_ContentPlaceHolder_SourceLong_HiddenField').val();
    Map.clearOverlays();
    point = new GLatLng(e.Latitude , e.Longitude);
    batch.push(createMarker(e));
    Map.setCenter(point, 12);
    manager = new MarkerManager(Map);
    $('#Locations .Venue').each( function() {
         e = new Venue();
         e.Title = $(this).find('.Title').text();
         e.VenueID = $(this).find('#LocationID').val();
         e.Latitude = $(this).find('#Latitude').val();
         e.Longitude = $(this).find('#Longitude').val();
         e.Icon = $(this).find('#Logo').val();
         marker = createMarker(e);
         var html = "<div class='Venue'>";
         html +=  $(this).find('.Item').html();
         html += "</div>";
         var handler = CreateMarkerClickHandler(marker, html);
         GEvent.addListener(marker, "click", handler);
         batch.push(marker);   
         $(this).find('.RestaurantLogo').click( function()
         {   
           handler();      
         });
         $(this).find('#TitleLink').click( function()
         {   
           handler();      
         });
     });    
    manager.addMarkers(batch, 11);
    manager.refresh();
    setTimeout(function(){ 
        $('.Panel').animate({right: '955px'}, 'slow');
        $('#Locations').show();
        //$('#MiniLogo').fadeIn('slow');
      
    },500);
}
function ApplicationUnloadHandler(){
    GUnload();
}
if (typeof(Sys) !== "undefined"){
    Sys.Application.notifyScriptLoaded();
}
function HomePageLoaded() 
{
    $('#StepThree .ItemDetails .Details .q').numeric();
    $('#StepFour .Cart .q').numeric();
    $('#ctl00_Body_ContentPlaceHolder_Zip_TextBox').numeric();
    $('#ctl00_Body_ContentPlaceHolder_Address_TextBox').select().focus();
    $('.ViewCart').click( function()
    {   
        __doPostBack('ctl00$Body_ContentPlaceHolder$ViewCart_LinkButton','');
    ;
    });
}
 
function UpdateCart() 
{
    __doPostBack('ctl00$Body_ContentPlaceHolder$UpdateCart_LinkButton','');       
}
function ViewCart() 
{
    //$('#MiniLogo').show();
    $('#StepThree .Menu').hide();
    $('#StepThree .MenuItems').hide();
    $('#StepThree .ItemDetails').hide();
    $('.Panel').animate({right: '2865px'}, 'slow');
    $('#StepFour').show();
}
$(document).ready(function() {
    //$('#MiniLogo').hide();
    if(location.hash == '#Cart'){
        __doPostBack('ctl00$Body_ContentPlaceHolder$ViewCart_LinkButton','');
    }
    if(location.hash == '#Menu'){
        $('#StepThree .LocationTitle').text($('#ctl00_Body_ContentPlaceHolder_LocationTitle_HiddenField').val());
        BackToMenu();
        $('#BackToRestaurantsButton').hide();   
    }
    var $form = $('#aspnetForm');
    $form.keypress(function(e){
        var t = e.target.id;
        if (e.which == 13 && (t == 'ctl00_Body_ContentPlaceHolder_Zip_TextBox' || t == 'ctl00_Body_ContentPlaceHolder_Address_TextBox')) {
            BeginSearch();
        }
        if (e.which == 13 && (t == 'ctl00_Body_ContentPlaceHolder_Email_TextBox')) {
           ReceiveCoupons();
        }
    });
});     
function CreateMarkerClickHandler(marker, Html) {
  return function() {
    marker.openInfoWindowHtml(Html,{maxWidth:400});
    return false;
  };
}
function createMarker(pointData) {
    var latlng = new GLatLng(pointData.Latitude, pointData.Longitude);
    var icon = new GIcon();
    icon.image = 'documents/'+ pointData.Icon;
    icon.iconSize = new GSize(32, 32);
    icon.iconAnchor = new GPoint(16, 16);
    icon.infoWindowAnchor = new GPoint(25, 7);
    opts = {"icon": icon,"clickable": true, "labelOffset": new GSize(-16, -16) };
    var marker = new LabeledMarker(latlng, opts);
    return marker;
}
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(HomePageLoaded);
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(HomeEndRequestHandler);
Sys.Application.add_unload(ApplicationUnloadHandler);
function CreateMarkerClickHandler(marker, Html) {
  return function() {
    marker.openInfoWindowHtml(Html,{maxWidth:400});
    return false;
  };
}
function createMarker(pointData) {
    var latlng = new GLatLng(pointData.Latitude, pointData.Longitude);
    var icon = new GIcon();
    icon.image = 'images/'+ pointData.Icon;
    icon.iconSize = new GSize(32, 32);
    icon.iconAnchor = new GPoint(16, 16);
    icon.infoWindowAnchor = new GPoint(25, 7);
    opts = {"icon": icon,"clickable": true, "labelOffset": new GSize(-16, -16) };
    var marker = new LabeledMarker(latlng, opts);
    return marker;
}

