/*
    json.js
    2006-04-28

    This file adds these methods to JavaScript:

        object.toJSONString()

            This method produces a JSON text from an object. The
            object must not contain any cyclical references.

        array.toJSONString()

            This method produces a JSON text from an array. The
            array must not contain any cyclical references.

        string.parseJSON()

            This method parses a JSON text to produce an object or
            array. It will return false if there is an error.
*/
(function () {
    var m = {
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        },
        s = {
            array: function (x) {
                var a = ['['], b, f, i, l = x.length, v;
                for (i = 0; i < l; i += 1) {
                    v = x[i];
                    f = s[typeof v];
                    if (f) {
                        v = f(v);
                        if (typeof v == 'string') {
                            if (b) {
                                a[a.length] = ',';
                            }
                            a[a.length] = v;
                            b = true;
                        }
                    }
                }
                a[a.length] = ']';
                return a.join('');
            },
            'boolean': function (x) {
                return String(x);
            },
            'null': function (x) {
                return "null";
            },
            number: function (x) {
                return isFinite(x) ? String(x) : 'null';
            },
            object: function (x) {
                if (x) {
                    if (x instanceof Array) {
                        return s.array(x);
                    }
                    var a = ['{'], b, f, i, v;
                    for (i in x) {
                        v = x[i];
                        f = s[typeof v];
                        if (f) {
                            v = f(v);
                            if (typeof v == 'string') {
                                if (b) {
                                    a[a.length] = ',';
                                }
                                a.push(s.string(i), ':', v);
                                b = true;
                            }
                        }
                    }
                    a[a.length] = '}';
                    return a.join('');
                }
                return 'null';
            },
            string: function (x) {
                if (/["\\\x00-\x1f]/.test(x)) {
                    x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
                        var c = m[b];
                        if (c) {
                            return c;
                        }
                        c = b.charCodeAt();
                        return '\\u00' +
                            Math.floor(c / 16).toString(16) +
                            (c % 16).toString(16);
                    });
                }
                return '"' + x + '"';
            }
        };

    Object.prototype.toJSONString = function () {
        return s.object(this);
    };

    Array.prototype.toJSONString = function () {
        return s.array(this);
    };
})();

String.prototype.parseJSON = function () {
    try {
        return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
                this.replace(/"(\\.|[^"\\])*"/g, ''))) &&
            eval('(' + this + ')');
    } catch (e) {
        return false;
    }
};

/*  Prototype JavaScript framework, version 1.5.0_rc1
 *  (c) 2005 Sam Stephenson <sam@conio.net>
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://prototype.conio.net/
 *
/*--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.5.0_rc1',
  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',

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

var Class = {
  create: function() {
    return function() {
      this.initialize.apply(this, arguments);
    }
  }
}

var Abstract = new Object();

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 == undefined) return 'undefined';
      if (object == null) return 'null';
      return object.inspect ? object.inspect() : object.toString();
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

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

Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}

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

Object.extend(Number.prototype, {
  toColorPart: function() {
    var digits = this.toString(16);
    if (this < 16) return '0' + digits;
    return digits;
  },

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

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

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

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

    return returnValue;
  }
}

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

var PeriodicalExecuter = Class.create();
PeriodicalExecuter.prototype = {
  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);
  },

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

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.callback(this);
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
}
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 += (replacement(match) || '').toString();
        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 = count === undefined ? 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 this;
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = truncation === undefined ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : 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 div = document.createElement('div');
    var text = document.createTextNode(this);
    div.appendChild(text);
    return div.innerHTML;
  },

  unescapeHTML: function() {
    var div = document.createElement('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
  },

  toQueryParams: function() {
    var pairs = this.match(/^\??(.*)$/)[1].split('&');
    return pairs.inject({}, function(params, pairString) {
      var pair  = pairString.split('=');
      var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
      params[decodeURIComponent(pair[0])] = value;
      return params;
    });
  },

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

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

    var camelizedString = this.indexOf('-') == 0
      ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
      : oStringList[0];

    for (var i = 1, len = oStringList.length; i < len; i++) {
      var s = oStringList[i];
      camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
    }

    return camelizedString;
  },

  inspect: function(useDoubleQuotes) {
    var escapedString = this.replace(/\\/g, '\\\\');
    if (useDoubleQuotes)
      return '"' + escapedString.replace(/"/g, '\\"') + '"';
    else
      return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  }
});

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

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

var Template = Class.create();
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
Template.prototype = {
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern  = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    return this.template.gsub(this.pattern, function(match) {
      var before = match[1];
      if (before == '\\') return match[2];
      return before + (object[match[3]] || '').toString();
    });
  }
}

var $break    = new Object();
var $continue = new Object();

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

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

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

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

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

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

  grep: function(pattern, iterator) {
    var results = [];
    this.each(function(value, index) {
      var stringValue = value.toString();
      if (stringValue.match(pattern))
        results.push((iterator || Prototype.K)(value, index));
    })
    return results;
  },

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

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

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

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

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

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

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

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

  sortBy: function(iterator) {
    return this.collect(function(value, index) {
      return {value: value, criteria: iterator(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.collect(Prototype.K);
  },

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

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

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

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray
});
var $A = Array.from = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0; i < iterable.length; i++)
      results.push(iterable[i]);
    return results;
  }
}

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; i < this.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 != undefined || value != null;
    });
  },

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(value && value.constructor == Array ?
        value.flatten() : [value]);
    });
  },

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

  indexOf: function(object) {
    for (var i = 0; i < this.length; i++)
      if (this[i] == object) return i;
    return -1;
  },

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

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

  uniq: function() {
    return this.inject([], function(array, value) {
      return array.include(value) ? array : array.concat([value]);
    });
  },

  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  }
});
var Hash = {
  _each: function(iterator) {
    for (var key in this) {
      var value = this[key];
      if (typeof value == 'function') continue;

      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  },

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

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

  merge: function(hash) {
    return $H(hash).inject($H(this), function(mergedHash, pair) {
      mergedHash[pair.key] = pair.value;
      return mergedHash;
    });
  },

  toQueryString: function() {
    return this.map(function(pair) {
      return pair.map(encodeURIComponent).join('=');
    }).join('&');
  },

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

function $H(object) {
  var hash = Object.extend({}, object || {});
  Object.extend(hash, Enumerable);
  Object.extend(hash, Hash);
  return hash;
}
ObjectRange = Class.create();
Object.extend(ObjectRange.prototype, Enumerable);
Object.extend(ObjectRange.prototype, {
  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(responderToAdd) {
    if (!this.include(responderToAdd))
      this.responders.push(responderToAdd);
  },

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

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (responder[callback] && typeof responder[callback] == 'function') {
        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 = function() {};
Ajax.Base.prototype = {
  setOptions: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      parameters:   ''
    }
    Object.extend(this.options, options || {});
  },

  responseIsSuccess: function() {
    return this.transport.status == undefined
        || this.transport.status == 0
        || (this.transport.status >= 200 && this.transport.status < 300);
  },

  responseIsFailure: function() {
    return !this.responseIsSuccess();
  }
}

Ajax.Request = Class.create();
Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(url, options) {
    this.transport = Ajax.getTransport();
    this.setOptions(options);
    this.request(url);
  },

  request: function(url) {
    var parameters = this.options.parameters || '';
    if (parameters.length > 0) parameters += '&_=';

    /* Simulate other verbs over post */
    if (this.options.method != 'get' && this.options.method != 'post') {
      parameters += (parameters.length > 0 ? '&' : '') + '_method=' + this.options.method;
      this.options.method = 'post';
    }

    try {
      this.url = url;
      if (this.options.method == 'get' && parameters.length > 0)
        this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;

      Ajax.Responders.dispatch('onCreate', this, this.transport);

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

      if (this.options.asynchronous)
        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);

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

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

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

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

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

    if (this.options.method == 'post') {
      requestHeaders.push('Content-type', this.options.contentType);

      /* Force "Connection: close" for Mozilla browsers to work around
       * a bug where XMLHttpReqeuest sends an incorrect Content-length
       * header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType)
        requestHeaders.push('Connection', 'close');
    }

    if (this.options.requestHeaders)
      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);

    for (var i = 0; i < requestHeaders.length; i += 2)
      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
  },

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

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

  evalJSON: function() {
    try {
      return eval('(' + this.header('X-JSON') + ')');
    } catch (e) {}
  },

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

  respondToReadyState: function(readyState) {
    var event = Ajax.Request.Events[readyState];
    var transport = this.transport, json = this.evalJSON();

    if (event == 'Complete') {
      try {
        (this.options['on' + this.transport.status]
         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(transport, json);
      } catch (e) {
        this.dispatchException(e);
      }

      if ((this.header('Content-type') || '').match(/^text\/javascript/i))
        this.evalResponse();
    }

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

    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
    if (event == 'Complete')
      this.transport.onreadystatechange = Prototype.emptyFunction;
  },

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

Ajax.Updater = Class.create();

Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
  initialize: function(container, url, options) {
    this.containers = {
      success: container.success ? $(container.success) : $(container),
      failure: container.failure ? $(container.failure) :
        (container.success ? null : $(container))
    }

    this.transport = Ajax.getTransport();
    this.setOptions(options);

    var onComplete = this.options.onComplete || Prototype.emptyFunction;
    this.options.onComplete = (function(transport, object) {
      this.updateContent();
      onComplete(transport, object);
    }).bind(this);

    this.request(url);
  },

  updateContent: function() {
    var receiver = this.responseIsSuccess() ?
      this.containers.success : this.containers.failure;
    var response = this.transport.responseText;

    if (!this.options.evalScripts)
      response = response.stripScripts();

    if (receiver) {
      if (this.options.insertion) {
        new this.options.insertion(receiver, response);
      } else {
        Element.update(receiver, response);
      }
    }

    if (this.responseIsSuccess()) {
      if (this.onComplete)
        setTimeout(this.onComplete.bind(this), 10);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create();
Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(container, url, options) {
    this.setOptions(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(request) {
    if (this.options.decay) {
      this.decay = (request.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = request.responseText;
    }
    this.timer = setTimeout(this.onTimerEvent.bind(this),
      this.decay * this.frequency * 1000);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});
function $() {
  var results = [], element;
  for (var i = 0; i < arguments.length; i++) {
    element = arguments[i];
    if (typeof element == 'string')
      element = document.getElementById(element);
    results.push(Element.extend(element));
  }
  return results.reduce();
}

document.getElementsByClassName = function(className, parentElement) {
  var children = ($(parentElement) || document.body).getElementsByTagName('*');
  return $A(children).inject([], function(elements, child) {
    if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
      elements.push(Element.extend(child));
    return elements;
  });
}

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

if (!window.Element)
  var Element = new Object();

Element.extend = function(element) {
  if (!element) return;
  if (_nativeExtensions || element.nodeType == 3) return element;

  if (!element._extended && element.tagName && element != window) {
    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;

    if (element.tagName == 'FORM')
      Object.extend(methods, Form.Methods);
    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
      Object.extend(methods, Form.Element.Methods);

    for (var property in methods) {
      var value = methods[property];
      if (typeof value == 'function')
        element[property] = cache.findOrStore(value);
    }
  }

  element._extended = true;
  return element;
}

Element.extend.cache = {
  findOrStore: function(value) {
    return this[value] = this[value] || function() {
      return value.apply(null, [this].concat($A(arguments)));
    }
  }
}

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).style.display = 'none';
    return element;
  },

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

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

  update: function(element, html) {
    $(element).innerHTML = html.stripScripts();
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  },

  replace: function(element, html) {
    element = $(element);
    if (element.outerHTML) {
      element.outerHTML = html.stripScripts();
    } else {
      var range = element.ownerDocument.createRange();
      range.selectNodeContents(element);
      element.parentNode.replaceChild(
        range.createContextualFragment(html.stripScripts()), element);
    }
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  },

  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) {
    element = $(element);
    return $A(element.getElementsByTagName('*'));
  },

  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) {
    element = $(element);
    if (typeof selector == 'string')
      selector = new Selector(selector);
    return selector.match(element);
  },

  up: function(element, expression, index) {
    return Selector.findElement($(element).ancestors(), expression, index);
  },

  down: function(element, expression, index) {
    return Selector.findElement($(element).descendants(), expression, index);
  },

  previous: function(element, expression, index) {
    return Selector.findElement($(element).previousSiblings(), expression, index);
  },

  next: function(element, expression, index) {
    return Selector.findElement($(element).nextSiblings(), expression, index);
  },

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

  getElementsByClassName: function(element, className) {
    element = $(element);
    return document.getElementsByClassName(className, element);
  },

  getHeight: function(element) {
    element = $(element);
    return element.offsetHeight;
  },

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

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    return Element.classNames(element).include(className);
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element).add(className);
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element).remove(className);
    return element;
  },

  observe: function() {
    Event.observe.apply(Event, arguments);
    return $A(arguments).first();
  },

  stopObserving: function() {
    Event.stopObserving.apply(Event, arguments);
    return $A(arguments).first();
  },

  // 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.match(/^\s*$/);
  },

  childOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);
    while (element = element.parentNode)
      if (element == ancestor) return true;
    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var x = element.x ? element.x : element.offsetLeft,
        y = element.y ? element.y : element.offsetTop;
    window.scrollTo(x, y);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    var value = element.style[style.camelize()];
    if (!value) {
      if (document.defaultView && document.defaultView.getComputedStyle) {
        var css = document.defaultView.getComputedStyle(element, null);
        value = css ? css.getPropertyValue(style) : null;
      } else if (element.currentStyle) {
        value = element.currentStyle[style.camelize()];
      }
    }

    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
      if (Element.getStyle(element, 'position') == 'static') value = 'auto';

    return value == 'auto' ? null : value;
  },

  setStyle: function(element, style) {
    element = $(element);
    for (var name in style)
      element.style[name.camelize()] = style[name];
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    if (Element.getStyle(element, 'display') != 'none')
      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;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = '';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = 'none';
    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 (window.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._overflow = element.style.overflow || 'auto';
    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

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

// IE is missing .innerHTML support for TABLE-related elements
if(document.all){
  Element.Methods.update = function(element, html) {
    element = $(element);
    var tagName = element.tagName.toUpperCase();
    if (['THEAD','TBODY','TR','TD'].indexOf(tagName) > -1) {
      var div = document.createElement('div');
      switch (tagName) {
        case 'THEAD':
        case 'TBODY':
          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
          depth = 2;
          break;
        case 'TR':
          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
          depth = 3;
          break;
        case 'TD':
          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
          depth = 4;
      }
      $A(element.childNodes).each(function(node){
        element.removeChild(node)
      });
      depth.times(function(){ div = div.firstChild });

      $A(div.childNodes).each(
        function(node){ element.appendChild(node) });
    } else {
      element.innerHTML = html.stripScripts();
    }
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  }
}

Object.extend(Element, Element.Methods);

var _nativeExtensions = false;
if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
    var className = 'HTML' + tag + 'Element';
    if(window[className]) return;
    var klass = window[className] = {};
    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
  });

Element.addMethods = function(methods) {
  Object.extend(Element.Methods, methods || {});

  function copy(methods, destination) {
    var cache = Element.extend.cache;
    for (var property in methods) {
      var value = methods[property];
      destination[property] = cache.findOrStore(value);
    }
  }

  if (typeof HTMLElement != 'undefined') {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Form.Methods, HTMLFormElement.prototype);
    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
      copy(Form.Element.Methods, klass.prototype);
    });
    _nativeExtensions = true;
  }
}

var Toggle = new Object();
Toggle.display = Element.toggle;

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

Abstract.Insertion = function(adjacency) {
  this.adjacency = adjacency;
}

Abstract.Insertion.prototype = {
  initialize: function(element, content) {
    this.element = $(element);
    this.content = content.stripScripts();

    if (this.adjacency && this.element.insertAdjacentHTML) {
      try {
        this.element.insertAdjacentHTML(this.adjacency, this.content);
      } catch (e) {
        var tagName = this.element.tagName.toLowerCase();
        if (tagName == 'tbody' || tagName == 'tr') {
          this.insertContent(this.contentFromAnonymousTable());
        } else {
          throw e;
        }
      }
    } else {
      this.range = this.element.ownerDocument.createRange();
      if (this.initializeRange) this.initializeRange();
      this.insertContent([this.range.createContextualFragment(this.content)]);
    }

    setTimeout(function() {content.evalScripts()}, 10);
  },

  contentFromAnonymousTable: function() {
    var div = document.createElement('div');
    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
    return $A(div.childNodes[0].childNodes[0].childNodes);
  }
}

var Insertion = new Object();

Insertion.Before = Class.create();
Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
  initializeRange: function() {
    this.range.setStartBefore(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.parentNode.insertBefore(fragment, this.element);
    }).bind(this));
  }
});

Insertion.Top = Class.create();
Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
  initializeRange: function() {
    this.range.selectNodeContents(this.element);
    this.range.collapse(true);
  },

  insertContent: function(fragments) {
    fragments.reverse(false).each((function(fragment) {
      this.element.insertBefore(fragment, this.element.firstChild);
    }).bind(this));
  }
});

Insertion.Bottom = Class.create();
Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
  initializeRange: function() {
    this.range.selectNodeContents(this.element);
    this.range.collapse(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.appendChild(fragment);
    }).bind(this));
  }
});

Insertion.After = Class.create();
Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
  initializeRange: function() {
    this.range.setStartAfter(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.parentNode.insertBefore(fragment,
        this.element.nextSibling);
    }).bind(this));
  }
});

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

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(this.toArray().concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set(this.select(function(className) {
      return className != classNameToRemove;
    }).join(' '));
  },

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

Object.extend(Element.ClassNames.prototype, Enumerable);
var Selector = Class.create();
Selector.prototype = {
  initialize: function(expression) {
    this.params = {classNames: []};
    this.expression = expression.toString().strip();
    this.parseExpression();
    this.compileMatcher();
  },

  parseExpression: function() {
    function abort(message) { throw 'Parse error in selector: ' + message; }

    if (this.expression == '')  abort('empty expression');

    var params = this.params, expr = this.expression, match, modifier, clause, rest;
    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
      params.attributes = params.attributes || [];
      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
      expr = match[1];
    }

    if (expr == '*') return this.params.wildcard = true;

    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
      modifier = match[1], clause = match[2], rest = match[3];
      switch (modifier) {
        case '#':       params.id = clause; break;
        case '.':       params.classNames.push(clause); break;
        case '':
        case undefined: params.tagName = clause.toUpperCase(); break;
        default:        abort(expr.inspect());
      }
      expr = rest;
    }

    if (expr.length > 0) abort(expr.inspect());
  },

  buildMatchExpression: function() {
    var params = this.params, conditions = [], clause;

    if (params.wildcard)
      conditions.push('true');
    if (clause = params.id)
      conditions.push('element.id == ' + clause.inspect());
    if (clause = params.tagName)
      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
    if ((clause = params.classNames).length > 0)
      for (var i = 0; i < clause.length; i++)
        conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
    if (clause = params.attributes) {
      clause.each(function(attribute) {
        var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
        var splitValueBy = function(delimiter) {
          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
        }

        switch (attribute.operator) {
          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
          case '|=':      conditions.push(
                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
                          ); break;
          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
          case '':
          case undefined: conditions.push(value + ' != null'); break;
          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
        }
      });
    }

    return conditions.join(' && ');
  },

  compileMatcher: function() {
    this.match = new Function('element', 'if (!element.tagName) return false; \
      return ' + this.buildMatchExpression());
  },

  findElements: function(scope) {
    var element;

    if (element = $(this.params.id))
      if (this.match(element))
        if (!scope || Element.childOf(element, scope))
          return [element];

    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');

    var results = [];
    for (var i = 0; i < scope.length; i++)
      if (this.match(element = scope[i]))
        results.push(Element.extend(element));

    return results;
  },

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

Object.extend(Selector, {
  matchElements: function(elements, expression) {
    var selector = new Selector(expression);
    return elements.select(selector.match.bind(selector));
  },

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

  findChildElements: function(element, expressions) {
    return expressions.map(function(expression) {
      return expression.strip().split(/\s+/).inject([null], function(results, expr) {
        var selector = new Selector(expr);
        return results.inject([], function(elements, result) {
          return elements.concat(selector.findElements(result || element));
        });
      });
    }).flatten();
  }
});

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

Form.Methods = {
  serialize: function(form) {
    var elements = Form.getElements($(form));
    var queryComponents = new Array();

    for (var i = 0; i < elements.length; i++) {
      var queryComponent = Form.Element.serialize(elements[i]);
      if (queryComponent)
        queryComponents.push(queryComponent);
    }

    return queryComponents.join('&');
  },

  getElements: function(form) {
    form = $(form);
    var elements = new Array();

    for (var tagName in Form.Element.Serializers) {
      var tagElements = form.getElementsByTagName(tagName);
      for (var j = 0; j < tagElements.length; j++)
        elements.push(tagElements[j]);
    }
    return elements;
  },

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

    if (!typeName && !name)
      return inputs;

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

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    var elements = Form.getElements(form);
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      element.blur();
      element.disabled = 'true';
    }
    return form;
  },

  enable: function(form) {
    form = $(form);
    var elements = Form.getElements(form);
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      element.disabled = '';
    }
    return form;
  },

  findFirstElement: function(form) {
    return Form.getElements(form).find(function(element) {
      return element.type != 'hidden' && !element.disabled &&
        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

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

Object.extend(Form, Form.Methods);

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

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

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

Form.Element.Methods = {
  serialize: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    var parameter = Form.Element.Serializers[method](element);

    if (parameter) {
      var key = encodeURIComponent(parameter[0]);
      if (key.length == 0) return;

      if (parameter[1].constructor != Array)
        parameter[1] = [parameter[1]];

      return parameter[1].map(function(value) {
        return key + '=' + encodeURIComponent(value);
      }).join('&');
    }
  },

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

    if (parameter)
      return parameter[1];
  },

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

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

  activate: function(element) {
    element = $(element);
    element.focus();
    if (element.select)
      element.select();
    return element;
  },

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

  enable: function(element) {
    element = $(element);
    element.blur();
    element.disabled = 'true';
    return element;
  }
}

Object.extend(Form.Element, Form.Element.Methods);
var Field = Form.Element;

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

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

  inputSelector: function(element) {
    if (element.checked)
      return [element.name, element.value];
  },

  textarea: function(element) {
    return [element.name, element.value];
  },

  select: function(element) {
    return Form.Element.Serializers[element.type == 'select-one' ?
      'selectOne' : 'selectMany'](element);
  },

  selectOne: function(element) {
    var value = '', opt, index = element.selectedIndex;
    if (index >= 0) {
      opt = element.options[index];
      value = opt.value || opt.text;
    }
    return [element.name, value];
  },

  selectMany: function(element) {
    var value = [];
    for (var i = 0; i < element.length; i++) {
      var opt = element.options[i];
      if (opt.selected)
        value.push(opt.value || opt.text);
    }
    return [element.name, value];
  }
}

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

var $F = Form.Element.getValue;

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

Abstract.TimedObserver = function() {}
Abstract.TimedObserver.prototype = {
  initialize: function(element, frequency, callback) {
    this.frequency = frequency;
    this.element   = $(element);
    this.callback  = callback;

    this.lastValue = this.getValue();
    this.registerCallback();
  },

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

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

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

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

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

Abstract.EventObserver = function() {}
Abstract.EventObserver.prototype = {
  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() {
    var elements = Form.getElements(this.element);
    for (var i = 0; i < elements.length; i++)
      this.registerCallback(elements[i]);
  },

  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();
Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

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

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,

  element: function(event) {
    return event.target || event.srcElement;
  },

  isLeftClick: function(event) {
    return (((event.which) && (event.which == 1)) ||
            ((event.button) && (event.button == 1)));
  },

  pointerX: function(event) {
    return event.pageX || (event.clientX +
      (document.documentElement.scrollLeft || document.body.scrollLeft));
  },

  pointerY: function(event) {
    return event.pageY || (event.clientY +
      (document.documentElement.scrollTop || document.body.scrollTop));
  },

  stop: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
      event.stopPropagation();
    } else {
      event.returnValue = false;
      event.cancelBubble = true;
    }
  },

  // find the first node with the given tagName, starting from the
  // node the event was triggered on; traverses the DOM upwards
  findElement: function(event, tagName) {
    var element = Event.element(event);
    while (element.parentNode && (!element.tagName ||
        (element.tagName.toUpperCase() != tagName.toUpperCase())))
      element = element.parentNode;
    return element;
  },

  observers: false,

  _observeAndCache: function(element, name, observer, useCapture) {
    if (!this.observers) this.observers = [];
    if (element.addEventListener) {
      this.observers.push([element, name, observer, useCapture]);
      element.addEventListener(name, observer, useCapture);
    } else if (element.attachEvent) {
      this.observers.push([element, name, observer, useCapture]);
      element.attachEvent('on' + name, observer);
    }
  },

  unloadCache: function() {
    if (!Event.observers) return;
    for (var i = 0; i < Event.observers.length; i++) {
      Event.stopObserving.apply(this, Event.observers[i]);
      Event.observers[i][0] = null;
    }
    Event.observers = false;
  },

  observe: function(element, name, observer, useCapture) {
    element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.attachEvent))
      name = 'keydown';

    Event._observeAndCache(element, name, observer, useCapture);
  },

  stopObserving: function(element, name, observer, useCapture) {
    element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.detachEvent))
      name = 'keydown';

    if (element.removeEventListener) {
      element.removeEventListener(name, observer, useCapture);
    } else if (element.detachEvent) {
      try {
        element.detachEvent('on' + name, observer);
      } catch (e) {}
    }
  }
});

/* prevent memory leaks in IE */
if (navigator.appVersion.match(/\bMSIE\b/))
  Event.observe(window, 'unload', Event.unloadCache, false);
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;
  },

  realOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return [valueL, valueT];
  },

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

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        p = Element.getStyle(element, 'position');
        if (p == 'relative' || p == 'absolute') break;
      }
    } while (element);
    return [valueL, valueT];
  },

  offsetParent: 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;
  },

  // 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 = this.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 = this.realOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = this.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;
  },

  page: 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)
        if (Element.getStyle(element,'position')=='absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!window.opera || element.tagName=='BODY') {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return [valueL, valueT];
  },

  clone: function(source, target) {
    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 = Position.page(source);

    // find coordinate system to use
    target = $(target);
    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(target,'position') == 'absolute') {
      parent = Position.offsetParent(target);
      delta = Position.page(parent);
    }

    // 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)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
  },

  absolutize: function(element) {
    element = $(element);
    if (element.style.position == 'absolute') return;
    Position.prepare();

    var offsets = Position.positionedOffset(element);
    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';;
  },

  relativize: function(element) {
    element = $(element);
    if (element.style.position == 'relative') return;
    Position.prepare();

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

// Safari returns margins on body which is incorrect if the child is absolutely
// positioned.  For performance reasons, redefine Position.cumulativeOffset for
// KHTML/WebKit only.
if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
  Position.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 [valueL, valueT];
  }
}

