/* 
  Prototype Extension Library 
  Extends Prototype 1.3.1
  Based on prototype-1.3.1.js - Prototype JavaScript framework, version 1.3.1 (c) 2005 Sam Stephenson <sam@conio.net>
  Lots of thanks also go to Peter-Paul Koch for his incredibly invaluable resource at http://quirksmode.com/.  
  (c) Copyright 2005-2006, e-numera, Inc.
  http://e-numera.com/ http://ajax-web2.com/
*/

/* Requires ptototype-1.3.1.js */

/* class String enhancements */
String.prototype.inject = function() {
  var result = this;
  for (var i = 0; i < arguments.length; i++) {
    result = result.replace('{' + i + '}', arguments[i]);
  }
  return result;
}
if (!String.trim) String.trim = function (str) {
  var ch = 0, start = 0, end = str.length - 1;
  for (start = 0; start < str.length; start++)
    if (str.charCodeAt(start) > 32) break;
  for (end = str.length - 1; end > start; end--)
    if (str.charCodeAt(end) > 32) break;
  if (start != 0 || end != str.length - 1)
    return str.substr(start, end - start + 1);
  else
    return str;
}
if (!String.prototype.trim) String.prototype.trim = function () {
  return String.trim(this);
}
/* class Url */
var Url = new Object();
Object.extend(Url, {
  makeParam: function (nameOrPair, value) {
    if (typeof(nameOrPair) == 'string')
      return nameOrPair + '=' + encodeURIComponent(value);
    else
      return nameOrPair.name + '=' + encodeURIComponent(nameOrPair.value);
  },
  appendParams: function (url) {
    var delim = (url && url.lastIndexOf('?') >= 0) ? '&' : '?',
        argType = typeof(arguments[1]) == 'string' ? 2 : 1,
        params = new Array(),
        j = 0;
    for (var i = 1; i < arguments.length; i += argType, j++) {
      params[j] = this.makeParam(arguments[i], arguments[i + 1]);
    }
    return url + delim + params.join('&');
  },
  removeParams: function (url) {
    var pos = url ? url.indexOf("?") : -1;
        result = url;
    if (pos >= 0)
      result = url.substr(0, pos);
    return result;
  }
});

/* class Object enhancements */
Object.copy = function (srcObj, destObj, filter) {
  var regex;
  if (filter) regex = new RegExp(filter);
  for (var prop in srcObj) 
    try {
      if (!regex || regex.test(prop) == false) 
        destObj[prop] = srcObj[prop];
    }
    catch (ex) {}
  return destObj;
}
Object.copyMapped = function (srcObj, destObj, mapping, contextObj) { // contextObj is optional
  // Mapping = array of strings qualifying properties (or aggregated properties) in src and dest
  // Dest properties are evaluated using srcObj.eval() so simple transforms are possible
  // (e.g. ['src', 'src', 'style.left', 'style.left+1', 'id', 'id+"_copy"'])
  // If contextObj is supplied, evaluations are done in it's context.  This allows nearly uinlimited
  // transformations.  (e.g. if contextObj = window: ['src', 'myObj.myFunc(event.clientX, 'foobar')'])
  if (!contextObj) contextObj = srcObj;
  for (var i = 0; i < mapping.length; i = i + 2) {
    var dest = destObj, destAgg = mapping[i].split('.');
    for (var d = 0; d < destAgg.length - 1; p++) dest = dest[d];
    dest[destAgg[destAgg.length]] = Object.findPropValue(contextObj, mapping[i + 1]); 
  }
  return destObj;
}
// finds the value of a property that may be several objects deep. e.g.: this.style.left
Object.findPropValue = function (obj, expr) {
  try {
    var val = '';
    if (obj.eval) // mozilla
      val = obj.eval(expr);
    else { // IE only supports eval at window scope (ignores eval.call())
      var derefs = expr.split('.'); // note: assumes no array syntax!
      var prop = obj[derefs[0]];
      for (var i = 1; i < derefs.length; i++) 
        prop = prop[derefs[i]];
      val = prop;
    }
    if (val)
      return val;
    else
      return null;
  }
  catch (ex) {
    return null;
  }
}
// calls a function in a separate thread in the context of another function / thread
// useful for executing code immediately *after* the current code / event
Object.execAsync = function(asyncFunc, contextFunc, delay) { 
  if (!delay) delay = 10;
  if (contextFunc)
    setTimeout(asyncFunc.bind(contextFunc), delay);
  else
    setTimeout(asyncFunc, delay);
};

/* class Array enhancements */
Array.prototype.indexOf = function (value) {
  for (var i = 0; i < this.length; i++)
    if (this[i] == value) return i;
  return null;
}

/* class Date extensions */
Object.extend(Date, {
  toDate: function (date) {
    return typeof(date) == 'date' ? date : new Date(date);
  },
  isDate: function (date) {
    date = this.toDate(date);
    return !isNaN(date.valueOf());
  },
  format: function (date, format) {
    if (typeof(date) != 'date') date = new Date(date);
    if (!format) 
      format = 'short';
    else
      format = format.toLowerCase();
    var tokens = ['yyyy', 'yy', 'mmmm', 'mmm', 'mm', 'm', 'dd', 'd', 'wdl', 'wd', 'hh24', 'hh', 'h24', 'h', 'nn', 'n', 'ss', 's', 'AMPM', 'ampm'];
    function matchToken (str, pos) {
      var result, i;
      for (i = 0; i < tokens.length && !result; i++)
        result = str.substr(pos, tokens[i].length) == tokens[i] ? tokens[i] : null;
      return result;
    }
    function prefixWithZero (number, digits) {
      if (!digits) digits = 2;
      var result = number.toString(),
          prefix = '0000000000'.substr(0, digits - result.length);
      return prefix.length == 0 ? result : prefix + result;
    }
    function translateToken (token) {
      switch (token) {
        case 'yyyy':
          return date.getFullYear().toString();
        case 'yy':
          return date.getFullYear().toString().substr(2);
        case 'mmmm':
          return 'Not implemented'; // extract from UTC string?
        case 'mmm':
          return 'Not implemented'; // extract from UTC string?
        case 'mm':
          return prefixWithZero(date.getMonth() + 1);
        case 'm':
          return (date.getMonth() + 1).toString();
        case 'dd': 
          return prefixWithZero(date.getDate());
        case 'd':
          return date.getDate().toString();
        case 'wdl':
          return 'Not implemented'; // extract from UTC string?
        case 'wd':
          return 'Not implemented'; // extract from UTC string?
        case 'hh24':
          return prefixWithZero(date.getHours());
        case 'hh':
          return prefixWithZero(date.getHours() % 12);
        case 'h24':
          return date.getHours().toString();
        case 'h':
          return (date.getHours() % 12).toString();
        case 'nn':
          return prefixWithZero(date.getMinutes());
        case 'nn':
          return date.getMinutes().toString();
        case 'ss':
          return prefixWithZero(date.getSeconds());
        case 's':
          return date.getSeconds().toString();
        case 'fffff':
          return prefixWithZero(date.getMilliseconds(), 5);
        case 'ffff':
          return prefixWithZero(date.getMilliseconds(), 4);
        case 'fff':
          return prefixWithZero(date.getMilliseconds(), 3);
        case 'ff':
          return prefixWithZero(date.getMilliseconds());
        case 'f':
          return date.getMilliseconds().toString();
        case 'AMPM':
          return date.getHours() >= 12 ? 'PM' : 'AM';
        case 'ampm':
          return date.getHours() >= 12 ? 'pm' : 'am';
      }
    }
    var snip = '', output = '', pos = 0;
    if (format == 'short')
      output =  date.toString();
    else if (format == 'long')
      output = date.toLocaleString();
    else
      while (pos < format.length) {
        snip = matchToken(format, pos);
        if (snip)
          snip = translateToken(snip);
        else
          snip = format.substr(pos, 1);
        output += snip;
        pos += snip.length;
      }
    return output;
  }
});
Date.prototype.format = function (format) {
  return Date.format(this, format);
}

