/*
Script: TAMap.js
  Contains <TAMap>

License:
  not free for public use
*/

/*
Class: TAMap
  A wrapper for handling GMaps with convenient defaults.

Arguments:
  elmt - required, the element that should spawn the window.
  options - optional, see options below.

Options:
  iconOptions     - (object) default options for icons
    iconPath      - (string) path to marker images (default: /img2/maps/icons/)
    name          - (string) name of the pin (default is no name)
    iconPrefix    - (string) image name prefix (default: pin)
    iconHover     - (string) image name suffix for hover imags (default: Over)
    iconExt       - (string) image file extension (default: .gif)
    iconWidth     - (integer) width of icon (default: 19)
    iconHeight    - (integer) height of icon (default: 23)
    iconAnchorX   - (integer) x offset to anchor point of image (default: 4)
    iconAnchorY   - (integer) y offset to anchor point of image (default: 22)
    shadow        - (string) name of shadow image file (default: shadowSmall.png)
  homeIconOps     - (object) options that override the iconOptions for the home icon
    name          - (string) name of the pin (default: CurrentHotel)
    iconWidth     - (integer) width of home icon (default: 23)
    iconHeight    - (integer) height of home icon (default: 27)
    iconAnchorX   - (integer) x offset to anchor point of home icon (default: 4)
    iconAnchorY   - (integer) y offset to anchor point of home icon (default: 26)
    shadow        - (string) name of shadow image file for home icon (default: shadowLarge.png)
    shadowWidth   - (integer) width of home shadow icon (default: 25)
    shadowHeight  - (integer) height of home shadow icon (default: 29)
  homeIcon        - (string) name of icon for original location (default: CurrentHotel)
  homeMinZoom     - (integer) minimum zoom at which to show home icon (default: 7)
  homeMaxZoom     - (integer) maximum zoom at which to show home icon (default: 17)
  homeOps         - (object) additional options for home marker (default: {})
  minZoom         - (integer) minimum zoom at which to show icons (default: 10)
  maxZoom         - (integer) maximum zoom at which to show icons (default: 17)
  origLat         - (float) latitude of original map center (default: false)
  origLng         - (float) longitude of original map center (default: false)
  zoom            - (integer) initial zoom level, if false map is not shown until zoom is set (default: false)
  hoverOffX       - (integer) x offset for custom hover layer (default: 0)
  hoverOffY       - (integer) y offset for custom hover layer (default: 0)
  padLeft         - (integer) width of "dead space" from left edge of map (default: 67)
  padTop          - (integer) width of "dead space" from top edge of map (default: 30) 
  servlet         - (string) name of the servlet that the floating map request comes from (default: "")
  smallMap        - (boolean) use the SmallMapControl or LargeMapControl
  typeControl     - (boolean) use the MapTypeControl
  scaleControl    - (boolean) use the ScaleControl
  panControl      - (boolean) use the MapControl at all
  staticMap       - (boolean) disable move/zoom
  enableInfoWindows - (boolean) enable mouse over MapInfoWindows

Events:
  beforeMove  - fired when the user starts to move the map (by drag, pan, or zoom)
  onMove      - fired when the user moves the map (by drag, pan, or zoom) [arguments: zoomChanged]
  onDrag      - fired when the user drags the map
  onZoom      - fired when the zoom level changes [arguments: oldZoom, newZoom]
  noPins      - fires when all pins hidden
  homePinOnly - fires when the zoom level causes only the home pin to be visible
  allPins     - fired when all pins become visible
*/
var TAMap = new Class({
  options: {
    iconOptions: {
      iconPath:     "/img2/maps/icons/",
      name:         '',
      iconPrefix:   "pin",
      iconHover:    "Over",
      iconExt:      ".gif",
      iconWidth:    19,
      iconHeight:   23,
      iconAnchorX:  4,
      iconAnchorY:  22,
      shadowPath:   null,
      shadow:       'shadowSmall.png',
      shadowWidth:  21,
      shadowHeight: 25,
      numbered:     false
    },
    homeIconOps: {
      name:        'CurrentHotel',
      iconWidth:    23,
      iconHeight:   27,
      iconAnchorX:  4,
      iconAnchorY:  26,
      shadow:       'shadowLarge.png',
      shadowWidth:  25,
      shadowHeight: 29
    },
    homeIcon:     true,
    homeMinZoom:  7,
    homeMaxZoom:  17,
    homeOps:      {},
    minZoom:      10,
    maxZoom:      17,
    origLat:      false,
    origLng:      false,
    zoom:         false,
    hoverOffX:    0,
    hoverOffY:    0,
    padLeft:      0,
    padTop:       0,
    servlet:      "",
    smallMap:     false,
    typeControl:  true,
    scaleControl: true,
    panControl:   true,
    pinCount:     25,
    staticMap:    false,
    enableInfoWindows: true
  },
  
  infoWindowsEnabled: function() { return this.options.enableInfoWindows; },
  
  initialize: function(elmt, options) {
    this.setOptions(options);
    this.src      = elmt;
    this.icons    = {};
    this.markers  = {};

    this.gmap2 = new GMap2(this.src);
    
    // add map controls
    if (this.options.panControl) {
      if (this.options.smallMap) {
        this.smallPanControl = new GSmallMapControl();
        this.gmap2.addControl(this.smallPanControl);
      }
      else {
        this.largePanControl = new GLargeMapControl();
        this.gmap2.addControl(this.largePanControl);
      }
    }
    if (this.options.typeControl) {
      this.typeControl = new GMapTypeControl();
      this.gmap2.addControl(this.typeControl);
    }
    if (this.options.scaleControl) {
      this.scaleControl = new GScaleControl();
      this.gmap2.addControl(this.scaleControl);
    }
    
    // position and zoom
    if (this.options.origLat && this.options.origLng)
    {
      this.gmap2.setCenter(new GLatLng(this.options.origLat, this.options.origLng));
    }
    if (this.options.zoom)
    {
      this.gmap2.setZoom(this.options.zoom);
    }
    this.gmap2.savePosition();
        
    // home icon
    if (this.options.homeIcon) {
      this.addIcon('home', this.options.homeIconOps);
      if (this.options.origLat && this.options.origLng) {
        this.homeMarker = this.createMarker($merge({lat: this.options.origLat, lng: this.options.origLng}, this.options.homeOps), 'home',
          {zIndexProcess: function () {return 65535;} });
        this.gmap2.addOverlay(this.homeMarker);
      }
    }

    // alt icons
    if (this.options.altIcons) {
      this.addIcon('alt', this.options.altIconOps);
      this.altMarkers = new Array();
      for (var i = 0; i < this.options.altLocs.length; i++) {
        var marker = this.createMarker(this.options.altLocs[i], 'alt',
          {zIndexProcess: function () {return 65535;} });
        this.altMarkers.push(marker);
        this.gmap2.addOverlay(marker);
      }
    }
    
    // set up various event listeners if needed
    if (this.options.staticMap) {
      this.gmap2.disableDragging();
      this.gmap2.disableDoubleClickZoom();
      this.eventsAdded = false;
    }
    else {
      this.moveStartHandler = this.beforeMove.bind(this);
      this.moveEndHandler = this.onMove.bind(this);
      this.dragEndHandler = this.onDrag.bind(this);
      this.zoomEndHandler = this.onZoom.bind(this);
      this.gel_moveStart  = GEvent.addListener(this.gmap2, "movestart", this.moveStartHandler);
      this.gel_moveEnd    = GEvent.addListener(this.gmap2, "moveend", this.moveEndHandler);
      this.gel_dragEnd    = GEvent.addListener(this.gmap2, "dragend", this.dragEndHandler);
      this.gel_zoomEnd    = GEvent.addListener(this.gmap2, "zoomend", this.zoomEndHandler);
      this.eventsAdded = true;
    }
  },
  
  // function: recenter - pan the map to center on the home marker
  recenter: function() {
    this.gmap2.panTo(new GLatLng(this.options.origLat, this.options.origLng));
  },
  
  // function: reset - reset the map center and zoom
  reset: function() {
    this.move(this.options.origLat, this.options.origLng, this.options.zoom);
    this.fireEvent("onReset");
  },
  
  _move: function(lat, lng, zoom) {
    this.gmap2.setCenter(new GLatLng(lat, lng));
    this.gmap2.setZoom(zoom);
    if (this.homeMarker) this.gmap2.addOverlay(this.homeMarker);
  },
  
  // function: move - move/zoom the map without firing any events
  move: function(lat, lng, zoom) {
    this.suppressEvents = true;
    this._move(lat, lng, zoom);
    this.suppressEvents = false;
  },

  // function: mapCenter - center of map not including "dead space"
  mapCenter: function() {
    if (this.options.padLeft == 0 && this.options.padTop == 0) return this.gmap2.getCenter();
    return this.gmap2.fromContainerPixelToLatLng(new GPoint(
      Math.round(this.mapWidth() / 2) + this.options.padLeft,
      Math.round(this.mapHeight() / 2) + this.options.padTop
    ));
  },

  // function: mapBounds - bounds of a map
  mapBounds: function() {
    return this.gmap2.getBounds();
  },
                        
  // function: mapWidth - width of map not including "dead space"
  mapWidth: function() {
    return this.gmap2.getSize().width - this.options.padLeft;
  },
  
  // function: mapHeight - height of map not including "dead space"
  mapHeight: function() {
    return this.gmap2.getSize().height - this.options.padTop;
  },
  
  getZoom: function() {
    return this.gmap2.getZoom();
  },
  
  // function: setHome - update the home marker
  setHome: function(ops) {
	this.removeHome();
    this.homeMarker = this.createMarker(ops, 'home', {zIndexProcess: function() {return 65535;}});
    this.gmap2.addOverlay(this.homeMarker);
    this.gmap2.setCenter(new GLatLng(ops.lat, ops.lng));
    this.fireEvent('onMove', false);
    return this;
  },
  
  // function: removeHome - remove the home marker if any
  removeHome: function() {
    if (this.homeMarker) {
      this.gmap2.removeOverlay(this.homeMarker);
      this.homeMarker = null;
    }
  },
  
  // function: addIcon - create and add an icon to the icon cache
  // parameters:
  //   type - type of marker that uses this icon
  //   options - map of icon options to override defaults
  // options:
  //     name - image file name, not including prefix or extension
  //     shadow - image file name for shadow (optional)
  //     w - icon width (optional)
  //     h - icon height (optional)
  //     x - x offset of anchor (optional)
  //     y - y offset of anchor (optional)
  //     sW - width of shadow image (optional)
  //     sH - height of shadow image (optional)
  // returns:
  //   this
  addIcon: function(type, options) {
    options = $merge(this.options.iconOptions, options);
    if (options.numbered) {
      this.icons[type] = {};
      for (i = 1; i <= this.options.pinCount; i++) {
        var icon              = new GIcon();
        icon.shadow           = cdnHost + (options.shadowPath || options.iconPath) + options.shadow;
        icon.iconSize         = new GSize(options.iconWidth, options.iconHeight);
        icon.shadowSize       = new GSize(options.shadowWidth, options.shadowHeight);
        icon.iconAnchor       = new GPoint(options.iconAnchorX, options.iconAnchorY);
        icon.infoWindowAnchor = icon.iconAnchor;
        icon.image            = cdnHost + options.iconPath + options.iconPrefix + options.name + i.toString() + options.iconExt;
        this.icons[type][i] = {
          'icon': icon,
          normal: icon.image,
          hover: cdnHost + options.iconPath + options.iconPrefix + options.name + i.toString() + options.iconHover + options.iconExt,
          num: i
        }
      }
    }
    else {
      var icon              = new GIcon();
      icon.shadow           = cdnHost + (options.shadowPath || options.iconPath) + options.shadow;
      icon.iconSize         = new GSize(options.iconWidth, options.iconHeight);
      icon.shadowSize       = new GSize(options.shadowWidth, options.shadowHeight);
      icon.iconAnchor       = new GPoint(options.iconAnchorX, options.iconAnchorY);
      icon.infoWindowAnchor = icon.iconAnchor;
      icon.image            = cdnHost + options.iconPath + options.iconPrefix + options.name + options.iconExt;
      this.icons[type]      = {
        'icon': icon,
        normal: icon.image,
        hover: cdnHost + options.iconPath + options.iconPrefix + options.name + options.iconHover + options.iconExt
      }
    }
    return this;
  },
      
  // function: createMarker - creates a GMarker and registers listeners
  // parameters:
  //   ops - options (see: https://twiki.tripadvisor.com/bin/view/Development/ClassTAMap#Marker_Options)
  //   type - type of marker
  //   markerOptions - additional options to pass to GMarker constructor
  // returns:
  //   the marker
  createMarker: function(ops, type, markerOptions) {
    var gicon = this.icons[type];
    if (!gicon.icon) {
      gicon = this.icons[type][ops.num];
      var pinCnt = this.options.pinCount;
      markerOptions = $merge(markerOptions, {zIndexProcess: function () {return pinCnt - gicon.num;} });
    } else {
      if(type.indexOf('sponsor') > -1) {  // sponsor, sponsorIC, etc. on TOP (z-index)
        markerOptions = $merge(markerOptions, {zIndexProcess: function () {return 1;} });
      }
    }
    var point   = new GLatLng(ops.lat, ops.lng);
    var marker  = new GMarker(point, $merge({icon: gicon.icon}, markerOptions));
    marker.type = type;
    marker.ops  = ops;
    GEvent.addListener(marker, "mouseover", this.markerOver.bind(this, [marker, gicon]));
    GEvent.addListener(marker, "mouseout",  this.markerOut.bind(this, [marker, gicon]));
    if (ops.url) GEvent.addListener(marker, "click", this.markerClick.bind(this, marker));
    return marker;
  },
  
  // function: markerInPixels - returns the position of the marker relative to the top-left corner
  //   of the map view
  // returns:
  //   GPoint (see http://code.google.com/apis/maps/documentation/reference.html#GPoint)
  markerInPixels: function(marker) {
    return this.gmap2.fromLatLngToContainerPixel(marker.getLatLng());
  },
  
  markerOver: function(marker, icon) {
    marker.setImage(icon.hover);
    if (marker.ops.customHover && !(marker.hoverDiv && marker.hoverDiv.inDocument()))
      marker.hoverPending = this.customHover.delay(250, this, marker);
    if (marker.unhoverPending) clearTimeout(marker.unhoverPending);
    return this;
  },
  
  markerOut: function(marker, icon) {
    marker.setImage(icon.normal);
    if (marker.hoverPending) {
      clearTimeout(marker.hoverPending);
      marker.hoverPending = false;
    }
    else if (marker.hoverDiv) {
      this.customHoverOut(marker);
    }
    return this;
  },
  
  markerClick: function(marker) {
    if (marker.ops.pid) Cookie.set('NPID', marker.ops.pid, {domain: cookieDomain, time:5});
    if (marker.ops.customHover.callback) {
      this.customHoverOff(marker);
      window[marker.ops.customHover.callback](marker.ops.url);
    }
    else {
      if (marker.ops.url) document.location = marker.ops.url;
    }
  },
  
  customHover: function(marker) {
    marker.hoverPending = false;

    if (marker.hoverDiv) {
      marker.hoverDiv.setStyles({left: '-999em', top: '-999em'});
    }
    else {
      marker.hoverDiv = new Element('div', {
        'class': 'js_hvrNfo',
        styles: {position: 'absolute', left: '-999em', top: '-999em'},
        events: {
          mouseenter: this.customHoverOver.bind(this, marker),
          mouseleave: this.customHoverOut.bind(this, marker)
        }
      });
      if (marker.ops.customHover.title)
      {
        var elmtOptions = marker.ops.customHover.titleUrl ? 
                          {'class':'hvrTtl', href: marker.ops.customHover.titleUrl} : 
                          {'class':'hvrTtl noLink'};
        if (marker.ops.customHover.target) {
            elmtOptions['target'] = marker.ops.customHover.target;
        }
        marker.hoverDiv.adopt(
          new Element('a', elmtOptions)
           .setText(marker.ops.customHover.title)
           .addEvent('click', this.customHoverTitleClick.bindWithEvent(this, marker)));
        if (marker.ops.customHover.contents) {
          marker.hoverDiv.adopt(
            new Element('div', {'class': 'hvrData'}).setHTML(marker.ops.customHover.contents));
        }
      }
      
      if (marker.ops.customHover.url) {
        var dyn = new Element('div', {'class':'hvrData'}).setText(JS_loading);
        marker.hoverDiv.adopt(dyn);
        new Ajax(marker.ops.customHover.url, {
          method: 'get',
          onSuccess: (function(t, x) {this.customHoverLoad.apply(this, [t, marker, dyn]);}).bind(this)
        }).request();
      }
    }
    marker.hoverDiv.injectInside(this.src);
    return this.customHoverPosition(marker).hoverDiv;
  },
  
  // function: customHoverPosition - positions the custom hover layer for a marker
  customHoverPosition: function(marker) {
    var offset = this.markerInPixels(marker);
    var l = Math.max(5, offset.x + this.options.hoverOffX);
    var t = offset.y + this.options.hoverOffY;
    var s = marker.hoverDiv.getSize().size;
    var ms = this.gmap2.getSize();
    var leftCorrection = l + s.x + 5 - ms.width;
    if (leftCorrection > 0) l -= leftCorrection;
    if (t + $pick(marker.ops.heightEstimate, s.y) + 5 > ms.height) {
      t = ms.height - offset.y + $pick(marker.getIcon().iconAnchor.y, 0);
      marker.hoverDiv.setStyles('position:absolute; left:'+l+'px; bottom:'+t+'px;');
    }
    else {
      marker.hoverDiv.setStyles({left: l, top: t});
    }
    return marker;
  },
  
  // function: customHoverLoad - called when the content has finished loading
  customHoverLoad: function(t, marker, dyn) {
    $(dyn).empty().setHTML(t);
    this.customHoverPosition(marker);
    if (typeof(behavior) != 'undefined') behavior.apply(dyn);
    return this;
  },
  
  customHoverTitleClick: function(e, marker) {
    if (marker.ops.titlePID) Cookie.set('NPID', marker.ops.titlePID, {domain: cookieDomain, time:5});
    if (marker.ops.customHover.callback) {
      new Event(e).stop();
      this.customHoverOff(marker);
      window[marker.ops.customHover.callback](marker.hoverDiv.getElement('a').href);
    }

  },
  
  customHoverOver: function(marker) {
    if (marker.unhoverPending) clearTimeout(marker.unhoverPending);
  },
  
  customHoverOut: function(marker) {
    if (marker.hoverDiv.inDocument()) marker.unhoverPending = this.customHoverOff.delay(150, this, marker);
  },
  
  customHoverOff: function(marker) {
    marker.hoverDiv.remove();
  },
  
  // function: addMarker - adds a single marker to the map
  // parameters:
  //   ops - marker options
  //   type - marker type
  //   hidden - if true overlay isn't added to the map
  addMarker: function(ops, type, hidden) {
    var marker = this.createMarker(ops, type);
    if (!marker) return;
    this.markers[type] = this.markers[type] || {visible: true, markers: []};
    this.markers[type].markers.push(marker);
    if (!hidden) this.gmap2.addOverlay(marker);
    return marker;
  },
  
  // function: addMarkers - adds multiple markers of the same type
  // parameters:
  //   points - array of marker options
  //   type - marker type
  //   ops - additional options to apply to all markers
  // returns:
  //   this
  addMarkers: function(points, type, ops) {
    ops = ops || {};
    points = points.map(function(p) {
      return this.addMarker($merge(ops, p), type, this.markers[type] && !this.markers[type].visible);
    }, this);
    return this;
  },
  
  // function: replaceMarkers - replace all markers of a type with an updated set. checks current
  //   markers so only new markers are added an old ones removed
  // parameters:
  //   points - array of marker options
  //   type - marker type
  //   ops - additional options to apply to all markers
  // returns:
  //   this
  replaceMarkers: function(points, type, ops) {
    if (!$defined(this.markers[type])) return this.addMarkers(points, type, ops);
    ops = ops || {};
    var rem = this.markers[type].markers.filter(function(m){ // for each marker
      return !points.some(function(p){ // if NO new one
        return p.lat == m.ops.lat && p.lng == m.ops.lng && // has same lat/lng
          (!m.ops.num || m.ops.num == p.num);
      }, this)
    }, this);
    rem.each(function(m) {
      this.gmap2.removeOverlay(m); // remove from map
      this.markers[type].markers.remove(m); // and the internal list
    }, this);
    
    var add = points.filter(function(p) { // for each new point
      return !this.markers[type].markers.some(function(m){ // if NO old one
        return p.lat == m.ops.lat && p.lng == m.ops.lng; // has same lat/lng
      }, this);
    }, this);
    return this.addMarkers(add, type, ops); // then add them
  },
  
  // function: clear - clears all markers except the home marker
  clear: function() {
    for (var type in this.markers) {
      this.markers[type].markers.each(function(m) {this.gmap2.removeOverlay(m);}, this);
      this.markers[type].markers = [];
    }
    for (var type in this.sMarkers) {
      this.sMarkers[type].markers.each(function(m) {this.gmap2.removeOverlay(m.toGOverlay());}, this);
      this.sMarkers[type].markers = [];
    }
    return this;
  },
  
  beforeMove: function() {
    if (this.suppressEvents) return;
    this.gmap2.savePosition();
    this.previousZoom = this.gmap2.getZoom();
    this.fireEvent("beforeMove");
  },
  
  onMove: function() {
    if (this.suppressEvents) return;
    this.fireEvent("onMove", [this.previousZoom != this.gmap2.getZoom()]);
  },
  
  onDrag:function() {
    if (this.suppressEvents) return;
    this.fireEvent("onDrag");
  },
  
  onZoom: function(oldZoom, newZoom) {
    if (oldZoom >= this.options.minZoom && newZoom < this.options.minZoom) {
      // zoomed OUT past min zoom boundary
      for (var type in this.markers) {
        this.markers[type].markers.each(function(m) {this.gmap2.removeOverlay(m);}, this);
      }
      if (!this.suppressEvents) this.fireEvent("homePinOnly");
    }
    else if (oldZoom < this.options.minZoom && newZoom >= this.options.minZoom) {
      // zoomed IN past min zoom boundary
      for (var type in this.markers) {
        this.markers[type].markers.each(function(m) {this.gmap2.addOverlay(m);}, this);
      }
      if (!this.suppressEvents) this.fireEvent("allPins");
    }
    
    if (oldZoom >= this.options.homeMinZoom && newZoom < this.options.homeMinZoom) {
      // zoomed OUT past home min zoom boundary
      if (this.homeMarker) this.gmap2.removeOverlay(this.homeMarker);
      if (!this.suppressEvents) this.fireEvent("noPins");
    }
    else if (oldZoom < this.options.homeMinZoom && newZoom >= this.options.homeMinZoom) {
      // zoomed IN past home min zoom boundary
      if (this.homeMarker) this.gmap2.addOverlay(this.homeMarker);
      if (!this.suppressEvents) this.fireEvent("homePinOnly");
    }
    
    if (!this.suppressEvents) this.fireEvent("onZoom", [oldZoom, newZoom]);
    return this;
  },
  
  // function: getDataForType - gets the marker options for each marker of the given type
  getDataForType: function(type) {
    if (!$defined(this.markers[type])) return [];
    var data = [];
    this.markers[type].markers.each(function (m) {
      data.push(m.ops);
    }, this);
    return data;
  },
  
  // function: hideType - hides all markers of a given type
  hideType: function(type) {
    if (!this.markers[type]) {
      this.markers[type] = {visible: false, markers: []};
      return this;
    }
    if (this.markers[type] && !this.markers[type].visible) return this;
    this.markers[type].markers.each(function(m) {
      this.gmap2.removeOverlay(m);
    }, this);
    this.markers[type].visible = false;
    return this;
  },
  
  // function: showType - shows all markers of a given type
  showType: function(type) {
    if (this.markers[type] && this.markers[type].visible) return;
    this.markers[type].markers.each(function(m) {
      this.gmap2.addOverlay(m);
    }, this);
    this.markers[type].visible = true;
    return this;
  },
  
  showOrHideType: function(show, type) {
    if (!$defined(this.markers[type])) return;
    return show ? this.showType(type) : this.hideType(type);
  },
  
  toggleType: function(type) {
    if (!$defined(this.markers[type])) return;
    return this.markers[type].visible ? this.hideType(type) : this.showType(type);
  },
  
  getServlet: function() {
  	return this.options.servlet;
  },
  
  countType: function(type) {
    if (!$defined(this.markers[type])) return 0;
    return this.markers[type].markers.length;
  }
});
TAMap.implement(new Options, new Events);
/**
  @class
    Suport class holding a width and height.
    
 @property {integer} width width
 @property {integer} height height
  
  @author Will Asche
*/
ta.support.Size = new Class({
  /**
    @param {integer} w width
    @param {integer} h height
  */
  initialize: function(w, h) {
    this.width = w;
    this.height = h || w;
  }
});
/**
 * @class ta.util.URL
 * Utility class to convert obfuscated URLs to real ones. This is to stop robots from parsing URLs
 * from the JavaScript.
 * 
 * @author wasche
 * @since  2009.02.17
 */
