

/************ START maps/extlargemapcontrol ********************/

/**
 * @name ExtLargeMapControl
 * @version 1.2
 * @author Masashi, Bjorn Brala
 * @fileoverview Creates a control with buttons to pan in four directions,
 * and zoom in and zoom out, and a zoom slider. The UI is based on the 
 * LargeMapControl from Google Maps (circa December 2008), but it does not
 * have any integration with Street View.
 */


/*global GKeyboardHandler, GDraggableObject*/

/**
 * @name ExtLargeMapControlOptions
 * @class This class represents optional arguments to the control.
 * @property {String} [zoomInBtnTitle="zoom in"] Specifies tooltip for 
 * zoom in button.
 * @property {String} [zoomOutBtnTitle="zoom out"] Specifies tooltip for 
 * zoom out button button.
 * @property {String} [moveNorthBtnTitle="north"] Specifies tooltip for 
 * pan north button.
 * @property {String} [moveSouthBtnTitle="south"] Specifies tooltip for 
 * pan south button.
 * @property {String} [moveEastBtnTitle="east"] Specifies tooltip for 
 * pan east button.
 * @property {String} [moveWestBtnTitle="west"] Specifies tooltip for 
 * pan west button.
 * @property {String} [returnBtnTitle="home position"] Specifies tooltip for 
 * center button that returns user to original location.
 * @property {String} [type] If set to "small", the control will consist of
 * only the zoom in/zoom out buttons.
 */

/**
 * @desc Creates an ExtLargeMapControl, with optional configuration settings.
 * @param {ExtLargeMapControlOptions} opts
 * @constructor
 */    
function ExtLargeMapControl(opts) {
  this.sliderStep = 9;
  this.imgSrc = "http://maps.google.com/mapfiles/mapcontrols3d.png";
  this.imgSmallSrc = "http://maps.google.com/mapfiles/szc3d.png";
  
  this.divTbl = {};
  this.divTbl.container = { "left" : 0, "top" : 0, "width" : 59};
  this.divTbl.topArrowBtn = { "left" : 20, "top" : 0, "width" : 18, "height" : 18};
  this.divTbl.leftArrowBtn = { "left" : 0, "top" : 20};
  this.divTbl.rightArrowBtn = { "left" : 40, "top" : 20};
  this.divTbl.bottomArrowBtn = { "left" : 20, "top" : 40};
  this.divTbl.centerBtn = { "left" : 20, "top" : 20};
  this.divTbl.zoomSlideBarContainer = { "left" : 19, "top" : 86, "width" : 22};
  this.divTbl.zoomSliderContainer = { "left" : 0, "top" : 0, "width" : 22, "height" : 14};
  this.divTbl.zoomSliderContainerImg = { "left" : 0, "top" : -384, "width" : 22, "height" : 14};
  this.divTbl.zoomOutBtnContainer = { "left" : 0, "top" : 0, "width" : 59, "height" : 23};
  this.divTbl.zoomOutBtnContainerImg = { "left" : 0, "top" : -360, "width" : 59, "height" : 23};

  opts = opts || {};
  this.zoomInBtnTitle = opts.zoomInBtnTitle || "zoom in";
  this.zoomOutBtnTitle = opts.zoomOutBtnTitle || "zoom out";
  this.moveNorthBtnTitle = opts.moveNorthBtnTitle || "north";
  this.moveSouthBtnTitle = opts.moveSouthBtnTitle || "south";
  this.moveEastBtnTitle = opts.moveEastBtnTitle || "east";
  this.moveWestBtnTitle = opts.moveWestBtnTitle || "west";
  this.homeBtnTitle = opts.homeBtnTitle || "home position";
  this.opts = opts;
  
  this.divSmallTbl = {};
  this.divSmallTbl.container = { "left" : 0, "top" : 0, "width" : 19, "height" : 42};
  this.divSmallTbl.zoomInBtn = { "left" : 0, "top" : 0, "width" : 19, "height" : 21};
  this.divSmallTbl.zoomOutBtnImg = { "left" : 0, "top" : -21, "width" : 19, "height" : 21};
  this.divSmallTbl.zoomOutBtn = { "left" : 0, "top" : 21, "width" : 19, "height" : 21};

}


/**
 * @private
 */
ExtLargeMapControl.prototype = new GControl();


/**
 * @desc Initialize the map control
 * @private
 */