/* class Element enhancements */
Object.extend(Element, {
  getElementsByClassName: function (el, className) {
    var children = el.getElementsByTagName('*');
    var elements = new Array();
    for (var i = 0; i < children.length; i++) {
      var classNames = children[i].className.split(' ');
      for (var j = 0; j < classNames.length; j++) {
        if (classNames[j] == className) {
          elements.push(children[i]);
          break;
        }
      }
    }
    return elements;
  },
  appear: function () {
    for (var i = 0; i < arguments.length; i++)
      $(arguments[i]).style.visibility = 'visible';
  },
  disappear: function () {
    for (var i = 0; i < arguments.length; i++)
      $(arguments[i]).style.visibility = 'hidden';
  },
  show: function () {
    for (var i = 0; i < arguments.length; i++)
      this._forceShow($(arguments[i]));
  },
  _forceShow: function (el) {
    el.style.display = '';
    var display = this.findStyleProperty(el, 'display');
    if (display == 'none')
      el.style.display = this._guessNativeDisplay(el);
  },
  _guessNativeDisplay: function (el) {
    var result = '';
    switch (el.tagName) { // try some common ones
      case 'DIV': case 'P': case 'TABLE': case 'UL': case 'OL':
        result = 'block'; break;
      case 'SPAN':
        result = 'inline'; break;
      case 'LI':
        result = 'list-item'; break;
      default: // brute-force test all others
        var elCont = Element.newElement('DIV'),
            elTest1 = Element.newElement(el.tagName),
            elTest2 = Element.newElement(el.tagName);
        elCont.style.visibility = elTest1.style.visibility = elTest2.style.visibility = 'hidden';
        elCont.style.position = elTest1.style.position = elTest2.style.position = 'relative';
        elTest1.style.innerHTML = elTest2.style.innerHTML = '&nbsp;';
        elTest1.style.width = elTest2.style.width = elTest1.style.height = elTest2.style.height = '10px';
        elCont.appendChild(elTest1);
        elCont.appendChild(elTest2);
        Page.getRootElement().appendChild(elCont);
        result = elTest1.offsetTop == elTest2.offsetTop ? 'inline' : 'block';
        Page.getRootElement().removeChild(elCont);
        elCont.removeChild(elTest1);
        elCont.removeChild(elTest2);
    }
    return result;
  },
  newElement: function (tagName, id, name) { // id and name are optional
    var el = document.createElement(tagName);
    if (id) el.id = id;
    if (name) el.name = name;
    return el;
  },
  disposeElement: function (element) {
    var el = $(element);
    if (el && el.parentNode) el.parentNode.removeChild(el);
  },
  setOpacity: function (element, alpha) {
    element.style.MozOpacity = alpha;
    element.style.opacity = alpha;
    if (alpha == 1.0) // fix for IE font rendering bug
      this.removeIEFilter(element, 'Alpha');
    else
      this.setIEFilter(element, 'Alpha(opacity=' + parseInt(100 * alpha) + ')');
  },
  setIEFilter: function (element, filterTerm) {
    this.removeIEFilter(element, filterTerm);
    element.style.filter += ' ' + filterTerm;
  },
  removeIEFilter: function (element, filterTerm) {
    var filter = element.style.filter || '', p = filterTerm.indexOf('('), 
      filterName = p >= 0 ? filterTerm.substr(0, p) : filter;
    p = filter.indexOf(filterName);
    if (p >= 0) {
      var start = filter.lastIndexOf(' ', p) + 1, end = filter.indexOf(')', p);
      if (end > start) { // if not, something is screwed up
        filter = filter.substr(0, start) + filter.substr(end + 1);
        element.style.filter = filter;
      }
    }
  },
  setCursor: function (element, cursor) {
    if ((Browser.isIE5 || Browser.isIE55) && cursor == 'pointer')
      element.style.cursor = 'hand';
    else
      element.style.cursor = cursor;
  },
  switchParent: function (element, newParent) {
    newParent.appendChild(element);
  },
  intToPx: function (i) {
    return parseInt(i).toString() + 'px';
  },
  pxToInt: function (px) {
    return parseInt(px);
  },
  findStyleProperty: function (element, property, pseudoElement) {
    // try inline property first
    var value = element.style[property]; 
    // try offset properties next for faster operation
    if (!value && property == 'left' || property == 'top' || property == 'width' || property == 'height') {
      var s = new String();
      value = element['offset' + property.substr(0, 1).toUpperCase() + property.substr(1)];
      if (value) value += 'px';
    }
    if (!value) { // if not found
      if (element.currentStyle) { //  IE 5-6
        if (property.indexOf('-') >= 0)
          value = element.currentStyle[this._textToJsStyle(property)];
        else
          value = element.currentStyle[property];
      }
      else {
        var styleObj;
        if (document.defaultView && document.defaultView.getComputedStyle) // W3C / Safari
          styleObj = document.defaultView.getComputedStyle(element, pseudoElement);
        else if (window.getComputedStyle) // W3C / Moz
          styleObj = window.getComputedStyle(element, pseudoElement);
        if (styleObj) value = styleObj.getPropertyValue(property);
      }
    }
    return value;
  },
  _textToJsStyle: function (text) {
    text = text.toLowerCase();
    var p = text.indexOf('-');
    while (p >= 0) {
      text = text.substr(0, p) + text.substr(p + 1, 1).toUpperCase() + text.substr(p + 2);
      p = text.indexOf('-');
    }
    return text;
  },
  resetStyleProperty: function (element, property, psuedoElement) {
    element.style[property] = '';
    element.style[property] = this.findStyleProperty(element, property, psuedoElement);
  },
  getSize: function (element) {
    // TO DO: this only works for border-box model, not content-box (borders and margins interfere)
    var w = this.findStyleProperty(element, 'width');
    var h = this.findStyleProperty(element, 'height');
    return new Size(this.pxToInt(w), this.pxToInt(h));
  },
  setSize: function (element, size) {
    element.style.width = this.intToPx(size.width);
    element.style.height = this.intToPx(size.height);
  },
  overlap: function (e1, e2) {
    var rect1 = new Rect(this.getClientPos(e1), this.getSize(e1)),
      rect2 = new Rect(this.getClientPos(e2), this.getSize(e2));
    return rect1.intersectsWith(rect2);
  }
});