ta.util.URL = {
  /**
   * Reconstructs a URL from an obfuscated form. Supported forms are:
   *   * DOM:css selector -- selector must match a link, uses the href of the matched element
   *   * GMapsLC:Action,gXXX,ext -- uses GMapsLocationController, automatically adds from as current servlet
   *   * ABS:path -- makes an absolute url using the current hostname and the given path
   *   * REL:path -- maeks an absolute url using the current hostname, location path, and the given path
   *
   * @example DOM:#hotel_38519 .title a
   * @example GMapsLC:info,g60745,ext
   *
   * @param {String} str The URL to parse.
   * @returns {String} The reconstructed URL.
   */
  parse: function(str) {
  	if (/^(\w+):(.*)/.test(str)) {
      str = RegExp.$2;
      switch (RegExp.$1) {
        case 'DOM':
          elmt = document.getElement(str);
          str = (elmt && elmt.href)  ? elmt.href : '#';
          break;
        case 'GMapsLC':
          if (str.length == 0) break;
          tmp = str.split(',');
          ops = {from: pageServlet, Action: tmp.shift()};
          tmp.each(function(v){
            if (/^g(\d+)$/.test(v)) ops.g = RegExp.$1;
            else if ('ext' == v) ops.ext = 'y';
            else if (/^(.*)=(.*)$/.test(v)) ops[RegExp.$1] = RegExp.$2;
            else ops[v] = '';
          });
          str = '/GMapsLocationController?' + Object.toQueryString(ops);
          break;
        case 'ABS':
          var port = (window.location.port == "80" || window.location.port == "")? "" : (":" + window.location.port);
          if (str.charAt(0) != '/') str = '/' + str;
          str = window.location.protocol + '//' + window.location.hostname + port + str;
          break;
        case 'ABSPAGE':
          var port = (window.location.port == "80" || window.location.port == "")? "" : (":" + window.location.port);
          if (str.charAt(0) != '/') str = '/' + str;
          str = window.location.protocol + '//' + window.location.hostname + port + str;
          if (str.indexOf('.html') == -1) str += '.html';
          break;
        case 'REL':
          var path = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/'));
          if (/\/$/.test(window.location.pathname)) path = window.location.pathname;
          var port = window.location.port == "80" ? "" : (":" + window.location.port);
          if (str.charAt(0) != '/') str = '/' + str;
          str = window.location.protocol + '//' + window.location.hostname + port + path + str;
          break;
        default: 
          str = RegExp.$1 + ":" + str;
          break;
      }
    }
    return str;
  }
};
/**
  @class
  GOverlay to Mootools class bridge.
  
  @author Will Asche
*/
ta.maps.Overlay = new Class({
  options: {
  },

  /**
    @param {object} options see below
  */
  initialize: function(options) {
    this.setOptions(options);
    var overlay = function(){};
    overlay.prototype = new GOverlay();
    overlay.prototype.initialize = this.addedToMap.bind(this);
    overlay.prototype.remove = this.removedFromMap.bind(this);
    overlay.prototype.copy = this.copy.bind(this);
    overlay.prototype.redraw = this.redraw.bind(this);
    this.overlay = new overlay;
  },

  /**
    Returns the GOverlay that backs this Overlay.
    @returns {GOverlay} the overlay backing this object so it can be added to/remove from a map
  */
  toGOverlay: function() {
    return this.overlay;
  },

  /**
    Called by GMap2 when this overlay is added.
    @param {GMap2} map map this overlay was added to
  */
  addedToMap: function(map) {
    this.map = map;
  },

  /**
    Called by GMap2 when this overlay is removed.
  */
  removedFromMap: function() {
  },

  /**
    Creates a copy of this overlay.
    @returns {ta.maps.Overlay} an empty overlay
  */
  copy: function() {
    return new ta.maps.Overlay(this.options);
  },

  /**
    Called by GMap2 when this overlay should be redrawn. Noop.
    @param {boolean} force true if coordinate system has changed (map resized)
  */
  redraw: function(force) {
  }
});
ta.maps.Overlay.implement(new Options);
/**
  @class
  Custom implementation of a map marker that uses sprites. Marker assumes the sprite is laid out
  such that each row is a different size/type icon, and each row can contain many version of that
  size/type. Default options will display the Home marker.
  
  @extends ta.maps.Overlay

  @option {String}          [image=/img2/maps/icons/sprite.png] path to sprite image
  @option {integer}         [pinOffset=80]                      vertical offset to pin backgrounuds
  @option {integer}         [numberOffset=0]                    vertical offset to numbers
  @option {integer}         [numberShift=0]                     horizontal offset to numbers
  @option {integer}         [shadowOffset=141]                  vertical offset to shadows
  @option {integer}         [iconOffset=24]                     vertical offset to icons
  @option {ta.support.Size} [pinSize=23x27]                     size of pin/number/icon images
  @option {ta.support.Size} [shadowSize=23x27]                  size of shadow image
  @option {ta.support.Size} [numberSize=6x8]                    size of numbers
  @option {integer}         [numberPosition=6]                  vertical position of numbers
  @option {integer}         [pinIndex=0]                        index of pin to use
  @option {integer}         numberIndex                         index of number to use
  @option {integer}         [shadowIndex=0]                     index of shadow to use
  @option {integer}         [hoverIndex=1]                      index of pin to use on hover
  @option {integer}         [iconIndex=0]                       index of icon to use
  @option {integer}         [shadowOffsetX=2]                   horizontal offset of shadow
  @option {integer}         [shadowOFfsetY=2]                   vertical offset of shadow
  @option {integer}         [anchorX=-4]                        pin anchor horizontal offset
  @option {integer}         [anchorY=-27]                       pin anchor vertical offset
  @option {String}          url                                 url to navigate to when the marker is clicked
  @option {integer}         [infoWindowDelay=250]               time to wait before showing info window (in ms)
  @option {integer}         [infoWindowOffsetX=0]             x-axis offset for info window
  @option {integer}         [infoWindowOffsetY=0]             y-axis offset for info window
  @option {boolean}         [supportsIcon=true]                 set to false to disable icons/numbering
  
  @author Will Asche
*/
ta.maps.Marker = ta.maps.Overlay.extend({
  options: {
    image: CDNHOST + '/img2/maps/icons/sprite-v11.png',
    numberOffset: 0,
    numberShift: 0,
    iconOffset: 8,
    pinOffset: 64,
    shadowOffset: 125,
    pinSize: new ta.support.Size(23, 27),    
    shadowSize: new ta.support.Size(23, 27),
    numberSize: new ta.support.Size(6, 9),
    numberPosition: 4,
    pinIndex: 0,
    numberIndex: false,
    shadowIndex: 0,
    hoverIndex: 1,
    iconIndex: 0,
    iconHoverIndex: 0,
    iconOffsetX: 0,
    iconOffsetY: 0,
    shadowOffsetX: 2,
    shadowOffsetY: 2,
    anchorX: -4,
    anchorY: -27,    
    url: null,
    infoWindowDelay: 250,
    infoWindowOffsetX: 0,
    infoWindowOffsetY: 0,
    supportsIcon: true,
    supportsHoverIcon: false,
    markType: 'hotel'
  },
  
  typeMap: {
    'hotel': 0, 
    'coffee': 1, 
    'rail': 2,
    'copy': 3,
    'shipping': 4,
    'conventionCenters': 5,
    'airports': 6, 
    'attraction': 7, 
    'restaurant': 8, 
    'bw': 9, 
    'BEST_WESTERN': 9,
    'HAMPTON_INN': 9,
    'location': 25,
    'rental':30},
  locationPinZIndex: 4999,
  onHoverPinZIndex: 5000,

  /**
    @param {ta.maps.Map} map
    @param {GLatLng} latlng location of marker
    @param {object} options see below
  */
  initialize: function(tamap, latlng, options) {    
    this.parent(options);
    this.tamap = tamap;
    this.latlng = latlng;
  },
  
  /**
   *
   */
  updateOptions: function(options) {
    if (options.pinIndex) {
      this.options.pinIndex = options.pinIndex;
      this.setBackground(this.marker, this.options.pinIndex * (this.options.pinSize.width + 1), this.options.pinOffset);
    }
    if (options.num) {
      this.setNumberIndex(options.num);
    }
  },
  
  /**
    Returns the pixel coordinates where this marker is anchored.
    @returns {GPoint} pixel coordinates where this marker is anchored
  */
  getAnchor: function() {
    return this.map.fromLatLngToDivPixel(this.latlng);
  },
  
  /**
    Called by addedToMap just before marker is created.
    @private
    Numbered markers should be the bottom layer on any map.
    In addition, lower numbers should be layered above higher
    numbers. Location markers should be layered above all other
    non-rolled over pins. POI pins are layered between numbered
    and location pins, using the logic below. Rolling over any
    pin should bring it to the top layer, above any other pin.
    Returns the z-index, calculated by the marker type.
    @returns {Number} the z-index for this marker
  */
  getZIndex: function() {
    // case: numbered type
    if(this.options.numberIndex && this.options.numberIndex > 0)
    {
      return 100 - this.options.numberIndex;
    }
    // case: location
    else if(this.typeMap[this.options.markType] == 25)
    {
      return this.locationPinZIndex;
    }
    
    var bounds = this.map.getBounds();
    var a = bounds.getNorthEast().lat();
    var b = bounds.getSouthWest().lat();
    var topToBottom = a - b;
    var markToTop = a - this.latlng.lat();
    var layer = this.typeMap[this.options.markType];
    return (layer * 100) + Math.round(markToTop/topToBottom * 100);
  },
    
  /**
    Called by GMap2 when this overlay is added.
    @private
    @param {GMap2} map map this overlay was added to
  */
  addedToMap: function(map) {
    this.parent(map);
    if (this.infoWindow) this.infoWindow.map = this.map;
    
    var p = this.map.fromLatLngToDivPixel(this.latlng);
    this.zindex = this.getZIndex();
    
    // create elements
    this.marker = new Element('div', {
      styles: {
        position: 'absolute',
        left: p.x - this.options.anchorX,
        top: p.y - this.options.anchorY,
        width: this.options.pinSize.width,
        height: this.options.pinSize.height,
        zIndex: this.zindex
      },
      events: {
        mouseenter: this.hoverOn.bindWithEvent(this),
        mouseleave: this.hoverOff.bindWithEvent(this),
        mousedown: this.mouseDown.bindWithEvent(this)
      }
    });
    
    // sponsor pins must hover - bug: 46851
    if( this.options.customHover.url && this.options.customHover.url.indexOf("sponsorInfo") > 0 ) { 
      this.marker.addClass("forceHvr");
    }
      
    if (this.options.locId) this.marker.id = "mm" + this.options.locId;
    
    if (this.options.url) {
      this.options.url = ta.util.URL.parse(this.options.url);
    }
    
    if (this.options.url || this.tamap.markerActivatesOnClick()) {
      this.clickFn = this.mouseClick.bindWithEvent(this);
      this.marker.addEvent('click', this.clickFn);
    }

    this.iconSize = this.options.iconSize || this.options.pinSize;
    
    this.icon = new Element('div', {
      styles: {
        position: 'absolute',
        left: this.options.iconOffsetX,
        top: this.options.iconOffsetY,
        width: this.iconSize.width,
        height: this.iconSize.height,
        lineHeight: 2,
        overflow: 'hidden'
      }
    });
    this.hoverIcon = new Element('div', {
      styles: {
        position: 'absolute',
        left: this.options.iconOffsetX,
        top: this.options.iconOffsetY,
        width: this.iconSize.width,
        height: this.iconSize.height,
        lineHeight: 2,
        overflow: 'hidden'
      }
    });
    if (this.options.supportsIcon) this.icon.inject(this.marker);
    if (this.options.supportsHoverIcon) this.hoverIcon.inject(this.marker);
    
    this.shadow = new Element('div', {
      styles: {
        position: 'absolute',
        left: p.x + this.options.shadowOffsetX,
        top: p.y - this.options.anchorY + this.options.shadowOffsetY,
        width: this.options.pinSize.width,
        height: this.options.pinSize.height,
        zIndex: this.zindex
      }
    });

    // position background
    this.setBackground(this.marker, this.options.pinIndex * (this.options.pinSize.width + 1), this.options.pinOffset);
    this.setBackground(this.shadow, this.options.shadowIndex * (this.options.shadowSize.width + 1), this.options.shadowOffset);
    if ($chk(this.options.numberIndex)) {
      this.updateNumbering();
    }
    else if (this.options.supportsIcon) {
      this.setBackground(this.icon, this.options.iconIndex * (this.iconSize.width + 1), this.options.iconOffset);
    }

    // add elements
    this.map.getPane(G_MAP_MARKER_SHADOW_PANE).appendChild(this.shadow);
    this.map.getPane(G_MAP_MARKER_PANE).appendChild(this.marker);
    this.has_been_added = true;
   
  },
  
  /**
   * Change the marker style.
   * @param {Hash} type Hash of options
   * @example marker.changeType(ta.maps.MARKER_DOT_HOTEL);
   * @returns {ta.maps.Marker} This Marker
   */
  changeType: function(type) {
    var hadIcon = this.options.supportsIcon;

    ops = new Hash().extend(type); // create a copy
    ops.remove('pinIndex');
    this.setOptions(ops.obj);
    
    // update position
    var p = this.map.fromLatLngToDivPixel(this.latlng);
    this.marker.setStyles({
      left:   p.x - this.options.anchorX,
      top:    p.y - this.options.anchorY,
      width:  this.options.pinSize.width,
      height: this.options.pinSize.height
    });
    this.shadow.setStyles({
      left:   p.x + this.options.shadowOffsetX,
      top:    p.y - this.options.anchorY + this.options.shadowOffsetY,
      width:  this.options.pinSize.width,
      height: this.options.pinSize.height
    });

    // position background
    this.setBackground(this.marker, this.options.pinIndex * (this.options.pinSize.width + 1), this.options.pinOffset);
    this.setBackground(this.shadow, this.options.shadowIndex * (this.options.shadowSize.width + 1), this.options.shadowOffset);
    
    // add or remove icon as needed
    if (hadIcon && !this.options.supportsIcon) {
      this.icon.remove();
      if (this.digit2) this.digit2.remove();
      if (this.digit3) this.digit3.remove();
    }
    else if (!hadIcon && this.options.supportsIcon) {
      this.icon.inject(this.marker);
      if (this.digit2) this.digit2.inject(this.marker);
      if (this.digit3) this.digit3.inject(this.marker);
      this.updateNumbering();
    }
    
    return this;
  },
  
  /**
    Updates the elements used for numbers.
    @private
  */
  updateNumbering: function() {
    if (!this.options.supportsIcon) return;
    if (!this.options.numberIndex) return;
    
    // this.icon is used for the first digit
    var num = this.options.numberIndex;
    var num2, num3;

    this.icon.setStyles({
      top: this.options.numberPosition,
      left: (this.options.pinSize.width - this.options.numberSize.width) / 2,
      width: this.options.numberSize.width,
      height: this.options.numberSize.height,
      lineHeight: 2,
      overflow: 'hidden',
      background: 'transparent url(' + this.options.image + ') no-repeat -' + (num * this.options.numberSize.width + this.options.numberShift) + 'px -' + this.options.numberOffset + 'px'
    });
    
    if (this.options.numberIndex > 9) { // 2 digits
      num = Math.floor(num / 10);
      num2 = this.options.numberIndex % 10;
      if (!this.digit2) {
        this.digit2 = new Element('div', {
          styles: {
            position: 'absolute',
            top: this.options.numberPosition,
            width: this.options.numberSize.width,
            height: this.options.numberSize.height,
            lineHeight: 2,
            overflow: 'hidden'
          }
        }).inject(this.marker);
      }
      var l2 = (this.options.pinSize.width - this.options.numberSize.width * 2 - 1) / 2;
      this.icon.setStyles({
        left: l2,
        background: 'transparent url(' + this.options.image + ') no-repeat -' + (num * this.options.numberSize.width + this.options.numberShift) + 'px -' + this.options.numberOffset + 'px'
      });
      this.digit2.setStyles({
        left: l2 + this.options.numberSize.width + 1,
        background: 'transparent url(' + this.options.image + ') no-repeat -' + (num2 * this.options.numberSize.width + this.options.numberShift) + 'px -' + this.options.numberOffset + 'px'
      });
    }
    else if (this.digit2) {
      this.digit2.remove();
      this.digit2 = null;
    }
    
    if (this.options.numberIndex > 99) { // 3 digits
      num2 = num % 10;
      num = Math.floor(num / 10);
      num3 = this.options.numberIndex % 10;
      if (!this.digit3) {
        this.digit3 = new Element('div', {
          styles: {
            position: 'absolute',
            top: this.options.numberPosition,
            width: this.options.numberSize.width,
            height: this.options.numberSize.height,
            lineHeight: 2,
            overflow: 'hidden'
          }
        }).inject(this.marker);
      }
      var l3 = (this.options.pinSize.width - this.options.numberSize.width * 3) / 2;
      this.icon.setStyles({
        left: l3,
        background: 'transparent url(' + this.options.image + ') no-repeat -' + (num * this.options.numberSize.width + this.options.numberShift) + 'px -' + this.options.numberOffset + 'px'
      });
      this.digit2.setStyles({
        left: l3 + this.options.numberSize.width,
        background: 'transparent url(' + this.options.image + ') no-repeat -' + (num2 * this.options.numberSize.width + this.options.numberShift) + 'px -' + this.options.numberOffset + 'px'
      });
      this.digit3.setStyles({
        left: l3 + this.options.numberSize.width * 2,
        background: 'transparent url(' + this.options.image + ') no-repeat -' + (num3 * this.options.numberSize.width + this.options.numberShift) + 'px -' + this.options.numberOffset + 'px'
      });
    }
    else if (this.digit3) {
      this.digit3.remove();
      this.digit3 = null;
    }
  },
  
  /**
    Updates the background position.
    @private
    @param {Element} element to update
    @param {integer} left left offset
    @param {integer} top top offset
  */
  setBackground: function(elmt, left, top) {
    elmt.setStyle('background', 'transparent url(' + this.options.image + ') no-repeat -' + left + 'px -' + top + 'px');
  },
  
  /**
    Called by GMap2 when this overlay is removed.
    @private
  */
  removedFromMap: function() {
    this.marker.remove();
    this.shadow.remove();
    this.has_been_added = false;
  },
  
  /**
    Creates a copy of this overlay.
    @returns {ta.maps.Marker} an copy of this Marker
  */
  copy: function() {
    return new ta.maps.Marker(this.tamap, this.latlng, this.options);
  },
  
  /**
    Called by GMap2 when this overlay should be redrawn.
    @param {boolean} force true if coordinate system has changed (map resized)
  */
  redraw: function(force) {
    if (!force || !this.marker) return;
    
    // recalculate pixel position and move elements
    var p = this.map.fromLatLngToDivPixel(this.latlng);
    this.marker.setStyles({
      left: p.x - this.options.anchorX,
      top: p.y - this.options.anchorY
    });
    this.shadow.setStyles({
      left: p.x - this.options.anchorX + this.options.shadowOffsetX,
      top: p.y - this.options.anchorY + this.options.shadowOffsetY
    });
  },
  
  /**
    Changes the number on the marker.
    @param {integer} index the number to show on the marker
    @returns {ta.maps.Marker} this
  */
  setNumberIndex: function(index) {
    this.options.numberIndex = index;
    if (this.icon) {
      this.updateNumbering();
      //update the z-index of the marker and shadow based on the new icon number
      this.zindex = this.getZIndex();
      this.marker.setStyle('zIndex', this.zindex);
      this.shadow.setStyle('zIndex', this.zindex);
    }
    return this;
  },
  
  /**
    Fired when the user hovers over this marker.
    @private
    @param {Event} e the event
  */
  hoverOn: function(e) {
    this.marker.setStyles({cursor: 'pointer', zIndex: this.onHoverPinZIndex});
    if (!this.tamap.markerEventsEnabled()) return;
    if(this.options.hoverIndex>=0) {
      this.setBackground(this.marker, this.options.hoverIndex * (this.options.pinSize.width + 1), this.options.pinOffset);
    }
    if (this.options.supportsHoverIcon) {
      this.hoverIcon.inject(this.marker);
      this.setBackground(this.hoverIcon, this.options.iconHoverIndex * (this.iconSize.width + 1), this.options.iconOffset);
    }
    this.numberShift = this.options.numberShift;
    this.options.numberShift = 0;
    this.updateNumbering();
    if (!this.tamap.markerActivatesOnClick() || e.target.hasClass("forceHvr")) { this.openInfoWindow(e); }
  },
  
  openInfoWindow: function(e) {
    if (this.infoWindow && this.tamap.infoWindowsEnabled()) this.infoWindow.onMarker(e);
  },
  
  /**
    Fired when the mouse leaves this marker.
    @private
    @param {Event} e the event
  */
  hoverOff: function(e) {
    this.marker.setStyles({cursor: 'default', zIndex: this.zindex});
    if (!this.tamap.markerEventsEnabled()) return;
    if(this.options.hoverIndex>=0) {
      this.setBackground(this.marker, this.options.pinIndex * (this.options.pinSize.width + 1), this.options.pinOffset);
    }
    if (this.options.supportsHoverIcon) {
      this.icon.inject(this.marker);
      this.setBackground(this.icon, this.options.iconIndex * (this.iconSize.width + 1), this.options.iconOffset);
    }
    this.options.numberShift = this.numberShift;
    this.updateNumbering();
    if (!this.tamap.markerActivatesOnClick() || e.target.hasClass("forceHvr")) { this.closeInfoWindow(e); }
  },
  
  closeInfoWindow: function(e) {
    if (this.infoWindow && this.tamap.infoWindowsEnabled()) this.infoWindow.offMarker(e);
  },
  
  /**
    Fired when the user clicks on this marker.
    @private
    @param {Event} e the event
  */
  mouseClick: function(e) {
    if (this.tamap.markerActivatesOnClick()) {
      this.openInfoWindow(e);
    } else if (this.tamap.markerActiviatesOnBoth()) {
      if (!this.tamap.markerEventsEnabled()) return;
      
      if (this.options.callback) {
        try {
          eval(this.options.url); 
        } catch (err) {
          // nothing to see here
        }
          
        return;
      }
      
      if (this.options.url) window.location = this.options.url;      
    }
    else {
      if (!this.tamap.markerEventsEnabled()) return;
      if (this.options.url) window.location = this.options.url;
    }
  },
  
  /**
    Called when the user clicks the close button in info window.
    @private
    @param {Event} e the event
  */
  infoWinCloseClick: function(e) {
    this.closeInfoWindow(e);
  },
  
  /**
    Fired on the first stage of a user click/drag over this marker.
    @private
    @param {Event} e the event
  */
  mouseDown: function(e) {
    e.stop();
  },
  
  /**
    Creates a new InfoWindow bound to this Marker.
    @param {object} options options for the info window
  */
  bindInfoWindow: function(options) {
    this.infoWindow = new ta.maps.InfoWindow(this, $merge({delay: this.options.infoWindowDelay, offsetX: this.options.infoWindowOffsetX , offsetY: this.options.infoWindowOffsetY}, options));
  }
});
/**
  Default options for Hotel markers.
  @type Hash
*/
ta.maps.MARKER_HOTEL = {
  pinOffset: 42,
  shadowOffset: 42,
  iconOffset: 18,
  pinSize: new ta.support.Size(19, 23),
  shadowSize: new ta.support.Size(19, 23),
  pinIndex: 2,
  hoverIndex: 1,
  iconIndex: 1,
  shadowOffsetX: 2,
  shadowOffsetY: 2,
  anchorX: -4,
  anchorY: 23,
  supportsIcon: true
};