ExtLargeMapControl.prototype.initialize = function (map) {

  this._map = map;

  GEvent.addListener(map, 'maptypechanged', GEvent.callback(this, this._updateZoomSliderRange));

  var _handleList = {};
  
  this._keyboardhandler = new GKeyboardHandler(map);
  var agt = navigator.userAgent.toLowerCase();
  
  this._is_ie    = ((agt.indexOf("msie") !== -1) && (agt.indexOf("opera") === -1));
  this._is_ie67  = (agt.indexOf("msie 6") !== -1 || agt.indexOf("msie 7"));
  this._is_ie8   = (agt.indexOf("msie 8") !== -1);
  this._is_gecko = (agt.indexOf('gecko') !== -1);
  this._is_opera = (agt.indexOf("opera") !== -1);

  //common image
  var commonImg = new Image();
  commonImg.src = this.imgSrc;

  var container;
  var zoomOutBtn;
  var zoomInBtn; 
  if (this.opts.type === "small") {
    // create container
    container = document.createElement("div");
    container.style.left = this.divSmallTbl.container.left + "px";
    container.style.top = this.divSmallTbl.container.top + "px";
    container.style.width = this.divSmallTbl.container.width + "px";
    container.style.height = this.divSmallTbl.container.height + "px";
    container.style.position = "absolute";
    container.style.overflow = "hidden";
    this._container = container;
    
    //zoom up button
    zoomInBtn = this.makeImgDiv_(this.imgSmallSrc, this.divSmallTbl.zoomInBtn);
    zoomInBtn.style.cursor = "pointer";
    zoomInBtn.title = this.zoomInBtnTitle;
    container.appendChild(zoomInBtn); 

    //zoom down button
    zoomOutBtn = this.makeImgDiv_(this.imgSmallSrc, this.divSmallTbl.zoomOutBtnImg);
    zoomOutBtn.style.cursor = "pointer";
    zoomOutBtn.style.overflow = "hidden";
    zoomOutBtn.style.position = "absolute";
    zoomOutBtn.style.left = this.divSmallTbl.zoomOutBtn.left + "px";
    zoomOutBtn.style.top = this.divSmallTbl.zoomOutBtn.top + "px";
    zoomOutBtn.style.width = this.divSmallTbl.zoomOutBtn.width + "px";
    zoomOutBtn.style.height = this.divSmallTbl.zoomOutBtn.height + "px";
    zoomOutBtn.title = this.zoomOutBtnTitle;
    container.appendChild(zoomOutBtn); 

    // events
    GEvent.bindDom(zoomOutBtn, "click", this, this._eventZoomOut);
    GEvent.bindDom(zoomInBtn, "click", this, this._eventZoomIn);
  } else {
    // calculation of controller size
    var currentMapType = map.getCurrentMapType();
    var minZoom = parseInt(currentMapType.getMinimumResolution(), 10);
    var maxZoom = parseInt(map.getCurrentMapType().getMaximumResolution(), 10);
    this._maxZoom = maxZoom;
    this._step = this.sliderStep;
    var ctrlHeight = (86 + 5) + (maxZoom - minZoom + 1) * this.sliderStep + 5;

    // create container
    container = this.makeImgDiv_(this.imgSrc, this.divTbl.container);
    container.style.height = (ctrlHeight + this.sliderStep + 2) + "px";
    _handleList.container = container;
    this._container = container;

    //top arrow button
    var topBtn = this.makeImgDiv_(this.imgSrc, this.divTbl.topArrowBtn);
    topBtn.style.cursor = "pointer";
    topBtn.style.left = "20px";
    topBtn.style.top = "0px";
    topBtn.title = this.moveNorthBtnTitle;
    container.appendChild(topBtn); 

    //left arrow button
    var leftBtn = topBtn.cloneNode(true);
    leftBtn.style.left = this.divTbl.leftArrowBtn.left + "px";
    leftBtn.style.top = this.divTbl.leftArrowBtn.top + "px";
    leftBtn.title = this.moveWestBtnTitle;
    container.appendChild(leftBtn); 

    //right arrow button
    var rightBtn = topBtn.cloneNode(true);
    rightBtn.style.left = this.divTbl.rightArrowBtn.left + "px";
    rightBtn.style.top = this.divTbl.rightArrowBtn.top + "px";
    rightBtn.title = this.moveEastBtnTitle;
    container.appendChild(rightBtn); 

    //bottom arrow button
    var bottomBtn = topBtn.cloneNode(true);
    bottomBtn.style.left = this.divTbl.bottomArrowBtn.left + "px";
    bottomBtn.style.top = this.divTbl.bottomArrowBtn.top + "px";
    bottomBtn.title = this.moveSouthBtnTitle;
    container.appendChild(bottomBtn); 

    //center button
    var homeBtn = topBtn.cloneNode(true);
    homeBtn.style.left = this.divTbl.centerBtn.left + "px";
    homeBtn.style.top = this.divTbl.centerBtn.top + "px";
    homeBtn.title = this.homeBtnTitle;
    container.appendChild(homeBtn); 

    _handleList.topBtn = topBtn;
    _handleList.leftBtn = leftBtn;
    _handleList.rightBtn = rightBtn;
    _handleList.bottomBtn = bottomBtn;
    _handleList.homeBtn = homeBtn;


    // zoom slider container
    var zoomSlideBarContainer = document.createElement("div");
    zoomSlideBarContainer.style.position  = "absolute";
    zoomSlideBarContainer.style.left = this.divTbl.zoomSlideBarContainer.left + "px";
    zoomSlideBarContainer.style.top = this.divTbl.zoomSlideBarContainer.top + "px";
    zoomSlideBarContainer.style.width = this.divTbl.zoomSlideBarContainer.width + "px";
    zoomSlideBarContainer.style.height = ((maxZoom - minZoom + 1) * this.sliderStep) + "px";
    zoomSlideBarContainer.style.overflow = "hidden";
    zoomSlideBarContainer.style.cursor = "pointer";
    container.appendChild(zoomSlideBarContainer); 
    _handleList.slideBar = zoomSlideBarContainer;

    // zoom slider Button
    var zoomLevel = map.getZoom();
    var zoomSliderContainer = this.makeImgDiv_(this.imgSrc, this.divTbl.zoomSliderContainerImg);
    
    zoomSliderContainer.style.top = ((maxZoom - zoomLevel) * this.sliderStep + 1) + "px";
    zoomSliderContainer.style.left = this.divTbl.zoomSliderContainer.left + "px";
    zoomSliderContainer.style.width = this.divTbl.zoomSliderContainer.width + "px";
    zoomSliderContainer.style.height = this.divTbl.zoomSliderContainer.height + "px";

    zoomSlideBarContainer.cursor = "url(http://maps.google.com/mapfiles/openhand.cur), default";
    zoomSlideBarContainer.appendChild(zoomSliderContainer); 
    _handleList.slideBarContainer = zoomSliderContainer;



    //zoomOut Btn container
    var zoomOutBtnContainer = this.makeImgDiv_(this.imgSrc, this.divTbl.zoomOutBtnContainerImg);
    zoomOutBtnContainer.style.top = (86 + (maxZoom - minZoom + 1) * this.sliderStep) + "px";
    zoomOutBtnContainer.style.left = this.divTbl.zoomOutBtnContainer.left + "px";
    zoomOutBtnContainer.style.width = this.divTbl.zoomOutBtnContainer.width + "px";
    zoomOutBtnContainer.style.height = this.divTbl.zoomOutBtnContainer.height + "px";

    zoomOutBtnContainer.cursor = "url(http://maps.google.com/mapfiles/openhand.cur), default";
    container.appendChild(zoomOutBtnContainer); 
    _handleList.zoomOutBtnContainer = zoomOutBtnContainer;


    //zoomOut button
    zoomOutBtn = document.createElement("div");
    zoomOutBtn.style.position = "absolute";
    zoomOutBtn.style.left = "20px";
    zoomOutBtn.style.top = (91 + (maxZoom - minZoom + 1) * this.sliderStep) + "px";
    zoomOutBtn.style.width = "18px";
    zoomOutBtn.style.height = "23px";
    zoomOutBtn.style.cursor = "pointer";
    zoomOutBtn.style.overflow = "hidden";
    zoomOutBtn.title = this.zoomOutBtnTitle;
    container.appendChild(zoomOutBtn); 
    _handleList.zoomOutBtn = zoomOutBtn;

    //zoomIn button
    zoomInBtn = document.createElement("div");
    zoomInBtn.style.position = "absolute";
    zoomInBtn.style.left = "20px";
    zoomInBtn.style.top = "65px";
    zoomInBtn.style.width = "18px";
    zoomInBtn.style.height = "23px";
    zoomInBtn.style.cursor = "pointer";
    zoomInBtn.style.overflow = "hidden";
    zoomInBtn.title = this.zoomInBtnTitle;
    container.appendChild(zoomInBtn); 
    _handleList.zoomInBtn = zoomInBtn;

    // events
    GEvent.bindDom(_handleList.topBtn, "click", this, this._eventTop);
    GEvent.bindDom(_handleList.leftBtn, "click", this, this._eventLeft);
    GEvent.bindDom(_handleList.rightBtn, "click", this, this._eventRight);
    GEvent.bindDom(_handleList.bottomBtn, "click", this, this._eventBottom);
    GEvent.bindDom(_handleList.homeBtn, "click", this, this._eventHome);
    GEvent.bindDom(_handleList.zoomOutBtn, "click", this, this._eventZoomOut);
    GEvent.bindDom(_handleList.zoomInBtn, "click", this, this._eventZoomIn);
    GEvent.bindDom(_handleList.slideBar, "click", this, this._eventSlideBar);
    GEvent.bind(map, "zoomend", this, this._eventZoomEnd);

    var drgOpt = {
      container : _handleList.slideBar
    };
    var drgCtrl = new GDraggableObject(_handleList.slideBarContainer, drgOpt);
    GEvent.bindDom(drgCtrl, "dragend", this, this._eventSlideDragEnd);
    this._slider =  drgCtrl;

    //set current slider position
    this._eventZoomEnd(map.getZoom(), map.getZoom());
  }

  // Save DOM element reference in the object.
  this._handleList = _handleList;

  map.getContainer().appendChild(container);
  
  return container;
};

/**
 * Update Zoomslider to ajust to Max and Min resolution on the current maptype
 * @private
**/
ExtLargeMapControl.prototype._updateZoomSliderRange = function (setMaxZoom) {
  
  
  var minZoom = parseInt(this._map.getCurrentMapType().getMinimumResolution(), 10);
  var maxZoom = parseInt(this._map.getCurrentMapType().getMaximumResolution(), 10);
  if (this.isNull(setMaxZoom) === false) {
    maxZoom = setMaxZoom;
  } else {
    this._maxZoom = maxZoom;
  }
  var ctrlHeight = (86 + 5) + (maxZoom - minZoom + 1) * this.sliderStep + 5;
  
  if (this.isNull(this._handleList) === true) {
    return;
  }

  // Update DOM elements to ajust to current Resolution range.
  this._handleList.container.style.height = (ctrlHeight + this.sliderStep + 2) + "px";
  this._handleList.slideBar.style.height = ((maxZoom - minZoom + 1) * this.sliderStep) + "px";
  this._handleList.zoomOutBtnContainer.style.top = (86 + (maxZoom - minZoom + 1) * this.sliderStep) + "px";
  this._handleList.zoomOutBtn.style.top = (91 + (maxZoom - minZoom + 1) * this.sliderStep) + "px";
  this._handleList.slideBarContainer.style.top = ((maxZoom - this._map.getZoom()) * this.sliderStep + 1) + "px";

};


/**
 * @private
 */
ExtLargeMapControl.prototype._eventTop = function () {
  this._map.panDirection(0, 1);
};


/**
 * @private
 */
ExtLargeMapControl.prototype._eventLeft = function () {
  this._map.panDirection(1, 0);
};

/**
 * @private
 */
ExtLargeMapControl.prototype._eventRight = function () {
  this._map.panDirection(-1, 0);
};

/**
 * @private
 */
ExtLargeMapControl.prototype._eventBottom = function () {
  this._map.panDirection(0, -1);
};

/**
 * @private
 */
ExtLargeMapControl.prototype._eventZoomOut = function () {
  this._map.zoomOut();
};

/**
 * @private
 */
ExtLargeMapControl.prototype._eventZoomIn = function () {
  this._map.zoomIn();
};


/**
 * @private
 */