Element.addMethods();
// script.aculo.us scriptaculous.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006

// Copyright (c) 2005 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.

var Scriptaculous = {
  Version: '1.6.4',
  require: function(libraryName) {
    // inserting via DOM fails in Safari 2.0, so brute force approach
    document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
  },
  load: function() {
    if((typeof Prototype=='undefined') ||
       (typeof Element == 'undefined') ||
       (typeof Element.Methods=='undefined') ||
       parseFloat(Prototype.Version.split(".")[0] + "." +
                  Prototype.Version.split(".")[1]) < 1.5)
       throw("script.aculo.us requires the Prototype JavaScript framework >= 1.5.0");

    $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] : 'effects,dragdrop,slider').split(',').each(
       function(include) { Scriptaculous.require(path+include+'.js') });
    });
  }
}

//Scriptaculous.load();
// script.aculo.us effects.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006

// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
//  Justin Palmer (http://encytemedia.com/)
//  Mark Pilgrim (http://diveintomark.org/)
//  Martin Bialasinki
// 
// See scriptaculous.js for full license.  

// 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(element, {fontSize: (percent/100) + 'em'});   
  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
}

Element.getOpacity = function(element){  
  var opacity;
  if (opacity = Element.getStyle(element, 'opacity'))  
    return parseFloat(opacity);  
  if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))  
    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
  return 1.0;  
}

Element.setOpacity = function(element, value){  
  element= $(element);  
  if (value == 1){
    Element.setStyle(element, { opacity: 
      (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
      0.999999 : 1.0 });
    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
      Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
  } else {  
    if(value < 0.00001) value = 0;  
    Element.setStyle(element, {opacity: value});
    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
     Element.setStyle(element, 
       { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
                 'alpha(opacity='+value*100+')' });  
  }
}  
 
Element.getInlineOpacity = function(element){  
  return $(element).style.opacity || '';
}  

Element.childrenWithClassName = function(element, className, findFirst) {
  var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
  var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) { 
    return (c.className && c.className.match(classNameRegExp));
  });
  if(!results) results = [];
  return results;
}

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

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

Array.prototype.call = function() {
  var args = arguments;
  this.each(function(f){ f.apply(this, args) });
}

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