/* class Floater */
var Floater = new Object();
Object.extend(Floater, Element);
Object.extend(Floater, {
  isFloating: function (element) {
    return element.style.position == 'absolute';
  },
  makeFloating: function () {
    for (var i = 0; i < arguments.length; i++)
      arguments[i].style.position = 'absolute';
  },
  switchParent: function (element, newParent, keepInPlace) {
    var pt;
    if (keepInPlace) {
      pt = this.getClientPos(element);
    }
    Element.switchParent(element, newParent);
    if (keepInPlace) {
      var parentPt = Floater.getClientPos(newParent);
      element.style.left = this.intToPx(pt.X - parentPt.X);
      element.style.top = this.intToPx(pt.Y - parentPt.Y);
    }
  },
  getSize: function (element) {
    // TO DO: this only works for border-box model, not content-box (borders and margins interfere)
    var w = element.offsetWidth;
    var h = element.offsetHeight;
    return new Size(w, h);
  },
  setSize: function (element, size) {
    element.style.width = this.intToPx(size.width);
    element.style.height = this.intToPx(size.height);
  },
  getClientPos: function (element) {
    // TO DO: this only works for border-box model, not content-box (borders and margins interfere)
    var el = element;
    var parent = el.offsetParent;
    var pos = new Point(0, 0);
    if (parent && parent.tagName == 'HTML') { // IE quirk
      pos.offset(this.getOffset(el));
      var body = document.documentElement || document.body;
      pos.offset(-body.scrollLeft, -body.scrollTop);
    }
    else 
      while (el && parent){// && (parent.tagName != 'HTML')) { 
        var pt = this.getOffset(el);
        pos.X += pt.X - (parent.scrollLeft || 0);
        pos.Y += pt.Y - (parent.scrollTop || 0);
        el = parent; 
        parent = el.offsetParent; 
      }
    return pos;
  },
  getOffset: function (element) {
    return new Point(element.offsetLeft, element.offsetTop);
  },
  setOffset: function (element, left, top) {
    element.style.left = Element.intToPx(left);
    element.style.top = Element.intToPx(top);
  },
  setLeft: function (element, left) {
    element.style.left = Element.intToPx(left);
  },
  setTop: function (element, top) {
    element.style.top = Element.intToPx(top);
  },
  getClientTop: function (element) {
    return this.getClientPos(element).Y;
  },
  getClientLeft: function (element) {
    return this.getClientPos(element).X;
  },
  setClientTop: function (element, top) { // element must be floating to work
    element.style.top = this.intToPx(this.getClientTop(element.parentNode) + top);
  },
  setClientLeft: function (element, left) { // element must be floating to work
    element.style.left = this.intToPx(this.getClientLeft(element.parentNode) + left);
  },
  show: function() { // overrides Element.show from base library
    for (var i = 0; i < arguments.length; i++) {
      var element = $(arguments[i]);
      element.style.visibility = 'visible';
    }
  },
  hide: function() { // overrides Element.hide from base library
    for (var i = 0; i < arguments.length; i++) {
      var element = $(arguments[i]);
      element.style.visibility = 'hidden';
    }
  },
  overlap: function (e1, e2) {
    var rect1 = new Rect(this.getClientPos(e1), this.getSize(e1)),
      rect2 = new Rect(this.getClientPos(e2), this.getSize(e2));
    return rect1.intersectsWith(rect2);
  }
});

/* class PropertyBag  */
// defines a list of properties and values
function PropertyBag(sourceObject) {
  if (sourceObject)
    Object.copy(sourceObject, this, 'extend');  // don't copy extend function
  this.extend = null; // good enough for now (see next line)
  //delete this.extend; // TO DO: why doesn't ths remove?
};
PropertyBag.prototype = {extend: null}; // do not add a constructor or it will end up in the property bag as a property
// TO DO: why doesn't this remove?
//delete PropertyBag.prototype.extend;  // remove extend function