ExtLargeMapControl.prototype._eventSlideBar = function (e) {
  var map = this._map;
  //calculate zoomlevel
  var mouseY = e.clientY;
  var slideStep = this._step;
  var maxZoom = this._maxZoom;
  var container = this._container;

  //set new zoomLevel
  var ctrlPos = this._getDomPosition(container);
  mouseY -= (ctrlPos.y + 91);
  var zoomLevel = Math.floor(maxZoom - (mouseY / slideStep));
  zoomLevel = zoomLevel < 0 ? 0 : zoomLevel;
  map.setZoom(zoomLevel);  
};

/**
  * @private
 */
ExtLargeMapControl.prototype._getDomPosition = function (that) {
  var targetEle = that;
  var pos = { x : 0, y : 0 };
  
  while (targetEle) {
    pos.x += targetEle.offsetLeft; 
    pos.y += targetEle.offsetTop; 
    targetEle = targetEle.offsetParent;

    if (targetEle && this._is_ie) {
      pos.x += (parseInt(ExtLargeMapControl.getElementStyle(targetEle, 
          "borderLeftWidth", "border-left-width"), 10) || 0);
      pos.y += (parseInt(ExtLargeMapControl.getElementStyle(targetEle, 
          "borderTopWidth", "border-top-width"), 10) || 0);
    }
  }

  if (this._is_gecko) {
    var bd = document.getElementsByTagName("BODY")[0];
    pos.x += 2 * (parseInt(ExtLargeMapControl.getElementStyle(bd, 
        "borderLeftWidth", "border-left-width"), 10) || 0);
    pos.y += 2 * (parseInt(ExtLargeMapControl.getElementStyle(bd, 
        "borderTopWidth", "border-top-width"), 10) || 0);
  }
  return pos;
};


/**
 * @private
 */
ExtLargeMapControl.getElementStyle = function (targetElm, IEStyleProp, CSSStyleProp) {
  var elem = targetElm;
  if (elem.currentStyle) {
    return elem.currentStyle[IEStyleProp];
  } else if (window.getComputedStyle) {
    var compStyle = window.getComputedStyle(elem, "");
    return compStyle.getPropertyValue(CSSStyleProp);
  }
};


/**
 * @private
 */
ExtLargeMapControl.prototype._eventSlideDragEnd = function (e) {
  //calculate zoomlevel
  var maxZoom = this._maxZoom;
  var mouseY = this._slider.top;
  var step = this._step;

  //set new zoomLevel
  var zoomLevel = Math.floor(maxZoom - (mouseY / step));
  zoomLevel = zoomLevel < 0 ? 0 : zoomLevel;
  this._map.setZoom(zoomLevel);
};


/**
 * @private
 */
ExtLargeMapControl.prototype._eventHome = function () {
  this._map.returnToSavedPosition();
};


/**
 * @private
 */
ExtLargeMapControl.prototype._eventZoomEnd = function (oldZoom, newZoom) {
  var maxZoom = this._maxZoom;
  if (newZoom < maxZoom) {
    this._updateZoomSliderRange();
  } else {
    this._updateZoomSliderRange(newZoom);
    maxZoom = newZoom;
  }
  var step = this._step;
  this._slider.moveTo(new GPoint(0, (maxZoom - newZoom) * step));
};

/**
 * @private
 * @ignore
 */
ExtLargeMapControl.prototype.copy = function () {
  return new ExtLargeMapControl(this.latlng_, this.opts);
};


/**
 * @private
 * @ignore
 */
ExtLargeMapControl.prototype.getDefaultPosition = function () {
  return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(10, 10));
};


/**
 * @private
 * @ignore
 */
ExtLargeMapControl.prototype.selectable = function () {
  return false;
};

/**
 * @private
 * @ignore
 */
ExtLargeMapControl.prototype.printable = function () {
  return true;
};

/**
 * @private
 * @desc      detect null,null string and undefined
 * @param     value
 * @return    true  :  value is nothing
 *            false :  value is not nothing
 */
ExtLargeMapControl.prototype.isNull = function (value) {
  if (!value && value !== 0 ||
     value === undefined ||
     value === "" ||
     value === null ||
     typeof value === "undefined") {
    return true;
  }
  return false;
};

/**
 * @private
 * @desc      create div element with PNG image
 */
ExtLargeMapControl.prototype.makeImgDiv_ = function (imgSrc, params) {
  var imgDiv = document.createElement("div");
  imgDiv.style.position = "absolute";
  imgDiv.style.overflow = "hidden";
  
  if (params.width) {
    imgDiv.style.width = params.width + "px";
  }
  if (params.height) {
    imgDiv.style.height = params.height + "px";
  }
  
  
  var img = null;
  if (!this._is_ie || this._is_ie8) {
    img = new Image();
    img.src = imgSrc;
  } else {
    img = document.createElement("div");
    if (params.width) {
      img.style.width = params.width + "px";
    }
    if (params.height) {
      img.style.height = params.height + "px";
    }
  }
  img.style.position = "relative";
  img.style.left = params.left + "px";
  img.style.top =  params.top + "px";
  img.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + imgSrc + "')";
  imgDiv.appendChild(img);
  return imgDiv;
};



/************ START maps/markerclusterer ********************/

/**
 * @name MarkerClusterer
 * @version 1.0
 * @author Xiaoxi Wu
 * @copyright (c) 2009 Xiaoxi Wu
 * @fileoverview
 * This javascript library creates and manages per-zoom-level
 * clusters for large amounts of markers (hundreds or thousands).
 * This library was inspired by the <a href="http://www.maptimize.com">
 * Maptimize</a> hosted clustering solution.
 * <br /><br/>
 * <b>How it works</b>:<br/>
 * The <code>MarkerClusterer</code> will group markers into clusters according to
 * their distance from a cluster's center. When a marker is added,
 * the marker cluster will find a position in all the clusters, and
 * if it fails to find one, it will create a new cluster with the marker.
 * The number of markers in a cluster will be displayed
 * on the cluster marker. When the map viewport changes,
 * <code>MarkerClusterer</code> will destroy the clusters in the viewport
 * and regroup them into new clusters.
 *
 */

/*
 * 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 MarkerClustererOptions
 * @class This class represents optional arguments to the {@link MarkerClusterer}
 * constructor.
 * @property {Number} [maxZoom] The max zoom level monitored by a
 * marker cluster. If not given, the marker cluster assumes the maximum map
 * zoom level. When maxZoom is reached or exceeded all markers will be shown
 * without cluster.
 * @property {Number} [gridSize=60] The grid size of a cluster in pixel. Each
 * cluster will be a square. If you want the algorithm to run faster, you can set
 * this value larger.
 * @property {Array of MarkerStyleOptions} [styles]
 * Custom styles for the cluster markers.
 * The array should be ordered according to increasing cluster size,
 * with the style for the smallest clusters first, and the style for the
 * largest clusters last.
 * @property {Function} [calculator] A function to calculator what will be showed
 * on cluster marker and what kind of style will cluster marker be.
 * This function auto called by Cluster. The default calculator will show number
 * of markers in a cluster. This function take one parm: markers.
 * You can add some property to marker of this markers array to calculat values.
 * This function returns an object:
 * {
 *   'text': 'The text to be showed on cluster marker',
 *   'index': 'Style index in array of MarginStylesOptions user passed.'
 * }
 */

/**
 * @name MarkerStyleOptions
 * @class An array of these is passed into the {@link MarkerClustererOptions}
 * styles option.
 * @property {String} [url] Image url.
 * @property {Number} [height] Image height.
 * @property {Number} [height] Image width.
 * @property {Array of Number} [opt_anchor] Anchor for label text, like [24, 12].
 *    If not set, the text will align center and middle.
 * @property {String} [opt_textColor="black"] Text color.
 */

/**
 * Creates a new MarkerClusterer to cluster markers on the map.
 *
 * @constructor
 * @param {GMap2} map The map that the markers should be added to.
 * @param {Array of GMarker} opt_markers Initial set of markers to be clustered.
 * @param {MarkerClustererOptions} opt_opts A container for optional arguments.
 */