var Effect = {
  _elementDoesNotExistError: {
    name: 'ElementDoesNotExistError',
    message: 'The specified DOM element does not exist, but is required for this effect to operate'
  },
  tagifyText: function(element) {
    if(typeof Builder == 'undefined')
      throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
      
    var tagifyStyle = 'position:relative';
    if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
    element = $(element);
    $A(element.childNodes).each( function(child) {
      if(child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            Builder.node('span',{style: tagifyStyle},
              character == ' ' ? String.fromCharCode(160) : character), 
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if(((typeof element == 'object') || 
        (typeof element == 'function')) && 
       (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);
  }
};

var Effect2 = Effect; // deprecated

/* ------------- transitions ------------- */

Effect.Transitions = {}

Effect.Transitions.linear = Prototype.K;

Effect.Transitions.sinoidal = function(pos) {
  return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse  = function(pos) {
  return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
  return (Math.floor(pos*10) % 2 == 0 ? 
    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
  return 0;
}
Effect.Transitions.full = function(pos) {
  return 1;
}

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

Effect.ScopedQueue = Class.create();
Object.extend(Object.extend(Effect.ScopedQueue.prototype, 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 = (typeof effect.options.queue == 'string') ? 
      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 '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), 40);
  },
  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();
    this.effects.invoke('loop', timePos);
  }
});

Effect.Queues = {
  instances: $H(),
  get: function(queueName) {
    if(typeof queueName != 'string') return queueName;
    
    if(!this.instances[queueName])
      this.instances[queueName] = new Effect.ScopedQueue();
      
    return this.instances[queueName];
  }
}
Effect.Queue = Effect.Queues.get('global');

Effect.DefaultOptions = {
  transition: Effect.Transitions.sinoidal,
  duration:   1.0,   // seconds
  fps:        25.0,  // max. 25fps due to Effect.Queue implementation
  sync:       false, // true for combining
  from:       0.0,
  to:         1.0,
  delay:      0.0,
  queue:      'parallel'
}

Effect.Base = function() {};
Effect.Base.prototype = {
  position: null,
  start: function(options) {
    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.event('beforeStart');
    if(!this.options.sync)
      Effect.Queues.get(typeof this.options.queue == 'string' ? 
        '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.finishOn - this.startOn);
      var frame = Math.round(pos * this.options.fps * this.options.duration);
      if(frame > this.currentFrame) {
        this.render(pos);
        this.currentFrame = frame;
      }
    }
  },
  render: function(pos) {
    if(this.state == 'idle') {
      this.state = 'running';
      this.event('beforeSetup');
      if(this.setup) this.setup();
      this.event('afterSetup');
    }
    if(this.state == 'running') {
      if(this.options.transition) pos = this.options.transition(pos);
      pos *= (this.options.to-this.options.from);
      pos += this.options.from;
      this.position = pos;
      this.event('beforeUpdate');
      if(this.update) this.update(pos);
      this.event('afterUpdate');
    }
  },
  cancel: function() {
    if(!this.options.sync)
      Effect.Queues.get(typeof this.options.queue == 'string' ? 
        '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() {
    return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
  }
}

Effect.Parallel = Class.create();
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
  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.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    if(!this.element) throw(Effect._elementDoesNotExistError);
    // make this work on IE on elements without 'layout'
    if(/MSIE/.test(navigator.userAgent) && !window.opera && (!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();
Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
  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() {
    // Bug in Opera: Opera returns the "real" position of a static element or
    // relative element that does not have top/left explicitly set.
    // ==> Always set top and left for position relative elements in your stylesheets 
    // (to 0 if you do not need them) 
    this.element.makePositioned();
    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
    if(this.options.mode == 'absolute') {
      // absolute movement, so we need to calc deltaX and deltaY
      this.options.x = this.options.x - this.originalLeft;
      this.options.y = this.options.y - this.originalTop;
    }
  },
  update: function(position) {
    this.element.setStyle({
      left: Math.round(this.options.x  * position + this.originalLeft) + 'px',
      top:  Math.round(this.options.y  * position + this.originalTop)  + '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();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
  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 = Math.round(width) + 'px';
    if(this.options.scaleY) d.height = Math.round(height) + '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();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
  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 = {
      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+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
  },
  finish: function() {
    this.element.setStyle(Object.extend(this.oldStyle, {
      backgroundColor: this.options.restorecolor
    }));
  }
});

Effect.ScrollTo = Class.create();
Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    this.start(arguments[1] || {});
  },
  setup: function() {
    Position.prepare();
    var offsets = Position.cumulativeOffset(this.element);
    if(this.options.offset) offsets[1] += this.options.offset;
    var max = window.innerHeight ? 
      window.height - window.innerHeight :
      document.body.scrollHeight - 
        (document.documentElement.clientHeight ? 
          document.documentElement.clientHeight : document.body.clientHeight);
    this.scrollStart = Position.deltaY;
    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
  },
  update: function(position) {
    Position.prepare();
    window.scrollTo(Position.deltaX, 
      this.scrollStart + (position*this.delta));
  }
});

/* ------------- 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();
    effect.element.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);
    effect.element.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();
         effect.effects[0].element.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();
        effect.element.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();
      effect.element.setStyle({height: '0px'});
      effect.element.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();
          effect.element.makeClipping();
        },
        afterFinishInternal: function(effect) {
          effect.element.hide();
          effect.element.undoClipping();
          effect.element.undoPositioned();
          effect.element.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();
          effect.effects[0].element.undoPositioned();
          effect.effects[0].element.setStyle(oldStyle);
        } 
      }, arguments[1] || {}));
}

Effect.Shake = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left') };
    return new Effect.Move(element, 
      { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
        effect.element.undoPositioned();
        effect.element.setStyle(oldStyle);
  }}) }}) }}) }}) }}) }});
}

Effect.SlideDown = function(element) {
  element = $(element);
  element.cleanWhitespace();
  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
  var oldInnerBottom = $(element.firstChild).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.firstChild.makePositioned();
      if(window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping();
      effect.element.setStyle({height: '0px'});
      effect.element.show(); },
    afterUpdateInternal: function(effect) {
      effect.element.firstChild.setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping(); 
      // IE will crash if child is undoPositioned first
      if(/MSIE/.test(navigator.userAgent) && !window.opera){
        effect.element.undoPositioned();
        effect.element.firstChild.undoPositioned();
      }else{
        effect.element.firstChild.undoPositioned();
        effect.element.undoPositioned();
      }
      effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
    }, arguments[1] || {})
  );
}

Effect.SlideUp = function(element) {
  element = $(element);
  element.cleanWhitespace();
  var oldInnerBottom = $(element.firstChild).getStyle('bottom');
  return new Effect.Scale(element, window.opera ? 0 : 1,
   Object.extend({ scaleContent: false, 
    scaleX: false, 
    scaleMode: 'box',
    scaleFrom: 100,
    restoreAfterFinish: true,
    beforeStartInternal: function(effect) {
      effect.element.makePositioned();
      effect.element.firstChild.makePositioned();
      if(window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping();
      effect.element.show(); },  
    afterUpdateInternal: function(effect) {
      effect.element.firstChild.setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' }); },
    afterFinishInternal: function(effect) {
      effect.element.hide();
      effect.element.undoClipping();
      effect.element.firstChild.undoPositioned();
      effect.element.undoPositioned();
      effect.element.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(effect.element); },  
      afterFinishInternal: function(effect) {
        effect.element.hide(effect.element); 
        effect.element.undoClipping(effect.element); }
  });
}

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();
      effect.element.makeClipping();
      effect.element.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'});
               effect.effects[0].element.show(); 
             },
             afterFinishInternal: function(effect) {
               effect.effects[0].element.undoClipping();
               effect.effects[0].element.undoPositioned();
               effect.effects[0].element.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();
           effect.effects[0].element.makeClipping(); },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide();
           effect.effects[0].element.undoClipping();
           effect.effects[0].element.undoPositioned();
           effect.effects[0].element.setStyle(oldStyle); }
       }, options)
  );
}

Effect.Pulsate = function(element) {
  element = $(element);
  var options    = arguments[1] || {};
  var oldOpacity = element.getInlineOpacity();
  var transition = options.transition || Effect.Transitions.sinoidal;
  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
  reverser.bind(transition);
  return new Effect.Opacity(element, 
    Object.extend(Object.extend({  duration: 3.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(element);
  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();
        effect.element.undoClipping(); 
        effect.element.setStyle(oldStyle);
      } });
  }}, arguments[1] || {}));
};

['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( 
  function(f) { Element.Methods[f] = Element[f]; }
);

Element.Methods.visualEffect = function(element, effect, options) {
  s = effect.gsub(/_/, '-').camelize();
  effect_class = s.charAt(0).toUpperCase() + s.substring(1);
  new Effect[effect_class](element, options);
  return $(element);
};

Element.addMethods();
/*
 * CCallWrapper.js
 * $Revision: 1.3 $ $Date: 2003/07/07 18:32:43 $
 */

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Netscape code.
 *
 * The Initial Developer of the Original Code is
 * Netscape Corporation.
 * Portions created by the Initial Developer are Copyright (C) 2003
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s): Bob Clary <bclary@netscape.com>
 *
 * ***** END LICENSE BLOCK ***** */

function CCallWrapper(aObjectReference, 
                      aDelay,
                      aMethodName, 
                      aArgument0,
                      aArgument1,
                      aArgument2,
                      aArgument3,
                      aArgument4,
                      aArgument5,
                      aArgument6,
                      aArgument7,
                      aArgument8,
                      aArgument9
                      )
{
  this.mId = 'CCallWrapper_' + (CCallWrapper.mCounter++);
  this.mObjectReference = aObjectReference;
  this.mDelay     = aDelay;
  this.mTimerId = 0;
  this.mMethodName = aMethodName;
  this.mArgument0 = aArgument0;
  this.mArgument1 = aArgument1;
  this.mArgument2 = aArgument2;
  this.mArgument3 = aArgument3;
  this.mArgument4 = aArgument4;
  this.mArgument5 = aArgument5;
  this.mArgument6 = aArgument6;
  this.mArgument7 = aArgument7;
  this.mArgument8 = aArgument8;
  this.mArgument9 = aArgument9;
  CCallWrapper.mPendingCalls[this.mId] = this;
}

CCallWrapper.prototype.execute = function()
{
  this.mObjectReference[this.mMethodName](this.mArgument0,
                                          this.mArgument1,
                                          this.mArgument2,
                                          this.mArgument3,
                                          this.mArgument4,
                                          this.mArgument5,
                                          this.mArgument6,
                                          this.mArgument7,
                                          this.mArgument8,
                                          this.mArgument9
                                          );
  delete CCallWrapper.mPendingCalls[this.mId];
};

CCallWrapper.prototype.cancel = function()
{
  clearTimeout(this.mTimerId);
  delete CCallWrapper.mPendingCalls[this.mId];
};

CCallWrapper.asyncExecute = function (/* CCallWrapper */ callwrapper)
{
  CCallWrapper.mPendingCalls[callwrapper.mId].mTimerId = setTimeout('CCallWrapper.mPendingCalls["' + callwrapper.mId + '"].execute()', callwrapper.mDelay);
};

CCallWrapper.mCounter = 0;
CCallWrapper.mPendingCalls = {};

/**
*	Hook an object into an event, allowing objects to act as DOM multi event handlers
*
*	@access public
*	@param The object to act as the handler
*	@param The object method to handle the event
*	@return Returns a function to handle the event, by calling SyObj.SyMethod
*/
function SycuseHookObjEvent(SyObj, SyMethod)
{
	return (function (evt)
	{
		// IE compatible hack
		if (!evt)
			evt = window.event;

		return SyObj[SyMethod](evt, this);
	});
}




/**
*	Sycuse JS Event Handler
*
*	Abstraction layer to provide cross platform event handling
*
*	@access public
*	@deprecated
*/
function SycuseJSEvents(evt)
{
	this.event = evt;

	/**
	*	Stop event propagation, and prevent default action
	*
	*	@access public
	*	@return void
	*/
	this.haltEvent = function ()
	{
		if (window.event)
		{
			window.event.cancelBubble = true;
			window.event.returnValue = false;
		}

		if (this.event)
		{
			if (this.event.stopPropagation)
				this.event.stopPropagation();

			if (this.event.preventDefault)
				this.event.preventDefault();
		}
	}


	/**
	*	Return the element that fired the event
	*
	*	@access public
	*	@return element The element, or null if it cannot be retrieved
	*/
	this.getTarget = function ()
	{
		if (window.event && window.event.srcElement)
		{
			return window.event.srcElement;
		}

		if (this.event.target)
		{
			return this.event.target;
		}

		return null;
	}

}



/**
*	Add DOM event listener
*
*	@access public
*	@param obj DOM object to add event to
*	@param evtName Name of trigger event
*	@param fHandler Handler function
*	@param doCapt Bool, whether to capture event
*/
function SycuseAddEvent (obj, evtName, fHandler, doCapt)
{
	if (obj.addEventListener)
	{
		obj.addEventListener(evtName, fHandler, doCapt);
		return true;
	}
	else if (obj.attachEvent)
	{
		var r = obj.attachEvent("on"+evtName, fHandler);
		return r;
	}
	else
		return false;
}


/**
 * Determine whether a node's text content is entirely whitespace.
 * Courtesy of Mozilla Foundation; http://www.mozilla.org/docs/dom/technote/whitespace/
 */
function Sycuseis_all_ws( nod )
{
  // Use ECMA-262 Edition 3 String and RegExp features
  return !(/[^\t\n\r ]/.test(nod.data));
}

/**
 * Determine if a node should be ignored by the iterator functions.
 * Courtesy of Mozilla Foundation; http://www.mozilla.org/docs/dom/technote/whitespace/
 */

function Sycuseis_ignorable( nod )
{
  return ( nod.nodeType == 8) || // A comment node
         ( (nod.nodeType == 3) && Sycuseis_all_ws(nod) ); // a text node, all ws
}


/**
*	Wrapper for node.firstChild which ignores whitespace nodes
*	Courtesy of Mozilla Foundation; http://www.mozilla.org/docs/dom/technote/whitespace/
*/
function SycuseFirstChild( par )
{
	var res=par.firstChild;
	while (res) {
	if (!Sycuseis_ignorable(res)) return res;
	res = res.nextSibling;
	}
	return null;
}


// Courtesy of quirksmode.org
function SycuseWindowSize()
{
	var x,y;
	if (window.innerHeight) // all except Explorer
	{
		x = window.innerWidth;
		y = window.innerHeight;
	}
	else if (document.documentElement && document.documentElement.clientHeight)
		// Explorer 6 Strict Mode
	{
		x = document.documentElement.clientWidth;
		y = document.documentElement.clientHeight;
	}
	else if (document.body) // other Explorers
	{
		x = document.body.clientWidth;
		y = document.body.clientHeight;
	}

	this.x = x;
	this.y = y;
}

SycuseWindowSize.prototype.getX = function () { return this.x; }
SycuseWindowSize.prototype.getY = function () { return this.y; }


/* For attaching confirmations to buttons */
function SycuseAttachConfirm(elemid)
{
	var elem;
	if (elem = $(elemid))
		elem.onclick = SycuseHookObjEvent(this, "validate");

}

SycuseAttachConfirm.prototype.validate = function (evt, elem)
{
	return confirm("Are you sure you want to delete this item?");
}

function SycuseBind(what, obj, handler, event)
{
	if (event == undefined)
		event = "click";

	Event.observe(what, event, handler.bindAsEventListener(obj), false);
}

function SycuseValJumpTo(elemid)
{
	if (this.elem = document.getElementById(elemid))
	{
	 	this.scrollto(this.elem);

		if (this.elem.select)
			this.elem.select();
	 	this.elem.focus();
	}
	else
		if (elem = document.getElementById('SFc_'+elemid))
			this.scrollto(elem);


}


SycuseValJumpTo.prototype.scrollto = function(elem)
{

	Position.prepare();
	var loc = Position.cumulativeOffset(elem);

	ss = new SycuseWindowSize();
	loc[1] = loc[1] - (ss.getY() / 2);

	if (loc[1] > 0)
		window.scrollTo(0, loc[1]);

}

// Courtesy of webreference.com

function ImagePreloader(images)
{
   // initialize internal state.
   this.nLoaded = 0;
   this.nProcessed = 0;
   this.aImages = new Array;

   // record the number of images.
   this.nImages = images.length;

   // for each image, call preload()
   for ( var i = 0; i < images.length; i++ )
      this.preload(images[i]);
}
ImagePreloader.prototype.preload = function(image)
{
   // create new Image object and add to array
   var oImage = new Image;
   this.aImages.push(oImage);

   // set up event handlers for the Image object
   oImage.onload = ImagePreloader.prototype.onload;
   oImage.onerror = ImagePreloader.prototype.onerror;
   oImage.onabort = ImagePreloader.prototype.onabort;

   // assign pointer back to this.
   oImage.oImagePreloader = this;
   oImage.bLoaded = false;

   // assign the .src property of the Image object
   oImage.src = image;
}
ImagePreloader.prototype.onComplete = function()
{
   this.nProcessed++;
   if ( this.nProcessed == this.nImages )
   {
   	// All done
   }
}
ImagePreloader.prototype.onload = function()
{
   this.bLoaded = true;
   this.oImagePreloader.nLoaded++;
   this.oImagePreloader.onComplete();
}
ImagePreloader.prototype.onerror = function()
{
   this.bError = true;
   this.oImagePreloader.onComplete();
}
ImagePreloader.prototype.onabort = function()
{
   this.bAbort = true;
   this.oImagePreloader.onComplete();
}


/*
	Flyout modeless dialog box

	Events:
		onLoadContent
		onShow
*/
function SycuseFlyOut(container, trigger, title, width, height)
{
	this.closeonclick = true;
	this.centered = false;

	this.swidth = (width) ? width : 250;
	this.sheight = (height) ? height : 200;

	this.container = container;

	// Render container (shadow?)
	var shadow = document.createElement('div');
	shadow.className = "flyoutholder";
	shadow.style.display = "none";
	shadow.style.position = "absolute";
	shadow.style.backgroundColor = "#eee";
	this.shadow = shadow;

	// Render heading element
	var heading = document.createElement("h2");

	this.closeButton = document.createElement("img");
	this.closeButton.title = 'Close';
	this.closeButton.style.styleFloat = this.closeButton.style.cssFloat = 'right';
	this.closeButton.style.cursor = 'pointer';
	this.closeButton.src = '/js/flyout-close.gif';

	heading.appendChild(this.closeButton);

	//heading.innerHTML = title;
	heading.appendChild(document.createTextNode(title));

	shadow.appendChild(heading);

	// Render content
	this.elem = document.createElement('div');
	this.elem.className = "flyoutinner";
	this.elem.style.display = "block";
	this.elem.style.position = "absolute";
	this.elem.style.margin = "3px 3px 3px 3px";
	this.elem.style.backgroundColor = "#fff";
	//this.elem.style.border = "1px outset #777";
	shadow.appendChild(this.elem);

	// Public property: canvas - client code fills this with content
	this.canvas = this.elem;
	this.visible = false;
	this.contentLoaded = false;

	container.appendChild(shadow);

	if (trigger)
	{
		Event.observe(trigger, "click", this.ToggleShow.bindAsEventListener(this), false);
		trigger.style.display = "inline";
	}

	// Clicks inside the flyout must be trapped
	//Event.observe(shadow, "click", function (evt) { Event.stop(evt); }, false);
	Event.observe(shadow, "keyup", this.onKeyUp.bindAsEventListener(this), false);

}
	SycuseFlyOut.prototype.onKeyUp = function (evt)
	{
		Event.stop(evt);

		if (evt.keyCode == Event.KEY_ESC)
			return this.hide();

		// Pass on event
		if (this.onkeyup)
			this.onkeyup(evt);
	}


	SycuseFlyOut.prototype.ToggleShow = function (evt)
	{
		if (this.visible)
		{
			var _self = this;
			Effect.Fade(this.realshadow, {duration: .1, afterFinish: function () { _self.killShadow()} });
			Effect.Fade(this.shadow, {duration: .1});
			//this.shadow.style.display = "none";
			//Element.removeClassName(this.trigger, "dismiss");
			this.visible = false;
		}
		else
		{
			var top = Event.pointerY(evt)+15;
			var left = Event.pointerX(evt);

			this.show(top, left);
		}


		Event.stop(evt);

		return true;
	}

	SycuseFlyOut.prototype.loadContent = function ()
	{
		// Integral close button
		Event.observe(this.closeButton, "click", this.hide.bindAsEventListener(this), false);

		this.contentLoaded = true;

		// Pass on event
		if (this.onLoadContent)
			this.onLoadContent();

		var elems = this.canvas.getElementsByTagName('input');
		for (var x = 0; x < elems.length; x++)
		{
			if (!this.lastfocus)
				this.lastfocus = elems[x];

			elems[x].onfocus = this.rememberFocus.bindAsEventListener(this);
		}
	}

	SycuseFlyOut.prototype.clearContent = function ()
	{
		this.hide();
		this.contentLoaded = false;
		this.canvas.innerHTML = '';
	}

	SycuseFlyOut.prototype.updateContent = function (html)
	{
		this.canvas.innerHTML = html;
	}

	SycuseFlyOut.prototype.rememberFocus = function (evt)
	{
		this.lastfocus = Event.element(evt);
	}

	SycuseFlyOut.prototype.show = function (top, left)
	{
			this.visible = true;

			if (!this.contentLoaded)
				this.loadContent();

			// Fire the onShow event
			if (this.onShow)
				this.onShow();

			if (this.closeonclick)
				Event.observe(document, "click", this.onCapturedClick.bindAsEventListener(this), false);

			cwidth = this.swidth;
			cheight = this.sheight;

			if (this.centered)
			{
				ws = new SycuseWindowSize;
				left = Math.max(0, (ws.getX() - cwidth) / 2);
				top = Math.max(0, (ws.getY() - cheight) / 2);
			}


			// Is the flyout really close to the RHS edge of the viewport?
			if (!Position.within(document.body, left+cwidth, 10))
				left = left - cwidth;

			this.shadow.style.top = top+"px";
			this.shadow.style.left = left+"px";

			this.shadow.style.width = cwidth+"px";
			this.shadow.style.height = (cheight+20)+"px";

			this.elem.style.width = (cwidth-10)+"px";
			this.elem.style.height = (cheight-10)+"px";

			realshadow = this.shadow.cloneNode(false);
			realshadow.style.backgroundColor = "#bbb";
			realshadow.style.border = "none";
			realshadow.style.padding = "0";
			realshadow.style.margin = "0";
			realshadow.style.top = top+5+"px";
			realshadow.style.left = left+5+"px";
			realshadow.style.zIndex = 999;
			this.realshadow = realshadow;

			this.container.appendChild(realshadow);


			// Display it
			//this.shadow.style.display = "block";
			var _this = this;
			Effect.Appear(this.shadow, {duration: .2, afterFinish: function () { if (_this.lastfocus) { _this.lastfocus.focus(); _this.lastfocus.select(); } } });
			Effect.Appear(realshadow, {duration: .2, to: 0.7});

			// And update our trigger anchor style
	//		Element.addClassName(this.trigger, "dismiss");

	}

	SycuseFlyOut.prototype.onCapturedClick = function (evt)
	{
		var elem = Event.element(evt);

		while (elem = elem.parentNode)
		{
			// The clicked element is within our flyout, so we capture it.
			if (elem == this.shadow)
				return true;
		}

		// Not captured, so we close
		this.hide();
	}

	SycuseFlyOut.prototype.hide = function(evt)
	{
		// FIXME: borken
		if (this.closeonclick)
			Event.stopObserving(document, "click", this.hide.bindAsEventListener(this), false);

		this.killShadow();
		this.shadow.style.display = "none";
		this.visible = false;
	}

	SycuseFlyOut.prototype.destroy = function()
	{
		this.killShadow();
		this.shadow.parentNode.removeChild(this.shadow);
	}

	SycuseFlyOut.prototype.killShadow = function ()
	{
		if (this.realshadow)
		{
			this.container.removeChild(this.realshadow);
			this.realshadow = null;
		}
	}




function showAdImg()
{
	ws = new SycuseWindowSize;
	if ($("adimg") && ws.getX() > 950)
		$("adimg").style.display = 'inline';
}

var crsearch;
var tagger;
function CycleRoutes(route)
{
	this.lng = 0;
	this.lat = 15;
	this.zoom = 2;
	this.plotting = false;
	var cPath;
	this.editable = false;
	this.saveInProgress = false;
	this.pinger = null;
	this.mode = "new";
	this.ignoreMouseMovements = false;
	this.visible = 0;
	this.oneroutemode = true;
	this.isloggedin = 0;

	this.timesince = 0;

	var initialview;

	if ($('waypoints'))
		$('waypoints').style.display = 'none';

//	$("frmRoutename").style.display="none";

	if (route)
	{
//		if (route.mode == 'finder')
//			this.enableFinder(route);
//		else


			if (route.embed)
				this.showEmbeddedMapRoute(route);
			else
			{
				this.showMapRoute(route);
				//this.googlead = new CRGoogleAd(this.map);
			}

			if (route.selectPointAtKilometre)
				this.cPath.gotoPointNearKilometre(route.selectPointAtKilometre);
	}


	//setTimeout('showAdImg()', 1);
}
	CycleRoutes.prototype.showEmbeddedMapRoute = function (route)
	{
		this.embed = true;
		this.bind(window, this.resizeToFit, "resize");
		this.resizeToFit();
		if (GBrowserIsCompatible())
		{
			_mSvgEnabled = true;
			_mSvgForced  = true;
			this.map = new GMap2(document.getElementById("map"), {draggableCursor: 'crosshair', logoPassive: true});
			this.map.addControl(new GSmallZoomControl(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(1,1)));
			this.map.addControl(new GMapTypeControl(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(21,0)));

			//console.log(this.map.va[1]);
			if (this.map.va)
			{
				var glogo = this.map.va[1];
				if (glogo)
				{
					var pos = new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(1,1));
					pos.apply(glogo.element);
				}
			}

			this.cPath = new CRPath(this.map);
			this.status = new CRStatus(this.cPath);
			this.cPath.status = this.status;

			this.map.setCenter(new GLatLng(this.lat, this.lng), this.zoom);

			this.loadExistingRoute(route);

			GEvent.bind(this.map, "movestart", this, this.onMapMoveStart);
			GEvent.bind(this.map, "moveend", this, this.onMapMoveEnd);

			this.doWatchMouse();
			GEvent.bind(this.map, "mouseout", this, this.onMapMouseOut);
		}

	}

	CycleRoutes.prototype.showMapRoute = function(route)
	{
		this.embed = false;
		this.bind(window, this.resizeToFit, "resize");
		this.resizeToFit();
		if (GBrowserIsCompatible())
		{
			_mSvgEnabled = true;
			_mSvgForced  = true;
			this.map = new GMap2(document.getElementById("map"), {draggableCursor: 'crosshair'});
			this.map.addControl(new GLargeMapControl(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(4,30)));
			this.map.addControl(new GMapTypeControl());
			this.map.addControl(new GScaleControl());
			this.map.enableScrollWheelZoom();


			// GMap keyboard bindings
			new GKeyboardHandler(this.map);

			// Our keyboard bindings
			this.bind(document, this.onKeyUp, "keyup");

			// The path
			this.cPath = new CRPath(this.map);

			// Status Pane
			this.status = new CRStatus(this.cPath);
			this.cPath.status = this.status;

			// Search tool
			// Global variable... yikes!
			crsearch = new CRSearch(this);

			this.map.setCenter(new GLatLng(this.lat, this.lng), this.zoom);

			if (route)
			{
				// Favorites
				if (route.isfave !== undefined)
					this.faves = new CRFaves(route);

				this.isloggedin = route.isloggedin;

				if (route.mode == "edit")
					this.loadExistingRoute(route);
				else
				{
					// Creating a new route
					this.editable = true;

					if (route.cityid)
					{
						this.setMapType(readCookie("m"));
						this.status.setLocale(route.cityid, route.locale);

						if (route.lat && route.lng)
							this.map.setCenter(new GLatLng(route.lat, route.lng), 12);
					}

					if (route.pathData)
					{
						this.loadImportedPoints(route);
					}

					this.status.showCalipers();
				}

			}

			this.cPath.editable = this.editable;

			if (this.editable)
			{
				// Tagging
				tagger = new CRTagging;

				if (this.mode == "edit")
				{
					if (!route.cityid)
					{
						// If not located, start on Locate tab
						var startingtab = 'Locate';
					}
					else
						var startingtab = 'Draw';


					this.status.selectPoint(0);


				}
				else
				{
					// Creating a new route
					var startingtab = 'Locate';

					if (/Safari/.test(navigator.userAgent))
					{
						alert("Some Safari versions have trouble saving routes. Please test that you can save your route before you spend a long time working on it!");
					}
				}

				this.setupDrawingButtons();

			}
			else
			{
				// Viewing someone elses route

				rater = new CRRatings(route);
				var startingtab = 'Explore';

			}

			var autoshowhelp = (this.mode == "new" && route.firsttimer == 1);
			this.maptabs = new CRMapTabs(this.status, this.map, this.editable, autoshowhelp);
			this.maptabs.selectTab(startingtab);

			this.sharemenu = new SycuseSocialBookmark('sharemenu', route.visible == 1 ? 1 : 0);
			this.setupCommentsLink(route.visible, route.pathid);


			// Start our periodic pinging
			if (this.editable)
			{
				this.pinger = new PeriodicalExecuter(this.pingServer.bindAsEventListener(this), 300);
				this.routesettings = new RouteSettings(this);
			}


			if (this.oneroutemode)
			{
				GEvent.bind(this.map, "click", this, this.onMapClick);
				GEvent.bind(this.map, "movestart", this, this.onMapMoveStart);
				GEvent.bind(this.map, "moveend", this, this.onMapMoveEnd);

				this.doWatchMouse();
				GEvent.bind(this.map, "mouseout", this, this.onMapMouseOut);
			}

			var athis = this;
			GEvent.addListener(this.map, "maptypechanged", function () { createCookie("m", athis.map.getCurrentMapType().getUrlArg(), 365); } );





			// UI bindings : FIXME moveto status or tabs

			this.bind("btnElevation", this.onElevationClick);

			this.bind("btnPFirst", this.onPointNavClick);
			this.bind("btnPPrev", this.onPointNavClick);
			this.bind("btnPNext", this.onPointNavClick);
			this.bind("btnPLast", this.onPointNavClick);



			if ($("btnStartTour"))
				this.bind("btnStartTour", this.onPointNavClick);

			this.bind("btnResetView", this.onResetClick);

			this.foNearby = new SycuseFlyOut(document.body, $("btnNearby"), "Nearby Routes", 370, 180);
			this.foNearby.onLoadContent = this.getNearbyRoutes.bindAsEventListener(this);

			//this.bind("selCity", this.onCityChange, "change");


		}
	}

	CycleRoutes.prototype.setupCommentsLink = function (visible, routeid)
	{
		var link = $('btnCommentOnRoute');
		if (!visible || link.href == '#')
		{
			this.commentstrigger = function () { alert("You must make this route publically visible before commenting on it."); }
			Event.observe(link, "click", this.commentstrigger, false);
		}
		else if (visible && link.href == 'javascript://')
			link.href = '/forum/post/category/3/?routeid='+routeid;
	}

	CycleRoutes.prototype.updateCommentsLink = function (routeid)
	{
		if (this.commentstrigger)
		{
			var link = $('btnCommentOnRoute');
			Event.stopObserving(link, "click", this.commentstrigger, false);
			this.commentstrigger = null;

			link.href = '/forum/post/category/3/?routeid='+routeid;
		}
	}

	CycleRoutes.prototype.setupDrawingButtons = function ()
	{
		this.bind("btnSave", this.onSaveClick);
		this.bind("btnSaveWork", this.onSaveClick);
		this.bind("btnInsertBefore", this.onInsertBeforeClick);
		this.bind("btnInsertAfter", this.onInsertAfterClick);
		this.bind("btnDelPoint", this.onDeletePointClick);
		this.bind("btnDeleteAll", this.onDeleteAllClick);

		this.bind("locator", this.onCitySearch, "submit");

		this.drawbuttons = new CRButtonBar;
		this.drawbuttons.isNotDrawing();
		this.bind("btnStopDraw", this.onStopDrawingClick);
		this.bind("btnStartDraw", this.onStartDrawingClick);

		var self = this;
		// Start drawing btn on Locate tab:
		this.bind("btnStartDrawing", function () {
			self.maptabs.selectTab("Draw");
			this.onStartDrawingClick();
		});

		// Finished drawing btn on Draw tab:
		this.bind("btnEndDrawing", function () {
			this.onStopDrawingClick();
			self.maptabs.selectTab("Describe");
		});


	}


	CycleRoutes.prototype.resizeToFit = function ()
	{
		var wsize = new SycuseWindowSize;

		var heights = wsize.getY();

		if (!this.embed)
			heights -= 114;

		var themap = $('map');

		themap.style.height = heights+"px";
		//themap.style.width = (wsize.getX() - 160)+"px";

		if ($('mapadcontainer'))
		{
			var ctleft = $('mapadcontainer').offsetLeft - 8;

			themap.style.width = ctleft+"px";
		}

		if (this.embed && this.cPath)
		{
			this.initialview = null
			var cw = new CCallWrapper(this, 150, 'onResetClick');
			CCallWrapper.asyncExecute(cw);
		}

	}

	CycleRoutes.prototype.selectCity = function (cityid, lng, lat, locale)
	{
		this.status.setLocale(cityid, locale);
		this.status.refreshTitle();

		// Store the location; we'll come back here next time they create a route
		createCookie("city", cityid, 365);

		if (this.mode == "new")
		{
			if (this.map.getZoom() != 12)
				this.map.setZoom(12);

			this.map.panTo(new GLatLng(lat, lng));
		}
	}

	CycleRoutes.prototype.onCityChange = function ()
	{
		eval('var geodata = '+$F('selCity'));
		this.map.setCenter(new GLatLng(geodata.lat, geodata.lng), 12);
	}

	CycleRoutes.prototype.onCitySearch = function ()
	{
		crsearch.execute($F('citysearch'));
	}

	CycleRoutes.prototype.onPointNavClick = function (evt)
	{
		this.status.onPointNavClick(evt);
	}

	CycleRoutes.prototype.onInsertBeforeClick = function ()
	{
		this.cPath.onInsertBeforeClick();
	}

	CycleRoutes.prototype.onInsertAfterClick = function ()
	{
		this.cPath.onInsertAfterClick();
	}

	CycleRoutes.prototype.onElevationClick = function ()
	{
		// Elevation window is already open?
		if ($('elevationFO'))
		{
			this.bg.closeGraph();
			delete this.bg;
			return;
		}

		this.bg = new BikelyEGraph(this.isloggedin);
		this.bg.showGraph(this.cPath);
	}


	CycleRoutes.prototype.onResetClick = function ()
	{
		if (this.initialview)
		{
			this.map.setCenter(this.initialview.center, this.initialview.zoom);
			this.cPath.softRedraw();
		}
		else
		{
			var points = this.cPath.getPoints();
			var p2 = new Array;
			for (x = 0; x < points.length; x++)
				p2.push({"lat": points[x].lat(), "lng" : points[x].lng()});

			this.zoomOutShowAll(p2);
		}
	}

	CycleRoutes.prototype.purgeMouseEvent = function ()
	{
		if (this.callwrapper)
		{
			this.callwrapper.cancel();
			delete this.callwrapper;
		}
	}

	CycleRoutes.prototype.onMapMouseOut = function ()
	{
		this.purgeMouseEvent();
	}

	CycleRoutes.prototype.onMapMouseMove = function (where)
	{
		if (this.ignoreMouseMovements)
			return;

		var myDat = new Date();
		if (myDat.getTime() - this.timesince > 450)
		{
			this.purgeMouseEvent();

			this.callwrapper = new CCallWrapper(this, 150, 'showNearbyPoints', where);
			CCallWrapper.asyncExecute(this.callwrapper);
		}
	}

	CycleRoutes.prototype.showNearbyPoints = function (point)
	{
		var myDat = new Date();
		if (myDat.getTime() - this.timesince > 450)
		{
			var mousepos = this.map.fromLatLngToDivPixel(point);
			this.cPath.showNearbyPoints(mousepos, point);

		}
	}

	CycleRoutes.prototype.overlayNearbyRoute = function (route)
	{
		if (this.secondpath)
		{
			this.secondpath.hide();
			this.secondpath = null;
		}

		this.secondpath = new CRPath(this.map);
		this.secondpath.linecolor = '#990099';
		this.secondpath.loadPoints(route);
		this.secondpath.show();
	}

	CycleRoutes.prototype.setMapType = function (type)
	{
		switch (type)
		{
			case "k":
				this.map.setMapType(G_SATELLITE_MAP);
			break;

			case "h":
				this.map.setMapType(G_HYBRID_MAP);
			break;

			default:
				this.map.setMapType(G_NORMAL_MAP);
			break;
		}

	}

	CycleRoutes.prototype.loadExistingRoute = function (route)
	{
		this.mode = "edit";

		this.setMapType(route.maptype);

		this.map.setCenter(new GLatLng(route.pathData[0].lat, route.pathData[0].lng), this.zoom);

		this.cPath.loadPoints(route);

		this.status.loadMeta(route);

		this.visible = route.visible;

		this.zoomOutShowAll(route.pathData);

		if (route.editable)
		{
			this.editable = true;
		}

		if (!route.editable || route.cityid)
		{
			this.status.selectPoint(0);
		}

		if (route.photos && !route.embed)
		{
			this.photos = new CRPhotos(route, this);
		}

	}


	CycleRoutes.prototype.loadImportedPoints = function (route)
	{
		route.editable = true;
		this.mode = "import";
		this.cPath.loadPoints(route);
		this.status.loadMeta(route);

		this.zoomOutShowAll(route.pathData);

		this.status.selectPoint(0);
	}

	CycleRoutes.prototype.getNearbyRoutes = function ()
	{
		this.foNearby.canvas.innerHTML = "<p>Searching...</p>";

		var cp = new CRProximity(this);

		var bounds = this.map.getBounds();
		var ne = bounds.getNorthEast();
		var sw = bounds.getSouthWest();

		cp.searchRoutes([{"lat": ne.lat(), "lng": ne.lng()}, {"lat": sw.lat(), "lng": sw.lng()}], this.foNearby.canvas);
	}

	CycleRoutes.prototype.zoomOutShowAll = function (points)
	{
		var latmin = latmax = points[0].lat;
		var lngmin = lngmax = points[0].lng;

		for (var x = 0; x < points.length; x++)
		{
			latmin = Math.min(latmin, points[x].lat);
			latmax = Math.max(latmax, points[x].lat);

			lngmin = Math.min(lngmin, points[x].lng);
			lngmax = Math.max(lngmax, points[x].lng);
		}

		var bounds = new GLatLngBounds(new GLatLng(latmin, lngmin), new GLatLng(latmax, lngmax));
		var zoom = this.map.getBoundsZoomLevel(bounds);

		var startwhere = new GLatLng(((latmin+latmax)/2), ((lngmin+lngmax)/2));
		this.initialview = {"center": startwhere, "zoom" : zoom};

		this.map.setCenter(this.initialview.center, this.initialview.zoom);

		this.cPath.redraw();
	}

	CycleRoutes.prototype.onKeyUp = function (evt)
	{
		if (evt.altKey)
		{
			switch (String.fromCharCode(evt.keyCode))
			{
				case "C":
					if (this.plotting)
						this.onStopDrawingClick();
					else
					{
						this.maptabs.selectTab("Draw");
						this.onStartDrawingClick();
					}

				break;

				case "S":
					this.onSaveClick();
				break;

				case "X":
					this.onDeletePointClick();
				break;
			}
		}
	}


	CycleRoutes.prototype.onMapClick = function(myNode, gpoint)
	{
		if (myNode || !this.editable)
			return;

		this.cPath.deselectPoint();

		$('txtLng').value = gpoint.lat();
		$('txtLat').value = gpoint.lng();
		$('txtZoom').value = this.map.getZoom();

		if (!this.plotting)
			return this.status.selectPoint(null);

		this.dontWatchMouse();

		if ($('chkFollowRoad').checked)
			this.calculateIntermediatePoints(gpoint);

		var pointid = this.cPath.addPoint(gpoint);
		// Clear zoom-out cache
		this.initialview = null;

		this.status.selectPoint(pointid);

		this.cPath.redraw();
		this.cPath.selectPoint(gpoint, 'create');

		this.doWatchMouse();
	}

	CycleRoutes.prototype.calculateIntermediatePoints = function (endpoint)
	{
		// One at a time, please
		if (this.gDirectionsService)
			return;

		var points = this.cPath.getPoints();

		if (points.length > 0)
		{
			var frompoint = points[points.length - 1];

			this.gDirectionsService = new GDirections();

			GEvent.addListener(this.gDirectionsService, "load",
				GEvent.callbackArgs(this, this.onIntermediatePointsLoaded,
				this.gDirectionsService, points.length));

			var self=this;
			GEvent.addListener(this.gDirectionsService, "error", function () { $('chkFollowRoad').checked = false; delete self.gDirectionsService;} );

			this.gDirectionsService.loadFromWaypoints([frompoint, endpoint], {preserveViewport: false, getPolyline: true, getSteps: false});
		}
	}

	CycleRoutes.prototype.onIntermediatePointsLoaded = function (pnts, idxLastpoint)
	{
		var polyline = pnts.getPolyline();

		var len = polyline.getVertexCount();

		var lastpoint = this.cPath.getPointByIdx(idxLastpoint);

		for (var x = 1; x < len-1; x++)
		{
			var step = polyline.getVertex(x);

			if (step.distanceFrom(lastpoint) > 15)
			{
				this.cPath.insertPointAfter(step, idxLastpoint);
				idxLastpoint++;
				lastpoint = step;
			}
		}
		this.cPath.redraw();

		this.selectLastPoint(false);

		delete this.gDirectionsService;
	}

	CycleRoutes.prototype.onMapMoveStart = function ()
	{
		this.dontWatchMouse();
		this.purgeMouseEvent();

		if (this.foNearby)
			this.foNearby.clearContent();

		//this.cPath.clearPoints();
	}

	CycleRoutes.prototype.onMapMoveEnd = function ()
	{
		this.doWatchMouse();

		this.cPath.softRedraw();

		if (this.secondpath)
			this.secondpath.softRedraw();
	}

	CycleRoutes.prototype.dontWatchMouse = function ()
	{
		GEvent.removeListener(this.handleMouseMove);
		this.ignoreMouseMovements = true;
	}

	CycleRoutes.prototype.doWatchMouse = function ()
	{
		this.handleMouseMove = GEvent.bind(this.map, "mousemove", this, this.onMapMouseMove);
		this.ignoreMouseMovements = false;
	}

	CycleRoutes.prototype.onStartDrawingClick = function ()
	{
		$('map').style.cursor="crosshair";
		this.plotting = true;

		this.drawbuttons.isDrawing();

		this.selectLastPoint();
	}

	CycleRoutes.prototype.selectLastPoint = function (hilight)
	{
		if (hilight === undefined)
			hilight = true;

		// Highlight last point
		var len = this.cPath.length();
		if (len > 0)
			this.status.selectPoint(len - 1, hilight);
	}

	CycleRoutes.prototype.onStopDrawingClick = function ()
	{
		this.plotting = false;
		$('map').style.cursor="";
		this.status.selectPoint(null);

		this.drawbuttons.isNotDrawing();
	}

	CycleRoutes.prototype.onDeletePointClick = function ()
	{
		if (this.status.currentPoint === null)
			return alert("You need to select the point you wish to delete by clicking on it.");

		this.cPath.deleteCurrentPoint();
		this.cPath.redraw();
	}

	CycleRoutes.prototype.onDeleteAllClick = function ()
	{
		if (!confirm("Delete all points in this route?"))
			return;

		this.cPath.deleteAllPoints();
		this.cPath.redraw();
	}

	CycleRoutes.prototype.onClearPathClick = function ()
	{
		this.cPath.hide();
		this.cPath.clear();
	}

	CycleRoutes.prototype.onSaveClick = function ()
	{
		if (this.saveInProgress == true)
			return;

		if (this.routesettings.visible && $('routename').value.length < 5)
		{
			return alert("You must provide a name of at least 5 characters before making this route public.");
		}

		this.saveInProgress = true;
		$('savemsg').style.display='none';

		CRShowProgress.start('saveindicator');

		this.status.saveChanges();
		var saver = new CRSaver(this.cPath, this.status, this.map, this);
		saver.saveRoute();
	}

	CycleRoutes.prototype.onSaveDone = function ()
	{
		this.saveInProgress = false;
		CRShowProgress.stop('saveindicator');

	}

	CycleRoutes.prototype.onSaveSuccess = function (rpcresponse)
	{
		var elem = $('savemsg');
		elem.style.display = 'block';
		elem.style.backgroundColor = "#ffffff";
		var hl = new Effect.Highlight(elem, {endcolor: "#990000", afterFinish: function () {
			elem.style.backgroundColor = "#990000";
			setTimeout(function () { elem.style.display = 'none'; }, 2500);
		} });

		if (rpcresponse.url)
		{
			this.sharemenu.applyTo('sharemenu', 1, rpcresponse.url, $F('routename'));

			// Update the "Comment on this route" link
			this.updateCommentsLink(rpcresponse.pathid);
		}


	}

	CycleRoutes.prototype.bind = function (what, handler, event)
	{
		if (!event)
			var event = "click";

		Event.observe(what, event, handler.bindAsEventListener(this), false);
	}

	CycleRoutes.prototype.pingServer = function ()
	{
		// We ping the server to keep the session alive.
		var myAjax = new Ajax.Request(
			"/rpc/do/ping",
			{
				method: 'get'
			});
	}

	CycleRoutes.prototype.enableFinder = function (route)
	{
		this.editable = false;
		this.oneroutemode = false;

		this.showMapRoute(null);

		if (route.cityid)
		{
			this.setMapType(readCookie("m"));
			this.status.setLocale(route.cityid, route.locale);
			this.map.setCenter(new GLatLng(route.lat, route.lng), 12);
		}

		this.finder = new CRFinder(this);
	}









// Move to sycuseres?
function createCookie(name,value,days)
{
	if (days)
	{
		var date = new Date();
		date.setTime(date.getTime()+(days*24*60*60*1000));
		var expires = "; expires="+date.toGMTString();
	}
	else var expires = "";
	document.cookie = name+"="+value+expires+"; path=/";
}

function readCookie(name)
{
	var nameEQ = name + "=";
	var ca = document.cookie.split(';');
	for(var i=0;i < ca.length;i++)
	{
		var c = ca[i];
		while (c.charAt(0)==' ') c = c.substring(1,c.length);
		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
	}
	return null;
}

function eraseCookie(name)
{
	createCookie(name,"",-1);
}

function RouteSettings(cycleroutes)
{
	// PUBLIC properties
	this.visible = cycleroutes.visible;
	////////////////////////////

	this.cycleroutes = cycleroutes;
	this.init();
	this.updateUI();
}

	RouteSettings.prototype.init = function ()
	{
		var athis = this;
		Event.observe($('rs_vis'), "click", function () { athis.visible = 1; } , false);
		Event.observe($('rs_invis'), "click", function () { athis.visible = 0; } , false);

/*		Event.observe($('btnAddPhotos'), "click", function () {
			$('btnAddPhotos').parentNode.style.display = 'none';
			$('panelFlickrPhotoset').style.display = '';
			$('linkPhotoHelp').target = "_blank";

			// Force popup, in case they use firefox and have disabled popups - don't want them to lose their route
			Event.observe($('linkPhotoHelp'), "click", function (evt) {
				Event.stop(evt);
				window.open(Event.element(evt).href, 'helpwin', 'scrollbars=1,resizable=1,status=1,address=1');
				return false;
			}, false);

		} , false);
*/
	}



	RouteSettings.prototype.updateUI = function ()
	{
		if (this.visible)
			$('rs_vis').checked = true;
		else
			$('rs_invis').checked = true;
	}


function CRPath(map)
{
	this.aPath = new Array;
	this.markers = new Array;
	this.map = map;
	this.status = null;
	this.lastCursor = '';
	this.pathid = null;
	this.hilightmarker = null;
	this.editable = false;
	this.showeachpoint = false;
	this.draggingmarker = false;
	//this.linecolor = "#ee1100";
	this.linecolor = "#CC00FF";

	this.elevationQueryQueue = new Array;
	this.elevationQueryQueueIndex = 0;


}
	CRPath.prototype.loadPoints = function (route)
	{
		for (var x = 0; x < route.pathData.length; x++)
		{
			var pnt = new GLatLng(route.pathData[x].lat, route.pathData[x].lng);

			if (route.pathData[x].e == undefined)
			{
				pnt.bkElev = 0;
			}
			else
				pnt.bkElev = route.pathData[x].e;

			this.addPoint(pnt);
		}

		if (route.pathid)
			this.pathid = route.pathid;
	}

	CRPath.prototype.addPoint = function (gpoint)
	{
		this.lastpoint = gpoint;

		gpoint.origPathIndex = this.aPath.length;

		if (gpoint.bkElev == undefined)
			this.requestElevation(gpoint);

		this.aPath.push(gpoint);

		return this.aPath.length-1;
	}

	CRPath.prototype.insertPointAfter = function(point, idx)
	{
		//this.addPoint(point);

		this.deselectPoint();
		this.clearPoints();

		this.aPath[idx].origPathIndex++;

		point.origPathIndex = idx;
		this.aPath.splice(idx, 0, point);

		this.status.insertPointBefore(idx);

		this.requestElevation(point);

/*		var lens = new Array;
		for (x = 0; x < this.aPath.length; x++)
			if (this.aPath[x].origPathIndex != x)
				console.log("mismatch");
*/
	}

	CRPath.prototype.gotoPointNearKilometre = function (kilometre)
	{
		metre = kilometre * 1000;

		var dist = 0;
		var distanceAtLastPoint = 0;
		for (var x = 0; x < this.aPath.length; x++)
		{
			if (x > 0)
			{
				distanceAtLastPoint = dist;
				dist += this.aPath[x].distanceFrom(this.aPath[x-1]);
			}

			if (dist >= metre)
			{
				if (dist - metre > metre - distanceAtLastPoint)
				{

					this.hilightNoInfoWindow(x-1);
					return 	this.status.selectPoint(x-1);

				}
				else
				{
					this.hilightNoInfoWindow(x);
					return 	this.status.selectPoint(x);
				}
			}

		}

	}

	CRPath.prototype.onInsertBeforeClick = function (pointid)
	{
		if (pointid === undefined)
			pointid = this.status.currentPoint;

		if (pointid > (this.aPath.length - 1))
		{
			return alert("You're at the last point, just keep plotting points on the map!");
		}


		this.deselectPoint();
		this.clearPoints();
		$('suburb').value = '';
		$('notes').value = '';


		if (pointid < 1)
		{
			// Need to insert a point before the route beginning
			// So we continue along the bearing of the first vertex, and make it equal in length.
			var p1 = this.aPath[pointid+1];
			var midpoint = this.aPath[pointid];
			var gpoint = new GLatLng(2*midpoint.lat() - p1.lat(), 2*midpoint.lng() - p1.lng());
		}
		else
		{
			// Inserting a point in between 2 existing points; just calc the midpoint
			var p1 = this.aPath[pointid];
			var p2 = this.aPath[pointid-1];

			var gpoint = new GLatLng((p1.lat() + p2.lat()) / 2, (p1.lng() + p2.lng()) / 2);
		}


		gpoint.origPathIndex = pointid;

		this.aPath.splice(pointid, 0, gpoint);
		this.status.insertPointBefore(pointid);

		this.redraw();
		this.status.selectPoint(pointid, 'create');
		this.selectPoint(this.aPath[pointid]);
	}

	CRPath.prototype.onInsertAfterClick = function ()
	{
		this.onInsertBeforeClick(this.status.currentPoint+1);
	}

	CRPath.prototype.deleteCurrentPoint = function ()
	{
		this.deselectPoint();

		var pointid = this.status.currentPoint;
		this.aPath.splice(pointid, 1);

		this.status.deleteCurrentPoint();

		this.clearPoints();

		var pathlen = this.aPath.length;
		if (this.status.currentPoint >= pathlen)
			this.status.currentPoint = 0;

		if (pathlen > 0)
		{
			this.selectPoint(this.aPath[this.status.currentPoint]);
			this.status.selectPoint(this.status.currentPoint);
		}
		else
			this.status.selectPoint(null);
	}

	CRPath.prototype.deleteAllPoints = function ()
	{
		this.deselectPoint();
		this.clearPoints();

		this.aPath = new Array;
		this.status.deleteAllPoints();
		this.status.selectPoint(null);
	}

	CRPath.prototype.showPointsInViewport = function ()
	{
		this.showeachpoint = true;

		//this.clearPoints();
		var points = this.getPointsInViewport(this.aPath, true);
		for (var x = 0; x < points.length; x++)
		{
			this.markPoint(points[x].origPathIndex);
		}
	}
	CRPath.prototype.markPoint = function (idx)
	{

		var icon = new GIcon();

		if (this.status.hasMeta(idx))
		{
			icon.image = "/css/pointnotes.png";
			icon.shadow = "/css/pointnotes_s.png";
			icon.iconSize = new GSize(12, 33);
			icon.shadowSize = new GSize(22, 33);
			icon.iconAnchor = new GPoint(7, 31);
		}
		else
		{
			icon.image = "http://labs.google.com/ridefinder/images/mm_20_red.png";
			icon.shadow = "http://labs.google.com/ridefinder/images/mm_20_shadow.png";
			icon.iconSize = new GSize(12, 20);
			icon.shadowSize = new GSize(22, 20);
			icon.iconAnchor = new GPoint(7, 20);
		}

		// FIXME undocumented features
		icon.dragCrossAnchor = new GPoint(7, 7);

		var gpoint = this.aPath[idx];

		var mymarker = new GMarker(gpoint, {
			 "icon": icon ,
			draggable: this.editable,
			bouncy: false,
			bounceGravity: 3
			});

		var athis = this;
		var mynode = mymarker;
		GEvent.addListener(mymarker, "click", function() {
			athis.onPointClick(mynode);
		});


		this.makeMarkerDraggable(mynode);

		mymarker.pathIndex = idx;
		this.markers.push(mymarker);

		this.map.addOverlay(mymarker);
	}

	CRPath.prototype.clearPoints = function ()
	{
		var pnt;
		while (pnt = this.markers.pop())
			this.hideMarker(pnt);
	}

	CRPath.prototype.pixelDistance = function (pointa, pointb)
	{
		return Math.sqrt(Math.pow(pointa.x - pointb.x,2) + Math.pow(pointa.y - pointb.y, 2));
	}

	CRPath.prototype.showNearbyPoints = function (point, pointlatlng)
	{
		if (this.draggingmarker)
			return;

		var wanted = this.getPointsNearMouse(point, pointlatlng);

		if (this.markers[0] && wanted[0] == this.markers[0].pathIndex)
			return;

		var unwanted = new Array;
		var newmarkers = new Array;

		if (this.hilightmarker)
			skipover = this.hilightmarker.pathIndex;
		else
			skipover = -1;

		// Based on what points we've already marked, we work out what we keep, and what needs removing
		for (var x = 0; x < this.markers.length; x++)
		{
			var idx = wanted.indexOf(this.markers[x].pathIndex);
			if (idx > -1)
			{
				// Wanted, so leave as is, and remove from wanted array since it's already drawn
				wanted.splice(idx, 1);
				newmarkers.push(this.markers[x]);
			}
			else
			{
				// Unwanted
				unwanted.push(x);
			}
		}

		// Clear unwanted markers from the map
		for (var y = 0; y < unwanted.length; y++)
		{
			x = unwanted[y];
			this.hideMarker(this.markers[x]);
		}

		// And replace the markers array with our revised version
		this.markers = new Array;

		// Add wanted markers
		for (var x = 0; x < wanted.length; x++)
		{
			if (wanted[x] !== undefined)
				if (skipover != wanted[x])
					this.markPoint(wanted[x]);
		}
	}

	CRPath.prototype.hideMarker = function (marker)
	{
		GEvent.clearInstanceListeners(marker);
		this.map.removeOverlay(marker);
	}

	CRPath.prototype.clear = function ()
	{
		this.aPath = new Array;
	}

	CRPath.prototype.deselectPoint = function ()
	{
		if (this.hilightmarker)
		{
			this.hideMarker(this.hilightmarker);
			delete this.hilightmarker;
		}
		this.map.closeInfoWindow();
		this.status.saveChanges();
	}

	CRPath.prototype.selectPoint = function (point, mode)
	{
		if (mode == undefined)
			mode = "";

		this.deselectPoint();

		var icon = new GIcon();

		if (mode == "create")
		{
			icon.image = "/dot.gif";

			icon.iconSize = new GSize(8, 8);
			icon.iconAnchor = new GPoint(3, 4);
			icon.infoWindowAnchor = new GPoint(4, 1);
			//icon.dragCrossAnchor = new GPoint(2, 7);
		}
		else
		{
			icon.image = "/css/dd-start.png";
			icon.shadow = "http://www.google.com/mapfiles/shadow50.png";
			icon.shadowSize = new GSize(37, 34);

			icon.iconSize = new GSize(20, 34);
			icon.iconAnchor = new GPoint(9, 34);
			icon.infoWindowAnchor = new GPoint(9, 1);
		}

		this.hilightmarker = new GMarker(point, {"icon": icon, draggable: this.editable, bouncy: false});

		var athis = this;
		var mynode = this.hilightmarker;

		mynode.pathIndex = point.origPathIndex;

		this.makeMarkerDraggable(mynode);

		this.map.addOverlay(mynode);

	}

	CRPath.prototype.makeMarkerDraggable = function (mynode)
	{
		var athis = this;

		GEvent.addListener(mynode, "dblclick", function() { athis.map.setZoom(15); athis.hilight(mynode.pathIndex, athis.status.getMeta(mynode.pathIndex));  });

		if (this.editable)
		{
			GEvent.addListener(mynode, "dragstart", function() { athis.draggingmarker = true;});
			GEvent.addListener(mynode, "dragend", function() {
				athis.draggingmarker = false;
				athis.updatePointInPath(mynode);
				//	athis.status.selectPoint(mynode.pathIndex, 'update');
			});
		}
	}

	CRPath.prototype.hilightNoInfoWindow = function (cpoint)
	{
		this.map.panTo(this.aPath[cpoint]);

		// Mark it
		this.selectPoint(this.aPath[cpoint]);
	}

	CRPath.prototype.hilight = function (cpoint, meta)
	{
		this.hilightNoInfoWindow(cpoint);

		if ((cpoint == (this.aPath.length - 1)) && !meta.suburb.length)
			meta.suburb = "End Point";


		// Only show infowindow if this point has annotations
		if (!meta.suburb.length && !meta.notes.length)
			return this.map.closeInfoWindow();

		var infContent = "<p>";

		if (meta.suburb.length)
			infContent += "<strong>"+meta.suburb.escapeHTML()+"</strong><br />";

		if (meta.notes.length)
			infContent += meta.notes.escapeHTML();

		infContent += "</p>";

		this.hilightmarker.openInfoWindowHtml(infContent);
	}

	CRPath.prototype.show = function (drawpath)
	{
		if (this.aPath.length < 2)
			return;

		if (!drawpath)
			var drawpath = this.getPointsInViewport(this.aPath);

		this.polyline = new GPolyline(drawpath, this.linecolor, 4, 0.7);
		this.map.addOverlay(this.polyline);
	}

	CRPath.prototype.softRedraw = function ()
	{
		var pointsShown = this.pointsShown;
		var drawpath = this.getPointsInViewport(this.aPath);

		for (var x = 0; x < this.pointsShown.length; x++)
			if (pointsShown.indexOf(this.pointsShown[x]) == -1)
			{
				// Found a missing point, so we redraw
				if (this.polyline)
					this.hide();

				this.show(drawpath);
				return;
			}

		// Not redrawing, so preserve our record of points already drawn
		this.pointsShown = pointsShown;
	}

	CRPath.prototype.getPointsNearMouse = function (mouse, mouselatlng)
	{
		var points = this.aPath;
		var wanted = new Array;

		var closest = -1;
		var nearpoint;

		for (var x = 0; x < points.length; x++)
		{
			// If the map is readonly, then we are only interested in points with metadata
			if (!this.editable)
				if (!this.status.hasMeta(x))
					continue;

			var distfrom = mouselatlng.distanceFrom(points[x]);
			if (closest == -1 || distfrom < closest)
			{
				nearpoint = x;
				closest = distfrom;
			}
		}

		wanted.push(nearpoint);

		return wanted;
	}

/*	CRPath.prototype.getPointsNearMouse = function (mouse, mouselatlng)
	{
		var points = this.aPath;
		var wanted = new Array;
		for (var x = 0; x < points.length; x++)
		{
			var pxPoint = this.map.fromLatLngToDivPixel(points[x]);
			var idx = points[x].origPathIndex;

			if (this.pixelDistance(mouse, pxPoint) <= 100)
			{
				if (mouselatlng.distanceFrom(points[x]) < 4000)
				{
					if ((lastpxPoint === undefined) || this.status.hasMeta(idx) || this.editable || (this.pixelDistance(lastpxPoint, pxPoint) > 18))
					{
						wanted.push(idx);
						var lastpxPoint = pxPoint;
					}
				}
			}

		}
		return wanted;
	}
	*/



	CRPath.prototype.getPointsInViewport = function (apoints, strictlimit)
	{
		var included = new Array;

		if (!strictlimit)
			var strictlimit = false;

		var bounds = this.map.getBounds();
		var ne = bounds.getNorthEast();
		var sw = bounds.getSouthWest();

		var showall = false;//(apoints.length <= 80);

		if (!showall)
		{
			var mapsize = this.map.getSize();
			var dist = Math.sqrt(mapsize.width*mapsize.width + mapsize.height*mapsize.height);
			var ppm = dist/ne.distanceFrom(sw);
		}

		this.pointsShown = new Array;

		// Viewport boundary
		var pxbounds = new GBounds([this.map.fromLatLngToDivPixel(sw), this.map.fromLatLngToDivPixel(ne)]);

		if (!strictlimit)
		{
			var xExpand = Math.round((pxbounds.maxX - pxbounds.minX) / 2);
			var yExpand = Math.round((pxbounds.maxY - pxbounds.minY) / 2);
			pxbounds.extend(new GPoint(pxbounds.minX-xExpand, pxbounds.minY-yExpand));
			pxbounds.extend(new GPoint(pxbounds.maxX+xExpand, pxbounds.maxY+yExpand));
		}

		var skipped = true;
		for (var x = 1; x < apoints.length; x++)
		{
			var linebounds = new GBounds([this.map.fromLatLngToDivPixel(apoints[x]), this.map.fromLatLngToDivPixel(apoints[x-1])]);

			if (this.doBoundsIntersect(pxbounds, linebounds) && (showall || this.notTooClose(apoints[x], included.last(), ppm)))
			{
				if (skipped)
				{
					apoints[x-1].origPathIndex = x-1;
					included.push(apoints[x-1]);
					this.pointsShown.push(x-1);
				}

				apoints[x].origPathIndex = x;
				included.push(apoints[x]);
				this.pointsShown.push(x);

				skipped = false;

			}
			else
				skipped = true;

		}

		return included;
	}

	CRPath.prototype.notTooClose = function (pointa, pointb, ppm)
	{
		if (!pointb)
			return true;

		var pxdist = pointa.distanceFrom(pointb) * ppm;
		if (pxdist > 18)
			return true;
		else
		{
			return false;
		}
	}

	// This method shamelessly pulled from http://www.gmap-pedometer.com/  Nice one ;-)
	CRPath.prototype.doBoundsIntersect = function (rect1, rect2)
	{
		var left1, right1, top1, bottom1;
		var left2, right2, top2, bottom2;

		left1 = rect1.minX;
		right1 = rect1.maxX;
		top1 = rect1.minY;
		bottom1 = rect1.maxY;

		left2 = rect2.minX;
		right2 = rect2.maxX;
		top2 = rect2.minY;
		bottom2 = rect2.maxY;

		return !(left2 > right1 || right2 < left1 || top2 > bottom1 || bottom2 < top1);

	}

	CRPath.prototype.hide = function ()
	{
		this.map.removeOverlay(this.polyline);

		//if (!this.showeachpoint)
		//	this.clearPoints();
	}

	CRPath.prototype.redraw = function ()
	{
		if (this.polyline)
			this.hide();

		this.show();
	}

	CRPath.prototype.updatePointInPath = function (marker)
	{
		this.aPath[marker.pathIndex] = marker.getPoint();
		this.redraw();
		this.requestElevation(this.aPath[marker.pathIndex]);
		this.status.showCalipers();
	}

	CRPath.prototype.length = function ()
	{
		return this.aPath.length;
	}

	CRPath.prototype.onPointClick = function (myNode)
	{
		this.hideMarker(myNode);
		this.selectPoint(this.aPath[myNode.pathIndex]);
		this.status.selectPoint(myNode.pathIndex, (this.editable ? 'update' : null));

		if (!this.editable)
		{
			this.hilight(myNode.pathIndex, this.status.getMeta(myNode.pathIndex));
		}
	}


	CRPath.prototype.getPoints = function ()
	{
		return this.aPath;
	}

	CRPath.prototype.getPointByIdx = function (idx)
	{
		return this.aPath[idx];
	}

	// Move this to CRStatus?
	CRPath.prototype.calculateDistances = function (cpoint)
	{
		if (this.aPath.length < 1)
			return {
				fs: this.prettyDist(0),
				fe: this.prettyDist(0),
				tot: this.prettyDist(0)
			};

		var fs = fe = 0;

		var lastpoint = this.aPath[0];
		for (var x = 1; x < this.aPath.length; x++)
		{
			if (x <= cpoint)
				fs += this.aPath[x].distanceFrom(lastpoint);
			else
				fe += this.aPath[x].distanceFrom(lastpoint);

			lastpoint = this.aPath[x];
		}

		return {
			fs: this.prettyDist(fs),
			fe: this.prettyDist(fe),
			tot: this.prettyDist(fs+fe)
		};

	}

	CRPath.prototype.distanceBetween = function (x, y)
	{
		return this.aPath[x].distanceFrom(this.aPath[y]);
	}

	CRPath.prototype.prettyDist = function (ival)
	{
		ival = Math.round(ival);
		if (readCookie("units") == "unitskm")
		{
			if (ival <= 1000)
				return ival+"m";

			ival = Math.round(ival / 100)/10;
			return ival+"km";
		}
		else
		{
			// Miles/feet
			ival = ival / 0.3048;

			if (ival <= 2000)
				return Math.round(ival)+"ft";

			ival = ival / 5280;
			ival = Math.round(ival*10)/10;
			return ival+"mi";

		}
	}

	CRPath.prototype.requestElevation = function (point)
	{
		this.elevationQueryQueue.push(point);

		this.setupElevQueueRunner();

	}

	CRPath.prototype.setupElevQueueRunner = function ()
	{
		if (!this.elevQueueRunner)
		{
			var self = this;
			this.elevQueueRunner = setTimeout(function () { self.onPollElevQueue() }, 150);
		}
	}

	CRPath.prototype.onPollElevQueue = function ()
	{
		delete this.elevQueueRunner;

		if (this.elevationQueryQueueIndex < this.elevationQueryQueue.length)
		{
			var x = 0;
			var limit = 100;
			if (this.elevationQueryQueue.length - this.elevationQueryQueueIndex < limit)
				limit = this.elevationQueryQueue.length - this.elevationQueryQueueIndex;

			var pointstofetch = new Array;
			var pointsref = new Array;
			for (x = 0; x < limit; x++)
			{
				var pt = this.elevationQueryQueue[this.elevationQueryQueueIndex++];
				pointstofetch.push([pt.lat(), pt.lng()]);
				pointsref.push(pt);

			}


			this.fetchElevationNow(pointstofetch, pointsref);
		}
	}

	CRPath.prototype.fetchElevationNow = function (points, pointsref)
	{
		var url="/rpc/do/getmultielev";

		var athis = this;
		var myAjax = new Ajax.Request(
			url,
			{
				method: 'post',
				parameters: "json="+encodeURIComponent(points.toJSONString()),
				onComplete: function (resp) {
					athis.receivedElevation(pointsref, resp);
					athis.setupElevQueueRunner();
					}
			});

	}

	CRPath.prototype.receivedElevation = function (pointsref, resp)
	{
		eval(resp.responseText);

		if (rpcerror)
		{
		}
		else
		{
			//point.bkElev = elev;
			var x = 0;
			for (x = 0; x < rpcresponse.length; x++)
			{
				pointsref[x].bkElev = rpcresponse[x];
			}
		}
	}

function CRStatus(cPath)
{
	this.cPath = cPath;
	this.currentPoint = null;
	this.meta = new Array;
	this.cityid = null;
	this.locale = '';
	this.editable = true;


	if ($("status"))
	{
		this.panels = document.getElementsByClassName("info", $("status"));

		Event.observe("unitskm", "click", this.onToggleUnits.bindAsEventListener(this), false);
		Event.observe("unitsmi", "click", this.onToggleUnits.bindAsEventListener(this), false);

		var units = readCookie("units");
		if (units)
		{
			$(units).checked = true;
		}
		else
		{
			$('unitskm').checked = true;
			createCookie("units", "unitskm", 365);
		}

		this.embed = false;
	}
	else
		this.embed = true;

}
	CRStatus.prototype.show = function (what)
	{
		var len = this.panels.length;
		for (var x=0; x<len;x++)
			this.panels[x].style.display='none';

		if (what != 'iHide')
			$(what).style.display='block';

		try {
		switch (what)
		{
			case 'iDescribe':
				Form.focusFirstElement($('routesettings'));
			break;

			case 'iLocate':
				Form.focusFirstElement($('locator'));
			break;
		}
		} catch (err) {}

	}

	CRStatus.prototype.loadMeta = function (route)
	{
		this.editable = route.editable;

		for (var x = 0; x < route.pathData.length; x++)
		{
			var m = route.pathData[x];
			var a = {
				suburb: (m.suburb) ? m.suburb : '',
				notes: (m.notes) ? m.notes : ''
				};

			this.meta[x] = a;
		}

		if (!this.embed)
		{
			$('routename').value = route.title;
			$('routedesc').value = route.description;
			this.setLocale(route.cityid, route.locale);
		}
	}


	CRStatus.prototype.selectPoint = function (selPoint, hilight)
	{
		if (hilight === undefined)
			hilight = false;

		this.saveChanges();

		this.currentPoint = selPoint;

		this.loadData();

		var pathlen = this.cPath.length();

		if (pathlen > 0)
		{
			$('valPoint').innerHTML = selPoint+1;
			$('valPointOf').innerHTML = this.cPath.length();
		}
		else
		{
			$('valPoint').innerHTML = 0;
			$('valPointOf').innerHTML = 0;
		}

		this.showCalipers();

		try
		{
			if (this.editable)
				$('suburb').focus();
		}
		catch (err) {} // Can't focus if element is hidden

		if (hilight)
			this.cPath.hilightNoInfoWindow(selPoint);

		return true;
	}

	CRStatus.prototype.deleteCurrentPoint = function ()
	{
		this.meta.splice(this.currentPoint, 1);

		if (this.cPath.length() == 0)
			return;

		if (this.currentPoint > 0)
			this.selectPoint(this.currentPoint - 1, "update");
		else
			this.selectPoint(this.currentPoint + 1, "update");
	}

	CRStatus.prototype.deleteAllPoints = function ()
	{
		this.meta = new Array;
	}

	CRStatus.prototype.insertPointBefore = function (pointid)
	{
		var a = {
			suburb: '',
			notes: ''
			};

		this.meta.splice(pointid, 0, a);
		this.currentPoint = pointid;
	}

	CRStatus.prototype.setLocale = function (id, locale)
	{
		this.cityid = id;
		this.locale = locale;
	}

	CRStatus.prototype.onPointNavClick = function (evt)
	{
		var lastpoint = this.cPath.length() - 1;
		var x;
		switch (Event.element(evt).id)
		{
			case 'btnStartTour':
			case "btnPFirst":
				this.navigateTo(0);
			break;

			case "btnPPrev":
				for (x = (this.currentPoint-1); x >= 0; x--)
					if (this.meta[x].suburb.length || this.meta[x].notes.length || (this.cPath.distanceBetween(this.currentPoint, x) > 1200 && !evt.ctrlKey))
						break;

				if (x < 0)
					x = 0;
				this.navigateTo(x);
			break;

			case "btnPNext":
				for (x = (this.currentPoint+1); x <= lastpoint; x++)
				{
					if (this.meta[x].suburb.length || this.meta[x].notes.length || (this.cPath.distanceBetween(this.currentPoint, x) > 1200 && !evt.ctrlKey))
						break;
				}

				if (x > lastpoint)
					x = lastpoint;

				this.navigateTo(x);
			break;

			case "btnPLast":
				this.navigateTo(lastpoint);
			break;
		}
	}

	CRStatus.prototype.navigateTo = function (cpoint)
	{
		if (this.cPath.length() > 0)
		{
			this.selectPoint(cpoint);
			this.cPath.hilight(cpoint, this.meta[cpoint]);
		}
		else
		{
			alert("You need to draw some points before you can navigate between them.");
		}
	}

	CRStatus.prototype.onToggleUnits = function (evt)
	{
		createCookie("units", Event.element(evt).id, 365);
		this.showCalipers();
	}

	CRStatus.prototype.showCalipers = function ()
	{
		res = this.cPath.calculateDistances(this.currentPoint);

		$('calipers').innerHTML = '<strong>'+res.fs+'</strong> from start + <br /><strong>'+res.fe+'</strong> from end = <strong>'+res.tot+'</strong>';
	}

	CRStatus.prototype.isSelected = function (selPoint)
	{
		return this.currentPoint == selPoint;
	}

	CRStatus.prototype.saveChanges = function ()
	{
		if (this.currentPoint != null)
		{
			var a = {
				suburb: $F('suburb'),
				notes: $F('notes')
				};

			this.meta[this.currentPoint] = a;
		}
	}

	CRStatus.prototype.loadData = function ()
	{
		if ((this.currentPoint != null ) && this.meta[this.currentPoint])
		{
			var a = this.meta[this.currentPoint];
			$('suburb').value = a.suburb;
			$('notes').value = a.notes;
		}
		else
		{
			$('suburb').value = '';
			$('notes').value = '';
		}
	}

	CRStatus.prototype.refreshTitle = function ()
	{
		$('lblRoutetitle').innerHTML = '<span class="locale">'+this.locale+'</span><br />'+$F('routename').escapeHTML();
	}

	CRStatus.prototype.getMeta = function (idx)
	{
		if (idx === undefined)
			return this.meta;
		else
			return this.meta[idx];
	}

	CRStatus.prototype.hasMeta = function (idx)
	{
		return this.meta[idx] && (this.meta[idx].suburb || this.meta[idx].notes);
	}

function CRSaver(cPath, status, map, callback)
{
	this.cPath = cPath;
	this.status = status;
	this.callback = callback;
	this.map = map;
}

	CRSaver.prototype.saveRoute = function ()
	{
		var points = this.cPath.getPoints();
		var meta = this.status.getMeta();

		var pathData = new Array;

		//$('currpointNote').innerHTML = meta.escapeHTML();

		var fields = new Array ("suburb", "notes");

		for (var x=0; x < points.length; x++)
		{
			var meta2 = new Object;
			var havemeta = false;

			for (var y=0;y<fields.length;y++)
			{
				if (meta[x][fields[y]].length > 0)
				{
					havemeta = true;
					meta2[fields[y]] = meta[x][fields[y]];
				}
			}
 
			var pathNode = {
				lng:	points[x].lng(),
				lat:	points[x].lat(),
				e:	points[x].bkElev
			};

			if (havemeta)
				pathNode.meta = meta2;

			pathData.push(pathNode);
		}

		var saveRequest = new Object;
		saveRequest.maptype = this.map.getCurrentMapType().getUrlArg();
		saveRequest.cityid = this.status.cityid;
		saveRequest.title = $F('routename');
		saveRequest.description = $F('routedesc');
		saveRequest.photoset = '';//$F('photoset');
		saveRequest.visible = this.callback.routesettings.visible;

		if (this.callback.mode == "import")
			saveRequest.haveelevation = 0;
		else
			saveRequest.haveelevation = 1;

		saveRequest.pathid = this.cPath.pathid;
		saveRequest.pathData = pathData;
		saveRequest.tags = tagger.getChosenTags();

		var athis = this;
		var myAjax = new Ajax.Request(
			"/rpc/do/save",
			{
				method: 'post',
				parameters: "json="+encodeURIComponent(saveRequest.toJSONString()),
				onSuccess: function (resp) { athis.onSaveSuccess(resp); },
				onFailure: function (resp) { athis.onSaveFailure(resp); }
			});
	}

	CRSaver.prototype.onSaveSuccess = function (resp)
	{
		this.callback.onSaveDone();
		//alert(resp.responseText);
		eval(resp.responseText);


		if (rpcerror)
		{
			alert(rpcerror);
		}
		else
		{
			//alert(rpcresponse.url);
			this.cPath.pathid = rpcresponse.pathid;
			this.status.refreshTitle();
			this.callback.onSaveSuccess(rpcresponse);
		}
	}

	CRSaver.prototype.onSaveFailure = function (resp)
	{
		this.callback.onSaveDone();
		alert("Failed to save route - please try again.");
	}


function CRSearch(cr)
{
	var cityhash;
	this.cache = new Array;
	this.lastsearch = "";
	this.cr = cr;
}

	CRSearch.prototype.execute = function (srchkey)
	{
		$('gosearch').style.display='none';

		CRShowProgress.start('searchindicator');

		var athis = this;

		if (this.cache[srchkey] !== undefined)
			return this.showResults(this.cache[srchkey]);

		this.lastsearch = srchkey;
		var myAjax = new Ajax.Request(
			"/rpc/do/search",
			{
				method: 'post',
				parameters: "key="+encodeURIComponent(srchkey),
				onSuccess: function (resp) { athis.AJAXResponse(resp); },
				onFailure: function () { athis.restoreUI();  alert("Couldn't connect to server - please try again.");}

			});
	}

	CRSearch.prototype.showResults = function (rpcresponse)
	{
		if (rpcresponse.length > 0)
		{
			this.cityhash = new Array;

			var output;
			output = '<div id="searchresults"><table>';
			for (var x=0; x < rpcresponse.length; x++)
			{
				var res = rpcresponse[x];
				output = output + ('<tr class="'+(x % 2 ? 'odd' : '')+'"><td><a href="javascript://" onclick="crsearch.selectCity('+res.id+');">'+res.city.escapeHTML()+'</a></td><td>'+res.reg.escapeHTML()+'</td><td>'+res.country.escapeHTML()+'</td></tr>');

				this.cityhash[res.id] = res;
			}

			output += '</table></div>';
			$('citysearchresults').update(output);
		}
		else
		{
			$('citysearchresults').update("<p>No matching cities found.  If we don't have your city, try entering a major nearby city and drag the map over to your desired location.</p>");
		}

		//alert(rpcresponse);
	}

	CRSearch.prototype.restoreUI = function ()
	{
		$('gosearch').style.display='';
		CRShowProgress.stop('searchindicator');
	}

	CRSearch.prototype.AJAXResponse = function (resp)
	{
		this.restoreUI();

		//alert(resp.responseText);
		eval(resp.responseText);
		if (rpcerror)
		{
			alert(rpcerror);
		}
		else
		{
			this.cache[this.lastsearch] = rpcresponse;
			this.showResults(rpcresponse);
		}
	}

	CRSearch.prototype.selectCity = function (id)
	{
		var locale = this.cityhash[id].country+' > '+this.cityhash[id].reg+' > '+this.cityhash[id].city;
		this.cr.selectCity(id, this.cityhash[id].lng, this.cityhash[id].lat, locale);
	}


function CRProximity (cr)
{
	this.cr = cr;
}

	CRProximity.prototype.searchRoutes = function (bounds, canvas)
	{
		this.canvas = canvas;
		var athis = this;
		var myAjax = new Ajax.Request(
			"/rpc/do/getnearby",
			{
				method: 'post',
				parameters: "bounds="+encodeURIComponent(bounds.toJSONString())+"&pathid="+this.cr.cPath.pathid,
				onComplete: function (resp) { athis.AJAXResponse(resp); }
			});
	}

	CRProximity.prototype.AJAXResponse = function (resp)
	{
//		alert(resp.responseText);
		eval(resp.responseText);

		if (rpcerror)
		{
			alert(rpcerror);
		}
		else
		{
			if (rpcresponse.length > 0)
			{
				this.routehash = new Array;

				var output;
				output = '<table id="nearbyroutes">';
				for (var x=0; x < rpcresponse.length; x++)
				{
					var res = rpcresponse[x];
					output = output + ('<tr class="'+(x % 2 ? 'odd' : '')+'"><td><a href="javascript://" id="nearroute'+res.routeid+'">'+res.title.escapeHTML()+'</a></td></tr>');

					this.routehash[res.id] = res;
				}

				output += '</table><p style="display:none" id="nrloadmsg">Loading Route...</p>';
				this.canvas.innerHTML = output;


				var links = this.canvas.getElementsByTagName("a");
				var self = this;
				for (var x=0; x<links.length; x++)
				{
					Event.observe(links[x], "click", this.routeSelected.bindAsEventListener(this), false);
				}
			}
			else
			{
				this.canvas.innerHTML = "<p>No routes found nearby.</p>";
			}
		}
	}

	CRProximity.prototype.routeSelected = function (what)
	{
		var target = Event.element(what).id;
		var routeid = target.substring(9,target.length);

		$('nearbyroutes').style.display = 'none';
		$('nrloadmsg').style.display = 'inline';

		var athis = this;
		var myAjax = new Ajax.Request(
			"/rpc/do/loadroute",
			{
				method: 'post',
				parameters: "routeid="+routeid,
				onComplete: function (resp) { athis.routeLoaded(resp); }
			});

	}

	CRProximity.prototype.routeLoaded = function (resp)
	{
		$('nearbyroutes').style.display = '';
		$('nrloadmsg').style.display = 'none';

//		alert(resp.responseText);
		eval(resp.responseText);

		if (rpcerror)
		{
			alert(rpcerror);
		}
		else
		{
			this.cr.overlayNearbyRoute(rpcresponse);
		}
	}



function CRTagging()
{
	this.chosen = new Array;
	this.anti = new Array;

	this.loadDefaults();

	this.states = 2;
}
	CRTagging.prototype.loadDefaults = function ()
	{
		var taglinks = $('routetags').getElementsByTagName('a');
		for (var x = 0; x < taglinks.length; x++)
		{
			var bits = taglinks[x].id.split("_");
			var tagid = parseInt(bits[1]);
			if (Element.hasClassName(taglinks[x], "on"))
				this.chosen.push(tagid);
		}
	}

	CRTagging.prototype.tag = function (taglink)
	{
		if ($('savetagnote'))
			$('savetagnote').style.display = 'block';

		var bits = taglink.id.split("_");
		var tagid = parseInt(bits[1]);
		if (this.chosen.indexOf(tagid) > -1)
		{
			if (this.states == 3)
			{
				if (this.anti.indexOf(tagid) == -1)
				{
					// Not anti, so enter anti state
					Element.addClassName(taglink, "anti");
					this.anti.push(tagid);
					taglink.blur();
					return;
				}
			}

			Element.addClassName(taglink, "off");
			Element.removeClassName(taglink, "on");
			Element.removeClassName(taglink, "anti");
			this.chosen = this.chosen.without(tagid);

			//this.anti = this.anti.without(tagid);
		}
		else
		{
			Element.addClassName(taglink, "on");
			Element.removeClassName(taglink, "off");
			this.chosen.push(tagid);
		}

		taglink.blur();
	}

	CRTagging.prototype.getChosenTags = function ()
	{
		return this.chosen;
	}

var tagger;
/**
 * SWFObject v1.4.4: Flash Player detection and embed - http://blog.deconcept.com/swfobject/
 *
 * SWFObject is (c) 2006 Geoff Stearns and is released under the MIT License:
 * http://www.opensource.org/licenses/mit-license.php
 *
 * **SWFObject is the SWF embed script formerly known as FlashObject. The name was changed for
 *   legal reasons.
 */
if(typeof deconcept == "undefined") var deconcept = new Object();
if(typeof deconcept.util == "undefined") deconcept.util = new Object();
if(typeof deconcept.SWFObjectUtil == "undefined") deconcept.SWFObjectUtil = new Object();
deconcept.SWFObject = function(swf, id, w, h, ver, c, useExpressInstall, quality, xiRedirectUrl, redirectUrl, detectKey){
	if (!document.getElementById) { return; }
	this.DETECT_KEY = detectKey ? detectKey : 'detectflash';
	this.skipDetect = deconcept.util.getRequestParameter(this.DETECT_KEY);
	this.params = new Object();
	this.variables = new Object();
	this.attributes = new Array();
	if(swf) { this.setAttribute('swf', swf); }
	if(id) { this.setAttribute('id', id); }
	if(w) { this.setAttribute('width', w); }
	if(h) { this.setAttribute('height', h); }
	if(ver) { this.setAttribute('version', new deconcept.PlayerVersion(ver.toString().split("."))); }
	this.installedVer = deconcept.SWFObjectUtil.getPlayerVersion();
	if(c) { this.addParam('bgcolor', c); }
	var q = quality ? quality : 'high';
	this.addParam('quality', q);
	this.setAttribute('useExpressInstall', useExpressInstall);
	this.setAttribute('doExpressInstall', false);
	var xir = (xiRedirectUrl) ? xiRedirectUrl : window.location;
	this.setAttribute('xiRedirectUrl', xir);
	this.setAttribute('redirectUrl', '');
	if(redirectUrl) { this.setAttribute('redirectUrl', redirectUrl); }
}
deconcept.SWFObject.prototype = {
	setAttribute: function(name, value){
		this.attributes[name] = value;
	},
	getAttribute: function(name){
		return this.attributes[name];
	},
	addParam: function(name, value){
		this.params[name] = value;
	},
	getParams: function(){
		return this.params;
	},
	addVariable: function(name, value){
		this.variables[name] = value;
	},
	getVariable: function(name){
		return this.variables[name];
	},
	getVariables: function(){
		return this.variables;
	},
	getVariablePairs: function(){
		var variablePairs = new Array();
		var key;
		var variables = this.getVariables();
		for(key in variables){
			variablePairs.push(key +"="+ variables[key]);
		}
		return variablePairs;
	},
	getSWFHTML: function() {
		var swfNode = "";
		if (navigator.plugins && navigator.mimeTypes && navigator.mimeTypes.length) { // netscape plugin architecture
			if (this.getAttribute("doExpressInstall")) { this.addVariable("MMplayerType", "PlugIn"); }
			swfNode = '<embed type="application/x-shockwave-flash" src="'+ this.getAttribute('swf') +'" width="'+ this.getAttribute('width') +'" height="'+ this.getAttribute('height') +'"';
			swfNode += ' id="'+ this.getAttribute('id') +'" name="'+ this.getAttribute('id') +'" ';
			var params = this.getParams();
			 for(var key in params){ swfNode += [key] +'="'+ params[key] +'" '; }
			var pairs = this.getVariablePairs().join("&");
			 if (pairs.length > 0){ swfNode += 'flashvars="'+ pairs +'"'; }
			swfNode += '/>';
		} else { // PC IE
			if (this.getAttribute("doExpressInstall")) { this.addVariable("MMplayerType", "ActiveX"); }
			swfNode = '<object id="'+ this.getAttribute('id') +'" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="'+ this.getAttribute('width') +'" height="'+ this.getAttribute('height') +'">';
			swfNode += '<param name="movie" value="'+ this.getAttribute('swf') +'" />';
			var params = this.getParams();
			for(var key in params) {
			 swfNode += '<param name="'+ key +'" value="'+ params[key] +'" />';
			}
			var pairs = this.getVariablePairs().join("&");
			if(pairs.length > 0) {swfNode += '<param name="flashvars" value="'+ pairs +'" />';}
			swfNode += "</object>";
		}
		return swfNode;
	},
	write: function(elementId){
		if(this.getAttribute('useExpressInstall')) {
			// check to see if we need to do an express install
			var expressInstallReqVer = new deconcept.PlayerVersion([6,0,65]);
			if (this.installedVer.versionIsValid(expressInstallReqVer) && !this.installedVer.versionIsValid(this.getAttribute('version'))) {
				this.setAttribute('doExpressInstall', true);
				this.addVariable("MMredirectURL", escape(this.getAttribute('xiRedirectUrl')));
				document.title = document.title.slice(0, 47) + " - Flash Player Installation";
				this.addVariable("MMdoctitle", document.title);
			}
		}
		if(this.skipDetect || this.getAttribute('doExpressInstall') || this.installedVer.versionIsValid(this.getAttribute('version'))){
			var n = (typeof elementId == 'string') ? document.getElementById(elementId) : elementId;
			n.innerHTML = this.getSWFHTML();
			return true;
		}else{
			if(this.getAttribute('redirectUrl') != "") {
				document.location.replace(this.getAttribute('redirectUrl'));
			}
		}
		return false;
	}
}

/* ---- detection functions ---- */
deconcept.SWFObjectUtil.getPlayerVersion = function(){
	var PlayerVersion = new deconcept.PlayerVersion([0,0,0]);
	if(navigator.plugins && navigator.mimeTypes.length){
		var x = navigator.plugins["Shockwave Flash"];
		if(x && x.description) {
			PlayerVersion = new deconcept.PlayerVersion(x.description.replace(/([a-zA-Z]|\s)+/, "").replace(/(\s+r|\s+b[0-9]+)/, ".").split("."));
		}
	}else{
		// do minor version lookup in IE, but avoid fp6 crashing issues
		// see http://blog.deconcept.com/2006/01/11/getvariable-setvariable-crash-internet-explorer-flash-6/
		try{
			var axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
		}catch(e){
			try {
				var axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");
				PlayerVersion = new deconcept.PlayerVersion([6,0,21]);
				axo.AllowScriptAccess = "always"; // throws if player version < 6.0.47 (thanks to Michael Williams @ Adobe for this code)
			} catch(e) {
				if (PlayerVersion.major == 6) {
					return PlayerVersion;
				}
			}
			try {
				axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
			} catch(e) {}
		}
		if (axo != null) {
			PlayerVersion = new deconcept.PlayerVersion(axo.GetVariable("$version").split(" ")[1].split(","));
		}
	}
	return PlayerVersion;
}
deconcept.PlayerVersion = function(arrVersion){
	this.major = arrVersion[0] != null ? parseInt(arrVersion[0]) : 0;
	this.minor = arrVersion[1] != null ? parseInt(arrVersion[1]) : 0;
	this.rev = arrVersion[2] != null ? parseInt(arrVersion[2]) : 0;
}
deconcept.PlayerVersion.prototype.versionIsValid = function(fv){
	if(this.major < fv.major) return false;
	if(this.major > fv.major) return true;
	if(this.minor < fv.minor) return false;
	if(this.minor > fv.minor) return true;
	if(this.rev < fv.rev) return false;
	return true;
}
/* ---- get value of query string param ---- */
deconcept.util = {
	getRequestParameter: function(param) {
		var q = document.location.search || document.location.hash;
		if(q) {
			var pairs = q.substring(1).split("&");
			for (var i=0; i < pairs.length; i++) {
				if (pairs[i].substring(0, pairs[i].indexOf("=")) == param) {
					return pairs[i].substring((pairs[i].indexOf("=")+1));
				}
			}
		}
		return "";
	}
}
/* fix for video streaming bug */
deconcept.SWFObjectUtil.cleanupSWFs = function() {
	if (window.opera || !document.all) return;
	var objects = document.getElementsByTagName("OBJECT");
	for (var i=0; i < objects.length; i++) {
		objects[i].style.display = 'none';
		for (var x in objects[i]) {
			if (typeof objects[i][x] == 'function') {
				objects[i][x] = function(){};
			}
		}
	}
}
// fixes bug in fp9 see http://blog.deconcept.com/2006/07/28/swfobject-143-released/
deconcept.SWFObjectUtil.prepUnload = function() {
	__flash_unloadHandler = function(){};
	__flash_savedUnloadHandler = function(){};
	if (typeof window.onunload == 'function') {
		var oldUnload = window.onunload;
		window.onunload = function() {
			deconcept.SWFObjectUtil.cleanupSWFs();
			oldUnload();
		}
	} else {
		window.onunload = deconcept.SWFObjectUtil.cleanupSWFs;
	}
}
if (typeof window.onbeforeunload == 'function') {
	var oldBeforeUnload = window.onbeforeunload;
	window.onbeforeunload = function() {
		deconcept.SWFObjectUtil.prepUnload();
		oldBeforeUnload();
	}
} else {
	window.onbeforeunload = deconcept.SWFObjectUtil.prepUnload;
}
/* add Array.push if needed (ie5) */
if (Array.prototype.push == null) { Array.prototype.push = function(item) { this[this.length] = item; return this.length; }}

/* add some aliases for ease of use/backwards compatibility */
var getQueryParamValue = deconcept.util.getRequestParameter;
var FlashObject = deconcept.SWFObject; // for legacy support
var SWFObject = deconcept.SWFObject;

function BikelyEGraph(isloggedin)
{
	this.width = 650;
	this.height = 300;
	this.isloggedin = isloggedin;
}

	BikelyEGraph.prototype.showGraph = function (path)
	{
		var points = path.getPoints();

		if (readCookie("units") == 'unitsmi')
			this.ismiles = true;
		else
			this.ismiles = false;

		if (points.length == 0)
			return alert("There are no points in this route!");

		this.graphpts = new Array;
		var cumdist = 0;

		for (x = 0; x < points.length; x++)
		{
			var pnt = points[x];

			if (x > 0)
				cumdist += pnt.distanceFrom(points[x-1]);

			if (pnt.bkElev == undefined || pnt.bkElev == NaN)
				pnt.bkElev = 0;

			this.graphpts.push({x: cumdist, y: pnt.bkElev});

		}


		this.fo = new SycuseFlyOut(document.body, null, "Elevation", 700, 360);
		this.fo.closeonclick = false;
		this.fo.centered = true;
		this.fo.canvas.innerHTML = '<div id="elevationFO"><div id="elevationgraph">If you can read this, you probably need to install or upgrade Macromedia Flash to version 8.</div><p style="float:right"><button style="display: none" type="button" id="btnRebuildElevGraph" class="SFbutton">Rebuild Graph</button> <button type="button" id="btnCloseElevGraph" class="SFbutton">Close graph »</button></p><p>Total climb: <span id="totalrise"></span>  Total descent: <span id="totalfall"></span></p></div>';
		this.fo.onLoadContent = this.renderGraph.bindAsEventListener(this);
		this.fo.show();

		var elevunit = (this.ismiles ? "ft" : "m");
		$('totalfall').innerHTML = Math.round(this.totalfall)+elevunit;
		$('totalrise').innerHTML = Math.round(this.totalrise)+elevunit;

		if (path.pathid && this.isloggedin)
		{
			this.pathid = path.pathid;
			$('btnRebuildElevGraph').style.display = '';
			Event.observe("btnRebuildElevGraph", "click", this.rebuildGraph.bindAsEventListener(this), false);

		}

		Event.observe("btnCloseElevGraph", "click", this.closeGraph.bindAsEventListener(this), false);

	}

	BikelyEGraph.prototype.renderGraph = function ()
	{
		out = this.generatePoints(this.graphpts);
		xml = this.buildXML(out);

		var fo = new deconcept.SWFObject("/Line.swf", "Line", this.width, this.height-18, "8", "#ffffff");


		fo.addVariable("wmode", "transparent");
		fo.addVariable("quality", "high");

		fo.addVariable("dataXML", xml);
		fo.addVariable("chartWidth", this.width);
		fo.addVariable("chartHeight", this.height);

		fo.write("elevationgraph");
	}

	BikelyEGraph.prototype.closeGraph = function ()
	{
		this.fo.hide();
		this.fo.canvas.innerHTML = "";
		delete this.fo;
	}

	BikelyEGraph.prototype.rebuildGraph = function ()
	{
		if (confirm('Do you want to re-fetch the elevation data?  This may help fix an incomplete or broken graph. \nThe process takes 24 hours.'))
		{
			var athis = this;
			var myAjax = new Ajax.Request(
				"/rpc/do/rebuildgraph",
				{
					method: 'post',
					parameters: "routeid="+this.pathid,
					onSuccess: function (resp) { alert("Your request has been queued - please check back in 24 hours."); }
				});

		}
	}

	BikelyEGraph.prototype.buildXML = function (out)
	{
		var elevunit = (this.ismiles ? "ft" : "m");
		var xml = "<graph bgColor='FFFFFF' showBorder='0' canvasBgColor='FFFFFF' caption='Route Elevation' subcaption='' showAnchors='0' xAxisName='Distance' yAxisName='Elevation' numberPrefix='' numberSuffix='"+elevunit+"' showNames='1' animation='0' showValues='0' yAxisMinValue='"+Math.round(this.emin)+"' formatNumberScale='1'>";

		while (out[0])
		{
			var currpoint = out.shift();

			var hexred = "00";
			var hexgreen = "00";

			var hue = Math.round(currpoint.grad*100) + 155;
			//var hue = Math.round(currpoint.grad*255);
			var hex = parseInt(hue).toString(16);

			if (hex.length == 1)
				//hex = hex+"0";
				hex = "0"+hex;


			if (currpoint.up)
				hexred = hex;
			else
				hexgreen = hex;

			xml += "<set ";
			if (currpoint.label)
			{
				if (this.ismiles)
					xml += "name='"+Math.round(currpoint.label/1609.34, 1)+"mi' ";
				else
					xml += "name='"+Math.round(currpoint.label/1000, 1)+"km' ";
			}

			xml += "value='"+Math.round(currpoint.elev)+"' color='"+hexred+hexgreen+"00' />\n";
		}

		xml += "</graph>";

		return xml;
	}

	BikelyEGraph.prototype.generatePoints = function (points)
	{
		// For m -> ft conversion
		var multiplier = (this.ismiles ? 3.2808399 : 1);

		var dist = points.last().x;
		this.emin = 1000000;
		var emax = -1000000;
		for (var x = 0; x < points.length; x++)
		{
			var p = points[x];
			this.emin = Math.min(this.emin, p.y);
			emax = Math.max(emax, p.y);
		}

		this.emin = (this.emin - 3) * multiplier;

		var sets = Math.min(250, (points.length+15));
		var xlabelcount = 10;
		var showLabelEvery = Math.round(sets/xlabelcount);

		var setwidth = dist / (sets+1);

		var out = new Array;

		var nextpoint = {x: -1};

		var totalrise = 0;
		var totalfall = 0;
		var gotpoints = false;


		for (y = 0; y <= dist; y+= setwidth)
		{
			if (y >= nextpoint.x)
			{
				var currpoint = points.shift();
				var nextpoint = points[0];

				if (nextpoint == undefined)
					break;

				while (y >= nextpoint.x)
				{
					var currpoint = points.shift();
					var nextpoint = points[0];

					if (nextpoint == undefined)
						break;
				}

				if (nextpoint == undefined)
					break;

				var currentelev = currpoint.y;
				var setsbetween = (nextpoint.x - currpoint.x) / setwidth;
				//setsbetween--;
				var riseby = (nextpoint.y - currpoint.y) / setsbetween;
				var gradient = (nextpoint.y - currpoint.y) / (nextpoint.x - currpoint.x);

			}
			else
			{
				currentelev += riseby;
			}


			var set = {elev: currentelev*multiplier, grad: Math.abs(gradient), up: (gradient > 0)};
			if (out.length % showLabelEvery == 0)
				set.label = Math.round(y);

			if (gotpoints)
			{
				var delta = set.elev - out.last().elev;
				if (delta > 0)
					totalrise += delta;
				else
					totalfall += -1*delta;
			}

			out.push(set);
			//console.log(set);
			gotpoints = true;
		}

		if (points.length > 0)
		{
			var currpoint = points.shift();
			var currentelev = currpoint.y*multiplier;
			var delta = currentelev - out.last().elev;
				if (delta > 0)
					totalrise += delta;
				else
					totalfall += -1*delta;
		}

		this.totalrise = totalrise;
		this.totalfall = totalfall;

		return out;
	}


CRFaves = function(route)
{
	this.isfave = route.isfave;
	this.routeid = route.pathid;
	this.trigger = $("btnFave");
	this.inprogress = false;

	SycuseBind(this.trigger, this, this.onFaveClick);

	SycuseBind(this.trigger, this, this.onFaveMouseOver, "mouseover");
	SycuseBind(this.trigger, this, this.onFaveMouseOut, "mouseout");

	this.updateUI();
	this.trigger.style.visibility = 'visible';

}

CRFaves.prototype =
{
	onFaveClick: function ()
	{
		if (this.inprogress)
			return;

		if (this.isfave)
			this.setFaveMode(0);
		else
			this.setFaveMode(1);

		this.trigger.blur();
	},

	onFaveMouseOver: function ()
	{
		if (this.inprogress)
			return;

		if (this.isfave)
			this.showImg('/css/heart_out.gif');
		else
			this.showImg('/css/heart.gif');
	},

	onFaveMouseOut: function ()
	{
		if (this.inprogress)
			return;

		if (this.isfave)
			this.showImg('/css/heart.gif');
		else
			this.showImg('/css/heart_add.gif');
	},


	setFaveMode: function (mode)
	{
		this.inprogress = true;
		this.isfave = mode;

		this.showImg('/indicator.gif');

		var athis = this;
		var url = "/rpc/do/makefave/routeid/"+this.routeid+"/mode/"+mode;

		var myAjax = new Ajax.Request(
			url,
			{
				method: 'post',
				parameters: "blah=blah",
				onComplete: function (resp) { athis.AJAXResponse(resp); }
			});
		delete myAjax;

	},

	AJAXResponse: function (resp)
	{
		this.updateUI();

		this.inprogress = false;
	},

	updateUI: function ()
	{
		if (this.isfave)
		{
			this.trigger.title = "Click to remove this route from your favorites";
			this.showImg('/css/heart.gif');
		}
		else
		{
			this.trigger.title = "Click to add this route to your favorites";
			this.showImg('/css/heart_add.gif');
		}
	},

	showImg: function (img)
	{
		this.trigger.style.backgroundImage = 'url('+img+')';
	}
}

CRPhotos = function (route, cr)
{
	this.route = route;
	this.cr = cr;

	this.createControl();
	this.init();
}

CRPhotos.prototype =
{
	init: function ()
	{
		this.photoWindowOpen = false;
		this.cr.map.addControl(new BPhotoControl(this));

		ws = new SycuseWindowSize;
		this.largemode = (ws.getY() >= 660);

		this.foPhotos = new SycuseFlyOut(document.body, $('btnPhotoControl'), "Photos", 730, this.largemode ? 640 : 400);

		this.foPhotos.centered = true;
		this.foPhotos.onLoadContent = this.onPhotoControlClick.bindAsEventListener(this);

	},

	onPhotoControlClick: function ()
	{
		var canvas = this.foPhotos.canvas;

		canvas.style.textAlign = "center";

		var numpics = this.route.photos.all.length;
		for (var x = 0; x < numpics; x++)
		{
			var pic = this.route.photos.all[x];

			var img = document.createElement("img");
			img.src="http://farm1.static.flickr.com/"+pic.server+"/"+pic.photoid+"_"+pic.secret+"_s."+pic.format;
			img.title=pic.title;
			img.style.cursor = "pointer";

			img.crPic = pic;
			Event.observe(img, "click", this.onPhotoClick.bindAsEventListener(this), false);

			canvas.appendChild(img);
		}

		canvas.appendChild(document.createElement("br"));


		var flicklink = document.createElement("a")
		flicklink.target = "_blank";
		flicklink.title = "View this photo on Flickr (new window)";

		var img = document.createElement("img");
		img.style.display = 'none';
		img.style.cursor = "pointer";
		img.style.border = "1px solid #aaa";

		this.previewImage = img;
		this.previewLink = flicklink;

		flicklink.appendChild(img);
		canvas.appendChild(flicklink);

		this.previewCaption = document.createElement("h3");
		this.previewCaption.innerHTML = 'Click thumbnails to enlarge an image.';
		canvas.appendChild(this.previewCaption);
	},

	onPhotoClick: function (evt)
	{
		var img = Event.element(evt);
		var pic = img.crPic;

		this.previewCaption.innerHTML = pic.title.escapeHTML();
		this.previewLink.href = 'http://www.flickr.com/photos/'+pic.username+'/'+pic.photoid+'/in/set-'+this.route.photos.set+'/';

		this.previewImage.src="http://farm1.static.flickr.com/"+pic.server+"/"+pic.photoid+"_"+pic.secret+(this.largemode ? "" : "_m")+"."+pic.format;
		this.previewImage.style.display = '';
	},

	createControl: function ()
	{
		BPhotoControl.prototype = new GControl();

		BPhotoControl.prototype.initialize = function(map)
		{
			var container = document.createElement("div");

			var photoImg = document.createElement("img");
			photoImg.id = "btnPhotoControl";

			photoImg.src='/css/camera.gif';
			photoImg.style.border = "1px solid #900";
			photoImg.style.padding = "1px";
			photoImg.style.backgroundColor = "#fff";
			photoImg.style.cursor = "pointer";
			photoImg.title = photoImg.alt = "Click to view photos of this route";

			var athis = this;
			container.appendChild(photoImg);

			map.getContainer().appendChild(container);
			//this.mapref = map;
			return container;
		}

		BPhotoControl.prototype.getDefaultPosition = function(map)
		{
			return new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(4,30));
		}
	}


}

function BPhotoControl(crp)
{
	this.crp = crp;
}


CRRatings = function(route)
{
	$('frmRouteRatings').style.display = 'block';

	this.elem = $('selRateRoute');

	if (route.uRating >= 0)
		this.elem.selectedIndex = route.uRating+1;

	SycuseBind(this.elem, this, this.onRatingChange, "change");

	this.routeid = route.pathid;
}

CRRatings.prototype =
{
	onRatingChange: function (evt)
	{
		var selectedrating = $F(this.elem);

		if (selectedrating < 0)
			return;

		var athis = this;
		var url = "/rpc/do/rateroute/routeid/"+this.routeid+"/rating/"+selectedrating;

		CRShowProgress.start('icoRatingProgress');

		var myAjax = new Ajax.Request(
			url,
			{
				method: 'post',
				parameters: "blah=blah",
				onComplete: function (resp) { athis.AJAXResponse(resp); }
			});
		delete myAjax;

	},

	AJAXResponse: function (resp)
	{
		CRShowProgress.stop('icoRatingProgress');

		this.inprogress = false;
	}
}
CRMapTabHelp = function (buttons)
{
	this.openwin = null;
	this.buttons = buttons;

	this.init();
}

CRMapTabHelp.prototype =
{
	showHelp: function (which, autoclose)
	{
		this.close();

		var helpwin = $('i'+which+'Help');

		if (helpwin)
		{
			this.openwin = helpwin;
			Effect.Appear(helpwin, {duration: .3});

			if (autoclose)
			{
				// Trap any click to close it
				this.trapper = this.close.bindAsEventListener(this);
				Event.observe(document, "click", this.trapper, false);
			}
		}
	},

	close: function()
	{
		if (this.openwin)
		{
			this.openwin.style.display = 'none';
			this.openwin = null;

			Event.stopObserving(document, "click", this.trapper, false);
		}
	},

// end public interface


	init: function()
	{
		for (var x = 0; x < this.buttons.length; x++)
		{
			var tabname = this.buttons[x];

			var helpwin = $('i'+tabname+'Help');

			if (helpwin)
			{
				// Add delete button to the help window
				var closeButton = document.createElement("img");
				closeButton.className = 'helpclosebutton';
				closeButton.style.styleFloat = closeButton.style.cssFloat = 'right';
				closeButton.style.cursor = 'pointer';
				closeButton.style.margin = '0 0 4px 4px';
				closeButton.src = '/js/flyout-close.gif';

				var closeButtonContainer = document.createElement("a");
				closeButtonContainer.appendChild(closeButton);
				closeButtonContainer.title = 'Close';

				helpwin.insertBefore(closeButtonContainer, helpwin.firstChild);
				Event.observe(closeButton, "click", this.close.bindAsEventListener(this), false);

				// Add help button to tab body
				var helpButton = document.createElement("img");
				helpButton.style.styleFloat = helpButton.style.cssFloat = 'right';
				helpButton.style.cursor = 'pointer';
				helpButton.src = '/css/icons/helpme.gif';

				var helpButtonContainer = document.createElement("a");
				helpButtonContainer.appendChild(helpButton);
				helpButtonContainer.title = 'Get Help';

				var tab = $('i'+tabname);
				tab.insertBefore(helpButtonContainer, tab.firstChild);
				helpButton.tabname = tabname;
				Event.observe(helpButton, "click", this.toggleHelp.bindAsEventListener(this), false);

			}
		}
	},

	toggleHelp: function (what)
	{
		if (this.openwin)
			return this.close();

		var tabname = Event.element(what).tabname;
		this.showHelp(tabname, false);
	}

}

/*****************************
MAP TABS
********************************/

CRMapTabs = function (status, map, editable, autoshowhelp)
{
	this.status = status;
	this.map = map;
	this.editable = editable;
	this.autoshowhelp = new Array;
	this.help = null;
	this.lasttab = null;
	this.tabsopen = true;

	this.buttonsEditable = ['Locate', 'Draw', 'Describe', 'Hide'];
	this.buttonsExploring = ['Explore', 'Hide'];

	this.createControl();
	this.init(autoshowhelp);
}

CRMapTabs.prototype =
{
	selectTab: function (name)
	{
		this.buttons.each(function(item)
		{
			var button = 'tabbtn'+item;

			if (name == item)
			{
				$(button).parentNode.className = 'active';
			}
			else
			{
				$(button).parentNode.className = '';
			}
		});

		this.help.close();

		if (name == 'Hide')
		{
			if (this.tabsopen)
			{
				this.status.show('i'+name);
				Element.update($('tabbtnHide'), "v");
				$('status').className = 'tabshidden';
				this.tabsopen = false;
				return;
			}
			else
			{
				this.selectTab(this.lasttab);
			}
		}
		else
		{
			this.status.show('i'+name);
			this.lasttab = name;

			if (this.autoshowhelp[name])
			{
				this.help.showHelp(name, true);
				this.autoshowhelp[name] = false;
			}

			if (!this.tabsopen)
			{
				Element.update($('tabbtnHide'), "^");
				$('status').className = '';
				this.tabsopen = true;
			}
		}
	},

// end public interface



	init: function (autoshowhelp)
	{
		this.map.addControl(new BMapTabControl());

		this.setupPanels();

		var self = this;
		this.buttons.each(function(item) {
			var btn = $('tabbtn'+item);
			Event.observe(btn, 'click', self.tabClicked.bindAsEventListener(self), false);
			btn.parentNode.style.display = 'block';

			self.autoshowhelp[item] = autoshowhelp;
		});

		this.help = new CRMapTabHelp(this.buttons);

	},

	setupPanels: function ()
	{
		var curpoint = $('curpoint');
		curpoint.parentNode.removeChild(curpoint);

		if (this.editable)
		{
			$('iDrawNav').appendChild(curpoint);
			this.buttons = this.buttonsEditable;
		}
		else
		{
			$('iExploreNav').appendChild(curpoint);
			this.buttons = this.buttonsExploring;
		}

		curpoint.style.display = 'block';
	},



	createControl: function ()
	{
		BMapTabControl.prototype = new GControl(false, true);

		BMapTabControl.prototype.initialize = function(map)
		{
			var container = document.createElement("div");

			var status = $('status');
			status.parentNode.removeChild(status);
			container.appendChild(status);

			status.style.display = 'block';

			map.getContainer().appendChild(container);

			return container;
		}

		BMapTabControl.prototype.getDefaultPosition = function(map)
		{
			return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(0,0));
		}
	},

	tabClicked: function(what)
	{
		var elem = Event.element(what);

		this.selectTab(elem.id.substr(6));

		elem.blur();

	}
}

function BMapTabControl()
{
}



CRButtonBar = function ()
{
}

CRButtonBar.prototype =
{
	isDrawing: function ()
	{
		$('btnStartDraw').style.display = 'none';
		$('btnStopDraw').style.display = 'block';
	},

	isNotDrawing: function ()
	{
		$('btnStopDraw').style.display = 'none';
		$('btnStartDraw').style.display = 'block';
	}
}
var CRShowProgress=
{
	start: function (what)
	{
		$(what).style.display='inline';
		$(what).style.visibility = "visible";

		setTimeout(function() {
			$(what).src = $(what).src;
			}, 80);
	},

	stop: function (what)
	{
		$(what).style.display='none';
	}
}


CRGoogleAd = function (map)
{
	if (!$('googleads'))
		return;

	this.map = map;

	this.createControl();

	this.map.addControl(new BGoogleAdControl());

//	this.addCloseButton();
}

CRGoogleAd.prototype =
{
	createControl: function ()
	{
		BGoogleAdControl.prototype = new GControl(false, true);

		BGoogleAdControl.prototype.initialize = function(map)
		{
			var container = document.createElement("div");

			var gad = $('googleads');
			gad.parentNode.removeChild(gad);
			container.appendChild(gad);

			gad.style.display = 'block';

			map.getContainer().appendChild(container);

			return container;
		}

		BGoogleAdControl.prototype.getDefaultPosition = function(map)
		{
			return new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(0,0));
		}
	},


	addCloseButton: function ()
	{
		// Add delete button to the help window
		var closeButton = $('btnHideAds');
		Event.observe(closeButton, "click", this.close.bindAsEventListener(this), false);
	},

	close: function ()
	{
		$('googleads').style.display = 'none';
	}
}