/**
  Pin options for vacation rentals with exact address
*/
ta.maps.MARKER_VR_EXACT =  $merge(ta.maps.MARKER_HOTEL, {
  pinIndex: 9,
  iconIndex: 12,
  iconOffsetX:-1
});

/**
  Pin options for vacation rentals with exact address and high rated
*/
ta.maps.MARKER_VR_EXACT_TOP =  $merge(ta.maps.MARKER_VR_EXACT, {
  pinIndex: 6,
  iconIndex: 13,
  iconHoverIndex: 12,
  supportsHoverIcon: true
});

/**
  Pin options for vacation rentals with multiple rentals at the exact same address
*/
ta.maps.MARKER_VR_MULTI_EXACT =  $merge(ta.maps.MARKER_VR_EXACT, {
  pinIndex: 1,
  hoverIndex: 2,
  pinOffset: 102,
  shadowOffset: 102,
  pinSize: new ta.support.Size(23, 28),
  shadowSize: new ta.support.Size(23, 28),
  iconSize: new ta.support.Size(19,23),
  iconOffsetY: 2,
  iconOffsetX: 1,
  iconIndex: 12,
  anchorX: 0,
  anchorY: 0,
  infoWindowOffsetX: -10,
  infoWindowOffsetY: 26
});

/**
  Pin options for vacation rentals with multiple rentals at the exact same address and high rated
*/
ta.maps.MARKER_VR_MULTI_EXACT_TOP =  $merge(ta.maps.MARKER_VR_MULTI_EXACT, {
  pinIndex: 3,
  iconIndex: 13,
  iconHoverIndex: 12,
  supportsHoverIcon: true
});