function MarkerClusterer(map, opt_markers, opt_opts, imageBaseURL) {
  // private members
  var clusters_ = [];
  var map_ = map;
  var maxZoom_ = null;
  var me_ = this;
  var gridSize_ = 30;
  var sizes = [40, 56, 66, 78, 90];
  var styles_ = [];
  var leftMarkers_ = [];
  var mcfn_ = null;
  var imageBaseURL_ = imageBaseURL;

  // default calculator function
  var calculator_ = function (markers) {
    var index = 0;
    var count = markers.length;
    var dv = count;
    while (dv !== 0) {
      dv = parseInt(dv / 10, 10);
      index ++;
    }
    var stylesCount = this.getStyles().length;
    if (stylesCount < index) {
      index = this.getStyles.length;
    }
    return {
      'text': count,
      'index': index
    };
  };

  var i = 0;
  for (i = 1; i <= 5; ++i) {
    styles_.push({
      //'url': imageBaseURL_ + 'markers/multiple_mark' + i + '.png',
      'url': imageBaseURL_ + 'markers/multiple_mark1.png',
      'height': 40,
      'width': 40,
      'opt_textColor': '#FFFFFF'
    });
  }

  if (typeof opt_opts === 'object' && opt_opts !== null) {
    if (typeof opt_opts.gridSize === 'number' && opt_opts.gridSize > 0) {
      gridSize_ = opt_opts.gridSize;
    }
    if (typeof opt_opts.maxZoom === 'number') {
      maxZoom_ = opt_opts.maxZoom;
    }
    if (typeof opt_opts.styles === 'object' && opt_opts.styles !== null && opt_opts.styles.length !== 0) {
      styles_ = opt_opts.styles;
    }
    if (typeof opt_opts.calculator === 'function') {
      calculator_ = opt_opts.calculator;
    }
  }

  /**
   * Set calculator function
   * @param {Function} calculator calculator function.
   */
  this.setCalculator = function (calculator) {
    calculator_ = calculator;
  };

  /**
   * Get calculator function.
   * @return {Object}
   */
  this.getCalculator = function () {
    return GEvent.callback(this, calculator_);
  };

  /**
   * When we add a marker, the marker may not in the viewport of map, then we don't deal with it, instead
   * we add the marker into a array called leftMarkers_. When we reset MarkerClusterer we should add the
   * leftMarkers_ into MarkerClusterer.
   */
  function addLeftMarkers_() {
    if (leftMarkers_.length === 0) {
      return;
    }
    var leftMarkers = [];

    for (i = 0; i < leftMarkers_.length; ++i) {
      if (isMarkerInViewport_(leftMarkers_[i])) {
        me_.addMarker(leftMarkers_[i], true, null, null, true);
      } else {
        leftMarkers.push(leftMarkers_[i]);
      }
    }
    leftMarkers_ = leftMarkers;
  }

  /**
   * Get cluster marker images of this marker cluster. Mostly used by {@link Cluster}
   * @return {Array of String}
   */
  this.getStyles = function () {
    return styles_;
  };

  /**
   * Remove all markers from MarkerClusterer.
   */
  this.clearMarkers = function () {
    for (var i = 0; i < clusters_.length; ++i) {
      if (typeof clusters_[i] !== "undefined" && clusters_[i] !== null) {
        clusters_[i].clearMarkers();
      }
    }
    clusters_ = [];
    leftMarkers_ = [];
//    GEvent.removeListener(mcfn_);
  };

  /**
   * Check a marker, whether it is in current map viewport.
   * @private
   * @return {Boolean} if it is in current map viewport
   */
  function isMarkerInViewport_(marker) {
    return map_.getBounds().containsLatLng(marker.getLatLng());
  }

  /**
   * When reset MarkerClusterer, there will be some markers get out of its cluster.
   * These markers should be add to new clusters.
   * @param {Array of GMarker} markers Markers to add.
   */
  function reAddMarkers_(markers) {
    var len = markers.length;
    var clusters = [];
    for (var i = len - 1; i >= 0; --i) {
      me_.addMarker(markers[i].marker, true, markers[i].isAdded, clusters, true);
    }
    addLeftMarkers_();
  }

  /**
   * Add a marker.
   * @private
   * @param {GMarker} marker Marker you want to add
   * @param {Boolean} opt_isNodraw Whether redraw the cluster contained the marker
   * @param {Boolean} opt_isAdded Whether the marker is added to map. Never use it.
   * @param {Array of Cluster} opt_clusters Provide a list of clusters, the marker
   *     cluster will only check these cluster where the marker should join.
   */
  this.addMarker = function (marker, opt_isNodraw, opt_isAdded, opt_clusters, opt_isNoCheck) {
    if (opt_isNoCheck !== true) {
      if (!isMarkerInViewport_(marker)) {
        leftMarkers_.push(marker);
        return;
      }
    }

    var isAdded = opt_isAdded;
    var clusters = opt_clusters;
    var pos = map_.fromLatLngToDivPixel(marker.getLatLng());

    if (typeof isAdded !== "boolean") {
      isAdded = false;
    }
    if (typeof clusters !== "object" || clusters === null) {
      clusters = clusters_;
    }

    var length = clusters.length;
    var cluster = null;
    for (var i = length - 1; i >= 0; i--) {
      cluster = clusters[i];
      var center = cluster.getCenter();
      if (center === null) {
        continue;
      }
      center = map_.fromLatLngToDivPixel(center);

      // Found a cluster which contains the marker.
      if (pos.x >= center.x - gridSize_ && pos.x <= center.x + gridSize_ &&
          pos.y >= center.y - gridSize_ && pos.y <= center.y + gridSize_) {
        cluster.addMarker({
          'isAdded': isAdded,
          'marker': marker
        });
        if (!opt_isNodraw) {
          cluster.redraw_();
        }
        return;
      }
    }

    // No cluster contain the marker, create a new cluster.
    cluster = new Cluster(this, map);
    cluster.addMarker({
      'isAdded': isAdded,
      'marker': marker
    });
    if (!opt_isNodraw) {
      cluster.redraw_();
    }

    // Add this cluster both in clusters provided and clusters_
    clusters.push(cluster);
    if (clusters !== clusters_) {
      clusters_.push(cluster);
    }
  };

  /**
   * Remove a marker.
   *
   * @param {GMarker} marker The marker you want to remove.
   */

  this.removeMarker = function (marker) {
    for (var i = 0; i < clusters_.length; ++i) {
      if (clusters_[i].removeMarker(marker)) {
        clusters_[i].redraw_();
        return;
      }
    }
  };

  /**
   * Redraw all clusters in viewport.
   */
  this.redraw_ = function () {
    var clusters = this.getClustersInViewport_();
    for (var i = 0; i < clusters.length; ++i) {
      clusters[i].redraw_(true);
    }
  };

  /**
   * Get all clusters in viewport.
   * @return {Array of Cluster}
   */
  this.getClustersInViewport_ = function () {
    var clusters = [];
    var curBounds = map_.getBounds();
    for (var i = 0; i < clusters_.length; i ++) {
      if (clusters_[i].isInBounds(curBounds)) {
        clusters.push(clusters_[i]);
      }
    }
    return clusters;
  };

  /**
   * Get max zoom level.
   * @private
   * @return {Number}
   */
  this.getMaxZoom_ = function () {
    return maxZoom_;
  };

  /**
   * Get map object.
   * @private
   * @return {GMap2}
   */
  this.getMap_ = function () {
    return map_;
  };

  /**
   * Get grid size
   * @private
   * @return {Number}
   */
  this.getGridSize_ = function () {
    return gridSize_;
  };

  /**
   * Get total number of markers.
   * @return {Number}
   */
  this.getTotalMarkers = function () {
    var result = 0;
    for (var i = 0; i < clusters_.length; ++i) {
      result += clusters_[i].getTotalMarkers();
    }
    return result;
  };

  /**
   * Get total number of clusters.
   * @return {int}
   */
  this.getTotalClusters = function () {
    return clusters_.length;
  };

  /**
   * Collect all markers of clusters in viewport and regroup them.
   */
  this.resetViewport = function () {
    var clusters = this.getClustersInViewport_();
    var tmpMarkers = [];
    var removed = 0;

    for (var i = 0; i < clusters.length; ++i) {
      var cluster = clusters[i];
      var oldZoom = cluster.getCurrentZoom();
      if (oldZoom === null) {
        continue;
      }
      var curZoom = map_.getZoom();
      if (curZoom !== oldZoom) {

        // If the cluster zoom level changed then destroy the cluster
        // and collect its markers.
        var mks = cluster.getMarkers();
        for (var j = 0; j < mks.length; ++j) {
          var newMarker = {
            'isAdded': false,
            'marker': mks[j].marker
          };
          tmpMarkers.push(newMarker);
        }
        cluster.clearMarkers();
        removed++;
        for (j = 0; j < clusters_.length; ++j) {
          if (cluster === clusters_[j]) {
            clusters_.splice(j, 1);
          }
        }
      }
    }

    // Add the markers collected into marker cluster to reset
    reAddMarkers_(tmpMarkers);
    this.redraw_();
  };


  /**
   * Add a set of markers.
   *
   * @param {Array of GMarker} markers The markers you want to add.
   */
  this.addMarkers = function (markers) {
    for (var i = 0; i < markers.length; ++i) {
      this.addMarker(markers[i], true);
    }
    this.redraw_();
  };

  // initialize
  if (typeof opt_markers === "object" && opt_markers !== null) {
    this.addMarkers(opt_markers);
  }

  // when map move end, regroup.
  mcfn_ = GEvent.addListener(map_, "moveend", function () {
    me_.resetViewport();
  });
}

