Mercurial > hg > STI-GWT
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); + } +}