/**
  Pin options for vacation rentals with approximate address
*/
ta.maps.MARKER_VR_APPROX =  $merge(ta.maps.MARKER_VR_EXACT, {
  pinIndex: 1,
  hoverIndex: 2,
  pinOffset: 223,
  shadowOffset: 223,
  pinSize: new ta.support.Size(19, 18),
  shadowSize: new ta.support.Size(19, 18),
  iconIndex: 12
});

/**
  Pin options for vacation rentals with approximate address and high rated
*/
ta.maps.MARKER_VR_APPROX_TOP =  $merge(ta.maps.MARKER_VR_APPROX, {
  pinIndex: 3,
  iconIndex: 13,
  iconHoverIndex: 12,
  supportsHoverIcon: true
});

/**
  Pin options for vacation rentals with multiple rentals at the approximate same address
*/
ta.maps.MARKER_VR_MULTI_APPROX =  $merge(ta.maps.MARKER_VR_MULTI_EXACT, {
  pinIndex: 9,
  hoverIndex: 10,
  pinOffset: 130,
  shadowOffset: 130,
  pinSize: new ta.support.Size(23, 22),
  shadowSize: new ta.support.Size(23, 22)
});

/**
  Pin options for vacation rentals with multiple rentals at the approximate same address and high rated
*/
ta.maps.MARKER_VR_MULTI_APPROX_TOP =  $merge(ta.maps.MARKER_VR_MULTI_APPROX, {
  pinIndex: 11,
  iconIndex: 13,
  iconHoverIndex: 12,
  supportsHoverIcon: true
});