/* class Color */
var Color = Class.create();
Object.extend(Color, {
  hexStringToRgb: function (hex) {
    var rgb = new Color(0, 0, 0);
    if (hex && hex.length > 0) {
      hex = hex.toLowerCase();
      if (hex.charAt(0) == '#') hex = hex.substr(1);
      if (hex.length == 3) hex = '0' + hex.charAt(0) + '0' + hex.charAt(1) + '0' + hex.charAt(2);
      if (hex.length == 6) {
        rgb.red = (hex.charCodeAt(0) - (hex.charAt(0) > 'a' ? 87 : 48)) * 16 
          + (hex.charCodeAt(1) - (hex.charAt(1) > 'a' ? 87 : 48));
        rgb.green = (hex.charCodeAt(2) - (hex.charAt(2) > 'a' ? 87 : 48)) * 16 
          + (hex.charCodeAt(3) - (hex.charAt(3) > 'a' ? 87 : 48));
        rgb.blue = (hex.charCodeAt(4) - (hex.charAt(4) > 'a' ? 87 : 48)) * 16 
          + (hex.charCodeAt(5) - (hex.charAt(5) > 'a' ? 87 : 48));
      }
    }
    return this._roundValues(rgb);
  },
  rgbToHexString: function (red, green, blue) {
    var lRed = red % 16,
        hRed = Math.floor(red / 16),
        sRed = String.fromCharCode(hRed + (hRed > 9 ? 87 : 48))
          + String.fromCharCode(lRed + (lRed > 9 ? 87 : 48));
    var lGreen = green % 16,
        hGreen = Math.floor(green / 16),
        sGreen = String.fromCharCode(hGreen + (hGreen > 9 ? 87 : 48))
          + String.fromCharCode(lGreen + (lGreen > 9 ? 87 : 48));
    var lBlue = blue % 16,
        hBlue = Math.floor(blue / 16),
        sBlue = String.fromCharCode(hBlue + (hBlue > 9 ? 87 : 48))
          + String.fromCharCode(lBlue + (lBlue > 9 ? 87 : 48));
    return '#' + sRed + sGreen + sBlue;
  },
  rgbToHsv: function (red, green, blue) {
    var hsv = {},
        max = Math.max(Math.max(red, green), blue),
        min = Math.min(Math.min(red, green), blue),
        delta = max - min;
    hsv.value = max / 255;
    if (max == 0) {
      hsv.saturation = 0;
      hsv.hue = -1;
    }
    else {
      hsv.saturation = delta / max;
      if (red == max)
        hsv.hue = (green - blue ) / delta;
      else if (green == max)
        hsv.hue = 2 + ( blue - red ) / delta;
      else
        hsv.hue = 4 + ( red - green ) / delta;
      hsv.hue = hsv.hue * 60 + (hsv.hue < 0 ? 360 : 0);
    }
    return hsv;
  },
  hsvToRgb: function (hue, saturation, value) {
    value *= 255;
    var rgb = new Color(value, value, value);
    if (saturation != 0) {
      var zone = Math.floor(hue / 60),
          frac = hue / 60 - zone,
          p = value * (1 - saturation),
          q = value * (1 - saturation * frac),
          t = value * (1 - saturation * (1 - frac));
	    switch (zone) {
	      case 0:
	        rgb.red = value;
	        rgb.green = t;
	        rgb.blue = p;
	        break;
	      case 1:
	        rgb.red = q;
	        rgb.green = value;
	        rgb.blue = p;
	        break;
	      case 2:
	        rgb.red = p;
	        rgb.green = value;
	        rgb.blue = t;
	        break;
	      case 3:
	        rgb.red = p;
	        rgb.green = q;
	        rgb.blue = value;
	        break;
	      case 4:
	        rgb.red = t;
	        rgb.green = p;
	        rgb.blue = value;
	        break;
	      case 5:
	        rgb.red = value;
	        rgb.green = p;
	        rgb.blue = q;
	        break;
	    }
    }
    return this._roundValues(rgb);
  },
  blendColors: function (color1, color2) {
    return new Color((color1.red + color2.red) / 2, 
      (color1.green + color2.green) / 2, 
      (color1.blue + color2.blue) / 2);
  },
  _roundValues: function (color) {
    for (var prop in color) 
      if (typeof(color[prop]) == 'number')
        color[prop] = Math.round(color[prop]);
    return color;
  }
});
Color.prototype = {
  initialize: function (hexOrRed, green, blue) {
    if (typeof(hexOrRed) == 'string')
      this.fromHex(hexOrRed);
    else {
      this.red = hexOrRed || 0;
      this.green = green || 0;
      this.blue = blue || 0;
      Color._roundValues(this);
    }
  },
  toHex: function () {
    return Color.rgbToHexString(this.red, this.green, this.blue);
  },
  fromHex: function (hex) {
    Object.copy(Color.hexStringToRgb(hex), this);
  },
  toHsv: function () {
    return Color.rgbToHsv(this.red, this.green, this.blue);
  },
  fromHsv: function (hue, saturation, value) {
    Object.copy(Color.hsvToRgb(hue, saturation, value), this);
  },
  saturateTo: function (saturation) {
    var hsv = this.toHsv();
    hsv.saturation = Math.min(Math.max(saturation, 0), 1);
    Object.copy(Color.hsvToRgb(hsv.hue, hsv.saturation, hsv.value), this);
  },
  valueTo: function (value) {
    var hsv = this.toHsv();
    hsv.value = Math.min(Math.max(value, 0), 1);
    Object.copy(Color.hsvToRgb(hsv.hue, hsv.saturation, hsv.value), this);
  },
  fade: function (factor) {
    var hsv = this.toHsv();
    hsv.saturation = Math.min(Math.max(hsv.saturation * (1 - factor), 0), 1);
    Object.copy(Color.hsvToRgb(hsv.hue, hsv.saturation, hsv.value), this);
  },
  darken: function (factor) {
    var hsv = this.toHsv();
    hsv.saturation = Math.min(Math.max(hsv.value * (1 - factor), 0), 1);
    Object.copy(Color.hsvToRgb(hsv.hue, hsv.saturation, hsv.value), this);
  }
};
Color.prototype.constructor = Color;

/* class Point */
var Point = Class.create();
Point.prototype = {
  initialize: function (x, y) {
   this.X = isNaN(parseInt(x)) ? 0 : parseInt(x);
   this.Y = isNaN(parseInt(y)) ? 0 : parseInt(y);
  },
  offset: function (dxOrPt, dy) { // pass a Point, or an x and y delta pair
    if (dxOrPt.constructor == Point) {
      this.X += dxOrPt.X;
      this.Y += dxOrPt.Y;
    }
    else {
      this.X += dxOrPt;
      this.Y += dy;
    }
    return this;
  },
  mult: function (factor) {
    this.X *= factor;
    this.Y *= factor;
    return this;
  }
}
Point.prototype.constructor = Point;