/**
 * Create a cluster to collect markers.
 * A cluster includes some markers which are in a block of area.
 * If there are more than one markers in cluster, the cluster
 * will create a {@link ClusterMarker_} and show the total number
 * of markers in cluster.
 *
 * @constructor
 * @private
 * @param {MarkerClusterer} markerClusterer The marker cluster object
 */
function Cluster(markerClusterer) {
  var center_ = null;
  var markers_ = [];
  var markerClusterer_ = markerClusterer;
  var map_ = markerClusterer.getMap_();
  var clusterMarker_ = null;
  var zoom_ = map_.getZoom();

  /**
   * Get markers of this cluster.
   *
   * @return {Array of GMarker}
   */
  this.getMarkers = function () {
    return markers_;
  };

  /**
   * If this cluster intersects certain bounds.
   *
   * @param {GLatLngBounds} bounds A bounds to test
   * @return {Boolean} Is this cluster intersects the bounds
   */
  this.isInBounds = function (bounds) {
    if (center_ === null) {
      return false;
    }

    if (!bounds) {
      bounds = map_.getBounds();
    }
    var sw = map_.fromLatLngToDivPixel(bounds.getSouthWest());
    var ne = map_.fromLatLngToDivPixel(bounds.getNorthEast());

    var centerxy = map_.fromLatLngToDivPixel(center_);
    var inViewport = true;
    var gridSize = markerClusterer.getGridSize_();
    if (zoom_ !== map_.getZoom()) {
      var dl = map_.getZoom() - zoom_;
      gridSize = Math.pow(2, dl) * gridSize;
    }
    if (ne.x !== sw.x && (centerxy.x + gridSize < sw.x || centerxy.x - gridSize > ne.x)) {
      inViewport = false;
    }
    if (inViewport && (centerxy.y + gridSize < ne.y || centerxy.y - gridSize > sw.y)) {
      inViewport = false;
    }
    return inViewport;
  };

  /**
   * Get cluster center.
   *
   * @return {GLatLng}
   */
  this.getCenter = function () {
    return center_;
  };

  /**
   * Add a marker.
   *
   * @param {Object} marker An object of marker you want to add:
   *   {Boolean} isAdded If the marker is added on map.
   *   {GMarker} marker The marker you want to add.
   */
  this.addMarker = function (marker) {
    if (center_ === null) {
      /*var pos = marker['marker'].getLatLng();
       pos = map.fromLatLngToContainerPixel(pos);
       pos.x = parseInt(pos.x - pos.x % (GRIDWIDTH * 2) + GRIDWIDTH);
       pos.y = parseInt(pos.y - pos.y % (GRIDWIDTH * 2) + GRIDWIDTH);
       center = map.fromContainerPixelToLatLng(pos);*/
      center_ = marker.marker.getLatLng();
    }
    markers_.push(marker);
  };

  /**
   * Remove a marker from cluster.
   *
   * @param {GMarker} marker The marker you want to remove.
   * @return {Boolean} Whether find the marker to be removed.
   */
  this.removeMarker = function (marker) {
    for (var i = 0; i < markers_.length; ++i) {
      if (marker === markers_[i].marker) {
        if (markers_[i].isAdded) {
          map_.removeOverlay(markers_[i].marker);
        }
        markers_.splice(i, 1);
        return true;
      }
    }
    return false;
  };

  /**
   * Get current zoom level of this cluster.
   * Note: the cluster zoom level and map zoom level not always the same.
   *
   * @return {Number}
   */
  this.getCurrentZoom = function () {
    return zoom_;
  };

  /**
   * Redraw a cluster.
   * @private
   * @param {Boolean} isForce If redraw by force, no matter if the cluster is
   *     in viewport.
   */
  this.redraw_ = function (isForce) {
    if (!isForce && !this.isInBounds()) {
      return;
    }

    // Set cluster zoom level.
    zoom_ = map_.getZoom();
    var i = 0;
    var mz = markerClusterer.getMaxZoom_();
    if (mz === null) {
      mz = map_.getCurrentMapType().getMaximumResolution();
    }
    if (zoom_ >= mz || this.getTotalMarkers() === 1) {

      // If current zoom level is beyond the max zoom level or the cluster
      // have only one marker, the marker(s) in cluster will be showed on map.
      for (i = 0; i < markers_.length; ++i) {
        if (markers_[i].isAdded) {
          if (markers_[i].marker.isHidden()) {
            markers_[i].marker.show();
          }
        } else {
          map_.addOverlay(markers_[i].marker);
          markers_[i].isAdded = true;
        }
      }
      if (clusterMarker_ !== null) {
        clusterMarker_.hide();
      }
    } else {
      // Else add a cluster marker on map to show the number of markers in
      // this cluster.
      for (i = 0; i < markers_.length; ++i) {
        if (markers_[i].isAdded && (!markers_[i].marker.isHidden())) {
          markers_[i].marker.hide();
        }
      }
      if (clusterMarker_ === null) {

        var sums = markerClusterer_.getCalculator()(this.getRealMarkers());
        clusterMarker_ = new ClusterMarker_(center_, sums, markerClusterer_.getStyles(), markerClusterer_.getGridSize_());
        map_.addOverlay(clusterMarker_);
      } else {
        if (clusterMarker_.isHidden()) {
          clusterMarker_.show();
        }
        clusterMarker_.redraw(true);
      }
    }
  };

  /**
   * Remove all the markers from this cluster.
   */
  this.clearMarkers = function () {
    if (clusterMarker_ !== null) {
      map_.removeOverlay(clusterMarker_);
    }
    for (var i = 0; i < markers_.length; ++i) {
      if (markers_[i].isAdded) {
        map_.removeOverlay(markers_[i].marker);
      }
    }
    markers_ = [];
  };

  /**
   * Get number of markers.
   * @return {Number}
   */
  this.getTotalMarkers = function () {
    return markers_.length;
  };

  /**
   * Get all real markers by array.
   * @return {GMarker}
   */
  this.getRealMarkers = function () {
    var result = [];
    for (var i = 0; i < markers_.length; ++i) {
      result.push(markers_[i].marker);
    }
    return result;
  };
}

/**
 * ClusterMarker_ creates a marker that shows the number of markers that
 * a cluster contains.
 *
 * @constructor
 * @private
 * @param {GLatLng} latlng Marker's lat and lng.
 * @param {Object} sums text and image to show:
 *   {String} text Text to show.
 *   {Number} index Image index by styles.
 * @param {Array of Object} styles The image list to be showed:
 *   {String} url Image url.
 *   {Number} height Image height.
 *   {Number} width Image width.
 *   {Array of Number} anchor Text anchor of image left and top.
 *   {String} textColor text color.
 * @param {Number} padding Padding of marker center.
 */
function ClusterMarker_(latlng, sums, styles, padding) {
/*  var index = 0;
  var dv = count;
  while (dv !== 0) {
    dv = parseInt(dv / 10, 10);
    index ++;
  }

  if (styles.length < index) {
    index = styles.length;
  }*/
  var index = sums.index;
  this.url_ = styles[index - 1].url;
  this.height_ = styles[index - 1].height;
  this.width_ = styles[index - 1].width;
  this.textColor_ = styles[index - 1].opt_textColor;
  this.anchor_ = styles[index - 1].opt_anchor;
  this.latlng_ = latlng;
  this.index_ = index;
  this.styles_ = styles;
  this.text_ = sums.text;
  this.padding_ = padding;
  this.sums_ = sums;
}