/**
  Pin options for vacation rentals pin cluster
*/
ta.maps.MARKER_VR_CLUSTER =  $merge(ta.maps.MARKER_VR_MULTI_EXACT, {
  pinIndex: 1,
  hoverIndex: 2,
  pinOffset: 243,
  shadowOffset: 243,
  pinSize: new ta.support.Size(29, 25),
  shadowSize: new ta.support.Size(29, 25),
  iconIndex: 12,
  iconOffsetY:6
});

/**
  Pin options for vacation rentals pin cluster and high rated
*/
ta.maps.MARKER_VR_CLUSTER_TOP =  $merge(ta.maps.MARKER_VR_CLUSTER, {
  pinIndex: 3,
  iconIndex: 13,
  iconHoverIndex: 12,
  supportsHoverIcon: true
});

/**
  Default options for Restaurant markers.
  @type Hash
*/
ta.maps.MARKER_RESTAURANT = $merge(ta.maps.MARKER_HOTEL, {
  pinIndex: 4,
  iconIndex: 2
});

/**
  Default options for Attraction markers.
  @type Hash
*/
ta.maps.MARKER_ATTRACTION = $merge(ta.maps.MARKER_HOTEL, {
  pinIndex: 3,
  iconIndex: 3
});

/**
  Default options for B&B markers.
  @type Hash
*/
ta.maps.MARKER_BB = $merge(ta.maps.MARKER_HOTEL, {
  pinIndex: 5,
  iconIndex: 4
});

/**
 * Default options for the small Hotel markers.
 * @type Hash
 */
ta.maps.MARKER_TINY_HOTEL = {
  pinOffset: 163,
  shadowOffset: 163,
  pinSize: new ta.support.Size(11, 14),
  shadowSize: new ta.support.Size(11, 14),
  pinIndex: 1,
  hoverIndex: 1,
  shadowOffsetX: 1,
  shadowOffsetY: 1,
  anchorX: 0,
  anchorY: 0,
  supportsIcon: false
}

/**
 * Default options for tiny dot Restaurant markers.
 * @type Hash
 */
ta.maps.MARKER_TINY_RESTAURANT = $merge(ta.maps.MARKER_TINY_HOTEL, {
  pinIndex: 2
});

/**
 * Default options for tiny dot Attraction markers.
 * @type Hash
 */
ta.maps.MARKER_TINY_ATTRACTION = $merge(ta.maps.MARKER_TINY_HOTEL, {
  pinIndex: 3
});

/**
 * Default options for tiny dot B&B markers.
 * @type Hash
 */
ta.maps.MARKER_TINY_BB = $merge(ta.maps.MARKER_TINY_HOTEL, {
  pinIndex: 4
});

/**
 * Default options for tiny dot Hotel markers.
 * @type Hash
 */
ta.maps.MARKER_DOT_HOTEL = {
  pinOffset: 153,
  shadowOffset: 153,
  pinSize: new ta.support.Size(9, 9),
  shadowSize: new ta.support.Size(9, 9),
  pinIndex: 2,
  hoverIndex: 1,
  shadowOffsetX: 1,
  shadowOffsetY: 1,
  anchorX: 0,
  anchorY: 0,
  supportsIcon: false
};

/**
 * Default options for tiny dot Restaurant markers.
 * @type Hash
 */
ta.maps.MARKER_DOT_RESTAURANT = $merge(ta.maps.MARKER_DOT_HOTEL, {
  pinIndex: 3
});

/**
 * Default options for tiny dot Attraction markers.
 * @type Hash
 */
ta.maps.MARKER_DOT_ATTRACTION = $merge(ta.maps.MARKER_DOT_HOTEL, {
  pinIndex: 4
});

/**
 * Default options for tiny dot B&B markers.
 * @type Hash
 */
ta.maps.MARKER_DOT_BB = $merge(ta.maps.MARKER_DOT_HOTEL, {
  pinIndex: 5
});
  
/**
 * Default options for hotel sponsor markers: Best Western etc.
 * @type Hash
 */
ta.maps.MARKER_HOTEL_SPONSOR = {
  pinOffset: 178,
  shadowOffset: 178,
  pinSize: new ta.support.Size(23, 27),
  shadowSize: new ta.support.Size(23, 27),
  hoverIndex: -1,
  anchorX: -2,
  anchorY: 27,
  supportsIcon: false
}

ta.maps.MARKER_BW = $merge(ta.maps.MARKER_HOTEL_SPONSOR, {
  pinIndex: 1
});

ta.maps.MARKER_HI = $merge(ta.maps.MARKER_HOTEL_SPONSOR, {
  pinIndex: 2
});

ta.maps.MARKER_BEST_WESTERN = ta.maps.MARKER_BW;
ta.maps.MARKER_HAMPTON_INN = ta.maps.MARKER_HI; 
/**
 * @class
 * Extension to deprecated TAMap that uses the new sprite marker and info window classes.
 *
 * @option {integer} [minZoom=10]                    minimum zoom at which to show icons
 * @option {integer} [maxZoom=17]                    maximum zoom at which to show icons
 * @option {float}   origLat                         latitude of original map center
 * @option {float}   origLng                         longitude of original map center
 * @option {integer} zoom                            initial zoom level, if false map is not shown until zoom is set
 * @option {integer} [padLeft=67]                    width of "dead space" from left edge of map
 * @option {integer} [padTop=30]                     width of "dead space" from top edge of map
 * @option {string}  servlet                         name of the servlet that the floating map request comes from
 * @option {boolean} [smallMap=false]                use the SmallMapControl or LargeMapControl
 * @option {boolean} [typeControl=true]              use the MapTypeControl
 * @option {boolean} [scaleControl=true]             use the ScaleControl
 * @option {boolean} [panControl=true]               use the MapControl at all
 * @option {boolean} [staticMap=false]               disable move/zoom
 * @option {boolean} [enableInfoWindows]             enable mouse over MapInfoWindows
 * @option {boolean} [markerActivateOn=2]            set to ta.overlays.ACTIVATE_HOVER (=2) for hover, ta.overlays.ACTIVATE_CLICK (=1) for click
 *
 * @author Will Asche
 */