function BGoogleAdControl() {}
var tagger;

function LPBehave()
{
	if ($('country'))
	{
		Event.observe($('country'), "change", this.onCountrySelect.bindAsEventListener(this), false);
		this.ajax = null;
	}

	if ($('tags_is'))
		tagger = new LPTagSuggest;
}

	LPBehave.prototype.onCountrySelect = function (evt)
	{
		if (this.ajax)
			this.ajax.transport.abort();

		$('region').style.cursor = 'wait';
		$('region').disabled = true;

		var self = this;

		this.ajax = new Ajax.Updater(
			'regionholder',
			'/rpc/do/getregions',
			{
				method: 'post',
				parameters: "countryid="+$F('country'),
				onSuccess: function (xhr) { self.done(); },
				onFailure: function (xhr) { self.done(); },
				evalScripts: false

			});
	}

	LPBehave.prototype.done = function ()
	{
		$('region').style.cursor = '';
		this.ajax = null;
		$('region').disabled = false;
	}



function LPTagSuggest()
{
	this.init();
}

LPTagSuggest.prototype =
{
	init: function ()
	{
		var handler_focus = this.onTagInputFocus.bindAsEventListener(this);
		var handler_blur = this.onTagInputBlur.bindAsEventListener(this);

		Event.observe($('tags_is'), "focus", handler_focus, false);
		Event.observe($('tags_isnot'), "focus", handler_focus, false);

		Event.observe($('tags_is'), "blur", handler_blur, false);
		Event.observe($('tags_isnot'), "blur", handler_blur, false);

		this.tagbox = $('routetags');
		this.tagbox.style.position = 'absolute';
		this.tagbox.style.width = '265px';
		this.tagbox.style.border = '1px solid #999';
		this.tagbox.style.padding = '5px';
		this.tagbox.style.backgroundColor = '#ffffff';
	},

	onTagInputFocus: function (e)
	{
		clearTimeout(this.hidetimer);
		var elem = Event.element(e);
		Position.clone(elem, this.tagbox, {setHeight: false, offsetTop: elem.offsetHeight-6});
		this.tagbox.style.display = 'block';
		this.currentelem = elem;
	},

	onTagInputBlur: function (e)
	{
		var elem = Event.element(e);
		var tb = this.tagbox;
		this.hidetimer = setTimeout("$('routetags').style.display = 'none';", 250);
//		this.tagbox.style.display = 'none';
	},

	tag: function (what)
	{
		var separator = this.currentelem.value.length == 0 ? "" : ",";

		this.currentelem.value = this.currentelem.value + separator + what.innerHTML;
		this.currentelem.focus();
	}

}
SycuseSocialBookmark = function (what, ispublic)
{
	if (ispublic == undefined)
		ispublic = 1;


	this.clickevents = new Array;
	this.applyTo(what, ispublic);
}