ClusterMarker_.prototype = new GOverlay();

/**
 * Initialize cluster marker.
 * @private
 */
ClusterMarker_.prototype.initialize = function (map) {
  this.map_ = map;
  var div = document.createElement("div");
  var latlng = this.latlng_;
  var pos = map.fromLatLngToDivPixel(latlng);
  pos.x -= parseInt(this.width_ / 2, 10);
  pos.y -= parseInt(this.height_ / 2, 10);
  var mstyle = "";
  if (document.all) {
    mstyle = 'filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src="' + this.url_ + '");';
  } else {
    mstyle = "background:url(" + this.url_ + ");";
  }
  if (typeof this.anchor_ === "object") {
    if (typeof this.anchor_[0] === "number" && this.anchor_[0] > 0 && this.anchor_[0] < this.height_) {
      mstyle += 'height:' + (this.height_ - this.anchor_[0]) + 'px;padding-top:' + this.anchor_[0] + 'px;';
    } else {
      mstyle += 'height:' + this.height_ + 'px;line-height:55px;';
    }
    if (typeof this.anchor_[1] === "number" && this.anchor_[1] > 0 && this.anchor_[1] < this.width_) {
      mstyle += 'width:' + (this.width_ - this.anchor_[1]) + 'px;padding-left:' + this.anchor_[1] + 'px;';
    } else {
      mstyle += 'width:' + this.width_ + 'px;text-align:center;';
    }
  } else {
    mstyle += 'height:' + this.height_ + 'px;line-height:55px;';
    mstyle += 'width:' + this.width_ + 'px;text-align:center;';
  }
  var txtColor = this.textColor_ ? this.textColor_ : 'black';

  div.style.cssText = mstyle + 'cursor:pointer;top:' + pos.y + "px;left:" +
      pos.x + "px;color:" + txtColor +  ";position:absolute;font-size:16px;" +
      'font-family:Arial,sans-serif;font-weight:bold';
  div.innerHTML = this.text_;
  map.getPane(G_MAP_MAP_PANE).appendChild(div);
  var padding = this.padding_;
  GEvent.addDomListener(div, "click", function () {
    var pos = map.fromLatLngToDivPixel(latlng);
    var sw = new GPoint(pos.x - padding, pos.y + padding);
    sw = map.fromDivPixelToLatLng(sw);
    var ne = new GPoint(pos.x + padding, pos.y - padding);
    ne = map.fromDivPixelToLatLng(ne);
    var zoom = map.getBoundsZoomLevel(new GLatLngBounds(sw, ne), map.getSize());
    map.setCenter(latlng, zoom);
  });
  this.div_ = div;
};

/**
 * Remove this overlay.
 * @private
 */
ClusterMarker_.prototype.remove = function () {
  this.div_.parentNode.removeChild(this.div_);
};

/**
 * Copy this overlay.
 * @private
 */
ClusterMarker_.prototype.copy = function () {
  return new ClusterMarker_(this.latlng_, this.sums_, this.text_, this.styles_, this.padding_);
};

/**
 * Redraw this overlay.
 * @private
 */
ClusterMarker_.prototype.redraw = function (force) {
  if (!force) {
    return;
  }
  var pos = this.map_.fromLatLngToDivPixel(this.latlng_);
  pos.x -= parseInt(this.width_ / 2, 10);
  pos.y -= parseInt(this.height_ / 2, 10);
  this.div_.style.top =  pos.y + "px";
  this.div_.style.left = pos.x + "px";
};

/**
 * Hide this cluster marker.
 */
ClusterMarker_.prototype.hide = function () {
  this.div_.style.display = "none";
};

/**
 * Show this cluster marker.
 */
ClusterMarker_.prototype.show = function () {
  this.div_.style.display = "";
};

/**
 * Get whether the cluster marker is hidden.
 * @return {Boolean}
 */
ClusterMarker_.prototype.isHidden = function () {
  return this.div_.style.display === "none";
};


/************ START maps/tooltip ********************/

/**
 * @author Marco Alionso Ramirez, marco@onemarco.com
 * @url http://onemarco.com
 * @version 1.0
 * This code is public domain
 */

/**
 * The Tooltip class is an addon designed for the Google Maps GMarker class. 
 * @constructor
 * @param {GMarker} marker
 * @param {String} content
 * @param {Number} padding
 */
function Tooltip(marker, content, padding){
	this.marker = marker;
	this.content = content;
	this.padding = padding;
	this.div = null;
	this.map = null;
}

Tooltip.prototype = new GOverlay();

Tooltip.prototype.initialize = function(map){
	
	this.div = document.createElement("div");
	var innerContainer = this.div.cloneNode(false);
	this.div.appendChild(innerContainer);
	this.div.style.position = 'absolute';
	this.div.style.visibility = 'hidden';
	
	this.shadowQuadrants = [{},{},{},{}]
	this.shadowQuadrants[0].div = document.createElement('div');
	this.shadowQuadrants[0].div.style.position = 'absolute';	
	this.shadowQuadrants[0].div.style.overflow = 'hidden';
	this.shadowQuadrants[0].img = createPngElement('/examples/Tooltip_v2/tooltip_shadow.png');
	this.shadowQuadrants[0].img.style.position = 'absolute';
	this.shadowQuadrants[0].div.appendChild(this.shadowQuadrants[0].img);
	this.shadowQuadrants[1].div = this.shadowQuadrants[0].div.cloneNode(false);
	this.shadowQuadrants[1].img = this.shadowQuadrants[0].img.cloneNode(true);
	this.shadowQuadrants[1].div.appendChild(this.shadowQuadrants[1].img);
	this.shadowQuadrants[2].div = this.shadowQuadrants[0].div.cloneNode(false);
	this.shadowQuadrants[2].img = this.shadowQuadrants[0].img.cloneNode(true);
	this.shadowQuadrants[2].div.appendChild(this.shadowQuadrants[2].img);
	this.shadowQuadrants[3].div = this.shadowQuadrants[0].div.cloneNode(false);
	this.shadowQuadrants[3].img = this.shadowQuadrants[0].img.cloneNode(true);
	this.shadowQuadrants[3].div.appendChild(this.shadowQuadrants[3].img);
	
	this.shadowQuadrants[0].div.style.right = '0px';	
	this.shadowQuadrants[0].div.style.top = '0px';
	this.shadowQuadrants[0].img.style.top = '0px';
	this.shadowQuadrants[0].img.style.right = '0px';
	this.shadowQuadrants[1].div.style.left = '0px';
	this.shadowQuadrants[1].div.style.top = '0px';
	this.shadowQuadrants[1].img.style.top = '0px';
	this.shadowQuadrants[2].div.style.left = '0px';
	this.shadowQuadrants[2].div.style.bottom = '0px';
	this.shadowQuadrants[2].img.style.bottom = '0px';
	this.shadowQuadrants[2].img.style.left = '0px';
	this.shadowQuadrants[3].div.style.right = '0px';
	this.shadowQuadrants[3].div.style.bottom = '0px';
	this.shadowQuadrants[3].img.style.bottom = '0px';	
	
	this.shadow = this.div.cloneNode(false);
	this.shadow.appendChild(this.shadowQuadrants[0].div);
	this.shadow.appendChild(this.shadowQuadrants[1].div);
	this.shadow.appendChild(this.shadowQuadrants[2].div);
	this.shadow.appendChild(this.shadowQuadrants[3].div);
	
	innerContainer.className = 'tooltip';
	
	var child = typeof this.content == 'string' ? 
		document.createTextNode(this.content) :
		this.content;
	innerContainer.appendChild(child);
	map.getPane(G_MAP_FLOAT_PANE).appendChild(this.div);
	map.getPane(G_MAP_MARKER_SHADOW_PANE).appendChild(this.shadow);
	this.map = map;
}

Tooltip.prototype.remove = function(){
	this.div.parentNode.removeChild(this.div);
}

Tooltip.prototype.copy = function(){
	var content = typeof this.content == 'string' ? this.content : this.content.cloneNode(true);
	return new Tooltip(this.marker,content,this.padding);
}