/* class Size */
var Size = Class.create();
Size.prototype.extend({
  initialize: function (width, height) {
    this.width = isNaN(parseInt(width)) ? 0 : parseInt(width);
    this.height = isNaN(parseInt(height)) ? 0 : parseInt(height);
  },
  inc: function (dxOrSize, dy) {
    if (dxOrSize.constructor == Size) {
      this.width += dxOrPt.width;
      this.height += dxOrPt.height;
    }
    else {
      this.width += dxOrSize;
      this.height += dy;
    }
    return this;
  },
  mult: function (factor) {
    var pt = new Point(this.width, this.height);
    pt.mult(factor);
    this.width = pt.X, this.height = pt.Y;
    return this;
  }
});
Object.extend(Size, {
  maxExtent: function (size1, size2) {
    return new Size(Math.max(size1.width, size2.width), Math.max(size1.height, size2.height));
  },
  minExtent: function (size1, size2) {
    return new Size(Math.min(size1.width, size2.width), Math.min(size1.height, size2.height));
  }
});
Size.prototype.constructor = Size;

/* class Rect */
var Rect = Class.create();
Rect.prototype.extend({
  initialize: function (pt, size) {
    this.left = pt ? pt.X : 0;
    this.top = pt ? pt.Y : 0;
    this.width = size ? size.width : 0;
    this.height = size ? size.height : 0;
  },
  getRight: function () {
    return this.left + this.width - 1;
  },
  getBottom: function () {
    return this.top + this.height - 1;
  },
  intersectsWith: function (rect) {
    return (((this.left <= rect.getRight()) && (this.left >= rect.left)) // left edge intersects
      || ((this.getRight() >= rect.left) && (this.getRight() <= rect.getRight()))) // right edge intersects
      && (((this.top <= rect.getBottom()) && (this.top >= rect.top)) // top edge intersects
      || ((this.getBottom() >= rect.top) && (this.getBottom() <= rect.getBottom()))); // bottom edge intersects
  },
  getOrigin: function () {
    return new Point(this.left, this.top);
  },
  getSize: function () {
    return new Point(this.width, this.height);
  }
});
Rect.prototype.constructor = Rect;

/* class Event extensions */
Object.extend(Event, {
  getSource: function (event) {
    return this.element(event);
  },
  getKey: function (event) {
    return event.which || event.keyCode;
  },
  addEvent: function (element, eventName, eventFunc, useCapture) {
    if (eventName.substr(0, 2) == 'on') eventName = eventName.substr(2);
    return this.observe(element, eventName, eventFunc, useCapture);
  },
  removeEvent: function (element, eventName, eventFunc, useCapture) {
    if (eventName.substr(0, 2) == 'on') eventName = eventName.substr(2);
    return this.stopObserving(element, eventName, eventFunc, useCapture);
  },
  isObserving: function (element, eventName, eventFunc, useCapture) {
    var result = false;
    if (this.observers) {
      for (var i = 0; i < this.observers.length && !result; i++)
        result = this.observers[i][0] == element 
          && this.observers[i][1] == eventName
          && this.observers[i][2] == eventFunc
          && (!useCapture || this.observers[i][3] == useCapture);
    }
    return result;
  },
  hasEvent: function (element, eventName, useCapture) {
    return this.isObserving(element, eventName, useCapture);
  }
});

/* class Page */
var Page = new Object();
Object.extend(Page, {
  getClientHeight: function() {
    return getClientSize.height;
  },
  getClientWidth: function() {
    return getClientSize.width;
  },
  getClientSize: function() {
    var x = 0, y = 0;
    // all except Explorer
    if (self.innerHeight) {
      x = self.innerWidth;
      y = self.innerHeight;
    }
    // Explorer 6 Strict Mode
    else if (document.documentElement && document.documentElement.clientHeight) {
      x = document.documentElement.clientWidth;
      y = document.documentElement.clientHeight;
    }
    // other Explorers
    else if (document.body) {
      x = document.body.clientWidth;
      y = document.body.clientHeight;
    }
    return new Size(x, y);
  },
  getRootElement: function () {
    if (document.getElementsByTagName)
      return document.getElementsByTagName('BODY')[0];
    else if (document.body)
      return document.body;
    else
      return null;
  },
  // TO DO: these two functions are redundant with Url class
  makeUrl: function () { // arg 1 = base url, other arg pairs are parameters
    if (arguments.length == 0) return "";
    var base = arguments[0], url = base;
    var sep = (base == "" ? "" : base.indexOf('?') <= 0 ? '?' : '&');
    for (var i = 1; i < arguments.length; i = i + 2) {
      url += sep + arguments[i].toString() + (arguments[i + 1] && arguments[i + 1] != "" ? '=' + encodeURIComponent(arguments[i + 1].toString()) : '');
      if (sep != '&') sep = '&';
    }
    return url;
  },
  makeParamList: function () {
    var params = arguments[0] + (arguments[1] && arguments[1] != "" ? '=' + encodeURIComponent(arguments[1].toString()) : '');
    for (var i = 2; i < arguments.length; i = i + 2) {
      params += '&' + arguments[i] + (arguments[i + 1] && arguments[i + 1] != "" ? '=' + encodeURIComponent(arguments[i + 1].toString()) : '');
    }
    return params;
  },
  getPageSize: function () { // doesn't inlcude any margins
    var x = 0, y = 0;
    if (document.body.scrollHeight > document.body.offsetHeight) 
      x = document.body.scrollWidth, y = document.body.scrollHeight;
    else
      x = document.body.offsetWidth, y = document.body.offsetHeight;
    return new Size(x, y);
  }
});