ta.maps.Map = TAMap.extend({
  options: {
    staticMarkers: false,
    markerActivateOn: ta.overlays.ACTIVATE_HOVER
  },
  
  markerEventsEnabled: function() { return !this.options.staticMarkers },
  markerActiviatesOnBoth: function() { return this.markerActivatesOn(ta.overlays.ACTIVATE_BOTH); },
  markerActivatesOnClick: function() { return this.markerActivatesOn(ta.overlays.ACTIVATE_CLICK); },
  markerActivatesOn: function(action) { return action == this.options.markerActivateOn; },

  /**
   * @param {Element} elmt map container
   * @param {object} [options] See below.
   */
  initialize: function(elmt, options) {
    this.parent(elmt, options);
    this.zoom = this.options.zoom;
    var markerActivateOn = ta.retrieve('maps.markerActivateOn');
    if (markerActivateOn) { this.options.markerActivateOn = markerActivateOn; }
  },
  
  /**
   * Move and/or zoom the map without firing any events.
   * @param {float} lat Latitude
   * @param {float} lng Longitude
   * @param {integer} [zoom] Zoom level
   */
  _move: function(lat, lng, zoom) {
    this.gmap2.setCenter(new GLatLng(lat, lng));
    if (zoom) {
      this.gmap2.setZoom(zoom);
      this.zoom = zoom;
    }
  },
  
  /**
   * Pan the map to center on the home marker.
   */
  recenter: function() {
    if (ta.retrieve('maps.saveable')) {
      ta.store('maps.moved', false);
      this.removeOption('mc');
      this.removeOption('mz');
      this.saveOptions();
    }
    this.parent();
  },
  
  /**
   * Reset the map center and zoom
   */
  reset: function() {
    if (ta.retrieve('maps.saveable')) {
      ta.store('maps.moved', false);
      this.removeOption('mc');
      this.removeOption('mz');
      this.saveOptions();
    }
    this.parent();
  },
  
  /**
   * Reset the map center and zoom without firing any events.
   */
  _reset: function() {
    this._move(this.options.origLat, this.options.origLng, this.options.zoom);
  },
  
  /**  
   * Adds a single Marker to the map
   * @param {ta.maps.Marker} marker Marker to add
   * @param {String} type marker type
   * @param {boolean} hidden if true overlay isn't added to the map
   * @returns {ta.maps.Map} This Map
   */
  addMarker: function(marker, type, hidden) {
    this.markers[type] = this.markers[type] || {visible: true, markers: []};
    this.markers[type].markers.push(marker);
    if (!hidden) {
      this.gmap2.addOverlay(marker.toGOverlay());
    }
    return this;
  },

  /**  
   * Create and add multiple Markers
   * @param {Array} list array of marker options, same as addMarkers uses
   * @param {String} type marker type
   * @param {Object} options options passed to constructor of Marker
   * @returns {ta.maps.Map} This Map
   */
  addMarkers: function(list, type, options) {
    var m;
    list.each(function(item) {
      if (item != null)
      {
        m = new ta.maps.Marker(this, new GLatLng(item.lat, item.lng), $merge({ "markType": type }, options, item));
        if (item.num) m.setNumberIndex(item.num);
        if (item.customHover) m.bindInfoWindow(item.customHover);
        this.addMarker(m, type, this.markers[type] && !this.markers[type].visible);
      }
    }, this);
    return this;
  },
  
  /**
   * Replace all markers of a type with an updated set. Checks current markers so only new
   * markers are added and old ones removed.
   * @param {Array} list array of marker options
   * @param {String} type marker type
   * @param {Object} options additional options to apply to all markers
   * @returns {ta.maps.Map} This Map
   */
  replaceMarkers: function(list, type, options) {
    if (!$defined(this.markers[type])) {
      return this.addMarkers(list, type, options);
    }
    
    var openIW = ta.retrieve('openInfoWindow');
    var openMarker = null;
    if (openIW) { openMarker = openIW.marker; }
    
    // remove any not in the new list
    var rem = this.markers[type].markers.filter(function(m) {
      return !list.some(function(item) { // if NO new one
        return item.lat == m.latlng.lat() && item.lng == m.latlng.lng(); // has same lat/lng
      }, this);
    }, this);
    rem.each(function(m) {
      this.gmap2.removeOverlay(m.toGOverlay()); // remove from map
      this.markers[type].markers.remove(m); // and the internal list
      // hide the info window if the marker that opened it is now not on the map 
      if (openMarker != null && openMarker.latlng.lat() == m.latlng.lat() && openMarker.latlng.lng() == m.latlng.lng())
      {
        ta.remove('openInfoWindow');
        openIW.hide();
      }
    }, this);
    
    // reposition currently still open info window
    var openIW = ta.retrieve('openInfoWindow');
    if (openIW)
    {
      var openMarker = openIW.marker;
      if (openMarker.options.markType == type) 
      {
        openIW.position();
      }
    }
    
    // and any new ones
    var add = list.filter(function(item) {
      return !this.markers[type].markers.some(function(m) { // if NO old one
        return item.lat == m.latlng.lat() && item.lng == m.latlng.lng(); // has same lat/lng
      }, this);
    }, this);
    this.addMarkers(add, type, options);
    
    // adjust the numbering on any reused ones
    var update = list.filter(function(item) {
      return !add.contains(item);
    }, this);
    update.each(function(item) {
      this.markers[type].markers.each(function(m) {
        if (item.lat == m.latlng.lat() && item.lng == m.latlng.lng()) {
          m.updateOptions(item);
        }
      }, this);
    }, this);
    
    return this;
  },

  /**
   * Map move event handler.
   */
  onMove: function() {
    if (this.suppressEvents) return;
    if (ta.retrieve('maps.saveable')) {
      ta.store('maps.moved', true);
      this.setOption('mc', this.mapCenter().toUrlValue());
      this.setOption('mz', this.getZoom());
      this.saveOptions();
    }
    this.fireEvent("onMove", [this.previousZoom != this.gmap2.getZoom(), this.getZoom()]);
  },
  
  /**
   * Fired when the user starts to move the map (by drag, pan, or zoom)
   * @name ta.maps.Map#beforeMove
   * @event
   */
  /**
   * Fired when the user moves the map (by drag, pan, or zoom)
   * @name ta.maps.Map#onMove
   * @event
   * @param {boolean} zoomChanged true if zoom level changed
   */
  /**
   * Fired when the user drags the map
   * @name ta.maps.Map#onDrag
   * @event
   */
  
  /**
   * Checks zoom level requirements and hides markers if needed.
   * 
   * @param {integer} oldZoom the previous zoom level
   * @param {integer} newZoom the new zoom level
   * @returns {ta.maps.Map} This Map
   */
  onZoom: function(oldZoom, newZoom) {
    if (oldZoom >= this.options.minZoom && newZoom < this.options.minZoom) {
      // zoomed OUT past min zoom boundary
      for (var type in this.markers) {
        this.markers[type].markers.each(function(m) {this.gmap2.removeOverlay(m.toGOverlay());}, this);
      }
      if (!this.suppressEvents) this.fireEvent("homePinOnly");
    }
    else if (oldZoom < this.options.minZoom && newZoom >= this.options.minZoom) {
      // zoomed IN past min zoom boundary
      for (var type in this.markers) {
        this.markers[type].markers.each(function(m) {
          if (!m.has_been_added) this.gmap2.addOverlay(m.toGOverlay());
        }, this);
      }
      if (!this.suppressEvents) this.fireEvent("allPins");
    }
    
    // TODO add support for home marker
    
    if (!this.suppressEvents) this.fireEvent("onZoom", [oldZoom, newZoom]);
    return this;
  },
  
  /**
   * Function to execute when the zoom level changes.
   * @name ta.maps.Map#onZoom
   * @event
   * @param {integer} oldZoom previous zoom level
   * @param {integer} newZoom new zoom level
   */
  /**
   * Fires when all pins hidden
   * @name ta.maps.Map#noPins
   * @event
   */
  /**
   * Fires when the zoom level causes only the home pin to be visible
   * @name ta.maps.Map#homePinOnly
   * @event
   */
  /**
   * Fired when all pins become visible
   * @name ta.maps.Map#allPins
   * @event
   */

  /**
   * Gets the marker options for each marker of the given type
   * @param {String} marker type
   * @returns {Array} an empty array
   */
  getDataForType: function(type) {
    return [];
  },
  
  /**
   * Hides all markers of a given type
   * @param {String} marker type
   * @returns {ta.maps.Map} This Map
   */
  hideType: function(type) {
    if (!this.markers[type]) {
      this.markers[type] = {visible: false, markers: []};
      return this;
    }
    if (this.markers[type] && !this.markers[type].visible) return this;
    this.markers[type].markers.each(function(m) {
      this.gmap2.removeOverlay(m.toGOverlay());
    }, this);
    this.markers[type].visible = false;
    
    // hide the info window if the marker that opened it is now not on the map 
    var openIW = ta.retrieve('openInfoWindow');
    if (openIW)
    {
      var openMarker = openIW.marker;
      if (this.markers[openMarker.options.markType].visible == false) 
      { 
        ta.remove('openInfoWindow');
        openIW.hide();
      }
    }

    return this;
  },
  
  /**
   * Shows all markers of a given type
   * @param {String} marker type
   * @returns {ta.maps.Map} This Map
   */
  showType: function(type) {
    if (!this.markers[type]) {
      this.markers[type] = {visible: true, markers: []};
      return this;
    }
    if (this.markers[type] && this.markers[type].visible) return;
    this.markers[type].markers.each(function(m) {
      this.gmap2.addOverlay(m.toGOverlay());
    }, this);
    this.markers[type].visible = true;
    return this;
  },
  
  /**
   * Change the style of markers.
   * @param {Hash} delta Hash of marker type to options
   * @example map.changeMarkerTypes({hotel: ta.maps.MARKER_DOT_HOTEL, bb: ta.maps.MARKER_BB});
   * @returns {ta.maps.Map} This Map
   */
  changeMarkerTypes: function(delta) {
    $H(delta).each(function(options, type) {
      if (!this.markers[type]) return;
      this.markers[type].markers.each(function(m) {
        m.changeType(options);
      });
    }, this);
    return this;
  },
  
  /**
   * Function to call after the location hash is parsed.
   * @param {Hash} options The options
   */
  restoreOptions: function(options) {
    if (options.hasKey('mc') && options.hasKey('mz')) {
      var mc = options.get('mc').split(',');
      ta.store('maps.moved', true);
      this._move(parseFloat(mc[0]), parseFloat(mc[1]), parseInt(options.get('mz')));
    }
  },
                             
  /**
   * Zooms the map out until the given point is visible.
   * The center of the map will shift depending on where the new point is,
   * then the onMove handler will be fired.
   */
  zoomAndPanToFit: function(lat, lng) {
    var bounds = this.mapBounds();
    var newPoint = new GLatLng(lat, lng);
    
    if (!bounds.containsLatLng(newPoint)) {
      bounds.extend(new GLatLng(lat, lng));
      var newCenter = bounds.getCenter();
      var oldZoom = this.getZoom();
      var newZoom = this.gmap2.getBoundsZoomLevel(bounds);
      this._move(newCenter.lat(), newCenter.lng(), newZoom);
    
      GEvent.trigger(this.gmap2, "moveend", (oldZoom != newZoom), newZoom);
    }
  }
});
if ($defined(ta.util.Saveable)) ta.maps.Map.implement(new ta.util.Saveable);
/**
 * @class
 * Map Factory
 */