SycuseSocialBookmark.prototype =
{
	applyTo: function (whatId, ispublic, url, title)
	{
		var what = $(whatId);
		if (what)
		{
			var anchors = what.getElementsByTagName('a');

			if (url == undefined)
				url = location.href;

			if (title == undefined)
				title = document.title;

			url = encodeURIComponent(url);
			title = encodeURIComponent(title);

			for (var x = 0; x < anchors.length; x++)
			{
				var anc = anchors[x];

				if (anc.rel.substr(0, 2) == "SB")
				{
					if (ispublic)
						this.setHREF(anc,url,title);
					else
						this.addWarning(anc);
				}

			}
		}
	},

	setHREF: function (anc, url, title)
	{
		if (this.clickevents[anc.id])
		{
			Event.stopObserving(anc, "click", this.clickevents[anc.id], false);
			this.clickevents[anc.id] = null;
		}

		switch (anc.rel)
		{
			case "SBdelicious":
				anc.href = 'http://del.icio.us/post?v=4&url='+url+'&title='+title;
			break;
			case "SBdigg":
				anc.href = "http://digg.com/submit?phase=2&url="+url;
			break;

			case "SBreddit":
				anc.href = 'http://reddit.com/submit?url='+url+'&title='+title;
			break;

			case "SBfurl":
				anc.href = 'http://www.furl.net/storeIt.jsp?t='+title+'&u='+url;
			break;

			case "SBnetscape":
				anc.href = 'http://www.netscape.com/submit/?U='+url+'&T='+title;
			break;
		}
	},

	addWarning: function (anc)
	{
		anc.href="javascript://";
		this.clickevents[anc.id] = this.showWarning.bindAsEventListener(this);
		Event.observe(anc, "click", this.clickevents[anc.id], false);
	},

	showWarning: function ()
	{
		alert("You must make this route publically visible before sharing it with others.");
	}
}
//Chrome Drop Down Menu v2.01- Author: Dynamic Drive (http://www.dynamicdrive.com)
//Last updated: November 14th 06- added iframe shim technique