/* base class DraggableElement */
var DraggableElement = Class.create();
DraggableElement.DragStates = {NOT_DRAGGING: 0, READY: 1, DRAGGING: 2};
DraggableElement.prototype = {
  initialize: function (element, constrainer) {
    this.draggedElement = element;
    if (constrainer && constrainer.constructor == Rect)
      this.constrainer = constrainer;
    else if (constrainer && constrainer.style)
      this.constrainer = new Rect(Floater.getClientPos(constrainer), Floater.getSize(constrainer));
    this.draggedElement.onmousedown = this._element_onmousedown.bind(this);
  },
  _element_onmousedown: function (e) {
    this.wasMoved = false;
    var event = (e || window.event);
    var mousekey = (event.which || event.button);
    if (mousekey == 1) {
      this._stopEventPropagation(event);
      this._startDrag(
        event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft), 
        event.clientY + (document.documentElement.scrollTop || document.body.scrollTop));
    }
  },
  _element_onmousemove: function (e) {
    var event = (e || window.event);
    var delta = new Point(
      event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft) - this.dragClientPt.X, 
      event.clientY + (document.documentElement.scrollTop || document.body.scrollTop) - this.dragClientPt.Y);
    if (this.dragState == DraggableElement.DragStates.READY) {
      if (Math.abs(delta.X) > 1 || Math.abs(delta.Y) > 1) { // moved far enough to be a drag instead of a click
        this.dragState = DraggableElement.DragStates.DRAGGING;
        this.wasMoved = true;
        if (this.onChangedDragState) 
          this.onChangedDragState(this, Floater.getOffset(this.draggedElement), this.dragState);
      }
    }
    if (this.dragState == DraggableElement.DragStates.DRAGGING) {
      // TO DO: prevent a user from placing icons off-parent (don't do modifiedCallback if not moved at all)
      this._moveElementTo(delta.X + this.dragOffsetPt.X, delta.Y + this.dragOffsetPt.Y);
    }
  },
  _element_onmouseup: function (e) {
    this._stopDrag();
  },
  _stopEventPropagation: function (event) {
    if (event.preventDefault) {
      event.preventDefault(); 
      event.stopPropagation();
    }
    else 
      window.event.returnValue = false;
  },
  _captureElementEvents: function () {
    if (this.draggedElement.setCapture) this.draggedElement.setCapture();
    this.prev_onmousemove = this.draggedElement.onmousemove;
    this.prev_onmouseup = this.draggedElement.onmouseup;
    this.prev_ondragstart = this.draggedElement.ondragstart;
    this.draggedElement.onmousemove = this._element_onmousemove.bind(this);
    this.draggedElement.onmouseup = this._element_onmouseup.bind(this);
    this.draggedElement.ondragstart = function() { return false; } // cancel IE's built-in (lame) functionality
  },
  _releaseElementEvents: function () {
    if (this.draggedElement.releaseCapture) this.draggedElement.releaseCapture();
    this.draggedElement.onmousemove = this.prev_onmousemove;
    this.draggedElement.onmouseup = this.prev_onmouseup;
    this.draggedElement.ondragstart = this.prev_ondragstart;
  },
  _captureDocumentEvents: function () {
    this.prev_document_ondragstart = document.ondragstart;
    this.prev_document_onmousemove = document.onmousemove;
    this.prev_document_onmouseup = document.onmouseup;
    this.prev_document_onselectstart = document.onselectstart;
    this.prev_document_ondragstart = document.ondragstart;
    document.onmousemove = this._element_onmousemove.bind(this);
    document.onmouseup = this._element_onmouseup.bind(this);
    document.onselectstart = function() {return false; }
    document.ondragstart = function() { return false; } // cancel IE's built-in (lame) functionality
  },
  _releaseDocumentEvents: function () {
    document.ondragstart = this.prev_document_ondragstart;
    document.onmousemove = this.prev_document_onmousemove;
    document.onmouseup = this.prev_document_onmouseup;
    document.onselectstart = this.prev_document_onselectstart;
    document.ondragstart = this.prev_document_ondragstart;
  },
  _moveElementTo: function (x, y) {
    if (this.constrainer) {
      var sz = Floater.getSize(this.draggedElement);
      x = Math.min(Math.max(this.constrainer.left, x), this.constrainer.getRight() - sz.width);
      y = Math.min(Math.max(this.constrainer.top, y), this.constrainer.getBottom() - sz.height);
    }
    Floater.setOffset(this.draggedElement, x, y);
  },
  _startDrag: function (x, y) {
    this.dragClientPt = new Point(x, y);
    this.dragOffsetPt = Floater.getOffset(this.draggedElement);
    this._captureElementEvents();
    this._captureDocumentEvents();
    this.dragState = DraggableElement.DragStates.READY;
    if (this.onChangedDragState) 
      this.onChangedDragState(this, this.dragOffsetPt, this.dragState);
  },
  _stopDrag: function () {
    this._releaseDocumentEvents();
    this._releaseElementEvents();
    this.dragState = DraggableElement.DragStates.NOT_DRAGGING;
    if (this.onChangedDragState) 
      this.onChangedDragState(this, Floater.getOffset(this.draggedElement), this.dragState);
  }
}
DraggableElement.prototype.constructor = DraggableElement;

/* class HtmlDialog */
var HtmlDialog = Class.create();
HtmlDialog.prototype.extend({
  onSubmit: null,
  onCancel: null,
  initialize: function (topElement, titleElement, submitElements, cancelElements, shadowElement) {
    this.topElement = topElement;
    if (titleElement) this.titleElement = titleElement;
    if (submitElements) this.submitElements = submitElements;
    if (cancelElements) this.cancelElements = cancelElements;
    if (shadowElement) this.shadowElement = shadowElement;
  },
  show: function () {
    if (this.submitElements && this.submitElements.length > 0) 
      for (var i = 0; i < this.submitElements.length; i++)
        this.submitElements[i].onclick = (function (e) { this.hide(); if (this.onSubmit) this.onSubmit(this, e); }).bind(this);
    if (this.cancelElements && this.cancelElements.length > 0) 
      for (var i = 0; i < this.cancelElements.length; i++)
        this.cancelElements[i].onclick = (function (e) { this.hide(); if (this.onCancel) this.onCancel(this, e); }).bind(this);
    this._setTitle(this.title);
    Floater.show(this.topElement);
    if (this.shadowElement) {
      this._sizeShadow();
    }
    var forms = this._getForms();
    if (forms.length > 0)
      Form.focusFirstElement(forms[0]);
    if (this.onShow)
      this.onShow(this);
  },
  hide: function () {
    Floater.hide(this.topElement);
    if (this.shadowElement) 
      Floater.hide(this.shadowElement);
    if (this.onHide)
      this.onHide(this);
  },
  clearForm: function (formIndex) {
    var forms = this._getForms();
    if (forms.length > 0)
      forms[formIndex || 0].clear();
  },
  setFormValues: function (propertyBag, formIndex) {
    var forms = this._getForms();
    var index = (formIndex || 0);
    if (forms.length > index) {
      for (var prop in propertyBag) {
      var el = forms[index][prop] || document.getElementById(prop);
        if (el && el.type)
          switch (el.type.toLowerCase()) {
            case 'checkbox':  
            case 'radio':
              el.checked = propertyBag[prop] == el.value;
              break;
            case 'select-one':
            case 'select-many':
              this._loadSelect(el, propertyBag[prop]);
              break;
            default:
              el.value = propertyBag[prop];
              break;
          } // switch
      } // for 
    } // if 
  },
  getFormValues: function (propertyBag, formIndex) {
    var forms = this._getForms();
    var index = (formIndex || 0);
    if (forms.length > index) {
      for (var prop in propertyBag) {
        var el = forms[index][prop] || document.getElementById(prop);
        propertyBag[prop] = null; // set default for checkboxes and radio buttons
        if (el && el.type) 
          switch (el.type.toLowerCase()) {
            case 'checkbox':  
            case 'radio':
              if (el.checked) propertyBag[prop] = el.value;
              break;
            case 'select-one':
              propertyBag[prop] = el.selectedIndex >= 0 ? el.options[el.selectedIndex].value : null;
              break;
            case 'select-many':
              this._unloadSelect(el, propertyBag[prop]);
              break;
            default:
              propertyBag[prop] = el.value;
              break;
          } // switch
      } // for 
    } // if 
  },
  _loadSelect: function (element, propertyBag) {
    element.options.length = 0;
    for (var prop in propertyBag) {
      if (propertyBag[prop] != null) {
        var opt = new Option(propertyBag[prop], prop);
        element.options[element.options.length] = opt;
      }
    }
  },
  _unloadSelect: function (element, propertyBag) { // for select-many / multi-selects
    for (var i = 0; i < element.options.length; i++)
      if (element.options[i].selected)
        propertyBag[element.options[i].value] = element.options[i].text;
  },
  _setTitle: function (title) {
    if (this.titleElement && (typeof title != 'undefined')) this.titleElement.innerHTML = title;
  },
  _getForms: function () {
    return this.topElement.getElementsByTagName('form');
  },
  _sizeShadow: function () {
    Floater.setSize(this.shadowElement, Floater.getSize(this.topElement));
    Floater.show(this.shadowElement);
  }
});
HtmlDialog.prototype.constructor = HtmlDialog;