Tooltip.prototype.redraw = function(force){
	if (!force) return;
	
	//draw tooltip
	var markerPos = this.map.fromLatLngToDivPixel(this.marker.getPoint());
	var iconAnchor = this.marker.getIcon().iconAnchor;
	var xPos = Math.round(markerPos.x - this.div.clientWidth / 2);
	var yPos = markerPos.y - iconAnchor.y - this.div.clientHeight - this.padding;
	this.div.style.top = yPos + 'px';
	this.div.style.left = xPos + 'px';
	
	//draw shadow
	//calculate shadow location
	shadowAnchor = new GPoint(
		markerPos.x + Math.round((this.marker.getIcon().iconSize.height + this.padding) / 2) ,
		markerPos.y - Math.round((this.marker.getIcon().iconSize.height + this.padding) / 2) + 4);
	
	//calculate shadow dimenstions
	var shadowSize = new GSize(this.div.clientWidth + Math.round(this.div.clientHeight / 2) + 8,
		Math.round(this.div.clientHeight / 2) + 10);
	if(shadowSize.width % 2 == 1) shadowSize.width--;
	if(shadowSize.height % 2 == 1) shadowSize.height--;
	
	//apply shodaw location and dimensions
	this.shadow.style.left = (shadowAnchor.x - (shadowSize.width - shadowSize.height - 10 )/ 2) + 'px';
	this.shadow.style.top = (shadowAnchor.y - shadowSize.height) + 'px';
	this.shadow.style.width = (shadowSize.width) + 'px';
	this.shadow.style.height =  shadowSize.height + 'px';	
	
	//get quadrant dimensions
	var qHeight = shadowSize.height / 2;
	var qOddWidth = shadowSize.height > shadowSize.width ?
		shadowSize.height / 2:
		(shadowSize.width) / 2;
	var qEvenWidth = shadowSize.width - qOddWidth;
	
	//apply quadrant dimensions, calculate and apply Q2 and Q4 image offsets
	this.shadowQuadrants[0].div.style.width = qOddWidth + 'px';
	this.shadowQuadrants[0].div.style.height = qHeight + 'px';
	
	this.shadowQuadrants[1].div.style.width = qEvenWidth + 'px';
	this.shadowQuadrants[1].div.style.height = qHeight + 'px';
	this.shadowQuadrants[1].img.style.left = -(160 - shadowSize.height) + 'px';
	
	this.shadowQuadrants[2].div.style.width = qOddWidth + 'px';
	this.shadowQuadrants[2].div.style.height = qHeight + 'px';
	
	this.shadowQuadrants[3].div.style.width = qEvenWidth + 'px';
	this.shadowQuadrants[3].div.style.height = qHeight + 'px';
	this.shadowQuadrants[3].img.style.right = -(160 - shadowSize.height) +'px';	
}

Tooltip.prototype.show = function(){
	this.div.style.visibility = 'visible';
	this.shadow.style.visibility = 'visible';
}

Tooltip.prototype.hide = function(){
	this.div.style.visibility = 'hidden';
	this.shadow.style.visibility = 'hidden';
}

//utility function for png compatibility in IE6
var IS_IE = false;
var IS_LT_IE7;
//@cc_on IS_IE = true;
//@cc_on IS_LT_IE7 = @_jscript_version < 5.7;

function createPngElement(src){
	var img = document.createElement('img');
	img.setAttribute('src',src);	
	if(IS_IE && IS_LT_IE7){
		img.style.visibility = 'hidden';
		var div = document.createElement('div');
		div.appendChild(img);
		div.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\'' + src + '\',sizingMethod=\'crop\')';
		return div;
	}
	return img;	
}

/************ START maps/mapping ********************/


var SpaciaMap = { 
    suppressData: false,
    currentSearch: {},
    debug: true,
    allowedBounds: null
};

SpaciaMap.createSideBar = function() {
//    SpaciaMap.progress = new Element('div', { id: 'progress-bar', style: 'visibility:hidden;', html:'Loading properties...' });
//    SpaciaMap.progress.inject($('gmap'));

    SpaciaMap.sideBar = new Element('div', { id: 'side-bar', style: 'visibility:hidden;' });
    SpaciaMap.sideBar.inject($('gmap'));

    SpaciaMap.sideBarContent = new Element('div', { id: 'side-bar-content', style: 'visibility:hidden;' });
    SpaciaMap.sideBarContent.inject($('gmap'));
    
    SpaciaMap.sideBarContentWrapper = new Element('div', { id: 'side-bar-content-wrapper', style: 'visibility:hidden;' });
    SpaciaMap.sideBarContentWrapper.inject(SpaciaMap.sideBarContent);

    SpaciaMap.sideBarImageWrapper = new Element('div', { id: 'side-bar-image-wrapper', style: 'visibility:hidden;' });
    SpaciaMap.sideBarImageWrapper.inject(SpaciaMap.sideBarContentWrapper);

    SpaciaMap.sideBarContentText = new Element('div', { id: 'side-bar-text', style: 'visibility:hidden;' });
    SpaciaMap.sideBarContentText.inject(SpaciaMap.sideBarContentWrapper);

    SpaciaMap.sideBarMore = new Element('a', { id: 'side-bar-more', href: '#', html: 'View More Info', style: 'visibility:hidden;' });
    SpaciaMap.sideBarMore.inject(SpaciaMap.sideBarContentWrapper);

    SpaciaMap.sideBarClose = new Element('a', { id: 'side-bar-close', href: '#', html: 'X', style: 'visibility:hidden;' });
    SpaciaMap.sideBarClose.inject(SpaciaMap.sideBarContent);


    SpaciaMap.sideBarClose.addEvent('click', function(e) {
        e.stop();
        SpaciaMap.closeSideBar();
    });
}

SpaciaMap.showSideBar = function(data) {
    SpaciaMap.sideBar.fade(0.9);
    SpaciaMap.sideBarContent.fade(1);
    SpaciaMap.sideBarClose.fade(1);
    SpaciaMap.loadSidebarContent(data);
}

SpaciaMap.loadSidebarContent = function(id) {
    if (SpaciaMap.propReq.running) {
        SpaciaMap.propReq.cancel();
    }
    SpaciaMap.sideBarImageWrapper.set('html', '');
    SpaciaMap.sideBarContentText.fade(0);
    SpaciaMap.sideBarImageWrapper.fade(0);
    SpaciaMap.sideBarMore.fade(0);
    SpaciaMap.sideBarContent.addClass('loading');
    SpaciaMap.propReq.send({ data: { refno: id} });

//    var img = new Image();
//    img.onload = function(img) {
//        var newimage = new Element('img', { src: SpaciaMap.imageRoot + 'images/home/example-property.jpg', width: 120, height: 84 });
//        newimage.inject(SpaciaMap.sideBarImageWrapper);
//        SpaciaMap.sideBarImageWrapper.removeClass('loading');
//    }
//    img.src = SpaciaMap.imageRoot + 'images/home/example-property.jpg';
};

SpaciaMap.closeSideBar = function() {
    var morph = new Fx.Morph(SpaciaMap.sideBar);
    morph.start({ opacity: 0 });

    SpaciaMap.sideBarContent.fade(0);
    SpaciaMap.sideBarContentText.fade(0);
    SpaciaMap.sideBarImageWrapper.fade(0);
    SpaciaMap.sideBarMore.fade(0);
    SpaciaMap.sideBarClose.fade(0);
}