var cssdropdown={
disappeardelay: 250, //set delay in miliseconds before menu disappears onmouseout
disablemenuclick: false, //when user clicks on a menu item with a drop down menu, disable menu item's link?
enableswipe: 1, //enable swipe effect? 1 for yes, 0 for no
enableiframeshim: 1, //enable "iframe shim" technique to get drop down menus to correctly appear on top of controls such as form objects in IE5.5/IE6? 1 for yes, 0 for no

//No need to edit beyond here////////////////////////
dropmenuobj: null, ie: document.all, firefox: document.getElementById&&!document.all, swipetimer: undefined, bottomclip:0,

getposOffset:function(what, offsettype)
{
var totaloffset=(offsettype=="left")? what.offsetLeft : what.offsetTop;
var parentEl=what.offsetParent;
while (parentEl!=null)
{
totaloffset=(offsettype=="left")? totaloffset+parentEl.offsetLeft : totaloffset+parentEl.offsetTop;
parentEl=parentEl.offsetParent;
}
return totaloffset;
},

swipeeffect:function()
{
	if (this.bottomclip<parseInt(this.dropmenuobj.offsetHeight))
	{
		this.bottomclip+=20+(this.bottomclip/20) //unclip drop down menu visibility gradually
		this.dropmenuobj.style.clip="rect(0 auto "+this.bottomclip+"px 0)"
	}
	else
		return
		this.swipetimer=setTimeout("cssdropdown.swipeeffect()", 5)
},

showhide:function(obj, e)
{
if (this.ie || this.firefox)
this.dropmenuobj.style.left=this.dropmenuobj.style.top="-500px"
if (e.type=="click" && obj.visibility==hidden || e.type=="mouseover"){
if (this.enableswipe==1){
if (typeof this.swipetimer!="undefined")
clearTimeout(this.swipetimer)
obj.clip="rect(0 auto 0 0)" //hide menu via clipping
this.bottomclip=0
this.swipeeffect()
}
obj.visibility="visible"
}
else if (e.type=="click")
obj.visibility="hidden"
},

iecompattest:function(){
return (document.compatMode && document.compatMode!="BackCompat")? document.documentElement : document.body
},

clearbrowseredge:function(obj, whichedge){
var edgeoffset=0
if (whichedge=="rightedge"){
var windowedge=this.ie && !window.opera? this.iecompattest().scrollLeft+this.iecompattest().clientWidth-15 : window.pageXOffset+window.innerWidth-15
this.dropmenuobj.contentmeasure=this.dropmenuobj.offsetWidth
if (windowedge-this.dropmenuobj.x < this.dropmenuobj.contentmeasure)  //move menu to the left?
edgeoffset=this.dropmenuobj.contentmeasure-obj.offsetWidth
}
else{
var topedge=this.ie && !window.opera? this.iecompattest().scrollTop : window.pageYOffset
var windowedge=this.ie && !window.opera? this.iecompattest().scrollTop+this.iecompattest().clientHeight-15 : window.pageYOffset+window.innerHeight-18
this.dropmenuobj.contentmeasure=this.dropmenuobj.offsetHeight
if (windowedge-this.dropmenuobj.y < this.dropmenuobj.contentmeasure){ //move up?
edgeoffset=this.dropmenuobj.contentmeasure+obj.offsetHeight
if ((this.dropmenuobj.y-topedge)<this.dropmenuobj.contentmeasure) //up no good either?
edgeoffset=this.dropmenuobj.y+obj.offsetHeight-topedge
}
}
return edgeoffset
},

dropit:function(obj, e, dropmenuID){
if (this.dropmenuobj!=null) //hide previous menu
this.dropmenuobj.style.visibility="hidden" //hide menu
this.clearhidemenu()
if (this.ie||this.firefox){
obj.onmouseout=function(){cssdropdown.delayhidemenu()}
obj.onclick=function(){return !cssdropdown.disablemenuclick} //disable main menu item link onclick?
this.dropmenuobj=document.getElementById(dropmenuID)
this.dropmenuobj.onmouseover=function(){cssdropdown.clearhidemenu()}
this.dropmenuobj.onmouseout=function(e){cssdropdown.dynamichide(e)}

Event.observe(this.dropmenuobj, "click",
//this.dropmenuobj.onclick=
function(e)
{
	var elem = Event.element(e);
	if (elem.tagName == 'INPUT')
	{
		cssdropdown.focussedinput = elem;

	}
	else
		cssdropdown.delayhidemenu();
}
, false);

this.showhide(this.dropmenuobj.style, e)
this.dropmenuobj.x=this.getposOffset(obj, "left")
this.dropmenuobj.y=this.getposOffset(obj, "top")
this.dropmenuobj.style.left=this.dropmenuobj.x-this.clearbrowseredge(obj, "rightedge")+"px"
this.dropmenuobj.style.top=this.dropmenuobj.y-this.clearbrowseredge(obj, "bottomedge")+obj.offsetHeight+1+"px"
this.positionshim() //call iframe shim function
}
},

positionshim:function(){ //display iframe shim function
if (this.enableiframeshim && typeof this.shimobject!="undefined"){
if (this.dropmenuobj.style.visibility=="visible"){
this.shimobject.style.width=this.dropmenuobj.offsetWidth+"px"
this.shimobject.style.height=this.dropmenuobj.offsetHeight+"px"
this.shimobject.style.left=this.dropmenuobj.style.left
this.shimobject.style.top=this.dropmenuobj.style.top
}
this.shimobject.style.display=(this.dropmenuobj.style.visibility=="visible")? "block" : "none"
}
},

hideshim:function(){
if (this.enableiframeshim && typeof this.shimobject!="undefined")
this.shimobject.style.display='none'
},

contains_firefox:function(a, b)
{
	if (b)
	{
		try
		{
			while (b.parentNode)
			{
				if ((b = b.parentNode) == a)
					return true;
			}
		}
		catch (err)
		{
		}
	}

	return false;
},

dynamichide:function(e)
{
	var evtobj=window.event? window.event : e

	if (this.ie&&!this.dropmenuobj.contains(evtobj.toElement))
		this.delayhidemenu()
	else if (this.firefox&&e.currentTarget!= evtobj.relatedTarget && !this.contains_firefox(evtobj.currentTarget, evtobj.relatedTarget))
		this.delayhidemenu()
},

delayhidemenu:function()
{
	this.delayhide=setTimeout("cssdropdown.dropmenuobj.style.visibility='hidden'; cssdropdown.hideshim()",this.disappeardelay) //hide menu

	if (cssdropdown.focussedinput)
	{
		try { cssdropdown.focussedinput.blur(); } catch (err) {}

		cssdropdown.focussedinput = null;
	}
},

clearhidemenu:function(){
if (this.delayhide!="undefined")
clearTimeout(this.delayhide)
},

	startchrome:function()
	{
		for (var ids=0; ids<arguments.length; ids++)
		{
			var menuitems=document.getElementById(arguments[ids]).getElementsByTagName("a");
			for (var i=0; i<menuitems.length; i++)
			{
				if (menuitems[i].getAttribute("rel"))
				{
					var relvalue=menuitems[i].getAttribute("rel")

					menuitems[i].onmouseover = function(e)
					{
						var event=typeof e!="undefined"? e : window.event;
						cssdropdown.dropit(this,event,this.getAttribute("rel"))
					}
				}
			}
		}

		if (window.createPopup && !window.XmlHttpRequest)
		{ //if IE5.5 to IE6, create iframe for iframe shim technique
			this.shimobject = document.createElement("<iframe scrolling='no' frameborder='0'"+
							      "style='position:absolute; top:0px;"+
							      "left:0px; display:none'></iframe>");
			this.shimobject.id = 'iframeshim';
			this.shimobject.style.filter="progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)";
			window.document.body.appendChild(this.shimobject);
		}

	}

}
/*
 * Based on Sweet Titles by Dustin Diaz.
 */