/* class DraggableDialog */
var DraggableDialog = Class.create();
DraggableDialog.prototype.extend(new DraggableElement()).extend(new HtmlDialog()).extend({
  initialize: function (draggedElement, grabElement, titleElement, submitElements, cancelElements, shadowElement) { // grabElement = element that user clicks to drag
    if (!grabElement) grabElement = draggedElement;
    HtmlDialog.prototype.initialize.call(this, draggedElement, titleElement, submitElements, cancelElements, shadowElement);
    this.grabElement = grabElement;
    this.draggedElement = draggedElement;
    this.grabElement.onmousedown = this._element_onmousedown.bind(this);
  }
});
DraggableDialog.prototype.constructor = DraggableDialog;

/* class DynamicDialog */
ReusableDialog = Class.create();
ReusableDialog.prototype.extend(new HtmlDialog()).extend({
  initialize: function (containerElement, grabElement, placeholder, content, titleElement, submitElements, cancelElements, shadowElement) { // content is inserted into placeholder
    HtmlDialog.prototype.initialize.call(this, content, titleElement, submitElements, cancelElements, shadowElement);
    this.placeholder = placeholder;
    this.content = content;
    this.containerElement = containerElement;
  },
  show: function () {
    if (this.placeholder.hasChildNodes()) { // while loop hangs sometimes????  use for
      for (var i = 0 ; i < this.placeholder.childNodes.length; i++)
        this.placeholder.removeChild(this.placeholder.childNodes[i]);
    }
    this.placeholder.appendChild(this.content);
    Floater.show(this.content);
    Floater.show(this.containerElement);
    HtmlDialog.prototype.show.call(this);
  },
  hide: function () {
    Floater.hide(this.containerElement);
    Floater.hide(this.content);
  },
  _sizeShadow: function () {
    Floater.setSize(this.shadowElement, Floater.getSize(this.containerElement));
    Floater.show(this.shadowElement);
  }
});
ReusableDialog.prototype.constructor = ReusableDialog;

/* class ModalDialog -- must inherit! */
var ModalDialog = Class.create();
ModalDialog.prototype.extend(new HtmlDialog()).extend({
  initialize: function (modalElement, titleElement, opacity, bgcolor, submitElements, cancelElements, shadowElement) {
    HtmlDialog.prototype.initialize.call(this, modalElement, titleElement, submitElements, cancelElements, shadowElement);
    this.opacity = opacity;
    this.bgcolor = bgcolor;
    this.modalElement = modalElement;
  },
  show: function () {
    HtmlDialog.prototype.show.call(this);
    var block = Element.newElement('Div');
    this.blockingElement = block;
    document.body.appendChild(block);
    Element.setOpacity(block, Number("0" + this.opacity));
    if (this.bgcolor) block.style.backgroundColor = this.bgcolor;
    Floater.makeFloating(block);
    this._sizeBlock();
    this._prev_window_onresize = window.onresize;
    window.onresize = this._sizeBlock.bind(this);
    block.style.zIndex = Element.findStyleProperty(this.modalElement, 'z-index') - 1; // assumes dialog is already at top of z-order
  },
  hide: function () {
    window.onresize = this._prev_window_onresize;
    Element.disposeElement(this.blockingElement);
    HtmlDialog.prototype.hide.call(this);
  },
  _sizeBlock: function () {
    var area1 = Page.getPageSize(), area2 = Page.getClientSize(),
      area = Size.maxExtent(area1, area2);
    this.blockingElement.style.top = '0px';
    this.blockingElement.style.left = '0px';
    // TO DO: this causes FF to add a second scrollbar if one is showing
    this.blockingElement.style.width = area.width + 'px';
    this.blockingElement.style.height = area.height + 'px';
  }
});
ModalDialog.prototype.constructor = ModalDialog;

/* class UltraDialog */
var UltraDialog = Class.create();
UltraDialog.prototype.extend(new ModalDialog()).extend(new DraggableDialog()).extend(new ReusableDialog()).extend({
  initialize: function (containerElement, submitElements, cancelElements, grabElement, placeholder, content, titleElement, modalOpacity, modalBgcolor, shadowElement) {
    DraggableDialog.prototype.initialize.call(this, containerElement, grabElement);
    ReusableDialog.prototype.initialize.call(this, containerElement, grabElement, placeholder, content);
    ModalDialog.prototype.initialize.call(this, containerElement, null, modalOpacity, modalBgcolor);
    // HtmlDialog must be last to override the setting of topElement (should be = content)
    HtmlDialog.prototype.initialize.call(this, content, titleElement, submitElements, cancelElements, shadowElement);
  },
  show: function () {
    ReusableDialog.prototype.show.call(this);
    ModalDialog.prototype.show.call(this);
  },
  hide: function () {
    ReusableDialog.prototype.hide.call(this);
    ModalDialog.prototype.hide.call(this);
  }
});
UltraDialog.prototype.constructor = UltraDialog;

