diff war/scripts/Timeplot/timeplot-complete.js @ 3:cf06b77a8bbd

Committed branch of the e4D repos sti-gwt branch 16384. git-svn-id: http://dev.dariah.eu/svn/repos/eu.dariah.de/ap1/sti-gwt-dariah-geobrowser@36 f2b5be40-def6-11e0-8a09-b3c1cc336c6b
author StefanFunk <StefanFunk@f2b5be40-def6-11e0-8a09-b3c1cc336c6b>
date Tue, 17 Jul 2012 13:34:40 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/war/scripts/Timeplot/timeplot-complete.js	Tue Jul 17 13:34:40 2012 +0000
@@ -0,0 +1,13110 @@
+/*==================================================
+ *  Simile Ajax API
+ *==================================================
+ */
+
+if (typeof SimileAjax == "undefined") {
+    var SimileAjax = {
+        loaded:                 false,
+        loadingScriptsCount:    0,
+        error:                  null,
+        params:                 { bundle:"true" }
+    };
+    
+    SimileAjax.Platform = new Object();
+        /*
+            HACK: We need these 2 things here because we cannot simply append
+            a <script> element containing code that accesses SimileAjax.Platform
+            to initialize it because IE executes that <script> code first
+            before it loads ajax.js and platform.js.
+        */
+        
+    var getHead = function(doc) {
+        return doc.getElementsByTagName("head")[0];
+    };
+    
+    SimileAjax.findScript = function(doc, substring) {
+        var heads = doc.documentElement.getElementsByTagName("head");
+        for (var h = 0; h < heads.length; h++) {
+            var node = heads[h].firstChild;
+            while (node != null) {
+                if (node.nodeType == 1 && node.tagName.toLowerCase() == "script") {
+                    var url = node.src;
+                    var i = url.indexOf(substring);
+                    if (i >= 0) {
+                        return url;
+                    }
+                }
+                node = node.nextSibling;
+            }
+        }
+        return null;
+    };
+    SimileAjax.includeJavascriptFile = function(doc, url, onerror, charset) {
+	return;
+        onerror = onerror || "";
+        if (doc.body == null) {
+            try {
+                var q = "'" + onerror.replace( /'/g, '&apos' ) + "'"; // "
+                doc.write("<script src='" + url + "' onerror="+ q +
+                          (charset ? " charset='"+ charset +"'" : "") +
+                          " type='text/javascript'>"+ onerror + "</script>");
+                return;
+            } catch (e) {
+                // fall through
+            }
+        }
+
+        var script = doc.createElement("script");
+        if (onerror) {
+            try { script.innerHTML = onerror; } catch(e) {}
+            script.setAttribute("onerror", onerror);
+        }
+        if (charset) {
+            script.setAttribute("charset", charset);
+        }
+        script.type = "text/javascript";
+        script.language = "JavaScript";
+        script.src = url;
+        return getHead(doc).appendChild(script);
+    };
+    SimileAjax.includeJavascriptFiles = function(doc, urlPrefix, filenames) {
+        for (var i = 0; i < filenames.length; i++) {
+            SimileAjax.includeJavascriptFile(doc, urlPrefix + filenames[i]);
+        }
+        SimileAjax.loadingScriptsCount += filenames.length;
+        SimileAjax.includeJavascriptFile(doc, SimileAjax.urlPrefix + "scripts/signal.js?" + filenames.length);
+    };
+    SimileAjax.includeCssFile = function(doc, url) {
+        if (doc.body == null) {
+            try {
+                doc.write("<link rel='stylesheet' href='" + url + "' type='text/css'/>");
+                return;
+            } catch (e) {
+                // fall through
+            }
+        }
+        
+        var link = doc.createElement("link");
+        link.setAttribute("rel", "stylesheet");
+        link.setAttribute("type", "text/css");
+        link.setAttribute("href", url);
+        getHead(doc).appendChild(link);
+    };
+    SimileAjax.includeCssFiles = function(doc, urlPrefix, filenames) {
+        for (var i = 0; i < filenames.length; i++) {
+            SimileAjax.includeCssFile(doc, urlPrefix + filenames[i]);
+        }
+    };
+    
+    /**
+     * Append into urls each string in suffixes after prefixing it with urlPrefix.
+     * @param {Array} urls
+     * @param {String} urlPrefix
+     * @param {Array} suffixes
+     */
+    SimileAjax.prefixURLs = function(urls, urlPrefix, suffixes) {
+        for (var i = 0; i < suffixes.length; i++) {
+            urls.push(urlPrefix + suffixes[i]);
+        }
+    };
+
+    /**
+     * Parse out the query parameters from a URL
+     * @param {String} url    the url to parse, or location.href if undefined
+     * @param {Object} to     optional object to extend with the parameters
+     * @param {Object} types  optional object mapping keys to value types
+     *        (String, Number, Boolean or Array, String by default)
+     * @return a key/value Object whose keys are the query parameter names
+     * @type Object
+     */
+    SimileAjax.parseURLParameters = function(url, to, types) {
+        to = to || {};
+        types = types || {};
+        
+        if (typeof url == "undefined") {
+            url = location.href;
+        }
+        var q = url.indexOf("?");
+        if (q < 0) {
+            return to;
+        }
+        url = (url+"#").slice(q+1, url.indexOf("#")); // toss the URL fragment
+        
+        var params = url.split("&"), param, parsed = {};
+        var decode = window.decodeURIComponent || unescape;
+        for (var i = 0; param = params[i]; i++) {
+            var eq = param.indexOf("=");
+            var name = decode(param.slice(0,eq));
+            var old = parsed[name];
+            if (typeof old == "undefined") {
+                old = [];
+            } else if (!(old instanceof Array)) {
+                old = [old];
+            }
+            parsed[name] = old.concat(decode(param.slice(eq+1)));
+        }
+        for (var i in parsed) {
+            if (!parsed.hasOwnProperty(i)) continue;
+            var type = types[i] || String;
+            var data = parsed[i];
+            if (!(data instanceof Array)) {
+                data = [data];
+            }
+            if (type === Boolean && data[0] == "false") {
+                to[i] = false; // because Boolean("false") === true
+            } else {
+                to[i] = type.apply(this, data);
+            }
+        }
+        return to;
+    };
+
+    (function() {
+        var javascriptFiles = [
+            "platform.js",
+            "debug.js",
+            "xmlhttp.js",
+            "json.js",
+            "dom.js",
+            "graphics.js",
+            "date-time.js",
+            "string.js",
+            "html.js",
+            "data-structure.js",
+            "units.js",
+            
+            "ajax.js",
+            "history.js",
+            "window-manager.js"
+        ];
+        var cssFiles = [
+            "timeplot-single.css"
+        ];
+        if (!("jQuery" in window) && !("$" in window)) {
+        	javascriptFiles.unshift("jquery-1.3.2.min.js");
+        }
+        
+        if (typeof SimileAjax_urlPrefix == "string") {
+            SimileAjax.urlPrefix = SimileAjax_urlPrefix;
+        } else {
+            var url = SimileAjax.findScript(document, "timeplot-complete.js");
+            if (url == null) {
+                SimileAjax.error = new Error("Failed to derive URL prefix for Simile Ajax API code files");
+                return;
+            }
+
+            SimileAjax.urlPrefix = url.substr(0, url.indexOf("timeplot-complete.js"));
+        }
+
+        SimileAjax.parseURLParameters(url, SimileAjax.params, {bundle:Boolean});
+        if (SimileAjax.params.bundle) {
+            SimileAjax.includeJavascriptFiles(document, SimileAjax.urlPrefix, [ "simile-ajax-bundle.js" ]);
+        } else {
+            SimileAjax.includeJavascriptFiles(document, SimileAjax.urlPrefix + "scripts/", javascriptFiles);
+        }
+        SimileAjax.includeCssFiles(document, SimileAjax.urlPrefix + "styles/", cssFiles);
+
+	/* We only use it once this way. */
+	SimileAjax.includeCssFile = function(doc, url) { return ; }
+
+        SimileAjax.loaded = true;
+    })();
+}
+
+/*==================================================
+ *  Platform Utility Functions and Constants
+ *==================================================
+ */
+
+/*  This must be called after our jQuery has been loaded 
+    but before control returns to user-code.
+*/
+
+
+/*==================================================
+ *  REMEMBER to update the Version!
+ *==================================================
+ */
+SimileAjax.version = '2.2.1';
+
+SimileAjax.jQuery = jQuery.noConflict(true);
+if (typeof window["$"] == "undefined") {
+    window.$ = SimileAjax.jQuery;
+}
+
+SimileAjax.Platform.os = {
+    isMac:   false,
+    isWin:   false,
+    isWin32: false,
+    isUnix:  false
+};
+SimileAjax.Platform.browser = {
+    isIE:           false,
+    isNetscape:     false,
+    isMozilla:      false,
+    isFirefox:      false,
+    isOpera:        false,
+    isSafari:       false,
+
+    majorVersion:   0,
+    minorVersion:   0
+};
+
+(function() {
+    var an = navigator.appName.toLowerCase();
+	var ua = navigator.userAgent.toLowerCase(); 
+    
+    /*
+     *  Operating system
+     */
+	SimileAjax.Platform.os.isMac = (ua.indexOf('mac') != -1);
+	SimileAjax.Platform.os.isWin = (ua.indexOf('win') != -1);
+	SimileAjax.Platform.os.isWin32 = SimileAjax.Platform.isWin && (   
+        ua.indexOf('95') != -1 || 
+        ua.indexOf('98') != -1 || 
+        ua.indexOf('nt') != -1 || 
+        ua.indexOf('win32') != -1 || 
+        ua.indexOf('32bit') != -1
+    );
+	SimileAjax.Platform.os.isUnix = (ua.indexOf('x11') != -1);
+    
+    /*
+     *  Browser
+     */
+    SimileAjax.Platform.browser.isIE = (an.indexOf("microsoft") != -1);
+    SimileAjax.Platform.browser.isNetscape = (an.indexOf("netscape") != -1);
+    SimileAjax.Platform.browser.isMozilla = (ua.indexOf("mozilla") != -1);
+    SimileAjax.Platform.browser.isFirefox = (ua.indexOf("firefox") != -1);
+    SimileAjax.Platform.browser.isOpera = (an.indexOf("opera") != -1);
+    SimileAjax.Platform.browser.isSafari = (an.indexOf("safari") != -1);
+    
+    var parseVersionString = function(s) {
+        var a = s.split(".");
+        SimileAjax.Platform.browser.majorVersion = parseInt(a[0]);
+        SimileAjax.Platform.browser.minorVersion = parseInt(a[1]);
+    };
+    var indexOf = function(s, sub, start) {
+        var i = s.indexOf(sub, start);
+        return i >= 0 ? i : s.length;
+    };
+    
+    if (SimileAjax.Platform.browser.isMozilla) {
+        var offset = ua.indexOf("mozilla/");
+        if (offset >= 0) {
+            parseVersionString(ua.substring(offset + 8, indexOf(ua, " ", offset)));
+        }
+    }
+    if (SimileAjax.Platform.browser.isIE) {
+        var offset = ua.indexOf("msie ");
+        if (offset >= 0) {
+            parseVersionString(ua.substring(offset + 5, indexOf(ua, ";", offset)));
+        }
+    }
+    if (SimileAjax.Platform.browser.isNetscape) {
+        var offset = ua.indexOf("rv:");
+        if (offset >= 0) {
+            parseVersionString(ua.substring(offset + 3, indexOf(ua, ")", offset)));
+        }
+    }
+    if (SimileAjax.Platform.browser.isFirefox) {
+        var offset = ua.indexOf("firefox/");
+        if (offset >= 0) {
+            parseVersionString(ua.substring(offset + 8, indexOf(ua, " ", offset)));
+        }
+    }
+    
+    if (!("localeCompare" in String.prototype)) {
+        String.prototype.localeCompare = function (s) {
+            if (this < s) return -1;
+            else if (this > s) return 1;
+            else return 0;
+        };
+    }
+})();
+
+SimileAjax.Platform.getDefaultLocale = function() {
+    return SimileAjax.Platform.clientLocale;
+};
+/*==================================================
+ *  Debug Utility Functions
+ *==================================================
+ */
+
+SimileAjax.Debug = {
+    silent: false
+};
+
+SimileAjax.Debug.log = function(msg) {
+    var f;
+    if ("console" in window && "log" in window.console) { // FireBug installed
+        f = function(msg2) {
+            console.log(msg2);
+        }
+    } else {
+        f = function(msg2) {
+            if (!SimileAjax.Debug.silent) {
+                alert(msg2);
+            }
+        }
+    }
+    SimileAjax.Debug.log = f;
+    f(msg);
+};
+
+SimileAjax.Debug.warn = function(msg) {
+    var f;
+    if ("console" in window && "warn" in window.console) { // FireBug installed
+        f = function(msg2) {
+            console.warn(msg2);
+        }
+    } else {
+        f = function(msg2) {
+            if (!SimileAjax.Debug.silent) {
+                alert(msg2);
+            }
+        }
+    }
+    SimileAjax.Debug.warn = f;
+    f(msg);
+};
+
+SimileAjax.Debug.exception = function(e, msg) {
+    var f, params = SimileAjax.parseURLParameters();
+    if (params.errors == "throw" || SimileAjax.params.errors == "throw") {
+        f = function(e2, msg2) {
+            throw(e2); // do not hide from browser's native debugging features
+        };
+    } else if ("console" in window && "error" in window.console) { // FireBug installed
+        f = function(e2, msg2) {
+            if (msg2 != null) {
+                console.error(msg2 + " %o", e2);
+            } else {
+                console.error(e2);
+            }
+            throw(e2); // do not hide from browser's native debugging features
+        };
+    } else {
+        f = function(e2, msg2) {
+            if (!SimileAjax.Debug.silent) {
+                alert("Caught exception: " + msg2 + "\n\nDetails: " + ("description" in e2 ? e2.description : e2));
+            }
+            throw(e2); // do not hide from browser's native debugging features
+        };
+    }
+    SimileAjax.Debug.exception = f;
+    f(e, msg);
+};
+
+SimileAjax.Debug.objectToString = function(o) {
+    return SimileAjax.Debug._objectToString(o, "");
+};
+
+SimileAjax.Debug._objectToString = function(o, indent) {
+    var indent2 = indent + " ";
+    if (typeof o == "object") {
+        var s = "{";
+        for (n in o) {
+            s += indent2 + n + ": " + SimileAjax.Debug._objectToString(o[n], indent2) + "\n";
+        }
+        s += indent + "}";
+        return s;
+    } else if (typeof o == "array") {
+        var s = "[";
+        for (var n = 0; n < o.length; n++) {
+            s += SimileAjax.Debug._objectToString(o[n], indent2) + "\n";
+        }
+        s += indent + "]";
+        return s;
+    } else {
+        return o;
+    }
+};
+/**
+ * @fileOverview XmlHttp utility functions
+ * @name SimileAjax.XmlHttp
+ */
+
+SimileAjax.XmlHttp = new Object();
+
+/**
+ *  Callback for XMLHttp onRequestStateChange.
+ */
+SimileAjax.XmlHttp._onReadyStateChange = function(xmlhttp, fError, fDone) {
+    switch (xmlhttp.readyState) {
+    // 1: Request not yet made
+    // 2: Contact established with server but nothing downloaded yet
+    // 3: Called multiple while downloading in progress
+    
+    // Download complete
+    case 4:
+        try {
+            if (xmlhttp.status == 0     // file:// urls, works on Firefox
+             || xmlhttp.status == 200   // http:// urls
+            ) {
+                if (fDone) {
+                    fDone(xmlhttp);
+                }
+            } else {
+                if (fError) {
+                    fError(
+                        xmlhttp.statusText,
+                        xmlhttp.status,
+                        xmlhttp
+                    );
+                }
+            }
+        } catch (e) {
+            SimileAjax.Debug.exception("XmlHttp: Error handling onReadyStateChange", e);
+        }
+        break;
+    }
+};
+
+/**
+ *  Creates an XMLHttpRequest object. On the first run, this
+ *  function creates a platform-specific function for
+ *  instantiating an XMLHttpRequest object and then replaces
+ *  itself with that function.
+ */
+SimileAjax.XmlHttp._createRequest = function() {
+    if (SimileAjax.Platform.browser.isIE) {
+        var programIDs = [
+        "Msxml2.XMLHTTP",
+        "Microsoft.XMLHTTP",
+        "Msxml2.XMLHTTP.4.0"
+        ];
+        for (var i = 0; i < programIDs.length; i++) {
+            try {
+                var programID = programIDs[i];
+                var f = function() {
+                    return new ActiveXObject(programID);
+                };
+                var o = f();
+                
+                // We are replacing the SimileAjax._createXmlHttpRequest
+                // function with this inner function as we've
+                // found out that it works. This is so that we
+                // don't have to do all the testing over again
+                // on subsequent calls.
+                SimileAjax.XmlHttp._createRequest = f;
+                
+                return o;
+            } catch (e) {
+                // silent
+            }
+        }
+        // fall through to try new XMLHttpRequest();
+    }
+
+    try {
+        var f = function() {
+            return new XMLHttpRequest();
+        };
+        var o = f();
+        
+        // We are replacing the SimileAjax._createXmlHttpRequest
+        // function with this inner function as we've
+        // found out that it works. This is so that we
+        // don't have to do all the testing over again
+        // on subsequent calls.
+        SimileAjax.XmlHttp._createRequest = f;
+        
+        return o;
+    } catch (e) {
+        throw new Error("Failed to create an XMLHttpRequest object");
+    }
+};
+
+/**
+ * Performs an asynchronous HTTP GET.
+ *  
+ * @param {Function} fError a function of the form 
+     function(statusText, statusCode, xmlhttp)
+ * @param {Function} fDone a function of the form function(xmlhttp)
+ */
+SimileAjax.XmlHttp.get = function(url, fError, fDone) {
+    var xmlhttp = SimileAjax.XmlHttp._createRequest();
+    
+    xmlhttp.open("GET", url, true);
+    xmlhttp.onreadystatechange = function() {
+        SimileAjax.XmlHttp._onReadyStateChange(xmlhttp, fError, fDone);
+    };
+    xmlhttp.send(null);
+};
+
+/**
+ * Performs an asynchronous HTTP POST.
+ *  
+ * @param {Function} fError a function of the form 
+     function(statusText, statusCode, xmlhttp)
+ * @param {Function} fDone a function of the form function(xmlhttp)
+ */
+SimileAjax.XmlHttp.post = function(url, body, fError, fDone) {
+    var xmlhttp = SimileAjax.XmlHttp._createRequest();
+    
+    xmlhttp.open("POST", url, true);
+    xmlhttp.onreadystatechange = function() {
+        SimileAjax.XmlHttp._onReadyStateChange(xmlhttp, fError, fDone);
+    };
+    xmlhttp.send(body);
+};
+
+SimileAjax.XmlHttp._forceXML = function(xmlhttp) {
+    try {
+        xmlhttp.overrideMimeType("text/xml");
+    } catch (e) {
+        xmlhttp.setrequestheader("Content-Type", "text/xml");
+    }
+};
+/*
+ *  Copied directly from http://www.json.org/json.js.
+ */
+
+/*
+    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.
+*/
+
+SimileAjax.JSON = new Object();
+
+(function () {
+    var m = {
+        '\b': '\\b',
+        '\t': '\\t',
+        '\n': '\\n',
+        '\f': '\\f',
+        '\r': '\\r',
+        '"' : '\\"',
+        '\\': '\\\\'
+    };
+    var 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 + '"';
+        }
+    };
+
+    SimileAjax.JSON.toJSONString = function(o) {
+        if (o instanceof Object) {
+            return s.object(o);
+        } else if (o instanceof Array) {
+            return s.array(o);
+        } else {
+            return o.toString();
+        }
+    };
+    
+    SimileAjax.JSON.parseJSON = function () {
+        try {
+            return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
+                    this.replace(/"(\\.|[^"\\])*"/g, ''))) &&
+                eval('(' + this + ')');
+        } catch (e) {
+            return false;
+        }
+    };
+})();
+/*==================================================
+ *  DOM Utility Functions
+ *==================================================
+ */
+
+SimileAjax.DOM = new Object();
+
+SimileAjax.DOM.registerEventWithObject = function(elmt, eventName, obj, handlerName) {
+    SimileAjax.DOM.registerEvent(elmt, eventName, function(elmt2, evt, target) {
+        return obj[handlerName].call(obj, elmt2, evt, target);
+    });
+};
+
+SimileAjax.DOM.registerEvent = function(elmt, eventName, handler) {
+    var handler2 = function(evt) {
+        evt = (evt) ? evt : ((event) ? event : null);
+        if (evt) {
+            var target = (evt.target) ? 
+                evt.target : ((evt.srcElement) ? evt.srcElement : null);
+            if (target) {
+                target = (target.nodeType == 1 || target.nodeType == 9) ? 
+                    target : target.parentNode;
+            }
+            
+            return handler(elmt, evt, target);
+        }
+        return true;
+    }
+    
+    if (SimileAjax.Platform.browser.isIE) {
+        elmt.attachEvent("on" + eventName, handler2);
+    } else {
+        elmt.addEventListener(eventName, handler2, false);
+    }
+};
+
+SimileAjax.DOM.getPageCoordinates = function(elmt) {
+    var left = 0;
+    var top = 0;
+    
+    if (elmt.nodeType != 1) {
+        elmt = elmt.parentNode;
+    }
+    
+    var elmt2 = elmt;
+    while (elmt2 != null) {
+        left += elmt2.offsetLeft;
+        top += elmt2.offsetTop;
+        elmt2 = elmt2.offsetParent;
+    }
+    
+    var body = document.body;
+    while (elmt != null && elmt != body) {
+        if ("scrollLeft" in elmt) {
+            left -= elmt.scrollLeft;
+            top -= elmt.scrollTop;
+        }
+        elmt = elmt.parentNode;
+    }
+    
+    return { left: left, top: top };
+};
+
+SimileAjax.DOM.getSize = function(elmt) {
+	var w = this.getStyle(elmt,"width");
+	var h = this.getStyle(elmt,"height");
+	if (w.indexOf("px") > -1) w = w.replace("px","");
+	if (h.indexOf("px") > -1) h = h.replace("px","");
+	return {
+		w: w,
+		h: h
+	}
+}
+
+SimileAjax.DOM.getStyle = function(elmt, styleProp) {
+    if (elmt.currentStyle) { // IE
+        var style = elmt.currentStyle[styleProp];
+    } else if (window.getComputedStyle) { // standard DOM
+        var style = document.defaultView.getComputedStyle(elmt, null).getPropertyValue(styleProp);
+    } else {
+    	var style = "";
+    }
+    return style;
+}
+
+SimileAjax.DOM.getEventRelativeCoordinates = function(evt, elmt) {
+    if (SimileAjax.Platform.browser.isIE) {
+      if (evt.type == "mousewheel") {
+        var coords = SimileAjax.DOM.getPageCoordinates(elmt);
+        return {
+          x: evt.clientX - coords.left, 
+          y: evt.clientY - coords.top
+        };        
+      } else {
+        return {
+          x: evt.offsetX,
+          y: evt.offsetY
+        };
+      }
+    } else {
+        var coords = SimileAjax.DOM.getPageCoordinates(elmt);
+
+        if ((evt.type == "DOMMouseScroll") &&
+          SimileAjax.Platform.browser.isFirefox &&
+          (SimileAjax.Platform.browser.majorVersion == 2)) {
+          // Due to: https://bugzilla.mozilla.org/show_bug.cgi?id=352179                  
+
+          return {
+            x: evt.screenX - coords.left,
+            y: evt.screenY - coords.top 
+          };
+        } else {
+          return {
+              x: evt.pageX - coords.left,
+              y: evt.pageY - coords.top
+          };
+        }
+    }
+};
+
+SimileAjax.DOM.getEventPageCoordinates = function(evt) {
+    if (SimileAjax.Platform.browser.isIE) {
+        return {
+            x: evt.clientX + document.body.scrollLeft,
+            y: evt.clientY + document.body.scrollTop
+        };
+    } else {
+        return {
+            x: evt.pageX,
+            y: evt.pageY
+        };
+    }
+};
+
+SimileAjax.DOM.hittest = function(x, y, except) {
+    return SimileAjax.DOM._hittest(document.body, x, y, except);
+};
+
+SimileAjax.DOM._hittest = function(elmt, x, y, except) {
+    var childNodes = elmt.childNodes;
+    outer: for (var i = 0; i < childNodes.length; i++) {
+        var childNode = childNodes[i];
+        for (var j = 0; j < except.length; j++) {
+            if (childNode == except[j]) {
+                continue outer;
+            }
+        }
+        
+        if (childNode.offsetWidth == 0 && childNode.offsetHeight == 0) {
+            /*
+             *  Sometimes SPAN elements have zero width and height but
+             *  they have children like DIVs that cover non-zero areas.
+             */
+            var hitNode = SimileAjax.DOM._hittest(childNode, x, y, except);
+            if (hitNode != childNode) {
+                return hitNode;
+            }
+        } else {
+            var top = 0;
+            var left = 0;
+            
+            var node = childNode;
+            while (node) {
+                top += node.offsetTop;
+                left += node.offsetLeft;
+                node = node.offsetParent;
+            }
+            
+            if (left <= x && top <= y && (x - left) < childNode.offsetWidth && (y - top) < childNode.offsetHeight) {
+                return SimileAjax.DOM._hittest(childNode, x, y, except);
+            } else if (childNode.nodeType == 1 && childNode.tagName == "TR") {
+                /*
+                 *  Table row might have cells that span several rows.
+                 */
+                var childNode2 = SimileAjax.DOM._hittest(childNode, x, y, except);
+                if (childNode2 != childNode) {
+                    return childNode2;
+                }
+            }
+        }
+    }
+    return elmt;
+};
+
+SimileAjax.DOM.cancelEvent = function(evt) {
+    evt.returnValue = false;
+    evt.cancelBubble = true;
+    if ("preventDefault" in evt) {
+        evt.preventDefault();
+    }
+};
+
+SimileAjax.DOM.appendClassName = function(elmt, className) {
+    var classes = elmt.className.split(" ");
+    for (var i = 0; i < classes.length; i++) {
+        if (classes[i] == className) {
+            return;
+        }
+    }
+    classes.push(className);
+    elmt.className = classes.join(" ");
+};
+
+SimileAjax.DOM.createInputElement = function(type) {
+    var div = document.createElement("div");
+    div.innerHTML = "<input type='" + type + "' />";
+    
+    return div.firstChild;
+};
+
+SimileAjax.DOM.createDOMFromTemplate = function(template) {
+    var result = {};
+    result.elmt = SimileAjax.DOM._createDOMFromTemplate(template, result, null);
+    
+    return result;
+};
+
+SimileAjax.DOM._createDOMFromTemplate = function(templateNode, result, parentElmt) {
+    if (templateNode == null) {
+        /*
+        var node = doc.createTextNode("--null--");
+        if (parentElmt != null) {
+            parentElmt.appendChild(node);
+        }
+        return node;
+        */
+        return null;
+    } else if (typeof templateNode != "object") {
+        var node = document.createTextNode(templateNode);
+        if (parentElmt != null) {
+            parentElmt.appendChild(node);
+        }
+        return node;
+    } else {
+        var elmt = null;
+        if ("tag" in templateNode) {
+            var tag = templateNode.tag;
+            if (parentElmt != null) {
+                if (tag == "tr") {
+                    elmt = parentElmt.insertRow(parentElmt.rows.length);
+                } else if (tag == "td") {
+                    elmt = parentElmt.insertCell(parentElmt.cells.length);
+                }
+            }
+            if (elmt == null) {
+                elmt = tag == "input" ?
+                    SimileAjax.DOM.createInputElement(templateNode.type) :
+                    document.createElement(tag);
+                    
+                if (parentElmt != null) {
+                    parentElmt.appendChild(elmt);
+                }
+            }
+        } else {
+            elmt = templateNode.elmt;
+            if (parentElmt != null) {
+                parentElmt.appendChild(elmt);
+            }
+        }
+        
+        for (var attribute in templateNode) {
+            var value = templateNode[attribute];
+            
+            if (attribute == "field") {
+                result[value] = elmt;
+                
+            } else if (attribute == "className") {
+                elmt.className = value;
+            } else if (attribute == "id") {
+                elmt.id = value;
+            } else if (attribute == "title") {
+                elmt.title = value;
+            } else if (attribute == "type" && elmt.tagName == "input") {
+                // do nothing
+            } else if (attribute == "style") {
+                for (n in value) {
+                    var v = value[n];
+                    if (n == "float") {
+                        n = SimileAjax.Platform.browser.isIE ? "styleFloat" : "cssFloat";
+                    }
+                    elmt.style[n] = v;
+                }
+            } else if (attribute == "children") {
+                for (var i = 0; i < value.length; i++) {
+                    SimileAjax.DOM._createDOMFromTemplate(value[i], result, elmt);
+                }
+            } else if (attribute != "tag" && attribute != "elmt") {
+                elmt.setAttribute(attribute, value);
+            }
+        }
+        return elmt;
+    }
+}
+
+SimileAjax.DOM._cachedParent = null;
+SimileAjax.DOM.createElementFromString = function(s) {
+    if (SimileAjax.DOM._cachedParent == null) {
+        SimileAjax.DOM._cachedParent = document.createElement("div");
+    }
+    SimileAjax.DOM._cachedParent.innerHTML = s;
+    return SimileAjax.DOM._cachedParent.firstChild;
+};
+
+SimileAjax.DOM.createDOMFromString = function(root, s, fieldElmts) {
+    var elmt = typeof root == "string" ? document.createElement(root) : root;
+    elmt.innerHTML = s;
+    
+    var dom = { elmt: elmt };
+    SimileAjax.DOM._processDOMChildrenConstructedFromString(dom, elmt, fieldElmts != null ? fieldElmts : {} );
+    
+    return dom;
+};
+
+SimileAjax.DOM._processDOMConstructedFromString = function(dom, elmt, fieldElmts) {
+    var id = elmt.id;
+    if (id != null && id.length > 0) {
+        elmt.removeAttribute("id");
+        if (id in fieldElmts) {
+            var parentElmt = elmt.parentNode;
+            parentElmt.insertBefore(fieldElmts[id], elmt);
+            parentElmt.removeChild(elmt);
+            
+            dom[id] = fieldElmts[id];
+            return;
+        } else {
+            dom[id] = elmt;
+        }
+    }
+    
+    if (elmt.hasChildNodes()) {
+        SimileAjax.DOM._processDOMChildrenConstructedFromString(dom, elmt, fieldElmts);
+    }
+};
+
+SimileAjax.DOM._processDOMChildrenConstructedFromString = function(dom, elmt, fieldElmts) {
+    var node = elmt.firstChild;
+    while (node != null) {
+        var node2 = node.nextSibling;
+        if (node.nodeType == 1) {
+            SimileAjax.DOM._processDOMConstructedFromString(dom, node, fieldElmts);
+        }
+        node = node2;
+    }
+};
+/**
+ * @fileOverview Graphics utility functions and constants
+ * @name SimileAjax.Graphics
+ */
+
+SimileAjax.Graphics = new Object();
+
+/**
+ * A boolean value indicating whether PNG translucency is supported on the
+ * user's browser or not.
+ *
+ * @type Boolean
+ */
+SimileAjax.Graphics.pngIsTranslucent = (!SimileAjax.Platform.browser.isIE) || (SimileAjax.Platform.browser.majorVersion > 6);
+if (!SimileAjax.Graphics.pngIsTranslucent) {
+    SimileAjax.includeCssFile(document, SimileAjax.urlPrefix + "styles/graphics-ie6.css");
+}
+
+/*==================================================
+ *  Opacity, translucency
+ *==================================================
+ */
+SimileAjax.Graphics._createTranslucentImage1 = function(url, verticalAlign) {
+    var elmt = document.createElement("img");
+    elmt.setAttribute("src", url);
+    if (verticalAlign != null) {
+        elmt.style.verticalAlign = verticalAlign;
+    }
+    return elmt;
+};
+SimileAjax.Graphics._createTranslucentImage2 = function(url, verticalAlign) {
+    var elmt = document.createElement("img");
+    elmt.style.width = "1px";  // just so that IE will calculate the size property
+    elmt.style.height = "1px";
+    elmt.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url +"', sizingMethod='image')";
+    elmt.style.verticalAlign = (verticalAlign != null) ? verticalAlign : "middle";
+    return elmt;
+};
+
+/**
+ * Creates a DOM element for an <code>img</code> tag using the URL given. This
+ * is a convenience method that automatically includes the necessary CSS to
+ * allow for translucency, even on IE.
+ * 
+ * @function
+ * @param {String} url the URL to the image
+ * @param {String} verticalAlign the CSS value for the image's vertical-align
+ * @return {Element} a DOM element containing the <code>img</code> tag
+ */
+SimileAjax.Graphics.createTranslucentImage = SimileAjax.Graphics.pngIsTranslucent ?
+    SimileAjax.Graphics._createTranslucentImage1 :
+    SimileAjax.Graphics._createTranslucentImage2;
+
+SimileAjax.Graphics._createTranslucentImageHTML1 = function(url, verticalAlign) {
+    return "<img src=\"" + url + "\"" +
+        (verticalAlign != null ? " style=\"vertical-align: " + verticalAlign + ";\"" : "") +
+        " />";
+};
+SimileAjax.Graphics._createTranslucentImageHTML2 = function(url, verticalAlign) {
+    var style = 
+        "width: 1px; height: 1px; " +
+        "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url +"', sizingMethod='image');" +
+        (verticalAlign != null ? " vertical-align: " + verticalAlign + ";" : "");
+        
+    return "<img src='" + url + "' style=\"" + style + "\" />";
+};
+
+/**
+ * Creates an HTML string for an <code>img</code> tag using the URL given.
+ * This is a convenience method that automatically includes the necessary CSS
+ * to allow for translucency, even on IE.
+ * 
+ * @function
+ * @param {String} url the URL to the image
+ * @param {String} verticalAlign the CSS value for the image's vertical-align
+ * @return {String} a string containing the <code>img</code> tag
+ */
+SimileAjax.Graphics.createTranslucentImageHTML = SimileAjax.Graphics.pngIsTranslucent ?
+    SimileAjax.Graphics._createTranslucentImageHTML1 :
+    SimileAjax.Graphics._createTranslucentImageHTML2;
+
+/**
+ * Sets the opacity on the given DOM element.
+ *
+ * @param {Element} elmt the DOM element to set the opacity on
+ * @param {Number} opacity an integer from 0 to 100 specifying the opacity
+ */
+SimileAjax.Graphics.setOpacity = function(elmt, opacity) {
+    if (SimileAjax.Platform.browser.isIE) {
+        elmt.style.filter = "progid:DXImageTransform.Microsoft.Alpha(Style=0,Opacity=" + opacity + ")";
+    } else {
+        var o = (opacity / 100).toString();
+        elmt.style.opacity = o;
+        elmt.style.MozOpacity = o;
+    }
+};
+
+/*==================================================
+ *  Bubble
+ *==================================================
+ */
+
+SimileAjax.Graphics.bubbleConfig = {
+    containerCSSClass:              "simileAjax-bubble-container",
+    innerContainerCSSClass:         "simileAjax-bubble-innerContainer",
+    contentContainerCSSClass:       "simileAjax-bubble-contentContainer",
+    
+    borderGraphicSize:              50,
+    borderGraphicCSSClassPrefix:    "simileAjax-bubble-border-",
+    
+    arrowGraphicTargetOffset:       33,  // from tip of arrow to the side of the graphic that touches the content of the bubble
+    arrowGraphicLength:             100, // dimension of arrow graphic along the direction that the arrow points
+    arrowGraphicWidth:              49,  // dimension of arrow graphic perpendicular to the direction that the arrow points
+    arrowGraphicCSSClassPrefix:     "simileAjax-bubble-arrow-",
+    
+    closeGraphicCSSClass:           "simileAjax-bubble-close",
+    
+    extraPadding:                   20
+};
+
+/**
+ * Creates a nice, rounded bubble popup with the given content in a div,
+ * page coordinates and a suggested width. The bubble will point to the 
+ * location on the page as described by pageX and pageY.  All measurements 
+ * should be given in pixels.
+ *
+ * @param {Element} the content div
+ * @param {Number} pageX the x coordinate of the point to point to
+ * @param {Number} pageY the y coordinate of the point to point to
+ * @param {Number} contentWidth a suggested width of the content
+ * @param {String} orientation a string ("top", "bottom", "left", or "right")
+ *   that describes the orientation of the arrow on the bubble
+ * @param {Number} maxHeight. Add a scrollbar div if bubble would be too tall.
+ *   Default of 0 or null means no maximum
+ */
+SimileAjax.Graphics.createBubbleForContentAndPoint = function(
+       div, pageX, pageY, contentWidth, orientation, maxHeight) {
+    if (typeof contentWidth != "number") {
+        contentWidth = 300;
+    }
+    if (typeof maxHeight != "number") {
+        maxHeight = 0;
+    }
+
+    div.style.position = "absolute";
+    div.style.left = "-5000px";
+    div.style.top = "0px";
+    div.style.width = contentWidth + "px";
+    document.body.appendChild(div);
+    
+    window.setTimeout(function() {
+        var width = div.scrollWidth + 10;
+        var height = div.scrollHeight + 10;
+        var scrollDivW = 0; // width of the possible inner container when we want vertical scrolling
+        if (maxHeight > 0 && height > maxHeight) {
+          height = maxHeight;
+          scrollDivW = width - 25;
+        }  
+       
+        var bubble = SimileAjax.Graphics.createBubbleForPoint(pageX, pageY, width, height, orientation);
+        
+        document.body.removeChild(div);
+        div.style.position = "static";
+        div.style.left = "";
+        div.style.top = "";
+        
+        // create a scroll div if needed
+        if (scrollDivW > 0) {
+          var scrollDiv = document.createElement("div");
+          div.style.width = "";
+          scrollDiv.style.width = scrollDivW + "px";
+          scrollDiv.appendChild(div);
+          bubble.content.appendChild(scrollDiv);
+        } else {
+          div.style.width = width + "px";
+          bubble.content.appendChild(div);
+        }
+    }, 200);
+};
+
+/**
+ * Creates a nice, rounded bubble popup with the given page coordinates and
+ * content dimensions.  The bubble will point to the location on the page
+ * as described by pageX and pageY.  All measurements should be given in
+ * pixels.
+ *
+ * @param {Number} pageX the x coordinate of the point to point to
+ * @param {Number} pageY the y coordinate of the point to point to
+ * @param {Number} contentWidth the width of the content box in the bubble
+ * @param {Number} contentHeight the height of the content box in the bubble
+ * @param {String} orientation a string ("top", "bottom", "left", or "right")
+ *   that describes the orientation of the arrow on the bubble
+ * @return {Element} a DOM element for the newly created bubble
+ */
+SimileAjax.Graphics.createBubbleForPoint = function(pageX, pageY, contentWidth, contentHeight, orientation) {
+    contentWidth = parseInt(contentWidth, 10); // harden against bad input bugs
+    contentHeight = parseInt(contentHeight, 10); // getting numbers-as-strings
+    
+    var bubbleConfig = SimileAjax.Graphics.bubbleConfig;
+    var pngTransparencyClassSuffix = 
+        SimileAjax.Graphics.pngIsTranslucent ? "pngTranslucent" : "pngNotTranslucent";
+    
+    var bubbleWidth = contentWidth + 2 * bubbleConfig.borderGraphicSize;
+    var bubbleHeight = contentHeight + 2 * bubbleConfig.borderGraphicSize;
+    
+    var generatePngSensitiveClass = function(className) {
+        return className + " " + className + "-" + pngTransparencyClassSuffix;
+    };
+    
+    /*
+     *  Render container divs
+     */
+    var div = document.createElement("div");
+    div.className = generatePngSensitiveClass(bubbleConfig.containerCSSClass);
+    div.style.width = contentWidth + "px";
+    div.style.height = contentHeight + "px";
+    
+    var divInnerContainer = document.createElement("div");
+    divInnerContainer.className = generatePngSensitiveClass(bubbleConfig.innerContainerCSSClass);
+    div.appendChild(divInnerContainer);
+    
+    /*
+     *  Create layer for bubble
+     */
+    var close = function() { 
+        if (!bubble._closed) {
+            document.body.removeChild(bubble._div);
+            bubble._doc = null;
+            bubble._div = null;
+            bubble._content = null;
+            bubble._closed = true;
+        }
+    }
+    var bubble = { _closed: false };
+    var layer = SimileAjax.WindowManager.pushLayer(close, true, div);
+    bubble._div = div;
+    bubble.close = function() { SimileAjax.WindowManager.popLayer(layer); }
+    
+    /*
+     *  Render border graphics
+     */
+    var createBorder = function(classNameSuffix) {
+        var divBorderGraphic = document.createElement("div");
+        divBorderGraphic.className = generatePngSensitiveClass(bubbleConfig.borderGraphicCSSClassPrefix + classNameSuffix);
+        divInnerContainer.appendChild(divBorderGraphic);
+    };
+    createBorder("top-left");
+    createBorder("top-right");
+    createBorder("bottom-left");
+    createBorder("bottom-right");
+    createBorder("left");
+    createBorder("right");
+    createBorder("top");
+    createBorder("bottom");
+    
+    /*
+     *  Render content
+     */
+    var divContentContainer = document.createElement("div");
+    divContentContainer.className = generatePngSensitiveClass(bubbleConfig.contentContainerCSSClass);
+    divInnerContainer.appendChild(divContentContainer);
+    bubble.content = divContentContainer;
+    
+    /*
+     *  Render close button
+     */
+    var divClose = document.createElement("div");
+    divClose.className = generatePngSensitiveClass(bubbleConfig.closeGraphicCSSClass);
+    divInnerContainer.appendChild(divClose);
+    SimileAjax.WindowManager.registerEventWithObject(divClose, "click", bubble, "close");
+    
+    (function() {
+        var dims = SimileAjax.Graphics.getWindowDimensions();
+        var docWidth = dims.w;
+        var docHeight = dims.h;
+        
+        var halfArrowGraphicWidth = Math.ceil(bubbleConfig.arrowGraphicWidth / 2);
+        
+        var createArrow = function(classNameSuffix) {
+            var divArrowGraphic = document.createElement("div");
+            divArrowGraphic.className = generatePngSensitiveClass(bubbleConfig.arrowGraphicCSSClassPrefix + "point-" + classNameSuffix);
+            divInnerContainer.appendChild(divArrowGraphic);
+            return divArrowGraphic;
+        };
+        
+        if (pageX - halfArrowGraphicWidth - bubbleConfig.borderGraphicSize - bubbleConfig.extraPadding > 0 &&
+            pageX + halfArrowGraphicWidth + bubbleConfig.borderGraphicSize + bubbleConfig.extraPadding < docWidth) {
+            
+            /*
+             *  Bubble can be positioned above or below the target point.
+             */
+            
+            var left = pageX - Math.round(contentWidth / 2);
+            left = pageX < (docWidth / 2) ?
+                Math.max(left, bubbleConfig.extraPadding + bubbleConfig.borderGraphicSize) : 
+                Math.min(left, docWidth - bubbleConfig.extraPadding - bubbleConfig.borderGraphicSize - contentWidth);
+                
+            if ((orientation && orientation == "top") || 
+                (!orientation && 
+                    (pageY 
+                        - bubbleConfig.arrowGraphicTargetOffset 
+                        - contentHeight 
+                        - bubbleConfig.borderGraphicSize 
+                        - bubbleConfig.extraPadding > 0))) {
+                
+                /*
+                 *  Position bubble above the target point.
+                 */
+                
+                var divArrow = createArrow("down");
+                divArrow.style.left = (pageX - halfArrowGraphicWidth - left) + "px";
+                
+                div.style.left = left + "px";
+                div.style.top = (pageY - bubbleConfig.arrowGraphicTargetOffset - contentHeight) + "px";
+                
+                return;
+            } else if ((orientation && orientation == "bottom") || 
+                (!orientation && 
+                    (pageY 
+                        + bubbleConfig.arrowGraphicTargetOffset 
+                        + contentHeight 
+                        + bubbleConfig.borderGraphicSize 
+                        + bubbleConfig.extraPadding < docHeight))) {
+                        
+                /*
+                 *  Position bubble below the target point.
+                 */
+                
+                var divArrow = createArrow("up");
+                divArrow.style.left = (pageX - halfArrowGraphicWidth - left) + "px";
+                
+                div.style.left = left + "px";
+                div.style.top = (pageY + bubbleConfig.arrowGraphicTargetOffset) + "px";
+                
+                return;
+            }
+        }
+        
+        var top = pageY - Math.round(contentHeight / 2);
+        top = pageY < (docHeight / 2) ?
+            Math.max(top, bubbleConfig.extraPadding + bubbleConfig.borderGraphicSize) : 
+            Math.min(top, docHeight - bubbleConfig.extraPadding - bubbleConfig.borderGraphicSize - contentHeight);
+            
+        if ((orientation && orientation == "left") || 
+            (!orientation && 
+                (pageX 
+                    - bubbleConfig.arrowGraphicTargetOffset 
+                    - contentWidth
+                    - bubbleConfig.borderGraphicSize 
+                    - bubbleConfig.extraPadding > 0))) {
+            
+            /*
+             *  Position bubble left of the target point.
+             */
+            
+            var divArrow = createArrow("right");
+            divArrow.style.top = (pageY - halfArrowGraphicWidth - top) + "px";
+            
+            div.style.top = top + "px";
+            div.style.left = (pageX - bubbleConfig.arrowGraphicTargetOffset - contentWidth) + "px";
+        } else {
+            
+            /*
+             *  Position bubble right of the target point, as the last resort.
+             */
+            
+            var divArrow = createArrow("left");
+            divArrow.style.top = (pageY - halfArrowGraphicWidth - top) + "px";
+            
+            div.style.top = top + "px";
+            div.style.left = (pageX + bubbleConfig.arrowGraphicTargetOffset) + "px";
+        }
+    })();
+    
+    document.body.appendChild(div);
+    
+    return bubble;
+};
+
+SimileAjax.Graphics.getWindowDimensions = function() {
+    if (typeof window.innerHeight == 'number') {
+        return { w:window.innerWidth, h:window.innerHeight }; // Non-IE
+    } else if (document.documentElement && document.documentElement.clientHeight) {
+        return { // IE6+, in "standards compliant mode"
+            w:document.documentElement.clientWidth,
+            h:document.documentElement.clientHeight
+        };
+    } else if (document.body && document.body.clientHeight) {
+        return { // IE 4 compatible
+            w:document.body.clientWidth,
+            h:document.body.clientHeight
+        };
+    }
+};
+
+
+/**
+ * Creates a floating, rounded message bubble in the center of the window for
+ * displaying modal information, e.g. "Loading..."
+ *
+ * @param {Document} doc the root document for the page to render on
+ * @param {Object} an object with two properties, contentDiv and containerDiv,
+ *   consisting of the newly created DOM elements
+ */
+SimileAjax.Graphics.createMessageBubble = function(doc) {
+    var containerDiv = doc.createElement("div");
+    if (SimileAjax.Graphics.pngIsTranslucent) {
+        var topDiv = doc.createElement("div");
+        topDiv.style.height = "33px";
+        topDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-top-left.png) top left no-repeat";
+        topDiv.style.paddingLeft = "44px";
+        containerDiv.appendChild(topDiv);
+        
+        var topRightDiv = doc.createElement("div");
+        topRightDiv.style.height = "33px";
+        topRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-top-right.png) top right no-repeat";
+        topDiv.appendChild(topRightDiv);
+        
+        var middleDiv = doc.createElement("div");
+        middleDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-left.png) top left repeat-y";
+        middleDiv.style.paddingLeft = "44px";
+        containerDiv.appendChild(middleDiv);
+        
+        var middleRightDiv = doc.createElement("div");
+        middleRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-right.png) top right repeat-y";
+        middleRightDiv.style.paddingRight = "44px";
+        middleDiv.appendChild(middleRightDiv);
+        
+        var contentDiv = doc.createElement("div");
+        middleRightDiv.appendChild(contentDiv);
+        
+        var bottomDiv = doc.createElement("div");
+        bottomDiv.style.height = "55px";
+        bottomDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-bottom-left.png) bottom left no-repeat";
+        bottomDiv.style.paddingLeft = "44px";
+        containerDiv.appendChild(bottomDiv);
+        
+        var bottomRightDiv = doc.createElement("div");
+        bottomRightDiv.style.height = "55px";
+        bottomRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-bottom-right.png) bottom right no-repeat";
+        bottomDiv.appendChild(bottomRightDiv);
+    } else {
+        containerDiv.style.border = "2px solid #7777AA";
+        containerDiv.style.padding = "20px";
+        containerDiv.style.background = "white";
+        SimileAjax.Graphics.setOpacity(containerDiv, 90);
+        
+        var contentDiv = doc.createElement("div");
+        containerDiv.appendChild(contentDiv);
+    }
+    
+    return {
+        containerDiv:   containerDiv,
+        contentDiv:     contentDiv
+    };
+};
+
+/*==================================================
+ *  Animation
+ *==================================================
+ */
+
+/**
+ * Creates an animation for a function, and an interval of values.  The word
+ * "animation" here is used in the sense of repeatedly calling a function with
+ * a current value from within an interval, and a delta value.
+ *
+ * @param {Function} f a function to be called every 50 milliseconds throughout
+ *   the animation duration, of the form f(current, delta), where current is
+ *   the current value within the range and delta is the current change.
+ * @param {Number} from a starting value
+ * @param {Number} to an ending value
+ * @param {Number} duration the duration of the animation in milliseconds
+ * @param {Function} [cont] an optional function that is called at the end of
+ *   the animation, i.e. a continuation.
+ * @return {SimileAjax.Graphics._Animation} a new animation object
+ */
+SimileAjax.Graphics.createAnimation = function(f, from, to, duration, cont) {
+    return new SimileAjax.Graphics._Animation(f, from, to, duration, cont);
+};
+
+SimileAjax.Graphics._Animation = function(f, from, to, duration, cont) {
+    this.f = f;
+    this.cont = (typeof cont == "function") ? cont : function() {};
+    
+    this.from = from;
+    this.to = to;
+    this.current = from;
+    
+    this.duration = duration;
+    this.start = new Date().getTime();
+    this.timePassed = 0;
+};
+
+/**
+ * Runs this animation.
+ */
+SimileAjax.Graphics._Animation.prototype.run = function() {
+    var a = this;
+    window.setTimeout(function() { a.step(); }, 50);
+};
+
+/**
+ * Increments this animation by one step, and then continues the animation with
+ * <code>run()</code>.
+ */
+SimileAjax.Graphics._Animation.prototype.step = function() {
+    this.timePassed += 50;
+    
+    var timePassedFraction = this.timePassed / this.duration;
+    var parameterFraction = -Math.cos(timePassedFraction * Math.PI) / 2 + 0.5;
+    var current = parameterFraction * (this.to - this.from) + this.from;
+    
+    try {
+        this.f(current, current - this.current);
+    } catch (e) {
+    }
+    this.current = current;
+    
+    if (this.timePassed < this.duration) {
+        this.run();
+    } else {
+        this.f(this.to, 0);
+        this["cont"]();
+    }
+};
+
+/*==================================================
+ *  CopyPasteButton
+ *
+ *  Adapted from http://spaces.live.com/editorial/rayozzie/demo/liveclip/liveclipsample/techPreview.html.
+ *==================================================
+ */
+
+/**
+ * Creates a button and textarea for displaying structured data and copying it
+ * to the clipboard.  The data is dynamically generated by the given 
+ * createDataFunction parameter.
+ *
+ * @param {String} image an image URL to use as the background for the 
+ *   generated box
+ * @param {Number} width the width in pixels of the generated box
+ * @param {Number} height the height in pixels of the generated box
+ * @param {Function} createDataFunction a function that is called with no
+ *   arguments to generate the structured data
+ * @return a new DOM element
+ */
+SimileAjax.Graphics.createStructuredDataCopyButton = function(image, width, height, createDataFunction) {
+    var div = document.createElement("div");
+    div.style.position = "relative";
+    div.style.display = "inline";
+    div.style.width = width + "px";
+    div.style.height = height + "px";
+    div.style.overflow = "hidden";
+    div.style.margin = "2px";
+    
+    if (SimileAjax.Graphics.pngIsTranslucent) {
+        div.style.background = "url(" + image + ") no-repeat";
+    } else {
+        div.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + image +"', sizingMethod='image')";
+    }
+    
+    var style;
+    if (SimileAjax.Platform.browser.isIE) {
+        style = "filter:alpha(opacity=0)";
+    } else {
+        style = "opacity: 0";
+    }
+    div.innerHTML = "<textarea rows='1' autocomplete='off' value='none' style='" + style + "' />";
+    
+    var textarea = div.firstChild;
+    textarea.style.width = width + "px";
+    textarea.style.height = height + "px";
+    textarea.onmousedown = function(evt) {
+        evt = (evt) ? evt : ((event) ? event : null);
+        if (evt.button == 2) {
+            textarea.value = createDataFunction();
+            textarea.select();
+        }
+    };
+    
+    return div;
+};
+
+/*==================================================
+ *  getWidthHeight
+ *==================================================
+ */
+SimileAjax.Graphics.getWidthHeight = function(el) {
+    // RETURNS hash {width:  w, height: h} in pixels
+    
+    var w, h;
+    // offsetWidth rounds on FF, so doesn't work for us.
+    // See https://bugzilla.mozilla.org/show_bug.cgi?id=458617
+    if (el.getBoundingClientRect == null) {
+    	// use offsetWidth
+      w = el.offsetWidth;
+      h = el.offsetHeight;
+    } else {
+    	// use getBoundingClientRect
+      var rect = el.getBoundingClientRect();
+      w = Math.ceil(rect.right - rect.left);
+    	h = Math.ceil(rect.bottom - rect.top);
+    }
+    return {
+        width:  w,
+        height: h
+    };
+};
+ 
+
+/*==================================================
+ *  FontRenderingContext
+ *==================================================
+ */
+SimileAjax.Graphics.getFontRenderingContext = function(elmt, width) {
+    return new SimileAjax.Graphics._FontRenderingContext(elmt, width);
+};
+
+SimileAjax.Graphics._FontRenderingContext = function(elmt, width) {
+    this._elmt = elmt;
+    this._elmt.style.visibility = "hidden";
+    if (typeof width == "string") {
+        this._elmt.style.width = width;
+    } else if (typeof width == "number") {
+        this._elmt.style.width = width + "px";
+    }
+};
+
+SimileAjax.Graphics._FontRenderingContext.prototype.dispose = function() {
+    this._elmt = null;
+};
+
+SimileAjax.Graphics._FontRenderingContext.prototype.update = function() {
+    this._elmt.innerHTML = "A";
+    this._lineHeight = this._elmt.offsetHeight;
+};
+
+SimileAjax.Graphics._FontRenderingContext.prototype.computeSize = function(text, className) {
+    // className arg is optional
+    var el = this._elmt;
+    el.innerHTML = text;
+    el.className = className === undefined ? '' : className;
+    var wh = SimileAjax.Graphics.getWidthHeight(el);
+    el.className = ''; // reset for the next guy
+    
+    return wh;
+};
+
+SimileAjax.Graphics._FontRenderingContext.prototype.getLineHeight = function() {
+    return this._lineHeight;
+};
+
+/**
+ * @fileOverview A collection of date/time utility functions
+ * @name SimileAjax.DateTime
+ */
+
+SimileAjax.DateTime = new Object();
+
+SimileAjax.DateTime.MILLISECOND    = 0;
+SimileAjax.DateTime.SECOND         = 1;
+SimileAjax.DateTime.MINUTE         = 2;
+SimileAjax.DateTime.HOUR           = 3;
+SimileAjax.DateTime.DAY            = 4;
+SimileAjax.DateTime.WEEK           = 5;
+SimileAjax.DateTime.MONTH          = 6;
+SimileAjax.DateTime.YEAR           = 7;
+SimileAjax.DateTime.DECADE         = 8;
+SimileAjax.DateTime.CENTURY        = 9;
+SimileAjax.DateTime.MILLENNIUM     = 10;
+
+SimileAjax.DateTime.EPOCH          = -1;
+SimileAjax.DateTime.ERA            = -2;
+
+/**
+ * An array of unit lengths, expressed in milliseconds, of various lengths of
+ * time.  The array indices are predefined and stored as properties of the
+ * SimileAjax.DateTime object, e.g. SimileAjax.DateTime.YEAR.
+ * @type Array
+ */
+SimileAjax.DateTime.gregorianUnitLengths = [];
+    (function() {
+        var d = SimileAjax.DateTime;
+        var a = d.gregorianUnitLengths;
+        
+        a[d.MILLISECOND] = 1;
+        a[d.SECOND]      = 1000;
+        a[d.MINUTE]      = a[d.SECOND] * 60;
+        a[d.HOUR]        = a[d.MINUTE] * 60;
+        a[d.DAY]         = a[d.HOUR] * 24;
+        a[d.WEEK]        = a[d.DAY] * 7;
+        a[d.MONTH]       = a[d.DAY] * 31;
+        a[d.YEAR]        = a[d.DAY] * 365;
+        a[d.DECADE]      = a[d.YEAR] * 10;
+        a[d.CENTURY]     = a[d.YEAR] * 100;
+        a[d.MILLENNIUM]  = a[d.YEAR] * 1000;
+    })();
+    
+SimileAjax.DateTime._dateRegexp = new RegExp(
+    "^(-?)([0-9]{4})(" + [
+        "(-?([0-9]{2})(-?([0-9]{2}))?)", // -month-dayOfMonth
+        "(-?([0-9]{3}))",                // -dayOfYear
+        "(-?W([0-9]{2})(-?([1-7]))?)"    // -Wweek-dayOfWeek
+    ].join("|") + ")?$"
+);
+SimileAjax.DateTime._timezoneRegexp = new RegExp(
+    "Z|(([-+])([0-9]{2})(:?([0-9]{2}))?)$"
+);
+SimileAjax.DateTime._timeRegexp = new RegExp(
+    "^([0-9]{2})(:?([0-9]{2})(:?([0-9]{2})(\.([0-9]+))?)?)?$"
+);
+
+/**
+ * Takes a date object and a string containing an ISO 8601 date and sets the
+ * the date using information parsed from the string.  Note that this method
+ * does not parse any time information.
+ *
+ * @param {Date} dateObject the date object to modify
+ * @param {String} string an ISO 8601 string to parse
+ * @return {Date} the modified date object
+ */
+SimileAjax.DateTime.setIso8601Date = function(dateObject, string) {
+    /*
+     *  This function has been adapted from dojo.date, v.0.3.0
+     *  http://dojotoolkit.org/.
+     */
+     
+    var d = string.match(SimileAjax.DateTime._dateRegexp);
+    if(!d) {
+        throw new Error("Invalid date string: " + string);
+    }
+    
+    var sign = (d[1] == "-") ? -1 : 1; // BC or AD
+    var year = sign * d[2];
+    var month = d[5];
+    var date = d[7];
+    var dayofyear = d[9];
+    var week = d[11];
+    var dayofweek = (d[13]) ? d[13] : 1;
+
+    dateObject.setUTCFullYear(year);
+    if (dayofyear) { 
+        dateObject.setUTCMonth(0);
+        dateObject.setUTCDate(Number(dayofyear));
+    } else if (week) {
+        dateObject.setUTCMonth(0);
+        dateObject.setUTCDate(1);
+        var gd = dateObject.getUTCDay();
+        var day =  (gd) ? gd : 7;
+        var offset = Number(dayofweek) + (7 * Number(week));
+        
+        if (day <= 4) { 
+            dateObject.setUTCDate(offset + 1 - day); 
+        } else { 
+            dateObject.setUTCDate(offset + 8 - day); 
+        }
+    } else {
+        if (month) { 
+            dateObject.setUTCDate(1);
+            dateObject.setUTCMonth(month - 1); 
+        }
+        if (date) { 
+            dateObject.setUTCDate(date); 
+        }
+    }
+    
+    return dateObject;
+};
+
+/**
+ * Takes a date object and a string containing an ISO 8601 time and sets the
+ * the time using information parsed from the string.  Note that this method
+ * does not parse any date information.
+ *
+ * @param {Date} dateObject the date object to modify
+ * @param {String} string an ISO 8601 string to parse
+ * @return {Date} the modified date object
+ */
+SimileAjax.DateTime.setIso8601Time = function (dateObject, string) {
+    /*
+     *  This function has been adapted from dojo.date, v.0.3.0
+     *  http://dojotoolkit.org/.
+     */
+    
+    var d = string.match(SimileAjax.DateTime._timeRegexp);
+    if(!d) {
+        SimileAjax.Debug.warn("Invalid time string: " + string);
+        return false;
+    }
+    var hours = d[1];
+    var mins = Number((d[3]) ? d[3] : 0);
+    var secs = (d[5]) ? d[5] : 0;
+    var ms = d[7] ? (Number("0." + d[7]) * 1000) : 0;
+
+    dateObject.setUTCHours(hours);
+    dateObject.setUTCMinutes(mins);
+    dateObject.setUTCSeconds(secs);
+    dateObject.setUTCMilliseconds(ms);
+    
+    return dateObject;
+};
+
+/**
+ * The timezone offset in minutes in the user's browser.
+ * @type Number
+ */
+SimileAjax.DateTime.timezoneOffset = new Date().getTimezoneOffset();
+
+/**
+ * Takes a date object and a string containing an ISO 8601 date and time and 
+ * sets the date object using information parsed from the string.
+ *
+ * @param {Date} dateObject the date object to modify
+ * @param {String} string an ISO 8601 string to parse
+ * @return {Date} the modified date object
+ */
+SimileAjax.DateTime.setIso8601 = function (dateObject, string){
+    /*
+     *  This function has been adapted from dojo.date, v.0.3.0
+     *  http://dojotoolkit.org/.
+     */
+     
+    var offset = null;
+    var comps = (string.indexOf("T") == -1) ? string.split(" ") : string.split("T");
+    
+    SimileAjax.DateTime.setIso8601Date(dateObject, comps[0]);
+    if (comps.length == 2) { 
+        // first strip timezone info from the end
+        var d = comps[1].match(SimileAjax.DateTime._timezoneRegexp);
+        if (d) {
+            if (d[0] == 'Z') {
+                offset = 0;
+            } else {
+                offset = (Number(d[3]) * 60) + Number(d[5]);
+                offset *= ((d[2] == '-') ? 1 : -1);
+            }
+            comps[1] = comps[1].substr(0, comps[1].length - d[0].length);
+        }
+
+        SimileAjax.DateTime.setIso8601Time(dateObject, comps[1]); 
+    }
+    if (offset == null) {
+        offset = dateObject.getTimezoneOffset(); // local time zone if no tz info
+    }
+    dateObject.setTime(dateObject.getTime() + offset * 60000);
+    
+    return dateObject;
+};
+
+/**
+ * Takes a string containing an ISO 8601 date and returns a newly instantiated
+ * date object with the parsed date and time information from the string.
+ *
+ * @param {String} string an ISO 8601 string to parse
+ * @return {Date} a new date object created from the string
+ */
+SimileAjax.DateTime.parseIso8601DateTime = function (string) {
+    try {
+        return SimileAjax.DateTime.setIso8601(new Date(0), string);
+    } catch (e) {
+        return null;
+    }
+};
+
+/**
+ * Takes a string containing a Gregorian date and time and returns a newly
+ * instantiated date object with the parsed date and time information from the
+ * string.  If the param is actually an instance of Date instead of a string, 
+ * simply returns the given date instead.
+ *
+ * @param {Object} o an object, to either return or parse as a string
+ * @return {Date} the date object
+ */
+SimileAjax.DateTime.parseGregorianDateTime = function(o) {
+    if (o == null) {
+        return null;
+    } else if (o instanceof Date) {
+        return o;
+    }
+    
+    var s = o.toString();
+    if (s.length > 0 && s.length < 8) {
+        var space = s.indexOf(" ");
+        if (space > 0) {
+            var year = parseInt(s.substr(0, space));
+            var suffix = s.substr(space + 1);
+            if (suffix.toLowerCase() == "bc") {
+                year = 1 - year;
+            }
+        } else {
+            var year = parseInt(s);
+        }
+            
+        var d = new Date(0);
+        d.setUTCFullYear(year);
+        
+        return d;
+    }
+    
+    try {
+        return new Date(Date.parse(s));
+    } catch (e) {
+        return null;
+    }
+};
+
+/**
+ * Rounds date objects down to the nearest interval or multiple of an interval.
+ * This method modifies the given date object, converting it to the given
+ * timezone if specified.
+ * 
+ * @param {Date} date the date object to round
+ * @param {Number} intervalUnit a constant, integer index specifying an 
+ *   interval, e.g. SimileAjax.DateTime.HOUR
+ * @param {Number} timeZone a timezone shift, given in hours
+ * @param {Number} multiple a multiple of the interval to round by
+ * @param {Number} firstDayOfWeek an integer specifying the first day of the
+ *   week, 0 corresponds to Sunday, 1 to Monday, etc.
+ */
+SimileAjax.DateTime.roundDownToInterval = function(date, intervalUnit, timeZone, multiple, firstDayOfWeek) {
+    var timeShift = timeZone * 
+        SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR];
+        
+    var date2 = new Date(date.getTime() + timeShift);
+    var clearInDay = function(d) {
+        d.setUTCMilliseconds(0);
+        d.setUTCSeconds(0);
+        d.setUTCMinutes(0);
+        d.setUTCHours(0);
+    };
+    var clearInYear = function(d) {
+        clearInDay(d);
+        d.setUTCDate(1);
+        d.setUTCMonth(0);
+    };
+    
+    switch(intervalUnit) {
+    case SimileAjax.DateTime.MILLISECOND:
+        var x = date2.getUTCMilliseconds();
+        date2.setUTCMilliseconds(x - (x % multiple));
+        break;
+    case SimileAjax.DateTime.SECOND:
+        date2.setUTCMilliseconds(0);
+        
+        var x = date2.getUTCSeconds();
+        date2.setUTCSeconds(x - (x % multiple));
+        break;
+    case SimileAjax.DateTime.MINUTE:
+        date2.setUTCMilliseconds(0);
+        date2.setUTCSeconds(0);
+        
+        var x = date2.getUTCMinutes();
+        date2.setTime(date2.getTime() - 
+            (x % multiple) * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.MINUTE]);
+        break;
+    case SimileAjax.DateTime.HOUR:
+        date2.setUTCMilliseconds(0);
+        date2.setUTCSeconds(0);
+        date2.setUTCMinutes(0);
+        
+        var x = date2.getUTCHours();
+        date2.setUTCHours(x - (x % multiple));
+        break;
+    case SimileAjax.DateTime.DAY:
+        clearInDay(date2);
+        break;
+    case SimileAjax.DateTime.WEEK:
+        clearInDay(date2);
+        var d = (date2.getUTCDay() + 7 - firstDayOfWeek) % 7;
+        date2.setTime(date2.getTime() - 
+            d * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.DAY]);
+        break;
+    case SimileAjax.DateTime.MONTH:
+        clearInDay(date2);
+        date2.setUTCDate(1);
+        
+        var x = date2.getUTCMonth();
+        date2.setUTCMonth(x - (x % multiple));
+        break;
+    case SimileAjax.DateTime.YEAR:
+        clearInYear(date2);
+        
+        var x = date2.getUTCFullYear();
+        date2.setUTCFullYear(x - (x % multiple));
+        break;
+    case SimileAjax.DateTime.DECADE:
+        clearInYear(date2);
+        date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 10) * 10);
+        break;
+    case SimileAjax.DateTime.CENTURY:
+        clearInYear(date2);
+        date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 100) * 100);
+        break;
+    case SimileAjax.DateTime.MILLENNIUM:
+        clearInYear(date2);
+        date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 1000) * 1000);
+        break;
+    }
+    date.setTime(date2.getTime() - timeShift);
+};
+
+/**
+ * Rounds date objects up to the nearest interval or multiple of an interval.
+ * This method modifies the given date object, converting it to the given
+ * timezone if specified.
+ * 
+ * @param {Date} date the date object to round
+ * @param {Number} intervalUnit a constant, integer index specifying an 
+ *   interval, e.g. SimileAjax.DateTime.HOUR
+ * @param {Number} timeZone a timezone shift, given in hours
+ * @param {Number} multiple a multiple of the interval to round by
+ * @param {Number} firstDayOfWeek an integer specifying the first day of the
+ *   week, 0 corresponds to Sunday, 1 to Monday, etc.
+ * @see SimileAjax.DateTime.roundDownToInterval
+ */
+SimileAjax.DateTime.roundUpToInterval = function(date, intervalUnit, timeZone, multiple, firstDayOfWeek) {
+    var originalTime = date.getTime();
+    SimileAjax.DateTime.roundDownToInterval(date, intervalUnit, timeZone, multiple, firstDayOfWeek);
+    if (date.getTime() < originalTime) {
+        date.setTime(date.getTime() + 
+            SimileAjax.DateTime.gregorianUnitLengths[intervalUnit] * multiple);
+    }
+};
+
+/**
+ * Increments a date object by a specified interval, taking into
+ * consideration the timezone.
+ *
+ * @param {Date} date the date object to increment
+ * @param {Number} intervalUnit a constant, integer index specifying an 
+ *   interval, e.g. SimileAjax.DateTime.HOUR
+ * @param {Number} timeZone the timezone offset in hours
+ */
+SimileAjax.DateTime.incrementByInterval = function(date, intervalUnit, timeZone) {
+    timeZone = (typeof timeZone == 'undefined') ? 0 : timeZone;
+
+    var timeShift = timeZone * 
+        SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR];
+        
+    var date2 = new Date(date.getTime() + timeShift);
+
+    switch(intervalUnit) {
+    case SimileAjax.DateTime.MILLISECOND:
+        date2.setTime(date2.getTime() + 1)
+        break;
+    case SimileAjax.DateTime.SECOND:
+        date2.setTime(date2.getTime() + 1000);
+        break;
+    case SimileAjax.DateTime.MINUTE:
+        date2.setTime(date2.getTime() + 
+            SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.MINUTE]);
+        break;
+    case SimileAjax.DateTime.HOUR:
+        date2.setTime(date2.getTime() + 
+            SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR]);
+        break;
+    case SimileAjax.DateTime.DAY:
+        date2.setUTCDate(date2.getUTCDate() + 1);
+        break;
+    case SimileAjax.DateTime.WEEK:
+        date2.setUTCDate(date2.getUTCDate() + 7);
+        break;
+    case SimileAjax.DateTime.MONTH:
+        date2.setUTCMonth(date2.getUTCMonth() + 1);
+        break;
+    case SimileAjax.DateTime.YEAR:
+        date2.setUTCFullYear(date2.getUTCFullYear() + 1);
+        break;
+    case SimileAjax.DateTime.DECADE:
+        date2.setUTCFullYear(date2.getUTCFullYear() + 10);
+        break;
+    case SimileAjax.DateTime.CENTURY:
+        date2.setUTCFullYear(date2.getUTCFullYear() + 100);
+        break;
+    case SimileAjax.DateTime.MILLENNIUM:
+        date2.setUTCFullYear(date2.getUTCFullYear() + 1000);
+        break;
+    }
+
+    date.setTime(date2.getTime() - timeShift);
+};
+
+/**
+ * Returns a new date object with the given time offset removed.
+ *
+ * @param {Date} date the starting date
+ * @param {Number} timeZone a timezone specified in an hour offset to remove
+ * @return {Date} a new date object with the offset removed
+ */
+SimileAjax.DateTime.removeTimeZoneOffset = function(date, timeZone) {
+    return new Date(date.getTime() + 
+        timeZone * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR]);
+};
+
+/**
+ * Returns the timezone of the user's browser.
+ *
+ * @return {Number} the timezone in the user's locale in hours
+ */
+SimileAjax.DateTime.getTimezone = function() {
+    var d = new Date().getTimezoneOffset();
+    return d / -60;
+};
+/*==================================================
+ *  String Utility Functions and Constants
+ *==================================================
+ */
+
+String.prototype.trim = function() {
+    return this.replace(/^\s+|\s+$/g, '');
+};
+
+String.prototype.startsWith = function(prefix) {
+    return this.length >= prefix.length && this.substr(0, prefix.length) == prefix;
+};
+
+String.prototype.endsWith = function(suffix) {
+    return this.length >= suffix.length && this.substr(this.length - suffix.length) == suffix;
+};
+
+String.substitute = function(s, objects) {
+    var result = "";
+    var start = 0;
+    while (start < s.length - 1) {
+        var percent = s.indexOf("%", start);
+        if (percent < 0 || percent == s.length - 1) {
+            break;
+        } else if (percent > start && s.charAt(percent - 1) == "\\") {
+            result += s.substring(start, percent - 1) + "%";
+            start = percent + 1;
+        } else {
+            var n = parseInt(s.charAt(percent + 1));
+            if (isNaN(n) || n >= objects.length) {
+                result += s.substring(start, percent + 2);
+            } else {
+                result += s.substring(start, percent) + objects[n].toString();
+            }
+            start = percent + 2;
+        }
+    }
+    
+    if (start < s.length) {
+        result += s.substring(start);
+    }
+    return result;
+};
+/*==================================================
+ *  HTML Utility Functions
+ *==================================================
+ */
+
+SimileAjax.HTML = new Object();
+
+SimileAjax.HTML._e2uHash = {};
+(function() {
+    var e2uHash = SimileAjax.HTML._e2uHash;
+    e2uHash['nbsp']= '\u00A0[space]';
+    e2uHash['iexcl']= '\u00A1';
+    e2uHash['cent']= '\u00A2';
+    e2uHash['pound']= '\u00A3';
+    e2uHash['curren']= '\u00A4';
+    e2uHash['yen']= '\u00A5';
+    e2uHash['brvbar']= '\u00A6';
+    e2uHash['sect']= '\u00A7';
+    e2uHash['uml']= '\u00A8';
+    e2uHash['copy']= '\u00A9';
+    e2uHash['ordf']= '\u00AA';
+    e2uHash['laquo']= '\u00AB';
+    e2uHash['not']= '\u00AC';
+    e2uHash['shy']= '\u00AD';
+    e2uHash['reg']= '\u00AE';
+    e2uHash['macr']= '\u00AF';
+    e2uHash['deg']= '\u00B0';
+    e2uHash['plusmn']= '\u00B1';
+    e2uHash['sup2']= '\u00B2';
+    e2uHash['sup3']= '\u00B3';
+    e2uHash['acute']= '\u00B4';
+    e2uHash['micro']= '\u00B5';
+    e2uHash['para']= '\u00B6';
+    e2uHash['middot']= '\u00B7';
+    e2uHash['cedil']= '\u00B8';
+    e2uHash['sup1']= '\u00B9';
+    e2uHash['ordm']= '\u00BA';
+    e2uHash['raquo']= '\u00BB';
+    e2uHash['frac14']= '\u00BC';
+    e2uHash['frac12']= '\u00BD';
+    e2uHash['frac34']= '\u00BE';
+    e2uHash['iquest']= '\u00BF';
+    e2uHash['Agrave']= '\u00C0';
+    e2uHash['Aacute']= '\u00C1';
+    e2uHash['Acirc']= '\u00C2';
+    e2uHash['Atilde']= '\u00C3';
+    e2uHash['Auml']= '\u00C4';
+    e2uHash['Aring']= '\u00C5';
+    e2uHash['AElig']= '\u00C6';
+    e2uHash['Ccedil']= '\u00C7';
+    e2uHash['Egrave']= '\u00C8';
+    e2uHash['Eacute']= '\u00C9';
+    e2uHash['Ecirc']= '\u00CA';
+    e2uHash['Euml']= '\u00CB';
+    e2uHash['Igrave']= '\u00CC';
+    e2uHash['Iacute']= '\u00CD';
+    e2uHash['Icirc']= '\u00CE';
+    e2uHash['Iuml']= '\u00CF';
+    e2uHash['ETH']= '\u00D0';
+    e2uHash['Ntilde']= '\u00D1';
+    e2uHash['Ograve']= '\u00D2';
+    e2uHash['Oacute']= '\u00D3';
+    e2uHash['Ocirc']= '\u00D4';
+    e2uHash['Otilde']= '\u00D5';
+    e2uHash['Ouml']= '\u00D6';
+    e2uHash['times']= '\u00D7';
+    e2uHash['Oslash']= '\u00D8';
+    e2uHash['Ugrave']= '\u00D9';
+    e2uHash['Uacute']= '\u00DA';
+    e2uHash['Ucirc']= '\u00DB';
+    e2uHash['Uuml']= '\u00DC';
+    e2uHash['Yacute']= '\u00DD';
+    e2uHash['THORN']= '\u00DE';
+    e2uHash['szlig']= '\u00DF';
+    e2uHash['agrave']= '\u00E0';
+    e2uHash['aacute']= '\u00E1';
+    e2uHash['acirc']= '\u00E2';
+    e2uHash['atilde']= '\u00E3';
+    e2uHash['auml']= '\u00E4';
+    e2uHash['aring']= '\u00E5';
+    e2uHash['aelig']= '\u00E6';
+    e2uHash['ccedil']= '\u00E7';
+    e2uHash['egrave']= '\u00E8';
+    e2uHash['eacute']= '\u00E9';
+    e2uHash['ecirc']= '\u00EA';
+    e2uHash['euml']= '\u00EB';
+    e2uHash['igrave']= '\u00EC';
+    e2uHash['iacute']= '\u00ED';
+    e2uHash['icirc']= '\u00EE';
+    e2uHash['iuml']= '\u00EF';
+    e2uHash['eth']= '\u00F0';
+    e2uHash['ntilde']= '\u00F1';
+    e2uHash['ograve']= '\u00F2';
+    e2uHash['oacute']= '\u00F3';
+    e2uHash['ocirc']= '\u00F4';
+    e2uHash['otilde']= '\u00F5';
+    e2uHash['ouml']= '\u00F6';
+    e2uHash['divide']= '\u00F7';
+    e2uHash['oslash']= '\u00F8';
+    e2uHash['ugrave']= '\u00F9';
+    e2uHash['uacute']= '\u00FA';
+    e2uHash['ucirc']= '\u00FB';
+    e2uHash['uuml']= '\u00FC';
+    e2uHash['yacute']= '\u00FD';
+    e2uHash['thorn']= '\u00FE';
+    e2uHash['yuml']= '\u00FF';
+    e2uHash['quot']= '\u0022';
+    e2uHash['amp']= '\u0026';
+    e2uHash['lt']= '\u003C';
+    e2uHash['gt']= '\u003E';
+    e2uHash['OElig']= '';
+    e2uHash['oelig']= '\u0153';
+    e2uHash['Scaron']= '\u0160';
+    e2uHash['scaron']= '\u0161';
+    e2uHash['Yuml']= '\u0178';
+    e2uHash['circ']= '\u02C6';
+    e2uHash['tilde']= '\u02DC';
+    e2uHash['ensp']= '\u2002';
+    e2uHash['emsp']= '\u2003';
+    e2uHash['thinsp']= '\u2009';
+    e2uHash['zwnj']= '\u200C';
+    e2uHash['zwj']= '\u200D';
+    e2uHash['lrm']= '\u200E';
+    e2uHash['rlm']= '\u200F';
+    e2uHash['ndash']= '\u2013';
+    e2uHash['mdash']= '\u2014';
+    e2uHash['lsquo']= '\u2018';
+    e2uHash['rsquo']= '\u2019';
+    e2uHash['sbquo']= '\u201A';
+    e2uHash['ldquo']= '\u201C';
+    e2uHash['rdquo']= '\u201D';
+    e2uHash['bdquo']= '\u201E';
+    e2uHash['dagger']= '\u2020';
+    e2uHash['Dagger']= '\u2021';
+    e2uHash['permil']= '\u2030';
+    e2uHash['lsaquo']= '\u2039';
+    e2uHash['rsaquo']= '\u203A';
+    e2uHash['euro']= '\u20AC';
+    e2uHash['fnof']= '\u0192';
+    e2uHash['Alpha']= '\u0391';
+    e2uHash['Beta']= '\u0392';
+    e2uHash['Gamma']= '\u0393';
+    e2uHash['Delta']= '\u0394';
+    e2uHash['Epsilon']= '\u0395';
+    e2uHash['Zeta']= '\u0396';
+    e2uHash['Eta']= '\u0397';
+    e2uHash['Theta']= '\u0398';
+    e2uHash['Iota']= '\u0399';
+    e2uHash['Kappa']= '\u039A';
+    e2uHash['Lambda']= '\u039B';
+    e2uHash['Mu']= '\u039C';
+    e2uHash['Nu']= '\u039D';
+    e2uHash['Xi']= '\u039E';
+    e2uHash['Omicron']= '\u039F';
+    e2uHash['Pi']= '\u03A0';
+    e2uHash['Rho']= '\u03A1';
+    e2uHash['Sigma']= '\u03A3';
+    e2uHash['Tau']= '\u03A4';
+    e2uHash['Upsilon']= '\u03A5';
+    e2uHash['Phi']= '\u03A6';
+    e2uHash['Chi']= '\u03A7';
+    e2uHash['Psi']= '\u03A8';
+    e2uHash['Omega']= '\u03A9';
+    e2uHash['alpha']= '\u03B1';
+    e2uHash['beta']= '\u03B2';
+    e2uHash['gamma']= '\u03B3';
+    e2uHash['delta']= '\u03B4';
+    e2uHash['epsilon']= '\u03B5';
+    e2uHash['zeta']= '\u03B6';
+    e2uHash['eta']= '\u03B7';
+    e2uHash['theta']= '\u03B8';
+    e2uHash['iota']= '\u03B9';
+    e2uHash['kappa']= '\u03BA';
+    e2uHash['lambda']= '\u03BB';
+    e2uHash['mu']= '\u03BC';
+    e2uHash['nu']= '\u03BD';
+    e2uHash['xi']= '\u03BE';
+    e2uHash['omicron']= '\u03BF';
+    e2uHash['pi']= '\u03C0';
+    e2uHash['rho']= '\u03C1';
+    e2uHash['sigmaf']= '\u03C2';
+    e2uHash['sigma']= '\u03C3';
+    e2uHash['tau']= '\u03C4';
+    e2uHash['upsilon']= '\u03C5';
+    e2uHash['phi']= '\u03C6';
+    e2uHash['chi']= '\u03C7';
+    e2uHash['psi']= '\u03C8';
+    e2uHash['omega']= '\u03C9';
+    e2uHash['thetasym']= '\u03D1';
+    e2uHash['upsih']= '\u03D2';
+    e2uHash['piv']= '\u03D6';
+    e2uHash['bull']= '\u2022';
+    e2uHash['hellip']= '\u2026';
+    e2uHash['prime']= '\u2032';
+    e2uHash['Prime']= '\u2033';
+    e2uHash['oline']= '\u203E';
+    e2uHash['frasl']= '\u2044';
+    e2uHash['weierp']= '\u2118';
+    e2uHash['image']= '\u2111';
+    e2uHash['real']= '\u211C';
+    e2uHash['trade']= '\u2122';
+    e2uHash['alefsym']= '\u2135';
+    e2uHash['larr']= '\u2190';
+    e2uHash['uarr']= '\u2191';
+    e2uHash['rarr']= '\u2192';
+    e2uHash['darr']= '\u2193';
+    e2uHash['harr']= '\u2194';
+    e2uHash['crarr']= '\u21B5';
+    e2uHash['lArr']= '\u21D0';
+    e2uHash['uArr']= '\u21D1';
+    e2uHash['rArr']= '\u21D2';
+    e2uHash['dArr']= '\u21D3';
+    e2uHash['hArr']= '\u21D4';
+    e2uHash['forall']= '\u2200';
+    e2uHash['part']= '\u2202';
+    e2uHash['exist']= '\u2203';
+    e2uHash['empty']= '\u2205';
+    e2uHash['nabla']= '\u2207';
+    e2uHash['isin']= '\u2208';
+    e2uHash['notin']= '\u2209';
+    e2uHash['ni']= '\u220B';
+    e2uHash['prod']= '\u220F';
+    e2uHash['sum']= '\u2211';
+    e2uHash['minus']= '\u2212';
+    e2uHash['lowast']= '\u2217';
+    e2uHash['radic']= '\u221A';
+    e2uHash['prop']= '\u221D';
+    e2uHash['infin']= '\u221E';
+    e2uHash['ang']= '\u2220';
+    e2uHash['and']= '\u2227';
+    e2uHash['or']= '\u2228';
+    e2uHash['cap']= '\u2229';
+    e2uHash['cup']= '\u222A';
+    e2uHash['int']= '\u222B';
+    e2uHash['there4']= '\u2234';
+    e2uHash['sim']= '\u223C';
+    e2uHash['cong']= '\u2245';
+    e2uHash['asymp']= '\u2248';
+    e2uHash['ne']= '\u2260';
+    e2uHash['equiv']= '\u2261';
+    e2uHash['le']= '\u2264';
+    e2uHash['ge']= '\u2265';
+    e2uHash['sub']= '\u2282';
+    e2uHash['sup']= '\u2283';
+    e2uHash['nsub']= '\u2284';
+    e2uHash['sube']= '\u2286';
+    e2uHash['supe']= '\u2287';
+    e2uHash['oplus']= '\u2295';
+    e2uHash['otimes']= '\u2297';
+    e2uHash['perp']= '\u22A5';
+    e2uHash['sdot']= '\u22C5';
+    e2uHash['lceil']= '\u2308';
+    e2uHash['rceil']= '\u2309';
+    e2uHash['lfloor']= '\u230A';
+    e2uHash['rfloor']= '\u230B';
+    e2uHash['lang']= '\u2329';
+    e2uHash['rang']= '\u232A';
+    e2uHash['loz']= '\u25CA';
+    e2uHash['spades']= '\u2660';
+    e2uHash['clubs']= '\u2663';
+    e2uHash['hearts']= '\u2665';
+    e2uHash['diams']= '\u2666'; 
+})();
+
+SimileAjax.HTML.deEntify = function(s) {
+    var e2uHash = SimileAjax.HTML._e2uHash;
+    
+    var re = /&(\w+?);/;
+    while (re.test(s)) {
+        var m = s.match(re);
+        s = s.replace(re, e2uHash[m[1]]);
+    }
+    return s;
+};
+/**
+ * A basic set (in the mathematical sense) data structure
+ *
+ * @constructor
+ * @param {Array or SimileAjax.Set} [a] an initial collection
+ */
+SimileAjax.Set = function(a) {
+    this._hash = {};
+    this._count = 0;
+    
+    if (a instanceof Array) {
+        for (var i = 0; i < a.length; i++) {
+            this.add(a[i]);
+        }
+    } else if (a instanceof SimileAjax.Set) {
+        this.addSet(a);
+    }
+}
+
+/**
+ * Adds the given object to this set, assuming there it does not already exist
+ *
+ * @param {Object} o the object to add
+ * @return {Boolean} true if the object was added, false if not
+ */
+SimileAjax.Set.prototype.add = function(o) {
+    if (!(o in this._hash)) {
+        this._hash[o] = true;
+        this._count++;
+        return true;
+    }
+    return false;
+}
+
+/**
+ * Adds each element in the given set to this set
+ *
+ * @param {SimileAjax.Set} set the set of elements to add
+ */
+SimileAjax.Set.prototype.addSet = function(set) {
+    for (var o in set._hash) {
+        this.add(o);
+    }
+}
+
+/**
+ * Removes the given element from this set
+ *
+ * @param {Object} o the object to remove
+ * @return {Boolean} true if the object was successfully removed,
+ *   false otherwise
+ */
+SimileAjax.Set.prototype.remove = function(o) {
+    if (o in this._hash) {
+        delete this._hash[o];
+        this._count--;
+        return true;
+    }
+    return false;
+}
+
+/**
+ * Removes the elements in this set that correspond to the elements in the
+ * given set
+ *
+ * @param {SimileAjax.Set} set the set of elements to remove
+ */
+SimileAjax.Set.prototype.removeSet = function(set) {
+    for (var o in set._hash) {
+        this.remove(o);
+    }
+}
+
+/**
+ * Removes all elements in this set that are not present in the given set, i.e.
+ * modifies this set to the intersection of the two sets
+ *
+ * @param {SimileAjax.Set} set the set to intersect
+ */
+SimileAjax.Set.prototype.retainSet = function(set) {
+    for (var o in this._hash) {
+        if (!set.contains(o)) {
+            delete this._hash[o];
+            this._count--;
+        }
+    }
+}
+
+/**
+ * Returns whether or not the given element exists in this set
+ *
+ * @param {SimileAjax.Set} o the object to test for
+ * @return {Boolean} true if the object is present, false otherwise
+ */
+SimileAjax.Set.prototype.contains = function(o) {
+    return (o in this._hash);
+}
+
+/**
+ * Returns the number of elements in this set
+ *
+ * @return {Number} the number of elements in this set
+ */
+SimileAjax.Set.prototype.size = function() {
+    return this._count;
+}
+
+/**
+ * Returns the elements of this set as an array
+ *
+ * @return {Array} a new array containing the elements of this set
+ */
+SimileAjax.Set.prototype.toArray = function() {
+    var a = [];
+    for (var o in this._hash) {
+        a.push(o);
+    }
+    return a;
+}
+
+/**
+ * Iterates through the elements of this set, order unspecified, executing the
+ * given function on each element until the function returns true
+ *
+ * @param {Function} f a function of form f(element)
+ */
+SimileAjax.Set.prototype.visit = function(f) {
+    for (var o in this._hash) {
+        if (f(o) == true) {
+            break;
+        }
+    }
+}
+
+/**
+ * A sorted array data structure
+ *
+ * @constructor
+ */
+SimileAjax.SortedArray = function(compare, initialArray) {
+    this._a = (initialArray instanceof Array) ? initialArray : [];
+    this._compare = compare;
+};
+
+SimileAjax.SortedArray.prototype.add = function(elmt) {
+    var sa = this;
+    var index = this.find(function(elmt2) {
+        return sa._compare(elmt2, elmt);
+    });
+    
+    if (index < this._a.length) {
+        this._a.splice(index, 0, elmt);
+    } else {
+        this._a.push(elmt);
+    }
+};
+
+SimileAjax.SortedArray.prototype.remove = function(elmt) {
+    var sa = this;
+    var index = this.find(function(elmt2) {
+        return sa._compare(elmt2, elmt);
+    });
+    
+    while (index < this._a.length && this._compare(this._a[index], elmt) == 0) {
+        if (this._a[index] == elmt) {
+            this._a.splice(index, 1);
+            return true;
+        } else {
+            index++;
+        }
+    }
+    return false;
+};
+
+SimileAjax.SortedArray.prototype.removeAll = function() {
+    this._a = [];
+};
+
+SimileAjax.SortedArray.prototype.elementAt = function(index) {
+    return this._a[index];
+};
+
+SimileAjax.SortedArray.prototype.length = function() {
+    return this._a.length;
+};
+
+SimileAjax.SortedArray.prototype.find = function(compare) {
+    var a = 0;
+    var b = this._a.length;
+    
+    while (a < b) {
+        var mid = Math.floor((a + b) / 2);
+        var c = compare(this._a[mid]);
+        if (mid == a) {
+            return c < 0 ? a+1 : a;
+        } else if (c < 0) {
+            a = mid;
+        } else {
+            b = mid;
+        }
+    }
+    return a;
+};
+
+SimileAjax.SortedArray.prototype.getFirst = function() {
+    return (this._a.length > 0) ? this._a[0] : null;
+};
+
+SimileAjax.SortedArray.prototype.getLast = function() {
+    return (this._a.length > 0) ? this._a[this._a.length - 1] : null;
+};
+
+/*==================================================
+ *  Event Index
+ *==================================================
+ */
+
+SimileAjax.EventIndex = function(unit) {
+    var eventIndex = this;
+    
+    this._unit = (unit != null) ? unit : SimileAjax.NativeDateUnit;
+    this._events = new SimileAjax.SortedArray(
+        function(event1, event2) {
+            return eventIndex._unit.compare(event1.getStart(), event2.getStart());
+        }
+    );
+    this._idToEvent = {};
+    this._indexed = true;
+};
+
+SimileAjax.EventIndex.prototype.getUnit = function() {
+    return this._unit;
+};
+
+SimileAjax.EventIndex.prototype.getEvent = function(id) {
+    return this._idToEvent[id];
+};
+
+SimileAjax.EventIndex.prototype.add = function(evt) {
+    this._events.add(evt);
+    this._idToEvent[evt.getID()] = evt;
+    this._indexed = false;
+};
+
+SimileAjax.EventIndex.prototype.removeAll = function() {
+    this._events.removeAll();
+    this._idToEvent = {};
+    this._indexed = false;
+};
+
+SimileAjax.EventIndex.prototype.getCount = function() {
+    return this._events.length();
+};
+
+SimileAjax.EventIndex.prototype.getIterator = function(startDate, endDate) {
+    if (!this._indexed) {
+        this._index();
+    }
+    return new SimileAjax.EventIndex._Iterator(this._events, startDate, endDate, this._unit);
+};
+
+SimileAjax.EventIndex.prototype.getReverseIterator = function(startDate, endDate) {
+    if (!this._indexed) {
+        this._index();
+    }
+    return new SimileAjax.EventIndex._ReverseIterator(this._events, startDate, endDate, this._unit);
+};
+
+SimileAjax.EventIndex.prototype.getAllIterator = function() {
+    return new SimileAjax.EventIndex._AllIterator(this._events);
+};
+
+SimileAjax.EventIndex.prototype.getEarliestDate = function() {
+    var evt = this._events.getFirst();
+    return (evt == null) ? null : evt.getStart();
+};
+
+SimileAjax.EventIndex.prototype.getLatestDate = function() {
+    var evt = this._events.getLast();
+    if (evt == null) {
+        return null;
+    }
+    
+    if (!this._indexed) {
+        this._index();
+    }
+    
+    var index = evt._earliestOverlapIndex;
+    var date = this._events.elementAt(index).getEnd();
+    for (var i = index + 1; i < this._events.length(); i++) {
+        date = this._unit.later(date, this._events.elementAt(i).getEnd());
+    }
+    
+    return date;
+};
+
+SimileAjax.EventIndex.prototype._index = function() {
+    /*
+     *  For each event, we want to find the earliest preceding
+     *  event that overlaps with it, if any.
+     */
+    
+    var l = this._events.length();
+    for (var i = 0; i < l; i++) {
+        var evt = this._events.elementAt(i);
+        evt._earliestOverlapIndex = i;
+    }
+    
+    var toIndex = 1;
+    for (var i = 0; i < l; i++) {
+        var evt = this._events.elementAt(i);
+        var end = evt.getEnd();
+        
+        toIndex = Math.max(toIndex, i + 1);
+        while (toIndex < l) {
+            var evt2 = this._events.elementAt(toIndex);
+            var start2 = evt2.getStart();
+            
+            if (this._unit.compare(start2, end) < 0) {
+                evt2._earliestOverlapIndex = i;
+                toIndex++;
+            } else {
+                break;
+            }
+        }
+    }
+    this._indexed = true;
+};
+
+SimileAjax.EventIndex._Iterator = function(events, startDate, endDate, unit) {
+    this._events = events;
+    this._startDate = startDate;
+    this._endDate = endDate;
+    this._unit = unit;
+    
+    this._currentIndex = events.find(function(evt) {
+        return unit.compare(evt.getStart(), startDate);
+    });
+    if (this._currentIndex - 1 >= 0) {
+        this._currentIndex = this._events.elementAt(this._currentIndex - 1)._earliestOverlapIndex;
+    }
+    this._currentIndex--;
+    
+    this._maxIndex = events.find(function(evt) {
+        return unit.compare(evt.getStart(), endDate);
+    });
+    
+    this._hasNext = false;
+    this._next = null;
+    this._findNext();
+};
+
+SimileAjax.EventIndex._Iterator.prototype = {
+    hasNext: function() { return this._hasNext; },
+    next: function() {
+        if (this._hasNext) {
+            var next = this._next;
+            this._findNext();
+            
+            return next;
+        } else {
+            return null;
+        }
+    },
+    _findNext: function() {
+        var unit = this._unit;
+        while ((++this._currentIndex) < this._maxIndex) {
+            var evt = this._events.elementAt(this._currentIndex);
+            if (unit.compare(evt.getStart(), this._endDate) < 0 &&
+                unit.compare(evt.getEnd(), this._startDate) > 0) {
+                
+                this._next = evt;
+                this._hasNext = true;
+                return;
+            }
+        }
+        this._next = null;
+        this._hasNext = false;
+    }
+};
+
+SimileAjax.EventIndex._ReverseIterator = function(events, startDate, endDate, unit) {
+    this._events = events;
+    this._startDate = startDate;
+    this._endDate = endDate;
+    this._unit = unit;
+    
+    this._minIndex = events.find(function(evt) {
+        return unit.compare(evt.getStart(), startDate);
+    });
+    if (this._minIndex - 1 >= 0) {
+        this._minIndex = this._events.elementAt(this._minIndex - 1)._earliestOverlapIndex;
+    }
+    
+    this._maxIndex = events.find(function(evt) {
+        return unit.compare(evt.getStart(), endDate);
+    });
+    
+    this._currentIndex = this._maxIndex;
+    this._hasNext = false;
+    this._next = null;
+    this._findNext();
+};
+
+SimileAjax.EventIndex._ReverseIterator.prototype = {
+    hasNext: function() { return this._hasNext; },
+    next: function() {
+        if (this._hasNext) {
+            var next = this._next;
+            this._findNext();
+            
+            return next;
+        } else {
+            return null;
+        }
+    },
+    _findNext: function() {
+        var unit = this._unit;
+        while ((--this._currentIndex) >= this._minIndex) {
+            var evt = this._events.elementAt(this._currentIndex);
+            if (unit.compare(evt.getStart(), this._endDate) < 0 &&
+                unit.compare(evt.getEnd(), this._startDate) > 0) {
+                
+                this._next = evt;
+                this._hasNext = true;
+                return;
+            }
+        }
+        this._next = null;
+        this._hasNext = false;
+    }
+};
+
+SimileAjax.EventIndex._AllIterator = function(events) {
+    this._events = events;
+    this._index = 0;
+};
+
+SimileAjax.EventIndex._AllIterator.prototype = {
+    hasNext: function() {
+        return this._index < this._events.length();
+    },
+    next: function() {
+        return this._index < this._events.length() ?
+            this._events.elementAt(this._index++) : null;
+    }
+};
+/*==================================================
+ *  Default Unit
+ *==================================================
+ */
+
+SimileAjax.NativeDateUnit = new Object();
+
+SimileAjax.NativeDateUnit.makeDefaultValue = function() {
+    return new Date();
+};
+
+SimileAjax.NativeDateUnit.cloneValue = function(v) {
+    return new Date(v.getTime());
+};
+
+SimileAjax.NativeDateUnit.getParser = function(format) {
+    if (typeof format == "string") {
+        format = format.toLowerCase();
+    }
+    return (format == "iso8601" || format == "iso 8601") ?
+        SimileAjax.DateTime.parseIso8601DateTime : 
+        SimileAjax.DateTime.parseGregorianDateTime;
+};
+
+SimileAjax.NativeDateUnit.parseFromObject = function(o) {
+    return SimileAjax.DateTime.parseGregorianDateTime(o);
+};
+
+SimileAjax.NativeDateUnit.toNumber = function(v) {
+    return v.getTime();
+};
+
+SimileAjax.NativeDateUnit.fromNumber = function(n) {
+    return new Date(n);
+};
+
+SimileAjax.NativeDateUnit.compare = function(v1, v2) {
+    var n1, n2;
+    if (typeof v1 == "object") {
+        n1 = v1.getTime();
+    } else {
+        n1 = Number(v1);
+    }
+    if (typeof v2 == "object") {
+        n2 = v2.getTime();
+    } else {
+        n2 = Number(v2);
+    }
+    
+    return n1 - n2;
+};
+
+SimileAjax.NativeDateUnit.earlier = function(v1, v2) {
+    return SimileAjax.NativeDateUnit.compare(v1, v2) < 0 ? v1 : v2;
+};
+
+SimileAjax.NativeDateUnit.later = function(v1, v2) {
+    return SimileAjax.NativeDateUnit.compare(v1, v2) > 0 ? v1 : v2;
+};
+
+SimileAjax.NativeDateUnit.change = function(v, n) {
+    return new Date(v.getTime() + n);
+};
+
+/*==================================================
+ *  General, miscellaneous SimileAjax stuff
+ *==================================================
+ */
+
+SimileAjax.ListenerQueue = function(wildcardHandlerName) {
+    this._listeners = [];
+    this._wildcardHandlerName = wildcardHandlerName;
+};
+
+SimileAjax.ListenerQueue.prototype.add = function(listener) {
+    this._listeners.push(listener);
+};
+
+SimileAjax.ListenerQueue.prototype.remove = function(listener) {
+    var listeners = this._listeners;
+    for (var i = 0; i < listeners.length; i++) {
+        if (listeners[i] == listener) {
+            listeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+SimileAjax.ListenerQueue.prototype.fire = function(handlerName, args) {
+    var listeners = [].concat(this._listeners);
+    for (var i = 0; i < listeners.length; i++) {
+        var listener = listeners[i];
+        if (handlerName in listener) {
+            try {
+                listener[handlerName].apply(listener, args);
+            } catch (e) {
+                SimileAjax.Debug.exception("Error firing event of name " + handlerName, e);
+            }
+        } else if (this._wildcardHandlerName != null &&
+            this._wildcardHandlerName in listener) {
+            try {
+                listener[this._wildcardHandlerName].apply(listener, [ handlerName ]);
+            } catch (e) {
+                SimileAjax.Debug.exception("Error firing event of name " + handlerName + " to wildcard handler", e);
+            }
+        }
+    }
+};
+
+/*======================================================================
+ *  History
+ *
+ *  This is a singleton that keeps track of undoable user actions and 
+ *  performs undos and redos in response to the browser's Back and 
+ *  Forward buttons.
+ *
+ *  Call addAction(action) to register an undoable user action. action
+ *  must have 4 fields:
+ *
+ *      perform: an argument-less function that carries out the action
+ *      undo:    an argument-less function that undos the action
+ *      label:   a short, user-friendly string describing the action
+ *      uiLayer: the UI layer on which the action takes place
+ *
+ *  By default, the history keeps track of upto 10 actions. You can 
+ *  configure this behavior by setting 
+ *      SimileAjax.History.maxHistoryLength
+ *  to a different number.
+ *
+ *  An iframe is inserted into the document's body element to track 
+ *  onload events.
+ *======================================================================
+ */
+ 
+SimileAjax.History = {
+    maxHistoryLength:       10,
+    historyFile:            "__history__.html",
+    enabled:               true,
+    
+    _initialized:           false,
+    _listeners:             new SimileAjax.ListenerQueue(),
+    
+    _actions:               [],
+    _baseIndex:             0,
+    _currentIndex:          0,
+    
+    _plainDocumentTitle:    document.title
+};
+
+SimileAjax.History.formatHistoryEntryTitle = function(actionLabel) {
+    return SimileAjax.History._plainDocumentTitle + " {" + actionLabel + "}";
+};
+
+SimileAjax.History.initialize = function() {
+    if (SimileAjax.History._initialized) {
+        return;
+    }
+    
+    if (SimileAjax.History.enabled) {
+        var iframe = document.createElement("iframe");
+        iframe.id = "simile-ajax-history";
+        iframe.style.position = "absolute";
+        iframe.style.width = "10px";
+        iframe.style.height = "10px";
+        iframe.style.top = "0px";
+        iframe.style.left = "0px";
+        iframe.style.visibility = "hidden";
+        iframe.src = SimileAjax.History.historyFile + "?0";
+        
+        document.body.appendChild(iframe);
+        SimileAjax.DOM.registerEvent(iframe, "load", SimileAjax.History._handleIFrameOnLoad);
+        
+        SimileAjax.History._iframe = iframe;
+    }
+    SimileAjax.History._initialized = true;
+};
+
+SimileAjax.History.addListener = function(listener) {
+    SimileAjax.History.initialize();
+    
+    SimileAjax.History._listeners.add(listener);
+};
+
+SimileAjax.History.removeListener = function(listener) {
+    SimileAjax.History.initialize();
+    
+    SimileAjax.History._listeners.remove(listener);
+};
+
+SimileAjax.History.addAction = function(action) {
+    SimileAjax.History.initialize();
+    
+    SimileAjax.History._listeners.fire("onBeforePerform", [ action ]);
+    window.setTimeout(function() {
+        try {
+            action.perform();
+            SimileAjax.History._listeners.fire("onAfterPerform", [ action ]);
+                
+            if (SimileAjax.History.enabled) {
+                SimileAjax.History._actions = SimileAjax.History._actions.slice(
+                    0, SimileAjax.History._currentIndex - SimileAjax.History._baseIndex);
+                    
+                SimileAjax.History._actions.push(action);
+                SimileAjax.History._currentIndex++;
+                
+                var diff = SimileAjax.History._actions.length - SimileAjax.History.maxHistoryLength;
+                if (diff > 0) {
+                    SimileAjax.History._actions = SimileAjax.History._actions.slice(diff);
+                    SimileAjax.History._baseIndex += diff;
+                }
+                
+                try {
+                    SimileAjax.History._iframe.contentWindow.location.search = 
+                        "?" + SimileAjax.History._currentIndex;
+                } catch (e) {
+                    /*
+                     *  We can't modify location.search most probably because it's a file:// url.
+                     *  We'll just going to modify the document's title.
+                     */
+                    var title = SimileAjax.History.formatHistoryEntryTitle(action.label);
+                    document.title = title;
+                }
+            }
+        } catch (e) {
+            SimileAjax.Debug.exception(e, "Error adding action {" + action.label + "} to history");
+        }
+    }, 0);
+};
+
+SimileAjax.History.addLengthyAction = function(perform, undo, label) {
+    SimileAjax.History.addAction({
+        perform:    perform,
+        undo:       undo,
+        label:      label,
+        uiLayer:    SimileAjax.WindowManager.getBaseLayer(),
+        lengthy:    true
+    });
+};
+
+SimileAjax.History._handleIFrameOnLoad = function() {
+    /*
+     *  This function is invoked when the user herself
+     *  navigates backward or forward. We need to adjust
+     *  the application's state accordingly.
+     */
+    
+    try {
+        var q = SimileAjax.History._iframe.contentWindow.location.search;
+        var c = (q.length == 0) ? 0 : Math.max(0, parseInt(q.substr(1)));
+        
+        var finishUp = function() {
+            var diff = c - SimileAjax.History._currentIndex;
+            SimileAjax.History._currentIndex += diff;
+            SimileAjax.History._baseIndex += diff;
+                
+            SimileAjax.History._iframe.contentWindow.location.search = "?" + c;
+        };
+        
+        if (c < SimileAjax.History._currentIndex) { // need to undo
+            SimileAjax.History._listeners.fire("onBeforeUndoSeveral", []);
+            window.setTimeout(function() {
+                while (SimileAjax.History._currentIndex > c && 
+                       SimileAjax.History._currentIndex > SimileAjax.History._baseIndex) {
+                       
+                    SimileAjax.History._currentIndex--;
+                    
+                    var action = SimileAjax.History._actions[SimileAjax.History._currentIndex - SimileAjax.History._baseIndex];
+                    
+                    try {
+                        action.undo();
+                    } catch (e) {
+                        SimileAjax.Debug.exception(e, "History: Failed to undo action {" + action.label + "}");
+                    }
+                }
+                
+                SimileAjax.History._listeners.fire("onAfterUndoSeveral", []);
+                finishUp();
+            }, 0);
+        } else if (c > SimileAjax.History._currentIndex) { // need to redo
+            SimileAjax.History._listeners.fire("onBeforeRedoSeveral", []);
+            window.setTimeout(function() {
+                while (SimileAjax.History._currentIndex < c && 
+                       SimileAjax.History._currentIndex - SimileAjax.History._baseIndex < SimileAjax.History._actions.length) {
+                       
+                    var action = SimileAjax.History._actions[SimileAjax.History._currentIndex - SimileAjax.History._baseIndex];
+                    
+                    try {
+                        action.perform();
+                    } catch (e) {
+                        SimileAjax.Debug.exception(e, "History: Failed to redo action {" + action.label + "}");
+                    }
+                    
+                    SimileAjax.History._currentIndex++;
+                }
+                
+                SimileAjax.History._listeners.fire("onAfterRedoSeveral", []);
+                finishUp();
+            }, 0);
+        } else {
+            var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex - 1;
+            var title = (index >= 0 && index < SimileAjax.History._actions.length) ?
+                SimileAjax.History.formatHistoryEntryTitle(SimileAjax.History._actions[index].label) :
+                SimileAjax.History._plainDocumentTitle;
+                
+            SimileAjax.History._iframe.contentWindow.document.title = title;
+            document.title = title;
+        }
+    } catch (e) {
+        // silent
+    }
+};
+
+SimileAjax.History.getNextUndoAction = function() {
+    try {
+        var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex - 1;
+        return SimileAjax.History._actions[index];
+    } catch (e) {
+        return null;
+    }
+};
+
+SimileAjax.History.getNextRedoAction = function() {
+    try {
+        var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex;
+        return SimileAjax.History._actions[index];
+    } catch (e) {
+        return null;
+    }
+};
+/**
+ * @fileOverview UI layers and window-wide dragging
+ * @name SimileAjax.WindowManager
+ */
+
+/**
+ *  This is a singleton that keeps track of UI layers (modal and 
+ *  modeless) and enables/disables UI elements based on which layers
+ *  they belong to. It also provides window-wide dragging 
+ *  implementation.
+ */ 
+SimileAjax.WindowManager = {
+    _initialized:       false,
+    _listeners:         [],
+    
+    _draggedElement:                null,
+    _draggedElementCallback:        null,
+    _dropTargetHighlightElement:    null,
+    _lastCoords:                    null,
+    _ghostCoords:                   null,
+    _draggingMode:                  "",
+    _dragging:                      false,
+    
+    _layers:            []
+};
+
+SimileAjax.WindowManager.initialize = function() {
+    if (SimileAjax.WindowManager._initialized) {
+        return;
+    }
+    
+    SimileAjax.DOM.registerEvent(document.body, "mousedown", SimileAjax.WindowManager._onBodyMouseDown);
+    SimileAjax.DOM.registerEvent(document.body, "mousemove", SimileAjax.WindowManager._onBodyMouseMove);
+    SimileAjax.DOM.registerEvent(document.body, "mouseup",   SimileAjax.WindowManager._onBodyMouseUp);
+    SimileAjax.DOM.registerEvent(document, "keydown",       SimileAjax.WindowManager._onBodyKeyDown);
+    SimileAjax.DOM.registerEvent(document, "keyup",         SimileAjax.WindowManager._onBodyKeyUp);
+    
+    SimileAjax.WindowManager._layers.push({index: 0});
+    
+    SimileAjax.WindowManager._historyListener = {
+        onBeforeUndoSeveral:    function() {},
+        onAfterUndoSeveral:     function() {},
+        onBeforeUndo:           function() {},
+        onAfterUndo:            function() {},
+        
+        onBeforeRedoSeveral:    function() {},
+        onAfterRedoSeveral:     function() {},
+        onBeforeRedo:           function() {},
+        onAfterRedo:            function() {}
+    };
+    SimileAjax.History.addListener(SimileAjax.WindowManager._historyListener);
+    
+    SimileAjax.WindowManager._initialized = true;
+};
+
+SimileAjax.WindowManager.getBaseLayer = function() {
+    SimileAjax.WindowManager.initialize();
+    return SimileAjax.WindowManager._layers[0];
+};
+
+SimileAjax.WindowManager.getHighestLayer = function() {
+    SimileAjax.WindowManager.initialize();
+    return SimileAjax.WindowManager._layers[SimileAjax.WindowManager._layers.length - 1];
+};
+
+SimileAjax.WindowManager.registerEventWithObject = function(elmt, eventName, obj, handlerName, layer) {
+    SimileAjax.WindowManager.registerEvent(
+        elmt, 
+        eventName, 
+        function(elmt2, evt, target) {
+            return obj[handlerName].call(obj, elmt2, evt, target);
+        },
+        layer
+    );
+};
+
+SimileAjax.WindowManager.registerEvent = function(elmt, eventName, handler, layer) {
+    if (layer == null) {
+        layer = SimileAjax.WindowManager.getHighestLayer();
+    }
+    
+    var handler2 = function(elmt, evt, target) {
+        if (SimileAjax.WindowManager._canProcessEventAtLayer(layer)) {
+            SimileAjax.WindowManager._popToLayer(layer.index);
+            try {
+                handler(elmt, evt, target);
+            } catch (e) {
+                SimileAjax.Debug.exception(e);
+            }
+        }
+        SimileAjax.DOM.cancelEvent(evt);
+        return false;
+    }
+    
+    SimileAjax.DOM.registerEvent(elmt, eventName, handler2);
+};
+
+SimileAjax.WindowManager.pushLayer = function(f, ephemeral, elmt) {
+    var layer = { onPop: f, index: SimileAjax.WindowManager._layers.length, ephemeral: (ephemeral), elmt: elmt };
+    SimileAjax.WindowManager._layers.push(layer);
+    
+    return layer;
+};
+
+SimileAjax.WindowManager.popLayer = function(layer) {
+    for (var i = 1; i < SimileAjax.WindowManager._layers.length; i++) {
+        if (SimileAjax.WindowManager._layers[i] == layer) {
+            SimileAjax.WindowManager._popToLayer(i - 1);
+            break;
+        }
+    }
+};
+
+SimileAjax.WindowManager.popAllLayers = function() {
+    SimileAjax.WindowManager._popToLayer(0);
+};
+
+SimileAjax.WindowManager.registerForDragging = function(elmt, callback, layer) {
+    SimileAjax.WindowManager.registerEvent(
+        elmt, 
+        "mousedown", 
+        function(elmt, evt, target) {
+            SimileAjax.WindowManager._handleMouseDown(elmt, evt, callback);
+        }, 
+        layer
+    );
+};
+
+SimileAjax.WindowManager._popToLayer = function(level) {
+    while (level+1 < SimileAjax.WindowManager._layers.length) {
+        try {
+            var layer = SimileAjax.WindowManager._layers.pop();
+            if (layer.onPop != null) {
+                layer.onPop();
+            }
+        } catch (e) {
+        }
+    }
+};
+
+SimileAjax.WindowManager._canProcessEventAtLayer = function(layer) {
+    if (layer.index == (SimileAjax.WindowManager._layers.length - 1)) {
+        return true;
+    }
+    for (var i = layer.index + 1; i < SimileAjax.WindowManager._layers.length; i++) {
+        if (!SimileAjax.WindowManager._layers[i].ephemeral) {
+            return false;
+        }
+    }
+    return true;
+};
+
+SimileAjax.WindowManager.cancelPopups = function(evt) {
+    var evtCoords = (evt) ? SimileAjax.DOM.getEventPageCoordinates(evt) : { x: -1, y: -1 };
+    
+    var i = SimileAjax.WindowManager._layers.length - 1;
+    while (i > 0 && SimileAjax.WindowManager._layers[i].ephemeral) {
+        var layer = SimileAjax.WindowManager._layers[i];
+        if (layer.elmt != null) { // if event falls within main element of layer then don't cancel
+            var elmt = layer.elmt;
+            var elmtCoords = SimileAjax.DOM.getPageCoordinates(elmt);
+            if (evtCoords.x >= elmtCoords.left && evtCoords.x < (elmtCoords.left + elmt.offsetWidth) &&
+                evtCoords.y >= elmtCoords.top && evtCoords.y < (elmtCoords.top + elmt.offsetHeight)) {
+                break;
+            }
+        }
+        i--;
+    }
+    SimileAjax.WindowManager._popToLayer(i);
+};
+
+SimileAjax.WindowManager._onBodyMouseDown = function(elmt, evt, target) {
+    if (!("eventPhase" in evt) || evt.eventPhase == evt.BUBBLING_PHASE) {
+        SimileAjax.WindowManager.cancelPopups(evt);
+    }
+};
+
+SimileAjax.WindowManager._handleMouseDown = function(elmt, evt, callback) {
+    SimileAjax.WindowManager._draggedElement = elmt;
+    SimileAjax.WindowManager._draggedElementCallback = callback;
+    SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
+        
+    SimileAjax.DOM.cancelEvent(evt);
+    return false;
+};
+
+SimileAjax.WindowManager._onBodyKeyDown = function(elmt, evt, target) {
+    if (SimileAjax.WindowManager._dragging) {
+        if (evt.keyCode == 27) { // esc
+            SimileAjax.WindowManager._cancelDragging();
+        } else if ((evt.keyCode == 17 || evt.keyCode == 16) && SimileAjax.WindowManager._draggingMode != "copy") {
+            SimileAjax.WindowManager._draggingMode = "copy";
+            
+            var img = SimileAjax.Graphics.createTranslucentImage(SimileAjax.urlPrefix + "images/copy.png");
+            img.style.position = "absolute";
+            img.style.left = (SimileAjax.WindowManager._ghostCoords.left - 16) + "px";
+            img.style.top = (SimileAjax.WindowManager._ghostCoords.top) + "px";
+            document.body.appendChild(img);
+            
+            SimileAjax.WindowManager._draggingModeIndicatorElmt = img;
+        }
+    }
+};
+
+SimileAjax.WindowManager._onBodyKeyUp = function(elmt, evt, target) {
+    if (SimileAjax.WindowManager._dragging) {
+        if (evt.keyCode == 17 || evt.keyCode == 16) {
+            SimileAjax.WindowManager._draggingMode = "";
+            if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
+                document.body.removeChild(SimileAjax.WindowManager._draggingModeIndicatorElmt);
+                SimileAjax.WindowManager._draggingModeIndicatorElmt = null;
+            }
+        }
+    }
+};
+
+SimileAjax.WindowManager._onBodyMouseMove = function(elmt, evt, target) {
+    if (SimileAjax.WindowManager._draggedElement != null) {
+        var callback = SimileAjax.WindowManager._draggedElementCallback;
+        
+        var lastCoords = SimileAjax.WindowManager._lastCoords;
+        var diffX = evt.clientX - lastCoords.x;
+        var diffY = evt.clientY - lastCoords.y;
+        
+        if (!SimileAjax.WindowManager._dragging) {
+            if (Math.abs(diffX) > 5 || Math.abs(diffY) > 5) {
+                try {
+                    if ("onDragStart" in callback) {
+                        callback.onDragStart();
+                    }
+                    
+                    if ("ghost" in callback && callback.ghost) {
+                        var draggedElmt = SimileAjax.WindowManager._draggedElement;
+                        
+                        SimileAjax.WindowManager._ghostCoords = SimileAjax.DOM.getPageCoordinates(draggedElmt);
+                        SimileAjax.WindowManager._ghostCoords.left += diffX;
+                        SimileAjax.WindowManager._ghostCoords.top += diffY;
+                        
+                        var ghostElmt = draggedElmt.cloneNode(true);
+                        ghostElmt.style.position = "absolute";
+                        ghostElmt.style.left = SimileAjax.WindowManager._ghostCoords.left + "px";
+                        ghostElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
+                        ghostElmt.style.zIndex = 1000;
+                        SimileAjax.Graphics.setOpacity(ghostElmt, 50);
+                        
+                        document.body.appendChild(ghostElmt);
+                        callback._ghostElmt = ghostElmt;
+                    }
+                    
+                    SimileAjax.WindowManager._dragging = true;
+                    SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
+                    
+                    document.body.focus();
+                } catch (e) {
+                    SimileAjax.Debug.exception("WindowManager: Error handling mouse down", e);
+                    SimileAjax.WindowManager._cancelDragging();
+                }
+            }
+        } else {
+            try {
+                SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
+                
+                if ("onDragBy" in callback) {
+                    callback.onDragBy(diffX, diffY);
+                }
+                
+                if ("_ghostElmt" in callback) {
+                    var ghostElmt = callback._ghostElmt;
+                    
+                    SimileAjax.WindowManager._ghostCoords.left += diffX;
+                    SimileAjax.WindowManager._ghostCoords.top += diffY;
+                    
+                    ghostElmt.style.left = SimileAjax.WindowManager._ghostCoords.left + "px";
+                    ghostElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
+                    if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
+                        var indicatorElmt = SimileAjax.WindowManager._draggingModeIndicatorElmt;
+                        
+                        indicatorElmt.style.left = (SimileAjax.WindowManager._ghostCoords.left - 16) + "px";
+                        indicatorElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
+                    }
+                    
+                    if ("droppable" in callback && callback.droppable) {
+                        var coords = SimileAjax.DOM.getEventPageCoordinates(evt);
+                        var target = SimileAjax.DOM.hittest(
+                            coords.x, coords.y, 
+                            [   SimileAjax.WindowManager._ghostElmt, 
+                                SimileAjax.WindowManager._dropTargetHighlightElement 
+                            ]
+                        );
+                        target = SimileAjax.WindowManager._findDropTarget(target);
+                        
+                        if (target != SimileAjax.WindowManager._potentialDropTarget) {
+                            if (SimileAjax.WindowManager._dropTargetHighlightElement != null) {
+                                document.body.removeChild(SimileAjax.WindowManager._dropTargetHighlightElement);
+                                
+                                SimileAjax.WindowManager._dropTargetHighlightElement = null;
+                                SimileAjax.WindowManager._potentialDropTarget = null;
+                            }
+
+                            var droppable = false;
+                            if (target != null) {
+                                if ((!("canDropOn" in callback) || callback.canDropOn(target)) &&
+                                    (!("canDrop" in target) || target.canDrop(SimileAjax.WindowManager._draggedElement))) {
+                                    
+                                    droppable = true;
+                                }
+                            }
+                            
+                            if (droppable) {
+                                var border = 4;
+                                var targetCoords = SimileAjax.DOM.getPageCoordinates(target);
+                                var highlight = document.createElement("div");
+                                highlight.style.border = border + "px solid yellow";
+                                highlight.style.backgroundColor = "yellow";
+                                highlight.style.position = "absolute";
+                                highlight.style.left = targetCoords.left + "px";
+                                highlight.style.top = targetCoords.top + "px";
+                                highlight.style.width = (target.offsetWidth - border * 2) + "px";
+                                highlight.style.height = (target.offsetHeight - border * 2) + "px";
+                                SimileAjax.Graphics.setOpacity(highlight, 30);
+                                document.body.appendChild(highlight);
+                                
+                                SimileAjax.WindowManager._potentialDropTarget = target;
+                                SimileAjax.WindowManager._dropTargetHighlightElement = highlight;
+                            }
+                        }
+                    }
+                }
+            } catch (e) {
+                SimileAjax.Debug.exception("WindowManager: Error handling mouse move", e);
+                SimileAjax.WindowManager._cancelDragging();
+            }
+        }
+        
+        SimileAjax.DOM.cancelEvent(evt);
+        return false;
+    }
+};
+
+SimileAjax.WindowManager._onBodyMouseUp = function(elmt, evt, target) {
+    if (SimileAjax.WindowManager._draggedElement != null) {
+        try {
+            if (SimileAjax.WindowManager._dragging) {
+                var callback = SimileAjax.WindowManager._draggedElementCallback;
+                if ("onDragEnd" in callback) {
+                    callback.onDragEnd();
+                }
+                if ("droppable" in callback && callback.droppable) {
+                    var dropped = false;
+                    
+                    var target = SimileAjax.WindowManager._potentialDropTarget;
+                    if (target != null) {
+                        if ((!("canDropOn" in callback) || callback.canDropOn(target)) &&
+                            (!("canDrop" in target) || target.canDrop(SimileAjax.WindowManager._draggedElement))) {
+                            
+                            if ("onDropOn" in callback) {
+                                callback.onDropOn(target);
+                            }
+                            target.ondrop(SimileAjax.WindowManager._draggedElement, SimileAjax.WindowManager._draggingMode);
+                            
+                            dropped = true;
+                        }
+                    }
+                    
+                    if (!dropped) {
+                        // TODO: do holywood explosion here
+                    }
+                }
+            }
+        } finally {
+            SimileAjax.WindowManager._cancelDragging();
+        }
+        
+        SimileAjax.DOM.cancelEvent(evt);
+        return false;
+    }
+};
+
+SimileAjax.WindowManager._cancelDragging = function() {
+    var callback = SimileAjax.WindowManager._draggedElementCallback;
+    if ("_ghostElmt" in callback) {
+        var ghostElmt = callback._ghostElmt;
+        document.body.removeChild(ghostElmt);
+        
+        delete callback._ghostElmt;
+    }
+    if (SimileAjax.WindowManager._dropTargetHighlightElement != null) {
+        document.body.removeChild(SimileAjax.WindowManager._dropTargetHighlightElement);
+        SimileAjax.WindowManager._dropTargetHighlightElement = null;
+    }
+    if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
+        document.body.removeChild(SimileAjax.WindowManager._draggingModeIndicatorElmt);
+        SimileAjax.WindowManager._draggingModeIndicatorElmt = null;
+    }
+    
+    SimileAjax.WindowManager._draggedElement = null;
+    SimileAjax.WindowManager._draggedElementCallback = null;
+    SimileAjax.WindowManager._potentialDropTarget = null;
+    SimileAjax.WindowManager._dropTargetHighlightElement = null;
+    SimileAjax.WindowManager._lastCoords = null;
+    SimileAjax.WindowManager._ghostCoords = null;
+    SimileAjax.WindowManager._draggingMode = "";
+    SimileAjax.WindowManager._dragging = false;
+};
+
+SimileAjax.WindowManager._findDropTarget = function(elmt) {
+    while (elmt != null) {
+        if ("ondrop" in elmt && (typeof elmt.ondrop) == "function") {
+            break;
+        }
+        elmt = elmt.parentNode;
+    }
+    return elmt;
+};
+/*==================================================
+ *
+ *              Timeline API
+ *              ------------
+ *
+ * This file will load all the Javascript files
+ * necessary to make the standard timeline work.
+ * It also detects the default locale.
+ *
+ * To run Timeline directly from the www.simile-widgets.org server
+ * include this fragment in your HTML file as follows:
+ *
+ *   <script src="http://api.simile-widgets.org/timeline/2.3.1/timeline-api.js" 
+ *    type="text/javascript"></script>
+ *
+ *
+ * To host the Timeline files on your own server:
+ *
+ * 1) Install the Timeline files onto your webserver using
+ *    the minimal distribution "timeline_<version>_minimal.(zip|tar.gz)" found at
+ *
+ *        http://code.google.com/p/simile-widgets/downloads/list
+ * 
+ * 2) Set the following global js variable used to send parameters to this script:
+ *     var Timeline_ajax_url  -- URL for simile-ajax-api.js
+ *     var Timeline_urlPrefix -- URL for the *directory* that contains timeline-api.js 
+ *                               on your web site (including the trailing slash!)
+ *      
+ * eg your HTML page would include
+ *
+ *   <script>
+ *     var Timeline_ajax_url="http://YOUR_SERVER/apis/timeline/ajax/simile-ajax-api.js";
+ *     var Timeline_urlPrefix='http://YOUR_SERVER/apis/timeline/';       
+ *   </script>
+ *   <script src="http://YOUR_SERVER/javascripts/timeline/timeline-api.js"    
+ *     type="text/javascript">
+ *   </script>
+ *
+ * SCRIPT PARAMETERS
+ *
+ * This script auto-magically figures out locale and has defaults for other parameters 
+ * To set parameters explicity, set js global variable Timeline_parameters or include as
+ * parameters on the url using GET style. Eg the two next lines pass the same parameters:
+ *     Timeline_parameters='bundle=true';                    // pass parameter via js variable
+ *     <script src="http://....timeline-api.js?bundle=true"  // pass parameter via url
+ * 
+ * Parameters 
+ *   timeline-use-local-resources -- 
+ *   bundle -- true: use the single js bundle file; false: load individual files (for debugging)
+ *   locales -- 
+ *   defaultLocale --
+ *   forceLocale -- force locale to be a particular value--used for debugging. Normally locale is determined
+ *                  by browser's and server's locale settings.
+ * 
+ * DEBUGGING
+ *
+ * If you have a problem with Timeline, the first step is to use the unbundled Javascript files. To do so:
+ * To use the unbundled Timeline and Ajax libraries
+ * Change
+ *   <script src="http://api.simile-widgets.org/timeline/2.3.1/api/timeline-api.js?bundle=true" type="text/javascript"></script>
+ * To
+ *   <script>var Timeline_ajax_url = "http://api.simile-widgets.org/ajax/2.2.1/simile-ajax-api.js?bundle=false"</script>
+ *   <script src="http://api.simile-widgets.org/timeline/2.3.1/api/timeline-api.js?bundle=false" type="text/javascript"></script>
+ * 
+ * Note that the Ajax version is usually NOT the same as the Timeline version.
+ * See variable simile_ajax_ver below for the current version
+ *
+ *================================================== 
+ */
+
+(function() {
+    
+    var simile_ajax_ver = "2.2.1"; // ===========>>>  current Simile-Ajax version
+  
+    var useLocalResources = false;
+    if (document.location.search.length > 0) {
+        var params = document.location.search.substr(1).split("&");
+        for (var i = 0; i < params.length; i++) {
+            if (params[i] == "timeline-use-local-resources") {
+                useLocalResources = true;
+            }
+        }
+    };
+    
+    var loadMe = function() {
+        if ("Timeline" in window) {
+            return;
+        }
+        
+        window.Timeline = new Object();
+        window.Timeline.DateTime = window.SimileAjax.DateTime; // for backward compatibility
+    
+        var bundle = false;
+        var javascriptFiles = [
+            "timeline.js",
+            "band.js",
+            "themes.js",
+            "ethers.js",
+            "ether-painters.js",
+            "event-utils.js",
+            "labellers.js",
+            "sources.js",
+            "original-painter.js",
+            "detailed-painter.js",
+            "overview-painter.js",
+            "compact-painter.js",
+            "decorators.js",
+            "units.js"
+        ];
+        var cssFiles = [
+            "timeline.css",
+            "ethers.css",
+            "events.css"
+        ];
+        
+        var localizedJavascriptFiles = [
+            "timeline.js",
+            "labellers.js"
+        ];
+        var localizedCssFiles = [
+        ];
+        
+        // ISO-639 language codes, ISO-3166 country codes (2 characters)
+        var supportedLocales = [
+            "cs",       // Czech
+            "de",       // German
+            "en",       // English
+            "es",       // Spanish
+            "fr",       // French
+            "it",       // Italian
+            "nl",       // Dutch (The Netherlands)
+            "ru",       // Russian
+            "se",       // Swedish
+            "tr",       // Turkish
+            "vi",       // Vietnamese
+            "zh"        // Chinese
+        ];
+        
+        try {
+            var desiredLocales = [ "en" ],
+                defaultServerLocale = "en",
+                forceLocale = null;
+            
+            var parseURLParameters = function(parameters) {
+                var params = parameters.split("&");
+                for (var p = 0; p < params.length; p++) {
+                    var pair = params[p].split("=");
+                    if (pair[0] == "locales") {
+                        desiredLocales = desiredLocales.concat(pair[1].split(","));
+                    } else if (pair[0] == "defaultLocale") {
+                        defaultServerLocale = pair[1];
+                    } else if (pair[0] == "forceLocale") {
+                        forceLocale = pair[1];
+                        desiredLocales = desiredLocales.concat(pair[1].split(","));                        
+                    } else if (pair[0] == "bundle") {
+                        bundle = pair[1] != "false";
+                    }
+                }
+            };
+            
+            (function() {
+                if (typeof Timeline_urlPrefix == "string") {
+                    Timeline.urlPrefix = Timeline_urlPrefix;
+                    if (typeof Timeline_parameters == "string") {
+                        parseURLParameters(Timeline_parameters);
+                    }
+                } else {
+                    var heads = document.documentElement.getElementsByTagName("head");
+                    for (var h = 0; h < heads.length; h++) {
+                        var scripts = heads[h].getElementsByTagName("script");
+                        for (var s = 0; s < scripts.length; s++) {
+                            var url = scripts[s].src;
+                            var i = url.indexOf("timeplot-complete.js");
+                            if (i >= 0) {
+                                Timeline.urlPrefix = url.substr(0, i);
+                                var q = url.indexOf("?");
+                                if (q > 0) {
+                                    parseURLParameters(url.substr(q + 1));
+                                }
+                                return;
+                            }
+                        }
+                    }
+                    throw new Error("Failed to derive URL prefix for Timeline API code files");
+                }
+            })();
+            
+            var includeJavascriptFiles = function(urlPrefix, filenames) {
+                SimileAjax.includeJavascriptFiles(document, urlPrefix, filenames);
+            }
+            var includeCssFiles = function(urlPrefix, filenames) {
+                SimileAjax.includeCssFiles(document, urlPrefix, filenames);
+            }
+            
+            /*
+             *  Include non-localized files
+             */
+            if (bundle) {
+                includeJavascriptFiles(Timeline.urlPrefix, [ "timeline-bundle.js" ]);
+                includeCssFiles(Timeline.urlPrefix, [ "timeline-bundle.css" ]);
+            } else {
+                includeJavascriptFiles(Timeline.urlPrefix + "scripts/", javascriptFiles);
+                includeCssFiles(Timeline.urlPrefix + "styles/", cssFiles);
+            }
+            
+            /*
+             *  Include localized files
+             */
+            var loadLocale = [];
+            loadLocale[defaultServerLocale] = true;
+            
+            var tryExactLocale = function(locale) {
+                for (var l = 0; l < supportedLocales.length; l++) {
+                    if (locale == supportedLocales[l]) {
+                        loadLocale[locale] = true;
+                        return true;
+                    }
+                }
+                return false;
+            }
+            var tryLocale = function(locale) {
+                if (tryExactLocale(locale)) {
+                    return locale;
+                }
+                
+                var dash = locale.indexOf("-");
+                if (dash > 0 && tryExactLocale(locale.substr(0, dash))) {
+                    return locale.substr(0, dash);
+                }
+                
+                return null;
+            }
+            
+            for (var l = 0; l < desiredLocales.length; l++) {
+                tryLocale(desiredLocales[l]);
+            }
+            
+            var defaultClientLocale = defaultServerLocale;
+            var defaultClientLocales = ("language" in navigator ? navigator.language : navigator.browserLanguage).split(";");
+            for (var l = 0; l < defaultClientLocales.length; l++) {
+                var locale = tryLocale(defaultClientLocales[l]);
+                if (locale != null) {
+                    defaultClientLocale = locale;
+                    break;
+                }
+            }
+            
+            for (var l = 0; l < supportedLocales.length; l++) {
+                var locale = supportedLocales[l];
+                if (loadLocale[locale]) {
+                    includeJavascriptFiles(Timeline.urlPrefix + "scripts/l10n/" + locale + "/", localizedJavascriptFiles);
+                    includeCssFiles(Timeline.urlPrefix + "styles/l10n/" + locale + "/", localizedCssFiles);
+                }
+            }
+            
+            if (forceLocale == null) {
+              Timeline.serverLocale = defaultServerLocale;
+              Timeline.clientLocale = defaultClientLocale;
+            } else {
+              Timeline.serverLocale = forceLocale;
+              Timeline.clientLocale = forceLocale;
+            }            	
+        } catch (e) {
+            alert(e);
+        }
+    };
+    
+    /*
+     *  Load SimileAjax if it's not already loaded
+     */
+    if (typeof SimileAjax == "undefined") {
+        window.SimileAjax_onLoad = loadMe;
+        
+        var url = useLocalResources ?
+            "http://127.0.0.1:9999/ajax/api/simile-ajax-api.js?bundle=false" :
+            "http://api.simile-widgets.org/ajax/" + simile_ajax_ver + "/simile-ajax-api.js";
+        if (typeof Timeline_ajax_url == "string") {
+           url = Timeline_ajax_url;
+        }
+        var createScriptElement = function() {
+            var script = document.createElement("script");
+            script.type = "text/javascript";
+            script.language = "JavaScript";
+            script.src = url;
+            document.getElementsByTagName("head")[0].appendChild(script);
+        }
+        if (document.body == null) {
+            try {
+                document.write("<script src='" + url + "' type='text/javascript'></script>");
+            } catch (e) {
+                createScriptElement();
+            }
+        } else {
+            createScriptElement();
+        }
+    } else {
+        loadMe();
+    }
+})();
+/*=================================================
+ *
+ * Coding standards:
+ *
+ * We aim towards Douglas Crockford's Javascript conventions.
+ * See:  http://javascript.crockford.com/code.html
+ * See also: http://www.crockford.com/javascript/javascript.html
+ *
+ * That said, this JS code was written before some recent JS
+ * support libraries became widely used or available.
+ * In particular, the _ character is used to indicate a class function or
+ * variable that should be considered private to the class.
+ *
+ * The code mostly uses accessor methods for getting/setting the private
+ * class variables.
+ *
+ * Over time, we'd like to formalize the convention by using support libraries
+ * which enforce privacy in objects.
+ *
+ * We also want to use jslint:  http://www.jslint.com/
+ *
+ *
+ *==================================================
+ */
+
+
+
+/*==================================================
+ *  Timeline VERSION     
+ *==================================================
+ */
+// Note: version is also stored in the build.xml file
+Timeline.version = 'pre 2.4.0';  // use format 'pre 1.2.3' for trunk versions 
+Timeline.ajax_lib_version = SimileAjax.version;
+Timeline.display_version = Timeline.version + ' (with Ajax lib ' + Timeline.ajax_lib_version + ')';
+ // cf method Timeline.writeVersion
+
+/*==================================================
+ *  Timeline
+ *==================================================
+ */
+Timeline.strings = {}; // localization string tables
+Timeline.HORIZONTAL = 0;
+Timeline.VERTICAL = 1;
+Timeline._defaultTheme = null;
+
+Timeline.getDefaultLocale = function() {
+    return Timeline.clientLocale;
+};
+
+Timeline.create = function(elmt, bandInfos, orientation, unit) {
+    if (Timeline.timelines == null) {
+        Timeline.timelines = [];
+        // Timeline.timelines array can have null members--Timelines that
+        // once existed on the page, but were later disposed of.
+    }
+    
+    var timelineID = Timeline.timelines.length;
+    Timeline.timelines[timelineID] = null; // placeholder until we have the object
+    var new_tl = new Timeline._Impl(elmt, bandInfos, orientation, unit,
+      timelineID);
+    Timeline.timelines[timelineID] = new_tl;    
+    return new_tl;
+};
+
+Timeline.createBandInfo = function(params) {
+    var theme = ("theme" in params) ? params.theme : Timeline.getDefaultTheme();
+    
+    var eventSource = ("eventSource" in params) ? params.eventSource : null;
+    
+    var ether = new Timeline.LinearEther({ 
+        centersOn:          ("date" in params) ? params.date : new Date(),
+        interval:           SimileAjax.DateTime.gregorianUnitLengths[params.intervalUnit],
+        pixelsPerInterval:  params.intervalPixels,
+        theme:              theme
+    });
+    
+    var etherPainter = new Timeline.GregorianEtherPainter({
+        unit:       params.intervalUnit, 
+        multiple:   ("multiple" in params) ? params.multiple : 1,
+        theme:      theme,
+        align:      ("align" in params) ? params.align : undefined
+    });
+    
+    var eventPainterParams = {
+        showText:   ("showEventText" in params) ? params.showEventText : true,
+        theme:      theme
+    };
+    // pass in custom parameters for the event painter
+    if ("eventPainterParams" in params) {
+        for (var prop in params.eventPainterParams) {
+            eventPainterParams[prop] = params.eventPainterParams[prop];
+        }
+    }
+    
+    if ("trackHeight" in params) {
+        eventPainterParams.trackHeight = params.trackHeight;
+    }
+    if ("trackGap" in params) {
+        eventPainterParams.trackGap = params.trackGap;
+    }
+    
+    var layout = ("overview" in params && params.overview) ? "overview" : ("layout" in params ? params.layout : "original");
+    var eventPainter;
+    if ("eventPainter" in params) {
+        eventPainter = new params.eventPainter(eventPainterParams);
+    } else {
+        switch (layout) {
+            case "overview" :
+                eventPainter = new Timeline.OverviewEventPainter(eventPainterParams);
+                break;
+            case "detailed" :
+                eventPainter = new Timeline.DetailedEventPainter(eventPainterParams);
+                break;
+            default:
+                eventPainter = new Timeline.OriginalEventPainter(eventPainterParams);
+        }
+    }
+    
+    return {   
+        width:          params.width,
+        eventSource:    eventSource,
+        timeZone:       ("timeZone" in params) ? params.timeZone : 0,
+        ether:          ether,
+        etherPainter:   etherPainter,
+        eventPainter:   eventPainter,
+        theme:          theme,
+        zoomIndex:      ("zoomIndex" in params) ? params.zoomIndex : 0,
+        zoomSteps:      ("zoomSteps" in params) ? params.zoomSteps : null
+    };
+};
+
+Timeline.createHotZoneBandInfo = function(params) {
+    var theme = ("theme" in params) ? params.theme : Timeline.getDefaultTheme();
+    
+    var eventSource = ("eventSource" in params) ? params.eventSource : null;
+    
+    var ether = new Timeline.HotZoneEther({ 
+        centersOn:          ("date" in params) ? params.date : new Date(),
+        interval:           SimileAjax.DateTime.gregorianUnitLengths[params.intervalUnit],
+        pixelsPerInterval:  params.intervalPixels,
+        zones:              params.zones,
+        theme:              theme
+    });
+    
+    var etherPainter = new Timeline.HotZoneGregorianEtherPainter({
+        unit:       params.intervalUnit, 
+        zones:      params.zones,
+        theme:      theme,
+        align:      ("align" in params) ? params.align : undefined
+    });
+    
+    var eventPainterParams = {
+        showText:   ("showEventText" in params) ? params.showEventText : true,
+        theme:      theme
+    };
+    // pass in custom parameters for the event painter
+    if ("eventPainterParams" in params) {
+        for (var prop in params.eventPainterParams) {
+            eventPainterParams[prop] = params.eventPainterParams[prop];
+        }
+    }
+    if ("trackHeight" in params) {
+        eventPainterParams.trackHeight = params.trackHeight;
+    }
+    if ("trackGap" in params) {
+        eventPainterParams.trackGap = params.trackGap;
+    }
+    
+    var layout = ("overview" in params && params.overview) ? "overview" : ("layout" in params ? params.layout : "original");
+    var eventPainter;
+    if ("eventPainter" in params) {
+        eventPainter = new params.eventPainter(eventPainterParams);
+    } else {
+        switch (layout) {
+            case "overview" :
+                eventPainter = new Timeline.OverviewEventPainter(eventPainterParams);
+                break;
+            case "detailed" :
+                eventPainter = new Timeline.DetailedEventPainter(eventPainterParams);
+                break;
+            default:
+                eventPainter = new Timeline.OriginalEventPainter(eventPainterParams);
+        }
+    }   
+    return {   
+        width:          params.width,
+        eventSource:    eventSource,
+        timeZone:       ("timeZone" in params) ? params.timeZone : 0,
+        ether:          ether,
+        etherPainter:   etherPainter,
+        eventPainter:   eventPainter,
+        theme:          theme,
+        zoomIndex:      ("zoomIndex" in params) ? params.zoomIndex : 0,
+        zoomSteps:      ("zoomSteps" in params) ? params.zoomSteps : null
+    };
+};
+
+Timeline.getDefaultTheme = function() {
+    if (Timeline._defaultTheme == null) {
+        Timeline._defaultTheme = Timeline.ClassicTheme.create(Timeline.getDefaultLocale());
+    }
+    return Timeline._defaultTheme;
+};
+
+Timeline.setDefaultTheme = function(theme) {
+    Timeline._defaultTheme = theme;
+};
+
+Timeline.loadXML = function(url, f) {
+    var fError = function(statusText, status, xmlhttp) {
+        alert("Failed to load data xml from " + url + "\n" + statusText);
+    };
+    var fDone = function(xmlhttp) {
+        var xml = xmlhttp.responseXML;
+        if (!xml.documentElement && xmlhttp.responseStream) {
+            xml.load(xmlhttp.responseStream);
+        } 
+        f(xml, url);
+    };
+    SimileAjax.XmlHttp.get(url, fError, fDone);
+};
+
+
+Timeline.loadJSON = function(url, f) {
+    var fError = function(statusText, status, xmlhttp) {
+        alert("Failed to load json data from " + url + "\n" + statusText);
+    };
+    var fDone = function(xmlhttp) {
+        f(eval('(' + xmlhttp.responseText + ')'), url);
+    };
+    SimileAjax.XmlHttp.get(url, fError, fDone);
+};
+
+Timeline.getTimelineFromID = function(timelineID) {
+    return Timeline.timelines[timelineID];
+};
+
+// Write the current Timeline version as the contents of element with id el_id
+Timeline.writeVersion = function(el_id) {
+  document.getElementById(el_id).innerHTML = this.display_version;    
+};
+
+
+
+/*==================================================
+ *  Timeline Implementation object
+ *==================================================
+ */
+Timeline._Impl = function(elmt, bandInfos, orientation, unit, timelineID) {
+    SimileAjax.WindowManager.initialize();
+    
+    this._containerDiv = elmt;
+    
+    this._bandInfos = bandInfos;
+    this._orientation = orientation == null ? Timeline.HORIZONTAL : orientation;
+    this._unit = (unit != null) ? unit : SimileAjax.NativeDateUnit;
+    this._starting = true; // is the Timeline being created? Used by autoWidth
+                           // functions
+    this._autoResizing = false;
+    
+    // autoWidth is a "public" property of the Timeline object
+    this.autoWidth = bandInfos && bandInfos[0] && bandInfos[0].theme && 
+                     bandInfos[0].theme.autoWidth;
+    this.autoWidthAnimationTime = bandInfos && bandInfos[0] && bandInfos[0].theme && 
+                     bandInfos[0].theme.autoWidthAnimationTime;
+    this.timelineID = timelineID; // also public attribute
+    this.timeline_start = bandInfos && bandInfos[0] && bandInfos[0].theme && 
+                     bandInfos[0].theme.timeline_start;
+    this.timeline_stop  = bandInfos && bandInfos[0] && bandInfos[0].theme && 
+                     bandInfos[0].theme.timeline_stop;
+    this.timeline_at_start = false; // already at start or stop? Then won't 
+    this.timeline_at_stop = false;  // try to move further in the wrong direction
+    
+    this._initialize();
+};
+
+//
+// Public functions used by client sw
+//
+Timeline._Impl.prototype.dispose = function() {
+    for (var i = 0; i < this._bands.length; i++) {
+        this._bands[i].dispose();
+    }
+    this._bands = null;
+    this._bandInfos = null;
+    this._containerDiv.innerHTML = "";
+    // remove from array of Timelines
+    Timeline.timelines[this.timelineID] = null;
+};
+
+Timeline._Impl.prototype.getBandCount = function() {
+    return this._bands.length;
+};
+
+Timeline._Impl.prototype.getBand = function(index) {
+    return this._bands[index];
+};
+
+Timeline._Impl.prototype.finishedEventLoading = function() {
+    // Called by client after events have been loaded into Timeline
+    // Only used if the client has set autoWidth
+    // Sets width to Timeline's requested amount and will shrink down the div if
+    // need be.
+    this._autoWidthCheck(true);
+    this._starting = false;
+};
+
+Timeline._Impl.prototype.layout = function() {
+    // called by client when browser is resized
+    this._autoWidthCheck(true);
+    this._distributeWidths();
+};
+
+Timeline._Impl.prototype.paint = function() {
+    for (var i = 0; i < this._bands.length; i++) {
+        this._bands[i].paint();
+    }
+};
+
+Timeline._Impl.prototype.getDocument = function() {
+    return this._containerDiv.ownerDocument;
+};
+
+Timeline._Impl.prototype.addDiv = function(div) {
+    this._containerDiv.appendChild(div);
+};
+
+Timeline._Impl.prototype.removeDiv = function(div) {
+    this._containerDiv.removeChild(div);
+};
+
+Timeline._Impl.prototype.isHorizontal = function() {
+    return this._orientation == Timeline.HORIZONTAL;
+};
+
+Timeline._Impl.prototype.isVertical = function() {
+    return this._orientation == Timeline.VERTICAL;
+};
+
+Timeline._Impl.prototype.getPixelLength = function() {
+    return this._orientation == Timeline.HORIZONTAL ? 
+        this._containerDiv.offsetWidth : this._containerDiv.offsetHeight;
+};
+
+Timeline._Impl.prototype.getPixelWidth = function() {
+    return this._orientation == Timeline.VERTICAL ? 
+        this._containerDiv.offsetWidth : this._containerDiv.offsetHeight;
+};
+
+Timeline._Impl.prototype.getUnit = function() {
+    return this._unit;
+};
+
+Timeline._Impl.prototype.getWidthStyle = function() {
+    // which element.style attribute should be changed to affect Timeline's "width"
+    return this._orientation == Timeline.HORIZONTAL ? 'height' : 'width';
+};
+
+Timeline._Impl.prototype.loadXML = function(url, f) {
+    var tl = this;
+    
+    
+    var fError = function(statusText, status, xmlhttp) {
+        alert("Failed to load data xml from " + url + "\n" + statusText);
+        tl.hideLoadingMessage();
+    };
+    var fDone = function(xmlhttp) {
+        try {
+            var xml = xmlhttp.responseXML;
+            if (!xml.documentElement && xmlhttp.responseStream) {
+                xml.load(xmlhttp.responseStream);
+            } 
+            f(xml, url);
+        } finally {
+            tl.hideLoadingMessage();
+        }
+    };
+    
+    this.showLoadingMessage();
+    window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
+};
+
+Timeline._Impl.prototype.loadJSON = function(url, f) {
+    var tl = this;
+    
+    var fError = function(statusText, status, xmlhttp) {
+        alert("Failed to load json data from " + url + "\n" + statusText);
+        tl.hideLoadingMessage();
+    };
+    var fDone = function(xmlhttp) {
+        try {
+            f(eval('(' + xmlhttp.responseText + ')'), url);
+        } finally {
+            tl.hideLoadingMessage();
+        }
+    };
+    
+    this.showLoadingMessage();
+    window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
+};
+
+
+//
+// Private functions used by Timeline object functions
+//
+
+Timeline._Impl.prototype._autoWidthScrollListener = function(band) {	
+    band.getTimeline()._autoWidthCheck(false);
+};
+
+// called to re-calculate auto width and adjust the overall Timeline div if needed
+Timeline._Impl.prototype._autoWidthCheck = function(okToShrink) {	
+    var timeline = this; // this Timeline
+    var immediateChange = timeline._starting;
+    var newWidth = 0;
+    
+    function changeTimelineWidth() {        
+        var widthStyle = timeline.getWidthStyle();
+        if (immediateChange) {
+            timeline._containerDiv.style[widthStyle] = newWidth + 'px';
+        } else {
+        	  // animate change
+        	  timeline._autoResizing = true;
+        	  var animateParam ={};
+        	  animateParam[widthStyle] = newWidth + 'px';
+        	  
+        	  SimileAjax.jQuery(timeline._containerDiv).animate(
+        	      animateParam, timeline.autoWidthAnimationTime,
+        	      'linear', function(){timeline._autoResizing = false;});
+        }
+    }
+        	
+    function checkTimelineWidth() {
+        var targetWidth = 0; // the new desired width
+        var currentWidth = timeline.getPixelWidth();
+        
+        if (timeline._autoResizing) {
+        	return; // early return
+        }
+
+        // compute targetWidth
+        for (var i = 0; i < timeline._bands.length; i++) {
+            timeline._bands[i].checkAutoWidth();
+            targetWidth += timeline._bandInfos[i].width;
+        }
+        
+        if (targetWidth > currentWidth || okToShrink) {
+            // yes, let's change the size
+            newWidth = targetWidth;
+            changeTimelineWidth();
+            timeline._distributeWidths();
+        }
+    }
+    
+    // function's mainline
+    if (!timeline.autoWidth) {
+        return; // early return
+    }
+
+    checkTimelineWidth();
+};
+
+Timeline._Impl.prototype._initialize = function() {
+    var containerDiv = this._containerDiv;
+    var doc = containerDiv.ownerDocument;
+    
+    containerDiv.className = 
+        containerDiv.className.split(" ").concat("timeline-container").join(" ");
+    
+	/*
+	 * Set css-class on container div that will define orientation
+	 */
+	var orientation = (this.isHorizontal()) ? 'horizontal' : 'vertical'
+	containerDiv.className +=' timeline-'+orientation;
+	
+	
+    while (containerDiv.firstChild) {
+        containerDiv.removeChild(containerDiv.firstChild);
+    }
+    
+    /*
+     *  inserting copyright and link
+     */
+    var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeline.urlPrefix + (this.isHorizontal() ? "images/copyright-vertical.png" : "images/copyright.png"));
+    elmtCopyright.className = "timeline-copyright";
+    elmtCopyright.title = "SIMILE Timeline - http://www.simile-widgets.org/";
+    SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://www.simile-widgets.org/"; });
+    containerDiv.appendChild(elmtCopyright);
+    
+    /*
+     *  creating bands
+     */
+    this._bands = [];
+    for (var i = 0; i < this._bandInfos.length; i++) {
+        var band = new Timeline._Band(this, this._bandInfos[i], i);
+        this._bands.push(band);
+    }
+    this._distributeWidths();
+    
+    /*
+     *  sync'ing bands
+     */
+    for (var i = 0; i < this._bandInfos.length; i++) {
+        var bandInfo = this._bandInfos[i];
+        if ("syncWith" in bandInfo) {
+            this._bands[i].setSyncWithBand(
+                this._bands[bandInfo.syncWith], 
+                ("highlight" in bandInfo) ? bandInfo.highlight : false
+            );
+        }
+    }
+    
+    
+    if (this.autoWidth) {
+        for (var i = 0; i < this._bands.length; i++) {
+            this._bands[i].addOnScrollListener(this._autoWidthScrollListener);
+        }
+    }
+    
+    
+    /*
+     *  creating loading UI
+     */
+    var message = SimileAjax.Graphics.createMessageBubble(doc);
+    message.containerDiv.className = "timeline-message-container";
+    containerDiv.appendChild(message.containerDiv);
+    
+    message.contentDiv.className = "timeline-message";
+    message.contentDiv.innerHTML = "<img src='" + Timeline.urlPrefix + "images/progress-running.gif' /> Loading...";
+    
+    this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; };
+    this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; };
+};
+
+Timeline._Impl.prototype._distributeWidths = function() {
+    var length = this.getPixelLength();
+    var width = this.getPixelWidth();
+    var cumulativeWidth = 0;
+    
+    for (var i = 0; i < this._bands.length; i++) {
+        var band = this._bands[i];
+        var bandInfos = this._bandInfos[i];
+        var widthString = bandInfos.width;
+        var bandWidth;
+        
+        if (typeof widthString == 'string') {
+          var x =  widthString.indexOf("%");
+          if (x > 0) {
+              var percent = parseInt(widthString.substr(0, x));
+              bandWidth = Math.round(percent * width / 100);
+          } else {
+              bandWidth = parseInt(widthString);
+          }
+        } else {
+        	// was given an integer
+        	bandWidth = widthString;
+        }
+        	 
+        band.setBandShiftAndWidth(cumulativeWidth, bandWidth);
+        band.setViewLength(length);
+        
+        cumulativeWidth += bandWidth;
+    }
+};
+
+Timeline._Impl.prototype.shiftOK = function(index, shift) {
+    // Returns true if the proposed shift is ok
+    //
+    // Positive shift means going back in time
+    var going_back = shift > 0,
+        going_forward = shift < 0;
+    
+    // Is there an edge?
+    if ((going_back    && this.timeline_start == null) ||
+        (going_forward && this.timeline_stop  == null) ||
+        (shift == 0)) {
+        return (true);  // early return
+    }
+    
+    // If any of the bands has noted that it is changing the others,
+    // then this shift is a secondary shift in reaction to the real shift,
+    // which already happened. In such cases, ignore it. (The issue is
+    // that a positive original shift can cause a negative secondary shift, 
+    // as the bands adjust.)
+    var secondary_shift = false;
+    for (var i = 0; i < this._bands.length && !secondary_shift; i++) {
+       secondary_shift = this._bands[i].busy();
+    }
+    if (secondary_shift) {
+        return(true); // early return
+    }
+    
+    // If we are already at an edge, then don't even think about going any further
+    if ((going_back    && this.timeline_at_start) ||
+        (going_forward && this.timeline_at_stop)) {
+        return (false);  // early return
+    }
+    
+    // Need to check all the bands
+    var ok = false; // return value
+    // If any of the bands will be or are showing an ok date, then let the shift proceed.
+    for (var i = 0; i < this._bands.length && !ok; i++) {
+       var band = this._bands[i];
+       if (going_back) {
+           ok = (i == index ? band.getMinVisibleDateAfterDelta(shift) : band.getMinVisibleDate())
+                >= this.timeline_start;
+       } else {
+           ok = (i == index ? band.getMaxVisibleDateAfterDelta(shift) : band.getMaxVisibleDate())
+                <= this.timeline_stop;
+       }	
+    }
+    
+    // process results
+    if (going_back) {
+       this.timeline_at_start = !ok;
+       this.timeline_at_stop = false;
+    } else {
+       this.timeline_at_stop = !ok;
+       this.timeline_at_start = false;
+    }
+    // This is where you could have an effect once per hitting an
+    // edge of the Timeline. Eg jitter the Timeline
+    //if (!ok) {
+        //alert(going_back ? "At beginning" : "At end");
+    //}
+    return (ok);
+};
+
+Timeline._Impl.prototype.zoom = function (zoomIn, x, y, target) {
+  var matcher = new RegExp("^timeline-band-([0-9]+)$");
+  var bandIndex = null;
+  
+  var result = matcher.exec(target.id);
+  if (result) {
+    bandIndex = parseInt(result[1]);
+  }
+
+  if (bandIndex != null) {
+    this._bands[bandIndex].zoom(zoomIn, x, y, target);
+  }   
+
+  this.paint();
+};
+
+/*=================================================
+ *
+ * Coding standards:
+ *
+ * We aim towards Douglas Crockford's Javascript conventions.
+ * See:  http://javascript.crockford.com/code.html
+ * See also: http://www.crockford.com/javascript/javascript.html
+ *
+ * That said, this JS code was written before some recent JS
+ * support libraries became widely used or available.
+ * In particular, the _ character is used to indicate a class function or
+ * variable that should be considered private to the class.
+ *
+ * The code mostly uses accessor methods for getting/setting the private
+ * class variables.
+ *
+ * Over time, we'd like to formalize the convention by using support libraries
+ * which enforce privacy in objects.
+ *
+ * We also want to use jslint:  http://www.jslint.com/
+ *
+ *
+ *==================================================
+ */
+
+
+
+/*==================================================
+ *  Band
+ *==================================================
+ */
+Timeline._Band = function(timeline, bandInfo, index) {
+    // Set up the band's object
+    
+    // Munge params: If autoWidth is on for the Timeline, then ensure that
+    // bandInfo.width is an integer     
+    if (timeline.autoWidth && typeof bandInfo.width == 'string') {
+        bandInfo.width = bandInfo.width.indexOf("%") > -1 ? 0 : parseInt(bandInfo.width);
+    }
+
+    this._timeline = timeline;
+    this._bandInfo = bandInfo;
+    
+    this._index = index;
+    
+    this._locale = ("locale" in bandInfo) ? bandInfo.locale : Timeline.getDefaultLocale();
+    this._timeZone = ("timeZone" in bandInfo) ? bandInfo.timeZone : 0;
+    this._labeller = ("labeller" in bandInfo) ? bandInfo.labeller : 
+        (("createLabeller" in timeline.getUnit()) ?
+            timeline.getUnit().createLabeller(this._locale, this._timeZone) :
+            new Timeline.GregorianDateLabeller(this._locale, this._timeZone));
+    this._theme = bandInfo.theme;
+    this._zoomIndex = ("zoomIndex" in bandInfo) ? bandInfo.zoomIndex : 0;
+    this._zoomSteps = ("zoomSteps" in bandInfo) ? bandInfo.zoomSteps : null;
+
+    this._dragging = false;
+    this._changing = false;
+    this._originalScrollSpeed = 5; // pixels
+    this._scrollSpeed = this._originalScrollSpeed;
+    this._viewOrthogonalOffset = 0; // vertical offset if the timeline is horizontal, and vice versa
+    this._onScrollListeners = [];
+    this._onOrthogonalScrollListeners = [];
+    
+    var b = this;
+    this._syncWithBand = null;
+    this._syncWithBandHandler = function(band) {
+        b._onHighlightBandScroll();
+    };
+    this._syncWithBandOrthogonalScrollHandler = function(band) {
+        b._onHighlightBandOrthogonalScroll();
+    };
+    this._selectorListener = function(band) {
+        b._onHighlightBandScroll();
+    };
+    
+    /*
+     *  Install a textbox to capture keyboard events
+     */
+    var inputDiv = this._timeline.getDocument().createElement("div");
+    inputDiv.className = "timeline-band-input";
+    this._timeline.addDiv(inputDiv);
+    
+    this._keyboardInput = document.createElement("input");
+    this._keyboardInput.type = "text";
+    inputDiv.appendChild(this._keyboardInput);
+    SimileAjax.DOM.registerEventWithObject(this._keyboardInput, "keydown", this, "_onKeyDown");
+    SimileAjax.DOM.registerEventWithObject(this._keyboardInput, "keyup", this, "_onKeyUp");
+    
+    /*
+     *  The band's outer most div that slides with respect to the timeline's div
+     */
+    this._div = this._timeline.getDocument().createElement("div");
+    this._div.id = "timeline-band-" + index;
+    this._div.className = "timeline-band timeline-band-" + index;
+    this._timeline.addDiv(this._div);
+    
+    SimileAjax.DOM.registerEventWithObject(this._div, "mousedown", this, "_onMouseDown");
+    SimileAjax.DOM.registerEventWithObject(this._div, "mousemove", this, "_onMouseMove");
+    SimileAjax.DOM.registerEventWithObject(this._div, "mouseup", this, "_onMouseUp");
+    SimileAjax.DOM.registerEventWithObject(this._div, "mouseout", this, "_onMouseOut");
+    SimileAjax.DOM.registerEventWithObject(this._div, "dblclick", this, "_onDblClick");
+    
+    var mouseWheel = this._theme!= null ? this._theme.mouseWheel : 'scroll'; // theme is not always defined
+    if (mouseWheel === 'zoom' || mouseWheel === 'scroll' || this._zoomSteps) {
+        // capture mouse scroll
+        if (SimileAjax.Platform.browser.isFirefox) {
+            SimileAjax.DOM.registerEventWithObject(this._div, "DOMMouseScroll", this, "_onMouseScroll");
+        } else {
+            SimileAjax.DOM.registerEventWithObject(this._div, "mousewheel", this, "_onMouseScroll");
+        }
+    }    
+    
+    /*
+     *  The inner div that contains layers
+     */
+    this._innerDiv = this._timeline.getDocument().createElement("div");
+    this._innerDiv.className = "timeline-band-inner";
+    this._div.appendChild(this._innerDiv);
+    
+    /*
+     *  Initialize parts of the band
+     */
+    this._ether = bandInfo.ether;
+    bandInfo.ether.initialize(this, timeline);
+        
+    this._etherPainter = bandInfo.etherPainter;
+    bandInfo.etherPainter.initialize(this, timeline);
+    
+    this._eventSource = bandInfo.eventSource;
+    if (this._eventSource) {
+        this._eventListener = {
+            onAddMany: function() { b._onAddMany(); },
+            onClear:   function() { b._onClear(); }
+        }
+        this._eventSource.addListener(this._eventListener);
+    }
+        
+    this._eventPainter = bandInfo.eventPainter;
+    this._eventTracksNeeded = 0;   // set by painter via updateEventTrackInfo
+    this._eventTrackIncrement = 0; 
+    bandInfo.eventPainter.initialize(this, timeline);
+    
+    this._decorators = ("decorators" in bandInfo) ? bandInfo.decorators : [];
+    for (var i = 0; i < this._decorators.length; i++) {
+        this._decorators[i].initialize(this, timeline);
+    }
+    
+    this._supportsOrthogonalScrolling = ("supportsOrthogonalScrolling" in this._eventPainter) &&
+        this._eventPainter.supportsOrthogonalScrolling();
+        
+    if (this._supportsOrthogonalScrolling) {
+        this._scrollBar = this._timeline.getDocument().createElement("div");
+        this._scrollBar.id = "timeline-band-scrollbar-" + index;
+        this._scrollBar.className = "timeline-band-scrollbar";
+        this._timeline.addDiv(this._scrollBar);
+        
+        this._scrollBar.innerHTML = '<div class="timeline-band-scrollbar-thumb"> </div>'
+    }
+};
+
+Timeline._Band.SCROLL_MULTIPLES = 5;
+
+Timeline._Band.prototype.dispose = function() {
+    this.closeBubble();
+    
+    if (this._eventSource) {
+        this._eventSource.removeListener(this._eventListener);
+        this._eventListener = null;
+        this._eventSource = null;
+    }
+    
+    this._timeline = null;
+    this._bandInfo = null;
+    
+    this._labeller = null;
+    this._ether = null;
+    this._etherPainter = null;
+    this._eventPainter = null;
+    this._decorators = null;
+    
+    this._onScrollListeners = null;
+    this._syncWithBandHandler = null;
+    this._syncWithBandOrthogonalScrollHandler = null;
+    this._selectorListener = null;
+    
+    this._div = null;
+    this._innerDiv = null;
+    this._keyboardInput = null;
+    this._scrollBar = null;
+};
+
+Timeline._Band.prototype.addOnScrollListener = function(listener) {
+    this._onScrollListeners.push(listener);
+};
+
+Timeline._Band.prototype.removeOnScrollListener = function(listener) {
+    for (var i = 0; i < this._onScrollListeners.length; i++) {
+        if (this._onScrollListeners[i] == listener) {
+            this._onScrollListeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+Timeline._Band.prototype.addOnOrthogonalScrollListener = function(listener) {
+    this._onOrthogonalScrollListeners.push(listener);
+};
+
+Timeline._Band.prototype.removeOnOrthogonalScrollListener = function(listener) {
+    for (var i = 0; i < this._onOrthogonalScrollListeners.length; i++) {
+        if (this._onOrthogonalScrollListeners[i] == listener) {
+            this._onOrthogonalScrollListeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+Timeline._Band.prototype.setSyncWithBand = function(band, highlight) {
+    if (this._syncWithBand) {
+        this._syncWithBand.removeOnScrollListener(this._syncWithBandHandler);
+        this._syncWithBand.removeOnOrthogonalScrollListener(this._syncWithBandOrthogonalScrollHandler);
+    }
+    
+    this._syncWithBand = band;
+    this._syncWithBand.addOnScrollListener(this._syncWithBandHandler);
+    this._syncWithBand.addOnOrthogonalScrollListener(this._syncWithBandOrthogonalScrollHandler);
+    this._highlight = highlight;
+    this._positionHighlight();
+};
+
+Timeline._Band.prototype.getLocale = function() {
+    return this._locale;
+};
+
+Timeline._Band.prototype.getTimeZone = function() {
+    return this._timeZone;
+};
+
+Timeline._Band.prototype.getLabeller = function() {
+    return this._labeller;
+};
+
+Timeline._Band.prototype.getIndex = function() {
+    return this._index;
+};
+
+Timeline._Band.prototype.getEther = function() {
+    return this._ether;
+};
+
+Timeline._Band.prototype.getEtherPainter = function() {
+    return this._etherPainter;
+};
+
+Timeline._Band.prototype.getEventSource = function() {
+    return this._eventSource;
+};
+
+Timeline._Band.prototype.getEventPainter = function() {
+    return this._eventPainter;
+};
+
+Timeline._Band.prototype.getTimeline = function() {
+    return this._timeline;
+};
+
+// Autowidth support
+Timeline._Band.prototype.updateEventTrackInfo = function(tracks, increment) {
+    this._eventTrackIncrement = increment; // doesn't vary for a specific band
+
+    if (tracks > this._eventTracksNeeded) {
+        this._eventTracksNeeded = tracks;
+    }
+};
+
+// Autowidth support
+Timeline._Band.prototype.checkAutoWidth = function() {
+    // if a new (larger) width is needed by the band
+    // then: a) updates the band's bandInfo.width
+    //
+    // desiredWidth for the band is 
+    //   (number of tracks + margin) * track increment
+    if (! this._timeline.autoWidth) {
+      return; // early return
+    }
+    
+    var overviewBand = this._eventPainter.getType() == 'overview';
+    var margin = overviewBand ? 
+       this._theme.event.overviewTrack.autoWidthMargin : 
+       this._theme.event.track.autoWidthMargin;
+    var desiredWidth = Math.ceil((this._eventTracksNeeded + margin) *
+                       this._eventTrackIncrement);
+    // add offset amount (additional margin)
+    desiredWidth += overviewBand ? this._theme.event.overviewTrack.offset : 
+                                   this._theme.event.track.offset;
+    var bandInfo = this._bandInfo;
+    
+    if (desiredWidth != bandInfo.width) {
+        bandInfo.width = desiredWidth;
+    }
+};
+
+Timeline._Band.prototype.layout = function() {
+    this.paint();
+};
+
+Timeline._Band.prototype.paint = function() {
+    this._etherPainter.paint();
+    this._paintDecorators();
+    this._paintEvents();
+};
+
+Timeline._Band.prototype.softLayout = function() {
+    this.softPaint();
+};
+
+Timeline._Band.prototype.softPaint = function() {
+    this._etherPainter.softPaint();
+    this._softPaintDecorators();
+    this._softPaintEvents();
+};
+
+Timeline._Band.prototype.setBandShiftAndWidth = function(shift, width) {
+    var inputDiv = this._keyboardInput.parentNode;
+    var middle = shift + Math.floor(width / 2);
+    if (this._timeline.isHorizontal()) {
+        this._div.style.top = shift + "px";
+        this._div.style.height = width + "px";
+        
+        inputDiv.style.top = middle + "px";
+        inputDiv.style.left = "-1em";
+    } else {
+        this._div.style.left = shift + "px";
+        this._div.style.width = width + "px";
+        
+        inputDiv.style.left = middle + "px";
+        inputDiv.style.top = "-1em";
+    }
+};
+
+Timeline._Band.prototype.getViewWidth = function() {
+    if (this._timeline.isHorizontal()) {
+        return this._div.offsetHeight;
+    } else {
+        return this._div.offsetWidth;
+    }
+};
+
+Timeline._Band.prototype.setViewLength = function(length) {
+    this._viewLength = length;
+    this._recenterDiv();
+    this._onChanging();
+};
+
+Timeline._Band.prototype.getViewLength = function() {
+    return this._viewLength;
+};
+
+Timeline._Band.prototype.getTotalViewLength = function() {
+    return Timeline._Band.SCROLL_MULTIPLES * this._viewLength;
+};
+
+Timeline._Band.prototype.getViewOffset = function() {
+    return this._viewOffset;
+};
+
+Timeline._Band.prototype.getMinDate = function() {
+    return this._ether.pixelOffsetToDate(this._viewOffset);
+};
+
+Timeline._Band.prototype.getMaxDate = function() {
+    return this._ether.pixelOffsetToDate(this._viewOffset + Timeline._Band.SCROLL_MULTIPLES * this._viewLength);
+};
+
+Timeline._Band.prototype.getMinVisibleDate = function() {
+    return this._ether.pixelOffsetToDate(0);
+};
+
+Timeline._Band.prototype.getMinVisibleDateAfterDelta = function(delta) {
+    return this._ether.pixelOffsetToDate(delta);
+};
+
+Timeline._Band.prototype.getMaxVisibleDate = function() {
+    // Max date currently visible on band
+    return this._ether.pixelOffsetToDate(this._viewLength);
+};
+
+Timeline._Band.prototype.getMaxVisibleDateAfterDelta = function(delta) {
+    // Max date visible on band after delta px view change is applied 
+    return this._ether.pixelOffsetToDate(this._viewLength + delta);
+};
+
+Timeline._Band.prototype.getCenterVisibleDate = function() {
+    return this._ether.pixelOffsetToDate(this._viewLength / 2);
+};
+
+Timeline._Band.prototype.setMinVisibleDate = function(date) {
+    if (!this._changing) {
+        this._moveEther(Math.round(-this._ether.dateToPixelOffset(date)));
+    }
+};
+
+Timeline._Band.prototype.setMaxVisibleDate = function(date) {
+    if (!this._changing) {
+        this._moveEther(Math.round(this._viewLength - this._ether.dateToPixelOffset(date)));
+    }
+};
+
+Timeline._Band.prototype.setCenterVisibleDate = function(date) {
+    if (!this._changing) {
+        this._moveEther(Math.round(this._viewLength / 2 - this._ether.dateToPixelOffset(date)));
+    }
+};
+
+Timeline._Band.prototype.dateToPixelOffset = function(date) {
+    return this._ether.dateToPixelOffset(date) - this._viewOffset;
+};
+
+Timeline._Band.prototype.pixelOffsetToDate = function(pixels) {
+    return this._ether.pixelOffsetToDate(pixels + this._viewOffset);
+};
+
+Timeline._Band.prototype.getViewOrthogonalOffset = function() {
+    return this._viewOrthogonalOffset;
+};
+
+Timeline._Band.prototype.setViewOrthogonalOffset = function(offset) {
+    this._viewOrthogonalOffset = Math.max(0, offset);
+};
+
+Timeline._Band.prototype.createLayerDiv = function(zIndex, className) {
+    var div = this._timeline.getDocument().createElement("div");
+    div.className = "timeline-band-layer" + (typeof className == "string" ? (" " + className) : "");
+    div.style.zIndex = zIndex;
+    this._innerDiv.appendChild(div);
+    
+    var innerDiv = this._timeline.getDocument().createElement("div");
+    innerDiv.className = "timeline-band-layer-inner";
+    if (SimileAjax.Platform.browser.isIE) {
+        innerDiv.style.cursor = "move";
+    } else {
+        innerDiv.style.cursor = "-moz-grab";
+    }
+    div.appendChild(innerDiv);
+    
+    return innerDiv;
+};
+
+Timeline._Band.prototype.removeLayerDiv = function(div) {
+    this._innerDiv.removeChild(div.parentNode);
+};
+
+Timeline._Band.prototype.scrollToCenter = function(date, f) {
+    var pixelOffset = this._ether.dateToPixelOffset(date);
+    if (pixelOffset < -this._viewLength / 2) {
+        this.setCenterVisibleDate(this.pixelOffsetToDate(pixelOffset + this._viewLength));
+    } else if (pixelOffset > 3 * this._viewLength / 2) {
+        this.setCenterVisibleDate(this.pixelOffsetToDate(pixelOffset - this._viewLength));
+    }
+    this._autoScroll(Math.round(this._viewLength / 2 - this._ether.dateToPixelOffset(date)), f);
+};
+
+Timeline._Band.prototype.showBubbleForEvent = function(eventID) {
+    var evt = this.getEventSource().getEvent(eventID);
+    if (evt) {
+        var self = this;
+        this.scrollToCenter(evt.getStart(), function() {
+            self._eventPainter.showBubble(evt);
+        });
+    }
+};
+
+Timeline._Band.prototype.zoom = function(zoomIn, x, y, target) {
+  if (!this._zoomSteps) {
+    // zoom disabled
+    return;
+  }
+  
+  // shift the x value by our offset
+  x += this._viewOffset;
+
+  var zoomDate = this._ether.pixelOffsetToDate(x);
+  var netIntervalChange = this._ether.zoom(zoomIn);
+  this._etherPainter.zoom(netIntervalChange);
+
+  // shift our zoom date to the far left
+  this._moveEther(Math.round(-this._ether.dateToPixelOffset(zoomDate)));
+  // then shift it back to where the mouse was
+  this._moveEther(x);
+};
+
+Timeline._Band.prototype._onMouseDown = function(innerFrame, evt, target) {
+    this.closeBubble();
+    
+    this._dragging = true;
+    this._dragX = evt.clientX;
+    this._dragY = evt.clientY;
+};
+
+Timeline._Band.prototype._onMouseMove = function(innerFrame, evt, target) {
+    if (this._dragging) {
+        var diffX = evt.clientX - this._dragX;
+        var diffY = evt.clientY - this._dragY;
+        
+        this._dragX = evt.clientX;
+        this._dragY = evt.clientY;
+        
+        if (this._timeline.isHorizontal()) {
+            this._moveEther(diffX, diffY);
+        } else {
+            this._moveEther(diffY, diffX);
+        }
+        this._positionHighlight();
+        this._showScrollbar();
+    }
+};
+
+Timeline._Band.prototype._onMouseUp = function(innerFrame, evt, target) {
+    this._dragging = false;
+    this._keyboardInput.focus();
+    this._bounceBack();
+};
+
+Timeline._Band.prototype._onMouseOut = function(innerFrame, evt, target) {
+    var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
+    coords.x += this._viewOffset;
+    if (coords.x < 0 || coords.x > innerFrame.offsetWidth ||
+        coords.y < 0 || coords.y > innerFrame.offsetHeight) {
+        this._dragging = false;
+        this._bounceBack();
+    }
+};
+
+Timeline._Band.prototype._onMouseScroll = function(innerFrame, evt, target) {
+  var now = new Date();
+  now = now.getTime();
+
+  if (!this._lastScrollTime || ((now - this._lastScrollTime) > 50)) {
+    // limit 1 scroll per 200ms due to FF3 sending multiple events back to back
+    this._lastScrollTime = now;
+
+    var delta = 0;
+    if (evt.wheelDelta) {
+      delta = evt.wheelDelta/120;
+    } else if (evt.detail) {
+      delta = -evt.detail/3;
+    }
+    
+    // either scroll or zoom
+    var mouseWheel = this._theme.mouseWheel;
+    
+    if (this._zoomSteps || mouseWheel === 'zoom') {
+      var loc = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
+      if (delta != 0) {
+        var zoomIn;
+        if (delta > 0)
+          zoomIn = true;
+        if (delta < 0)
+          zoomIn = false;
+        // call zoom on the timeline so we could zoom multiple bands if desired
+        this._timeline.zoom(zoomIn, loc.x, loc.y, innerFrame);
+      }
+    }
+    else if (mouseWheel === 'scroll') {
+    	var move_amt = 50 * (delta < 0 ? -1 : 1);
+      this._moveEther(move_amt);
+    }
+  }
+
+  // prevent bubble
+  if (evt.stopPropagation) {
+    evt.stopPropagation();
+  }
+  evt.cancelBubble = true;
+
+  // prevent the default action
+  if (evt.preventDefault) {
+    evt.preventDefault();
+  }
+  evt.returnValue = false;
+};
+
+Timeline._Band.prototype._onDblClick = function(innerFrame, evt, target) {
+    var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
+    var distance = coords.x - (this._viewLength / 2 - this._viewOffset);
+    
+    this._autoScroll(-distance);
+};
+
+Timeline._Band.prototype._onKeyDown = function(keyboardInput, evt, target) {
+    if (!this._dragging) {
+        switch (evt.keyCode) {
+        case 27: // ESC
+            break;
+        case 37: // left arrow
+        case 38: // up arrow
+            this._scrollSpeed = Math.min(50, Math.abs(this._scrollSpeed * 1.05));
+            this._moveEther(this._scrollSpeed);
+            break;
+        case 39: // right arrow
+        case 40: // down arrow
+            this._scrollSpeed = -Math.min(50, Math.abs(this._scrollSpeed * 1.05));
+            this._moveEther(this._scrollSpeed);
+            break;
+        default:
+            return true;
+        }
+        this.closeBubble();
+        
+        SimileAjax.DOM.cancelEvent(evt);
+        return false;
+    }
+    return true;
+};
+
+Timeline._Band.prototype._onKeyUp = function(keyboardInput, evt, target) {
+    if (!this._dragging) {
+        this._scrollSpeed = this._originalScrollSpeed;
+        
+        switch (evt.keyCode) {
+        case 35: // end
+            this.setCenterVisibleDate(this._eventSource.getLatestDate());
+            break;
+        case 36: // home
+            this.setCenterVisibleDate(this._eventSource.getEarliestDate());
+            break;
+        case 33: // page up
+            this._autoScroll(this._timeline.getPixelLength());
+            break;
+        case 34: // page down
+            this._autoScroll(-this._timeline.getPixelLength());
+            break;
+        default:
+            return true;
+        }
+        
+        this.closeBubble();
+        
+        SimileAjax.DOM.cancelEvent(evt);
+        return false;
+    }
+    return true;
+};
+
+Timeline._Band.prototype._autoScroll = function(distance, f) {
+    var b = this;
+    var a = SimileAjax.Graphics.createAnimation(
+        function(abs, diff) {
+            b._moveEther(diff);
+        }, 
+        0, 
+        distance, 
+        1000, 
+        f
+    );
+    a.run();
+};
+
+Timeline._Band.prototype._moveEther = function(shift, orthogonalShift) {
+    if (orthogonalShift === undefined) {
+        orthogonalShift = 0;
+    }
+    
+    this.closeBubble();
+    
+    // A positive shift means back in time
+    // Check that we're not moving beyond Timeline's limits
+    if (!this._timeline.shiftOK(this._index, shift)) {
+        return; // early return
+    }
+
+    this._viewOffset += shift;
+    this._ether.shiftPixels(-shift);
+    if (this._timeline.isHorizontal()) {
+        this._div.style.left = this._viewOffset + "px";
+    } else {
+        this._div.style.top = this._viewOffset + "px";
+    }
+    
+    if (this._supportsOrthogonalScrolling) {
+        this._viewOrthogonalOffset = this._viewOrthogonalOffset + orthogonalShift;
+    }
+    
+    if (this._viewOffset > -this._viewLength * 0.5 ||
+        this._viewOffset < -this._viewLength * (Timeline._Band.SCROLL_MULTIPLES - 1.5)) {
+        
+        this._recenterDiv();
+    } else {
+        this.softLayout();
+    }    
+    
+    this._onChanging();
+}
+
+Timeline._Band.prototype._onChanging = function() {
+    this._changing = true;
+
+    this._fireOnScroll();
+    this._setSyncWithBandDate();
+    
+    this._changing = false;
+};
+
+Timeline._Band.prototype.busy = function() {
+    // Is this band busy changing other bands?
+    return(this._changing);
+};
+
+Timeline._Band.prototype._fireOnScroll = function() {
+    for (var i = 0; i < this._onScrollListeners.length; i++) {
+        this._onScrollListeners[i](this);
+    }
+};
+
+Timeline._Band.prototype._fireOnOrthogonalScroll = function() {
+    for (var i = 0; i < this._onOrthogonalScrollListeners.length; i++) {
+        this._onOrthogonalScrollListeners[i](this);
+    }
+};
+
+Timeline._Band.prototype._setSyncWithBandDate = function() {
+    if (this._syncWithBand) {
+        var centerDate = this._ether.pixelOffsetToDate(this.getViewLength() / 2);
+        this._syncWithBand.setCenterVisibleDate(centerDate);
+    }
+};
+
+Timeline._Band.prototype._onHighlightBandScroll = function() {
+    if (this._syncWithBand) {
+        var centerDate = this._syncWithBand.getCenterVisibleDate();
+        var centerPixelOffset = this._ether.dateToPixelOffset(centerDate);
+        
+        this._moveEther(Math.round(this._viewLength / 2 - centerPixelOffset));
+        this._positionHighlight();
+    }
+};
+
+Timeline._Band.prototype._onHighlightBandOrthogonalScroll = function() {
+    if (this._syncWithBand) {
+        this._positionHighlight();
+    }
+};
+
+Timeline._Band.prototype._onAddMany = function() {
+    this._paintEvents();
+};
+
+Timeline._Band.prototype._onClear = function() {
+    this._paintEvents();
+};
+
+Timeline._Band.prototype._positionHighlight = function() {
+    if (this._syncWithBand) {
+        var startDate = this._syncWithBand.getMinVisibleDate();
+        var endDate = this._syncWithBand.getMaxVisibleDate();
+        
+        if (this._highlight) {
+            var offset = 0; // percent
+            var extent = 1.0; // percent
+            var syncEventPainter = this._syncWithBand.getEventPainter();
+            if ("supportsOrthogonalScrolling" in syncEventPainter && 
+                syncEventPainter.supportsOrthogonalScrolling()) {
+
+                var orthogonalExtent = syncEventPainter.getOrthogonalExtent();
+                var visibleWidth = this._syncWithBand.getViewWidth();
+                var totalWidth = Math.max(visibleWidth, orthogonalExtent);
+                extent = visibleWidth / totalWidth;
+                offset = -this._syncWithBand.getViewOrthogonalOffset() / totalWidth;
+            }
+            
+            this._etherPainter.setHighlight(startDate, endDate, offset, extent);
+        }
+    }
+};
+
+Timeline._Band.prototype._recenterDiv = function() {
+    this._viewOffset = -this._viewLength * (Timeline._Band.SCROLL_MULTIPLES - 1) / 2;
+    if (this._timeline.isHorizontal()) {
+        this._div.style.left = this._viewOffset + "px";
+        this._div.style.width = (Timeline._Band.SCROLL_MULTIPLES * this._viewLength) + "px";
+    } else {
+        this._div.style.top = this._viewOffset + "px";
+        this._div.style.height = (Timeline._Band.SCROLL_MULTIPLES * this._viewLength) + "px";
+    }
+    this.layout();
+};
+
+Timeline._Band.prototype._paintEvents = function() {
+    this._eventPainter.paint();
+    this._showScrollbar();
+    this._fireOnOrthogonalScroll();
+};
+
+Timeline._Band.prototype._softPaintEvents = function() {
+    this._eventPainter.softPaint();
+};
+
+Timeline._Band.prototype._paintDecorators = function() {
+    for (var i = 0; i < this._decorators.length; i++) {
+        this._decorators[i].paint();
+    }
+};
+
+Timeline._Band.prototype._softPaintDecorators = function() {
+    for (var i = 0; i < this._decorators.length; i++) {
+        this._decorators[i].softPaint();
+    }
+};
+
+Timeline._Band.prototype.closeBubble = function() {
+    SimileAjax.WindowManager.cancelPopups();
+};
+
+Timeline._Band.prototype._bounceBack = function(f) {
+    if (!this._supportsOrthogonalScrolling) {
+        return;
+    }
+    
+    var target = 0;
+    if (this._viewOrthogonalOffset < 0) {
+        var orthogonalExtent = this._eventPainter.getOrthogonalExtent();
+        if (this._viewOrthogonalOffset + orthogonalExtent >= this.getViewWidth()) {
+            target = this._viewOrthogonalOffset;
+        } else {    
+            target = Math.min(0, this.getViewWidth() - orthogonalExtent);
+        }
+    }
+    
+    if (target != this._viewOrthogonalOffset) {
+        var self = this;
+        SimileAjax.Graphics.createAnimation(
+            function(abs, diff) {
+                self._viewOrthogonalOffset = abs;
+                self._eventPainter.softPaint();
+                self._showScrollbar();
+                self._fireOnOrthogonalScroll();
+            }, 
+            this._viewOrthogonalOffset, 
+            target, 
+            300, 
+            function() {
+                self._hideScrollbar();
+            }
+        ).run();
+    } else {
+        this._hideScrollbar();
+    }
+};
+
+Timeline._Band.prototype._showScrollbar = function() {
+    if (!this._supportsOrthogonalScrolling) {
+        return;
+    }
+    
+    var orthogonalExtent = this._eventPainter.getOrthogonalExtent();
+    var visibleWidth = this.getViewWidth();
+    var totalWidth = Math.max(visibleWidth, orthogonalExtent);
+    var ratio = (visibleWidth / totalWidth);
+    var thumbWidth = Math.round(visibleWidth * ratio) + "px";
+    var thumbOffset = Math.round(-this._viewOrthogonalOffset * ratio) + "px";
+    
+    var thumb = this._scrollBar.firstChild;
+    if (this._timeline.isHorizontal()) {
+        this._scrollBar.style.top = this._div.style.top;
+        this._scrollBar.style.height = this._div.style.height;
+        
+        this._scrollBar.style.right = "0px";
+        this._scrollBar.style.width = "10px";
+        
+        thumb.style.top = thumbOffset;
+        thumb.style.height = thumbWidth;
+    } else {
+        this._scrollBar.style.left = this._div.style.left;
+        this._scrollBar.style.width = this._div.style.width;
+        
+        this._scrollBar.style.bottom = "0px";
+        this._scrollBar.style.height = "10px";
+        
+        thumb.style.left = thumbOffset;
+        thumb.style.width = thumbWidth;
+    }
+    
+    if (ratio >= 1 && this._viewOrthogonalOffset == 0) {
+        this._scrollBar.style.display = "none";
+    } else {
+        this._scrollBar.style.display = "block";
+    }
+};
+
+Timeline._Band.prototype._hideScrollbar = function() {
+    if (!this._supportsOrthogonalScrolling) {
+        return;
+    }
+    //this._scrollBar.style.display = "none";
+};
+
+/*==================================================
+ *  Classic Theme
+ *==================================================
+ */
+
+
+
+Timeline.ClassicTheme = new Object();
+
+Timeline.ClassicTheme.implementations = [];
+
+Timeline.ClassicTheme.create = function(locale) {
+    if (locale == null) {
+        locale = Timeline.getDefaultLocale();
+    }
+    
+    var f = Timeline.ClassicTheme.implementations[locale];
+    if (f == null) {
+        f = Timeline.ClassicTheme._Impl;
+    }
+    return new f();
+};
+
+Timeline.ClassicTheme._Impl = function() {
+    this.firstDayOfWeek = 0; // Sunday
+          
+    // Note: Many styles previously set here are now set using CSS
+    //       The comments indicate settings controlled by CSS, not
+    //       lines to be un-commented.
+    //
+    //
+    // Attributes autoWidth, autoWidthAnimationTime, timeline_start 
+    // and timeline_stop must be set on the first band's theme.
+    // The other attributes can be set differently for each 
+    // band by using different themes for the bands.
+    this.autoWidth = false; // Should the Timeline automatically grow itself, as
+                            // needed when too many events for the available width
+                            // are painted on the visible part of the Timeline?
+    this.autoWidthAnimationTime = 500; // mSec
+    this.timeline_start = null; // Setting a date, eg new Date(Date.UTC(2008,0,17,20,00,00,0)) will prevent the
+                                // Timeline from being moved to anytime before the date.
+    this.timeline_stop = null;  // Use for setting a maximum date. The Timeline will not be able 
+                                // to be moved to anytime after this date.
+    this.ether = {
+        backgroundColors: [
+        //    "#EEE",
+        //    "#DDD",
+        //    "#CCC",
+        //    "#AAA"
+        ],
+     //   highlightColor:     "white",
+        highlightOpacity:   50,
+        interval: {
+            line: {
+                show:       true,
+                opacity:    25
+               // color:      "#aaa",
+            },
+            weekend: {
+                opacity:    30
+              //  color:      "#FFFFE0",
+            },
+            marker: {
+                hAlign:     "Bottom",
+                vAlign:     "Right"
+                                        /*
+                hBottomStyler: function(elmt) {
+                    elmt.className = "timeline-ether-marker-bottom";
+                },
+                hBottomEmphasizedStyler: function(elmt) {
+                    elmt.className = "timeline-ether-marker-bottom-emphasized";
+                },
+                hTopStyler: function(elmt) {
+                    elmt.className = "timeline-ether-marker-top";
+                },
+                hTopEmphasizedStyler: function(elmt) {
+                    elmt.className = "timeline-ether-marker-top-emphasized";
+                },
+                */
+                                        
+                    
+               /*
+                                  vRightStyler: function(elmt) {
+                    elmt.className = "timeline-ether-marker-right";
+                },
+                vRightEmphasizedStyler: function(elmt) {
+                    elmt.className = "timeline-ether-marker-right-emphasized";
+                },
+                vLeftStyler: function(elmt) {
+                    elmt.className = "timeline-ether-marker-left";
+                },
+                vLeftEmphasizedStyler:function(elmt) {
+                    elmt.className = "timeline-ether-marker-left-emphasized";
+                }
+                */
+            }
+        }
+    };
+    
+    this.event = {
+        track: {
+                   height: 10, // px. You will need to change the track
+                               //     height if you change the tape height.
+                      gap:  2, // px. Gap between tracks
+                   offset:  2, // px. top margin above tapes
+          autoWidthMargin:  1.5
+          /* autoWidthMargin is only used if autoWidth (see above) is true.
+             The autoWidthMargin setting is used to set how close the bottom of the
+             lowest track is to the edge of the band's div. The units are total track
+             width (tape + label + gap). A min of 0.5 is suggested. Use this setting to
+             move the bottom track's tapes above the axis markers, if needed for your
+             Timeline.
+          */
+        },
+        overviewTrack: {
+                  offset: 20, // px -- top margin above tapes 
+              tickHeight:  6, // px
+                  height:  2, // px
+                     gap:  1, // px
+         autoWidthMargin:  5 // This attribute is only used if autoWidth (see above) is true.
+        },
+        tape: {
+            height:         4 // px. For thicker tapes, remember to change track height too.
+        },
+        instant: {
+                           icon: Timeline.urlPrefix + "images/dull-blue-circle.png", 
+                                 // default icon. Icon can also be specified per event
+                      iconWidth: 10,
+                     iconHeight: 10,
+               impreciseOpacity: 20, // opacity of the tape when durationEvent is false
+            impreciseIconMargin: 3   // A tape and an icon are painted for imprecise instant
+                                     // events. This attribute is the margin between the
+                                     // bottom of the tape and the top of the icon in that
+                                     // case.
+    //        color:             "#58A0DC",
+    //        impreciseColor:    "#58A0DC",
+        },
+        duration: {
+            impreciseOpacity: 20 // tape opacity for imprecise part of duration events
+      //      color:            "#58A0DC",
+      //      impreciseColor:   "#58A0DC",
+        },
+        label: {
+            backgroundOpacity: 50,// only used in detailed painter
+               offsetFromLine:  3 // px left margin amount from icon's right edge
+      //      backgroundColor:   "white",
+      //      lineColor:         "#58A0DC",
+        },
+        highlightColors: [  // Use with getEventPainter().setHighlightMatcher
+                            // See webapp/examples/examples.js
+            "#FFFF00",
+            "#FFC000",
+            "#FF0000",
+            "#0000FF"
+        ],
+        highlightLabelBackground: false, // When highlighting an event, also change the event's label background?
+        bubble: {
+            width:          250, // px
+            maxHeight:        0, // px Maximum height of bubbles. 0 means no max height. 
+                                 // scrollbar will be added for taller bubbles
+            titleStyler: function(elmt) {
+                elmt.className = "timeline-event-bubble-title";
+            },
+            bodyStyler: function(elmt) {
+                elmt.className = "timeline-event-bubble-body";
+            },
+            imageStyler: function(elmt) {
+                elmt.className = "timeline-event-bubble-image";
+            },
+            wikiStyler: function(elmt) {
+                elmt.className = "timeline-event-bubble-wiki";
+            },
+            timeStyler: function(elmt) {
+                elmt.className = "timeline-event-bubble-time";
+            }
+        }
+    };
+    
+    this.mouseWheel = 'scroll'; // 'default', 'zoom', 'scroll'
+};
+/*==================================================
+ *  An "ether" is a object that maps date/time to pixel coordinates.
+ *==================================================
+ */
+
+/*==================================================
+ *  Linear Ether
+ *==================================================
+ */
+ 
+Timeline.LinearEther = function(params) {
+    this._params = params;
+    this._interval = params.interval;
+    this._pixelsPerInterval = params.pixelsPerInterval;
+};
+
+Timeline.LinearEther.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    this._unit = timeline.getUnit();
+    
+    if ("startsOn" in this._params) {
+        this._start = this._unit.parseFromObject(this._params.startsOn);
+    } else if ("endsOn" in this._params) {
+        this._start = this._unit.parseFromObject(this._params.endsOn);
+        this.shiftPixels(-this._timeline.getPixelLength());
+    } else if ("centersOn" in this._params) {
+        this._start = this._unit.parseFromObject(this._params.centersOn);
+        this.shiftPixels(-this._timeline.getPixelLength() / 2);
+    } else {
+        this._start = this._unit.makeDefaultValue();
+        this.shiftPixels(-this._timeline.getPixelLength() / 2);
+    }
+};
+
+Timeline.LinearEther.prototype.setDate = function(date) {
+    this._start = this._unit.cloneValue(date);
+};
+
+Timeline.LinearEther.prototype.shiftPixels = function(pixels) {
+    var numeric = this._interval * pixels / this._pixelsPerInterval;
+    this._start = this._unit.change(this._start, numeric);
+};
+
+Timeline.LinearEther.prototype.dateToPixelOffset = function(date) {
+    var numeric = this._unit.compare(date, this._start);
+    return this._pixelsPerInterval * numeric / this._interval;
+};
+
+Timeline.LinearEther.prototype.pixelOffsetToDate = function(pixels) {
+    var numeric = pixels * this._interval / this._pixelsPerInterval;
+    return this._unit.change(this._start, numeric);
+};
+
+Timeline.LinearEther.prototype.zoom = function(zoomIn) {
+  var netIntervalChange = 0;
+  var currentZoomIndex = this._band._zoomIndex;
+  var newZoomIndex = currentZoomIndex;
+
+  if (zoomIn && (currentZoomIndex > 0)) {
+    newZoomIndex = currentZoomIndex - 1;
+  }
+  
+  if (!zoomIn && (currentZoomIndex < (this._band._zoomSteps.length - 1))) {
+    newZoomIndex = currentZoomIndex + 1;
+  }
+
+  this._band._zoomIndex = newZoomIndex;  
+  this._interval = 
+    SimileAjax.DateTime.gregorianUnitLengths[this._band._zoomSteps[newZoomIndex].unit];
+  this._pixelsPerInterval = this._band._zoomSteps[newZoomIndex].pixelsPerInterval;
+  netIntervalChange = this._band._zoomSteps[newZoomIndex].unit - 
+    this._band._zoomSteps[currentZoomIndex].unit;
+
+  return netIntervalChange;
+};
+
+
+/*==================================================
+ *  Hot Zone Ether
+ *==================================================
+ */
+ 
+Timeline.HotZoneEther = function(params) {
+    this._params = params;
+    this._interval = params.interval;
+    this._pixelsPerInterval = params.pixelsPerInterval;
+    this._theme = params.theme;
+};
+
+Timeline.HotZoneEther.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    this._unit = timeline.getUnit();
+    
+    this._zones = [{
+        startTime:  Number.NEGATIVE_INFINITY,
+        endTime:    Number.POSITIVE_INFINITY,
+        magnify:    1
+    }];
+    var params = this._params;
+    for (var i = 0; i < params.zones.length; i++) {
+        var zone = params.zones[i];
+        var zoneStart = this._unit.parseFromObject(zone.start);
+        var zoneEnd =   this._unit.parseFromObject(zone.end);
+        
+        for (var j = 0; j < this._zones.length && this._unit.compare(zoneEnd, zoneStart) > 0; j++) {
+            var zone2 = this._zones[j];
+            
+            if (this._unit.compare(zoneStart, zone2.endTime) < 0) {
+                if (this._unit.compare(zoneStart, zone2.startTime) > 0) {
+                    this._zones.splice(j, 0, {
+                        startTime:   zone2.startTime,
+                        endTime:     zoneStart,
+                        magnify:     zone2.magnify
+                    });
+                    j++;
+                    
+                    zone2.startTime = zoneStart;
+                }
+                
+                if (this._unit.compare(zoneEnd, zone2.endTime) < 0) {
+                    this._zones.splice(j, 0, {
+                        startTime:  zoneStart,
+                        endTime:    zoneEnd,
+                        magnify:    zone.magnify * zone2.magnify
+                    });
+                    j++;
+                    
+                    zone2.startTime = zoneEnd;
+                    zoneStart = zoneEnd;
+                } else {
+                    zone2.magnify *= zone.magnify;
+                    zoneStart = zone2.endTime;
+                }
+            } // else, try the next existing zone
+        }
+    }
+
+    if ("startsOn" in this._params) {
+        this._start = this._unit.parseFromObject(this._params.startsOn);
+    } else if ("endsOn" in this._params) {
+        this._start = this._unit.parseFromObject(this._params.endsOn);
+        this.shiftPixels(-this._timeline.getPixelLength());
+    } else if ("centersOn" in this._params) {
+        this._start = this._unit.parseFromObject(this._params.centersOn);
+        this.shiftPixels(-this._timeline.getPixelLength() / 2);
+    } else {
+        this._start = this._unit.makeDefaultValue();
+        this.shiftPixels(-this._timeline.getPixelLength() / 2);
+    }
+};
+
+Timeline.HotZoneEther.prototype.setDate = function(date) {
+    this._start = this._unit.cloneValue(date);
+};
+
+Timeline.HotZoneEther.prototype.shiftPixels = function(pixels) {
+    this._start = this.pixelOffsetToDate(pixels);
+};
+
+Timeline.HotZoneEther.prototype.dateToPixelOffset = function(date) {
+    return this._dateDiffToPixelOffset(this._start, date);
+};
+
+Timeline.HotZoneEther.prototype.pixelOffsetToDate = function(pixels) {
+    return this._pixelOffsetToDate(pixels, this._start);
+};
+
+Timeline.HotZoneEther.prototype.zoom = function(zoomIn) {
+  var netIntervalChange = 0;
+  var currentZoomIndex = this._band._zoomIndex;
+  var newZoomIndex = currentZoomIndex;
+
+  if (zoomIn && (currentZoomIndex > 0)) {
+    newZoomIndex = currentZoomIndex - 1;
+  }
+  
+  if (!zoomIn && (currentZoomIndex < (this._band._zoomSteps.length - 1))) {
+    newZoomIndex = currentZoomIndex + 1;
+  }
+
+  this._band._zoomIndex = newZoomIndex;  
+  this._interval = 
+    SimileAjax.DateTime.gregorianUnitLengths[this._band._zoomSteps[newZoomIndex].unit];
+  this._pixelsPerInterval = this._band._zoomSteps[newZoomIndex].pixelsPerInterval;
+  netIntervalChange = this._band._zoomSteps[newZoomIndex].unit - 
+    this._band._zoomSteps[currentZoomIndex].unit;
+
+  return netIntervalChange;
+};
+
+Timeline.HotZoneEther.prototype._dateDiffToPixelOffset = function(fromDate, toDate) {
+    var scale = this._getScale();
+    var fromTime = fromDate;
+    var toTime = toDate;
+    
+    var pixels = 0;
+    if (this._unit.compare(fromTime, toTime) < 0) {
+        var z = 0;
+        while (z < this._zones.length) {
+            if (this._unit.compare(fromTime, this._zones[z].endTime) < 0) {
+                break;
+            }
+            z++;
+        }
+        
+        while (this._unit.compare(fromTime, toTime) < 0) {
+            var zone = this._zones[z];
+            var toTime2 = this._unit.earlier(toTime, zone.endTime);
+            
+            pixels += (this._unit.compare(toTime2, fromTime) / (scale / zone.magnify));
+            
+            fromTime = toTime2;
+            z++;
+        }
+    } else {
+        var z = this._zones.length - 1;
+        while (z >= 0) {
+            if (this._unit.compare(fromTime, this._zones[z].startTime) > 0) {
+                break;
+            }
+            z--;
+        }
+        
+        while (this._unit.compare(fromTime, toTime) > 0) {
+            var zone = this._zones[z];
+            var toTime2 = this._unit.later(toTime, zone.startTime);
+            
+            pixels += (this._unit.compare(toTime2, fromTime) / (scale / zone.magnify));
+            
+            fromTime = toTime2;
+            z--;
+        }
+    }
+    return pixels;
+};
+
+Timeline.HotZoneEther.prototype._pixelOffsetToDate = function(pixels, fromDate) {
+    var scale = this._getScale();
+    var time = fromDate;
+    if (pixels > 0) {
+        var z = 0;
+        while (z < this._zones.length) {
+            if (this._unit.compare(time, this._zones[z].endTime) < 0) {
+                break;
+            }
+            z++;
+        }
+        
+        while (pixels > 0) {
+            var zone = this._zones[z];
+            var scale2 = scale / zone.magnify;
+            
+            if (zone.endTime == Number.POSITIVE_INFINITY) {
+                time = this._unit.change(time, pixels * scale2);
+                pixels = 0;
+            } else {
+                var pixels2 = this._unit.compare(zone.endTime, time) / scale2;
+                if (pixels2 > pixels) {
+                    time = this._unit.change(time, pixels * scale2);
+                    pixels = 0;
+                } else {
+                    time = zone.endTime;
+                    pixels -= pixels2;
+                }
+            }
+            z++;
+        }
+    } else {
+        var z = this._zones.length - 1;
+        while (z >= 0) {
+            if (this._unit.compare(time, this._zones[z].startTime) > 0) {
+                break;
+            }
+            z--;
+        }
+        
+        pixels = -pixels;
+        while (pixels > 0) {
+            var zone = this._zones[z];
+            var scale2 = scale / zone.magnify;
+            
+            if (zone.startTime == Number.NEGATIVE_INFINITY) {
+                time = this._unit.change(time, -pixels * scale2);
+                pixels = 0;
+            } else {
+                var pixels2 = this._unit.compare(time, zone.startTime) / scale2;
+                if (pixels2 > pixels) {
+                    time = this._unit.change(time, -pixels * scale2);
+                    pixels = 0;
+                } else {
+                    time = zone.startTime;
+                    pixels -= pixels2;
+                }
+            }
+            z--;
+        }
+    }
+    return time;
+};
+
+Timeline.HotZoneEther.prototype._getScale = function() {
+    return this._interval / this._pixelsPerInterval;
+};
+/*==================================================
+ *  Gregorian Ether Painter
+ *==================================================
+ */
+ 
+Timeline.GregorianEtherPainter = function(params) {
+    this._params = params;
+    this._theme = params.theme;
+    this._unit = params.unit;
+    this._multiple = ("multiple" in params) ? params.multiple : 1;
+};
+
+Timeline.GregorianEtherPainter.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._backgroundLayer = band.createLayerDiv(0);
+    this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
+    this._backgroundLayer.className = 'timeline-ether-bg';
+  //  this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
+
+    
+    this._markerLayer = null;
+    this._lineLayer = null;
+    
+    var align = ("align" in this._params && this._params.align != undefined) ? this._params.align : 
+        this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
+    var showLine = ("showLine" in this._params) ? this._params.showLine : 
+        this._theme.ether.interval.line.show;
+        
+    this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
+        this._timeline, this._band, this._theme, align, showLine);
+        
+    this._highlight = new Timeline.EtherHighlight(
+        this._timeline, this._band, this._theme, this._backgroundLayer);
+}
+
+Timeline.GregorianEtherPainter.prototype.setHighlight = function(startDate, endDate, orthogonalOffset, orthogonalExtent) {
+    this._highlight.position(startDate, endDate, orthogonalOffset, orthogonalExtent);
+}
+
+Timeline.GregorianEtherPainter.prototype.paint = function() {
+    if (this._markerLayer) {
+        this._band.removeLayerDiv(this._markerLayer);
+    }
+    this._markerLayer = this._band.createLayerDiv(100);
+    this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
+    this._markerLayer.style.display = "none";
+    
+    if (this._lineLayer) {
+        this._band.removeLayerDiv(this._lineLayer);
+    }
+    this._lineLayer = this._band.createLayerDiv(1);
+    this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
+    this._lineLayer.style.display = "none";
+    
+    var minDate = this._band.getMinDate();
+    var maxDate = this._band.getMaxDate();
+    
+    var timeZone = this._band.getTimeZone();
+    var labeller = this._band.getLabeller();
+    
+    SimileAjax.DateTime.roundDownToInterval(minDate, this._unit, timeZone, this._multiple, this._theme.firstDayOfWeek);
+    
+    var p = this;
+    var incrementDate = function(date) {
+        for (var i = 0; i < p._multiple; i++) {
+            SimileAjax.DateTime.incrementByInterval(date, p._unit);
+        }
+    };
+    
+    while (minDate.getTime() < maxDate.getTime()) {
+        this._intervalMarkerLayout.createIntervalMarker(
+            minDate, labeller, this._unit, this._markerLayer, this._lineLayer);
+            
+        incrementDate(minDate);
+    }
+    this._markerLayer.style.display = "block";
+    this._lineLayer.style.display = "block";
+};
+
+Timeline.GregorianEtherPainter.prototype.softPaint = function() {
+};
+
+Timeline.GregorianEtherPainter.prototype.zoom = function(netIntervalChange) {
+  if (netIntervalChange != 0) {
+    this._unit += netIntervalChange;
+  }
+};
+
+
+/*==================================================
+ *  Hot Zone Gregorian Ether Painter
+ *==================================================
+ */
+ 
+Timeline.HotZoneGregorianEtherPainter = function(params) {
+    this._params = params;
+    this._theme = params.theme;
+    
+    this._zones = [{
+        startTime:  Number.NEGATIVE_INFINITY,
+        endTime:    Number.POSITIVE_INFINITY,
+        unit:       params.unit,
+        multiple:   1
+    }];
+    for (var i = 0; i < params.zones.length; i++) {
+        var zone = params.zones[i];
+        var zoneStart = SimileAjax.DateTime.parseGregorianDateTime(zone.start).getTime();
+        var zoneEnd = SimileAjax.DateTime.parseGregorianDateTime(zone.end).getTime();
+        
+        for (var j = 0; j < this._zones.length && zoneEnd > zoneStart; j++) {
+            var zone2 = this._zones[j];
+            
+            if (zoneStart < zone2.endTime) {
+                if (zoneStart > zone2.startTime) {
+                    this._zones.splice(j, 0, {
+                        startTime:   zone2.startTime,
+                        endTime:     zoneStart,
+                        unit:        zone2.unit,
+                        multiple:    zone2.multiple
+                    });
+                    j++;
+                    
+                    zone2.startTime = zoneStart;
+                }
+                
+                if (zoneEnd < zone2.endTime) {
+                    this._zones.splice(j, 0, {
+                        startTime:  zoneStart,
+                        endTime:    zoneEnd,
+                        unit:       zone.unit,
+                        multiple:   (zone.multiple) ? zone.multiple : 1
+                    });
+                    j++;
+                    
+                    zone2.startTime = zoneEnd;
+                    zoneStart = zoneEnd;
+                } else {
+                    zone2.multiple = zone.multiple;
+                    zone2.unit = zone.unit;
+                    zoneStart = zone2.endTime;
+                }
+            } // else, try the next existing zone
+        }
+    }
+};
+
+Timeline.HotZoneGregorianEtherPainter.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._backgroundLayer = band.createLayerDiv(0);
+    this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
+    this._backgroundLayer.className ='timeline-ether-bg';
+    //this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
+    
+    this._markerLayer = null;
+    this._lineLayer = null;
+    
+    var align = ("align" in this._params && this._params.align != undefined) ? this._params.align : 
+        this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
+    var showLine = ("showLine" in this._params) ? this._params.showLine : 
+        this._theme.ether.interval.line.show;
+        
+    this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
+        this._timeline, this._band, this._theme, align, showLine);
+        
+    this._highlight = new Timeline.EtherHighlight(
+        this._timeline, this._band, this._theme, this._backgroundLayer);
+}
+
+Timeline.HotZoneGregorianEtherPainter.prototype.setHighlight = function(startDate, endDate) {
+    this._highlight.position(startDate, endDate);
+}
+
+Timeline.HotZoneGregorianEtherPainter.prototype.paint = function() {
+    if (this._markerLayer) {
+        this._band.removeLayerDiv(this._markerLayer);
+    }
+    this._markerLayer = this._band.createLayerDiv(100);
+    this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
+    this._markerLayer.style.display = "none";
+    
+    if (this._lineLayer) {
+        this._band.removeLayerDiv(this._lineLayer);
+    }
+    this._lineLayer = this._band.createLayerDiv(1);
+    this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
+    this._lineLayer.style.display = "none";
+    
+    var minDate = this._band.getMinDate();
+    var maxDate = this._band.getMaxDate();
+    
+    var timeZone = this._band.getTimeZone();
+    var labeller = this._band.getLabeller();
+    
+    var p = this;
+    var incrementDate = function(date, zone) {
+        for (var i = 0; i < zone.multiple; i++) {
+            SimileAjax.DateTime.incrementByInterval(date, zone.unit);
+        }
+    };
+    
+    var zStart = 0;
+    while (zStart < this._zones.length) {
+        if (minDate.getTime() < this._zones[zStart].endTime) {
+            break;
+        }
+        zStart++;
+    }
+    var zEnd = this._zones.length - 1;
+    while (zEnd >= 0) {
+        if (maxDate.getTime() > this._zones[zEnd].startTime) {
+            break;
+        }
+        zEnd--;
+    }
+    
+    for (var z = zStart; z <= zEnd; z++) {
+        var zone = this._zones[z];
+        
+        var minDate2 = new Date(Math.max(minDate.getTime(), zone.startTime));
+        var maxDate2 = new Date(Math.min(maxDate.getTime(), zone.endTime));
+        
+        SimileAjax.DateTime.roundDownToInterval(minDate2, zone.unit, timeZone, zone.multiple, this._theme.firstDayOfWeek);
+        SimileAjax.DateTime.roundUpToInterval(maxDate2, zone.unit, timeZone, zone.multiple, this._theme.firstDayOfWeek);
+        
+        while (minDate2.getTime() < maxDate2.getTime()) {
+            this._intervalMarkerLayout.createIntervalMarker(
+                minDate2, labeller, zone.unit, this._markerLayer, this._lineLayer);
+                
+            incrementDate(minDate2, zone);
+        }
+    }
+    this._markerLayer.style.display = "block";
+    this._lineLayer.style.display = "block";
+};
+
+Timeline.HotZoneGregorianEtherPainter.prototype.softPaint = function() {
+};
+
+Timeline.HotZoneGregorianEtherPainter.prototype.zoom = function(netIntervalChange) {
+  if (netIntervalChange != 0) {
+    for (var i = 0; i < this._zones.length; ++i) {
+      if (this._zones[i]) {
+        this._zones[i].unit += netIntervalChange;
+      }
+    }
+  }
+};
+
+/*==================================================
+ *  Year Count Ether Painter
+ *==================================================
+ */
+ 
+Timeline.YearCountEtherPainter = function(params) {
+    this._params = params;
+    this._theme = params.theme;
+    this._startDate = SimileAjax.DateTime.parseGregorianDateTime(params.startDate);
+    this._multiple = ("multiple" in params) ? params.multiple : 1;
+};
+
+Timeline.YearCountEtherPainter.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._backgroundLayer = band.createLayerDiv(0);
+    this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
+    this._backgroundLayer.className = 'timeline-ether-bg';
+   // this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
+    
+    this._markerLayer = null;
+    this._lineLayer = null;
+    
+    var align = ("align" in this._params) ? this._params.align : 
+        this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
+    var showLine = ("showLine" in this._params) ? this._params.showLine : 
+        this._theme.ether.interval.line.show;
+        
+    this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
+        this._timeline, this._band, this._theme, align, showLine);
+        
+    this._highlight = new Timeline.EtherHighlight(
+        this._timeline, this._band, this._theme, this._backgroundLayer);
+};
+
+Timeline.YearCountEtherPainter.prototype.setHighlight = function(startDate, endDate) {
+    this._highlight.position(startDate, endDate);
+};
+
+Timeline.YearCountEtherPainter.prototype.paint = function() {
+    if (this._markerLayer) {
+        this._band.removeLayerDiv(this._markerLayer);
+    }
+    this._markerLayer = this._band.createLayerDiv(100);
+    this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
+    this._markerLayer.style.display = "none";
+    
+    if (this._lineLayer) {
+        this._band.removeLayerDiv(this._lineLayer);
+    }
+    this._lineLayer = this._band.createLayerDiv(1);
+    this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
+    this._lineLayer.style.display = "none";
+    
+    var minDate = new Date(this._startDate.getTime());
+    var maxDate = this._band.getMaxDate();
+    var yearDiff = this._band.getMinDate().getUTCFullYear() - this._startDate.getUTCFullYear();
+    minDate.setUTCFullYear(this._band.getMinDate().getUTCFullYear() - yearDiff % this._multiple);
+    
+    var p = this;
+    var incrementDate = function(date) {
+        for (var i = 0; i < p._multiple; i++) {
+            SimileAjax.DateTime.incrementByInterval(date, SimileAjax.DateTime.YEAR);
+        }
+    };
+    var labeller = {
+        labelInterval: function(date, intervalUnit) {
+            var diff = date.getUTCFullYear() - p._startDate.getUTCFullYear();
+            return {
+                text: diff,
+                emphasized: diff == 0
+            };
+        }
+    };
+    
+    while (minDate.getTime() < maxDate.getTime()) {
+        this._intervalMarkerLayout.createIntervalMarker(
+            minDate, labeller, SimileAjax.DateTime.YEAR, this._markerLayer, this._lineLayer);
+            
+        incrementDate(minDate);
+    }
+    this._markerLayer.style.display = "block";
+    this._lineLayer.style.display = "block";
+};
+
+Timeline.YearCountEtherPainter.prototype.softPaint = function() {
+};
+
+/*==================================================
+ *  Quarterly Ether Painter
+ *==================================================
+ */
+ 
+Timeline.QuarterlyEtherPainter = function(params) {
+    this._params = params;
+    this._theme = params.theme;
+    this._startDate = SimileAjax.DateTime.parseGregorianDateTime(params.startDate);
+};
+
+Timeline.QuarterlyEtherPainter.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._backgroundLayer = band.createLayerDiv(0);
+    this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
+    this._backgroundLayer.className = 'timeline-ether-bg';
+ //   this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
+    
+    this._markerLayer = null;
+    this._lineLayer = null;
+    
+    var align = ("align" in this._params) ? this._params.align : 
+        this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
+    var showLine = ("showLine" in this._params) ? this._params.showLine : 
+        this._theme.ether.interval.line.show;
+        
+    this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
+        this._timeline, this._band, this._theme, align, showLine);
+        
+    this._highlight = new Timeline.EtherHighlight(
+        this._timeline, this._band, this._theme, this._backgroundLayer);
+};
+
+Timeline.QuarterlyEtherPainter.prototype.setHighlight = function(startDate, endDate) {
+    this._highlight.position(startDate, endDate);
+};
+
+Timeline.QuarterlyEtherPainter.prototype.paint = function() {
+    if (this._markerLayer) {
+        this._band.removeLayerDiv(this._markerLayer);
+    }
+    this._markerLayer = this._band.createLayerDiv(100);
+    this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
+    this._markerLayer.style.display = "none";
+    
+    if (this._lineLayer) {
+        this._band.removeLayerDiv(this._lineLayer);
+    }
+    this._lineLayer = this._band.createLayerDiv(1);
+    this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
+    this._lineLayer.style.display = "none";
+    
+    var minDate = new Date(0);
+    var maxDate = this._band.getMaxDate();
+    
+    minDate.setUTCFullYear(Math.max(this._startDate.getUTCFullYear(), this._band.getMinDate().getUTCFullYear()));
+    minDate.setUTCMonth(this._startDate.getUTCMonth());
+    
+    var p = this;
+    var incrementDate = function(date) {
+        date.setUTCMonth(date.getUTCMonth() + 3);
+    };
+    var labeller = {
+        labelInterval: function(date, intervalUnit) {
+            var quarters = (4 + (date.getUTCMonth() - p._startDate.getUTCMonth()) / 3) % 4;
+            if (quarters != 0) {
+                return { text: "Q" + (quarters + 1), emphasized: false };
+            } else {
+                return { text: "Y" + (date.getUTCFullYear() - p._startDate.getUTCFullYear() + 1), emphasized: true };
+            }
+        }
+    };
+    
+    while (minDate.getTime() < maxDate.getTime()) {
+        this._intervalMarkerLayout.createIntervalMarker(
+            minDate, labeller, SimileAjax.DateTime.YEAR, this._markerLayer, this._lineLayer);
+            
+        incrementDate(minDate);
+    }
+    this._markerLayer.style.display = "block";
+    this._lineLayer.style.display = "block";
+};
+
+Timeline.QuarterlyEtherPainter.prototype.softPaint = function() {
+};
+
+/*==================================================
+ *  Ether Interval Marker Layout
+ *==================================================
+ */
+ 
+Timeline.EtherIntervalMarkerLayout = function(timeline, band, theme, align, showLine) {
+    var horizontal = timeline.isHorizontal();
+    if (horizontal) {
+        if (align == "Top") {
+            this.positionDiv = function(div, offset) {
+                div.style.left = offset + "px";
+                div.style.top = "0px";
+            };
+        } else {
+            this.positionDiv = function(div, offset) {
+                div.style.left = offset + "px";
+                div.style.bottom = "0px";
+            };
+        }
+    } else {
+        if (align == "Left") {
+            this.positionDiv = function(div, offset) {
+                div.style.top = offset + "px";
+                div.style.left = "0px";
+            };
+        } else {
+            this.positionDiv = function(div, offset) {
+                div.style.top = offset + "px";
+                div.style.right = "0px";
+            };
+        }
+    }
+    
+    var markerTheme = theme.ether.interval.marker;
+    var lineTheme = theme.ether.interval.line;
+    var weekendTheme = theme.ether.interval.weekend;
+    
+    var stylePrefix = (horizontal ? "h" : "v") + align;
+    var labelStyler = markerTheme[stylePrefix + "Styler"];
+    var emphasizedLabelStyler = markerTheme[stylePrefix + "EmphasizedStyler"];
+    var day = SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.DAY];
+    
+    this.createIntervalMarker = function(date, labeller, unit, markerDiv, lineDiv) {
+        var offset = Math.round(band.dateToPixelOffset(date));
+
+        if (showLine && unit != SimileAjax.DateTime.WEEK) {
+            var divLine = timeline.getDocument().createElement("div");
+            divLine.className = "timeline-ether-lines";
+
+            if (lineTheme.opacity < 100) {
+                SimileAjax.Graphics.setOpacity(divLine, lineTheme.opacity);
+            }
+            
+            if (horizontal) {
+				//divLine.className += " timeline-ether-lines-vertical";
+				divLine.style.left = offset + "px";
+            } else {
+				//divLine.className += " timeline-ether-lines-horizontal";
+                divLine.style.top = offset + "px";
+            }
+            lineDiv.appendChild(divLine);
+        }
+        if (unit == SimileAjax.DateTime.WEEK) {
+            var firstDayOfWeek = theme.firstDayOfWeek;
+            
+            var saturday = new Date(date.getTime() + (6 - firstDayOfWeek - 7) * day);
+            var monday = new Date(saturday.getTime() + 2 * day);
+            
+            var saturdayPixel = Math.round(band.dateToPixelOffset(saturday));
+            var mondayPixel = Math.round(band.dateToPixelOffset(monday));
+            var length = Math.max(1, mondayPixel - saturdayPixel);
+            
+            var divWeekend = timeline.getDocument().createElement("div");            
+			divWeekend.className = 'timeline-ether-weekends'
+
+            if (weekendTheme.opacity < 100) {
+                SimileAjax.Graphics.setOpacity(divWeekend, weekendTheme.opacity);
+            }
+            
+            if (horizontal) {				
+                divWeekend.style.left = saturdayPixel + "px";
+                divWeekend.style.width = length + "px";                
+            } else {				
+                divWeekend.style.top = saturdayPixel + "px";
+                divWeekend.style.height = length + "px";                
+            }
+            lineDiv.appendChild(divWeekend);
+        }
+        
+        var label = labeller.labelInterval(date, unit);
+        
+        var div = timeline.getDocument().createElement("div");
+        div.innerHTML = label.text;
+        
+        
+        
+		div.className = 'timeline-date-label'
+		if(label.emphasized) div.className += ' timeline-date-label-em'
+		
+        this.positionDiv(div, offset);
+        markerDiv.appendChild(div);
+        
+        return div;
+    };
+};
+
+/*==================================================
+ *  Ether Highlight Layout
+ *==================================================
+ */
+ 
+Timeline.EtherHighlight = function(timeline, band, theme, backgroundLayer) {
+    var horizontal = timeline.isHorizontal();
+    
+    this._highlightDiv = null;
+    this._createHighlightDiv = function() {
+        if (this._highlightDiv == null) {
+            this._highlightDiv = timeline.getDocument().createElement("div");
+            this._highlightDiv.setAttribute("name", "ether-highlight"); // for debugging
+            this._highlightDiv.className = 'timeline-ether-highlight'            
+            
+            var opacity = theme.ether.highlightOpacity;
+            if (opacity < 100) {
+                SimileAjax.Graphics.setOpacity(this._highlightDiv, opacity);
+            }
+            
+            backgroundLayer.appendChild(this._highlightDiv);
+        }
+    }
+    
+    this.position = function(startDate, endDate, orthogonalOffset, orthogonalExtent) {
+        orthogonalOffset = orthogonalOffset || 0;
+        orthogonalExtent = orthogonalExtent || 1.0;
+        
+        this._createHighlightDiv();
+        
+        var startPixel = Math.round(band.dateToPixelOffset(startDate));
+        var endPixel = Math.round(band.dateToPixelOffset(endDate));
+        var length = Math.max(endPixel - startPixel, 3);
+        var totalWidth = band.getViewWidth() - 4;
+        if (horizontal) {
+            this._highlightDiv.style.left = startPixel + "px";
+            this._highlightDiv.style.width = length + "px";
+            this._highlightDiv.style.top = Math.round(orthogonalOffset * totalWidth) + "px";
+            this._highlightDiv.style.height = Math.round(orthogonalExtent * totalWidth) + "px";
+        } else {
+            this._highlightDiv.style.top = startPixel + "px";
+            this._highlightDiv.style.height = length + "px";
+            this._highlightDiv.style.left = Math.round(orthogonalOffset * totalWidth) + "px";
+            this._highlightDiv.style.width = Math.round(orthogonalExtent * totalWidth) + "px";
+        }
+    }
+};
+/*==================================================
+ *  Event Utils
+ *==================================================
+ */
+Timeline.EventUtils = {};
+
+Timeline.EventUtils.getNewEventID = function() {
+    // global across page
+    if (this._lastEventID == null) {
+        this._lastEventID = 0;
+    }
+    
+    this._lastEventID += 1;
+    return "e" + this._lastEventID;
+};
+
+Timeline.EventUtils.decodeEventElID = function(elementID) {
+    /*==================================================
+     * 
+     * Use this function to decode an event element's id on a band (label div,
+     * tape div or icon img).
+     *
+     * Returns {band: <bandObj>, evt: <eventObj>} 
+     *
+     * To enable a single event listener to monitor everything
+     * on a Timeline, a set format is used for the id's of the 
+     * elements on the Timeline--
+     * 
+     * element id format for labels, icons, tapes:
+     *   labels: label-tl-<timelineID>-<band_index>-<evt.id>
+     *    icons: icon-tl-<timelineID>-<band_index>-<evt.id>
+     *    tapes: tape1-tl-<timelineID>-<band_index>-<evt.id>
+     *           tape2-tl-<timelineID>-<band_index>-<evt.id>
+     *           // some events have more than one tape
+     *    highlight: highlight1-tl-<timelineID>-<band_index>-<evt.id>
+     *               highlight2-tl-<timelineID>-<band_index>-<evt.id>
+     *           // some events have more than one highlight div (future) 
+     * Note: use split('-') to get array of the format's parts
+     * 
+     * You can then retrieve the timeline object and event object
+     * by using Timeline.getTimeline, Timeline.getBand, or
+     * Timeline.getEvent and passing in the element's id
+     *
+     *==================================================
+     */
+    
+    var parts = elementID.split('-');
+    if (parts[1] != 'tl') {
+        alert("Internal Timeline problem 101, please consult support");
+        return {band: null, evt: null}; // early return
+    }
+    
+    var timeline = Timeline.getTimelineFromID(parts[2]);
+    var band = timeline.getBand(parts[3]);
+    var evt = band.getEventSource.getEvent(parts[4]);
+    
+    return {band: band, evt: evt};
+};
+
+Timeline.EventUtils.encodeEventElID = function(timeline, band, elType, evt) {
+    // elType should be one of {label | icon | tapeN | highlightN}
+    return elType + "-tl-" + timeline.timelineID + 
+       "-" + band.getIndex() + "-" + evt.getID();
+};
+/*==================================================
+ *  Gregorian Date Labeller
+ *==================================================
+ */
+
+Timeline.GregorianDateLabeller = function(locale, timeZone) {
+    this._locale = locale;
+    this._timeZone = timeZone;
+};
+
+Timeline.GregorianDateLabeller.monthNames = [];
+Timeline.GregorianDateLabeller.dayNames = [];
+Timeline.GregorianDateLabeller.labelIntervalFunctions = [];
+
+Timeline.GregorianDateLabeller.getMonthName = function(month, locale) {
+    return Timeline.GregorianDateLabeller.monthNames[locale][month];
+};
+
+Timeline.GregorianDateLabeller.prototype.labelInterval = function(date, intervalUnit) {
+    var f = Timeline.GregorianDateLabeller.labelIntervalFunctions[this._locale];
+    if (f == null) {
+        f = Timeline.GregorianDateLabeller.prototype.defaultLabelInterval;
+    }
+    return f.call(this, date, intervalUnit);
+};
+
+Timeline.GregorianDateLabeller.prototype.labelPrecise = function(date) {
+    return SimileAjax.DateTime.removeTimeZoneOffset(
+        date, 
+        this._timeZone //+ (new Date().getTimezoneOffset() / 60)
+    ).toUTCString();
+};
+
+Timeline.GregorianDateLabeller.prototype.defaultLabelInterval = function(date, intervalUnit) {
+    var text;
+    var emphasized = false;
+    
+    date = SimileAjax.DateTime.removeTimeZoneOffset(date, this._timeZone);
+    
+    switch(intervalUnit) {
+    case SimileAjax.DateTime.MILLISECOND:
+        text = date.getUTCMilliseconds();
+        break;
+    case SimileAjax.DateTime.SECOND:
+        text = date.getUTCSeconds();
+        break;
+    case SimileAjax.DateTime.MINUTE:
+        var m = date.getUTCMinutes();
+        if (m == 0) {
+            text = date.getUTCHours() + ":00";
+            emphasized = true;
+        } else {
+            text = m;
+        }
+        break;
+    case SimileAjax.DateTime.HOUR:
+        text = date.getUTCHours() + "hr";
+        break;
+    case SimileAjax.DateTime.DAY:
+        text = Timeline.GregorianDateLabeller.getMonthName(date.getUTCMonth(), this._locale) + " " + date.getUTCDate();
+        break;
+    case SimileAjax.DateTime.WEEK:
+        text = Timeline.GregorianDateLabeller.getMonthName(date.getUTCMonth(), this._locale) + " " + date.getUTCDate();
+        break;
+    case SimileAjax.DateTime.MONTH:
+        var m = date.getUTCMonth();
+        if (m != 0) {
+            text = Timeline.GregorianDateLabeller.getMonthName(m, this._locale);
+            break;
+        } // else, fall through
+    case SimileAjax.DateTime.YEAR:
+    case SimileAjax.DateTime.DECADE:
+    case SimileAjax.DateTime.CENTURY:
+    case SimileAjax.DateTime.MILLENNIUM:
+        var y = date.getUTCFullYear();
+        if (y > 0) {
+            text = date.getUTCFullYear();
+        } else {
+            text = (1 - y) + "BC";
+        }
+        emphasized = 
+            (intervalUnit == SimileAjax.DateTime.MONTH) ||
+            (intervalUnit == SimileAjax.DateTime.DECADE && y % 100 == 0) || 
+            (intervalUnit == SimileAjax.DateTime.CENTURY && y % 1000 == 0);
+        break;
+    default:
+        text = date.toUTCString();
+    }
+    return { text: text, emphasized: emphasized };
+}
+
+/*==================================================
+ *  Default Event Source
+ *==================================================
+ */
+
+
+Timeline.DefaultEventSource = function(eventIndex) {
+    this._events = (eventIndex instanceof Object) ? eventIndex : new SimileAjax.EventIndex();
+    this._listeners = [];
+};
+
+Timeline.DefaultEventSource.prototype.addListener = function(listener) {
+    this._listeners.push(listener);
+};
+
+Timeline.DefaultEventSource.prototype.removeListener = function(listener) {
+    for (var i = 0; i < this._listeners.length; i++) {
+        if (this._listeners[i] == listener) {
+            this._listeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+Timeline.DefaultEventSource.prototype.loadXML = function(xml, url) {
+    var base = this._getBaseURL(url);
+    
+    var wikiURL = xml.documentElement.getAttribute("wiki-url");
+    var wikiSection = xml.documentElement.getAttribute("wiki-section");
+
+    var dateTimeFormat = xml.documentElement.getAttribute("date-time-format");
+    var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
+
+    var node = xml.documentElement.firstChild;
+    var added = false;
+    while (node != null) {
+        if (node.nodeType == 1) {
+            var description = "";
+            if (node.firstChild != null && node.firstChild.nodeType == 3) {
+                description = node.firstChild.nodeValue;
+            }
+            // instant event: default is true. Or use values from isDuration or durationEvent
+            var instant = (node.getAttribute("isDuration")    === null &&
+                           node.getAttribute("durationEvent") === null) ||
+                          node.getAttribute("isDuration") == "false" ||
+                          node.getAttribute("durationEvent") == "false";
+            
+            var evt = new Timeline.DefaultEventSource.Event( {
+                          id: node.getAttribute("id"),
+                       start: parseDateTimeFunction(node.getAttribute("start")),
+                         end: parseDateTimeFunction(node.getAttribute("end")),
+                 latestStart: parseDateTimeFunction(node.getAttribute("latestStart")),
+                 earliestEnd: parseDateTimeFunction(node.getAttribute("earliestEnd")),
+                     instant: instant,
+                        text: node.getAttribute("title"),
+                 description: description,
+                       image: this._resolveRelativeURL(node.getAttribute("image"), base),
+                        link: this._resolveRelativeURL(node.getAttribute("link") , base),
+                        icon: this._resolveRelativeURL(node.getAttribute("icon") , base),
+                       color: node.getAttribute("color"),
+                   textColor: node.getAttribute("textColor"),
+                   hoverText: node.getAttribute("hoverText"),
+                   classname: node.getAttribute("classname"),
+                   tapeImage: node.getAttribute("tapeImage"),
+                  tapeRepeat: node.getAttribute("tapeRepeat"),
+                     caption: node.getAttribute("caption"),
+                     eventID: node.getAttribute("eventID"),
+                    trackNum: node.getAttribute("trackNum")
+            });
+
+            evt._node = node;
+            evt.getProperty = function(name) {
+                return this._node.getAttribute(name);
+            };
+            evt.setWikiInfo(wikiURL, wikiSection);
+            
+            this._events.add(evt);
+            
+            added = true;
+        }
+        node = node.nextSibling;
+    }
+
+    if (added) {
+        this._fire("onAddMany", []);
+    }
+};
+
+
+Timeline.DefaultEventSource.prototype.loadJSON = function(data, url) {
+    var base = this._getBaseURL(url);
+    var added = false;  
+    if (data && data.events){
+        var wikiURL = ("wikiURL" in data) ? data.wikiURL : null;
+        var wikiSection = ("wikiSection" in data) ? data.wikiSection : null;
+    
+        var dateTimeFormat = ("dateTimeFormat" in data) ? data.dateTimeFormat : null;
+        var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
+       
+        for (var i=0; i < data.events.length; i++){
+            var evnt = data.events[i];
+            
+            // New feature: attribute synonyms. The following attribute names are interchangable.
+            // The shorter names enable smaller load files.
+            //    eid -- eventID
+            //      s -- start
+            //      e -- end
+            //     ls -- latestStart
+            //     ee -- earliestEnd
+            //      d -- description
+            //     de -- durationEvent
+            //      t -- title,
+            //      c -- classname
+
+            // Fixing issue 33:
+            // instant event: default (for JSON only) is false. Or use values from isDuration or durationEvent
+            // isDuration was negated (see issue 33, so keep that interpretation
+            var instant = evnt.isDuration ||
+                          (('durationEvent' in evnt) && !evnt.durationEvent) ||
+                          (('de' in evnt) && !evnt.de);
+            var evt = new Timeline.DefaultEventSource.Event({
+                          id: ("id" in evnt) ? evnt.id : undefined,
+                       start: parseDateTimeFunction(evnt.start || evnt.s),
+                         end: parseDateTimeFunction(evnt.end || evnt.e),
+                 latestStart: parseDateTimeFunction(evnt.latestStart || evnt.ls),
+                 earliestEnd: parseDateTimeFunction(evnt.earliestEnd || evnt.ee),
+                     instant: instant,
+                        text: evnt.title || evnt.t,
+                 description: evnt.description || evnt.d,
+                       image: this._resolveRelativeURL(evnt.image, base),
+                        link: this._resolveRelativeURL(evnt.link , base),
+                        icon: this._resolveRelativeURL(evnt.icon , base),
+                       color: evnt.color,                                      
+                   textColor: evnt.textColor,
+                   hoverText: evnt.hoverText,
+                   classname: evnt.classname || evnt.c,
+                   tapeImage: evnt.tapeImage,
+                  tapeRepeat: evnt.tapeRepeat,
+                     caption: evnt.caption,
+                     eventID: evnt.eventID  || evnt.eid,
+                    trackNum: evnt.trackNum
+            });
+            evt._obj = evnt;
+            evt.getProperty = function(name) {
+                return this._obj[name];
+            };
+            evt.setWikiInfo(wikiURL, wikiSection);
+
+            this._events.add(evt);
+            added = true;
+        }
+    }
+   
+    if (added) {
+        this._fire("onAddMany", []);
+    }
+};
+
+/*
+ *  Contributed by Morten Frederiksen, http://www.wasab.dk/morten/
+ */
+Timeline.DefaultEventSource.prototype.loadSPARQL = function(xml, url) {
+    var base = this._getBaseURL(url);
+    
+    var dateTimeFormat = 'iso8601';
+    var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
+
+    if (xml == null) {
+        return;
+    }
+    
+    /*
+     *  Find <results> tag
+     */
+    var node = xml.documentElement.firstChild;
+    while (node != null && (node.nodeType != 1 || node.nodeName != 'results')) {
+        node = node.nextSibling;
+    }
+    
+    var wikiURL = null;
+    var wikiSection = null;
+    if (node != null) {
+        wikiURL = node.getAttribute("wiki-url");
+        wikiSection = node.getAttribute("wiki-section");
+        
+        node = node.firstChild;
+    }
+    
+    var added = false;
+    while (node != null) {
+        if (node.nodeType == 1) {
+            var bindings = { };
+            var binding = node.firstChild;
+            while (binding != null) {
+                if (binding.nodeType == 1 && 
+                    binding.firstChild != null && 
+                    binding.firstChild.nodeType == 1 && 
+                    binding.firstChild.firstChild != null && 
+                    binding.firstChild.firstChild.nodeType == 3) {
+                    bindings[binding.getAttribute('name')] = binding.firstChild.firstChild.nodeValue;
+                }
+                binding = binding.nextSibling;
+            }
+            
+            if (bindings["start"] == null && bindings["date"] != null) {
+                bindings["start"] = bindings["date"];
+            }
+            
+            // instant event: default is true. Or use values from isDuration or durationEvent
+            var instant = (bindings["isDuration"]    === null &&
+                           bindings["durationEvent"] === null) ||
+                          bindings["isDuration"] == "false" ||
+                          bindings["durationEvent"] == "false";
+
+            var evt = new Timeline.DefaultEventSource.Event({
+                          id: bindings["id"],
+                       start: parseDateTimeFunction(bindings["start"]),
+                         end: parseDateTimeFunction(bindings["end"]),
+                 latestStart: parseDateTimeFunction(bindings["latestStart"]),
+                 earliestEnd: parseDateTimeFunction(bindings["earliestEnd"]),
+                     instant: instant, // instant
+                        text: bindings["title"], // text
+                 description: bindings["description"],
+                       image: this._resolveRelativeURL(bindings["image"], base),
+                        link: this._resolveRelativeURL(bindings["link"] , base),
+                        icon: this._resolveRelativeURL(bindings["icon"] , base),
+                       color: bindings["color"],                                
+                   textColor: bindings["textColor"],
+                   hoverText: bindings["hoverText"],
+                     caption: bindings["caption"],
+                   classname: bindings["classname"],
+                   tapeImage: bindings["tapeImage"],
+                  tapeRepeat: bindings["tapeRepeat"],
+                     eventID: bindings["eventID"],
+                    trackNum: bindings["trackNum"]
+            });
+            evt._bindings = bindings;
+            evt.getProperty = function(name) {
+                return this._bindings[name];
+            };
+            evt.setWikiInfo(wikiURL, wikiSection);
+            
+            this._events.add(evt);
+            added = true;
+        }
+        node = node.nextSibling;
+    }
+
+    if (added) {
+        this._fire("onAddMany", []);
+    }
+};
+
+Timeline.DefaultEventSource.prototype.add = function(evt) {
+    this._events.add(evt);
+    this._fire("onAddOne", [evt]);
+};
+
+Timeline.DefaultEventSource.prototype.addMany = function(events) {
+    for (var i = 0; i < events.length; i++) {
+        this._events.add(events[i]);
+    }
+    this._fire("onAddMany", []);
+};
+
+Timeline.DefaultEventSource.prototype.clear = function() {
+    this._events.removeAll();
+    this._fire("onClear", []);
+};
+
+Timeline.DefaultEventSource.prototype.getEvent = function(id) {
+    return this._events.getEvent(id);
+};
+
+Timeline.DefaultEventSource.prototype.getEventIterator = function(startDate, endDate) {
+    return this._events.getIterator(startDate, endDate);
+};
+
+Timeline.DefaultEventSource.prototype.getEventReverseIterator = function(startDate, endDate) {
+    return this._events.getReverseIterator(startDate, endDate);
+};
+
+Timeline.DefaultEventSource.prototype.getAllEventIterator = function() {
+    return this._events.getAllIterator();
+};
+
+Timeline.DefaultEventSource.prototype.getCount = function() {
+    return this._events.getCount();
+};
+
+Timeline.DefaultEventSource.prototype.getEarliestDate = function() {
+    return this._events.getEarliestDate();
+};
+
+Timeline.DefaultEventSource.prototype.getLatestDate = function() {
+    return this._events.getLatestDate();
+};
+
+Timeline.DefaultEventSource.prototype._fire = function(handlerName, args) {
+    for (var i = 0; i < this._listeners.length; i++) {
+        var listener = this._listeners[i];
+        if (handlerName in listener) {
+            try {
+                listener[handlerName].apply(listener, args);
+            } catch (e) {
+                SimileAjax.Debug.exception(e);
+            }
+        }
+    }
+};
+
+Timeline.DefaultEventSource.prototype._getBaseURL = function(url) {
+    if (url.indexOf("://") < 0) {
+        var url2 = this._getBaseURL(document.location.href);
+        if (url.substr(0,1) == "/") {
+            url = url2.substr(0, url2.indexOf("/", url2.indexOf("://") + 3)) + url;
+        } else {
+            url = url2 + url;
+        }
+    }
+    
+    var i = url.lastIndexOf("/");
+    if (i < 0) {
+        return "";
+    } else {
+        return url.substr(0, i+1);
+    }
+};
+
+Timeline.DefaultEventSource.prototype._resolveRelativeURL = function(url, base) {
+    if (url == null || url == "") {
+        return url;
+    } else if (url.indexOf("://") > 0) {
+        return url;
+    } else if (url.substr(0,1) == "/") {
+        return base.substr(0, base.indexOf("/", base.indexOf("://") + 3)) + url;
+    } else {
+        return base + url;
+    }
+};
+
+
+Timeline.DefaultEventSource.Event = function(args) {
+  //
+  // Attention developers!
+  // If you add a new event attribute, please be sure to add it to
+  // all three load functions: loadXML, loadSPARCL, loadJSON. 
+  // Thanks!
+  //
+  // args is a hash/object. It supports the following keys. Most are optional
+  //   id            -- an internal id. Really shouldn't be used by events.
+  //                    Timeline library clients should use eventID
+  //   eventID       -- For use by library client when writing custom painters or
+  //                    custom fillInfoBubble    
+  //   start
+  //   end
+  //   latestStart
+  //   earliestEnd
+  //   instant      -- boolean. Controls precise/non-precise logic & duration/instant issues
+  //   text         -- event source attribute 'title' -- used as the label on Timelines and in bubbles.
+  //   description  -- used in bubbles   
+  //   image        -- used in bubbles
+  //   link         -- used in bubbles
+  //   icon         -- on the Timeline
+  //   color        -- Timeline label and tape color
+  //   textColor    -- Timeline label color, overrides color attribute
+  //   hoverText    -- deprecated, here for backwards compatibility.
+  //                   Superceeded by caption
+  //   caption      -- tooltip-like caption on the Timeline. Uses HTML title attribute 
+  //   classname    -- used to set classname in Timeline. Enables better CSS selector rules
+  //   tapeImage    -- background image of the duration event's tape div on the Timeline
+  //   tapeRepeat   -- repeat attribute for tapeImage. {repeat | repeat-x | repeat-y }
+       
+  function cleanArg(arg) {
+      // clean up an arg
+      return (args[arg] != null && args[arg] != "") ? args[arg] : null;
+  }
+   
+  var id = args.id ? args.id.trim() : "";
+  this._id = id.length > 0 ? id : Timeline.EventUtils.getNewEventID();
+  
+  this._instant = args.instant || (args.end == null);
+  
+  this._start = args.start;
+  this._end = (args.end != null) ? args.end : args.start;
+  
+  this._latestStart = (args.latestStart != null) ?
+                       args.latestStart : (args.instant ? this._end : this._start);
+  this._earliestEnd = (args.earliestEnd != null) ? args.earliestEnd : this._end;
+  
+  // check sanity of dates since incorrect dates will later cause calculation errors
+  // when painting
+  var err=[];
+  if (this._start > this._latestStart) {
+          this._latestStart = this._start;
+          err.push("start is > latestStart");}
+  if (this._start > this._earliestEnd) {
+          this._earliestEnd = this._latestStart;
+          err.push("start is > earliestEnd");}
+  if (this._start > this._end) {
+          this._end = this._earliestEnd;
+          err.push("start is > end");}
+  if (this._latestStart > this._earliestEnd) {
+          this._earliestEnd = this._latestStart;
+          err.push("latestStart is > earliestEnd");}
+  if (this._latestStart > this._end) {
+          this._end = this._earliestEnd;
+          err.push("latestStart is > end");}
+  if (this._earliestEnd > this._end) {
+          this._end = this._earliestEnd;
+          err.push("earliestEnd is > end");}  
+  
+  this._eventID = cleanArg('eventID');
+  this._text = (args.text != null) ? SimileAjax.HTML.deEntify(args.text) : ""; // Change blank titles to ""
+  if (err.length > 0) {
+          this._text += " PROBLEM: " + err.join(", ");
+  }
+
+  this._description = SimileAjax.HTML.deEntify(args.description);
+  this._image = cleanArg('image');
+  this._link =  cleanArg('link');
+  this._title = cleanArg('hoverText');
+  this._title = cleanArg('caption');
+  
+  this._icon = cleanArg('icon');
+  this._color = cleanArg('color');      
+  this._textColor = cleanArg('textColor');
+  this._classname = cleanArg('classname');
+  this._tapeImage = cleanArg('tapeImage');
+  this._tapeRepeat = cleanArg('tapeRepeat');
+  this._trackNum = cleanArg('trackNum');
+  if (this._trackNum != null) {
+      this._trackNum = parseInt(this._trackNum);
+  }
+    
+  this._wikiURL = null;
+  this._wikiSection = null;
+};
+
+Timeline.DefaultEventSource.Event.prototype = {
+    getID:          function() { return this._id; },
+    
+    isInstant:      function() { return this._instant; },
+    isImprecise:    function() { return this._start != this._latestStart || this._end != this._earliestEnd; },
+    
+    getStart:       function() { return this._start; },
+    getEnd:         function() { return this._end; },
+    getLatestStart: function() { return this._latestStart; },
+    getEarliestEnd: function() { return this._earliestEnd; },
+    
+    getEventID:     function() { return this._eventID; },
+    getText:        function() { return this._text; }, // title
+    getDescription: function() { return this._description; },
+    getImage:       function() { return this._image; },
+    getLink:        function() { return this._link; },
+    
+    getIcon:        function() { return this._icon; },
+    getColor:       function() { return this._color; },
+    getTextColor:   function() { return this._textColor; },
+    getClassName:   function() { return this._classname; },
+    getTapeImage:   function() { return this._tapeImage; },
+    getTapeRepeat:  function() { return this._tapeRepeat; },
+    getTrackNum:    function() { return this._trackNum; },
+    
+    getProperty:    function(name) { return null; },
+    
+    getWikiURL:     function() { return this._wikiURL; },
+    getWikiSection: function() { return this._wikiSection; },
+    setWikiInfo: function(wikiURL, wikiSection) {
+        this._wikiURL = wikiURL;
+        this._wikiSection = wikiSection;
+    },
+    
+    fillDescription: function(elmt) {
+        elmt.innerHTML = this._description;
+    },
+    fillWikiInfo: function(elmt) {
+        // Many bubbles will not support a wiki link. 
+        // 
+        // Strategy: assume no wiki link. If we do have
+        // enough parameters for one, then create it.
+        elmt.style.display = "none"; // default
+        
+        if (this._wikiURL == null || this._wikiSection == null) {
+          return; // EARLY RETURN
+        }
+
+        // create the wikiID from the property or from the event text (the title)      
+        var wikiID = this.getProperty("wikiID");
+        if (wikiID == null || wikiID.length == 0) {
+            wikiID = this.getText(); // use the title as the backup wiki id
+        }
+        
+        if (wikiID == null || wikiID.length == 0) {
+          return; // No wikiID. Thus EARLY RETURN
+        }
+          
+        // ready to go...
+        elmt.style.display = "inline";
+        wikiID = wikiID.replace(/\s/g, "_");
+        var url = this._wikiURL + this._wikiSection.replace(/\s/g, "_") + "/" + wikiID;
+        var a = document.createElement("a");
+        a.href = url;
+        a.target = "new";
+        a.innerHTML = Timeline.strings[Timeline.clientLocale].wikiLinkLabel;
+        
+        elmt.appendChild(document.createTextNode("["));
+        elmt.appendChild(a);
+        elmt.appendChild(document.createTextNode("]"));
+    },
+    
+    fillTime: function(elmt, labeller) {
+        if (this._instant) {
+            if (this.isImprecise()) {
+                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
+                elmt.appendChild(elmt.ownerDocument.createElement("br"));
+                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
+            } else {
+                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
+            }
+        } else {
+            if (this.isImprecise()) {
+                elmt.appendChild(elmt.ownerDocument.createTextNode(
+                    labeller.labelPrecise(this._start) + " ~ " + labeller.labelPrecise(this._latestStart)));
+                elmt.appendChild(elmt.ownerDocument.createElement("br"));
+                elmt.appendChild(elmt.ownerDocument.createTextNode(
+                    labeller.labelPrecise(this._earliestEnd) + " ~ " + labeller.labelPrecise(this._end)));
+            } else {
+                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
+                elmt.appendChild(elmt.ownerDocument.createElement("br"));
+                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
+            }
+        }
+    },
+    
+    fillInfoBubble: function(elmt, theme, labeller) {
+        var doc = elmt.ownerDocument;
+        
+        var title = this.getText();
+        var link = this.getLink();
+        var image = this.getImage();
+        
+        if (image != null) {
+            var img = doc.createElement("img");
+            img.src = image;
+            
+            theme.event.bubble.imageStyler(img);
+            elmt.appendChild(img);
+        }
+        
+        var divTitle = doc.createElement("div");
+        var textTitle = doc.createTextNode(title);
+        if (link != null) {
+            var a = doc.createElement("a");
+            a.href = link;
+            a.appendChild(textTitle);
+            divTitle.appendChild(a);
+        } else {
+            divTitle.appendChild(textTitle);
+        }
+        theme.event.bubble.titleStyler(divTitle);
+        elmt.appendChild(divTitle);
+        
+        var divBody = doc.createElement("div");
+        this.fillDescription(divBody);
+        theme.event.bubble.bodyStyler(divBody);
+        elmt.appendChild(divBody);
+        
+        var divTime = doc.createElement("div");
+        this.fillTime(divTime, labeller);
+        theme.event.bubble.timeStyler(divTime);
+        elmt.appendChild(divTime);
+        
+        var divWiki = doc.createElement("div");
+        this.fillWikiInfo(divWiki);
+        theme.event.bubble.wikiStyler(divWiki);
+        elmt.appendChild(divWiki);
+    }
+};
+
+
+/*==================================================
+ *  Original Event Painter
+ *==================================================
+ */
+
+/*==================================================
+ * 
+ * To enable a single event listener to monitor everything
+ * on a Timeline, we need a way to map from an event's icon,
+ * label or tape element to the associated timeline, band and
+ * specific event.
+ *
+ * Thus a set format is used for the id's of the 
+ * events' elements on the Timeline--
+ * 
+ * element id format for labels, icons, tapes:
+ *   labels: label-tl-<timelineID>-<band_index>-<evt.id>
+ *    icons: icon-tl-<timelineID>-<band_index>-<evt.id>
+ *    tapes: tape1-tl-<timelineID>-<band_index>-<evt.id>
+ *           tape2-tl-<timelineID>-<band_index>-<evt.id>
+ *           // some events have more than one tape
+ *    highlight: highlight1-tl-<timelineID>-<band_index>-<evt.id>
+ *               highlight2-tl-<timelineID>-<band_index>-<evt.id>
+ *           // some events have more than one highlight div (future) 
+ * You can then retrieve the band/timeline objects and event object
+ * by using Timeline.EventUtils.decodeEventElID
+ *
+ *==================================================
+ */
+ 
+/* 
+ *    eventPaintListener functions receive calls about painting.
+ *    function(band, op, evt, els)
+ *       context: 'this' will be an OriginalEventPainter object.
+ *                It has properties and methods for obtaining
+ *                the relevant band, timeline, etc    
+ *       band = the band being painted
+ *       op = 'paintStarting' // the painter is about to remove
+ *            all previously painted events, if any. It will
+ *            then start painting all of the visible events that
+ *            pass the filter. 
+ *            evt = null, els = null
+ *       op = 'paintEnded' // the painter has finished painting
+ *            all of the visible events that passed the filter
+ *            evt = null, els = null
+ *       op = 'paintedEvent' // the painter just finished painting an event
+ *            evt = event just painted
+ *            els = array of painted elements' divs. Depending on the event,
+ *                  the array could be just a tape or icon (if no label).
+ *                  Or could include label, multiple tape divs (imprecise event),
+ *                  highlight divs. The array is not ordered. The meaning of
+ *                  each el is available by decoding the el's id 
+ *      Note that there may be no paintedEvent calls if no events were visible
+ *      or passed the filter.
+ */
+
+Timeline.OriginalEventPainter = function(params) {
+    this._params = params;
+    this._onSelectListeners = [];
+    this._eventPaintListeners = [];
+    
+    this._filterMatcher = null;
+    this._highlightMatcher = null;
+    this._frc = null;
+    
+    this._eventIdToElmt = {};
+};
+
+Timeline.OriginalEventPainter.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._backLayer = null;
+    this._eventLayer = null;
+    this._lineLayer = null;
+    this._highlightLayer = null;
+    
+    this._eventIdToElmt = null;
+};
+
+Timeline.OriginalEventPainter.prototype.getType = function() {
+    return 'original';
+};
+
+Timeline.OriginalEventPainter.prototype.supportsOrthogonalScrolling = function() {
+    return true;
+};
+
+Timeline.OriginalEventPainter.prototype.addOnSelectListener = function(listener) {
+    this._onSelectListeners.push(listener);
+};
+
+Timeline.OriginalEventPainter.prototype.removeOnSelectListener = function(listener) {
+    for (var i = 0; i < this._onSelectListeners.length; i++) {
+        if (this._onSelectListeners[i] == listener) {
+            this._onSelectListeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+Timeline.OriginalEventPainter.prototype.addEventPaintListener = function(listener) {
+    this._eventPaintListeners.push(listener);
+};
+
+Timeline.OriginalEventPainter.prototype.removeEventPaintListener = function(listener) {
+    for (var i = 0; i < this._eventPaintListeners.length; i++) {
+        if (this._eventPaintListeners[i] == listener) {
+            this._eventPaintListeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+Timeline.OriginalEventPainter.prototype.getFilterMatcher = function() {
+    return this._filterMatcher;
+};
+
+Timeline.OriginalEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
+    this._filterMatcher = filterMatcher;
+};
+
+Timeline.OriginalEventPainter.prototype.getHighlightMatcher = function() {
+    return this._highlightMatcher;
+};
+
+Timeline.OriginalEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
+    this._highlightMatcher = highlightMatcher;
+};
+
+Timeline.OriginalEventPainter.prototype.paint = function() {
+    // Paints the events for a given section of the band--what is
+    // visible on screen and some extra.
+    var eventSource = this._band.getEventSource();
+    if (eventSource == null) {
+        return;
+    }
+    
+    this._eventIdToElmt = {};
+    this._fireEventPaintListeners('paintStarting', null, null);
+    this._prepareForPainting();
+    
+    var metrics = this._computeMetrics();
+    var minDate = this._band.getMinDate();
+    var maxDate = this._band.getMaxDate();
+    
+    var filterMatcher = (this._filterMatcher != null) ? 
+        this._filterMatcher :
+        function(evt) { return true; };
+    var highlightMatcher = (this._highlightMatcher != null) ? 
+        this._highlightMatcher :
+        function(evt) { return -1; };
+    
+    var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
+    while (iterator.hasNext()) {
+        var evt = iterator.next();
+        if (filterMatcher(evt)) {
+            this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
+        }
+    }
+    
+    this._highlightLayer.style.display = "block";
+    this._lineLayer.style.display = "block";
+    this._eventLayer.style.display = "block";
+    // update the band object for max number of tracks in this section of the ether
+    this._band.updateEventTrackInfo(this._tracks.length, metrics.trackIncrement); 
+    this._fireEventPaintListeners('paintEnded', null, null);
+    
+    this._setOrthogonalOffset(metrics);
+};
+
+Timeline.OriginalEventPainter.prototype.softPaint = function() {
+    this._setOrthogonalOffset(this._computeMetrics());
+};
+
+Timeline.OriginalEventPainter.prototype.getOrthogonalExtent = function() {
+    var metrics = this._computeMetrics();
+    return 2 * metrics.trackOffset + this._tracks.length * metrics.trackIncrement;
+};
+
+Timeline.OriginalEventPainter.prototype._setOrthogonalOffset = function(metrics) {
+    var orthogonalOffset = this._band.getViewOrthogonalOffset();
+    
+    this._highlightLayer.style.top = 
+        this._lineLayer.style.top = 
+            this._eventLayer.style.top = 
+                orthogonalOffset + "px";
+};
+
+Timeline.OriginalEventPainter.prototype._computeMetrics = function() {
+     var eventTheme = this._params.theme.event;
+     var trackHeight = Math.max(eventTheme.track.height, eventTheme.tape.height + 
+                         this._frc.getLineHeight());
+     var metrics = {
+            trackOffset: eventTheme.track.offset,
+            trackHeight: trackHeight,
+               trackGap: eventTheme.track.gap,
+         trackIncrement: trackHeight + eventTheme.track.gap,
+                   icon: eventTheme.instant.icon,
+              iconWidth: eventTheme.instant.iconWidth,
+             iconHeight: eventTheme.instant.iconHeight,
+             labelWidth: eventTheme.label.width,
+           maxLabelChar: eventTheme.label.maxLabelChar,
+    impreciseIconMargin: eventTheme.instant.impreciseIconMargin
+     };
+     
+     return metrics;
+};
+
+Timeline.OriginalEventPainter.prototype._prepareForPainting = function() {
+    // Remove everything previously painted: highlight, line and event layers.
+    // Prepare blank layers for painting. 
+    var band = this._band;
+        
+    if (this._backLayer == null) {
+        this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
+        this._backLayer.style.visibility = "hidden";
+        
+        var eventLabelPrototype = document.createElement("span");
+        eventLabelPrototype.className = "timeline-event-label";
+        this._backLayer.appendChild(eventLabelPrototype);
+        this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
+    }
+    this._frc.update();
+    this._tracks = [];
+    
+    if (this._highlightLayer != null) {
+        band.removeLayerDiv(this._highlightLayer);
+    }
+    this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
+    this._highlightLayer.style.display = "none";
+    
+    if (this._lineLayer != null) {
+        band.removeLayerDiv(this._lineLayer);
+    }
+    this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
+    this._lineLayer.style.display = "none";
+    
+    if (this._eventLayer != null) {
+        band.removeLayerDiv(this._eventLayer);
+    }
+    this._eventLayer = band.createLayerDiv(115, "timeline-band-events");
+    this._eventLayer.style.display = "none";
+};
+
+Timeline.OriginalEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isInstant()) {
+        this.paintInstantEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintDurationEvent(evt, metrics, theme, highlightIndex);
+    }
+};
+    
+Timeline.OriginalEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isImprecise()) {
+        this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
+    }
+}
+
+Timeline.OriginalEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isImprecise()) {
+        this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
+    }
+}
+    
+Timeline.OriginalEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    var text = evt.getText();
+    
+    var startDate = evt.getStart();
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
+    var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
+
+    var labelDivClassName = this._getLabelDivClassName(evt);
+    var labelSize = this._frc.computeSize(text, labelDivClassName);
+    var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
+    var labelRight = labelLeft + labelSize.width;
+    
+    var rightEdge = labelRight;
+    var track = this._findFreeTrack(evt, rightEdge);
+    
+    var labelTop = Math.round(
+        metrics.trackOffset + track * metrics.trackIncrement + 
+        metrics.trackHeight / 2 - labelSize.height / 2);
+        
+    var iconElmtData = this._paintEventIcon(evt, track, iconLeftEdge, metrics, theme, 0);
+    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
+        labelSize.height, theme, labelDivClassName, highlightIndex);
+    var els = [iconElmtData.elmt, labelElmtData.elmt];
+
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+    
+    var hDiv = this._createHighlightDiv(highlightIndex, iconElmtData, theme, evt);
+    if (hDiv != null) {els.push(hDiv);}
+    this._fireEventPaintListeners('paintedEvent', evt, els);
+
+    
+    this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
+    this._tracks[track] = iconLeftEdge;
+};
+
+Timeline.OriginalEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    var text = evt.getText();
+    
+    var startDate = evt.getStart();
+    var endDate = evt.getEnd();
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
+    
+    var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
+    var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
+    
+    var labelDivClassName = this._getLabelDivClassName(evt);
+    var labelSize = this._frc.computeSize(text, labelDivClassName);
+    var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
+    var labelRight = labelLeft + labelSize.width;
+    
+    var rightEdge = Math.max(labelRight, endPixel);
+    var track = this._findFreeTrack(evt, rightEdge);
+    var tapeHeight = theme.event.tape.height;
+    var labelTop = Math.round(
+        metrics.trackOffset + track * metrics.trackIncrement + tapeHeight);
+
+    var iconElmtData = this._paintEventIcon(evt, track, iconLeftEdge, metrics, theme, tapeHeight);
+    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
+                        labelSize.height, theme, labelDivClassName, highlightIndex);
+
+    var color = evt.getColor();
+    color = color != null ? color : theme.event.instant.impreciseColor;
+
+    var tapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel, 
+        color, theme.event.instant.impreciseOpacity, metrics, theme, 0);
+    var els = [iconElmtData.elmt, labelElmtData.elmt, tapeElmtData.elmt];    
+    
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+    
+    var hDiv = this._createHighlightDiv(highlightIndex, iconElmtData, theme, evt);
+    if (hDiv != null) {els.push(hDiv);}
+    this._fireEventPaintListeners('paintedEvent', evt, els);
+
+    this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
+    this._tracks[track] = iconLeftEdge;
+};
+
+Timeline.OriginalEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    var text = evt.getText();
+    
+    var startDate = evt.getStart();
+    var endDate = evt.getEnd();
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
+    
+    var labelDivClassName = this._getLabelDivClassName(evt);
+    var labelSize = this._frc.computeSize(text, labelDivClassName);
+    var labelLeft = startPixel;
+    var labelRight = labelLeft + labelSize.width;
+    
+    var rightEdge = Math.max(labelRight, endPixel);
+    var track = this._findFreeTrack(evt, rightEdge);
+    var labelTop = Math.round(
+        metrics.trackOffset + track * metrics.trackIncrement + theme.event.tape.height);
+    
+    var color = evt.getColor();
+    color = color != null ? color : theme.event.duration.color;
+    
+    var tapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel, color, 100, metrics, theme, 0);
+    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
+      labelSize.height, theme, labelDivClassName, highlightIndex);
+    var els = [tapeElmtData.elmt, labelElmtData.elmt];
+    
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+    
+    var hDiv = this._createHighlightDiv(highlightIndex, tapeElmtData, theme, evt);
+    if (hDiv != null) {els.push(hDiv);}
+    this._fireEventPaintListeners('paintedEvent', evt, els);
+    
+    this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
+    this._tracks[track] = startPixel;
+};
+
+Timeline.OriginalEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    var text = evt.getText();
+    
+    var startDate = evt.getStart();
+    var latestStartDate = evt.getLatestStart();
+    var endDate = evt.getEnd();
+    var earliestEndDate = evt.getEarliestEnd();
+    
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
+    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
+    var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
+    
+    var labelDivClassName = this._getLabelDivClassName(evt);
+    var labelSize = this._frc.computeSize(text, labelDivClassName);
+    var labelLeft = latestStartPixel;
+    var labelRight = labelLeft + labelSize.width;
+    
+    var rightEdge = Math.max(labelRight, endPixel);
+    var track = this._findFreeTrack(evt, rightEdge);
+    var labelTop = Math.round(
+        metrics.trackOffset + track * metrics.trackIncrement + theme.event.tape.height);
+    
+    var color = evt.getColor();
+    color = color != null ? color : theme.event.duration.color;
+    
+    // Imprecise events can have two event tapes
+    // The imprecise dates tape, uses opacity to be dimmer than precise dates
+    var impreciseTapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel, 
+        theme.event.duration.impreciseColor,
+        theme.event.duration.impreciseOpacity, metrics, theme, 0);
+    // The precise dates tape, regular (100%) opacity
+    var tapeElmtData = this._paintEventTape(evt, track, latestStartPixel,
+        earliestEndPixel, color, 100, metrics, theme, 1);
+    
+    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop,
+        labelSize.width, labelSize.height, theme, labelDivClassName, highlightIndex);
+    var els = [impreciseTapeElmtData.elmt, tapeElmtData.elmt, labelElmtData.elmt];
+    
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+    
+    var hDiv = this._createHighlightDiv(highlightIndex, tapeElmtData, theme, evt);
+    if (hDiv != null) {els.push(hDiv);}
+    this._fireEventPaintListeners('paintedEvent', evt, els);
+    
+    this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
+    this._tracks[track] = startPixel;
+};
+
+Timeline.OriginalEventPainter.prototype._encodeEventElID = function(elType, evt) {
+    return Timeline.EventUtils.encodeEventElID(this._timeline, this._band, elType, evt);
+};
+
+Timeline.OriginalEventPainter.prototype._findFreeTrack = function(event, rightEdge) {
+    var trackAttribute = event.getTrackNum();
+    if (trackAttribute != null) {
+        return trackAttribute; // early return since event includes track number
+    }
+    
+    // normal case: find an open track
+    for (var i = 0; i < this._tracks.length; i++) {
+        var t = this._tracks[i];
+        if (t > rightEdge) {
+            break;
+        }
+    }
+    return i;
+};
+
+Timeline.OriginalEventPainter.prototype._paintEventIcon = function(evt, iconTrack, left, metrics, theme, tapeHeight) {
+    // If no tape, then paint the icon in the middle of the track.
+    // If there is a tape, paint the icon below the tape + impreciseIconMargin
+    var icon = evt.getIcon();
+    icon = icon != null ? icon : metrics.icon;
+    
+    var top; // top of the icon
+    if (tapeHeight > 0) {
+        top = metrics.trackOffset + iconTrack * metrics.trackIncrement + 
+              tapeHeight + metrics.impreciseIconMargin;
+    } else {
+        var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement +
+                     metrics.trackHeight / 2;
+        top = Math.round(middle - metrics.iconHeight / 2);
+    }
+    var img = SimileAjax.Graphics.createTranslucentImage(icon);
+    var iconDiv = this._timeline.getDocument().createElement("div");
+    iconDiv.className = this._getElClassName('timeline-event-icon', evt, 'icon');
+    iconDiv.id = this._encodeEventElID('icon', evt);
+    iconDiv.style.left = left + "px";
+    iconDiv.style.top = top + "px";
+    iconDiv.appendChild(img);
+
+    if(evt._title != null)
+        iconDiv.title = evt._title;
+
+    this._eventLayer.appendChild(iconDiv);
+    
+    return {
+        left:   left,
+        top:    top,
+        width:  metrics.iconWidth,
+        height: metrics.iconHeight,
+        elmt:   iconDiv
+    };
+};
+
+Timeline.OriginalEventPainter.prototype._paintEventLabel = function(evt, text, left, top, width,
+    height, theme, labelDivClassName, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    
+    var labelDiv = doc.createElement("div");
+    labelDiv.className = labelDivClassName;
+    labelDiv.id = this._encodeEventElID('label', evt);
+    labelDiv.style.left = left + "px";
+    labelDiv.style.width = width + "px";
+    labelDiv.style.top = top + "px";
+    labelDiv.innerHTML = text;
+
+    if(evt._title != null)
+        labelDiv.title = evt._title;    
+
+    var color = evt.getTextColor();
+    if (color == null) {
+        color = evt.getColor();
+    }
+    if (color != null) {
+        labelDiv.style.color = color;
+    }
+    if (theme.event.highlightLabelBackground && highlightIndex >= 0) {
+        labelDiv.style.background = this._getHighlightColor(highlightIndex, theme);
+    }
+    
+    this._eventLayer.appendChild(labelDiv);
+    
+    return {
+        left:   left,
+        top:    top,
+        width:  width,
+        height: height,
+        elmt:   labelDiv
+    };
+};
+
+Timeline.OriginalEventPainter.prototype._paintEventTape = function(
+    evt, iconTrack, startPixel, endPixel, color, opacity, metrics, theme, tape_index) {
+    
+    var tapeWidth = endPixel - startPixel;
+    var tapeHeight = theme.event.tape.height;
+    var top = metrics.trackOffset + iconTrack * metrics.trackIncrement;
+    
+    var tapeDiv = this._timeline.getDocument().createElement("div");
+    tapeDiv.className = this._getElClassName('timeline-event-tape', evt, 'tape');
+    tapeDiv.id = this._encodeEventElID('tape' + tape_index, evt);
+    tapeDiv.style.left = startPixel + "px";
+    tapeDiv.style.width = tapeWidth + "px";
+    tapeDiv.style.height = tapeHeight + "px";
+    tapeDiv.style.top = top + "px";
+
+    if(evt._title != null)
+        tapeDiv.title = evt._title;   
+   
+    if(color != null) {
+        tapeDiv.style.backgroundColor = color;
+    }
+    
+    var backgroundImage = evt.getTapeImage();
+    var backgroundRepeat = evt.getTapeRepeat();
+    backgroundRepeat = backgroundRepeat != null ? backgroundRepeat : 'repeat';
+    if(backgroundImage != null) {
+      tapeDiv.style.backgroundImage = "url(" + backgroundImage + ")";
+      tapeDiv.style.backgroundRepeat = backgroundRepeat;
+    } 	
+    
+    SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
+        
+    this._eventLayer.appendChild(tapeDiv);
+    
+    return {
+        left:   startPixel,
+        top:    top,
+        width:  tapeWidth,
+        height: tapeHeight,
+        elmt:   tapeDiv
+    };
+}
+
+Timeline.OriginalEventPainter.prototype._getLabelDivClassName = function(evt) {
+    return this._getElClassName('timeline-event-label', evt, 'label');
+};
+
+Timeline.OriginalEventPainter.prototype._getElClassName = function(elClassName, evt, prefix) {
+    // Prefix and '_' is added to the event's classname. Set to null for no prefix
+    var evt_classname = evt.getClassName(),
+        pieces = [];
+
+    if (evt_classname) {
+      if (prefix) {pieces.push(prefix + '-' + evt_classname + ' ');}
+      pieces.push(evt_classname + ' ');
+    }
+    pieces.push(elClassName);
+    return(pieces.join(''));
+};
+
+Timeline.OriginalEventPainter.prototype._getHighlightColor = function(highlightIndex, theme) {
+    var highlightColors = theme.event.highlightColors;    
+    return highlightColors[Math.min(highlightIndex, highlightColors.length - 1)];
+};
+
+Timeline.OriginalEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme, evt) {
+    var div = null;
+    if (highlightIndex >= 0) {
+        var doc = this._timeline.getDocument();        
+        var color = this._getHighlightColor(highlightIndex, theme);
+        
+        div = doc.createElement("div");
+        div.className = this._getElClassName('timeline-event-highlight', evt, 'highlight');
+        div.id = this._encodeEventElID('highlight0', evt); // in future will have other
+                                                           // highlight divs for tapes + icons
+        div.style.position = "absolute";
+        div.style.overflow = "hidden";
+        div.style.left =    (dimensions.left - 2) + "px";
+        div.style.width =   (dimensions.width + 4) + "px";
+        div.style.top =     (dimensions.top - 2) + "px";
+        div.style.height =  (dimensions.height + 4) + "px";
+        div.style.background = color;
+        
+        this._highlightLayer.appendChild(div);
+    }
+    return div;
+};
+
+Timeline.OriginalEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
+    var c = SimileAjax.DOM.getPageCoordinates(icon);
+    this._showBubble(
+        c.left + Math.ceil(icon.offsetWidth / 2), 
+        c.top + Math.ceil(icon.offsetHeight / 2),
+        evt
+    );
+    this._fireOnSelect(evt.getID());
+    
+    domEvt.cancelBubble = true;
+    SimileAjax.DOM.cancelEvent(domEvt);
+    return false;
+};
+
+Timeline.OriginalEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
+    if ("pageX" in domEvt) {
+        var x = domEvt.pageX;
+        var y = domEvt.pageY;
+    } else {
+        var c = SimileAjax.DOM.getPageCoordinates(target);
+        var x = domEvt.offsetX + c.left;
+        var y = domEvt.offsetY + c.top;
+    }
+    this._showBubble(x, y, evt);
+    this._fireOnSelect(evt.getID());
+    
+    domEvt.cancelBubble = true;
+    SimileAjax.DOM.cancelEvent(domEvt);
+    return false;
+};
+
+Timeline.OriginalEventPainter.prototype.showBubble = function(evt) {
+    var elmt = this._eventIdToElmt[evt.getID()];
+    if (elmt) {
+        var c = SimileAjax.DOM.getPageCoordinates(elmt);
+        this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, evt);
+    }
+};
+
+Timeline.OriginalEventPainter.prototype._showBubble = function(x, y, evt) {
+    var div = document.createElement("div");
+    var themeBubble = this._params.theme.event.bubble;
+    evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
+    
+    SimileAjax.WindowManager.cancelPopups();
+    SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y,
+        themeBubble.width, null, themeBubble.maxHeight);
+};
+
+Timeline.OriginalEventPainter.prototype._fireOnSelect = function(eventID) {
+    for (var i = 0; i < this._onSelectListeners.length; i++) {
+        this._onSelectListeners[i](eventID);
+    }
+};
+
+Timeline.OriginalEventPainter.prototype._fireEventPaintListeners = function(op, evt, els) {
+    for (var i = 0; i < this._eventPaintListeners.length; i++) {
+        this._eventPaintListeners[i](this._band, op, evt, els);
+    }
+};
+/*==================================================
+ *  Detailed Event Painter
+ *==================================================
+ */
+
+// Note: a number of features from original-painter 
+//       are not yet implemented in detailed painter.
+//       Eg classname, id attributes for icons, labels, tapes
+
+Timeline.DetailedEventPainter = function(params) {
+    this._params = params;
+    this._onSelectListeners = [];
+    
+    this._filterMatcher = null;
+    this._highlightMatcher = null;
+    this._frc = null;
+    
+    this._eventIdToElmt = {};
+};
+
+Timeline.DetailedEventPainter.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._backLayer = null;
+    this._eventLayer = null;
+    this._lineLayer = null;
+    this._highlightLayer = null;
+    
+    this._eventIdToElmt = null;
+};
+
+Timeline.DetailedEventPainter.prototype.getType = function() {
+    return 'detailed';
+};
+
+Timeline.DetailedEventPainter.prototype.addOnSelectListener = function(listener) {
+    this._onSelectListeners.push(listener);
+};
+
+Timeline.DetailedEventPainter.prototype.removeOnSelectListener = function(listener) {
+    for (var i = 0; i < this._onSelectListeners.length; i++) {
+        if (this._onSelectListeners[i] == listener) {
+            this._onSelectListeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+Timeline.DetailedEventPainter.prototype.getFilterMatcher = function() {
+    return this._filterMatcher;
+};
+
+Timeline.DetailedEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
+    this._filterMatcher = filterMatcher;
+};
+
+Timeline.DetailedEventPainter.prototype.getHighlightMatcher = function() {
+    return this._highlightMatcher;
+};
+
+Timeline.DetailedEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
+    this._highlightMatcher = highlightMatcher;
+};
+
+Timeline.DetailedEventPainter.prototype.paint = function() {
+    var eventSource = this._band.getEventSource();
+    if (eventSource == null) {
+        return;
+    }
+    
+    this._eventIdToElmt = {};
+    this._prepareForPainting();
+    
+    var eventTheme = this._params.theme.event;
+    var trackHeight = Math.max(eventTheme.track.height, this._frc.getLineHeight());
+    var metrics = {
+        trackOffset:    Math.round(this._band.getViewWidth() / 2 - trackHeight / 2),
+        trackHeight:    trackHeight,
+        trackGap:       eventTheme.track.gap,
+        trackIncrement: trackHeight + eventTheme.track.gap,
+        icon:           eventTheme.instant.icon,
+        iconWidth:      eventTheme.instant.iconWidth,
+        iconHeight:     eventTheme.instant.iconHeight,
+        labelWidth:     eventTheme.label.width
+    }
+    
+    var minDate = this._band.getMinDate();
+    var maxDate = this._band.getMaxDate();
+    
+    var filterMatcher = (this._filterMatcher != null) ? 
+        this._filterMatcher :
+        function(evt) { return true; };
+    var highlightMatcher = (this._highlightMatcher != null) ? 
+        this._highlightMatcher :
+        function(evt) { return -1; };
+    
+    var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
+    while (iterator.hasNext()) {
+        var evt = iterator.next();
+        if (filterMatcher(evt)) {
+            this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
+        }
+    }
+    
+    this._highlightLayer.style.display = "block";
+    this._lineLayer.style.display = "block";
+    this._eventLayer.style.display = "block";
+    // update the band object for max number of tracks in this section of the ether
+    this._band.updateEventTrackInfo(this._lowerTracks.length + this._upperTracks.length,
+                                 metrics.trackIncrement); 
+};
+
+Timeline.DetailedEventPainter.prototype.softPaint = function() {
+};
+
+Timeline.DetailedEventPainter.prototype._prepareForPainting = function() {
+    var band = this._band;
+        
+    if (this._backLayer == null) {
+        this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
+        this._backLayer.style.visibility = "hidden";
+        
+        var eventLabelPrototype = document.createElement("span");
+        eventLabelPrototype.className = "timeline-event-label";
+        this._backLayer.appendChild(eventLabelPrototype);
+        this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
+    }
+    this._frc.update();
+    this._lowerTracks = [];
+    this._upperTracks = [];
+    
+    if (this._highlightLayer != null) {
+        band.removeLayerDiv(this._highlightLayer);
+    }
+    this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
+    this._highlightLayer.style.display = "none";
+    
+    if (this._lineLayer != null) {
+        band.removeLayerDiv(this._lineLayer);
+    }
+    this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
+    this._lineLayer.style.display = "none";
+    
+    if (this._eventLayer != null) {
+        band.removeLayerDiv(this._eventLayer);
+    }
+    this._eventLayer = band.createLayerDiv(110, "timeline-band-events");
+    this._eventLayer.style.display = "none";
+};
+
+Timeline.DetailedEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isInstant()) {
+        this.paintInstantEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintDurationEvent(evt, metrics, theme, highlightIndex);
+    }
+};
+    
+Timeline.DetailedEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isImprecise()) {
+        this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
+    }
+}
+
+Timeline.DetailedEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isImprecise()) {
+        this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
+    }
+}
+    
+Timeline.DetailedEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    var text = evt.getText();
+    
+    var startDate = evt.getStart();
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
+    var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
+    
+    var labelSize = this._frc.computeSize(text);
+    var iconTrack = this._findFreeTrackForSolid(iconRightEdge, startPixel);
+    var iconElmtData = this._paintEventIcon(evt, iconTrack, iconLeftEdge, metrics, theme);
+    
+    var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
+    var labelTrack = iconTrack;
+    
+    var iconTrackData = this._getTrackData(iconTrack);
+    if (Math.min(iconTrackData.solid, iconTrackData.text) >= labelLeft + labelSize.width) { // label on the same track, to the right of icon
+        iconTrackData.solid = iconLeftEdge;
+        iconTrackData.text = labelLeft;
+    } else { // label on a different track, below icon
+        iconTrackData.solid = iconLeftEdge;
+        
+        labelLeft = startPixel + theme.event.label.offsetFromLine;
+        labelTrack = this._findFreeTrackForText(iconTrack, labelLeft + labelSize.width, function(t) { t.line = startPixel - 2; });
+        this._getTrackData(labelTrack).text = iconLeftEdge;
+        
+        this._paintEventLine(evt, startPixel, iconTrack, labelTrack, metrics, theme);
+    }
+    
+    var labelTop = Math.round(
+        metrics.trackOffset + labelTrack * metrics.trackIncrement + 
+        metrics.trackHeight / 2 - labelSize.height / 2);
+        
+    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
+
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+    
+    this._createHighlightDiv(highlightIndex, iconElmtData, theme);
+    
+    this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
+};
+
+Timeline.DetailedEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    var text = evt.getText();
+    
+    var startDate = evt.getStart();
+    var endDate = evt.getEnd();
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
+    
+    var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
+    var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
+    
+    var labelSize = this._frc.computeSize(text);
+    var iconTrack = this._findFreeTrackForSolid(endPixel, startPixel);
+    
+    var tapeElmtData = this._paintEventTape(evt, iconTrack, startPixel, endPixel, 
+        theme.event.instant.impreciseColor, theme.event.instant.impreciseOpacity, metrics, theme);
+    var iconElmtData = this._paintEventIcon(evt, iconTrack, iconLeftEdge, metrics, theme);
+    
+    var iconTrackData = this._getTrackData(iconTrack);
+    iconTrackData.solid = iconLeftEdge;
+    
+    var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
+    var labelRight = labelLeft + labelSize.width;
+    var labelTrack;
+    if (labelRight < endPixel) {
+        labelTrack = iconTrack;
+    } else {
+        labelLeft = startPixel + theme.event.label.offsetFromLine;
+        labelRight = labelLeft + labelSize.width;
+    
+        labelTrack = this._findFreeTrackForText(iconTrack, labelRight, function(t) { t.line = startPixel - 2; });
+        this._getTrackData(labelTrack).text = iconLeftEdge;
+        
+        this._paintEventLine(evt, startPixel, iconTrack, labelTrack, metrics, theme);
+    }
+    var labelTop = Math.round(
+        metrics.trackOffset + labelTrack * metrics.trackIncrement + 
+        metrics.trackHeight / 2 - labelSize.height / 2);
+        
+    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
+    
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+    
+    this._createHighlightDiv(highlightIndex, iconElmtData, theme);
+    
+    this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
+};
+
+Timeline.DetailedEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    var text = evt.getText();
+    
+    var startDate = evt.getStart();
+    var endDate = evt.getEnd();
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
+    
+    var labelSize = this._frc.computeSize(text);
+    var tapeTrack = this._findFreeTrackForSolid(endPixel);
+    var color = evt.getColor();
+    color = color != null ? color : theme.event.duration.color;
+    
+    var tapeElmtData = this._paintEventTape(evt, tapeTrack, startPixel, endPixel, color, 100, metrics, theme);
+    
+    var tapeTrackData = this._getTrackData(tapeTrack);
+    tapeTrackData.solid = startPixel;
+    
+    var labelLeft = startPixel + theme.event.label.offsetFromLine;
+    var labelTrack = this._findFreeTrackForText(tapeTrack, labelLeft + labelSize.width, function(t) { t.line = startPixel - 2; });
+    this._getTrackData(labelTrack).text = startPixel - 2;
+    
+    this._paintEventLine(evt, startPixel, tapeTrack, labelTrack, metrics, theme);
+    
+    var labelTop = Math.round(
+        metrics.trackOffset + labelTrack * metrics.trackIncrement + 
+        metrics.trackHeight / 2 - labelSize.height / 2);
+        
+    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
+
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+    
+    this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
+    
+    this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
+};
+
+Timeline.DetailedEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    var doc = this._timeline.getDocument();
+    var text = evt.getText();
+    
+    var startDate = evt.getStart();
+    var latestStartDate = evt.getLatestStart();
+    var endDate = evt.getEnd();
+    var earliestEndDate = evt.getEarliestEnd();
+    
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
+    var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
+    var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
+    
+    var labelSize = this._frc.computeSize(text);
+    var tapeTrack = this._findFreeTrackForSolid(endPixel);
+    var color = evt.getColor();
+    color = color != null ? color : theme.event.duration.color;
+    
+    var impreciseTapeElmtData = this._paintEventTape(evt, tapeTrack, startPixel, endPixel, 
+        theme.event.duration.impreciseColor, theme.event.duration.impreciseOpacity, metrics, theme);
+    var tapeElmtData = this._paintEventTape(evt, tapeTrack, latestStartPixel, earliestEndPixel, color, 100, metrics, theme);
+    
+    var tapeTrackData = this._getTrackData(tapeTrack);
+    tapeTrackData.solid = startPixel;
+    
+    var labelLeft = latestStartPixel + theme.event.label.offsetFromLine;
+    var labelTrack = this._findFreeTrackForText(tapeTrack, labelLeft + labelSize.width, function(t) { t.line = latestStartPixel - 2; });
+    this._getTrackData(labelTrack).text = latestStartPixel - 2;
+    
+    this._paintEventLine(evt, latestStartPixel, tapeTrack, labelTrack, metrics, theme);
+    
+    var labelTop = Math.round(
+        metrics.trackOffset + labelTrack * metrics.trackIncrement + 
+        metrics.trackHeight / 2 - labelSize.height / 2);
+        
+    var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
+    
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+    
+    this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
+    
+    this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
+};
+
+Timeline.DetailedEventPainter.prototype._findFreeTrackForSolid = function(solidEdge, softEdge) {
+    for (var i = 0; true; i++) {
+        if (i < this._lowerTracks.length) {
+            var t = this._lowerTracks[i];
+            if (Math.min(t.solid, t.text) > solidEdge && (!(softEdge) || t.line > softEdge)) {
+                return i;
+            }
+        } else {
+            this._lowerTracks.push({
+                solid:  Number.POSITIVE_INFINITY,
+                text:   Number.POSITIVE_INFINITY,
+                line:   Number.POSITIVE_INFINITY
+            });
+            
+            return i;
+        }
+        
+        if (i < this._upperTracks.length) {
+            var t = this._upperTracks[i];
+            if (Math.min(t.solid, t.text) > solidEdge && (!(softEdge) || t.line > softEdge)) {
+                return -1 - i;
+            }
+        } else {
+            this._upperTracks.push({
+                solid:  Number.POSITIVE_INFINITY,
+                text:   Number.POSITIVE_INFINITY,
+                line:   Number.POSITIVE_INFINITY
+            });
+            
+            return -1 - i;
+        }
+    }
+};
+
+Timeline.DetailedEventPainter.prototype._findFreeTrackForText = function(fromTrack, edge, occupiedTrackVisitor) {
+    var extendUp;
+    var index;
+    var firstIndex;
+    var result;
+    
+    if (fromTrack < 0) {
+        extendUp = true;
+        firstIndex = -fromTrack;
+        
+        index = this._findFreeUpperTrackForText(firstIndex, edge);
+        result = -1 - index;
+    } else if (fromTrack > 0) {
+        extendUp = false;
+        firstIndex = fromTrack + 1;
+        
+        index = this._findFreeLowerTrackForText(firstIndex, edge);
+        result = index;
+    } else {
+        var upIndex = this._findFreeUpperTrackForText(0, edge);
+        var downIndex = this._findFreeLowerTrackForText(1, edge);
+        
+        if (downIndex - 1 <= upIndex) {
+            extendUp = false;
+            firstIndex = 1;
+            index = downIndex;
+            result = index;
+        } else {
+            extendUp = true;
+            firstIndex = 0;
+            index = upIndex;
+            result = -1 - index;
+        }
+    }
+    
+    if (extendUp) {
+        if (index == this._upperTracks.length) {
+            this._upperTracks.push({
+                solid:  Number.POSITIVE_INFINITY,
+                text:   Number.POSITIVE_INFINITY,
+                line:   Number.POSITIVE_INFINITY
+            });
+        }
+        for (var i = firstIndex; i < index; i++) {
+            occupiedTrackVisitor(this._upperTracks[i]);
+        }
+    } else {
+        if (index == this._lowerTracks.length) {
+            this._lowerTracks.push({
+                solid:  Number.POSITIVE_INFINITY,
+                text:   Number.POSITIVE_INFINITY,
+                line:   Number.POSITIVE_INFINITY
+            });
+        }
+        for (var i = firstIndex; i < index; i++) {
+            occupiedTrackVisitor(this._lowerTracks[i]);
+        }
+    }
+    return result;
+};
+
+Timeline.DetailedEventPainter.prototype._findFreeLowerTrackForText = function(index, edge) {
+    for (; index < this._lowerTracks.length; index++) {
+        var t = this._lowerTracks[index];
+        if (Math.min(t.solid, t.text) >= edge) {
+            break;
+        }
+    }
+    return index;
+};
+
+Timeline.DetailedEventPainter.prototype._findFreeUpperTrackForText = function(index, edge) {
+    for (; index < this._upperTracks.length; index++) {
+        var t = this._upperTracks[index];
+        if (Math.min(t.solid, t.text) >= edge) {
+            break;
+        }
+    }
+    return index;
+};
+
+Timeline.DetailedEventPainter.prototype._getTrackData = function(index) {
+    return (index < 0) ? this._upperTracks[-index - 1] : this._lowerTracks[index];
+};
+
+Timeline.DetailedEventPainter.prototype._paintEventLine = function(evt, left, startTrack, endTrack, metrics, theme) {
+    var top = Math.round(metrics.trackOffset + startTrack * metrics.trackIncrement + metrics.trackHeight / 2);
+    var height = Math.round(Math.abs(endTrack - startTrack) * metrics.trackIncrement);
+    
+    var lineStyle = "1px solid " + theme.event.label.lineColor;
+    var lineDiv = this._timeline.getDocument().createElement("div");
+	lineDiv.style.position = "absolute";
+    lineDiv.style.left = left + "px";
+    lineDiv.style.width = theme.event.label.offsetFromLine + "px";
+    lineDiv.style.height = height + "px";
+    if (startTrack > endTrack) {
+        lineDiv.style.top = (top - height) + "px";
+        lineDiv.style.borderTop = lineStyle;
+    } else {
+        lineDiv.style.top = top + "px";
+        lineDiv.style.borderBottom = lineStyle;
+    }
+    lineDiv.style.borderLeft = lineStyle;
+    this._lineLayer.appendChild(lineDiv);
+};
+
+Timeline.DetailedEventPainter.prototype._paintEventIcon = function(evt, iconTrack, left, metrics, theme) {
+    var icon = evt.getIcon();
+    icon = icon != null ? icon : metrics.icon;
+    
+    var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement + metrics.trackHeight / 2;
+    var top = Math.round(middle - metrics.iconHeight / 2);
+
+    var img = SimileAjax.Graphics.createTranslucentImage(icon);
+    var iconDiv = this._timeline.getDocument().createElement("div");
+    iconDiv.style.position = "absolute";
+    iconDiv.style.left = left + "px";
+    iconDiv.style.top = top + "px";
+    iconDiv.appendChild(img);
+    iconDiv.style.cursor = "pointer";
+
+    if(evt._title != null)
+        iconDiv.title = evt._title
+
+    this._eventLayer.appendChild(iconDiv);
+    
+    return {
+        left:   left,
+        top:    top,
+        width:  metrics.iconWidth,
+        height: metrics.iconHeight,
+        elmt:   iconDiv
+    };
+};
+
+Timeline.DetailedEventPainter.prototype._paintEventLabel = function(evt, text, left, top, width, height, theme) {
+    var doc = this._timeline.getDocument();
+    
+    var labelBackgroundDiv = doc.createElement("div");
+    labelBackgroundDiv.style.position = "absolute";
+    labelBackgroundDiv.style.left = left + "px";
+    labelBackgroundDiv.style.width = width + "px";
+    labelBackgroundDiv.style.top = top + "px";
+    labelBackgroundDiv.style.height = height + "px";
+    labelBackgroundDiv.style.backgroundColor = theme.event.label.backgroundColor;
+    SimileAjax.Graphics.setOpacity(labelBackgroundDiv, theme.event.label.backgroundOpacity);
+    this._eventLayer.appendChild(labelBackgroundDiv);
+    
+    var labelDiv = doc.createElement("div");
+    labelDiv.style.position = "absolute";
+    labelDiv.style.left = left + "px";
+    labelDiv.style.width = width + "px";
+    labelDiv.style.top = top + "px";
+    labelDiv.innerHTML = text;
+    labelDiv.style.cursor = "pointer";
+
+    if(evt._title != null)
+        labelDiv.title = evt._title;
+    
+    var color = evt.getTextColor();
+    if (color == null) {
+        color = evt.getColor();
+    }
+    if (color != null) {
+        labelDiv.style.color = color;
+    }
+    
+    this._eventLayer.appendChild(labelDiv);
+    
+    return {
+        left:   left,
+        top:    top,
+        width:  width,
+        height: height,
+        elmt:   labelDiv
+    };
+};
+
+Timeline.DetailedEventPainter.prototype._paintEventTape = function(
+    evt, iconTrack, startPixel, endPixel, color, opacity, metrics, theme) {
+    
+    var tapeWidth = endPixel - startPixel;
+    var tapeHeight = theme.event.tape.height;
+    var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement + metrics.trackHeight / 2;
+    var top = Math.round(middle - tapeHeight / 2);
+    
+    var tapeDiv = this._timeline.getDocument().createElement("div");
+    tapeDiv.style.position = "absolute";
+    tapeDiv.style.left = startPixel + "px";
+    tapeDiv.style.width = tapeWidth + "px";
+    tapeDiv.style.top = top + "px";
+    tapeDiv.style.height = tapeHeight + "px";
+    tapeDiv.style.backgroundColor = color;
+    tapeDiv.style.overflow = "hidden";
+    tapeDiv.style.cursor = "pointer";
+
+    if(evt._title != null)
+        tapeDiv.title = evt._title;
+
+    SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
+    
+    this._eventLayer.appendChild(tapeDiv);
+    
+    return {
+        left:   startPixel,
+        top:    top,
+        width:  tapeWidth,
+        height: tapeHeight,
+        elmt:   tapeDiv
+    };
+}
+
+Timeline.DetailedEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
+    if (highlightIndex >= 0) {
+        var doc = this._timeline.getDocument();
+        var eventTheme = theme.event;
+        
+        var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
+        
+        var div = doc.createElement("div");
+        div.style.position = "absolute";
+        div.style.overflow = "hidden";
+        div.style.left =    (dimensions.left - 2) + "px";
+        div.style.width =   (dimensions.width + 4) + "px";
+        div.style.top =     (dimensions.top - 2) + "px";
+        div.style.height =  (dimensions.height + 4) + "px";
+        div.style.background = color;
+        
+        this._highlightLayer.appendChild(div);
+    }
+};
+
+Timeline.DetailedEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
+    var c = SimileAjax.DOM.getPageCoordinates(icon);
+    this._showBubble(
+        c.left + Math.ceil(icon.offsetWidth / 2), 
+        c.top + Math.ceil(icon.offsetHeight / 2),
+        evt
+    );
+    this._fireOnSelect(evt.getID());
+    
+    domEvt.cancelBubble = true;
+    SimileAjax.DOM.cancelEvent(domEvt);
+    return false;
+};
+
+Timeline.DetailedEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
+    if ("pageX" in domEvt) {
+        var x = domEvt.pageX;
+        var y = domEvt.pageY;
+    } else {
+        var c = SimileAjax.DOM.getPageCoordinates(target);
+        var x = domEvt.offsetX + c.left;
+        var y = domEvt.offsetY + c.top;
+    }
+    this._showBubble(x, y, evt);
+    this._fireOnSelect(evt.getID());
+    
+    domEvt.cancelBubble = true;
+    SimileAjax.DOM.cancelEvent(domEvt);
+    return false;
+};
+
+Timeline.DetailedEventPainter.prototype.showBubble = function(evt) {
+    var elmt = this._eventIdToElmt[evt.getID()];
+    if (elmt) {
+        var c = SimileAjax.DOM.getPageCoordinates(elmt);
+        this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, evt);
+    }
+};
+
+Timeline.DetailedEventPainter.prototype._showBubble = function(x, y, evt) {
+    var div = document.createElement("div");
+    var themeBubble = this._params.theme.event.bubble;
+    evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
+    
+    SimileAjax.WindowManager.cancelPopups();
+    SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, 
+       themeBubble.width, null, themeBubble.maxHeight);
+};
+
+Timeline.DetailedEventPainter.prototype._fireOnSelect = function(eventID) {
+    for (var i = 0; i < this._onSelectListeners.length; i++) {
+        this._onSelectListeners[i](eventID);
+    }
+};
+/*==================================================
+ *  Overview Event Painter
+ *==================================================
+ */
+
+Timeline.OverviewEventPainter = function(params) {
+    this._params = params;
+    this._onSelectListeners = [];
+    
+    this._filterMatcher = null;
+    this._highlightMatcher = null;
+};
+
+Timeline.OverviewEventPainter.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._eventLayer = null;
+    this._highlightLayer = null;
+};
+
+Timeline.OverviewEventPainter.prototype.getType = function() {
+    return 'overview';
+};
+
+Timeline.OverviewEventPainter.prototype.addOnSelectListener = function(listener) {
+    this._onSelectListeners.push(listener);
+};
+
+Timeline.OverviewEventPainter.prototype.removeOnSelectListener = function(listener) {
+    for (var i = 0; i < this._onSelectListeners.length; i++) {
+        if (this._onSelectListeners[i] == listener) {
+            this._onSelectListeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+Timeline.OverviewEventPainter.prototype.getFilterMatcher = function() {
+    return this._filterMatcher;
+};
+
+Timeline.OverviewEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
+    this._filterMatcher = filterMatcher;
+};
+
+Timeline.OverviewEventPainter.prototype.getHighlightMatcher = function() {
+    return this._highlightMatcher;
+};
+
+Timeline.OverviewEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
+    this._highlightMatcher = highlightMatcher;
+};
+
+Timeline.OverviewEventPainter.prototype.paint = function() {
+    var eventSource = this._band.getEventSource();
+    if (eventSource == null) {
+        return;
+    }
+    
+    this._prepareForPainting();
+    
+    var eventTheme = this._params.theme.event;
+    var metrics = {
+        trackOffset:    eventTheme.overviewTrack.offset,
+        trackHeight:    eventTheme.overviewTrack.height,
+        trackGap:       eventTheme.overviewTrack.gap,
+        trackIncrement: eventTheme.overviewTrack.height + eventTheme.overviewTrack.gap
+    }
+    
+    var minDate = this._band.getMinDate();
+    var maxDate = this._band.getMaxDate();
+    
+    var filterMatcher = (this._filterMatcher != null) ? 
+        this._filterMatcher :
+        function(evt) { return true; };
+    var highlightMatcher = (this._highlightMatcher != null) ? 
+        this._highlightMatcher :
+        function(evt) { return -1; };
+    
+    var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
+    while (iterator.hasNext()) {
+        var evt = iterator.next();
+        if (filterMatcher(evt)) {
+            this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
+        }
+    }
+    
+    this._highlightLayer.style.display = "block";
+    this._eventLayer.style.display = "block";
+    // update the band object for max number of tracks in this section of the ether
+    this._band.updateEventTrackInfo(this._tracks.length, metrics.trackIncrement); 
+};
+
+Timeline.OverviewEventPainter.prototype.softPaint = function() {
+};
+
+Timeline.OverviewEventPainter.prototype._prepareForPainting = function() {
+    var band = this._band;
+        
+    this._tracks = [];
+    
+    if (this._highlightLayer != null) {
+        band.removeLayerDiv(this._highlightLayer);
+    }
+    this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
+    this._highlightLayer.style.display = "none";
+    
+    if (this._eventLayer != null) {
+        band.removeLayerDiv(this._eventLayer);
+    }
+    this._eventLayer = band.createLayerDiv(110, "timeline-band-events");
+    this._eventLayer.style.display = "none";
+};
+
+Timeline.OverviewEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isInstant()) {
+        this.paintInstantEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintDurationEvent(evt, metrics, theme, highlightIndex);
+    }
+};
+
+Timeline.OverviewEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    var startDate = evt.getStart();
+    var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
+    
+    var color = evt.getColor(),
+        klassName = evt.getClassName();
+    if (klassName) {
+      color = null;
+    } else {
+      color = color != null ? color : theme.event.duration.color;
+    }
+    
+    var tickElmtData = this._paintEventTick(evt, startPixel, color, 100, metrics, theme);
+    
+    this._createHighlightDiv(highlightIndex, tickElmtData, theme);
+};
+
+Timeline.OverviewEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    var latestStartDate = evt.getLatestStart();
+    var earliestEndDate = evt.getEarliestEnd();
+    
+    var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
+    var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
+    
+    var tapeTrack = 0;
+    for (; tapeTrack < this._tracks.length; tapeTrack++) {
+        if (earliestEndPixel < this._tracks[tapeTrack]) {
+            break;
+        }
+    }
+    this._tracks[tapeTrack] = earliestEndPixel;
+    
+    var color = evt.getColor(),
+        klassName = evt.getClassName();
+    if (klassName) {
+      color = null;
+    } else {
+      color = color != null ? color : theme.event.duration.color;
+    }
+    
+    var tapeElmtData = this._paintEventTape(evt, tapeTrack, latestStartPixel, earliestEndPixel,
+      color, 100, metrics, theme, klassName);
+    
+    this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
+};
+
+Timeline.OverviewEventPainter.prototype._paintEventTape = function(
+    evt, track, left, right, color, opacity, metrics, theme, klassName) {
+    
+    var top = metrics.trackOffset + track * metrics.trackIncrement;
+    var width = right - left;
+    var height = metrics.trackHeight;
+    
+    var tapeDiv = this._timeline.getDocument().createElement("div");
+    tapeDiv.className = 'timeline-small-event-tape'
+    if (klassName) {tapeDiv.className += ' small-' + klassName;}
+    tapeDiv.style.left = left + "px";
+    tapeDiv.style.width = width + "px";
+    tapeDiv.style.top = top + "px";
+    tapeDiv.style.height = height + "px";
+    
+    if (color) {
+      tapeDiv.style.backgroundColor = color; // set color here if defined by event. Else use css
+    }
+ //   tapeDiv.style.overflow = "hidden";   // now set in css
+ //   tapeDiv.style.position = "absolute";
+    if(opacity<100) SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
+    
+    this._eventLayer.appendChild(tapeDiv);
+    
+    return {
+        left:   left,
+        top:    top,
+        width:  width,
+        height: height,
+        elmt:   tapeDiv
+    };
+}
+
+Timeline.OverviewEventPainter.prototype._paintEventTick = function(
+    evt, left, color, opacity, metrics, theme) {
+    
+    var height = theme.event.overviewTrack.tickHeight;
+    var top = metrics.trackOffset - height;
+    var width = 1;
+    
+    var tickDiv = this._timeline.getDocument().createElement("div");
+	  tickDiv.className = 'timeline-small-event-icon'
+    tickDiv.style.left = left + "px";
+    tickDiv.style.top = top + "px";
+  //  tickDiv.style.width = width + "px";
+  //  tickDiv.style.position = "absolute";
+  //  tickDiv.style.height = height + "px";
+  //  tickDiv.style.backgroundColor = color;
+  //  tickDiv.style.overflow = "hidden";
+
+    var klassName = evt.getClassName()
+    if (klassName) {tickDiv.className +=' small-' + klassName};
+	
+    if(opacity<100) {SimileAjax.Graphics.setOpacity(tickDiv, opacity)};
+    
+    this._eventLayer.appendChild(tickDiv);
+    
+    return {
+        left:   left,
+        top:    top,
+        width:  width,
+        height: height,
+        elmt:   tickDiv
+    };
+}
+
+Timeline.OverviewEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
+    if (highlightIndex >= 0) {
+        var doc = this._timeline.getDocument();
+        var eventTheme = theme.event;
+        
+        var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
+        
+        var div = doc.createElement("div");
+        div.style.position = "absolute";
+        div.style.overflow = "hidden";
+        div.style.left =    (dimensions.left - 1) + "px";
+        div.style.width =   (dimensions.width + 2) + "px";
+        div.style.top =     (dimensions.top - 1) + "px";
+        div.style.height =  (dimensions.height + 2) + "px";
+        div.style.background = color;
+        
+        this._highlightLayer.appendChild(div);
+    }
+};
+
+Timeline.OverviewEventPainter.prototype.showBubble = function(evt) {
+    // not implemented
+};
+/*==================================================
+ *  Compact Event Painter
+ *==================================================
+ */
+
+Timeline.CompactEventPainter = function(params) {
+    this._params = params;
+    this._onSelectListeners = [];
+    
+    this._filterMatcher = null;
+    this._highlightMatcher = null;
+    this._frc = null;
+    
+    this._eventIdToElmt = {};
+};
+
+Timeline.CompactEventPainter.prototype.getType = function() {
+    return 'compact';
+};
+
+Timeline.CompactEventPainter.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._backLayer = null;
+    this._eventLayer = null;
+    this._lineLayer = null;
+    this._highlightLayer = null;
+    
+    this._eventIdToElmt = null;
+};
+
+Timeline.CompactEventPainter.prototype.supportsOrthogonalScrolling = function() {
+    return true;
+};
+
+Timeline.CompactEventPainter.prototype.addOnSelectListener = function(listener) {
+    this._onSelectListeners.push(listener);
+};
+
+Timeline.CompactEventPainter.prototype.removeOnSelectListener = function(listener) {
+    for (var i = 0; i < this._onSelectListeners.length; i++) {
+        if (this._onSelectListeners[i] == listener) {
+            this._onSelectListeners.splice(i, 1);
+            break;
+        }
+    }
+};
+
+Timeline.CompactEventPainter.prototype.getFilterMatcher = function() {
+    return this._filterMatcher;
+};
+
+Timeline.CompactEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
+    this._filterMatcher = filterMatcher;
+};
+
+Timeline.CompactEventPainter.prototype.getHighlightMatcher = function() {
+    return this._highlightMatcher;
+};
+
+Timeline.CompactEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
+    this._highlightMatcher = highlightMatcher;
+};
+
+Timeline.CompactEventPainter.prototype.paint = function() {
+    var eventSource = this._band.getEventSource();
+    if (eventSource == null) {
+        return;
+    }
+    
+    this._eventIdToElmt = {};
+    this._prepareForPainting();
+    
+    var metrics = this._computeMetrics();
+    var minDate = this._band.getMinDate();
+    var maxDate = this._band.getMaxDate();
+    
+    var filterMatcher = (this._filterMatcher != null) ? 
+        this._filterMatcher :
+        function(evt) { return true; };
+        
+    var highlightMatcher = (this._highlightMatcher != null) ? 
+        this._highlightMatcher :
+        function(evt) { return -1; };
+    
+    var iterator = eventSource.getEventIterator(minDate, maxDate);
+    
+    var stackConcurrentPreciseInstantEvents = "stackConcurrentPreciseInstantEvents" in this._params && typeof this._params.stackConcurrentPreciseInstantEvents == "object";
+    var collapseConcurrentPreciseInstantEvents = "collapseConcurrentPreciseInstantEvents" in this._params && this._params.collapseConcurrentPreciseInstantEvents;
+    if (collapseConcurrentPreciseInstantEvents || stackConcurrentPreciseInstantEvents) {
+        var bufferedEvents = [];
+        var previousInstantEvent = null;
+        
+        while (iterator.hasNext()) {
+            var evt = iterator.next();
+            if (filterMatcher(evt)) {
+                if (!evt.isInstant() || evt.isImprecise()) {
+                    this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
+                } else if (previousInstantEvent != null &&
+                        previousInstantEvent.getStart().getTime() == evt.getStart().getTime()) {
+                    bufferedEvents[bufferedEvents.length - 1].push(evt);
+                } else {
+                    bufferedEvents.push([ evt ]);
+                    previousInstantEvent = evt;
+                }
+            }
+        }
+        
+        for (var i = 0; i < bufferedEvents.length; i++) {
+            var compositeEvents = bufferedEvents[i];
+            if (compositeEvents.length == 1) {
+                this.paintEvent(compositeEvents[0], metrics, this._params.theme, highlightMatcher(evt)); 
+            } else {
+                var match = -1;
+                for (var j = 0; match < 0 && j < compositeEvents.length; j++) {
+                    match = highlightMatcher(compositeEvents[j]);
+                }
+                
+                if (stackConcurrentPreciseInstantEvents) {
+                    this.paintStackedPreciseInstantEvents(compositeEvents, metrics, this._params.theme, match);
+                } else {
+                    this.paintCompositePreciseInstantEvents(compositeEvents, metrics, this._params.theme, match);
+                }
+            }
+        }
+    } else {
+        while (iterator.hasNext()) {
+            var evt = iterator.next();
+            if (filterMatcher(evt)) {
+                this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
+            }
+        }
+    }
+    
+    this._highlightLayer.style.display = "block";
+    this._lineLayer.style.display = "block";
+    this._eventLayer.style.display = "block";
+    
+    this._setOrthogonalOffset(metrics);
+};
+
+Timeline.CompactEventPainter.prototype.softPaint = function() {
+    this._setOrthogonalOffset(this._computeMetrics());
+};
+
+Timeline.CompactEventPainter.prototype.getOrthogonalExtent = function() {
+    var metrics = this._computeMetrics();
+    return 2 * metrics.trackOffset + this._tracks.length * metrics.trackHeight;
+};
+
+Timeline.CompactEventPainter.prototype._setOrthogonalOffset = function(metrics) {
+    var orthogonalOffset = this._band.getViewOrthogonalOffset();
+    
+    this._highlightLayer.style.top = 
+        this._lineLayer.style.top = 
+            this._eventLayer.style.top = 
+                orthogonalOffset + "px";
+};
+
+Timeline.CompactEventPainter.prototype._computeMetrics = function() {
+    var theme = this._params.theme;
+    var eventTheme = theme.event;
+    
+    var metrics = {
+        trackOffset:            "trackOffset" in this._params ? this._params.trackOffset : 10,
+        trackHeight:            "trackHeight" in this._params ? this._params.trackHeight : 10,
+        
+        tapeHeight:             theme.event.tape.height,
+        tapeBottomMargin:       "tapeBottomMargin" in this._params ? this._params.tapeBottomMargin : 2,
+        
+        labelBottomMargin:      "labelBottomMargin" in this._params ? this._params.labelBottomMargin : 5,
+        labelRightMargin:       "labelRightMargin" in this._params ? this._params.labelRightMargin : 5,
+        
+        defaultIcon:            eventTheme.instant.icon,
+        defaultIconWidth:       eventTheme.instant.iconWidth,
+        defaultIconHeight:      eventTheme.instant.iconHeight,
+        
+        customIconWidth:        "iconWidth" in this._params ? this._params.iconWidth : eventTheme.instant.iconWidth,
+        customIconHeight:       "iconHeight" in this._params ? this._params.iconHeight : eventTheme.instant.iconHeight,
+        
+        iconLabelGap:           "iconLabelGap" in this._params ? this._params.iconLabelGap : 2,
+        iconBottomMargin:       "iconBottomMargin" in this._params ? this._params.iconBottomMargin : 2
+    };
+    if ("compositeIcon" in this._params) {
+        metrics.compositeIcon = this._params.compositeIcon;
+        metrics.compositeIconWidth = this._params.compositeIconWidth || metrics.customIconWidth;
+        metrics.compositeIconHeight = this._params.compositeIconHeight || metrics.customIconHeight;
+    } else {
+        metrics.compositeIcon = metrics.defaultIcon;
+        metrics.compositeIconWidth = metrics.defaultIconWidth;
+        metrics.compositeIconHeight = metrics.defaultIconHeight;
+    }
+    metrics.defaultStackIcon = "icon" in this._params.stackConcurrentPreciseInstantEvents ?
+        this._params.stackConcurrentPreciseInstantEvents.icon : metrics.defaultIcon;
+    metrics.defaultStackIconWidth = "iconWidth" in this._params.stackConcurrentPreciseInstantEvents ?
+        this._params.stackConcurrentPreciseInstantEvents.iconWidth : metrics.defaultIconWidth;
+    metrics.defaultStackIconHeight = "iconHeight" in this._params.stackConcurrentPreciseInstantEvents ?
+        this._params.stackConcurrentPreciseInstantEvents.iconHeight : metrics.defaultIconHeight;
+    
+    return metrics;
+};
+
+Timeline.CompactEventPainter.prototype._prepareForPainting = function() {
+    var band = this._band;
+        
+    if (this._backLayer == null) {
+        this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
+        this._backLayer.style.visibility = "hidden";
+        
+        var eventLabelPrototype = document.createElement("span");
+        eventLabelPrototype.className = "timeline-event-label";
+        this._backLayer.appendChild(eventLabelPrototype);
+        this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
+    }
+    this._frc.update();
+    this._tracks = [];
+    
+    if (this._highlightLayer != null) {
+        band.removeLayerDiv(this._highlightLayer);
+    }
+    this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
+    this._highlightLayer.style.display = "none";
+    
+    if (this._lineLayer != null) {
+        band.removeLayerDiv(this._lineLayer);
+    }
+    this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
+    this._lineLayer.style.display = "none";
+    
+    if (this._eventLayer != null) {
+        band.removeLayerDiv(this._eventLayer);
+    }
+    this._eventLayer = band.createLayerDiv(115, "timeline-band-events");
+    this._eventLayer.style.display = "none";
+};
+
+Timeline.CompactEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isInstant()) {
+        this.paintInstantEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintDurationEvent(evt, metrics, theme, highlightIndex);
+    }
+};
+    
+Timeline.CompactEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isImprecise()) {
+        this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
+    }
+}
+
+Timeline.CompactEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    if (evt.isImprecise()) {
+        this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
+    } else {
+        this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
+    }
+}
+    
+Timeline.CompactEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    var commonData = {
+        tooltip: evt.getProperty("tooltip") || evt.getText()
+    };
+    
+    var iconData = {
+        url: evt.getIcon()
+    };
+    if (iconData.url == null) {
+        iconData.url = metrics.defaultIcon;
+        iconData.width = metrics.defaultIconWidth;
+        iconData.height = metrics.defaultIconHeight;
+        iconData.className = "timeline-event-icon-default";
+    } else {
+        iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
+        iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
+    }
+    
+    var labelData = {
+        text:       evt.getText(),
+        color:      evt.getTextColor() || evt.getColor(),
+        className:  evt.getClassName()
+    };
+    
+    var result = this.paintTapeIconLabel(
+        evt.getStart(),
+        commonData,
+        null, // no tape data
+        iconData,
+        labelData,
+        metrics,
+        theme,
+        highlightIndex
+    );
+
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
+    };
+    SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
+    
+    this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
+};
+
+Timeline.CompactEventPainter.prototype.paintCompositePreciseInstantEvents = function(events, metrics, theme, highlightIndex) {
+    var evt = events[0];
+    
+    var tooltips = [];
+    for (var i = 0; i < events.length; i++) {
+        tooltips.push(events[i].getProperty("tooltip") || events[i].getText());
+    }
+    var commonData = {
+        tooltip: tooltips.join("; ")
+    };
+    
+    var iconData = {
+        url: metrics.compositeIcon,
+        width: metrics.compositeIconWidth,
+        height: metrics.compositeIconHeight,
+        className: "timeline-event-icon-composite"
+    };
+    
+    var labelData = {
+        text: String.substitute(this._params.compositeEventLabelTemplate, [ events.length ])
+    };
+    
+    var result = this.paintTapeIconLabel(
+        evt.getStart(),
+        commonData,
+        null, // no tape data
+        iconData,
+        labelData,
+        metrics,
+        theme,
+        highlightIndex
+    );
+    
+    var self = this;
+    var clickHandler = function(elmt, domEvt, target) {
+        return self._onClickMultiplePreciseInstantEvent(result.iconElmtData.elmt, domEvt, events);
+    };
+    
+    SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
+    
+    for (var i = 0; i < events.length; i++) {
+        this._eventIdToElmt[events[i].getID()] = result.iconElmtData.elmt;
+    }
+};
+
+Timeline.CompactEventPainter.prototype.paintStackedPreciseInstantEvents = function(events, metrics, theme, highlightIndex) {
+    var limit = "limit" in this._params.stackConcurrentPreciseInstantEvents ? 
+        this._params.stackConcurrentPreciseInstantEvents.limit : 10;
+    var moreMessageTemplate = "moreMessageTemplate" in this._params.stackConcurrentPreciseInstantEvents ? 
+        this._params.stackConcurrentPreciseInstantEvents.moreMessageTemplate : "%0 More Events";
+    var showMoreMessage = limit <= events.length - 2; // We want at least 2 more events above the limit.
+                                                      // Otherwise we'd need the singular case of "1 More Event"
+
+    var band = this._band;
+    var getPixelOffset = function(date) {
+        return Math.round(band.dateToPixelOffset(date));
+    };
+    var getIconData = function(evt) {
+        var iconData = {
+            url: evt.getIcon()
+        };
+        if (iconData.url == null) {
+            iconData.url = metrics.defaultStackIcon;
+            iconData.width = metrics.defaultStackIconWidth;
+            iconData.height = metrics.defaultStackIconHeight;
+            iconData.className = "timeline-event-icon-stack timeline-event-icon-default";
+        } else {
+            iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
+            iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
+            iconData.className = "timeline-event-icon-stack";
+        }
+        return iconData;
+    };
+    
+    var firstIconData = getIconData(events[0]);
+    var horizontalIncrement = 5;
+    var leftIconEdge = 0;
+    var totalLabelWidth = 0;
+    var totalLabelHeight = 0;
+    var totalIconHeight = 0;
+    
+    var records = [];
+    for (var i = 0; i < events.length && (!showMoreMessage || i < limit); i++) {
+        var evt = events[i];
+        var text = evt.getText();
+        var iconData = getIconData(evt);
+        var labelSize = this._frc.computeSize(text);
+        var record = {
+            text:       text,
+            iconData:   iconData,
+            labelSize:  labelSize,
+            iconLeft:   firstIconData.width + i * horizontalIncrement - iconData.width
+        };
+        record.labelLeft = firstIconData.width + i * horizontalIncrement + metrics.iconLabelGap;
+        record.top = totalLabelHeight;
+        records.push(record);
+        
+        leftIconEdge = Math.min(leftIconEdge, record.iconLeft);
+        totalLabelHeight += labelSize.height;
+        totalLabelWidth = Math.max(totalLabelWidth, record.labelLeft + labelSize.width);
+        totalIconHeight = Math.max(totalIconHeight, record.top + iconData.height);
+    }
+    if (showMoreMessage) {
+        var moreMessage = String.substitute(moreMessageTemplate, [ events.length - limit ]);
+    
+        var moreMessageLabelSize = this._frc.computeSize(moreMessage);
+        var moreMessageLabelLeft = firstIconData.width + (limit - 1) * horizontalIncrement + metrics.iconLabelGap;
+        var moreMessageLabelTop = totalLabelHeight;
+        
+        totalLabelHeight += moreMessageLabelSize.height;
+        totalLabelWidth = Math.max(totalLabelWidth, moreMessageLabelLeft + moreMessageLabelSize.width);
+    }
+    totalLabelWidth += metrics.labelRightMargin;
+    totalLabelHeight += metrics.labelBottomMargin;
+    totalIconHeight += metrics.iconBottomMargin;
+    
+    var anchorPixel = getPixelOffset(events[0].getStart());
+    var newTracks = [];
+    
+    var trackCount = Math.ceil(Math.max(totalIconHeight, totalLabelHeight) / metrics.trackHeight);
+    var rightIconEdge = firstIconData.width + (events.length - 1) * horizontalIncrement;
+    for (var i = 0; i < trackCount; i++) {
+        newTracks.push({ start: leftIconEdge, end: rightIconEdge });
+    }
+    var labelTrackCount = Math.ceil(totalLabelHeight / metrics.trackHeight);
+    for (var i = 0; i < labelTrackCount; i++) {
+        var track = newTracks[i];
+        track.end = Math.max(track.end, totalLabelWidth);
+    }
+
+    var firstTrack = this._fitTracks(anchorPixel, newTracks);
+    var verticalPixelOffset = firstTrack * metrics.trackHeight + metrics.trackOffset;
+    
+    var iconStackDiv = this._timeline.getDocument().createElement("div");
+    iconStackDiv.className = 'timeline-event-icon-stack';
+    iconStackDiv.style.position = "absolute";
+    iconStackDiv.style.overflow = "visible";
+    iconStackDiv.style.left = anchorPixel + "px";
+    iconStackDiv.style.top = verticalPixelOffset + "px";
+    iconStackDiv.style.width = rightIconEdge + "px";
+    iconStackDiv.style.height = totalIconHeight + "px";
+    iconStackDiv.innerHTML = "<div style='position: relative'></div>";
+    this._eventLayer.appendChild(iconStackDiv);
+    
+    var self = this;
+    var onMouseOver = function(domEvt) {
+        try {
+            var n = parseInt(this.getAttribute("index"));
+            var childNodes = iconStackDiv.firstChild.childNodes;
+            for (var i = 0; i < childNodes.length; i++) {
+                var child = childNodes[i];
+                if (i == n) {
+                    child.style.zIndex = childNodes.length;
+                } else {
+                    child.style.zIndex = childNodes.length - i;
+                }
+            }
+        } catch (e) {
+        }
+    };
+    var paintEvent = function(index) {
+        var record = records[index];
+        var evt = events[index];
+        var tooltip = evt.getProperty("tooltip") || evt.getText();
+        
+        var labelElmtData = self._paintEventLabel(
+            { tooltip: tooltip },
+            { text: record.text },
+            anchorPixel + record.labelLeft,
+            verticalPixelOffset + record.top,
+            record.labelSize.width, 
+            record.labelSize.height, 
+            theme
+        );
+        labelElmtData.elmt.setAttribute("index", index);
+        labelElmtData.elmt.onmouseover = onMouseOver;
+        
+        var img = SimileAjax.Graphics.createTranslucentImage(record.iconData.url);
+        var iconDiv = self._timeline.getDocument().createElement("div");
+        iconDiv.className = 'timeline-event-icon' + ("className" in record.iconData ? (" " + record.iconData.className) : "");
+        iconDiv.style.left = record.iconLeft + "px";
+        iconDiv.style.top = record.top + "px";
+        iconDiv.style.zIndex = (records.length - index);
+        iconDiv.appendChild(img);
+        iconDiv.setAttribute("index", index);
+        iconDiv.onmouseover = onMouseOver;
+        
+        iconStackDiv.firstChild.appendChild(iconDiv);
+        
+        var clickHandler = function(elmt, domEvt, target) {
+            return self._onClickInstantEvent(labelElmtData.elmt, domEvt, evt);
+        };
+        
+        SimileAjax.DOM.registerEvent(iconDiv, "mousedown", clickHandler);
+        SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
+        
+        self._eventIdToElmt[evt.getID()] = iconDiv;
+    };
+    for (var i = 0; i < records.length; i++) {
+        paintEvent(i);
+    }
+    
+    if (showMoreMessage) {
+        var moreEvents = events.slice(limit);
+        var moreMessageLabelElmtData = this._paintEventLabel(
+            { tooltip: moreMessage },
+            { text: moreMessage },
+            anchorPixel + moreMessageLabelLeft,
+            verticalPixelOffset + moreMessageLabelTop,
+            moreMessageLabelSize.width, 
+            moreMessageLabelSize.height, 
+            theme
+        );
+        
+        var moreMessageClickHandler = function(elmt, domEvt, target) {
+            return self._onClickMultiplePreciseInstantEvent(moreMessageLabelElmtData.elmt, domEvt, moreEvents);
+        };
+        SimileAjax.DOM.registerEvent(moreMessageLabelElmtData.elmt, "mousedown", moreMessageClickHandler);
+        
+        for (var i = 0; i < moreEvents.length; i++) {
+            this._eventIdToElmt[moreEvents[i].getID()] = moreMessageLabelElmtData.elmt;
+        }
+    }
+    //this._createHighlightDiv(highlightIndex, iconElmtData, theme);
+};
+
+Timeline.CompactEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
+    var commonData = {
+        tooltip: evt.getProperty("tooltip") || evt.getText()
+    };
+    
+    var tapeData = {
+        start:          evt.getStart(),
+        end:            evt.getEnd(),
+        latestStart:    evt.getLatestStart(),
+        earliestEnd:    evt.getEarliestEnd(),
+        isInstant:      true
+    };
+    
+    var iconData = {
+        url: evt.getIcon()
+    };
+    if (iconData.url == null) {
+        iconData = null;
+    } else {
+        iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
+        iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
+    }
+    
+    var labelData = {
+        text:       evt.getText(),
+        color:      evt.getTextColor() || evt.getColor(),
+        className:  evt.getClassName()
+    };
+    
+    var result = this.paintTapeIconLabel(
+        evt.getStart(),
+        commonData,
+        tapeData, // no tape data
+        iconData,
+        labelData,
+        metrics,
+        theme,
+        highlightIndex
+    );
+
+    var self = this;
+    var clickHandler = iconData != null ? 
+        function(elmt, domEvt, target) {
+            return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
+        } :
+        function(elmt, domEvt, target) {
+            return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
+        };
+        
+    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(result.impreciseTapeElmtData.elmt, "mousedown", clickHandler);
+    
+    if (iconData != null) {
+        SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
+        this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
+    } else {
+        this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
+    }
+};
+
+Timeline.CompactEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    var commonData = {
+        tooltip: evt.getProperty("tooltip") || evt.getText()
+    };
+    
+    var tapeData = {
+        start:          evt.getStart(),
+        end:            evt.getEnd(),
+        isInstant:      false
+    };
+    
+    var iconData = {
+        url: evt.getIcon()
+    };
+    if (iconData.url == null) {
+        iconData = null;
+    } else {
+        iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
+        iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
+    }
+    
+    var labelData = {
+        text:       evt.getText(),
+        color:      evt.getTextColor() || evt.getColor(),
+        className:  evt.getClassName()
+    };
+    
+    var result = this.paintTapeIconLabel(
+        evt.getLatestStart(),
+        commonData,
+        tapeData, // no tape data
+        iconData,
+        labelData,
+        metrics,
+        theme,
+        highlightIndex
+    );
+
+    var self = this;
+    var clickHandler = iconData != null ? 
+        function(elmt, domEvt, target) {
+            return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
+        } :
+        function(elmt, domEvt, target) {
+            return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
+        };
+        
+    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(result.tapeElmtData.elmt, "mousedown", clickHandler);
+    
+    if (iconData != null) {
+        SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
+        this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
+    } else {
+        this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
+    }
+};
+
+Timeline.CompactEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
+    var commonData = {
+        tooltip: evt.getProperty("tooltip") || evt.getText()
+    };
+    
+    var tapeData = {
+        start:          evt.getStart(),
+        end:            evt.getEnd(),
+        latestStart:    evt.getLatestStart(),
+        earliestEnd:    evt.getEarliestEnd(),
+        isInstant:      false
+    };
+    
+    var iconData = {
+        url: evt.getIcon()
+    };
+    if (iconData.url == null) {
+        iconData = null;
+    } else {
+        iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
+        iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
+    }
+    
+    var labelData = {
+        text:       evt.getText(),
+        color:      evt.getTextColor() || evt.getColor(),
+        className:  evt.getClassName()
+    };
+    
+    var result = this.paintTapeIconLabel(
+        evt.getLatestStart(),
+        commonData,
+        tapeData, // no tape data
+        iconData,
+        labelData,
+        metrics,
+        theme,
+        highlightIndex
+    );
+
+    var self = this;
+    var clickHandler = iconData != null ? 
+        function(elmt, domEvt, target) {
+            return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
+        } :
+        function(elmt, domEvt, target) {
+            return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
+        };
+        
+    SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
+    SimileAjax.DOM.registerEvent(result.tapeElmtData.elmt, "mousedown", clickHandler);
+    
+    if (iconData != null) {
+        SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
+        this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
+    } else {
+        this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
+    }
+};
+
+Timeline.CompactEventPainter.prototype.paintTapeIconLabel = function(
+    anchorDate, 
+    commonData,
+    tapeData, 
+    iconData, 
+    labelData, 
+    metrics, 
+    theme, 
+    highlightIndex
+) {
+    var band = this._band;
+    var getPixelOffset = function(date) {
+        return Math.round(band.dateToPixelOffset(date));
+    };
+    
+    var anchorPixel = getPixelOffset(anchorDate);
+    var newTracks = [];
+    
+    var tapeHeightOccupied = 0;         // how many pixels (vertically) the tape occupies, including bottom margin
+    var tapeTrackCount = 0;             // how many tracks the tape takes up, usually just 1
+    var tapeLastTrackExtraSpace = 0;    // on the last track that the tape occupies, how many pixels are left (for icon and label to occupy as well)
+    if (tapeData != null) {
+        tapeHeightOccupied = metrics.tapeHeight + metrics.tapeBottomMargin;
+        tapeTrackCount = Math.ceil(metrics.tapeHeight / metrics.trackHeight);
+        
+        var tapeEndPixelOffset = getPixelOffset(tapeData.end) - anchorPixel;
+        var tapeStartPixelOffset = getPixelOffset(tapeData.start) - anchorPixel;
+        
+        for (var t = 0; t < tapeTrackCount; t++) {
+            newTracks.push({ start: tapeStartPixelOffset, end: tapeEndPixelOffset });
+        }
+        
+        tapeLastTrackExtraSpace = metrics.trackHeight - (tapeHeightOccupied % metrics.tapeHeight);
+    }
+    
+    var iconStartPixelOffset = 0;        // where the icon starts compared to the anchor pixel; 
+                                         // this can be negative if the icon is center-aligned around the anchor
+    var iconHorizontalSpaceOccupied = 0; // how many pixels the icon take up from the anchor pixel, 
+                                         // including the gap between the icon and the label
+    if (iconData != null) {
+        if ("iconAlign" in iconData && iconData.iconAlign == "center") {
+            iconStartPixelOffset = -Math.floor(iconData.width / 2);
+        }
+        iconHorizontalSpaceOccupied = iconStartPixelOffset + iconData.width + metrics.iconLabelGap;
+        
+        if (tapeTrackCount > 0) {
+            newTracks[tapeTrackCount - 1].end = Math.max(newTracks[tapeTrackCount - 1].end, iconHorizontalSpaceOccupied);
+        }
+        
+        var iconHeight = iconData.height + metrics.iconBottomMargin + tapeLastTrackExtraSpace;
+        while (iconHeight > 0) {
+            newTracks.push({ start: iconStartPixelOffset, end: iconHorizontalSpaceOccupied });
+            iconHeight -= metrics.trackHeight;
+        }
+    }
+    
+    var text = labelData.text;
+    var labelSize = this._frc.computeSize(text);
+    var labelHeight = labelSize.height + metrics.labelBottomMargin + tapeLastTrackExtraSpace;
+    var labelEndPixelOffset = iconHorizontalSpaceOccupied + labelSize.width + metrics.labelRightMargin;
+    if (tapeTrackCount > 0) {
+        newTracks[tapeTrackCount - 1].end = Math.max(newTracks[tapeTrackCount - 1].end, labelEndPixelOffset);
+    }
+    for (var i = 0; labelHeight > 0; i++) {
+        if (tapeTrackCount + i < newTracks.length) {
+            var track = newTracks[tapeTrackCount + i];
+            track.end = labelEndPixelOffset;
+        } else {
+            newTracks.push({ start: 0, end: labelEndPixelOffset });
+        }
+        labelHeight -= metrics.trackHeight;
+    }
+    
+    /*
+     *  Try to fit the new track on top of the existing tracks, then
+     *  render the various elements.
+     */
+    var firstTrack = this._fitTracks(anchorPixel, newTracks);
+    var verticalPixelOffset = firstTrack * metrics.trackHeight + metrics.trackOffset;
+    var result = {};
+    
+    result.labelElmtData = this._paintEventLabel(
+        commonData,
+        labelData,
+        anchorPixel + iconHorizontalSpaceOccupied,
+        verticalPixelOffset + tapeHeightOccupied,
+        labelSize.width, 
+        labelSize.height, 
+        theme
+    );
+    
+    if (tapeData != null) {
+        if ("latestStart" in tapeData || "earliestEnd" in tapeData) {
+            result.impreciseTapeElmtData = this._paintEventTape(
+                commonData,
+                tapeData,
+                metrics.tapeHeight,
+                verticalPixelOffset, 
+                getPixelOffset(tapeData.start),
+                getPixelOffset(tapeData.end),
+                theme.event.duration.impreciseColor,
+                theme.event.duration.impreciseOpacity, 
+                metrics, 
+                theme
+            );
+        }
+        if (!tapeData.isInstant && "start" in tapeData && "end" in tapeData) {
+            result.tapeElmtData = this._paintEventTape(
+                commonData,
+                tapeData,
+                metrics.tapeHeight,
+                verticalPixelOffset,
+                anchorPixel,
+                getPixelOffset("earliestEnd" in tapeData ? tapeData.earliestEnd : tapeData.end), 
+                tapeData.color, 
+                100, 
+                metrics, 
+                theme
+            );
+        }
+    }
+    
+    if (iconData != null) {
+        result.iconElmtData = this._paintEventIcon(
+            commonData,
+            iconData,
+            verticalPixelOffset + tapeHeightOccupied,
+            anchorPixel + iconStartPixelOffset,
+            metrics, 
+            theme
+        );
+    }
+    //this._createHighlightDiv(highlightIndex, iconElmtData, theme);
+    
+    return result;
+};
+
+Timeline.CompactEventPainter.prototype._fitTracks = function(anchorPixel, newTracks) {
+    var firstTrack;
+    for (firstTrack = 0; firstTrack < this._tracks.length; firstTrack++) {
+        var fit = true;
+        for (var j = 0; j < newTracks.length && (firstTrack + j) < this._tracks.length; j++) {
+            var existingTrack = this._tracks[firstTrack + j];
+            var newTrack = newTracks[j];
+            if (anchorPixel + newTrack.start < existingTrack) {
+                fit = false;
+                break;
+            }
+        }
+        
+        if (fit) {
+            break;
+        }
+    }
+    for (var i = 0; i < newTracks.length; i++) {
+        this._tracks[firstTrack + i] = anchorPixel + newTracks[i].end;
+    }
+    
+    return firstTrack;
+};
+
+
+Timeline.CompactEventPainter.prototype._paintEventIcon = function(commonData, iconData, top, left, metrics, theme) {
+    var img = SimileAjax.Graphics.createTranslucentImage(iconData.url);
+    var iconDiv = this._timeline.getDocument().createElement("div");
+    iconDiv.className = 'timeline-event-icon' + ("className" in iconData ? (" " + iconData.className) : "");
+    iconDiv.style.left = left + "px";
+    iconDiv.style.top = top + "px";
+    iconDiv.appendChild(img);
+    
+    if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
+        iconDiv.title = commonData.tooltip;
+    }
+    
+    this._eventLayer.appendChild(iconDiv);
+    
+    return {
+        left:   left,
+        top:    top,
+        width:  metrics.iconWidth,
+        height: metrics.iconHeight,
+        elmt:   iconDiv
+    };
+};
+
+Timeline.CompactEventPainter.prototype._paintEventLabel = function(commonData, labelData, left, top, width, height, theme) {
+    var doc = this._timeline.getDocument();
+    
+    var labelDiv = doc.createElement("div");
+    labelDiv.className = 'timeline-event-label';
+
+    labelDiv.style.left = left + "px";
+    labelDiv.style.width = (width + 1) + "px";
+    labelDiv.style.top = top + "px";
+    labelDiv.innerHTML = labelData.text;
+
+    if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
+        labelDiv.title = commonData.tooltip;
+    }
+    if ("color" in labelData && typeof labelData.color == "string") {
+        labelDiv.style.color = labelData.color;
+    }
+    if ("className" in labelData && typeof labelData.className == "string") {
+        labelDiv.className += ' ' + labelData.className;
+    }
+    
+    this._eventLayer.appendChild(labelDiv);
+    
+    return {
+        left:   left,
+        top:    top,
+        width:  width,
+        height: height,
+        elmt:   labelDiv
+    };
+};
+
+Timeline.CompactEventPainter.prototype._paintEventTape = function(
+    commonData, tapeData, height, top, startPixel, endPixel, color, opacity, metrics, theme) {
+    
+    var width = endPixel - startPixel;
+    
+    var tapeDiv = this._timeline.getDocument().createElement("div");
+    tapeDiv.className = "timeline-event-tape"
+
+    tapeDiv.style.left = startPixel + "px";
+    tapeDiv.style.top = top + "px";
+    tapeDiv.style.width = width + "px";
+    tapeDiv.style.height = height + "px";
+
+    if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
+        tapeDiv.title = commonData.tooltip;
+    }
+    if (color != null && typeof tapeData.color == "string") {
+        tapeDiv.style.backgroundColor = color;
+    }
+    
+    if ("backgroundImage" in tapeData && typeof tapeData.backgroundImage == "string") {
+        tapeDiv.style.backgroundImage = "url(" + backgroundImage + ")";
+        tapeDiv.style.backgroundRepeat = 
+            ("backgroundRepeat" in tapeData && typeof tapeData.backgroundRepeat == "string") 
+                ? tapeData.backgroundRepeat : 'repeat';
+    }
+    
+    SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
+    
+    if ("className" in tapeData && typeof tapeData.className == "string") {
+        tapeDiv.className += ' ' + tapeData.className;
+    }
+
+    this._eventLayer.appendChild(tapeDiv);
+    
+    return {
+        left:   startPixel,
+        top:    top,
+        width:  width,
+        height: height,
+        elmt:   tapeDiv
+    };
+}
+
+Timeline.CompactEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
+    if (highlightIndex >= 0) {
+        var doc = this._timeline.getDocument();
+        var eventTheme = theme.event;
+        
+        var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
+        
+        var div = doc.createElement("div");
+        div.style.position = "absolute";
+        div.style.overflow = "hidden";
+        div.style.left =    (dimensions.left - 2) + "px";
+        div.style.width =   (dimensions.width + 4) + "px";
+        div.style.top =     (dimensions.top - 2) + "px";
+        div.style.height =  (dimensions.height + 4) + "px";
+//        div.style.background = color;
+        
+        this._highlightLayer.appendChild(div);
+    }
+};
+
+Timeline.CompactEventPainter.prototype._onClickMultiplePreciseInstantEvent = function(icon, domEvt, events) {
+    var c = SimileAjax.DOM.getPageCoordinates(icon);
+    this._showBubble(
+        c.left + Math.ceil(icon.offsetWidth / 2), 
+        c.top + Math.ceil(icon.offsetHeight / 2),
+        events
+    );
+    
+    var ids = [];
+    for (var i = 0; i < events.length; i++) {
+        ids.push(events[i].getID());
+    }
+    this._fireOnSelect(ids);
+    
+    domEvt.cancelBubble = true;
+    SimileAjax.DOM.cancelEvent(domEvt);
+    
+    return false;
+};
+
+Timeline.CompactEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
+    var c = SimileAjax.DOM.getPageCoordinates(icon);
+    this._showBubble(
+        c.left + Math.ceil(icon.offsetWidth / 2), 
+        c.top + Math.ceil(icon.offsetHeight / 2),
+        [evt]
+    );
+    this._fireOnSelect([evt.getID()]);
+    
+    domEvt.cancelBubble = true;
+    SimileAjax.DOM.cancelEvent(domEvt);
+    return false;
+};
+
+Timeline.CompactEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
+    if ("pageX" in domEvt) {
+        var x = domEvt.pageX;
+        var y = domEvt.pageY;
+    } else {
+        var c = SimileAjax.DOM.getPageCoordinates(target);
+        var x = domEvt.offsetX + c.left;
+        var y = domEvt.offsetY + c.top;
+    }
+    this._showBubble(x, y, [evt]);
+    this._fireOnSelect([evt.getID()]);
+    
+    domEvt.cancelBubble = true;
+    SimileAjax.DOM.cancelEvent(domEvt);
+    return false;
+};
+
+Timeline.CompactEventPainter.prototype.showBubble = function(evt) {
+    var elmt = this._eventIdToElmt[evt.getID()];
+    if (elmt) {
+        var c = SimileAjax.DOM.getPageCoordinates(elmt);
+        this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, [evt]);
+    }
+};
+
+Timeline.CompactEventPainter.prototype._showBubble = function(x, y, evts) {
+    var div = document.createElement("div");
+    
+    evts = ("fillInfoBubble" in evts) ? [evts] : evts;
+    for (var i = 0; i < evts.length; i++) {
+        var div2 = document.createElement("div");
+        div.appendChild(div2);
+        
+        evts[i].fillInfoBubble(div2, this._params.theme, this._band.getLabeller());
+    }
+    
+    SimileAjax.WindowManager.cancelPopups();
+    SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, this._params.theme.event.bubble.width);
+};
+
+Timeline.CompactEventPainter.prototype._fireOnSelect = function(eventIDs) {
+    for (var i = 0; i < this._onSelectListeners.length; i++) {
+        this._onSelectListeners[i](eventIDs);
+    }
+};
+/*==================================================
+ *  Span Highlight Decorator
+ *==================================================
+ */
+
+Timeline.SpanHighlightDecorator = function(params) {
+    // When evaluating params, test against null. Not "p in params". Testing against
+    // null enables caller to explicitly request the default. Testing against "in" means
+    // that the param has to be ommitted to get the default.
+    this._unit = params.unit != null ? params.unit : SimileAjax.NativeDateUnit;
+    this._startDate = (typeof params.startDate == "string") ? 
+        this._unit.parseFromObject(params.startDate) : params.startDate;
+    this._endDate = (typeof params.endDate == "string") ?
+        this._unit.parseFromObject(params.endDate) : params.endDate;
+    this._startLabel = params.startLabel != null ? params.startLabel : ""; // not null!
+    this._endLabel   = params.endLabel   != null ? params.endLabel   : ""; // not null!
+    this._color = params.color;
+    this._cssClass = params.cssClass != null ? params.cssClass : null;
+    this._opacity = params.opacity != null ? params.opacity : 100;
+         // Default z is 10, behind everything but background grid.
+         // If inFront, then place just behind events, in front of everything else
+    this._zIndex = (params.inFront != null && params.inFront) ? 113 : 10;
+};
+
+Timeline.SpanHighlightDecorator.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;
+    
+    this._layerDiv = null;
+};
+
+Timeline.SpanHighlightDecorator.prototype.paint = function() {
+    if (this._layerDiv != null) {
+        this._band.removeLayerDiv(this._layerDiv);
+    }
+    this._layerDiv = this._band.createLayerDiv(this._zIndex);
+    this._layerDiv.setAttribute("name", "span-highlight-decorator"); // for debugging
+    this._layerDiv.style.display = "none";
+    
+    var minDate = this._band.getMinDate();
+    var maxDate = this._band.getMaxDate();
+    
+    if (this._unit.compare(this._startDate, maxDate) < 0 &&
+        this._unit.compare(this._endDate, minDate) > 0) {
+        
+        minDate = this._unit.later(minDate, this._startDate);
+        maxDate = this._unit.earlier(maxDate, this._endDate);
+        
+        var minPixel = this._band.dateToPixelOffset(minDate);
+        var maxPixel = this._band.dateToPixelOffset(maxDate);
+        
+        var doc = this._timeline.getDocument();
+        
+        var createTable = function() {
+            var table = doc.createElement("table");
+            table.insertRow(0).insertCell(0);
+            return table;
+        };
+    
+        var div = doc.createElement("div");
+        div.className='timeline-highlight-decorator'
+        if(this._cssClass) {
+        	  div.className += ' ' + this._cssClass;
+        }
+        if(this._color != null) {
+        	  div.style.backgroundColor = this._color;
+        }                      
+        if (this._opacity < 100) {
+            SimileAjax.Graphics.setOpacity(div, this._opacity);
+        }
+        this._layerDiv.appendChild(div);
+            
+        var tableStartLabel = createTable();
+        tableStartLabel.className = 'timeline-highlight-label timeline-highlight-label-start'
+        var tdStart =  tableStartLabel.rows[0].cells[0]
+        tdStart.innerHTML = this._startLabel;
+        if (this._cssClass) {
+        	  tdStart.className = 'label_' + this._cssClass;
+        }
+        this._layerDiv.appendChild(tableStartLabel);
+                    
+        var tableEndLabel = createTable();
+        tableEndLabel.className = 'timeline-highlight-label timeline-highlight-label-end'
+        var tdEnd = tableEndLabel.rows[0].cells[0]
+        tdEnd.innerHTML = this._endLabel;
+        if (this._cssClass) {
+        	   tdEnd.className = 'label_' + this._cssClass;
+        }
+        this._layerDiv.appendChild(tableEndLabel);
+        
+        if (this._timeline.isHorizontal()){
+            div.style.left = minPixel + "px";
+            div.style.width = (maxPixel - minPixel) + "px";
+                              
+            tableStartLabel.style.right = (this._band.getTotalViewLength() - minPixel) + "px";
+            tableStartLabel.style.width = (this._startLabel.length) + "em";       
+                                          
+            tableEndLabel.style.left = maxPixel + "px";
+            tableEndLabel.style.width = (this._endLabel.length) + "em";
+            
+        } else {
+            div.style.top = minPixel + "px";
+            div.style.height = (maxPixel - minPixel) + "px";
+            
+            tableStartLabel.style.bottom = minPixel + "px";
+            tableStartLabel.style.height = "1.5px";
+            
+            tableEndLabel.style.top = maxPixel + "px";
+            tableEndLabel.style.height = "1.5px";        
+        }
+    }
+    this._layerDiv.style.display = "block";
+};
+
+Timeline.SpanHighlightDecorator.prototype.softPaint = function() {
+};
+
+/*==================================================
+ *  Point Highlight Decorator
+ *==================================================
+ */
+
+Timeline.PointHighlightDecorator = function(params) {
+    this._unit = params.unit != null ? params.unit : SimileAjax.NativeDateUnit;
+    this._date = (typeof params.date == "string") ? 
+        this._unit.parseFromObject(params.date) : params.date;
+    this._width = params.width != null ? params.width : 10;
+      // Since the width is used to calculate placements (see minPixel, below), we
+      // specify width here, not in css.
+    this._color = params.color;
+    this._cssClass = params.cssClass != null ? params.cssClass : '';
+    this._opacity = params.opacity != null ? params.opacity : 100;
+};
+
+Timeline.PointHighlightDecorator.prototype.initialize = function(band, timeline) {
+    this._band = band;
+    this._timeline = timeline;    
+    this._layerDiv = null;
+};
+
+Timeline.PointHighlightDecorator.prototype.paint = function() {
+    if (this._layerDiv != null) {
+        this._band.removeLayerDiv(this._layerDiv);
+    }
+    this._layerDiv = this._band.createLayerDiv(10);
+    this._layerDiv.setAttribute("name", "span-highlight-decorator"); // for debugging
+    this._layerDiv.style.display = "none";
+    
+    var minDate = this._band.getMinDate();
+    var maxDate = this._band.getMaxDate();
+    
+    if (this._unit.compare(this._date, maxDate) < 0 &&
+        this._unit.compare(this._date, minDate) > 0) {
+        
+        var pixel = this._band.dateToPixelOffset(this._date);
+        var minPixel = pixel - Math.round(this._width / 2);
+        
+        var doc = this._timeline.getDocument();
+    
+        var div = doc.createElement("div");
+        div.className='timeline-highlight-point-decorator';
+        div.className += ' ' + this._cssClass;
+                    
+        if(this._color != null) {
+        	  div.style.backgroundColor = this._color;
+        }
+        if (this._opacity < 100) {
+            SimileAjax.Graphics.setOpacity(div, this._opacity);
+        }
+        this._layerDiv.appendChild(div);
+            
+        if (this._timeline.isHorizontal()) {
+            div.style.left = minPixel + "px";
+            div.style.width = this._width;
+        } else {
+            div.style.top = minPixel + "px";
+            div.style.height = this._width;
+        }
+    }
+    this._layerDiv.style.display = "block";
+};
+
+Timeline.PointHighlightDecorator.prototype.softPaint = function() {
+};
+/*==================================================
+ *  Default Unit
+ *==================================================
+ */
+
+Timeline.NativeDateUnit = new Object();
+
+Timeline.NativeDateUnit.createLabeller = function(locale, timeZone) {
+    return new Timeline.GregorianDateLabeller(locale, timeZone);
+};
+
+Timeline.NativeDateUnit.makeDefaultValue = function() {
+    return new Date();
+};
+
+Timeline.NativeDateUnit.cloneValue = function(v) {
+    return new Date(v.getTime());
+};
+
+Timeline.NativeDateUnit.getParser = function(format) {
+    if (typeof format == "string") {
+        format = format.toLowerCase();
+    }
+    return (format == "iso8601" || format == "iso 8601") ?
+        Timeline.DateTime.parseIso8601DateTime : 
+        Timeline.DateTime.parseGregorianDateTime;
+};
+
+Timeline.NativeDateUnit.parseFromObject = function(o) {
+    return Timeline.DateTime.parseGregorianDateTime(o);
+};
+
+Timeline.NativeDateUnit.toNumber = function(v) {
+    return v.getTime();
+};
+
+Timeline.NativeDateUnit.fromNumber = function(n) {
+    return new Date(n);
+};
+
+Timeline.NativeDateUnit.compare = function(v1, v2) {
+    var n1, n2;
+    if (typeof v1 == "object") {
+        n1 = v1.getTime();
+    } else {
+        n1 = Number(v1);
+    }
+    if (typeof v2 == "object") {
+        n2 = v2.getTime();
+    } else {
+        n2 = Number(v2);
+    }
+    
+    return n1 - n2;
+};
+
+Timeline.NativeDateUnit.earlier = function(v1, v2) {
+    return Timeline.NativeDateUnit.compare(v1, v2) < 0 ? v1 : v2;
+};
+
+Timeline.NativeDateUnit.later = function(v1, v2) {
+    return Timeline.NativeDateUnit.compare(v1, v2) > 0 ? v1 : v2;
+};
+
+Timeline.NativeDateUnit.change = function(v, n) {
+    return new Date(v.getTime() + n);
+};
+
+/*==================================================
+ *  Simile Timeplot API
+ *
+ *  Include Timeplot in your HTML file as follows:
+ *    <script src="http://static.simile.mit.edu/timeplot/api/1.0/timeplot-api.js" type="text/javascript"></script>
+ *
+ *==================================================*/
+
+(function() {
+
+    var local = false;
+
+    // obtain local mode from the document URL    
+    if (document.location.search.length > 0) {
+        var params = document.location.search.substr(1).split("&");
+        for (var i = 0; i < params.length; i++) {
+            if (params[i] == "local") {
+                local = true;
+            }
+        }
+    }
+
+    // obtain local mode from the script URL params attribute
+    if (!local) {
+        var heads = document.documentElement.getElementsByTagName("head");
+        for (var h = 0; h < heads.length; h++) {
+            var node = heads[h].firstChild;
+            while (node != null) {
+                if (node.nodeType == 1 && node.tagName.toLowerCase() == "script") {
+                    var url = node.src;
+                    if (url.indexOf("timeplot-complete") >= 0) {
+                        local = (url.indexOf("local") >= 0);
+                    }
+                }
+                node = node.nextSibling;
+            }
+        }
+    }
+
+    // Load Timeplot if it's not already loaded (after SimileAjax and Timeline)
+    var loadTimeplot = function() {
+
+        if (typeof window.Timeplot != "undefined") {
+            return;
+        }
+        
+        window.Timeplot = {
+            loaded:     false,
+            params:     { bundle: true, autoCreate: true },
+            namespace:  "http://simile.mit.edu/2007/06/timeplot#",
+            importers:  {}
+        };
+    
+        var javascriptFiles = [
+            "timeplot.js",
+            "plot.js",
+            "sources.js",
+            "geometry.js",
+            "color.js",
+            "math.js",
+            "processor.js"
+        ];
+        var cssFiles = [
+            "timeplot.css"
+        ];
+        
+        var locales = [ "en" ];
+
+        var defaultClientLocales = ("language" in navigator ? navigator.language : navigator.browserLanguage).split(";");
+        for (var l = 0; l < defaultClientLocales.length; l++) {
+            var locale = defaultClientLocales[l];
+            if (locale != "en") {
+                var segments = locale.split("-");
+                if (segments.length > 1 && segments[0] != "en") {
+                    locales.push(segments[0]);
+                }
+                locales.push(locale);
+            }
+        }
+
+        var paramTypes = { bundle:Boolean, js:Array, css:Array, autoCreate:Boolean };
+        if (typeof Timeplot_urlPrefix == "string") {
+            Timeplot.urlPrefix = Timeplot_urlPrefix;
+            if ("Timeplot_parameters" in window) {
+                SimileAjax.parseURLParameters(Timeplot_parameters, Timeplot.params, paramTypes);
+            }
+        } else {
+            var url = SimileAjax.findScript(document, "timeplot-complete.js");
+            if (url == null) {
+                Timeplot.error = new Error("Failed to derive URL prefix for Simile Timeplot API code files");
+                return;
+            }
+            Timeplot.urlPrefix = url.substr(0, url.indexOf("timeplot-complete.js"));
+            SimileAjax.parseURLParameters(url, Timeplot.params, paramTypes);
+        }
+
+        if (Timeplot.params.locale) { // ISO-639 language codes,
+            // optional ISO-3166 country codes (2 characters)
+            if (Timeplot.params.locale != "en") {
+                var segments = Timeplot.params.locale.split("-");
+                if (segments.length > 1 && segments[0] != "en") {
+                    locales.push(segments[0]);
+                }
+                locales.push(Timeplot.params.locale);
+            }
+        }
+
+        var timeplotURLPrefix = (local) ? "/timeplot/api/1.0/" : Timeplot.urlPrefix;
+
+        if (local && !("console" in window)) {
+            var firebug = [ timeplotURLPrefix + "lib/firebug/firebug.js" ];
+            SimileAjax.includeJavascriptFiles(document, "", firebug);
+        }
+        
+        var canvas = document.createElement("canvas");
+
+        if (!canvas.getContext) {
+            var excanvas = [ timeplotURLPrefix + "lib/excanvas.js" ];
+            SimileAjax.includeJavascriptFiles(document, "", excanvas);
+        }
+        
+        var scriptURLs = Timeplot.params.js || [];
+        var cssURLs = Timeplot.params.css || [];
+
+        // Core scripts and styles
+        if (Timeplot.params.bundle && !local) {
+            scriptURLs.push(timeplotURLPrefix + "timeplot-bundle.js");
+            cssURLs.push(timeplotURLPrefix + "timeplot-bundle.css");
+        } else {
+            SimileAjax.prefixURLs(scriptURLs, timeplotURLPrefix + "scripts/", javascriptFiles);
+            SimileAjax.prefixURLs(cssURLs, timeplotURLPrefix + "styles/", cssFiles);
+        }
+        
+        // Localization
+        //for (var i = 0; i < locales.length; i++) {
+        //    scriptURLs.push(Timeplot.urlPrefix + "locales/" + locales[i] + "/locale.js");
+        //};
+        
+        window.SimileAjax_onLoad = function() {
+            if (local && window.console.open) window.console.open();
+            if (Timeplot.params.callback) {
+                eval(Timeplot.params.callback + "()");
+            }
+        }
+        
+        SimileAjax.includeJavascriptFiles(document, "", scriptURLs);
+        SimileAjax.includeCssFiles(document, "", cssURLs);
+        Timeplot.loaded = true;
+    };
+
+    // Load Timeline if it's not already loaded (after SimileAjax and before Timeplot)
+    var loadTimeline = function() {
+        if (typeof Timeline != "undefined") {
+            loadTimeplot();
+        } else {
+            var timelineURL = (local) ? "/timeline/api-2.0/timeline-api.js?bundle=false" : "http://static.simile.mit.edu/timeline/api-2.0/timeline-api.js";
+            window.SimileAjax_onLoad = loadTimeplot;
+            SimileAjax.includeJavascriptFile(document, timelineURL);
+        }
+    };
+    
+    // Load SimileAjax if it's not already loaded
+    if (typeof SimileAjax == "undefined") {
+        window.SimileAjax_onLoad = loadTimeline;
+        
+        var url = local ?
+            "/ajax/api-2.0/simile-ajax-api.js?bundle=false" :
+            "http://static.simile.mit.edu/ajax/api-2.0/simile-ajax-api.js?bundle=true";
+                
+        var createScriptElement = function() {
+            var script = document.createElement("script");
+            script.type = "text/javascript";
+            script.language = "JavaScript";
+            script.src = url;
+            document.getElementsByTagName("head")[0].appendChild(script);
+        }
+        
+        if (document.body == null) {
+            try {
+                document.write("<script src='" + url + "' type='text/javascript'></script>");
+            } catch (e) {
+                createScriptElement();
+            }
+        } else {
+            createScriptElement();
+        }
+    } else {
+        loadTimeline();
+    }
+})();
+/**
+ * Timeplot
+ * 
+ * @fileOverview Timeplot
+ * @name Timeplot
+ */
+
+Timeline.Debug = SimileAjax.Debug; // timeline uses it's own debug system which is not as advanced
+var log = SimileAjax.Debug.log; // shorter name is easier to use
+
+/*
+ * This function is used to implement a raw but effective OOP-like inheritance
+ * in various Timeplot classes.
+ */
+Object.extend = function(destination, source) {
+    for (var property in source) {
+        destination[property] = source[property];
+    }
+    return destination;
+}
+
+// ---------------------------------------------
+
+/**
+ * Create a timeplot attached to the given element and using the configuration from the given array of PlotInfos
+ */
+Timeplot.create = function(elmt, plotInfos) {
+    return new Timeplot._Impl(elmt, plotInfos);
+};
+
+/**
+ * Create a PlotInfo configuration from the given map of params
+ */
+Timeplot.createPlotInfo = function(params) {
+    return {   
+        id:                ("id" in params) ? params.id : "p" + Math.round(Math.random() * 1000000),
+        dataSource:        ("dataSource" in params) ? params.dataSource : null,
+        eventSource:       ("eventSource" in params) ? params.eventSource : null,
+        timeGeometry:      ("timeGeometry" in params) ? params.timeGeometry : new Timeplot.DefaultTimeGeometry(),
+        valueGeometry:     ("valueGeometry" in params) ? params.valueGeometry : new Timeplot.DefaultValueGeometry(),
+        timeZone:          ("timeZone" in params) ? params.timeZone : 0,
+        fillColor:         ("fillColor" in params) ? ((params.fillColor == "string") ? new Timeplot.Color(params.fillColor) : params.fillColor) : null,
+        fillGradient:      ("fillGradient" in params) ? params.fillGradient : true,
+        fillFrom:          ("fillFrom" in params) ? params.fillFrom : Number.NEGATIVE_INFINITY,
+        lineColor:         ("lineColor" in params) ? ((params.lineColor == "string") ? new Timeplot.Color(params.lineColor) : params.lineColor) : new Timeplot.Color("#606060"),
+        lineWidth:         ("lineWidth" in params) ? params.lineWidth : 1.0,
+        dotRadius:         ("dotRadius" in params) ? params.dotRadius : 2.0,
+        dotColor:          ("dotColor" in params) ? params.dotColor : null,
+        eventLineWidth:    ("eventLineWidth" in params) ? params.eventLineWidth : 1.0,
+        showValues:        ("showValues" in params) ? params.showValues : false,
+        roundValues:       ("roundValues" in params) ? params.roundValues : true,
+        valuesOpacity:     ("valuesOpacity" in params) ? params.valuesOpacity : 75,
+        bubbleWidth:       ("bubbleWidth" in params) ? params.bubbleWidth : 300,
+        bubbleHeight:      ("bubbleHeight" in params) ? params.bubbleHeight : 200
+    };
+};
+
+// -------------------------------------------------------
+
+/**
+ * This is the implementation of the Timeplot object.
+ *  
+ * @constructor 
+ */
+Timeplot._Impl = function(elmt, plotInfos) {
+    this._id = "t" + Math.round(Math.random() * 1000000);
+    this._containerDiv = elmt;
+    this._plotInfos = plotInfos;
+    this._painters = {
+        background: [],
+        foreground: []
+    };
+    this._painter = null;
+    this._active = false;
+    this._upright = false;
+    this._initialize();
+};
+
+Timeplot._Impl.prototype = {
+
+    dispose: function() {
+        for (var i = 0; i < this._plots.length; i++) {
+            this._plots[i].dispose();
+        }
+        this._plots = null;
+        this._plotsInfos = null;
+        this._containerDiv.innerHTML = "";
+    },
+    
+    /**
+     * Returns the main container div this timeplot is operating on.
+     */
+    getElement: function() {
+        return this._containerDiv;
+    },
+    
+    /**
+     * Returns document this timeplot belongs to.
+     */
+    getDocument: function() {
+        return this._containerDiv.ownerDocument;
+    },
+
+    /**
+     * Append the given element to the timeplot DOM
+     */
+    add: function(div) {
+        this._containerDiv.appendChild(div);
+    },
+
+    /**
+     * Remove the given element to the timeplot DOM
+     */
+    remove: function(div) {
+        this._containerDiv.removeChild(div);
+    },
+
+    /**
+     * Add a painter to the timeplot
+     */
+    addPainter: function(layerName, painter) {
+        var layer = this._painters[layerName];
+        if (layer) {
+            for (var i = 0; i < layer.length; i++) {
+                if (layer[i].context._id == painter.context._id) {
+                    return;
+                }
+            }
+            layer.push(painter);
+        }
+    },
+    
+    /**
+     * Remove a painter from the timeplot
+     */
+    removePainter: function(layerName, painter) {
+        var layer = this._painters[layerName];
+        if (layer) {
+            for (var i = 0; i < layer.length; i++) {
+                if (layer[i].context._id == painter.context._id) {
+                    layer.splice(i, 1);
+                    break;
+                }
+            }
+        }
+    },
+    
+    /**
+     * Get the width in pixels of the area occupied by the entire timeplot in the page
+     */
+    getWidth: function() {
+        return this._containerDiv.clientWidth;
+    },
+
+    /**
+     * Get the height in pixels of the area occupied by the entire timeplot in the page
+     */
+    getHeight: function() {
+        return this._containerDiv.clientHeight;
+    },
+    
+    /**
+     * Get the drawing canvas associated with this timeplot
+     */
+    getCanvas: function() {
+        return this._canvas;
+    },
+    
+    /**
+     * <p>Load the data from the given url into the given eventSource, using
+     * the given separator to parse the columns and preprocess it before parsing
+     * thru the optional filter function. The filter is useful for when 
+     * the data is row-oriented but the format is not compatible with the
+     * one that Timeplot expects.</p> 
+     * 
+     * <p>Here is an example of a filter that changes dates in the form 'yyyy/mm/dd'
+     * in the required 'yyyy-mm-dd' format:
+     * <pre>var dataFilter = function(data) {
+     *     for (var i = 0; i < data.length; i++) {
+     *         var row = data[i];
+     *         row[0] = row[0].replace(/\//g,"-");
+     *     }
+     *     return data;
+     * };</pre></p>
+     */
+    loadText: function(url, separator, eventSource, filter, format) {
+        if (this._active) {
+            var tp = this;
+            
+            var fError = function(statusText, status, xmlhttp) {
+                alert("Failed to load data xml from " + url + "\n" + statusText);
+                tp.hideLoadingMessage();
+            };
+            
+            var fDone = function(xmlhttp) {
+                try {
+                    eventSource.loadText(xmlhttp.responseText, separator, url, filter, format);
+                } catch (e) {
+                    SimileAjax.Debug.exception(e);
+                } finally {
+                    tp.hideLoadingMessage();
+                }
+            };
+            
+            this.showLoadingMessage();
+            window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
+        }
+    },
+
+    /**
+     * Load event data from the given url into the given eventSource, using
+     * the Timeline XML event format.
+     */
+    loadXML: function(url, eventSource) {
+        if (this._active) {
+            var tl = this;
+            
+            var fError = function(statusText, status, xmlhttp) {
+                alert("Failed to load data xml from " + url + "\n" + statusText);
+                tl.hideLoadingMessage();
+            };
+            
+            var fDone = function(xmlhttp) {
+                try {
+                    var xml = xmlhttp.responseXML;
+                    if (!xml.documentElement && xmlhttp.responseStream) {
+                        xml.load(xmlhttp.responseStream);
+                    } 
+                    eventSource.loadXML(xml, url);
+                } finally {
+                    tl.hideLoadingMessage();
+                }
+            };
+            
+            this.showLoadingMessage();
+            window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
+        }
+    },
+    
+    /**
+     * Overlay a 'div' element filled with the given text and styles to this timeplot
+     * This is used to implement labels since canvas does not support drawing text.
+     */
+    putText: function(id, text, clazz, styles) {
+        var div = this.putDiv(id, "timeplot-div " + clazz, styles);
+        div.innerHTML = text;
+        return div;
+    },
+
+    /**
+     * Overlay a 'div' element, with the given class and the given styles to this timeplot.
+     * This is used for labels and horizontal and vertical grids. 
+     */
+    putDiv: function(id, clazz, styles) {
+        var tid = this._id + "-" + id;
+        var div = document.getElementById(tid);
+        if (!div) {
+            var container = this._containerDiv.firstChild; // get the divs container
+            div = document.createElement("div");
+            div.setAttribute("id",tid);
+            container.appendChild(div);
+        }
+        div.setAttribute("class","timeplot-div " + clazz);
+        div.setAttribute("className","timeplot-div " + clazz);
+        this.placeDiv(div,styles);
+        return div;
+    },
+    
+    /**
+     * Associate the given map of styles to the given element. 
+     * In case such styles indicate position (left,right,top,bottom) correct them
+     * with the padding information so that they align to the 'internal' area
+     * of the timeplot.
+     */
+    placeDiv: function(div, styles) {
+        if (styles) {
+            for (style in styles) {
+                if (style == "left") {
+                    styles[style] += this._paddingX;
+                    styles[style] += "px";
+                } else if (style == "right") {
+                    styles[style] += this._paddingX;
+                    styles[style] += "px";
+                } else if (style == "top") {
+                    styles[style] += this._paddingY;
+                    styles[style] += "px";
+                } else if (style == "bottom") {
+                    styles[style] += this._paddingY;
+                    styles[style] += "px";
+                } else if (style == "width") {
+                    if (styles[style] < 0) styles[style] = 0;
+                    styles[style] += "px";
+                } else if (style == "height") {
+                    if (styles[style] < 0) styles[style] = 0;
+                    styles[style] += "px";
+                }
+                div.style[style] = styles[style];
+            }
+        }
+    },
+    
+    /**
+     * return a {x,y} map with the location of the given element relative to the 'internal' area of the timeplot
+     * (that is, without the container padding)
+     */
+    locate: function(div) {
+        return {
+            x: div.offsetLeft - this._paddingX,
+            y: div.offsetTop - this._paddingY
+        }
+    },
+    
+    /**
+     * Forces timeplot to re-evaluate the various value and time geometries
+     * associated with its plot layers and repaint accordingly. This should
+     * be invoked after the data in any of the data sources has been
+     * modified.
+     */
+    update: function() {
+        if (this._active) {
+            for (var i = 0; i < this._plots.length; i++) {
+                var plot = this._plots[i];
+                var dataSource = plot.getDataSource();
+                if (dataSource) {
+                    var range = dataSource.getRange();
+                    if (range) {
+                        plot._valueGeometry.setRange(range);
+                        plot._timeGeometry.setRange(range);
+                    }
+                }
+                plot.hideValues();
+            }
+            this.paint();
+        }
+    },
+    
+    /**
+     * Forces timeplot to re-evaluate its own geometry, clear itself and paint.
+     * This should be used instead of paint() when you're not sure if the 
+     * geometry of the page has changed or not. 
+     */
+    repaint: function() {
+        if (this._active) {
+            this._prepareCanvas();
+            for (var i = 0; i < this._plots.length; i++) {
+                var plot = this._plots[i];
+                if (plot._timeGeometry) plot._timeGeometry.reset();
+                if (plot._valueGeometry) plot._valueGeometry.reset();
+            }
+            this.paint();
+        }
+    },
+    
+    /**
+     * Calls all the painters that were registered to this timeplot and makes them
+     * paint the timeplot. This should be used only when you're sure that the geometry
+     * of the page hasn't changed.
+     * NOTE: painting is performed by a different thread and it's safe to call this
+     * function in bursts (as in mousemove or during window resizing
+     */
+    paint: function() {
+        if (this._active && this._painter == null) {
+            var timeplot = this;
+            this._painter = window.setTimeout(function() {
+                timeplot._clearCanvas();
+                
+                var run = function(action,context) {
+                    try {
+                        if (context.setTimeplot) context.setTimeplot(timeplot);
+                        action.apply(context,[]);
+                    } catch (e) {
+                        SimileAjax.Debug.exception(e);
+                    }
+                }
+                
+                var background = timeplot._painters.background;
+                for (var i = 0; i < background.length; i++) {
+                    run(background[i].action, background[i].context); 
+                }
+                var foreground = timeplot._painters.foreground;
+                for (var i = 0; i < foreground.length; i++) {
+                    run(foreground[i].action, foreground[i].context); 
+                }
+                
+                timeplot._painter = null;
+            }, 20);
+        }
+    },
+
+    _clearCanvas: function() {
+        var canvas = this.getCanvas();
+        var ctx = canvas.getContext('2d');
+        ctx.clearRect(0,0,canvas.width,canvas.height);
+    },
+    
+    _clearLabels: function() {
+        var labels = this._containerDiv.firstChild;
+        if (labels) this._containerDiv.removeChild(labels);
+        labels = document.createElement("div");
+        this._containerDiv.appendChild(labels);
+    },
+    
+    _prepareCanvas: function() {
+        var canvas = this.getCanvas();
+
+        // using jQuery.  note we calculate the average padding; if your
+        // padding settings are not symmetrical, the labels will be off
+        // since they expect to be centered on the canvas.
+        var con = SimileAjax.jQuery(this._containerDiv);
+        this._paddingX = (parseInt(con.css('paddingLeft')) +
+                          parseInt(con.css('paddingRight'))) / 2;
+        this._paddingY = (parseInt(con.css('paddingTop')) +
+                          parseInt(con.css('paddingBottom'))) / 2;
+
+        canvas.width = this.getWidth() - (this._paddingX * 2);
+        canvas.height = this.getHeight() - (this._paddingY * 2);
+
+        var ctx = canvas.getContext('2d');
+        this._setUpright(ctx, canvas);
+        ctx.globalCompositeOperation = 'source-over';
+    },
+
+    _setUpright: function(ctx, canvas) {
+        // excanvas+IE requires this to be done only once, ever; actual canvas
+        // implementations reset and require this for each call to re-layout
+        if (!SimileAjax.Platform.browser.isIE) this._upright = false;
+        if (!this._upright) {
+            this._upright = true;
+            ctx.translate(0, canvas.height);
+            ctx.scale(1,-1);
+        }
+    },
+    
+    _isBrowserSupported: function(canvas) {
+        var browser = SimileAjax.Platform.browser;
+        if ((canvas.getContext && window.getComputedStyle) ||
+            (browser.isIE && browser.majorVersion >= 6)) {
+            return true;
+        } else {
+            return false;
+        }
+    },
+    
+    _initialize: function() {
+        
+        // initialize the window manager (used to handle the popups)
+        // NOTE: this is a singleton and it's safe to call multiple times
+        SimileAjax.WindowManager.initialize(); 
+        
+        var containerDiv = this._containerDiv;
+        var doc = containerDiv.ownerDocument;
+    
+        // make sure the timeplot div has the right class    
+        containerDiv.className = "timeplot-container " + containerDiv.className;
+            
+        // clean it up if it contains some content
+        while (containerDiv.firstChild) {
+            containerDiv.removeChild(containerDiv.firstChild);
+        }
+        
+        var canvas = doc.createElement("canvas");
+        
+        if (this._isBrowserSupported(canvas)) {
+            this._clearLabels();
+
+            this._canvas = canvas;
+            canvas.className = "timeplot-canvas";
+            containerDiv.appendChild(canvas);
+            if(!canvas.getContext && G_vmlCanvasManager) {
+                canvas = G_vmlCanvasManager.initElement(this._canvas);
+                this._canvas = canvas;
+            }
+            this._prepareCanvas();
+    
+            // inserting copyright and link to simile
+            var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/copyright.png");
+            elmtCopyright.className = "timeplot-copyright";
+            elmtCopyright.title = "Timeplot (c) SIMILE - http://simile.mit.edu/timeplot/";
+            SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://simile.mit.edu/timeplot/"; });
+            containerDiv.appendChild(elmtCopyright);
+            
+            var timeplot = this;
+            var painter = {
+                onAddMany: function() { timeplot.update(); },
+                onClear:   function() { timeplot.update(); }
+            }
+
+            // creating painters
+            this._plots = [];
+            if (this._plotInfos) {
+                for (var i = 0; i < this._plotInfos.length; i++) {
+                    var plot = new Timeplot.Plot(this, this._plotInfos[i]);
+                    var dataSource = plot.getDataSource();
+                    if (dataSource) {
+                        dataSource.addListener(painter);
+                    }
+                    this.addPainter("background", {
+                        context: plot.getTimeGeometry(),
+                        action: plot.getTimeGeometry().paint
+                    });
+                    this.addPainter("background", {
+                        context: plot.getValueGeometry(),
+                        action: plot.getValueGeometry().paint
+                    });
+                    this.addPainter("foreground", {
+                        context: plot,
+                        action: plot.paint
+                    });
+                    this._plots.push(plot);
+                    plot.initialize();
+                }
+            }
+                
+            // creating loading UI
+            var message = SimileAjax.Graphics.createMessageBubble(doc);
+            message.containerDiv.className = "timeplot-message-container";
+            containerDiv.appendChild(message.containerDiv);
+            
+            message.contentDiv.className = "timeplot-message";
+            message.contentDiv.innerHTML = "<img src='" + Timeplot.urlPrefix + "images/progress-running.gif' /> Loading...";
+            
+            this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; };
+            this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; };
+    
+            this._active = true;
+            
+        } else {
+    
+            this._message = SimileAjax.Graphics.createMessageBubble(doc);
+            this._message.containerDiv.className = "timeplot-message-container";
+            this._message.containerDiv.style.top = "15%";
+            this._message.containerDiv.style.left = "20%";
+            this._message.containerDiv.style.right = "20%";
+            this._message.containerDiv.style.minWidth = "20em";
+            this._message.contentDiv.className = "timeplot-message";
+            this._message.contentDiv.innerHTML = "We're terribly sorry, but your browser is not currently supported by <a href='http://simile.mit.edu/timeplot/'>Timeplot</a>.<br><br> We are working on supporting it in the near future but, for now, see the <a href='http://simile.mit.edu/wiki/Timeplot_Limitations'>list of currently supported browsers</a>.";
+            this._message.containerDiv.style.display = "block";
+
+            containerDiv.appendChild(this._message.containerDiv);
+    
+        }
+    }
+};
+/**
+ * Plot Layer
+ * 
+ * @fileOverview Plot Layer
+ * @name Plot
+ */
+ 
+/**
+ * A plot layer is the main building block for timeplots and it's the object
+ * that is responsible for painting the plot itself. Each plot needs to have
+ * a time geometry, either a DataSource (for time series
+ * plots) or an EventSource (for event plots) and a value geometry in case 
+ * of time series plots. Such parameters are passed along
+ * in the 'plotInfo' map.
+ * 
+ * @constructor
+ */
+Timeplot.Plot = function(timeplot, plotInfo) {
+    this._timeplot = timeplot;
+    this._canvas = timeplot.getCanvas();
+    this._plotInfo = plotInfo;
+    this._id = plotInfo.id;
+    this._timeGeometry = plotInfo.timeGeometry;
+    this._valueGeometry = plotInfo.valueGeometry;
+    this._theme = new Timeline.getDefaultTheme();
+    this._dataSource = plotInfo.dataSource;
+    this._eventSource = plotInfo.eventSource;
+    this._bubble = null;
+};
+
+Timeplot.Plot.prototype = {
+    
+    /**
+     * Initialize the plot layer
+     */
+    initialize: function() {
+        if (this._dataSource && this._dataSource.getValue) {
+            this._timeFlag = this._timeplot.putDiv("timeflag","timeplot-timeflag");
+            this._valueFlag = this._timeplot.putDiv(this._id + "valueflag","timeplot-valueflag");
+            this._valueFlagLineLeft = this._timeplot.putDiv(this._id + "valueflagLineLeft","timeplot-valueflag-line");
+            this._valueFlagLineRight = this._timeplot.putDiv(this._id + "valueflagLineRight","timeplot-valueflag-line");
+            if (!this._valueFlagLineLeft.firstChild) {
+                this._valueFlagLineLeft.appendChild(SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/line_left.png"));
+                this._valueFlagLineRight.appendChild(SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/line_right.png"));
+            }
+            this._valueFlagPole = this._timeplot.putDiv(this._id + "valuepole","timeplot-valueflag-pole");
+
+            var opacity = this._plotInfo.valuesOpacity;
+            
+            SimileAjax.Graphics.setOpacity(this._timeFlag, opacity);
+            SimileAjax.Graphics.setOpacity(this._valueFlag, opacity);
+            SimileAjax.Graphics.setOpacity(this._valueFlagLineLeft, opacity);
+            SimileAjax.Graphics.setOpacity(this._valueFlagLineRight, opacity);
+            SimileAjax.Graphics.setOpacity(this._valueFlagPole, opacity);
+
+            var plot = this;
+            
+            var mouseOverHandler = function(elmt, evt, target) {
+                if (plot._plotInfo.showValues) { 
+	                plot._valueFlag.style.display = "block";
+	                mouseMoveHandler(elmt, evt, target);
+	            }
+            }
+        
+            var day = 24 * 60 * 60 * 1000;
+            var month = 30 * day;
+            
+            var mouseMoveHandler = function(elmt, evt, target) {
+                if (typeof SimileAjax != "undefined" && plot._plotInfo.showValues) {
+                    var c = plot._canvas;
+                    var x = Math.round(SimileAjax.DOM.getEventRelativeCoordinates(evt,plot._canvas).x);
+                    if (x > c.width) x = c.width;
+                    if (isNaN(x) || x < 0) x = 0;
+                    var t = plot._timeGeometry.fromScreen(x);
+                    if (t == 0) { // something is wrong
+                        plot._valueFlag.style.display = "none";
+                        return;
+                    }
+                    
+                    var validTime = plot._dataSource.getClosestValidTime(t);
+                    x = plot._timeGeometry.toScreen(validTime);
+                    var v = plot._dataSource.getValue(validTime);
+                    if (plot._plotInfo.roundValues) v = Math.round(v);
+                    plot._valueFlag.innerHTML = new String(v);
+                    var d = new Date(validTime);
+                    var p = plot._timeGeometry.getPeriod(); 
+                    if (p < day) {
+                        plot._timeFlag.innerHTML = d.toLocaleTimeString();
+                    } else if (p > month) {
+                        plot._timeFlag.innerHTML = d.toLocaleDateString();
+                    } else {
+                        plot._timeFlag.innerHTML = d.toLocaleString();
+                    }
+
+                    var tw = plot._timeFlag.clientWidth;
+                    var th = plot._timeFlag.clientHeight;
+                    var tdw = Math.round(tw / 2);
+                    var vw = plot._valueFlag.clientWidth;
+                    var vh = plot._valueFlag.clientHeight;
+                    var y = plot._valueGeometry.toScreen(v);
+
+                    if (x + tdw > c.width) {
+                        var tx = c.width - tdw;
+                    } else if (x - tdw < 0) {
+                        var tx = tdw;
+                    } else {
+                        var tx = x;
+                    }
+
+                    if (plot._timeGeometry._timeValuePosition == "top") {
+                        plot._timeplot.placeDiv(plot._valueFlagPole, {
+                            left: x,
+                            top: th - 5,
+                            height: c.height - y - th + 6,
+                            display: "block"
+                        });
+                        plot._timeplot.placeDiv(plot._timeFlag,{
+                            left: tx - tdw,
+                            top: -6,
+                            display: "block"
+                        });
+                    } else {
+                        plot._timeplot.placeDiv(plot._valueFlagPole, {
+                            left: x,
+                            bottom: th - 5,
+                            height: y - th + 6,
+                            display: "block"
+                        });
+                        plot._timeplot.placeDiv(plot._timeFlag,{
+                            left: tx - tdw,
+                            bottom: -6,
+                            display: "block"
+                        });
+                    }
+
+                    if (x + vw + 14 > c.width && y + vh + 4 > c.height) {
+                        plot._valueFlagLineLeft.style.display = "none";
+                        plot._timeplot.placeDiv(plot._valueFlagLineRight,{
+                            left: x - 14,
+                            bottom: y - 14,
+                            display: "block"
+                        });
+                        plot._timeplot.placeDiv(plot._valueFlag,{
+                            left: x - vw - 13,
+                            bottom: y - vh - 13,
+                            display: "block"
+                        });
+                    } else if (x + vw + 14 > c.width && y + vh + 4 < c.height) {
+                        plot._valueFlagLineRight.style.display = "none";
+                        plot._timeplot.placeDiv(plot._valueFlagLineLeft,{
+                            left: x - 14,
+                            bottom: y,
+                            display: "block"
+                        });
+                        plot._timeplot.placeDiv(plot._valueFlag,{
+                            left: x - vw - 13,
+                            bottom: y + 13,
+                            display: "block"
+                        });
+                    } else if (x + vw + 14 < c.width && y + vh + 4 > c.height) {
+                        plot._valueFlagLineRight.style.display = "none";
+                        plot._timeplot.placeDiv(plot._valueFlagLineLeft,{
+                            left: x,
+                            bottom: y - 13,
+                            display: "block"
+                        });
+                        plot._timeplot.placeDiv(plot._valueFlag,{
+                            left: x + 13,
+                            bottom: y - 13,
+                            display: "block"
+                        });
+                    } else {
+                        plot._valueFlagLineLeft.style.display = "none";
+                        plot._timeplot.placeDiv(plot._valueFlagLineRight,{
+                            left: x,
+                            bottom: y,
+                            display: "block"
+                        });
+                        plot._timeplot.placeDiv(plot._valueFlag,{
+                            left: x + 13,
+                            bottom: y + 13,
+                            display: "block"
+                        });
+                    }
+                }
+            }
+
+            var timeplotElement = this._timeplot.getElement();
+            SimileAjax.DOM.registerEvent(timeplotElement, "mouseover", mouseOverHandler);
+            SimileAjax.DOM.registerEvent(timeplotElement, "mousemove", mouseMoveHandler);
+        }
+    },
+
+    /**
+     * Dispose the plot layer and all the data sources and listeners associated to it
+     */
+    dispose: function() {
+        if (this._dataSource) {
+            this._dataSource.removeListener(this._paintingListener);
+            this._paintingListener = null;
+            this._dataSource.dispose();
+            this._dataSource = null;
+        }
+    },
+
+    /**
+     * Hide the values
+     */
+    hideValues: function() {
+        if (this._valueFlag) this._valueFlag.style.display = "none";
+        if (this._timeFlag) this._timeFlag.style.display = "none";
+        if (this._valueFlagLineLeft) this._valueFlagLineLeft.style.display = "none";
+        if (this._valueFlagLineRight) this._valueFlagLineRight.style.display = "none";
+        if (this._valueFlagPole) this._valueFlagPole.style.display = "none";
+    },
+    
+    /**
+     * Return the data source of this plot layer (it could be either a DataSource or an EventSource)
+     */
+    getDataSource: function() {
+        return (this._dataSource) ? this._dataSource : this._eventSource;
+    },
+
+    /**
+     * Return the time geometry associated with this plot layer
+     */
+    getTimeGeometry: function() {
+        return this._timeGeometry;
+    },
+
+    /**
+     * Return the value geometry associated with this plot layer
+     */
+    getValueGeometry: function() {
+        return this._valueGeometry;
+    },
+
+    /**
+     * Paint this plot layer
+     */
+    paint: function() {
+        var ctx = this._canvas.getContext('2d');
+
+        ctx.lineWidth = this._plotInfo.lineWidth;
+        ctx.lineJoin = 'miter';
+
+        if (this._dataSource) {     
+            if (this._plotInfo.fillColor) {
+                if (this._plotInfo.fillGradient) {
+                    var gradient = ctx.createLinearGradient(0,this._canvas.height,0,0);
+                    gradient.addColorStop(0,this._plotInfo.fillColor.toString());
+                    gradient.addColorStop(0.5,this._plotInfo.fillColor.toString());
+                    gradient.addColorStop(1, 'rgba(255,255,255,0)');
+
+                    ctx.fillStyle = gradient;
+                } else {
+                    ctx.fillStyle = this._plotInfo.fillColor.toString();
+                }
+
+                ctx.beginPath();
+                ctx.moveTo(0,0);
+                this._plot(function(x,y) {
+                    ctx.lineTo(x,y);
+                });
+                if (this._plotInfo.fillFrom == Number.NEGATIVE_INFINITY) {
+                    ctx.lineTo(this._canvas.width, 0);
+                } else if (this._plotInfo.fillFrom == Number.POSITIVE_INFINITY) {
+                    ctx.lineTo(this._canvas.width, this._canvas.height);
+                    ctx.lineTo(0, this._canvas.height);
+                } else {
+                    ctx.lineTo(this._canvas.width, this._valueGeometry.toScreen(this._plotInfo.fillFrom));
+                    ctx.lineTo(0, this._valueGeometry.toScreen(this._plotInfo.fillFrom));
+                }
+                ctx.fill();
+            }
+                    
+            if (this._plotInfo.lineColor) {
+                ctx.strokeStyle = this._plotInfo.lineColor.toString();
+                ctx.beginPath();
+                var first = true;
+                this._plot(function(x,y) {
+                        if (first) {
+                             first = false;
+                             ctx.moveTo(x,y);
+                        }
+                    ctx.lineTo(x,y);
+                });
+                ctx.stroke();
+            }
+
+            if (this._plotInfo.dotColor) {
+                ctx.fillStyle = this._plotInfo.dotColor.toString();
+                var r = this._plotInfo.dotRadius;
+                this._plot(function(x,y) {
+                    ctx.beginPath();
+                    ctx.arc(x,y,r,0,2*Math.PI,true);
+                    ctx.fill();
+                });
+            }
+        }
+
+        if (this._eventSource) {
+            var gradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
+            gradient.addColorStop(1, 'rgba(255,255,255,0)');
+
+            ctx.strokeStyle = gradient;
+            ctx.fillStyle = gradient; 
+            ctx.lineWidth = this._plotInfo.eventLineWidth;
+            ctx.lineJoin = 'miter';
+            
+            var i = this._eventSource.getAllEventIterator();
+            while (i.hasNext()) {
+                var event = i.next();
+                var color = event.getColor();
+                color = (color) ? new Timeplot.Color(color) : this._plotInfo.lineColor;
+                var eventStart = event.getStart().getTime();
+                var eventEnd = event.getEnd().getTime();
+                if (eventStart == eventEnd) {
+                    var c = color.toString();
+                    gradient.addColorStop(0, c);
+                    var start = this._timeGeometry.toScreen(eventStart);
+                    start = Math.floor(start) + 0.5; // center it between two pixels (makes the rendering nicer)
+                    var end = start;
+                    ctx.beginPath();
+                    ctx.moveTo(start,0);
+                    ctx.lineTo(start,this._canvas.height);
+                    ctx.stroke();
+                    var x = start - 4;
+                    var w = 7;
+                } else {
+                    var c = color.toString(0.5);
+                    gradient.addColorStop(0, c);
+                    var start = this._timeGeometry.toScreen(eventStart);
+                    start = Math.floor(start) + 0.5; // center it between two pixels (makes the rendering nicer)
+                    var end = this._timeGeometry.toScreen(eventEnd);
+                    end = Math.floor(end) + 0.5; // center it between two pixels (makes the rendering nicer)
+                    ctx.fillRect(start,0,end - start, this._canvas.height);
+                    var x = start;
+                    var w = end - start - 1;
+                }
+
+                var div = this._timeplot.putDiv(event.getID(),"timeplot-event-box",{
+                    left: Math.round(x),
+                    width: Math.round(w),
+                    top: 0,
+                    height: this._canvas.height - 1
+                });
+
+                var plot = this;
+                var clickHandler = function(event) { 
+                    return function(elmt, evt, target) { 
+                        var doc = plot._timeplot.getDocument();
+                        plot._closeBubble();
+                        var coords = SimileAjax.DOM.getEventPageCoordinates(evt);
+                        var elmtCoords = SimileAjax.DOM.getPageCoordinates(elmt);
+                        plot._bubble = SimileAjax.Graphics.createBubbleForPoint(coords.x, elmtCoords.top + plot._canvas.height, plot._plotInfo.bubbleWidth, plot._plotInfo.bubbleHeight, "bottom");
+                        event.fillInfoBubble(plot._bubble.content, plot._theme, plot._timeGeometry.getLabeler());
+                    }
+                };
+                var mouseOverHandler = function(elmt, evt, target) {
+                    elmt.oldClass = elmt.className;
+                    elmt.className = elmt.className + " timeplot-event-box-highlight";
+                };
+                var mouseOutHandler = function(elmt, evt, target) {
+                    elmt.className = elmt.oldClass;
+                    elmt.oldClass = null;
+                }
+                
+                if (!div.instrumented) {
+                    SimileAjax.DOM.registerEvent(div, "click"    , clickHandler(event));
+                    SimileAjax.DOM.registerEvent(div, "mouseover", mouseOverHandler);
+                    SimileAjax.DOM.registerEvent(div, "mouseout" , mouseOutHandler);
+                    div.instrumented = true;
+                }
+            }
+        }
+    },
+
+    _plot: function(f) {
+        var data = this._dataSource.getData();
+        if (data) {
+            var times = data.times;
+            var values = data.values;
+            var T = times.length;
+            for (var t = 0; t < T; t++) {
+                var x = this._timeGeometry.toScreen(times[t]);
+                var y = this._valueGeometry.toScreen(values[t]);
+                f(x, y);
+            }
+        }
+    },
+    
+    _closeBubble: function() {
+        if (this._bubble != null) {
+            this._bubble.close();
+            this._bubble = null;
+        }
+    }
+
+}
+/**
+ * Sources
+ * 
+ * @fileOverview Sources
+ * @name Sources
+ */
+
+/**
+ * Timeplot.DefaultEventSource is an extension of Timeline.DefaultEventSource
+ * and therefore reuses the exact same event loading subsystem that
+ * Timeline uses.
+ * 
+ * @constructor
+ */
+Timeplot.DefaultEventSource = function(eventIndex) {
+    Timeline.DefaultEventSource.apply(this, arguments);
+};
+
+Object.extend(Timeplot.DefaultEventSource.prototype, Timeline.DefaultEventSource.prototype);
+
+/**
+ * Function used by Timeplot to load time series data from a text file.
+ */
+Timeplot.DefaultEventSource.prototype.loadText = function(text, separator, url, filter, format) {
+
+    if (text == null) {
+        return;
+    }
+
+    this._events.maxValues = new Array();
+    var base = this._getBaseURL(url);
+
+    if (!format) format = 'iso8601';
+    var parseDateTimeFunction = this._events.getUnit().getParser(format);
+
+    var data = this._parseText(text, separator);
+
+    var added = false;
+
+    if (filter) {
+        data = filter(data);
+    }
+
+    if (data) {
+        for (var i = 0; i < data.length; i++){
+            var row = data[i];
+            if (row.length > 1) {
+                var dateStr = SimileAjax.jQuery.trim(row[0]);
+                var date = parseDateTimeFunction(dateStr);
+                if (date) {
+                    var evt = new Timeplot.DefaultEventSource.NumericEvent(date,row.slice(1));
+                    this._events.add(evt);
+                    added = true;
+                }
+            }
+        }
+    }
+
+    if (added) {
+        this._fire("onAddMany", []);
+    }
+}
+
+/*
+ * Parse the data file.
+ * 
+ * Adapted from http://www.kawa.net/works/js/jkl/js/jkl-parsexml.js by Yusuke Kawasaki
+ */
+Timeplot.DefaultEventSource.prototype._parseText = function (text, separator) {
+    text = text.replace( /\r\n?/g, "\n" ); // normalize newlines
+    var pos = 0;
+    var len = text.length;
+    var table = [];
+    while (pos < len) {
+        var line = [];
+        if (text.charAt(pos) != '#') { // if it's not a comment, process
+            while (pos < len) {
+                if (text.charAt(pos) == '"') {            // "..." quoted column
+                    var nextquote = text.indexOf('"', pos+1 );
+                    while (nextquote<len && nextquote > -1) {
+                        if (text.charAt(nextquote+1) != '"') {
+                            break;                          // end of column
+                        }
+                        nextquote = text.indexOf('"', nextquote + 2);
+                    }
+                    if ( nextquote < 0 ) {
+                        // unclosed quote
+                    } else if (text.charAt(nextquote + 1) == separator) { // end of column
+                        var quoted = text.substr(pos + 1, nextquote-pos - 1);
+                        quoted = quoted.replace(/""/g,'"');
+                        line[line.length] = quoted;
+                        pos = nextquote + 2;
+                        continue;
+                    } else if (text.charAt(nextquote + 1) == "\n" || // end of line
+                               len == nextquote + 1 ) {              // end of file
+                        var quoted = text.substr(pos + 1, nextquote-pos - 1);
+                        quoted = quoted.replace(/""/g,'"');
+                        line[line.length] = quoted;
+                        pos = nextquote + 2;
+                        break;
+                    } else {
+                        // invalid column
+                    }
+                }
+                var nextseparator = text.indexOf(separator, pos);
+                var nextnline = text.indexOf("\n", pos);
+                if (nextnline < 0) nextnline = len;
+                if (nextseparator > -1 && nextseparator < nextnline) {
+                    line[line.length] = text.substr(pos, nextseparator-pos);
+                    pos = nextseparator + 1;
+                } else {                                    // end of line
+                    line[line.length] = text.substr(pos, nextnline-pos);
+                    pos = nextnline + 1;
+                    break;
+                }
+            }
+        } else { // if it's a comment, ignore
+            var nextnline = text.indexOf("\n", pos);
+            pos = (nextnline > -1) ? nextnline + 1 : cur;
+        }
+        if (line.length > 0) {
+            table[table.length] = line;                 // push line
+        }
+    }
+    if (table.length < 0) return;                     // null data
+    return table;
+}
+
+/**
+ * Return the range of the loaded data
+ */
+Timeplot.DefaultEventSource.prototype.getRange = function() {
+    var earliestDate = this.getEarliestDate();
+    var latestDate = this.getLatestDate();
+    return {
+        earliestDate: (earliestDate) ? earliestDate : null,
+        latestDate: (latestDate) ? latestDate : null,
+        min: 0,
+        max: 0
+    };
+}
+
+// -----------------------------------------------------------------------
+
+/**
+ * A NumericEvent is an Event that also contains an array of values, 
+ * one for each columns in the loaded data file.
+ * 
+ * @constructor
+ */
+Timeplot.DefaultEventSource.NumericEvent = function(time, values) {
+    this._id = "e" + Math.round(Math.random() * 1000000);
+    this._time = time;
+    this._values = values;
+};
+
+Timeplot.DefaultEventSource.NumericEvent.prototype = {
+    getID:          function() { return this._id; },
+    getTime:        function() { return this._time; },
+    getValues:      function() { return this._values; },
+
+    // these are required by the EventSource
+    getStart:       function() { return this._time; },
+    getEnd:         function() { return this._time; }
+};
+
+// -----------------------------------------------------------------------
+
+/**
+ * A DataSource represent an abstract class that represents a monodimensional time series.
+ * 
+ * @constructor
+ */
+Timeplot.DataSource = function(eventSource) {
+    this._eventSource = eventSource;
+    var source = this;
+    this._processingListener = {
+        onAddMany: function() { source._process(); },
+        onClear:   function() { source._clear(); }
+    }
+    this.addListener(this._processingListener);
+    this._listeners = [];
+    this._data = null;
+    this._range = null;
+};
+
+Timeplot.DataSource.prototype = {
+  
+    _clear: function() {
+        this._data = null;
+        this._range = null;
+    },
+
+    _process: function() {
+        this._data = {
+            times: new Array(),
+            values: new Array()
+        };
+        this._range = {
+            earliestDate: null,
+            latestDate: null,
+            min: 0,
+            max: 0
+        };
+    },
+
+    /**
+     * Return the range of this data source
+     */
+    getRange: function() {
+        return this._range;
+    },
+
+    /**
+     * Return the actual data that this data source represents.
+     * NOTE: _data = { times: [], values: [] }
+     */
+    getData: function() {
+        return this._data;
+    },
+    
+    /**
+     * Return the value associated with the given time in this time series
+     */
+    getValue: function(t) {
+        if (this._data) {
+            for (var i = 0; i < this._data.times.length; i++) {
+                var l = this._data.times[i];
+                if (l >= t) {
+                    return this._data.values[i];
+                }
+            }
+        }
+        return 0;
+    },
+
+    /**
+     * Return the time of the data point closest to the given time.
+     */
+    getClosestValidTime: function(t) {
+        if (this._data) {
+            for (var i = 0; i < this._data.times.length; i++) {
+                var currentTime = this._data.times[i];
+                if (currentTime >= t) {
+                    if (i <= 0) {
+                        return currentTime;
+                    } else {
+                        var lastTime = this._data.times[i - 1];
+                        // t must be between currentTime and lastTime.
+                        // Find the closest one.
+                        if (t - lastTime < currentTime - t) {
+                            return lastTime;
+                        } else {
+                            return currentTime;
+                        }
+                    }
+                }
+            }
+        }
+        return 0;
+    },
+
+    /**
+     * Add a listener to the underlying event source
+     */
+    addListener: function(listener) {
+        this._eventSource.addListener(listener);
+    },
+
+    /**
+     * Remove a listener from the underlying event source
+     */
+    removeListener: function(listener) {
+        this._eventSource.removeListener(listener);
+    },
+
+    /**
+     * Replace a listener from the underlying event source
+     */
+    replaceListener: function(oldListener, newListener) {
+        this.removeListener(oldListener);
+        this.addListener(newListener);
+    }
+
+}
+
+// -----------------------------------------------------------------------
+
+/**
+ * Implementation of a DataSource that extracts the time series out of a 
+ * single column from the events
+ * 
+ * @constructor
+ */
+Timeplot.ColumnSource = function(eventSource, column) {
+    Timeplot.DataSource.apply(this, arguments);
+    this._column = column - 1;
+};
+
+Object.extend(Timeplot.ColumnSource.prototype,Timeplot.DataSource.prototype);
+
+Timeplot.ColumnSource.prototype.dispose = function() {
+    this.removeListener(this._processingListener);
+    this._clear();
+}
+
+Timeplot.ColumnSource.prototype._process = function() {
+    var count = this._eventSource.getCount();
+    var times = new Array(count);
+    var values = new Array(count);
+    var min = Number.MAX_VALUE;
+    var max = Number.MIN_VALUE;
+    var i = 0;
+
+    var iterator = this._eventSource.getAllEventIterator();
+    while (iterator.hasNext()) {
+        var event = iterator.next();
+        var time = event.getTime();
+        times[i] = time;
+        var value = this._getValue(event);
+        if (!isNaN(value)) {
+           if (value < min) {
+               min = value;
+           }
+           if (value > max) {
+               max = value;
+           }    
+            values[i] = value;
+        }
+        i++;
+    }
+
+    this._data = {
+        times: times,
+        values: values
+    };
+
+    if (max == Number.MIN_VALUE) max = 1;
+    
+    this._range = {
+        earliestDate: this._eventSource.getEarliestDate(),
+        latestDate: this._eventSource.getLatestDate(),
+        min: min,
+        max: max
+    };
+}
+
+Timeplot.ColumnSource.prototype._getValue = function(event) {
+    return parseFloat(event.getValues()[this._column]);
+}
+
+// ---------------------------------------------------------------
+
+/**
+ * Data Source that generates the time series out of the difference
+ * between the first and the second column
+ * 
+ * @constructor
+ */
+Timeplot.ColumnDiffSource = function(eventSource, column1, column2) {
+    Timeplot.ColumnSource.apply(this, arguments);
+    this._column2 = column2 - 1;
+};
+
+Object.extend(Timeplot.ColumnDiffSource.prototype,Timeplot.ColumnSource.prototype);
+
+Timeplot.ColumnDiffSource.prototype._getValue = function(event) {
+    var a = parseFloat(event.getValues()[this._column]);
+    var b = parseFloat(event.getValues()[this._column2]);
+    return a - b;
+}
+/**
+ * Geometries
+ * 
+ * @fileOverview Geometries
+ * @name Geometries
+ */
+
+/**
+ * This is the constructor for the default value geometry.
+ * A value geometry is what regulates mapping of the plot values to the screen y coordinate.
+ * If two plots share the same value geometry, they will be drawn using the same scale.
+ * If "min" and "max" parameters are not set, the geometry will stretch itself automatically
+ * so that the entire plot will be drawn without overflowing. The stretching happens also
+ * when a geometry is shared between multiple plots, the one with the biggest range will
+ * win over the others.
+ * 
+ * @constructor
+ */
+Timeplot.DefaultValueGeometry = function(params) {
+    if (!params) params = {};
+    this._id = ("id" in params) ? params.id : "g" + Math.round(Math.random() * 1000000);
+    this._axisColor = ("axisColor" in params) ? ((typeof params.axisColor == "string") ? new Timeplot.Color(params.axisColor) : params.axisColor) : new Timeplot.Color("#606060"),
+    this._gridColor = ("gridColor" in params) ? ((typeof params.gridColor == "string") ? new Timeplot.Color(params.gridColor) : params.gridColor) : null,
+    this._gridLineWidth = ("gridLineWidth" in params) ? params.gridLineWidth : 0.5;
+    this._axisLabelsPlacement = ("axisLabelsPlacement" in params) ? params.axisLabelsPlacement : "right";
+    this._gridSpacing = ("gridSpacing" in params) ? params.gridStep : 50;
+    this._gridType = ("gridType" in params) ? params.gridType : "short";
+    this._gridShortSize = ("gridShortSize" in params) ? params.gridShortSize : 10;
+    this._minValue = ("min" in params) ? params.min : null;
+    this._maxValue = ("max" in params) ? params.max : null;
+    this._linMap = {
+        direct: function(v) {
+            return v;
+        },
+        inverse: function(y) {
+            return y;
+        }
+    }
+    this._map = this._linMap;
+    this._labels = [];
+    this._grid = [];
+}
+
+Timeplot.DefaultValueGeometry.prototype = {
+
+    /**
+     * Since geometries can be reused across timeplots, we need to call this function
+     * before we can paint using this geometry.
+     */
+    setTimeplot: function(timeplot) {
+        this._timeplot = timeplot;
+        this._canvas = timeplot.getCanvas();
+        this.reset();
+    },
+
+    /**
+     * Called by all the plot layers this geometry is associated with
+     * to update the value range. Unless min/max values are specified
+     * in the parameters, the biggest value range will be used.
+     */
+    setRange: function(range) {
+        if ((this._minValue == null) || ((this._minValue != null) && (range.min < this._minValue))) {
+            this._minValue = range.min;
+        }
+        if ((this._maxValue == null) || ((this._maxValue != null) && (range.max * 1.05 > this._maxValue))) {
+            this._maxValue = range.max * 1.05; // get a little more head room to avoid hitting the ceiling
+        }
+
+        this._updateMappedValues();
+
+        if (!(this._minValue == 0 && this._maxValue == 0)) {
+            this._grid = this._calculateGrid();
+        }
+    },
+
+    /**
+     * Called after changing ranges or canvas size to reset the grid values
+     */
+    reset: function() {
+        this._clearLabels();
+        this._updateMappedValues();
+        this._grid = this._calculateGrid();
+    },
+
+    /**
+     * Map the given value to a y screen coordinate.
+     */
+    toScreen: function(value) {
+        if (this._canvas && this._maxValue) {
+            var v = value - this._minValue;
+            return this._canvas.height * (this._map.direct(v)) / this._mappedRange;
+        } else {
+            return -50;
+        }
+    },
+
+    /**
+     * Map the given y screen coordinate to a value
+     */
+    fromScreen: function(y) {
+        if (this._canvas) {
+            return this._map.inverse(this._mappedRange * y / this._canvas.height) + this._minValue;
+        } else {
+            return 0;
+        }
+    },
+
+    /**
+     * Each geometry is also a painter and paints the value grid and grid labels.
+     */
+    paint: function() {
+        if (this._timeplot) {
+            var ctx = this._canvas.getContext('2d');
+    
+            ctx.lineJoin = 'miter';
+    
+            // paint grid
+            if (this._gridColor) {        
+                var gridGradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
+                gridGradient.addColorStop(0, this._gridColor.toHexString());
+                gridGradient.addColorStop(0.3, this._gridColor.toHexString());
+                gridGradient.addColorStop(1, "rgba(255,255,255,0.5)");
+
+                ctx.lineWidth = this._gridLineWidth;
+                ctx.strokeStyle = gridGradient;
+    
+                for (var i = 0; i < this._grid.length; i++) {
+                    var tick = this._grid[i];
+                    var y = Math.floor(tick.y) + 0.5;
+                    if (typeof tick.label != "undefined") {
+                        if (this._axisLabelsPlacement == "left") {
+                            var div = this._timeplot.putText(this._id + "-" + i, tick.label,"timeplot-grid-label",{
+                                left: 4,
+                                bottom: y + 2,
+                                color: this._gridColor.toHexString(),
+                                visibility: "hidden"
+                            });
+                            this._labels.push(div);
+                        } else if (this._axisLabelsPlacement == "right") {
+                            var div = this._timeplot.putText(this._id + "-" + i, tick.label, "timeplot-grid-label",{
+                                right: 4,
+                                bottom: y + 2,
+                                color: this._gridColor.toHexString(),
+                                visibility: "hidden"
+                            });
+                            this._labels.push(div);
+                        }
+                        if (y + div.clientHeight < this._canvas.height + 10) {
+                            div.style.visibility = "visible"; // avoid the labels that would overflow
+                        }
+                    }
+
+                    // draw grid
+                    ctx.beginPath();
+                    if (this._gridType == "long" || tick.label == 0) {
+                        ctx.moveTo(0, y);
+                        ctx.lineTo(this._canvas.width, y);
+                    } else if (this._gridType == "short") {
+                        if (this._axisLabelsPlacement == "left") {
+                            ctx.moveTo(0, y);
+                            ctx.lineTo(this._gridShortSize, y);
+                        } else if (this._axisLabelsPlacement == "right") {
+                            ctx.moveTo(this._canvas.width, y);
+                            ctx.lineTo(this._canvas.width - this._gridShortSize, y);
+                        }                       
+                    }
+                    ctx.stroke();
+                }
+            }
+        
+            // paint axis
+            var axisGradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
+            axisGradient.addColorStop(0, this._axisColor.toString());
+            axisGradient.addColorStop(0.5, this._axisColor.toString());
+            axisGradient.addColorStop(1, "rgba(255,255,255,0.5)");
+            
+            ctx.lineWidth = 1;
+            ctx.strokeStyle = axisGradient;
+    
+            // left axis
+            ctx.beginPath();
+            ctx.moveTo(0,this._canvas.height);
+            ctx.lineTo(0,0);
+            ctx.stroke();
+            
+            // right axis
+            ctx.beginPath();
+            ctx.moveTo(this._canvas.width,0);
+            ctx.lineTo(this._canvas.width,this._canvas.height);
+            ctx.stroke();
+        }
+    },
+    
+    /**
+     * Removes all the labels that were added by this geometry
+     */
+    _clearLabels: function() {
+        for (var i = 0; i < this._labels.length; i++) {
+            var l = this._labels[i];
+            var parent = l.parentNode;
+            if (parent) parent.removeChild(l);
+        }
+    },
+    
+    /*
+     * This function calculates the grid spacing that it will be used 
+     * by this geometry to draw the grid in order to reduce clutter. 
+     */
+    _calculateGrid: function() {
+        var grid = [];
+        
+        if (!this._canvas || this._valueRange == 0) return grid;
+                
+        var power = 0;
+        if (this._valueRange > 1) {
+            while (Math.pow(10,power) < this._valueRange) {
+                power++;
+            }
+            power--;
+        } else {
+            while (Math.pow(10,power) > this._valueRange) {
+                power--;
+            }
+        }
+
+        var unit = Math.pow(10,power);
+        var inc = unit;
+        while (true) {
+            var dy = this.toScreen(this._minValue + inc);
+
+            while (dy < this._gridSpacing) {
+                inc += unit;
+                dy = this.toScreen(this._minValue + inc);
+            }
+
+            if (dy > 2 * this._gridSpacing) { // grids are too spaced out
+                unit /= 10;
+                inc = unit;
+            } else {
+                break;
+            }
+        }
+        
+        var v = 0;
+        var y = this.toScreen(v);
+        if (this._minValue >= 0) {
+            while (y < this._canvas.height) {
+                if (y > 0) {
+                    grid.push({ y: y, label: v });
+                }
+                v += inc;
+                y = this.toScreen(v);
+            }
+        } else if (this._maxValue <= 0) {
+            while (y > 0) {
+                if (y < this._canvas.height) {
+                    grid.push({ y: y, label: v });
+                }
+                v -= inc;
+                y = this.toScreen(v);
+            }
+        } else {
+            while (y < this._canvas.height) {
+                if (y > 0) {
+                    grid.push({ y: y, label: v });
+                }
+                v += inc;
+                y = this.toScreen(v);
+            }
+            v = -inc;
+            y = this.toScreen(v);
+            while (y > 0) {
+                if (y < this._canvas.height) {
+                    grid.push({ y: y, label: v });
+                }
+                v -= inc;
+                y = this.toScreen(v);
+            }
+        }
+        
+        return grid;
+    },
+
+    /*
+     * Update the values that are used by the paint function so that
+     * we don't have to calculate them at every repaint.
+     */
+    _updateMappedValues: function() {
+        this._valueRange = Math.abs(this._maxValue - this._minValue);
+        this._mappedRange = this._map.direct(this._valueRange);
+    }
+    
+}
+
+// --------------------------------------------------
+
+/**
+ * This is the constructor for a Logarithmic value geometry, which
+ * is useful when plots have values in different magnitudes but 
+ * exhibit similar trends and such trends want to be shown on the same
+ * plot (here a cartesian geometry would make the small magnitudes 
+ * disappear).
+ * 
+ * NOTE: this class extends Timeplot.DefaultValueGeometry and inherits
+ * all of the methods of that class. So refer to that class. 
+ * 
+ * @constructor
+ */
+Timeplot.LogarithmicValueGeometry = function(params) {
+    Timeplot.DefaultValueGeometry.apply(this, arguments);
+    this._logMap = {
+        direct: function(v) {
+            return Math.log(v + 1) / Math.log(10);
+        },
+        inverse: function(y) {
+            return Math.exp(Math.log(10) * y) - 1;
+        }
+    }
+    this._mode = "log";
+    this._map = this._logMap;
+    this._calculateGrid = this._logarithmicCalculateGrid;
+};
+
+Timeplot.LogarithmicValueGeometry.prototype._linearCalculateGrid = Timeplot.DefaultValueGeometry.prototype._calculateGrid;
+
+Object.extend(Timeplot.LogarithmicValueGeometry.prototype,Timeplot.DefaultValueGeometry.prototype);
+
+/*
+ * This function calculates the grid spacing that it will be used 
+ * by this geometry to draw the grid in order to reduce clutter. 
+ */
+Timeplot.LogarithmicValueGeometry.prototype._logarithmicCalculateGrid = function() {
+    var grid = [];
+    
+    if (!this._canvas || this._valueRange == 0) return grid;
+
+    var v = 1;
+    var y = this.toScreen(v);
+    while (y < this._canvas.height || isNaN(y)) {
+        if (y > 0) {
+            grid.push({ y: y, label: v });
+        }
+        v *= 10;
+        y = this.toScreen(v);
+    }
+    
+    return grid;
+};
+
+/**
+ * Turn the logarithmic scaling off. 
+ */
+Timeplot.LogarithmicValueGeometry.prototype.actLinear = function() {
+    this._mode = "lin";
+    this._map = this._linMap;
+    this._calculateGrid = this._linearCalculateGrid;
+    this.reset();
+}
+
+/**
+ * Turn the logarithmic scaling on. 
+ */
+Timeplot.LogarithmicValueGeometry.prototype.actLogarithmic = function() {
+    this._mode = "log";
+    this._map = this._logMap;
+    this._calculateGrid = this._logarithmicCalculateGrid;
+    this.reset();
+}
+
+/**
+ * Toggle logarithmic scaling seeting it to on if off and viceversa. 
+ */
+Timeplot.LogarithmicValueGeometry.prototype.toggle = function() {
+    if (this._mode == "log") {
+        this.actLinear();
+    } else {
+        this.actLogarithmic();
+    }
+}
+
+// -----------------------------------------------------
+
+/**
+ * This is the constructor for the default time geometry.
+ * 
+ * @constructor
+ */
+Timeplot.DefaultTimeGeometry = function(params) {
+    if (!params) params = {};
+    this._id = ("id" in params) ? params.id : "g" + Math.round(Math.random() * 1000000);
+    this._locale = ("locale" in params) ? params.locale : "en";
+    this._timeZone = ("timeZone" in params) ? params.timeZone : SimileAjax.DateTime.getTimezone();
+    this._labeler = ("labeller" in params) ? params.labeller : null;
+    this._axisColor = ("axisColor" in params) ? ((params.axisColor == "string") ? new Timeplot.Color(params.axisColor) : params.axisColor) : new Timeplot.Color("#606060"),
+    this._gridColor = ("gridColor" in params) ? ((params.gridColor == "string") ? new Timeplot.Color(params.gridColor) : params.gridColor) : null,
+    this._gridLineWidth = ("gridLineWidth" in params) ? params.gridLineWidth : 0.5;
+    this._axisLabelsPlacement = ("axisLabelsPlacement" in params) ? params.axisLabelsPlacement : "bottom";
+    this._gridStep = ("gridStep" in params) ? params.gridStep : 100;
+    this._gridStepRange = ("gridStepRange" in params) ? params.gridStepRange : 20;
+    this._min = ("min" in params) ? params.min : null;
+    this._max = ("max" in params) ? params.max : null;
+    this._timeValuePosition =("timeValuePosition" in params) ? params.timeValuePosition : "bottom";
+    this._unit = ("unit" in params) ? params.unit : Timeline.NativeDateUnit;
+    this._linMap = {
+        direct: function(t) {
+            return t;
+        },
+        inverse: function(x) {
+            return x;
+        }
+    }
+    this._map = this._linMap;
+    if (!this._labeler)
+        this._labeler = this._unit.createLabeller(this._locale, this._timeZone);
+    var dateParser = this._unit.getParser("iso8601");
+    if (this._min && !this._min.getTime) {
+        this._min = dateParser(this._min);
+    }
+    if (this._max && !this._max.getTime) {
+        this._max = dateParser(this._max);
+    }
+    this._labels = [];
+    this._grid = [];
+}
+
+Timeplot.DefaultTimeGeometry.prototype = {
+
+    /**
+     * Since geometries can be reused across timeplots, we need to call this function
+     * before we can paint using this geometry.
+     */
+    setTimeplot: function(timeplot) {
+        this._timeplot = timeplot;
+        this._canvas = timeplot.getCanvas();
+        this.reset();
+    },
+
+    /**
+     * Called by all the plot layers this geometry is associated with
+     * to update the time range. Unless min/max values are specified
+     * in the parameters, the biggest range will be used.
+     */
+    setRange: function(range) {
+        if (this._min) {
+            this._earliestDate = this._min;
+        } else if (range.earliestDate && ((this._earliestDate == null) || ((this._earliestDate != null) && (range.earliestDate.getTime() < this._earliestDate.getTime())))) {
+            this._earliestDate = range.earliestDate;
+        }
+        
+        if (this._max) {
+            this._latestDate = this._max;
+        } else if (range.latestDate && ((this._latestDate == null) || ((this._latestDate != null) && (range.latestDate.getTime() > this._latestDate.getTime())))) {
+            this._latestDate = range.latestDate;
+        }
+
+        if (!this._earliestDate && !this._latestDate) {
+            this._grid = [];
+        } else {
+            this.reset(); 
+        }
+    },
+    
+    /**
+     * Called after changing ranges or canvas size to reset the grid values
+     */
+    reset: function() {
+        this._updateMappedValues();
+        if (this._canvas) this._grid = this._calculateGrid();
+    },
+    
+    /**
+     * Map the given date to a x screen coordinate.
+     */
+    toScreen: function(time) {
+        if (this._canvas && this._latestDate) {
+            var t = time - this._earliestDate.getTime();
+            var fraction = (this._mappedPeriod > 0) ? this._map.direct(t) / this._mappedPeriod : 0;
+            return this._canvas.width * fraction;
+        } else {
+            return -50;
+        } 
+    },
+
+    /**
+     * Map the given x screen coordinate to a date.
+     */
+    fromScreen: function(x) {
+        if (this._canvas) {
+            return this._map.inverse(this._mappedPeriod * x / this._canvas.width) + this._earliestDate.getTime();
+        } else {
+            return 0;
+        } 
+    },
+    
+    /**
+     * Get a period (in milliseconds) this time geometry spans.
+     */
+    getPeriod: function() {
+        return this._period;
+    },
+    
+    /**
+     * Return the labeler that has been associated with this time geometry
+     */
+    getLabeler: function() {
+        return this._labeler;
+    },
+
+    /**
+     * Return the time unit associated with this time geometry
+     */
+    getUnit: function() {
+        return this._unit;
+    },
+
+   /**
+    * Each geometry is also a painter and paints the value grid and grid labels.
+    */
+    paint: function() {
+        if (this._canvas) {
+            var unit = this._unit;
+            var ctx = this._canvas.getContext('2d');
+    
+            var gradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
+    
+            ctx.strokeStyle = gradient;
+            ctx.lineWidth = this._gridLineWidth;
+            ctx.lineJoin = 'miter';
+    
+            // paint grid
+            if (this._gridColor) {        
+                gradient.addColorStop(0, this._gridColor.toString());
+                gradient.addColorStop(1, "rgba(255,255,255,0.9)");
+    
+                for (var i = 0; i < this._grid.length; i++) {
+                    var tick = this._grid[i];
+                    var x = Math.floor(tick.x) + 0.5;
+                    if (this._axisLabelsPlacement == "top") {
+                        var div = this._timeplot.putText(this._id + "-" + i, tick.label,"timeplot-grid-label",{
+                            left: x + 4,
+                            top: 2,
+                            visibility: "hidden"
+                        });
+                        this._labels.push(div);
+                    } else if (this._axisLabelsPlacement == "bottom") {
+                        var div = this._timeplot.putText(this._id + "-" + i, tick.label, "timeplot-grid-label",{
+                            left: x + 4,
+                            bottom: 2,
+                            visibility: "hidden"
+                        });
+                        this._labels.push(div);
+                    }
+                    if (x + div.clientWidth < this._canvas.width + 10) {
+                        div.style.visibility = "visible"; // avoid the labels that would overflow
+                    }
+
+                    // draw separator
+                    ctx.beginPath();
+                    ctx.moveTo(x,0);
+                    ctx.lineTo(x,this._canvas.height);
+                    ctx.stroke();
+                }
+            }
+    
+            // paint axis
+            gradient.addColorStop(0, this._axisColor.toString());
+            gradient.addColorStop(1, "rgba(255,255,255,0.5)");
+            
+            ctx.lineWidth = 1;
+            gradient.addColorStop(0, this._axisColor.toString());
+    
+            ctx.beginPath();
+            ctx.moveTo(0,0);
+            ctx.lineTo(this._canvas.width,0);
+            ctx.stroke();
+        }
+    },
+    
+    /*
+     * This function calculates the grid spacing that it will be used 
+     * by this geometry to draw the grid in order to reduce clutter. 
+     */
+    _calculateGrid: function() {
+        var grid = [];
+        
+        var time = SimileAjax.DateTime;
+        var u = this._unit;
+        var p = this._period;
+        
+        if (p == 0) return grid;
+        
+        // find the time units nearest to the time period
+        if (p > time.gregorianUnitLengths[time.MILLENNIUM]) {
+            unit = time.MILLENNIUM; 
+        } else {
+            for (var unit = time.MILLENNIUM; unit > 0; unit--) {
+                if (time.gregorianUnitLengths[unit-1] <= p && p < time.gregorianUnitLengths[unit]) {
+                    unit--;
+                    break;
+                }
+            }
+        }
+
+        var t = u.cloneValue(this._earliestDate);
+
+        do {
+            time.roundDownToInterval(t, unit, this._timeZone, 1, 0);
+            var x = this.toScreen(u.toNumber(t));
+            switch (unit) {
+                case time.SECOND:
+                  var l = t.toLocaleTimeString();
+                  break;
+                case time.MINUTE:
+                  var m = t.getMinutes();
+                  var l = t.getHours() + ":" + ((m < 10) ? "0" : "") + m;
+                  break;
+                case time.HOUR:
+                  var l = t.getHours() + ":00";
+                  break;
+                case time.DAY:
+                case time.WEEK:
+                case time.MONTH:
+                  var l = t.toLocaleDateString();
+                  break;  
+                case time.YEAR:
+                case time.DECADE:
+                case time.CENTURY:
+                case time.MILLENNIUM:
+		  var tmpd = new Date(t.getTime());
+                  tmpd.setDate(t.getDate() + 1);
+                  var l = tmpd.getUTCFullYear();
+                  break;
+            }
+            if (x > 0) { 
+                grid.push({ x: x, label: l });
+            }
+            time.incrementByInterval(t, unit, this._timeZone);
+        } while (t.getTime() < this._latestDate.getTime());
+        
+        return grid;
+    },
+
+    /*
+     * Clear labels generated by this time geometry.
+     */
+    _clearLabels: function() {
+        for (var i = 0; i < this._labels.length; i++) {
+            var l = this._labels[i];
+            var parent = l.parentNode;
+            if (parent) parent.removeChild(l);
+        }
+    },
+        
+    /*
+     * Update the values that are used by the paint function so that
+     * we don't have to calculate them at every repaint.
+     */
+    _updateMappedValues: function() {
+        if (this._latestDate && this._earliestDate) {
+            this._period = this._latestDate.getTime() - this._earliestDate.getTime();
+            this._mappedPeriod = this._map.direct(this._period);
+        } else {
+            this._period = 0;
+            this._mappedPeriod = 0;
+        }
+    }
+    
+}
+
+// --------------------------------------------------------------
+
+/**
+ * This is the constructor for the magnifying time geometry.
+ * Users can interact with this geometry and 'magnify' certain areas of the
+ * plot to see the plot enlarged and resolve details that would otherwise
+ * get lost or cluttered with a linear time geometry.
+ * 
+ * @constructor
+ */
+Timeplot.MagnifyingTimeGeometry = function(params) {
+    Timeplot.DefaultTimeGeometry.apply(this, arguments);
+        
+    var g = this;
+    this._MagnifyingMap = {
+        direct: function(t) {
+            if (t < g._leftTimeMargin) {
+                var x = t * g._leftRate;
+            } else if ( g._leftTimeMargin < t && t < g._rightTimeMargin ) {
+                var x = t * g._expandedRate + g._expandedTimeTranslation;
+            } else {
+                var x = t * g._rightRate + g._rightTimeTranslation;
+            }
+            return x;
+        },
+        inverse: function(x) {
+            if (x < g._leftScreenMargin) {
+                var t = x / g._leftRate;
+            } else if ( g._leftScreenMargin < x && x < g._rightScreenMargin ) {
+                var t = x / g._expandedRate + g._expandedScreenTranslation;
+            } else {
+                var t = x / g._rightRate + g._rightScreenTranslation;
+            }
+            return t;
+        }
+    }
+
+    this._mode = "lin";
+    this._map = this._linMap;
+};
+
+Object.extend(Timeplot.MagnifyingTimeGeometry.prototype,Timeplot.DefaultTimeGeometry.prototype);
+
+/**
+ * Initialize this geometry associating it with the given timeplot and 
+ * register the geometry event handlers to the timeplot so that it can
+ * interact with the user.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.initialize = function(timeplot) {
+    Timeplot.DefaultTimeGeometry.prototype.initialize.apply(this, arguments);
+
+    if (!this._lens) {
+        this._lens = this._timeplot.putDiv("lens","timeplot-lens");
+    }
+
+    var period = 1000 * 60 * 60 * 24 * 30; // a month in the magnifying lens
+
+    var geometry = this;
+    
+    var magnifyWith = function(lens) {
+        var aperture = lens.clientWidth;
+        var loc = geometry._timeplot.locate(lens);
+        geometry.setMagnifyingParams(loc.x + aperture / 2, aperture, period);
+        geometry.actMagnifying();
+        geometry._timeplot.paint();
+    }
+    
+    var canvasMouseDown = function(elmt, evt, target) {
+        geometry._canvas.startCoords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
+        geometry._canvas.pressed = true;
+    }
+    
+    var canvasMouseUp = function(elmt, evt, target) {
+        geometry._canvas.pressed = false;
+        var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
+        if (Timeplot.Math.isClose(coords,geometry._canvas.startCoords,5)) {
+            geometry._lens.style.display = "none";
+            geometry.actLinear();
+            geometry._timeplot.paint();
+        } else {
+            geometry._lens.style.cursor = "move";
+            magnifyWith(geometry._lens);
+        }
+    }
+
+    var canvasMouseMove = function(elmt, evt, target) {
+        if (geometry._canvas.pressed) {
+            var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
+            if (coords.x < 0) coords.x = 0;
+            if (coords.x > geometry._canvas.width) coords.x = geometry._canvas.width;
+            geometry._timeplot.placeDiv(geometry._lens, {
+                left: geometry._canvas.startCoords.x,
+                width: coords.x - geometry._canvas.startCoords.x,
+                bottom: 0,
+                height: geometry._canvas.height,
+                display: "block"
+            });
+        }
+    }
+
+    var lensMouseDown = function(elmt, evt, target) {
+        geometry._lens.startCoords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);;
+        geometry._lens.pressed = true; 
+    }
+    
+    var lensMouseUp = function(elmt, evt, target) {
+        geometry._lens.pressed = false;
+    }
+    
+    var lensMouseMove = function(elmt, evt, target) {
+        if (geometry._lens.pressed) {
+            var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
+            var lens = geometry._lens;
+            var left = lens.offsetLeft + coords.x - lens.startCoords.x;
+            if (left < geometry._timeplot._paddingX) left = geometry._timeplot._paddingX;
+            if (left + lens.clientWidth > geometry._canvas.width - geometry._timeplot._paddingX) left = geometry._canvas.width - lens.clientWidth + geometry._timeplot._paddingX;
+            lens.style.left = left;
+            magnifyWith(lens);
+        }
+    }
+    
+    if (!this._canvas.instrumented) {
+        SimileAjax.DOM.registerEvent(this._canvas, "mousedown", canvasMouseDown);
+        SimileAjax.DOM.registerEvent(this._canvas, "mousemove", canvasMouseMove);
+        SimileAjax.DOM.registerEvent(this._canvas, "mouseup"  , canvasMouseUp);
+        SimileAjax.DOM.registerEvent(this._canvas, "mouseup"  , lensMouseUp);
+        this._canvas.instrumented = true;
+    }
+    
+    if (!this._lens.instrumented) {
+        SimileAjax.DOM.registerEvent(this._lens, "mousedown", lensMouseDown);
+        SimileAjax.DOM.registerEvent(this._lens, "mousemove", lensMouseMove);
+        SimileAjax.DOM.registerEvent(this._lens, "mouseup"  , lensMouseUp);
+        SimileAjax.DOM.registerEvent(this._lens, "mouseup"  , canvasMouseUp);
+        this._lens.instrumented = true;
+    }
+}
+
+/**
+ * Set the Magnifying parameters. c is the location in pixels where the Magnifying
+ * center should be located in the timeplot, a is the aperture in pixel of
+ * the Magnifying and b is the time period in milliseconds that the Magnifying 
+ * should span.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.setMagnifyingParams = function(c,a,b) {
+    a = a / 2;
+    b = b / 2;
+
+    var w = this._canvas.width;
+    var d = this._period;
+
+    if (c < 0) c = 0;
+    if (c > w) c = w;
+    
+    if (c - a < 0) a = c;
+    if (c + a > w) a = w - c;
+    
+    var ct = this.fromScreen(c) - this._earliestDate.getTime();
+    if (ct - b < 0) b = ct;
+    if (ct + b > d) b = d - ct;
+
+    this._centerX = c;
+    this._centerTime = ct;
+    this._aperture = a;
+    this._aperturePeriod = b;
+    
+    this._leftScreenMargin = this._centerX - this._aperture;
+    this._rightScreenMargin = this._centerX + this._aperture;
+    this._leftTimeMargin = this._centerTime - this._aperturePeriod;
+    this._rightTimeMargin = this._centerTime + this._aperturePeriod;
+        
+    this._leftRate = (c - a) / (ct - b);
+    this._expandedRate = a / b;
+    this._rightRate = (w - c - a) / (d - ct - b);
+
+    this._expandedTimeTranslation = this._centerX - this._centerTime * this._expandedRate; 
+    this._expandedScreenTranslation = this._centerTime - this._centerX / this._expandedRate;
+    this._rightTimeTranslation = (c + a) - (ct + b) * this._rightRate;
+    this._rightScreenTranslation = (ct + b) - (c + a) / this._rightRate;
+
+    this._updateMappedValues();
+}
+
+/*
+ * Turn magnification off.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.actLinear = function() {
+    this._mode = "lin";
+    this._map = this._linMap;
+    this.reset();
+}
+
+/*
+ * Turn magnification on.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.actMagnifying = function() {
+    this._mode = "Magnifying";
+    this._map = this._MagnifyingMap;
+    this.reset();
+}
+
+/*
+ * Toggle magnification.
+ */
+Timeplot.MagnifyingTimeGeometry.prototype.toggle = function() {
+    if (this._mode == "Magnifying") {
+        this.actLinear();
+    } else {
+        this.actMagnifying();
+    }
+}
+
+/**
+ * Color
+ *
+ * @fileOverview Color
+ * @name Color
+ */
+
+/*
+ * Inspired by Plotr
+ * Copyright 2007 (c) Bas Wenneker <sabmann[a]gmail[d]com>
+ * For use under the BSD license. <http://www.solutoire.com/plotr>
+ */
+
+/**
+ * Create a Color object that can be used to manipulate colors programmatically.
+ */
+Timeplot.Color = function(color) {
+    this._fromHex(color);
+};
+
+Timeplot.Color.prototype = {
+
+    /**
+     * Sets the RGB values of this coor
+     * 
+     * @param {Number} r,g,b    Red green and blue values (between 0 and 255)
+     */
+    set: function (r,g,b,a) {
+        this.r = r;
+        this.g = g;
+        this.b = b;
+        this.a = (a) ? a : 1.0;
+        return this.check();
+    },
+
+    /**
+     * Set the color transparency
+     * 
+     * @param {float} a   Transparency value, between 0.0 (fully transparent) and 1.0 (fully opaque).
+     */
+    transparency: function(a) {
+    	this.a = a;
+    	return this.check();
+    },
+    
+    /**
+     * Lightens the color.
+     * 
+     * @param {integer} level   Level to lighten the color with.
+     */
+    lighten: function(level) {
+        var color = new Timeplot.Color();
+        return color.set(
+            this.r += parseInt(level, 10),
+            this.g += parseInt(level, 10),
+            this.b += parseInt(level, 10)
+        );
+    },
+
+    /**
+     * Darkens the color.
+     * 
+     * @param {integer} level   Level to darken the color with.
+     */
+    darken: function(level){
+        var color = new Timeplot.Color();
+        return color.set(
+            this.r -= parseInt(level, 10),
+            this.g -= parseInt(level, 10),
+            this.b -= parseInt(level, 10)
+        );
+    },
+
+    /**
+     * Checks and validates if the hex values r, g and b are
+     * between 0 and 255.
+     */
+    check: function() {
+        if (this.r > 255) { 
+        	this.r = 255;
+        } else if (this.r < 0){
+        	this.r = 0;
+        }
+        if (this.g > 255) {
+        	this.g = 255;
+        } else if (this.g < 0) {
+        	this.g = 0;
+        }
+        if (this.b > 255){
+        	this.b = 255;
+        } else if (this.b < 0){
+        	this.b = 0;
+        }
+        if (this.a > 1.0){
+            this.a = 1.0;
+        } else if (this.a < 0.0){
+            this.a = 0.0;
+        }
+        return this;
+    },
+
+    /**
+     * Returns a string representation of this color.
+     * 
+     * @param {float} alpha   (optional) Transparency value, between 0.0 (fully transparent) and 1.0 (fully opaque).
+     */
+    toString: function(alpha) {
+        var a = (alpha) ? alpha : ((this.a) ? this.a : 1.0);
+        return 'rgba(' + this.r + ',' + this.g + ',' + this.b + ',' + a + ')';
+    },
+
+    /**
+     * Returns the hexadecimal representation of this color (without the alpha channel as hex colors don't support it)
+     */
+    toHexString: function() {
+    	return "#" + this._toHex(this.r) + this._toHex(this.g) + this._toHex(this.b); 
+    },
+    
+    /*
+     * Parses and stores the hex values of the input color string.
+     * 
+     * @param {String} color    Hex or rgb() css string.
+     */
+    _fromHex: function(color) {
+        if(/^#?([\da-f]{3}|[\da-f]{6})$/i.test(color)){
+            color = color.replace(/^#/, '').replace(/^([\da-f])([\da-f])([\da-f])$/i, "$1$1$2$2$3$3");
+            this.r = parseInt(color.substr(0,2), 16);
+            this.g = parseInt(color.substr(2,2), 16);
+            this.b = parseInt(color.substr(4,2), 16);
+        } else if(/^rgb *\( *\d{0,3} *, *\d{0,3} *, *\d{0,3} *\)$/i.test(color)){
+            color = color.match(/^rgb *\( *(\d{0,3}) *, *(\d{0,3}) *, *(\d{0,3}) *\)$/i);
+            this.r = parseInt(color[1], 10);
+            this.g = parseInt(color[2], 10);
+            this.b = parseInt(color[3], 10);
+        }
+        this.a = 1.0;
+        return this.check();
+    },
+    
+    /*
+     * Returns an hexadecimal representation of a 8 bit integer 
+     */
+    _toHex: function(dec) {
+        var hex = "0123456789ABCDEF"
+        if (dec < 0) return "00";
+        if (dec > 255) return "FF";
+        var i = Math.floor(dec / 16);
+        var j = dec % 16;
+        return hex.charAt(i) + hex.charAt(j);
+    }
+
+};
+/**
+ * Math Utility functions
+ * 
+ * @fileOverview Math Utility functions
+ * @name Math
+ */
+
+Timeplot.Math = { 
+
+    /**
+     * Evaluates the range (min and max values) of the given array
+     */
+    range: function(f) {
+        var F = f.length;
+        var min = Number.MAX_VALUE;
+        var max = Number.MIN_VALUE;
+
+        for (var t = 0; t < F; t++) {
+            var value = f[t];
+            if (value < min) {
+                min = value;
+            }
+            if (value > max) {
+                max = value;
+            }    
+        }
+
+        return {
+            min: min,
+            max: max
+        }
+    },
+
+    /**
+     * Evaluates the windows average of a given array based on the
+     * given window size
+     */
+    movingAverage: function(f, size) {
+        var F = f.length;
+        var g = new Array(F);
+        for (var n = 0; n < F; n++) {
+            var value = 0;
+            for (var m = n - size; m < n + size; m++) {
+                if (m < 0) {
+                    var v = f[0];
+                } else if (m >= F) {
+                    var v = g[n-1];
+                } else {
+                    var v = f[m];
+                }
+                value += v;
+            }
+            g[n] = value / (2 * size);
+        }
+        return g;
+    },
+
+    /**
+     * Returns an array with the integral of the given array
+     */
+    integral: function(f) {
+        var F = f.length;
+
+        var g = new Array(F);
+        var sum = 0;
+
+        for (var t = 0; t < F; t++) {
+           sum += f[t];
+           g[t] = sum;  
+        }
+
+        return g;
+    },
+
+    /**
+     * Normalizes an array so that its complete integral is 1.
+     * This is useful to obtain arrays that preserve the overall
+     * integral of a convolution. 
+     */
+    normalize: function(f) {
+        var F = f.length;
+        var sum = 0.0;
+
+        for (var t = 0; t < F; t++) {
+            sum += f[t];
+        }
+
+        for (var t = 0; t < F; t++) {
+            f[t] /= sum;
+        }
+
+        return f;
+    },
+
+    /**
+     * Calculates the convolution between two arrays
+     */
+    convolution: function(f,g) {
+        var F = f.length;
+        var G = g.length;
+
+        var c = new Array(F);
+
+        for (var m = 0; m < F; m++) {
+            var r = 0;
+            var end = (m + G < F) ? m + G : F;
+            for (var n = m; n < end; n++) {
+                var a = f[n - G];
+                var b = g[n - m];
+                r += a * b;
+            }
+            c[m] = r;
+        }
+
+        return c;
+    },
+
+    // ------ Array generators ------------------------------------------------- 
+    // Functions that generate arrays based on mathematical functions
+    // Normally these are used to produce operators by convolving them with the input array
+    // The returned arrays have the property of having 
+
+    /**
+     * Generate the heavyside step function of given size
+     */
+    heavyside: function(size) {
+        var f =  new Array(size);
+        var value = 1 / size;
+        for (var t = 0; t < size; t++) {
+            f[t] = value;
+        }
+        return f;
+    },
+
+    /**
+     * Generate the gaussian function so that at the given 'size' it has value 'threshold'
+     * and make sure its integral is one.
+     */
+    gaussian: function(size, threshold) {
+        with (Math) {
+            var radius = size / 2;
+            var variance = radius * radius / log(threshold); 
+            var g = new Array(size);
+            for (var t = 0; t < size; t++) {
+                var l = t - radius;
+                g[t] = exp(-variance * l * l);
+            }
+        }
+
+        return this.normalize(g);
+    },
+
+    // ---- Utility Methods --------------------------------------------------
+
+    /**
+     * Return x with n significant figures 
+     */
+    round: function(x,n) {
+        with (Math) {
+            if (abs(x) > 1) {
+                var l = floor(log(x)/log(10));
+                var d = round(exp((l-n+1)*log(10)));
+                var y = round(round(x / d) * d);
+                return y;
+            } else {
+                log("FIXME(SM): still to implement for 0 < abs(x) < 1");
+                return x;
+            }
+        }
+    },
+    
+    /**
+     * Return the hyperbolic tangent of x
+     */
+    tanh: function(x) {
+        if (x > 5) {
+            return 1;
+        } else if (x < 5) {
+            return -1;
+        } else {
+            var expx2 = Math.exp(2 * x);
+            return (expx2 - 1) / (expx2 + 1);
+        }
+    },
+    
+    /** 
+     * Returns true if |a.x - b.x| < value && | a.y - b.y | < value
+     */
+    isClose: function(a,b,value) {
+        return (a && b && Math.abs(a.x - b.x) < value && Math.abs(a.y - b.y) < value);
+    }
+
+}
+/**
+ * Processing Data Source
+ * 
+ * @fileOverview Processing Data Source and Operators
+ * @name Processor
+ */
+
+/* -----------------------------------------------------------------------------
+ * Operators
+ * 
+ * These are functions that can be used directly as Timeplot.Processor operators
+ * ----------------------------------------------------------------------------- */
+
+Timeplot.Operator = { 
+
+    /**
+     * This is the operator used when you want to draw the cumulative sum
+     * of a time series and not, for example, their daily values.
+     */
+    sum: function(data, params) {
+        return Timeplot.Math.integral(data.values);
+    },
+
+    /**
+     * This is the operator that is used to 'smooth' a given time series
+     * by taking the average value of a moving window centered around
+     * each value. The size of the moving window is influenced by the 'size'
+     * parameters in the params map.
+     */
+    average: function(data, params) {
+        var size = ("size" in params) ? params.size : 30;
+        var result = Timeplot.Math.movingAverage(data.values, size);
+        return result;
+    }
+}
+
+/*==================================================
+ *  Processing Data Source
+ *==================================================*/
+
+/**
+ * A Processor is a special DataSource that can apply an Operator
+ * to the DataSource values and thus return a different one.
+ * 
+ * @constructor
+ */
+Timeplot.Processor = function(dataSource, operator, params) {
+    this._dataSource = dataSource;
+    this._operator = operator;
+    this._params = params;
+
+    this._data = {
+        times: new Array(),
+        values: new Array()
+    };
+
+    this._range = {
+        earliestDate: null,
+        latestDate: null,
+        min: 0,
+        max: 0
+    };
+
+    var processor = this;
+    this._processingListener = {
+        onAddMany: function() { processor._process(); },
+        onClear:   function() { processor._clear(); }
+    }
+    this.addListener(this._processingListener);
+};
+
+Timeplot.Processor.prototype = {
+
+    _clear: function() {
+        this.removeListener(this._processingListener);
+        this._dataSource._clear();
+    },
+
+    _process: function() {
+        // this method requires the dataSource._process() method to be
+        // called first as to setup the data and range used below
+        // this should be guaranteed by the order of the listener registration  
+
+        var data = this._dataSource.getData();
+        var range = this._dataSource.getRange();
+
+        var newValues = this._operator(data, this._params);
+        var newValueRange = Timeplot.Math.range(newValues);
+
+        this._data = {
+            times: data.times,
+            values: newValues
+        };
+
+        this._range = {
+            earliestDate: range.earliestDate,
+            latestDate: range.latestDate,
+            min: newValueRange.min,
+            max: newValueRange.max
+        };
+    },
+
+    getRange: function() {
+        return this._range;
+    },
+
+    getData: function() {
+        return this._data;
+    },
+    
+    getValue: Timeplot.DataSource.prototype.getValue,
+
+    getClosestValidTime: Timeplot.DataSource.prototype.getClosestValidTime,
+
+    addListener: function(listener) {
+        this._dataSource.addListener(listener);
+    },
+
+    removeListener: function(listener) {
+        this._dataSource.removeListener(listener);
+    }
+}