ta.maps.Factory = {
  /**
   * Create a morphable map. Uses the following settings:
   *   * maps.container - div to use for map
   *   * maps.type - map type, any of ta.maps.MAP_* (only specify the *, case insensitive)
   *   * maps.options - any additional options
   *   * maps.morphs - list of morph options
   *   * maps.zoom - default zoom level
   *   * maps.latitude - latitude
   *   * maps.longitude - longitude
   *   * maps.marker-data - hash of marker type to list of marker options
   * 
   * @author wasche
   */
  createMorphableMap: function() {
    // TODO: require that settings exist
    var mapDiv = $(ta.retrieve('maps.container'));
    var morphs = ta.retrieve('maps.morphs');
    var morphOptions = morphs.shift();
    var options = $merge({
      zoom: ta.asInt('maps.zoom'),
      origLat: ta.asFloat('maps.latitude'),
      origLng: ta.asFloat('maps.longitude')
    }, ta.retrieve('maps.options'));
    
    var map = new ta.maps.MorphableMap(mapDiv, options, morphOptions);

    ta.store('maps.map', map);
    
    // add any additional morphs
    while (morphs.length > 0) map.addMorph(morphs.shift());
    
    // add any pre-defined markers
    var markerData = ta.remove('maps.marker-data');
    if (markerData) {
      $H(markerData).each(function(data, type) {
        map.addMarkers(data.markers, type, $merge(ta.maps['MARKER_'+type.toUpperCase()], data.options));
      });
    }
      
    // update from location hash -- needs to happen after all the morphs are added
    if (ta.retrieve('maps.saveable')) {
      map.registerForLocationHash();
    }
  },
  
  /**
   *
   */
  updateMapMarkers: function() {
    var map = ta.retrieve('maps.map');
    if (!map) return false;
    var markerData = ta.remove('maps.marker-data');
    if (markerData) {
      markerData = $H(markerData);
      markerData.each(function(data, type) {
        map.replaceMarkers(data.markers, type, $merge(ta.maps['MARKER_'+type.toUpperCase()], data.options));
      });
    }
    return true;
  },
  
  /**
   * Reset the main map to it default state.
   * @param {Event} evnt The event
   * @param {Element} elmt The element
   * @example onclick="ta.call('ta.maps.Factory.resetMap', event)"
   */
  resetMap: function(evnt, elmt) {
    if (!ta.has('maps.map')) return;
    var map = ta.retrieve('maps.map');
    map.reset();
  },
  
  /**
   * Toggle a marker type using the value of a checkbox or radio button.
   * @param {Event} evnt The Event
   * @param {Element} elmt Checkbox or Radio button
   */
  toggleTypeByValue: function(evnt, elmt) {
    if (!['checkbox', 'radio'].contains(elmt.type)) return;
    if (!ta.has('maps.map')) return;
    
    var map = ta.retrieve('maps.map');
    map.showOrHideType(elmt.checked, elmt.value);
  },
  
  /**
   * Creates a new ta.maps.Map. Uses the following settings:
   *   * maps.container - div to use for map
   *   * maps.options - any additional options
   *   * maps.zoom - default zoom level
   *   * maps.latitude - latitude
   *   * maps.longitude - longitude
   *   * maps.marker-data - hash of marker type to list of marker options
   */
  createMap: function() {
    var container = $(ta.retrieve('maps.container'));
    if (!container) {
      if (IS_DEBUG) alert('Cannot find map container');
      return;
    }
    
    container.empty();

    var ops = $merge({
      zoom:     ta.asInt('maps.zoom'),
      origLat:  ta.asFloat('maps.latitude'),
      origLng:  ta.asFloat('maps.longitude')
    }, ta.retrieve('maps.options'));

    var map = new ta.maps.Map(container, ops);

    ta.store('maps.map', map);

    // add any pre-defined markers
    var markerData = ta.remove('maps.marker-data');
    if (markerData) {
      $H(markerData).each(function(data, type) {
        map.addMarkers(data.markers, type, $merge(ta.maps['MARKER_'+type.toUpperCase()], data.options));
      });
    }
    if (ta.has('maps.postcreate')) {
      var postcreate = ta.remove('maps.postcreate');
      postcreate(map);
    }
    return map;
  },

  /**
   *
   */
  createFloatMap: function(evnt, elmt) {
    if (evnt) evnt.preventDefault();

    if (!window['GUnload']) {
      ta.util.load.GMaps(ta.maps.Factory.createFloatMap);
      return false;
    }
    
    if (!ta.has('maps.floater')) {
      var container = $('FMRD');
      
      Cookie.set('NPID', 1102, {domain: cookieDomain, time:5});

      var ops = window[mapDivId];
      if (!ops) {
        alert("Options not found for map: " + mapDivId);
        return; // options required
      }
      var mapDiv    = container.getElement('.js_map');
      var errorDiv  = container.getElement('.js_error');
      var recenter  = container.getElement('.js_mapHome');
  
      var fwin = new FloatableWin(container);
      ta.store('maps.floater', fwin);
      fwin.show();

      var servlet = window["pageServlet"];

      // Display the map, with some controls and set the initial location
      var map = new TAMap(mapDiv, {
        origLat: ops.lat,
        origLng: ops.lng,
        zoom: 14,
        hoverOffX: -122,
        homeOps: ops.geoMap ? {} : {customHover: {title: ops.title, url: getBaseMapsUrl("info", ops.locId, servlet), titleUrl: ops.url}},
        servlet: servlet
      });
      map.origOps = ops;
      map.addIcon('hotel', {name: 'Hotel'});
      map.addIcon('restaurant', {name: 'Restaurant'});
      map.addIcon('attraction', {name: 'ThingToDo'});
      map.addIcon('sponsor_BEST_WESTERN', {
        name: 'SponsorBW2',
        shadow: 'shadowLarge.png',
        iconWidth: 23,
        iconHeight: 27,
        iconAnchorX: 4,
        iconAnchorY: 26,  
        shadowWidth: 25,
        shadowHeight: 29
      });
      map.addIcon('sponsor_HAMPTON_INN', {
        name: 'SponsorHI2',
        shadow: 'shadowLarge.png',
        iconWidth: 23,
        iconHeight: 27,
        iconAnchorX: 4,
        iconAnchorY: 26,  
        shadowWidth: 25,
        shadowHeight: 29
      });
      
      map.addEvent("noPins", function() {errorDiv.show();});
      map.addEvent("allPins", function() {errorDiv.hide();});
      map.addEvent("homePinOnly", function() {errorDiv.show();});
      map.addEvent("onReset", requestReset.bind(map, ['reset']));
      
      // recenter the map when user clicks on hotel name
      recenter.addEvent('click', map.reset.bind(map));
      
      // request nearby locations
      requestReset.apply(map, ['display']);
      
      updateTrkPxl();

      mapDiv.getParent('.inner').getElements('.js_markerClass').each(function(elmt) {
        var input = elmt.getElement('input');
        var c = input.getProperty('class');
        elmt.addEvent('click', toggleMarkerType.bindAsEventListener(mapDiv, [input, c]));
        if (!input.checked) map.hideType(c);
      });
      mapDiv.getParent('.inner').getElements('.js_markerClassSponsor').each(function(elmt) {
        var input = elmt.getElement('input');
        var c = input.getProperty('class');
        elmt.addEvent('click', toggleSponsor.bindAsEventListener(mapDiv, [input, c]));
      });
      
      // deprecated: link map to div
      mapDiv.map = map;
    }
    else {
      fwin = ta.retrieve('maps.floater');
      fwin.show();
    }

    return false;
  }
};
/**
  @class
  Customizable info window for google maps.
  
  @extends ta.maps.Overlay

  @option {String}  title                    Text to show while content is loading
  @option {String}  titleUrl                 url to navigate to when title is clicked
  @option {String}  url                      url to load contents from
  @option {integer} [heightEstimate]         an estimate of total window height after dynamic contents are loaded
  @option {integer} [delay=250]              delay in ms to wait before showing
  @option {integer} [offsetY=0]              y-axis offset for overlay
  @option {integer} [offsetX=0]              x-axis offset for overlay
  @option {integer} [timeout=250]            delay in ms to wait before hiding
  
  @author Will Asche
*/
ta.maps.InfoWindow = ta.maps.Overlay.extend({
  options: {
    title: null,
    titleUrl: null,
    url: null,
    heightEstimate: false,
    delay: 250,
    offsetY: 0,
    offsetX: 0,
    timeout: 150
  },
  
  /**
    @param {ta.maps.Marker} marker the Marker this info window is bound to
    @param {object} options see below
   */
  initialize: function(marker, options) {
    this.parent(options);
    this.marker = marker;
    this.map = this.marker.map;
    if (this.options.url) this.options.url = ta.util.URL.parse(this.options.url);
    if (this.options.titleUrl) this.options.titleUrl = ta.util.URL.parse(this.options.titleUrl);
  },

  /**
    Called by GMap2 when this overlay is added
    @param {GMap2} map map this overlay was added to
   */
  addedToMap: function(map) {
    if (this.container) {
      this.container.setStyles({left: '-999em', top: '-999em'});
    }
    else {
      var container = new Element('div', {
        'class': 'js_hvrNfo wCls',
        styles: {position: 'absolute', left: '-999em', top: '-999em'}
      });
      if (!this.marker.tamap.markerActivatesOnClick() || ( this.options.url && this.options.url.indexOf("sponsorInfo") > 0))  // sponsor pins must hover - bug: 46851
      {
        container.addEvent('mouseenter', this.hoverOn.bindWithEvent(this));
        container.addEvent('mouseleave', this.hoverOff.bindWithEvent(this));
      }
      this.container = container;

      if (this.marker.tamap.markerActivatesOnClick())
      {
        this.container.adopt(
          new Element('div', {'class': 'hvrCls'})
          .addEvent('click', this.closeClicked.bindWithEvent(this)));
      }

      if (this.options.title)
      {
        var titleElem = null;
        if (this.options.titleUrl) {
          titleElem = new Element('a', {'class': 'hvrTtl', href: this.options.titleUrl})
            .setText(this.options.title)
            .addEvent('click', this.titleClicked.bindWithEvent(this));
        }
        else {
          titleElem = new Element('span', {'class': 'hvrTtl'}).setText(this.options.title);
        }
        var wrapElem = new Element('div', {'class': 'wrap'});
        wrapElem.adopt(titleElem);
        this.container.adopt(wrapElem);
      }
      
      if (this.options.url) {
        this.info = new Element('div', {'class':'hvrData'}).setText(JS_loading);
        this.container.adopt(this.info);
        new Ajax(this.options.url, {
          method: 'get',
          onSuccess: this.setContent.bind(this)
        }).request();
      }
      
      if (this.options.contents) {
        var text = new Element('div', {'class':'hvrData'}).adopt(this.options.contents);
        this.container.adopt(text);
      }
    }

    this.map.getPane(G_MAP_FLOAT_PANE).appendChild(this.container);
    this.position();
  },

  /**
    Called by GMap2 when this overlay is removed
   */
  removedFromMap: function() {
    this.container.remove();
  },

  /**
    Create a copy of this overlay
   */
  copy: function() {
    return new ta.maps.InfoWindow(this.options);
  },

  /**
    Redraw overlay. Noop.
    @param {boolean} force true if coordinate system has changed (map resized)
   */
  redraw: function(force) {
    // only exists on mouse over, so never need to redraw on map changes
  },
  
  /**
    Recalculate the position of the container.
   */
  position: function() {
    var anchor = this.marker.getAnchor(); // position in layer
    var offset = this.map.fromLatLngToContainerPixel(this.marker.latlng); // position in container
    
    var containerSize = this.container.getSize().size;
    var mapSize = this.map.getSize();
    
    // must be at least 5 pixels from left edge
    if (offset.x + this.options.offsetX < 5) anchor.x += 5 - offset.x - this.options.offsetX;

    // cannot be within 5 pixels of bottom edge
    if (offset.y + 5 + containerSize.y + this.options.offsetY > mapSize.height) {
      anchor.y -= containerSize.y + this.marker.options.anchorY + 2 + this.options.offsetY;
    }
    
    // cannot be within 5 pixels of right edge
    if (offset.x + 5 + containerSize.x + this.options.offsetX > mapSize.width) {
      var difference = (offset.x + 5 + containerSize.x + this.options.offsetX) - mapSize.width;
      anchor.x = anchor.x - difference;
    }
    this.container.setStyles({left: anchor.x + this.options.offsetX, top: anchor.y + this.options.offsetY});
  },
  
  /**
    Called by the marker when the mouse enters its bounds
    @param {Event} e the mouse event
   */
  onMarker: function(e) {
    $clear(this.hideTimer);
    this.showTimer = this.show.delay(this.options.delay, this);
  },
  
  /**
    Called by the marker when the mouse leaves its bounds
    @param {Event} e the mouse event
   */
  offMarker: function(e) {
    $clear(this.showTimer);
    this.hideTimer = this.hide.delay(this.options.timeout, this);
  },
  
  /**
    Event handler when mouse enters the container's bounds
    @param {Event} e the mouse event
   */
  hoverOn: function(e) {
    $clear(this.hideTimer);
  },
  
  /**
    Event handler when mouse leaves the container's bounds
    @param {Event} e the mouse event
   */
  hoverOff: function(e) {
    this.hideTimer = this.hide.delay(this.options.timeout, this);
  },
  
  /**
    add this overlay to the map
   */
  show: function() {
    var openIW = ta.remove('openInfoWindow');
    if (openIW) { openIW.hide(); }    
    
    ta.store('openInfoWindow', this);
    this.map.addOverlay(this.toGOverlay());
  },
  
  /**
    remove this overlay from the map
   */
  hide: function() {
    this.map.removeOverlay(this.toGOverlay());
  },
  
  /**
    AJAX response handler
    @param {String} t response text
   */
  setContent: function(t) {
    this.info.setHTML(t);
    window.behavior.apply(this.info);
    this.position();
    
    // update title length if top value image present
    var topValElem = this.container.getElement('img.tv');
    if(topValElem)
    {
      var titleElem = this.container.getElement('.hvrTtl');
      var marginRight = 65;
      if (this.marker.tamap.markerActivatesOnClick()) { marginRight += 13; }
      var marginRightPx = marginRight + 'px';
      titleElem.setStyle('margin-right', marginRightPx);
    }
  },
  
  /**
    Event handler when user clicks on title in info window. Support setting PID and callback function.
   */
  titleClicked: function() {
    if (this.options.titlePID) Cookie.set('NPID', this.options.titlePID, {domain: cookieDomain, time:5});
    if (this.options.callback) {
      new Event(e).stop();
      this.hide();
      window[this.options.callback](this.options.titleUrl);
    }
  },
  
  /**
    Event handler when user clicks the close box in info window.
   */
  closeClicked: function(e) {
    this.hide();
    this.marker.infoWinCloseClick(e);
  }
});
/**
 * @class
 * A TAMap with the ability to move between containers with per-container settings.
 * 
 * @extends ta.maps.Map
 * @extends ta.util.Saveable
 * 
 * @fires onMorph fired after the map changes states
 *
 * @option {boolean} [snapRecenter=true] don't panTo when recentering after a morph
 */