var MacZoomDialog = Class.create();
MacZoomDialog.prototype.extend(new HtmlDialog()).extend({
  initialize: function (topElement, titleElement, submitElements, cancelElements, srcElement, transparentImageFile, shadowElement) {
    this.srcElement = srcElement;
    this.transparentImageFile = transparentImageFile;
    this.image = new Image();
    this.image.src = this.transparentImageFile; // pre-load
    this.image.style.zIndex = 4000; // TO DO: don't hard-code
    Floater.makeFloating(this.image);
    Floater.hide(this.image);
    this.image.style.border = '1px dotted black';
    Page.getRootElement().appendChild(this.image);
    this._isZooming = false;
    this.isShowing = false;
    this._animTime = 360; // msec
    this.isReusable = false; // set to true if this dialog will service multple srcElement targets
    HtmlDialog.prototype.initialize.call(this, topElement, titleElement, submitElements, cancelElements, shadowElement);
  },
  show: function () {
    if (this._currSrc && (this._currSrc != this.srcEleemnt) && this.isReusable) { // zoom back
      this._startZooming(-1, this._currSrc);
      this.hideDialog();
    }
    else
      this._startZooming(1, this.srcElement);
  },
  hide: function () {
    this._startZooming(-1, this.srcElement);
    this.hideDialog();
  },
  hideDialog: function () {
    HtmlDialog.prototype.hide.call(this);
    this.isShowing = false;
  },
  _startZooming: function (direction, source) {
    this._currSrc = source;
    if (!this._isZooming) {
      this._isZooming = true;
      this._startPt = Floater.getClientPos(source);
      this._startSize = Floater.getSize(source);
    }
    else { // keep going at current state (may be opposite direction, though)
      this._startPt = this._currPt;
      this._startSize = this._currSize;
    }
    this._stopPt = Floater.getClientPos(this.topElement);
    // TO DO: this won't always work: need to know coordinates before showing!
    this._stopSize = Floater.getSize(this.topElement);
    if (direction < 0) { // switch start and stop
      var tmp = this._startPt;
      this._startPt = this._stopPt;
      this._stopPt = tmp;
      tmp = this._startSize;
      this._startSize = this._stopSize;
      this._stopSize = tmp;
    }
    this._leftRate = (this._stopPt.X - this._startPt.X) / this._animTime;
    this._topRate = 1.0 * (this._stopPt.Y - this._startPt.Y) / this._animTime;
    this._widthRate = 1.0 * (this._stopSize.width - this._startSize.width) / this._animTime;
    this._heightRate = 1.0 * (this._stopSize.height - this._startSize.height) / this._animTime;
    this._direction = direction;
    this._startTime = new Date();
    clearInterval(this._animThread);
    this._animThread = setInterval(this._doZoom.bind(this), 20);
    this._placeImage(this._startPt, this._startSize);
    Floater.show(this.image);
  },
  _stopZooming: function () {
    clearInterval(this._animThread);
    Floater.hide(this.image);
    this._isZooming = false;
    if (this._direction > 0) // zoomed out
      this.showDialog();
    else {
      if (this._currSrc != this.srcElement) // needed to zoom back before zooming out to new source
        this._startZooming(1, this.srcElement);
      else
        this._currSrc = null;
    }
  },
  showDialog: function () {
    HtmlDialog.prototype.show.call(this);
    this.isShowing = true;
  },
  _doZoom: function () {
    var delta = (new Date()).getTime() - this._startTime.getTime();
    if (delta > this._animTime)
      this._stopZooming();
    else {
      this._currPt = new Point(this._startPt.X + delta * this._leftRate, this._startPt.Y + delta * this._topRate);
      this._currSize = new Size(this._startSize.width + delta * this._widthRate, this._startSize.height + delta * this._heightRate);
      this._placeImage(this._currPt, this._currSize);
    }
  },
  _placeImage: function (pt, size) {
    this.image.style.left = Element.intToPx(pt.X);
    this.image.style.top = Element.intToPx(pt.Y);
    this.image.style.width = Element.intToPx(size.width);
    this.image.style.height = Element.intToPx(size.height);
  }
});
MacZoomDialog.prototype.constructor = MacZoomDialog;

/* class ShadowDecorator */
var ShadowDecorator = Class.create();
ShadowDecorator.prototype = {
  initialize: function (mainElement, intensity, width, hideInitially) {
    // mainElement must be able to accept child elements (DIV, SPAN, LI, LABEL, P, etc...) and must have overflow set to visible
    this.element = mainElement;
    this._i = intensity || 0.5;
    this._w = width;
    if (!hideInitially)
      this.show();
  },
  show: function () {
    this._prepare();
    this._show();
  },
  hide: function () {
    this._prepare();
    this._hide();
  },
  _createDiv: function () {
    var div = Element.newElement('DIV');
    with (div.style) {
      display = 'none';
      position = 'absolute';
      backgroundColor = 'black';
      top = Element.intToPx(this._w);
      left = Element.intToPx(this._w);
      width = '100%';
      height = '100%'; // IE doesn't work if parent's height is 'auto'
      zIndex = '-10000';
    }
    Element.setOpacity(div, this._i);
    Element.switchParent(div, this.element);
    return div;
  },
  _prepare: function () {
    if (!this._prepared) {
      this._s = this._createDiv();
      this._prepared = true;
    }
  },
  _show: function () {
    if (this._prepared) {
      this._setSize();
      this._s.style.display = 'block';
    }
  },
  _setSize: function () {
    with (this._s.style) {
      var msize = Floater.getSize(this.element), ssize = Floater.getSize(this._s);
      if (msize.width != ssize.width)
        width = Element.intToPx(msize.width);
      if (msize.height != ssize.height)
        height = Element.intToPx(msize.height);
    }
  },
  _hide: function () {
    if (this._prepared) 
      this._s.style.display = 'none';
  },
  dispose: function () {
    if (this._prepared) 
      Element.disposeElement(this._s);
  }
};
ShadowDecorator.prototype.constructor = ShadowDecorator;