/*
* ExtInfoWindow Class, v1.0 
*  Copyright (c) 2007, Joe Monahan (http://www.seejoecode.com)
* 
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 
*       http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This class lets you add an info window to the map which mimics GInfoWindow
* and allows for users to skin it via CSS.  Additionally it has options to
* pull in HTML content from an ajax request, triggered when a user clicks on
* the associated marker.
*/


/**
 * Creates a new ExtInfoWindow that will initialize by reading styles from css
 *
 * @constructor
 * @param {GMarker} marker The marker associated with the info window
 * @param {String} windowId The DOM Id we will use to reference the info window
 * @param {String} html The HTML contents
 * @param {Object} opt_opts A contianer for optional arguments:
 *    {String} ajaxUrl The Url to hit on the server to request some contents 
 *    {Number} paddingX The padding size in pixels that the info window will leave on 
 *                    the left and right sides of the map when panning is involved.
 *    {Number} paddingY The padding size in pixels that the info window will leave on 
 *                    the top and bottom sides of the map when panning is involved.
 *    {Number} beakOffset The repositioning offset for when aligning the beak element. 
 *                    This is used to make sure the beak lines up correcting if the 
 *                    info window styling containers a border.
 */
function ExtInfoWindow(marker, windowId, html, opt_opts) {
  this.html_ = html;
  this.marker_ = marker;
  this.infoWindowId_ = windowId;

  this.options_ = opt_opts == null ? {} : opt_opts;
  this.ajaxUrl_ = this.options_.ajaxUrl == null ? null : this.options_.ajaxUrl;
  this.callback_ = this.options_.ajaxCallback == null ? null : this.options_.ajaxCallback;

  this.borderSize_ = this.options_.beakOffset == null ? 0 : this.options_.beakOffset;
  this.paddingX_ = this.options_.paddingX == null ? 0 + this.borderSize_ : this.options_.paddingX + this.borderSize_;
  this.paddingY_ = this.options_.paddingY == null ? 0 + this.borderSize_ : this.options_.paddingY + this.borderSize_;

  this.map_ = null;

  this.container_ = document.createElement('div');
  this.container_.style.position = 'relative';
  this.container_.style.display = 'none';

  this.contentDiv_ = document.createElement('div');
  this.contentDiv_.id = this.infoWindowId_ + '_contents';
  this.contentDiv_.innerHTML = this.html_;
  this.contentDiv_.style.display = 'block';
  this.contentDiv_.style.visibility = 'hidden';

  this.wrapperDiv_ = document.createElement('div');
};

//use the GOverlay class
ExtInfoWindow.prototype = new GOverlay();

/**
 * Called by GMap2's addOverlay method.  Creates the wrapping div for our info window and adds
 * it to the relevant map pane.  Also binds mousedown event to a private function so that they
 * are not passed to the underlying map.  Finally, performs ajax request if set up to use ajax
 * in the constructor.
 * @param {GMap2} map The map that has had this extInfoWindow is added to it.
 */
ExtInfoWindow.prototype.initialize = function(map) {
  this.map_ = map;

  this.defaultStyles = {
    containerWidth: this.map_.getSize().width / 2,
    borderSize: 1
  };

  this.wrapperParts = {
    tl:{t:0, l:0, w:0, h:0, domElement: null},
    t:{t:0, l:0, w:0, h:0, domElement: null},
    tr:{t:0, l:0, w:0, h:0, domElement: null},
    l:{t:0, l:0, w:0, h:0, domElement: null},
    r:{t:0, l:0, w:0, h:0, domElement: null},
    bl:{t:0, l:0, w:0, h:0, domElement: null},
    b:{t:0, l:0, w:0, h:0, domElement: null},
    br:{t:0, l:0, w:0, h:0, domElement: null},
    beak:{t:0, l:0, w:0, h:0, domElement: null},
    close:{t:0, l:0, w:0, h:0, domElement: null}
  };

  for (var i in this.wrapperParts ) {
    var tempElement = document.createElement('div');
    tempElement.id = this.infoWindowId_ + '_' + i;
    tempElement.style.visibility = 'hidden';
    document.body.appendChild(tempElement);
    tempElement = document.getElementById(this.infoWindowId_ + '_' + i);
    var tempWrapperPart = eval('this.wrapperParts.' + i);    
    tempWrapperPart.w = parseInt(this.getStyle_(tempElement, 'width'));
    tempWrapperPart.h = parseInt(this.getStyle_(tempElement, 'height'));
    document.body.removeChild(tempElement);
  }
  for (var i in this.wrapperParts) {
    if (i == 'close' ) {
      //first append the content so the close button is layered above it
      this.wrapperDiv_.appendChild(this.contentDiv_);
    }
    var wrapperPartsDiv = null;
    if (this.wrapperParts[i].domElement == null) {
      wrapperPartsDiv = document.createElement('div');
      this.wrapperDiv_.appendChild(wrapperPartsDiv);
    } else {
      wrapperPartsDiv = this.wrapperParts[i].domElement;
    }
    wrapperPartsDiv.id = this.infoWindowId_ + '_' + i;
    wrapperPartsDiv.style.position = 'absolute';
    wrapperPartsDiv.style.width = this.wrapperParts[i].w + 'px';
    wrapperPartsDiv.style.height = this.wrapperParts[i].h + 'px';
    wrapperPartsDiv.style.top = this.wrapperParts[i].t + 'px';
    wrapperPartsDiv.style.left = this.wrapperParts[i].l + 'px';
    this.wrapperParts[i].domElement = wrapperPartsDiv;
  }
  
  this.map_.getPane(G_MAP_FLOAT_PANE).appendChild(this.container_);
  this.container_.id = this.infoWindowId_;
  var containerWidth  = this.getStyle_(document.getElementById(this.infoWindowId_), 'width');
  this.container_.style.width = (containerWidth == null ? this.defaultStyles.containerWidth : containerWidth);

  this.map_.getContainer().appendChild(this.contentDiv_);
  this.contentWidth = this.getDimensions_(this.container_).width;
  this.contentDiv_.style.width = this.contentWidth + 'px';
  this.contentDiv_.style.position = 'absolute';

  this.container_.appendChild(this.wrapperDiv_);

  GEvent.bindDom(this.container_, 'mousedown', this,this.onClick_);
  GEvent.bindDom(this.container_, 'dblclick', this,this.onClick_);
  GEvent.bindDom(this.container_, 'DOMMouseScroll', this, this.onClick_);
  

  GEvent.trigger(this.map_, 'extinfowindowopen');
  if (this.ajaxUrl_ != null ) {
    this.ajaxRequest_(this.ajaxUrl_);
  }
};

/**
 * Private function to steal mouse click events to prevent it from returning to the map.
 * Without this links in the ExtInfoWindow would not work, and you could click to zoom or drag 
 * the map behind it.
 * @private
 * @param {MouseEvent} e The mouse event caught by this function
 */
ExtInfoWindow.prototype.onClick_ = function(e) {
  if(navigator.userAgent.toLowerCase().indexOf('msie') != -1 && document.all) {
    window.event.cancelBubble = true;
    window.event.returnValue = false;
  } else {
    //e.preventDefault();
    e.stopPropagation();
  }
};

/**
 * Remove the extInfoWindow container from the map pane. 
 */
ExtInfoWindow.prototype.remove = function() {
  if (this.map_.getExtInfoWindow() != null) {
    GEvent.trigger(this.map_, 'extinfowindowbeforeclose');
    
    GEvent.clearInstanceListeners(this.container_);
    if (this.container_.outerHTML) {
      this.container_.outerHTML = ''; //prevent pseudo-leak in IE
    }
    if (this.container_.parentNode) {
      this.container_.parentNode.removeChild(this.container_);
    }
    this.container_ = null;
    GEvent.trigger(this.map_, 'extinfowindowclose');
    this.map_.setExtInfoWindow_(null);
  }
};

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

/**
 * Draw extInfoWindow and wrapping decorators onto the map.  Resize and reposition
 * the map as necessary. 
 * @param {Boolean} force Will be true when pixel coordinates need to be recomputed.
 */
ExtInfoWindow.prototype.redraw = function(force) {
  if (!force || this.container_ == null) return;

  //set the content section's height, needed so  browser font resizing does not affect the window's dimensions
  var contentHeight = this.contentDiv_.offsetHeight;
  this.contentDiv_.style.height = contentHeight + 'px';

  //reposition contents depending on wrapper parts.
  //this is necessary for content that is pulled in via ajax
  this.contentDiv_.style.left = this.wrapperParts.l.w + 'px';
  this.contentDiv_.style.top = this.wrapperParts.tl.h + 'px';
  this.contentDiv_.style.visibility = 'visible';

  //Finish configuring wrapper parts that were not set in initialization
  this.wrapperParts.tl.t = 0;
  this.wrapperParts.tl.l = 0;
  this.wrapperParts.t.l = this.wrapperParts.tl.w;
  this.wrapperParts.t.w = (this.wrapperParts.l.w + this.contentWidth + this.wrapperParts.r.w) - this.wrapperParts.tl.w - this.wrapperParts.tr.w;
  this.wrapperParts.t.h = this.wrapperParts.tl.h;
  this.wrapperParts.tr.l = this.wrapperParts.t.w + this.wrapperParts.tl.w;
  this.wrapperParts.l.t = this.wrapperParts.tl.h;
  this.wrapperParts.l.h = contentHeight;
  this.wrapperParts.r.l = this.contentWidth + this.wrapperParts.l.w;
  this.wrapperParts.r.t = this.wrapperParts.tr.h;
  this.wrapperParts.r.h = contentHeight;
  this.wrapperParts.bl.t = contentHeight + this.wrapperParts.tl.h;
  this.wrapperParts.b.l = this.wrapperParts.bl.w;
  this.wrapperParts.b.t = contentHeight + this.wrapperParts.tl.h;
  this.wrapperParts.b.w = (this.wrapperParts.l.w + this.contentWidth + this.wrapperParts.r.w) - this.wrapperParts.bl.w - this.wrapperParts.br.w;
  this.wrapperParts.b.h = this.wrapperParts.bl.h;
  this.wrapperParts.br.l = this.wrapperParts.b.w + this.wrapperParts.bl.w;
  this.wrapperParts.br.t = contentHeight + this.wrapperParts.tr.h;
  this.wrapperParts.close.l = this.wrapperParts.tr.l +this.wrapperParts.tr.w - this.wrapperParts.close.w - this.borderSize_;
  this.wrapperParts.close.t = this.borderSize_;
  this.wrapperParts.beak.l = this.borderSize_ + (this.contentWidth / 2) - (this.wrapperParts.beak.w / 2);
  this.wrapperParts.beak.t = this.wrapperParts.bl.t + this.wrapperParts.bl.h - this.borderSize_;

  //create the decoration wrapper DOM objects
  //append the styled info window to the container
  for (var i in this.wrapperParts) {
    if (i == 'close' ) {
      //first append the content so the close button is layered above it
      this.wrapperDiv_.insertBefore(this.contentDiv_, this.wrapperParts[i].domElement);
    }
    var wrapperPartsDiv = null;
    if (this.wrapperParts[i].domElement == null) {
      wrapperPartsDiv = document.createElement('div');
      this.wrapperDiv_.appendChild(wrapperPartsDiv);
    } else {
      wrapperPartsDiv = this.wrapperParts[i].domElement;
    }
    wrapperPartsDiv.id = this.infoWindowId_ + '_' + i;
    wrapperPartsDiv.style.position='absolute';
    wrapperPartsDiv.style.width = this.wrapperParts[i].w + 'px';
    wrapperPartsDiv.style.height = this.wrapperParts[i].h + 'px';
    wrapperPartsDiv.style.top = this.wrapperParts[i].t + 'px';
    wrapperPartsDiv.style.left = this.wrapperParts[i].l + 'px';
    this.wrapperParts[i].domElement = wrapperPartsDiv;
  }

  //add event handler for the close box
  var currentMarker = this.marker_;
  var thisMap = this.map_;
  GEvent.addDomListener(this.wrapperParts.close.domElement, 'click', 
    function() {
      thisMap.closeExtInfoWindow();
    }
  );

  //position the container on the map, over the marker
  var pixelLocation = this.map_.fromLatLngToDivPixel(this.marker_.getPoint());
  this.container_.style.position = 'absolute';
  var markerIcon = this.marker_.getIcon();
  this.container_.style.left = (pixelLocation.x 
    - (this.contentWidth / 2) 
    - markerIcon.iconAnchor.x 
    + markerIcon.infoWindowAnchor.x
  ) + 'px';

  this.container_.style.top = (pixelLocation.y
    - this.wrapperParts.bl.h
    - contentHeight
    - this.wrapperParts.tl.h
    - this.wrapperParts.beak.h
    - markerIcon.iconAnchor.y
    + markerIcon.infoWindowAnchor.y
    + this.borderSize_
  ) + 'px';

  this.container_.style.display = 'block';

  if(this.map_.getExtInfoWindow() != null) {
    this.repositionMap_();
  }
};

/**
 * Determine the dimensions of the contents to recalculate and reposition the 
 * wrapping decorator elements accordingly.
 */
ExtInfoWindow.prototype.resize = function(){
  
  //Create temporary DOM node for new contents to get new height
  //This is done because if you manipulate this.contentDiv_ directly it causes visual errors in IE6
  var tempElement = this.contentDiv_.cloneNode(true);
  tempElement.id = this.infoWindowId_ + '_tempContents';
  tempElement.style.visibility = 'hidden';	
  tempElement.style.height = 'auto';
  document.body.appendChild(tempElement);
  tempElement = document.getElementById(this.infoWindowId_ + '_tempContents');
  var contentHeight = tempElement.offsetHeight;
  document.body.removeChild(tempElement);

  //Set the new height to eliminate visual defects that can be caused by font resizing in browser
  this.contentDiv_.style.height = contentHeight + 'px';

  var contentWidth = this.contentDiv_.offsetWidth;
  var pixelLocation = this.map_.fromLatLngToDivPixel(this.marker_.getPoint());

  var oldWindowHeight = this.wrapperParts.t.domElement.offsetHeight + this.wrapperParts.l.domElement.offsetHeight + this.wrapperParts.b.domElement.offsetHeight;	
  var oldWindowPosTop = this.wrapperParts.t.domElement.offsetTop;

  //resize info window to look correct for new height
  this.wrapperParts.l.domElement.style.height = contentHeight + 'px';
  this.wrapperParts.r.domElement.style.height = contentHeight + 'px';
  var newPosTop = this.wrapperParts.b.domElement.offsetTop - contentHeight;
  this.wrapperParts.l.domElement.style.top = newPosTop + 'px';
  this.wrapperParts.r.domElement.style.top = newPosTop + 'px';
  this.contentDiv_.style.top = newPosTop + 'px';
  windowTHeight = parseInt(this.wrapperParts.t.domElement.style.height);
  newPosTop -= windowTHeight;
  this.wrapperParts.close.domElement.style.top = newPosTop + this.borderSize_ + 'px';
  this.wrapperParts.tl.domElement.style.top = newPosTop + 'px';
  this.wrapperParts.t.domElement.style.top = newPosTop + 'px';
  this.wrapperParts.tr.domElement.style.top = newPosTop + 'px';

  this.repositionMap_();
};

/**
 * Check to see if the displayed extInfoWindow is positioned off the viewable 
 * map region and by how much.  Use that information to pan the map so that 
 * the extInfoWindow is completely displayed.
 * @private
 */
ExtInfoWindow.prototype.repositionMap_ = function(){
  //pan if necessary so it shows on the screen
  var mapNE = this.map_.fromLatLngToDivPixel(
    this.map_.getBounds().getNorthEast()
  );
  var mapSW = this.map_.fromLatLngToDivPixel(
    this.map_.getBounds().getSouthWest()
  );
  var markerPosition = this.map_.fromLatLngToDivPixel(
    this.marker_.getPoint()
  );

  var panX = 0;
  var panY = 0;
  var paddingX = this.paddingX_;
  var paddingY = this.paddingY_;
  var infoWindowAnchor = this.marker_.getIcon().infoWindowAnchor;
  var iconAnchor = this.marker_.getIcon().iconAnchor;

  //test top of screen	
  var windowT = this.wrapperParts.t.domElement;
  var windowL = this.wrapperParts.l.domElement;
  var windowB = this.wrapperParts.b.domElement;
  var windowR = this.wrapperParts.r.domElement;
  var windowBeak = this.wrapperParts.beak.domElement;

  var offsetTop = markerPosition.y - ( -infoWindowAnchor.y + iconAnchor.y +  this.getDimensions_(windowBeak).height + this.getDimensions_(windowB).height + this.getDimensions_(windowL).height + this.getDimensions_(windowT).height + this.paddingY_);
  if (offsetTop < mapNE.y) {
    panY = mapNE.y - offsetTop;
  } else {
    //test bottom of screen
    var offsetBottom = markerPosition.y + this.paddingY_;
    if (offsetBottom >= mapSW.y) {
      panY = -(offsetBottom - mapSW.y);
    }
  }

  //test right of screen
  var offsetRight = Math.round(markerPosition.x + this.getDimensions_(this.container_).width/2 + this.getDimensions_(windowR).width + this.paddingX_ + infoWindowAnchor.x - iconAnchor.x);
  if (offsetRight > mapNE.x) {
    panX = -( offsetRight - mapNE.x);
  } else {
    //test left of screen
    var offsetLeft = - (Math.round( (this.getDimensions_(this.container_).width/2 - this.marker_.getIcon().iconSize.width/2) + this.getDimensions_(windowL).width + this.borderSize_ + this.paddingX_) - markerPosition.x - infoWindowAnchor.x + iconAnchor.x);
    if( offsetLeft < mapSW.x) {
      panX = mapSW.x - offsetLeft;
    }
  }

  if (panX != 0 || panY != 0 && this.map_.getExtInfoWindow() != null ) {
    this.map_.panBy(new GSize(panX,panY));
  }
};

/**
 * Private function that handles performing an ajax request to the server.  The response
 * information is assumed to be HTML and is placed inside this extInfoWindow's contents region.
 * Last, check to see if the height has changed, and resize the extInfoWindow accordingly.
 * @private
 * @param {String} url The Url of where to make the ajax request on the server
 */
ExtInfoWindow.prototype.ajaxRequest_ = function(url){
  var thisMap = this.map_;
  var thisCallback = this.callback_;
  GDownloadUrl(url, function(response, status){
    var infoWindow = document.getElementById(thisMap.getExtInfoWindow().infoWindowId_ + '_contents');
    if (response == null || status == -1 ) {
      infoWindow.innerHTML = '<span class="error">ERROR: The Ajax request failed to get HTML content from "' + url + '"</span>';
    } else {
      infoWindow.innerHTML = response;
    }
    if (thisCallback != null ) {
      thisCallback();
    }
    thisMap.getExtInfoWindow().resize();
    GEvent.trigger(thisMap, 'extinfowindowupdate');
  });
};

/**
 * Private function derived from Prototype.js to get a given element's
 * height and width
 * @private
 * @param {Object} element The DOM element that will have height and 
 *                    width will be calculated for it.
 * @return {Object} Object with keys: width, height
 */
ExtInfoWindow.prototype.getDimensions_ = function(element) {
  var display = this.getStyle_(element, 'display');
  if (display != 'none' && display != null) { // Safari bug
    return {width: element.offsetWidth, height: element.offsetHeight};
  }

  // All *Width and *Height properties give 0 on elements with display none,
  // so enable the element temporarily
  var els = element.style;
  var originalVisibility = els.visibility;
  var originalPosition = els.position;
  var originalDisplay = els.display;
  els.visibility = 'hidden';
  els.position = 'absolute';
  els.display = 'block';
  var originalWidth = element.clientWidth;
  var originalHeight = element.clientHeight;
  els.display = originalDisplay;
  els.position = originalPosition;
  els.visibility = originalVisibility;
  return {width: originalWidth, height: originalHeight};
};

/**
 * Private function derived from Prototype.js to get a given element's
 * value that is associated with the passed style
 * @private
 * @param {Object} element The DOM element that will be checked.
 * @param {String} style The style name that will be have it's value returned.
 * @return {Object}
 */
ExtInfoWindow.prototype.getStyle_ = function(element, style) {
  var found = false;
  style = this.camelize_(style);
  var value = element.style[style];
  if (!value) {
    if (document.defaultView && document.defaultView.getComputedStyle) {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    } else if (element.currentStyle) {
      value = element.currentStyle[style];
    }
  }
  if((value == 'auto') && (style == 'width' || style == 'height') && (this.getStyle_(element, 'display') != 'none')) {
    if( style == 'width' ) {
      value = element.offsetWidth;
    }else {
      value = element.offsetHeight;
    }
  }
  return (value == 'auto') ? null : value;
};

/**
 * Private function pulled from Prototype.js that will change a hyphened
 * style name into camel case.
 * @private
 * @param {String} element The string that will be parsed and made into camel case
 * @return {String}
 */
ExtInfoWindow.prototype.camelize_ = function(element) {
  var parts = element.split('-'), len = parts.length;
  if (len == 1) return parts[0];
  var camelized = element.charAt(0) == '-'
    ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
    : parts[0];

  for (var i = 1; i < len; i++) {
    camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
  }
  return camelized;
};

GMap.prototype.ExtInfoWindowInstance_ = null;
GMap.prototype.ClickListener_ = null;
GMap.prototype.InfoWindowListener_ = null;

/**
 * Creates a new instance of ExtInfoWindow for the GMarker.  Register the newly created 
 * instance with the map, ensuring only one window is open at a time. If this is the first
 * ExtInfoWindow ever opened, add event listeners to the map to close the ExtInfoWindow on 
 * zoom and click, to mimic the default GInfoWindow behavior.
 *
 * @param {GMap} map The GMap2 object where the ExtInfoWindow will open
 * @param {String} cssId The id we will use to reference the info window
 * @param {String} html The HTML contents
 * @param {Object} opt_opts A contianer for optional arguments:
 *    {String} ajaxUrl The Url to hit on the server to request some contents 
 *    {Number} paddingX The padding size in pixels that the info window will leave on 
 *                    the left and right sides of the map when panning is involved.
 *    {Number} paddingX The padding size in pixels that the info window will leave on 
 *                    the top and bottom sides of the map when panning is involved.
 *    {Number} beakOffset The repositioning offset for when aligning the beak element. 
 *                    This is used to make sure the beak lines up correcting if the 
 *                    info window styling containers a border.
 */
GMarker.prototype.openExtInfoWindow = function(map, cssId, html, opt_opts) {
  if (map == null) {
    throw 'Error in GMarker.openExtInfoWindow: map cannot be null';
    return false;
  }
  if (cssId == null || cssId == '') {
    throw 'Error in GMarker.openExtInfoWindow: must specify a cssId';
    return false;
  }
  
  map.closeInfoWindow();
  if (map.getExtInfoWindow() != null) {
    map.closeExtInfoWindow();
  }
  if (map.getExtInfoWindow() == null) {
    map.setExtInfoWindow_( new ExtInfoWindow(
      this,
      cssId,
      html,
      opt_opts
    ) );
    if (map.ClickListener_ == null) {
      //listen for map click, close ExtInfoWindow if open
      map.ClickListener_ = GEvent.addListener(map, 'click',
      function(event) {
          if( !event && map.getExtInfoWindow() != null ){
            map.closeExtInfoWindow();
          }
        }
      );
    }
    if (map.InfoWindowListener_ == null) {
      //listen for default info window open, close ExtInfoWindow if open
      map.InfoWindowListener_ = GEvent.addListener(map, 'infowindowopen', 
      function(event) {
          if (map.getExtInfoWindow() != null) {
            map.closeExtInfoWindow();
          }
        }
      );
    }
    map.addOverlay(map.getExtInfoWindow());
  }
};

/**
 * Remove the ExtInfoWindow instance
 * @param {GMap2} map The map where the GMarker and ExtInfoWindow exist
 */
GMarker.prototype.closeExtInfoWindow = function(map) {
  if( map.getExtInfWindow() != null ){
    map.closeExtInfoWindow();
  }
};

/**
 * Get the ExtInfoWindow instance from the map
 */
GMap2.prototype.getExtInfoWindow = function(){
  return this.ExtInfoWindowInstance_;
};
/**
 * Set the ExtInfoWindow instance for the map
 * @private
 */
GMap2.prototype.setExtInfoWindow_ = function( extInfoWindow ){
  this.ExtInfoWindowInstance_ = extInfoWindow;
}
/**
 * Remove the ExtInfoWindow from the map
 */
GMap2.prototype.closeExtInfoWindow = function(){
  if( this.getExtInfoWindow() != null ){
    this.ExtInfoWindowInstance_.remove();
  }
};


/*
* ExtMapTypeControl Class v1.3 
*  Copyright (c) 2007, Google 
*  Author: Pamela Fox, others
* 
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 
*       http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This class lets you add a control to the map which mimics GMapTypeControl
*  and allows for the addition of a traffic button/traffic key.
*/

/*
 * Constructor for ExtMapTypeControl, which uses an option hash
 * to decide what elements to put in the control.
 * @param {opt_opts} Named optional arguments:
 *   opt_opts.showTraffic {Boolean} Controls whether traffic button is shown
 *   opt_opts.showTrafficKey {Boolean} Controls whether traffic key is shown
 */
function ExtMapTypeControl(opt_opts) {
  this.options = opt_opts || {};
};


ExtMapTypeControl.prototype = new GControl();

/**
 * Is called by GMap2's addOverlay method. Creates the button 
 *  and appends to the map div.
 * @param {GMap2} map The map that has had this ExtMapTypeControl added to it.
 * @return {DOM Object} Div that holds the control
 */ 
ExtMapTypeControl.prototype.initialize = function(map) {
  var container = document.createElement("div");
  var me = this;

  var mapTypes = map.getMapTypes();
  var mapTypeDivs = me.addMapTypeButtons_(map);

  GEvent.addListener(map, "addmaptype", function() {
    var newMapTypes = map.getMapTypes();
    var newMapType = newMapTypes.pop();
    var newMapTypeDiv = me.createButton_(newMapType.getName());
    newMapTypeDiv.setAttribute('title', newMapType.getAlt());
    mapTypes.push(newMapType);
    mapTypeDivs.push(newMapTypeDiv);
    me.resetButtonEvents_(map, mapTypeDivs);
    container.appendChild(newMapTypeDiv);
  });
  GEvent.addListener(map, "removemaptype", function() {
    for (var i = 0; i < mapTypeDivs.length; i++) {
      GEvent.clearListeners(mapTypeDivs[i], "click");
      container.removeChild(mapTypeDivs[i]);
    }
    mapTypeDivs = me.addMapTypeButtons_(map);
    me.resetButtonEvents_(map, mapTypeDivs);
    for (var i = 0; i < mapTypeDivs.length; i++ ) {
      container.appendChild(mapTypeDivs[i]);
    }
  });

  if (me.options.showTraffic) {
    var trafficDiv = me.createButton_("Traffic");
    trafficDiv.setAttribute('title', 'Show Traffic');
    trafficDiv.style.marginRight = "8px";
    trafficDiv.style.visibility = 'hidden';
    trafficDiv.firstChild.style.cssFloat = "left";
    trafficDiv.firstChild.style.styleFloat = "left";
    // Sending true makes overlay hidden by default
    me.trafficInfo = new GTrafficOverlay({hide:true});
    me.trafficInfo.hidden = true;
    // We have to do this so that we can sense if traffic is in view
    GEvent.addListener(me.trafficInfo, "changed", function(hasTrafficInView) {
      if (hasTrafficInView) {
        trafficDiv.style.visibility = 'visible';
      } else {
        trafficDiv.style.visibility = 'hidden';
      }
    });
    map.addOverlay(me.trafficInfo);

    GEvent.addDomListener(trafficDiv.firstChild, "click", function() {
      if (me.trafficInfo.hidden) {
        me.trafficInfo.hidden = false;
        me.trafficInfo.show();
      } else {
        me.trafficInfo.hidden = true;
        me.trafficInfo.hide();
      }
      me.toggleButton_(trafficDiv.firstChild, !me.trafficInfo.hidden);
    });

    if (me.options.showTrafficKey) {
      keyDiv = document.createElement("div");
      keyDiv.style.cssFloat = "left";
      keyDiv.style.styleFloat = "left";
      keyDiv.innerHTML = "&nbsp;?&nbsp;";
  
      var keyExpandedDiv = document.createElement("div");
      keyExpandedDiv.style.clear = "both";
      keyExpandedDiv.style.padding = "2px";
      var keyInfo = [{"color": "#30ac3e", "text": "&gt; 50 MPH"},
                     {"color": "#ffcf00", "text": "25-50 MPH"},
                     {"color": "#ff0000", "text": "&lt; 25 MPH"},
                     {"color": "#c0c0c0", "text": "No data"}];
      for (var i = 0; i < keyInfo.length; i++) {
        keyExpandedDiv.innerHTML += "<div style='text-align: left'><span style='background-color: " + keyInfo[i].color + "'>&nbsp;&nbsp</span>"
            +  "<span style='color: " + keyInfo[i].color + "'> " + keyInfo[i].text + " </span>" + "</div>"; 
      }
      keyExpandedDiv.style.display = "none";

      GEvent.addDomListener(keyDiv, "click", function() {
        if (me.keyExpanded) {
          me.keyExpanded = false;
          keyExpandedDiv.style.display = "none";
        } else {
          me.keyExpanded = true;
          keyExpandedDiv.style.display = "block";
        }
        me.toggleButton_(keyDiv, me.keyExpanded);
      });

      me.toggleButton_(keyDiv, me.keyExpanded);
    }

    var separatorDiv = document.createElement("div");
    separatorDiv.style.clear = "both";

    if (me.options.showTrafficKey) trafficDiv.appendChild(keyDiv);
    trafficDiv.appendChild(separatorDiv);
    if (me.options.showTrafficKey) trafficDiv.appendChild(keyExpandedDiv);
    me.toggleButton_(trafficDiv.firstChild, false);

    container.appendChild(trafficDiv);
  }

  for (var i = 0; i < mapTypeDivs.length; i++ ) {
    container.appendChild(mapTypeDivs[i]);
  }

  map.getContainer().appendChild(container);

  return container;
};

/*
 * Creates buttons for map types.
 * @param {GMap2} Map object for which to create buttons.
 * @return {Array} Divs containing the buttons.
 */
ExtMapTypeControl.prototype.addMapTypeButtons_ = function(map) {
  var me = this;
  var mapTypes = map.getMapTypes();
  var mapTypeDivs = new Array();
  for (var i = 0; i < mapTypes.length; i++) {
    mapTypeDivs[i] = me.createButton_(mapTypes[i].getName());
    mapTypeDivs[i].setAttribute('title', mapTypes[i].getAlt());
  }
  me.resetButtonEvents_(map, mapTypeDivs);
  return mapTypeDivs;
};

/*
 * Ensures that map type button events are assigned correctly.
 * @param {GMap2} Map object for which to reset events.
 * @param {Array} mapTypeDivs Divs containing map type buttons.
 */
ExtMapTypeControl.prototype.resetButtonEvents_ = function(map, mapTypeDivs) {
  var me = this;
  var mapTypes = map.getMapTypes();
  for (var i = 0; i < mapTypeDivs.length; i++) {
    var otherDivs = new Array;
    for (var j = 0; j < mapTypes.length; j++ ) {
      if (j != i) {
        otherDivs.push(mapTypeDivs[j]);
      }
    }
    me.assignButtonEvent_(mapTypeDivs[i], map, mapTypes[i], otherDivs);
  }
  GEvent.addListener(map, "maptypechanged", function() {
    var divIndex = 0;
    var mapType = map.getCurrentMapType();
    for (var i = 0; i < mapTypes.length; i++) {
      if (mapTypes[i] == mapType) {
        divIndex = i;
      }
    }
    GEvent.trigger(mapTypeDivs[divIndex], "click");
  });
};

/*
 * Creates simple buttons with text nodes. 
 * @param {String} text Text to display in button
 * @return {DOM Object} The div for the button.
 */
ExtMapTypeControl.prototype.createButton_ = function(text) {
  var buttonDiv = document.createElement("div");
  this.setButtonStyle_(buttonDiv);
  buttonDiv.style.cssFloat = "left";
  buttonDiv.style.styleFloat = "left";
  var textDiv = document.createElement("div");
  textDiv.appendChild(document.createTextNode(text));
  textDiv.style.width = "6em";
  buttonDiv.appendChild(textDiv);
  return buttonDiv;
};

/*
 * Assigns events to MapType buttons to change maptype
 *  and toggle button styles correctly for all buttons
 *  when button is clicked.
 *  @param {DOM Object} div Button's div to assign click to
 *  @param {GMap2} Map object to change maptype of.
 *  @param {Object} mapType GMapType to change map to when clicked
 *  @param {Array} otherDivs Array of other button divs to toggle off
 */  
ExtMapTypeControl.prototype.assignButtonEvent_ = function(div, map, mapType, otherDivs) {
  var me = this;

  GEvent.addDomListener(div, "click", function() {
    for (var i = 0; i < otherDivs.length; i++) {
      me.toggleButton_(otherDivs[i].firstChild, false);
    }
    me.toggleButton_(div.firstChild, true);
    map.setMapType(mapType);
  });
};

/*
 * Changes style of button to appear on/off depending on boolean passed in.
 * @param {DOM Object} div  Button div to change style of
 * @param {Boolean} boolCheck Used to decide to use on style or off style
 */
ExtMapTypeControl.prototype.toggleButton_ = function(div, boolCheck) {
   div.style.fontWeight = boolCheck ? "bold" : "";
   div.style.border = "1px solid white";
   var shadows = boolCheck ? ["Top", "Left"] : ["Bottom", "Right"];
   for (var j = 0; j < shadows.length; j++) {
     div.style["border" + shadows[j]] = "1px solid #b0b0b0";
  } 
};

/*
 * Required by GMaps API for controls. 
 * @return {GControlPosition} Default location for control
 */
ExtMapTypeControl.prototype.getDefaultPosition = function() {
  return new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(7, 7));
};

/*
 * Sets the proper CSS for the given button element.
 * @param {DOM Object} button Button div to set style for
 */
ExtMapTypeControl.prototype.setButtonStyle_ = function(button) {
  button.style.color = "#000000";
  button.style.backgroundColor = "white";
  button.style.font = "small Arial";
  button.style.border = "1px solid black";
  button.style.padding = "0px";
  button.style.margin= "0px";
  button.style.textAlign = "center";
  button.style.fontSize = "12px"; 
  button.style.cursor = "pointer";
};


/*
* LabeledMarker Class, v1.2
*
* Copyright 2007 Mike Purvis (http://uwmike.com)
* 
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 
*       http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This class extends the Maps API's standard GMarker class with the ability
* to support markers with textual labels. Please see articles here:
*
*       http://googlemapsbook.com/2007/01/22/extending-gmarker/
*       http://googlemapsbook.com/2007/03/06/clickable-labeledmarker/
*/

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

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


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

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

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

/**
 * Call the redraw() handler in GMarker and our our redrawLabel() function.
 *
 * @param {Boolean} force will be true when pixel coordinates need to be recomputed.
 */
LabeledMarker.prototype.redraw = function(force) {
  GMarker.prototype.redraw.apply(this, arguments);
  this.redrawLabel_();  
};

/**
 * Moves the text div based on current projection and zoom level.
 */
LabeledMarker.prototype.redrawLabel_ = function() {
  // Calculate the DIV coordinates of two opposite corners of our bounds to
  // get the size and position of our rectangle
  var p = this.map_.fromLatLngToDivPixel(this.latlng_);
  var z = GOverlay.getZIndex(this.latlng_.lat());
  
  // Now position our div based on the div coordinates of our bounds
  this.div_.style.left = (p.x + this.labelOffset_.width) + "px";
  this.div_.style.top = (p.y + this.labelOffset_.height) + "px";
  this.div_.style.zIndex = z; // in front of the marker
};

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

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


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


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


/**
 * Repositions label and marker when setLatLng is called.
 */
LabeledMarker.prototype.setLatLng = function(latlng) {
  this.latlng_ = latlng;
  GMarker.prototype.setLatLng.apply(this, arguments);
  this.redrawLabel_();
};

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


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


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


/**
 * Shows the label of the marker.
 */
LabeledMarker.prototype.showLabel = function() {
  this.div_.style.visibility = 'visible';
};


/**
 * @name MapIconMaker
 * @version 1.1
 * @author Pamela Fox
 * @copyright (c) 2008 Pamela Fox
 * @fileoverview This gives you static functions for creating dynamically
 *     sized and colored marker icons using the Charts API marker output.
 */

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

/**
 * @name MarkerIconOptions
 * @class This class represents optional arguments to {@link createMarkerIcon}, 
 *     {@link createFlatIcon}, or {@link createLabeledMarkerIcon}. Each of the
 *     functions use a subset of these arguments. See the function descriptions
 *     for the list of supported options.
 * @property {Number} [width=32] Specifies, in pixels, the width of the icon.
 *     The width may include some blank space on the side, depending on the
 *     height of the icon, as the icon will scale its shape proportionately.
 * @property {Number} [height=32] Specifies, in pixels, the height of the icon.
 * @property {String} [primaryColor="#ff0000"] Specifies, as a hexadecimal
 *     string, the color used for the majority of the icon body.
 * @property {String} [cornerColor="#ffffff"] Specifies, as a hexadecimal
 *     string, the color used for the top corner of the icon. If you'd like the
 *     icon to have a consistent color, make the this the same as the
 *     {@link primaryColor}.
 * @property {String} [strokeColor="#000000"] Specifies, as a hexadecimal
 *     string, the color used for the outside line (stroke) of the icon.
 * @property {String} [shadowColor="#000000"] Specifies, as a hexadecimal
 *     string, the color used for the shadow of the icon. 
 * @property {String} [label=""] Specifies a character or string to display
 *     inside the body of the icon. Generally, one or two characters looks best.
 * @property {String} [labelColor="#000000"] Specifies, as a hexadecimal 
 *     string, the color used for the label text.
 * @property {Number} [labelSize=0] Specifies, in pixels, the size of the label
 *     text. If set to 0, the text auto-sizes to fit the icon body.
 * @property {String} [shape="circle"] Specifies shape of the icon. Current
 *     options are "circle" for a circle or "roundrect" for a rounded rectangle.
 * @property {Boolean} [addStar = false] Specifies whether to add a star to the
 *     edge of the icon.
 * @property {String} [starPrimaryColor="#FFFF00"] Specifies, as a hexadecimal
 *     string, the color used for the star body.
 * @property {String} [starStrokeColor="#0000FF"] Specifies, as a hexadecimal
 *     string, the color used for the outside line (stroke) of the star.
 */

/**
 * This namespace contains functions that you can use to easily create
 *     dynamically sized, colored, and labeled icons.
 * @namespace
 */
var MapIconMaker = {};

/**
 * Creates an icon based on the specified options in the 
 *   {@link MarkerIconOptions} argument.
 *   Supported options are: width, height, primaryColor, 
 *   strokeColor, and cornerColor.
 * @param {MarkerIconOptions} [opts]
 * @return {GIcon}
 */
MapIconMaker.createMarkerIcon = function (opts) {
  var width = opts.width || 32;
  var height = opts.height || 32;
  var primaryColor = opts.primaryColor || "#ff0000";
  var strokeColor = opts.strokeColor || "#000000";
  var cornerColor = opts.cornerColor || "#ffffff";
   
  var baseUrl = "http://chart.apis.google.com/chart?cht=mm";
  var iconUrl = baseUrl + "&chs=" + width + "x" + height + 
      "&chco=" + cornerColor.replace("#", "") + "," + 
      primaryColor.replace("#", "") + "," + 
      strokeColor.replace("#", "") + "&ext=.png";
  var icon = new GIcon(G_DEFAULT_ICON);
  icon.image = iconUrl;
  icon.iconSize = new GSize(width, height);
  icon.shadowSize = new GSize(Math.floor(width * 1.6), height);
  icon.iconAnchor = new GPoint(width / 2, height);
  icon.infoWindowAnchor = new GPoint(width / 2, Math.floor(height / 12));
  icon.printImage = iconUrl + "&chof=gif";
  icon.mozPrintImage = iconUrl + "&chf=bg,s,ECECD8" + "&chof=gif";
  iconUrl = baseUrl + "&chs=" + width + "x" + height + 
      "&chco=" + cornerColor.replace("#", "") + "," + 
      primaryColor.replace("#", "") + "," + 
      strokeColor.replace("#", "");
  icon.transparent = iconUrl + "&chf=a,s,ffffff11&ext=.png";

  icon.imageMap = [
    width / 2, height,
    (7 / 16) * width, (5 / 8) * height,
    (5 / 16) * width, (7 / 16) * height,
    (7 / 32) * width, (5 / 16) * height,
    (5 / 16) * width, (1 / 8) * height,
    (1 / 2) * width, 0,
    (11 / 16) * width, (1 / 8) * height,
    (25 / 32) * width, (5 / 16) * height,
    (11 / 16) * width, (7 / 16) * height,
    (9 / 16) * width, (5 / 8) * height
  ];
  for (var i = 0; i < icon.imageMap.length; i++) {
    icon.imageMap[i] = parseInt(icon.imageMap[i]);
  }

  return icon;
};


/**
 * Creates a flat icon based on the specified options in the 
 *     {@link MarkerIconOptions} argument.
 *     Supported options are: width, height, primaryColor,
 *     shadowColor, label, labelColor, labelSize, and shape..
 * @param {MarkerIconOptions} [opts]
 * @return {GIcon}
 */
MapIconMaker.createFlatIcon = function (opts) {
  var width = opts.width || 32;
  var height = opts.height || 32;
  var primaryColor = opts.primaryColor || "#ff0000";
  var shadowColor = opts.shadowColor || "#000000";
  var label = MapIconMaker.escapeUserText_(opts.label) || "";
  var labelColor = opts.labelColor || "#000000";
  var labelSize = opts.labelSize || 0;
  var shape = opts.shape ||  "circle";
  var shapeCode = (shape === "circle") ? "it" : "itr";

  var baseUrl = "http://chart.apis.google.com/chart?cht=" + shapeCode;
  var iconUrl = baseUrl + "&chs=" + width + "x" + height + 
      "&chco=" + primaryColor.replace("#", "") + "," + 
      shadowColor.replace("#", "") + "ff,ffffff01" +
      "&chl=" + label + "&chx=" + labelColor.replace("#", "") + 
      "," + labelSize;
  var icon = new GIcon(G_DEFAULT_ICON);
  icon.image = iconUrl + "&chf=bg,s,00000000" + "&ext=.png";
  icon.iconSize = new GSize(width, height);
  icon.shadowSize = new GSize(0, 0);
  icon.iconAnchor = new GPoint(width / 2, height / 2);
  icon.infoWindowAnchor = new GPoint(width / 2, height / 2);
  icon.printImage = iconUrl + "&chof=gif";
  icon.mozPrintImage = iconUrl + "&chf=bg,s,ECECD8" + "&chof=gif";
  icon.transparent = iconUrl + "&chf=a,s,ffffff01&ext=.png";
  icon.imageMap = []; 
  if (shapeCode === "itr") {
    icon.imageMap = [0, 0, width, 0, width, height, 0, height];
  } else {
    var polyNumSides = 8;
    var polySideLength = 360 / polyNumSides;
    var polyRadius = Math.min(width, height) / 2;
    for (var a = 0; a < (polyNumSides + 1); a++) {
      var aRad = polySideLength * a * (Math.PI / 180);
      var pixelX = polyRadius + polyRadius * Math.cos(aRad);
      var pixelY = polyRadius + polyRadius * Math.sin(aRad);
      icon.imageMap.push(parseInt(pixelX), parseInt(pixelY));
    }
  }

  return icon;
};


/**
 * Creates a labeled marker icon based on the specified options in the 
 *     {@link MarkerIconOptions} argument.
 *     Supported options are: primaryColor, strokeColor, 
 *     starPrimaryColor, starStrokeColor, label, labelColor, and addStar.
 * @param {MarkerIconOptions} [opts]
 * @return {GIcon}
 */
MapIconMaker.createLabeledMarkerIcon = function (opts) {
  var primaryColor = opts.primaryColor || "#DA7187";
  var strokeColor = opts.strokeColor || "#000000";
  var starPrimaryColor = opts.starPrimaryColor || "#FFFF00";
  var starStrokeColor = opts.starStrokeColor || "#0000FF";
  var label = MapIconMaker.escapeUserText_(opts.label) || "";
  var labelColor = opts.labelColor || "#000000";
  var addStar = opts.addStar || false;
  
  var pinProgram = (addStar) ? "pin_star" : "pin";
  var baseUrl = "http://chart.apis.google.com/chart?cht=d&chdp=mapsapi&chl=";
  var iconUrl = baseUrl + pinProgram + "'i\\" + "'[" + label + 
      "'-2'f\\"  + "hv'a\\]" + "h\\]o\\" + 
      primaryColor.replace("#", "")  + "'fC\\" + 
      labelColor.replace("#", "")  + "'tC\\" + 
      strokeColor.replace("#", "")  + "'eC\\";
  if (addStar) {
    iconUrl += starPrimaryColor.replace("#", "") + "'1C\\" + 
        starStrokeColor.replace("#", "") + "'0C\\";
  }
  iconUrl += "Lauto'f\\";

  var icon = new GIcon(G_DEFAULT_ICON);
  icon.image = iconUrl + "&ext=.png";
  icon.iconSize = (addStar) ? new GSize(23, 39) : new GSize(21, 34);
  return icon;
};


/**
 * Utility function for doing special chart API escaping first,
 *  and then typical URL escaping. Must be applied to user-supplied text.
 * @private
 */
MapIconMaker.escapeUserText_ = function (text) {
  if (text === undefined) {
    return null;
  }
  text = text.replace(/@/, "@@");
  text = text.replace(/\\/, "@\\");
  text = text.replace(/'/, "@'");
  text = text.replace(/\[/, "@[");
  text = text.replace(/\]/, "@]");
  return encodeURIComponent(text);
};



/* 
 * MarkerManager, v1.0
 * Copyright (c) 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 *
 *
 * Author: Doug Ricket, others
 * 
 * Marker manager is an interface between the map and the user, designed
 * to manage adding and removing many points when the viewport changes.
 *
 *
 * Algorithm: The MM places its markers onto a grid, similar to the map tiles.
 * When the user moves the viewport, the MM computes which grid cells have
 * entered or left the viewport, and shows or hides all the markers in those
 * cells.
 * (If the users scrolls the viewport beyond the markers that are loaded,
 * no markers will be visible until the EVENT_moveend triggers an update.)
 *
 * In practical consequences, this allows 10,000 markers to be distributed over
 * a large area, and as long as only 100-200 are visible in any given viewport,
 * the user will see good performance corresponding to the 100 visible markers,
 * rather than poor performance corresponding to the total 10,000 markers.
 *
 * Note that some code is optimized for speed over space,
 * with the goal of accommodating thousands of markers.
 *
 */



/**
 * Creates a new MarkerManager that will show/hide markers on a map.
 *
 * @constructor
 * @param {Map} map The map to manage.
 * @param {Object} opt_opts A container for optional arguments:
 *   {Number} maxZoom The maximum zoom level for which to create tiles.
 *   {Number} borderPadding The width in pixels beyond the map border,
 *                   where markers should be display.
 *   {Boolean} trackMarkers Whether or not this manager should track marker
 *                   movements.
 */
function MarkerManager(map, opt_opts) {
  var me = this;
  me.map_ = map;
  me.mapZoom_ = map.getZoom();
  me.projection_ = map.getCurrentMapType().getProjection();

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

  me.trackMarkers_ = opt_opts.trackMarkers;

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

  me.gridWidth_ = [];

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

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

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

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

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

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


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

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


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


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

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

  for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
    var cell = this.getGridCellCreate_(gridPoint.x, gridPoint.y, zoom);
    cell.push(marker);

    gridPoint.x = gridPoint.x >> 1;
    gridPoint.y = gridPoint.y >> 1;
  }
};


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


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


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

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


/**
 * Add many markers at once.
 * Does not actually update the map, just the internal grid.
 *
 * @param {Array of Marker} markers The markers to add.
 * @param {Number} minZoom The minimum zoom level to display the markers.
 * @param {Number} opt_maxZoom The maximum zoom level to display the markers.
 */
MarkerManager.prototype.addMarkers = function(markers, minZoom, opt_maxZoom) {
  var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
  for (var i = markers.length - 1; i >= 0; i--) {
    this.addMarkerBatch_(markers[i], minZoom, maxZoom);
  }

  this.numMarkers_[minZoom] += markers.length;
};


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


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


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

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

/**
 * Get a cell in the grid, creating it first if necessary.
 *
 * Optimization candidate
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @return {Array} The cell in the array.
 */
MarkerManager.prototype.getGridCellCreate_ = function(x, y, z) {
  var grid = this.grid_[z];
  if (x < 0) {
    x += this.gridWidth_[z];
  }
  var gridCol = grid[x];
  if (!gridCol) {
    gridCol = grid[x] = [];
    return gridCol[y] = [];
  }
  var gridCell = gridCol[y];
  if (!gridCell) {
    return gridCol[y] = [];
  }
  return gridCell;
};


/**
 * Get a cell in the grid, returning undefined if it does not exist.
 *
 * NOTE: Optimized for speed -- otherwise could combine with getGridCellCreate_.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @return {Array} The cell in the array.
 */
MarkerManager.prototype.getGridCellNoCreate_ = function(x, y, z) {
  var grid = this.grid_[z];
  if (x < 0) {
    x += this.gridWidth_[z];
  }
  var gridCol = grid[x];
  return gridCol ? gridCol[y] : undefined;
};


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


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


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


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


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


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

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

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

  me.notifyListeners_();
};


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


/**
 * Process all markers in the bounds provided, using a callback.
 *
 * @param {Bounds} bounds The bounds in grid space.
 * @param {Function} callback The function to call for each marker.
 */
MarkerManager.prototype.processAll_ = function(bounds, callback) {
  for (var x = bounds.minX; x <= bounds.maxX; x++) {
    for (var y = bounds.minY; y <= bounds.maxY; y++) {
      this.processCellMarkers_(x, y,  bounds.z, callback);
    }
  }
};


/**
 * Process all markers in the grid cell, using a callback.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @param {Function} callback The function to call for each marker.
 */
MarkerManager.prototype.processCellMarkers_ = function(x, y, z, callback) {
  var cell = this.getGridCellNoCreate_(x, y, z);
  if (cell) {
    for (var i = cell.length - 1; i >= 0; i--) {
      callback(cell[i]);
    }
  }
};


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


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


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


/**
 * Calls the function for all points in bounds1, not in bounds2
 *
 * @param {Bounds} bounds1 The bounds of all points we may process.
 * @param {Bounds} bounds2 The bounds of points to exclude.
 * @param {Function} callback The callback function to call
 *                   for each grid coordinate.
 */
MarkerManager.prototype.rectangleDiffCoords = function(bounds1, bounds2, callback) {
  var minX1 = bounds1.minX;
  var minY1 = bounds1.minY;
  var maxX1 = bounds1.maxX;
  var maxY1 = bounds1.maxY;
  var minX2 = bounds2.minX;
  var minY2 = bounds2.minY;
  var maxX2 = bounds2.maxX;
  var maxY2 = bounds2.maxY;

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

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


/**
 * Removes value from array. O(N).
 *
 * @param {Array} array  The array to modify.
 * @param {any} value  The value to remove.
 * @param {Boolean} opt_notype  Flag to disable type checking in equality.
 * @return {Number}  The number of instances of value that were removed.
 */
MarkerManager.prototype.removeFromArray = function(array, value, opt_notype) {
  var shift = 0;
  for (var i = 0; i < array.length; ++i) {
    if (array[i] === value || (opt_notype && array[i] == value)) {
      array.splice(i--, 1);
      shift++;
    }
  }
  return shift;
};


/**
 * MarkerTracker v1.0
 * Copyright (c) 2008 Dan Rummel
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 *
 *
 * Author: Dan Rummel (www.seero.com)
 *
 *  This ulitily displays directional indicators for "important" markers
 *  on that are out of your maps view.
 */


/**
 *  Creates a MarkerTracker for the given marker and displays it ont he map as needed.
 *
 * @constructor
 * @param {Map} map The map that will display the MarkerTracker. 
 * @param {GMarker} marker the marker to be tracked.
 * @param {Object} opts? Object that contains the options for coustomizing the 
 *                  look and behavior of arrows:
 *   {Number} iconScale Scales the icon size by this value, 0 = no icon.
 *   {Number} padding The padding between the arrow and the edge of the map.
 *   {String} color Color of the arrow.
 *   {Number} weight Thickness of the lines that make up the arrows.
 *   {Number} length length of the arrow.
 *   {Number} opacity opacity of the arrow.
 *   {String} updateEvent The GMap2 event name that triggers the arrows to update.
 *   {String} panEvent The GMarker event name that triggers a quick zoom to the tracked marker.
 *   {Boolean} quickPanEnabled The GMarker event name that triggers a quick zoom to the tracked marker.
 */

function MarkerTracker(marker, map, opts) {
  this.map_ = map;
  this.marker_ = marker;
  this.enabled_ = true;
  this.arrowDisplayed_ = false;
  this.arrow_ = null;
  this.oldArrow_ = null;
  this.control_ = null;
  
  // setup the options
  opts = opts || {};
  this.iconScale_ = MarkerTracker.DEFAULT_ICON_SCALE_;
  if (opts.iconScale != undefined ) {
    this.iconScale_ = opts.iconScale;
  }
  this.padding_ = MarkerTracker.DEFAULT_EDGE_PADDING_;
  if (opts.padding != undefined ) {
    this.padding_ = opts.padding;
  }
  this.color_ = MarkerTracker.DEFAULT_ARROW_COLOR_;
  if (opts.color != undefined ) {
    this.color_ = opts.color;
  }
  this.weight_ = MarkerTracker.DEFAULT_ARROW_WEIGHT_;
  if (opts.weight != undefined ) {
    this.weight_ = opts.weight;
  }
  this.length_ = MarkerTracker.DEFAULT_ARROW_LENGTH_;
  if (opts.length != undefined ) {
    this.length_ = opts.length;
  }
  this.opacity_ = MarkerTracker.DEFAULT_ARROW_OPACITY_;
  if (opts.opacity != undefined ) {
    this.opacity_ = opts.opacity;
  }
  this.updateEvent_ = MarkerTracker.DEFAULT_UPDATE_EVENT_;
  if (opts.updateEvent != undefined ) {
    this.updateEvent_ = opts.updateEvent;
  }
  this.panEvent_ = MarkerTracker.DEFAULT_PAN_EVENT_;
  if (opts.panEvent != undefined ) {
    this.panEvent_ = opts.panEvent;
  }
  this.quickPanEnabled_ = MarkerTracker.DEFAULT_QUICK_PAN_ENABLED_;
  if (opts.quickPanEnabled != undefined ) {
    this.quickPanEnabled_ = opts.quickPanEnabled;
  }
  
  //replicate a different sized icon 
  var babyIcon = new GIcon(marker.getIcon());
  babyIcon.iconSize = new GSize( 
    marker.getIcon().iconSize.width * this.iconScale_,
    marker.getIcon().iconSize.height * this.iconScale_ );
  babyIcon.iconAnchor = new GPoint( 
    marker.getIcon().iconAnchor.x * this.iconScale_,
    marker.getIcon().iconAnchor.y * this.iconScale_/2);
  // kill the shadow
  babyIcon.shadow = null;
  this.babyMarker_ = new GMarker(new GPoint(0, 0), babyIcon);
  
  //bind the update task to the event trigger
  GEvent.bind(this.map_, this.updateEvent_, this, this.updateArrow_ );
  //update the arrow if the marker moves
  GEvent.bind(this.marker_, 'changed', this, this.updateArrow_ );
  if (this.quickPanEnabled_) {
    GEvent.bind(this.babyMarker_, this.panEvent_, this, this.panToMarker_ );
  }
  
  //do an inital check
  this.updateArrow_();
};


//Default Arrow Constants
MarkerTracker.DEFAULT_EDGE_PADDING_ = 25;
MarkerTracker.DEFAULT_ICON_SCALE_ = 0.6;
MarkerTracker.DEFAULT_ARROW_COLOR_ = '#ff0000';
MarkerTracker.DEFAULT_ARROW_WEIGHT_ = 20;
MarkerTracker.DEFAULT_ARROW_LENGTH_ = 20;
MarkerTracker.DEFAULT_ARROW_OPACITY_ = 0.8;
MarkerTracker.DEFAULT_UPDATE_EVENT_ = 'move';
MarkerTracker.DEFAULT_PAN_EVENT_ = 'click';
MarkerTracker.DEFAULT_QUICK_PAN_ENABLED_ = true;

//Default Control Constants

/**
 *  Disables the marker tracker.
 */
MarkerTracker.prototype.disable = function() {
  this.enabled_ = false;
  this.updateArrow_();
};

/**
 *  Enables the marker tracker.
 */
MarkerTracker.prototype.enable = function() {
  this.enabled_ = true;
  this.updateArrow_();
};

/**
 *  Called on on the trigger event to update the arrow. Primary function is to
 *  check if the parent marker is in view, if not draw the tracking arrow.
 */

MarkerTracker.prototype.updateArrow_ = function() {
  if(!this.map_.getBounds().containsLatLng(this.marker_.getLatLng()) && this.enabled_) {
    this.drawArrow_();
  } else if(this.arrowDisplayed_) {
    this.hideArrow_();
  }
};



/**
 *  Draws or redraws the arrow as needed, called when the parent marker is
 *  not with in the map view.
 */

MarkerTracker.prototype.drawArrow_ = function() {

  //convert to pixels
  var bounds = this.map_.getBounds();
  var SE = this.map_.fromLatLngToDivPixel(bounds.getSouthWest());
  var NE = this.map_.fromLatLngToDivPixel(bounds.getNorthEast());
  //include the padding while deciding on the arrow location
  var minX =  SE.x + this.padding_;
  var minY =  NE.y + this.padding_;
  var maxX =  NE.x - this.padding_;
  var maxY =  SE.y - this.padding_;
  
  // find the geometric info for the marker realative to the center of the map
  var center = this.map_.fromLatLngToDivPixel(this.map_.getCenter());
  var loc = this.map_.fromLatLngToDivPixel(this.marker_.getLatLng());
  
  //get the slope of the line
  var m = (center.y-loc.y) / (center.x-loc.x);
  var b = (center.y - m*center.x);
  
  // end the line within the bounds
  if ( loc.x < maxX && loc.x > minX ) {
    var x = loc.x;
  } else if (center.x > loc.x) {
    var x = minX; 
  } else {
    var x = maxX;
  }

  //calculate y and check boundaries again  
  var y = m * x + b;
  if( y > maxY ) {
    y = maxY;
    x = (y - b)/m;
  } else if(y < minY) {
    y = minY;
    x = (y - b) / m;
  }
  
  // get the proper angle of the arrow
  var ang = Math.atan(-m);
  if(x > center.x ) {
    ang = ang + Math.PI; 
  } 
  
  // define the point of the arrow
  var arrowLoc = this.map_.fromDivPixelToLatLng(new GPoint(x, y));
  
  // left side of marker is at -1,1
  var arrowLeft = this.map_.fromDivPixelToLatLng( 
            this.getRotatedPoint_(((-1) * this.length_), this.length_, ang, x, y) );
            
  // right side of marker is at -1,-1
  var arrowRight = this.map_.fromDivPixelToLatLng( 
            this.getRotatedPoint_(((-1)*this.length_), ((-1)*this.length_), ang, x, y));
  
  
  var center = this.map_.getCenter();
  var loc = this.marker_.getLatLng();
  
  this.oldArrow_ = this.arrow_;
  this.arrow_ = new GPolyline([arrowLeft, arrowLoc, arrowRight],
                this.color_, this.weight_, this.opacity_) ;
  this.map_.addOverlay(this.arrow_);
  
  // move the babyMarker to -1,0
  this.babyMarker_.setLatLng(this.map_.fromDivPixelToLatLng( 
            this.getRotatedPoint_(((-2)*this.length_), 0, ang, x, y)));
          
  if (!this.arrowDisplayed_) {
    this.map_.addOverlay(this.babyMarker_);
    this.arrowDisplayed_ = true;
  }
  if (this.oldArrow_) {
    this.map_.removeOverlay(this.oldArrow_);
  }
};



/**
 *  Hides the arrows.
 */
 
MarkerTracker.prototype.hideArrow_ = function() {
  this.map_.removeOverlay(this.babyMarker_);
  if(this.arrow_) {
    this.map_.removeOverlay(this.arrow_);
  }
  if(this.oldArrow_) {
    this.map_.removeOverlay(this.oldArrow_);
  }
  this.arrowDisplayed_ = false;
};


/**
 *  Pans the map to the parent marker.
 */

MarkerTracker.prototype.panToMarker_ = function() {
  this.map_.panTo(this.marker_.getLatLng());
};

/**
 *  This applies a counter-clockwise rotation to any point.
 *  
 * @param {Number} x The x value of the point.
 * @param {Number} y The y value of the point.
 * @param {Number} ang The counter clockwise angle of rotation.
 * @param {Number} xoffset Adds a position offset to the x position.
 * @param {Number} yoffset Adds a position offset to the y position.
 * @return {GPoint} A rotated GPoint.
 */

MarkerTracker.prototype.getRotatedPoint_ = function(x, y, ang, xoffset, yoffset) {
  var newx = y * Math.sin(ang) - x * Math.cos(ang) + xoffset;
  var newy = x * Math.sin(ang) + y * Math.cos(ang) + yoffset;
  var rotatedPoint = new GPoint(newx, newy);
  return(rotatedPoint);
};







/*  Prototype JavaScript framework, version 1.6.0.3
 *  (c) 2005-2008 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.6.0.3',

  Browser: {
    IE:     !!(window.attachEvent &&
      navigator.userAgent.indexOf('Opera') === -1),
    Opera:  navigator.userAgent.indexOf('Opera') > -1,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 &&
      navigator.userAgent.indexOf('KHTML') === -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  },

  BrowserFeatures: {
    XPath: !!document.evaluate,
    SelectorsAPI: !!document.querySelector,
    ElementExtensions: !!window.HTMLElement,
    SpecificElementExtensions:
      document.createElement('div')['__proto__'] &&
      document.createElement('div')['__proto__'] !==
        document.createElement('form')['__proto__']
  },

  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

  emptyFunction: function() { },
  K: function(x) { return x }
};

if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions = false;


/* Based on Alex Arnell's inheritance implementation. */
var Class = {
  create: function() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;

    return klass;
  }
};

Class.Methods = {
  addMethods: function(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length)
      properties.push("toString", "valueOf");

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value;
        value = (function(m) {
          return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }
};

var Abstract = { };

Object.extend = function(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
};

Object.extend(Object, {
  inspect: function(object) {
    try {
      if (Object.isUndefined(object)) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : String(object);
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

  toJSON: function(object) {
    var type = typeof object;
    switch (type) {
      case 'undefined':
      case 'function':
      case 'unknown': return;
      case 'boolean': return object.toString();
    }

    if (object === null) return 'null';
    if (object.toJSON) return object.toJSON();
    if (Object.isElement(object)) return;

    var results = [];
    for (var property in object) {
      var value = Object.toJSON(object[property]);
      if (!Object.isUndefined(value))
        results.push(property.toJSON() + ': ' + value);
    }

    return '{' + results.join(', ') + '}';
  },

  toQueryString: function(object) {
    return $H(object).toQueryString();
  },

  toHTML: function(object) {
    return object && object.toHTML ? object.toHTML() : String.interpret(object);
  },

  keys: function(object) {
    var keys = [];
    for (var property in object)
      keys.push(property);
    return keys;
  },

  values: function(object) {
    var values = [];
    for (var property in object)
      values.push(object[property]);
    return values;
  },

  clone: function(object) {
    return Object.extend({ }, object);
  },

  isElement: function(object) {
    return !!(object && object.nodeType == 1);
  },

  isArray: function(object) {
    return object != null && typeof object == "object" &&
      'splice' in object && 'join' in object;
  },

  isHash: function(object) {
    return object instanceof Hash;
  },

  isFunction: function(object) {
    return typeof object == "function";
  },

  isString: function(object) {
    return typeof object == "string";
  },

  isNumber: function(object) {
    return typeof object == "number";
  },

  isUndefined: function(object) {
    return typeof object == "undefined";
  }
});

Object.extend(Function.prototype, {
  argumentNames: function() {
    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
      .replace(/\s+/g, '').split(',');
    return names.length == 1 && !names[0] ? [] : names;
  },

  bind: function() {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = $A(arguments), object = args.shift();
    return function() {
      return __method.apply(object, args.concat($A(arguments)));
    }
  },

  bindAsEventListener: function() {
    var __method = this, args = $A(arguments), object = args.shift();
    return function(event) {
      return __method.apply(object, [event || window.event].concat(args));
    }
  },

  curry: function() {
    if (!arguments.length) return this;
    var __method = this, args = $A(arguments);
    return function() {
      return __method.apply(this, args.concat($A(arguments)));
    }
  },

  delay: function() {
    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  },

  defer: function() {
    var args = [0.01].concat($A(arguments));
    return this.delay.apply(this, args);
  },

  wrap: function(wrapper) {
    var __method = this;
    return function() {
      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
    }
  },

  methodize: function() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      return __method.apply(null, [this].concat($A(arguments)));
    };
  }
});

Date.prototype.toJSON = function() {
  return '"' + this.getUTCFullYear() + '-' +
    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
    this.getUTCDate().toPaddedString(2) + 'T' +
    this.getUTCHours().toPaddedString(2) + ':' +
    this.getUTCMinutes().toPaddedString(2) + ':' +
    this.getUTCSeconds().toPaddedString(2) + 'Z"';
};

var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) { }
    }

    return returnValue;
  }
};

RegExp.prototype.match = RegExp.prototype.test;

RegExp.escape = function(str) {
  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};

/*--------------------------------------------------------------------------*/

var PeriodicalExecuter = Class.create({
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  execute: function() {
    this.callback(this);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.execute();
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, {
  gsub: function(pattern, replacement) {
    var result = '', source = this, match;
    replacement = arguments.callee.prepareReplacement(replacement);

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  },

  sub: function(pattern, replacement, count) {
    replacement = this.gsub.prepareReplacement(replacement);
    count = Object.isUndefined(count) ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  },

  scan: function(pattern, iterator) {
    this.gsub(pattern, iterator);
    return String(this);
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = Object.isUndefined(truncation) ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : String(this);
  },

  strip: function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  },

  stripTags: function() {
    return this.replace(/<\/?[^>]+>/gi, '');
  },

  stripScripts: function() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  },

  extractScripts: function() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  },

  evalScripts: function() {
    return this.extractScripts().map(function(script) { return eval(script) });
  },

  escapeHTML: function() {
    var self = arguments.callee;
    self.text.data = this;
    return self.div.innerHTML;
  },

  unescapeHTML: function() {
    var div = new Element('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? (div.childNodes.length > 1 ?
      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
      div.childNodes[0].nodeValue) : '';
  },

  toQueryParams: function(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return { };

    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var key = decodeURIComponent(pair.shift());
        var value = pair.length > 1 ? pair.join('=') : pair[0];
        if (value != undefined) value = decodeURIComponent(value);

        if (key in hash) {
          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  },

  toArray: function() {
    return this.split('');
  },

  succ: function() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  },

  times: function(count) {
    return count < 1 ? '' : new Array(count + 1).join(this);
  },

  camelize: function() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  },

  capitalize: function() {
    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
  },

  underscore: function() {
    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
  },

  dasherize: function() {
    return this.gsub(/_/,'-');
  },

  inspect: function(useDoubleQuotes) {
    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
      var character = String.specialChar[match[0]];
      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
    });
    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  },

  toJSON: function() {
    return this.inspect(true);
  },

  unfilterJSON: function(filter) {
    return this.sub(filter || Prototype.JSONFilter, '#{1}');
  },

  isJSON: function() {
    var str = this;
    if (str.blank()) return false;
    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
  },

  evalJSON: function(sanitize) {
    var json = this.unfilterJSON();
    try {
      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
    } catch (e) { }
    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
  },

  include: function(pattern) {
    return this.indexOf(pattern) > -1;
  },

  startsWith: function(pattern) {
    return this.indexOf(pattern) === 0;
  },

  endsWith: function(pattern) {
    var d = this.length - pattern.length;
    return d >= 0 && this.lastIndexOf(pattern) === d;
  },

  empty: function() {
    return this == '';
  },

  blank: function() {
    return /^\s*$/.test(this);
  },

  interpolate: function(object, pattern) {
    return new Template(this, pattern).evaluate(object);
  }
});

if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
  escapeHTML: function() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  },
  unescapeHTML: function() {
    return this.stripTags().replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
  }
});

String.prototype.gsub.prepareReplacement = function(replacement) {
  if (Object.isFunction(replacement)) return replacement;
  var template = new Template(replacement);
  return function(match) { return template.evaluate(match) };
};

String.prototype.parseQuery = String.prototype.toQueryParams;

Object.extend(String.prototype.escapeHTML, {
  div:  document.createElement('div'),
  text: document.createTextNode('')
});

String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);

var Template = Class.create({
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    if (Object.isFunction(object.toTemplateReplacements))
      object = object.toTemplateReplacements();

    return this.template.gsub(this.pattern, function(match) {
      if (object == null) return '';

      var before = match[1] || '';
      if (before == '\\') return match[2];

      var ctx = object, expr = match[3];
      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
      match = pattern.exec(expr);
      if (match == null) return before;

      while (match != null) {
        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
        ctx = ctx[comp];
        if (null == ctx || '' == match[3]) break;
        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
        match = pattern.exec(expr);
      }

      return before + String.interpret(ctx);
    });
  }
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

var $break = { };

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    try {
      this._each(function(value) {
        iterator.call(context, value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },

  eachSlice: function(number, iterator, context) {
    var index = -number, slices = [], array = this.toArray();
    if (number < 1) return array;
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.collect(iterator, context);
  },

  all: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = true;
    this.each(function(value, index) {
      result = result && !!iterator.call(context, value, index);
      if (!result) throw $break;
    });
    return result;
  },

  any: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = false;
    this.each(function(value, index) {
      if (result = !!iterator.call(context, value, index))
        throw $break;
    });
    return result;
  },

  collect: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];
    this.each(function(value, index) {
      results.push(iterator.call(context, value, index));
    });
    return results;
  },

  detect: function(iterator, context) {
    var result;
    this.each(function(value, index) {
      if (iterator.call(context, value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  },

  findAll: function(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  },

  grep: function(filter, iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];

    if (Object.isString(filter))
      filter = new RegExp(filter);

    this.each(function(value, index) {
      if (filter.match(value))
        results.push(iterator.call(context, value, index));
    });
    return results;
  },

  include: function(object) {
    if (Object.isFunction(this.indexOf))
      if (this.indexOf(object) != -1) return true;

    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  },

  inGroupsOf: function(number, fillWith) {
    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  },

  inject: function(memo, iterator, context) {
    this.each(function(value, index) {
      memo = iterator.call(context, memo, value, index);
    });
    return memo;
  },

  invoke: function(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  },

  max: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value >= result)
        result = value;
    });
    return result;
  },

  min: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value < result)
        result = value;
    });
    return result;
  },

  partition: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var trues = [], falses = [];
    this.each(function(value, index) {
      (iterator.call(context, value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  },

  pluck: function(property) {
    var results = [];
    this.each(function(value) {
      results.push(value[property]);
    });
    return results;
  },

  reject: function(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (!iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  },

  sortBy: function(iterator, context) {
    return this.map(function(value, index) {
      return {
        value: value,
        criteria: iterator.call(context, value, index)
      };
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  },

  toArray: function() {
    return this.map();
  },

  zip: function() {
    var iterator = Prototype.K, args = $A(arguments);
    if (Object.isFunction(args.last()))
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  },

  size: function() {
    return this.toArray().length;
  },

  inspect: function() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }
};

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  filter:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray,
  every:   Enumerable.all,
  some:    Enumerable.any
});
function $A(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

if (Prototype.Browser.WebKit) {
  $A = function(iterable) {
    if (!iterable) return [];
    // In Safari, only use the `toArray` method if it's not a NodeList.
    // A NodeList is a function, has an function `item` property, and a numeric
    // `length` property. Adapted from Google Doctype.
    if (!(typeof iterable === 'function' && typeof iterable.length ===
        'number' && typeof iterable.item === 'function') && iterable.toArray)
      return iterable.toArray();
    var length = iterable.length || 0, results = new Array(length);
    while (length--) results[length] = iterable[length];
    return results;
  };
}

Array.from = $A;

Object.extend(Array.prototype, Enumerable);

if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },

  clear: function() {
    this.length = 0;
    return this;
  },

  first: function() {
    return this[0];
  },

  last: function() {
    return this[this.length - 1];
  },

  compact: function() {
    return this.select(function(value) {
      return value != null;
    });
  },

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(Object.isArray(value) ?
        value.flatten() : [value]);
    });
  },

  without: function() {
    var values = $A(arguments);
    return this.select(function(value) {
      return !values.include(value);
    });
  },

  reverse: function(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  },

  reduce: function() {
    return this.length > 1 ? this : this[0];
  },

  uniq: function(sorted) {
    return this.inject([], function(array, value, index) {
      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
        array.push(value);
      return array;
    });
  },

  intersect: function(array) {
    return this.uniq().findAll(function(item) {
      return array.detect(function(value) { return item === value });
    });
  },

  clone: function() {
    return [].concat(this);
  },

  size: function() {
    return this.length;
  },

  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  },

  toJSON: function() {
    var results = [];
    this.each(function(object) {
      var value = Object.toJSON(object);
      if (!Object.isUndefined(value)) results.push(value);
    });
    return '[' + results.join(', ') + ']';
  }
});

// use native browser JS 1.6 implementation if available
if (Object.isFunction(Array.prototype.forEach))
  Array.prototype._each = Array.prototype.forEach;

if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
  i || (i = 0);
  var length = this.length;
  if (i < 0) i = length + i;
  for (; i < length; i++)
    if (this[i] === item) return i;
  return -1;
};

if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
  var n = this.slice(0, i).reverse().indexOf(item);
  return (n < 0) ? n : i - n - 1;
};

Array.prototype.toArray = Array.prototype.clone;

function $w(string) {
  if (!Object.isString(string)) return [];
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

if (Prototype.Browser.Opera){
  Array.prototype.concat = function() {
    var array = [];
    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
    for (var i = 0, length = arguments.length; i < length; i++) {
      if (Object.isArray(arguments[i])) {
        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
          array.push(arguments[i][j]);
      } else {
        array.push(arguments[i]);
      }
    }
    return array;
  };
}
Object.extend(Number.prototype, {
  toColorPart: function() {
    return this.toPaddedString(2, 16);
  },

  succ: function() {
    return this + 1;
  },

  times: function(iterator, context) {
    $R(0, this, true).each(iterator, context);
    return this;
  },

  toPaddedString: function(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  },

  toJSON: function() {
    return isFinite(this) ? this.toString() : 'null';
  }
});

$w('abs round ceil floor').each(function(method){
  Number.prototype[method] = Math[method].methodize();
});
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  return {
    initialize: function(object) {
      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
    },

    _each: function(iterator) {
      for (var key in this._object) {
        var value = this._object[key], pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    },

    set: function(key, value) {
      return this._object[key] = value;
    },

    get: function(key) {
      // simulating poorly supported hasOwnProperty
      if (this._object[key] !== Object.prototype[key])
        return this._object[key];
    },

    unset: function(key) {
      var value = this._object[key];
      delete this._object[key];
      return value;
    },

    toObject: function() {
      return Object.clone(this._object);
    },

    keys: function() {
      return this.pluck('key');
    },

    values: function() {
      return this.pluck('value');
    },

    index: function(value) {
      var match = this.detect(function(pair) {
        return pair.value === value;
      });
      return match && match.key;
    },

    merge: function(object) {
      return this.clone().update(object);
    },

    update: function(object) {
      return new Hash(object).inject(this, function(result, pair) {
        result.set(pair.key, pair.value);
        return result;
      });
    },

    toQueryString: function() {
      return this.inject([], function(results, pair) {
        var key = encodeURIComponent(pair.key), values = pair.value;

        if (values && typeof values == 'object') {
          if (Object.isArray(values))
            return results.concat(values.map(toQueryPair.curry(key)));
        } else results.push(toQueryPair(key, values));
        return results;
      }).join('&');
    },

    inspect: function() {
      return '#<Hash:{' + this.map(function(pair) {
        return pair.map(Object.inspect).join(': ');
      }).join(', ') + '}>';
    },

    toJSON: function() {
      return Object.toJSON(this.toObject());
    },

    clone: function() {
      return new Hash(this);
    }
  }
})());

Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
Hash.from = $H;
var ObjectRange = Class.create(Enumerable, {
  initialize: function(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  },

  _each: function(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  },

  include: function(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }
});

var $R = function(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
};

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (Object.isFunction(responder[callback])) {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) { }
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate:   function() { Ajax.activeRequestCount++ },
  onComplete: function() { Ajax.activeRequestCount-- }
});

Ajax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   '',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});

Ajax.Request = Class.create(Ajax.Base, {
  _complete: false,

  initialize: function($super, url, options) {
    $super(options);
    this.transport = Ajax.getTransport();
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      // simulate other verbs over post
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Object.toQueryString(params)) {
      // when GET, append parameters to URL
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      var response = new Ajax.Response(this);
      if (this.options.onCreate) this.options.onCreate(response);
      Ajax.Responders.dispatch('onCreate', this, response);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    // user-defined headers
    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (Object.isFunction(extras.push))
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

  getStatus: function() {
    try {
      return this.transport.status || 0;
    } catch (e) { return 0 }
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && this.isSameOrigin() && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  isSameOrigin: function() {
    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
      protocol: location.protocol,
      domain: document.domain,
      port: location.port ? ':' + location.port : ''
    }));
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name) || null;
    } catch (e) { return null }
  },

  evalResponse: function() {
    try {
      return eval((this.transport.responseText || '').unfilterJSON());
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Response = Class.create({
  initialize: function(request){
    this.request = request;
    var transport  = this.transport  = request.transport,
        readyState = this.readyState = transport.readyState;

    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
      this.status       = this.getStatus();
      this.statusText   = this.getStatusText();
      this.responseText = String.interpret(transport.responseText);
      this.headerJSON   = this._getHeaderJSON();
    }

    if(readyState == 4) {
      var xml = transport.responseXML;
      this.responseXML  = Object.isUndefined(xml) ? null : xml;
      this.responseJSON = this._getResponseJSON();
    }
  },

  status:      0,
  statusText: '',

  getStatus: Ajax.Request.prototype.getStatus,

  getStatusText: function() {
    try {
      return this.transport.statusText || '';
    } catch (e) { return '' }
  },

  getHeader: Ajax.Request.prototype.getHeader,

  getAllHeaders: function() {
    try {
      return this.getAllResponseHeaders();
    } catch (e) { return null }
  },

  getResponseHeader: function(name) {
    return this.transport.getResponseHeader(name);
  },

  getAllResponseHeaders: function() {
    return this.transport.getAllResponseHeaders();
  },

  _getHeaderJSON: function() {
    var json = this.getHeader('X-JSON');
    if (!json) return null;
    json = decodeURIComponent(escape(json));
    try {
      return json.evalJSON(this.request.options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  },

  _getResponseJSON: function() {
    var options = this.request.options;
    if (!options.evalJSON || (options.evalJSON != 'force' &&
      !(this.getHeader('Content-type') || '').include('application/json')) ||
        this.responseText.blank())
          return null;
    try {
      return this.responseText.evalJSON(options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  }
});

Ajax.Updater = Class.create(Ajax.Request, {
  initialize: function($super, container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    };

    options = Object.clone(options);
    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.updateContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    $super(url, options);
  },

  updateContent: function(responseText) {
    var receiver = this.container[this.success() ? 'success' : 'failure'],
        options = this.options;

    if (!options.evalScripts) responseText = responseText.stripScripts();

    if (receiver = $(receiver)) {
      if (options.insertion) {
        if (Object.isString(options.insertion)) {
          var insertion = { }; insertion[options.insertion] = responseText;
          receiver.insert(insertion);
        }
        else options.insertion(receiver, responseText);
      }
      else receiver.update(responseText);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
  initialize: function($super, container, url, options) {
    $super(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = { };
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(response) {
    if (this.options.decay) {
      this.decay = (response.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = response.responseText;
    }
    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});
function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (Object.isString(element))
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(Element.extend(query.snapshotItem(i)));
    return results;
  };
}

/*--------------------------------------------------------------------------*/

if (!window.Node) var Node = { };

if (!Node.ELEMENT_NODE) {
  // DOM level 2 ECMAScript Language Binding
  Object.extend(Node, {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    CDATA_SECTION_NODE: 4,
    ENTITY_REFERENCE_NODE: 5,
    ENTITY_NODE: 6,
    PROCESSING_INSTRUCTION_NODE: 7,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_TYPE_NODE: 10,
    DOCUMENT_FRAGMENT_NODE: 11,
    NOTATION_NODE: 12
  });
}

(function() {
  var element = this.Element;
  this.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (Prototype.Browser.IE && attributes.name) {
      tagName = '<' + tagName + ' name="' + attributes.name + '">';
      delete attributes.name;
      return Element.writeAttribute(document.createElement(tagName), attributes);
    }
    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
  };
  Object.extend(this.Element, element || { });
  if (element) this.Element.prototype = element.prototype;
}).call(window);

Element.cache = { };

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },

  hide: function(element) {
    element = $(element);
    element.style.display = 'none';
    return element;
  },

  show: function(element) {
    element = $(element);
    element.style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);
    content = Object.toHTML(content);
    element.innerHTML = content.stripScripts();
    content.evalScripts.bind(content).defer();
    return element;
  },

  replace: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    else if (!Object.isElement(content)) {
      content = Object.toHTML(content);
      var range = element.ownerDocument.createRange();
      range.selectNode(element);
      content.evalScripts.bind(content).defer();
      content = range.createContextualFragment(content.stripScripts());
    }
    element.parentNode.replaceChild(content, element);
    return element;
  },

  insert: function(element, insertions) {
    element = $(element);

    if (Object.isString(insertions) || Object.isNumber(insertions) ||
        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
          insertions = {bottom:insertions};

    var content, insert, tagName, childNodes;

    for (var position in insertions) {
      content  = insertions[position];
      position = position.toLowerCase();
      insert = Element._insertionTranslations[position];

      if (content && content.toElement) content = content.toElement();
      if (Object.isElement(content)) {
        insert(element, content);
        continue;
      }

      content = Object.toHTML(content);

      tagName = ((position == 'before' || position == 'after')
        ? element.parentNode : element).tagName.toUpperCase();

      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

      if (position == 'top' || position == 'after') childNodes.reverse();
      childNodes.each(insert.curry(element));

      content.evalScripts.bind(content).defer();
    }

    return element;
  },

  wrap: function(element, wrapper, attributes) {
    element = $(element);
    if (Object.isElement(wrapper))
      $(wrapper).writeAttribute(attributes || { });
    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
    else wrapper = new Element('div', wrapper);
    if (element.parentNode)
      element.parentNode.replaceChild(wrapper, element);
    wrapper.appendChild(element);
    return wrapper;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return $(element).recursivelyCollect('parentNode');
  },

  descendants: function(element) {
    return $(element).select("*");
  },

  firstDescendant: function(element) {
    element = $(element).firstChild;
    while (element && element.nodeType != 1) element = element.nextSibling;
    return $(element);
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return $(element).recursivelyCollect('previousSibling');
  },

  nextSiblings: function(element) {
    return $(element).recursivelyCollect('nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return element.previousSiblings().reverse().concat(element.nextSiblings());
  },

  match: function(element, selector) {
    if (Object.isString(selector))
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(element.parentNode);
    var ancestors = element.ancestors();
    return Object.isNumber(expression) ? ancestors[expression] :
      Selector.findElement(ancestors, expression, index);
  },

  down: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return element.firstDescendant();
    return Object.isNumber(expression) ? element.descendants()[expression] :
      Element.select(element, expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = element.previousSiblings();
    return Object.isNumber(expression) ? previousSiblings[expression] :
      Selector.findElement(previousSiblings, expression, index);
  },

  next: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
    var nextSiblings = element.nextSiblings();
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },

  select: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element, args);
  },

  adjacent: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = element.readAttribute('id'), self = arguments.callee;
    if (id) return id;
    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
    element.writeAttribute('id', id);
    return id;
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (Prototype.Browser.IE) {
      var t = Element._attributeTranslations.read;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name]) name = t.names[name];
      if (name.include(':')) {
        return (!element.attributes || !element.attributes[name]) ? null :
         element.attributes[name].value;
      }
    }
    return element.getAttribute(name);
  },

  writeAttribute: function(element, name, value) {
    element = $(element);
    var attributes = { }, t = Element._attributeTranslations.write;

    if (typeof name == 'object') attributes = name;
    else attributes[name] = Object.isUndefined(value) ? true : value;

    for (var attr in attributes) {
      name = t.names[attr] || attr;
      value = attributes[attr];
      if (t.values[attr]) name = t.values[attr](element, value);
      if (value === false || value === null)
        element.removeAttribute(name);
      else if (value === true)
        element.setAttribute(name, name);
      else element.setAttribute(name, value);
    }
    return element;
  },

  getHeight: function(element) {
    return $(element).getDimensions().height;
  },

  getWidth: function(element) {
    return $(element).getDimensions().width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    return (elementClassName.length > 0 && (elementClassName == className ||
      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    if (!element.hasClassName(className))
      element.className += (element.className ? ' ' : '') + className;
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    element.className = element.className.replace(
      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    return element[element.hasClassName(className) ?
      'removeClassName' : 'addClassName'](className);
  },

  // removes whitespace-only text node children
  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.blank();
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);

    if (element.compareDocumentPosition)
      return (element.compareDocumentPosition(ancestor) & 8) === 8;

    if (ancestor.contains)
      return ancestor.contains(element) && ancestor !== element;

    while (element = element.parentNode)
      if (element == ancestor) return true;

    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = element.cumulativeOffset();
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    style = style == 'float' ? 'cssFloat' : style.camelize();
    var value = element.style[style];
    if (!value || value == 'auto') {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    }
    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
    return value == 'auto' ? null : value;
  },

  getOpacity: function(element) {
    return $(element).getStyle('opacity');
  },

  setStyle: function(element, styles) {
    element = $(element);
    var elementStyle = element.style, match;
    if (Object.isString(styles)) {
      element.style.cssText += ';' + styles;
      return styles.include('opacity') ?
        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
    }
    for (var property in styles)
      if (property == 'opacity') element.setOpacity(styles[property]);
      else
        elementStyle[(property == 'float' || property == 'cssFloat') ?
          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
            property] = styles[property];

    return element;
  },

  setOpacity: function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = element.getStyle('display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily
    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      // Opera returns the offset relative to the positioning context, when an
      // element is position relative but top and left have not been defined
      if (Prototype.Browser.Opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
    if (element._overflow !== 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if (element.tagName.toUpperCase() == 'BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p !== 'static') break;
      }
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  absolutize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'absolute') return element;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    var offsets = element.positionedOffset();
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
    return element;
  },

  relativize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'relative') return element;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
    return element;
  },

  cumulativeScrollOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  getOffsetParent: function(element) {
    if (element.offsetParent) return $(element.offsetParent);
    if (element == document.body) return $(element);

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return $(element);

    return $(document.body);
  },

  viewportOffset: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      // Safari fix
      if (element.offsetParent == document.body &&
        Element.getStyle(element, 'position') == 'absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return Element._returnOffset(valueL, valueT);
  },

  clonePosition: function(element, source) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || { });

    // find page position of source
    source = $(source);
    var p = source.viewportOffset();

    // find coordinate system to use
    element = $(element);
    var delta = [0, 0];
    var parent = null;
    // delta [0,0] will do fine with position: fixed elements,
    // position:absolute needs offsetParent deltas
    if (Element.getStyle(element, 'position') == 'absolute') {
      parent = element.getOffsetParent();
      delta = parent.viewportOffset();
    }

    // correct by body offsets (fixes Safari)
    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    // set position
    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
    return element;
  }
};

Element.Methods.identify.counter = 1;

Object.extend(Element.Methods, {
  getElementsBySelector: Element.Methods.select,
  childElements: Element.Methods.immediateDescendants
});

Element._attributeTranslations = {
  write: {
    names: {
      className: 'class',
      htmlFor:   'for'
    },
    values: { }
  }
};

if (Prototype.Browser.Opera) {
  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
    function(proceed, element, style) {
      switch (style) {
        case 'left': case 'top': case 'right': case 'bottom':
          if (proceed(element, 'position') === 'static') return null;
        case 'height': case 'width':
          // returns '0px' for hidden elements; we want it to return null
          if (!Element.visible(element)) return null;

          // returns the border-box dimensions rather than the content-box
          // dimensions, so we subtract padding and borders from the value
          var dim = parseInt(proceed(element, style), 10);

          if (dim !== element['offset' + style.capitalize()])
            return dim + 'px';

          var properties;
          if (style === 'height') {
            properties = ['border-top-width', 'padding-top',
             'padding-bottom', 'border-bottom-width'];
          }
          else {
            properties = ['border-left-width', 'padding-left',
             'padding-right', 'border-right-width'];
          }
          return properties.inject(dim, function(memo, property) {
            var val = proceed(element, property);
            return val === null ? memo : memo - parseInt(val, 10);
          }) + 'px';
        default: return proceed(element, style);
      }
    }
  );

  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
    function(proceed, element, attribute) {
      if (attribute === 'title') return element.title;
      return proceed(element, attribute);
    }
  );
}

else if (Prototype.Browser.IE) {
  // IE doesn't report offsets correctly for static elements, so we change them
  // to "relative" to get the values, then change them back.
  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
    function(proceed, element) {
      element = $(element);
      // IE throws an error if element is not in document
      try { element.offsetParent }
      catch(e) { return $(document.body) }
      var position = element.getStyle('position');
      if (position !== 'static') return proceed(element);
      element.setStyle({ position: 'relative' });
      var value = proceed(element);
      element.setStyle({ position: position });
      return value;
    }
  );

  $w('positionedOffset viewportOffset').each(function(method) {
    Element.Methods[method] = Element.Methods[method].wrap(
      function(proceed, element) {
        element = $(element);
        try { element.offsetParent }
        catch(e) { return Element._returnOffset(0,0) }
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        // Trigger hasLayout on the offset parent so that IE6 reports
        // accurate offsetTop and offsetLeft values for position: fixed.
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
    function(proceed, element) {
      try { element.offsetParent }
      catch(e) { return Element._returnOffset(0,0) }
      return proceed(element);
    }
  );

  Element.Methods.getStyle = function(element, style) {
    element = $(element);
    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];
    if (!value && element.currentStyle) value = element.currentStyle[style];

    if (style == 'opacity') {
      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if (value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }

    if (value == 'auto') {
      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
        return element['offset' + style.capitalize()] + 'px';
      return null;
    }
    return value;
  };

  Element.Methods.setOpacity = function(element, value) {
    function stripAlpha(filter){
      return filter.replace(/alpha\([^\)]*\)/gi,'');
    }
    element = $(element);
    var currentStyle = element.currentStyle;
    if ((currentStyle && !currentStyle.hasLayout) ||
      (!currentStyle && element.style.zoom == 'normal'))
        element.style.zoom = 1;

    var filter = element.getStyle('filter'), style = element.style;
    if (value == 1 || value === '') {
      (filter = stripAlpha(filter)) ?
        style.filter = filter : style.removeAttribute('filter');
      return element;
    } else if (value < 0.00001) value = 0;
    style.filter = stripAlpha(filter) +
      'alpha(opacity=' + (value * 100) + ')';
    return element;
  };

  Element._attributeTranslations = {
    read: {
      names: {
        'class': 'className',
        'for':   'htmlFor'
      },
      values: {
        _getAttr: function(element, attribute) {
          return element.getAttribute(attribute, 2);
        },
        _getAttrNode: function(element, attribute) {
          var node = element.getAttributeNode(attribute);
          return node ? node.value : "";
        },
        _getEv: function(element, attribute) {
          attribute = element.getAttribute(attribute);
          return attribute ? attribute.toString().slice(23, -2) : null;
        },
        _flag: function(element, attribute) {
          return $(element).hasAttribute(attribute) ? attribute : null;
        },
        style: function(element) {
          return element.style.cssText.toLowerCase();
        },
        title: function(element) {
          return element.title;
        }
      }
    }
  };

  Element._attributeTranslations.write = {
    names: Object.extend({
      cellpadding: 'cellPadding',
      cellspacing: 'cellSpacing'
    }, Element._attributeTranslations.read.names),
    values: {
      checked: function(element, value) {
        element.checked = !!value;
      },

      style: function(element, value) {
        element.style.cssText = value ? value : '';
      }
    }
  };

  Element._attributeTranslations.has = {};

  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
      'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

  (function(v) {
    Object.extend(v, {
      href:        v._getAttr,
      src:         v._getAttr,
      type:        v._getAttr,
      action:      v._getAttrNode,
      disabled:    v._flag,
      checked:     v._flag,
      readonly:    v._flag,
      multiple:    v._flag,
      onload:      v._getEv,
      onunload:    v._getEv,
      onclick:     v._getEv,
      ondblclick:  v._getEv,
      onmousedown: v._getEv,
      onmouseup:   v._getEv,
      onmouseover: v._getEv,
      onmousemove: v._getEv,
      onmouseout:  v._getEv,
      onfocus:     v._getEv,
      onblur:      v._getEv,
      onkeypress:  v._getEv,
      onkeydown:   v._getEv,
      onkeyup:     v._getEv,
      onsubmit:    v._getEv,
      onreset:     v._getEv,
      onselect:    v._getEv,
      onchange:    v._getEv
    });
  })(Element._attributeTranslations.read.values);
}

else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1) ? 0.999999 :
      (value === '') ? '' : (value < 0.00001) ? 0 : value;
    return element;
  };
}

else if (Prototype.Browser.WebKit) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;

    if (value == 1)
      if(element.tagName.toUpperCase() == 'IMG' && element.width) {
        element.width++; element.width--;
      } else try {
        var n = document.createTextNode(' ');
        element.appendChild(n);
        element.removeChild(n);
      } catch (e) { }

    return element;
  };

  // Safari returns margins on body which is incorrect if the child is absolutely
  // positioned.  For performance reasons, redefine Element#cumulativeOffset for
  // KHTML/WebKit only.
  Element.Methods.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return Element._returnOffset(valueL, valueT);
  };
}

if (Prototype.Browser.IE || Prototype.Browser.Opera) {
  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
  Element.Methods.update = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);

    content = Object.toHTML(content);
    var tagName = element.tagName.toUpperCase();

    if (tagName in Element._insertionTranslations.tags) {
      $A(element.childNodes).each(function(node) { element.removeChild(node) });
      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
        .each(function(node) { element.appendChild(node) });
    }
    else element.innerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

if ('outerHTML' in document.createElement('div')) {
  Element.Methods.replace = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) {
      element.parentNode.replaceChild(content, element);
      return element;
    }

    content = Object.toHTML(content);
    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

    if (Element._insertionTranslations.tags[tagName]) {
      var nextSibling = element.next();
      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
      parent.removeChild(element);
      if (nextSibling)
        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
      else
        fragments.each(function(node) { parent.appendChild(node) });
    }
    else element.outerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

Element._returnOffset = function(l, t) {
  var result = [l, t];
  result.left = l;
  result.top = t;
  return result;
};

Element._getContentFromAnonymousElement = function(tagName, html) {
  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
  if (t) {
    div.innerHTML = t[0] + html + t[1];
    t[2].times(function() { div = div.firstChild });
  } else div.innerHTML = html;
  return $A(div.childNodes);
};

Element._insertionTranslations = {
  before: function(element, node) {
    element.parentNode.insertBefore(node, element);
  },
  top: function(element, node) {
    element.insertBefore(node, element.firstChild);
  },
  bottom: function(element, node) {
    element.appendChild(node);
  },
  after: function(element, node) {
    element.parentNode.insertBefore(node, element.nextSibling);
  },
  tags: {
    TABLE:  ['<table>',                '</table>',                   1],
    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
    SELECT: ['<select>',               '</select>',                  1]
  }
};

(function() {
  Object.extend(this.tags, {
    THEAD: this.tags.TBODY,
    TFOOT: this.tags.TBODY,
    TH:    this.tags.TD
  });
}).call(Element._insertionTranslations);

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    attribute = Element._attributeTranslations.has[attribute] || attribute;
    var node = $(element).getAttributeNode(attribute);
    return !!(node && node.specified);
  }
};

Element.Methods.ByTag = { };

Object.extend(Element, Element.Methods);

if (!Prototype.BrowserFeatures.ElementExtensions &&
    document.createElement('div')['__proto__']) {
  window.HTMLElement = { };
  window.HTMLElement.prototype = document.createElement('div')['__proto__'];
  Prototype.BrowserFeatures.ElementExtensions = true;
}

Element.extend = (function() {
  if (Prototype.BrowserFeatures.SpecificElementExtensions)
    return Prototype.K;

  var Methods = { }, ByTag = Element.Methods.ByTag;

  var extend = Object.extend(function(element) {
    if (!element || element._extendedByPrototype ||
        element.nodeType != 1 || element == window) return element;

    var methods = Object.clone(Methods),
      tagName = element.tagName.toUpperCase(), property, value;

    // extend methods for specific tags
    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

    for (property in methods) {
      value = methods[property];
      if (Object.isFunction(value) && !(property in element))
        element[property] = value.methodize();
    }

    element._extendedByPrototype = Prototype.emptyFunction;
    return element;

  }, {
    refresh: function() {
      // extend methods for all tags (Safari doesn't need this)
      if (!Prototype.BrowserFeatures.ElementExtensions) {
        Object.extend(Methods, Element.Methods);
        Object.extend(Methods, Element.Methods.Simulated);
      }
    }
  });

  extend.refresh();
  return extend;
})();

Element.hasAttribute = function(element, attribute) {
  if (element.hasAttribute) return element.hasAttribute(attribute);
  return Element.Methods.Simulated.hasAttribute(element, attribute);
};

Element.addMethods = function(methods) {
  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

  if (!methods) {
    Object.extend(Form, Form.Methods);
    Object.extend(Form.Element, Form.Element.Methods);
    Object.extend(Element.Methods.ByTag, {
      "FORM":     Object.clone(Form.Methods),
      "INPUT":    Object.clone(Form.Element.Methods),
      "SELECT":   Object.clone(Form.Element.Methods),
      "TEXTAREA": Object.clone(Form.Element.Methods)
    });
  }

  if (arguments.length == 2) {
    var tagName = methods;
    methods = arguments[1];
  }

  if (!tagName) Object.extend(Element.Methods, methods || { });
  else {
    if (Object.isArray(tagName)) tagName.each(extend);
    else extend(tagName);
  }

  function extend(tagName) {
    tagName = tagName.toUpperCase();
    if (!Element.Methods.ByTag[tagName])
      Element.Methods.ByTag[tagName] = { };
    Object.extend(Element.Methods.ByTag[tagName], methods);
  }

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    for (var property in methods) {
      var value = methods[property];
      if (!Object.isFunction(value)) continue;
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = value.methodize();
    }
  }

  function findDOMClass(tagName) {
    var klass;
    var trans = {
      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
      "FrameSet", "IFRAME": "IFrame"
    };
    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName.capitalize() + 'Element';
    if (window[klass]) return window[klass];

    window[klass] = { };
    window[klass].prototype = document.createElement(tagName)['__proto__'];
    return window[klass];
  }

  if (F.ElementExtensions) {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
  }

  if (F.SpecificElementExtensions) {
    for (var tag in Element.Methods.ByTag) {
      var klass = findDOMClass(tag);
      if (Object.isUndefined(klass)) continue;
      copy(T[tag], klass.prototype);
    }
  }

  Object.extend(Element, Element.Methods);
  delete Element.ByTag;

  if (Element.extend.refresh) Element.extend.refresh();
  Element.cache = { };
};

document.viewport = {
  getDimensions: function() {
    var dimensions = { }, B = Prototype.Browser;
    $w('width height').each(function(d) {
      var D = d.capitalize();
      if (B.WebKit && !document.evaluate) {
        // Safari <3.0 needs self.innerWidth/Height
        dimensions[d] = self['inner' + D];
      } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
        // Opera <9.5 needs document.body.clientWidth/Height
        dimensions[d] = document.body['client' + D]
      } else {
        dimensions[d] = document.documentElement['client' + D];
      }
    });
    return dimensions;
  },

  getWidth: function() {
    return this.getDimensions().width;
  },

  getHeight: function() {
    return this.getDimensions().height;
  },

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  }
};
/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
 * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 * license.  Please see http://www.yui-ext.com/ for more information. */

var Selector = Class.create({
  initialize: function(expression) {
    this.expression = expression.strip();

    if (this.shouldUseSelectorsAPI()) {
      this.mode = 'selectorsAPI';
    } else if (this.shouldUseXPath()) {
      this.mode = 'xpath';
      this.compileXPathMatcher();
    } else {
      this.mode = "normal";
      this.compileMatcher();
    }

  },

  shouldUseXPath: function() {
    if (!Prototype.BrowserFeatures.XPath) return false;

    var e = this.expression;

    // Safari 3 chokes on :*-of-type and :empty
    if (Prototype.Browser.WebKit &&
     (e.include("-of-type") || e.include(":empty")))
      return false;

    // XPath can't do namespaced attributes, nor can it read
    // the "checked" property from DOM nodes
    if ((/(\[[\w-]*?:|:checked)/).test(e))
      return false;

    return true;
  },

  shouldUseSelectorsAPI: function() {
    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;

    if (!Selector._div) Selector._div = new Element('div');

    // Make sure the browser treats the selector as valid. Test on an
    // isolated element to minimize cost of this check.
    try {
      Selector._div.querySelector(this.expression);
    } catch(e) {
      return false;
    }

    return true;
  },

  compileMatcher: function() {
    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m;

    if (Selector._cache[e]) {
      this.matcher = Selector._cache[e];
      return;
    }

    this.matcher = ["this.matcher = function(root) {",
                    "var r = root, h = Selector.handlers, c = false, n;"];

    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
            new Template(c[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.matcher.push("return h.unique(n);\n}");
    eval(this.matcher.join('\n'));
    Selector._cache[this.expression] = this.matcher;
  },

  compileXPathMatcher: function() {
    var e = this.expression, ps = Selector.patterns,
        x = Selector.xpath, le, m;

    if (Selector._cache[e]) {
      this.xpath = Selector._cache[e]; return;
    }

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        if (m = e.match(ps[i])) {
          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
            new Template(x[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.xpath = this.matcher.join('');
    Selector._cache[this.expression] = this.xpath;
  },

  findElements: function(root) {
    root = root || document;
    var e = this.expression, results;

    switch (this.mode) {
      case 'selectorsAPI':
        // querySelectorAll queries document-wide, then filters to descendants
        // of the context element. That's not what we want.
        // Add an explicit context to the selector if necessary.
        if (root !== document) {
          var oldId = root.id, id = $(root).identify();
          e = "#" + id + " " + e;
        }

        results = $A(root.querySelectorAll(e)).map(Element.extend);
        root.id = oldId;

        return results;
      case 'xpath':
        return document._getElementsByXPath(this.xpath, root);
      default:
       return this.matcher(root);
    }
  },

  match: function(element) {
    this.tokens = [];

    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
    var le, p, m;

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          // use the Selector.assertions methods unless the selector
          // is too complex.
          if (as[i]) {
            this.tokens.push([i, Object.clone(m)]);
            e = e.replace(m[0], '');
          } else {
            // reluctantly do a document-wide search
            // and look for a match in the array
            return this.findElements(document).include(element);
          }
        }
      }
    }

    var match = true, name, matches;
    for (var i = 0, token; token = this.tokens[i]; i++) {
      name = token[0], matches = token[1];
      if (!Selector.assertions[name](element, matches)) {
        match = false; break;
      }
    }

    return match;
  },

  toString: function() {
    return this.expression;
  },

  inspect: function() {
    return "#<Selector:" + this.expression.inspect() + ">";
  }
});

Object.extend(Selector, {
  _cache: { },

  xpath: {
    descendant:   "//*",
    child:        "/*",
    adjacent:     "/following-sibling::*[1]",
    laterSibling: '/following-sibling::*',
    tagName:      function(m) {
      if (m[1] == '*') return '';
      return "[local-name()='" + m[1].toLowerCase() +
             "' or local-name()='" + m[1].toUpperCase() + "']";
    },
    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
    id:           "[@id='#{1}']",
    attrPresence: function(m) {
      m[1] = m[1].toLowerCase();
      return new Template("[@#{1}]").evaluate(m);
    },
    attr: function(m) {
      m[1] = m[1].toLowerCase();
      m[3] = m[5] || m[6];
      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
    },
    pseudo: function(m) {
      var h = Selector.xpath.pseudos[m[1]];
      if (!h) return '';
      if (Object.isFunction(h)) return h(m);
      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
    },
    operators: {
      '=':  "[@#{1}='#{3}']",
      '!=': "[@#{1}!='#{3}']",
      '^=': "[starts-with(@#{1}, '#{3}')]",
      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
      '*=': "[contains(@#{1}, '#{3}')]",
      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
    },
    pseudos: {
      'first-child': '[not(preceding-sibling::*)]',
      'last-child':  '[not(following-sibling::*)]',
      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
      'checked':     "[@checked]",
      'disabled':    "[(@disabled) and (@type!='hidden')]",
      'enabled':     "[not(@disabled) and (@type!='hidden')]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i in p) {
            if (m = e.match(p[i])) {
              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
              e = e.replace(m[0], '');
              break;
            }
          }
        }
        return "[not(" + exclusion.join(" and ") + ")]";
      },
      'nth-child':      function(m) {
        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
      },
      'nth-last-child': function(m) {
        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
      },
      'nth-of-type':    function(m) {
        return Selector.xpath.pseudos.nth("position() ", m);
      },
      'nth-last-of-type': function(m) {
        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
      },
      'first-of-type':  function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
      },
      'last-of-type':   function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
      },
      'only-of-type':   function(m) {
        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
      },
      nth: function(fragment, m) {
        var mm, formula = m[6], predicate;
        if (formula == 'even') formula = '2n+0';
        if (formula == 'odd')  formula = '2n+1';
        if (mm = formula.match(/^(\d+)$/)) // digit only
          return '[' + fragment + "= " + mm[1] + ']';
        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
          if (mm[1] == "-") mm[1] = -1;
          var a = mm[1] ? Number(mm[1]) : 1;
          var b = mm[2] ? Number(mm[2]) : 0;
          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
          "((#{fragment} - #{b}) div #{a} >= 0)]";
          return new Template(predicate).evaluate({
            fragment: fragment, a: a, b: b });
        }
      }
    }
  },

  criteria: {
    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
    attr: function(m) {
      m[3] = (m[5] || m[6]);
      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
    },
    pseudo: function(m) {
      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
    },
    descendant:   'c = "descendant";',
    child:        'c = "child";',
    adjacent:     'c = "adjacent";',
    laterSibling: 'c = "laterSibling";'
  },

  patterns: {
    // combinators must be listed first
    // (and descendant needs to be last combinator)
    laterSibling: /^\s*~\s*/,
    child:        /^\s*>\s*/,
    adjacent:     /^\s*\+\s*/,
    descendant:   /^\s/,

    // selectors follow
    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
    id:           /^#([\w\-\*]+)(\b|$)/,
    className:    /^\.([\w\-\*]+)(\b|$)/,
    pseudo:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
    attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
  },

  // for Selector.match and Element#match
  assertions: {
    tagName: function(element, matches) {
      return matches[1].toUpperCase() == element.tagName.toUpperCase();
    },

    className: function(element, matches) {
      return Element.hasClassName(element, matches[1]);
    },

    id: function(element, matches) {
      return element.id === matches[1];
    },

    attrPresence: function(element, matches) {
      return Element.hasAttribute(element, matches[1]);
    },

    attr: function(element, matches) {
      var nodeValue = Element.readAttribute(element, matches[1]);
      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
    }
  },

  handlers: {
    // UTILITY FUNCTIONS
    // joins two collections
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        a.push(node);
      return a;
    },

    // marks an array of nodes for counting
    mark: function(nodes) {
      var _true = Prototype.emptyFunction;
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = _true;
      return nodes;
    },

    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = undefined;
      return nodes;
    },

    // mark each child node with its position (for nth calls)
    // "ofType" flag indicates whether we're indexing for nth-of-type
    // rather than nth-child
    index: function(parentNode, reverse, ofType) {
      parentNode._countedByPrototype = Prototype.emptyFunction;
      if (reverse) {
        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
          var node = nodes[i];
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
        }
      } else {
        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
      }
    },

    // filters out duplicates and extends all nodes
    unique: function(nodes) {
      if (nodes.length == 0) return nodes;
      var results = [], n;
      for (var i = 0, l = nodes.length; i < l; i++)
        if (!(n = nodes[i])._countedByPrototype) {
          n._countedByPrototype = Prototype.emptyFunction;
          results.push(Element.extend(n));
        }
      return Selector.handlers.unmark(results);
    },

    // COMBINATOR FUNCTIONS
    descendant: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, node.getElementsByTagName('*'));
      return results;
    },

    child: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        for (var j = 0, child; child = node.childNodes[j]; j++)
          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
      }
      return results;
    },

    adjacent: function(nodes) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        var next = this.nextElementSibling(node);
        if (next) results.push(next);
      }
      return results;
    },

    laterSibling: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, Element.nextSiblings(node));
      return results;
    },

    nextElementSibling: function(node) {
      while (node = node.nextSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    // TOKEN FUNCTIONS
    tagName: function(nodes, root, tagName, combinator) {
      var uTagName = tagName.toUpperCase();
      var results = [], h = Selector.handlers;
      if (nodes) {
        if (combinator) {
          // fastlane for ordinary descendant combinators
          if (combinator == "descendant") {
            for (var i = 0, node; node = nodes[i]; i++)
              h.concat(results, node.getElementsByTagName(tagName));
            return results;
          } else nodes = this[combinator](nodes);
          if (tagName == "*") return nodes;
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName.toUpperCase() === uTagName) results.push(node);
        return results;
      } else return root.getElementsByTagName(tagName);
    },

    id: function(nodes, root, id, combinator) {
      var targetNode = $(id), h = Selector.handlers;
      if (!targetNode) return [];
      if (!nodes && root == document) return [targetNode];
      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

    className: function(nodes, root, className, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      return Selector.handlers.byClassName(nodes, root, className);
    },

    byClassName: function(nodes, root, className) {
      if (!nodes) nodes = Selector.handlers.descendant([root]);
      var needle = ' ' + className + ' ';
      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
        nodeClassName = node.className;
        if (nodeClassName.length == 0) continue;
        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
          results.push(node);
      }
      return results;
    },

    attrPresence: function(nodes, root, attr, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var results = [];
      for (var i = 0, node; node = nodes[i]; i++)
        if (Element.hasAttribute(node, attr)) results.push(node);
      return results;
    },

    attr: function(nodes, root, attr, value, operator, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var handler = Selector.operators[operator], results = [];
      for (var i = 0, node; node = nodes[i]; i++) {
        var nodeValue = Element.readAttribute(node, attr);
        if (nodeValue === null) continue;
        if (handler(nodeValue, value)) results.push(node);
      }
      return results;
    },

    pseudo: function(nodes, name, value, root, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      if (!nodes) nodes = root.getElementsByTagName("*");
      return Selector.pseudos[name](nodes, value, root);
    }
  },

  pseudos: {
    'first-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.previousElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'last-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.nextElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'only-child': function(nodes, value, root) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
          results.push(node);
      return results;
    },
    'nth-child':        function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root);
    },
    'nth-last-child':   function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true);
    },
    'nth-of-type':      function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, false, true);
    },
    'nth-last-of-type': function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true, true);
    },
    'first-of-type':    function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, false, true);
    },
    'last-of-type':     function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, true, true);
    },
    'only-of-type':     function(nodes, formula, root) {
      var p = Selector.pseudos;
      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
    },

    // handles the an+b logic
    getIndices: function(a, b, total) {
      if (a == 0) return b > 0 ? [b] : [];
      return $R(1, total).inject([], function(memo, i) {
        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
        return memo;
      });
    },

    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
    nth: function(nodes, formula, root, reverse, ofType) {
      if (nodes.length == 0) return [];
      if (formula == 'even') formula = '2n+0';
      if (formula == 'odd')  formula = '2n+1';
      var h = Selector.handlers, results = [], indexed = [], m;
      h.mark(nodes);
      for (var i = 0, node; node = nodes[i]; i++) {
        if (!node.parentNode._countedByPrototype) {
          h.index(node.parentNode, reverse, ofType);
          indexed.push(node.parentNode);
        }
      }
      if (formula.match(/^\d+$/)) { // just a number
        formula = Number(formula);
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.nodeIndex == formula) results.push(node);
      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
        if (m[1] == "-") m[1] = -1;
        var a = m[1] ? Number(m[1]) : 1;
        var b = m[2] ? Number(m[2]) : 0;
        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
          for (var j = 0; j < l; j++)
            if (node.nodeIndex == indices[j]) results.push(node);
        }
      }
      h.unmark(nodes);
      h.unmark(indexed);
      return results;
    },

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        // IE treats comments as element nodes
        if (node.tagName == '!' || node.firstChild) continue;
        results.push(node);
      }
      return results;
    },

    'not': function(nodes, selector, root) {
      var h = Selector.handlers, selectorType, m;
      var exclusions = new Selector(selector).findElements(root);
      h.mark(exclusions);
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node._countedByPrototype) results.push(node);
      h.unmark(exclusions);
      return results;
    },

    'enabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node.disabled && (!node.type || node.type !== 'hidden'))
          results.push(node);
      return results;
    },

    'disabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.disabled) results.push(node);
      return results;
    },

    'checked': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.checked) results.push(node);
      return results;
    }
  },

  operators: {
    '=':  function(nv, v) { return nv == v; },
    '!=': function(nv, v) { return nv != v; },
    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
    '$=': function(nv, v) { return nv.endsWith(v); },
    '*=': function(nv, v) { return nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
     '-').include('-' + (v || "").toUpperCase() + '-'); }
  },

  split: function(expression) {
    var expressions = [];
    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
      expressions.push(m[1].strip());
    });
    return expressions;
  },

  matchElements: function(elements, expression) {
    var matches = $$(expression), h = Selector.handlers;
    h.mark(matches);
    for (var i = 0, results = [], element; element = elements[i]; i++)
      if (element._countedByPrototype) results.push(element);
    h.unmark(matches);
    return results;
  },

  findElement: function(elements, expression, index) {
    if (Object.isNumber(expression)) {
      index = expression; expression = false;
    }
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    expressions = Selector.split(expressions.join(','));
    var results = [], h = Selector.handlers;
    for (var i = 0, l = expressions.length, selector; i < l; i++) {
      selector = new Selector(expressions[i].strip());
      h.concat(results, selector.findElements(element));
    }
    return (l > 1) ? h.unique(results) : results;
  }
});

if (Prototype.Browser.IE) {
  Object.extend(Selector.handlers, {
    // IE returns comment nodes on getElementsByTagName("*").
    // Filter them out.
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        if (node.tagName !== "!") a.push(node);
      return a;
    },

    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node.removeAttribute('_countedByPrototype');
      return nodes;
    }
  });
}

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}
var Form = {
  reset: function(form) {
    $(form).reset();
    return form;
  },

  serializeElements: function(elements, options) {
    if (typeof options != 'object') options = { hash: !!options };
    else if (Object.isUndefined(options.hash)) options.hash = true;
    var key, value, submitted = false, submit = options.submit;

    var data = elements.inject({ }, function(result, element) {
      if (!element.disabled && element.name) {
        key = element.name; value = $(element).getValue();
        if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted = true)))) {
          if (key in result) {
            // a key is already present; construct an array of values
            if (!Object.isArray(result[key])) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return options.hash ? data : Object.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, options) {
    return Form.serializeElements(Form.getElements(form), options);
  },

  getElements: function(form) {
    return $A($(form).getElementsByTagName('*')).inject([],
      function(elements, child) {
        if (Form.Element.Serializers[child.tagName.toLowerCase()])
          elements.push(Element.extend(child));
        return elements;
      }
    );
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('disable');
    return form;
  },

  enable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('enable');
    return form;
  },

  findFirstElement: function(form) {
    var elements = $(form).getElements().findAll(function(element) {
      return 'hidden' != element.type && !element.disabled;
    });
    var firstByIndex = elements.findAll(function(element) {
      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
    }).sortBy(function(element) { return element.tabIndex }).first();

    return firstByIndex ? firstByIndex : elements.find(function(element) {
      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  },

  request: function(form, options) {
    form = $(form), options = Object.clone(options || { });

    var params = options.parameters, action = form.readAttribute('action') || '';
    if (action.blank()) action = window.location.href;
    options.parameters = form.serialize(true);

    if (params) {
      if (Object.isString(params)) params = params.toQueryParams();
      Object.extend(options.parameters, params);
    }

    if (form.hasAttribute('method') && !options.method)
      options.method = form.method;

    return new Ajax.Request(action, options);
  }
};

/*--------------------------------------------------------------------------*/

Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
};

Form.Element.Methods = {
  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = { };
        pair[element.name] = value;
        return Object.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  setValue: function(element, value) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    Form.Element.Serializers[method](element, value);
    return element;
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    try {
      element.focus();
      if (element.select && (element.tagName.toLowerCase() != 'input' ||
          !['button', 'reset', 'submit'].include(element.type)))
        element.select();
    } catch (e) { }
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.disabled = false;
    return element;
  }
};

/*--------------------------------------------------------------------------*/

var Field = Form.Element;
var $F = Form.Element.Methods.getValue;

/*--------------------------------------------------------------------------*/

Form.Element.Serializers = {
  input: function(element, value) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element, value);
      default:
        return Form.Element.Serializers.textarea(element, value);
    }
  },

  inputSelector: function(element, value) {
    if (Object.isUndefined(value)) return element.checked ? element.value : null;
    else element.checked = !!value;
  },

  textarea: function(element, value) {
    if (Object.isUndefined(value)) return element.value;
    else element.value = value;
  },

  select: function(element, value) {
    if (Object.isUndefined(value))
      return this[element.type == 'select-one' ?
        'selectOne' : 'selectMany'](element);
    else {
      var opt, currentValue, single = !Object.isArray(value);
      for (var i = 0, length = element.length; i < length; i++) {
        opt = element.options[i];
        currentValue = this.optionValue(opt);
        if (single) {
          if (currentValue == value) {
            opt.selected = true;
            return;
          }
        }
        else opt.selected = value.include(currentValue);
      }
    }
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    // extend element because hasAttribute may not be native
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
};

/*--------------------------------------------------------------------------*/

Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
  initialize: function($super, element, frequency, callback) {
    $super(callback, frequency);
    this.element   = $(element);
    this.lastValue = this.getValue();
  },

  execute: function() {
    var value = this.getValue();
    if (Object.isString(this.lastValue) && Object.isString(value) ?
        this.lastValue != value : String(this.lastValue) != String(value)) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
});

Form.Element.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

/*--------------------------------------------------------------------------*/

Abstract.EventObserver = Class.create({
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
});

Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
if (!window.Event) var Event = { };

Object.extend(Event, {
  KEY_BACKSPACE: 8,
  KEY_TAB:       9,
  KEY_RETURN:   13,
  KEY_ESC:      27,
  KEY_LEFT:     37,
  KEY_UP:       38,
  KEY_RIGHT:    39,
  KEY_DOWN:     40,
  KEY_DELETE:   46,
  KEY_HOME:     36,
  KEY_END:      35,
  KEY_PAGEUP:   33,
  KEY_PAGEDOWN: 34,
  KEY_INSERT:   45,

  cache: { },

  relatedTarget: function(event) {
    var element;
    switch(event.type) {
      case 'mouseover': element = event.fromElement; break;
      case 'mouseout':  element = event.toElement;   break;
      default: return null;
    }
    return Element.extend(element);
  }
});

Event.Methods = (function() {
  var isButton;

  if (Prototype.Browser.IE) {
    var buttonMap = { 0: 1, 1: 4, 2: 2 };
    isButton = function(event, code) {
      return event.button == buttonMap[code];
    };

  } else if (Prototype.Browser.WebKit) {
    isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };

  } else {
    isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }

  return {
    isLeftClick:   function(event) { return isButton(event, 0) },
    isMiddleClick: function(event) { return isButton(event, 1) },
    isRightClick:  function(event) { return isButton(event, 2) },

    element: function(event) {
      event = Event.extend(event);

      var node          = event.target,
          type          = event.type,
          currentTarget = event.currentTarget;

      if (currentTarget && currentTarget.tagName) {
        // Firefox screws up the "click" event when moving between radio buttons
        // via arrow keys. It also screws up the "load" and "error" events on images,
        // reporting the document as the target instead of the original image.
        if (type === 'load' || type === 'error' ||
          (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
            && currentTarget.type === 'radio'))
              node = currentTarget;
      }
      if (node.nodeType == Node.TEXT_NODE) node = node.parentNode;
      return Element.extend(node);
    },

    findElement: function(event, expression) {
      var element = Event.element(event);
      if (!expression) return element;
      var elements = [element].concat(element.ancestors());
      return Selector.findElement(elements, expression, 0);
    },

    pointer: function(event) {
      var docElement = document.documentElement,
      body = document.body || { scrollLeft: 0, scrollTop: 0 };
      return {
        x: event.pageX || (event.clientX +
          (docElement.scrollLeft || body.scrollLeft) -
          (docElement.clientLeft || 0)),
        y: event.pageY || (event.clientY +
          (docElement.scrollTop || body.scrollTop) -
          (docElement.clientTop || 0))
      };
    },

    pointerX: function(event) { return Event.pointer(event).x },
    pointerY: function(event) { return Event.pointer(event).y },

    stop: function(event) {
      Event.extend(event);
      event.preventDefault();
      event.stopPropagation();
      event.stopped = true;
    }
  };
})();

Event.extend = (function() {
  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
    m[name] = Event.Methods[name].methodize();
    return m;
  });

  if (Prototype.Browser.IE) {
    Object.extend(methods, {
      stopPropagation: function() { this.cancelBubble = true },
      preventDefault:  function() { this.returnValue = false },
      inspect: function() { return "[object Event]" }
    });

    return function(event) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);
      Object.extend(event, {
        target: event.srcElement,
        relatedTarget: Event.relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });
      return Object.extend(event, methods);
    };

  } else {
    Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__'];
    Object.extend(Event.prototype, methods);
    return Prototype.K;
  }
})();

Object.extend(Event, (function() {
  var cache = Event.cache;

  function getEventID(element) {
    if (element._prototypeEventID) return element._prototypeEventID[0];
    arguments.callee.id = arguments.callee.id || 1;
    return element._prototypeEventID = [++arguments.callee.id];
  }

  function getDOMEventName(eventName) {
    if (eventName && eventName.include(':')) return "dataavailable";
    return eventName;
  }

  function getCacheForID(id) {
    return cache[id] = cache[id] || { };
  }

  function getWrappersForEventName(id, eventName) {
    var c = getCacheForID(id);
    return c[eventName] = c[eventName] || [];
  }

  function createWrapper(element, eventName, handler) {
    var id = getEventID(element);
    var c = getWrappersForEventName(id, eventName);
    if (c.pluck("handler").include(handler)) return false;

    var wrapper = function(event) {
      if (!Event || !Event.extend ||
        (event.eventName && event.eventName != eventName))
          return false;

      Event.extend(event);
      handler.call(element, event);
    };

    wrapper.handler = handler;
    c.push(wrapper);
    return wrapper;
  }

  function findWrapper(id, eventName, handler) {
    var c = getWrappersForEventName(id, eventName);
    return c.find(function(wrapper) { return wrapper.handler == handler });
  }

  function destroyWrapper(id, eventName, handler) {
    var c = getCacheForID(id);
    if (!c[eventName]) return false;
    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
  }

  function destroyCache() {
    for (var id in cache)
      for (var eventName in cache[id])
        cache[id][eventName] = null;
  }


  // Internet Explorer needs to remove event handlers on page unload
  // in order to avoid memory leaks.
  if (window.attachEvent) {
    window.attachEvent("onunload", destroyCache);
  }

  // Safari has a dummy event handler on page unload so that it won't
  // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
  // object when page is returned to via the back button using its bfcache.
  if (Prototype.Browser.WebKit) {
    window.addEventListener('unload', Prototype.emptyFunction, false);
  }

  return {
    observe: function(element, eventName, handler) {
      element = $(element);
      var name = getDOMEventName(eventName);

      var wrapper = createWrapper(element, eventName, handler);
      if (!wrapper) return element;

      if (element.addEventListener) {
        element.addEventListener(name, wrapper, false);
      } else {
        element.attachEvent("on" + name, wrapper);
      }

      return element;
    },

    stopObserving: function(element, eventName, handler) {
      element = $(element);
      var id = getEventID(element), name = getDOMEventName(eventName);

      if (!handler && eventName) {
        getWrappersForEventName(id, eventName).each(function(wrapper) {
          element.stopObserving(eventName, wrapper.handler);
        });
        return element;

      } else if (!eventName) {
        Object.keys(getCacheForID(id)).each(function(eventName) {
          element.stopObserving(eventName);
        });
        return element;
      }

      var wrapper = findWrapper(id, eventName, handler);
      if (!wrapper) return element;

      if (element.removeEventListener) {
        element.removeEventListener(name, wrapper, false);
      } else {
        element.detachEvent("on" + name, wrapper);
      }

      destroyWrapper(id, eventName, handler);

      return element;
    },

    fire: function(element, eventName, memo) {
      element = $(element);
      if (element == document && document.createEvent && !element.dispatchEvent)
        element = document.documentElement;

      var event;
      if (document.createEvent) {
        event = document.createEvent("HTMLEvents");
        event.initEvent("dataavailable", true, true);
      } else {
        event = document.createEventObject();
        event.eventType = "ondataavailable";
      }

      event.eventName = eventName;
      event.memo = memo || { };

      if (document.createEvent) {
        element.dispatchEvent(event);
      } else {
        element.fireEvent(event.eventType, event);
      }

      return Event.extend(event);
    }
  };
})());

Object.extend(Event, Event.Methods);

Element.addMethods({
  fire:          Event.fire,
  observe:       Event.observe,
  stopObserving: Event.stopObserving
});

Object.extend(document, {
  fire:          Element.Methods.fire.methodize(),
  observe:       Element.Methods.observe.methodize(),
  stopObserving: Element.Methods.stopObserving.methodize(),
  loaded:        false
});

(function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards and John Resig. */

  var timer;

  function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearInterval(timer);
    document.fire("dom:loaded");
    document.loaded = true;
  }

  if (document.addEventListener) {
    if (Prototype.Browser.WebKit) {
      timer = window.setInterval(function() {
        if (/loaded|complete/.test(document.readyState))
          fireContentLoadedEvent();
      }, 0);

      Event.observe(window, "load", fireContentLoadedEvent);

    } else {
      document.addEventListener("DOMContentLoaded",
        fireContentLoadedEvent, false);
    }

  } else {
    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
    $("__onDOMContentLoaded").onreadystatechange = function() {
      if (this.readyState == "complete") {
        this.onreadystatechange = null;
        fireContentLoadedEvent();
      }
    };
  }
})();
/*------------------------------- DEPRECATED -------------------------------*/

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

Element.Methods.childOf = Element.Methods.descendantOf;

var Insertion = {
  Before: function(element, content) {
    return Element.insert(element, {before:content});
  },

  Top: function(element, content) {
    return Element.insert(element, {top:content});
  },

  Bottom: function(element, content) {
    return Element.insert(element, {bottom:content});
  },

  After: function(element, content) {
    return Element.insert(element, {after:content});
  }
};

var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

// This should be moved to script.aculo.us; notice the deprecated methods
// further below, that map to the newer Element methods.
var Position = {
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

  // must be called before calling withinIncludingScrolloffset, every time the
  // page is scrolled
  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  // caches x/y coordinate pair to use with overlap
  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = Element.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = Element.cumulativeScrollOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = Element.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  // within must be called directly before
  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },

  // Deprecation layer -- use newer Element methods now (1.5.2).

  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

  absolutize: function(element) {
    Position.prepare();
    return Element.absolutize(element);
  },

  relativize: function(element) {
    Position.prepare();
    return Element.relativize(element);
  },

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

  clone: function(source, target, options) {
    options = options || { };
    return Element.clonePosition(target, source, options);
  }
};

/*--------------------------------------------------------------------------*/

if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
  function iter(name) {
    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
  }

  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
  function(element, className) {
    className = className.toString().strip();
    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
  } : function(element, className) {
    className = className.toString().strip();
    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
    if (!classNames && !className) return elements;

    var nodes = $(element).getElementsByTagName('*');
    className = ' ' + className + ' ';

    for (var i = 0, child, cn; child = nodes[i]; i++) {
      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
          (classNames && classNames.all(function(name) {
            return !name.toString().blank() && cn.include(' ' + name + ' ');
          }))))
        elements.push(Element.extend(child));
    }
    return elements;
  };

  return function(className, parentElement) {
    return $(parentElement || document.body).getElementsByClassName(className);
  };
}(Element.Methods);

/*--------------------------------------------------------------------------*/

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);

/*--------------------------------------------------------------------------*/

Element.addMethods();

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
//  Justin Palmer (http://encytemedia.com/)
//  Mark Pilgrim (http://diveintomark.org/)
//  Martin Bialasinki
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
  var color = '#';
  if (this.slice(0,4) == 'rgb(') {
    var cols = this.slice(4,this.length-1).split(',');
    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
  } else {
    if (this.slice(0,1) == '#') {
      if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
      if (this.length==7) color = this.toLowerCase();
    }
  }
  return (color.length==7 ? color : (arguments[0] || this));
};

/*--------------------------------------------------------------------------*/

Element.collectTextNodes = function(element) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
  }).flatten().join('');
};

Element.collectTextNodesIgnoreClass = function(element, className) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
        Element.collectTextNodesIgnoreClass(node, className) : ''));
  }).flatten().join('');
};

Element.setContentZoom = function(element, percent) {
  element = $(element);
  element.setStyle({fontSize: (percent/100) + 'em'});
  if (Prototype.Browser.WebKit) window.scrollBy(0,0);
  return element;
};

Element.getInlineOpacity = function(element){
  return $(element).style.opacity || '';
};

Element.forceRerendering = function(element) {
  try {
    element = $(element);
    var n = document.createTextNode(' ');
    element.appendChild(n);
    element.removeChild(n);
  } catch(e) { }
};

/*--------------------------------------------------------------------------*/

var Effect = {
  _elementDoesNotExistError: {
    name: 'ElementDoesNotExistError',
    message: 'The specified DOM element does not exist, but is required for this effect to operate'
  },
  Transitions: {
    linear: Prototype.K,
    sinoidal: function(pos) {
      return (-Math.cos(pos*Math.PI)/2) + .5;
    },
    reverse: function(pos) {
      return 1-pos;
    },
    flicker: function(pos) {
      var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
      return pos > 1 ? 1 : pos;
    },
    wobble: function(pos) {
      return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
    },
    pulse: function(pos, pulses) {
      return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
    },
    spring: function(pos) {
      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
    },
    none: function(pos) {
      return 0;
    },
    full: function(pos) {
      return 1;
    }
  },
  DefaultOptions: {
    duration:   1.0,   // seconds
    fps:        100,   // 100= assume 66fps max.
    sync:       false, // true for combining
    from:       0.0,
    to:         1.0,
    delay:      0.0,
    queue:      'parallel'
  },
  tagifyText: function(element) {
    var tagifyStyle = 'position:relative';
    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';

    element = $(element);
    $A(element.childNodes).each( function(child) {
      if (child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            new Element('span', {style: tagifyStyle}).update(
              character == ' ' ? String.fromCharCode(160) : character),
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if (((typeof element == 'object') ||
        Object.isFunction(element)) &&
       (element.length))
      elements = element;
    else
      elements = $(element).childNodes;

    var options = Object.extend({
      speed: 0.1,
      delay: 0.0
    }, arguments[2] || { });
    var masterDelay = options.delay;

    $A(elements).each( function(element, index) {
      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
    });
  },
  PAIRS: {
    'slide':  ['SlideDown','SlideUp'],
    'blind':  ['BlindDown','BlindUp'],
    'appear': ['Appear','Fade']
  },
  toggle: function(element, effect) {
    element = $(element);
    effect = (effect || 'appear').toLowerCase();
    var options = Object.extend({
      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
    }, arguments[2] || { });
    Effect[element.visible() ?
      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
  }
};

Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;

/* ------------- core effects ------------- */

Effect.ScopedQueue = Class.create(Enumerable, {
  initialize: function() {
    this.effects  = [];
    this.interval = null;
  },
  _each: function(iterator) {
    this.effects._each(iterator);
  },
  add: function(effect) {
    var timestamp = new Date().getTime();

    var position = Object.isString(effect.options.queue) ?
      effect.options.queue : effect.options.queue.position;

    switch(position) {
      case 'front':
        // move unstarted effects after this effect
        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
            e.startOn  += effect.finishOn;
            e.finishOn += effect.finishOn;
          });
        break;
      case 'with-last':
        timestamp = this.effects.pluck('startOn').max() || timestamp;
        break;
      case 'end':
        // start effect after last queued effect has finished
        timestamp = this.effects.pluck('finishOn').max() || timestamp;
        break;
    }

    effect.startOn  += timestamp;
    effect.finishOn += timestamp;

    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
      this.effects.push(effect);

    if (!this.interval)
      this.interval = setInterval(this.loop.bind(this), 15);
  },
  remove: function(effect) {
    this.effects = this.effects.reject(function(e) { return e==effect });
    if (this.effects.length == 0) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },
  loop: function() {
    var timePos = new Date().getTime();
    for(var i=0, len=this.effects.length;i<len;i++)
      this.effects[i] && this.effects[i].loop(timePos);
  }
});

Effect.Queues = {
  instances: $H(),
  get: function(queueName) {
    if (!Object.isString(queueName)) return queueName;

    return this.instances.get(queueName) ||
      this.instances.set(queueName, new Effect.ScopedQueue());
  }
};
Effect.Queue = Effect.Queues.get('global');

Effect.Base = Class.create({
  position: null,
  start: function(options) {
    function codeForEvent(options,eventName){
      return (
        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
      );
    }
    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
    this.currentFrame = 0;
    this.state        = 'idle';
    this.startOn      = this.options.delay*1000;
    this.finishOn     = this.startOn+(this.options.duration*1000);
    this.fromToDelta  = this.options.to-this.options.from;
    this.totalTime    = this.finishOn-this.startOn;
    this.totalFrames  = this.options.fps*this.options.duration;

    this.render = (function() {
      function dispatch(effect, eventName) {
        if (effect.options[eventName + 'Internal'])
          effect.options[eventName + 'Internal'](effect);
        if (effect.options[eventName])
          effect.options[eventName](effect);
      }

      return function(pos) {
        if (this.state === "idle") {
          this.state = "running";
          dispatch(this, 'beforeSetup');
          if (this.setup) this.setup();
          dispatch(this, 'afterSetup');
        }
        if (this.state === "running") {
          pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
          this.position = pos;
          dispatch(this, 'beforeUpdate');
          if (this.update) this.update(pos);
          dispatch(this, 'afterUpdate');
        }
      };
    })();

    this.event('beforeStart');
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).add(this);
  },
  loop: function(timePos) {
    if (timePos >= this.startOn) {
      if (timePos >= this.finishOn) {
        this.render(1.0);
        this.cancel();
        this.event('beforeFinish');
        if (this.finish) this.finish();
        this.event('afterFinish');
        return;
      }
      var pos   = (timePos - this.startOn) / this.totalTime,
          frame = (pos * this.totalFrames).round();
      if (frame > this.currentFrame) {
        this.render(pos);
        this.currentFrame = frame;
      }
    }
  },
  cancel: function() {
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).remove(this);
    this.state = 'finished';
  },
  event: function(eventName) {
    if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
    if (this.options[eventName]) this.options[eventName](this);
  },
  inspect: function() {
    var data = $H();
    for(property in this)
      if (!Object.isFunction(this[property])) data.set(property, this[property]);
    return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
  }
});

Effect.Parallel = Class.create(Effect.Base, {
  initialize: function(effects) {
    this.effects = effects || [];
    this.start(arguments[1]);
  },
  update: function(position) {
    this.effects.invoke('render', position);
  },
  finish: function(position) {
    this.effects.each( function(effect) {
      effect.render(1.0);
      effect.cancel();
      effect.event('beforeFinish');
      if (effect.finish) effect.finish(position);
      effect.event('afterFinish');
    });
  }
});

Effect.Tween = Class.create(Effect.Base, {
  initialize: function(object, from, to) {
    object = Object.isString(object) ? $(object) : object;
    var args = $A(arguments), method = args.last(),
      options = args.length == 5 ? args[3] : null;
    this.method = Object.isFunction(method) ? method.bind(object) :
      Object.isFunction(object[method]) ? object[method].bind(object) :
      function(value) { object[method] = value };
    this.start(Object.extend({ from: from, to: to }, options || { }));
  },
  update: function(position) {
    this.method(position);
  }
});

Effect.Event = Class.create(Effect.Base, {
  initialize: function() {
    this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
  },
  update: Prototype.emptyFunction
});

Effect.Opacity = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    // make this work on IE on elements without 'layout'
    if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
      this.element.setStyle({zoom: 1});
    var options = Object.extend({
      from: this.element.getOpacity() || 0.0,
      to:   1.0
    }, arguments[1] || { });
    this.start(options);
  },
  update: function(position) {
    this.element.setOpacity(position);
  }
});

Effect.Move = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      x:    0,
      y:    0,
      mode: 'relative'
    }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    this.element.makePositioned();
    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
    if (this.options.mode == 'absolute') {
      this.options.x = this.options.x - this.originalLeft;
      this.options.y = this.options.y - this.originalTop;
    }
  },
  update: function(position) {
    this.element.setStyle({
      left: (this.options.x  * position + this.originalLeft).round() + 'px',
      top:  (this.options.y  * position + this.originalTop).round()  + 'px'
    });
  }
});

// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
  return new Effect.Move(element,
    Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
};

Effect.Scale = Class.create(Effect.Base, {
  initialize: function(element, percent) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      scaleX: true,
      scaleY: true,
      scaleContent: true,
      scaleFromCenter: false,
      scaleMode: 'box',        // 'box' or 'contents' or { } with provided values
      scaleFrom: 100.0,
      scaleTo:   percent
    }, arguments[2] || { });
    this.start(options);
  },
  setup: function() {
    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
    this.elementPositioning = this.element.getStyle('position');

    this.originalStyle = { };
    ['top','left','width','height','fontSize'].each( function(k) {
      this.originalStyle[k] = this.element.style[k];
    }.bind(this));

    this.originalTop  = this.element.offsetTop;
    this.originalLeft = this.element.offsetLeft;

    var fontSize = this.element.getStyle('font-size') || '100%';
    ['em','px','%','pt'].each( function(fontSizeType) {
      if (fontSize.indexOf(fontSizeType)>0) {
        this.fontSize     = parseFloat(fontSize);
        this.fontSizeType = fontSizeType;
      }
    }.bind(this));

    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;

    this.dims = null;
    if (this.options.scaleMode=='box')
      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
    if (/^content/.test(this.options.scaleMode))
      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
    if (!this.dims)
      this.dims = [this.options.scaleMode.originalHeight,
                   this.options.scaleMode.originalWidth];
  },
  update: function(position) {
    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
    if (this.options.scaleContent && this.fontSize)
      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
  },
  finish: function(position) {
    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
  },
  setDimensions: function(height, width) {
    var d = { };
    if (this.options.scaleX) d.width = width.round() + 'px';
    if (this.options.scaleY) d.height = height.round() + 'px';
    if (this.options.scaleFromCenter) {
      var topd  = (height - this.dims[0])/2;
      var leftd = (width  - this.dims[1])/2;
      if (this.elementPositioning == 'absolute') {
        if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
        if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
      } else {
        if (this.options.scaleY) d.top = -topd + 'px';
        if (this.options.scaleX) d.left = -leftd + 'px';
      }
    }
    this.element.setStyle(d);
  }
});

Effect.Highlight = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    // Prevent executing on elements not in the layout flow
    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
    // Disable background image during the effect
    this.oldStyle = { };
    if (!this.options.keepBackgroundImage) {
      this.oldStyle.backgroundImage = this.element.getStyle('background-image');
      this.element.setStyle({backgroundImage: 'none'});
    }
    if (!this.options.endcolor)
      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
    if (!this.options.restorecolor)
      this.options.restorecolor = this.element.getStyle('background-color');
    // init color calculations
    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
  },
  update: function(position) {
    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
      return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
  },
  finish: function() {
    this.element.setStyle(Object.extend(this.oldStyle, {
      backgroundColor: this.options.restorecolor
    }));
  }
});

Effect.ScrollTo = function(element) {
  var options = arguments[1] || { },
  scrollOffsets = document.viewport.getScrollOffsets(),
  elementOffsets = $(element).cumulativeOffset();

  if (options.offset) elementOffsets[1] += options.offset;

  return new Effect.Tween(null,
    scrollOffsets.top,
    elementOffsets[1],
    options,
    function(p){ scrollTo(scrollOffsets.left, p.round()); }
  );
};

/* ------------- combination effects ------------- */

Effect.Fade = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  var options = Object.extend({
    from: element.getOpacity() || 1.0,
    to:   0.0,
    afterFinishInternal: function(effect) {
      if (effect.options.to!=0) return;
      effect.element.hide().setStyle({opacity: oldOpacity});
    }
  }, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Appear = function(element) {
  element = $(element);
  var options = Object.extend({
  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
  to:   1.0,
  // force Safari to render floated elements properly
  afterFinishInternal: function(effect) {
    effect.element.forceRerendering();
  },
  beforeSetup: function(effect) {
    effect.element.setOpacity(effect.options.from).show();
  }}, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Puff = function(element) {
  element = $(element);
  var oldStyle = {
    opacity: element.getInlineOpacity(),
    position: element.getStyle('position'),
    top:  element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height
  };
  return new Effect.Parallel(
   [ new Effect.Scale(element, 200,
      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
     Object.extend({ duration: 1.0,
      beforeSetupInternal: function(effect) {
        Position.absolutize(effect.effects[0].element);
      },
      afterFinishInternal: function(effect) {
         effect.effects[0].element.hide().setStyle(oldStyle); }
     }, arguments[1] || { })
   );
};

Effect.BlindUp = function(element) {
  element = $(element);
  element.makeClipping();
  return new Effect.Scale(element, 0,
    Object.extend({ scaleContent: false,
      scaleX: false,
      restoreAfterFinish: true,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping();
      }
    }, arguments[1] || { })
  );
};

Effect.BlindDown = function(element) {
  element = $(element);
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: 0,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping();
    }
  }, arguments[1] || { }));
};

Effect.SwitchOff = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  return new Effect.Appear(element, Object.extend({
    duration: 0.4,
    from: 0,
    transition: Effect.Transitions.flicker,
    afterFinishInternal: function(effect) {
      new Effect.Scale(effect.element, 1, {
        duration: 0.3, scaleFromCenter: true,
        scaleX: false, scaleContent: false, restoreAfterFinish: true,
        beforeSetup: function(effect) {
          effect.element.makePositioned().makeClipping();
        },
        afterFinishInternal: function(effect) {
          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
        }
      });
    }
  }, arguments[1] || { }));
};

Effect.DropOut = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left'),
    opacity: element.getInlineOpacity() };
  return new Effect.Parallel(
    [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
    Object.extend(
      { duration: 0.5,
        beforeSetup: function(effect) {
          effect.effects[0].element.makePositioned();
        },
        afterFinishInternal: function(effect) {
          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
        }
      }, arguments[1] || { }));
};

Effect.Shake = function(element) {
  element = $(element);
  var options = Object.extend({
    distance: 20,
    duration: 0.5
  }, arguments[1] || {});
  var distance = parseFloat(options.distance);
  var split = parseFloat(options.duration) / 10.0;
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left') };
    return new Effect.Move(element,
      { x:  distance, y: 0, duration: split, afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
        effect.element.undoPositioned().setStyle(oldStyle);
  }}); }}); }}); }}); }}); }});
};

Effect.SlideDown = function(element) {
  element = $(element).cleanWhitespace();
  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: window.opera ? 0 : 1,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
    }, arguments[1] || { })
  );
};

Effect.SlideUp = function(element) {
  element = $(element).cleanWhitespace();
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, window.opera ? 0 : 1,
   Object.extend({ scaleContent: false,
    scaleX: false,
    scaleMode: 'box',
    scaleFrom: 100,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
    }
   }, arguments[1] || { })
  );
};

// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
  return new Effect.Scale(element, window.opera ? 1 : 0, {
    restoreAfterFinish: true,
    beforeSetup: function(effect) {
      effect.element.makeClipping();
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping();
    }
  });
};

Effect.Grow = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.full
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var initialMoveX, initialMoveY;
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      initialMoveX = initialMoveY = moveX = moveY = 0;
      break;
    case 'top-right':
      initialMoveX = dims.width;
      initialMoveY = moveY = 0;
      moveX = -dims.width;
      break;
    case 'bottom-left':
      initialMoveX = moveX = 0;
      initialMoveY = dims.height;
      moveY = -dims.height;
      break;
    case 'bottom-right':
      initialMoveX = dims.width;
      initialMoveY = dims.height;
      moveX = -dims.width;
      moveY = -dims.height;
      break;
    case 'center':
      initialMoveX = dims.width / 2;
      initialMoveY = dims.height / 2;
      moveX = -dims.width / 2;
      moveY = -dims.height / 2;
      break;
  }

  return new Effect.Move(element, {
    x: initialMoveX,
    y: initialMoveY,
    duration: 0.01,
    beforeSetup: function(effect) {
      effect.element.hide().makeClipping().makePositioned();
    },
    afterFinishInternal: function(effect) {
      new Effect.Parallel(
        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
          new Effect.Scale(effect.element, 100, {
            scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
        ], Object.extend({
             beforeSetup: function(effect) {
               effect.effects[0].element.setStyle({height: '0px'}).show();
             },
             afterFinishInternal: function(effect) {
               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
             }
           }, options)
      );
    }
  });
};

Effect.Shrink = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.none
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      moveX = moveY = 0;
      break;
    case 'top-right':
      moveX = dims.width;
      moveY = 0;
      break;
    case 'bottom-left':
      moveX = 0;
      moveY = dims.height;
      break;
    case 'bottom-right':
      moveX = dims.width;
      moveY = dims.height;
      break;
    case 'center':
      moveX = dims.width / 2;
      moveY = dims.height / 2;
      break;
  }

  return new Effect.Parallel(
    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
    ], Object.extend({
         beforeStartInternal: function(effect) {
           effect.effects[0].element.makePositioned().makeClipping();
         },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
       }, options)
  );
};

Effect.Pulsate = function(element) {
  element = $(element);
  var options    = arguments[1] || { },
    oldOpacity = element.getInlineOpacity(),
    transition = options.transition || Effect.Transitions.linear,
    reverser   = function(pos){
      return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
    };

  return new Effect.Opacity(element,
    Object.extend(Object.extend({  duration: 2.0, from: 0,
      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
    }, options), {transition: reverser}));
};

Effect.Fold = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height };
  element.makeClipping();
  return new Effect.Scale(element, 5, Object.extend({
    scaleContent: false,
    scaleX: false,
    afterFinishInternal: function(effect) {
    new Effect.Scale(element, 1, {
      scaleContent: false,
      scaleY: false,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping().setStyle(oldStyle);
      } });
  }}, arguments[1] || { }));
};

Effect.Morph = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      style: { }
    }, arguments[1] || { });

    if (!Object.isString(options.style)) this.style = $H(options.style);
    else {
      if (options.style.include(':'))
        this.style = options.style.parseStyle();
      else {
        this.element.addClassName(options.style);
        this.style = $H(this.element.getStyles());
        this.element.removeClassName(options.style);
        var css = this.element.getStyles();
        this.style = this.style.reject(function(style) {
          return style.value == css[style.key];
        });
        options.afterFinishInternal = function(effect) {
          effect.element.addClassName(effect.options.style);
          effect.transforms.each(function(transform) {
            effect.element.style[transform.style] = '';
          });
        };
      }
    }
    this.start(options);
  },

  setup: function(){
    function parseColor(color){
      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
      color = color.parseColor();
      return $R(0,2).map(function(i){
        return parseInt( color.slice(i*2+1,i*2+3), 16 );
      });
    }
    this.transforms = this.style.map(function(pair){
      var property = pair[0], value = pair[1], unit = null;

      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
        value = value.parseColor();
        unit  = 'color';
      } else if (property == 'opacity') {
        value = parseFloat(value);
        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
          this.element.setStyle({zoom: 1});
      } else if (Element.CSS_LENGTH.test(value)) {
          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
          value = parseFloat(components[1]);
          unit = (components.length == 3) ? components[2] : null;
      }

      var originalValue = this.element.getStyle(property);
      return {
        style: property.camelize(),
        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
        targetValue: unit=='color' ? parseColor(value) : value,
        unit: unit
      };
    }.bind(this)).reject(function(transform){
      return (
        (transform.originalValue == transform.targetValue) ||
        (
          transform.unit != 'color' &&
          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
        )
      );
    });
  },
  update: function(position) {
    var style = { }, transform, i = this.transforms.length;
    while(i--)
      style[(transform = this.transforms[i]).style] =
        transform.unit=='color' ? '#'+
          (Math.round(transform.originalValue[0]+
            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
          (Math.round(transform.originalValue[1]+
            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
          (Math.round(transform.originalValue[2]+
            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
        (transform.originalValue +
          (transform.targetValue - transform.originalValue) * position).toFixed(3) +
            (transform.unit === null ? '' : transform.unit);
    this.element.setStyle(style, true);
  }
});

Effect.Transform = Class.create({
  initialize: function(tracks){
    this.tracks  = [];
    this.options = arguments[1] || { };
    this.addTracks(tracks);
  },
  addTracks: function(tracks){
    tracks.each(function(track){
      track = $H(track);
      var data = track.values().first();
      this.tracks.push($H({
        ids:     track.keys().first(),
        effect:  Effect.Morph,
        options: { style: data }
      }));
    }.bind(this));
    return this;
  },
  play: function(){
    return new Effect.Parallel(
      this.tracks.map(function(track){
        var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
        var elements = [$(ids) || $$(ids)].flatten();
        return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
      }).flatten(),
      this.options
    );
  }
});

Element.CSS_PROPERTIES = $w(
  'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
  'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
  'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
  'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
  'fontSize fontWeight height left letterSpacing lineHeight ' +
  'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
  'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
  'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
  'right textIndent top width wordSpacing zIndex');

Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;

String.__parseStyleElement = document.createElement('div');
String.prototype.parseStyle = function(){
  var style, styleRules = $H();
  if (Prototype.Browser.WebKit)
    style = new Element('div',{style:this}).style;
  else {
    String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
    style = String.__parseStyleElement.childNodes[0].style;
  }

  Element.CSS_PROPERTIES.each(function(property){
    if (style[property]) styleRules.set(property, style[property]);
  });

  if (Prototype.Browser.IE && this.include('opacity'))
    styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);

  return styleRules;
};

if (document.defaultView && document.defaultView.getComputedStyle) {
  Element.getStyles = function(element) {
    var css = document.defaultView.getComputedStyle($(element), null);
    return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
      styles[property] = css[property];
      return styles;
    });
  };
} else {
  Element.getStyles = function(element) {
    element = $(element);
    var css = element.currentStyle, styles;
    styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
      results[property] = css[property];
      return results;
    });
    if (!styles.opacity) styles.opacity = element.getOpacity();
    return styles;
  };
}

Effect.Methods = {
  morph: function(element, style) {
    element = $(element);
    new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
    return element;
  },
  visualEffect: function(element, effect, options) {
    element = $(element);
    var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
    new Effect[klass](element, options);
    return element;
  },
  highlight: function(element, options) {
    element = $(element);
    new Effect.Highlight(element, options);
    return element;
  }
};

$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
  'pulsate shake puff squish switchOff dropOut').each(
  function(effect) {
    Effect.Methods[effect] = function(element, options){
      element = $(element);
      Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
      return element;
    };
  }
);

$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
  function(f) { Effect.Methods[f] = Element[f]; }
);

Element.addMethods(Effect.Methods);

// script.aculo.us builder.js v1.8.1, Thu Jan 03 22:07:12 -0500 2008

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Builder = {
  NODEMAP: {
    AREA: 'map',
    CAPTION: 'table',
    COL: 'table',
    COLGROUP: 'table',
    LEGEND: 'fieldset',
    OPTGROUP: 'select',
    OPTION: 'select',
    PARAM: 'object',
    TBODY: 'table',
    TD: 'table',
    TFOOT: 'table',
    TH: 'table',
    THEAD: 'table',
    TR: 'table'
  },
  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
  //       due to a Firefox bug
  node: function(elementName) {
    elementName = elementName.toUpperCase();
    
    // try innerHTML approach
    var parentTag = this.NODEMAP[elementName] || 'div';
    var parentElement = document.createElement(parentTag);
    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
    } catch(e) {}
    var element = parentElement.firstChild || null;
      
    // see if browser added wrapping tags
    if(element && (element.tagName.toUpperCase() != elementName))
      element = element.getElementsByTagName(elementName)[0];
    
    // fallback to createElement approach
    if(!element) element = document.createElement(elementName);
    
    // abort if nothing could be created
    if(!element) return;

    // attributes (or text)
    if(arguments[1])
      if(this._isStringOrNumber(arguments[1]) ||
        (arguments[1] instanceof Array) ||
        arguments[1].tagName) {
          this._children(element, arguments[1]);
        } else {
          var attrs = this._attributes(arguments[1]);
          if(attrs.length) {
            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
              parentElement.innerHTML = "<" +elementName + " " +
                attrs + "></" + elementName + ">";
            } catch(e) {}
            element = parentElement.firstChild || null;
            // workaround firefox 1.0.X bug
            if(!element) {
              element = document.createElement(elementName);
              for(attr in arguments[1]) 
                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
            }
            if(element.tagName.toUpperCase() != elementName)
              element = parentElement.getElementsByTagName(elementName)[0];
          }
        } 

    // text, or array of children
    if(arguments[2])
      this._children(element, arguments[2]);

     return element;
  },
  _text: function(text) {
     return document.createTextNode(text);
  },

  ATTR_MAP: {
    'className': 'class',
    'htmlFor': 'for'
  },

  _attributes: function(attributes) {
    var attrs = [];
    for(attribute in attributes)
      attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) +
          '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'&quot;') + '"');
    return attrs.join(" ");
  },
  _children: function(element, children) {
    if(children.tagName) {
      element.appendChild(children);
      return;
    }
    if(typeof children=='object') { // array can hold nodes and text
      children.flatten().each( function(e) {
        if(typeof e=='object')
          element.appendChild(e)
        else
          if(Builder._isStringOrNumber(e))
            element.appendChild(Builder._text(e));
      });
    } else
      if(Builder._isStringOrNumber(children))
        element.appendChild(Builder._text(children));
  },
  _isStringOrNumber: function(param) {
    return(typeof param=='string' || typeof param=='number');
  },
  build: function(html) {
    var element = this.node('div');
    $(element).update(html.strip());
    return element.down();
  },
  dump: function(scope) { 
    if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope 
  
    var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
      "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
      "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
      "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
      "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
      "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);
  
    tags.each( function(tag){ 
      scope[tag] = function() { 
        return Builder.node.apply(Builder, [tag].concat($A(arguments)));  
      } 
    });
  }
}


// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.

if(typeof Effect == 'undefined')
  throw("controls.js requires including script.aculo.us' effects.js library");

var Autocompleter = { };
Autocompleter.Base = Class.create({
  baseInitialize: function(element, update, options) {
    element          = $(element);
    this.element     = element;
    this.update      = $(update);
    this.hasFocus    = false;
    this.changed     = false;
    this.active      = false;
    this.index       = 0;
    this.entryCount  = 0;
    this.oldElementValue = this.element.value;

    if(this.setOptions)
      this.setOptions(options);
    else
      this.options = options || { };

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 1;
    this.options.onShow       = this.options.onShow ||
      function(element, update){
        if(!update.style.position || update.style.position=='absolute') {
          update.style.position = 'absolute';
          Position.clone(element, update, {
            setHeight: false,
            offsetTop: element.offsetHeight
          });
        }
        Effect.Appear(update,{duration:0.15});
      };
    this.options.onHide = this.options.onHide ||
      function(element, update){ new Effect.Fade(update,{duration:0.15}) };

    if(typeof(this.options.tokens) == 'string')
      this.options.tokens = new Array(this.options.tokens);
    // Force carriage returns as token delimiters anyway
    if (!this.options.tokens.include('\n'))
      this.options.tokens.push('\n');

    this.observer = null;

    this.element.setAttribute('autocomplete','off');

    Element.hide(this.update);

    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
  },

  show: function() {
    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
    if(!this.iefix &&
      (Prototype.Browser.IE) &&
      (Element.getStyle(this.update, 'position')=='absolute')) {
      new Insertion.After(this.update,
       '<iframe id="' + this.update.id + '_iefix" '+
       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
      this.iefix = $(this.update.id+'_iefix');
    }
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
  },

  fixIEOverlapping: function() {
    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
    this.iefix.style.zIndex = 1;
    this.update.style.zIndex = 2;
    Element.show(this.iefix);
  },

  hide: function() {
    this.stopIndicator();
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
    if(this.iefix) Element.hide(this.iefix);
  },

  startIndicator: function() {
    if(this.options.indicator) Element.show(this.options.indicator);
  },

  stopIndicator: function() {
    if(this.options.indicator) Element.hide(this.options.indicator);
  },

  onKeyPress: function(event) {
    if(this.active)
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
         this.selectEntry();
         Event.stop(event);
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
         Event.stop(event);
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
         this.markPrevious();
         this.render();
         Event.stop(event);
         return;
       case Event.KEY_DOWN:
         this.markNext();
         this.render();
         Event.stop(event);
         return;
      }
     else
       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;

    this.changed = true;
    this.hasFocus = true;

    if(this.observer) clearTimeout(this.observer);
      this.observer =
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },

  activate: function() {
    this.changed = false;
    this.hasFocus = true;
    this.getUpdatedChoices();
  },

  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex)
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },

  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },

  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;
  },

  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ?
          Element.addClassName(this.getEntry(i),"selected") :
          Element.removeClassName(this.getEntry(i),"selected");
      if(this.hasFocus) {
        this.show();
        this.active = true;
      }
    } else {
      this.active = false;
      this.hide();
    }
  },

  markPrevious: function() {
    if(this.index > 0) this.index--;
      else this.index = this.entryCount-1;
    this.getEntry(this.index).scrollIntoView(true);
  },

  markNext: function() {
    if(this.index < this.entryCount-1) this.index++;
      else this.index = 0;
    this.getEntry(this.index).scrollIntoView(false);
  },

  getEntry: function(index) {
    return this.update.firstChild.childNodes[index];
  },

  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },

  selectEntry: function() {
    this.active = false;
    this.updateElement(this.getCurrentEntry());
  },

  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
    var value = '';
    if (this.options.select) {
      var nodes = $(selectedElement).select('.' + this.options.select) || [];
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
    } else
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');

    var bounds = this.getTokenBounds();
    if (bounds[0] != -1) {
      var newValue = this.element.value.substr(0, bounds[0]);
      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
      if (whitespace)
        newValue += whitespace[0];
      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
    } else {
      this.element.value = value;
    }
    this.oldElementValue = this.element.value;
    this.element.focus();

    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },

  updateChoices: function(choices) {
    if(!this.changed && this.hasFocus) {
      this.update.innerHTML = choices;
      Element.cleanWhitespace(this.update);
      Element.cleanWhitespace(this.update.down());

      if(this.update.firstChild && this.update.down().childNodes) {
        this.entryCount =
          this.update.down().childNodes.length;
        for (var i = 0; i < this.entryCount; i++) {
          var entry = this.getEntry(i);
          entry.autocompleteIndex = i;
          this.addObservers(entry);
        }
      } else {
        this.entryCount = 0;
      }

      this.stopIndicator();
      this.index = 0;

      if(this.entryCount==1 && this.options.autoSelect) {
        this.selectEntry();
        this.hide();
      } else {
        this.render();
      }
    }
  },

  addObservers: function(element) {
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  },

  onObserverEvent: function() {
    this.changed = false;
    this.tokenBounds = null;
    if(this.getToken().length>=this.options.minChars) {
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
    this.oldElementValue = this.element.value;
  },

  getToken: function() {
    var bounds = this.getTokenBounds();
    return this.element.value.substring(bounds[0], bounds[1]).strip();
  },

  getTokenBounds: function() {
    if (null != this.tokenBounds) return this.tokenBounds;
    var value = this.element.value;
    if (value.strip().empty()) return [-1, 0];
    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
    var offset = (diff == this.oldElementValue.length ? 1 : 0);
    var prevTokenPos = -1, nextTokenPos = value.length;
    var tp;
    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
      if (tp > prevTokenPos) prevTokenPos = tp;
      tp = value.indexOf(this.options.tokens[index], diff + offset);
      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
    }
    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
  }
});

Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
  var boundary = Math.min(newS.length, oldS.length);
  for (var index = 0; index < boundary; ++index)
    if (newS[index] != oldS[index])
      return index;
  return boundary;
};

Ajax.Autocompleter = Class.create(Autocompleter.Base, {
  initialize: function(element, update, url, options) {
    this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.onComplete    = this.onComplete.bind(this);
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

  getUpdatedChoices: function() {
    this.startIndicator();

    var entry = encodeURIComponent(this.options.paramName) + '=' +
      encodeURIComponent(this.getToken());

    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;

    if(this.options.defaultParams)
      this.options.parameters += '&' + this.options.defaultParams;

    new Ajax.Request(this.url, this.options);
  },

  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }
});

// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
//                    text only at the beginning of strings in the
//                    autocomplete array. Defaults to true, which will
//                    match text at the beginning of any *word* in the
//                    strings in the autocomplete array. If you want to
//                    search anywhere in the string, additionally set
//                    the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
//                   a partial match (unlike minChars, which defines
//                   how many characters are required to do any match
//                   at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
//                 Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.

Autocompleter.Local = Class.create(Autocompleter.Base, {
  initialize: function(element, update, array, options) {
    this.baseInitialize(element, update, options);
    this.options.array = array;
  },

  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },

  setOptions: function(options) {
    this.options = Object.extend({
      choices: 10,
      partialSearch: true,
      partialChars: 2,
      ignoreCase: true,
      fullSearch: false,
      selector: function(instance) {
        var ret       = []; // Beginning matches
        var partial   = []; // Inside matches
        var entry     = instance.getToken();
        var count     = 0;

        for (var i = 0; i < instance.options.array.length &&
          ret.length < instance.options.choices ; i++) {

          var elem = instance.options.array[i];
          var foundPos = instance.options.ignoreCase ?
            elem.toLowerCase().indexOf(entry.toLowerCase()) :
            elem.indexOf(entry);

          while (foundPos != -1) {
            if (foundPos == 0 && elem.length != entry.length) {
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
                elem.substr(entry.length) + "</li>");
              break;
            } else if (entry.length >= instance.options.partialChars &&
              instance.options.partialSearch && foundPos != -1) {
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                  foundPos + entry.length) + "</li>");
                break;
              }
            }

            foundPos = instance.options.ignoreCase ?
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
              elem.indexOf(entry, foundPos + 1);

          }
        }
        if (partial.length)
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
        return "<ul>" + ret.join('') + "</ul>";
      }
    }, options || { });
  }
});

// AJAX in-place editor and collection editor
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).

// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
  setTimeout(function() {
    Field.activate(field);
  }, 1);
};

Ajax.InPlaceEditor = Class.create({
  initialize: function(element, url, options) {
    this.url = url;
    this.element = element = $(element);
    this.prepareOptions();
    this._controls = { };
    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
    Object.extend(this.options, options || { });
    if (!this.options.formId && this.element.id) {
      this.options.formId = this.element.id + '-inplaceeditor';
      if ($(this.options.formId))
        this.options.formId = '';
    }
    if (this.options.externalControl)
      this.options.externalControl = $(this.options.externalControl);
    if (!this.options.externalControl)
      this.options.externalControlOnly = false;
    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
    this.element.title = this.options.clickToEditText;
    this._boundCancelHandler = this.handleFormCancellation.bind(this);
    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
    this._boundWrapperHandler = this.wrapUp.bind(this);
    this.registerListeners();
  },
  checkForEscapeOrReturn: function(e) {
    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
    if (Event.KEY_ESC == e.keyCode)
      this.handleFormCancellation(e);
    else if (Event.KEY_RETURN == e.keyCode)
      this.handleFormSubmission(e);
  },
  createControl: function(mode, handler, extraClasses) {
    var control = this.options[mode + 'Control'];
    var text = this.options[mode + 'Text'];
    if ('button' == control) {
      var btn = document.createElement('input');
      btn.type = 'submit';
      btn.value = text;
      btn.className = 'editor_' + mode + '_button';
      if ('cancel' == mode)
        btn.onclick = this._boundCancelHandler;
      this._form.appendChild(btn);
      this._controls[mode] = btn;
    } else if ('link' == control) {
      var link = document.createElement('a');
      link.href = '#';
      link.appendChild(document.createTextNode(text));
      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
      link.className = 'editor_' + mode + '_link';
      if (extraClasses)
        link.className += ' ' + extraClasses;
      this._form.appendChild(link);
      this._controls[mode] = link;
    }
  },
  createEditField: function() {
    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
    var fld;
    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
      fld = document.createElement('input');
      fld.type = 'text';
      var size = this.options.size || this.options.cols || 0;
      if (0 < size) fld.size = size;
    } else {
      fld = document.createElement('textarea');
      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
      fld.cols = this.options.cols || 40;
    }
    fld.name = this.options.paramName;
    fld.value = text; // No HTML breaks conversion anymore
    fld.className = 'editor_field';
    if (this.options.submitOnBlur)
      fld.onblur = this._boundSubmitHandler;
    this._controls.editor = fld;
    if (this.options.loadTextURL)
      this.loadExternalText();
    this._form.appendChild(this._controls.editor);
  },
  createForm: function() {
    var ipe = this;
    function addText(mode, condition) {
      var text = ipe.options['text' + mode + 'Controls'];
      if (!text || condition === false) return;
      ipe._form.appendChild(document.createTextNode(text));
    };
    this._form = $(document.createElement('form'));
    this._form.id = this.options.formId;
    this._form.addClassName(this.options.formClassName);
    this._form.onsubmit = this._boundSubmitHandler;
    this.createEditField();
    if ('textarea' == this._controls.editor.tagName.toLowerCase())
      this._form.appendChild(document.createElement('br'));
    if (this.options.onFormCustomization)
      this.options.onFormCustomization(this, this._form);
    addText('Before', this.options.okControl || this.options.cancelControl);
    this.createControl('ok', this._boundSubmitHandler);
    addText('Between', this.options.okControl && this.options.cancelControl);
    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
    addText('After', this.options.okControl || this.options.cancelControl);
  },
  destroy: function() {
    if (this._oldInnerHTML)
      this.element.innerHTML = this._oldInnerHTML;
    this.leaveEditMode();
    this.unregisterListeners();
  },
  enterEditMode: function(e) {
    if (this._saving || this._editing) return;
    this._editing = true;
    this.triggerCallback('onEnterEditMode');
    if (this.options.externalControl)
      this.options.externalControl.hide();
    this.element.hide();
    this.createForm();
    this.element.parentNode.insertBefore(this._form, this.element);
    if (!this.options.loadTextURL)
      this.postProcessEditField();
    if (e) Event.stop(e);
  },
  enterHover: function(e) {
    if (this.options.hoverClassName)
      this.element.addClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onEnterHover');
  },
  getText: function() {
    return this.element.innerHTML.unescapeHTML();
  },
  handleAJAXFailure: function(transport) {
    this.triggerCallback('onFailure', transport);
    if (this._oldInnerHTML) {
      this.element.innerHTML = this._oldInnerHTML;
      this._oldInnerHTML = null;
    }
  },
  handleFormCancellation: function(e) {
    this.wrapUp();
    if (e) Event.stop(e);
  },
  handleFormSubmission: function(e) {
    var form = this._form;
    var value = $F(this._controls.editor);
    this.prepareSubmission();
    var params = this.options.callback(form, value) || '';
    if (Object.isString(params))
      params = params.toQueryParams();
    params.editorId = this.element.id;
    if (this.options.htmlResponse) {
      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Updater({ success: this.element }, this.url, options);
    } else {
      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Request(this.url, options);
    }
    if (e) Event.stop(e);
  },
  leaveEditMode: function() {
    this.element.removeClassName(this.options.savingClassName);
    this.removeForm();
    this.leaveHover();
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
    if (this.options.externalControl)
      this.options.externalControl.show();
    this._saving = false;
    this._editing = false;
    this._oldInnerHTML = null;
    this.triggerCallback('onLeaveEditMode');
  },
  leaveHover: function(e) {
    if (this.options.hoverClassName)
      this.element.removeClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onLeaveHover');
  },
  loadExternalText: function() {
    this._form.addClassName(this.options.loadingClassName);
    this._controls.editor.disabled = true;
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._form.removeClassName(this.options.loadingClassName);
        var text = transport.responseText;
        if (this.options.stripLoadedTextTags)
          text = text.stripTags();
        this._controls.editor.value = text;
        this._controls.editor.disabled = false;
        this.postProcessEditField();
      }.bind(this),
      onFailure: this._boundFailureHandler
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },
  postProcessEditField: function() {
    var fpc = this.options.fieldPostCreation;
    if (fpc)
      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
  },
  prepareOptions: function() {
    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
      Object.extend(this.options, defs);
    }.bind(this));
  },
  prepareSubmission: function() {
    this._saving = true;
    this.removeForm();
    this.leaveHover();
    this.showSaving();
  },
  registerListeners: function() {
    this._listeners = { };
    var listener;
    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
      listener = this[pair.value].bind(this);
      this._listeners[pair.key] = listener;
      if (!this.options.externalControlOnly)
        this.element.observe(pair.key, listener);
      if (this.options.externalControl)
        this.options.externalControl.observe(pair.key, listener);
    }.bind(this));
  },
  removeForm: function() {
    if (!this._form) return;
    this._form.remove();
    this._form = null;
    this._controls = { };
  },
  showSaving: function() {
    this._oldInnerHTML = this.element.innerHTML;
    this.element.innerHTML = this.options.savingText;
    this.element.addClassName(this.options.savingClassName);
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
  },
  triggerCallback: function(cbName, arg) {
    if ('function' == typeof this.options[cbName]) {
      this.options[cbName](this, arg);
    }
  },
  unregisterListeners: function() {
    $H(this._listeners).each(function(pair) {
      if (!this.options.externalControlOnly)
        this.element.stopObserving(pair.key, pair.value);
      if (this.options.externalControl)
        this.options.externalControl.stopObserving(pair.key, pair.value);
    }.bind(this));
  },
  wrapUp: function(transport) {
    this.leaveEditMode();
    // Can't use triggerCallback due to backward compatibility: requires
    // binding + direct element
    this._boundComplete(transport, this.element);
  }
});

Object.extend(Ajax.InPlaceEditor.prototype, {
  dispose: Ajax.InPlaceEditor.prototype.destroy
});

Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
  initialize: function($super, element, url, options) {
    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
    $super(element, url, options);
  },

  createEditField: function() {
    var list = document.createElement('select');
    list.name = this.options.paramName;
    list.size = 1;
    this._controls.editor = list;
    this._collection = this.options.collection || [];
    if (this.options.loadCollectionURL)
      this.loadCollection();
    else
      this.checkForExternalText();
    this._form.appendChild(this._controls.editor);
  },

  loadCollection: function() {
    this._form.addClassName(this.options.loadingClassName);
    this.showLoadingText(this.options.loadingCollectionText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        var js = transport.responseText.strip();
        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
          throw('Server returned an invalid collection representation.');
        this._collection = eval(js);
        this.checkForExternalText();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadCollectionURL, options);
  },

  showLoadingText: function(text) {
    this._controls.editor.disabled = true;
    var tempOption = this._controls.editor.firstChild;
    if (!tempOption) {
      tempOption = document.createElement('option');
      tempOption.value = '';
      this._controls.editor.appendChild(tempOption);
      tempOption.selected = true;
    }
    tempOption.update((text || '').stripScripts().stripTags());
  },

  checkForExternalText: function() {
    this._text = this.getText();
    if (this.options.loadTextURL)
      this.loadExternalText();
    else
      this.buildOptionList();
  },

  loadExternalText: function() {
    this.showLoadingText(this.options.loadingText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._text = transport.responseText.strip();
        this.buildOptionList();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },

  buildOptionList: function() {
    this._form.removeClassName(this.options.loadingClassName);
    this._collection = this._collection.map(function(entry) {
      return 2 === entry.length ? entry : [entry, entry].flatten();
    });
    var marker = ('value' in this.options) ? this.options.value : this._text;
    var textFound = this._collection.any(function(entry) {
      return entry[0] == marker;
    }.bind(this));
    this._controls.editor.update('');
    var option;
    this._collection.each(function(entry, index) {
      option = document.createElement('option');
      option.value = entry[0];
      option.selected = textFound ? entry[0] == marker : 0 == index;
      option.appendChild(document.createTextNode(entry[1]));
      this._controls.editor.appendChild(option);
    }.bind(this));
    this._controls.editor.disabled = false;
    Field.scrollFreeActivate(this._controls.editor);
  }
});

//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
//**** This only  exists for a while,  in order to  let ****
//**** users adapt to  the new API.  Read up on the new ****
//**** API and convert your code to it ASAP!            ****

Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
  if (!options) return;
  function fallback(name, expr) {
    if (name in options || expr === undefined) return;
    options[name] = expr;
  };
  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
    options.cancelLink == options.cancelButton == false ? false : undefined)));
  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
    options.okLink == options.okButton == false ? false : undefined)));
  fallback('highlightColor', options.highlightcolor);
  fallback('highlightEndColor', options.highlightendcolor);
};

Object.extend(Ajax.InPlaceEditor, {
  DefaultOptions: {
    ajaxOptions: { },
    autoRows: 3,                                // Use when multi-line w/ rows == 1
    cancelControl: 'link',                      // 'link'|'button'|false
    cancelText: 'cancel',
    clickToEditText: 'Click to edit',
    externalControl: null,                      // id|elt
    externalControlOnly: false,
    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
    formClassName: 'inplaceeditor-form',
    formId: null,                               // id|elt
    highlightColor: '#ffff99',
    highlightEndColor: '#ffffff',
    hoverClassName: '',
    htmlResponse: true,
    loadingClassName: 'inplaceeditor-loading',
    loadingText: 'Loading...',
    okControl: 'button',                        // 'link'|'button'|false
    okText: 'ok',
    paramName: 'value',
    rows: 1,                                    // If 1 and multi-line, uses autoRows
    savingClassName: 'inplaceeditor-saving',
    savingText: 'Saving...',
    size: 0,
    stripLoadedTextTags: false,
    submitOnBlur: false,
    textAfterControls: '',
    textBeforeControls: '',
    textBetweenControls: ''
  },
  DefaultCallbacks: {
    callback: function(form) {
      return Form.serialize(form);
    },
    onComplete: function(transport, element) {
      // For backward compatibility, this one is bound to the IPE, and passes
      // the element directly.  It was too often customized, so we don't break it.
      new Effect.Highlight(element, {
        startcolor: this.options.highlightColor, keepBackgroundImage: true });
    },
    onEnterEditMode: null,
    onEnterHover: function(ipe) {
      ipe.element.style.backgroundColor = ipe.options.highlightColor;
      if (ipe._effect)
        ipe._effect.cancel();
    },
    onFailure: function(transport, ipe) {
      alert('Error communication with the server: ' + transport.responseText.stripTags());
    },
    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
    onLeaveEditMode: null,
    onLeaveHover: function(ipe) {
      ipe._effect = new Effect.Highlight(ipe.element, {
        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
        restorecolor: ipe._originalBackground, keepBackgroundImage: true
      });
    }
  },
  Listeners: {
    click: 'enterEditMode',
    keydown: 'checkForEscapeOrReturn',
    mouseover: 'enterHover',
    mouseout: 'leaveHover'
  }
});

Ajax.InPlaceCollectionEditor.DefaultOptions = {
  loadingCollectionText: 'Loading options...'
};

// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields

Form.Element.DelayedObserver = Class.create({
  initialize: function(element, delay, callback) {
    this.delay     = delay || 0.5;
    this.element   = $(element);
    this.callback  = callback;
    this.timer     = null;
    this.lastValue = $F(this.element);
    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
  },
  delayedListener: function(event) {
    if(this.lastValue == $F(this.element)) return;
    if(this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
    this.lastValue = $F(this.element);
  },
  onTimerEvent: function() {
    this.timer = null;
    this.callback(this.element, $F(this.element));
  }
});

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if(Object.isUndefined(Effect))
  throw("dragdrop.js requires including script.aculo.us' effects.js library");

var Droppables = {
  drops: [],

  remove: function(element) {
    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  },

  add: function(element) {
    element = $(element);
    var options = Object.extend({
      greedy:     true,
      hoverclass: null,
      tree:       false
    }, arguments[1] || { });

    // cache containers
    if(options.containment) {
      options._containers = [];
      var containment = options.containment;
      if(Object.isArray(containment)) {
        containment.each( function(c) { options._containers.push($(c)) });
      } else {
        options._containers.push($(containment));
      }
    }

    if(options.accept) options.accept = [options.accept].flatten();

    Element.makePositioned(element); // fix IE
    options.element = element;

    this.drops.push(options);
  },

  findDeepestChild: function(drops) {
    deepest = drops[0];

    for (i = 1; i < drops.length; ++i)
      if (Element.isParent(drops[i].element, deepest.element))
        deepest = drops[i];

    return deepest;
  },

  isContained: function(element, drop) {
    var containmentNode;
    if(drop.tree) {
      containmentNode = element.treeNode;
    } else {
      containmentNode = element.parentNode;
    }
    return drop._containers.detect(function(c) { return containmentNode == c });
  },

  isAffected: function(point, element, drop) {
    return (
      (drop.element!=element) &&
      ((!drop._containers) ||
        this.isContained(element, drop)) &&
      ((!drop.accept) ||
        (Element.classNames(element).detect(
          function(v) { return drop.accept.include(v) } ) )) &&
      Position.within(drop.element, point[0], point[1]) );
  },

  deactivate: function(drop) {
    if(drop.hoverclass)
      Element.removeClassName(drop.element, drop.hoverclass);
    this.last_active = null;
  },

  activate: function(drop) {
    if(drop.hoverclass)
      Element.addClassName(drop.element, drop.hoverclass);
    this.last_active = drop;
  },

  show: function(point, element) {
    if(!this.drops.length) return;
    var drop, affected = [];

    this.drops.each( function(drop) {
      if(Droppables.isAffected(point, element, drop))
        affected.push(drop);
    });

    if(affected.length>0)
      drop = Droppables.findDeepestChild(affected);

    if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
    if (drop) {
      Position.within(drop.element, point[0], point[1]);
      if(drop.onHover)
        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));

      if (drop != this.last_active) Droppables.activate(drop);
    }
  },

  fire: function(event, element) {
    if(!this.last_active) return;
    Position.prepare();

    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
      if (this.last_active.onDrop) {
        this.last_active.onDrop(element, this.last_active.element, event);
        return true;
      }
  },

  reset: function() {
    if(this.last_active)
      this.deactivate(this.last_active);
  }
};

var Draggables = {
  drags: [],
  observers: [],

  register: function(draggable) {
    if(this.drags.length == 0) {
      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
      this.eventKeypress  = this.keyPress.bindAsEventListener(this);

      Event.observe(document, "mouseup", this.eventMouseUp);
      Event.observe(document, "mousemove", this.eventMouseMove);
      Event.observe(document, "keypress", this.eventKeypress);
    }
    this.drags.push(draggable);
  },

  unregister: function(draggable) {
    this.drags = this.drags.reject(function(d) { return d==draggable });
    if(this.drags.length == 0) {
      Event.stopObserving(document, "mouseup", this.eventMouseUp);
      Event.stopObserving(document, "mousemove", this.eventMouseMove);
      Event.stopObserving(document, "keypress", this.eventKeypress);
    }
  },

  activate: function(draggable) {
    if(draggable.options.delay) {
      this._timeout = setTimeout(function() {
        Draggables._timeout = null;
        window.focus();
        Draggables.activeDraggable = draggable;
      }.bind(this), draggable.options.delay);
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeDraggable = draggable;
    }
  },

  deactivate: function() {
    this.activeDraggable = null;
  },

  updateDrag: function(event) {
    if(!this.activeDraggable) return;
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    // Mozilla-based browsers fire successive mousemove events with
    // the same coordinates, prevent needless redrawing (moz bug?)
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;

    this.activeDraggable.updateDrag(event, pointer);
  },

  endDrag: function(event) {
    if(this._timeout) {
      clearTimeout(this._timeout);
      this._timeout = null;
    }
    if(!this.activeDraggable) return;
    this._lastPointer = null;
    this.activeDraggable.endDrag(event);
    this.activeDraggable = null;
  },

  keyPress: function(event) {
    if(this.activeDraggable)
      this.activeDraggable.keyPress(event);
  },

  addObserver: function(observer) {
    this.observers.push(observer);
    this._cacheObserverCallbacks();
  },

  removeObserver: function(element) {  // element instead of observer fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
    this._cacheObserverCallbacks();
  },

  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, draggable, event);
      });
    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  },

  _cacheObserverCallbacks: function() {
    ['onStart','onEnd','onDrag'].each( function(eventName) {
      Draggables[eventName+'Count'] = Draggables.observers.select(
        function(o) { return o[eventName]; }
      ).length;
    });
  }
};

/*--------------------------------------------------------------------------*/

var Draggable = Class.create({
  initialize: function(element) {
    var defaults = {
      handle: false,
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
          queue: {scope:'_draggable', position:'end'}
        });
      },
      endeffect: function(element) {
        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
          queue: {scope:'_draggable', position:'end'},
          afterFinish: function(){
            Draggable._dragging[element] = false
          }
        });
      },
      zindex: 1000,
      revert: false,
      quiet: false,
      scroll: false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };

    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Draggable._dragging[element] = true;
          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
        }
      });

    var options = Object.extend(defaults, arguments[1] || { });

    this.element = $(element);

    if(options.handle && Object.isString(options.handle))
      this.handle = this.element.down('.'+options.handle, 0);

    if(!this.handle) this.handle = $(options.handle);
    if(!this.handle) this.handle = this.element;

    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
      options.scroll = $(options.scroll);
      this._isScrollChild = Element.childOf(this.element, options.scroll);
    }

    Element.makePositioned(this.element); // fix IE

    this.options  = options;
    this.dragging = false;

    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);

    Draggables.register(this);
  },

  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    Draggables.unregister(this);
  },

  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },

  initDrag: function(event) {
    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
      Draggable._dragging[this.element]) return;
    if(Event.isLeftClick(event)) {
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if((tag_name = src.tagName.toUpperCase()) && (
        tag_name=='INPUT' ||
        tag_name=='SELECT' ||
        tag_name=='OPTION' ||
        tag_name=='BUTTON' ||
        tag_name=='TEXTAREA')) return;

      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      var pos     = Position.cumulativeOffset(this.element);
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });

      Draggables.activate(this);
      Event.stop(event);
    }
  },

  startDrag: function(event) {
    this.dragging = true;
    if(!this.delta)
      this.delta = this.currentDelta();

    if(this.options.zindex) {
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
      this.element.style.zIndex = this.options.zindex;
    }

    if(this.options.ghosting) {
      this._clone = this.element.cloneNode(true);
      this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
      if (!this._originallyAbsolute)
        Position.absolutize(this.element);
      this.element.parentNode.insertBefore(this._clone, this.element);
    }

    if(this.options.scroll) {
      if (this.options.scroll == window) {
        var where = this._getWindowScroll(this.options.scroll);
        this.originalScrollLeft = where.left;
        this.originalScrollTop = where.top;
      } else {
        this.originalScrollLeft = this.options.scroll.scrollLeft;
        this.originalScrollTop = this.options.scroll.scrollTop;
      }
    }

    Draggables.notify('onStart', this, event);

    if(this.options.starteffect) this.options.starteffect(this.element);
  },

  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);

    if(!this.options.quiet){
      Position.prepare();
      Droppables.show(pointer, this.element);
    }

    Draggables.notify('onDrag', this, event);

    this.draw(pointer);
    if(this.options.change) this.options.change(this);

    if(this.options.scroll) {
      this.stopScrolling();

      var p;
      if (this.options.scroll == window) {
        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
      } else {
        p = Position.page(this.options.scroll);
        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
        p[1] += this.options.scroll.scrollTop + Position.deltaY;
        p.push(p[0]+this.options.scroll.offsetWidth);
        p.push(p[1]+this.options.scroll.offsetHeight);
      }
      var speed = [0,0];
      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
      this.startScrolling(speed);
    }

    // fix AppleWebKit rendering
    if(Prototype.Browser.WebKit) window.scrollBy(0,0);

    Event.stop(event);
  },

  finishDrag: function(event, success) {
    this.dragging = false;

    if(this.options.quiet){
      Position.prepare();
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      Droppables.show(pointer, this.element);
    }

    if(this.options.ghosting) {
      if (!this._originallyAbsolute)
        Position.relativize(this.element);
      delete this._originallyAbsolute;
      Element.remove(this._clone);
      this._clone = null;
    }

    var dropped = false;
    if(success) {
      dropped = Droppables.fire(event, this.element);
      if (!dropped) dropped = false;
    }
    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
    Draggables.notify('onEnd', this, event);

    var revert = this.options.revert;
    if(revert && Object.isFunction(revert)) revert = revert(this.element);

    var d = this.currentDelta();
    if(revert && this.options.reverteffect) {
      if (dropped == 0 || revert != 'failure')
        this.options.reverteffect(this.element,
          d[1]-this.delta[1], d[0]-this.delta[0]);
    } else {
      this.delta = d;
    }

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect)
      this.options.endeffect(this.element);

    Draggables.deactivate(this);
    Droppables.reset();
  },

  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishDrag(event, false);
    Event.stop(event);
  },

  endDrag: function(event) {
    if(!this.dragging) return;
    this.stopScrolling();
    this.finishDrag(event, true);
    Event.stop(event);
  },

  draw: function(point) {
    var pos = Position.cumulativeOffset(this.element);
    if(this.options.ghosting) {
      var r   = Position.realOffset(this.element);
      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
    }

    var d = this.currentDelta();
    pos[0] -= d[0]; pos[1] -= d[1];

    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
    }

    var p = [0,1].map(function(i){
      return (point[i]-pos[i]-this.offset[i])
    }.bind(this));

    if(this.options.snap) {
      if(Object.isFunction(this.options.snap)) {
        p = this.options.snap(p[0],p[1],this);
      } else {
      if(Object.isArray(this.options.snap)) {
        p = p.map( function(v, i) {
          return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
      } else {
        p = p.map( function(v) {
          return (v/this.options.snap).round()*this.options.snap }.bind(this));
      }
    }}

    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = p[0] + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = p[1] + "px";

    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },

  stopScrolling: function() {
    if(this.scrollInterval) {
      clearInterval(this.scrollInterval);
      this.scrollInterval = null;
      Draggables._lastScrollPointer = null;
    }
  },

  startScrolling: function(speed) {
    if(!(speed[0] || speed[1])) return;
    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
    this.lastScrolled = new Date();
    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  },

  scroll: function() {
    var current = new Date();
    var delta = current - this.lastScrolled;
    this.lastScrolled = current;
    if(this.options.scroll == window) {
      with (this._getWindowScroll(this.options.scroll)) {
        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
          var d = delta / 1000;
          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
        }
      }
    } else {
      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
    }

    Position.prepare();
    Droppables.show(Draggables._lastPointer, this.element);
    Draggables.notify('onDrag', this);
    if (this._isScrollChild) {
      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
      if (Draggables._lastScrollPointer[0] < 0)
        Draggables._lastScrollPointer[0] = 0;
      if (Draggables._lastScrollPointer[1] < 0)
        Draggables._lastScrollPointer[1] = 0;
      this.draw(Draggables._lastScrollPointer);
    }

    if(this.options.change) this.options.change(this);
  },

  _getWindowScroll: function(w) {
    var T, L, W, H;
    with (w.document) {
      if (w.document.documentElement && documentElement.scrollTop) {
        T = documentElement.scrollTop;
        L = documentElement.scrollLeft;
      } else if (w.document.body) {
        T = body.scrollTop;
        L = body.scrollLeft;
      }
      if (w.innerWidth) {
        W = w.innerWidth;
        H = w.innerHeight;
      } else if (w.document.documentElement && documentElement.clientWidth) {
        W = documentElement.clientWidth;
        H = documentElement.clientHeight;
      } else {
        W = body.offsetWidth;
        H = body.offsetHeight;
      }
    }
    return { top: T, left: L, width: W, height: H };
  }
});

Draggable._dragging = { };

/*--------------------------------------------------------------------------*/

var SortableObserver = Class.create({
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },

  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },

  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
});

var Sortable = {
  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,

  sortables: { },

  _findRootElement: function(element) {
    while (element.tagName.toUpperCase() != "BODY") {
      if(element.id && Sortable.sortables[element.id]) return element;
      element = element.parentNode;
    }
  },

  options: function(element) {
    element = Sortable._findRootElement($(element));
    if(!element) return;
    return Sortable.sortables[element.id];
  },

  destroy: function(element){
    element = $(element);
    var s = Sortable.sortables[element.id];

    if(s) {
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');

      delete Sortable.sortables[s.element.id];
    }
  },

  create: function(element) {
    element = $(element);
    var options = Object.extend({
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,
      treeTag:     'ul',
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      delay:       0,
      hoverclass:  null,
      ghosting:    false,
      quiet:       false,
      scroll:      false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      format:      this.SERIALIZE_RULE,

      // these take arrays of elements or ids and can be
      // used for better initialization performance
      elements:    false,
      handles:     false,

      onChange:    Prototype.emptyFunction,
      onUpdate:    Prototype.emptyFunction
    }, arguments[1] || { });

    // clear any old sortable with same element
    this.destroy(element);

    // build options for the draggables
    var options_for_draggable = {
      revert:      true,
      quiet:       options.quiet,
      scroll:      options.scroll,
      scrollSpeed: options.scrollSpeed,
      scrollSensitivity: options.scrollSensitivity,
      delay:       options.delay,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

    if(options.starteffect)
      options_for_draggable.starteffect = options.starteffect;

    if(options.reverteffect)
      options_for_draggable.reverteffect = options.reverteffect;
    else
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
        element.style.top  = 0;
        element.style.left = 0;
      };

    if(options.endeffect)
      options_for_draggable.endeffect = options.endeffect;

    if(options.zindex)
      options_for_draggable.zindex = options.zindex;

    // build options for the droppables
    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      tree:        options.tree,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover
    };

    var options_for_tree = {
      onHover:      Sortable.onEmptyHover,
      overlap:      options.overlap,
      containment:  options.containment,
      hoverclass:   options.hoverclass
    };

    // fix for gecko engine
    Element.cleanWhitespace(element);

    options.draggables = [];
    options.droppables = [];

    // drop on empty handling
    if(options.dropOnEmpty || options.tree) {
      Droppables.add(element, options_for_tree);
      options.droppables.push(element);
    }

    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
      var handle = options.handles ? $(options.handles[i]) :
        (options.handle ? $(e).select('.' + options.handle)[0] : e);
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      if(options.tree) e.treeNode = element;
      options.droppables.push(e);
    });

    if(options.tree) {
      (Sortable.findTreeElements(element, options) || []).each( function(e) {
        Droppables.add(e, options_for_tree);
        e.treeNode = element;
        options.droppables.push(e);
      });
    }

    // keep reference
    this.sortables[element.id] = options;

    // for onupdate
    Draggables.addObserver(new SortableObserver(element, options.onUpdate));

  },

  // return all suitable-for-sortable elements in a guaranteed order
  findElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.tag);
  },

  findTreeElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.treeTag);
  },

  onHover: function(element, dropon, overlap) {
    if(Element.isParent(dropon, element)) return;

    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
      return;
    } else if(overlap>0.5) {
      Sortable.mark(dropon, 'before');
      if(dropon.previousSibling != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, dropon);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    } else {
      Sortable.mark(dropon, 'after');
      var nextElement = dropon.nextSibling || null;
      if(nextElement != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, nextElement);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    }
  },

  onEmptyHover: function(element, dropon, overlap) {
    var oldParentNode = element.parentNode;
    var droponOptions = Sortable.options(dropon);

    if(!Element.isParent(dropon, element)) {
      var index;

      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
      var child = null;

      if(children) {
        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);

        for (index = 0; index < children.length; index += 1) {
          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
            offset -= Element.offsetSize (children[index], droponOptions.overlap);
          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
            child = index + 1 < children.length ? children[index + 1] : null;
            break;
          } else {
            child = children[index];
            break;
          }
        }
      }

      dropon.insertBefore(element, child);

      Sortable.options(oldParentNode).onChange(element);
      droponOptions.onChange(element);
    }
  },

  unmark: function() {
    if(Sortable._marker) Sortable._marker.hide();
  },

  mark: function(dropon, position) {
    // mark on ghosting only
    var sortable = Sortable.options(dropon.parentNode);
    if(sortable && !sortable.ghosting) return;

    if(!Sortable._marker) {
      Sortable._marker =
        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
          hide().addClassName('dropmarker').setStyle({position:'absolute'});
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
    }
    var offsets = Position.cumulativeOffset(dropon);
    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});

    if(position=='after')
      if(sortable.overlap == 'horizontal')
        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
      else
        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});

    Sortable._marker.show();
  },

  _tree: function(element, options, parent) {
    var children = Sortable.findElements(element, options) || [];

    for (var i = 0; i < children.length; ++i) {
      var match = children[i].id.match(options.format);

      if (!match) continue;

      var child = {
        id: encodeURIComponent(match ? match[1] : null),
        element: element,
        parent: parent,
        children: [],
        position: parent.children.length,
        container: $(children[i]).down(options.treeTag)
      };

      /* Get the element containing the children and recurse over it */
      if (child.container)
        this._tree(child.container, options, child);

      parent.children.push (child);
    }

    return parent;
  },

  tree: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag: sortableOptions.tag,
      treeTag: sortableOptions.treeTag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format
    }, arguments[1] || { });

    var root = {
      id: null,
      parent: null,
      children: [],
      container: element,
      position: 0
    };

    return Sortable._tree(element, options, root);
  },

  /* Construct a [i] index for a particular node */
  _constructIndex: function(node) {
    var index = '';
    do {
      if (node.id) index = '[' + node.position + ']' + index;
    } while ((node = node.parent) != null);
    return index;
  },

  sequence: function(element) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[1] || { });

    return $(this.findElements(element, options) || []).map( function(item) {
      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
    });
  },

  setSequence: function(element, new_sequence) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[2] || { });

    var nodeMap = { };
    this.findElements(element, options).each( function(n) {
        if (n.id.match(options.format))
            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
        n.parentNode.removeChild(n);
    });

    new_sequence.each(function(ident) {
      var n = nodeMap[ident];
      if (n) {
        n[1].appendChild(n[0]);
        delete nodeMap[ident];
      }
    });
  },

  serialize: function(element) {
    element = $(element);
    var options = Object.extend(Sortable.options(element), arguments[1] || { });
    var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);

    if (options.tree) {
      return Sortable.tree(element, arguments[1]).children.map( function (item) {
        return [name + Sortable._constructIndex(item) + "[id]=" +
                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
      }).flatten().join('&');
    } else {
      return Sortable.sequence(element, arguments[1]).map( function(item) {
        return name + "[]=" + encodeURIComponent(item);
      }).join('&');
    }
  }
};

// Returns true if child is contained within element
Element.isParent = function(child, element) {
  if (!child.parentNode || child == element) return false;
  if (child.parentNode == element) return true;
  return Element.isParent(child.parentNode, element);
};

Element.findChildren = function(element, only, recursive, tagName) {
  if(!element.hasChildNodes()) return null;
  tagName = tagName.toUpperCase();
  if(only) only = [only].flatten();
  var elements = [];
  $A(element.childNodes).each( function(e) {
    if(e.tagName && e.tagName.toUpperCase()==tagName &&
      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
        elements.push(e);
    if(recursive) {
      var grandchildren = Element.findChildren(e, only, recursive, tagName);
      if(grandchildren) elements.push(grandchildren);
    }
  });

  return (elements.length>0 ? elements.flatten() : []);
};

Element.offsetSize = function (element, type) {
  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
};

// script.aculo.us slider.js v1.8.1, Thu Jan 03 22:07:12 -0500 2008

// Copyright (c) 2005-2007 Marty Haught, Thomas Fuchs 
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if (!Control) var Control = { };

// options:
//  axis: 'vertical', or 'horizontal' (default)
//
// callbacks:
//  onChange(value)
//  onSlide(value)
Control.Slider = Class.create({
  initialize: function(handle, track, options) {
    var slider = this;
    
    if (Object.isArray(handle)) {
      this.handles = handle.collect( function(e) { return $(e) });
    } else {
      this.handles = [$(handle)];
    }
    
    this.track   = $(track);
    this.options = options || { };

    this.axis      = this.options.axis || 'horizontal';
    this.increment = this.options.increment || 1;
    this.step      = parseInt(this.options.step || '1');
    this.range     = this.options.range || $R(0,1);
    
    this.value     = 0; // assure backwards compat
    this.values    = this.handles.map( function() { return 0 });
    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
    this.options.startSpan = $(this.options.startSpan || null);
    this.options.endSpan   = $(this.options.endSpan || null);

    this.restricted = this.options.restricted || false;

    this.maximum   = this.options.maximum || this.range.end;
    this.minimum   = this.options.minimum || this.range.start;

    // Will be used to align the handle onto the track, if necessary
    this.alignX = parseInt(this.options.alignX || '0');
    this.alignY = parseInt(this.options.alignY || '0');
    
    this.trackLength = this.maximumOffset() - this.minimumOffset();

    this.handleLength = this.isVertical() ? 
      (this.handles[0].offsetHeight != 0 ? 
        this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) : 
      (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth : 
        this.handles[0].style.width.replace(/px$/,""));

    this.active   = false;
    this.dragging = false;
    this.disabled = false;

    if (this.options.disabled) this.setDisabled();

    // Allowed values array
    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
    if (this.allowedValues) {
      this.minimum = this.allowedValues.min();
      this.maximum = this.allowedValues.max();
    }

    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
    this.eventMouseMove = this.update.bindAsEventListener(this);

    // Initialize handles in reverse (make sure first handle is active)
    this.handles.each( function(h,i) {
      i = slider.handles.length-1-i;
      slider.setValue(parseFloat(
        (Object.isArray(slider.options.sliderValue) ? 
          slider.options.sliderValue[i] : slider.options.sliderValue) || 
         slider.range.start), i);
      h.makePositioned().observe("mousedown", slider.eventMouseDown);
    });
    
    this.track.observe("mousedown", this.eventMouseDown);
    document.observe("mouseup", this.eventMouseUp);
    document.observe("mousemove", this.eventMouseMove);
    
    this.initialized = true;
  },
  dispose: function() {
    var slider = this;    
    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
    Event.stopObserving(document, "mouseup", this.eventMouseUp);
    Event.stopObserving(document, "mousemove", this.eventMouseMove);
    this.handles.each( function(h) {
      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
    });
  },
  setDisabled: function(){
    this.disabled = true;
  },
  setEnabled: function(){
    this.disabled = false;
  },  
  getNearestValue: function(value){
    if (this.allowedValues){
      if (value >= this.allowedValues.max()) return(this.allowedValues.max());
      if (value <= this.allowedValues.min()) return(this.allowedValues.min());
      
      var offset = Math.abs(this.allowedValues[0] - value);
      var newValue = this.allowedValues[0];
      this.allowedValues.each( function(v) {
        var currentOffset = Math.abs(v - value);
        if (currentOffset <= offset){
          newValue = v;
          offset = currentOffset;
        } 
      });
      return newValue;
    }
    if (value > this.range.end) return this.range.end;
    if (value < this.range.start) return this.range.start;
    return value;
  },
  setValue: function(sliderValue, handleIdx){
    if (!this.active) {
      this.activeHandleIdx = handleIdx || 0;
      this.activeHandle    = this.handles[this.activeHandleIdx];
      this.updateStyles();
    }
    handleIdx = handleIdx || this.activeHandleIdx || 0;
    if (this.initialized && this.restricted) {
      if ((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
        sliderValue = this.values[handleIdx-1];
      if ((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
        sliderValue = this.values[handleIdx+1];
    }
    sliderValue = this.getNearestValue(sliderValue);
    this.values[handleIdx] = sliderValue;
    this.value = this.values[0]; // assure backwards compat
    
    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = 
      this.translateToPx(sliderValue);
    
    this.drawSpans();
    if (!this.dragging || !this.event) this.updateFinished();
  },
  setValueBy: function(delta, handleIdx) {
    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 
      handleIdx || this.activeHandleIdx || 0);
  },
  translateToPx: function(value) {
    return Math.round(
      ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * 
      (value - this.range.start)) + "px";
  },
  translateToValue: function(offset) {
    return ((offset/(this.trackLength-this.handleLength) * 
      (this.range.end-this.range.start)) + this.range.start);
  },
  getRange: function(range) {
    var v = this.values.sortBy(Prototype.K); 
    range = range || 0;
    return $R(v[range],v[range+1]);
  },
  minimumOffset: function(){
    return(this.isVertical() ? this.alignY : this.alignX);
  },
  maximumOffset: function(){
    return(this.isVertical() ? 
      (this.track.offsetHeight != 0 ? this.track.offsetHeight :
        this.track.style.height.replace(/px$/,"")) - this.alignY : 
      (this.track.offsetWidth != 0 ? this.track.offsetWidth : 
        this.track.style.width.replace(/px$/,"")) - this.alignX);
  },  
  isVertical:  function(){
    return (this.axis == 'vertical');
  },
  drawSpans: function() {
    var slider = this;
    if (this.spans)
      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
    if (this.options.startSpan)
      this.setSpan(this.options.startSpan,
        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
    if (this.options.endSpan)
      this.setSpan(this.options.endSpan, 
        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
  },
  setSpan: function(span, range) {
    if (this.isVertical()) {
      span.style.top = this.translateToPx(range.start);
      span.style.height = this.translateToPx(range.end - range.start + this.range.start);
    } else {
      span.style.left = this.translateToPx(range.start);
      span.style.width = this.translateToPx(range.end - range.start + this.range.start);
    }
  },
  updateStyles: function() {
    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
    Element.addClassName(this.activeHandle, 'selected');
  },
  startDrag: function(event) {
    if (Event.isLeftClick(event)) {
      if (!this.disabled){
        this.active = true;
        
        var handle = Event.element(event);
        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
        var track = handle;
        if (track==this.track) {
          var offsets  = Position.cumulativeOffset(this.track); 
          this.event = event;
          this.setValue(this.translateToValue( 
           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
          ));
          var offsets  = Position.cumulativeOffset(this.activeHandle);
          this.offsetX = (pointer[0] - offsets[0]);
          this.offsetY = (pointer[1] - offsets[1]);
        } else {
          // find the handle (prevents issues with Safari)
          while((this.handles.indexOf(handle) == -1) && handle.parentNode) 
            handle = handle.parentNode;
            
          if (this.handles.indexOf(handle)!=-1) {
            this.activeHandle    = handle;
            this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
            this.updateStyles();
            
            var offsets  = Position.cumulativeOffset(this.activeHandle);
            this.offsetX = (pointer[0] - offsets[0]);
            this.offsetY = (pointer[1] - offsets[1]);
          }
        }
      }
      Event.stop(event);
    }
  },
  update: function(event) {
   if (this.active) {
      if (!this.dragging) this.dragging = true;
      this.draw(event);
      if (Prototype.Browser.WebKit) window.scrollBy(0,0);
      Event.stop(event);
   }
  },
  draw: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var offsets = Position.cumulativeOffset(this.track);
    pointer[0] -= this.offsetX + offsets[0];
    pointer[1] -= this.offsetY + offsets[1];
    this.event = event;
    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
    if (this.initialized && this.options.onSlide)
      this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
  },
  endDrag: function(event) {
    if (this.active && this.dragging) {
      this.finishDrag(event, true);
      Event.stop(event);
    }
    this.active = false;
    this.dragging = false;
  },  
  finishDrag: function(event, success) {
    this.active = false;
    this.dragging = false;
    this.updateFinished();
  },
  updateFinished: function() {
    if (this.initialized && this.options.onChange) 
      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
    this.event = null;
  }
});


// script.aculo.us sound.js v1.8.1, Thu Jan 03 22:07:12 -0500 2008

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Based on code created by Jules Gravinese (http://www.webveteran.com/)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

Sound = {
  tracks: {},
  _enabled: true,
  template:
    new Template('<embed style="height:0" id="sound_#{track}_#{id}" src="#{url}" loop="false" autostart="true" hidden="true"/>'),
  enable: function(){
    Sound._enabled = true;
  },
  disable: function(){
    Sound._enabled = false;
  },
  play: function(url){
    if(!Sound._enabled) return;
    var options = Object.extend({
      track: 'global', url: url, replace: false
    }, arguments[1] || {});
    
    if(options.replace && this.tracks[options.track]) {
      $R(0, this.tracks[options.track].id).each(function(id){
        var sound = $('sound_'+options.track+'_'+id);
        sound.Stop && sound.Stop();
        sound.remove();
      })
      this.tracks[options.track] = null;
    }
      
    if(!this.tracks[options.track])
      this.tracks[options.track] = { id: 0 }
    else
      this.tracks[options.track].id++;
      
    options.id = this.tracks[options.track].id;
    $$('body')[0].insert( 
      Prototype.Browser.IE ? new Element('bgsound',{
        id: 'sound_'+options.track+'_'+options.id,
        src: options.url, loop: 1, autostart: true
      }) : Sound.template.evaluate(options));
  }
};

if(Prototype.Browser.Gecko && navigator.userAgent.indexOf("Win") > 0){
  if(navigator.plugins && $A(navigator.plugins).detect(function(p){ return p.name.indexOf('QuickTime') != -1 }))
    Sound.template = new Template('<object id="sound_#{track}_#{id}" width="0" height="0" type="audio/mpeg" data="#{url}"/>')
  else
    Sound.play = function(){}
}


// script.aculo.us scriptaculous.js v1.8.1, Thu Jan 03 22:07:12 -0500 2008

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// 
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Scriptaculous = {
  Version: '1.8.1',
  require: function(libraryName) {
    // inserting via DOM fails in Safari 2.0, so brute force approach
    document.write('<script type="text/javascript" src="'+libraryName+'"><\/script>');
  },
  REQUIRED_PROTOTYPE: '1.6.0',
  load: function() {
    function convertVersionString(versionString){
      var r = versionString.split('.');
      return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
    }
 
    if((typeof Prototype=='undefined') || 
       (typeof Element == 'undefined') || 
       (typeof Element.Methods=='undefined') ||
       (convertVersionString(Prototype.Version) < 
        convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE)))
       throw("script.aculo.us requires the Prototype JavaScript framework >= " +
        Scriptaculous.REQUIRED_PROTOTYPE);
    
    $A(document.getElementsByTagName("script")).findAll( function(s) {
      return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/))
    }).each( function(s) {
      var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,'');
      var includes = s.src.match(/\?.*load=([a-z,]*)/);
      (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each(
       function(include) { Scriptaculous.require(path+include+'.js') });
    });
  }
}

Scriptaculous.load();

/*
	Modified by Tom Donaldson for ContinuousTraveler.com to work-around changes to Safari.
	New WebKit (June 2009) includes a JSON symbol that keeps this file from defining
	its functions. Changed top level JSON function to JSONorg in this file. Changed error
	messages to match.
	
    http://www.JSON.org/json2.js
    2008-07-15

    Public Domain.

    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods: stringify
    and parse.

        JSON.stringify(value, replacer, space)
            value       any JavaScript value, usually an object or array.

            replacer    an optional parameter that determines how object
                        values are stringified for objects. It can be a
                        function or an array.

            space       an optional parameter that specifies the indentation
                        of nested structures. If it is omitted, the text will
                        be packed without extra whitespace. If it is a number,
                        it will specify the number of spaces to indent at each
                        level. If it is a string (such as '\t' or '&nbsp;'),
                        it contains the characters used to indent at each level.

            This method produces a JSON text from a JavaScript value.

            When an object value is found, if the object contains a toJSON
            method, its toJSON method will be called and the result will be
            stringified. A toJSON method does not serialize: it returns the
            value represented by the name/value pair that should be serialized,
            or undefined if nothing should be serialized. The toJSON method
            will be passed the key associated with the value, and this will be
            bound to the object holding the key.

            For example, this would serialize Dates as ISO strings.

                Date.prototype.toJSON = function (key) {
                    function f(n) {
                        // Format integers to have at least two digits.
                        return n < 10 ? '0' + n : n;
                    }

                    return this.getUTCFullYear()   + '-' +
                         f(this.getUTCMonth() + 1) + '-' +
                         f(this.getUTCDate())      + 'T' +
                         f(this.getUTCHours())     + ':' +
                         f(this.getUTCMinutes())   + ':' +
                         f(this.getUTCSeconds())   + 'Z';
                };

            You can provide an optional replacer method. It will be passed the
            key and value of each member, with this bound to the containing
            object. The value that is returned from your method will be
            serialized. If your method returns undefined, then the member will
            be excluded from the serialization.

            If the replacer parameter is an array, then it will be used to
            select the members to be serialized. It filters the results such
            that only members with keys listed in the replacer array are
            stringified.

            Values that do not have JSON representations, such as undefined or
            functions, will not be serialized. Such values in objects will be
            dropped; in arrays they will be replaced with null. You can use
            a replacer function to replace those with JSON values.
            JSON.stringify(undefined) returns undefined.

            The optional space parameter produces a stringification of the
            value that is filled with line breaks and indentation to make it
            easier to read.

            If the space parameter is a non-empty string, then that string will
            be used for indentation. If the space parameter is a number, then
            the indentation will be that many spaces.

            Example:

            text = JSON.stringify(['e', {pluribus: 'unum'}]);
            // text is '["e",{"pluribus":"unum"}]'


            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'

            text = JSON.stringify([new Date()], function (key, value) {
                return this[key] instanceof Date ?
                    'Date(' + this[key] + ')' : value;
            });
            // text is '["Date(---current time---)"]'


        JSON.parse(text, reviver)
            This method parses a JSON text to produce an object or array.
            It can throw a SyntaxError exception.

            The optional reviver parameter is a function that can filter and
            transform the results. It receives each of the keys and values,
            and its return value is used instead of the original value.
            If it returns what it received, then the structure is not modified.
            If it returns undefined then the member is deleted.

            Example:

            // Parse the text. Values that look like ISO date strings will
            // be converted to Date objects.

            myData = JSON.parse(text, function (key, value) {
                var a;
                if (typeof value === 'string') {
                    a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
                    if (a) {
                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
                            +a[5], +a[6]));
                    }
                }
                return value;
            });

            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
                var d;
                if (typeof value === 'string' &&
                        value.slice(0, 5) === 'Date(' &&
                        value.slice(-1) === ')') {
                    d = new Date(value.slice(5, -1));
                    if (d) {
                        return d;
                    }
                }
                return value;
            });


    This is a reference implementation. You are free to copy, modify, or
    redistribute.

    This code should be minified before deployment.
    See http://javascript.crockford.com/jsmin.html

    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
    NOT CONTROL.
*/

/*jslint evil: true */

/*global JSON */

/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", call,
    charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes,
    getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length,
    parse, propertyIsEnumerable, prototype, push, replace, slice, stringify,
    test, toJSON, toString
*/

if (!this.JSONorg) {

// Create a JSON object only if one does not already exist. We create the
// object in a closure to avoid creating global variables.

    JSONorg = function () {

        function f(n) {
            // Format integers to have at least two digits.
            return n < 10 ? '0' + n : n;
        }

        Date.prototype.toJSON = function (key) {

            return this.getUTCFullYear()   + '-' +
                 f(this.getUTCMonth() + 1) + '-' +
                 f(this.getUTCDate())      + 'T' +
                 f(this.getUTCHours())     + ':' +
                 f(this.getUTCMinutes())   + ':' +
                 f(this.getUTCSeconds())   + 'Z';
        };

        String.prototype.toJSON =
        Number.prototype.toJSON =
        Boolean.prototype.toJSON = function (key) {
            return this.valueOf();
        };

        var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
            escapeable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
            gap,
            indent,
            meta = {    // table of character substitutions
                '\b': '\\b',
                '\t': '\\t',
                '\n': '\\n',
                '\f': '\\f',
                '\r': '\\r',
                '"' : '\\"',
                '\\': '\\\\'
            },
            rep;


        function quote(string) {

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.

            escapeable.lastIndex = 0;
            return escapeable.test(string) ?
                '"' + string.replace(escapeable, function (a) {
                    var c = meta[a];
                    if (typeof c === 'string') {
                        return c;
                    }
                    return '\\u' + ('0000' +
                            (+(a.charCodeAt(0))).toString(16)).slice(-4);
                }) + '"' :
                '"' + string + '"';
        }


        function str(key, holder) {

// Produce a string from holder[key].

            var i,          // The loop counter.
                k,          // The member key.
                v,          // The member value.
                length,
                mind = gap,
                partial,
                value = holder[key];

// If the value has a toJSON method, call it to obtain a replacement value.

            if (value && typeof value === 'object' &&
                    typeof value.toJSON === 'function') {
                value = value.toJSON(key);
            }

// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.

            if (typeof rep === 'function') {
                value = rep.call(holder, key, value);
            }

// What happens next depends on the value's type.

            switch (typeof value) {
            case 'string':
                return quote(value);

            case 'number':

// JSON numbers must be finite. Encode non-finite numbers as null.

                return isFinite(value) ? String(value) : 'null';

            case 'boolean':
            case 'null':

// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.

                return String(value);

// If the type is 'object', we might be dealing with an object or an array or
// null.

            case 'object':

// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.

                if (!value) {
                    return 'null';
                }

// Make an array to hold the partial results of stringifying this object value.

                gap += indent;
                partial = [];

// If the object has a dontEnum length property, we'll treat it as an array.

                if (typeof value.length === 'number' &&
                        !(value.propertyIsEnumerable('length'))) {

// The object is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.

                    length = value.length;
                    for (i = 0; i < length; i += 1) {
                        partial[i] = str(i, value) || 'null';
                    }

// Join all of the elements together, separated with commas, and wrap them in
// brackets.

                    v = partial.length === 0 ? '[]' :
                        gap ? '[\n' + gap +
                                partial.join(',\n' + gap) + '\n' +
                                    mind + ']' :
                              '[' + partial.join(',') + ']';
                    gap = mind;
                    return v;
                }

// If the replacer is an array, use it to select the members to be stringified.

                if (rep && typeof rep === 'object') {
                    length = rep.length;
                    for (i = 0; i < length; i += 1) {
                        k = rep[i];
                        if (typeof k === 'string') {
                            v = str(k, value);
                            if (v) {
                                partial.push(quote(k) + (gap ? ': ' : ':') + v);
                            }
                        }
                    }
                } else {

// Otherwise, iterate through all of the keys in the object.

                    for (k in value) {
                        if (Object.hasOwnProperty.call(value, k)) {
                            v = str(k, value);
                            if (v) {
                                partial.push(quote(k) + (gap ? ': ' : ':') + v);
                            }
                        }
                    }
                }

// Join all of the member texts together, separated with commas,
// and wrap them in braces.

                v = partial.length === 0 ? '{}' :
                    gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
                            mind + '}' : '{' + partial.join(',') + '}';
                gap = mind;
                return v;
            }
        }

// Return the JSON object containing the stringify and parse methods.

        return {
            stringify: function (value, replacer, space) {

// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.

                var i;
                gap = '';
                indent = '';

// If the space parameter is a number, make an indent string containing that
// many spaces.

                if (typeof space === 'number') {
                    for (i = 0; i < space; i += 1) {
                        indent += ' ';
                    }

// If the space parameter is a string, it will be used as the indent string.

                } else if (typeof space === 'string') {
                    indent = space;
                }

// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.

                rep = replacer;
                if (replacer && typeof replacer !== 'function' &&
                        (typeof replacer !== 'object' ||
                         typeof replacer.length !== 'number')) {
                    throw new Error('JSONorg.stringify');
                }

// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.

                return str('', {'': value});
            },


            parse: function (text, reviver) {

// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.

                var j;

                function walk(holder, key) {

// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.

                    var k, v, value = holder[key];
                    if (value && typeof value === 'object') {
                        for (k in value) {
                            if (Object.hasOwnProperty.call(value, k)) {
                                v = walk(value, k);
                                if (v !== undefined) {
                                    value[k] = v;
                                } else {
                                    delete value[k];
                                }
                            }
                        }
                    }
                    return reviver.call(holder, key, value);
                }


// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.

                cx.lastIndex = 0;
                if (cx.test(text)) {
                    text = text.replace(cx, function (a) {
                        return '\\u' + ('0000' +
                                (+(a.charCodeAt(0))).toString(16)).slice(-4);
                    });
                }

// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.

// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

                if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                    j = eval('(' + text + ')');

// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.

                    return typeof reviver === 'function' ?
                        walk({'': j}, '') : j;
                }

// If the text is not JSON parseable, then a SyntaxError is thrown.

                throw new SyntaxError('JSONorg.parse');
            }
        };
    }();
}


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

var ContinuousTraveler = {};

ContinuousTraveler.app = null;

ContinuousTraveler.Models = {};
ContinuousTraveler.Views = {};
ContinuousTraveler.Views.Icons = {};
ContinuousTraveler.Views.Geometry = {};
ContinuousTraveler.Views.Lines = {};
ContinuousTraveler.Views.Markers = {};
ContinuousTraveler.Controllers = {};

////////////////////////////////////////////////////////////////////////////////
//
// Class: ContinuousTravelerApplication ("classic")
//
// Per-page thin client. Going to a new page destroys this app and creates
// a new one. Thus, the application is instantiated for one ctrl/act/objid
// combination.
//
// Method: Initialize
//
//   Purpose: Start the application, if current document supports it.
//
//   Args:
//     "ctrl":  name of the controller
//     "act":   name of the action the controller is to perform
//     "objid": id of object on which the controller is to perform the action
//     "det":   id of sub-object detail to select, operate on, highlight, whatever
//     "mdv":   name of the document DIV in which to display the map
//
//   Operation: If specified map DIV exists, instantiate map on it; create 
//     requested controller; tell the controller to perform the requested
//     action on the specified object and object detail.
// 
////////////////////////////////////////////////////////////////////////////////


ContinuousTraveler.Application = Class.create ({
	
	initialize: function (request) {
		if (ContinuousTraveler.app) { 
			return ContinuousTraveler.app;
		} else {
			ContinuousTraveler.app = this;
		}
		
		this.unloader = new ContinuousTraveler.Unloader();
		this.controller = new ContinuousTraveler.Controllers.Map(request);
		
		
		if (this.isValid()) {
			window.onresize = ContinuousTraveler.Resize;
			window.onunload = ContinuousTraveler.UnLoad;
			this.request(request);
		} else {
			this.destroy();
		}
	}, // end initialize
	

	isValid: function () {
		return (
			this.unloader && this.unloader.isValid() && 
			this.controller && this.controller.isValid()
		);
	},

	
	destroy: function () {
		window.onresize = null;
		window.onunload = null;
		window.onload = null;
		
		if (this.controller) {
			this.controller.destroy();
			this.controller = null;
		}
		this.unloader.destroy();
		this.unloader = null;
		ContinuousTraveler.app = null;
	},
	
	addUnloader: function (unloader_func) {
		this.unloader.addUnloader(unloader_func);
	},
	
	request: function (request) {
		if (this.isValid()) {
			this.controller.request(request);
		}
	},

	resize: function () {
		if (this.isValid()) {
			this.controller.resize();
		}
	},
	
	zoomToWithin: function (latitude, longitude, radius_miles) {
		if (this.isValid()) {
			this.controller.zoomToWithin(latitude, longitude, radius_miles);
		}
	}

}); // end ContinuousTravelerApplication




////////////////////////////
// GLOBAL FUNCTIONS

ContinuousTraveler.Resize = function () { ContinuousTraveler.app.resize(); }
ContinuousTraveler.UnLoad = function () { ContinuousTraveler.app.destroy(); }
	

ContinuousTraveler.GetWindowDimensions = function () {
	return {
		height: ContinuousTraveler.GetWindowHeight(),
		width: ContinuousTraveler.GetWindowWidth()
	}
}

ContinuousTraveler.GetWindowHeight = function () {
	var height = 0;

	if (window.self && self.innerHeight) {
	    height = self.innerHeight;
	} else if (document.documentElement && document.documentElement.clientHeight) {
	    height = document.documentElement.clientHeight;
	}
	return height - 15;
}

ContinuousTraveler.GetWindowWidth = function () {
	var width = 0;

	if (window.self && self.innerWidth) {
	    width = self.innerWidth;
	} else if (document.documentElement && document.documentElement.clientWidth) {
	    width = document.documentElement.clientWidth;
	}
	return width - 20;
}




ContinuousTraveler.Unloader = Class.create({
	
	initialize: function () {
		this.ordered_list = [];
		this.dict = new Hash();
	},
	
	isValid: function () {
		return (this.ordered_list && this.dict);
	},
	
	unload: function () {
		if (this.ordered_list) {
			while (this.ordered_list.length > 0) {
				var unload_func = this.ordered_list.pop();
				unload_func();
			}
			this.ordered_list = null;
		}
		this.dict = null;
	},
	
	destroy: function () {
		this.unload();
	},
	
	addUnloader: function (unloader_func) {
		if (this.isValid() && unloader_func && (typeof(unloader_func)=="function") && (! this.dict.get(unloader_func))) {
			this.dict.set(unloader_func, unloader_func);
			this.ordered_list.push(unloader_func);
		}
	}
	
}); // end ContinuousTraveler_Unloader





////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////


ContinuousTraveler.OrderedHash = Class.create({
	
	initialize: function (values_should_be_destroyed) {
		this.key_to_index_hash = new Hash();
		this.key_array = [];
		this.destroy_values = !(! values_should_be_destroyed);
	},
	
	destroy: function () {
		if (this.destroy_values) {
			for (var ix=0; ix < this.key_array.length; ix++) {
				this.getValue(this.key_array[ix]).destroy();
			}
		}
		this.key_to_index_hash = null;
		this.key_array = null;
	},
	
	isValid: function () {
		return (
			this.key_to_index_hash &&
			this.key_array
			);
	},
	
	
	empty: function () {
		this.destroy();
		this.key_to_index_hash = new Hash();
		this.key_array = [];
	},
	isEmpty: function () {
		return (this.key_array.length == 0);
	},
	
	
	length: function () {
		return this.key_array.length;
	},
	
	push: function (key, value) {
		if (! this.getValue(key)) {
			var ix = this.key_array.length;
			this.key_array.push(key);
			this.key_to_index_hash.set(key, {ix: ix, value: value});
		}
		return this.key_array.length;
	},
	
	
	
	keys: function () {
		return [].concat(this.key_array);
	},
	values: function () {
		values = [];
		for (var ix=0; ix < this.key_array.length; ix++) {
			values.push(this.getValue(this.key_array[ix]));
		}
		return values;
	},
	
	
	
	first: function () {
		var first_key = -1;
		if (this.key_array.length > 0) {
			first_key = this.key_array.first();
		}
		return first_key;
	},
	getKeyAt: function (index) {
		var key = null;
		if ((index >= 0) && (index < this.key_array.length)) {
			key = this.key_array[index];
		}
		return key;
	},
	getValueAt: function (index) {
		var value = null;
		var key = this.getKeyAt(index);
		if (key) {
			value = this.getValue(key);
		}
		return value;
	},
	last: function () {
		var last_key = -1;
		if (this.key_array.length > 0) {
			last_key = this.key_array.last();
		}
		return last_key;
	},


	set: function (key, value) {
		this.push(key, value);
	},
	get: function (key) {
		return this.getValue(key);
	},
	
	
	
	
	getValue: function (key) {
		var value = null;
		var pair = this.key_to_index_hash.get(key);
		if (pair) {
			value = pair.value;
		}
		return value;
	},
	getIndex: function (key) {
		var ix = -1;
		if (this.key_array.length > 0) {
			var pair = this.key_to_index_hash.get(key);
			if (pair) {
				ix = pair.ix;
			}
			if ((! ix) && (ix != 0)) {
				ix = -1;
			}
		}
		return ix;
	},
	
	
	
	seek: function (key, offset, do_wrap_around) {
		var next_key = -1;
		
		if ((offset != 0) && (this.key_array.length > 0)) {
			var ix = this.getIndex(key);
			
			if (ix >= 0) {
				ix += offset;
			} else if (do_wrap_around) {
				ix = (offset > 0 ? -1 : this.key_array.length);
				ix += offset;
			}
			
			if (do_wrap_around) {
				if (ix < 0) {
					ix = (this.key_array.length - 1) - ((Math.abs(ix)-1) % this.key_array.length)
				} else if (ix >= this.key_array.length) {
					ix = ix % this.key_array.length
				}
			}

			if ((ix >= 0) && (ix < this.key_array.length)) {
				next_key = this.key_array[ix];
			}
		}
		
		return next_key;
	},
	successor: function (key, do_wrap_around) {
		return this.seek (key, 1, do_wrap_around);
	},
	predecessor: function (key, do_wrap_around) {
		return this.seek (key, -1, do_wrap_around);
	}
	
}); // end ContinuousTraveler_OrderedHash


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
//// Passed to JSON.parse() within a lambda to convert values that the parser cannot.
////
////////////////////////////////////////////////////////////////////////////////


ContinuousTraveler.Models.Reviver = Class.create ({
	
	initialize: function () {
	},
	
	revive: function (key, value) {
		
		if (this.DATETIME_KEY_PAT.test(key)) {
			value = this.revive_datetime(value);
		}
		
		return value;
		
	}, // end revive
	
	
	
	revive_datetime: function (value) {
		if (value) {
			// Example date string: "2008-09-02 13:46:40"
			var parse = this.DATETIME_VALUE_PAT.exec(value);
			if (parse) {
				var year = parseInt(parse[1]);
				// parseInt() bug: leading zero causes string to be parsed as octal.
				// So: get rid of leading zeroes.
				var month = parseInt(parse[2].replace(/^0+/g, ''));
				var day = parseInt(parse[3].replace(/^0+/g, ''));
				var hour = parseInt(parse[4].replace(/^0+/g, ''));
				var minutes = parseInt(parse[5].replace(/^0+/g, ''));
				var seconds = parseInt(parse[6].replace(/^0+/g, ''));
				value = new Date(Date.UTC(year, month, day, hour, minutes, seconds))
			}
		}
		return value;
	} // end revive_datetime
	
	
}); // end ContinuousTraveler_Reviver

// Example field key: "edited_at_utc"
ContinuousTraveler.Models.Reviver.prototype.DATETIME_KEY_PAT = /^[\w]+_at_utc$/;
// Example date string: "2008-09-02 13:46:40"
ContinuousTraveler.Models.Reviver.prototype.DATETIME_VALUE_PAT = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////



ContinuousTraveler.Controllers.Map = Class.create({
	
	initialize: function (config) {
		this.map_view = new ContinuousTraveler.Views.Map(config);
	}, // end initialize
	
	
	destroy: function () {
		if (this.map_view) {
			this.map_view.destroy();
			this.map_view = null;
		}
	},
	
	isValid: function () {
		return (this.map_view && this.map_view.isValid());
	},
	
	view: function () {
		return this.map_view;
	},
	
	
	onCreate: function(xmlRequest) {
	},
	onFailure: function(xmlRequest) {
		this.map_view.warningMessage('Problem getting data from server.');
	},
	onException: function(error, source_data) {
		if (! error) {
			error = 'unknown problem';
		}
		this.map_view.warningMessage('Problem getting data from server: ' + error.toString());
	},
	onSuccess: function(xmlRequest) {
	},
	onComplete: function(user_request, result_data) {
		if (this.isValid()) {
			this.map_view.render(user_request, result_data);
		}
	},
	
	resize: function () {
		if (this.isValid()) {
			this.map_view.resize();
		}
	},
	
	
	request: function (user_request) {
		if (user_request.kind == 'push') {
			if (user_request.results) {
				var result_data = this.fromJSON(user_request.results);
				this.onComplete(user_request, result_data);
			} else {
				this.onComplete(user_request, null);
			}
		} else if (user_request.kind == 'revived') {
			this.onComplete(user_request, user_request.results);
		} else {
			this.callbackRequest(user_request)
		}
	},
	

	fromJSON: function (json_data) {
		var result_data = null;
		if (json_data) {
			try {
				var reviver = new ContinuousTraveler.Models.Reviver();
				/*
				** Modified local copy of http://www.JSON.org/json2.js to work-around changes to Safari.
				** New WebKit (June 2009) includes a JSON symbol that keeps this file from defining
				** its functions. Changed top level JSON function to JSONorg in this file. Changed error
				** messages to match.
				*/
				result_data = JSONorg.parse(
					json_data,
					function (key, value) {
						return reviver.revive(key, value)
					}
				);
			} catch (err) {
				this.onException(err, json_data)
			}
		}
		return result_data;
	},
	
	
	
	callbackRequest: function (request) {
		var url = '/' + request.ctrl + '/' + request.act + '_json/';
		if (request.objid) {
			url += String(request.objid);
		}
		var parameters = request.params;
		var method = 'put';
	
		var observer = this;
		
		new Ajax.Request(
			url,
			{
				method: method,
				parameters: parameters,
				
				evalJS: false,
				evalJSON: false,
			
				onCreate: function(xmlRequest) {
					observer.onCreate(xmlRequest);
				},
				onFailure: function(xmlRequest) {
					observer.onFailure(xmlRequest);
				},
				onException: function(xmlRequest, error) {
					observer.onException(xmlRequest, error);
				},
				onSuccess: function(xmlRequest) {
					observer.onSuccess(xmlRequest);
				},
			
				onComplete: function(xmlRequest) {
					var result_data = observer.fromJSON(xmlRequest.responseText);
					if (result_data) {
						var user_request = observer.reconstructRequest(xmlRequest);
						observer.onComplete(user_request, result_data);
					}
				}
			}
		);
		
	}, // end request
	
	
	reconstructRequest: function (xmlRequest) {
		var pieces = xmlRequest.request.url.split('/');
		if (pieces && (pieces.length >= 3)) {
			return {
				ctrl: pieces[1],
				act: pieces[2].replace('_json', ''),
				objid: pieces[3],
				params: xmlRequest.request.parameters
			};
		}
	},
	
	zoomToWithin: function (latitude, longitude, radius_miles) {
		if (this.isValid()) {
			this.map_view.zoomToWithin(latitude, longitude, radius_miles);
		}
	}
	
	
}); // end ContinuousTraveler_MapController


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
//// NOT GENERAL PURPOSE GEOGRAPHIC COORDINATES
////
//// Only valid in north-western quarter of the globe, excluding extremes.
//// For example, will not support campsites located at the North Pole.
////
//// Latitude range: 0.0 <= degrees <= 90.0
//// Longitude range: -180.0 <= degrees <= 0
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Geometry.Coords = Class.create ({
	
	initialize: function (latitude, longitude) {
		this.latitude = this.limitLatitude(latitude);
		this.longitude = this.limitLongitude(longitude);
	},
	
	destroy: function () {
		this.ne_coords = null;
		this.sw_coords = null;
	},
	
	isValid: function () {
		return this._local_isValid();
	},
	
	// Can only become invalid after destroy(), or if encapsulation is violated.
	_local_isValid: function () {
		return this.isValidLatitudeLongitude(this.latitude, this.longitude);
	},
	
	isNotNull: function () {
		return (
			this._local_isValid() &&
			this.latitude > 0.0 &&
			this.longitude < 0.0
		);
	},
	
	isNull: function () {
		return (! this.isNotNull());
	},
	
	toString: function () {
		return '(' + this.latitude.toString() + ',' + this.longitude.toString() + ')';
	},
	
	isEqual: function (other_coords) {
		return (
			(this.latitude == other_coords.latitude) &&
			(this.longitude == other_coords.longitude)
		);
	},
	isNotEqual: function (other_coords) {
		return (! this.isEqual(other_coords));
	},
	getLatitude: function () {
		return this.latitude;
	},
	setLatitude: function (latitude) {
		this.latitude = this.limitLatitude(latitude);
	},
	getLongitude: function () {
		return this.longitude;
	},
	setLongitude: function (longitude) {
		this.longitude = this.limitLongitude(longitude);
	},

	isNorthOf: function (other_coords) {
		return (this.latitude > other_coords.latitude);
	},
	isSameLatitude: function (other_coords) {
		return (this.latitude == other_coords.latitude);
	},
	isSouthOf: function (other_coords) {
		return (this.latitude < other_coords.latitude);
	},
	isWestOf: function (other_coords) {
		return (this.longitude < other_coords.longitude);
	},
	isSameLongitude: function (other_coords) {
		return (this.longitude == other_coords.longitude);
	},
	isEastOf: function (other_coords) {
		return (this.longitude > other_coords.longitude);
	},
	
	
	
	distanceMiles: function (other_coord) {
		var miles = 0.0;

		var latr = this.degrees_to_radians(this.latitude)
		var longr = this.degrees_to_radians(this.longitude)
		
		var otherlatr = this.degrees_to_radians(other_coord.latitude)
		var otherlongr = this.degrees_to_radians(other_coord.longitude)
		
		if ((latr == otherlatr) && (longr == otherlongr)) {
			miles = 0.0;
		} else {
			miles = (
				this.EARTHRADIUS * 
        		Math.acos(
					(Math.sin(latr) * Math.sin(otherlatr)) + 
          			(Math.cos(latr) * Math.cos(otherlatr) * Math.cos(longr-otherlongr))
				)
			);
		}
		return miles;
	}, // end distanceMiles
	
	
	// Heading from other_coord to self.
	headingDegrees: function (other_coord) {
		return this.radians_to_degrees(this.headingRadians(other_coord));
	},
	
	headingRadians: function (other_coord) {
		var heading = 0.0;

		var latr = this.degrees_to_radians(this.latitude)
		var longr = this.degrees_to_radians(this.longitude)
		
		var otherlatr = this.degrees_to_radians(other_coord.latitude)
		var otherlongr = this.degrees_to_radians(other_coord.longitude)
		
		var delta_long = (otherlongr - longr)
		heading = (
			Math.atan2(Math.sin(delta_long) * Math.cos(otherlatr),
            (Math.cos(latr) * Math.sin(otherlatr)) - 
                (Math.sin(latr) * Math.cos(otherlatr) * Math.cos(delta_long)))
		);
		return heading;
	}, // end headingRadians

	




	// Return coordinates of point the specified number of miles away on the specified heading.
	// Good for distances of up to 1/4 longitudinal circumference
	translateMiles: function (heading_degrees, miles) {
		var heading_radians = this.degrees_to_radians(heading_degrees);
		var distance = this.statute_miles_to_radians(miles);
		var lat1_radians = this.degrees_to_radians(this.latitude);
		var long1_radians = this.degrees_to_radians(this.longitude);

		// http://williams.best.vwh.net/avform.htm#LL
		var lat2_radians = (
			(Math.asin(Math.sin(lat1_radians) * Math.cos(distance)) +
            (Math.cos(lat1_radians) * Math.sin(distance) * Math.cos(heading_radians)))
		);
                        
		// If endpoint is a pole: special case
		if (Math.cos(lat2_radians) == 0) {
			long2_radians=long1_radians      
		} else {
			long2_radians = (
				(
					(
						long1_radians + 
						(Math.asin(Math.sin(heading_radians) * Math.sin(distance) / Math.cos(lat2_radians))) + 
						Math.PI
					) % (2 * Math.PI)
				) - 
				Math.PI
			);
		}
		
		var lat_degrees = this.radians_to_degrees(lat2_radians);
		var long_degrees = this.radians_to_degrees(long2_radians) - 360;

		var new_coords = new ContinuousTraveler.Views.Geometry.Coords(lat_degrees, long_degrees);
		
		return new_coords;
		
	}, // end translateMiles
	
	
	getMapPoint: function () {
		return new google.maps.LatLng(this.latitude, this.longitude);
	}




}); // end ContinuousTraveler.Views.Geometry.Coords

ContinuousTraveler.Views.Geometry.Coords.prototype.MINIMUM_LATITUDE = 0.0;
ContinuousTraveler.Views.Geometry.Coords.prototype.MAXIMUM_LATITUDE = 90.0;
ContinuousTraveler.Views.Geometry.Coords.prototype.MINIMUM_LONGITUDE = -180.0;
ContinuousTraveler.Views.Geometry.Coords.prototype.MAXIMUM_LONGITUDE = 0.0;

ContinuousTraveler.Views.Geometry.Coords.prototype.isValidLatitude = function (latitude) {
	return (
		(latitude >= this.MINIMUM_LATITUDE) &&
		(latitude <= this.MAXIMUM_LATITUDE)
	);
};
ContinuousTraveler.Views.Geometry.Coords.prototype.isValidLongitude = function (longitude) {
	return (
		(longitude >= this.MINIMUM_LONGITUDE) &&
		(longitude <= this.MAXIMUM_LONGITUDE)
	);
};
ContinuousTraveler.Views.Geometry.Coords.prototype.isValidLatitudeLongitude = function (latitude, longitude) {
	return (
		this.isValidLatitude(latitude) &&
		this.isValidLongitude(longitude)
	);
};
ContinuousTraveler.Views.Geometry.Coords.prototype.limitLatitude = function (latitude) {
	if ((latitude > this.MAXIMUM_LATITUDE)) {
		latitude = this.MAXIMUM_LATITUDE;
	} else if ((latitude < this.MINIMUM_LATITUDE)) {
		latitude = this.MINIMUM_LATITUDE;
	}
	return latitude;
};
ContinuousTraveler.Views.Geometry.Coords.prototype.limitLongitude = function (longitude) {
	if ((longitude > this.MAXIMUM_LONGITUDE)) {
		longitude = this.MAXIMUM_LONGITUDE;
	} else if ((longitude < this.MINIMUM_LONGITUDE)) {
		longitude = this.MINIMUM_LONGITUDE;
	}
	return longitude;
};


ContinuousTraveler.Views.Geometry.Coords.prototype.parseDMS = function (dmsString) {
	var workString = dmsString.replace(/(\.[^\d])/g, ' ');
	workString = workString.replace(/([^\d]\.)/g, ' ');
	workString = workString.replace(/[^-\d.\s]+/g, " ")
	var splitArray = workString.split(" ");

	var dmsArray = [];
	for (var ix=0; ix < splitArray.length; ix++) {
		if (splitArray[ix].length > 0) {
			dmsArray.push(parseFloat(splitArray[ix]));
		}
	}
	while (dmsArray.length < 3) {
		dmsArray.push(0.0);
	}
	return this.DMSToDegrees(dmsArray[0], dmsArray[1], dmsArray[2]);
};

ContinuousTraveler.Views.Geometry.Coords.prototype.DMSToDegrees = function (dmsDegrees, dmsMinutes, dmsSeconds) {
	var degrees = Math.abs(dmsDegrees) + (Math.abs(dmsMinutes) / 60) + (Math.abs(dmsSeconds) / 3600);
	if (dmsDegrees < 0) {
		degrees = - degrees;
	}
	return degrees;
};

ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint = function (google_latlng) {
	return new ContinuousTraveler.Views.Geometry.Coords(google_latlng.lat(), google_latlng.lng());
};




// References:
//     http://en.wikipedia.org/wiki/Great-circle_distance
//     http://www.movable-type.co.uk/scripts/LatLong.html
//     http://williams.best.vwh.net/avform.htm#LL
//
ContinuousTraveler.Views.Geometry.Coords.prototype.EARTHRADIUS = 3959.8712; // statute miles

ContinuousTraveler.Views.Geometry.Coords.prototype.DEGREESTORADIANS = Math.PI/180;
ContinuousTraveler.Views.Geometry.Coords.prototype.RADIANSTODEGREES = 180/Math.PI;

ContinuousTraveler.Views.Geometry.Coords.prototype.NAUTICALMILESTORADIANS = Math.PI/(180*60);
ContinuousTraveler.Views.Geometry.Coords.prototype.RADIANSTONAUTICALMILES = (180*60)/Math.PI;

ContinuousTraveler.Views.Geometry.Coords.prototype.NAUTICALMILESTOSTATUTEMILES = 1.15077945;
ContinuousTraveler.Views.Geometry.Coords.prototype.STATUTEMILESTONAUTICALMILES = 1/1.15077945;

ContinuousTraveler.Views.Geometry.Coords.prototype.degrees_to_radians = function (degrees) {
	return (this.DEGREESTORADIANS * degrees);
};
ContinuousTraveler.Views.Geometry.Coords.prototype.radians_to_degrees = function (radians) {
	return (((radians * this.RADIANSTODEGREES) + 360) % 360);
};

ContinuousTraveler.Views.Geometry.Coords.prototype.nautical_miles_to_radians = function (nautical_miles) {
	return (this.NAUTICALMILESTORADIANS * nautical_miles);
};
ContinuousTraveler.Views.Geometry.Coords.prototype.radians_to_nautical_miles = function (radians) {
	return (this.RADIANSTONAUTICALMILES * radians);
};

ContinuousTraveler.Views.Geometry.Coords.prototype.nautical_miles_to_statute_miles = function (nautical_miles) {
	return (this.NAUTICALMILESTOSTATUTEMILES * nautical_miles);
};
ContinuousTraveler.Views.Geometry.Coords.prototype.statute_miles_to_nautical_miles = function (statute_miles) {
	return (this.STATUTEMILESTONAUTICALMILES * statute_miles);
};

ContinuousTraveler.Views.Geometry.Coords.prototype.statute_miles_to_radians = function (statute_miles) {
	return this.nautical_miles_to_radians(this.statute_miles_to_nautical_miles(statute_miles));
};
ContinuousTraveler.Views.Geometry.Coords.prototype.radians_to_statute_miles = function (radians) {
	return this.nautical_miles_to_statute_miles(this.radians_to_nautical_miles(radians));
};






////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Geometry.Rectangle = Class.create ({
	
	initialize: function (coords1, coords2) {
		var north = null;
		var south = null;
		var east = null;
		var west = null;
		if (coords1.isNorthOf(coords2)) {
			north = coords1.getLatitude();
			south = coords2.getLatitude();
		} else {
			south = coords1.getLatitude();
			north = coords2.getLatitude();
		}
		if (coords1.isWestOf(coords2)) {
			west = coords1.getLongitude();
			east = coords2.getLongitude();
		} else {
			east = coords1.getLongitude();
			west = coords2.getLongitude();
		}
		this.ne_coords = new ContinuousTraveler.Views.Geometry.Coords(north, east);
		this.sw_coords = new ContinuousTraveler.Views.Geometry.Coords(south, west);
	},
	
	destroy: function () {
		this.ne_coords = null;
		this.sw_coords = null;
	},
	
	isValid: function () {
		return this._local_isValid();
	},
	
	_local_isValid: function () {
		return (
			this.ne_coords && this.ne_coords.isValid() &&
			this.sw_coords && this.sw_coords.isValid() &&
			(! this.sw_coords.isNorthOf(this.ne_coords)) &&
			(! this.sw_coords.isEastOf(this.ne_coords)) &&
			(! this.ne_coords.isSouthOf(this.sw_coords)) &&
			(! this.ne_coords.isWestOf(this.sw_coords))
		);
	},
	
	clone: function () {
		return new ContinuousTraveler.Views.Geometry.Rectangle(this.ne_coords, this.sw_coords);
	},
	
	toString: function () {
		return '(' + this.ne_coords + ',' + this.sw_coords + ')';
	},
	
	getMidPoint: function () {
		return new ContinuousTraveler.Views.Geometry.Coords(this.getMidLatitude(), this.getMidLongitude());
	},
	
	getMidLatitude: function () {
		return (this.getSouth() + (this.getHeightDegrees() / 2));
	},
	getMidLongitude: function () {
		return (this.getWest() + (this.getWidthDegrees() / 2));
	},
	
	getHeightDegrees: function () {
		return (this.getNorth() - this.getSouth());
	},
	getWidthDegrees: function () {
		return (this.getEast() - this.getWest());
	},
	
	getNorth: function () {
		return this.ne_coords.getLatitude();
	},
	setNorth: function (latitude) {
		this.ne_coords.setLatitude(latitude);
	},

	getSouth: function () {
		return this.sw_coords.getLatitude();
	},
	setSouth: function (latitude) {
		this.sw_coords.setLatitude(latitude);
	},

	getEast: function () {
		return this.ne_coords.getLongitude();
	},
	setEast: function (longitude) {
		this.ne_coords.setLongitude(longitude);
	},

	getWest: function () {
		return this.sw_coords.getLongitude();
	},
	setWest: function (longitude) {
		this.sw_coords.setLongitude(longitude);
	},

	
	getNorthEast: function () {
		return new ContinuousTraveler.Views.Geometry.Coords(this.getNorth(), this.getEast());
	},
	getSouthWest: function () {
		return new ContinuousTraveler.Views.Geometry.Coords(this.getSouth(), this.getWest());
	},
	getNorthWest: function () {
		return new ContinuousTraveler.Views.Geometry.Coords(this.getNorth(), this.getWest());
	},
	getSouthEast: function () {
		return new ContinuousTraveler.Views.Geometry.Coords(this.getSouth(), this.getEast());
	},
	
	
	extendToCoords: function (coords) {
		if (coords.isNorthOf(this.ne_coords)) {
			this.setNorth(coords.getLatitude());
		} else if (coords.isSouthOf(this.sw_coords)) {
			this.setSouth(coords.getLatitude());
		}
		if (coords.isWestOf(this.sw_coords)) {
			this.setWest(coords.getLongitude());
		} else if (coords.isEastOf(this.ne_coords)) {
			this.setEast(coords.getLongitude());
		}
	},
	
	union: function (rect) {
		this.extendToCoords(rect.sw_coords);
		this.extendToCoords(rect.ne_coords);
	},
	

	// Google Map objects
	getMapNorthEast: function () {
		return new google.maps.LatLng(this.getNorth(), this.getEast());
	},
	getMapSouthWest: function () {
		return new google.maps.LatLng(this.getSouth(), this.getWest());
	},
	getMapNorthWest: function () {
		return new google.maps.LatLng(this.getNorth(), this.getWest());
	},
	getMapSouthEast: function () {
		return new google.maps.LatLng(this.getSouth(), this.getEast());
	},
	getMapMidPoint: function () {
		return this.getMidPoint().getMapPoint();
	},
	getMapBounds: function () {
		var ne = this.getMapNorthEast();
		var sw = this.getMapSouthWest();
		var bounds = new google.maps.LatLngBounds();
		bounds.extend(ne);
		bounds.extend(sw);
		return bounds;
	}
	
}); // end ContinuousTraveler.Views.Geometry.Rectangle





ContinuousTraveler.Views.Geometry.Rectangle.prototype.createLatitudeLongitude = function (
	latitude1, longitude1, latitude2, longitude2
) {
	var coord1 = new ContinuousTraveler.Views.Geometry.Coords(latitude1, longitude1);
	var coord2 = new ContinuousTraveler.Views.Geometry.Coords(latitude2, longitude2);
	return new ContinuousTraveler.Views.Geometry.Rectangle(coord1, coord2);
};

ContinuousTraveler.Views.Geometry.Rectangle.prototype.createFromMapBounds = function (
	google_bounds
) {
	var ne = google_bounds.getNorthEast();
	var sw = google_bounds.getSouthWest();
	return this.createLatitudeLongitude(ne.lat(), ne.lng(), sw.lat(), sw.lng());
};



ContinuousTraveler.Views.Geometry.Rectangle.prototype.createCentered_Degrees = function (
	coord_center, height_degrees, width_degrees
) {
	return this.createLatitudeLongitude(
		coord_center.getLatitude() + (height_degrees/2), 
        coord_center.getLongitude() - (width_degrees/2), 
        coord_center.getLatitude() - (height_degrees/2), 
        coord_center.getLongitude() + (width_degrees/2)
	);
};



ContinuousTraveler.Views.Geometry.Rectangle.prototype.createCentered_Miles = function (
	coord_center, height_miles, width_miles
) {
	var npt = coord_center.translateMiles(0, (height_miles/2));
	var spt = coord_center.translateMiles(180, (height_miles/2));
	var ept = coord_center.translateMiles(90, (width_miles/2));
	var wpt = coord_center.translateMiles(270, (width_miles/2));
	return this.createCentered_Degrees(
		coord_center,
		(npt.getLatitude() - spt.getLatitude()),
		(ept.getLongitude() - wpt.getLongitude())
	);
};





////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////




ContinuousTraveler.Views.Element = Class.create({

	initialize: function (dom_element) {
		this.setDomElement(dom_element);
	},
	destroy: function () {
		this.dom_element = null;
	},
	
	// Validity is not checked internally. Constructor responsibility.
	isValid: function () {
		return ! (! this.dom_element);
	},
	
	alias: function () {
		return this;
	},
	
	getDomElement: function () {
		return this.dom_element;
	},
	setDomElement: function (dom_element) {
		this.dom_element = (dom_element ? dom_element : null);
		return this;
	},
	
	getId: function () {
		return this.readAttribute('id');
	},
	
	toggle: function () {
		this.dom_element.toggle();
	},
	hide: function () {
		this.dom_element.hide();
	},
	show: function () {
		this.dom_element.show();
	},
	isVisible: function () {
		return this.dom_element.visible();
	},
	

	readAttribute: function (key) {
		return this.dom_element.readAttribute(key);
	},
	
	observe: function (event_name, observer_function, use_capture) {
		return this.dom_element.observe(event_name, observer_function, use_capture);
	},
	
	innerContents: function () {
		var text = null;
		if (this.dom_element.innerHTML) {
			text = this.dom_element.innerHTML.strip();
		}
		return text;
	},
	innerContentsLength: function () {
		var text = this.innerContents();
		return (text ? text.length : 0);
	},
	hasInnerContents: function () {
		return (this.innerContentsLength() != 0);
	},
	
	
	getDimensions: function () {
		return this.dom_element.getDimensions();
	},
	getHeight: function () {
		return this.getDimensions().height;
	},
	setHeight: function (height) {
		this.dom_element.setStyle({height: String(height) + 'px'});
	},
	getWidth: function () {
		return this.getDimensions().width;
	},
	setWidth: function (width) {
		this.dom_element.setStyle({width: String(width) + 'px'});
	},
	setWidthPercent: function (percentage) {
		this.dom_element.setStyle({width: percentage.toString() + "%"})
	},
	setDimensions: function (dims) {
		this.setHeight(dims.height);
		this.setWidth(dims.width);
	},
	
	
	getBounds: function () {
		var offsets = this.getOffsets();
		var dims = this.getDimensions();
		return {
			top: offsets.top, left: offsets.left,
			bottom: offsets.top + dims.height, right: offsets.left + dims.width
		};
	},
	getOffsets: function () {
		return this.dom_element.positionedOffset();
	},
	setOffsets: function (offsets) {
		this.dom_element.setStyle({top: String(offsets.top) + 'px'});
		this.dom_element.setStyle({left: String(offsets.left) + 'px'});
	},
	
	getTop: function () {
		return this.getOffsets().top;
	},
	getBottom: function () {
		return this.getTop() + this.getHeight();
	},
	
	getVerticalCenter: function () {
		return (this.getTop() + (this.getHeight() / 2));
	},
	
	getLeft: function () {
		return this.getOffsets().left;
	},
	getRight: function () {
		return this.getLeft() + this.getWidth();
	},
	
	setTop: function (top) {
		this.dom_element.setStyle({top: String(top) + 'px'});
	},
	setLeft: function (left) {
		this.dom_element.setStyle({left: String(left) + 'px'});
	},
	
	
	setCursor: function (cursor_type) {
		this.dom_element.setStyle({cursor: cursor_type});
	},
	
	

	resize: function () {
	},
	
	
	
	clear: function () {
		return this.dom_element.update();
	},
	update: function (inner_html) {
		return this.dom_element.update(inner_html);
	},
	replace: function (outer_html) {
		return this.dom_element.replace(outer_html)
	},
	remove: function () {
		return this.dom_element.replace();
	},
	
	
	
	insertTop: function (ct_view_element) {
		new Insertion.Top(this.dom_element, ct_view_element.dom_element);
	},
	insertBottom: function (ct_view_element) {
		new Insertion.Bottom(this.dom_element, ct_view_element.dom_element);
	},
	insertBefore: function (ct_view_element) {
		new Insertion.Before(this.dom_element, ct_view_element.dom_element);
	},
	insertAfter: function (ct_view_element) {
		new Insertion.After(this.dom_element, ct_view_element.dom_element);
	},
	
	
	insertDomElementTop: function (html_or_dom_element) {
		new Insertion.Top(this.dom_element, html_or_dom_element);
	},
	insertDomElementBottom: function (html_or_dom_element) {
		new Insertion.Bottom(this.dom_element, html_or_dom_element);
	},
	insertDomElementBefore: function (html_or_dom_element) {
		new Insertion.Before(this.dom_element, html_or_dom_element);
	},
	insertDomElementAfter: function (html_or_dom_element) {
		new Insertion.After(this.dom_element, html_or_dom_element);
	},
	
	
	isSelected: function () {
		return this.dom_element.hasClassName("selected");
	},
	select: function () {
		if (! this.isSelected()) {
			this.dom_element.addClassName("selected");
		}
	},
	deselect: function () {
		if (this.isSelected()) {
			this.dom_element.removeClassName("selected");
		}
	},
	
	
	draggable: function (options) {
		new Draggable(this.dom_element, options)
	}
	

}); // end ContinuousTraveler_Element








////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
//// Virtual constructor: creates map data based on contents of user request.
////
//// Sets created object up to display on mapview.
////
////////////////////////////////////////////////////////////////////////////////


ContinuousTraveler.Views.createMapData = function (request, mapview) {
	var mapdata = null;
	
	if (mapview) {
		var rectype = request['ctrl'];
		if (rectype) {
			rectype = rectype.toLowerCase();
		}

		switch(rectype) {
			case 'locversions':
				mapdata = ContinuousTraveler.Views.createMapData_Locversion(request, mapview);
				break;
			case 'locations':
				mapdata = ContinuousTraveler.Views.createMapData_Location(request, mapview);
				break;
			case 'trips':
				mapdata = ContinuousTraveler.Views.createMapData_Trip(request, mapview);
				break;
			case 'users':
				mapdata = ContinuousTraveler.Views.createMapData_User(request, mapview);
				break;
		}
	}
	
	return mapdata;
}; // end function ContinuousTraveler_MapData




ContinuousTraveler.Views.createMapData_Locversion = function (request, mapview) {
	var mapdata = null;
	
	if (mapview) {
		switch(request['act']) {
			case 'show':
				mapdata = new ContinuousTraveler.Views.Locversion_Show(mapview);
				break;
			case 'index':
				mapdata = new ContinuousTraveler.Views.Locversion_List(mapview);
				break;
 			case 'edit':
				mapdata = new ContinuousTraveler.Views.Locversion_Edit(mapview);
				break;
		}
	}
	
	return mapdata;
}; // end function createMapData_Locversion









ContinuousTraveler.Views.createMapData_Location = function (request, mapview) {
	var mapdata = null;
	
	if (mapview) {
		switch(request['act']) {
			case 'show':
				mapdata = new ContinuousTraveler.Views.Location_Show(mapview);
				break;
			case 'index':
				mapdata = new ContinuousTraveler.Views.Location_List(mapview);
				break;
		}
	}
	
	return mapdata;
}; // end function createMapData_Location





ContinuousTraveler.Views.createMapData_Trip = function (request, mapview) {
	var mapdata = null;
	
	if (mapview) {
		switch(request['act']) {
			case 'show':
				mapdata = new ContinuousTraveler.Views.Trip_Show(mapview);
				break;
			case 'edit':
				mapdata = new ContinuousTraveler.Views.Trip_Edit(mapview);
				break;
			case 'index':
				mapdata = new ContinuousTraveler.Views.Trip_List(mapview);
				break;
		}
	}
	
	return mapdata;
}; // end function createMapData_Trip




ContinuousTraveler.Views.createMapData_User = function (request, mapview) {
	var mapdata = null;
	
	if (mapview) {
		switch(request['act']) {
			case 'show':
				mapdata = new ContinuousTraveler.Views.User_Show(mapview);
				break;
			case 'edit':
				mapdata = new ContinuousTraveler.Views.User_Edit(mapview);
				break;
		}
	}
	
	return mapdata;
}; // end function createMapData_User




////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////


ContinuousTraveler.Views.Map = Class.create(ContinuousTraveler.Views.Element, {

	initialize: function ($super, request) {
		$super(null);
		this.setup_message_area();
		this.setupSubmitForm();
		
		if (this.isValidMessageArea()) {
			this.map_container_id = 'page-map-container';
			this.menu_id = 'page-map-menu';
			this.zoom_submenu_id = 'page-map-zoom';
			this.sizetoggle_submenu_id = 'page-map-sizetoggle';
			this.map_id = 'page-map';
			
			this.setDomElement( $(this.map_container_id) );
			this.menu_div = new ContinuousTraveler.Views.Element( $(this.menu_id) );
			this.zoom_submenu_div = new ContinuousTraveler.Views.Element( $(this.zoom_submenu_id) );
			this.sizetoggle_button_div = new ContinuousTraveler.Views.Element( $(this.sizetoggle_submenu_id) );
			this.map_div = new ContinuousTraveler.Views.Element( $(this.map_id) );

			if (this.getDomElement() && this.map_div.isValid()) {
				this.setup_map(request);
				this.dataview = ContinuousTraveler.Views.createMapData(request, this);
			}
		}
		
	}, // end initialize
	
	

	
	setup_map: function (request) {
		if (google.maps.BrowserIsCompatible()) {

			ContinuousTraveler.app.addUnloader(google.maps.Unload);
			
			var minzoom = this.MIN_ZOOM;
			var zoomlimitfunc = function () { return minzoom; };
			for (var ix=0 ; ix < G_DEFAULT_MAP_TYPES.length; ix++) {
				G_DEFAULT_MAP_TYPES[ix].getMinimumResolution = zoomlimitfunc;
			}
			//G_NORMAL_MAP.getMinimumResolution = zoomlimitfunc;
			//G_HYBRID_MAP.getMinimumResolution = zoomlimitfunc;
			//G_SATELLITE_MAP.getMinimumResolution = zoomlimitfunc;
			
			this.googlemap = new google.maps.Map2(this.map_div.getDomElement());

			this.resizeTimerId = null;

			this.max_bounds = ContinuousTraveler.Views.Geometry.Rectangle.prototype.createLatitudeLongitude(
				this.NORTHAMERICAN_BOUNDARIES.north, 
				this.NORTHAMERICAN_BOUNDARIES.east,
				this.NORTHAMERICAN_BOUNDARIES.south, 
				this.NORTHAMERICAN_BOUNDARIES.west
			);
			this.setCenter();

			this.map_control = new google.maps.LargeMapControl();
			this.googlemap.addControl(this.map_control);

			this.map_scale_control = new google.maps.ScaleControl();
			this.googlemap.addControl(this.map_scale_control);

//			this.map_type_control = new google.maps.MapTypeControl();
			this.map_type_control = new ExtMapTypeControl({showTraffic: true, showTrafficKey: true});
			this.googlemap.addControl(this.map_type_control);
			
			this.map_div_normal_height = this.map_div.getHeight();
			this.setup_optional_menu();
			
			this.info_window = null;
			this.info_window_html = null;

		} // end if found map div && browser is Google Map compatible
	}, // end setup_map
	
	
	
	setup_message_area: function () {
		var message_div = $( "flash-messages" );
		if (message_div) {
			this.message_area = new ContinuousTraveler.Views.Element(message_div);
		}
	},
	
	
	setCenter: function () {
		var max_gmap_zoom = this.getBoundsZoom(this.max_bounds);
		this.googlemap.setCenter(this.max_bounds.getMapMidPoint(), max_gmap_zoom);
	},
	
	
	destroy: function ($super) {
		if (this.dataview) {
			this.dataview.destroy();
			this.dataview = null;
		}
		if (this.info_window) {
			this.info_window.destroy();
			this.info_window = null;
		}
		if (this.googlemap) {
			this.map_type_control = null;
	        this.map_scale_control = null;
	        this.map_control = null;
	        this.max_bounds = null;
			this.googlemap = null;
		}
		if (this.message_area) {
			this.message_area.destroy();
			this.message_area = null;
		}
		if (this.menu_div) {
			this.menu_div.destroy();
			this.menu_div = null;
		}
		if (this.zoom_submenu_div) {
			this.zoom_submenu_div.destroy();
			this.zoom_submenu_div = null;
		}
		if (this.sizetoggle_button_div) {
			this.sizetoggle_button_div.destroy();
			this.sizetoggle_button_div = null;
		}
		if (this.map_div) {
			this.map_div.destroy();
			this.map_div = null;
		}
		
		$super();
	}, // end unload
	
	
	isValid: function ($super) {
		return ($super() && 
			this.map_div && this.map_div.isValid() &&
			this.googlemap && 
			this.dataview && this.dataview.isValid() &&
			this.isValidMessageArea()
		);
	},

	
	isValidMessageArea: function () {
		return (this.message_area && this.message_area.isValid());
	},
	
	
	
	setup_optional_menu: function () {
		if (this.menu_div && this.menu_div.isValid()) {
			this.map_is_expanded = false;
			var togglefun = this.toggleMapSizeCallback(this);
			this.sizetoggle_button_div.observe('click', togglefun);
			this.map_is_expanded = true;
			this.toggleMapSize();
		} else if (this.menu_div) {
			this.menu_div.destroy();
			this.menu_div = null;
		}
	},
	
	toggleMapSizeCallback: function (observer) {
		return function () {
			observer.toggleMapSize();
			return false;
		};
	},
	toggleMapSize: function () {
		var buttonname = null;
		if (this.map_is_expanded) {
			this.map_is_expanded = false;
			buttonname = 'Expand';
			this.map_div.setHeight(this.map_div_normal_height);
			this.resize();
		} else {
			this.map_is_expanded = true;
			var window_height = ContinuousTraveler.GetWindowHeight() * 0.95;
			this.map_div.setHeight(window_height);
			this.resize();
			buttonname = 'Shrink'
		}
		this.sizetoggle_button_div.update(buttonname);
	},

	
	setupSubmitForm: function () {
		var form_elt = $$('form')
		if (form_elt) {
			this.form = form_elt[0];
		}
	},
	submitForm: function (request) {
		// Hidden command field gets overwritten each time, so must be
		// refetched each time.
		var hidden_command = $('hidden_command');
		
		if (request && request.command && hidden_command && this.form) {
			var formatted_command = 'command[' + request.command + ']';
			var url = '/' + request.ctrl + '/' + request.act + '/';
			if (request.objid) {
				url += String(request.objid);
			}
			hidden_command.name = formatted_command;
			new Ajax.Request(
				url, 
				{
					asynchronous:true, 
					evalScripts:true, 
					parameters:Form.serialize(this.form)
				}
			);
		}
	}, // submitForm
	
	
	clearMessage: function () {
		if (this.isValidMessageArea()) {
			this.message_area.clear();
		}
	},
	warningMessage: function (text) {
		if (this.isValidMessageArea()) {
			var msg = this.messageHtml('warning', text)
			this.message_area.update(msg)
		}
	},
	infoMessage: function (text) {
		if (this.isValidMessageArea()) {
			var msg = this.messageHtml('info', text)
			this.message_area.update(msg)
		}
	},
	successMessage: function (text) {
		if (this.isValidMessageArea()) {
			var msg = this.messageHtml('success', text)
			this.message_area.update(msg)
		}
	},
	debugMessage: function (text) {
		if (this.isValidMessageArea()) {
			var msg = this.messageHtml('debug', text)
			this.message_area.insertDomElementBottom(msg)
		}
	},
	
	messageHtml: function (msgType, text) {
		return (
			'<div class="' + msgType + '">' + text + '</div>'
		);
	},
	
	

    map: function () {
	    return this.googlemap;
    },
	panToLatLng: function (point) {
		this.googlemap.panTo(point);
	},
	panToLatitudeLongitude: function (latitude, longitude) {
		this.panToLatLng(new google.maps.LatLng(latitude, longitude, true));
	},
	panTo: function (coords) {
		this.panToLatitudeLongitude(coords.latitude, coords.longitude);
	},
    
	getZoomMenu: function () {
		if (! this.zoom_menu_div) {
			var div = $( this.menu_div.getId() + "-zoom" );
			if (div) {
				this.zoom_menu_div = new ContinuousTraveler.Views.Element(div);
				if (this.zoom_menu_div && (! this.zoom_menu_div.isValid())) {
					this.zoom_menu_div = null;
				}
			}
		}
		return this.zoom_menu_div
	},
	
	getBounds: function () {
		var map_bounds = this.googlemap.getBounds();
		var bounds = ContinuousTraveler.Views.Geometry.Rectangle.prototype.createFromMapBounds(map_bounds);
		return bounds;
	},
	
	getCenter: function () {
		return ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint(this.googlemap.getCenter());
	},
	
    getBoundsZoom: function (bounds) {
        if (this.googlemap) {
            return this.googlemap.getBoundsZoomLevel(bounds.getMapBounds());
        }
    },

	zoomToWithin: function (latitude, longitude, radius_miles) {
		var center = new ContinuousTraveler.Views.Geometry.Coords(latitude, longitude);
		var bounds = ContinuousTraveler.Views.Geometry.Rectangle.prototype.createCentered_Miles(center, radius_miles, radius_miles)
		this.zoomTo(bounds);
	},

    zoomTo: function (bounds) {
        if (this.googlemap) {
			var zoom = this.getBoundsZoom(bounds);
			var mid = bounds.getMapMidPoint();
            this.googlemap.setZoom(zoom);
            this.googlemap.panTo(mid);
        }
    },
    zoomToMax: function () {
        this.zoomTo(this.max_bounds);
    },
    setZoom: function (zoom) {
        if (this.googlemap) {
            this.googlemap.setZoom(zoom);
        }
    },
    

    resize: function () {
        if (this.googlemap) {
			this.googlemap.savePosition();
            this.googlemap.checkResize();
			this.googlemap.returnToSavedPosition();
		}
    },


	render: function (user_request, result_data) {
		if (this.isValid()) {
			this.dataview.update(user_request, result_data);
		}
	},
	
	
	addListener: function (event_name, callback_function) {
		if (event_name && callback_function) {
			return google.maps.Event.addListener(this.googlemap, event_name, callback_function);
		}
	},
	removeListener: function (listener) {
		if (listener) {
			google.maps.Event.removeListener(listener);
		}
	},
	
	
	addObject: function (map_object) {
		return this.googlemap.addOverlay(map_object);
	},
	removeObject: function (map_object) {
		this.googlemap.removeOverlay(map_object);
	},
	
	setInfoWindowHtml: function (html_text) {
		// If new html is same as old html, do nothing.
		if (this.info_window_html != html_text) {
			this.info_window_html = html_text;

			// If we have info window contents, make sure that we are listening for the
			// events that we use to display/undisplay the content. Else, stop listening.
			if (this.info_window_html) {
				var observer = this;
				if (! this.click_listener) {
					this.click_listener = this.addListener(
						'click',
						function (overlay, latlng) {
			          		observer.onClick(overlay, latlng);
			        	}
					);
				}
				if (! this.infowindowopen_listener) {
					this.infowindowopen_listener = this.addListener(
						"infowindowopen", 
						function () {
							observer.onInfoWindowOpen();
						}
					);
				}
				if (! this.infowindowclose_listener) {
					this.infowindowclose_listener = this.addListener(
						"infowindowclose", 
						function () {
			          		observer.onInfoWindowClose();
			        	}
					);
				}
			} else {
				if (this.click_listener) {
					google.maps.Event.removeListener(this.click_listener);
					this.click_listener = null;
				}
				if (this.infowindowopen_listener) {
					google.maps.Event.removeListener(this.infowindowopen_listener);
					this.infowindowopen_listener = null;
				}
				if (this.infowindowclose_listener) {
					google.maps.Event.removeListener(this.infowindowclose_listener);
					this.infowindowclose_listener = null;
				}
			}
		}
	}, // end setInfoWindowHtml
	
	
	
	infoWindow: function () {
		return this.info_window;
	},
	isInfoWindowOpen: function () {
		return (this.info_window != null);
	},
	closeInfoWindow: function () {
		if (this.info_window) {
			this.googlemap.closeInfoWindow();
			this.info_window.destroy();
			this.info_window = null;
		}
	},
	
	onClick: function (overlay, latlng) {
		if (this.info_window_html) {
			var html_div = ContinuousTraveler.Views.InfoWindow.prototype.createHtml(this.info_window_html);
			this.googlemap.openInfoWindowHtml(latlng, html_div);
		}
	},
	onInfoWindowOpen: function () {
		if (this.map()) {
			this.info_window = new ContinuousTraveler.Views.InfoWindow(this.googlemap.getInfoWindow());
			if (this.info_window && ! this.info_window.isValid()) {
				this.info_window.destroy();
				this.info_window = null;
			}
		}
	},
	onInfoWindowClose: function () {
		if (this.info_window) {
			this.info_window.destroy();
			this.info_window = null;
		}
	},
	
	displayError: function (google_status) {
		if (google_status) {
			this.warningMessage('Error getting data from Google.');
		}
	}
	


}); // end ContinuousTraveler_MapView



//////////////
// Class Properties
//////////////

ContinuousTraveler.Views.Map.prototype.MIN_ZOOM = 3;
ContinuousTraveler.Views.Map.prototype.NORTHAMERICAN_BOUNDARIES = {
	north: 64.75, 
	south: 30.25, 
	east: -66.5125, 
	west: -151.7875
};

//////////////
// Global Functions
//////////////


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.InfoWindow = Class.create ({
	
	initialize: function (googlewindow) {
		this.googlewindow = googlewindow;
	},
	
	destroy: function () {
		if (this.googlewindow) {
			this.googlewindow = null;
		}
	},
	
	isValid: function () {
		return (this.googlewindow != null);
	},
	
	childCount: function () {
		var count = 0
		if (this.isValid()) {
			var contents = this.googlewindow.getContentContainers();
			count = (contents ? contents.length : 0);
		}
		return count;
	},
	
	container: function () {
		var elt = $(this.DIV_ID);
		if (elt) {
			return new ContinuousTraveler.Views.Element(elt);
		}
	}
	
}); // end ContinuousTraveler.Views.InfoWindow

ContinuousTraveler.Views.InfoWindow.prototype.DIV_ID = 'map-info-window-container';

ContinuousTraveler.Views.InfoWindow.prototype.TEMPLATE_INFO_WINDOW = new Template(
	'<div id="' + ContinuousTraveler.Views.InfoWindow.prototype.DIV_ID + '">#{body}</div>'
);

ContinuousTraveler.Views.InfoWindow.prototype.createHtml = function (html_text) {
	return ContinuousTraveler.Views.InfoWindow.prototype.TEMPLATE_INFO_WINDOW.evaluate({body: html_text});
}

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.DBRecordBase = Class.create ({
	
	initialize: function (mapview, options) {
		this.map_view = mapview;
		
		this.options = options;
		if (! this.options) { this.options = {}; }
		
		this.secondary_records = new ContinuousTraveler.OrderedHash(true);
		this.secondary_records_dirty = false;
	},
	
	destroy: function () {
		this.map_view = null;
		this.options = null;
		if (this.secondary_records) {
			this.secondary_records.destroy();
			this.secondary_records = null;
			this.secondary_records_dirty = false;
		}
	},
	
	isValid: function () {
		return this._local_isValid();
	},
	_local_isValid: function () {
		return (this.map_view && this.secondary_records && this.secondary_records.isValid());
	},
	
	getOptions: function () {
		return this.options;
	},
	
	
	isEditing: function () {
		return this.options.is_editing;
	},
	
	view: function () {
		return this.map_view;
	},
	
	addListener: function (event_name, callback_function) {
		return null;
	},
	removeListener: function (listener) {
	},
	
	addViewListener: function (event_name, callback_function) {
		if (this.map_view) {
			return this.map_view.addListener(event_name, callback_function);
		}
	},
	removeViewListener: function (listener) {
		if (this.map_view) {
			return this.map_view.removeListener(listener);
		}
	},
	
	isSecondaryRecordsDirty: function () {
		return this.secondary_records_dirty;
	},
	setSecondaryRecordsDirty: function () {
		this.secondary_records_dirty = true;
	},
	clearSecondaryRecordsDirty: function () {
		this.secondary_records_dirty = false;
	},
	displaySecondaryRecords: function () {
		if (this.secondary_records_dirty) {
			var recs = this.secondary_records.values();
			var count = recs.length;
			for (var ix=0; ix<count; ix++) {
				recs[ix].show();
			}
			this.secondary_records_dirty = false;
		}
	},
	secondaryRecords: function () {
		return this.secondary_records;
	},
	addSecondaryRecord: function (record) {
		this.secondary_records_dirty = true;
		this.secondary_records.push(record.getRecordId(), record);
	},
	clearSecondaryRecords: function () {
		this.secondary_records_dirty = true;
		this.secondary_records.empty();
	},
	secondaryRecordsAreEmpty: function () {
		return this.secondary_records.isEmpty();
	},
	getSecondaryRecord: function (recordId) {
		return this.secondary_records.get(recordId);
	},
	getSecondaryRecordIds: function () {
		return this.secondary_records.keys();
	},
	
	addSecondaryRecordListeners: function (event_name, callback_function) {
		var listeners = null;
		if (this.secondary_records && (! this.secondary_records.isEmpty()) && event_name && callback_function) {
			listeners = [];
			var recs = this.secondary_records.values();
			var count = recs.length;
			for (var ix=0; ix<count; ix++) {
				var listener = recs[ix].addListener(event_name, callback_function);
				listeners.push({
					index: ix,
					listener: listener
				});
			}
		}
		return listeners;
	},
	removeSecondaryRecordListeners: function (listener_list) {
		if (this.secondary_records && (! this.secondary_records.isEmpty()) && listener_list) {
			var recs = this.secondary_records.values();
			var count = recs.length;
			for (var ix=0; ix<count; ix++) {
				recs[listener_list[ix].index].removeListener(listener_list[ix].listener);
			}
		}
	}
	
	
}); // end ContinuousTraveler.Views.DBRecordBase


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.DBRecord = Class.create (ContinuousTraveler.Views.DBRecordBase, {
	
	initialize: function ($super, mapview, options) {
		$super(mapview, options);
		this.marker = null;
		this.attrs = {};
		this.show_all_button = null;
		this.show_selected_button = null;
		this.show_map_checkbox = null;
	},
	
	destroy: function ($super) {
		this.show_all_button = null;
		this.show_selected_button = null;
		this.show_map_checkbox = null;
		this.attrs = null;
		if (this.marker) {
			this.marker.destroy();
			this.marker = null;
		}
		$super();
	},
	
	isValid: function ($super) {
		return (this._local_isValid() && $super());
	},
	_local_isValid: function () {
		return (this.attrs);
	},
	
	
	
	isEqual: function (other_rec_view) {
		var attr_count = this.getAttributeCount();
		
		var is_equal = other_rec_view && (attr_count == other_rec_view.getAttributeCount())
		
		for (var ix=0; is_equal && (ix<attr_count); ix++) {
			is_equal = (this.attrs[ix] == other_rec_view[ix])
		}
		
		return is_equal;
	},
	isNotEqual: function (other_rec_view) {
		return (! this.isEqual(other_rec_view));
	},
	
	// should be first line in update()
	setupMapViewControls: function () {
		this.show_all_button = $$('input[name="command[dataslices.show(all)]"]');
		this.show_selected_button = $$('input[name="command[dataslices.show(selected)]"]');
		this.show_map_checkbox = $$('input[name="mediaslices(0)(map)"]');
		if (this.show_all_button && this.show_selected_button && this.show_map_checkbox) {
			var observer = this;
			this.show_all_button[0].observe('click', function(){observer.showMapView();});
			this.show_selected_button[0].observe('click', function(){observer.showHideMapView();});
			this.showHideMapView();
		}
	},
	
	
	showMapView: function () {
		this.view().show();
		return true;
	},
	
	showHideMapView: function () {
		if (this.show_map_checkbox[0].checked) {
			this.view().show();
		} else {
			this.view().hide();
		}
		return true;
	},
	
	
	
	getMarker: function () {
		return this.marker;
	},
	setMarker: function (new_marker) {
		this.marker = new_marker;
	},
	
	getMarkerOptions: function () {
		var options = this.getOptions();
		return ( options ? options.marker : null );
	},
	
	
	
	isMouseOver: function () {
		return ( this.marker ? this.marker.isMouseOver() : false );
	},
	
	
	addListener: function (event_name, callback_function) {
		if (this.marker) {
			return this.marker.addListener(event_name, callback_function);
		}
	},
	removeListener: function (listener) {
		if (this.marker) {
			this.marker.removeListener(listener);
		}
	},
	
	
	
	
	getSecondaryRecord_WithMarkerAtCoords: function (coords) {
		var rec = null;
		var recs = this.secondary_records.values();
		var count = recs.length;
		for (var ix=0; (! rec) && (ix<count); ix++) {
			var marker = recs[ix].getMarker();
			var marker_coords = marker.getCoords();
			if (coords.isEqual(marker_coords)) {
				rec = recs[ix];
			}
		}
		return rec;
	},
	
	getSecondaryRecord_WithMouseOver: function (coords) {
		var rec = null;
		var recs = this.secondary_records.values();
		var count = recs.length;
		for (var ix=0; (! rec) && (ix<count); ix++) {
			var marker = recs[ix].getMarker();
			if (marker.isMouseOver()) {
				rec = recs[ix];
			}
		}
		return rec;
	},
	
	
	getSecondaryRecord_ClosestWithin: function (miles, coords) {
		var rec = null;
		
		if ((miles > 0) && coords && coords.isValid()) {
			var recs = this.secondary_records.values();
			var count = recs.length;
			var rec_distance = miles + 1;
			
			for (var ix=0; ix<count; ix++) {
				var rec_coords = recs[ix].getCoords();
				var distance = rec_coords.distanceMiles(coords);
				if (distance <= miles) {
					if ((! rec) || (distance < rec_distance)) {
						rec = recs[ix];
						rec_distance = distance;
					}
				}
			}
		}
		
		return rec;
	}, // end getSecondaryRecord_ClosestWithin
	
	
	
	applyChanges: function (data) {
		var is_changed = false;
		for (var name in data) {
			if (this.attrs[name] != data[name]) {
				is_changed = true;
				this.attrs[name] = data[name];
			}
		}
		return is_changed;
	}, // applyChanges
	
	
	// Derived classes should overload this to avoid simple replacement of data.
	update: function (user_request, result_data) {
		this.attrs = data;
	},
	setAttributes: function (attrs) {
		this.attrs = attrs;
	},
	getAttributes: function () {
		return this.attrs;
	},
	getAttributeCount: function () {
		return ( this.attrs ? this.attrs.length : 0 );
	},
	
	getRecord: function () {
		if (this._local_isValid()) {
			return this.attrs;
		}
	},
	
	getRecordId: function () {
		return this.getInteger('id', 0);
	},
	
	getAllRecordIds: function () {
		return this.getSecondaryRecordIds().concat(this.getRecordId());
	},
	
	// Rails model name
	getRecordType: function () {
		return this.attrs['record_type'];
	},
	
	getLatitude: function () {
		return this.getInteger('latitude', null);
	},
	setLatitude: function (lat) {
		this.attrs['latitude'] = lat;
	},
	getLongitude: function () {
		return this.getInteger('longitude', null);
	},
	setLongitude: function (lng) {
		this.attrs['longitude'] = lng;
	},
	hasCoords: function () {
		return (
			(this.getLatitude() > 0.0) &&
			(this.getLongitude() < 0.0)
		);
	},
	setCoords: function (coords) {
		this.setLatitude(coords.getLatitude());
		this.setLongitude(coords.getLongitude());
	},
	getCoords: function () {
		var lat = this.getLatitude();
		var lng = this.getLongitude();
		if ((lat != null) && (lng != null)) {
			return new ContinuousTraveler.Views.Geometry.Coords(lat, lng);
		}
	},
	
	
	coordsAreChanging: function (new_result_data) {
		var lat = this.getLatitude();
		var lng = this.getLongitude();
		return (
			(lat != new_result_data.latitude) ||
			(lng != new_result_data.longitude)
		);
	},
	
	
	
	
	getInteger: function (attr_path, default_value) {
		var value = this.get(attr_path);
		if (value == null) {
			value = default_value;
		} else if (typeof(value) == 'string'){
			value = parseInt(value);
		}
		return value;
	},
	
	getString: function (attr_path, default_value) {
		var value = this.get(attr_path);
		if (value == null) {
			value = default_value;
		} else if (typeof(value) != 'string') {
			value = value.toString();
		}
		return value;
	},
	
	get: function (attr_path) {
		if (this._local_isValid()) {
			var ref_n_key = this.getRef(attr_path);
			if (ref_n_key) {
				var ref = ref_n_key[0];
				var key = ref_n_key[1];
				return ref[key];
			}
		}
	},
	set: function (attr_path, new_value) {
		if (this._local_isValid()) {
			var ref_n_key = this.getRef(attr_path);
			if (ref_n_key) {
				var ref = ref_n_key[0];
				var key = ref_n_key[1];
				var old_value = ref[key];
				ref[key] = new_value;
				return old_value;
			}
		}
	},
	clear: function (attr_path) {
		if (this._local_isValid()) {
			return this.set(attr_path, null);
		}
	},
	hasValue: function (attr_path) {
		var val = this.get(attr_path);
		if ((type_of(val) == 'string') && (val == '')) {
			val = null;
		}
		return (val != null);
	},
	getRef: function (attr_path) {
		if (this._local_isValid()) {
			var ref = this.attrs;
			var key = attr_path;
			if (attr_path && (attr_path != '')) {
				var keys = attr_path.split('.');
				var leaf_ix = keys.length - 1;
				var ix = 0;
				while (ref && (ix < leaf_ix)) {
					key = keys[ix];
					ref = ref[key];
					ix ++;
				}
				key = keys[ix];
			}
			
			if (ref && key) {
				return [ref, key];
			}
		}
	}, // end getRef
	
	
	setPrimaryViewData: function (bool_val) {
		this.set('is_primary_view_data', !(! bool_val));
	},
	isPrimaryViewData: function () {
		return (this.get('is_primary_view_data') == true);
	},
	
	
	getBoundsForRadius: function (radius_miles) {
		var coords = this.getCoords();
		if (coords && coords.isValid()) {
			if (radius_miles < this.AT_RADIUS_MILES) {
				radius_miles = this.AT_RADIUS_MILES
			}
			if (radius_miles > this.MOVEON_RADIUS_MILES) {
				radius_miles = this.MOVEON_RADIUS_MILES
			}
			var dim = radius_miles * 2;
			return ContinuousTraveler.Views.Geometry.Rectangle.prototype.createCentered_Miles(coords, dim, dim);
		}
	},
	getBoundsForAt: function () {
		return this.getBoundsForRadius(this.AT_RADIUS_MILES);
	},
	getBoundsForLocal: function () {
		return this.getBoundsForRadius(this.LOCAL_RADIUS_MILES);
	},
	getBoundsForNear: function () {
		return this.getBoundsForRadius(this.NEAR_RADIUS_MILES);
	},
	getBoundsForDayTrip: function () {
		return this.getBoundsForRadius(this.DAYTRIP_RADIUS_MILES);
	},
	getBoundsForMoveOn: function () {
		return this.getBoundsForRadius(this.MOVEON_RADIUS_MILES);
	},
	
	zoomForRadius: function (radius_miles) {
		var bounds = this.getBoundsForRadius(radius_miles);
		if (bounds && bounds.isValid()) {
			this.view().zoomTo(bounds);
		}
	},
	
	zoomForAt: function () {
		return this.zoomForRadius(this.AT_RADIUS_MILES);
	},
	zoomForLocal: function () {
		return this.zoomForRadius(this.LOCAL_RADIUS_MILES);
	},
	zoomForNear: function () {
		return this.zoomForRadius(this.NEAR_RADIUS_MILES);
	},
	zoomForDayTrip: function () {
		return this.zoomForRadius(this.DAYTRIP_RADIUS_MILES);
	},
	zoomForMoveOn: function () {
		return this.zoomForRadius(this.MOVEON_RADIUS_MILES);
	},
	
	
	
	zoomMenu: function () {
		return this.view().getZoomMenu();
	},
	refreshZoomMenu: function () {
		var menu = this.zoomMenu();
		if (menu && this.hasCoords()) {
			this.refreshZoomMenuOnElement(menu, '|');
		}
	},
	
	refreshZoomMenuOnElement: function (element, final_separator) {
		if (element && element.isValid()) {
			var base_id = element.getId();
			var button_names = ['At', 'Local', 'Near'].reverse();
			var button_ids = [];

			var text = '';
			for (var ix=0; ix<button_names.length; ix++) {
				if (ix == 0) {
					if (final_separator) {
						text += '<div class="zoom">&nbsp;' + final_separator + '&nbsp;</div>'
					}
				} else {
					text += '<div class="zoom">&nbsp;|&nbsp;</div>'
				}
				button_ids.push(base_id + '_' + button_names[ix] + '_button');
				text +=
					'<div class="zoom pseudolink" id="' + button_ids[ix] + '">' +
					button_names[ix] + 
					'</div>';
			}
			text += '<div class="clearfloat"></div>'
			element.update(text);
			
			var observer = this;
			for (var ix=0; ix<button_names.length; ix++) {
				switch(button_names[ix]) {
					case 'At':
						$(button_ids[ix]).observe('click', function () {observer.zoomForAt();return false;});
						break;
					case 'Local':
						$(button_ids[ix]).observe('click', function () {observer.zoomForLocal();return false;});
						break;
					case 'Near':
						$(button_ids[ix]).observe('click', function () {observer.zoomForNear();return false;});
						break;
					case 'DayTrip':
						$(button_ids[ix]).observe('click', function () {observer.zoomForDayTrip();return false;});
						break;
					case 'MoveOn':
						$(button_ids[ix]).observe('click', function () {observer.zoomForMoveOn();return false;});
						break;
				} // end switch
			} // end for each button name
		} // end if element
	} // end refreshZoomMenuOnElement


}); // end ContinuousTraveler.Views.DBRecord


ContinuousTraveler.Views.DBRecord.prototype.AT_RADIUS_MILES = 0.25 * 1.4;
ContinuousTraveler.Views.DBRecord.prototype.LOCAL_RADIUS_MILES = 10.0 * 1.3;
ContinuousTraveler.Views.DBRecord.prototype.NEAR_RADIUS_MILES = 50.0 * 1.2;
ContinuousTraveler.Views.DBRecord.prototype.DAYTRIP_RADIUS_MILES = 100.0 * 1.1;
ContinuousTraveler.Views.DBRecord.prototype.MOVEON_RADIUS_MILES = 400.0 * 1.0;





////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Icons.icons = new Hash;





// Options:
//     type: dynamic-size-color
ContinuousTraveler.Views.Icons.create = function (options) {
	
	var icon = null;
	
	if (options) {
	 	switch(options.type) {
	 		case 'dynamic-size-color':
				icon = new ContinuousTraveler.Views.Icons.DynamicSizeColor(options);
				break;
	 		case 'flat-dynamic-size-color':
				icon = new ContinuousTraveler.Views.Icons.FlatDynamicSizeColor(options);
				break;
		}
	}
	
	return icon;
	
}; // end ContinuousTraveler.Views.Icons.create(options)




ContinuousTraveler.Views.Icons.Base = Class.create({

	initialize: function (options) {
		this.options = options;
		if (! this.options) {
			this.options = {};
		}
		this.googleicon = null;
	},
	destroy: function () {
		this.options = null;
		this.googleicon = null;
	},
	getType: function () {
		if (this.options) {
			return this.options.type;
		}
	},
	getOptions: function () {
		return this.options;
	},

	getMapIcon: function () {
		if (! this.googleicon) {
			this.googleicon = this.createMapIcon();
		}
		return this.googleicon;
	},
	
	getForgroundImageUrl: function () {
		var map_icon = this.getMapIcon();
		if (map_icon) {
			return map_icon.image;
		}
	},
	
	createMapIcon: function () {
		throw "ContinuousTraveler.Views.Icons.Base.createMapIcon(): Not implemented. Subclass responsibility";
	}
	
	
	
}); // end ContinuousTraveler.Views.Icons.Base



// http://googlegeodevelopers.blogspot.com/2008/08/mapiconmaker-11-create-dynamic-flat-and.html
//
// Options:
//     width:
//     height:
//     primaryColor:
//     strokeColor:
//     cornerColor:
//
ContinuousTraveler.Views.Icons.DynamicSizeColor = Class.create(ContinuousTraveler.Views.Icons.Base, {
	
	initialize: function ($super, options) {
		$super(options);
	},
	
	createMapIcon: function () {
		var options = this.getOptions();
		var icon = MapIconMaker.createMarkerIcon(options);
		return icon;
	}
	
}); // end ContinuousTraveler.Views.Icons.DynamicSizeColor


// http://googlegeodevelopers.blogspot.com/2008/08/mapiconmaker-11-create-dynamic-flat-and.html
//
// Options:
//     width:
//     height:
//     label:
//     labelSize:
//     labelColor:
//     shape: 'roundrect' or 'circle'
//     
//     
//
ContinuousTraveler.Views.Icons.FlatDynamicSizeColor = Class.create(ContinuousTraveler.Views.Icons.Base, {
	
	initialize: function ($super, options) {
		$super(options);
	},
	
	createMapIcon: function () {
		var options = this.getOptions();
		var icon = MapIconMaker.createFlatIcon(options);
		return icon;
	}
	
}); // end ContinuousTraveler.Views.Icons.DynamicSizeColor






ContinuousTraveler.Views.Icons.icons.set('Base',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#00ff00"
	})
);


ContinuousTraveler.Views.Icons.icons.set('locversion-primary',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#800000", 
		strokeColor: "#FFFFFF", 
		cornerColor: "#FFE4E1"
	})
);

ContinuousTraveler.Views.Icons.icons.set('locversion-secondary',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#FF0000", 
		strokeColor: "#FFE4E1", 
		cornerColor: "#000000"
	})
);


ContinuousTraveler.Views.Icons.icons.set('location-primary',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#800000", 
		strokeColor: "#FFFFFF", 
		cornerColor: "#FFE4E1"
	})
);


ContinuousTraveler.Views.Icons.icons.set('location-secondary',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#FF0000", 
		strokeColor: "#FFE4E1", 
		cornerColor: "#000000"
	})
);




ContinuousTraveler.Views.Icons.icons.set('viapoint-minimal',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#800000", 
		strokeColor: "#FFFFFF", 
		cornerColor: "#FFE4E1"
	})
);

ContinuousTraveler.Views.Icons.icons.set('viapoint-needloc',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#FFFFFF", 
		strokeColor: "#7F06FB", 
		cornerColor: "#C086FB"
	})
);

ContinuousTraveler.Views.Icons.icons.set('viapoint-passthru',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#1DBE11", 
		strokeColor: "#F8FE94", 
		cornerColor: "#FBFFE5"
	})
);

ContinuousTraveler.Views.Icons.icons.set('user-needloc',
	ContinuousTraveler.Views.Icons.create({
		type: 'dynamic-size-color',
		width: 32, height: 32, 
		primaryColor: "#FFFFFF", 
		strokeColor: "#7F06FB", 
		cornerColor: "#C086FB"
	})
);

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Lines.PolyLine = Class.create ({
	
	initialize: function (mapview, coord_array, options) {
		this.mapview = mapview;
		this.coord_list = [].concat(coord_array);

		if (! options) { options = {} };

		this.color = options.color;
		if (! this.color) { this.color = "#000000"; };

		this.weight = options.weight;
		if (! this.weight) { this.weight = 2; };
		
		this.opacity = options.opacity;
		if (! this.opacity) { this.opacity = 0.5; };
		
		this.clickable = true;
		if (! (options.clickable == undefined)) {
			this.clickable = options.clickable;
		}
		
		this.geodesic = false;
		if (! (options.geodesic == undefined)) {
			this.geodesic = options.geodesic;
		}
		
		this.add_arrowhead = false;
		if (! (options.add_arrowhead == undefined)) {
			this.add_arrowhead = options.add_arrowhead;
		}
		
		this.editable = false;
		if (! (options.editable == undefined)) {
			this.editable = options.editable;
		}
		
		this.edit_from_start = false;
		if (! (options.edit_from_start == undefined)) {
			this.edit_from_start = options.edit_from_start;
		}
		
		this.max_nodes = null;
		if (! (options.max_nodes == undefined)) {
			this.max_nodes = options.max_nodes;
		}

		this.googlepolyline = null;
		this.arrowhead = null;
		
		this.original_google_points = null;
	},
	
	destroy: function () {
		if (this.mapview) {
			if (this.googlepolyline) {
				if (this.editable) {
					this.googlepolyline.disableEditing();
				}
				this.mapview.removeObject(this.googlepolyline);
			}
		}
		if (this.arrowhead) {
			this.arrowhead.destroy();
			this.arrowhead = null;
		}
		this.original_google_points = null;
		this.googlepolyline = null;
		this.mapview = null;
		this.coord_list = null;
	},
	
	isValid: function () {
		return this._local_isValid();
	},
	
	_local_isValid: function () {
		return (
			this.mapview &&
			this.coord_list &&
			(this.coord_list.length > 1)
		);
	},
	
	view: function () {
		return this.mapview;
	},
	
	vertices: function () {
		this.coord_list.length;
	},
	
	
	getOriginalVertices: function () {
		return [].concat(this.coord_list);
	},
	
	
	getCurrentVertices: function () {
		var list = null;
		
		if (! (this.googlepolyline && this.editable)) {
			list = this.getOriginalVertices();
		} else {
			var count = this.googlepolyline.getVertexCount();
			if (count > 0) {
				list = [];
				for (var ix=0; ix< count; ix++) {
					var pnt = this.googlepolyline.getVertex(ix);
					var coords = ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint(pnt);
					list.push(coords);
				}
			}
		}
		
		return list;
	}, // getCurrentVertices
	
	
	
	getVertices: function () {
		return this.getCurrentVertices();
	},
	
	
	
	
	getCurrentMapVertices: function () {
		var list = null;
		if (this.googlepolyline) {
			var count = this.googlepolyline.getVertexCount();
			list = [];
			for (var ix=0; ix<count; ix++) {
				list.push(this.googlepolyline.getVertex(ix));
			}
		} else {
			list = this.getOriginalMapVertices();
		}
		return list;
	},
	
	getOriginalMapVertices: function () {
		if (! this.original_google_points) {
			this.original_google_points = [];
			var len = this.coord_list.length;
			for (var ix=0; ix<len; ix++) {
				this.original_google_points.push(this.coord_list[ix].getMapPoint());
			}
		}
		return this.original_google_points;
	},
	
	
	getChangedVertex: function () {
		var change = null;
		
		if (this.editable && this.googlepolyline && this.original_google_points) {
			var original_count = this.original_google_points.length;
			var current_count = this.googlepolyline.getVertexCount();
			var delta = current_count - original_count;
			var count = Math.max(original_count, current_count);
			
			var original = null;
			var current = null;
			var index = 0;

			for (var ix=0; ix<count; ix++) {
				index = ix;
				original = (ix<original_count ? this.original_google_points[ix] : null);
				current = (ix<current_count ? this.googlepolyline.getVertex(ix) : null);
				
				if ( (current && original) && original.equals(current) ) {
					original = null;
					current = null;
				} else {
					break;
				}
				
			} // end for each node
			
			if (current || original) {
				var original_coord = (original ? ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint(original) : null);
				var current_coord = (current ? ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint(current) : null);
				change = {
					count: current_count,
					delta: delta,
					index: index,
					original: original_coord,
					current: current_coord
				};
			}
		}
		
		return change;
	}, // getChangedVertex
	

	
	getMapVertices: function () {
		return this.getCurrentMapVertices();
	},
	


	addListener: function (event_name, callback_function) {
		if (this.googlepolyline && event_name && callback_function) {
			return google.maps.Event.addListener(this.googlepolyline, event_name, callback_function);
		}
	},
	removeListener: function (listener) {
		if (listener) {
			google.maps.Event.removeListener(listener);
		}
	},


	
	show: function () {
		if (this._local_isValid()) {
			if (! this.googlepolyline) {
				this.googlepolyline = new google.maps.Polyline(
					this.getOriginalMapVertices(),
					this.color,
					this.weight,
					this.opacity,
					{
						clickable: this.clickable,
						geodesic: this.geodesic
					}
				);
				this.mapview.addObject(this.googlepolyline);
				
				if (this.editable) {
					this.googlepolyline.enableEditing({
						fromStart: this.edit_from_start,
						maxVertices: this.max_nodes
					});
				}
				
				if (this.add_arrowhead) {
					this.arrowhead = new ContinuousTraveler.Views.Lines.ArrowHead(
						this.mapview,
						this.coord_list[this.coord_list.length-2],
						this.coord_list[this.coord_list.length-1],
						{
							color: this.color,
							weight: this.weight,
							opacity: this.opacity,
							clickable: this.clickable,
							geodesic: this.geodesic,
							add_arrowhead: false
						}
					);
					this.arrowhead.show();
				}
			} else {
				this.googlepolyline.show();
				if (this.arrowhead) {
					this.arrowhead.show();
				}
			}
		}
	},
	hide: function () {
		if (this.googlepolyline) {
			this.googlepolyline.hide();
			if (this.arrowhead) {
				this.arrowhead.hide();
			}
		}
	},
	isVisible: function () {
		return (
			this.googlepolyline &&
			(! this.googlepolyline.isHidden())
		);
	}

	
	
}); // end ContinuousTraveler.Views.Lines.PolyLine

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Lines.ArrowHead = Class.create ({
	
	initialize: function (mapview, from_coord, to_coord, options) {
		this.mapview = mapview;
		this.polyline = this.createLine(from_coord, to_coord, options);
	},
	
	destroy: function () {
		if (this.polyline) {
			this.polyline.destroy();
			this.polyline = null;
		}
		this.mapview = null;
	},
	
	isValid: function () {
		return this._local_isValid();
	},
	
	_local_isValid: function () {
		return (
			this.mapview &&
			this.polyline
		);
	},
	
	view: function () {
		return this.mapview;
	},
	
	getPolyline: function () {
		return this.polyline;
	},
	
	show: function () {
		if (this._local_isValid()) {
			this.polyline.show();
		}
	},
	hide: function () {
		if (this._local_isValid()) {
			this.polyline.hide();
		}
	},
	isVisible: function () {
		if (this._local_isValid()) {
			return this.polyline.isVisible();
		}
	},
	
	
	createLine: function (from_coord, to_coord, options) {
		var heading = to_coord.headingDegrees(from_coord);
		var left_heading = (heading + this.OFFSET_ANGLE) % 360;
		var right_heading = (360 + (heading - this.OFFSET_ANGLE)) % 360;
		
		var left_point = to_coord.translateMiles(left_heading, this.BARB_MILES);
		var right_point = to_coord.translateMiles(right_heading, this.BARB_MILES);
		var coord_list = [ left_point, to_coord, right_point ];
		
		var line = new ContinuousTraveler.Views.Lines.PolyLine(
			this.mapview,
			coord_list,
			options
		);
		
		return line;
	}

	
	
}); // end ContinuousTraveler.Views.Lines.ArrowHead

ContinuousTraveler.Views.Lines.ArrowHead.prototype.OFFSET_ANGLE = 30.0;
ContinuousTraveler.Views.Lines.ArrowHead.prototype.BARB_MILES = 10.0;

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Lines.Directions = Class.create ({
	
	initialize: function (mapview, coord_array, options) {
		this.mapview = mapview;
		this.coord_list = [].concat(coord_array);

		if (! options) { options = {} };
		this.options = options;
		
		this.googledirections = null;
		this.googlepolyline = null;
	},
	
	destroy: function () {
		this.hide();
		this.mapview = null;
		this.coord_list = null;
		this.options = null;
	},
	
	isValid: function () {
		return this._local_isValid();
	},
	
	_local_isValid: function () {
		return (
			this.mapview &&
			this.coord_list
		);
	},
	
	view: function () {
		return this.mapview;
	},
	
	show: function () {
		if (this._local_isValid && (! this.googledirections)) {
			// Do NOT let directions object display line itself: it adds markers.
			// So: do NO pass map arg; DO get polyline and add to map.
			this.googledirections = new google.maps.Directions();
			
			var observer = this;
			this.load_listener = this.addListener(
				'load',
				function () { observer.loadListener(); }
			);
			var observer = this;
			this.error_listener = this.addListener(
				'error',
				function () { observer.errorListener(); }
			);

			var endpoints = [];
			for (var ix=0; ix<this.coord_list.length; ix++) {
				endpoints.push(this.coord_list[ix].getMapPoint())
			}
			this.googledirections.loadFromWaypoints(
				endpoints,
				{
					getPolyline: true,
					preserveViewport: true
				}
			);
		}
	},
	
	hide: function () {
		if (this.load_listener) {
			this.removeListener(this.load_listener);
			this.load_listener = null;
		}
		if (this.error_listener) {
			this.removeListener(this.error_listener);
			this.error_listener = null;
		}
		if (this.googlepolyline) {
			this.mapview.removeObject(this.googlepolyline);
			this.googlepolyline = null;
		}
		if (this.googledirections) {
			this.googledirections.clear();
			this.googledirections = null;
		}
	},
	
	addListener: function (event_name, callback_function) {
		if (this.googledirections && event_name && callback_function) {
			return google.maps.Event.addListener(this.googledirections, event_name, callback_function);
		}
	},
	removeListener: function (listener) {
		if (listener) {
			google.maps.Event.removeListener(listener);
		}
	},
	
	loadListener: function () {
		if (this.googledirections) {
			this.googlepolyline = this.googledirections.getPolyline();
			if (this.googlepolyline) {
				this.mapview.addObject(this.googlepolyline);
			}
		}
	},
	
	errorListener: function () {
		if (this.googledirections) {
			var status = this.googledirections.getStatus();
			if (status)
			{
				this.mapview.warningMessage('Could not get directions. Unable to compute route.');
			}
		}
	}
	
}); // end ContinuousTraveler.Views.Lines.Directions

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Markers.Base = Class.create ({
	
	initialize: function (mapview, options) {
		this._reset();
		this.mapview = mapview;
		this.marker_options = options;
		if (! this.marker_options) {
			this.marker_options = {
				draggable: false
			};
		}
	},
	
	destroy: function () {
		this._reset();
	},
	_reset: function () {
		this.removeFromMap();
		this.icon = null;
		this.mapview = null;
		this.marker_options = null;
		this.googlemarker = null;
		this.html = null;
		this.point = null;
		this.is_visible = false;
		this.is_dirty = false;
		this.label_top_offset = 0;
		this.label_left_offset = 0;
		this.rollover_image_url = 0;
		this.is_mouse_over = false;
	},
	
	isValid: function () {
		return this._local_IsValid();
	},
	_local_IsValid: function () {
		return (
			(this.mapview != null) &&
			(
				(! this.is_visible) ||
				(this.googlemarker && this.point)
			)
		);
	},
	
	view: function () {
		return this.mapview;
	},
	map: function () {
		if (this.mapview) {
			return this.mapview.map();
		}
	},
	
	setRolloverImageUrl: function (url) {
		this.rollover_image_url = url;
	},
	isMouseOver: function () {
		return this.is_mouse_over;
	},
	clearMouseOver: function () {
		this.is_mouse_over = false;
	},
	
	setLabel: function (text) {
		if (this.marker_options.labelText != text) {
			this.marker_options.labelText = text;
			this.is_dirty = true;
		}
	},
	getLabel: function () {
		return this.marker_options.labelText;
	},
	
	setLabelClass: function (css_class) {
		this.marker_options.labelClass = css_class;
		this.is_dirty = true;
	},
	
	setLabelOffset: function (pixels_from_top, pixels_from_left) {
		this.label_top_offset = pixels_from_top;
		this.label_left_offset = pixels_from_left;
		this.is_dirty = true;
	},
	
	setHtml: function (text) {
		if (this.html != text) {
			this.html = text;
			this.is_dirty = true;
		}
	},
	getHtml: function () {
		return this.html;
	},
	
	setTitle: function (text) {
		if (this.marker_options.title != text) {
			this.marker_options.title = text;
			this.is_dirty = true;
		}
	},
	getTitle: function () {
		return this.marker_options.title;
	},
	
	enableDragging: function () {
		this.marker_options.draggable = true;
		if (this.googlemarker) {
			this.googlemarker.enableDragging();
		} else {
			this.is_dirty = true;
		}
	},
	disableDragging: function () {
		this.marker_options.draggable = false;
		if (this.googlemarker) {
			this.googlemarker.disableDragging();
		} else {
			this.is_dirty = true;
		}
	},
	isDraggable: function () {
		return this.marker_options.draggable;
	},
	
	setPoint: function (point) {
		if (! (this.point && this.point.equals(point))) {
			this.point = point;
			if (this.googlemarker) {
				this.googlemarker.setPoint(this.point);
			}
		}
	},
	setPointLatLong: function (latitude, longitude) {
		this.setPoint(new google.maps.LatLng(latitude, longitude, true));
	},
	getPoint: function () {
		if (this.googlemarker) {
			this.point = this.googlemarker.getPoint();
		}
		return this.point;
	},
	setCoords: function (coords) {
		this.setPointLatLong(coords.latitude, coords.longitude);
	},
	getCoords: function () {
		return ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint(this.getPoint());
	},

	setIcon: function (icon) {
		this.icon = icon;
		this.is_dirty = true;
	},
	
	getIcon: function () {
		return this.icon;
	},
	
	setLabelClass: function (css_class) {
		if (this.marker_options.labelClass != css_class) {
			this.marker_options.labelClass = css_class;
			this.is_dirty = true;
		}
	},
	getLabelClass: function () {
		return this.marker_options.labelClass;
	},

	
	setTracker: function (options) {
		this.tracker_options = options;
		if (! this.tracker_options) {
			this.tracker_options = {}
		}
		if (! this.tracker_options.iconScale) {
			this.tracker_options.iconScale = 0.6;
		}
		if (! this.tracker_options.padding) {
			this.tracker_options.padding = 25;
		}
		if (! this.tracker_options.color) {
			this.tracker_options.color =  '#ff0000';
		}
		if (! this.tracker_options.weight) {
			this.tracker_options.weight = 10;
		}
		if (! this.tracker_options.length) {
			this.tracker_options.length = 20;
		}
		if (! this.tracker_options.opacity) {
			this.tracker_options.opacity = 0.8;
		}
		if (! this.tracker_options.updateEvent) {
			this.tracker_options.updateEvent = 'moveend';
		}
		if (! this.tracker_options.panEvent) {
			this.tracker_options.panEvent = 'click';
		}
		if (! this.tracker_options.quickPanEnabled) {
			this.tracker_options.quickPanEnabled = true;
		}
		this.is_dirty = true;
	},
	clearTracker: function () {
		this.tracker_options = null;
		this.googlemarker_tracker = null;
		this.is_dirty = true;
	},
	
	
	
	
	needRefresh: function () {
		return (this._local_IsValid() && (this.is_dirty || (! this.is_visible)));
	},
	isDirty: function () {
		return this.is_dirty;
	},
	isVisible: function () {
		return this.is_visible;
	},
	show: function () {
		if (this._local_IsValid() && this.point && this.needRefresh()) {
			if (this.is_dirty || (! this.googlemarker)) {
				this.removeFromMap(); // Cleanup any previous display
				
				if (! this.marker_options) {
					this.marker_options = {};
				}

				if (! this.icon) {
					this.icon = ContinuousTraveler.Views.Icons.icons.get('Base').getMapIcon();
				}
				this.marker_options.icon = this.icon.getMapIcon();

				// Cannot be BOTH labelled and draggable at the same time. Give preference
				// to dragging for editing purposes.
				if (this.marker_options.labelText && (! this.marker_options.draggable)) {
					if (! this.marker_options.labelClass) {
						this.marker_options.labelClass = "LabeledMarker_markerLabel";
					}
					var x_off = (this.label_left_offset - this.marker_options.icon.iconSize.width);
					var y_off = (this.label_top_offset - this.marker_options.icon.iconSize.height);

					this.marker_options.labelOffset = new GSize(x_off, y_off);

					this.googlemarker = new LabeledMarker(this.point, this.marker_options);
				} else {
					this.googlemarker = new google.maps.Marker(this.point, this.marker_options);
				}
			}
			if (this.googlemarker) {
				this.view().addObject(this.googlemarker);
				if (this.html) {
					var observer = this;
					this.addListener("click", function (overlay, latlng) { observer.onClick(overlay, latlng); });
					this.addListener("infowindowopen", function () { observer.onInfoWindowOpen(); });
					this.addListener("infowindowclose", function () { observer.onInfoWindowClose(); });
				}
				if (this.tracker_options) {
					this.googlemarker_tracker = new MarkerTracker(this.googlemarker, this.map(), this.tracker_options);
				}
				if (this.rollover_image_url) {
					this.is_mouse_over = false;
					var observer = this;
					this.addListener("mouseover", function (latlng) { observer.onMouseOver(latlng); });
					this.addListener("mouseout", function (latlng) { observer.onMouseOut(latlng); });
				}
				this.is_visible = true;
				this.is_dirty = false;
			}
		}
	}, // end show
	
	onMouseOver: function (latlng) {
		if ((! this.is_mouse_over) && this.rollover_image_url && this.googlemarker) {
			this.is_mouse_over = true;
			this.googlemarker.setImage(this.rollover_image_url);
		}
	},
	onMouseOut: function (latlng) {
		if (this.googlemarker) {
			var original_image = this.googlemarker.getIcon().image;
			this.googlemarker.setImage(original_image);
			this.is_mouse_over = false;
		}
	},
	
	hide: function () {
		if (this._local_IsValid() && this.is_visible) {
			this.googlemarker_tracker = null;
			this.view().removeObject(this.googlemarker);
			this.is_visible = false;
		}
	},
	
	removeFromMap: function () {
		this.googlemarker_tracker = null;
		this.tracker_options = null;
		if (this.info_window) {
			this.info_window.destroy();
			this.info_window = null;
		}
		if (this.googlemarker) {
			google.maps.Event.clearInstanceListeners(this.googlemarker);
			this.view().removeObject(this.googlemarker);
			this.googlemarker = null;
		}
	},
	
	refresh: function () {
		this.show();
	},
	
	render: function () {
		this.show();
	},
	
	
	addListener: function (event_name, callback_function) {
		if (this.googlemarker && event_name && callback_function) {
			return google.maps.Event.addListener(this.googlemarker, event_name, callback_function);
		}
	},
	removeListener: function (listener) {
		if (listener) {
			google.maps.Event.removeListener(listener);
		}
	},
	
	
	infoWindow: function () {
		return this.info_window;
	},
	isInfoWindowOpen: function () {
		return (this.info_window != null);
	},
	openInfoWindow: function () {
		if (! this.info_window && this.googlemarker && this.html) {
			var infowindow_html = ContinuousTraveler.Views.InfoWindow.prototype.createHtml(this.html)
			this.googlemarker.openInfoWindowHtml(infowindow_html);
		}
	},
	
	onClick: function (overlay, latlng) {
		this.openInfoWindow();
	},
	onInfoWindowOpen: function () {
		if (this.map()) {
			this.info_window = new ContinuousTraveler.Views.InfoWindow(this.map().getInfoWindow());
			if (this.info_window && ! this.info_window.isValid()) {
				this.info_window.destroy();
				this.info_window = null;
			}
		}
	},
	onInfoWindowClose: function () {
		if (this.info_window) {
			this.info_window.destroy();
			this.info_window = null;
		}
	},
	
	
	
	comment_Html: function (text, want_div) {
		if (text) {
			if (want_div) {
				return this.TEMPLATE_COMMENT_DIV.evaluate({ text: text });
			} else {
				return this.TEMPLATE_COMMENT.evaluate({ text: text });
			}
		}
	},
	unknown_Html: function (want_div) {
		return this.comment_Html('Unknown', want_div);
	},
	
	instructions_Html: function (text) {
		return this.TEMPLATE_INSTRUCTIONS.evaluate({ text: text });
	},


	
	street_Html: function (street_address_value) {
		if (street_address_value) {
			return this.TEMPLATE_STREET.evaluate({street: street_address_value});
		}
	},
	poBoxSuiteApt_Html: function (pobox_suite_apt_address_value) {
		if (pobox_suite_apt_address_value) {
			return this.TEMPLATE_POBOX_SUITE_APT.evaluate({pobox_suite_apt: pobox_suite_apt_address_value});
		}
	},
	cityStatePostal_Html: function (city_state_postal_attributes) {
		if (city_state_postal_attributes) {
			return this.TEMPLATE_CITY_STATE_POSTAL.evaluate(city_state_postal_attributes);
		}
	},
	address_Html: function (address_attributes) {
		if (address_attributes) {
			var street = this.street_Html(address_attributes.street);
			var pobox = this.poBoxSuiteApt_Html(address_attributes.pobox_suite_apt);
			
			var street_lines = null;
			if (street) {
				street_lines = street;
			}
			if (pobox) {
				if (street_lines) {
					street_lines += '<br />' + pobox;
				} else {
					street_lines = pobox;
				}
			}
			if (! street_lines) {
				street_lines = this.TEMPLATE_COMMENT.evaluate({
					text: 'Street address unknown'
				});
			}
			var city = this.cityStatePostal_Html(address_attributes);
			if (! city) {
				city = this.TEMPLATE_COMMENT.evaluate({
					text: 'City, state/province unknown'
				});
			}
			var address = this.TEMPLATE_ADDRESS.evaluate({
				address_lines: street_lines,
				city_state_postal: city
			});
			
			return address;
		}
	}, // end address_Html
	
	
	
	localPhone_Html: function (local_phone_value) {
		if (local_phone_value) {
			return this.TEMPLATE_PHONE_LOCAL.evaluate({phone_local: local_phone_value});
		}
	},
	
	tollFreePhone_Html: function (tollfree_phone_value) {
		if (tollfree_phone_value) {
			return this.TEMPLATE_PHONE_TOLLFREE.evaluate({phone_tollfree: tollfree_phone_value});
		}
	},
	
	faxPhone_Html: function (fax_phone_number_value) {
		if (fax_phone_number_value) {
			return this.TEMPLATE_PHONE_FAX.evaluate({fax: fax_phone_number_value});
		}
	},
	
	
	phones_Html: function (phone_attributes) {
		if (phone_attributes) {
			var local = this.localPhone_Html(phone_attributes.phone_local);
			var tollfree = this.tollFreePhone_Html(phone_attributes.phone_tollfree);
			var fax = this.faxPhone_Html(phone_attributes.fax);
			
			var html = null;
			if (local) {
				html = local;
			}
			if (tollfree) {
				if (html) {
					html += '<br />' + tollfree
				} else {
					html = tollfree;
				}
			}
			if (fax) {
				if (html) {
					html += '<br />' + fax
				} else {
					html = fax;
				}
			}
			if (! html) {
				html = this.unknown_Html();
			}
			return this.TEMPLATE_PHONES.evaluate({ phones: html });
		}
	}, // end phones_Html
	
	
	
	
	
	email_Html: function (email_value) {
		if (email_value) {
			return this.TEMPLATE_EMAIL.evaluate({ email: email_value });
		}
	},
	website_Html: function (website_value) {
		if (website_value) {
			return this.TEMPLATE_WEBSITE.evaluate({ website: website_value });
		}
	},
	internet_Html: function (internet_attributes) {
		if (internet_attributes) {
			var website = this.website_Html(internet_attributes.website);
			var email = this.email_Html(internet_attributes.email);

			var html = null;
			if (website) {
				html = website;
			}
			if (email) {
				if (html) {
					html += '<br />' + email;
				} else {
					html = email;
				}
			}
			if (! html) {
				html = this.unknown_Html();
			}
			return this.TEMPLATE_INTERNET.evaluate({ internet: html });
		}
	}, // end internet_Html
	
	
	contactInfo_Html: function (contact_info_attributes) {
		if (contact_info_attributes) {
			var address = this.address_Html(contact_info_attributes);
			var phones = this.phones_Html(contact_info_attributes);
			var internet = this.internet_Html(contact_info_attributes);
			
			var html = null;
			if (address) {
				html = address;
			}
			if (phones) {
				if (html) {
					html += phones;
				} else {
					html = phones;
				}
			}
			if (internet) {
				if (html) {
					html += internet;
				} else {
					html = internet;
				}
			}
			if (! html) {
				html = this.unknown_Html();
			}
			return html;
		}
	} // end contactInfo_Html
	
	


}); // end ContinuousTraveler.Views.Markers.Base



ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_COMMENT = new Template(
	'<span class="comment">(#{text})</span>'
);

ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_COMMENT_DIV = new Template(
	'<div class="comment">(#{text})</div>'
);

ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_INSTRUCTIONS = new Template(
	'<div class="instructions">#{text}</div>'
);

ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_STREET = new Template(
	'<span class="subline"><span class=" location street">#{street}</span></span>'
);

ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_POBOX_SUITE_APT = new Template(
	'<span class="subline"><span class=" location pobox-suite-apt">#{pobox_suite_apt}</span></span>'
);

ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_CITY_STATE_POSTAL = new Template(
	'<span class="subline"><span class=" location where">' +
		'<span class="subfield"><span class=" location city">#{city}</span></span>, ' +
		'<span class="subfield"><span class=" location state-province">#{state_province}</span></span> ' + 
		'<span class="subfield"><span class=" location postal-code">#{postal_code}</span></span> ' +
		'<span class="subfield"><span class=" location country">#{country_abbr}</span></span>' +
	'</span></span>'
);

ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_ADDRESS = new Template(
	'<div class="item">' +
		'<label>Address:</label>' +
		'<div class="subitem location address">' +
			'#{address_lines}<br />' +
			'#{city_state_postal}' +
		'</div>' +
	'</div>'
);


ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_PHONE_LOCAL = new Template(
	'<span class="subline"><label>Local:</label> <span class=" location phone-local">#{phone_local}</span></span>'
);
ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_PHONE_TOLLFREE = new Template(
	'<span class="subline"><label>Free:</label> <span class=" location phone-tollfree">#{phone_tollfree}</span></span>'
);
ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_PHONE_FAX = new Template(
	'<span class="subline"><label>Fax:</label> <span class=" location fax">#{fax}</span></span>'
);
ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_PHONES = new Template(
	'<div class="item">' +
		'<label>Phones:</label>' +
		'<div class="subitem location phones">' +
			'#{phones}' +
		'</div>' +
	'</div>'
);

ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_WEBSITE = new Template(
	'<span class="subline"><label>Web Site:</label> <span class=" location website"><a href="#{website}" target="_blank">#{website}</a></span></span>'
);
ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_EMAIL = new Template(
	'<span class="subline"><label>EMail:</label> <span class=" location email"><a href="mailto:#{email}">#{email}</a></span></span>'
);
ContinuousTraveler.Views.Markers.Base.prototype.TEMPLATE_INTERNET = new Template(
	'<div class="item">' +
		'<label>Internet:</label>' +
		'<div class="subitem location internet">' +
			'#{internet}' +
		'</div>' +
	'</div>'
);

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Markers.RecBase = Class.create (ContinuousTraveler.Views.Markers.Base, {
	
	initialize: function ($super, db_rec, options) {
		$super(db_rec.view(), options);
		this.record = db_rec;
	},
	
	destroy: function ($super) {
		this.record = null;
		$super();
	},
	
	isValid: function ($super) {
		return ( $super() && this.record );
	},
	
	getRecord: function () {
		return this.record;
	},
	
	show: function ($super) {
		this.setPointLatLong(this.record.getLatitude(), this.record.getLongitude());
		$super();
	},

	
	onInfoWindowOpen: function ($super) {
		$super();
		var info_window = this.infoWindow();
		if (info_window) {
			var div = info_window.container();
			var menulist = div.getDomElement().getElementsByClassName('zoom');
			if (menulist && (menulist.length>0)) {
				var elt = new ContinuousTraveler.Views.Element(menulist[0]);
				this.record.refreshZoomMenuOnElement(elt, null);
			}
		}
	},
	
	setInfoWindowBody_Html: function (attributes) {
		if (attributes) {
			var html = this.TEMPLATE_INFO_WINDOW.evaluate(attributes);
			this.setHtml(html);
		}
	} // setInfoWindowBody
	
}); // ContinuousTraveler.Views.Markers.RecBase


ContinuousTraveler.Views.Markers.RecBase.prototype.TEMPLATE_INFO_WINDOW = new Template(
	'<div class="title">#{title}</div>' +
	'<div class="clearfloat">&nbsp;</div>' +
	'<div>#{body}</div>' +
	'<div class="clearfloat">&nbsp;</div>' +
	'<div class="menu">' +
		'<div class="commands">#{commands}</div>' +
		'<div class="zoom">#{items}</div>' +
		'<div class="clearfloat">&nbsp;</div>' +
	'</div>'
);


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Markers.LocversionPrimary = Class.create (ContinuousTraveler.Views.Markers.RecBase, {
	
	initialize: function ($super, locversion_record, options) {
		$super(locversion_record, options);
		this.setTracker({});
		var icon = ContinuousTraveler.Views.Icons.icons.get('locversion-primary');
		this.setIcon(icon);
		this.setLabelClass('locversion-primary-marker-label');
	},
	
	destroy: function ($super) {
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	}
	
}); // ContinuousTraveler.Views.Markers.LocversionPrimary




ContinuousTraveler.Views.Markers.LocversionSecondary = Class.create (ContinuousTraveler.Views.Markers.RecBase, {
	
	initialize: function ($super, locversion_record, options) {
		$super(locversion_record, options);
		// this.setTracker({});
		var icon = ContinuousTraveler.Views.Icons.icons.get('locversion-secondary');
		this.setIcon(icon);
		this.setLabelClass('locversion-secondary-marker-label');
	},
	
	destroy: function ($super) {
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	}
	
}); // ContinuousTraveler.Views.Markers.LocversionSecondary

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Markers.LocationPrimary = Class.create (ContinuousTraveler.Views.Markers.RecBase, {
	
	initialize: function ($super, location_record, options) {
		$super(location_record, options);
		//this.setTracker({});
		var icon = ContinuousTraveler.Views.Icons.icons.get('location-primary');
		this.setIcon(icon);
		this.setLabelClass('location-primary-marker-label');
		this.setLabelOffset(2, 1);
	},
	
	destroy: function ($super) {
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	}
	
}); // ContinuousTraveler.Views.Markers.LocationPrimary




ContinuousTraveler.Views.Markers.LocationSecondary = Class.create (ContinuousTraveler.Views.Markers.RecBase, {
	
	initialize: function ($super, location_record, options) {
		$super(location_record, options);
		// this.setTracker({});
		var icon = ContinuousTraveler.Views.Icons.icons.get('location-secondary');
		this.setIcon(icon);
		this.setLabelClass('location-secondary-marker-label');
		this.setLabelOffset(2, 1);
		
/* 		icon = ContinuousTraveler.Views.Icons.icons.get('location-secondary-rollover');
		this.setRolloverImageUrl(icon.getForgroundImageUrl()); */
	},
	
	destroy: function ($super) {
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	}
	
}); // ContinuousTraveler.Views.Markers.LocationSecondary

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Markers.ViaBase = Class.create (ContinuousTraveler.Views.Markers.RecBase, {
	
	initialize: function ($super, viapoint_record, options) {
		$super(viapoint_record, options);
		this.line_from = null;
	},
	
	destroy: function ($super) {
		this.clearLine();
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	},
	
	clearLine: function () {
		if (this.line_from) {
			this.line_from.destroy();
			this.line_from = null;
		}
	},
	
	populate: function () {
		if (! this.getLabel()) {
			var rec = this.getRecord();
			this.setLabel(rec.getPosition().toString());
		}
	},
	
	
	cityState_Html: function () {
		var rec = this.getRecord();
		var html = this.TEMPLATE_CITY_STATE_POSTAL.evaluate({
			city: rec.get('city'),
			state_province: rec.get('state_province'),
			postal_code: rec.get('postal_code'),
			country_abbr: rec.get('country_abbr')
		});
		return html;
	},
	
	
	cityStatePopupTitle: function () {
		var rec = this.getRecord();
		var str = rec.getPosition().toString() + '. ' +
			rec.get('city') + ', ' +
			rec.get('state_province') + ', ' +
			rec.get('country_abbr')
		;
		return str;
	}
	
	
}); // ContinuousTraveler.Views.Markers.ViaBase





ContinuousTraveler.Views.Markers.ViapointMinimal = Class.create (ContinuousTraveler.Views.Markers.ViaBase, {
	
	initialize: function ($super, viapoint_record, options) {
		$super(viapoint_record, options);
		this.setTracker({});
		var icon = ContinuousTraveler.Views.Icons.icons.get('viapoint-minimal');
		this.setIcon(icon);
		this.setLabelClass('viapoint-minimal-marker-label');
		this.setLabelOffset(2, 1);
	},
	
	destroy: function ($super) {
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	}
	
}); // ContinuousTraveler.Views.Markers.ViapointMinimal


// Is stopover, but location of stopover is unknown
ContinuousTraveler.Views.Markers.ViapointNeedLocation = Class.create (ContinuousTraveler.Views.Markers.ViaBase, {
	
	initialize: function ($super, viapoint_record, options) {
		$super(viapoint_record, options);
		this.setTracker({});
		var icon = ContinuousTraveler.Views.Icons.icons.get('viapoint-needloc');
		this.setIcon(icon);
		this.setLabelClass('viapoint-needloc-marker-label');
		this.setLabelOffset(2, 1);
	},
	
	destroy: function ($super) {
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	},
	
	populate: function ($super) {
		$super();
		var rec = this.getRecord();
		
		var edit_commands = '';
		var html = this.comment_Html('RV park not yet selected', true);
		html += this.cityState_Html();
		
		if (rec.isEditing()) {
			html += this.instructions_Html(
				'<p>Drag this icon to the RV park where you want to stay.</p>' +
				'<p>Drag this icon to a place away from any RV park icons to change stopover location.</p>'
			);
			edit_commands += rec.becomePassThrough_Button_Html();
			edit_commands += rec.delete_Button_Html();
		}
		
		this.setInfoWindowBody_Html({
			title: 'Stopover Viapoint',
			body: html,
			commands: edit_commands
		});
		this.setTitle(this.cityStatePopupTitle());
	}
	
}); // ContinuousTraveler.Views.Markers.ViapointNeedLocation





// Not a stopover. Just passing through. Used to specify a route without specifying a stopping place.
ContinuousTraveler.Views.Markers.ViapointPassingThrough = Class.create (ContinuousTraveler.Views.Markers.ViaBase, {
	
	initialize: function ($super, viapoint_record, options) {
		$super(viapoint_record, options);
		this.setTracker({});
		var icon = ContinuousTraveler.Views.Icons.icons.get('viapoint-passthru');
		this.setIcon(icon);
		this.setLabelClass('viapoint-passthru-marker-label');
		this.setLabelOffset(2, 1);
	},
	
	destroy: function ($super) {
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	},
	
	
	populate: function ($super) {
		$super()
		var rec = this.getRecord();
		
		var edit_commands = '';
		var html = this.comment_Html('Not stopping here', true);
		html += this.cityState_Html();
		
		if (rec.isEditing()) {
			html += this.instructions_Html(
				'<p>Drag this icon to change viapoint position.</p>'
			);
			edit_commands += rec.becomeStopover_Button_Html();
			edit_commands += rec.delete_Button_Html();
		}
		
		this.setInfoWindowBody_Html({
			title: 'Pass-Through Viapoint',
			body: html,
			commands: edit_commands
		});
		this.setTitle(this.cityStatePopupTitle());
	}
	
	
}); // ContinuousTraveler.Views.Markers.ViapointPassingThrough






////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Markers.UserNeedsLocation = Class.create (ContinuousTraveler.Views.Markers.RecBase, {
	
	initialize: function ($super, user_record, options) {
		$super(user_record, options);
		this.setTracker({});
		var icon = ContinuousTraveler.Views.Icons.icons.get('user-needloc');
		this.setIcon(icon);
		this.setLabelClass('user-needloc-marker-label');
		this.setLabelOffset(2, 1);
	},
	
	
	populate: function ($super) {
		var rec = this.getRecord();
		
		var edit_commands = '';
		var html = this.comment_Html('RV park not yet selected', true);
		
		if (rec.isEditing()) {
			html += this.instructions_Html(
				'<p>Drag this icon to the RV park that is your Home Base.</p>' +
				'<p>Drag this icon to a place away from any RV park icons to Home Base location.</p>'
			);
		} else {
			html += this.instructions_Html(
				'<p>Click the Edit button, above, to set your Home Base.</p>' +
				'<p>Home Base is used as the default starting point for<br />any new trips that you create.</p>'
			);
		}
		
		this.setInfoWindowBody_Html({
			title: 'Home Base',
			body: html,
			commands: edit_commands
		});
		this.setTitle('Home Base');
	}
	
	
}); // ContinuousTraveler.Views.Markers.UserNeedsLocation



////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.LocRecordBase = Class.create (ContinuousTraveler.Views.DBRecord, {
	
	initialize: function ($super, mapview, options) {
		$super(mapview, options);
		this.position = null;
		this.role = null;
		this.marker_command_html = null;
		this.setPrimaryViewData(true);
	},
	
	destroy: function ($super) {
		this.marker_command_html = null;
		this.position = null;
		this.role = null;
		$super();
	},
	
	getPosition: function () {
		return this.position;
	},
	setPosition: function (pos) {
		this.position = pos;
	},
	
	getRole: function () {
		return this.role;
	},
	setRole: function (role) {
		this.role = role;
	},
	
	update: function ($super, user_request, result_data) {
		if (this.isValid()) {
			var action = user_request['act'];
			switch (action) {
				case 'show':
					this.update_show(result_data);
					break;
				case 'nearby':
					this.update_nearby(result_data);
					break;
			}; // end switch (action)
		}
	}, // end update
	
	
	update_show: function (data) {
		var is_changed = this.applyChanges(data);
		if (is_changed) {
			this.show();
			//this.zoomForLocal();
			this.refreshZoomMenu();
			//this.requestNearByLocations(0, this.NEAR_RADIUS_MILES);
		}
	},
	
	
	
	excludeNearByRecordIds: function () {
		var exclude_ids = this.getSecondaryRecordIds();
		if (! exclude_ids) {
			exclude_ids = [];
		}
		exclude_ids.push(this.getRecordId());
		return exclude_ids;
	},
	excludeNearByLocationIds: function () {
		return [0];
	},
	
	requestNearByLocations: function (min_miles, max_miles) {
		var request = {
			ctrl: this.getRecordType().toLowerCase() + 's',
			act: 'nearby',
			objid: this.getRecordId(),
			params: {
				nlids: this.excludeNearByLocationIds().join(','),
				lat: this.getLatitude(),
				lng: this.getLongitude(),
				mnr: min_miles,
				mxr: max_miles //, noids: this.excludeNearByRecordIds().join(',')
			}
		};
		ContinuousTraveler.app.request(request);
	},
	
	
	update_nearby: function (data, options) {
		this.clearSecondaryRecords();
		for (var ix=0; ix<data.length; ix++) {
			var loc = this.createSecondaryRecord(options);
			loc.setAttributes(data[ix]);
			this.addSecondaryRecord(loc);
			loc.setPrimaryViewData(false);
			loc.updateMarker();
		}
	}, // end update_nearby
	
	getNearBy_ClosestWithin: function (miles, coords) {
		return this.getSecondaryRecord_ClosestWithin(miles, coords);
	},
	
	
	
	show: function () {
		this.updateMarker();
		this.displaySecondaryRecords();
	},
	
	updateMarker: function () {
		this.populateMarker();
		this.marker.show();
	},
	
	
	setMarkerCommandButtons_Html: function (html) {
		this.marker_command_html = html;
	},
	getMarkerCommandButtons_Html: function () {
		return (this.marker_command_html ? this.marker_command_html : '');
	},
	
	
	populateMarker: function () {
		var marker = this.getMarker();
		
		var title = '';
		if (this.role) {
			title += this.role + ': ';
		} else if (this.position) {
			title += this.position.toString() + '. ';
		}
		title += this.get('name');
		marker.setTitle(title);
		marker.setPointLatLong(this.getLatitude(), this.getLongitude());
		
		if (this.position && (! marker.getLabel())) {
			marker.setLabel(this.position.toString());
		}
		
		var html = '';
		
		html += marker.contactInfo_Html({
			street: this.get('address1'),
			pobox_suite_apt: this.get('address2'),
			city: this.get('city'),
			state_province: this.get('state_province'),
			postal_code: this.get('postal_code'),
			country_abbr: this.get('country.abbr'),
			phone_local: this.get('phone_local'),
			phone_tollfree: this.get('phone_tollfree'),
			fax: this.get('fax'),
			website: this.get('website'),
			email: this.get('email')
		});
		
		var name_link = this.TEMPLATE_NAME_LINK.evaluate({
			name: title,
			record_type: this.getRecordType().toLowerCase(),
			id: this.getRecordId()
		});

		var edit_commands = '';
		marker.setInfoWindowBody_Html({
			title: name_link,
			body: html,
			commands: this.getMarkerCommandButtons_Html()
		});

	} // end populateMarker
	
}); // end ContinuousTraveler.Views.LocRecordBase


ContinuousTraveler.Views.LocRecordBase.prototype.TEMPLATE_NAME_LINK = new Template(
	'<div class="item location name">' +
		'<a href="/#{record_type}s/show/#{id}">#{name}</a>' +
	'</div>'
);


ContinuousTraveler.Views.LocRecordBase.prototype.TEMPLATE_LOCATION_TYPE = new Template(
	'<div class="item">' +
		'<label>Type:</label> <span class=" location type">#{locationtype_name}</span>' +
	'</div>'
);

ContinuousTraveler.Views.LocRecordBase.prototype.TEMPLATE_GPS = new Template(
	'<div class="item">' +
		'<label>GPS:</label> <span class=" coords longitude"> ' +
		'<div class="subitem">' +
			'<label>Coordinates:</label>' +
			'<div class="subitem coords longitude">' +
				'<span class="subline"><label>Latitude:</label> <span class=" coords latitude">#{latitude}&deg;</span></span><br />' +
				'<span class="subline"><label>Longitude:</label> <span class=" coords longitude">#{longitude}&deg;</span></span><br />' +
			'</div>' +
		'</div>' +
		'<div class="subitem">' +
			'<label>Quality:</label> <span class=" gps quality">#{gpsstate_category_name}</span>' +
		'</div>' +
		'<div class="subitem">' +
			'<label>Basis:</label> <span class=" gps basis">#{gpsstate_name}</span>&nbsp;<span class="help-comment"><a href="#" title="#{gpsstate_description}">&nbsp;?&nbsp;</a></span>' +
		'</div> ' +
		'</span>' +
	'</div>'
);




////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Locversion_Show = Class.create (ContinuousTraveler.Views.LocRecordBase, {
	
	initialize: function ($super, mapview, options) {
		$super(mapview, options);
		this.nearby_locations = null;
		this.setupMapViewControls();
		this.is_placeholder = false;
	},
	
	destroy: function ($super) {
		if (this.nearby_locations) {
			this.nearby_locations.destroy();
			this.nearby_locations = null;
		}
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	},
	
	isPlaceholder: function () {
		return this.is_placeholder;
	},
	setIsPlaceholder: function (on_off) {
		this.is_placeholder = !(! on_off);
	},
	
	update_show: function ($super, data) {
		$super(data);
		if (! this.isPlaceholder()) {
			this.requestNearByLocations(0, this.NEAR_RADIUS_MILES);
			this.zoomForLocal();
		}
	},
	
	update: function ($super, user_request, result_data) {
		this.setupMapViewControls();
		$super(user_request, result_data);
	}, // end update
	
	
	applyChanges: function ($super, data) {
		var is_changed = $super(data);
		if (is_changed) {
			this.is_placeholder = (! this.hasCoords());
			if (this.is_placeholder) {
				var coords = this.view().getCenter();
				this.setCoords(coords);
			}
		}
		return is_changed;
	},
	
	getMarker: function ($super) {
		if (! $super()) {
			var marker = null;
			var marker_options = this.getMarkerOptions();
			if (this.isPrimaryViewData()) {
				marker = new ContinuousTraveler.Views.Markers.LocversionPrimary(this, marker_options);
			} else {
				marker = new ContinuousTraveler.Views.Markers.LocversionSecondary(this, marker_options);
			}
			this.setMarker(marker);
		}
		return $super();
	},
	
	
	createSecondaryRecord: function (options) {
		return new ContinuousTraveler.Views.Locversion_Show(this.view(), options);
	},
	
	
	
	// exclude all nearby variants of self from requestNearByLocations()
	excludeNearByLocationIds: function () {
		var location_ids = [ this.get('location_id') ];
		return location_ids;
	}
	
	
	


}); // end ContinuousTraveler.Views.Locversion_Show







////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Locversion_Edit = Class.create (ContinuousTraveler.Views.Locversion_Show, {
	
	initialize: function ($super, mapview, options) {
		if (! options) { options = {}; }
		options.is_editing = true;
		if (! options.marker) { options.marker = {}; }
		options.marker.draggable = true;
		
		$super(mapview, options);

		this.setupMapViewControls();
	},
	
	
	isValid: function ($super) {
		return $super();
	},
	
	update: function ($super, user_request, result_data) {
		this.setupMapViewControls();
		if (this.isValid()) {
			switch (user_request.act) {
				case 'edit':
					this.update_show(result_data);
					break;
				case 'edit_update':
					this.update_show(result_data);
					break;
				case 'nearby':
					this.update_nearby(result_data);
					break;
				case 'closest_city':
					this.update_city(result_data);
					break;
			}; // end switch (action)
		}
	}, // end update
	
	
	
	update_show: function ($super, data) {
		if (this.coordsAreChanging(data)) {
			this.clearSecondaryRecords();
		}
		$super(data);
	},
	
	
	update_city: function (result_data) {
		var city_elt = $$('span.city input');
		if (city_elt && (city_elt.length > 0)) {
			city_elt[0].value = result_data.city;
		}
		
		var state_elt = $$('span.state_province input');
		if (state_elt && (state_elt.length > 0)) {
			state_elt[0].value = result_data.state_province;
		}
		
		var zip_elt = $$('span.postal_code input');
		if (zip_elt && (zip_elt.length > 0)) {
			zip_elt[0].value = result_data.postal_code;
		}
	},
	
	
	show: function () {
		this.setPrimaryViewData(true);
		this.updateMarker();
		this.setupLatLongFieldObserver();
	},
	
	
	
	updateMarker: function ($super) {
		$super();
		if (this.getMarker().isDraggable()) {
			var observer = this;
			this.getMarker().addListener(
				'dragend', 
				function () {
					return observer.setPosFromMarker();
				}
			);
		}
	},
	
	
	setupLatLongFieldObserver: function () {
		var lat_elt = $$('span.location.latitude input');
		if (lat_elt.length == 0) { lat_elt = null; }
		
		var lng_elt = $$('span.location.longitude input');
		if (lng_elt.length == 0) { lng_elt = null; }
		
		if (lat_elt && lng_elt) {
			this.lat_input_field = lat_elt[0];
			this.lng_input_field = lng_elt[0];

			var observer = this;
			this.lat_input_field.observe(
				'change',
				function () {
					observer.setPosFromFields();
				}
			);
			this.lng_input_field.observe(
				'change',
				function () {
					observer.setPosFromFields();
				}
			);
		}

	}, // end setupLatLongFieldObserver
	
	
	
	requestNearestCity: function (coords) {
		var request = {
			ctrl: this.getRecordType().toLowerCase() + 's',
			act: 'closest_city',
			objid: this.getRecordId(),
			params: {
				lat: this.getLatitude(),
				lng: this.getLongitude()
			}
		};
		ContinuousTraveler.app.request(request);
	},
	
	
	setPos: function (coords) {
		this.setLatitude(coords.latitude);
		this.setLongitude(coords.longitude);
		this.lat_input_field.value = coords.latitude.toString();
		this.lng_input_field.value = coords.longitude.toString();
		this.getMarker().setCoords(coords);
		this.view().panTo(coords);
		this.requestNearestCity(coords);
		this.requestNearByLocations(0, this.NEAR_RADIUS_MILES);
	},
	setPosFromFields: function () {
		if (this.lat_input_field && this.lng_input_field) {
			var latitude = ContinuousTraveler.Views.Geometry.Coords.prototype.parseDMS(this.lat_input_field.value.toString());
			var longitude = ContinuousTraveler.Views.Geometry.Coords.prototype.parseDMS(this.lng_input_field.value.toString());
			if (longitude > 0.0) {
				longitude = - longitude;
			}
			var coords = new ContinuousTraveler.Views.Geometry.Coords(latitude, longitude);
			this.setPos(coords);
		}
	},
	setPosFromMarker: function () {
		var point = this.getMarker().getPoint();
		var coords = ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint(point);
		this.setPos(coords)
	}
	
}); // end ContinuousTraveler.Views.Locversion_Edit


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Locversion_List = Class.create (ContinuousTraveler.Views.DBRecord, {
	
	initialize: function ($super, mapview) {
		$super(mapview);
		this.setupMapViewControls();
	},
	
	destroy: function ($super) {
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	},
	
	update: function ($super, user_request, result_data) {
		this.setupMapViewControls();
		if (this.isValid()) {
			this.clearSecondaryRecords();
			var result_data_count = (result_data ? result_data.length : 0);
			if (result_data_count > 0) {
				for (var ix=0; ix<result_data_count; ix++) {
					var rec = result_data[ix];
					var loc = new ContinuousTraveler.Views.Locversion_Show(this.view());
					loc.applyChanges(rec);
					loc.setPosition(ix+1);
					loc.show();
					this.addSecondaryRecord(loc);
				
				} // end for each record
			
				var dataset_bounds = null;
				var recs = this.secondaryRecords();
				var rec_count = recs.length();
			
				for (var ix=0; ix<rec_count; ix++) {
					var loc = recs.getValueAt(ix);
					var at_bounds = loc.getBoundsForLocal();
				
					if (dataset_bounds) {
						dataset_bounds.union(at_bounds);
					} else {
						dataset_bounds = at_bounds;
					}
				}
				if (dataset_bounds) {
					this.view().zoomTo(dataset_bounds);
				}
			} else {
				this.clearSecondaryRecords();
			}

			var observer = this;
			this.view().addListener(
				'click', 
				function (overlay, latlng) {
					return observer.setLatLng(overlay, latlng);
				}
			);
		}
	}, // end update
	
	
	setLatLng: function (overlay, latlng) {
		if (latlng) {
			var lat_input_field = $$('span.location.latitude input[value]');
			var lng_input_field = $$('span.location.longitude input[value]');
		
			if (lat_input_field) {
				lat_input_field[0].value = latlng.lat().toString();
			}
			if (lng_input_field) {
				lng_input_field[0].value = latlng.lng().toString();
			}
		}
		return true;
	}
	
}); // end ContinuousTraveler.Views.Locversion_List


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Location_Show = Class.create (ContinuousTraveler.Views.LocRecordBase, {
	
	initialize: function ($super, mapview, options) {
		$super(mapview, options);
		this.setupMapViewControls();
	},
	
	update: function ($super, user_request, result_data) {
		this.setupMapViewControls();
		$super(user_request, result_data);
	}, // end update
	
	update_show: function ($super, data) {
		$super(data);
		this.zoomForLocal();
	},
	
	getMarker: function ($super) {
		if (! $super()) {
			var marker = null;
			var marker_options = this.getMarkerOptions();
			if (this.isPrimaryViewData()) {
				marker = new ContinuousTraveler.Views.Markers.LocationPrimary(this, marker_options);
			} else {
				marker = new ContinuousTraveler.Views.Markers.LocationSecondary(this, marker_options);
			}
			this.setMarker(marker);
			var html = this.newTrip_Button_Html()
			this.setMarkerCommandButtons_Html(html);
		}
		return $super();
	},
	
	
	createSecondaryRecord: function (options) {
		return new ContinuousTraveler.Views.Location_Show(this.view(), options);
	},
	
	
	
	newTrip_Button_Html: function () {
		return this.TEMPLATE_BUTTON_NEW_TRIP_FROM_HERE.evaluate({
			location_id: this.getRecordId()
		});
	}

}); // end ContinuousTraveler.Views.Location_Show



ContinuousTraveler.Views.Location_Show.prototype.TEMPLATE_BUTTON_NEW_TRIP_FROM_HERE = new Template(
	'<div class="new-trip-button">' +
		'<input  type="submit" class="menu-button-small" ' +
			'onclick="$(\'hidden_command\').name = \'command[location.new_from_to(#{location_id})]\'" ' +
			'name="command[location.new_from_to(#{location_id})]" ' +
			'value="Start New Trip Here" ' +
			'title="Create a new trip with this RV park as its first viapoint." ' +
		'/>' +
	'</div>'
);


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Location_List = Class.create (ContinuousTraveler.Views.DBRecord, {
	
	initialize: function ($super, mapview) {
		$super(mapview);
		this.setupClickObserver();
	},
	
	destroy: function ($super) {
		if (this.click_observer) {
			this.view().removeListener(this.click_observer);
			this.click_observer = null;
		}
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	},
	
	update: function ($super, user_request, result_data) {
		if (this.isValid()) {
			this.clearSecondaryRecords();
			if (result_data) {
				var result_data_count = result_data.length;
				for (var ix=0; ix<result_data_count; ix++) {
					var rec = result_data[ix];
					var loc = new ContinuousTraveler.Views.Location_Show(this.view());
					loc.applyChanges(rec);
					loc.setPosition(ix+1);
					loc.show();
					this.addSecondaryRecord(loc);
				
				} // end for each record
			
				var dataset_bounds = null;
				var recs = this.secondaryRecords();
				var rec_count = recs.length();
			
				for (var ix=0; ix<rec_count; ix++) {
					var loc = recs.getValueAt(ix);
					var at_bounds = loc.getBoundsForLocal();
				
					if (dataset_bounds) {
						dataset_bounds.union(at_bounds);
					} else {
						dataset_bounds = at_bounds;
					}
				}
				if (dataset_bounds) {
					this.view().zoomTo(dataset_bounds);
				}
			}
		}
	}, // end update
	
	
	setupClickObserver: function () {
		if (this.click_observer) {
			this.view().removeListener(this.click_observer);
			this.click_observer = null;
		}
		var observer = this;
		this.click_observer = this.view().addListener(
			'click', 
			function (overlay, latlng) {
				return observer.setLatLng(overlay, latlng);
			}
		);
	},
	
	
	setLatLng: function (overlay, latlng) {
		if (latlng) {
			var lat_input_field = $$('span.location.latitude input');
			var lng_input_field = $$('span.location.longitude input');
		
			if (lat_input_field) {
				lat_input_field[0].value = latlng.lat().toString();
			}
			if (lng_input_field) {
				lng_input_field[0].value = latlng.lng().toString();
			}
		}
		return true;
	}
	
}); // end ContinuousTraveler.Views.Location_List


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Trip_Show = Class.create (ContinuousTraveler.Views.DBRecord, {
	
	initialize: function ($super, mapview, options) {
		$super(mapview);
		
		this.options = options;
		if (! this.options) { this.options = {}; }
		if (! this.options.viapoints_ctor) {
			this.options.viapoints_ctor = ContinuousTraveler.Views.Viapoint_Show;
		}
		if (! this.options.viapoints) { this.options.viapoints = {}; }

		this.label = null;
		this.setupMapViewControls();
		this.directions = null;
	},
	
	destroy: function ($super) {
		this.clearDirections();
		this.destroy_viapoints();
		this.label = null;
		this.options = null;
		$super();
	},
	
	destroy_viapoints: function () {
		var viapoints = this.getViapoints();
		var count = (viapoints ? viapoints.length : 0)
		for (var ix=0; ix<count; ix++) {
			var viapoint = viapoints[ix];
			if (viapoint) {
				viapoint.destroy();
			}
		}
		this.set('viapoints', null);
	},
	
	
	clearDirections: function () {
		if (this.directions) {
			this.directions.destroy();
			this.directions = null;
		}
	},
	getViapointConstructor: function () {
		return this.options.viapoints_ctor;
	},
	
	setViapointConstructor: function (ctor) {
		this.options.viapoints_ctor = ctor;
	},
	
	getViapointOptions: function () {
		return this.options.viapoints;
	},
	
	isValid: function ($super) {
		return $super();
	},
	
	getLabel: function () {
		return this.label;
	},
	setLabel: function (text) {
		this.label = text;
	},
	
	
	update: function (user_request, result_data) {
		this.setupMapViewControls();
		if (this.isValid()) {
			var action = user_request['act'];
			switch (action) {
				case 'show':
					this.update_show(result_data, false);
					break;
				case 'near_viapoint':
					this.update_nearby(result_data);
					break;
			}; // end switch (action)
		}
	}, // end update
	
	
	
	update_show: function (data, no_zoom) {
		var is_changed = this.applyChanges(data);
		if (is_changed) {
			this.show();
			if (! no_zoom) {
				this.zoomToBounds();
			}
		}
	},
	

	show: function () {
		this.refreshViapoints();
		this.refreshDirections();
	}, // end show
	
	
	
	update_nearby: function (data) {
		var trip_id = data.id;
		var viapoint_id = data.viapoint;
		var locations = data.locations;
		
		if ((trip_id == this.getRecordId() && locations && (locations.length > 0))) {
			var viapoints = this.getViapoints();
			for (var ix=0; ix<viapoints.length; ix++) {
				var viapoint = viapoints[ix];
				if (viapoint.getRecordId() == viapoint_id) {
					viapoint.update_nearby(locations);
					break;
				}
			}
		}

	}, // end update_nearby
	
	
	
	refreshDirections: function () {
		this.clearDirections();
		
		var coords = this.getViapointCoordinates();
		if (coords && (coords.length>1)) {
			this.directions = new ContinuousTraveler.Views.Lines.Directions(this.view(), coords);
			this.directions.show();
		}
	},
	
	getViapointCoordinates: function () {
		var list = null;
		var viapoints = this.getViapoints();
		var count = (viapoints ? viapoints.length : 0)
		if (count > 0) {
			list = [];
			for (var ix=0; ix<count; ix++) {
				var viapoint = viapoints[ix];
				if (viapoint) {
					list.push(viapoint.getCoords());
				}
			}
		}
		return list;
	},
	
	
	
	
	
	
	
	refreshViapoints: function () {
		var viapoints = this.getViapoints();
		var count = (viapoints ? viapoints.length : 0)
		for (var ix=0; ix<count; ix++) {
			var viapoint = viapoints[ix];
			if (viapoint) {
				viapoint.show();
			}
		}
	},
	
	
	zoomToBounds: function () {
		var data_set_bounds = this.getBoundsForRadius(this.DAYTRIP_RADIUS_MILES);

		if (data_set_bounds) {
			this.view().zoomTo(data_set_bounds);
		}
	},
	
	
	getBoundsForRadius: function (miles) {
		var viapoints = this.getViapoints();
		var count = (viapoints ? viapoints.length : 0)
		var data_set_bounds = null;
		for (var ix=0; ix<count; ix++) {
			var viapoint = viapoints[ix];
			if (viapoint) {
				var at_bounds = viapoint.getBoundsForRadius(miles);

				if (data_set_bounds) {
					data_set_bounds.union(at_bounds);
				} else {
					data_set_bounds = at_bounds;
				}
			}
		}
		return data_set_bounds;
	},
	
	
	
	applyChanges: function (data) {
		var is_changed = false;
		for (var name in data) {
			if (this.attrs[name] != data[name]) {
				is_changed = true;
				if (name == 'viapoints') {
					this.applyViapointChanges(data[name]);
				} else {
					this.attrs[name] = data[name];
				}
			}
		}
		if (is_changed) {
			// Tell viapoints that trip is updated:
			var viapoints = this.getViapoints();
			var count = (viapoints ? viapoints.length : 0)
			for (var ix=0; ix<count; ix++) {
				viapoints[ix].tripUpdateCompleted();
			}
		}
		return is_changed;
	}, // applyChanges
	
	
	
	applyViapointChanges: function (data) {
		var is_changed = false;
		
		var viapoint_ctor = this.getViapointConstructor();
		var viapoint_options = this.getViapointOptions();
		
		var existing_viapoints = this.getViapoints();
		var existing_length = (existing_viapoints ? existing_viapoints.length : 0);
		var new_viapoints = [];
		
		var incoming_length = data.length;
		
		var previous_viapoint = null;
		
		for (var in_ix=0; in_ix<incoming_length; in_ix++) {
			var incoming = data[in_ix];
			var id = incoming.id;

			var current_viapoint = null;
			
			// Find and updating the existing viapoint, if any.
			for (var ex_ix=0; ex_ix<existing_length; ex_ix++) {
				var existing = existing_viapoints[ex_ix];
				
				if (existing && existing.getRecordId() == id) {
					existing_viapoints[ex_ix] = null;
					current_viapoint = existing;
					found = true;
					break;
				}
			} // find existing and update
			
			if (! current_viapoint) {
				current_viapoint = new viapoint_ctor(this.view(), this, viapoint_options);
			}
			
			is_changed = current_viapoint.applyChanges(incoming) || is_changed;
			current_viapoint.setPreviousLocation(previous_viapoint);
			new_viapoints.push(current_viapoint);
			previous_viapoint = current_viapoint;
		}
		
		if (new_viapoints.length == 0) {
			new_viapoints = null;
		}
		this.set('viapoints', new_viapoints);
		
		
		// Delete any leftovers that are no longer used.
		if (existing_viapoints) {
			existing_viapoints = existing_viapoints.compact();
			existing_length = existing_viapoints.length;
			is_changed = (existing_length > 0) || is_changed;
			for (var ex_ix=0; ex_ix<existing_length; ex_ix++) {
				existing_viapoints[ex_ix].destroy();
				existing_viapoints[ex_ix] = null;
			}
		}
		
		return is_changed;
		
	}, // applyViapointChanges
	
	
	
	
	getViapoints: function () {
		return this.get('viapoints');
	},
	getViapointCount: function () {
		var viapoints = this.getViapoints();
		return (viapoints ? viapoints.length : 0);
	},
	getViapointAtIndex: function (index) {
		if (index >= 0) {
			var viapoints = this.get('viapoints');
			if (viapoints && (viapoints.length > index)) {
				return viapoints[index];
			}
		}
	},
	getViapointAtPosition: function (position) {
		return this.getViapointAtIndex(position - 1);
	}
	
	
	
}); // end ContinuousTraveler.Views.Trip_Show


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Trip_Edit = Class.create (ContinuousTraveler.Views.Trip_Show, {
	
	initialize: function ($super, mapview, options) {
		if (! options) { options = {}; }
		options.viapoints_ctor = ContinuousTraveler.Views.Viapoint_Edit;
		options.is_editing = true;
		
		$super(mapview, options);
		this.edit_line = null;
		this.line_listeners = [];
		this.setupMapViewControls();
		this.selected_viapoint_position = 0;
	},

	
	destroy: function ($super) {
		this.destroyLineListeners();
		
		$super();
	},
	
	destroyEditLine: function () {
		if (this.edit_line) {
			this.edit_line.destroy();
			this.edit_line = null;
		}
	},
	
	getEditLine: function () {
		return this.edit_line;
	},
	
	setEditLine: function (line) {
		this.edit_line = line;
	},
	
	
	update: function (user_request, result_data) {
		this.setupMapViewControls();
		if (this.isValid()) {
			var action = user_request['act'];
			switch (action) {
				case 'edit':
					this.update_show(result_data);
					break;
				case 'update':
					this.update_show(result_data, true);
					break;
				case 'select':
					this.update_selection(result_data);
					break;
				case 'near_viapoint':
					this.update_nearby(result_data);
					break;
			}; // end switch (action)
		}
	}, // end update
	
	show: function () {
		this.refreshViapoints();
		this.refreshDirections();
		this.refreshEditLine();
	}, // end show
	
	
	update_selection: function (result_data) {
		if (
			result_data.viapoint_position && 
			(result_data.viapoint_position >= 1) &&
			(result_data.viapoint_position <= this.getViapointCount())
			)
		{
			this.selected_viapoint_position = result_data.viapoint_position;
			var viapoints = this.getViapoints();
			var viapoint = viapoints[this.selected_viapoint_position - 1];
			viapoint.zoomForNear();
			viapoint.openInfoWindow();
		}
	}, // end update_selection
	
	
	
	
	refreshEditLine: function () {
		this.destroyLineListeners();
		
		this.destroyEditLine();
		
		var coords = this.getViapointCoordinates();
		if (! coords) {
			coords = [ this.view().getCenter() ];
		}
		var max_vertex_count = (coords ? ((coords.length * 2) + 1) : 0);
		
		var heading = 45;
		if (coords.length > 1) {
			heading = coords[1].headingDegrees(coords[0]);
		}
		var extension_length = 200;
		var front_extension = coords[0].translateMiles(heading, extension_length);
		coords = [ front_extension, coords ].flatten();

		heading = heading = coords[coords.length-2].headingDegrees(coords[coords.length-1]);
		var back_extension = coords[coords.length-1].translateMiles(heading, extension_length);
		coords.push(back_extension);
				
		var line_opts = {
			color: "#800000",
			weight: 5,
			opacity: 0.4,
			editable: this.isEditing(),
			max_nodes: max_vertex_count
		};
	
		var line = new ContinuousTraveler.Views.Lines.PolyLine(this.view(), coords, line_opts);
		line.show();
		this.setEditLine(line);
		
		this.setupEditLineListener();
		
	}, // end refreshEditLine
	
	
	
	
	destroyLineListeners: function () {
		var line = this.getEditLine();
		if (line) {
			while (this.line_listeners.length > 0) {
				var listener = this.line_listeners.pop();
				line.removeListener(listener);
			}
		}
	},
	setupEditLineListener: function () {
		var line = this.getEditLine();
		if (line) {
			observer = this;
			var listener = line.addListener(
				'lineupdated', 
				function () {
					observer.editLineUpdated();
				}
			);
			this.line_listeners.push(listener);
		}
	}, // setupEditLineListener
	
	
	
	editLineUpdated: function () {
		var line = this.getEditLine();
		if (line) {
			var change = line.getChangedVertex();
			if (change) {
				if (change.delta == 0) {
					this.editLine_MoveViapoint(change);
				} else if (change.delta > 0) {
					this.addViapoint(change);
				} else {
					this.deleteViapoint(change);
				}
			}
		}
	}, // editLineUpdated
	
	
	viewZoomed: function (old_zoom, new_zoom) {
		this.refreshEditLine();
	},
	
	
	
	editLine_MoveViapoint: function (change) {
		var latitude = change.current.getLatitude();
		var longitude = change.current.getLongitude();
		var viapoints = this.getViapoints();
		
		if ((! viapoints) || (change.index < 1)) {
			this.requestNewViapointAtCoords(1, latitude, longitude);
		
		} else if (change.index == (change.count - 1)) {
			this.requestNewViapointAtCoords(this.getViapointCount()+1, latitude, longitude);
		
		} else {
			var viapoint = viapoints[change.index - 1];
			var marker = viapoint.getMarker();
			marker.setPointLatLong(latitude, longitude);
			var viapoint_id = viapoint.getRecordId();
			this.requestChangeViapointCoords(viapoint_id, latitude, longitude);
		}

	}, // editLine_MoveViapoint
	
	
	
	addViapoint: function (change) {
		var latitude = change.current.getLatitude();
		var longitude = change.current.getLongitude();
		var position = change.index;
		
		this.requestNewViapointAtCoords(position, latitude, longitude);
	}, // addViapoint
	
	
	
	deleteViapoint: function (change) {
		
	}, // deleteViapoint
	
	
	
	
	
	requestChangeViapointCoords: function (viapoint_id, latitude, longitude) {
		this.selected_viapoint_position = 0;
		
		var trip_id_str = this.getRecordId().toString();
		
		var command = 'trip.change_gps_coords(' +
			trip_id_str + ',' +
		 	viapoint_id.toString() + ',' +
			latitude + ',' +
			longitude + ')';
		
		var request = {
			ctrl: 'trips',
			act: 'update',
			objid: trip_id_str,
			command: command
		};
		this.view().submitForm(request);
	},
	
	
	
	requestNewViapointAtCoords: function (position, latitude, longitude) {
		this.selected_viapoint_position = 0;
		
		var trip_id_str = this.getRecordId().toString();
		
		var command = 'trip.add_viapoint_at_coords(' +
			trip_id_str + ',' +
		 	position + ',' +
			latitude + ',' +
			longitude + ')';
		
		var request = {
			ctrl: 'trips',
			act: 'update',
			objid: trip_id_str,
			command: command
		};
		this.view().submitForm(request);
	},
	
	
	viapointClosestWithin: function (max_miles, coords) {
		var closest_via = null;
		var closest_distance = max_miles + 1;
		
		var viapoints = this.getViapoints();
		var count = viapoints.length;
		
		for (var ix=0; ix<count; ix++) {
			var via = viapoints[ix];
			var distance = coords.distanceMiles(via.getCoords());
			if (distance < closest_distance) {
				closest_distance = distance;
				closest_via = via;
			}
		}
		
		return closest_via;
	}, // viapointClosestWithin
	
	
	
	
	nearByLocationClosestWithin: function (max_miles, coords) {
		var closest_loc = null;
		var closest_distance = max_miles + 1;
		
		var viapoints = this.getViapoints();
		var count = viapoints.length;
		
		for (var ix=0; ix<count; ix++) {
			var via = viapoints[ix];
			var nearby_loc = via.getNearBy_ClosestWithin(max_miles, coords);
			
			if (nearby_loc) {
				var distance = coords.distanceMiles(nearby_loc.getCoords());
				if (distance < closest_distance) {
					closest_distance = distance;
					closest_loc = nearby_loc;
				}
			}
		}
		
		return closest_loc;
	}, // nearByLocationClosestWithin
	
	

	
	requestSetLocationId: function (viapoint_id, location_id) {
		var trip_id = this.getRecordId();
		
		var command = 'trip.change_viapoint_location(' +
			trip_id + ',' +
		 	viapoint_id + ',' +
			location_id + ')';
		
		var request = {
			ctrl: 'trips',
			act: 'update',
			objid: trip_id,
			command: command
		};
		this.view().submitForm(request);
	}
	
	
	
	
	
}); // end ContinuousTraveler.Views.Trip_Edit


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Trip_List = Class.create (ContinuousTraveler.Views.DBRecord, {
	
	initialize: function ($super, mapview) {
		$super(mapview);
		this.setupMapViewControls();
		this.show_trip_interval = null;
		this.showing_trips = null;
	},
	
	destroy: function ($super) {
		if (this.show_trip_interval) {
			clearInterval(this.show_trip_interval);
			this.show_trip_interval = null;
		}
		this.showing_trips = null;
		$super();
	},
	
	isValid: function ($super) {
		return $super();
	},
	
	getTrips: function () {
		return this.secondaryRecords().values();
	},
	
	// Showing a trip causes a google.maps.Directions load request. If they come
	// too close together, they error out. So: ensure a minimum delay between requests.
	//
	showTrips: function () {
		if (! this.show_trip_interval) {
			this.showing_trips = this.getTrips();
			if (this.showing_trips && (this.showing_trips.length > 0)) {
				var first_trip = this.showing_trips.pop();
				first_trip.show();
				
				if (this.showing_trips.length > 0) {
					var observer = this;
					this.show_trip_interval = setInterval(
						function () {
							observer.showTrip();
						},
						500
					);
				}
			}
		}
	},
	showTrip: function () {
		if (this.show_trip_interval) {
			var trip = this.showing_trips.pop();
			if (trip) {
				trip.show();
			} else {
				clearInterval(this.show_trip_interval);
				this.show_trip_interval = null;
				this.showing_trips = null;
			}
		} else {
			this.showing_trips = null;
		}
	},
	
	
	update: function ($super, user_request, result_data) {
		this.setupMapViewControls();
		if (this.isValid()) {
			this.clearSecondaryRecords();
			if (result_data) {
				var trip_options = { viapoints_ctor: ContinuousTraveler.Views.Viapoint_Minimal };
				var result_data_count = result_data.length;

				for (var ix=0; ix<result_data_count; ix++) {
					var rec = result_data[ix];
					var trip = new ContinuousTraveler.Views.Trip_Show(this.view(), trip_options);
					trip.setLabel((ix+1).toString());
					trip.applyChanges(rec);
					this.addSecondaryRecord(trip);
				
				} // end for each record
				
				this.showTrips();
			
				var dataset_bounds = null;
				var recs = this.secondaryRecords();
				var rec_count = recs.length();
			
				for (var ix=0; ix<rec_count; ix++) {
					var trip = recs.getValueAt(ix);
					var at_bounds = trip.getBoundsForLocal();
				
					if (dataset_bounds) {
						dataset_bounds.union(at_bounds);
					} else {
						dataset_bounds = at_bounds;
					}
				}
				if (dataset_bounds) {
					this.view().zoomTo(dataset_bounds);
				}
			}
		}

		var observer = this;
		this.view().addListener(
			'click', 
			function (overlay, latlng) {
				return observer.setLatLng(overlay, latlng);
			}
		);

	}, // end update
	
	
	setLatLng: function (overlay, latlng) {
		if (latlng) {
			var lat_input_field = $$('span.location.latitude input[value]');
			var lng_input_field = $$('span.location.longitude input[value]');
		
			if (lat_input_field) {
				lat_input_field[0].value = latlng.lat().toString();
			}
			if (lng_input_field) {
				lng_input_field[0].value = latlng.lng().toString();
			}
		}
		return true;
	}
	
}); // end ContinuousTraveler.Views.Trip_List


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Via_Placeholder_Loc = Class.create (ContinuousTraveler.Views.LocRecordBase, {
	
	initialize: function ($super, viapoint, options) {
		$super(viapoint.view(), options);
		this.viapoint = viapoint;
	},
	
	destroy: function ($super) {
		this.viapoint = null;
		$super();
	},
	
	isValid: function ($super) {
		return ($super() && this.viapoint);
	},
	
	getMarker: function ($super) {
		if (! $super()) {
			
			var marker_options = this.getMarkerOptions();
			var marker = null;
			
			if (this.viapoint.isStopover()) {
				marker = new ContinuousTraveler.Views.Markers.ViapointNeedLocation(this.viapoint, marker_options);
			} else {
				marker = new ContinuousTraveler.Views.Markers.ViapointPassingThrough(this.viapoint, marker_options);
			}
			this.setMarker(marker);
		}
		return $super();
	},
	
	
	populateMarker: function () {
		var marker = this.getMarker();
		if (marker) {
			marker.populate();
		}
	},
	
	
	createSecondaryRecord: function (options) {
		return new ContinuousTraveler.Views.Location_Show(this.view(), options);
	}
	
	
	
	
}); // end ContinuousTraveler.Views.Via_Placeholder_Loc


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Viapoint_Base = Class.create (ContinuousTraveler.Views.DBRecord, {
	
	initialize: function ($super, mapview, trip, options) {
		$super(mapview, options);
		this.trip = trip;
		this.is_placeholder = false;
	},
	
	destroy: function ($super) {
		this.clearNearbyListeners();
		this.trip = null;
		this.clearLocation();
		$super();
	},
	
	isValid: function ($super) {
		return ($super() && this.trip);
	},
	
	getTrip: function () {
		return this.trip;
	},
	getViapointCount: function () {
		return this.trip.getViapointCount();
	},
	isOrigin: function () {
		return (this.getPosition() == 1);
	},
	isDestination: function () {
		return (this.getPosition() == this.getViapointCount());
	},
	tripUpdateCompleted: function () {
		var loc = this.getLocation();
		loc.setPosition(this.getPosition());
		
		if (this.isOrigin()) {
			loc.setRole('Origin');
		} else if (this.isDestination()) {
			loc.setRole('Destination');
		}
	},
	
	getLocation: function () {
		return this.get('location');
	},
	setLocation: function (new_location) {
		var current = this.getLocation();
		if (current != new_location) {
			if (current) {
				current.destroy();
			}
			this.set('location', new_location)
		}
	},
	
	clearLocation: function () {
		this.setLocation(null);
	},
	
	isStopover: function () {
		return this.get('is_stopover');
	},
	
	isPlaceholder: function () {
		return this.is_placeholder;
	},
	
	getTripId: function () {
		return this.get('trip_id');
	},
	
	getPosition: function () {
		return this.get('position');
	},
	
	openInfoWindow: function () {
		var marker = this.getMarker();
		if (marker) {
			marker.openInfoWindow();
		}
	},
	
	getMarker: function () {
		return this.getLocation().getMarker();
	},
	
	
	needsLocation: function () {
		return (this.isStopover() && this.isPlaceholder());
	},
	
	getLocationOptions: function () {
		var opts = this.getOptions();
		return ( opts ? opts.location : null );
	},
	
	getPreviousLocation: function () {
		return this.get('previous');
	},
	setPreviousLocation: function (loc) {
		this.set('previous', loc);
	},
	
	getPreviousCoords: function () {
		var loc = this.getPreviousLocation();
		return (
			loc ? loc.getCoords() : null
		);
	},
	
	
	
	
	update_show: function (data) {
		var is_changed = this.applyChanges(data);
		if (is_changed) {
			this.show();
		}
	},
	

	show: function () {
		var loc = this.getLocation();
		loc.show();

		if (this.needsNearbyLocations()) {
			this.requestNearByLocations();
		}
	},
	
	
	needsNearbyLocations: function () {
		return (
			this.isStopover() &&
			this.isPlaceholder() &&
			this.getLocation().secondaryRecordsAreEmpty()
		);
	},
	
	
	requestNearByLocations: function () {
		var request = {
			ctrl: 'trips',
			act: 'near_viapoint',
			objid: this.getTripId(),
			params: {
				viapoint: this.getRecordId()
			}
		};
		ContinuousTraveler.app.request(request);
	},
	
	
	addNearByListener: function (event_name, callback_function) {
		var loc = this.getLocation();
		if (loc) {
			return loc.addSecondaryRecordListeners(event_name, callback_function);
		}
	},
	removeNearByListener: function (listener_list) {
		var loc = this.getLocation();
		if (loc) {
			loc.removeSecondaryRecordListeners(listener_list);
		}
	},
	
	clearNearbyListeners: function () {
		if (this.nearby_listeners) {
			this.removeNearByListener(this.nearby_listeners);
			this.nearby_listeners = null;
		}
	},
	
	getNearBy_WithMouseOver: function () {
		return this.getLocation().getSecondaryRecord_WithMouseOver();
	},
	getNearBy_ClosestWithin: function (miles, coords) {
		return this.getLocation().getNearBy_ClosestWithin(miles, coords);
	},
	
	
	update_nearby: function (location_data, nearby_options) {
		this.getLocation().update_nearby(location_data, nearby_options);
	},

	
	stopoverChanging: function (data) {
		return (
			(this.isStopover() && (! data['is_stopover'])) ||
			((! this.isStopover()) && data['is_stopover'])
		);
	},
	
	applyChanges: function (data) {
		var is_changed = false;
		var is_stopover_location = false;
		
		// If a stopover location was previously set and coordinates are changing,
		// then the previously set location is no longer valid. Force selection
		// Of a more appropriate one by deleting the invalid one.
		if (this.coordsAreChanging(data)) {
			this.clearLocation();
		}
		if (this.stopoverChanging(data)) {
			this.clearLocation();
		}
		var location_options = this.getLocationOptions();
		
		for (var name in data) {
			// Location values are objects, not 'scalars'; need special handling.
			if (name == 'location') {
				var current_loc = this.getLocation();
				var new_loc_data = data[name]
				
				if (current_loc && new_loc_data) {
					is_changed = current_loc.applyChanges(new_loc_data);
					
				} else if (new_loc_data) {
					is_changed = true;
					is_stopover_location = true;
					var new_loc = new ContinuousTraveler.Views.Location_Show(this.view(), location_options);
					new_loc.applyChanges(new_loc_data);
					this.setLocation(new_loc);
					this.is_placeholder = false;
					
				} else if (current_loc) {
					var placeholder = new ContinuousTraveler.Views.Via_Placeholder_Loc(this, location_options);
					is_changed = (this.coordsAreChanging(data) || current_loc.isNotEqual(placeholder));
					if (is_changed) {
						this.clearLocation();
						this.setLocation(placeholder);
						this.is_placeholder = true;
					}
				} else {
					is_changed = true;
					var placeholder = new ContinuousTraveler.Views.Via_Placeholder_Loc(this, location_options);
					this.setLocation(placeholder);
					this.is_placeholder = true;
				}

			} else if (this.attrs[name] != data[name]) {
				is_changed = true;
				this.attrs[name] = data[name];
			}
		} // end for
		
		if (! this.hasCoords()) {
			is_changed = true;
			var view_center = this.view().getCenter();
			this.setCoords(view_center);
		}
		
		var loc = this.getLocation();
		if (! loc) {
			is_changed = true;
			loc = new ContinuousTraveler.Views.Via_Placeholder_Loc(this, location_options);
			this.setLocation(loc);
			this.is_placeholder = true;
		}
		
		
		if (is_stopover_location && this.isEditing()) {
			var html = this.becomePassThrough_Button_Html()
			html += this.delete_Button_Html();
			loc.setMarkerCommandButtons_Html(html);
		}

		return is_changed;
		
	}, // applyChanges
	
	
	
	delete_Button_Html: function () {
		return this.TEMPLATE_BUTTON_DELETE_VIAPOINT.evaluate({
			trip_id: this.getTripId(),
			viapoint_id: this.getRecordId()
		});
	},
	
	becomeStopover_Button_Html: function () {
		return this.TEMPLATE_BUTTON_BECOME_STOPOVER.evaluate({
			trip_id: this.getTripId(),
			viapoint_id: this.getRecordId()
		});
	},
	
	becomePassThrough_Button_Html: function () {
		return this.TEMPLATE_BUTTON_BECOME_PASSTHROUGH.evaluate({
			trip_id: this.getTripId(),
			viapoint_id: this.getRecordId()
		});
	}


}); // end ContinuousTraveler.Views.Viapoint_Base



ContinuousTraveler.Views.Viapoint_Base.prototype.TEMPLATE_BUTTON_DELETE_VIAPOINT = new Template(
	'<div class="delete-button">' +
		'<input  type="submit" class="menu-button-small menu-button-danger" ' +
			'onclick="$(\'hidden_command\').name = \'command[trip.delete_viapoint(#{trip_id},#{viapoint_id})]\'" ' +
			'name="command[trip.delete_viapoint(#{trip_id},#{viapoint_id})]" ' +
			'value="Delete" ' +
			'title="Remove this viapoint from the trip." ' +
		'/>' +
	'</div>'
);

ContinuousTraveler.Views.Viapoint_Base.prototype.TEMPLATE_BUTTON_BECOME_STOPOVER = new Template(
	'<div class="stopover-button">' +
		'<input  type="submit" class="menu-button-small" ' +
			'onclick="$(\'hidden_command\').name = \'command[trip.viapoint_stopover(#{trip_id},#{viapoint_id},true)]\'" ' +
			'name="command[trip.viapoint_stopover(#{trip_id},#{viapoint_id},true)]" ' +
			'value="Stopover" ' +
			'title="Change from pass-through to stopover." ' +
		'/>' +
	'</div>'
);

ContinuousTraveler.Views.Viapoint_Base.prototype.TEMPLATE_BUTTON_BECOME_PASSTHROUGH = new Template(
	'<div class="passthrough-button">' +
		'<input  type="submit" class="menu-button-small" ' +
			'onclick="$(\'hidden_command\').name = \'command[trip.viapoint_stopover(#{trip_id},#{viapoint_id},false)]\'" ' +
			'name="command[trip.viapoint_stopover(#{trip_id},#{viapoint_id},false)]" ' +
			'value="Pass-Through" ' +
			'title="Change from stopover to pass-through." ' +
		'/>' +
	'</div>'
);




////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Viapoint_Show = Class.create (ContinuousTraveler.Views.Viapoint_Base, {
	
	initialize: function ($super, mapview, trip, options) {
		$super(mapview, trip, options);
	}

}); // end ContinuousTraveler.Views.Viapoint_Show


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Viapoint_Edit = Class.create (ContinuousTraveler.Views.Viapoint_Base, {
	
	initialize: function ($super, mapview, trip, options) {
		if (! options) { options = {}; }
		options.is_editing = true;
		
		if (! options.location) { options.location = {}; }
		if (! options.location.marker) { options.location.marker = {}; }
		options.location.marker.draggable = true;
		
		$super(mapview, trip, options);
		
		this.marker_drag_listener = null;
	},
	
	destroy: function ($super) {
		this.clearMarkerListener();
		$super();
	},
	
	show: function ($super) {
		this.clearMarkerListener();
		$super();
		this.setupMarkerCallback();
	},
	
	clearMarkerListener: function () {
		var marker = this.getMarker();
		if (marker && this.marker_drag_listener) {
			marker.removeListener(this.marker_drag_listener);
		}
		this.marker_drag_listener = null;
	},
	
	setupMarkerCallback: function () {
		var marker = this.getMarker();
		
		if (marker) {
			var observer = this;
			this.marker_drag_listener = marker.addListener(
				'dragend',
				function (latlng) {
					return observer.markerDragged(latlng);
				}
			);
		}
	},
	
	
	
	markerDragged: function (latlng) {
		var coords = new ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint(latlng);
		var trip = this.getTrip();

		var loc = null;
		if (this.needsLocation()) {
			loc = trip.nearByLocationClosestWithin(5, coords);
		}
		
		if (loc) {
			trip.requestSetLocationId(this.getRecordId(), loc.getRecordId());
		} else {
			trip.requestChangeViapointCoords(this.getRecordId(), coords.getLatitude(), coords.getLongitude());
		}
	}
	
}); // end ContinuousTraveler.Views.Viapoint_Edit







////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.Viapoint_Minimal = Class.create (ContinuousTraveler.Views.Viapoint_Base, {
	
	initialize: function ($super, mapview, trip, options) {
		$super(mapview, trip, options);
	},
	

	show: function () {
		this.marker = new ContinuousTraveler.Views.Markers.ViapointMinimal(this);
		this.marker.setPointLatLong(this.getLatitude(), this.getLongitude());
		
		var trip_label = this.getTrip().getLabel();
		
		var html = this.TEMPLATE_TRIP_POPUP.evaluate({
			label: trip_label,
			id: this.trip.getRecordId(),
			title: this.trip.get('title')
		});
		this.marker.setHtml(html);
		this.marker.setLabel(trip_label);
		this.marker.show();
		
	} // end show
	
	
	

}); // end ContinuousTraveler.Views.Viapoint_Minimal

ContinuousTraveler.Views.Viapoint_Minimal.prototype.TEMPLATE_TRIP_POPUP = new Template(
	'<div class="item">' +
		'<span class="trip title">' +
			'#{label}. <a href="/trips/show/#{id}">#{title}</a>' +
		'</span>' +
	'</div>'
);


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.User_Show = Class.create (ContinuousTraveler.Views.DBRecord, {
	
	initialize: function ($super, mapview, options) {
		$super(mapview, options);
		this.is_placeholder = false;
		this.setupMapViewControls();
	},
	
	destroy: function ($super) {
		this.clearLocation();
		this.is_placeholder = false;
		$super();
	},
	
	
	
	
	getLocation: function () {
		return this.get('location');
	},
	setLocation: function (new_location) {
		var current = this.getLocation();
		if (current != new_location) {
			if (current) {
				current.destroy();
			}
			this.set('location', new_location)
		}
	},
	
	clearLocation: function () {
		this.setLocation(null);
	},
	needsLocation: function () {
		return this.is_placeholder;
	},
	
	
	getLocationOptions: function () {
		var opts = this.getOptions();
		return ( opts ? opts.location : null );
	},
	
	getMarker: function () {
		var loc = this.getLocation();
		return loc.getMarker();
	},
	
	
	update: function ($super, user_request, result_data) {
		this.setupMapViewControls();
		if (this.isValid()) {
			var action = user_request['act'];
			switch (action) {
				case 'show':
					this.update_show(result_data);
					this.zoomForMoveOn();
					break;
				case 'update':
					this.update_show(result_data);
					this.zoomForMoveOn();
					break;
			}; // end switch (action)
		}
	}, // end update
	
	
	
	update_show: function (data) {
		var is_changed = this.applyChanges(data);
		if (is_changed) {
			this.show();
		}
	},
	

	show: function () {
		var loc = this.getLocation();
		loc.setRole('Home Base');
		loc.show();
	},
	

	
	locationChanging: function (data) {
		var loc = this.getLocation();
		var loc_data = data['location'];
		
		var is_changing = (
			(loc && (! loc_data)) ||
			((! loc) && loc_data) ||
			(this.getLatitude() != data['latitude']) ||
			(this.getLongitude() != data['longitude'])
		);
		
		return is_changing;
	},
	
	
	applyChanges: function (data) {
		var is_changed = false;
		var location_options = this.getLocationOptions();
		
		if (this.locationChanging(data)) {
			this.clearLocation();
		}
		
		var nearby_data = data.nearby;
		data.nearby = null;
		
		for (var name in data) {
			// Location values are objects, not 'scalars'; need special handling.
			if (name == 'location') {
				var new_loc_data = data[name]
				if (new_loc_data) {
					this.is_placeholder = false;
					is_changed = true;
					var new_loc = new ContinuousTraveler.Views.Location_Show(this.view(), location_options);
					new_loc.applyChanges(new_loc_data);
					this.setLocation(new_loc);
					
				} else {
					this.is_placeholder = true;
					is_changed = true;
					var new_loc = new ContinuousTraveler.Views.User_Placeholder_Loc(this, location_options);
					new_loc.applyChanges(new_loc_data);
					this.setLocation(new_loc);
				}

			} else if (this.attrs[name] != data[name]) {
				is_changed = true;
				this.attrs[name] = data[name];
			}
		} // end for
		
		
		if (! this.hasCoords()) {
			var coords = this.view().getCenter();
			this.setCoords(coords);
		}
		var loc = this.getLocation();
		if (loc && (! loc.hasCoords())) {
			var coords = this.getCoords();
			loc.setCoords(coords);
		}
		
		if (nearby_data && (nearby_data.length > 0)) {
			loc.update_nearby(nearby_data);
		} else {
			loc.clearSecondaryRecords();
		}
		
		return is_changed;
		
	} // applyChanges
	
	
}); // end ContinuousTraveler.Views.User_Show

////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.User_Placeholder_Loc = Class.create (ContinuousTraveler.Views.LocRecordBase, {
	
	initialize: function ($super, user, options) {
		$super(user.view(), options);
		this.user = user;
	},
	
	destroy: function ($super) {
		this.user = null;
		$super();
	},
	
	isValid: function ($super) {
		return ($super() && this.user);
	},
	
	getMarker: function ($super) {
		if (! $super()) {
			
			var marker_options = this.getMarkerOptions();
			var marker = new ContinuousTraveler.Views.Markers.UserNeedsLocation(this.user, marker_options);
			this.setMarker(marker);
		}
		return $super();
	},
	
	
	populateMarker: function () {
		var marker = this.getMarker();
		if (marker) {
			marker.populate();
		}
	},
	
	
	createSecondaryRecord: function (options) {
		return new ContinuousTraveler.Views.Location_Show(this.view(), options);
	}
	
	
	
	
}); // end ContinuousTraveler.Views.User_Placeholder_Loc


////////////////////////////////////////////////////////////////////////////////
////
//// Copyright © 2007-2008 Tom Donaldson, 2009 MacTom, Inc., tomd@mactom.com
////
////////////////////////////////////////////////////////////////////////////////

ContinuousTraveler.Views.User_Edit = Class.create (ContinuousTraveler.Views.User_Show, {
	
	initialize: function ($super, mapview, options) {
		if (! options) { options = {}; }
		options.is_editing = true;
		
		if (! options.location) { options.location = {}; }
		if (! options.location.marker) { options.location.marker = {}; }
		options.location.marker.draggable = true;
		
		$super(mapview, options);
		
		this.marker_drag_listener = null;
		this.setupMapViewControls();
	},
	
	
	
	destroy: function ($super) {
		this.clearMarkerListener();
		$super();
	},
	
	
	update: function (user_request, result_data) {
		this.setupMapViewControls();
		if (this.isValid()) {
			var action = user_request['act'];
			switch (action) {
				case 'edit':
					this.update_show(result_data);
					this.zoomForMoveOn();
					break;
				case 'update':
					this.update_show(result_data);
					var loc = this.getLocation();
					var coords = loc.getCoords();
					if (coords) {
						this.view().panTo(coords);
					}
					break;
			}; // end switch (action)
		}
		var observer = this;
		this.view().addListener(
			'click', 
			function (overlay, latlng) {
				return observer.setLatLng(overlay, latlng);
			}
		);
	}, // end update
	
	show: function ($super) {
		this.clearMarkerListener();
		$super();
		this.setupMarkerCallback();
	},
	
	
	
	clearMarkerListener: function () {
		var marker = this.getMarker();
		if (marker && this.marker_drag_listener) {
			marker.removeListener(this.marker_drag_listener);
		}
		this.marker_drag_listener = null;
	},
	
	setupMarkerCallback: function () {
		var marker = this.getMarker();
		
		if (marker) {
			var observer = this;
			this.marker_drag_listener = marker.addListener(
				'dragend',
				function (latlng) {
					return observer.markerDragged(latlng);
				}
			);
		}
	},
	
	
	
	markerDragged: function (latlng) {
		var coords = new ContinuousTraveler.Views.Geometry.Coords.prototype.fromMapPoint(latlng);

		var loc = null;
		if (this.needsLocation()) {
			loc = this.getLocation().getNearBy_ClosestWithin(10, coords);
		}
		
		if (loc) {
			this.requestSetLocationId(loc.getRecordId());
		} else {
			this.requestChangeCoordinates(coords);
		}
	},
	
	requestSetLocationId: function (location_id) {
		var command = 'user.change_location(' +
			location_id + ')';
		
		var request = {
			ctrl: 'users',
			act: 'update_edit',
			objid: 0,
			command: command
		};
		this.view().submitForm(request);
	},
	
	requestChangeCoordinates: function (coords) {
		
		var command = 'user.change_gps_coords(' +
			coords.getLatitude() + ',' +
			coords.getLongitude() + ')';
		
		var request = {
			ctrl: 'users',
			act: 'update_edit',
			objid: 0,
			command: command
		};
		this.view().submitForm(request);