ta.maps.MorphableMap = ta.maps.Map.extend({
  options: {
    snapRecenter: true
  },

  /**
    @param {Element} elmt the element that should spawn the window.
    @param {object} [options] see options below.
    @param {object} [mOptions] options for the initial morph
  */
  initialize: function(elmt, options, mOptions) {
    // support options by option set
    if (mOptions.type) {
      mOptions = $merge(ta.maps['MAP_'+mOptions.type.toUpperCase()], mOptions);
      delete mOptions.type;
    }
    options = $merge(options, mOptions);
    this.parent(elmt, options);
    this.initialContainer = $(mOptions.container || elmt.getParent());
    this.morphs = [];
    this.addMorph(mOptions);
    this.index = 0;
    if (this.morphs[this.index].zoomDelta) {
      this.suppressEvents = true;
      this.gmap2.setZoom(this.zoom + this.morphs[this.index].zoomDelta);
      this.suppressEvents = false;
    }
    if (this.morphs[this.index].markerTypes) this.changeMarkerTypes(this.morphs[this.index].markerTypes);
  },
  
  /**
   * Returns the index of the current morph.
   * @returns {integer} the current morph index
   */
  getMorphIndex: function() {
    return this.index;
  },
  
  /**
   * Returns the adjusted map zoom level.
   * @returns {integer} zoom
   */
  getZoom: function() {
    var z = this.gmap2.getZoom();
    if (this.morphs[this.index].zoomDelta) z -= this.morphs[this.index].zoomDelta;
    return z;
  },
  
  /**
   * Move and/or zoom the map without firing any events.
   * @param {float} lat Latitude
   * @param {float} lng Longitude
   * @param {integer} [zoom] Zoom level
   */
  _move: function(lat, lng, zoom) {
    this.gmap2.setCenter(new GLatLng(lat, lng));
    if (zoom) {
      this.gmap2.setZoom(zoom + $pick(this.morphs[this.index].zoomDelta, 0));
      this.zoom = zoom;
    }
  },
  
  /**  
    adds a single Sarker to the map
    @param {ta.maps.Marker} marker Marker to add
    @param {String} type marker type
    @param {boolean} hidden if true overlay isn't added to the map
    @returns {ta.maps.Map} This Map
  */
  addMarker: function(marker, type, hidden) {
    this.parent(marker, type, hidden);
    if (this.morphs[this.index].markerTypes && this.morphs[this.index].markerTypes[type]) {
      marker.changeType(this.morphs[this.index].markerTypes[type]);
    }
    return this;
  },
  
  /**
   * Adds a morph that will use the given options. Supported options are:
   *   * container - element that will adopt the map div when it moves (defaults to the initial container, ie: no move)
   *   * smallMap - use the SmallMapControl or LargeMapControl
   *   * typeControl - use the MapTypeControl
   *   * scaleControl - use the ScaleControl
   *   * panControl - use the MapControl at all
   *   * staticMap - disable move/zoom
   *   * collapse - show/hide the container (defaults to false)
   *   * togglers - list of elements to show/hide when changing to/from this morph
   *   * zoomDelta - amount to alter zoom by
   * @param {Object} [options] morph options
   * @returns {ta.maps.MorphableMap} this
   */
  addMorph: function(options) {
    var base = {
      container:    this.initialContainer,
      smallMap:     this.options.smallMap,
      typeControl:  this.options.typeControl,
      scaleControl: this.options.scaleControl,
      panControl:   this.options.panControl,
      staticMap:    this.options.staticMap,
      enableInfoWindows:  this.options.enableInfoWindows,
      staticMarkers:      this.options.staticMarkers,
      collapse:     false,
      togglers:     [],
      zoomDelta:    0
    };
  
    // support options by option set
    if (options.type) {
      base = $merge(base, ta.maps['MAP_'+options.type.toUpperCase()]);
      delete options.type;
    }
    
    // support marker type by name
    if (options.markerTypes) {
      $H(options.markerTypes).map(function(v, k) {
        if ($type(v) != 'string') return v;
        return ta.maps['MARKER_'+v.toUpperCase()];
      });
    }

    options = $merge(base, options);
    
    // support container by ID
    options.container = $(options.container);
    
    // support alsoShowHide by ID
    if (options.alsoShowHide) {
      options.alsoShowHide = options.alsoShowHide.map(function(item, index) { return $(item); });
    }
    
    // support togglers by selector
    if ($type(options.togglers) == 'string') options.togglers = $$(options.togglers);
    
    this.morphs.push(options);
    this.morphs.getLast().togglers.each(function(t) {
      t.addEvent('click', this.morph.bind(this, this.morphs.length - 1));
    }, this);
    return this;
  },
  
  /**
    Change the map parameters to that of the new morph.
    @param {integer} index index of morph to switch to
    @returns {ta.maps.MorphableMap} this
  */
  morph: function(index) {
    if (index < 0 || index >= this.morphs.length || this.index == index) return this;
    var was = this.morphs[this.index];
    var now = this.morphs[index];
    this.index = index;
    
    was.togglers.each(function(t) { t.show(); });
    now.togglers.each(function(t) { t.hide(); });
    
    // container
    if (was.container != now.container) {
      now.container.adopt(this.src);
      if (now.collapse) {
        now.container.show();
        if (now.alsoShowHide) {
          now.alsoShowHide.each(function(n) { n.show(); });
        }
      }
      if (was.collapse) {
        was.container.hide();
        if (was.alsoShowHide) {
          was.alsoShowHide.each(function(w) { w.hide(); });
        }
      }
    }

    // type
    if (now.typeControl) {
      if (!this.typeControl) this.typeControl = new GMapTypeControl();
      this.gmap2.addControl(this.typeControl);
    }
    else if (this.typeControl) this.gmap2.removeControl(this.typeControl);

    // scale
    if (now.scaleControl) {
      if (!this.scaleControl) this.scaleControl = new GScaleControl();
      this.gmap2.addControl(this.scaleControl);
    }
    else if (this.scaleControl) this.gmap2.removeControl(this.scaleControl);

    // pan
    if (now.panControl) {
      if (now.smallMap) {
        if (!this.smallPanControl) this.smallPanControl = new GSmallMapControl();
        this.gmap2.addControl(this.smallPanControl);
      }
      else {
        if (!this.largePanControl) this.largePanControl = new GLargeMapControl();
        this.gmap2.addControl(this.largePanControl);
      }
    }
    else {
      if (this.smallPanControl) this.gmap2.removeControl(this.smallPanControl);
      if (this.largePanControl) this.gmap2.removeControl(this.largePanControl);
    }
    
    // enable/disable move/zoom
    if (now.staticMap) {
      this.gmap2.disableDragging();
      this.gmap2.disableDoubleClickZoom();
      if (this.eventsAdded) {
        GEvent.removeListener(this.gel_moveStart);
        GEvent.removeListener(this.gel_moveEnd);
        GEvent.removeListener(this.gel_dragEnd);
        GEvent.removeListener(this.gel_zoomEnd);
      }
      this.eventsAdded = false;
    }
    else {
      this.gmap2.enableDragging();
      this.gmap2.enableDoubleClickZoom();
      if (!this.moveStartHandler) this.moveStartHandler = this.beforeMove.bind(this);
      if (!this.moveEndHandler) this.moveEndHandler = this.onMove.bind(this);
      if (!this.dragEndHandler) this.dragEndHandler = this.onDrag.bind(this);
      if (!this.zoomEndHandler) this.zoomEndHandler = this.onZoom.bind(this);
      if (!this.eventsAdded) {
        this.gel_moveStart  = GEvent.addListener(this.gmap2, "movestart", this.moveStartHandler);
        this.gel_moveEnd    = GEvent.addListener(this.gmap2, "moveend",   this.moveEndHandler);
        this.gel_dragEnd    = GEvent.addListener(this.gmap2, "dragend",   this.dragEndHandler);
        this.gel_zoomEnd    = GEvent.addListener(this.gmap2, "zoomend",   this.zoomEndHandler);
      }
      this.eventsAdded = true;
    }
    
    // enable/disable info windows
    this.options.enableInfoWindows = now.enableInfoWindows;
    this.options.staticMarkers = now.staticMarkers;
    
    var center = this.mapCenter();
    this.gmap2.checkResize();

    this.suppressEvents = true;

    // adjust center for size change
    if (this.options.snapRecenter) this.gmap2.setCenter(center);
    else this.gmap2.panTo(center);

    if (now.zoomDelta) this.gmap2.setZoom(this.zoom + now.zoomDelta);
    else this.gmap2.setZoom(this.zoom);

    this.suppressEvents = false;
    
    // change marker type
    if (now.markerTypes) {
      this.changeMarkerTypes(now.markerTypes);
    }
    
    if (ta.retrieve('maps.saveable')) {
      if (this.index == 0) this.removeOption('map');
      else this.setOption('map', this.index);
      this.saveOptions();
    }
    
    this.fireEvent('onMorph', [this.index, this]);
    
    return this;
  },
  
  /**
   * Function to call after morphing between map states.
   * @name ta.maps.MorphableMap#onMorph
   * @event
   * @param {integer} index index of the new morph
   * @param {ta.maps.MorphableMap} map the map
   */
  
  /**
   * ta.util.LocationHash#onLoad event handler.
   * @see ta.util.LocationHash#event:onLoad
   * @param {Hash} options The options
   */
  restoreOptions: function(options) {
    if (options.hasKey('map')) {
      this.morph(parseInt(options.get('map')));
    }
    // code duplicated here instead of calling this.parent() due to function closure bug in mootools 1.11
    if (options.hasKey('mc') && options.hasKey('mz')) {
      var mc = options.get('mc').split(',');
      ta.store('maps.moved', true);
      this._move(parseFloat(mc[0]), parseFloat(mc[1]), parseInt(options.get('mz')));
    }
  }
});
ta.maps.MorphableMap.implement(new Events);

/**
  Default map options. Small pan control, scale control, and info windows enabled.
  @type Hash
*/
ta.maps.MAP_DEFAULT = {
  smallMap: true,
  typeControl: false,
  scaleControl: true,
  panControl: true,
  enableInfoWindows: true,
  zoomDelta: false,
  staticMap: false,
  staticMarkers: false
};

/**
  Default options for a thumbnail map. All controls and info windows disabled.
  @type Hash
*/
ta.maps.MAP_THUMB = $merge(ta.maps.MAP_DEFAULT, {
  scaleControl: false,
  panControl: false,
  enableInfoWindows: false,
  staticMap: true,
  staticMarkers: true
});