SpaciaMap.initMap = function(rootUrl, imageRoot, isZoomSearch) {
   
    $('view-list').setStyle('display','none');
    SpaciaMap.setListView();
   
    SpaciaMap.rootUrl = rootUrl;
    SpaciaMap.imageRoot = imageRoot;
    
    if (GBrowserIsCompatible()) {
        // Display the map, with some controls
        SpaciaMap.allowedBounds = new GLatLngBounds(new GLatLng(49.5,-10), new GLatLng(59,3.6));
        SpaciaMap.Gmap = new GMap2(document.getElementById("gmap"));
        
        var maptypes = SpaciaMap.Gmap.getMapTypes();
        
        for (var i=0; i<maptypes.length; i++) {
	        maptypes[i].getMinimumResolution = function() { return 5; }
        }
        
        SpaciaMap.Gmap.addControl(new GLargeMapControl());
        SpaciaMap.Gmap.addControl(new GMapTypeControl());
        SpaciaMap.Gmap.setCenter(new GLatLng(55.378051, -3.435973), 5);
        
        SpaciaMap.MarkerCluster = new MarkerClusterer(SpaciaMap.Gmap, [], null, imageRoot); 
        
//        $$('input.check').each(function(chk) {
//            chk.addEvent('click', function(){
//                SpaciaMap.drawMarkers(false, true);
//            });
//        }, this);

//        GEvent.addListener(SpaciaMap.Gmap, 'moveend', function(){
//            console.log('moveend!');
//            console.log('supress: ' + SpaciaMap.suppressData);
//            if (!SpaciaMap.suppressData){
//                SpaciaMap.drawMarkers(false, false);
//            }
//        });
        
        GEvent.addListener(SpaciaMap.Gmap, 'dragend', function(){
            //if (!SpaciaMap.suppressData){
                SpaciaMap.resetForm();
                SpaciaMap.drawMarkers(false, true);
            //}
        });

        SpaciaMap.propReq = new Request.JSON({
            url: SpaciaMap.rootUrl + 'handlers/custom/propertydetails.ashx',
            onComplete: function(result) {
                var html = '';
                if (result.success && result.success === true){
                    html = '<h4>' + result.data.types + '</h4>';
                    html += '<p><strong>Ref No:</strong> ' + result.data.refno + '</p>';
                    html += '<p><strong>Last Edited Date:</strong> ' + result.data.lastedited + '</p>';
                    html += '<p>' + result.data.street + '</p>';
                    html += '<p>' + result.data.towncity + '</p>';
                    html += '<p>' + result.data.postcode + '</p>';
                    html += '<p><strong>Size:</strong> ' + result.data.sizesqft + ' sq ft</p>';
                    if (result.data.rentother && result.data.rentother.length > 0){
                        html += '<p><strong>Rent:</strong> ' + result.data.rentother + '</p>';
                    } else {
                        html += '<p><strong>Rent:</strong> £' + result.data.rent + ' per annum</p>';
                    }
                    
                    
                    
                    SpaciaMap.sideBarMore.set('href', SpaciaMap.rootUrl +'FindAProperty/Details/' + result.data.id + '/' + result.data.refno + '.aspx');
                    
                    if (result.data.image && result.data.image.length > 0) {
                        var img = new Image(), src = SpaciaMap.imageRoot + result.data.image;
                        img.onload = function(img) {
                            SpaciaMap.sideBarImageWrapper.empty();
                            var newimage = new Element('img', { src: src, width: 120, height: 84 });
                            newimage.inject(SpaciaMap.sideBarImageWrapper);
                            SpaciaMap.sideBarImageWrapper.removeClass('loading');
                        }
                        img.src = src; 
                    }else{
                        var img = new Image(), src = SpaciaMap.imageRoot + 'generic/image-coming-soon.jpg';
                        img.onload = function(img) {
                            SpaciaMap.sideBarImageWrapper.empty();
                            var newimage = new Element('img', { src: src, width: 120 });
                            newimage.inject(SpaciaMap.sideBarImageWrapper);
                            SpaciaMap.sideBarImageWrapper.removeClass('loading');
                        }
                        img.src = src; 
                    }
                    
                    
                } else {
                    //an error occured for some reason - let client know?
                    html = '<p>An error occured retrieving this property\'s details. Please try again.</p>'
                }
                 
                
                SpaciaMap.sideBarContent.removeClass('loading');
                SpaciaMap.sideBarContentText.set('html', html);
                SpaciaMap.sideBarContentText.fade(1);
                SpaciaMap.sideBarImageWrapper.fade(1);
                SpaciaMap.sideBarImageWrapper.addClass('loading');
                SpaciaMap.sideBarMore.fade(1);
                
            }
        });

        SpaciaMap.createSideBar();
        
        SpaciaMap.currentSearch = {
            proptypes: SpaciaMap.getPropertyTypes().join(','),
            town: $('town').value,
            postcode: $('postcode').value,
            street: $('street').value,
            station: $('station').value,
            size: $('size').value,
            rent: $('rent').value
        };

        SpaciaMap.drawMarkers(isZoomSearch, true);
    }

    else {
        SpaciaMap.setMessage("Sorry, the Google Maps API is not compatible with this browser", 'error');
    }
}

SpaciaMap.drawMarkers = function(zoom, isSearch) {
    
    SpaciaMap.closeSideBar();
    SpaciaMap.MarkerCluster.clearMarkers();
    
    var bounds = SpaciaMap.Gmap.getBounds();
    var southWest = bounds.getSouthWest();
    var northEast = bounds.getNorthEast();
    
    if (isSearch){
        SpaciaMap.currentSearch = {
            proptypes: SpaciaMap.getPropertyTypes().join(','),
            town: $('town').value,
            postcode: $('postcode').value,
            street: $('street').value,
            station: $('station').value,
            size: $('size').value,
            rent: $('rent').value
        };
    }
    
    SpaciaMap.currentSearch.southwest_lng = southWest.lng();
    SpaciaMap.currentSearch.northeast_lng = northEast.lng();
    SpaciaMap.currentSearch.southwest_lat = southWest.lat();
    SpaciaMap.currentSearch.northeast_lat = northEast.lat();
    SpaciaMap.currentSearch.zoom = SpaciaMap.Gmap.getZoom();

    var request = new Request.JSON({
        url: SpaciaMap.rootUrl + 'handlers/custom/mapdata.ashx',
        data: SpaciaMap.currentSearch,
        onComplete: function(jsonObj) {
            var zoomBounds = new GLatLngBounds();
                
            var count = 0;
            var batch = [];
            
            if (jsonObj.markers.length > 0){
                for (var i = 0; i < jsonObj.markers.length; i++) {
                    var pt = new GLatLng(jsonObj.markers[i].lat, jsonObj.markers[i].lng);
                    if (zoom && zoom === true){
                        if (SpaciaMap.allowedBounds.containsLatLng(pt)) {
                            if (zoom && zoom === true){
                                zoomBounds.extend(pt);
                            }
                            var marker = SpaciaMap.createMarker(pt, jsonObj.markers[i]);
                            count++;
                            
                            batch.push(marker);
                        }
                    } else {
                        if (SpaciaMap.Gmap.getBounds().containsLatLng(pt) && SpaciaMap.allowedBounds.containsLatLng(pt)) {
                            var marker = SpaciaMap.createMarker(pt, jsonObj.markers[i]);
                            count++;
                            
                            batch.push(marker);
                        }
                    }
                    
                    
                }
                
                SpaciaMap.MarkerCluster.addMarkers(batch);
                
                if (zoom && zoom === true){
                    SpaciaMap.suppressData = true;
                    SpaciaMap.Gmap.setZoom(SpaciaMap.Gmap.getBoundsZoomLevel(zoomBounds));
                    SpaciaMap.Gmap.setCenter(zoomBounds.getCenter());
                    SpaciaMap.suppressData = false;
                }
                
                $('view-list').setStyle('display','block');
            }else{
                SpaciaMap.Gmap.setCenter(new GLatLng(55.378051, -3.435973), 5);
                $('view-list').setStyle('display','none');
            }

            if (SpaciaMap.debug) {
                SpaciaMap.setMessage('Found ' + count + ' properties.');
            }
        }
    }).send();
}

SpaciaMap.createMarker = function(pt, data) {
    var icon = new GIcon(G_DEFAULT_ICON);
    
    icon.image = SpaciaMap.imageRoot + 'markers/' + data.type + '_marker.png';
    icon.shadow = SpaciaMap.imageRoot + 'markers/marker_shadow.png';
    icon.shadowSize = new GSize(40,34);

    var marker = new GMarker(pt, {icon: icon});
    
    GEvent.addListener(marker, "click", function() {
        SpaciaMap.showSideBar(data.ref);
    });
    return marker;
}

SpaciaMap.getPropertyTypes = function(){
    var propTypes = [];
    $$('input.check').each(function(chk) {    
        if (chk.checked === true) {
            propTypes.push(chk.value);
        }
    });
    return propTypes;
};

SpaciaMap.resetForm = function(){
    $$('input.check').each(function(chk) { 
        chk.set('checked', false);   
    });
    $('town').set('value', 'Enter a town or city...');
    $('postcode').set('value', 'Enter a postcode...');
    $('street').set('value', 'Enter a street...');
    $('station').set('value', 'Enter a station name...');
    $('size').set('value', '0-1000000');
    $('rent').set('value', '0-1000000');
    
};

SpaciaMap.setMessage = function(msg, cls) {
    if (cls) {
        $('messages').set('class', cls);
    }
    $('messages').set('html', '<p>' + msg + '</p>');
};

SpaciaMap.setListView = function(){
    $('view-list').addEvent('click', function(e) {
        e.stop();
        $('resulttype_list').checked = true;
        document.forms['property-search-form'].submit();
    });
}