var ProtoTip =
{
    xCord : 0,
    yCord : 0,
    obj : null,
    tooltipTagNames: ["a", "span", "acronym", "abbr"],

    attachTooltipBehavior: function()
    {
        if (   !document.getElementById
            || !document.createElement
            || !document.getElementsByTagName)
        {
            return;
        }

        Event.observe(document, "mousemove", ProtoTip.updateXY, false);
        if (document.captureEvents)
        {
            document.captureEvents(Event.MOUSEMOVE);
        }

        for (var i = 0, tagName; tagName = ProtoTip.tooltipTagNames[i]; i++)
        {
          var elements = document.getElementsByTagName(tagName);
            for (var j = 0, element; element = elements[j]; j++)
            {
          	if (element.tagName != "SPAN" || Element.hasClassName(element, "hover"))
          	{
			if (element.title.length > 0 && !element.getAttribute("tip")) // Safely allow multiple calls
			{
			    Event.observe(element, "mouseover", ProtoTip.tipOver, false);
			    Event.observe(element, "click", ProtoTip.tipOut, false);
			    Event.observe(element, "mouseout", ProtoTip.tipOut, false);
			    element.setAttribute("tip", element.title);
			    element.removeAttribute("title");

			    var subelems = $A(element.getElementsByTagName('img'));
			    subelems.each(function (elem)
			    	{
			    		 if (elem.alt)
			    		 	elem.alt = "";
			    	});

			}
		}
            }
        }
    },

    updateXY: function(e)
    {
        ProtoTip.xCord = Event.pointerX(e);
        ProtoTip.yCord = Event.pointerY(e);
    },

    tipOut: function(e)
    {
        if (window.tID)
            clearTimeout(tID);

        var div = $("toolTip");
        if (div != null)
        {
            div.parentNode.removeChild($('ttShadow'));
            div.parentNode.removeChild(div);
        }
    },

    checkNode: function(obj)
    {
        for (var i = 0, tagName; tagName = ProtoTip.tooltipTagNames[i]; i++)
        {
            if (obj.nodeName.toLowerCase() == tagName)
            {
                return obj;
            }
        }
        return obj.parentNode;
    },

    tipOver: function(e)
    {
        ProtoTip.obj = Event.element(e);
        tID = setTimeout("ProtoTip.tipShow()", 200)
    },

    tipShow: function()
    {
        var element = ProtoTip.checkNode(ProtoTip.obj);
        var extraInfo = "";
        var accessKey = "";
/*        if (element.nodeName.toLowerCase() == "a")
        {
            extraInfo = (element.href.length > 25 ? element.href.toString().substring(0,25) + "..." : element.href);
//            accessKey = (element.accessKey ? " <span>[" + element.accessKey + "]</span> " : "");
        }
        else
        {
            extraInfo = element.firstChild.nodeValue;
        }
*/
        var tooltip = document.createElement("div");

        tooltip.id = "toolTip";
        tooltip.style.display = 'none';
        Element.addClassName(tooltip, "toolTip");
        document.body.appendChild(tooltip);

        //tooltip.innerHTML = "<p>" + element.getAttribute("tip") + "<em>" + accessKey + extraInfo + "</em></p>";
        tooltip.innerHTML = "<p id='ttText'>" + element.getAttribute("tip") + "</p>";

	//$('ttText').style.width = (element.getAttribute("tip").length) + "ex";

        ttshadow = tooltip.cloneNode(true);
        ttshadow.id = "ttShadow";

        document.body.appendChild(ttshadow);

        var top = ProtoTip.yCord + 15;
        var left = ProtoTip.xCord + 10;

        // Prevent horizontal hiding
        if (!Position.within(document.body, left+80, 10))
        {
            tooltip.style.right = "2px";
            ttshadow.style.right = "0px";
        }
        else
        {
            tooltip.style.left = left + "px";
            ttshadow.style.left = (left+4) + "px";
        }

	    tooltip.style.top = top + "px";
	    ttshadow.style.top = (top+4) + "px";

        tooltip.style.display = '';
        ttshadow.style.display = '';
        ProtoTip.tipelement = tooltip;

        tooltip.style.opacity = ".1";
        tooltip.style.filter = "alpha(opacity:10)";
        ProtoTip.tipFade("toolTip", 10);
        ProtoTip.tipFade("ttShadow", 10, 45);

    },

    tipFade: function(element, opac, max)
    {
    	if (!max)
    		max = 99;

        var tooltip = $(element);
        if (!tooltip)
        	return;

        var newOpacity = opac + 13;
        if (newOpacity <= max)
        {
	    tooltip.style.display = '';
            tooltip.style.opacity = "." + newOpacity;
            tooltip.style.filter = "alpha(opacity:" + newOpacity + ")";
            var thistimeout = setTimeout("ProtoTip.tipFade('"+element+"', " + newOpacity + ", "+max+")", 10);
        }
        else
        {
            tooltip.style.opacity = "0."+max;
            tooltip.style.filter = "alpha(opacity:"+max+")";
        }
    }
};


/*
*
* Copyright (c) 2007 Andrew Tetlaw
* 
* 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.
* * 
*
*
* FastInit
* http://tetlaw.id.au/view/javascript/fastinit
* Andrew Tetlaw
* Version 1.3 (2007-01-09)
* Based on:
* http://dean.edwards.name/weblog/2006/03/faster
* http://dean.edwards.name/weblog/2006/06/again/
* Help from:
* http://www.cherny.com/webdev/26/domloaded-object-literal-updated
* 
*/
var FastInit = {
	done : false,
	onload : function() {
		if (FastInit.done) return;
		FastInit.done = true;
		FastInit.actions.each(function(func) {
			func();
		})
	},
	actions : $A([]),
	addOnLoad : function() {
		for(var x = 0; x < arguments.length; x++) {
			var func = arguments[x];
			if(!func || typeof func != 'function') continue;
			FastInit.actions.push(func);
		}
	},
	listen : function() {
		if (/WebKit|khtml/i.test(navigator.userAgent)) {
			FastInit._timer = setInterval(function() {
				if (/loaded|complete/.test(document.readyState)) {
					clearInterval(FastInit._timer);
					delete FastInit._timer;
					FastInit.onload();
				}
			}, 10);
		} else if (document.addEventListener) {
			document.addEventListener('DOMContentLoaded', FastInit.onload, false);
		} else if(!FastInit._iew32) {
			Event.observe(window, 'load', FastInit.onload);
		}
	},
	_timer : null,
	_iew32 : false
}
/*@cc_on @*/
/*@if (@_win32)
FastInit._iew32 = true;
document.write('<script id="__ie_onload" defer src="' + ((location.protocol == 'https:') ? '//0' : 'javascript:void(0)') + '"><\/script>');
$('__ie_onload').onreadystatechange = function(){if (this.readyState == 'complete') FastInit.onload();};
/*@end @*/
FastInit.listen();

