comparison 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
comparison
equal deleted inserted replaced
2:2897af43ccc6 3:cf06b77a8bbd
1 /*==================================================
2 * Simile Ajax API
3 *==================================================
4 */
5
6 if (typeof SimileAjax == "undefined") {
7 var SimileAjax = {
8 loaded: false,
9 loadingScriptsCount: 0,
10 error: null,
11 params: { bundle:"true" }
12 };
13
14 SimileAjax.Platform = new Object();
15 /*
16 HACK: We need these 2 things here because we cannot simply append
17 a <script> element containing code that accesses SimileAjax.Platform
18 to initialize it because IE executes that <script> code first
19 before it loads ajax.js and platform.js.
20 */
21
22 var getHead = function(doc) {
23 return doc.getElementsByTagName("head")[0];
24 };
25
26 SimileAjax.findScript = function(doc, substring) {
27 var heads = doc.documentElement.getElementsByTagName("head");
28 for (var h = 0; h < heads.length; h++) {
29 var node = heads[h].firstChild;
30 while (node != null) {
31 if (node.nodeType == 1 && node.tagName.toLowerCase() == "script") {
32 var url = node.src;
33 var i = url.indexOf(substring);
34 if (i >= 0) {
35 return url;
36 }
37 }
38 node = node.nextSibling;
39 }
40 }
41 return null;
42 };
43 SimileAjax.includeJavascriptFile = function(doc, url, onerror, charset) {
44 return;
45 onerror = onerror || "";
46 if (doc.body == null) {
47 try {
48 var q = "'" + onerror.replace( /'/g, '&apos' ) + "'"; // "
49 doc.write("<script src='" + url + "' onerror="+ q +
50 (charset ? " charset='"+ charset +"'" : "") +
51 " type='text/javascript'>"+ onerror + "</script>");
52 return;
53 } catch (e) {
54 // fall through
55 }
56 }
57
58 var script = doc.createElement("script");
59 if (onerror) {
60 try { script.innerHTML = onerror; } catch(e) {}
61 script.setAttribute("onerror", onerror);
62 }
63 if (charset) {
64 script.setAttribute("charset", charset);
65 }
66 script.type = "text/javascript";
67 script.language = "JavaScript";
68 script.src = url;
69 return getHead(doc).appendChild(script);
70 };
71 SimileAjax.includeJavascriptFiles = function(doc, urlPrefix, filenames) {
72 for (var i = 0; i < filenames.length; i++) {
73 SimileAjax.includeJavascriptFile(doc, urlPrefix + filenames[i]);
74 }
75 SimileAjax.loadingScriptsCount += filenames.length;
76 SimileAjax.includeJavascriptFile(doc, SimileAjax.urlPrefix + "scripts/signal.js?" + filenames.length);
77 };
78 SimileAjax.includeCssFile = function(doc, url) {
79 if (doc.body == null) {
80 try {
81 doc.write("<link rel='stylesheet' href='" + url + "' type='text/css'/>");
82 return;
83 } catch (e) {
84 // fall through
85 }
86 }
87
88 var link = doc.createElement("link");
89 link.setAttribute("rel", "stylesheet");
90 link.setAttribute("type", "text/css");
91 link.setAttribute("href", url);
92 getHead(doc).appendChild(link);
93 };
94 SimileAjax.includeCssFiles = function(doc, urlPrefix, filenames) {
95 for (var i = 0; i < filenames.length; i++) {
96 SimileAjax.includeCssFile(doc, urlPrefix + filenames[i]);
97 }
98 };
99
100 /**
101 * Append into urls each string in suffixes after prefixing it with urlPrefix.
102 * @param {Array} urls
103 * @param {String} urlPrefix
104 * @param {Array} suffixes
105 */
106 SimileAjax.prefixURLs = function(urls, urlPrefix, suffixes) {
107 for (var i = 0; i < suffixes.length; i++) {
108 urls.push(urlPrefix + suffixes[i]);
109 }
110 };
111
112 /**
113 * Parse out the query parameters from a URL
114 * @param {String} url the url to parse, or location.href if undefined
115 * @param {Object} to optional object to extend with the parameters
116 * @param {Object} types optional object mapping keys to value types
117 * (String, Number, Boolean or Array, String by default)
118 * @return a key/value Object whose keys are the query parameter names
119 * @type Object
120 */
121 SimileAjax.parseURLParameters = function(url, to, types) {
122 to = to || {};
123 types = types || {};
124
125 if (typeof url == "undefined") {
126 url = location.href;
127 }
128 var q = url.indexOf("?");
129 if (q < 0) {
130 return to;
131 }
132 url = (url+"#").slice(q+1, url.indexOf("#")); // toss the URL fragment
133
134 var params = url.split("&"), param, parsed = {};
135 var decode = window.decodeURIComponent || unescape;
136 for (var i = 0; param = params[i]; i++) {
137 var eq = param.indexOf("=");
138 var name = decode(param.slice(0,eq));
139 var old = parsed[name];
140 if (typeof old == "undefined") {
141 old = [];
142 } else if (!(old instanceof Array)) {
143 old = [old];
144 }
145 parsed[name] = old.concat(decode(param.slice(eq+1)));
146 }
147 for (var i in parsed) {
148 if (!parsed.hasOwnProperty(i)) continue;
149 var type = types[i] || String;
150 var data = parsed[i];
151 if (!(data instanceof Array)) {
152 data = [data];
153 }
154 if (type === Boolean && data[0] == "false") {
155 to[i] = false; // because Boolean("false") === true
156 } else {
157 to[i] = type.apply(this, data);
158 }
159 }
160 return to;
161 };
162
163 (function() {
164 var javascriptFiles = [
165 "platform.js",
166 "debug.js",
167 "xmlhttp.js",
168 "json.js",
169 "dom.js",
170 "graphics.js",
171 "date-time.js",
172 "string.js",
173 "html.js",
174 "data-structure.js",
175 "units.js",
176
177 "ajax.js",
178 "history.js",
179 "window-manager.js"
180 ];
181 var cssFiles = [
182 "timeplot-single.css"
183 ];
184 if (!("jQuery" in window) && !("$" in window)) {
185 javascriptFiles.unshift("jquery-1.3.2.min.js");
186 }
187
188 if (typeof SimileAjax_urlPrefix == "string") {
189 SimileAjax.urlPrefix = SimileAjax_urlPrefix;
190 } else {
191 var url = SimileAjax.findScript(document, "timeplot-complete.js");
192 if (url == null) {
193 SimileAjax.error = new Error("Failed to derive URL prefix for Simile Ajax API code files");
194 return;
195 }
196
197 SimileAjax.urlPrefix = url.substr(0, url.indexOf("timeplot-complete.js"));
198 }
199
200 SimileAjax.parseURLParameters(url, SimileAjax.params, {bundle:Boolean});
201 if (SimileAjax.params.bundle) {
202 SimileAjax.includeJavascriptFiles(document, SimileAjax.urlPrefix, [ "simile-ajax-bundle.js" ]);
203 } else {
204 SimileAjax.includeJavascriptFiles(document, SimileAjax.urlPrefix + "scripts/", javascriptFiles);
205 }
206 SimileAjax.includeCssFiles(document, SimileAjax.urlPrefix + "styles/", cssFiles);
207
208 /* We only use it once this way. */
209 SimileAjax.includeCssFile = function(doc, url) { return ; }
210
211 SimileAjax.loaded = true;
212 })();
213 }
214
215 /*==================================================
216 * Platform Utility Functions and Constants
217 *==================================================
218 */
219
220 /* This must be called after our jQuery has been loaded
221 but before control returns to user-code.
222 */
223
224
225 /*==================================================
226 * REMEMBER to update the Version!
227 *==================================================
228 */
229 SimileAjax.version = '2.2.1';
230
231 SimileAjax.jQuery = jQuery.noConflict(true);
232 if (typeof window["$"] == "undefined") {
233 window.$ = SimileAjax.jQuery;
234 }
235
236 SimileAjax.Platform.os = {
237 isMac: false,
238 isWin: false,
239 isWin32: false,
240 isUnix: false
241 };
242 SimileAjax.Platform.browser = {
243 isIE: false,
244 isNetscape: false,
245 isMozilla: false,
246 isFirefox: false,
247 isOpera: false,
248 isSafari: false,
249
250 majorVersion: 0,
251 minorVersion: 0
252 };
253
254 (function() {
255 var an = navigator.appName.toLowerCase();
256 var ua = navigator.userAgent.toLowerCase();
257
258 /*
259 * Operating system
260 */
261 SimileAjax.Platform.os.isMac = (ua.indexOf('mac') != -1);
262 SimileAjax.Platform.os.isWin = (ua.indexOf('win') != -1);
263 SimileAjax.Platform.os.isWin32 = SimileAjax.Platform.isWin && (
264 ua.indexOf('95') != -1 ||
265 ua.indexOf('98') != -1 ||
266 ua.indexOf('nt') != -1 ||
267 ua.indexOf('win32') != -1 ||
268 ua.indexOf('32bit') != -1
269 );
270 SimileAjax.Platform.os.isUnix = (ua.indexOf('x11') != -1);
271
272 /*
273 * Browser
274 */
275 SimileAjax.Platform.browser.isIE = (an.indexOf("microsoft") != -1);
276 SimileAjax.Platform.browser.isNetscape = (an.indexOf("netscape") != -1);
277 SimileAjax.Platform.browser.isMozilla = (ua.indexOf("mozilla") != -1);
278 SimileAjax.Platform.browser.isFirefox = (ua.indexOf("firefox") != -1);
279 SimileAjax.Platform.browser.isOpera = (an.indexOf("opera") != -1);
280 SimileAjax.Platform.browser.isSafari = (an.indexOf("safari") != -1);
281
282 var parseVersionString = function(s) {
283 var a = s.split(".");
284 SimileAjax.Platform.browser.majorVersion = parseInt(a[0]);
285 SimileAjax.Platform.browser.minorVersion = parseInt(a[1]);
286 };
287 var indexOf = function(s, sub, start) {
288 var i = s.indexOf(sub, start);
289 return i >= 0 ? i : s.length;
290 };
291
292 if (SimileAjax.Platform.browser.isMozilla) {
293 var offset = ua.indexOf("mozilla/");
294 if (offset >= 0) {
295 parseVersionString(ua.substring(offset + 8, indexOf(ua, " ", offset)));
296 }
297 }
298 if (SimileAjax.Platform.browser.isIE) {
299 var offset = ua.indexOf("msie ");
300 if (offset >= 0) {
301 parseVersionString(ua.substring(offset + 5, indexOf(ua, ";", offset)));
302 }
303 }
304 if (SimileAjax.Platform.browser.isNetscape) {
305 var offset = ua.indexOf("rv:");
306 if (offset >= 0) {
307 parseVersionString(ua.substring(offset + 3, indexOf(ua, ")", offset)));
308 }
309 }
310 if (SimileAjax.Platform.browser.isFirefox) {
311 var offset = ua.indexOf("firefox/");
312 if (offset >= 0) {
313 parseVersionString(ua.substring(offset + 8, indexOf(ua, " ", offset)));
314 }
315 }
316
317 if (!("localeCompare" in String.prototype)) {
318 String.prototype.localeCompare = function (s) {
319 if (this < s) return -1;
320 else if (this > s) return 1;
321 else return 0;
322 };
323 }
324 })();
325
326 SimileAjax.Platform.getDefaultLocale = function() {
327 return SimileAjax.Platform.clientLocale;
328 };
329 /*==================================================
330 * Debug Utility Functions
331 *==================================================
332 */
333
334 SimileAjax.Debug = {
335 silent: false
336 };
337
338 SimileAjax.Debug.log = function(msg) {
339 var f;
340 if ("console" in window && "log" in window.console) { // FireBug installed
341 f = function(msg2) {
342 console.log(msg2);
343 }
344 } else {
345 f = function(msg2) {
346 if (!SimileAjax.Debug.silent) {
347 alert(msg2);
348 }
349 }
350 }
351 SimileAjax.Debug.log = f;
352 f(msg);
353 };
354
355 SimileAjax.Debug.warn = function(msg) {
356 var f;
357 if ("console" in window && "warn" in window.console) { // FireBug installed
358 f = function(msg2) {
359 console.warn(msg2);
360 }
361 } else {
362 f = function(msg2) {
363 if (!SimileAjax.Debug.silent) {
364 alert(msg2);
365 }
366 }
367 }
368 SimileAjax.Debug.warn = f;
369 f(msg);
370 };
371
372 SimileAjax.Debug.exception = function(e, msg) {
373 var f, params = SimileAjax.parseURLParameters();
374 if (params.errors == "throw" || SimileAjax.params.errors == "throw") {
375 f = function(e2, msg2) {
376 throw(e2); // do not hide from browser's native debugging features
377 };
378 } else if ("console" in window && "error" in window.console) { // FireBug installed
379 f = function(e2, msg2) {
380 if (msg2 != null) {
381 console.error(msg2 + " %o", e2);
382 } else {
383 console.error(e2);
384 }
385 throw(e2); // do not hide from browser's native debugging features
386 };
387 } else {
388 f = function(e2, msg2) {
389 if (!SimileAjax.Debug.silent) {
390 alert("Caught exception: " + msg2 + "\n\nDetails: " + ("description" in e2 ? e2.description : e2));
391 }
392 throw(e2); // do not hide from browser's native debugging features
393 };
394 }
395 SimileAjax.Debug.exception = f;
396 f(e, msg);
397 };
398
399 SimileAjax.Debug.objectToString = function(o) {
400 return SimileAjax.Debug._objectToString(o, "");
401 };
402
403 SimileAjax.Debug._objectToString = function(o, indent) {
404 var indent2 = indent + " ";
405 if (typeof o == "object") {
406 var s = "{";
407 for (n in o) {
408 s += indent2 + n + ": " + SimileAjax.Debug._objectToString(o[n], indent2) + "\n";
409 }
410 s += indent + "}";
411 return s;
412 } else if (typeof o == "array") {
413 var s = "[";
414 for (var n = 0; n < o.length; n++) {
415 s += SimileAjax.Debug._objectToString(o[n], indent2) + "\n";
416 }
417 s += indent + "]";
418 return s;
419 } else {
420 return o;
421 }
422 };
423 /**
424 * @fileOverview XmlHttp utility functions
425 * @name SimileAjax.XmlHttp
426 */
427
428 SimileAjax.XmlHttp = new Object();
429
430 /**
431 * Callback for XMLHttp onRequestStateChange.
432 */
433 SimileAjax.XmlHttp._onReadyStateChange = function(xmlhttp, fError, fDone) {
434 switch (xmlhttp.readyState) {
435 // 1: Request not yet made
436 // 2: Contact established with server but nothing downloaded yet
437 // 3: Called multiple while downloading in progress
438
439 // Download complete
440 case 4:
441 try {
442 if (xmlhttp.status == 0 // file:// urls, works on Firefox
443 || xmlhttp.status == 200 // http:// urls
444 ) {
445 if (fDone) {
446 fDone(xmlhttp);
447 }
448 } else {
449 if (fError) {
450 fError(
451 xmlhttp.statusText,
452 xmlhttp.status,
453 xmlhttp
454 );
455 }
456 }
457 } catch (e) {
458 SimileAjax.Debug.exception("XmlHttp: Error handling onReadyStateChange", e);
459 }
460 break;
461 }
462 };
463
464 /**
465 * Creates an XMLHttpRequest object. On the first run, this
466 * function creates a platform-specific function for
467 * instantiating an XMLHttpRequest object and then replaces
468 * itself with that function.
469 */
470 SimileAjax.XmlHttp._createRequest = function() {
471 if (SimileAjax.Platform.browser.isIE) {
472 var programIDs = [
473 "Msxml2.XMLHTTP",
474 "Microsoft.XMLHTTP",
475 "Msxml2.XMLHTTP.4.0"
476 ];
477 for (var i = 0; i < programIDs.length; i++) {
478 try {
479 var programID = programIDs[i];
480 var f = function() {
481 return new ActiveXObject(programID);
482 };
483 var o = f();
484
485 // We are replacing the SimileAjax._createXmlHttpRequest
486 // function with this inner function as we've
487 // found out that it works. This is so that we
488 // don't have to do all the testing over again
489 // on subsequent calls.
490 SimileAjax.XmlHttp._createRequest = f;
491
492 return o;
493 } catch (e) {
494 // silent
495 }
496 }
497 // fall through to try new XMLHttpRequest();
498 }
499
500 try {
501 var f = function() {
502 return new XMLHttpRequest();
503 };
504 var o = f();
505
506 // We are replacing the SimileAjax._createXmlHttpRequest
507 // function with this inner function as we've
508 // found out that it works. This is so that we
509 // don't have to do all the testing over again
510 // on subsequent calls.
511 SimileAjax.XmlHttp._createRequest = f;
512
513 return o;
514 } catch (e) {
515 throw new Error("Failed to create an XMLHttpRequest object");
516 }
517 };
518
519 /**
520 * Performs an asynchronous HTTP GET.
521 *
522 * @param {Function} fError a function of the form
523 function(statusText, statusCode, xmlhttp)
524 * @param {Function} fDone a function of the form function(xmlhttp)
525 */
526 SimileAjax.XmlHttp.get = function(url, fError, fDone) {
527 var xmlhttp = SimileAjax.XmlHttp._createRequest();
528
529 xmlhttp.open("GET", url, true);
530 xmlhttp.onreadystatechange = function() {
531 SimileAjax.XmlHttp._onReadyStateChange(xmlhttp, fError, fDone);
532 };
533 xmlhttp.send(null);
534 };
535
536 /**
537 * Performs an asynchronous HTTP POST.
538 *
539 * @param {Function} fError a function of the form
540 function(statusText, statusCode, xmlhttp)
541 * @param {Function} fDone a function of the form function(xmlhttp)
542 */
543 SimileAjax.XmlHttp.post = function(url, body, fError, fDone) {
544 var xmlhttp = SimileAjax.XmlHttp._createRequest();
545
546 xmlhttp.open("POST", url, true);
547 xmlhttp.onreadystatechange = function() {
548 SimileAjax.XmlHttp._onReadyStateChange(xmlhttp, fError, fDone);
549 };
550 xmlhttp.send(body);
551 };
552
553 SimileAjax.XmlHttp._forceXML = function(xmlhttp) {
554 try {
555 xmlhttp.overrideMimeType("text/xml");
556 } catch (e) {
557 xmlhttp.setrequestheader("Content-Type", "text/xml");
558 }
559 };
560 /*
561 * Copied directly from http://www.json.org/json.js.
562 */
563
564 /*
565 json.js
566 2006-04-28
567
568 This file adds these methods to JavaScript:
569
570 object.toJSONString()
571
572 This method produces a JSON text from an object. The
573 object must not contain any cyclical references.
574
575 array.toJSONString()
576
577 This method produces a JSON text from an array. The
578 array must not contain any cyclical references.
579
580 string.parseJSON()
581
582 This method parses a JSON text to produce an object or
583 array. It will return false if there is an error.
584 */
585
586 SimileAjax.JSON = new Object();
587
588 (function () {
589 var m = {
590 '\b': '\\b',
591 '\t': '\\t',
592 '\n': '\\n',
593 '\f': '\\f',
594 '\r': '\\r',
595 '"' : '\\"',
596 '\\': '\\\\'
597 };
598 var s = {
599 array: function (x) {
600 var a = ['['], b, f, i, l = x.length, v;
601 for (i = 0; i < l; i += 1) {
602 v = x[i];
603 f = s[typeof v];
604 if (f) {
605 v = f(v);
606 if (typeof v == 'string') {
607 if (b) {
608 a[a.length] = ',';
609 }
610 a[a.length] = v;
611 b = true;
612 }
613 }
614 }
615 a[a.length] = ']';
616 return a.join('');
617 },
618 'boolean': function (x) {
619 return String(x);
620 },
621 'null': function (x) {
622 return "null";
623 },
624 number: function (x) {
625 return isFinite(x) ? String(x) : 'null';
626 },
627 object: function (x) {
628 if (x) {
629 if (x instanceof Array) {
630 return s.array(x);
631 }
632 var a = ['{'], b, f, i, v;
633 for (i in x) {
634 v = x[i];
635 f = s[typeof v];
636 if (f) {
637 v = f(v);
638 if (typeof v == 'string') {
639 if (b) {
640 a[a.length] = ',';
641 }
642 a.push(s.string(i), ':', v);
643 b = true;
644 }
645 }
646 }
647 a[a.length] = '}';
648 return a.join('');
649 }
650 return 'null';
651 },
652 string: function (x) {
653 if (/["\\\x00-\x1f]/.test(x)) {
654 x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
655 var c = m[b];
656 if (c) {
657 return c;
658 }
659 c = b.charCodeAt();
660 return '\\u00' +
661 Math.floor(c / 16).toString(16) +
662 (c % 16).toString(16);
663 });
664 }
665 return '"' + x + '"';
666 }
667 };
668
669 SimileAjax.JSON.toJSONString = function(o) {
670 if (o instanceof Object) {
671 return s.object(o);
672 } else if (o instanceof Array) {
673 return s.array(o);
674 } else {
675 return o.toString();
676 }
677 };
678
679 SimileAjax.JSON.parseJSON = function () {
680 try {
681 return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
682 this.replace(/"(\\.|[^"\\])*"/g, ''))) &&
683 eval('(' + this + ')');
684 } catch (e) {
685 return false;
686 }
687 };
688 })();
689 /*==================================================
690 * DOM Utility Functions
691 *==================================================
692 */
693
694 SimileAjax.DOM = new Object();
695
696 SimileAjax.DOM.registerEventWithObject = function(elmt, eventName, obj, handlerName) {
697 SimileAjax.DOM.registerEvent(elmt, eventName, function(elmt2, evt, target) {
698 return obj[handlerName].call(obj, elmt2, evt, target);
699 });
700 };
701
702 SimileAjax.DOM.registerEvent = function(elmt, eventName, handler) {
703 var handler2 = function(evt) {
704 evt = (evt) ? evt : ((event) ? event : null);
705 if (evt) {
706 var target = (evt.target) ?
707 evt.target : ((evt.srcElement) ? evt.srcElement : null);
708 if (target) {
709 target = (target.nodeType == 1 || target.nodeType == 9) ?
710 target : target.parentNode;
711 }
712
713 return handler(elmt, evt, target);
714 }
715 return true;
716 }
717
718 if (SimileAjax.Platform.browser.isIE) {
719 elmt.attachEvent("on" + eventName, handler2);
720 } else {
721 elmt.addEventListener(eventName, handler2, false);
722 }
723 };
724
725 SimileAjax.DOM.getPageCoordinates = function(elmt) {
726 var left = 0;
727 var top = 0;
728
729 if (elmt.nodeType != 1) {
730 elmt = elmt.parentNode;
731 }
732
733 var elmt2 = elmt;
734 while (elmt2 != null) {
735 left += elmt2.offsetLeft;
736 top += elmt2.offsetTop;
737 elmt2 = elmt2.offsetParent;
738 }
739
740 var body = document.body;
741 while (elmt != null && elmt != body) {
742 if ("scrollLeft" in elmt) {
743 left -= elmt.scrollLeft;
744 top -= elmt.scrollTop;
745 }
746 elmt = elmt.parentNode;
747 }
748
749 return { left: left, top: top };
750 };
751
752 SimileAjax.DOM.getSize = function(elmt) {
753 var w = this.getStyle(elmt,"width");
754 var h = this.getStyle(elmt,"height");
755 if (w.indexOf("px") > -1) w = w.replace("px","");
756 if (h.indexOf("px") > -1) h = h.replace("px","");
757 return {
758 w: w,
759 h: h
760 }
761 }
762
763 SimileAjax.DOM.getStyle = function(elmt, styleProp) {
764 if (elmt.currentStyle) { // IE
765 var style = elmt.currentStyle[styleProp];
766 } else if (window.getComputedStyle) { // standard DOM
767 var style = document.defaultView.getComputedStyle(elmt, null).getPropertyValue(styleProp);
768 } else {
769 var style = "";
770 }
771 return style;
772 }
773
774 SimileAjax.DOM.getEventRelativeCoordinates = function(evt, elmt) {
775 if (SimileAjax.Platform.browser.isIE) {
776 if (evt.type == "mousewheel") {
777 var coords = SimileAjax.DOM.getPageCoordinates(elmt);
778 return {
779 x: evt.clientX - coords.left,
780 y: evt.clientY - coords.top
781 };
782 } else {
783 return {
784 x: evt.offsetX,
785 y: evt.offsetY
786 };
787 }
788 } else {
789 var coords = SimileAjax.DOM.getPageCoordinates(elmt);
790
791 if ((evt.type == "DOMMouseScroll") &&
792 SimileAjax.Platform.browser.isFirefox &&
793 (SimileAjax.Platform.browser.majorVersion == 2)) {
794 // Due to: https://bugzilla.mozilla.org/show_bug.cgi?id=352179
795
796 return {
797 x: evt.screenX - coords.left,
798 y: evt.screenY - coords.top
799 };
800 } else {
801 return {
802 x: evt.pageX - coords.left,
803 y: evt.pageY - coords.top
804 };
805 }
806 }
807 };
808
809 SimileAjax.DOM.getEventPageCoordinates = function(evt) {
810 if (SimileAjax.Platform.browser.isIE) {
811 return {
812 x: evt.clientX + document.body.scrollLeft,
813 y: evt.clientY + document.body.scrollTop
814 };
815 } else {
816 return {
817 x: evt.pageX,
818 y: evt.pageY
819 };
820 }
821 };
822
823 SimileAjax.DOM.hittest = function(x, y, except) {
824 return SimileAjax.DOM._hittest(document.body, x, y, except);
825 };
826
827 SimileAjax.DOM._hittest = function(elmt, x, y, except) {
828 var childNodes = elmt.childNodes;
829 outer: for (var i = 0; i < childNodes.length; i++) {
830 var childNode = childNodes[i];
831 for (var j = 0; j < except.length; j++) {
832 if (childNode == except[j]) {
833 continue outer;
834 }
835 }
836
837 if (childNode.offsetWidth == 0 && childNode.offsetHeight == 0) {
838 /*
839 * Sometimes SPAN elements have zero width and height but
840 * they have children like DIVs that cover non-zero areas.
841 */
842 var hitNode = SimileAjax.DOM._hittest(childNode, x, y, except);
843 if (hitNode != childNode) {
844 return hitNode;
845 }
846 } else {
847 var top = 0;
848 var left = 0;
849
850 var node = childNode;
851 while (node) {
852 top += node.offsetTop;
853 left += node.offsetLeft;
854 node = node.offsetParent;
855 }
856
857 if (left <= x && top <= y && (x - left) < childNode.offsetWidth && (y - top) < childNode.offsetHeight) {
858 return SimileAjax.DOM._hittest(childNode, x, y, except);
859 } else if (childNode.nodeType == 1 && childNode.tagName == "TR") {
860 /*
861 * Table row might have cells that span several rows.
862 */
863 var childNode2 = SimileAjax.DOM._hittest(childNode, x, y, except);
864 if (childNode2 != childNode) {
865 return childNode2;
866 }
867 }
868 }
869 }
870 return elmt;
871 };
872
873 SimileAjax.DOM.cancelEvent = function(evt) {
874 evt.returnValue = false;
875 evt.cancelBubble = true;
876 if ("preventDefault" in evt) {
877 evt.preventDefault();
878 }
879 };
880
881 SimileAjax.DOM.appendClassName = function(elmt, className) {
882 var classes = elmt.className.split(" ");
883 for (var i = 0; i < classes.length; i++) {
884 if (classes[i] == className) {
885 return;
886 }
887 }
888 classes.push(className);
889 elmt.className = classes.join(" ");
890 };
891
892 SimileAjax.DOM.createInputElement = function(type) {
893 var div = document.createElement("div");
894 div.innerHTML = "<input type='" + type + "' />";
895
896 return div.firstChild;
897 };
898
899 SimileAjax.DOM.createDOMFromTemplate = function(template) {
900 var result = {};
901 result.elmt = SimileAjax.DOM._createDOMFromTemplate(template, result, null);
902
903 return result;
904 };
905
906 SimileAjax.DOM._createDOMFromTemplate = function(templateNode, result, parentElmt) {
907 if (templateNode == null) {
908 /*
909 var node = doc.createTextNode("--null--");
910 if (parentElmt != null) {
911 parentElmt.appendChild(node);
912 }
913 return node;
914 */
915 return null;
916 } else if (typeof templateNode != "object") {
917 var node = document.createTextNode(templateNode);
918 if (parentElmt != null) {
919 parentElmt.appendChild(node);
920 }
921 return node;
922 } else {
923 var elmt = null;
924 if ("tag" in templateNode) {
925 var tag = templateNode.tag;
926 if (parentElmt != null) {
927 if (tag == "tr") {
928 elmt = parentElmt.insertRow(parentElmt.rows.length);
929 } else if (tag == "td") {
930 elmt = parentElmt.insertCell(parentElmt.cells.length);
931 }
932 }
933 if (elmt == null) {
934 elmt = tag == "input" ?
935 SimileAjax.DOM.createInputElement(templateNode.type) :
936 document.createElement(tag);
937
938 if (parentElmt != null) {
939 parentElmt.appendChild(elmt);
940 }
941 }
942 } else {
943 elmt = templateNode.elmt;
944 if (parentElmt != null) {
945 parentElmt.appendChild(elmt);
946 }
947 }
948
949 for (var attribute in templateNode) {
950 var value = templateNode[attribute];
951
952 if (attribute == "field") {
953 result[value] = elmt;
954
955 } else if (attribute == "className") {
956 elmt.className = value;
957 } else if (attribute == "id") {
958 elmt.id = value;
959 } else if (attribute == "title") {
960 elmt.title = value;
961 } else if (attribute == "type" && elmt.tagName == "input") {
962 // do nothing
963 } else if (attribute == "style") {
964 for (n in value) {
965 var v = value[n];
966 if (n == "float") {
967 n = SimileAjax.Platform.browser.isIE ? "styleFloat" : "cssFloat";
968 }
969 elmt.style[n] = v;
970 }
971 } else if (attribute == "children") {
972 for (var i = 0; i < value.length; i++) {
973 SimileAjax.DOM._createDOMFromTemplate(value[i], result, elmt);
974 }
975 } else if (attribute != "tag" && attribute != "elmt") {
976 elmt.setAttribute(attribute, value);
977 }
978 }
979 return elmt;
980 }
981 }
982
983 SimileAjax.DOM._cachedParent = null;
984 SimileAjax.DOM.createElementFromString = function(s) {
985 if (SimileAjax.DOM._cachedParent == null) {
986 SimileAjax.DOM._cachedParent = document.createElement("div");
987 }
988 SimileAjax.DOM._cachedParent.innerHTML = s;
989 return SimileAjax.DOM._cachedParent.firstChild;
990 };
991
992 SimileAjax.DOM.createDOMFromString = function(root, s, fieldElmts) {
993 var elmt = typeof root == "string" ? document.createElement(root) : root;
994 elmt.innerHTML = s;
995
996 var dom = { elmt: elmt };
997 SimileAjax.DOM._processDOMChildrenConstructedFromString(dom, elmt, fieldElmts != null ? fieldElmts : {} );
998
999 return dom;
1000 };
1001
1002 SimileAjax.DOM._processDOMConstructedFromString = function(dom, elmt, fieldElmts) {
1003 var id = elmt.id;
1004 if (id != null && id.length > 0) {
1005 elmt.removeAttribute("id");
1006 if (id in fieldElmts) {
1007 var parentElmt = elmt.parentNode;
1008 parentElmt.insertBefore(fieldElmts[id], elmt);
1009 parentElmt.removeChild(elmt);
1010
1011 dom[id] = fieldElmts[id];
1012 return;
1013 } else {
1014 dom[id] = elmt;
1015 }
1016 }
1017
1018 if (elmt.hasChildNodes()) {
1019 SimileAjax.DOM._processDOMChildrenConstructedFromString(dom, elmt, fieldElmts);
1020 }
1021 };
1022
1023 SimileAjax.DOM._processDOMChildrenConstructedFromString = function(dom, elmt, fieldElmts) {
1024 var node = elmt.firstChild;
1025 while (node != null) {
1026 var node2 = node.nextSibling;
1027 if (node.nodeType == 1) {
1028 SimileAjax.DOM._processDOMConstructedFromString(dom, node, fieldElmts);
1029 }
1030 node = node2;
1031 }
1032 };
1033 /**
1034 * @fileOverview Graphics utility functions and constants
1035 * @name SimileAjax.Graphics
1036 */
1037
1038 SimileAjax.Graphics = new Object();
1039
1040 /**
1041 * A boolean value indicating whether PNG translucency is supported on the
1042 * user's browser or not.
1043 *
1044 * @type Boolean
1045 */
1046 SimileAjax.Graphics.pngIsTranslucent = (!SimileAjax.Platform.browser.isIE) || (SimileAjax.Platform.browser.majorVersion > 6);
1047 if (!SimileAjax.Graphics.pngIsTranslucent) {
1048 SimileAjax.includeCssFile(document, SimileAjax.urlPrefix + "styles/graphics-ie6.css");
1049 }
1050
1051 /*==================================================
1052 * Opacity, translucency
1053 *==================================================
1054 */
1055 SimileAjax.Graphics._createTranslucentImage1 = function(url, verticalAlign) {
1056 var elmt = document.createElement("img");
1057 elmt.setAttribute("src", url);
1058 if (verticalAlign != null) {
1059 elmt.style.verticalAlign = verticalAlign;
1060 }
1061 return elmt;
1062 };
1063 SimileAjax.Graphics._createTranslucentImage2 = function(url, verticalAlign) {
1064 var elmt = document.createElement("img");
1065 elmt.style.width = "1px"; // just so that IE will calculate the size property
1066 elmt.style.height = "1px";
1067 elmt.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url +"', sizingMethod='image')";
1068 elmt.style.verticalAlign = (verticalAlign != null) ? verticalAlign : "middle";
1069 return elmt;
1070 };
1071
1072 /**
1073 * Creates a DOM element for an <code>img</code> tag using the URL given. This
1074 * is a convenience method that automatically includes the necessary CSS to
1075 * allow for translucency, even on IE.
1076 *
1077 * @function
1078 * @param {String} url the URL to the image
1079 * @param {String} verticalAlign the CSS value for the image's vertical-align
1080 * @return {Element} a DOM element containing the <code>img</code> tag
1081 */
1082 SimileAjax.Graphics.createTranslucentImage = SimileAjax.Graphics.pngIsTranslucent ?
1083 SimileAjax.Graphics._createTranslucentImage1 :
1084 SimileAjax.Graphics._createTranslucentImage2;
1085
1086 SimileAjax.Graphics._createTranslucentImageHTML1 = function(url, verticalAlign) {
1087 return "<img src=\"" + url + "\"" +
1088 (verticalAlign != null ? " style=\"vertical-align: " + verticalAlign + ";\"" : "") +
1089 " />";
1090 };
1091 SimileAjax.Graphics._createTranslucentImageHTML2 = function(url, verticalAlign) {
1092 var style =
1093 "width: 1px; height: 1px; " +
1094 "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url +"', sizingMethod='image');" +
1095 (verticalAlign != null ? " vertical-align: " + verticalAlign + ";" : "");
1096
1097 return "<img src='" + url + "' style=\"" + style + "\" />";
1098 };
1099
1100 /**
1101 * Creates an HTML string for an <code>img</code> tag using the URL given.
1102 * This is a convenience method that automatically includes the necessary CSS
1103 * to allow for translucency, even on IE.
1104 *
1105 * @function
1106 * @param {String} url the URL to the image
1107 * @param {String} verticalAlign the CSS value for the image's vertical-align
1108 * @return {String} a string containing the <code>img</code> tag
1109 */
1110 SimileAjax.Graphics.createTranslucentImageHTML = SimileAjax.Graphics.pngIsTranslucent ?
1111 SimileAjax.Graphics._createTranslucentImageHTML1 :
1112 SimileAjax.Graphics._createTranslucentImageHTML2;
1113
1114 /**
1115 * Sets the opacity on the given DOM element.
1116 *
1117 * @param {Element} elmt the DOM element to set the opacity on
1118 * @param {Number} opacity an integer from 0 to 100 specifying the opacity
1119 */
1120 SimileAjax.Graphics.setOpacity = function(elmt, opacity) {
1121 if (SimileAjax.Platform.browser.isIE) {
1122 elmt.style.filter = "progid:DXImageTransform.Microsoft.Alpha(Style=0,Opacity=" + opacity + ")";
1123 } else {
1124 var o = (opacity / 100).toString();
1125 elmt.style.opacity = o;
1126 elmt.style.MozOpacity = o;
1127 }
1128 };
1129
1130 /*==================================================
1131 * Bubble
1132 *==================================================
1133 */
1134
1135 SimileAjax.Graphics.bubbleConfig = {
1136 containerCSSClass: "simileAjax-bubble-container",
1137 innerContainerCSSClass: "simileAjax-bubble-innerContainer",
1138 contentContainerCSSClass: "simileAjax-bubble-contentContainer",
1139
1140 borderGraphicSize: 50,
1141 borderGraphicCSSClassPrefix: "simileAjax-bubble-border-",
1142
1143 arrowGraphicTargetOffset: 33, // from tip of arrow to the side of the graphic that touches the content of the bubble
1144 arrowGraphicLength: 100, // dimension of arrow graphic along the direction that the arrow points
1145 arrowGraphicWidth: 49, // dimension of arrow graphic perpendicular to the direction that the arrow points
1146 arrowGraphicCSSClassPrefix: "simileAjax-bubble-arrow-",
1147
1148 closeGraphicCSSClass: "simileAjax-bubble-close",
1149
1150 extraPadding: 20
1151 };
1152
1153 /**
1154 * Creates a nice, rounded bubble popup with the given content in a div,
1155 * page coordinates and a suggested width. The bubble will point to the
1156 * location on the page as described by pageX and pageY. All measurements
1157 * should be given in pixels.
1158 *
1159 * @param {Element} the content div
1160 * @param {Number} pageX the x coordinate of the point to point to
1161 * @param {Number} pageY the y coordinate of the point to point to
1162 * @param {Number} contentWidth a suggested width of the content
1163 * @param {String} orientation a string ("top", "bottom", "left", or "right")
1164 * that describes the orientation of the arrow on the bubble
1165 * @param {Number} maxHeight. Add a scrollbar div if bubble would be too tall.
1166 * Default of 0 or null means no maximum
1167 */
1168 SimileAjax.Graphics.createBubbleForContentAndPoint = function(
1169 div, pageX, pageY, contentWidth, orientation, maxHeight) {
1170 if (typeof contentWidth != "number") {
1171 contentWidth = 300;
1172 }
1173 if (typeof maxHeight != "number") {
1174 maxHeight = 0;
1175 }
1176
1177 div.style.position = "absolute";
1178 div.style.left = "-5000px";
1179 div.style.top = "0px";
1180 div.style.width = contentWidth + "px";
1181 document.body.appendChild(div);
1182
1183 window.setTimeout(function() {
1184 var width = div.scrollWidth + 10;
1185 var height = div.scrollHeight + 10;
1186 var scrollDivW = 0; // width of the possible inner container when we want vertical scrolling
1187 if (maxHeight > 0 && height > maxHeight) {
1188 height = maxHeight;
1189 scrollDivW = width - 25;
1190 }
1191
1192 var bubble = SimileAjax.Graphics.createBubbleForPoint(pageX, pageY, width, height, orientation);
1193
1194 document.body.removeChild(div);
1195 div.style.position = "static";
1196 div.style.left = "";
1197 div.style.top = "";
1198
1199 // create a scroll div if needed
1200 if (scrollDivW > 0) {
1201 var scrollDiv = document.createElement("div");
1202 div.style.width = "";
1203 scrollDiv.style.width = scrollDivW + "px";
1204 scrollDiv.appendChild(div);
1205 bubble.content.appendChild(scrollDiv);
1206 } else {
1207 div.style.width = width + "px";
1208 bubble.content.appendChild(div);
1209 }
1210 }, 200);
1211 };
1212
1213 /**
1214 * Creates a nice, rounded bubble popup with the given page coordinates and
1215 * content dimensions. The bubble will point to the location on the page
1216 * as described by pageX and pageY. All measurements should be given in
1217 * pixels.
1218 *
1219 * @param {Number} pageX the x coordinate of the point to point to
1220 * @param {Number} pageY the y coordinate of the point to point to
1221 * @param {Number} contentWidth the width of the content box in the bubble
1222 * @param {Number} contentHeight the height of the content box in the bubble
1223 * @param {String} orientation a string ("top", "bottom", "left", or "right")
1224 * that describes the orientation of the arrow on the bubble
1225 * @return {Element} a DOM element for the newly created bubble
1226 */
1227 SimileAjax.Graphics.createBubbleForPoint = function(pageX, pageY, contentWidth, contentHeight, orientation) {
1228 contentWidth = parseInt(contentWidth, 10); // harden against bad input bugs
1229 contentHeight = parseInt(contentHeight, 10); // getting numbers-as-strings
1230
1231 var bubbleConfig = SimileAjax.Graphics.bubbleConfig;
1232 var pngTransparencyClassSuffix =
1233 SimileAjax.Graphics.pngIsTranslucent ? "pngTranslucent" : "pngNotTranslucent";
1234
1235 var bubbleWidth = contentWidth + 2 * bubbleConfig.borderGraphicSize;
1236 var bubbleHeight = contentHeight + 2 * bubbleConfig.borderGraphicSize;
1237
1238 var generatePngSensitiveClass = function(className) {
1239 return className + " " + className + "-" + pngTransparencyClassSuffix;
1240 };
1241
1242 /*
1243 * Render container divs
1244 */
1245 var div = document.createElement("div");
1246 div.className = generatePngSensitiveClass(bubbleConfig.containerCSSClass);
1247 div.style.width = contentWidth + "px";
1248 div.style.height = contentHeight + "px";
1249
1250 var divInnerContainer = document.createElement("div");
1251 divInnerContainer.className = generatePngSensitiveClass(bubbleConfig.innerContainerCSSClass);
1252 div.appendChild(divInnerContainer);
1253
1254 /*
1255 * Create layer for bubble
1256 */
1257 var close = function() {
1258 if (!bubble._closed) {
1259 document.body.removeChild(bubble._div);
1260 bubble._doc = null;
1261 bubble._div = null;
1262 bubble._content = null;
1263 bubble._closed = true;
1264 }
1265 }
1266 var bubble = { _closed: false };
1267 var layer = SimileAjax.WindowManager.pushLayer(close, true, div);
1268 bubble._div = div;
1269 bubble.close = function() { SimileAjax.WindowManager.popLayer(layer); }
1270
1271 /*
1272 * Render border graphics
1273 */
1274 var createBorder = function(classNameSuffix) {
1275 var divBorderGraphic = document.createElement("div");
1276 divBorderGraphic.className = generatePngSensitiveClass(bubbleConfig.borderGraphicCSSClassPrefix + classNameSuffix);
1277 divInnerContainer.appendChild(divBorderGraphic);
1278 };
1279 createBorder("top-left");
1280 createBorder("top-right");
1281 createBorder("bottom-left");
1282 createBorder("bottom-right");
1283 createBorder("left");
1284 createBorder("right");
1285 createBorder("top");
1286 createBorder("bottom");
1287
1288 /*
1289 * Render content
1290 */
1291 var divContentContainer = document.createElement("div");
1292 divContentContainer.className = generatePngSensitiveClass(bubbleConfig.contentContainerCSSClass);
1293 divInnerContainer.appendChild(divContentContainer);
1294 bubble.content = divContentContainer;
1295
1296 /*
1297 * Render close button
1298 */
1299 var divClose = document.createElement("div");
1300 divClose.className = generatePngSensitiveClass(bubbleConfig.closeGraphicCSSClass);
1301 divInnerContainer.appendChild(divClose);
1302 SimileAjax.WindowManager.registerEventWithObject(divClose, "click", bubble, "close");
1303
1304 (function() {
1305 var dims = SimileAjax.Graphics.getWindowDimensions();
1306 var docWidth = dims.w;
1307 var docHeight = dims.h;
1308
1309 var halfArrowGraphicWidth = Math.ceil(bubbleConfig.arrowGraphicWidth / 2);
1310
1311 var createArrow = function(classNameSuffix) {
1312 var divArrowGraphic = document.createElement("div");
1313 divArrowGraphic.className = generatePngSensitiveClass(bubbleConfig.arrowGraphicCSSClassPrefix + "point-" + classNameSuffix);
1314 divInnerContainer.appendChild(divArrowGraphic);
1315 return divArrowGraphic;
1316 };
1317
1318 if (pageX - halfArrowGraphicWidth - bubbleConfig.borderGraphicSize - bubbleConfig.extraPadding > 0 &&
1319 pageX + halfArrowGraphicWidth + bubbleConfig.borderGraphicSize + bubbleConfig.extraPadding < docWidth) {
1320
1321 /*
1322 * Bubble can be positioned above or below the target point.
1323 */
1324
1325 var left = pageX - Math.round(contentWidth / 2);
1326 left = pageX < (docWidth / 2) ?
1327 Math.max(left, bubbleConfig.extraPadding + bubbleConfig.borderGraphicSize) :
1328 Math.min(left, docWidth - bubbleConfig.extraPadding - bubbleConfig.borderGraphicSize - contentWidth);
1329
1330 if ((orientation && orientation == "top") ||
1331 (!orientation &&
1332 (pageY
1333 - bubbleConfig.arrowGraphicTargetOffset
1334 - contentHeight
1335 - bubbleConfig.borderGraphicSize
1336 - bubbleConfig.extraPadding > 0))) {
1337
1338 /*
1339 * Position bubble above the target point.
1340 */
1341
1342 var divArrow = createArrow("down");
1343 divArrow.style.left = (pageX - halfArrowGraphicWidth - left) + "px";
1344
1345 div.style.left = left + "px";
1346 div.style.top = (pageY - bubbleConfig.arrowGraphicTargetOffset - contentHeight) + "px";
1347
1348 return;
1349 } else if ((orientation && orientation == "bottom") ||
1350 (!orientation &&
1351 (pageY
1352 + bubbleConfig.arrowGraphicTargetOffset
1353 + contentHeight
1354 + bubbleConfig.borderGraphicSize
1355 + bubbleConfig.extraPadding < docHeight))) {
1356
1357 /*
1358 * Position bubble below the target point.
1359 */
1360
1361 var divArrow = createArrow("up");
1362 divArrow.style.left = (pageX - halfArrowGraphicWidth - left) + "px";
1363
1364 div.style.left = left + "px";
1365 div.style.top = (pageY + bubbleConfig.arrowGraphicTargetOffset) + "px";
1366
1367 return;
1368 }
1369 }
1370
1371 var top = pageY - Math.round(contentHeight / 2);
1372 top = pageY < (docHeight / 2) ?
1373 Math.max(top, bubbleConfig.extraPadding + bubbleConfig.borderGraphicSize) :
1374 Math.min(top, docHeight - bubbleConfig.extraPadding - bubbleConfig.borderGraphicSize - contentHeight);
1375
1376 if ((orientation && orientation == "left") ||
1377 (!orientation &&
1378 (pageX
1379 - bubbleConfig.arrowGraphicTargetOffset
1380 - contentWidth
1381 - bubbleConfig.borderGraphicSize
1382 - bubbleConfig.extraPadding > 0))) {
1383
1384 /*
1385 * Position bubble left of the target point.
1386 */
1387
1388 var divArrow = createArrow("right");
1389 divArrow.style.top = (pageY - halfArrowGraphicWidth - top) + "px";
1390
1391 div.style.top = top + "px";
1392 div.style.left = (pageX - bubbleConfig.arrowGraphicTargetOffset - contentWidth) + "px";
1393 } else {
1394
1395 /*
1396 * Position bubble right of the target point, as the last resort.
1397 */
1398
1399 var divArrow = createArrow("left");
1400 divArrow.style.top = (pageY - halfArrowGraphicWidth - top) + "px";
1401
1402 div.style.top = top + "px";
1403 div.style.left = (pageX + bubbleConfig.arrowGraphicTargetOffset) + "px";
1404 }
1405 })();
1406
1407 document.body.appendChild(div);
1408
1409 return bubble;
1410 };
1411
1412 SimileAjax.Graphics.getWindowDimensions = function() {
1413 if (typeof window.innerHeight == 'number') {
1414 return { w:window.innerWidth, h:window.innerHeight }; // Non-IE
1415 } else if (document.documentElement && document.documentElement.clientHeight) {
1416 return { // IE6+, in "standards compliant mode"
1417 w:document.documentElement.clientWidth,
1418 h:document.documentElement.clientHeight
1419 };
1420 } else if (document.body && document.body.clientHeight) {
1421 return { // IE 4 compatible
1422 w:document.body.clientWidth,
1423 h:document.body.clientHeight
1424 };
1425 }
1426 };
1427
1428
1429 /**
1430 * Creates a floating, rounded message bubble in the center of the window for
1431 * displaying modal information, e.g. "Loading..."
1432 *
1433 * @param {Document} doc the root document for the page to render on
1434 * @param {Object} an object with two properties, contentDiv and containerDiv,
1435 * consisting of the newly created DOM elements
1436 */
1437 SimileAjax.Graphics.createMessageBubble = function(doc) {
1438 var containerDiv = doc.createElement("div");
1439 if (SimileAjax.Graphics.pngIsTranslucent) {
1440 var topDiv = doc.createElement("div");
1441 topDiv.style.height = "33px";
1442 topDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-top-left.png) top left no-repeat";
1443 topDiv.style.paddingLeft = "44px";
1444 containerDiv.appendChild(topDiv);
1445
1446 var topRightDiv = doc.createElement("div");
1447 topRightDiv.style.height = "33px";
1448 topRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-top-right.png) top right no-repeat";
1449 topDiv.appendChild(topRightDiv);
1450
1451 var middleDiv = doc.createElement("div");
1452 middleDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-left.png) top left repeat-y";
1453 middleDiv.style.paddingLeft = "44px";
1454 containerDiv.appendChild(middleDiv);
1455
1456 var middleRightDiv = doc.createElement("div");
1457 middleRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-right.png) top right repeat-y";
1458 middleRightDiv.style.paddingRight = "44px";
1459 middleDiv.appendChild(middleRightDiv);
1460
1461 var contentDiv = doc.createElement("div");
1462 middleRightDiv.appendChild(contentDiv);
1463
1464 var bottomDiv = doc.createElement("div");
1465 bottomDiv.style.height = "55px";
1466 bottomDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-bottom-left.png) bottom left no-repeat";
1467 bottomDiv.style.paddingLeft = "44px";
1468 containerDiv.appendChild(bottomDiv);
1469
1470 var bottomRightDiv = doc.createElement("div");
1471 bottomRightDiv.style.height = "55px";
1472 bottomRightDiv.style.background = "url(" + SimileAjax.urlPrefix + "images/message-bottom-right.png) bottom right no-repeat";
1473 bottomDiv.appendChild(bottomRightDiv);
1474 } else {
1475 containerDiv.style.border = "2px solid #7777AA";
1476 containerDiv.style.padding = "20px";
1477 containerDiv.style.background = "white";
1478 SimileAjax.Graphics.setOpacity(containerDiv, 90);
1479
1480 var contentDiv = doc.createElement("div");
1481 containerDiv.appendChild(contentDiv);
1482 }
1483
1484 return {
1485 containerDiv: containerDiv,
1486 contentDiv: contentDiv
1487 };
1488 };
1489
1490 /*==================================================
1491 * Animation
1492 *==================================================
1493 */
1494
1495 /**
1496 * Creates an animation for a function, and an interval of values. The word
1497 * "animation" here is used in the sense of repeatedly calling a function with
1498 * a current value from within an interval, and a delta value.
1499 *
1500 * @param {Function} f a function to be called every 50 milliseconds throughout
1501 * the animation duration, of the form f(current, delta), where current is
1502 * the current value within the range and delta is the current change.
1503 * @param {Number} from a starting value
1504 * @param {Number} to an ending value
1505 * @param {Number} duration the duration of the animation in milliseconds
1506 * @param {Function} [cont] an optional function that is called at the end of
1507 * the animation, i.e. a continuation.
1508 * @return {SimileAjax.Graphics._Animation} a new animation object
1509 */
1510 SimileAjax.Graphics.createAnimation = function(f, from, to, duration, cont) {
1511 return new SimileAjax.Graphics._Animation(f, from, to, duration, cont);
1512 };
1513
1514 SimileAjax.Graphics._Animation = function(f, from, to, duration, cont) {
1515 this.f = f;
1516 this.cont = (typeof cont == "function") ? cont : function() {};
1517
1518 this.from = from;
1519 this.to = to;
1520 this.current = from;
1521
1522 this.duration = duration;
1523 this.start = new Date().getTime();
1524 this.timePassed = 0;
1525 };
1526
1527 /**
1528 * Runs this animation.
1529 */
1530 SimileAjax.Graphics._Animation.prototype.run = function() {
1531 var a = this;
1532 window.setTimeout(function() { a.step(); }, 50);
1533 };
1534
1535 /**
1536 * Increments this animation by one step, and then continues the animation with
1537 * <code>run()</code>.
1538 */
1539 SimileAjax.Graphics._Animation.prototype.step = function() {
1540 this.timePassed += 50;
1541
1542 var timePassedFraction = this.timePassed / this.duration;
1543 var parameterFraction = -Math.cos(timePassedFraction * Math.PI) / 2 + 0.5;
1544 var current = parameterFraction * (this.to - this.from) + this.from;
1545
1546 try {
1547 this.f(current, current - this.current);
1548 } catch (e) {
1549 }
1550 this.current = current;
1551
1552 if (this.timePassed < this.duration) {
1553 this.run();
1554 } else {
1555 this.f(this.to, 0);
1556 this["cont"]();
1557 }
1558 };
1559
1560 /*==================================================
1561 * CopyPasteButton
1562 *
1563 * Adapted from http://spaces.live.com/editorial/rayozzie/demo/liveclip/liveclipsample/techPreview.html.
1564 *==================================================
1565 */
1566
1567 /**
1568 * Creates a button and textarea for displaying structured data and copying it
1569 * to the clipboard. The data is dynamically generated by the given
1570 * createDataFunction parameter.
1571 *
1572 * @param {String} image an image URL to use as the background for the
1573 * generated box
1574 * @param {Number} width the width in pixels of the generated box
1575 * @param {Number} height the height in pixels of the generated box
1576 * @param {Function} createDataFunction a function that is called with no
1577 * arguments to generate the structured data
1578 * @return a new DOM element
1579 */
1580 SimileAjax.Graphics.createStructuredDataCopyButton = function(image, width, height, createDataFunction) {
1581 var div = document.createElement("div");
1582 div.style.position = "relative";
1583 div.style.display = "inline";
1584 div.style.width = width + "px";
1585 div.style.height = height + "px";
1586 div.style.overflow = "hidden";
1587 div.style.margin = "2px";
1588
1589 if (SimileAjax.Graphics.pngIsTranslucent) {
1590 div.style.background = "url(" + image + ") no-repeat";
1591 } else {
1592 div.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + image +"', sizingMethod='image')";
1593 }
1594
1595 var style;
1596 if (SimileAjax.Platform.browser.isIE) {
1597 style = "filter:alpha(opacity=0)";
1598 } else {
1599 style = "opacity: 0";
1600 }
1601 div.innerHTML = "<textarea rows='1' autocomplete='off' value='none' style='" + style + "' />";
1602
1603 var textarea = div.firstChild;
1604 textarea.style.width = width + "px";
1605 textarea.style.height = height + "px";
1606 textarea.onmousedown = function(evt) {
1607 evt = (evt) ? evt : ((event) ? event : null);
1608 if (evt.button == 2) {
1609 textarea.value = createDataFunction();
1610 textarea.select();
1611 }
1612 };
1613
1614 return div;
1615 };
1616
1617 /*==================================================
1618 * getWidthHeight
1619 *==================================================
1620 */
1621 SimileAjax.Graphics.getWidthHeight = function(el) {
1622 // RETURNS hash {width: w, height: h} in pixels
1623
1624 var w, h;
1625 // offsetWidth rounds on FF, so doesn't work for us.
1626 // See https://bugzilla.mozilla.org/show_bug.cgi?id=458617
1627 if (el.getBoundingClientRect == null) {
1628 // use offsetWidth
1629 w = el.offsetWidth;
1630 h = el.offsetHeight;
1631 } else {
1632 // use getBoundingClientRect
1633 var rect = el.getBoundingClientRect();
1634 w = Math.ceil(rect.right - rect.left);
1635 h = Math.ceil(rect.bottom - rect.top);
1636 }
1637 return {
1638 width: w,
1639 height: h
1640 };
1641 };
1642
1643
1644 /*==================================================
1645 * FontRenderingContext
1646 *==================================================
1647 */
1648 SimileAjax.Graphics.getFontRenderingContext = function(elmt, width) {
1649 return new SimileAjax.Graphics._FontRenderingContext(elmt, width);
1650 };
1651
1652 SimileAjax.Graphics._FontRenderingContext = function(elmt, width) {
1653 this._elmt = elmt;
1654 this._elmt.style.visibility = "hidden";
1655 if (typeof width == "string") {
1656 this._elmt.style.width = width;
1657 } else if (typeof width == "number") {
1658 this._elmt.style.width = width + "px";
1659 }
1660 };
1661
1662 SimileAjax.Graphics._FontRenderingContext.prototype.dispose = function() {
1663 this._elmt = null;
1664 };
1665
1666 SimileAjax.Graphics._FontRenderingContext.prototype.update = function() {
1667 this._elmt.innerHTML = "A";
1668 this._lineHeight = this._elmt.offsetHeight;
1669 };
1670
1671 SimileAjax.Graphics._FontRenderingContext.prototype.computeSize = function(text, className) {
1672 // className arg is optional
1673 var el = this._elmt;
1674 el.innerHTML = text;
1675 el.className = className === undefined ? '' : className;
1676 var wh = SimileAjax.Graphics.getWidthHeight(el);
1677 el.className = ''; // reset for the next guy
1678
1679 return wh;
1680 };
1681
1682 SimileAjax.Graphics._FontRenderingContext.prototype.getLineHeight = function() {
1683 return this._lineHeight;
1684 };
1685
1686 /**
1687 * @fileOverview A collection of date/time utility functions
1688 * @name SimileAjax.DateTime
1689 */
1690
1691 SimileAjax.DateTime = new Object();
1692
1693 SimileAjax.DateTime.MILLISECOND = 0;
1694 SimileAjax.DateTime.SECOND = 1;
1695 SimileAjax.DateTime.MINUTE = 2;
1696 SimileAjax.DateTime.HOUR = 3;
1697 SimileAjax.DateTime.DAY = 4;
1698 SimileAjax.DateTime.WEEK = 5;
1699 SimileAjax.DateTime.MONTH = 6;
1700 SimileAjax.DateTime.YEAR = 7;
1701 SimileAjax.DateTime.DECADE = 8;
1702 SimileAjax.DateTime.CENTURY = 9;
1703 SimileAjax.DateTime.MILLENNIUM = 10;
1704
1705 SimileAjax.DateTime.EPOCH = -1;
1706 SimileAjax.DateTime.ERA = -2;
1707
1708 /**
1709 * An array of unit lengths, expressed in milliseconds, of various lengths of
1710 * time. The array indices are predefined and stored as properties of the
1711 * SimileAjax.DateTime object, e.g. SimileAjax.DateTime.YEAR.
1712 * @type Array
1713 */
1714 SimileAjax.DateTime.gregorianUnitLengths = [];
1715 (function() {
1716 var d = SimileAjax.DateTime;
1717 var a = d.gregorianUnitLengths;
1718
1719 a[d.MILLISECOND] = 1;
1720 a[d.SECOND] = 1000;
1721 a[d.MINUTE] = a[d.SECOND] * 60;
1722 a[d.HOUR] = a[d.MINUTE] * 60;
1723 a[d.DAY] = a[d.HOUR] * 24;
1724 a[d.WEEK] = a[d.DAY] * 7;
1725 a[d.MONTH] = a[d.DAY] * 31;
1726 a[d.YEAR] = a[d.DAY] * 365;
1727 a[d.DECADE] = a[d.YEAR] * 10;
1728 a[d.CENTURY] = a[d.YEAR] * 100;
1729 a[d.MILLENNIUM] = a[d.YEAR] * 1000;
1730 })();
1731
1732 SimileAjax.DateTime._dateRegexp = new RegExp(
1733 "^(-?)([0-9]{4})(" + [
1734 "(-?([0-9]{2})(-?([0-9]{2}))?)", // -month-dayOfMonth
1735 "(-?([0-9]{3}))", // -dayOfYear
1736 "(-?W([0-9]{2})(-?([1-7]))?)" // -Wweek-dayOfWeek
1737 ].join("|") + ")?$"
1738 );
1739 SimileAjax.DateTime._timezoneRegexp = new RegExp(
1740 "Z|(([-+])([0-9]{2})(:?([0-9]{2}))?)$"
1741 );
1742 SimileAjax.DateTime._timeRegexp = new RegExp(
1743 "^([0-9]{2})(:?([0-9]{2})(:?([0-9]{2})(\.([0-9]+))?)?)?$"
1744 );
1745
1746 /**
1747 * Takes a date object and a string containing an ISO 8601 date and sets the
1748 * the date using information parsed from the string. Note that this method
1749 * does not parse any time information.
1750 *
1751 * @param {Date} dateObject the date object to modify
1752 * @param {String} string an ISO 8601 string to parse
1753 * @return {Date} the modified date object
1754 */
1755 SimileAjax.DateTime.setIso8601Date = function(dateObject, string) {
1756 /*
1757 * This function has been adapted from dojo.date, v.0.3.0
1758 * http://dojotoolkit.org/.
1759 */
1760
1761 var d = string.match(SimileAjax.DateTime._dateRegexp);
1762 if(!d) {
1763 throw new Error("Invalid date string: " + string);
1764 }
1765
1766 var sign = (d[1] == "-") ? -1 : 1; // BC or AD
1767 var year = sign * d[2];
1768 var month = d[5];
1769 var date = d[7];
1770 var dayofyear = d[9];
1771 var week = d[11];
1772 var dayofweek = (d[13]) ? d[13] : 1;
1773
1774 dateObject.setUTCFullYear(year);
1775 if (dayofyear) {
1776 dateObject.setUTCMonth(0);
1777 dateObject.setUTCDate(Number(dayofyear));
1778 } else if (week) {
1779 dateObject.setUTCMonth(0);
1780 dateObject.setUTCDate(1);
1781 var gd = dateObject.getUTCDay();
1782 var day = (gd) ? gd : 7;
1783 var offset = Number(dayofweek) + (7 * Number(week));
1784
1785 if (day <= 4) {
1786 dateObject.setUTCDate(offset + 1 - day);
1787 } else {
1788 dateObject.setUTCDate(offset + 8 - day);
1789 }
1790 } else {
1791 if (month) {
1792 dateObject.setUTCDate(1);
1793 dateObject.setUTCMonth(month - 1);
1794 }
1795 if (date) {
1796 dateObject.setUTCDate(date);
1797 }
1798 }
1799
1800 return dateObject;
1801 };
1802
1803 /**
1804 * Takes a date object and a string containing an ISO 8601 time and sets the
1805 * the time using information parsed from the string. Note that this method
1806 * does not parse any date information.
1807 *
1808 * @param {Date} dateObject the date object to modify
1809 * @param {String} string an ISO 8601 string to parse
1810 * @return {Date} the modified date object
1811 */
1812 SimileAjax.DateTime.setIso8601Time = function (dateObject, string) {
1813 /*
1814 * This function has been adapted from dojo.date, v.0.3.0
1815 * http://dojotoolkit.org/.
1816 */
1817
1818 var d = string.match(SimileAjax.DateTime._timeRegexp);
1819 if(!d) {
1820 SimileAjax.Debug.warn("Invalid time string: " + string);
1821 return false;
1822 }
1823 var hours = d[1];
1824 var mins = Number((d[3]) ? d[3] : 0);
1825 var secs = (d[5]) ? d[5] : 0;
1826 var ms = d[7] ? (Number("0." + d[7]) * 1000) : 0;
1827
1828 dateObject.setUTCHours(hours);
1829 dateObject.setUTCMinutes(mins);
1830 dateObject.setUTCSeconds(secs);
1831 dateObject.setUTCMilliseconds(ms);
1832
1833 return dateObject;
1834 };
1835
1836 /**
1837 * The timezone offset in minutes in the user's browser.
1838 * @type Number
1839 */
1840 SimileAjax.DateTime.timezoneOffset = new Date().getTimezoneOffset();
1841
1842 /**
1843 * Takes a date object and a string containing an ISO 8601 date and time and
1844 * sets the date object using information parsed from the string.
1845 *
1846 * @param {Date} dateObject the date object to modify
1847 * @param {String} string an ISO 8601 string to parse
1848 * @return {Date} the modified date object
1849 */
1850 SimileAjax.DateTime.setIso8601 = function (dateObject, string){
1851 /*
1852 * This function has been adapted from dojo.date, v.0.3.0
1853 * http://dojotoolkit.org/.
1854 */
1855
1856 var offset = null;
1857 var comps = (string.indexOf("T") == -1) ? string.split(" ") : string.split("T");
1858
1859 SimileAjax.DateTime.setIso8601Date(dateObject, comps[0]);
1860 if (comps.length == 2) {
1861 // first strip timezone info from the end
1862 var d = comps[1].match(SimileAjax.DateTime._timezoneRegexp);
1863 if (d) {
1864 if (d[0] == 'Z') {
1865 offset = 0;
1866 } else {
1867 offset = (Number(d[3]) * 60) + Number(d[5]);
1868 offset *= ((d[2] == '-') ? 1 : -1);
1869 }
1870 comps[1] = comps[1].substr(0, comps[1].length - d[0].length);
1871 }
1872
1873 SimileAjax.DateTime.setIso8601Time(dateObject, comps[1]);
1874 }
1875 if (offset == null) {
1876 offset = dateObject.getTimezoneOffset(); // local time zone if no tz info
1877 }
1878 dateObject.setTime(dateObject.getTime() + offset * 60000);
1879
1880 return dateObject;
1881 };
1882
1883 /**
1884 * Takes a string containing an ISO 8601 date and returns a newly instantiated
1885 * date object with the parsed date and time information from the string.
1886 *
1887 * @param {String} string an ISO 8601 string to parse
1888 * @return {Date} a new date object created from the string
1889 */
1890 SimileAjax.DateTime.parseIso8601DateTime = function (string) {
1891 try {
1892 return SimileAjax.DateTime.setIso8601(new Date(0), string);
1893 } catch (e) {
1894 return null;
1895 }
1896 };
1897
1898 /**
1899 * Takes a string containing a Gregorian date and time and returns a newly
1900 * instantiated date object with the parsed date and time information from the
1901 * string. If the param is actually an instance of Date instead of a string,
1902 * simply returns the given date instead.
1903 *
1904 * @param {Object} o an object, to either return or parse as a string
1905 * @return {Date} the date object
1906 */
1907 SimileAjax.DateTime.parseGregorianDateTime = function(o) {
1908 if (o == null) {
1909 return null;
1910 } else if (o instanceof Date) {
1911 return o;
1912 }
1913
1914 var s = o.toString();
1915 if (s.length > 0 && s.length < 8) {
1916 var space = s.indexOf(" ");
1917 if (space > 0) {
1918 var year = parseInt(s.substr(0, space));
1919 var suffix = s.substr(space + 1);
1920 if (suffix.toLowerCase() == "bc") {
1921 year = 1 - year;
1922 }
1923 } else {
1924 var year = parseInt(s);
1925 }
1926
1927 var d = new Date(0);
1928 d.setUTCFullYear(year);
1929
1930 return d;
1931 }
1932
1933 try {
1934 return new Date(Date.parse(s));
1935 } catch (e) {
1936 return null;
1937 }
1938 };
1939
1940 /**
1941 * Rounds date objects down to the nearest interval or multiple of an interval.
1942 * This method modifies the given date object, converting it to the given
1943 * timezone if specified.
1944 *
1945 * @param {Date} date the date object to round
1946 * @param {Number} intervalUnit a constant, integer index specifying an
1947 * interval, e.g. SimileAjax.DateTime.HOUR
1948 * @param {Number} timeZone a timezone shift, given in hours
1949 * @param {Number} multiple a multiple of the interval to round by
1950 * @param {Number} firstDayOfWeek an integer specifying the first day of the
1951 * week, 0 corresponds to Sunday, 1 to Monday, etc.
1952 */
1953 SimileAjax.DateTime.roundDownToInterval = function(date, intervalUnit, timeZone, multiple, firstDayOfWeek) {
1954 var timeShift = timeZone *
1955 SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR];
1956
1957 var date2 = new Date(date.getTime() + timeShift);
1958 var clearInDay = function(d) {
1959 d.setUTCMilliseconds(0);
1960 d.setUTCSeconds(0);
1961 d.setUTCMinutes(0);
1962 d.setUTCHours(0);
1963 };
1964 var clearInYear = function(d) {
1965 clearInDay(d);
1966 d.setUTCDate(1);
1967 d.setUTCMonth(0);
1968 };
1969
1970 switch(intervalUnit) {
1971 case SimileAjax.DateTime.MILLISECOND:
1972 var x = date2.getUTCMilliseconds();
1973 date2.setUTCMilliseconds(x - (x % multiple));
1974 break;
1975 case SimileAjax.DateTime.SECOND:
1976 date2.setUTCMilliseconds(0);
1977
1978 var x = date2.getUTCSeconds();
1979 date2.setUTCSeconds(x - (x % multiple));
1980 break;
1981 case SimileAjax.DateTime.MINUTE:
1982 date2.setUTCMilliseconds(0);
1983 date2.setUTCSeconds(0);
1984
1985 var x = date2.getUTCMinutes();
1986 date2.setTime(date2.getTime() -
1987 (x % multiple) * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.MINUTE]);
1988 break;
1989 case SimileAjax.DateTime.HOUR:
1990 date2.setUTCMilliseconds(0);
1991 date2.setUTCSeconds(0);
1992 date2.setUTCMinutes(0);
1993
1994 var x = date2.getUTCHours();
1995 date2.setUTCHours(x - (x % multiple));
1996 break;
1997 case SimileAjax.DateTime.DAY:
1998 clearInDay(date2);
1999 break;
2000 case SimileAjax.DateTime.WEEK:
2001 clearInDay(date2);
2002 var d = (date2.getUTCDay() + 7 - firstDayOfWeek) % 7;
2003 date2.setTime(date2.getTime() -
2004 d * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.DAY]);
2005 break;
2006 case SimileAjax.DateTime.MONTH:
2007 clearInDay(date2);
2008 date2.setUTCDate(1);
2009
2010 var x = date2.getUTCMonth();
2011 date2.setUTCMonth(x - (x % multiple));
2012 break;
2013 case SimileAjax.DateTime.YEAR:
2014 clearInYear(date2);
2015
2016 var x = date2.getUTCFullYear();
2017 date2.setUTCFullYear(x - (x % multiple));
2018 break;
2019 case SimileAjax.DateTime.DECADE:
2020 clearInYear(date2);
2021 date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 10) * 10);
2022 break;
2023 case SimileAjax.DateTime.CENTURY:
2024 clearInYear(date2);
2025 date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 100) * 100);
2026 break;
2027 case SimileAjax.DateTime.MILLENNIUM:
2028 clearInYear(date2);
2029 date2.setUTCFullYear(Math.floor(date2.getUTCFullYear() / 1000) * 1000);
2030 break;
2031 }
2032 date.setTime(date2.getTime() - timeShift);
2033 };
2034
2035 /**
2036 * Rounds date objects up to the nearest interval or multiple of an interval.
2037 * This method modifies the given date object, converting it to the given
2038 * timezone if specified.
2039 *
2040 * @param {Date} date the date object to round
2041 * @param {Number} intervalUnit a constant, integer index specifying an
2042 * interval, e.g. SimileAjax.DateTime.HOUR
2043 * @param {Number} timeZone a timezone shift, given in hours
2044 * @param {Number} multiple a multiple of the interval to round by
2045 * @param {Number} firstDayOfWeek an integer specifying the first day of the
2046 * week, 0 corresponds to Sunday, 1 to Monday, etc.
2047 * @see SimileAjax.DateTime.roundDownToInterval
2048 */
2049 SimileAjax.DateTime.roundUpToInterval = function(date, intervalUnit, timeZone, multiple, firstDayOfWeek) {
2050 var originalTime = date.getTime();
2051 SimileAjax.DateTime.roundDownToInterval(date, intervalUnit, timeZone, multiple, firstDayOfWeek);
2052 if (date.getTime() < originalTime) {
2053 date.setTime(date.getTime() +
2054 SimileAjax.DateTime.gregorianUnitLengths[intervalUnit] * multiple);
2055 }
2056 };
2057
2058 /**
2059 * Increments a date object by a specified interval, taking into
2060 * consideration the timezone.
2061 *
2062 * @param {Date} date the date object to increment
2063 * @param {Number} intervalUnit a constant, integer index specifying an
2064 * interval, e.g. SimileAjax.DateTime.HOUR
2065 * @param {Number} timeZone the timezone offset in hours
2066 */
2067 SimileAjax.DateTime.incrementByInterval = function(date, intervalUnit, timeZone) {
2068 timeZone = (typeof timeZone == 'undefined') ? 0 : timeZone;
2069
2070 var timeShift = timeZone *
2071 SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR];
2072
2073 var date2 = new Date(date.getTime() + timeShift);
2074
2075 switch(intervalUnit) {
2076 case SimileAjax.DateTime.MILLISECOND:
2077 date2.setTime(date2.getTime() + 1)
2078 break;
2079 case SimileAjax.DateTime.SECOND:
2080 date2.setTime(date2.getTime() + 1000);
2081 break;
2082 case SimileAjax.DateTime.MINUTE:
2083 date2.setTime(date2.getTime() +
2084 SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.MINUTE]);
2085 break;
2086 case SimileAjax.DateTime.HOUR:
2087 date2.setTime(date2.getTime() +
2088 SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR]);
2089 break;
2090 case SimileAjax.DateTime.DAY:
2091 date2.setUTCDate(date2.getUTCDate() + 1);
2092 break;
2093 case SimileAjax.DateTime.WEEK:
2094 date2.setUTCDate(date2.getUTCDate() + 7);
2095 break;
2096 case SimileAjax.DateTime.MONTH:
2097 date2.setUTCMonth(date2.getUTCMonth() + 1);
2098 break;
2099 case SimileAjax.DateTime.YEAR:
2100 date2.setUTCFullYear(date2.getUTCFullYear() + 1);
2101 break;
2102 case SimileAjax.DateTime.DECADE:
2103 date2.setUTCFullYear(date2.getUTCFullYear() + 10);
2104 break;
2105 case SimileAjax.DateTime.CENTURY:
2106 date2.setUTCFullYear(date2.getUTCFullYear() + 100);
2107 break;
2108 case SimileAjax.DateTime.MILLENNIUM:
2109 date2.setUTCFullYear(date2.getUTCFullYear() + 1000);
2110 break;
2111 }
2112
2113 date.setTime(date2.getTime() - timeShift);
2114 };
2115
2116 /**
2117 * Returns a new date object with the given time offset removed.
2118 *
2119 * @param {Date} date the starting date
2120 * @param {Number} timeZone a timezone specified in an hour offset to remove
2121 * @return {Date} a new date object with the offset removed
2122 */
2123 SimileAjax.DateTime.removeTimeZoneOffset = function(date, timeZone) {
2124 return new Date(date.getTime() +
2125 timeZone * SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.HOUR]);
2126 };
2127
2128 /**
2129 * Returns the timezone of the user's browser.
2130 *
2131 * @return {Number} the timezone in the user's locale in hours
2132 */
2133 SimileAjax.DateTime.getTimezone = function() {
2134 var d = new Date().getTimezoneOffset();
2135 return d / -60;
2136 };
2137 /*==================================================
2138 * String Utility Functions and Constants
2139 *==================================================
2140 */
2141
2142 String.prototype.trim = function() {
2143 return this.replace(/^\s+|\s+$/g, '');
2144 };
2145
2146 String.prototype.startsWith = function(prefix) {
2147 return this.length >= prefix.length && this.substr(0, prefix.length) == prefix;
2148 };
2149
2150 String.prototype.endsWith = function(suffix) {
2151 return this.length >= suffix.length && this.substr(this.length - suffix.length) == suffix;
2152 };
2153
2154 String.substitute = function(s, objects) {
2155 var result = "";
2156 var start = 0;
2157 while (start < s.length - 1) {
2158 var percent = s.indexOf("%", start);
2159 if (percent < 0 || percent == s.length - 1) {
2160 break;
2161 } else if (percent > start && s.charAt(percent - 1) == "\\") {
2162 result += s.substring(start, percent - 1) + "%";
2163 start = percent + 1;
2164 } else {
2165 var n = parseInt(s.charAt(percent + 1));
2166 if (isNaN(n) || n >= objects.length) {
2167 result += s.substring(start, percent + 2);
2168 } else {
2169 result += s.substring(start, percent) + objects[n].toString();
2170 }
2171 start = percent + 2;
2172 }
2173 }
2174
2175 if (start < s.length) {
2176 result += s.substring(start);
2177 }
2178 return result;
2179 };
2180 /*==================================================
2181 * HTML Utility Functions
2182 *==================================================
2183 */
2184
2185 SimileAjax.HTML = new Object();
2186
2187 SimileAjax.HTML._e2uHash = {};
2188 (function() {
2189 var e2uHash = SimileAjax.HTML._e2uHash;
2190 e2uHash['nbsp']= '\u00A0[space]';
2191 e2uHash['iexcl']= '\u00A1';
2192 e2uHash['cent']= '\u00A2';
2193 e2uHash['pound']= '\u00A3';
2194 e2uHash['curren']= '\u00A4';
2195 e2uHash['yen']= '\u00A5';
2196 e2uHash['brvbar']= '\u00A6';
2197 e2uHash['sect']= '\u00A7';
2198 e2uHash['uml']= '\u00A8';
2199 e2uHash['copy']= '\u00A9';
2200 e2uHash['ordf']= '\u00AA';
2201 e2uHash['laquo']= '\u00AB';
2202 e2uHash['not']= '\u00AC';
2203 e2uHash['shy']= '\u00AD';
2204 e2uHash['reg']= '\u00AE';
2205 e2uHash['macr']= '\u00AF';
2206 e2uHash['deg']= '\u00B0';
2207 e2uHash['plusmn']= '\u00B1';
2208 e2uHash['sup2']= '\u00B2';
2209 e2uHash['sup3']= '\u00B3';
2210 e2uHash['acute']= '\u00B4';
2211 e2uHash['micro']= '\u00B5';
2212 e2uHash['para']= '\u00B6';
2213 e2uHash['middot']= '\u00B7';
2214 e2uHash['cedil']= '\u00B8';
2215 e2uHash['sup1']= '\u00B9';
2216 e2uHash['ordm']= '\u00BA';
2217 e2uHash['raquo']= '\u00BB';
2218 e2uHash['frac14']= '\u00BC';
2219 e2uHash['frac12']= '\u00BD';
2220 e2uHash['frac34']= '\u00BE';
2221 e2uHash['iquest']= '\u00BF';
2222 e2uHash['Agrave']= '\u00C0';
2223 e2uHash['Aacute']= '\u00C1';
2224 e2uHash['Acirc']= '\u00C2';
2225 e2uHash['Atilde']= '\u00C3';
2226 e2uHash['Auml']= '\u00C4';
2227 e2uHash['Aring']= '\u00C5';
2228 e2uHash['AElig']= '\u00C6';
2229 e2uHash['Ccedil']= '\u00C7';
2230 e2uHash['Egrave']= '\u00C8';
2231 e2uHash['Eacute']= '\u00C9';
2232 e2uHash['Ecirc']= '\u00CA';
2233 e2uHash['Euml']= '\u00CB';
2234 e2uHash['Igrave']= '\u00CC';
2235 e2uHash['Iacute']= '\u00CD';
2236 e2uHash['Icirc']= '\u00CE';
2237 e2uHash['Iuml']= '\u00CF';
2238 e2uHash['ETH']= '\u00D0';
2239 e2uHash['Ntilde']= '\u00D1';
2240 e2uHash['Ograve']= '\u00D2';
2241 e2uHash['Oacute']= '\u00D3';
2242 e2uHash['Ocirc']= '\u00D4';
2243 e2uHash['Otilde']= '\u00D5';
2244 e2uHash['Ouml']= '\u00D6';
2245 e2uHash['times']= '\u00D7';
2246 e2uHash['Oslash']= '\u00D8';
2247 e2uHash['Ugrave']= '\u00D9';
2248 e2uHash['Uacute']= '\u00DA';
2249 e2uHash['Ucirc']= '\u00DB';
2250 e2uHash['Uuml']= '\u00DC';
2251 e2uHash['Yacute']= '\u00DD';
2252 e2uHash['THORN']= '\u00DE';
2253 e2uHash['szlig']= '\u00DF';
2254 e2uHash['agrave']= '\u00E0';
2255 e2uHash['aacute']= '\u00E1';
2256 e2uHash['acirc']= '\u00E2';
2257 e2uHash['atilde']= '\u00E3';
2258 e2uHash['auml']= '\u00E4';
2259 e2uHash['aring']= '\u00E5';
2260 e2uHash['aelig']= '\u00E6';
2261 e2uHash['ccedil']= '\u00E7';
2262 e2uHash['egrave']= '\u00E8';
2263 e2uHash['eacute']= '\u00E9';
2264 e2uHash['ecirc']= '\u00EA';
2265 e2uHash['euml']= '\u00EB';
2266 e2uHash['igrave']= '\u00EC';
2267 e2uHash['iacute']= '\u00ED';
2268 e2uHash['icirc']= '\u00EE';
2269 e2uHash['iuml']= '\u00EF';
2270 e2uHash['eth']= '\u00F0';
2271 e2uHash['ntilde']= '\u00F1';
2272 e2uHash['ograve']= '\u00F2';
2273 e2uHash['oacute']= '\u00F3';
2274 e2uHash['ocirc']= '\u00F4';
2275 e2uHash['otilde']= '\u00F5';
2276 e2uHash['ouml']= '\u00F6';
2277 e2uHash['divide']= '\u00F7';
2278 e2uHash['oslash']= '\u00F8';
2279 e2uHash['ugrave']= '\u00F9';
2280 e2uHash['uacute']= '\u00FA';
2281 e2uHash['ucirc']= '\u00FB';
2282 e2uHash['uuml']= '\u00FC';
2283 e2uHash['yacute']= '\u00FD';
2284 e2uHash['thorn']= '\u00FE';
2285 e2uHash['yuml']= '\u00FF';
2286 e2uHash['quot']= '\u0022';
2287 e2uHash['amp']= '\u0026';
2288 e2uHash['lt']= '\u003C';
2289 e2uHash['gt']= '\u003E';
2290 e2uHash['OElig']= '';
2291 e2uHash['oelig']= '\u0153';
2292 e2uHash['Scaron']= '\u0160';
2293 e2uHash['scaron']= '\u0161';
2294 e2uHash['Yuml']= '\u0178';
2295 e2uHash['circ']= '\u02C6';
2296 e2uHash['tilde']= '\u02DC';
2297 e2uHash['ensp']= '\u2002';
2298 e2uHash['emsp']= '\u2003';
2299 e2uHash['thinsp']= '\u2009';
2300 e2uHash['zwnj']= '\u200C';
2301 e2uHash['zwj']= '\u200D';
2302 e2uHash['lrm']= '\u200E';
2303 e2uHash['rlm']= '\u200F';
2304 e2uHash['ndash']= '\u2013';
2305 e2uHash['mdash']= '\u2014';
2306 e2uHash['lsquo']= '\u2018';
2307 e2uHash['rsquo']= '\u2019';
2308 e2uHash['sbquo']= '\u201A';
2309 e2uHash['ldquo']= '\u201C';
2310 e2uHash['rdquo']= '\u201D';
2311 e2uHash['bdquo']= '\u201E';
2312 e2uHash['dagger']= '\u2020';
2313 e2uHash['Dagger']= '\u2021';
2314 e2uHash['permil']= '\u2030';
2315 e2uHash['lsaquo']= '\u2039';
2316 e2uHash['rsaquo']= '\u203A';
2317 e2uHash['euro']= '\u20AC';
2318 e2uHash['fnof']= '\u0192';
2319 e2uHash['Alpha']= '\u0391';
2320 e2uHash['Beta']= '\u0392';
2321 e2uHash['Gamma']= '\u0393';
2322 e2uHash['Delta']= '\u0394';
2323 e2uHash['Epsilon']= '\u0395';
2324 e2uHash['Zeta']= '\u0396';
2325 e2uHash['Eta']= '\u0397';
2326 e2uHash['Theta']= '\u0398';
2327 e2uHash['Iota']= '\u0399';
2328 e2uHash['Kappa']= '\u039A';
2329 e2uHash['Lambda']= '\u039B';
2330 e2uHash['Mu']= '\u039C';
2331 e2uHash['Nu']= '\u039D';
2332 e2uHash['Xi']= '\u039E';
2333 e2uHash['Omicron']= '\u039F';
2334 e2uHash['Pi']= '\u03A0';
2335 e2uHash['Rho']= '\u03A1';
2336 e2uHash['Sigma']= '\u03A3';
2337 e2uHash['Tau']= '\u03A4';
2338 e2uHash['Upsilon']= '\u03A5';
2339 e2uHash['Phi']= '\u03A6';
2340 e2uHash['Chi']= '\u03A7';
2341 e2uHash['Psi']= '\u03A8';
2342 e2uHash['Omega']= '\u03A9';
2343 e2uHash['alpha']= '\u03B1';
2344 e2uHash['beta']= '\u03B2';
2345 e2uHash['gamma']= '\u03B3';
2346 e2uHash['delta']= '\u03B4';
2347 e2uHash['epsilon']= '\u03B5';
2348 e2uHash['zeta']= '\u03B6';
2349 e2uHash['eta']= '\u03B7';
2350 e2uHash['theta']= '\u03B8';
2351 e2uHash['iota']= '\u03B9';
2352 e2uHash['kappa']= '\u03BA';
2353 e2uHash['lambda']= '\u03BB';
2354 e2uHash['mu']= '\u03BC';
2355 e2uHash['nu']= '\u03BD';
2356 e2uHash['xi']= '\u03BE';
2357 e2uHash['omicron']= '\u03BF';
2358 e2uHash['pi']= '\u03C0';
2359 e2uHash['rho']= '\u03C1';
2360 e2uHash['sigmaf']= '\u03C2';
2361 e2uHash['sigma']= '\u03C3';
2362 e2uHash['tau']= '\u03C4';
2363 e2uHash['upsilon']= '\u03C5';
2364 e2uHash['phi']= '\u03C6';
2365 e2uHash['chi']= '\u03C7';
2366 e2uHash['psi']= '\u03C8';
2367 e2uHash['omega']= '\u03C9';
2368 e2uHash['thetasym']= '\u03D1';
2369 e2uHash['upsih']= '\u03D2';
2370 e2uHash['piv']= '\u03D6';
2371 e2uHash['bull']= '\u2022';
2372 e2uHash['hellip']= '\u2026';
2373 e2uHash['prime']= '\u2032';
2374 e2uHash['Prime']= '\u2033';
2375 e2uHash['oline']= '\u203E';
2376 e2uHash['frasl']= '\u2044';
2377 e2uHash['weierp']= '\u2118';
2378 e2uHash['image']= '\u2111';
2379 e2uHash['real']= '\u211C';
2380 e2uHash['trade']= '\u2122';
2381 e2uHash['alefsym']= '\u2135';
2382 e2uHash['larr']= '\u2190';
2383 e2uHash['uarr']= '\u2191';
2384 e2uHash['rarr']= '\u2192';
2385 e2uHash['darr']= '\u2193';
2386 e2uHash['harr']= '\u2194';
2387 e2uHash['crarr']= '\u21B5';
2388 e2uHash['lArr']= '\u21D0';
2389 e2uHash['uArr']= '\u21D1';
2390 e2uHash['rArr']= '\u21D2';
2391 e2uHash['dArr']= '\u21D3';
2392 e2uHash['hArr']= '\u21D4';
2393 e2uHash['forall']= '\u2200';
2394 e2uHash['part']= '\u2202';
2395 e2uHash['exist']= '\u2203';
2396 e2uHash['empty']= '\u2205';
2397 e2uHash['nabla']= '\u2207';
2398 e2uHash['isin']= '\u2208';
2399 e2uHash['notin']= '\u2209';
2400 e2uHash['ni']= '\u220B';
2401 e2uHash['prod']= '\u220F';
2402 e2uHash['sum']= '\u2211';
2403 e2uHash['minus']= '\u2212';
2404 e2uHash['lowast']= '\u2217';
2405 e2uHash['radic']= '\u221A';
2406 e2uHash['prop']= '\u221D';
2407 e2uHash['infin']= '\u221E';
2408 e2uHash['ang']= '\u2220';
2409 e2uHash['and']= '\u2227';
2410 e2uHash['or']= '\u2228';
2411 e2uHash['cap']= '\u2229';
2412 e2uHash['cup']= '\u222A';
2413 e2uHash['int']= '\u222B';
2414 e2uHash['there4']= '\u2234';
2415 e2uHash['sim']= '\u223C';
2416 e2uHash['cong']= '\u2245';
2417 e2uHash['asymp']= '\u2248';
2418 e2uHash['ne']= '\u2260';
2419 e2uHash['equiv']= '\u2261';
2420 e2uHash['le']= '\u2264';
2421 e2uHash['ge']= '\u2265';
2422 e2uHash['sub']= '\u2282';
2423 e2uHash['sup']= '\u2283';
2424 e2uHash['nsub']= '\u2284';
2425 e2uHash['sube']= '\u2286';
2426 e2uHash['supe']= '\u2287';
2427 e2uHash['oplus']= '\u2295';
2428 e2uHash['otimes']= '\u2297';
2429 e2uHash['perp']= '\u22A5';
2430 e2uHash['sdot']= '\u22C5';
2431 e2uHash['lceil']= '\u2308';
2432 e2uHash['rceil']= '\u2309';
2433 e2uHash['lfloor']= '\u230A';
2434 e2uHash['rfloor']= '\u230B';
2435 e2uHash['lang']= '\u2329';
2436 e2uHash['rang']= '\u232A';
2437 e2uHash['loz']= '\u25CA';
2438 e2uHash['spades']= '\u2660';
2439 e2uHash['clubs']= '\u2663';
2440 e2uHash['hearts']= '\u2665';
2441 e2uHash['diams']= '\u2666';
2442 })();
2443
2444 SimileAjax.HTML.deEntify = function(s) {
2445 var e2uHash = SimileAjax.HTML._e2uHash;
2446
2447 var re = /&(\w+?);/;
2448 while (re.test(s)) {
2449 var m = s.match(re);
2450 s = s.replace(re, e2uHash[m[1]]);
2451 }
2452 return s;
2453 };
2454 /**
2455 * A basic set (in the mathematical sense) data structure
2456 *
2457 * @constructor
2458 * @param {Array or SimileAjax.Set} [a] an initial collection
2459 */
2460 SimileAjax.Set = function(a) {
2461 this._hash = {};
2462 this._count = 0;
2463
2464 if (a instanceof Array) {
2465 for (var i = 0; i < a.length; i++) {
2466 this.add(a[i]);
2467 }
2468 } else if (a instanceof SimileAjax.Set) {
2469 this.addSet(a);
2470 }
2471 }
2472
2473 /**
2474 * Adds the given object to this set, assuming there it does not already exist
2475 *
2476 * @param {Object} o the object to add
2477 * @return {Boolean} true if the object was added, false if not
2478 */
2479 SimileAjax.Set.prototype.add = function(o) {
2480 if (!(o in this._hash)) {
2481 this._hash[o] = true;
2482 this._count++;
2483 return true;
2484 }
2485 return false;
2486 }
2487
2488 /**
2489 * Adds each element in the given set to this set
2490 *
2491 * @param {SimileAjax.Set} set the set of elements to add
2492 */
2493 SimileAjax.Set.prototype.addSet = function(set) {
2494 for (var o in set._hash) {
2495 this.add(o);
2496 }
2497 }
2498
2499 /**
2500 * Removes the given element from this set
2501 *
2502 * @param {Object} o the object to remove
2503 * @return {Boolean} true if the object was successfully removed,
2504 * false otherwise
2505 */
2506 SimileAjax.Set.prototype.remove = function(o) {
2507 if (o in this._hash) {
2508 delete this._hash[o];
2509 this._count--;
2510 return true;
2511 }
2512 return false;
2513 }
2514
2515 /**
2516 * Removes the elements in this set that correspond to the elements in the
2517 * given set
2518 *
2519 * @param {SimileAjax.Set} set the set of elements to remove
2520 */
2521 SimileAjax.Set.prototype.removeSet = function(set) {
2522 for (var o in set._hash) {
2523 this.remove(o);
2524 }
2525 }
2526
2527 /**
2528 * Removes all elements in this set that are not present in the given set, i.e.
2529 * modifies this set to the intersection of the two sets
2530 *
2531 * @param {SimileAjax.Set} set the set to intersect
2532 */
2533 SimileAjax.Set.prototype.retainSet = function(set) {
2534 for (var o in this._hash) {
2535 if (!set.contains(o)) {
2536 delete this._hash[o];
2537 this._count--;
2538 }
2539 }
2540 }
2541
2542 /**
2543 * Returns whether or not the given element exists in this set
2544 *
2545 * @param {SimileAjax.Set} o the object to test for
2546 * @return {Boolean} true if the object is present, false otherwise
2547 */
2548 SimileAjax.Set.prototype.contains = function(o) {
2549 return (o in this._hash);
2550 }
2551
2552 /**
2553 * Returns the number of elements in this set
2554 *
2555 * @return {Number} the number of elements in this set
2556 */
2557 SimileAjax.Set.prototype.size = function() {
2558 return this._count;
2559 }
2560
2561 /**
2562 * Returns the elements of this set as an array
2563 *
2564 * @return {Array} a new array containing the elements of this set
2565 */
2566 SimileAjax.Set.prototype.toArray = function() {
2567 var a = [];
2568 for (var o in this._hash) {
2569 a.push(o);
2570 }
2571 return a;
2572 }
2573
2574 /**
2575 * Iterates through the elements of this set, order unspecified, executing the
2576 * given function on each element until the function returns true
2577 *
2578 * @param {Function} f a function of form f(element)
2579 */
2580 SimileAjax.Set.prototype.visit = function(f) {
2581 for (var o in this._hash) {
2582 if (f(o) == true) {
2583 break;
2584 }
2585 }
2586 }
2587
2588 /**
2589 * A sorted array data structure
2590 *
2591 * @constructor
2592 */
2593 SimileAjax.SortedArray = function(compare, initialArray) {
2594 this._a = (initialArray instanceof Array) ? initialArray : [];
2595 this._compare = compare;
2596 };
2597
2598 SimileAjax.SortedArray.prototype.add = function(elmt) {
2599 var sa = this;
2600 var index = this.find(function(elmt2) {
2601 return sa._compare(elmt2, elmt);
2602 });
2603
2604 if (index < this._a.length) {
2605 this._a.splice(index, 0, elmt);
2606 } else {
2607 this._a.push(elmt);
2608 }
2609 };
2610
2611 SimileAjax.SortedArray.prototype.remove = function(elmt) {
2612 var sa = this;
2613 var index = this.find(function(elmt2) {
2614 return sa._compare(elmt2, elmt);
2615 });
2616
2617 while (index < this._a.length && this._compare(this._a[index], elmt) == 0) {
2618 if (this._a[index] == elmt) {
2619 this._a.splice(index, 1);
2620 return true;
2621 } else {
2622 index++;
2623 }
2624 }
2625 return false;
2626 };
2627
2628 SimileAjax.SortedArray.prototype.removeAll = function() {
2629 this._a = [];
2630 };
2631
2632 SimileAjax.SortedArray.prototype.elementAt = function(index) {
2633 return this._a[index];
2634 };
2635
2636 SimileAjax.SortedArray.prototype.length = function() {
2637 return this._a.length;
2638 };
2639
2640 SimileAjax.SortedArray.prototype.find = function(compare) {
2641 var a = 0;
2642 var b = this._a.length;
2643
2644 while (a < b) {
2645 var mid = Math.floor((a + b) / 2);
2646 var c = compare(this._a[mid]);
2647 if (mid == a) {
2648 return c < 0 ? a+1 : a;
2649 } else if (c < 0) {
2650 a = mid;
2651 } else {
2652 b = mid;
2653 }
2654 }
2655 return a;
2656 };
2657
2658 SimileAjax.SortedArray.prototype.getFirst = function() {
2659 return (this._a.length > 0) ? this._a[0] : null;
2660 };
2661
2662 SimileAjax.SortedArray.prototype.getLast = function() {
2663 return (this._a.length > 0) ? this._a[this._a.length - 1] : null;
2664 };
2665
2666 /*==================================================
2667 * Event Index
2668 *==================================================
2669 */
2670
2671 SimileAjax.EventIndex = function(unit) {
2672 var eventIndex = this;
2673
2674 this._unit = (unit != null) ? unit : SimileAjax.NativeDateUnit;
2675 this._events = new SimileAjax.SortedArray(
2676 function(event1, event2) {
2677 return eventIndex._unit.compare(event1.getStart(), event2.getStart());
2678 }
2679 );
2680 this._idToEvent = {};
2681 this._indexed = true;
2682 };
2683
2684 SimileAjax.EventIndex.prototype.getUnit = function() {
2685 return this._unit;
2686 };
2687
2688 SimileAjax.EventIndex.prototype.getEvent = function(id) {
2689 return this._idToEvent[id];
2690 };
2691
2692 SimileAjax.EventIndex.prototype.add = function(evt) {
2693 this._events.add(evt);
2694 this._idToEvent[evt.getID()] = evt;
2695 this._indexed = false;
2696 };
2697
2698 SimileAjax.EventIndex.prototype.removeAll = function() {
2699 this._events.removeAll();
2700 this._idToEvent = {};
2701 this._indexed = false;
2702 };
2703
2704 SimileAjax.EventIndex.prototype.getCount = function() {
2705 return this._events.length();
2706 };
2707
2708 SimileAjax.EventIndex.prototype.getIterator = function(startDate, endDate) {
2709 if (!this._indexed) {
2710 this._index();
2711 }
2712 return new SimileAjax.EventIndex._Iterator(this._events, startDate, endDate, this._unit);
2713 };
2714
2715 SimileAjax.EventIndex.prototype.getReverseIterator = function(startDate, endDate) {
2716 if (!this._indexed) {
2717 this._index();
2718 }
2719 return new SimileAjax.EventIndex._ReverseIterator(this._events, startDate, endDate, this._unit);
2720 };
2721
2722 SimileAjax.EventIndex.prototype.getAllIterator = function() {
2723 return new SimileAjax.EventIndex._AllIterator(this._events);
2724 };
2725
2726 SimileAjax.EventIndex.prototype.getEarliestDate = function() {
2727 var evt = this._events.getFirst();
2728 return (evt == null) ? null : evt.getStart();
2729 };
2730
2731 SimileAjax.EventIndex.prototype.getLatestDate = function() {
2732 var evt = this._events.getLast();
2733 if (evt == null) {
2734 return null;
2735 }
2736
2737 if (!this._indexed) {
2738 this._index();
2739 }
2740
2741 var index = evt._earliestOverlapIndex;
2742 var date = this._events.elementAt(index).getEnd();
2743 for (var i = index + 1; i < this._events.length(); i++) {
2744 date = this._unit.later(date, this._events.elementAt(i).getEnd());
2745 }
2746
2747 return date;
2748 };
2749
2750 SimileAjax.EventIndex.prototype._index = function() {
2751 /*
2752 * For each event, we want to find the earliest preceding
2753 * event that overlaps with it, if any.
2754 */
2755
2756 var l = this._events.length();
2757 for (var i = 0; i < l; i++) {
2758 var evt = this._events.elementAt(i);
2759 evt._earliestOverlapIndex = i;
2760 }
2761
2762 var toIndex = 1;
2763 for (var i = 0; i < l; i++) {
2764 var evt = this._events.elementAt(i);
2765 var end = evt.getEnd();
2766
2767 toIndex = Math.max(toIndex, i + 1);
2768 while (toIndex < l) {
2769 var evt2 = this._events.elementAt(toIndex);
2770 var start2 = evt2.getStart();
2771
2772 if (this._unit.compare(start2, end) < 0) {
2773 evt2._earliestOverlapIndex = i;
2774 toIndex++;
2775 } else {
2776 break;
2777 }
2778 }
2779 }
2780 this._indexed = true;
2781 };
2782
2783 SimileAjax.EventIndex._Iterator = function(events, startDate, endDate, unit) {
2784 this._events = events;
2785 this._startDate = startDate;
2786 this._endDate = endDate;
2787 this._unit = unit;
2788
2789 this._currentIndex = events.find(function(evt) {
2790 return unit.compare(evt.getStart(), startDate);
2791 });
2792 if (this._currentIndex - 1 >= 0) {
2793 this._currentIndex = this._events.elementAt(this._currentIndex - 1)._earliestOverlapIndex;
2794 }
2795 this._currentIndex--;
2796
2797 this._maxIndex = events.find(function(evt) {
2798 return unit.compare(evt.getStart(), endDate);
2799 });
2800
2801 this._hasNext = false;
2802 this._next = null;
2803 this._findNext();
2804 };
2805
2806 SimileAjax.EventIndex._Iterator.prototype = {
2807 hasNext: function() { return this._hasNext; },
2808 next: function() {
2809 if (this._hasNext) {
2810 var next = this._next;
2811 this._findNext();
2812
2813 return next;
2814 } else {
2815 return null;
2816 }
2817 },
2818 _findNext: function() {
2819 var unit = this._unit;
2820 while ((++this._currentIndex) < this._maxIndex) {
2821 var evt = this._events.elementAt(this._currentIndex);
2822 if (unit.compare(evt.getStart(), this._endDate) < 0 &&
2823 unit.compare(evt.getEnd(), this._startDate) > 0) {
2824
2825 this._next = evt;
2826 this._hasNext = true;
2827 return;
2828 }
2829 }
2830 this._next = null;
2831 this._hasNext = false;
2832 }
2833 };
2834
2835 SimileAjax.EventIndex._ReverseIterator = function(events, startDate, endDate, unit) {
2836 this._events = events;
2837 this._startDate = startDate;
2838 this._endDate = endDate;
2839 this._unit = unit;
2840
2841 this._minIndex = events.find(function(evt) {
2842 return unit.compare(evt.getStart(), startDate);
2843 });
2844 if (this._minIndex - 1 >= 0) {
2845 this._minIndex = this._events.elementAt(this._minIndex - 1)._earliestOverlapIndex;
2846 }
2847
2848 this._maxIndex = events.find(function(evt) {
2849 return unit.compare(evt.getStart(), endDate);
2850 });
2851
2852 this._currentIndex = this._maxIndex;
2853 this._hasNext = false;
2854 this._next = null;
2855 this._findNext();
2856 };
2857
2858 SimileAjax.EventIndex._ReverseIterator.prototype = {
2859 hasNext: function() { return this._hasNext; },
2860 next: function() {
2861 if (this._hasNext) {
2862 var next = this._next;
2863 this._findNext();
2864
2865 return next;
2866 } else {
2867 return null;
2868 }
2869 },
2870 _findNext: function() {
2871 var unit = this._unit;
2872 while ((--this._currentIndex) >= this._minIndex) {
2873 var evt = this._events.elementAt(this._currentIndex);
2874 if (unit.compare(evt.getStart(), this._endDate) < 0 &&
2875 unit.compare(evt.getEnd(), this._startDate) > 0) {
2876
2877 this._next = evt;
2878 this._hasNext = true;
2879 return;
2880 }
2881 }
2882 this._next = null;
2883 this._hasNext = false;
2884 }
2885 };
2886
2887 SimileAjax.EventIndex._AllIterator = function(events) {
2888 this._events = events;
2889 this._index = 0;
2890 };
2891
2892 SimileAjax.EventIndex._AllIterator.prototype = {
2893 hasNext: function() {
2894 return this._index < this._events.length();
2895 },
2896 next: function() {
2897 return this._index < this._events.length() ?
2898 this._events.elementAt(this._index++) : null;
2899 }
2900 };
2901 /*==================================================
2902 * Default Unit
2903 *==================================================
2904 */
2905
2906 SimileAjax.NativeDateUnit = new Object();
2907
2908 SimileAjax.NativeDateUnit.makeDefaultValue = function() {
2909 return new Date();
2910 };
2911
2912 SimileAjax.NativeDateUnit.cloneValue = function(v) {
2913 return new Date(v.getTime());
2914 };
2915
2916 SimileAjax.NativeDateUnit.getParser = function(format) {
2917 if (typeof format == "string") {
2918 format = format.toLowerCase();
2919 }
2920 return (format == "iso8601" || format == "iso 8601") ?
2921 SimileAjax.DateTime.parseIso8601DateTime :
2922 SimileAjax.DateTime.parseGregorianDateTime;
2923 };
2924
2925 SimileAjax.NativeDateUnit.parseFromObject = function(o) {
2926 return SimileAjax.DateTime.parseGregorianDateTime(o);
2927 };
2928
2929 SimileAjax.NativeDateUnit.toNumber = function(v) {
2930 return v.getTime();
2931 };
2932
2933 SimileAjax.NativeDateUnit.fromNumber = function(n) {
2934 return new Date(n);
2935 };
2936
2937 SimileAjax.NativeDateUnit.compare = function(v1, v2) {
2938 var n1, n2;
2939 if (typeof v1 == "object") {
2940 n1 = v1.getTime();
2941 } else {
2942 n1 = Number(v1);
2943 }
2944 if (typeof v2 == "object") {
2945 n2 = v2.getTime();
2946 } else {
2947 n2 = Number(v2);
2948 }
2949
2950 return n1 - n2;
2951 };
2952
2953 SimileAjax.NativeDateUnit.earlier = function(v1, v2) {
2954 return SimileAjax.NativeDateUnit.compare(v1, v2) < 0 ? v1 : v2;
2955 };
2956
2957 SimileAjax.NativeDateUnit.later = function(v1, v2) {
2958 return SimileAjax.NativeDateUnit.compare(v1, v2) > 0 ? v1 : v2;
2959 };
2960
2961 SimileAjax.NativeDateUnit.change = function(v, n) {
2962 return new Date(v.getTime() + n);
2963 };
2964
2965 /*==================================================
2966 * General, miscellaneous SimileAjax stuff
2967 *==================================================
2968 */
2969
2970 SimileAjax.ListenerQueue = function(wildcardHandlerName) {
2971 this._listeners = [];
2972 this._wildcardHandlerName = wildcardHandlerName;
2973 };
2974
2975 SimileAjax.ListenerQueue.prototype.add = function(listener) {
2976 this._listeners.push(listener);
2977 };
2978
2979 SimileAjax.ListenerQueue.prototype.remove = function(listener) {
2980 var listeners = this._listeners;
2981 for (var i = 0; i < listeners.length; i++) {
2982 if (listeners[i] == listener) {
2983 listeners.splice(i, 1);
2984 break;
2985 }
2986 }
2987 };
2988
2989 SimileAjax.ListenerQueue.prototype.fire = function(handlerName, args) {
2990 var listeners = [].concat(this._listeners);
2991 for (var i = 0; i < listeners.length; i++) {
2992 var listener = listeners[i];
2993 if (handlerName in listener) {
2994 try {
2995 listener[handlerName].apply(listener, args);
2996 } catch (e) {
2997 SimileAjax.Debug.exception("Error firing event of name " + handlerName, e);
2998 }
2999 } else if (this._wildcardHandlerName != null &&
3000 this._wildcardHandlerName in listener) {
3001 try {
3002 listener[this._wildcardHandlerName].apply(listener, [ handlerName ]);
3003 } catch (e) {
3004 SimileAjax.Debug.exception("Error firing event of name " + handlerName + " to wildcard handler", e);
3005 }
3006 }
3007 }
3008 };
3009
3010 /*======================================================================
3011 * History
3012 *
3013 * This is a singleton that keeps track of undoable user actions and
3014 * performs undos and redos in response to the browser's Back and
3015 * Forward buttons.
3016 *
3017 * Call addAction(action) to register an undoable user action. action
3018 * must have 4 fields:
3019 *
3020 * perform: an argument-less function that carries out the action
3021 * undo: an argument-less function that undos the action
3022 * label: a short, user-friendly string describing the action
3023 * uiLayer: the UI layer on which the action takes place
3024 *
3025 * By default, the history keeps track of upto 10 actions. You can
3026 * configure this behavior by setting
3027 * SimileAjax.History.maxHistoryLength
3028 * to a different number.
3029 *
3030 * An iframe is inserted into the document's body element to track
3031 * onload events.
3032 *======================================================================
3033 */
3034
3035 SimileAjax.History = {
3036 maxHistoryLength: 10,
3037 historyFile: "__history__.html",
3038 enabled: true,
3039
3040 _initialized: false,
3041 _listeners: new SimileAjax.ListenerQueue(),
3042
3043 _actions: [],
3044 _baseIndex: 0,
3045 _currentIndex: 0,
3046
3047 _plainDocumentTitle: document.title
3048 };
3049
3050 SimileAjax.History.formatHistoryEntryTitle = function(actionLabel) {
3051 return SimileAjax.History._plainDocumentTitle + " {" + actionLabel + "}";
3052 };
3053
3054 SimileAjax.History.initialize = function() {
3055 if (SimileAjax.History._initialized) {
3056 return;
3057 }
3058
3059 if (SimileAjax.History.enabled) {
3060 var iframe = document.createElement("iframe");
3061 iframe.id = "simile-ajax-history";
3062 iframe.style.position = "absolute";
3063 iframe.style.width = "10px";
3064 iframe.style.height = "10px";
3065 iframe.style.top = "0px";
3066 iframe.style.left = "0px";
3067 iframe.style.visibility = "hidden";
3068 iframe.src = SimileAjax.History.historyFile + "?0";
3069
3070 document.body.appendChild(iframe);
3071 SimileAjax.DOM.registerEvent(iframe, "load", SimileAjax.History._handleIFrameOnLoad);
3072
3073 SimileAjax.History._iframe = iframe;
3074 }
3075 SimileAjax.History._initialized = true;
3076 };
3077
3078 SimileAjax.History.addListener = function(listener) {
3079 SimileAjax.History.initialize();
3080
3081 SimileAjax.History._listeners.add(listener);
3082 };
3083
3084 SimileAjax.History.removeListener = function(listener) {
3085 SimileAjax.History.initialize();
3086
3087 SimileAjax.History._listeners.remove(listener);
3088 };
3089
3090 SimileAjax.History.addAction = function(action) {
3091 SimileAjax.History.initialize();
3092
3093 SimileAjax.History._listeners.fire("onBeforePerform", [ action ]);
3094 window.setTimeout(function() {
3095 try {
3096 action.perform();
3097 SimileAjax.History._listeners.fire("onAfterPerform", [ action ]);
3098
3099 if (SimileAjax.History.enabled) {
3100 SimileAjax.History._actions = SimileAjax.History._actions.slice(
3101 0, SimileAjax.History._currentIndex - SimileAjax.History._baseIndex);
3102
3103 SimileAjax.History._actions.push(action);
3104 SimileAjax.History._currentIndex++;
3105
3106 var diff = SimileAjax.History._actions.length - SimileAjax.History.maxHistoryLength;
3107 if (diff > 0) {
3108 SimileAjax.History._actions = SimileAjax.History._actions.slice(diff);
3109 SimileAjax.History._baseIndex += diff;
3110 }
3111
3112 try {
3113 SimileAjax.History._iframe.contentWindow.location.search =
3114 "?" + SimileAjax.History._currentIndex;
3115 } catch (e) {
3116 /*
3117 * We can't modify location.search most probably because it's a file:// url.
3118 * We'll just going to modify the document's title.
3119 */
3120 var title = SimileAjax.History.formatHistoryEntryTitle(action.label);
3121 document.title = title;
3122 }
3123 }
3124 } catch (e) {
3125 SimileAjax.Debug.exception(e, "Error adding action {" + action.label + "} to history");
3126 }
3127 }, 0);
3128 };
3129
3130 SimileAjax.History.addLengthyAction = function(perform, undo, label) {
3131 SimileAjax.History.addAction({
3132 perform: perform,
3133 undo: undo,
3134 label: label,
3135 uiLayer: SimileAjax.WindowManager.getBaseLayer(),
3136 lengthy: true
3137 });
3138 };
3139
3140 SimileAjax.History._handleIFrameOnLoad = function() {
3141 /*
3142 * This function is invoked when the user herself
3143 * navigates backward or forward. We need to adjust
3144 * the application's state accordingly.
3145 */
3146
3147 try {
3148 var q = SimileAjax.History._iframe.contentWindow.location.search;
3149 var c = (q.length == 0) ? 0 : Math.max(0, parseInt(q.substr(1)));
3150
3151 var finishUp = function() {
3152 var diff = c - SimileAjax.History._currentIndex;
3153 SimileAjax.History._currentIndex += diff;
3154 SimileAjax.History._baseIndex += diff;
3155
3156 SimileAjax.History._iframe.contentWindow.location.search = "?" + c;
3157 };
3158
3159 if (c < SimileAjax.History._currentIndex) { // need to undo
3160 SimileAjax.History._listeners.fire("onBeforeUndoSeveral", []);
3161 window.setTimeout(function() {
3162 while (SimileAjax.History._currentIndex > c &&
3163 SimileAjax.History._currentIndex > SimileAjax.History._baseIndex) {
3164
3165 SimileAjax.History._currentIndex--;
3166
3167 var action = SimileAjax.History._actions[SimileAjax.History._currentIndex - SimileAjax.History._baseIndex];
3168
3169 try {
3170 action.undo();
3171 } catch (e) {
3172 SimileAjax.Debug.exception(e, "History: Failed to undo action {" + action.label + "}");
3173 }
3174 }
3175
3176 SimileAjax.History._listeners.fire("onAfterUndoSeveral", []);
3177 finishUp();
3178 }, 0);
3179 } else if (c > SimileAjax.History._currentIndex) { // need to redo
3180 SimileAjax.History._listeners.fire("onBeforeRedoSeveral", []);
3181 window.setTimeout(function() {
3182 while (SimileAjax.History._currentIndex < c &&
3183 SimileAjax.History._currentIndex - SimileAjax.History._baseIndex < SimileAjax.History._actions.length) {
3184
3185 var action = SimileAjax.History._actions[SimileAjax.History._currentIndex - SimileAjax.History._baseIndex];
3186
3187 try {
3188 action.perform();
3189 } catch (e) {
3190 SimileAjax.Debug.exception(e, "History: Failed to redo action {" + action.label + "}");
3191 }
3192
3193 SimileAjax.History._currentIndex++;
3194 }
3195
3196 SimileAjax.History._listeners.fire("onAfterRedoSeveral", []);
3197 finishUp();
3198 }, 0);
3199 } else {
3200 var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex - 1;
3201 var title = (index >= 0 && index < SimileAjax.History._actions.length) ?
3202 SimileAjax.History.formatHistoryEntryTitle(SimileAjax.History._actions[index].label) :
3203 SimileAjax.History._plainDocumentTitle;
3204
3205 SimileAjax.History._iframe.contentWindow.document.title = title;
3206 document.title = title;
3207 }
3208 } catch (e) {
3209 // silent
3210 }
3211 };
3212
3213 SimileAjax.History.getNextUndoAction = function() {
3214 try {
3215 var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex - 1;
3216 return SimileAjax.History._actions[index];
3217 } catch (e) {
3218 return null;
3219 }
3220 };
3221
3222 SimileAjax.History.getNextRedoAction = function() {
3223 try {
3224 var index = SimileAjax.History._currentIndex - SimileAjax.History._baseIndex;
3225 return SimileAjax.History._actions[index];
3226 } catch (e) {
3227 return null;
3228 }
3229 };
3230 /**
3231 * @fileOverview UI layers and window-wide dragging
3232 * @name SimileAjax.WindowManager
3233 */
3234
3235 /**
3236 * This is a singleton that keeps track of UI layers (modal and
3237 * modeless) and enables/disables UI elements based on which layers
3238 * they belong to. It also provides window-wide dragging
3239 * implementation.
3240 */
3241 SimileAjax.WindowManager = {
3242 _initialized: false,
3243 _listeners: [],
3244
3245 _draggedElement: null,
3246 _draggedElementCallback: null,
3247 _dropTargetHighlightElement: null,
3248 _lastCoords: null,
3249 _ghostCoords: null,
3250 _draggingMode: "",
3251 _dragging: false,
3252
3253 _layers: []
3254 };
3255
3256 SimileAjax.WindowManager.initialize = function() {
3257 if (SimileAjax.WindowManager._initialized) {
3258 return;
3259 }
3260
3261 SimileAjax.DOM.registerEvent(document.body, "mousedown", SimileAjax.WindowManager._onBodyMouseDown);
3262 SimileAjax.DOM.registerEvent(document.body, "mousemove", SimileAjax.WindowManager._onBodyMouseMove);
3263 SimileAjax.DOM.registerEvent(document.body, "mouseup", SimileAjax.WindowManager._onBodyMouseUp);
3264 SimileAjax.DOM.registerEvent(document, "keydown", SimileAjax.WindowManager._onBodyKeyDown);
3265 SimileAjax.DOM.registerEvent(document, "keyup", SimileAjax.WindowManager._onBodyKeyUp);
3266
3267 SimileAjax.WindowManager._layers.push({index: 0});
3268
3269 SimileAjax.WindowManager._historyListener = {
3270 onBeforeUndoSeveral: function() {},
3271 onAfterUndoSeveral: function() {},
3272 onBeforeUndo: function() {},
3273 onAfterUndo: function() {},
3274
3275 onBeforeRedoSeveral: function() {},
3276 onAfterRedoSeveral: function() {},
3277 onBeforeRedo: function() {},
3278 onAfterRedo: function() {}
3279 };
3280 SimileAjax.History.addListener(SimileAjax.WindowManager._historyListener);
3281
3282 SimileAjax.WindowManager._initialized = true;
3283 };
3284
3285 SimileAjax.WindowManager.getBaseLayer = function() {
3286 SimileAjax.WindowManager.initialize();
3287 return SimileAjax.WindowManager._layers[0];
3288 };
3289
3290 SimileAjax.WindowManager.getHighestLayer = function() {
3291 SimileAjax.WindowManager.initialize();
3292 return SimileAjax.WindowManager._layers[SimileAjax.WindowManager._layers.length - 1];
3293 };
3294
3295 SimileAjax.WindowManager.registerEventWithObject = function(elmt, eventName, obj, handlerName, layer) {
3296 SimileAjax.WindowManager.registerEvent(
3297 elmt,
3298 eventName,
3299 function(elmt2, evt, target) {
3300 return obj[handlerName].call(obj, elmt2, evt, target);
3301 },
3302 layer
3303 );
3304 };
3305
3306 SimileAjax.WindowManager.registerEvent = function(elmt, eventName, handler, layer) {
3307 if (layer == null) {
3308 layer = SimileAjax.WindowManager.getHighestLayer();
3309 }
3310
3311 var handler2 = function(elmt, evt, target) {
3312 if (SimileAjax.WindowManager._canProcessEventAtLayer(layer)) {
3313 SimileAjax.WindowManager._popToLayer(layer.index);
3314 try {
3315 handler(elmt, evt, target);
3316 } catch (e) {
3317 SimileAjax.Debug.exception(e);
3318 }
3319 }
3320 SimileAjax.DOM.cancelEvent(evt);
3321 return false;
3322 }
3323
3324 SimileAjax.DOM.registerEvent(elmt, eventName, handler2);
3325 };
3326
3327 SimileAjax.WindowManager.pushLayer = function(f, ephemeral, elmt) {
3328 var layer = { onPop: f, index: SimileAjax.WindowManager._layers.length, ephemeral: (ephemeral), elmt: elmt };
3329 SimileAjax.WindowManager._layers.push(layer);
3330
3331 return layer;
3332 };
3333
3334 SimileAjax.WindowManager.popLayer = function(layer) {
3335 for (var i = 1; i < SimileAjax.WindowManager._layers.length; i++) {
3336 if (SimileAjax.WindowManager._layers[i] == layer) {
3337 SimileAjax.WindowManager._popToLayer(i - 1);
3338 break;
3339 }
3340 }
3341 };
3342
3343 SimileAjax.WindowManager.popAllLayers = function() {
3344 SimileAjax.WindowManager._popToLayer(0);
3345 };
3346
3347 SimileAjax.WindowManager.registerForDragging = function(elmt, callback, layer) {
3348 SimileAjax.WindowManager.registerEvent(
3349 elmt,
3350 "mousedown",
3351 function(elmt, evt, target) {
3352 SimileAjax.WindowManager._handleMouseDown(elmt, evt, callback);
3353 },
3354 layer
3355 );
3356 };
3357
3358 SimileAjax.WindowManager._popToLayer = function(level) {
3359 while (level+1 < SimileAjax.WindowManager._layers.length) {
3360 try {
3361 var layer = SimileAjax.WindowManager._layers.pop();
3362 if (layer.onPop != null) {
3363 layer.onPop();
3364 }
3365 } catch (e) {
3366 }
3367 }
3368 };
3369
3370 SimileAjax.WindowManager._canProcessEventAtLayer = function(layer) {
3371 if (layer.index == (SimileAjax.WindowManager._layers.length - 1)) {
3372 return true;
3373 }
3374 for (var i = layer.index + 1; i < SimileAjax.WindowManager._layers.length; i++) {
3375 if (!SimileAjax.WindowManager._layers[i].ephemeral) {
3376 return false;
3377 }
3378 }
3379 return true;
3380 };
3381
3382 SimileAjax.WindowManager.cancelPopups = function(evt) {
3383 var evtCoords = (evt) ? SimileAjax.DOM.getEventPageCoordinates(evt) : { x: -1, y: -1 };
3384
3385 var i = SimileAjax.WindowManager._layers.length - 1;
3386 while (i > 0 && SimileAjax.WindowManager._layers[i].ephemeral) {
3387 var layer = SimileAjax.WindowManager._layers[i];
3388 if (layer.elmt != null) { // if event falls within main element of layer then don't cancel
3389 var elmt = layer.elmt;
3390 var elmtCoords = SimileAjax.DOM.getPageCoordinates(elmt);
3391 if (evtCoords.x >= elmtCoords.left && evtCoords.x < (elmtCoords.left + elmt.offsetWidth) &&
3392 evtCoords.y >= elmtCoords.top && evtCoords.y < (elmtCoords.top + elmt.offsetHeight)) {
3393 break;
3394 }
3395 }
3396 i--;
3397 }
3398 SimileAjax.WindowManager._popToLayer(i);
3399 };
3400
3401 SimileAjax.WindowManager._onBodyMouseDown = function(elmt, evt, target) {
3402 if (!("eventPhase" in evt) || evt.eventPhase == evt.BUBBLING_PHASE) {
3403 SimileAjax.WindowManager.cancelPopups(evt);
3404 }
3405 };
3406
3407 SimileAjax.WindowManager._handleMouseDown = function(elmt, evt, callback) {
3408 SimileAjax.WindowManager._draggedElement = elmt;
3409 SimileAjax.WindowManager._draggedElementCallback = callback;
3410 SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
3411
3412 SimileAjax.DOM.cancelEvent(evt);
3413 return false;
3414 };
3415
3416 SimileAjax.WindowManager._onBodyKeyDown = function(elmt, evt, target) {
3417 if (SimileAjax.WindowManager._dragging) {
3418 if (evt.keyCode == 27) { // esc
3419 SimileAjax.WindowManager._cancelDragging();
3420 } else if ((evt.keyCode == 17 || evt.keyCode == 16) && SimileAjax.WindowManager._draggingMode != "copy") {
3421 SimileAjax.WindowManager._draggingMode = "copy";
3422
3423 var img = SimileAjax.Graphics.createTranslucentImage(SimileAjax.urlPrefix + "images/copy.png");
3424 img.style.position = "absolute";
3425 img.style.left = (SimileAjax.WindowManager._ghostCoords.left - 16) + "px";
3426 img.style.top = (SimileAjax.WindowManager._ghostCoords.top) + "px";
3427 document.body.appendChild(img);
3428
3429 SimileAjax.WindowManager._draggingModeIndicatorElmt = img;
3430 }
3431 }
3432 };
3433
3434 SimileAjax.WindowManager._onBodyKeyUp = function(elmt, evt, target) {
3435 if (SimileAjax.WindowManager._dragging) {
3436 if (evt.keyCode == 17 || evt.keyCode == 16) {
3437 SimileAjax.WindowManager._draggingMode = "";
3438 if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
3439 document.body.removeChild(SimileAjax.WindowManager._draggingModeIndicatorElmt);
3440 SimileAjax.WindowManager._draggingModeIndicatorElmt = null;
3441 }
3442 }
3443 }
3444 };
3445
3446 SimileAjax.WindowManager._onBodyMouseMove = function(elmt, evt, target) {
3447 if (SimileAjax.WindowManager._draggedElement != null) {
3448 var callback = SimileAjax.WindowManager._draggedElementCallback;
3449
3450 var lastCoords = SimileAjax.WindowManager._lastCoords;
3451 var diffX = evt.clientX - lastCoords.x;
3452 var diffY = evt.clientY - lastCoords.y;
3453
3454 if (!SimileAjax.WindowManager._dragging) {
3455 if (Math.abs(diffX) > 5 || Math.abs(diffY) > 5) {
3456 try {
3457 if ("onDragStart" in callback) {
3458 callback.onDragStart();
3459 }
3460
3461 if ("ghost" in callback && callback.ghost) {
3462 var draggedElmt = SimileAjax.WindowManager._draggedElement;
3463
3464 SimileAjax.WindowManager._ghostCoords = SimileAjax.DOM.getPageCoordinates(draggedElmt);
3465 SimileAjax.WindowManager._ghostCoords.left += diffX;
3466 SimileAjax.WindowManager._ghostCoords.top += diffY;
3467
3468 var ghostElmt = draggedElmt.cloneNode(true);
3469 ghostElmt.style.position = "absolute";
3470 ghostElmt.style.left = SimileAjax.WindowManager._ghostCoords.left + "px";
3471 ghostElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
3472 ghostElmt.style.zIndex = 1000;
3473 SimileAjax.Graphics.setOpacity(ghostElmt, 50);
3474
3475 document.body.appendChild(ghostElmt);
3476 callback._ghostElmt = ghostElmt;
3477 }
3478
3479 SimileAjax.WindowManager._dragging = true;
3480 SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
3481
3482 document.body.focus();
3483 } catch (e) {
3484 SimileAjax.Debug.exception("WindowManager: Error handling mouse down", e);
3485 SimileAjax.WindowManager._cancelDragging();
3486 }
3487 }
3488 } else {
3489 try {
3490 SimileAjax.WindowManager._lastCoords = { x: evt.clientX, y: evt.clientY };
3491
3492 if ("onDragBy" in callback) {
3493 callback.onDragBy(diffX, diffY);
3494 }
3495
3496 if ("_ghostElmt" in callback) {
3497 var ghostElmt = callback._ghostElmt;
3498
3499 SimileAjax.WindowManager._ghostCoords.left += diffX;
3500 SimileAjax.WindowManager._ghostCoords.top += diffY;
3501
3502 ghostElmt.style.left = SimileAjax.WindowManager._ghostCoords.left + "px";
3503 ghostElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
3504 if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
3505 var indicatorElmt = SimileAjax.WindowManager._draggingModeIndicatorElmt;
3506
3507 indicatorElmt.style.left = (SimileAjax.WindowManager._ghostCoords.left - 16) + "px";
3508 indicatorElmt.style.top = SimileAjax.WindowManager._ghostCoords.top + "px";
3509 }
3510
3511 if ("droppable" in callback && callback.droppable) {
3512 var coords = SimileAjax.DOM.getEventPageCoordinates(evt);
3513 var target = SimileAjax.DOM.hittest(
3514 coords.x, coords.y,
3515 [ SimileAjax.WindowManager._ghostElmt,
3516 SimileAjax.WindowManager._dropTargetHighlightElement
3517 ]
3518 );
3519 target = SimileAjax.WindowManager._findDropTarget(target);
3520
3521 if (target != SimileAjax.WindowManager._potentialDropTarget) {
3522 if (SimileAjax.WindowManager._dropTargetHighlightElement != null) {
3523 document.body.removeChild(SimileAjax.WindowManager._dropTargetHighlightElement);
3524
3525 SimileAjax.WindowManager._dropTargetHighlightElement = null;
3526 SimileAjax.WindowManager._potentialDropTarget = null;
3527 }
3528
3529 var droppable = false;
3530 if (target != null) {
3531 if ((!("canDropOn" in callback) || callback.canDropOn(target)) &&
3532 (!("canDrop" in target) || target.canDrop(SimileAjax.WindowManager._draggedElement))) {
3533
3534 droppable = true;
3535 }
3536 }
3537
3538 if (droppable) {
3539 var border = 4;
3540 var targetCoords = SimileAjax.DOM.getPageCoordinates(target);
3541 var highlight = document.createElement("div");
3542 highlight.style.border = border + "px solid yellow";
3543 highlight.style.backgroundColor = "yellow";
3544 highlight.style.position = "absolute";
3545 highlight.style.left = targetCoords.left + "px";
3546 highlight.style.top = targetCoords.top + "px";
3547 highlight.style.width = (target.offsetWidth - border * 2) + "px";
3548 highlight.style.height = (target.offsetHeight - border * 2) + "px";
3549 SimileAjax.Graphics.setOpacity(highlight, 30);
3550 document.body.appendChild(highlight);
3551
3552 SimileAjax.WindowManager._potentialDropTarget = target;
3553 SimileAjax.WindowManager._dropTargetHighlightElement = highlight;
3554 }
3555 }
3556 }
3557 }
3558 } catch (e) {
3559 SimileAjax.Debug.exception("WindowManager: Error handling mouse move", e);
3560 SimileAjax.WindowManager._cancelDragging();
3561 }
3562 }
3563
3564 SimileAjax.DOM.cancelEvent(evt);
3565 return false;
3566 }
3567 };
3568
3569 SimileAjax.WindowManager._onBodyMouseUp = function(elmt, evt, target) {
3570 if (SimileAjax.WindowManager._draggedElement != null) {
3571 try {
3572 if (SimileAjax.WindowManager._dragging) {
3573 var callback = SimileAjax.WindowManager._draggedElementCallback;
3574 if ("onDragEnd" in callback) {
3575 callback.onDragEnd();
3576 }
3577 if ("droppable" in callback && callback.droppable) {
3578 var dropped = false;
3579
3580 var target = SimileAjax.WindowManager._potentialDropTarget;
3581 if (target != null) {
3582 if ((!("canDropOn" in callback) || callback.canDropOn(target)) &&
3583 (!("canDrop" in target) || target.canDrop(SimileAjax.WindowManager._draggedElement))) {
3584
3585 if ("onDropOn" in callback) {
3586 callback.onDropOn(target);
3587 }
3588 target.ondrop(SimileAjax.WindowManager._draggedElement, SimileAjax.WindowManager._draggingMode);
3589
3590 dropped = true;
3591 }
3592 }
3593
3594 if (!dropped) {
3595 // TODO: do holywood explosion here
3596 }
3597 }
3598 }
3599 } finally {
3600 SimileAjax.WindowManager._cancelDragging();
3601 }
3602
3603 SimileAjax.DOM.cancelEvent(evt);
3604 return false;
3605 }
3606 };
3607
3608 SimileAjax.WindowManager._cancelDragging = function() {
3609 var callback = SimileAjax.WindowManager._draggedElementCallback;
3610 if ("_ghostElmt" in callback) {
3611 var ghostElmt = callback._ghostElmt;
3612 document.body.removeChild(ghostElmt);
3613
3614 delete callback._ghostElmt;
3615 }
3616 if (SimileAjax.WindowManager._dropTargetHighlightElement != null) {
3617 document.body.removeChild(SimileAjax.WindowManager._dropTargetHighlightElement);
3618 SimileAjax.WindowManager._dropTargetHighlightElement = null;
3619 }
3620 if (SimileAjax.WindowManager._draggingModeIndicatorElmt != null) {
3621 document.body.removeChild(SimileAjax.WindowManager._draggingModeIndicatorElmt);
3622 SimileAjax.WindowManager._draggingModeIndicatorElmt = null;
3623 }
3624
3625 SimileAjax.WindowManager._draggedElement = null;
3626 SimileAjax.WindowManager._draggedElementCallback = null;
3627 SimileAjax.WindowManager._potentialDropTarget = null;
3628 SimileAjax.WindowManager._dropTargetHighlightElement = null;
3629 SimileAjax.WindowManager._lastCoords = null;
3630 SimileAjax.WindowManager._ghostCoords = null;
3631 SimileAjax.WindowManager._draggingMode = "";
3632 SimileAjax.WindowManager._dragging = false;
3633 };
3634
3635 SimileAjax.WindowManager._findDropTarget = function(elmt) {
3636 while (elmt != null) {
3637 if ("ondrop" in elmt && (typeof elmt.ondrop) == "function") {
3638 break;
3639 }
3640 elmt = elmt.parentNode;
3641 }
3642 return elmt;
3643 };
3644 /*==================================================
3645 *
3646 * Timeline API
3647 * ------------
3648 *
3649 * This file will load all the Javascript files
3650 * necessary to make the standard timeline work.
3651 * It also detects the default locale.
3652 *
3653 * To run Timeline directly from the www.simile-widgets.org server
3654 * include this fragment in your HTML file as follows:
3655 *
3656 * <script src="http://api.simile-widgets.org/timeline/2.3.1/timeline-api.js"
3657 * type="text/javascript"></script>
3658 *
3659 *
3660 * To host the Timeline files on your own server:
3661 *
3662 * 1) Install the Timeline files onto your webserver using
3663 * the minimal distribution "timeline_<version>_minimal.(zip|tar.gz)" found at
3664 *
3665 * http://code.google.com/p/simile-widgets/downloads/list
3666 *
3667 * 2) Set the following global js variable used to send parameters to this script:
3668 * var Timeline_ajax_url -- URL for simile-ajax-api.js
3669 * var Timeline_urlPrefix -- URL for the *directory* that contains timeline-api.js
3670 * on your web site (including the trailing slash!)
3671 *
3672 * eg your HTML page would include
3673 *
3674 * <script>
3675 * var Timeline_ajax_url="http://YOUR_SERVER/apis/timeline/ajax/simile-ajax-api.js";
3676 * var Timeline_urlPrefix='http://YOUR_SERVER/apis/timeline/';
3677 * </script>
3678 * <script src="http://YOUR_SERVER/javascripts/timeline/timeline-api.js"
3679 * type="text/javascript">
3680 * </script>
3681 *
3682 * SCRIPT PARAMETERS
3683 *
3684 * This script auto-magically figures out locale and has defaults for other parameters
3685 * To set parameters explicity, set js global variable Timeline_parameters or include as
3686 * parameters on the url using GET style. Eg the two next lines pass the same parameters:
3687 * Timeline_parameters='bundle=true'; // pass parameter via js variable
3688 * <script src="http://....timeline-api.js?bundle=true" // pass parameter via url
3689 *
3690 * Parameters
3691 * timeline-use-local-resources --
3692 * bundle -- true: use the single js bundle file; false: load individual files (for debugging)
3693 * locales --
3694 * defaultLocale --
3695 * forceLocale -- force locale to be a particular value--used for debugging. Normally locale is determined
3696 * by browser's and server's locale settings.
3697 *
3698 * DEBUGGING
3699 *
3700 * If you have a problem with Timeline, the first step is to use the unbundled Javascript files. To do so:
3701 * To use the unbundled Timeline and Ajax libraries
3702 * Change
3703 * <script src="http://api.simile-widgets.org/timeline/2.3.1/api/timeline-api.js?bundle=true" type="text/javascript"></script>
3704 * To
3705 * <script>var Timeline_ajax_url = "http://api.simile-widgets.org/ajax/2.2.1/simile-ajax-api.js?bundle=false"</script>
3706 * <script src="http://api.simile-widgets.org/timeline/2.3.1/api/timeline-api.js?bundle=false" type="text/javascript"></script>
3707 *
3708 * Note that the Ajax version is usually NOT the same as the Timeline version.
3709 * See variable simile_ajax_ver below for the current version
3710 *
3711 *==================================================
3712 */
3713
3714 (function() {
3715
3716 var simile_ajax_ver = "2.2.1"; // ===========>>> current Simile-Ajax version
3717
3718 var useLocalResources = false;
3719 if (document.location.search.length > 0) {
3720 var params = document.location.search.substr(1).split("&");
3721 for (var i = 0; i < params.length; i++) {
3722 if (params[i] == "timeline-use-local-resources") {
3723 useLocalResources = true;
3724 }
3725 }
3726 };
3727
3728 var loadMe = function() {
3729 if ("Timeline" in window) {
3730 return;
3731 }
3732
3733 window.Timeline = new Object();
3734 window.Timeline.DateTime = window.SimileAjax.DateTime; // for backward compatibility
3735
3736 var bundle = false;
3737 var javascriptFiles = [
3738 "timeline.js",
3739 "band.js",
3740 "themes.js",
3741 "ethers.js",
3742 "ether-painters.js",
3743 "event-utils.js",
3744 "labellers.js",
3745 "sources.js",
3746 "original-painter.js",
3747 "detailed-painter.js",
3748 "overview-painter.js",
3749 "compact-painter.js",
3750 "decorators.js",
3751 "units.js"
3752 ];
3753 var cssFiles = [
3754 "timeline.css",
3755 "ethers.css",
3756 "events.css"
3757 ];
3758
3759 var localizedJavascriptFiles = [
3760 "timeline.js",
3761 "labellers.js"
3762 ];
3763 var localizedCssFiles = [
3764 ];
3765
3766 // ISO-639 language codes, ISO-3166 country codes (2 characters)
3767 var supportedLocales = [
3768 "cs", // Czech
3769 "de", // German
3770 "en", // English
3771 "es", // Spanish
3772 "fr", // French
3773 "it", // Italian
3774 "nl", // Dutch (The Netherlands)
3775 "ru", // Russian
3776 "se", // Swedish
3777 "tr", // Turkish
3778 "vi", // Vietnamese
3779 "zh" // Chinese
3780 ];
3781
3782 try {
3783 var desiredLocales = [ "en" ],
3784 defaultServerLocale = "en",
3785 forceLocale = null;
3786
3787 var parseURLParameters = function(parameters) {
3788 var params = parameters.split("&");
3789 for (var p = 0; p < params.length; p++) {
3790 var pair = params[p].split("=");
3791 if (pair[0] == "locales") {
3792 desiredLocales = desiredLocales.concat(pair[1].split(","));
3793 } else if (pair[0] == "defaultLocale") {
3794 defaultServerLocale = pair[1];
3795 } else if (pair[0] == "forceLocale") {
3796 forceLocale = pair[1];
3797 desiredLocales = desiredLocales.concat(pair[1].split(","));
3798 } else if (pair[0] == "bundle") {
3799 bundle = pair[1] != "false";
3800 }
3801 }
3802 };
3803
3804 (function() {
3805 if (typeof Timeline_urlPrefix == "string") {
3806 Timeline.urlPrefix = Timeline_urlPrefix;
3807 if (typeof Timeline_parameters == "string") {
3808 parseURLParameters(Timeline_parameters);
3809 }
3810 } else {
3811 var heads = document.documentElement.getElementsByTagName("head");
3812 for (var h = 0; h < heads.length; h++) {
3813 var scripts = heads[h].getElementsByTagName("script");
3814 for (var s = 0; s < scripts.length; s++) {
3815 var url = scripts[s].src;
3816 var i = url.indexOf("timeplot-complete.js");
3817 if (i >= 0) {
3818 Timeline.urlPrefix = url.substr(0, i);
3819 var q = url.indexOf("?");
3820 if (q > 0) {
3821 parseURLParameters(url.substr(q + 1));
3822 }
3823 return;
3824 }
3825 }
3826 }
3827 throw new Error("Failed to derive URL prefix for Timeline API code files");
3828 }
3829 })();
3830
3831 var includeJavascriptFiles = function(urlPrefix, filenames) {
3832 SimileAjax.includeJavascriptFiles(document, urlPrefix, filenames);
3833 }
3834 var includeCssFiles = function(urlPrefix, filenames) {
3835 SimileAjax.includeCssFiles(document, urlPrefix, filenames);
3836 }
3837
3838 /*
3839 * Include non-localized files
3840 */
3841 if (bundle) {
3842 includeJavascriptFiles(Timeline.urlPrefix, [ "timeline-bundle.js" ]);
3843 includeCssFiles(Timeline.urlPrefix, [ "timeline-bundle.css" ]);
3844 } else {
3845 includeJavascriptFiles(Timeline.urlPrefix + "scripts/", javascriptFiles);
3846 includeCssFiles(Timeline.urlPrefix + "styles/", cssFiles);
3847 }
3848
3849 /*
3850 * Include localized files
3851 */
3852 var loadLocale = [];
3853 loadLocale[defaultServerLocale] = true;
3854
3855 var tryExactLocale = function(locale) {
3856 for (var l = 0; l < supportedLocales.length; l++) {
3857 if (locale == supportedLocales[l]) {
3858 loadLocale[locale] = true;
3859 return true;
3860 }
3861 }
3862 return false;
3863 }
3864 var tryLocale = function(locale) {
3865 if (tryExactLocale(locale)) {
3866 return locale;
3867 }
3868
3869 var dash = locale.indexOf("-");
3870 if (dash > 0 && tryExactLocale(locale.substr(0, dash))) {
3871 return locale.substr(0, dash);
3872 }
3873
3874 return null;
3875 }
3876
3877 for (var l = 0; l < desiredLocales.length; l++) {
3878 tryLocale(desiredLocales[l]);
3879 }
3880
3881 var defaultClientLocale = defaultServerLocale;
3882 var defaultClientLocales = ("language" in navigator ? navigator.language : navigator.browserLanguage).split(";");
3883 for (var l = 0; l < defaultClientLocales.length; l++) {
3884 var locale = tryLocale(defaultClientLocales[l]);
3885 if (locale != null) {
3886 defaultClientLocale = locale;
3887 break;
3888 }
3889 }
3890
3891 for (var l = 0; l < supportedLocales.length; l++) {
3892 var locale = supportedLocales[l];
3893 if (loadLocale[locale]) {
3894 includeJavascriptFiles(Timeline.urlPrefix + "scripts/l10n/" + locale + "/", localizedJavascriptFiles);
3895 includeCssFiles(Timeline.urlPrefix + "styles/l10n/" + locale + "/", localizedCssFiles);
3896 }
3897 }
3898
3899 if (forceLocale == null) {
3900 Timeline.serverLocale = defaultServerLocale;
3901 Timeline.clientLocale = defaultClientLocale;
3902 } else {
3903 Timeline.serverLocale = forceLocale;
3904 Timeline.clientLocale = forceLocale;
3905 }
3906 } catch (e) {
3907 alert(e);
3908 }
3909 };
3910
3911 /*
3912 * Load SimileAjax if it's not already loaded
3913 */
3914 if (typeof SimileAjax == "undefined") {
3915 window.SimileAjax_onLoad = loadMe;
3916
3917 var url = useLocalResources ?
3918 "http://127.0.0.1:9999/ajax/api/simile-ajax-api.js?bundle=false" :
3919 "http://api.simile-widgets.org/ajax/" + simile_ajax_ver + "/simile-ajax-api.js";
3920 if (typeof Timeline_ajax_url == "string") {
3921 url = Timeline_ajax_url;
3922 }
3923 var createScriptElement = function() {
3924 var script = document.createElement("script");
3925 script.type = "text/javascript";
3926 script.language = "JavaScript";
3927 script.src = url;
3928 document.getElementsByTagName("head")[0].appendChild(script);
3929 }
3930 if (document.body == null) {
3931 try {
3932 document.write("<script src='" + url + "' type='text/javascript'></script>");
3933 } catch (e) {
3934 createScriptElement();
3935 }
3936 } else {
3937 createScriptElement();
3938 }
3939 } else {
3940 loadMe();
3941 }
3942 })();
3943 /*=================================================
3944 *
3945 * Coding standards:
3946 *
3947 * We aim towards Douglas Crockford's Javascript conventions.
3948 * See: http://javascript.crockford.com/code.html
3949 * See also: http://www.crockford.com/javascript/javascript.html
3950 *
3951 * That said, this JS code was written before some recent JS
3952 * support libraries became widely used or available.
3953 * In particular, the _ character is used to indicate a class function or
3954 * variable that should be considered private to the class.
3955 *
3956 * The code mostly uses accessor methods for getting/setting the private
3957 * class variables.
3958 *
3959 * Over time, we'd like to formalize the convention by using support libraries
3960 * which enforce privacy in objects.
3961 *
3962 * We also want to use jslint: http://www.jslint.com/
3963 *
3964 *
3965 *==================================================
3966 */
3967
3968
3969
3970 /*==================================================
3971 * Timeline VERSION
3972 *==================================================
3973 */
3974 // Note: version is also stored in the build.xml file
3975 Timeline.version = 'pre 2.4.0'; // use format 'pre 1.2.3' for trunk versions
3976 Timeline.ajax_lib_version = SimileAjax.version;
3977 Timeline.display_version = Timeline.version + ' (with Ajax lib ' + Timeline.ajax_lib_version + ')';
3978 // cf method Timeline.writeVersion
3979
3980 /*==================================================
3981 * Timeline
3982 *==================================================
3983 */
3984 Timeline.strings = {}; // localization string tables
3985 Timeline.HORIZONTAL = 0;
3986 Timeline.VERTICAL = 1;
3987 Timeline._defaultTheme = null;
3988
3989 Timeline.getDefaultLocale = function() {
3990 return Timeline.clientLocale;
3991 };
3992
3993 Timeline.create = function(elmt, bandInfos, orientation, unit) {
3994 if (Timeline.timelines == null) {
3995 Timeline.timelines = [];
3996 // Timeline.timelines array can have null members--Timelines that
3997 // once existed on the page, but were later disposed of.
3998 }
3999
4000 var timelineID = Timeline.timelines.length;
4001 Timeline.timelines[timelineID] = null; // placeholder until we have the object
4002 var new_tl = new Timeline._Impl(elmt, bandInfos, orientation, unit,
4003 timelineID);
4004 Timeline.timelines[timelineID] = new_tl;
4005 return new_tl;
4006 };
4007
4008 Timeline.createBandInfo = function(params) {
4009 var theme = ("theme" in params) ? params.theme : Timeline.getDefaultTheme();
4010
4011 var eventSource = ("eventSource" in params) ? params.eventSource : null;
4012
4013 var ether = new Timeline.LinearEther({
4014 centersOn: ("date" in params) ? params.date : new Date(),
4015 interval: SimileAjax.DateTime.gregorianUnitLengths[params.intervalUnit],
4016 pixelsPerInterval: params.intervalPixels,
4017 theme: theme
4018 });
4019
4020 var etherPainter = new Timeline.GregorianEtherPainter({
4021 unit: params.intervalUnit,
4022 multiple: ("multiple" in params) ? params.multiple : 1,
4023 theme: theme,
4024 align: ("align" in params) ? params.align : undefined
4025 });
4026
4027 var eventPainterParams = {
4028 showText: ("showEventText" in params) ? params.showEventText : true,
4029 theme: theme
4030 };
4031 // pass in custom parameters for the event painter
4032 if ("eventPainterParams" in params) {
4033 for (var prop in params.eventPainterParams) {
4034 eventPainterParams[prop] = params.eventPainterParams[prop];
4035 }
4036 }
4037
4038 if ("trackHeight" in params) {
4039 eventPainterParams.trackHeight = params.trackHeight;
4040 }
4041 if ("trackGap" in params) {
4042 eventPainterParams.trackGap = params.trackGap;
4043 }
4044
4045 var layout = ("overview" in params && params.overview) ? "overview" : ("layout" in params ? params.layout : "original");
4046 var eventPainter;
4047 if ("eventPainter" in params) {
4048 eventPainter = new params.eventPainter(eventPainterParams);
4049 } else {
4050 switch (layout) {
4051 case "overview" :
4052 eventPainter = new Timeline.OverviewEventPainter(eventPainterParams);
4053 break;
4054 case "detailed" :
4055 eventPainter = new Timeline.DetailedEventPainter(eventPainterParams);
4056 break;
4057 default:
4058 eventPainter = new Timeline.OriginalEventPainter(eventPainterParams);
4059 }
4060 }
4061
4062 return {
4063 width: params.width,
4064 eventSource: eventSource,
4065 timeZone: ("timeZone" in params) ? params.timeZone : 0,
4066 ether: ether,
4067 etherPainter: etherPainter,
4068 eventPainter: eventPainter,
4069 theme: theme,
4070 zoomIndex: ("zoomIndex" in params) ? params.zoomIndex : 0,
4071 zoomSteps: ("zoomSteps" in params) ? params.zoomSteps : null
4072 };
4073 };
4074
4075 Timeline.createHotZoneBandInfo = function(params) {
4076 var theme = ("theme" in params) ? params.theme : Timeline.getDefaultTheme();
4077
4078 var eventSource = ("eventSource" in params) ? params.eventSource : null;
4079
4080 var ether = new Timeline.HotZoneEther({
4081 centersOn: ("date" in params) ? params.date : new Date(),
4082 interval: SimileAjax.DateTime.gregorianUnitLengths[params.intervalUnit],
4083 pixelsPerInterval: params.intervalPixels,
4084 zones: params.zones,
4085 theme: theme
4086 });
4087
4088 var etherPainter = new Timeline.HotZoneGregorianEtherPainter({
4089 unit: params.intervalUnit,
4090 zones: params.zones,
4091 theme: theme,
4092 align: ("align" in params) ? params.align : undefined
4093 });
4094
4095 var eventPainterParams = {
4096 showText: ("showEventText" in params) ? params.showEventText : true,
4097 theme: theme
4098 };
4099 // pass in custom parameters for the event painter
4100 if ("eventPainterParams" in params) {
4101 for (var prop in params.eventPainterParams) {
4102 eventPainterParams[prop] = params.eventPainterParams[prop];
4103 }
4104 }
4105 if ("trackHeight" in params) {
4106 eventPainterParams.trackHeight = params.trackHeight;
4107 }
4108 if ("trackGap" in params) {
4109 eventPainterParams.trackGap = params.trackGap;
4110 }
4111
4112 var layout = ("overview" in params && params.overview) ? "overview" : ("layout" in params ? params.layout : "original");
4113 var eventPainter;
4114 if ("eventPainter" in params) {
4115 eventPainter = new params.eventPainter(eventPainterParams);
4116 } else {
4117 switch (layout) {
4118 case "overview" :
4119 eventPainter = new Timeline.OverviewEventPainter(eventPainterParams);
4120 break;
4121 case "detailed" :
4122 eventPainter = new Timeline.DetailedEventPainter(eventPainterParams);
4123 break;
4124 default:
4125 eventPainter = new Timeline.OriginalEventPainter(eventPainterParams);
4126 }
4127 }
4128 return {
4129 width: params.width,
4130 eventSource: eventSource,
4131 timeZone: ("timeZone" in params) ? params.timeZone : 0,
4132 ether: ether,
4133 etherPainter: etherPainter,
4134 eventPainter: eventPainter,
4135 theme: theme,
4136 zoomIndex: ("zoomIndex" in params) ? params.zoomIndex : 0,
4137 zoomSteps: ("zoomSteps" in params) ? params.zoomSteps : null
4138 };
4139 };
4140
4141 Timeline.getDefaultTheme = function() {
4142 if (Timeline._defaultTheme == null) {
4143 Timeline._defaultTheme = Timeline.ClassicTheme.create(Timeline.getDefaultLocale());
4144 }
4145 return Timeline._defaultTheme;
4146 };
4147
4148 Timeline.setDefaultTheme = function(theme) {
4149 Timeline._defaultTheme = theme;
4150 };
4151
4152 Timeline.loadXML = function(url, f) {
4153 var fError = function(statusText, status, xmlhttp) {
4154 alert("Failed to load data xml from " + url + "\n" + statusText);
4155 };
4156 var fDone = function(xmlhttp) {
4157 var xml = xmlhttp.responseXML;
4158 if (!xml.documentElement && xmlhttp.responseStream) {
4159 xml.load(xmlhttp.responseStream);
4160 }
4161 f(xml, url);
4162 };
4163 SimileAjax.XmlHttp.get(url, fError, fDone);
4164 };
4165
4166
4167 Timeline.loadJSON = function(url, f) {
4168 var fError = function(statusText, status, xmlhttp) {
4169 alert("Failed to load json data from " + url + "\n" + statusText);
4170 };
4171 var fDone = function(xmlhttp) {
4172 f(eval('(' + xmlhttp.responseText + ')'), url);
4173 };
4174 SimileAjax.XmlHttp.get(url, fError, fDone);
4175 };
4176
4177 Timeline.getTimelineFromID = function(timelineID) {
4178 return Timeline.timelines[timelineID];
4179 };
4180
4181 // Write the current Timeline version as the contents of element with id el_id
4182 Timeline.writeVersion = function(el_id) {
4183 document.getElementById(el_id).innerHTML = this.display_version;
4184 };
4185
4186
4187
4188 /*==================================================
4189 * Timeline Implementation object
4190 *==================================================
4191 */
4192 Timeline._Impl = function(elmt, bandInfos, orientation, unit, timelineID) {
4193 SimileAjax.WindowManager.initialize();
4194
4195 this._containerDiv = elmt;
4196
4197 this._bandInfos = bandInfos;
4198 this._orientation = orientation == null ? Timeline.HORIZONTAL : orientation;
4199 this._unit = (unit != null) ? unit : SimileAjax.NativeDateUnit;
4200 this._starting = true; // is the Timeline being created? Used by autoWidth
4201 // functions
4202 this._autoResizing = false;
4203
4204 // autoWidth is a "public" property of the Timeline object
4205 this.autoWidth = bandInfos && bandInfos[0] && bandInfos[0].theme &&
4206 bandInfos[0].theme.autoWidth;
4207 this.autoWidthAnimationTime = bandInfos && bandInfos[0] && bandInfos[0].theme &&
4208 bandInfos[0].theme.autoWidthAnimationTime;
4209 this.timelineID = timelineID; // also public attribute
4210 this.timeline_start = bandInfos && bandInfos[0] && bandInfos[0].theme &&
4211 bandInfos[0].theme.timeline_start;
4212 this.timeline_stop = bandInfos && bandInfos[0] && bandInfos[0].theme &&
4213 bandInfos[0].theme.timeline_stop;
4214 this.timeline_at_start = false; // already at start or stop? Then won't
4215 this.timeline_at_stop = false; // try to move further in the wrong direction
4216
4217 this._initialize();
4218 };
4219
4220 //
4221 // Public functions used by client sw
4222 //
4223 Timeline._Impl.prototype.dispose = function() {
4224 for (var i = 0; i < this._bands.length; i++) {
4225 this._bands[i].dispose();
4226 }
4227 this._bands = null;
4228 this._bandInfos = null;
4229 this._containerDiv.innerHTML = "";
4230 // remove from array of Timelines
4231 Timeline.timelines[this.timelineID] = null;
4232 };
4233
4234 Timeline._Impl.prototype.getBandCount = function() {
4235 return this._bands.length;
4236 };
4237
4238 Timeline._Impl.prototype.getBand = function(index) {
4239 return this._bands[index];
4240 };
4241
4242 Timeline._Impl.prototype.finishedEventLoading = function() {
4243 // Called by client after events have been loaded into Timeline
4244 // Only used if the client has set autoWidth
4245 // Sets width to Timeline's requested amount and will shrink down the div if
4246 // need be.
4247 this._autoWidthCheck(true);
4248 this._starting = false;
4249 };
4250
4251 Timeline._Impl.prototype.layout = function() {
4252 // called by client when browser is resized
4253 this._autoWidthCheck(true);
4254 this._distributeWidths();
4255 };
4256
4257 Timeline._Impl.prototype.paint = function() {
4258 for (var i = 0; i < this._bands.length; i++) {
4259 this._bands[i].paint();
4260 }
4261 };
4262
4263 Timeline._Impl.prototype.getDocument = function() {
4264 return this._containerDiv.ownerDocument;
4265 };
4266
4267 Timeline._Impl.prototype.addDiv = function(div) {
4268 this._containerDiv.appendChild(div);
4269 };
4270
4271 Timeline._Impl.prototype.removeDiv = function(div) {
4272 this._containerDiv.removeChild(div);
4273 };
4274
4275 Timeline._Impl.prototype.isHorizontal = function() {
4276 return this._orientation == Timeline.HORIZONTAL;
4277 };
4278
4279 Timeline._Impl.prototype.isVertical = function() {
4280 return this._orientation == Timeline.VERTICAL;
4281 };
4282
4283 Timeline._Impl.prototype.getPixelLength = function() {
4284 return this._orientation == Timeline.HORIZONTAL ?
4285 this._containerDiv.offsetWidth : this._containerDiv.offsetHeight;
4286 };
4287
4288 Timeline._Impl.prototype.getPixelWidth = function() {
4289 return this._orientation == Timeline.VERTICAL ?
4290 this._containerDiv.offsetWidth : this._containerDiv.offsetHeight;
4291 };
4292
4293 Timeline._Impl.prototype.getUnit = function() {
4294 return this._unit;
4295 };
4296
4297 Timeline._Impl.prototype.getWidthStyle = function() {
4298 // which element.style attribute should be changed to affect Timeline's "width"
4299 return this._orientation == Timeline.HORIZONTAL ? 'height' : 'width';
4300 };
4301
4302 Timeline._Impl.prototype.loadXML = function(url, f) {
4303 var tl = this;
4304
4305
4306 var fError = function(statusText, status, xmlhttp) {
4307 alert("Failed to load data xml from " + url + "\n" + statusText);
4308 tl.hideLoadingMessage();
4309 };
4310 var fDone = function(xmlhttp) {
4311 try {
4312 var xml = xmlhttp.responseXML;
4313 if (!xml.documentElement && xmlhttp.responseStream) {
4314 xml.load(xmlhttp.responseStream);
4315 }
4316 f(xml, url);
4317 } finally {
4318 tl.hideLoadingMessage();
4319 }
4320 };
4321
4322 this.showLoadingMessage();
4323 window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
4324 };
4325
4326 Timeline._Impl.prototype.loadJSON = function(url, f) {
4327 var tl = this;
4328
4329 var fError = function(statusText, status, xmlhttp) {
4330 alert("Failed to load json data from " + url + "\n" + statusText);
4331 tl.hideLoadingMessage();
4332 };
4333 var fDone = function(xmlhttp) {
4334 try {
4335 f(eval('(' + xmlhttp.responseText + ')'), url);
4336 } finally {
4337 tl.hideLoadingMessage();
4338 }
4339 };
4340
4341 this.showLoadingMessage();
4342 window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
4343 };
4344
4345
4346 //
4347 // Private functions used by Timeline object functions
4348 //
4349
4350 Timeline._Impl.prototype._autoWidthScrollListener = function(band) {
4351 band.getTimeline()._autoWidthCheck(false);
4352 };
4353
4354 // called to re-calculate auto width and adjust the overall Timeline div if needed
4355 Timeline._Impl.prototype._autoWidthCheck = function(okToShrink) {
4356 var timeline = this; // this Timeline
4357 var immediateChange = timeline._starting;
4358 var newWidth = 0;
4359
4360 function changeTimelineWidth() {
4361 var widthStyle = timeline.getWidthStyle();
4362 if (immediateChange) {
4363 timeline._containerDiv.style[widthStyle] = newWidth + 'px';
4364 } else {
4365 // animate change
4366 timeline._autoResizing = true;
4367 var animateParam ={};
4368 animateParam[widthStyle] = newWidth + 'px';
4369
4370 SimileAjax.jQuery(timeline._containerDiv).animate(
4371 animateParam, timeline.autoWidthAnimationTime,
4372 'linear', function(){timeline._autoResizing = false;});
4373 }
4374 }
4375
4376 function checkTimelineWidth() {
4377 var targetWidth = 0; // the new desired width
4378 var currentWidth = timeline.getPixelWidth();
4379
4380 if (timeline._autoResizing) {
4381 return; // early return
4382 }
4383
4384 // compute targetWidth
4385 for (var i = 0; i < timeline._bands.length; i++) {
4386 timeline._bands[i].checkAutoWidth();
4387 targetWidth += timeline._bandInfos[i].width;
4388 }
4389
4390 if (targetWidth > currentWidth || okToShrink) {
4391 // yes, let's change the size
4392 newWidth = targetWidth;
4393 changeTimelineWidth();
4394 timeline._distributeWidths();
4395 }
4396 }
4397
4398 // function's mainline
4399 if (!timeline.autoWidth) {
4400 return; // early return
4401 }
4402
4403 checkTimelineWidth();
4404 };
4405
4406 Timeline._Impl.prototype._initialize = function() {
4407 var containerDiv = this._containerDiv;
4408 var doc = containerDiv.ownerDocument;
4409
4410 containerDiv.className =
4411 containerDiv.className.split(" ").concat("timeline-container").join(" ");
4412
4413 /*
4414 * Set css-class on container div that will define orientation
4415 */
4416 var orientation = (this.isHorizontal()) ? 'horizontal' : 'vertical'
4417 containerDiv.className +=' timeline-'+orientation;
4418
4419
4420 while (containerDiv.firstChild) {
4421 containerDiv.removeChild(containerDiv.firstChild);
4422 }
4423
4424 /*
4425 * inserting copyright and link
4426 */
4427 var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeline.urlPrefix + (this.isHorizontal() ? "images/copyright-vertical.png" : "images/copyright.png"));
4428 elmtCopyright.className = "timeline-copyright";
4429 elmtCopyright.title = "SIMILE Timeline - http://www.simile-widgets.org/";
4430 SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://www.simile-widgets.org/"; });
4431 containerDiv.appendChild(elmtCopyright);
4432
4433 /*
4434 * creating bands
4435 */
4436 this._bands = [];
4437 for (var i = 0; i < this._bandInfos.length; i++) {
4438 var band = new Timeline._Band(this, this._bandInfos[i], i);
4439 this._bands.push(band);
4440 }
4441 this._distributeWidths();
4442
4443 /*
4444 * sync'ing bands
4445 */
4446 for (var i = 0; i < this._bandInfos.length; i++) {
4447 var bandInfo = this._bandInfos[i];
4448 if ("syncWith" in bandInfo) {
4449 this._bands[i].setSyncWithBand(
4450 this._bands[bandInfo.syncWith],
4451 ("highlight" in bandInfo) ? bandInfo.highlight : false
4452 );
4453 }
4454 }
4455
4456
4457 if (this.autoWidth) {
4458 for (var i = 0; i < this._bands.length; i++) {
4459 this._bands[i].addOnScrollListener(this._autoWidthScrollListener);
4460 }
4461 }
4462
4463
4464 /*
4465 * creating loading UI
4466 */
4467 var message = SimileAjax.Graphics.createMessageBubble(doc);
4468 message.containerDiv.className = "timeline-message-container";
4469 containerDiv.appendChild(message.containerDiv);
4470
4471 message.contentDiv.className = "timeline-message";
4472 message.contentDiv.innerHTML = "<img src='" + Timeline.urlPrefix + "images/progress-running.gif' /> Loading...";
4473
4474 this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; };
4475 this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; };
4476 };
4477
4478 Timeline._Impl.prototype._distributeWidths = function() {
4479 var length = this.getPixelLength();
4480 var width = this.getPixelWidth();
4481 var cumulativeWidth = 0;
4482
4483 for (var i = 0; i < this._bands.length; i++) {
4484 var band = this._bands[i];
4485 var bandInfos = this._bandInfos[i];
4486 var widthString = bandInfos.width;
4487 var bandWidth;
4488
4489 if (typeof widthString == 'string') {
4490 var x = widthString.indexOf("%");
4491 if (x > 0) {
4492 var percent = parseInt(widthString.substr(0, x));
4493 bandWidth = Math.round(percent * width / 100);
4494 } else {
4495 bandWidth = parseInt(widthString);
4496 }
4497 } else {
4498 // was given an integer
4499 bandWidth = widthString;
4500 }
4501
4502 band.setBandShiftAndWidth(cumulativeWidth, bandWidth);
4503 band.setViewLength(length);
4504
4505 cumulativeWidth += bandWidth;
4506 }
4507 };
4508
4509 Timeline._Impl.prototype.shiftOK = function(index, shift) {
4510 // Returns true if the proposed shift is ok
4511 //
4512 // Positive shift means going back in time
4513 var going_back = shift > 0,
4514 going_forward = shift < 0;
4515
4516 // Is there an edge?
4517 if ((going_back && this.timeline_start == null) ||
4518 (going_forward && this.timeline_stop == null) ||
4519 (shift == 0)) {
4520 return (true); // early return
4521 }
4522
4523 // If any of the bands has noted that it is changing the others,
4524 // then this shift is a secondary shift in reaction to the real shift,
4525 // which already happened. In such cases, ignore it. (The issue is
4526 // that a positive original shift can cause a negative secondary shift,
4527 // as the bands adjust.)
4528 var secondary_shift = false;
4529 for (var i = 0; i < this._bands.length && !secondary_shift; i++) {
4530 secondary_shift = this._bands[i].busy();
4531 }
4532 if (secondary_shift) {
4533 return(true); // early return
4534 }
4535
4536 // If we are already at an edge, then don't even think about going any further
4537 if ((going_back && this.timeline_at_start) ||
4538 (going_forward && this.timeline_at_stop)) {
4539 return (false); // early return
4540 }
4541
4542 // Need to check all the bands
4543 var ok = false; // return value
4544 // If any of the bands will be or are showing an ok date, then let the shift proceed.
4545 for (var i = 0; i < this._bands.length && !ok; i++) {
4546 var band = this._bands[i];
4547 if (going_back) {
4548 ok = (i == index ? band.getMinVisibleDateAfterDelta(shift) : band.getMinVisibleDate())
4549 >= this.timeline_start;
4550 } else {
4551 ok = (i == index ? band.getMaxVisibleDateAfterDelta(shift) : band.getMaxVisibleDate())
4552 <= this.timeline_stop;
4553 }
4554 }
4555
4556 // process results
4557 if (going_back) {
4558 this.timeline_at_start = !ok;
4559 this.timeline_at_stop = false;
4560 } else {
4561 this.timeline_at_stop = !ok;
4562 this.timeline_at_start = false;
4563 }
4564 // This is where you could have an effect once per hitting an
4565 // edge of the Timeline. Eg jitter the Timeline
4566 //if (!ok) {
4567 //alert(going_back ? "At beginning" : "At end");
4568 //}
4569 return (ok);
4570 };
4571
4572 Timeline._Impl.prototype.zoom = function (zoomIn, x, y, target) {
4573 var matcher = new RegExp("^timeline-band-([0-9]+)$");
4574 var bandIndex = null;
4575
4576 var result = matcher.exec(target.id);
4577 if (result) {
4578 bandIndex = parseInt(result[1]);
4579 }
4580
4581 if (bandIndex != null) {
4582 this._bands[bandIndex].zoom(zoomIn, x, y, target);
4583 }
4584
4585 this.paint();
4586 };
4587
4588 /*=================================================
4589 *
4590 * Coding standards:
4591 *
4592 * We aim towards Douglas Crockford's Javascript conventions.
4593 * See: http://javascript.crockford.com/code.html
4594 * See also: http://www.crockford.com/javascript/javascript.html
4595 *
4596 * That said, this JS code was written before some recent JS
4597 * support libraries became widely used or available.
4598 * In particular, the _ character is used to indicate a class function or
4599 * variable that should be considered private to the class.
4600 *
4601 * The code mostly uses accessor methods for getting/setting the private
4602 * class variables.
4603 *
4604 * Over time, we'd like to formalize the convention by using support libraries
4605 * which enforce privacy in objects.
4606 *
4607 * We also want to use jslint: http://www.jslint.com/
4608 *
4609 *
4610 *==================================================
4611 */
4612
4613
4614
4615 /*==================================================
4616 * Band
4617 *==================================================
4618 */
4619 Timeline._Band = function(timeline, bandInfo, index) {
4620 // Set up the band's object
4621
4622 // Munge params: If autoWidth is on for the Timeline, then ensure that
4623 // bandInfo.width is an integer
4624 if (timeline.autoWidth && typeof bandInfo.width == 'string') {
4625 bandInfo.width = bandInfo.width.indexOf("%") > -1 ? 0 : parseInt(bandInfo.width);
4626 }
4627
4628 this._timeline = timeline;
4629 this._bandInfo = bandInfo;
4630
4631 this._index = index;
4632
4633 this._locale = ("locale" in bandInfo) ? bandInfo.locale : Timeline.getDefaultLocale();
4634 this._timeZone = ("timeZone" in bandInfo) ? bandInfo.timeZone : 0;
4635 this._labeller = ("labeller" in bandInfo) ? bandInfo.labeller :
4636 (("createLabeller" in timeline.getUnit()) ?
4637 timeline.getUnit().createLabeller(this._locale, this._timeZone) :
4638 new Timeline.GregorianDateLabeller(this._locale, this._timeZone));
4639 this._theme = bandInfo.theme;
4640 this._zoomIndex = ("zoomIndex" in bandInfo) ? bandInfo.zoomIndex : 0;
4641 this._zoomSteps = ("zoomSteps" in bandInfo) ? bandInfo.zoomSteps : null;
4642
4643 this._dragging = false;
4644 this._changing = false;
4645 this._originalScrollSpeed = 5; // pixels
4646 this._scrollSpeed = this._originalScrollSpeed;
4647 this._viewOrthogonalOffset = 0; // vertical offset if the timeline is horizontal, and vice versa
4648 this._onScrollListeners = [];
4649 this._onOrthogonalScrollListeners = [];
4650
4651 var b = this;
4652 this._syncWithBand = null;
4653 this._syncWithBandHandler = function(band) {
4654 b._onHighlightBandScroll();
4655 };
4656 this._syncWithBandOrthogonalScrollHandler = function(band) {
4657 b._onHighlightBandOrthogonalScroll();
4658 };
4659 this._selectorListener = function(band) {
4660 b._onHighlightBandScroll();
4661 };
4662
4663 /*
4664 * Install a textbox to capture keyboard events
4665 */
4666 var inputDiv = this._timeline.getDocument().createElement("div");
4667 inputDiv.className = "timeline-band-input";
4668 this._timeline.addDiv(inputDiv);
4669
4670 this._keyboardInput = document.createElement("input");
4671 this._keyboardInput.type = "text";
4672 inputDiv.appendChild(this._keyboardInput);
4673 SimileAjax.DOM.registerEventWithObject(this._keyboardInput, "keydown", this, "_onKeyDown");
4674 SimileAjax.DOM.registerEventWithObject(this._keyboardInput, "keyup", this, "_onKeyUp");
4675
4676 /*
4677 * The band's outer most div that slides with respect to the timeline's div
4678 */
4679 this._div = this._timeline.getDocument().createElement("div");
4680 this._div.id = "timeline-band-" + index;
4681 this._div.className = "timeline-band timeline-band-" + index;
4682 this._timeline.addDiv(this._div);
4683
4684 SimileAjax.DOM.registerEventWithObject(this._div, "mousedown", this, "_onMouseDown");
4685 SimileAjax.DOM.registerEventWithObject(this._div, "mousemove", this, "_onMouseMove");
4686 SimileAjax.DOM.registerEventWithObject(this._div, "mouseup", this, "_onMouseUp");
4687 SimileAjax.DOM.registerEventWithObject(this._div, "mouseout", this, "_onMouseOut");
4688 SimileAjax.DOM.registerEventWithObject(this._div, "dblclick", this, "_onDblClick");
4689
4690 var mouseWheel = this._theme!= null ? this._theme.mouseWheel : 'scroll'; // theme is not always defined
4691 if (mouseWheel === 'zoom' || mouseWheel === 'scroll' || this._zoomSteps) {
4692 // capture mouse scroll
4693 if (SimileAjax.Platform.browser.isFirefox) {
4694 SimileAjax.DOM.registerEventWithObject(this._div, "DOMMouseScroll", this, "_onMouseScroll");
4695 } else {
4696 SimileAjax.DOM.registerEventWithObject(this._div, "mousewheel", this, "_onMouseScroll");
4697 }
4698 }
4699
4700 /*
4701 * The inner div that contains layers
4702 */
4703 this._innerDiv = this._timeline.getDocument().createElement("div");
4704 this._innerDiv.className = "timeline-band-inner";
4705 this._div.appendChild(this._innerDiv);
4706
4707 /*
4708 * Initialize parts of the band
4709 */
4710 this._ether = bandInfo.ether;
4711 bandInfo.ether.initialize(this, timeline);
4712
4713 this._etherPainter = bandInfo.etherPainter;
4714 bandInfo.etherPainter.initialize(this, timeline);
4715
4716 this._eventSource = bandInfo.eventSource;
4717 if (this._eventSource) {
4718 this._eventListener = {
4719 onAddMany: function() { b._onAddMany(); },
4720 onClear: function() { b._onClear(); }
4721 }
4722 this._eventSource.addListener(this._eventListener);
4723 }
4724
4725 this._eventPainter = bandInfo.eventPainter;
4726 this._eventTracksNeeded = 0; // set by painter via updateEventTrackInfo
4727 this._eventTrackIncrement = 0;
4728 bandInfo.eventPainter.initialize(this, timeline);
4729
4730 this._decorators = ("decorators" in bandInfo) ? bandInfo.decorators : [];
4731 for (var i = 0; i < this._decorators.length; i++) {
4732 this._decorators[i].initialize(this, timeline);
4733 }
4734
4735 this._supportsOrthogonalScrolling = ("supportsOrthogonalScrolling" in this._eventPainter) &&
4736 this._eventPainter.supportsOrthogonalScrolling();
4737
4738 if (this._supportsOrthogonalScrolling) {
4739 this._scrollBar = this._timeline.getDocument().createElement("div");
4740 this._scrollBar.id = "timeline-band-scrollbar-" + index;
4741 this._scrollBar.className = "timeline-band-scrollbar";
4742 this._timeline.addDiv(this._scrollBar);
4743
4744 this._scrollBar.innerHTML = '<div class="timeline-band-scrollbar-thumb"> </div>'
4745 }
4746 };
4747
4748 Timeline._Band.SCROLL_MULTIPLES = 5;
4749
4750 Timeline._Band.prototype.dispose = function() {
4751 this.closeBubble();
4752
4753 if (this._eventSource) {
4754 this._eventSource.removeListener(this._eventListener);
4755 this._eventListener = null;
4756 this._eventSource = null;
4757 }
4758
4759 this._timeline = null;
4760 this._bandInfo = null;
4761
4762 this._labeller = null;
4763 this._ether = null;
4764 this._etherPainter = null;
4765 this._eventPainter = null;
4766 this._decorators = null;
4767
4768 this._onScrollListeners = null;
4769 this._syncWithBandHandler = null;
4770 this._syncWithBandOrthogonalScrollHandler = null;
4771 this._selectorListener = null;
4772
4773 this._div = null;
4774 this._innerDiv = null;
4775 this._keyboardInput = null;
4776 this._scrollBar = null;
4777 };
4778
4779 Timeline._Band.prototype.addOnScrollListener = function(listener) {
4780 this._onScrollListeners.push(listener);
4781 };
4782
4783 Timeline._Band.prototype.removeOnScrollListener = function(listener) {
4784 for (var i = 0; i < this._onScrollListeners.length; i++) {
4785 if (this._onScrollListeners[i] == listener) {
4786 this._onScrollListeners.splice(i, 1);
4787 break;
4788 }
4789 }
4790 };
4791
4792 Timeline._Band.prototype.addOnOrthogonalScrollListener = function(listener) {
4793 this._onOrthogonalScrollListeners.push(listener);
4794 };
4795
4796 Timeline._Band.prototype.removeOnOrthogonalScrollListener = function(listener) {
4797 for (var i = 0; i < this._onOrthogonalScrollListeners.length; i++) {
4798 if (this._onOrthogonalScrollListeners[i] == listener) {
4799 this._onOrthogonalScrollListeners.splice(i, 1);
4800 break;
4801 }
4802 }
4803 };
4804
4805 Timeline._Band.prototype.setSyncWithBand = function(band, highlight) {
4806 if (this._syncWithBand) {
4807 this._syncWithBand.removeOnScrollListener(this._syncWithBandHandler);
4808 this._syncWithBand.removeOnOrthogonalScrollListener(this._syncWithBandOrthogonalScrollHandler);
4809 }
4810
4811 this._syncWithBand = band;
4812 this._syncWithBand.addOnScrollListener(this._syncWithBandHandler);
4813 this._syncWithBand.addOnOrthogonalScrollListener(this._syncWithBandOrthogonalScrollHandler);
4814 this._highlight = highlight;
4815 this._positionHighlight();
4816 };
4817
4818 Timeline._Band.prototype.getLocale = function() {
4819 return this._locale;
4820 };
4821
4822 Timeline._Band.prototype.getTimeZone = function() {
4823 return this._timeZone;
4824 };
4825
4826 Timeline._Band.prototype.getLabeller = function() {
4827 return this._labeller;
4828 };
4829
4830 Timeline._Band.prototype.getIndex = function() {
4831 return this._index;
4832 };
4833
4834 Timeline._Band.prototype.getEther = function() {
4835 return this._ether;
4836 };
4837
4838 Timeline._Band.prototype.getEtherPainter = function() {
4839 return this._etherPainter;
4840 };
4841
4842 Timeline._Band.prototype.getEventSource = function() {
4843 return this._eventSource;
4844 };
4845
4846 Timeline._Band.prototype.getEventPainter = function() {
4847 return this._eventPainter;
4848 };
4849
4850 Timeline._Band.prototype.getTimeline = function() {
4851 return this._timeline;
4852 };
4853
4854 // Autowidth support
4855 Timeline._Band.prototype.updateEventTrackInfo = function(tracks, increment) {
4856 this._eventTrackIncrement = increment; // doesn't vary for a specific band
4857
4858 if (tracks > this._eventTracksNeeded) {
4859 this._eventTracksNeeded = tracks;
4860 }
4861 };
4862
4863 // Autowidth support
4864 Timeline._Band.prototype.checkAutoWidth = function() {
4865 // if a new (larger) width is needed by the band
4866 // then: a) updates the band's bandInfo.width
4867 //
4868 // desiredWidth for the band is
4869 // (number of tracks + margin) * track increment
4870 if (! this._timeline.autoWidth) {
4871 return; // early return
4872 }
4873
4874 var overviewBand = this._eventPainter.getType() == 'overview';
4875 var margin = overviewBand ?
4876 this._theme.event.overviewTrack.autoWidthMargin :
4877 this._theme.event.track.autoWidthMargin;
4878 var desiredWidth = Math.ceil((this._eventTracksNeeded + margin) *
4879 this._eventTrackIncrement);
4880 // add offset amount (additional margin)
4881 desiredWidth += overviewBand ? this._theme.event.overviewTrack.offset :
4882 this._theme.event.track.offset;
4883 var bandInfo = this._bandInfo;
4884
4885 if (desiredWidth != bandInfo.width) {
4886 bandInfo.width = desiredWidth;
4887 }
4888 };
4889
4890 Timeline._Band.prototype.layout = function() {
4891 this.paint();
4892 };
4893
4894 Timeline._Band.prototype.paint = function() {
4895 this._etherPainter.paint();
4896 this._paintDecorators();
4897 this._paintEvents();
4898 };
4899
4900 Timeline._Band.prototype.softLayout = function() {
4901 this.softPaint();
4902 };
4903
4904 Timeline._Band.prototype.softPaint = function() {
4905 this._etherPainter.softPaint();
4906 this._softPaintDecorators();
4907 this._softPaintEvents();
4908 };
4909
4910 Timeline._Band.prototype.setBandShiftAndWidth = function(shift, width) {
4911 var inputDiv = this._keyboardInput.parentNode;
4912 var middle = shift + Math.floor(width / 2);
4913 if (this._timeline.isHorizontal()) {
4914 this._div.style.top = shift + "px";
4915 this._div.style.height = width + "px";
4916
4917 inputDiv.style.top = middle + "px";
4918 inputDiv.style.left = "-1em";
4919 } else {
4920 this._div.style.left = shift + "px";
4921 this._div.style.width = width + "px";
4922
4923 inputDiv.style.left = middle + "px";
4924 inputDiv.style.top = "-1em";
4925 }
4926 };
4927
4928 Timeline._Band.prototype.getViewWidth = function() {
4929 if (this._timeline.isHorizontal()) {
4930 return this._div.offsetHeight;
4931 } else {
4932 return this._div.offsetWidth;
4933 }
4934 };
4935
4936 Timeline._Band.prototype.setViewLength = function(length) {
4937 this._viewLength = length;
4938 this._recenterDiv();
4939 this._onChanging();
4940 };
4941
4942 Timeline._Band.prototype.getViewLength = function() {
4943 return this._viewLength;
4944 };
4945
4946 Timeline._Band.prototype.getTotalViewLength = function() {
4947 return Timeline._Band.SCROLL_MULTIPLES * this._viewLength;
4948 };
4949
4950 Timeline._Band.prototype.getViewOffset = function() {
4951 return this._viewOffset;
4952 };
4953
4954 Timeline._Band.prototype.getMinDate = function() {
4955 return this._ether.pixelOffsetToDate(this._viewOffset);
4956 };
4957
4958 Timeline._Band.prototype.getMaxDate = function() {
4959 return this._ether.pixelOffsetToDate(this._viewOffset + Timeline._Band.SCROLL_MULTIPLES * this._viewLength);
4960 };
4961
4962 Timeline._Band.prototype.getMinVisibleDate = function() {
4963 return this._ether.pixelOffsetToDate(0);
4964 };
4965
4966 Timeline._Band.prototype.getMinVisibleDateAfterDelta = function(delta) {
4967 return this._ether.pixelOffsetToDate(delta);
4968 };
4969
4970 Timeline._Band.prototype.getMaxVisibleDate = function() {
4971 // Max date currently visible on band
4972 return this._ether.pixelOffsetToDate(this._viewLength);
4973 };
4974
4975 Timeline._Band.prototype.getMaxVisibleDateAfterDelta = function(delta) {
4976 // Max date visible on band after delta px view change is applied
4977 return this._ether.pixelOffsetToDate(this._viewLength + delta);
4978 };
4979
4980 Timeline._Band.prototype.getCenterVisibleDate = function() {
4981 return this._ether.pixelOffsetToDate(this._viewLength / 2);
4982 };
4983
4984 Timeline._Band.prototype.setMinVisibleDate = function(date) {
4985 if (!this._changing) {
4986 this._moveEther(Math.round(-this._ether.dateToPixelOffset(date)));
4987 }
4988 };
4989
4990 Timeline._Band.prototype.setMaxVisibleDate = function(date) {
4991 if (!this._changing) {
4992 this._moveEther(Math.round(this._viewLength - this._ether.dateToPixelOffset(date)));
4993 }
4994 };
4995
4996 Timeline._Band.prototype.setCenterVisibleDate = function(date) {
4997 if (!this._changing) {
4998 this._moveEther(Math.round(this._viewLength / 2 - this._ether.dateToPixelOffset(date)));
4999 }
5000 };
5001
5002 Timeline._Band.prototype.dateToPixelOffset = function(date) {
5003 return this._ether.dateToPixelOffset(date) - this._viewOffset;
5004 };
5005
5006 Timeline._Band.prototype.pixelOffsetToDate = function(pixels) {
5007 return this._ether.pixelOffsetToDate(pixels + this._viewOffset);
5008 };
5009
5010 Timeline._Band.prototype.getViewOrthogonalOffset = function() {
5011 return this._viewOrthogonalOffset;
5012 };
5013
5014 Timeline._Band.prototype.setViewOrthogonalOffset = function(offset) {
5015 this._viewOrthogonalOffset = Math.max(0, offset);
5016 };
5017
5018 Timeline._Band.prototype.createLayerDiv = function(zIndex, className) {
5019 var div = this._timeline.getDocument().createElement("div");
5020 div.className = "timeline-band-layer" + (typeof className == "string" ? (" " + className) : "");
5021 div.style.zIndex = zIndex;
5022 this._innerDiv.appendChild(div);
5023
5024 var innerDiv = this._timeline.getDocument().createElement("div");
5025 innerDiv.className = "timeline-band-layer-inner";
5026 if (SimileAjax.Platform.browser.isIE) {
5027 innerDiv.style.cursor = "move";
5028 } else {
5029 innerDiv.style.cursor = "-moz-grab";
5030 }
5031 div.appendChild(innerDiv);
5032
5033 return innerDiv;
5034 };
5035
5036 Timeline._Band.prototype.removeLayerDiv = function(div) {
5037 this._innerDiv.removeChild(div.parentNode);
5038 };
5039
5040 Timeline._Band.prototype.scrollToCenter = function(date, f) {
5041 var pixelOffset = this._ether.dateToPixelOffset(date);
5042 if (pixelOffset < -this._viewLength / 2) {
5043 this.setCenterVisibleDate(this.pixelOffsetToDate(pixelOffset + this._viewLength));
5044 } else if (pixelOffset > 3 * this._viewLength / 2) {
5045 this.setCenterVisibleDate(this.pixelOffsetToDate(pixelOffset - this._viewLength));
5046 }
5047 this._autoScroll(Math.round(this._viewLength / 2 - this._ether.dateToPixelOffset(date)), f);
5048 };
5049
5050 Timeline._Band.prototype.showBubbleForEvent = function(eventID) {
5051 var evt = this.getEventSource().getEvent(eventID);
5052 if (evt) {
5053 var self = this;
5054 this.scrollToCenter(evt.getStart(), function() {
5055 self._eventPainter.showBubble(evt);
5056 });
5057 }
5058 };
5059
5060 Timeline._Band.prototype.zoom = function(zoomIn, x, y, target) {
5061 if (!this._zoomSteps) {
5062 // zoom disabled
5063 return;
5064 }
5065
5066 // shift the x value by our offset
5067 x += this._viewOffset;
5068
5069 var zoomDate = this._ether.pixelOffsetToDate(x);
5070 var netIntervalChange = this._ether.zoom(zoomIn);
5071 this._etherPainter.zoom(netIntervalChange);
5072
5073 // shift our zoom date to the far left
5074 this._moveEther(Math.round(-this._ether.dateToPixelOffset(zoomDate)));
5075 // then shift it back to where the mouse was
5076 this._moveEther(x);
5077 };
5078
5079 Timeline._Band.prototype._onMouseDown = function(innerFrame, evt, target) {
5080 this.closeBubble();
5081
5082 this._dragging = true;
5083 this._dragX = evt.clientX;
5084 this._dragY = evt.clientY;
5085 };
5086
5087 Timeline._Band.prototype._onMouseMove = function(innerFrame, evt, target) {
5088 if (this._dragging) {
5089 var diffX = evt.clientX - this._dragX;
5090 var diffY = evt.clientY - this._dragY;
5091
5092 this._dragX = evt.clientX;
5093 this._dragY = evt.clientY;
5094
5095 if (this._timeline.isHorizontal()) {
5096 this._moveEther(diffX, diffY);
5097 } else {
5098 this._moveEther(diffY, diffX);
5099 }
5100 this._positionHighlight();
5101 this._showScrollbar();
5102 }
5103 };
5104
5105 Timeline._Band.prototype._onMouseUp = function(innerFrame, evt, target) {
5106 this._dragging = false;
5107 this._keyboardInput.focus();
5108 this._bounceBack();
5109 };
5110
5111 Timeline._Band.prototype._onMouseOut = function(innerFrame, evt, target) {
5112 var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
5113 coords.x += this._viewOffset;
5114 if (coords.x < 0 || coords.x > innerFrame.offsetWidth ||
5115 coords.y < 0 || coords.y > innerFrame.offsetHeight) {
5116 this._dragging = false;
5117 this._bounceBack();
5118 }
5119 };
5120
5121 Timeline._Band.prototype._onMouseScroll = function(innerFrame, evt, target) {
5122 var now = new Date();
5123 now = now.getTime();
5124
5125 if (!this._lastScrollTime || ((now - this._lastScrollTime) > 50)) {
5126 // limit 1 scroll per 200ms due to FF3 sending multiple events back to back
5127 this._lastScrollTime = now;
5128
5129 var delta = 0;
5130 if (evt.wheelDelta) {
5131 delta = evt.wheelDelta/120;
5132 } else if (evt.detail) {
5133 delta = -evt.detail/3;
5134 }
5135
5136 // either scroll or zoom
5137 var mouseWheel = this._theme.mouseWheel;
5138
5139 if (this._zoomSteps || mouseWheel === 'zoom') {
5140 var loc = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
5141 if (delta != 0) {
5142 var zoomIn;
5143 if (delta > 0)
5144 zoomIn = true;
5145 if (delta < 0)
5146 zoomIn = false;
5147 // call zoom on the timeline so we could zoom multiple bands if desired
5148 this._timeline.zoom(zoomIn, loc.x, loc.y, innerFrame);
5149 }
5150 }
5151 else if (mouseWheel === 'scroll') {
5152 var move_amt = 50 * (delta < 0 ? -1 : 1);
5153 this._moveEther(move_amt);
5154 }
5155 }
5156
5157 // prevent bubble
5158 if (evt.stopPropagation) {
5159 evt.stopPropagation();
5160 }
5161 evt.cancelBubble = true;
5162
5163 // prevent the default action
5164 if (evt.preventDefault) {
5165 evt.preventDefault();
5166 }
5167 evt.returnValue = false;
5168 };
5169
5170 Timeline._Band.prototype._onDblClick = function(innerFrame, evt, target) {
5171 var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt, innerFrame);
5172 var distance = coords.x - (this._viewLength / 2 - this._viewOffset);
5173
5174 this._autoScroll(-distance);
5175 };
5176
5177 Timeline._Band.prototype._onKeyDown = function(keyboardInput, evt, target) {
5178 if (!this._dragging) {
5179 switch (evt.keyCode) {
5180 case 27: // ESC
5181 break;
5182 case 37: // left arrow
5183 case 38: // up arrow
5184 this._scrollSpeed = Math.min(50, Math.abs(this._scrollSpeed * 1.05));
5185 this._moveEther(this._scrollSpeed);
5186 break;
5187 case 39: // right arrow
5188 case 40: // down arrow
5189 this._scrollSpeed = -Math.min(50, Math.abs(this._scrollSpeed * 1.05));
5190 this._moveEther(this._scrollSpeed);
5191 break;
5192 default:
5193 return true;
5194 }
5195 this.closeBubble();
5196
5197 SimileAjax.DOM.cancelEvent(evt);
5198 return false;
5199 }
5200 return true;
5201 };
5202
5203 Timeline._Band.prototype._onKeyUp = function(keyboardInput, evt, target) {
5204 if (!this._dragging) {
5205 this._scrollSpeed = this._originalScrollSpeed;
5206
5207 switch (evt.keyCode) {
5208 case 35: // end
5209 this.setCenterVisibleDate(this._eventSource.getLatestDate());
5210 break;
5211 case 36: // home
5212 this.setCenterVisibleDate(this._eventSource.getEarliestDate());
5213 break;
5214 case 33: // page up
5215 this._autoScroll(this._timeline.getPixelLength());
5216 break;
5217 case 34: // page down
5218 this._autoScroll(-this._timeline.getPixelLength());
5219 break;
5220 default:
5221 return true;
5222 }
5223
5224 this.closeBubble();
5225
5226 SimileAjax.DOM.cancelEvent(evt);
5227 return false;
5228 }
5229 return true;
5230 };
5231
5232 Timeline._Band.prototype._autoScroll = function(distance, f) {
5233 var b = this;
5234 var a = SimileAjax.Graphics.createAnimation(
5235 function(abs, diff) {
5236 b._moveEther(diff);
5237 },
5238 0,
5239 distance,
5240 1000,
5241 f
5242 );
5243 a.run();
5244 };
5245
5246 Timeline._Band.prototype._moveEther = function(shift, orthogonalShift) {
5247 if (orthogonalShift === undefined) {
5248 orthogonalShift = 0;
5249 }
5250
5251 this.closeBubble();
5252
5253 // A positive shift means back in time
5254 // Check that we're not moving beyond Timeline's limits
5255 if (!this._timeline.shiftOK(this._index, shift)) {
5256 return; // early return
5257 }
5258
5259 this._viewOffset += shift;
5260 this._ether.shiftPixels(-shift);
5261 if (this._timeline.isHorizontal()) {
5262 this._div.style.left = this._viewOffset + "px";
5263 } else {
5264 this._div.style.top = this._viewOffset + "px";
5265 }
5266
5267 if (this._supportsOrthogonalScrolling) {
5268 this._viewOrthogonalOffset = this._viewOrthogonalOffset + orthogonalShift;
5269 }
5270
5271 if (this._viewOffset > -this._viewLength * 0.5 ||
5272 this._viewOffset < -this._viewLength * (Timeline._Band.SCROLL_MULTIPLES - 1.5)) {
5273
5274 this._recenterDiv();
5275 } else {
5276 this.softLayout();
5277 }
5278
5279 this._onChanging();
5280 }
5281
5282 Timeline._Band.prototype._onChanging = function() {
5283 this._changing = true;
5284
5285 this._fireOnScroll();
5286 this._setSyncWithBandDate();
5287
5288 this._changing = false;
5289 };
5290
5291 Timeline._Band.prototype.busy = function() {
5292 // Is this band busy changing other bands?
5293 return(this._changing);
5294 };
5295
5296 Timeline._Band.prototype._fireOnScroll = function() {
5297 for (var i = 0; i < this._onScrollListeners.length; i++) {
5298 this._onScrollListeners[i](this);
5299 }
5300 };
5301
5302 Timeline._Band.prototype._fireOnOrthogonalScroll = function() {
5303 for (var i = 0; i < this._onOrthogonalScrollListeners.length; i++) {
5304 this._onOrthogonalScrollListeners[i](this);
5305 }
5306 };
5307
5308 Timeline._Band.prototype._setSyncWithBandDate = function() {
5309 if (this._syncWithBand) {
5310 var centerDate = this._ether.pixelOffsetToDate(this.getViewLength() / 2);
5311 this._syncWithBand.setCenterVisibleDate(centerDate);
5312 }
5313 };
5314
5315 Timeline._Band.prototype._onHighlightBandScroll = function() {
5316 if (this._syncWithBand) {
5317 var centerDate = this._syncWithBand.getCenterVisibleDate();
5318 var centerPixelOffset = this._ether.dateToPixelOffset(centerDate);
5319
5320 this._moveEther(Math.round(this._viewLength / 2 - centerPixelOffset));
5321 this._positionHighlight();
5322 }
5323 };
5324
5325 Timeline._Band.prototype._onHighlightBandOrthogonalScroll = function() {
5326 if (this._syncWithBand) {
5327 this._positionHighlight();
5328 }
5329 };
5330
5331 Timeline._Band.prototype._onAddMany = function() {
5332 this._paintEvents();
5333 };
5334
5335 Timeline._Band.prototype._onClear = function() {
5336 this._paintEvents();
5337 };
5338
5339 Timeline._Band.prototype._positionHighlight = function() {
5340 if (this._syncWithBand) {
5341 var startDate = this._syncWithBand.getMinVisibleDate();
5342 var endDate = this._syncWithBand.getMaxVisibleDate();
5343
5344 if (this._highlight) {
5345 var offset = 0; // percent
5346 var extent = 1.0; // percent
5347 var syncEventPainter = this._syncWithBand.getEventPainter();
5348 if ("supportsOrthogonalScrolling" in syncEventPainter &&
5349 syncEventPainter.supportsOrthogonalScrolling()) {
5350
5351 var orthogonalExtent = syncEventPainter.getOrthogonalExtent();
5352 var visibleWidth = this._syncWithBand.getViewWidth();
5353 var totalWidth = Math.max(visibleWidth, orthogonalExtent);
5354 extent = visibleWidth / totalWidth;
5355 offset = -this._syncWithBand.getViewOrthogonalOffset() / totalWidth;
5356 }
5357
5358 this._etherPainter.setHighlight(startDate, endDate, offset, extent);
5359 }
5360 }
5361 };
5362
5363 Timeline._Band.prototype._recenterDiv = function() {
5364 this._viewOffset = -this._viewLength * (Timeline._Band.SCROLL_MULTIPLES - 1) / 2;
5365 if (this._timeline.isHorizontal()) {
5366 this._div.style.left = this._viewOffset + "px";
5367 this._div.style.width = (Timeline._Band.SCROLL_MULTIPLES * this._viewLength) + "px";
5368 } else {
5369 this._div.style.top = this._viewOffset + "px";
5370 this._div.style.height = (Timeline._Band.SCROLL_MULTIPLES * this._viewLength) + "px";
5371 }
5372 this.layout();
5373 };
5374
5375 Timeline._Band.prototype._paintEvents = function() {
5376 this._eventPainter.paint();
5377 this._showScrollbar();
5378 this._fireOnOrthogonalScroll();
5379 };
5380
5381 Timeline._Band.prototype._softPaintEvents = function() {
5382 this._eventPainter.softPaint();
5383 };
5384
5385 Timeline._Band.prototype._paintDecorators = function() {
5386 for (var i = 0; i < this._decorators.length; i++) {
5387 this._decorators[i].paint();
5388 }
5389 };
5390
5391 Timeline._Band.prototype._softPaintDecorators = function() {
5392 for (var i = 0; i < this._decorators.length; i++) {
5393 this._decorators[i].softPaint();
5394 }
5395 };
5396
5397 Timeline._Band.prototype.closeBubble = function() {
5398 SimileAjax.WindowManager.cancelPopups();
5399 };
5400
5401 Timeline._Band.prototype._bounceBack = function(f) {
5402 if (!this._supportsOrthogonalScrolling) {
5403 return;
5404 }
5405
5406 var target = 0;
5407 if (this._viewOrthogonalOffset < 0) {
5408 var orthogonalExtent = this._eventPainter.getOrthogonalExtent();
5409 if (this._viewOrthogonalOffset + orthogonalExtent >= this.getViewWidth()) {
5410 target = this._viewOrthogonalOffset;
5411 } else {
5412 target = Math.min(0, this.getViewWidth() - orthogonalExtent);
5413 }
5414 }
5415
5416 if (target != this._viewOrthogonalOffset) {
5417 var self = this;
5418 SimileAjax.Graphics.createAnimation(
5419 function(abs, diff) {
5420 self._viewOrthogonalOffset = abs;
5421 self._eventPainter.softPaint();
5422 self._showScrollbar();
5423 self._fireOnOrthogonalScroll();
5424 },
5425 this._viewOrthogonalOffset,
5426 target,
5427 300,
5428 function() {
5429 self._hideScrollbar();
5430 }
5431 ).run();
5432 } else {
5433 this._hideScrollbar();
5434 }
5435 };
5436
5437 Timeline._Band.prototype._showScrollbar = function() {
5438 if (!this._supportsOrthogonalScrolling) {
5439 return;
5440 }
5441
5442 var orthogonalExtent = this._eventPainter.getOrthogonalExtent();
5443 var visibleWidth = this.getViewWidth();
5444 var totalWidth = Math.max(visibleWidth, orthogonalExtent);
5445 var ratio = (visibleWidth / totalWidth);
5446 var thumbWidth = Math.round(visibleWidth * ratio) + "px";
5447 var thumbOffset = Math.round(-this._viewOrthogonalOffset * ratio) + "px";
5448
5449 var thumb = this._scrollBar.firstChild;
5450 if (this._timeline.isHorizontal()) {
5451 this._scrollBar.style.top = this._div.style.top;
5452 this._scrollBar.style.height = this._div.style.height;
5453
5454 this._scrollBar.style.right = "0px";
5455 this._scrollBar.style.width = "10px";
5456
5457 thumb.style.top = thumbOffset;
5458 thumb.style.height = thumbWidth;
5459 } else {
5460 this._scrollBar.style.left = this._div.style.left;
5461 this._scrollBar.style.width = this._div.style.width;
5462
5463 this._scrollBar.style.bottom = "0px";
5464 this._scrollBar.style.height = "10px";
5465
5466 thumb.style.left = thumbOffset;
5467 thumb.style.width = thumbWidth;
5468 }
5469
5470 if (ratio >= 1 && this._viewOrthogonalOffset == 0) {
5471 this._scrollBar.style.display = "none";
5472 } else {
5473 this._scrollBar.style.display = "block";
5474 }
5475 };
5476
5477 Timeline._Band.prototype._hideScrollbar = function() {
5478 if (!this._supportsOrthogonalScrolling) {
5479 return;
5480 }
5481 //this._scrollBar.style.display = "none";
5482 };
5483
5484 /*==================================================
5485 * Classic Theme
5486 *==================================================
5487 */
5488
5489
5490
5491 Timeline.ClassicTheme = new Object();
5492
5493 Timeline.ClassicTheme.implementations = [];
5494
5495 Timeline.ClassicTheme.create = function(locale) {
5496 if (locale == null) {
5497 locale = Timeline.getDefaultLocale();
5498 }
5499
5500 var f = Timeline.ClassicTheme.implementations[locale];
5501 if (f == null) {
5502 f = Timeline.ClassicTheme._Impl;
5503 }
5504 return new f();
5505 };
5506
5507 Timeline.ClassicTheme._Impl = function() {
5508 this.firstDayOfWeek = 0; // Sunday
5509
5510 // Note: Many styles previously set here are now set using CSS
5511 // The comments indicate settings controlled by CSS, not
5512 // lines to be un-commented.
5513 //
5514 //
5515 // Attributes autoWidth, autoWidthAnimationTime, timeline_start
5516 // and timeline_stop must be set on the first band's theme.
5517 // The other attributes can be set differently for each
5518 // band by using different themes for the bands.
5519 this.autoWidth = false; // Should the Timeline automatically grow itself, as
5520 // needed when too many events for the available width
5521 // are painted on the visible part of the Timeline?
5522 this.autoWidthAnimationTime = 500; // mSec
5523 this.timeline_start = null; // Setting a date, eg new Date(Date.UTC(2008,0,17,20,00,00,0)) will prevent the
5524 // Timeline from being moved to anytime before the date.
5525 this.timeline_stop = null; // Use for setting a maximum date. The Timeline will not be able
5526 // to be moved to anytime after this date.
5527 this.ether = {
5528 backgroundColors: [
5529 // "#EEE",
5530 // "#DDD",
5531 // "#CCC",
5532 // "#AAA"
5533 ],
5534 // highlightColor: "white",
5535 highlightOpacity: 50,
5536 interval: {
5537 line: {
5538 show: true,
5539 opacity: 25
5540 // color: "#aaa",
5541 },
5542 weekend: {
5543 opacity: 30
5544 // color: "#FFFFE0",
5545 },
5546 marker: {
5547 hAlign: "Bottom",
5548 vAlign: "Right"
5549 /*
5550 hBottomStyler: function(elmt) {
5551 elmt.className = "timeline-ether-marker-bottom";
5552 },
5553 hBottomEmphasizedStyler: function(elmt) {
5554 elmt.className = "timeline-ether-marker-bottom-emphasized";
5555 },
5556 hTopStyler: function(elmt) {
5557 elmt.className = "timeline-ether-marker-top";
5558 },
5559 hTopEmphasizedStyler: function(elmt) {
5560 elmt.className = "timeline-ether-marker-top-emphasized";
5561 },
5562 */
5563
5564
5565 /*
5566 vRightStyler: function(elmt) {
5567 elmt.className = "timeline-ether-marker-right";
5568 },
5569 vRightEmphasizedStyler: function(elmt) {
5570 elmt.className = "timeline-ether-marker-right-emphasized";
5571 },
5572 vLeftStyler: function(elmt) {
5573 elmt.className = "timeline-ether-marker-left";
5574 },
5575 vLeftEmphasizedStyler:function(elmt) {
5576 elmt.className = "timeline-ether-marker-left-emphasized";
5577 }
5578 */
5579 }
5580 }
5581 };
5582
5583 this.event = {
5584 track: {
5585 height: 10, // px. You will need to change the track
5586 // height if you change the tape height.
5587 gap: 2, // px. Gap between tracks
5588 offset: 2, // px. top margin above tapes
5589 autoWidthMargin: 1.5
5590 /* autoWidthMargin is only used if autoWidth (see above) is true.
5591 The autoWidthMargin setting is used to set how close the bottom of the
5592 lowest track is to the edge of the band's div. The units are total track
5593 width (tape + label + gap). A min of 0.5 is suggested. Use this setting to
5594 move the bottom track's tapes above the axis markers, if needed for your
5595 Timeline.
5596 */
5597 },
5598 overviewTrack: {
5599 offset: 20, // px -- top margin above tapes
5600 tickHeight: 6, // px
5601 height: 2, // px
5602 gap: 1, // px
5603 autoWidthMargin: 5 // This attribute is only used if autoWidth (see above) is true.
5604 },
5605 tape: {
5606 height: 4 // px. For thicker tapes, remember to change track height too.
5607 },
5608 instant: {
5609 icon: Timeline.urlPrefix + "images/dull-blue-circle.png",
5610 // default icon. Icon can also be specified per event
5611 iconWidth: 10,
5612 iconHeight: 10,
5613 impreciseOpacity: 20, // opacity of the tape when durationEvent is false
5614 impreciseIconMargin: 3 // A tape and an icon are painted for imprecise instant
5615 // events. This attribute is the margin between the
5616 // bottom of the tape and the top of the icon in that
5617 // case.
5618 // color: "#58A0DC",
5619 // impreciseColor: "#58A0DC",
5620 },
5621 duration: {
5622 impreciseOpacity: 20 // tape opacity for imprecise part of duration events
5623 // color: "#58A0DC",
5624 // impreciseColor: "#58A0DC",
5625 },
5626 label: {
5627 backgroundOpacity: 50,// only used in detailed painter
5628 offsetFromLine: 3 // px left margin amount from icon's right edge
5629 // backgroundColor: "white",
5630 // lineColor: "#58A0DC",
5631 },
5632 highlightColors: [ // Use with getEventPainter().setHighlightMatcher
5633 // See webapp/examples/examples.js
5634 "#FFFF00",
5635 "#FFC000",
5636 "#FF0000",
5637 "#0000FF"
5638 ],
5639 highlightLabelBackground: false, // When highlighting an event, also change the event's label background?
5640 bubble: {
5641 width: 250, // px
5642 maxHeight: 0, // px Maximum height of bubbles. 0 means no max height.
5643 // scrollbar will be added for taller bubbles
5644 titleStyler: function(elmt) {
5645 elmt.className = "timeline-event-bubble-title";
5646 },
5647 bodyStyler: function(elmt) {
5648 elmt.className = "timeline-event-bubble-body";
5649 },
5650 imageStyler: function(elmt) {
5651 elmt.className = "timeline-event-bubble-image";
5652 },
5653 wikiStyler: function(elmt) {
5654 elmt.className = "timeline-event-bubble-wiki";
5655 },
5656 timeStyler: function(elmt) {
5657 elmt.className = "timeline-event-bubble-time";
5658 }
5659 }
5660 };
5661
5662 this.mouseWheel = 'scroll'; // 'default', 'zoom', 'scroll'
5663 };
5664 /*==================================================
5665 * An "ether" is a object that maps date/time to pixel coordinates.
5666 *==================================================
5667 */
5668
5669 /*==================================================
5670 * Linear Ether
5671 *==================================================
5672 */
5673
5674 Timeline.LinearEther = function(params) {
5675 this._params = params;
5676 this._interval = params.interval;
5677 this._pixelsPerInterval = params.pixelsPerInterval;
5678 };
5679
5680 Timeline.LinearEther.prototype.initialize = function(band, timeline) {
5681 this._band = band;
5682 this._timeline = timeline;
5683 this._unit = timeline.getUnit();
5684
5685 if ("startsOn" in this._params) {
5686 this._start = this._unit.parseFromObject(this._params.startsOn);
5687 } else if ("endsOn" in this._params) {
5688 this._start = this._unit.parseFromObject(this._params.endsOn);
5689 this.shiftPixels(-this._timeline.getPixelLength());
5690 } else if ("centersOn" in this._params) {
5691 this._start = this._unit.parseFromObject(this._params.centersOn);
5692 this.shiftPixels(-this._timeline.getPixelLength() / 2);
5693 } else {
5694 this._start = this._unit.makeDefaultValue();
5695 this.shiftPixels(-this._timeline.getPixelLength() / 2);
5696 }
5697 };
5698
5699 Timeline.LinearEther.prototype.setDate = function(date) {
5700 this._start = this._unit.cloneValue(date);
5701 };
5702
5703 Timeline.LinearEther.prototype.shiftPixels = function(pixels) {
5704 var numeric = this._interval * pixels / this._pixelsPerInterval;
5705 this._start = this._unit.change(this._start, numeric);
5706 };
5707
5708 Timeline.LinearEther.prototype.dateToPixelOffset = function(date) {
5709 var numeric = this._unit.compare(date, this._start);
5710 return this._pixelsPerInterval * numeric / this._interval;
5711 };
5712
5713 Timeline.LinearEther.prototype.pixelOffsetToDate = function(pixels) {
5714 var numeric = pixels * this._interval / this._pixelsPerInterval;
5715 return this._unit.change(this._start, numeric);
5716 };
5717
5718 Timeline.LinearEther.prototype.zoom = function(zoomIn) {
5719 var netIntervalChange = 0;
5720 var currentZoomIndex = this._band._zoomIndex;
5721 var newZoomIndex = currentZoomIndex;
5722
5723 if (zoomIn && (currentZoomIndex > 0)) {
5724 newZoomIndex = currentZoomIndex - 1;
5725 }
5726
5727 if (!zoomIn && (currentZoomIndex < (this._band._zoomSteps.length - 1))) {
5728 newZoomIndex = currentZoomIndex + 1;
5729 }
5730
5731 this._band._zoomIndex = newZoomIndex;
5732 this._interval =
5733 SimileAjax.DateTime.gregorianUnitLengths[this._band._zoomSteps[newZoomIndex].unit];
5734 this._pixelsPerInterval = this._band._zoomSteps[newZoomIndex].pixelsPerInterval;
5735 netIntervalChange = this._band._zoomSteps[newZoomIndex].unit -
5736 this._band._zoomSteps[currentZoomIndex].unit;
5737
5738 return netIntervalChange;
5739 };
5740
5741
5742 /*==================================================
5743 * Hot Zone Ether
5744 *==================================================
5745 */
5746
5747 Timeline.HotZoneEther = function(params) {
5748 this._params = params;
5749 this._interval = params.interval;
5750 this._pixelsPerInterval = params.pixelsPerInterval;
5751 this._theme = params.theme;
5752 };
5753
5754 Timeline.HotZoneEther.prototype.initialize = function(band, timeline) {
5755 this._band = band;
5756 this._timeline = timeline;
5757 this._unit = timeline.getUnit();
5758
5759 this._zones = [{
5760 startTime: Number.NEGATIVE_INFINITY,
5761 endTime: Number.POSITIVE_INFINITY,
5762 magnify: 1
5763 }];
5764 var params = this._params;
5765 for (var i = 0; i < params.zones.length; i++) {
5766 var zone = params.zones[i];
5767 var zoneStart = this._unit.parseFromObject(zone.start);
5768 var zoneEnd = this._unit.parseFromObject(zone.end);
5769
5770 for (var j = 0; j < this._zones.length && this._unit.compare(zoneEnd, zoneStart) > 0; j++) {
5771 var zone2 = this._zones[j];
5772
5773 if (this._unit.compare(zoneStart, zone2.endTime) < 0) {
5774 if (this._unit.compare(zoneStart, zone2.startTime) > 0) {
5775 this._zones.splice(j, 0, {
5776 startTime: zone2.startTime,
5777 endTime: zoneStart,
5778 magnify: zone2.magnify
5779 });
5780 j++;
5781
5782 zone2.startTime = zoneStart;
5783 }
5784
5785 if (this._unit.compare(zoneEnd, zone2.endTime) < 0) {
5786 this._zones.splice(j, 0, {
5787 startTime: zoneStart,
5788 endTime: zoneEnd,
5789 magnify: zone.magnify * zone2.magnify
5790 });
5791 j++;
5792
5793 zone2.startTime = zoneEnd;
5794 zoneStart = zoneEnd;
5795 } else {
5796 zone2.magnify *= zone.magnify;
5797 zoneStart = zone2.endTime;
5798 }
5799 } // else, try the next existing zone
5800 }
5801 }
5802
5803 if ("startsOn" in this._params) {
5804 this._start = this._unit.parseFromObject(this._params.startsOn);
5805 } else if ("endsOn" in this._params) {
5806 this._start = this._unit.parseFromObject(this._params.endsOn);
5807 this.shiftPixels(-this._timeline.getPixelLength());
5808 } else if ("centersOn" in this._params) {
5809 this._start = this._unit.parseFromObject(this._params.centersOn);
5810 this.shiftPixels(-this._timeline.getPixelLength() / 2);
5811 } else {
5812 this._start = this._unit.makeDefaultValue();
5813 this.shiftPixels(-this._timeline.getPixelLength() / 2);
5814 }
5815 };
5816
5817 Timeline.HotZoneEther.prototype.setDate = function(date) {
5818 this._start = this._unit.cloneValue(date);
5819 };
5820
5821 Timeline.HotZoneEther.prototype.shiftPixels = function(pixels) {
5822 this._start = this.pixelOffsetToDate(pixels);
5823 };
5824
5825 Timeline.HotZoneEther.prototype.dateToPixelOffset = function(date) {
5826 return this._dateDiffToPixelOffset(this._start, date);
5827 };
5828
5829 Timeline.HotZoneEther.prototype.pixelOffsetToDate = function(pixels) {
5830 return this._pixelOffsetToDate(pixels, this._start);
5831 };
5832
5833 Timeline.HotZoneEther.prototype.zoom = function(zoomIn) {
5834 var netIntervalChange = 0;
5835 var currentZoomIndex = this._band._zoomIndex;
5836 var newZoomIndex = currentZoomIndex;
5837
5838 if (zoomIn && (currentZoomIndex > 0)) {
5839 newZoomIndex = currentZoomIndex - 1;
5840 }
5841
5842 if (!zoomIn && (currentZoomIndex < (this._band._zoomSteps.length - 1))) {
5843 newZoomIndex = currentZoomIndex + 1;
5844 }
5845
5846 this._band._zoomIndex = newZoomIndex;
5847 this._interval =
5848 SimileAjax.DateTime.gregorianUnitLengths[this._band._zoomSteps[newZoomIndex].unit];
5849 this._pixelsPerInterval = this._band._zoomSteps[newZoomIndex].pixelsPerInterval;
5850 netIntervalChange = this._band._zoomSteps[newZoomIndex].unit -
5851 this._band._zoomSteps[currentZoomIndex].unit;
5852
5853 return netIntervalChange;
5854 };
5855
5856 Timeline.HotZoneEther.prototype._dateDiffToPixelOffset = function(fromDate, toDate) {
5857 var scale = this._getScale();
5858 var fromTime = fromDate;
5859 var toTime = toDate;
5860
5861 var pixels = 0;
5862 if (this._unit.compare(fromTime, toTime) < 0) {
5863 var z = 0;
5864 while (z < this._zones.length) {
5865 if (this._unit.compare(fromTime, this._zones[z].endTime) < 0) {
5866 break;
5867 }
5868 z++;
5869 }
5870
5871 while (this._unit.compare(fromTime, toTime) < 0) {
5872 var zone = this._zones[z];
5873 var toTime2 = this._unit.earlier(toTime, zone.endTime);
5874
5875 pixels += (this._unit.compare(toTime2, fromTime) / (scale / zone.magnify));
5876
5877 fromTime = toTime2;
5878 z++;
5879 }
5880 } else {
5881 var z = this._zones.length - 1;
5882 while (z >= 0) {
5883 if (this._unit.compare(fromTime, this._zones[z].startTime) > 0) {
5884 break;
5885 }
5886 z--;
5887 }
5888
5889 while (this._unit.compare(fromTime, toTime) > 0) {
5890 var zone = this._zones[z];
5891 var toTime2 = this._unit.later(toTime, zone.startTime);
5892
5893 pixels += (this._unit.compare(toTime2, fromTime) / (scale / zone.magnify));
5894
5895 fromTime = toTime2;
5896 z--;
5897 }
5898 }
5899 return pixels;
5900 };
5901
5902 Timeline.HotZoneEther.prototype._pixelOffsetToDate = function(pixels, fromDate) {
5903 var scale = this._getScale();
5904 var time = fromDate;
5905 if (pixels > 0) {
5906 var z = 0;
5907 while (z < this._zones.length) {
5908 if (this._unit.compare(time, this._zones[z].endTime) < 0) {
5909 break;
5910 }
5911 z++;
5912 }
5913
5914 while (pixels > 0) {
5915 var zone = this._zones[z];
5916 var scale2 = scale / zone.magnify;
5917
5918 if (zone.endTime == Number.POSITIVE_INFINITY) {
5919 time = this._unit.change(time, pixels * scale2);
5920 pixels = 0;
5921 } else {
5922 var pixels2 = this._unit.compare(zone.endTime, time) / scale2;
5923 if (pixels2 > pixels) {
5924 time = this._unit.change(time, pixels * scale2);
5925 pixels = 0;
5926 } else {
5927 time = zone.endTime;
5928 pixels -= pixels2;
5929 }
5930 }
5931 z++;
5932 }
5933 } else {
5934 var z = this._zones.length - 1;
5935 while (z >= 0) {
5936 if (this._unit.compare(time, this._zones[z].startTime) > 0) {
5937 break;
5938 }
5939 z--;
5940 }
5941
5942 pixels = -pixels;
5943 while (pixels > 0) {
5944 var zone = this._zones[z];
5945 var scale2 = scale / zone.magnify;
5946
5947 if (zone.startTime == Number.NEGATIVE_INFINITY) {
5948 time = this._unit.change(time, -pixels * scale2);
5949 pixels = 0;
5950 } else {
5951 var pixels2 = this._unit.compare(time, zone.startTime) / scale2;
5952 if (pixels2 > pixels) {
5953 time = this._unit.change(time, -pixels * scale2);
5954 pixels = 0;
5955 } else {
5956 time = zone.startTime;
5957 pixels -= pixels2;
5958 }
5959 }
5960 z--;
5961 }
5962 }
5963 return time;
5964 };
5965
5966 Timeline.HotZoneEther.prototype._getScale = function() {
5967 return this._interval / this._pixelsPerInterval;
5968 };
5969 /*==================================================
5970 * Gregorian Ether Painter
5971 *==================================================
5972 */
5973
5974 Timeline.GregorianEtherPainter = function(params) {
5975 this._params = params;
5976 this._theme = params.theme;
5977 this._unit = params.unit;
5978 this._multiple = ("multiple" in params) ? params.multiple : 1;
5979 };
5980
5981 Timeline.GregorianEtherPainter.prototype.initialize = function(band, timeline) {
5982 this._band = band;
5983 this._timeline = timeline;
5984
5985 this._backgroundLayer = band.createLayerDiv(0);
5986 this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
5987 this._backgroundLayer.className = 'timeline-ether-bg';
5988 // this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
5989
5990
5991 this._markerLayer = null;
5992 this._lineLayer = null;
5993
5994 var align = ("align" in this._params && this._params.align != undefined) ? this._params.align :
5995 this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
5996 var showLine = ("showLine" in this._params) ? this._params.showLine :
5997 this._theme.ether.interval.line.show;
5998
5999 this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
6000 this._timeline, this._band, this._theme, align, showLine);
6001
6002 this._highlight = new Timeline.EtherHighlight(
6003 this._timeline, this._band, this._theme, this._backgroundLayer);
6004 }
6005
6006 Timeline.GregorianEtherPainter.prototype.setHighlight = function(startDate, endDate, orthogonalOffset, orthogonalExtent) {
6007 this._highlight.position(startDate, endDate, orthogonalOffset, orthogonalExtent);
6008 }
6009
6010 Timeline.GregorianEtherPainter.prototype.paint = function() {
6011 if (this._markerLayer) {
6012 this._band.removeLayerDiv(this._markerLayer);
6013 }
6014 this._markerLayer = this._band.createLayerDiv(100);
6015 this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
6016 this._markerLayer.style.display = "none";
6017
6018 if (this._lineLayer) {
6019 this._band.removeLayerDiv(this._lineLayer);
6020 }
6021 this._lineLayer = this._band.createLayerDiv(1);
6022 this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
6023 this._lineLayer.style.display = "none";
6024
6025 var minDate = this._band.getMinDate();
6026 var maxDate = this._band.getMaxDate();
6027
6028 var timeZone = this._band.getTimeZone();
6029 var labeller = this._band.getLabeller();
6030
6031 SimileAjax.DateTime.roundDownToInterval(minDate, this._unit, timeZone, this._multiple, this._theme.firstDayOfWeek);
6032
6033 var p = this;
6034 var incrementDate = function(date) {
6035 for (var i = 0; i < p._multiple; i++) {
6036 SimileAjax.DateTime.incrementByInterval(date, p._unit);
6037 }
6038 };
6039
6040 while (minDate.getTime() < maxDate.getTime()) {
6041 this._intervalMarkerLayout.createIntervalMarker(
6042 minDate, labeller, this._unit, this._markerLayer, this._lineLayer);
6043
6044 incrementDate(minDate);
6045 }
6046 this._markerLayer.style.display = "block";
6047 this._lineLayer.style.display = "block";
6048 };
6049
6050 Timeline.GregorianEtherPainter.prototype.softPaint = function() {
6051 };
6052
6053 Timeline.GregorianEtherPainter.prototype.zoom = function(netIntervalChange) {
6054 if (netIntervalChange != 0) {
6055 this._unit += netIntervalChange;
6056 }
6057 };
6058
6059
6060 /*==================================================
6061 * Hot Zone Gregorian Ether Painter
6062 *==================================================
6063 */
6064
6065 Timeline.HotZoneGregorianEtherPainter = function(params) {
6066 this._params = params;
6067 this._theme = params.theme;
6068
6069 this._zones = [{
6070 startTime: Number.NEGATIVE_INFINITY,
6071 endTime: Number.POSITIVE_INFINITY,
6072 unit: params.unit,
6073 multiple: 1
6074 }];
6075 for (var i = 0; i < params.zones.length; i++) {
6076 var zone = params.zones[i];
6077 var zoneStart = SimileAjax.DateTime.parseGregorianDateTime(zone.start).getTime();
6078 var zoneEnd = SimileAjax.DateTime.parseGregorianDateTime(zone.end).getTime();
6079
6080 for (var j = 0; j < this._zones.length && zoneEnd > zoneStart; j++) {
6081 var zone2 = this._zones[j];
6082
6083 if (zoneStart < zone2.endTime) {
6084 if (zoneStart > zone2.startTime) {
6085 this._zones.splice(j, 0, {
6086 startTime: zone2.startTime,
6087 endTime: zoneStart,
6088 unit: zone2.unit,
6089 multiple: zone2.multiple
6090 });
6091 j++;
6092
6093 zone2.startTime = zoneStart;
6094 }
6095
6096 if (zoneEnd < zone2.endTime) {
6097 this._zones.splice(j, 0, {
6098 startTime: zoneStart,
6099 endTime: zoneEnd,
6100 unit: zone.unit,
6101 multiple: (zone.multiple) ? zone.multiple : 1
6102 });
6103 j++;
6104
6105 zone2.startTime = zoneEnd;
6106 zoneStart = zoneEnd;
6107 } else {
6108 zone2.multiple = zone.multiple;
6109 zone2.unit = zone.unit;
6110 zoneStart = zone2.endTime;
6111 }
6112 } // else, try the next existing zone
6113 }
6114 }
6115 };
6116
6117 Timeline.HotZoneGregorianEtherPainter.prototype.initialize = function(band, timeline) {
6118 this._band = band;
6119 this._timeline = timeline;
6120
6121 this._backgroundLayer = band.createLayerDiv(0);
6122 this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
6123 this._backgroundLayer.className ='timeline-ether-bg';
6124 //this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
6125
6126 this._markerLayer = null;
6127 this._lineLayer = null;
6128
6129 var align = ("align" in this._params && this._params.align != undefined) ? this._params.align :
6130 this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
6131 var showLine = ("showLine" in this._params) ? this._params.showLine :
6132 this._theme.ether.interval.line.show;
6133
6134 this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
6135 this._timeline, this._band, this._theme, align, showLine);
6136
6137 this._highlight = new Timeline.EtherHighlight(
6138 this._timeline, this._band, this._theme, this._backgroundLayer);
6139 }
6140
6141 Timeline.HotZoneGregorianEtherPainter.prototype.setHighlight = function(startDate, endDate) {
6142 this._highlight.position(startDate, endDate);
6143 }
6144
6145 Timeline.HotZoneGregorianEtherPainter.prototype.paint = function() {
6146 if (this._markerLayer) {
6147 this._band.removeLayerDiv(this._markerLayer);
6148 }
6149 this._markerLayer = this._band.createLayerDiv(100);
6150 this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
6151 this._markerLayer.style.display = "none";
6152
6153 if (this._lineLayer) {
6154 this._band.removeLayerDiv(this._lineLayer);
6155 }
6156 this._lineLayer = this._band.createLayerDiv(1);
6157 this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
6158 this._lineLayer.style.display = "none";
6159
6160 var minDate = this._band.getMinDate();
6161 var maxDate = this._band.getMaxDate();
6162
6163 var timeZone = this._band.getTimeZone();
6164 var labeller = this._band.getLabeller();
6165
6166 var p = this;
6167 var incrementDate = function(date, zone) {
6168 for (var i = 0; i < zone.multiple; i++) {
6169 SimileAjax.DateTime.incrementByInterval(date, zone.unit);
6170 }
6171 };
6172
6173 var zStart = 0;
6174 while (zStart < this._zones.length) {
6175 if (minDate.getTime() < this._zones[zStart].endTime) {
6176 break;
6177 }
6178 zStart++;
6179 }
6180 var zEnd = this._zones.length - 1;
6181 while (zEnd >= 0) {
6182 if (maxDate.getTime() > this._zones[zEnd].startTime) {
6183 break;
6184 }
6185 zEnd--;
6186 }
6187
6188 for (var z = zStart; z <= zEnd; z++) {
6189 var zone = this._zones[z];
6190
6191 var minDate2 = new Date(Math.max(minDate.getTime(), zone.startTime));
6192 var maxDate2 = new Date(Math.min(maxDate.getTime(), zone.endTime));
6193
6194 SimileAjax.DateTime.roundDownToInterval(minDate2, zone.unit, timeZone, zone.multiple, this._theme.firstDayOfWeek);
6195 SimileAjax.DateTime.roundUpToInterval(maxDate2, zone.unit, timeZone, zone.multiple, this._theme.firstDayOfWeek);
6196
6197 while (minDate2.getTime() < maxDate2.getTime()) {
6198 this._intervalMarkerLayout.createIntervalMarker(
6199 minDate2, labeller, zone.unit, this._markerLayer, this._lineLayer);
6200
6201 incrementDate(minDate2, zone);
6202 }
6203 }
6204 this._markerLayer.style.display = "block";
6205 this._lineLayer.style.display = "block";
6206 };
6207
6208 Timeline.HotZoneGregorianEtherPainter.prototype.softPaint = function() {
6209 };
6210
6211 Timeline.HotZoneGregorianEtherPainter.prototype.zoom = function(netIntervalChange) {
6212 if (netIntervalChange != 0) {
6213 for (var i = 0; i < this._zones.length; ++i) {
6214 if (this._zones[i]) {
6215 this._zones[i].unit += netIntervalChange;
6216 }
6217 }
6218 }
6219 };
6220
6221 /*==================================================
6222 * Year Count Ether Painter
6223 *==================================================
6224 */
6225
6226 Timeline.YearCountEtherPainter = function(params) {
6227 this._params = params;
6228 this._theme = params.theme;
6229 this._startDate = SimileAjax.DateTime.parseGregorianDateTime(params.startDate);
6230 this._multiple = ("multiple" in params) ? params.multiple : 1;
6231 };
6232
6233 Timeline.YearCountEtherPainter.prototype.initialize = function(band, timeline) {
6234 this._band = band;
6235 this._timeline = timeline;
6236
6237 this._backgroundLayer = band.createLayerDiv(0);
6238 this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
6239 this._backgroundLayer.className = 'timeline-ether-bg';
6240 // this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
6241
6242 this._markerLayer = null;
6243 this._lineLayer = null;
6244
6245 var align = ("align" in this._params) ? this._params.align :
6246 this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
6247 var showLine = ("showLine" in this._params) ? this._params.showLine :
6248 this._theme.ether.interval.line.show;
6249
6250 this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
6251 this._timeline, this._band, this._theme, align, showLine);
6252
6253 this._highlight = new Timeline.EtherHighlight(
6254 this._timeline, this._band, this._theme, this._backgroundLayer);
6255 };
6256
6257 Timeline.YearCountEtherPainter.prototype.setHighlight = function(startDate, endDate) {
6258 this._highlight.position(startDate, endDate);
6259 };
6260
6261 Timeline.YearCountEtherPainter.prototype.paint = function() {
6262 if (this._markerLayer) {
6263 this._band.removeLayerDiv(this._markerLayer);
6264 }
6265 this._markerLayer = this._band.createLayerDiv(100);
6266 this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
6267 this._markerLayer.style.display = "none";
6268
6269 if (this._lineLayer) {
6270 this._band.removeLayerDiv(this._lineLayer);
6271 }
6272 this._lineLayer = this._band.createLayerDiv(1);
6273 this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
6274 this._lineLayer.style.display = "none";
6275
6276 var minDate = new Date(this._startDate.getTime());
6277 var maxDate = this._band.getMaxDate();
6278 var yearDiff = this._band.getMinDate().getUTCFullYear() - this._startDate.getUTCFullYear();
6279 minDate.setUTCFullYear(this._band.getMinDate().getUTCFullYear() - yearDiff % this._multiple);
6280
6281 var p = this;
6282 var incrementDate = function(date) {
6283 for (var i = 0; i < p._multiple; i++) {
6284 SimileAjax.DateTime.incrementByInterval(date, SimileAjax.DateTime.YEAR);
6285 }
6286 };
6287 var labeller = {
6288 labelInterval: function(date, intervalUnit) {
6289 var diff = date.getUTCFullYear() - p._startDate.getUTCFullYear();
6290 return {
6291 text: diff,
6292 emphasized: diff == 0
6293 };
6294 }
6295 };
6296
6297 while (minDate.getTime() < maxDate.getTime()) {
6298 this._intervalMarkerLayout.createIntervalMarker(
6299 minDate, labeller, SimileAjax.DateTime.YEAR, this._markerLayer, this._lineLayer);
6300
6301 incrementDate(minDate);
6302 }
6303 this._markerLayer.style.display = "block";
6304 this._lineLayer.style.display = "block";
6305 };
6306
6307 Timeline.YearCountEtherPainter.prototype.softPaint = function() {
6308 };
6309
6310 /*==================================================
6311 * Quarterly Ether Painter
6312 *==================================================
6313 */
6314
6315 Timeline.QuarterlyEtherPainter = function(params) {
6316 this._params = params;
6317 this._theme = params.theme;
6318 this._startDate = SimileAjax.DateTime.parseGregorianDateTime(params.startDate);
6319 };
6320
6321 Timeline.QuarterlyEtherPainter.prototype.initialize = function(band, timeline) {
6322 this._band = band;
6323 this._timeline = timeline;
6324
6325 this._backgroundLayer = band.createLayerDiv(0);
6326 this._backgroundLayer.setAttribute("name", "ether-background"); // for debugging
6327 this._backgroundLayer.className = 'timeline-ether-bg';
6328 // this._backgroundLayer.style.background = this._theme.ether.backgroundColors[band.getIndex()];
6329
6330 this._markerLayer = null;
6331 this._lineLayer = null;
6332
6333 var align = ("align" in this._params) ? this._params.align :
6334 this._theme.ether.interval.marker[timeline.isHorizontal() ? "hAlign" : "vAlign"];
6335 var showLine = ("showLine" in this._params) ? this._params.showLine :
6336 this._theme.ether.interval.line.show;
6337
6338 this._intervalMarkerLayout = new Timeline.EtherIntervalMarkerLayout(
6339 this._timeline, this._band, this._theme, align, showLine);
6340
6341 this._highlight = new Timeline.EtherHighlight(
6342 this._timeline, this._band, this._theme, this._backgroundLayer);
6343 };
6344
6345 Timeline.QuarterlyEtherPainter.prototype.setHighlight = function(startDate, endDate) {
6346 this._highlight.position(startDate, endDate);
6347 };
6348
6349 Timeline.QuarterlyEtherPainter.prototype.paint = function() {
6350 if (this._markerLayer) {
6351 this._band.removeLayerDiv(this._markerLayer);
6352 }
6353 this._markerLayer = this._band.createLayerDiv(100);
6354 this._markerLayer.setAttribute("name", "ether-markers"); // for debugging
6355 this._markerLayer.style.display = "none";
6356
6357 if (this._lineLayer) {
6358 this._band.removeLayerDiv(this._lineLayer);
6359 }
6360 this._lineLayer = this._band.createLayerDiv(1);
6361 this._lineLayer.setAttribute("name", "ether-lines"); // for debugging
6362 this._lineLayer.style.display = "none";
6363
6364 var minDate = new Date(0);
6365 var maxDate = this._band.getMaxDate();
6366
6367 minDate.setUTCFullYear(Math.max(this._startDate.getUTCFullYear(), this._band.getMinDate().getUTCFullYear()));
6368 minDate.setUTCMonth(this._startDate.getUTCMonth());
6369
6370 var p = this;
6371 var incrementDate = function(date) {
6372 date.setUTCMonth(date.getUTCMonth() + 3);
6373 };
6374 var labeller = {
6375 labelInterval: function(date, intervalUnit) {
6376 var quarters = (4 + (date.getUTCMonth() - p._startDate.getUTCMonth()) / 3) % 4;
6377 if (quarters != 0) {
6378 return { text: "Q" + (quarters + 1), emphasized: false };
6379 } else {
6380 return { text: "Y" + (date.getUTCFullYear() - p._startDate.getUTCFullYear() + 1), emphasized: true };
6381 }
6382 }
6383 };
6384
6385 while (minDate.getTime() < maxDate.getTime()) {
6386 this._intervalMarkerLayout.createIntervalMarker(
6387 minDate, labeller, SimileAjax.DateTime.YEAR, this._markerLayer, this._lineLayer);
6388
6389 incrementDate(minDate);
6390 }
6391 this._markerLayer.style.display = "block";
6392 this._lineLayer.style.display = "block";
6393 };
6394
6395 Timeline.QuarterlyEtherPainter.prototype.softPaint = function() {
6396 };
6397
6398 /*==================================================
6399 * Ether Interval Marker Layout
6400 *==================================================
6401 */
6402
6403 Timeline.EtherIntervalMarkerLayout = function(timeline, band, theme, align, showLine) {
6404 var horizontal = timeline.isHorizontal();
6405 if (horizontal) {
6406 if (align == "Top") {
6407 this.positionDiv = function(div, offset) {
6408 div.style.left = offset + "px";
6409 div.style.top = "0px";
6410 };
6411 } else {
6412 this.positionDiv = function(div, offset) {
6413 div.style.left = offset + "px";
6414 div.style.bottom = "0px";
6415 };
6416 }
6417 } else {
6418 if (align == "Left") {
6419 this.positionDiv = function(div, offset) {
6420 div.style.top = offset + "px";
6421 div.style.left = "0px";
6422 };
6423 } else {
6424 this.positionDiv = function(div, offset) {
6425 div.style.top = offset + "px";
6426 div.style.right = "0px";
6427 };
6428 }
6429 }
6430
6431 var markerTheme = theme.ether.interval.marker;
6432 var lineTheme = theme.ether.interval.line;
6433 var weekendTheme = theme.ether.interval.weekend;
6434
6435 var stylePrefix = (horizontal ? "h" : "v") + align;
6436 var labelStyler = markerTheme[stylePrefix + "Styler"];
6437 var emphasizedLabelStyler = markerTheme[stylePrefix + "EmphasizedStyler"];
6438 var day = SimileAjax.DateTime.gregorianUnitLengths[SimileAjax.DateTime.DAY];
6439
6440 this.createIntervalMarker = function(date, labeller, unit, markerDiv, lineDiv) {
6441 var offset = Math.round(band.dateToPixelOffset(date));
6442
6443 if (showLine && unit != SimileAjax.DateTime.WEEK) {
6444 var divLine = timeline.getDocument().createElement("div");
6445 divLine.className = "timeline-ether-lines";
6446
6447 if (lineTheme.opacity < 100) {
6448 SimileAjax.Graphics.setOpacity(divLine, lineTheme.opacity);
6449 }
6450
6451 if (horizontal) {
6452 //divLine.className += " timeline-ether-lines-vertical";
6453 divLine.style.left = offset + "px";
6454 } else {
6455 //divLine.className += " timeline-ether-lines-horizontal";
6456 divLine.style.top = offset + "px";
6457 }
6458 lineDiv.appendChild(divLine);
6459 }
6460 if (unit == SimileAjax.DateTime.WEEK) {
6461 var firstDayOfWeek = theme.firstDayOfWeek;
6462
6463 var saturday = new Date(date.getTime() + (6 - firstDayOfWeek - 7) * day);
6464 var monday = new Date(saturday.getTime() + 2 * day);
6465
6466 var saturdayPixel = Math.round(band.dateToPixelOffset(saturday));
6467 var mondayPixel = Math.round(band.dateToPixelOffset(monday));
6468 var length = Math.max(1, mondayPixel - saturdayPixel);
6469
6470 var divWeekend = timeline.getDocument().createElement("div");
6471 divWeekend.className = 'timeline-ether-weekends'
6472
6473 if (weekendTheme.opacity < 100) {
6474 SimileAjax.Graphics.setOpacity(divWeekend, weekendTheme.opacity);
6475 }
6476
6477 if (horizontal) {
6478 divWeekend.style.left = saturdayPixel + "px";
6479 divWeekend.style.width = length + "px";
6480 } else {
6481 divWeekend.style.top = saturdayPixel + "px";
6482 divWeekend.style.height = length + "px";
6483 }
6484 lineDiv.appendChild(divWeekend);
6485 }
6486
6487 var label = labeller.labelInterval(date, unit);
6488
6489 var div = timeline.getDocument().createElement("div");
6490 div.innerHTML = label.text;
6491
6492
6493
6494 div.className = 'timeline-date-label'
6495 if(label.emphasized) div.className += ' timeline-date-label-em'
6496
6497 this.positionDiv(div, offset);
6498 markerDiv.appendChild(div);
6499
6500 return div;
6501 };
6502 };
6503
6504 /*==================================================
6505 * Ether Highlight Layout
6506 *==================================================
6507 */
6508
6509 Timeline.EtherHighlight = function(timeline, band, theme, backgroundLayer) {
6510 var horizontal = timeline.isHorizontal();
6511
6512 this._highlightDiv = null;
6513 this._createHighlightDiv = function() {
6514 if (this._highlightDiv == null) {
6515 this._highlightDiv = timeline.getDocument().createElement("div");
6516 this._highlightDiv.setAttribute("name", "ether-highlight"); // for debugging
6517 this._highlightDiv.className = 'timeline-ether-highlight'
6518
6519 var opacity = theme.ether.highlightOpacity;
6520 if (opacity < 100) {
6521 SimileAjax.Graphics.setOpacity(this._highlightDiv, opacity);
6522 }
6523
6524 backgroundLayer.appendChild(this._highlightDiv);
6525 }
6526 }
6527
6528 this.position = function(startDate, endDate, orthogonalOffset, orthogonalExtent) {
6529 orthogonalOffset = orthogonalOffset || 0;
6530 orthogonalExtent = orthogonalExtent || 1.0;
6531
6532 this._createHighlightDiv();
6533
6534 var startPixel = Math.round(band.dateToPixelOffset(startDate));
6535 var endPixel = Math.round(band.dateToPixelOffset(endDate));
6536 var length = Math.max(endPixel - startPixel, 3);
6537 var totalWidth = band.getViewWidth() - 4;
6538 if (horizontal) {
6539 this._highlightDiv.style.left = startPixel + "px";
6540 this._highlightDiv.style.width = length + "px";
6541 this._highlightDiv.style.top = Math.round(orthogonalOffset * totalWidth) + "px";
6542 this._highlightDiv.style.height = Math.round(orthogonalExtent * totalWidth) + "px";
6543 } else {
6544 this._highlightDiv.style.top = startPixel + "px";
6545 this._highlightDiv.style.height = length + "px";
6546 this._highlightDiv.style.left = Math.round(orthogonalOffset * totalWidth) + "px";
6547 this._highlightDiv.style.width = Math.round(orthogonalExtent * totalWidth) + "px";
6548 }
6549 }
6550 };
6551 /*==================================================
6552 * Event Utils
6553 *==================================================
6554 */
6555 Timeline.EventUtils = {};
6556
6557 Timeline.EventUtils.getNewEventID = function() {
6558 // global across page
6559 if (this._lastEventID == null) {
6560 this._lastEventID = 0;
6561 }
6562
6563 this._lastEventID += 1;
6564 return "e" + this._lastEventID;
6565 };
6566
6567 Timeline.EventUtils.decodeEventElID = function(elementID) {
6568 /*==================================================
6569 *
6570 * Use this function to decode an event element's id on a band (label div,
6571 * tape div or icon img).
6572 *
6573 * Returns {band: <bandObj>, evt: <eventObj>}
6574 *
6575 * To enable a single event listener to monitor everything
6576 * on a Timeline, a set format is used for the id's of the
6577 * elements on the Timeline--
6578 *
6579 * element id format for labels, icons, tapes:
6580 * labels: label-tl-<timelineID>-<band_index>-<evt.id>
6581 * icons: icon-tl-<timelineID>-<band_index>-<evt.id>
6582 * tapes: tape1-tl-<timelineID>-<band_index>-<evt.id>
6583 * tape2-tl-<timelineID>-<band_index>-<evt.id>
6584 * // some events have more than one tape
6585 * highlight: highlight1-tl-<timelineID>-<band_index>-<evt.id>
6586 * highlight2-tl-<timelineID>-<band_index>-<evt.id>
6587 * // some events have more than one highlight div (future)
6588 * Note: use split('-') to get array of the format's parts
6589 *
6590 * You can then retrieve the timeline object and event object
6591 * by using Timeline.getTimeline, Timeline.getBand, or
6592 * Timeline.getEvent and passing in the element's id
6593 *
6594 *==================================================
6595 */
6596
6597 var parts = elementID.split('-');
6598 if (parts[1] != 'tl') {
6599 alert("Internal Timeline problem 101, please consult support");
6600 return {band: null, evt: null}; // early return
6601 }
6602
6603 var timeline = Timeline.getTimelineFromID(parts[2]);
6604 var band = timeline.getBand(parts[3]);
6605 var evt = band.getEventSource.getEvent(parts[4]);
6606
6607 return {band: band, evt: evt};
6608 };
6609
6610 Timeline.EventUtils.encodeEventElID = function(timeline, band, elType, evt) {
6611 // elType should be one of {label | icon | tapeN | highlightN}
6612 return elType + "-tl-" + timeline.timelineID +
6613 "-" + band.getIndex() + "-" + evt.getID();
6614 };
6615 /*==================================================
6616 * Gregorian Date Labeller
6617 *==================================================
6618 */
6619
6620 Timeline.GregorianDateLabeller = function(locale, timeZone) {
6621 this._locale = locale;
6622 this._timeZone = timeZone;
6623 };
6624
6625 Timeline.GregorianDateLabeller.monthNames = [];
6626 Timeline.GregorianDateLabeller.dayNames = [];
6627 Timeline.GregorianDateLabeller.labelIntervalFunctions = [];
6628
6629 Timeline.GregorianDateLabeller.getMonthName = function(month, locale) {
6630 return Timeline.GregorianDateLabeller.monthNames[locale][month];
6631 };
6632
6633 Timeline.GregorianDateLabeller.prototype.labelInterval = function(date, intervalUnit) {
6634 var f = Timeline.GregorianDateLabeller.labelIntervalFunctions[this._locale];
6635 if (f == null) {
6636 f = Timeline.GregorianDateLabeller.prototype.defaultLabelInterval;
6637 }
6638 return f.call(this, date, intervalUnit);
6639 };
6640
6641 Timeline.GregorianDateLabeller.prototype.labelPrecise = function(date) {
6642 return SimileAjax.DateTime.removeTimeZoneOffset(
6643 date,
6644 this._timeZone //+ (new Date().getTimezoneOffset() / 60)
6645 ).toUTCString();
6646 };
6647
6648 Timeline.GregorianDateLabeller.prototype.defaultLabelInterval = function(date, intervalUnit) {
6649 var text;
6650 var emphasized = false;
6651
6652 date = SimileAjax.DateTime.removeTimeZoneOffset(date, this._timeZone);
6653
6654 switch(intervalUnit) {
6655 case SimileAjax.DateTime.MILLISECOND:
6656 text = date.getUTCMilliseconds();
6657 break;
6658 case SimileAjax.DateTime.SECOND:
6659 text = date.getUTCSeconds();
6660 break;
6661 case SimileAjax.DateTime.MINUTE:
6662 var m = date.getUTCMinutes();
6663 if (m == 0) {
6664 text = date.getUTCHours() + ":00";
6665 emphasized = true;
6666 } else {
6667 text = m;
6668 }
6669 break;
6670 case SimileAjax.DateTime.HOUR:
6671 text = date.getUTCHours() + "hr";
6672 break;
6673 case SimileAjax.DateTime.DAY:
6674 text = Timeline.GregorianDateLabeller.getMonthName(date.getUTCMonth(), this._locale) + " " + date.getUTCDate();
6675 break;
6676 case SimileAjax.DateTime.WEEK:
6677 text = Timeline.GregorianDateLabeller.getMonthName(date.getUTCMonth(), this._locale) + " " + date.getUTCDate();
6678 break;
6679 case SimileAjax.DateTime.MONTH:
6680 var m = date.getUTCMonth();
6681 if (m != 0) {
6682 text = Timeline.GregorianDateLabeller.getMonthName(m, this._locale);
6683 break;
6684 } // else, fall through
6685 case SimileAjax.DateTime.YEAR:
6686 case SimileAjax.DateTime.DECADE:
6687 case SimileAjax.DateTime.CENTURY:
6688 case SimileAjax.DateTime.MILLENNIUM:
6689 var y = date.getUTCFullYear();
6690 if (y > 0) {
6691 text = date.getUTCFullYear();
6692 } else {
6693 text = (1 - y) + "BC";
6694 }
6695 emphasized =
6696 (intervalUnit == SimileAjax.DateTime.MONTH) ||
6697 (intervalUnit == SimileAjax.DateTime.DECADE && y % 100 == 0) ||
6698 (intervalUnit == SimileAjax.DateTime.CENTURY && y % 1000 == 0);
6699 break;
6700 default:
6701 text = date.toUTCString();
6702 }
6703 return { text: text, emphasized: emphasized };
6704 }
6705
6706 /*==================================================
6707 * Default Event Source
6708 *==================================================
6709 */
6710
6711
6712 Timeline.DefaultEventSource = function(eventIndex) {
6713 this._events = (eventIndex instanceof Object) ? eventIndex : new SimileAjax.EventIndex();
6714 this._listeners = [];
6715 };
6716
6717 Timeline.DefaultEventSource.prototype.addListener = function(listener) {
6718 this._listeners.push(listener);
6719 };
6720
6721 Timeline.DefaultEventSource.prototype.removeListener = function(listener) {
6722 for (var i = 0; i < this._listeners.length; i++) {
6723 if (this._listeners[i] == listener) {
6724 this._listeners.splice(i, 1);
6725 break;
6726 }
6727 }
6728 };
6729
6730 Timeline.DefaultEventSource.prototype.loadXML = function(xml, url) {
6731 var base = this._getBaseURL(url);
6732
6733 var wikiURL = xml.documentElement.getAttribute("wiki-url");
6734 var wikiSection = xml.documentElement.getAttribute("wiki-section");
6735
6736 var dateTimeFormat = xml.documentElement.getAttribute("date-time-format");
6737 var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
6738
6739 var node = xml.documentElement.firstChild;
6740 var added = false;
6741 while (node != null) {
6742 if (node.nodeType == 1) {
6743 var description = "";
6744 if (node.firstChild != null && node.firstChild.nodeType == 3) {
6745 description = node.firstChild.nodeValue;
6746 }
6747 // instant event: default is true. Or use values from isDuration or durationEvent
6748 var instant = (node.getAttribute("isDuration") === null &&
6749 node.getAttribute("durationEvent") === null) ||
6750 node.getAttribute("isDuration") == "false" ||
6751 node.getAttribute("durationEvent") == "false";
6752
6753 var evt = new Timeline.DefaultEventSource.Event( {
6754 id: node.getAttribute("id"),
6755 start: parseDateTimeFunction(node.getAttribute("start")),
6756 end: parseDateTimeFunction(node.getAttribute("end")),
6757 latestStart: parseDateTimeFunction(node.getAttribute("latestStart")),
6758 earliestEnd: parseDateTimeFunction(node.getAttribute("earliestEnd")),
6759 instant: instant,
6760 text: node.getAttribute("title"),
6761 description: description,
6762 image: this._resolveRelativeURL(node.getAttribute("image"), base),
6763 link: this._resolveRelativeURL(node.getAttribute("link") , base),
6764 icon: this._resolveRelativeURL(node.getAttribute("icon") , base),
6765 color: node.getAttribute("color"),
6766 textColor: node.getAttribute("textColor"),
6767 hoverText: node.getAttribute("hoverText"),
6768 classname: node.getAttribute("classname"),
6769 tapeImage: node.getAttribute("tapeImage"),
6770 tapeRepeat: node.getAttribute("tapeRepeat"),
6771 caption: node.getAttribute("caption"),
6772 eventID: node.getAttribute("eventID"),
6773 trackNum: node.getAttribute("trackNum")
6774 });
6775
6776 evt._node = node;
6777 evt.getProperty = function(name) {
6778 return this._node.getAttribute(name);
6779 };
6780 evt.setWikiInfo(wikiURL, wikiSection);
6781
6782 this._events.add(evt);
6783
6784 added = true;
6785 }
6786 node = node.nextSibling;
6787 }
6788
6789 if (added) {
6790 this._fire("onAddMany", []);
6791 }
6792 };
6793
6794
6795 Timeline.DefaultEventSource.prototype.loadJSON = function(data, url) {
6796 var base = this._getBaseURL(url);
6797 var added = false;
6798 if (data && data.events){
6799 var wikiURL = ("wikiURL" in data) ? data.wikiURL : null;
6800 var wikiSection = ("wikiSection" in data) ? data.wikiSection : null;
6801
6802 var dateTimeFormat = ("dateTimeFormat" in data) ? data.dateTimeFormat : null;
6803 var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
6804
6805 for (var i=0; i < data.events.length; i++){
6806 var evnt = data.events[i];
6807
6808 // New feature: attribute synonyms. The following attribute names are interchangable.
6809 // The shorter names enable smaller load files.
6810 // eid -- eventID
6811 // s -- start
6812 // e -- end
6813 // ls -- latestStart
6814 // ee -- earliestEnd
6815 // d -- description
6816 // de -- durationEvent
6817 // t -- title,
6818 // c -- classname
6819
6820 // Fixing issue 33:
6821 // instant event: default (for JSON only) is false. Or use values from isDuration or durationEvent
6822 // isDuration was negated (see issue 33, so keep that interpretation
6823 var instant = evnt.isDuration ||
6824 (('durationEvent' in evnt) && !evnt.durationEvent) ||
6825 (('de' in evnt) && !evnt.de);
6826 var evt = new Timeline.DefaultEventSource.Event({
6827 id: ("id" in evnt) ? evnt.id : undefined,
6828 start: parseDateTimeFunction(evnt.start || evnt.s),
6829 end: parseDateTimeFunction(evnt.end || evnt.e),
6830 latestStart: parseDateTimeFunction(evnt.latestStart || evnt.ls),
6831 earliestEnd: parseDateTimeFunction(evnt.earliestEnd || evnt.ee),
6832 instant: instant,
6833 text: evnt.title || evnt.t,
6834 description: evnt.description || evnt.d,
6835 image: this._resolveRelativeURL(evnt.image, base),
6836 link: this._resolveRelativeURL(evnt.link , base),
6837 icon: this._resolveRelativeURL(evnt.icon , base),
6838 color: evnt.color,
6839 textColor: evnt.textColor,
6840 hoverText: evnt.hoverText,
6841 classname: evnt.classname || evnt.c,
6842 tapeImage: evnt.tapeImage,
6843 tapeRepeat: evnt.tapeRepeat,
6844 caption: evnt.caption,
6845 eventID: evnt.eventID || evnt.eid,
6846 trackNum: evnt.trackNum
6847 });
6848 evt._obj = evnt;
6849 evt.getProperty = function(name) {
6850 return this._obj[name];
6851 };
6852 evt.setWikiInfo(wikiURL, wikiSection);
6853
6854 this._events.add(evt);
6855 added = true;
6856 }
6857 }
6858
6859 if (added) {
6860 this._fire("onAddMany", []);
6861 }
6862 };
6863
6864 /*
6865 * Contributed by Morten Frederiksen, http://www.wasab.dk/morten/
6866 */
6867 Timeline.DefaultEventSource.prototype.loadSPARQL = function(xml, url) {
6868 var base = this._getBaseURL(url);
6869
6870 var dateTimeFormat = 'iso8601';
6871 var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
6872
6873 if (xml == null) {
6874 return;
6875 }
6876
6877 /*
6878 * Find <results> tag
6879 */
6880 var node = xml.documentElement.firstChild;
6881 while (node != null && (node.nodeType != 1 || node.nodeName != 'results')) {
6882 node = node.nextSibling;
6883 }
6884
6885 var wikiURL = null;
6886 var wikiSection = null;
6887 if (node != null) {
6888 wikiURL = node.getAttribute("wiki-url");
6889 wikiSection = node.getAttribute("wiki-section");
6890
6891 node = node.firstChild;
6892 }
6893
6894 var added = false;
6895 while (node != null) {
6896 if (node.nodeType == 1) {
6897 var bindings = { };
6898 var binding = node.firstChild;
6899 while (binding != null) {
6900 if (binding.nodeType == 1 &&
6901 binding.firstChild != null &&
6902 binding.firstChild.nodeType == 1 &&
6903 binding.firstChild.firstChild != null &&
6904 binding.firstChild.firstChild.nodeType == 3) {
6905 bindings[binding.getAttribute('name')] = binding.firstChild.firstChild.nodeValue;
6906 }
6907 binding = binding.nextSibling;
6908 }
6909
6910 if (bindings["start"] == null && bindings["date"] != null) {
6911 bindings["start"] = bindings["date"];
6912 }
6913
6914 // instant event: default is true. Or use values from isDuration or durationEvent
6915 var instant = (bindings["isDuration"] === null &&
6916 bindings["durationEvent"] === null) ||
6917 bindings["isDuration"] == "false" ||
6918 bindings["durationEvent"] == "false";
6919
6920 var evt = new Timeline.DefaultEventSource.Event({
6921 id: bindings["id"],
6922 start: parseDateTimeFunction(bindings["start"]),
6923 end: parseDateTimeFunction(bindings["end"]),
6924 latestStart: parseDateTimeFunction(bindings["latestStart"]),
6925 earliestEnd: parseDateTimeFunction(bindings["earliestEnd"]),
6926 instant: instant, // instant
6927 text: bindings["title"], // text
6928 description: bindings["description"],
6929 image: this._resolveRelativeURL(bindings["image"], base),
6930 link: this._resolveRelativeURL(bindings["link"] , base),
6931 icon: this._resolveRelativeURL(bindings["icon"] , base),
6932 color: bindings["color"],
6933 textColor: bindings["textColor"],
6934 hoverText: bindings["hoverText"],
6935 caption: bindings["caption"],
6936 classname: bindings["classname"],
6937 tapeImage: bindings["tapeImage"],
6938 tapeRepeat: bindings["tapeRepeat"],
6939 eventID: bindings["eventID"],
6940 trackNum: bindings["trackNum"]
6941 });
6942 evt._bindings = bindings;
6943 evt.getProperty = function(name) {
6944 return this._bindings[name];
6945 };
6946 evt.setWikiInfo(wikiURL, wikiSection);
6947
6948 this._events.add(evt);
6949 added = true;
6950 }
6951 node = node.nextSibling;
6952 }
6953
6954 if (added) {
6955 this._fire("onAddMany", []);
6956 }
6957 };
6958
6959 Timeline.DefaultEventSource.prototype.add = function(evt) {
6960 this._events.add(evt);
6961 this._fire("onAddOne", [evt]);
6962 };
6963
6964 Timeline.DefaultEventSource.prototype.addMany = function(events) {
6965 for (var i = 0; i < events.length; i++) {
6966 this._events.add(events[i]);
6967 }
6968 this._fire("onAddMany", []);
6969 };
6970
6971 Timeline.DefaultEventSource.prototype.clear = function() {
6972 this._events.removeAll();
6973 this._fire("onClear", []);
6974 };
6975
6976 Timeline.DefaultEventSource.prototype.getEvent = function(id) {
6977 return this._events.getEvent(id);
6978 };
6979
6980 Timeline.DefaultEventSource.prototype.getEventIterator = function(startDate, endDate) {
6981 return this._events.getIterator(startDate, endDate);
6982 };
6983
6984 Timeline.DefaultEventSource.prototype.getEventReverseIterator = function(startDate, endDate) {
6985 return this._events.getReverseIterator(startDate, endDate);
6986 };
6987
6988 Timeline.DefaultEventSource.prototype.getAllEventIterator = function() {
6989 return this._events.getAllIterator();
6990 };
6991
6992 Timeline.DefaultEventSource.prototype.getCount = function() {
6993 return this._events.getCount();
6994 };
6995
6996 Timeline.DefaultEventSource.prototype.getEarliestDate = function() {
6997 return this._events.getEarliestDate();
6998 };
6999
7000 Timeline.DefaultEventSource.prototype.getLatestDate = function() {
7001 return this._events.getLatestDate();
7002 };
7003
7004 Timeline.DefaultEventSource.prototype._fire = function(handlerName, args) {
7005 for (var i = 0; i < this._listeners.length; i++) {
7006 var listener = this._listeners[i];
7007 if (handlerName in listener) {
7008 try {
7009 listener[handlerName].apply(listener, args);
7010 } catch (e) {
7011 SimileAjax.Debug.exception(e);
7012 }
7013 }
7014 }
7015 };
7016
7017 Timeline.DefaultEventSource.prototype._getBaseURL = function(url) {
7018 if (url.indexOf("://") < 0) {
7019 var url2 = this._getBaseURL(document.location.href);
7020 if (url.substr(0,1) == "/") {
7021 url = url2.substr(0, url2.indexOf("/", url2.indexOf("://") + 3)) + url;
7022 } else {
7023 url = url2 + url;
7024 }
7025 }
7026
7027 var i = url.lastIndexOf("/");
7028 if (i < 0) {
7029 return "";
7030 } else {
7031 return url.substr(0, i+1);
7032 }
7033 };
7034
7035 Timeline.DefaultEventSource.prototype._resolveRelativeURL = function(url, base) {
7036 if (url == null || url == "") {
7037 return url;
7038 } else if (url.indexOf("://") > 0) {
7039 return url;
7040 } else if (url.substr(0,1) == "/") {
7041 return base.substr(0, base.indexOf("/", base.indexOf("://") + 3)) + url;
7042 } else {
7043 return base + url;
7044 }
7045 };
7046
7047
7048 Timeline.DefaultEventSource.Event = function(args) {
7049 //
7050 // Attention developers!
7051 // If you add a new event attribute, please be sure to add it to
7052 // all three load functions: loadXML, loadSPARCL, loadJSON.
7053 // Thanks!
7054 //
7055 // args is a hash/object. It supports the following keys. Most are optional
7056 // id -- an internal id. Really shouldn't be used by events.
7057 // Timeline library clients should use eventID
7058 // eventID -- For use by library client when writing custom painters or
7059 // custom fillInfoBubble
7060 // start
7061 // end
7062 // latestStart
7063 // earliestEnd
7064 // instant -- boolean. Controls precise/non-precise logic & duration/instant issues
7065 // text -- event source attribute 'title' -- used as the label on Timelines and in bubbles.
7066 // description -- used in bubbles
7067 // image -- used in bubbles
7068 // link -- used in bubbles
7069 // icon -- on the Timeline
7070 // color -- Timeline label and tape color
7071 // textColor -- Timeline label color, overrides color attribute
7072 // hoverText -- deprecated, here for backwards compatibility.
7073 // Superceeded by caption
7074 // caption -- tooltip-like caption on the Timeline. Uses HTML title attribute
7075 // classname -- used to set classname in Timeline. Enables better CSS selector rules
7076 // tapeImage -- background image of the duration event's tape div on the Timeline
7077 // tapeRepeat -- repeat attribute for tapeImage. {repeat | repeat-x | repeat-y }
7078
7079 function cleanArg(arg) {
7080 // clean up an arg
7081 return (args[arg] != null && args[arg] != "") ? args[arg] : null;
7082 }
7083
7084 var id = args.id ? args.id.trim() : "";
7085 this._id = id.length > 0 ? id : Timeline.EventUtils.getNewEventID();
7086
7087 this._instant = args.instant || (args.end == null);
7088
7089 this._start = args.start;
7090 this._end = (args.end != null) ? args.end : args.start;
7091
7092 this._latestStart = (args.latestStart != null) ?
7093 args.latestStart : (args.instant ? this._end : this._start);
7094 this._earliestEnd = (args.earliestEnd != null) ? args.earliestEnd : this._end;
7095
7096 // check sanity of dates since incorrect dates will later cause calculation errors
7097 // when painting
7098 var err=[];
7099 if (this._start > this._latestStart) {
7100 this._latestStart = this._start;
7101 err.push("start is > latestStart");}
7102 if (this._start > this._earliestEnd) {
7103 this._earliestEnd = this._latestStart;
7104 err.push("start is > earliestEnd");}
7105 if (this._start > this._end) {
7106 this._end = this._earliestEnd;
7107 err.push("start is > end");}
7108 if (this._latestStart > this._earliestEnd) {
7109 this._earliestEnd = this._latestStart;
7110 err.push("latestStart is > earliestEnd");}
7111 if (this._latestStart > this._end) {
7112 this._end = this._earliestEnd;
7113 err.push("latestStart is > end");}
7114 if (this._earliestEnd > this._end) {
7115 this._end = this._earliestEnd;
7116 err.push("earliestEnd is > end");}
7117
7118 this._eventID = cleanArg('eventID');
7119 this._text = (args.text != null) ? SimileAjax.HTML.deEntify(args.text) : ""; // Change blank titles to ""
7120 if (err.length > 0) {
7121 this._text += " PROBLEM: " + err.join(", ");
7122 }
7123
7124 this._description = SimileAjax.HTML.deEntify(args.description);
7125 this._image = cleanArg('image');
7126 this._link = cleanArg('link');
7127 this._title = cleanArg('hoverText');
7128 this._title = cleanArg('caption');
7129
7130 this._icon = cleanArg('icon');
7131 this._color = cleanArg('color');
7132 this._textColor = cleanArg('textColor');
7133 this._classname = cleanArg('classname');
7134 this._tapeImage = cleanArg('tapeImage');
7135 this._tapeRepeat = cleanArg('tapeRepeat');
7136 this._trackNum = cleanArg('trackNum');
7137 if (this._trackNum != null) {
7138 this._trackNum = parseInt(this._trackNum);
7139 }
7140
7141 this._wikiURL = null;
7142 this._wikiSection = null;
7143 };
7144
7145 Timeline.DefaultEventSource.Event.prototype = {
7146 getID: function() { return this._id; },
7147
7148 isInstant: function() { return this._instant; },
7149 isImprecise: function() { return this._start != this._latestStart || this._end != this._earliestEnd; },
7150
7151 getStart: function() { return this._start; },
7152 getEnd: function() { return this._end; },
7153 getLatestStart: function() { return this._latestStart; },
7154 getEarliestEnd: function() { return this._earliestEnd; },
7155
7156 getEventID: function() { return this._eventID; },
7157 getText: function() { return this._text; }, // title
7158 getDescription: function() { return this._description; },
7159 getImage: function() { return this._image; },
7160 getLink: function() { return this._link; },
7161
7162 getIcon: function() { return this._icon; },
7163 getColor: function() { return this._color; },
7164 getTextColor: function() { return this._textColor; },
7165 getClassName: function() { return this._classname; },
7166 getTapeImage: function() { return this._tapeImage; },
7167 getTapeRepeat: function() { return this._tapeRepeat; },
7168 getTrackNum: function() { return this._trackNum; },
7169
7170 getProperty: function(name) { return null; },
7171
7172 getWikiURL: function() { return this._wikiURL; },
7173 getWikiSection: function() { return this._wikiSection; },
7174 setWikiInfo: function(wikiURL, wikiSection) {
7175 this._wikiURL = wikiURL;
7176 this._wikiSection = wikiSection;
7177 },
7178
7179 fillDescription: function(elmt) {
7180 elmt.innerHTML = this._description;
7181 },
7182 fillWikiInfo: function(elmt) {
7183 // Many bubbles will not support a wiki link.
7184 //
7185 // Strategy: assume no wiki link. If we do have
7186 // enough parameters for one, then create it.
7187 elmt.style.display = "none"; // default
7188
7189 if (this._wikiURL == null || this._wikiSection == null) {
7190 return; // EARLY RETURN
7191 }
7192
7193 // create the wikiID from the property or from the event text (the title)
7194 var wikiID = this.getProperty("wikiID");
7195 if (wikiID == null || wikiID.length == 0) {
7196 wikiID = this.getText(); // use the title as the backup wiki id
7197 }
7198
7199 if (wikiID == null || wikiID.length == 0) {
7200 return; // No wikiID. Thus EARLY RETURN
7201 }
7202
7203 // ready to go...
7204 elmt.style.display = "inline";
7205 wikiID = wikiID.replace(/\s/g, "_");
7206 var url = this._wikiURL + this._wikiSection.replace(/\s/g, "_") + "/" + wikiID;
7207 var a = document.createElement("a");
7208 a.href = url;
7209 a.target = "new";
7210 a.innerHTML = Timeline.strings[Timeline.clientLocale].wikiLinkLabel;
7211
7212 elmt.appendChild(document.createTextNode("["));
7213 elmt.appendChild(a);
7214 elmt.appendChild(document.createTextNode("]"));
7215 },
7216
7217 fillTime: function(elmt, labeller) {
7218 if (this._instant) {
7219 if (this.isImprecise()) {
7220 elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
7221 elmt.appendChild(elmt.ownerDocument.createElement("br"));
7222 elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
7223 } else {
7224 elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
7225 }
7226 } else {
7227 if (this.isImprecise()) {
7228 elmt.appendChild(elmt.ownerDocument.createTextNode(
7229 labeller.labelPrecise(this._start) + " ~ " + labeller.labelPrecise(this._latestStart)));
7230 elmt.appendChild(elmt.ownerDocument.createElement("br"));
7231 elmt.appendChild(elmt.ownerDocument.createTextNode(
7232 labeller.labelPrecise(this._earliestEnd) + " ~ " + labeller.labelPrecise(this._end)));
7233 } else {
7234 elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
7235 elmt.appendChild(elmt.ownerDocument.createElement("br"));
7236 elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
7237 }
7238 }
7239 },
7240
7241 fillInfoBubble: function(elmt, theme, labeller) {
7242 var doc = elmt.ownerDocument;
7243
7244 var title = this.getText();
7245 var link = this.getLink();
7246 var image = this.getImage();
7247
7248 if (image != null) {
7249 var img = doc.createElement("img");
7250 img.src = image;
7251
7252 theme.event.bubble.imageStyler(img);
7253 elmt.appendChild(img);
7254 }
7255
7256 var divTitle = doc.createElement("div");
7257 var textTitle = doc.createTextNode(title);
7258 if (link != null) {
7259 var a = doc.createElement("a");
7260 a.href = link;
7261 a.appendChild(textTitle);
7262 divTitle.appendChild(a);
7263 } else {
7264 divTitle.appendChild(textTitle);
7265 }
7266 theme.event.bubble.titleStyler(divTitle);
7267 elmt.appendChild(divTitle);
7268
7269 var divBody = doc.createElement("div");
7270 this.fillDescription(divBody);
7271 theme.event.bubble.bodyStyler(divBody);
7272 elmt.appendChild(divBody);
7273
7274 var divTime = doc.createElement("div");
7275 this.fillTime(divTime, labeller);
7276 theme.event.bubble.timeStyler(divTime);
7277 elmt.appendChild(divTime);
7278
7279 var divWiki = doc.createElement("div");
7280 this.fillWikiInfo(divWiki);
7281 theme.event.bubble.wikiStyler(divWiki);
7282 elmt.appendChild(divWiki);
7283 }
7284 };
7285
7286
7287 /*==================================================
7288 * Original Event Painter
7289 *==================================================
7290 */
7291
7292 /*==================================================
7293 *
7294 * To enable a single event listener to monitor everything
7295 * on a Timeline, we need a way to map from an event's icon,
7296 * label or tape element to the associated timeline, band and
7297 * specific event.
7298 *
7299 * Thus a set format is used for the id's of the
7300 * events' elements on the Timeline--
7301 *
7302 * element id format for labels, icons, tapes:
7303 * labels: label-tl-<timelineID>-<band_index>-<evt.id>
7304 * icons: icon-tl-<timelineID>-<band_index>-<evt.id>
7305 * tapes: tape1-tl-<timelineID>-<band_index>-<evt.id>
7306 * tape2-tl-<timelineID>-<band_index>-<evt.id>
7307 * // some events have more than one tape
7308 * highlight: highlight1-tl-<timelineID>-<band_index>-<evt.id>
7309 * highlight2-tl-<timelineID>-<band_index>-<evt.id>
7310 * // some events have more than one highlight div (future)
7311 * You can then retrieve the band/timeline objects and event object
7312 * by using Timeline.EventUtils.decodeEventElID
7313 *
7314 *==================================================
7315 */
7316
7317 /*
7318 * eventPaintListener functions receive calls about painting.
7319 * function(band, op, evt, els)
7320 * context: 'this' will be an OriginalEventPainter object.
7321 * It has properties and methods for obtaining
7322 * the relevant band, timeline, etc
7323 * band = the band being painted
7324 * op = 'paintStarting' // the painter is about to remove
7325 * all previously painted events, if any. It will
7326 * then start painting all of the visible events that
7327 * pass the filter.
7328 * evt = null, els = null
7329 * op = 'paintEnded' // the painter has finished painting
7330 * all of the visible events that passed the filter
7331 * evt = null, els = null
7332 * op = 'paintedEvent' // the painter just finished painting an event
7333 * evt = event just painted
7334 * els = array of painted elements' divs. Depending on the event,
7335 * the array could be just a tape or icon (if no label).
7336 * Or could include label, multiple tape divs (imprecise event),
7337 * highlight divs. The array is not ordered. The meaning of
7338 * each el is available by decoding the el's id
7339 * Note that there may be no paintedEvent calls if no events were visible
7340 * or passed the filter.
7341 */
7342
7343 Timeline.OriginalEventPainter = function(params) {
7344 this._params = params;
7345 this._onSelectListeners = [];
7346 this._eventPaintListeners = [];
7347
7348 this._filterMatcher = null;
7349 this._highlightMatcher = null;
7350 this._frc = null;
7351
7352 this._eventIdToElmt = {};
7353 };
7354
7355 Timeline.OriginalEventPainter.prototype.initialize = function(band, timeline) {
7356 this._band = band;
7357 this._timeline = timeline;
7358
7359 this._backLayer = null;
7360 this._eventLayer = null;
7361 this._lineLayer = null;
7362 this._highlightLayer = null;
7363
7364 this._eventIdToElmt = null;
7365 };
7366
7367 Timeline.OriginalEventPainter.prototype.getType = function() {
7368 return 'original';
7369 };
7370
7371 Timeline.OriginalEventPainter.prototype.supportsOrthogonalScrolling = function() {
7372 return true;
7373 };
7374
7375 Timeline.OriginalEventPainter.prototype.addOnSelectListener = function(listener) {
7376 this._onSelectListeners.push(listener);
7377 };
7378
7379 Timeline.OriginalEventPainter.prototype.removeOnSelectListener = function(listener) {
7380 for (var i = 0; i < this._onSelectListeners.length; i++) {
7381 if (this._onSelectListeners[i] == listener) {
7382 this._onSelectListeners.splice(i, 1);
7383 break;
7384 }
7385 }
7386 };
7387
7388 Timeline.OriginalEventPainter.prototype.addEventPaintListener = function(listener) {
7389 this._eventPaintListeners.push(listener);
7390 };
7391
7392 Timeline.OriginalEventPainter.prototype.removeEventPaintListener = function(listener) {
7393 for (var i = 0; i < this._eventPaintListeners.length; i++) {
7394 if (this._eventPaintListeners[i] == listener) {
7395 this._eventPaintListeners.splice(i, 1);
7396 break;
7397 }
7398 }
7399 };
7400
7401 Timeline.OriginalEventPainter.prototype.getFilterMatcher = function() {
7402 return this._filterMatcher;
7403 };
7404
7405 Timeline.OriginalEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
7406 this._filterMatcher = filterMatcher;
7407 };
7408
7409 Timeline.OriginalEventPainter.prototype.getHighlightMatcher = function() {
7410 return this._highlightMatcher;
7411 };
7412
7413 Timeline.OriginalEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
7414 this._highlightMatcher = highlightMatcher;
7415 };
7416
7417 Timeline.OriginalEventPainter.prototype.paint = function() {
7418 // Paints the events for a given section of the band--what is
7419 // visible on screen and some extra.
7420 var eventSource = this._band.getEventSource();
7421 if (eventSource == null) {
7422 return;
7423 }
7424
7425 this._eventIdToElmt = {};
7426 this._fireEventPaintListeners('paintStarting', null, null);
7427 this._prepareForPainting();
7428
7429 var metrics = this._computeMetrics();
7430 var minDate = this._band.getMinDate();
7431 var maxDate = this._band.getMaxDate();
7432
7433 var filterMatcher = (this._filterMatcher != null) ?
7434 this._filterMatcher :
7435 function(evt) { return true; };
7436 var highlightMatcher = (this._highlightMatcher != null) ?
7437 this._highlightMatcher :
7438 function(evt) { return -1; };
7439
7440 var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
7441 while (iterator.hasNext()) {
7442 var evt = iterator.next();
7443 if (filterMatcher(evt)) {
7444 this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
7445 }
7446 }
7447
7448 this._highlightLayer.style.display = "block";
7449 this._lineLayer.style.display = "block";
7450 this._eventLayer.style.display = "block";
7451 // update the band object for max number of tracks in this section of the ether
7452 this._band.updateEventTrackInfo(this._tracks.length, metrics.trackIncrement);
7453 this._fireEventPaintListeners('paintEnded', null, null);
7454
7455 this._setOrthogonalOffset(metrics);
7456 };
7457
7458 Timeline.OriginalEventPainter.prototype.softPaint = function() {
7459 this._setOrthogonalOffset(this._computeMetrics());
7460 };
7461
7462 Timeline.OriginalEventPainter.prototype.getOrthogonalExtent = function() {
7463 var metrics = this._computeMetrics();
7464 return 2 * metrics.trackOffset + this._tracks.length * metrics.trackIncrement;
7465 };
7466
7467 Timeline.OriginalEventPainter.prototype._setOrthogonalOffset = function(metrics) {
7468 var orthogonalOffset = this._band.getViewOrthogonalOffset();
7469
7470 this._highlightLayer.style.top =
7471 this._lineLayer.style.top =
7472 this._eventLayer.style.top =
7473 orthogonalOffset + "px";
7474 };
7475
7476 Timeline.OriginalEventPainter.prototype._computeMetrics = function() {
7477 var eventTheme = this._params.theme.event;
7478 var trackHeight = Math.max(eventTheme.track.height, eventTheme.tape.height +
7479 this._frc.getLineHeight());
7480 var metrics = {
7481 trackOffset: eventTheme.track.offset,
7482 trackHeight: trackHeight,
7483 trackGap: eventTheme.track.gap,
7484 trackIncrement: trackHeight + eventTheme.track.gap,
7485 icon: eventTheme.instant.icon,
7486 iconWidth: eventTheme.instant.iconWidth,
7487 iconHeight: eventTheme.instant.iconHeight,
7488 labelWidth: eventTheme.label.width,
7489 maxLabelChar: eventTheme.label.maxLabelChar,
7490 impreciseIconMargin: eventTheme.instant.impreciseIconMargin
7491 };
7492
7493 return metrics;
7494 };
7495
7496 Timeline.OriginalEventPainter.prototype._prepareForPainting = function() {
7497 // Remove everything previously painted: highlight, line and event layers.
7498 // Prepare blank layers for painting.
7499 var band = this._band;
7500
7501 if (this._backLayer == null) {
7502 this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
7503 this._backLayer.style.visibility = "hidden";
7504
7505 var eventLabelPrototype = document.createElement("span");
7506 eventLabelPrototype.className = "timeline-event-label";
7507 this._backLayer.appendChild(eventLabelPrototype);
7508 this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
7509 }
7510 this._frc.update();
7511 this._tracks = [];
7512
7513 if (this._highlightLayer != null) {
7514 band.removeLayerDiv(this._highlightLayer);
7515 }
7516 this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
7517 this._highlightLayer.style.display = "none";
7518
7519 if (this._lineLayer != null) {
7520 band.removeLayerDiv(this._lineLayer);
7521 }
7522 this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
7523 this._lineLayer.style.display = "none";
7524
7525 if (this._eventLayer != null) {
7526 band.removeLayerDiv(this._eventLayer);
7527 }
7528 this._eventLayer = band.createLayerDiv(115, "timeline-band-events");
7529 this._eventLayer.style.display = "none";
7530 };
7531
7532 Timeline.OriginalEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
7533 if (evt.isInstant()) {
7534 this.paintInstantEvent(evt, metrics, theme, highlightIndex);
7535 } else {
7536 this.paintDurationEvent(evt, metrics, theme, highlightIndex);
7537 }
7538 };
7539
7540 Timeline.OriginalEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
7541 if (evt.isImprecise()) {
7542 this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
7543 } else {
7544 this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
7545 }
7546 }
7547
7548 Timeline.OriginalEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
7549 if (evt.isImprecise()) {
7550 this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
7551 } else {
7552 this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
7553 }
7554 }
7555
7556 Timeline.OriginalEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
7557 var doc = this._timeline.getDocument();
7558 var text = evt.getText();
7559
7560 var startDate = evt.getStart();
7561 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
7562 var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
7563 var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
7564
7565 var labelDivClassName = this._getLabelDivClassName(evt);
7566 var labelSize = this._frc.computeSize(text, labelDivClassName);
7567 var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
7568 var labelRight = labelLeft + labelSize.width;
7569
7570 var rightEdge = labelRight;
7571 var track = this._findFreeTrack(evt, rightEdge);
7572
7573 var labelTop = Math.round(
7574 metrics.trackOffset + track * metrics.trackIncrement +
7575 metrics.trackHeight / 2 - labelSize.height / 2);
7576
7577 var iconElmtData = this._paintEventIcon(evt, track, iconLeftEdge, metrics, theme, 0);
7578 var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
7579 labelSize.height, theme, labelDivClassName, highlightIndex);
7580 var els = [iconElmtData.elmt, labelElmtData.elmt];
7581
7582 var self = this;
7583 var clickHandler = function(elmt, domEvt, target) {
7584 return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
7585 };
7586 SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
7587 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
7588
7589 var hDiv = this._createHighlightDiv(highlightIndex, iconElmtData, theme, evt);
7590 if (hDiv != null) {els.push(hDiv);}
7591 this._fireEventPaintListeners('paintedEvent', evt, els);
7592
7593
7594 this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
7595 this._tracks[track] = iconLeftEdge;
7596 };
7597
7598 Timeline.OriginalEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
7599 var doc = this._timeline.getDocument();
7600 var text = evt.getText();
7601
7602 var startDate = evt.getStart();
7603 var endDate = evt.getEnd();
7604 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
7605 var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
7606
7607 var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
7608 var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
7609
7610 var labelDivClassName = this._getLabelDivClassName(evt);
7611 var labelSize = this._frc.computeSize(text, labelDivClassName);
7612 var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
7613 var labelRight = labelLeft + labelSize.width;
7614
7615 var rightEdge = Math.max(labelRight, endPixel);
7616 var track = this._findFreeTrack(evt, rightEdge);
7617 var tapeHeight = theme.event.tape.height;
7618 var labelTop = Math.round(
7619 metrics.trackOffset + track * metrics.trackIncrement + tapeHeight);
7620
7621 var iconElmtData = this._paintEventIcon(evt, track, iconLeftEdge, metrics, theme, tapeHeight);
7622 var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
7623 labelSize.height, theme, labelDivClassName, highlightIndex);
7624
7625 var color = evt.getColor();
7626 color = color != null ? color : theme.event.instant.impreciseColor;
7627
7628 var tapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel,
7629 color, theme.event.instant.impreciseOpacity, metrics, theme, 0);
7630 var els = [iconElmtData.elmt, labelElmtData.elmt, tapeElmtData.elmt];
7631
7632 var self = this;
7633 var clickHandler = function(elmt, domEvt, target) {
7634 return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
7635 };
7636 SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
7637 SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
7638 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
7639
7640 var hDiv = this._createHighlightDiv(highlightIndex, iconElmtData, theme, evt);
7641 if (hDiv != null) {els.push(hDiv);}
7642 this._fireEventPaintListeners('paintedEvent', evt, els);
7643
7644 this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
7645 this._tracks[track] = iconLeftEdge;
7646 };
7647
7648 Timeline.OriginalEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
7649 var doc = this._timeline.getDocument();
7650 var text = evt.getText();
7651
7652 var startDate = evt.getStart();
7653 var endDate = evt.getEnd();
7654 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
7655 var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
7656
7657 var labelDivClassName = this._getLabelDivClassName(evt);
7658 var labelSize = this._frc.computeSize(text, labelDivClassName);
7659 var labelLeft = startPixel;
7660 var labelRight = labelLeft + labelSize.width;
7661
7662 var rightEdge = Math.max(labelRight, endPixel);
7663 var track = this._findFreeTrack(evt, rightEdge);
7664 var labelTop = Math.round(
7665 metrics.trackOffset + track * metrics.trackIncrement + theme.event.tape.height);
7666
7667 var color = evt.getColor();
7668 color = color != null ? color : theme.event.duration.color;
7669
7670 var tapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel, color, 100, metrics, theme, 0);
7671 var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width,
7672 labelSize.height, theme, labelDivClassName, highlightIndex);
7673 var els = [tapeElmtData.elmt, labelElmtData.elmt];
7674
7675 var self = this;
7676 var clickHandler = function(elmt, domEvt, target) {
7677 return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
7678 };
7679 SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
7680 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
7681
7682 var hDiv = this._createHighlightDiv(highlightIndex, tapeElmtData, theme, evt);
7683 if (hDiv != null) {els.push(hDiv);}
7684 this._fireEventPaintListeners('paintedEvent', evt, els);
7685
7686 this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
7687 this._tracks[track] = startPixel;
7688 };
7689
7690 Timeline.OriginalEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
7691 var doc = this._timeline.getDocument();
7692 var text = evt.getText();
7693
7694 var startDate = evt.getStart();
7695 var latestStartDate = evt.getLatestStart();
7696 var endDate = evt.getEnd();
7697 var earliestEndDate = evt.getEarliestEnd();
7698
7699 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
7700 var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
7701 var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
7702 var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
7703
7704 var labelDivClassName = this._getLabelDivClassName(evt);
7705 var labelSize = this._frc.computeSize(text, labelDivClassName);
7706 var labelLeft = latestStartPixel;
7707 var labelRight = labelLeft + labelSize.width;
7708
7709 var rightEdge = Math.max(labelRight, endPixel);
7710 var track = this._findFreeTrack(evt, rightEdge);
7711 var labelTop = Math.round(
7712 metrics.trackOffset + track * metrics.trackIncrement + theme.event.tape.height);
7713
7714 var color = evt.getColor();
7715 color = color != null ? color : theme.event.duration.color;
7716
7717 // Imprecise events can have two event tapes
7718 // The imprecise dates tape, uses opacity to be dimmer than precise dates
7719 var impreciseTapeElmtData = this._paintEventTape(evt, track, startPixel, endPixel,
7720 theme.event.duration.impreciseColor,
7721 theme.event.duration.impreciseOpacity, metrics, theme, 0);
7722 // The precise dates tape, regular (100%) opacity
7723 var tapeElmtData = this._paintEventTape(evt, track, latestStartPixel,
7724 earliestEndPixel, color, 100, metrics, theme, 1);
7725
7726 var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop,
7727 labelSize.width, labelSize.height, theme, labelDivClassName, highlightIndex);
7728 var els = [impreciseTapeElmtData.elmt, tapeElmtData.elmt, labelElmtData.elmt];
7729
7730 var self = this;
7731 var clickHandler = function(elmt, domEvt, target) {
7732 return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
7733 };
7734 SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
7735 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
7736
7737 var hDiv = this._createHighlightDiv(highlightIndex, tapeElmtData, theme, evt);
7738 if (hDiv != null) {els.push(hDiv);}
7739 this._fireEventPaintListeners('paintedEvent', evt, els);
7740
7741 this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
7742 this._tracks[track] = startPixel;
7743 };
7744
7745 Timeline.OriginalEventPainter.prototype._encodeEventElID = function(elType, evt) {
7746 return Timeline.EventUtils.encodeEventElID(this._timeline, this._band, elType, evt);
7747 };
7748
7749 Timeline.OriginalEventPainter.prototype._findFreeTrack = function(event, rightEdge) {
7750 var trackAttribute = event.getTrackNum();
7751 if (trackAttribute != null) {
7752 return trackAttribute; // early return since event includes track number
7753 }
7754
7755 // normal case: find an open track
7756 for (var i = 0; i < this._tracks.length; i++) {
7757 var t = this._tracks[i];
7758 if (t > rightEdge) {
7759 break;
7760 }
7761 }
7762 return i;
7763 };
7764
7765 Timeline.OriginalEventPainter.prototype._paintEventIcon = function(evt, iconTrack, left, metrics, theme, tapeHeight) {
7766 // If no tape, then paint the icon in the middle of the track.
7767 // If there is a tape, paint the icon below the tape + impreciseIconMargin
7768 var icon = evt.getIcon();
7769 icon = icon != null ? icon : metrics.icon;
7770
7771 var top; // top of the icon
7772 if (tapeHeight > 0) {
7773 top = metrics.trackOffset + iconTrack * metrics.trackIncrement +
7774 tapeHeight + metrics.impreciseIconMargin;
7775 } else {
7776 var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement +
7777 metrics.trackHeight / 2;
7778 top = Math.round(middle - metrics.iconHeight / 2);
7779 }
7780 var img = SimileAjax.Graphics.createTranslucentImage(icon);
7781 var iconDiv = this._timeline.getDocument().createElement("div");
7782 iconDiv.className = this._getElClassName('timeline-event-icon', evt, 'icon');
7783 iconDiv.id = this._encodeEventElID('icon', evt);
7784 iconDiv.style.left = left + "px";
7785 iconDiv.style.top = top + "px";
7786 iconDiv.appendChild(img);
7787
7788 if(evt._title != null)
7789 iconDiv.title = evt._title;
7790
7791 this._eventLayer.appendChild(iconDiv);
7792
7793 return {
7794 left: left,
7795 top: top,
7796 width: metrics.iconWidth,
7797 height: metrics.iconHeight,
7798 elmt: iconDiv
7799 };
7800 };
7801
7802 Timeline.OriginalEventPainter.prototype._paintEventLabel = function(evt, text, left, top, width,
7803 height, theme, labelDivClassName, highlightIndex) {
7804 var doc = this._timeline.getDocument();
7805
7806 var labelDiv = doc.createElement("div");
7807 labelDiv.className = labelDivClassName;
7808 labelDiv.id = this._encodeEventElID('label', evt);
7809 labelDiv.style.left = left + "px";
7810 labelDiv.style.width = width + "px";
7811 labelDiv.style.top = top + "px";
7812 labelDiv.innerHTML = text;
7813
7814 if(evt._title != null)
7815 labelDiv.title = evt._title;
7816
7817 var color = evt.getTextColor();
7818 if (color == null) {
7819 color = evt.getColor();
7820 }
7821 if (color != null) {
7822 labelDiv.style.color = color;
7823 }
7824 if (theme.event.highlightLabelBackground && highlightIndex >= 0) {
7825 labelDiv.style.background = this._getHighlightColor(highlightIndex, theme);
7826 }
7827
7828 this._eventLayer.appendChild(labelDiv);
7829
7830 return {
7831 left: left,
7832 top: top,
7833 width: width,
7834 height: height,
7835 elmt: labelDiv
7836 };
7837 };
7838
7839 Timeline.OriginalEventPainter.prototype._paintEventTape = function(
7840 evt, iconTrack, startPixel, endPixel, color, opacity, metrics, theme, tape_index) {
7841
7842 var tapeWidth = endPixel - startPixel;
7843 var tapeHeight = theme.event.tape.height;
7844 var top = metrics.trackOffset + iconTrack * metrics.trackIncrement;
7845
7846 var tapeDiv = this._timeline.getDocument().createElement("div");
7847 tapeDiv.className = this._getElClassName('timeline-event-tape', evt, 'tape');
7848 tapeDiv.id = this._encodeEventElID('tape' + tape_index, evt);
7849 tapeDiv.style.left = startPixel + "px";
7850 tapeDiv.style.width = tapeWidth + "px";
7851 tapeDiv.style.height = tapeHeight + "px";
7852 tapeDiv.style.top = top + "px";
7853
7854 if(evt._title != null)
7855 tapeDiv.title = evt._title;
7856
7857 if(color != null) {
7858 tapeDiv.style.backgroundColor = color;
7859 }
7860
7861 var backgroundImage = evt.getTapeImage();
7862 var backgroundRepeat = evt.getTapeRepeat();
7863 backgroundRepeat = backgroundRepeat != null ? backgroundRepeat : 'repeat';
7864 if(backgroundImage != null) {
7865 tapeDiv.style.backgroundImage = "url(" + backgroundImage + ")";
7866 tapeDiv.style.backgroundRepeat = backgroundRepeat;
7867 }
7868
7869 SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
7870
7871 this._eventLayer.appendChild(tapeDiv);
7872
7873 return {
7874 left: startPixel,
7875 top: top,
7876 width: tapeWidth,
7877 height: tapeHeight,
7878 elmt: tapeDiv
7879 };
7880 }
7881
7882 Timeline.OriginalEventPainter.prototype._getLabelDivClassName = function(evt) {
7883 return this._getElClassName('timeline-event-label', evt, 'label');
7884 };
7885
7886 Timeline.OriginalEventPainter.prototype._getElClassName = function(elClassName, evt, prefix) {
7887 // Prefix and '_' is added to the event's classname. Set to null for no prefix
7888 var evt_classname = evt.getClassName(),
7889 pieces = [];
7890
7891 if (evt_classname) {
7892 if (prefix) {pieces.push(prefix + '-' + evt_classname + ' ');}
7893 pieces.push(evt_classname + ' ');
7894 }
7895 pieces.push(elClassName);
7896 return(pieces.join(''));
7897 };
7898
7899 Timeline.OriginalEventPainter.prototype._getHighlightColor = function(highlightIndex, theme) {
7900 var highlightColors = theme.event.highlightColors;
7901 return highlightColors[Math.min(highlightIndex, highlightColors.length - 1)];
7902 };
7903
7904 Timeline.OriginalEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme, evt) {
7905 var div = null;
7906 if (highlightIndex >= 0) {
7907 var doc = this._timeline.getDocument();
7908 var color = this._getHighlightColor(highlightIndex, theme);
7909
7910 div = doc.createElement("div");
7911 div.className = this._getElClassName('timeline-event-highlight', evt, 'highlight');
7912 div.id = this._encodeEventElID('highlight0', evt); // in future will have other
7913 // highlight divs for tapes + icons
7914 div.style.position = "absolute";
7915 div.style.overflow = "hidden";
7916 div.style.left = (dimensions.left - 2) + "px";
7917 div.style.width = (dimensions.width + 4) + "px";
7918 div.style.top = (dimensions.top - 2) + "px";
7919 div.style.height = (dimensions.height + 4) + "px";
7920 div.style.background = color;
7921
7922 this._highlightLayer.appendChild(div);
7923 }
7924 return div;
7925 };
7926
7927 Timeline.OriginalEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
7928 var c = SimileAjax.DOM.getPageCoordinates(icon);
7929 this._showBubble(
7930 c.left + Math.ceil(icon.offsetWidth / 2),
7931 c.top + Math.ceil(icon.offsetHeight / 2),
7932 evt
7933 );
7934 this._fireOnSelect(evt.getID());
7935
7936 domEvt.cancelBubble = true;
7937 SimileAjax.DOM.cancelEvent(domEvt);
7938 return false;
7939 };
7940
7941 Timeline.OriginalEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
7942 if ("pageX" in domEvt) {
7943 var x = domEvt.pageX;
7944 var y = domEvt.pageY;
7945 } else {
7946 var c = SimileAjax.DOM.getPageCoordinates(target);
7947 var x = domEvt.offsetX + c.left;
7948 var y = domEvt.offsetY + c.top;
7949 }
7950 this._showBubble(x, y, evt);
7951 this._fireOnSelect(evt.getID());
7952
7953 domEvt.cancelBubble = true;
7954 SimileAjax.DOM.cancelEvent(domEvt);
7955 return false;
7956 };
7957
7958 Timeline.OriginalEventPainter.prototype.showBubble = function(evt) {
7959 var elmt = this._eventIdToElmt[evt.getID()];
7960 if (elmt) {
7961 var c = SimileAjax.DOM.getPageCoordinates(elmt);
7962 this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, evt);
7963 }
7964 };
7965
7966 Timeline.OriginalEventPainter.prototype._showBubble = function(x, y, evt) {
7967 var div = document.createElement("div");
7968 var themeBubble = this._params.theme.event.bubble;
7969 evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
7970
7971 SimileAjax.WindowManager.cancelPopups();
7972 SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y,
7973 themeBubble.width, null, themeBubble.maxHeight);
7974 };
7975
7976 Timeline.OriginalEventPainter.prototype._fireOnSelect = function(eventID) {
7977 for (var i = 0; i < this._onSelectListeners.length; i++) {
7978 this._onSelectListeners[i](eventID);
7979 }
7980 };
7981
7982 Timeline.OriginalEventPainter.prototype._fireEventPaintListeners = function(op, evt, els) {
7983 for (var i = 0; i < this._eventPaintListeners.length; i++) {
7984 this._eventPaintListeners[i](this._band, op, evt, els);
7985 }
7986 };
7987 /*==================================================
7988 * Detailed Event Painter
7989 *==================================================
7990 */
7991
7992 // Note: a number of features from original-painter
7993 // are not yet implemented in detailed painter.
7994 // Eg classname, id attributes for icons, labels, tapes
7995
7996 Timeline.DetailedEventPainter = function(params) {
7997 this._params = params;
7998 this._onSelectListeners = [];
7999
8000 this._filterMatcher = null;
8001 this._highlightMatcher = null;
8002 this._frc = null;
8003
8004 this._eventIdToElmt = {};
8005 };
8006
8007 Timeline.DetailedEventPainter.prototype.initialize = function(band, timeline) {
8008 this._band = band;
8009 this._timeline = timeline;
8010
8011 this._backLayer = null;
8012 this._eventLayer = null;
8013 this._lineLayer = null;
8014 this._highlightLayer = null;
8015
8016 this._eventIdToElmt = null;
8017 };
8018
8019 Timeline.DetailedEventPainter.prototype.getType = function() {
8020 return 'detailed';
8021 };
8022
8023 Timeline.DetailedEventPainter.prototype.addOnSelectListener = function(listener) {
8024 this._onSelectListeners.push(listener);
8025 };
8026
8027 Timeline.DetailedEventPainter.prototype.removeOnSelectListener = function(listener) {
8028 for (var i = 0; i < this._onSelectListeners.length; i++) {
8029 if (this._onSelectListeners[i] == listener) {
8030 this._onSelectListeners.splice(i, 1);
8031 break;
8032 }
8033 }
8034 };
8035
8036 Timeline.DetailedEventPainter.prototype.getFilterMatcher = function() {
8037 return this._filterMatcher;
8038 };
8039
8040 Timeline.DetailedEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
8041 this._filterMatcher = filterMatcher;
8042 };
8043
8044 Timeline.DetailedEventPainter.prototype.getHighlightMatcher = function() {
8045 return this._highlightMatcher;
8046 };
8047
8048 Timeline.DetailedEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
8049 this._highlightMatcher = highlightMatcher;
8050 };
8051
8052 Timeline.DetailedEventPainter.prototype.paint = function() {
8053 var eventSource = this._band.getEventSource();
8054 if (eventSource == null) {
8055 return;
8056 }
8057
8058 this._eventIdToElmt = {};
8059 this._prepareForPainting();
8060
8061 var eventTheme = this._params.theme.event;
8062 var trackHeight = Math.max(eventTheme.track.height, this._frc.getLineHeight());
8063 var metrics = {
8064 trackOffset: Math.round(this._band.getViewWidth() / 2 - trackHeight / 2),
8065 trackHeight: trackHeight,
8066 trackGap: eventTheme.track.gap,
8067 trackIncrement: trackHeight + eventTheme.track.gap,
8068 icon: eventTheme.instant.icon,
8069 iconWidth: eventTheme.instant.iconWidth,
8070 iconHeight: eventTheme.instant.iconHeight,
8071 labelWidth: eventTheme.label.width
8072 }
8073
8074 var minDate = this._band.getMinDate();
8075 var maxDate = this._band.getMaxDate();
8076
8077 var filterMatcher = (this._filterMatcher != null) ?
8078 this._filterMatcher :
8079 function(evt) { return true; };
8080 var highlightMatcher = (this._highlightMatcher != null) ?
8081 this._highlightMatcher :
8082 function(evt) { return -1; };
8083
8084 var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
8085 while (iterator.hasNext()) {
8086 var evt = iterator.next();
8087 if (filterMatcher(evt)) {
8088 this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
8089 }
8090 }
8091
8092 this._highlightLayer.style.display = "block";
8093 this._lineLayer.style.display = "block";
8094 this._eventLayer.style.display = "block";
8095 // update the band object for max number of tracks in this section of the ether
8096 this._band.updateEventTrackInfo(this._lowerTracks.length + this._upperTracks.length,
8097 metrics.trackIncrement);
8098 };
8099
8100 Timeline.DetailedEventPainter.prototype.softPaint = function() {
8101 };
8102
8103 Timeline.DetailedEventPainter.prototype._prepareForPainting = function() {
8104 var band = this._band;
8105
8106 if (this._backLayer == null) {
8107 this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
8108 this._backLayer.style.visibility = "hidden";
8109
8110 var eventLabelPrototype = document.createElement("span");
8111 eventLabelPrototype.className = "timeline-event-label";
8112 this._backLayer.appendChild(eventLabelPrototype);
8113 this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
8114 }
8115 this._frc.update();
8116 this._lowerTracks = [];
8117 this._upperTracks = [];
8118
8119 if (this._highlightLayer != null) {
8120 band.removeLayerDiv(this._highlightLayer);
8121 }
8122 this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
8123 this._highlightLayer.style.display = "none";
8124
8125 if (this._lineLayer != null) {
8126 band.removeLayerDiv(this._lineLayer);
8127 }
8128 this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
8129 this._lineLayer.style.display = "none";
8130
8131 if (this._eventLayer != null) {
8132 band.removeLayerDiv(this._eventLayer);
8133 }
8134 this._eventLayer = band.createLayerDiv(110, "timeline-band-events");
8135 this._eventLayer.style.display = "none";
8136 };
8137
8138 Timeline.DetailedEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
8139 if (evt.isInstant()) {
8140 this.paintInstantEvent(evt, metrics, theme, highlightIndex);
8141 } else {
8142 this.paintDurationEvent(evt, metrics, theme, highlightIndex);
8143 }
8144 };
8145
8146 Timeline.DetailedEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
8147 if (evt.isImprecise()) {
8148 this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
8149 } else {
8150 this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
8151 }
8152 }
8153
8154 Timeline.DetailedEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
8155 if (evt.isImprecise()) {
8156 this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
8157 } else {
8158 this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
8159 }
8160 }
8161
8162 Timeline.DetailedEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
8163 var doc = this._timeline.getDocument();
8164 var text = evt.getText();
8165
8166 var startDate = evt.getStart();
8167 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
8168 var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
8169 var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
8170
8171 var labelSize = this._frc.computeSize(text);
8172 var iconTrack = this._findFreeTrackForSolid(iconRightEdge, startPixel);
8173 var iconElmtData = this._paintEventIcon(evt, iconTrack, iconLeftEdge, metrics, theme);
8174
8175 var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
8176 var labelTrack = iconTrack;
8177
8178 var iconTrackData = this._getTrackData(iconTrack);
8179 if (Math.min(iconTrackData.solid, iconTrackData.text) >= labelLeft + labelSize.width) { // label on the same track, to the right of icon
8180 iconTrackData.solid = iconLeftEdge;
8181 iconTrackData.text = labelLeft;
8182 } else { // label on a different track, below icon
8183 iconTrackData.solid = iconLeftEdge;
8184
8185 labelLeft = startPixel + theme.event.label.offsetFromLine;
8186 labelTrack = this._findFreeTrackForText(iconTrack, labelLeft + labelSize.width, function(t) { t.line = startPixel - 2; });
8187 this._getTrackData(labelTrack).text = iconLeftEdge;
8188
8189 this._paintEventLine(evt, startPixel, iconTrack, labelTrack, metrics, theme);
8190 }
8191
8192 var labelTop = Math.round(
8193 metrics.trackOffset + labelTrack * metrics.trackIncrement +
8194 metrics.trackHeight / 2 - labelSize.height / 2);
8195
8196 var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
8197
8198 var self = this;
8199 var clickHandler = function(elmt, domEvt, target) {
8200 return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
8201 };
8202 SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
8203 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
8204
8205 this._createHighlightDiv(highlightIndex, iconElmtData, theme);
8206
8207 this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
8208 };
8209
8210 Timeline.DetailedEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
8211 var doc = this._timeline.getDocument();
8212 var text = evt.getText();
8213
8214 var startDate = evt.getStart();
8215 var endDate = evt.getEnd();
8216 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
8217 var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
8218
8219 var iconRightEdge = Math.round(startPixel + metrics.iconWidth / 2);
8220 var iconLeftEdge = Math.round(startPixel - metrics.iconWidth / 2);
8221
8222 var labelSize = this._frc.computeSize(text);
8223 var iconTrack = this._findFreeTrackForSolid(endPixel, startPixel);
8224
8225 var tapeElmtData = this._paintEventTape(evt, iconTrack, startPixel, endPixel,
8226 theme.event.instant.impreciseColor, theme.event.instant.impreciseOpacity, metrics, theme);
8227 var iconElmtData = this._paintEventIcon(evt, iconTrack, iconLeftEdge, metrics, theme);
8228
8229 var iconTrackData = this._getTrackData(iconTrack);
8230 iconTrackData.solid = iconLeftEdge;
8231
8232 var labelLeft = iconRightEdge + theme.event.label.offsetFromLine;
8233 var labelRight = labelLeft + labelSize.width;
8234 var labelTrack;
8235 if (labelRight < endPixel) {
8236 labelTrack = iconTrack;
8237 } else {
8238 labelLeft = startPixel + theme.event.label.offsetFromLine;
8239 labelRight = labelLeft + labelSize.width;
8240
8241 labelTrack = this._findFreeTrackForText(iconTrack, labelRight, function(t) { t.line = startPixel - 2; });
8242 this._getTrackData(labelTrack).text = iconLeftEdge;
8243
8244 this._paintEventLine(evt, startPixel, iconTrack, labelTrack, metrics, theme);
8245 }
8246 var labelTop = Math.round(
8247 metrics.trackOffset + labelTrack * metrics.trackIncrement +
8248 metrics.trackHeight / 2 - labelSize.height / 2);
8249
8250 var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
8251
8252 var self = this;
8253 var clickHandler = function(elmt, domEvt, target) {
8254 return self._onClickInstantEvent(iconElmtData.elmt, domEvt, evt);
8255 };
8256 SimileAjax.DOM.registerEvent(iconElmtData.elmt, "mousedown", clickHandler);
8257 SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
8258 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
8259
8260 this._createHighlightDiv(highlightIndex, iconElmtData, theme);
8261
8262 this._eventIdToElmt[evt.getID()] = iconElmtData.elmt;
8263 };
8264
8265 Timeline.DetailedEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
8266 var doc = this._timeline.getDocument();
8267 var text = evt.getText();
8268
8269 var startDate = evt.getStart();
8270 var endDate = evt.getEnd();
8271 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
8272 var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
8273
8274 var labelSize = this._frc.computeSize(text);
8275 var tapeTrack = this._findFreeTrackForSolid(endPixel);
8276 var color = evt.getColor();
8277 color = color != null ? color : theme.event.duration.color;
8278
8279 var tapeElmtData = this._paintEventTape(evt, tapeTrack, startPixel, endPixel, color, 100, metrics, theme);
8280
8281 var tapeTrackData = this._getTrackData(tapeTrack);
8282 tapeTrackData.solid = startPixel;
8283
8284 var labelLeft = startPixel + theme.event.label.offsetFromLine;
8285 var labelTrack = this._findFreeTrackForText(tapeTrack, labelLeft + labelSize.width, function(t) { t.line = startPixel - 2; });
8286 this._getTrackData(labelTrack).text = startPixel - 2;
8287
8288 this._paintEventLine(evt, startPixel, tapeTrack, labelTrack, metrics, theme);
8289
8290 var labelTop = Math.round(
8291 metrics.trackOffset + labelTrack * metrics.trackIncrement +
8292 metrics.trackHeight / 2 - labelSize.height / 2);
8293
8294 var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
8295
8296 var self = this;
8297 var clickHandler = function(elmt, domEvt, target) {
8298 return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
8299 };
8300 SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
8301 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
8302
8303 this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
8304
8305 this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
8306 };
8307
8308 Timeline.DetailedEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
8309 var doc = this._timeline.getDocument();
8310 var text = evt.getText();
8311
8312 var startDate = evt.getStart();
8313 var latestStartDate = evt.getLatestStart();
8314 var endDate = evt.getEnd();
8315 var earliestEndDate = evt.getEarliestEnd();
8316
8317 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
8318 var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
8319 var endPixel = Math.round(this._band.dateToPixelOffset(endDate));
8320 var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
8321
8322 var labelSize = this._frc.computeSize(text);
8323 var tapeTrack = this._findFreeTrackForSolid(endPixel);
8324 var color = evt.getColor();
8325 color = color != null ? color : theme.event.duration.color;
8326
8327 var impreciseTapeElmtData = this._paintEventTape(evt, tapeTrack, startPixel, endPixel,
8328 theme.event.duration.impreciseColor, theme.event.duration.impreciseOpacity, metrics, theme);
8329 var tapeElmtData = this._paintEventTape(evt, tapeTrack, latestStartPixel, earliestEndPixel, color, 100, metrics, theme);
8330
8331 var tapeTrackData = this._getTrackData(tapeTrack);
8332 tapeTrackData.solid = startPixel;
8333
8334 var labelLeft = latestStartPixel + theme.event.label.offsetFromLine;
8335 var labelTrack = this._findFreeTrackForText(tapeTrack, labelLeft + labelSize.width, function(t) { t.line = latestStartPixel - 2; });
8336 this._getTrackData(labelTrack).text = latestStartPixel - 2;
8337
8338 this._paintEventLine(evt, latestStartPixel, tapeTrack, labelTrack, metrics, theme);
8339
8340 var labelTop = Math.round(
8341 metrics.trackOffset + labelTrack * metrics.trackIncrement +
8342 metrics.trackHeight / 2 - labelSize.height / 2);
8343
8344 var labelElmtData = this._paintEventLabel(evt, text, labelLeft, labelTop, labelSize.width, labelSize.height, theme);
8345
8346 var self = this;
8347 var clickHandler = function(elmt, domEvt, target) {
8348 return self._onClickDurationEvent(tapeElmtData.elmt, domEvt, evt);
8349 };
8350 SimileAjax.DOM.registerEvent(tapeElmtData.elmt, "mousedown", clickHandler);
8351 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
8352
8353 this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
8354
8355 this._eventIdToElmt[evt.getID()] = tapeElmtData.elmt;
8356 };
8357
8358 Timeline.DetailedEventPainter.prototype._findFreeTrackForSolid = function(solidEdge, softEdge) {
8359 for (var i = 0; true; i++) {
8360 if (i < this._lowerTracks.length) {
8361 var t = this._lowerTracks[i];
8362 if (Math.min(t.solid, t.text) > solidEdge && (!(softEdge) || t.line > softEdge)) {
8363 return i;
8364 }
8365 } else {
8366 this._lowerTracks.push({
8367 solid: Number.POSITIVE_INFINITY,
8368 text: Number.POSITIVE_INFINITY,
8369 line: Number.POSITIVE_INFINITY
8370 });
8371
8372 return i;
8373 }
8374
8375 if (i < this._upperTracks.length) {
8376 var t = this._upperTracks[i];
8377 if (Math.min(t.solid, t.text) > solidEdge && (!(softEdge) || t.line > softEdge)) {
8378 return -1 - i;
8379 }
8380 } else {
8381 this._upperTracks.push({
8382 solid: Number.POSITIVE_INFINITY,
8383 text: Number.POSITIVE_INFINITY,
8384 line: Number.POSITIVE_INFINITY
8385 });
8386
8387 return -1 - i;
8388 }
8389 }
8390 };
8391
8392 Timeline.DetailedEventPainter.prototype._findFreeTrackForText = function(fromTrack, edge, occupiedTrackVisitor) {
8393 var extendUp;
8394 var index;
8395 var firstIndex;
8396 var result;
8397
8398 if (fromTrack < 0) {
8399 extendUp = true;
8400 firstIndex = -fromTrack;
8401
8402 index = this._findFreeUpperTrackForText(firstIndex, edge);
8403 result = -1 - index;
8404 } else if (fromTrack > 0) {
8405 extendUp = false;
8406 firstIndex = fromTrack + 1;
8407
8408 index = this._findFreeLowerTrackForText(firstIndex, edge);
8409 result = index;
8410 } else {
8411 var upIndex = this._findFreeUpperTrackForText(0, edge);
8412 var downIndex = this._findFreeLowerTrackForText(1, edge);
8413
8414 if (downIndex - 1 <= upIndex) {
8415 extendUp = false;
8416 firstIndex = 1;
8417 index = downIndex;
8418 result = index;
8419 } else {
8420 extendUp = true;
8421 firstIndex = 0;
8422 index = upIndex;
8423 result = -1 - index;
8424 }
8425 }
8426
8427 if (extendUp) {
8428 if (index == this._upperTracks.length) {
8429 this._upperTracks.push({
8430 solid: Number.POSITIVE_INFINITY,
8431 text: Number.POSITIVE_INFINITY,
8432 line: Number.POSITIVE_INFINITY
8433 });
8434 }
8435 for (var i = firstIndex; i < index; i++) {
8436 occupiedTrackVisitor(this._upperTracks[i]);
8437 }
8438 } else {
8439 if (index == this._lowerTracks.length) {
8440 this._lowerTracks.push({
8441 solid: Number.POSITIVE_INFINITY,
8442 text: Number.POSITIVE_INFINITY,
8443 line: Number.POSITIVE_INFINITY
8444 });
8445 }
8446 for (var i = firstIndex; i < index; i++) {
8447 occupiedTrackVisitor(this._lowerTracks[i]);
8448 }
8449 }
8450 return result;
8451 };
8452
8453 Timeline.DetailedEventPainter.prototype._findFreeLowerTrackForText = function(index, edge) {
8454 for (; index < this._lowerTracks.length; index++) {
8455 var t = this._lowerTracks[index];
8456 if (Math.min(t.solid, t.text) >= edge) {
8457 break;
8458 }
8459 }
8460 return index;
8461 };
8462
8463 Timeline.DetailedEventPainter.prototype._findFreeUpperTrackForText = function(index, edge) {
8464 for (; index < this._upperTracks.length; index++) {
8465 var t = this._upperTracks[index];
8466 if (Math.min(t.solid, t.text) >= edge) {
8467 break;
8468 }
8469 }
8470 return index;
8471 };
8472
8473 Timeline.DetailedEventPainter.prototype._getTrackData = function(index) {
8474 return (index < 0) ? this._upperTracks[-index - 1] : this._lowerTracks[index];
8475 };
8476
8477 Timeline.DetailedEventPainter.prototype._paintEventLine = function(evt, left, startTrack, endTrack, metrics, theme) {
8478 var top = Math.round(metrics.trackOffset + startTrack * metrics.trackIncrement + metrics.trackHeight / 2);
8479 var height = Math.round(Math.abs(endTrack - startTrack) * metrics.trackIncrement);
8480
8481 var lineStyle = "1px solid " + theme.event.label.lineColor;
8482 var lineDiv = this._timeline.getDocument().createElement("div");
8483 lineDiv.style.position = "absolute";
8484 lineDiv.style.left = left + "px";
8485 lineDiv.style.width = theme.event.label.offsetFromLine + "px";
8486 lineDiv.style.height = height + "px";
8487 if (startTrack > endTrack) {
8488 lineDiv.style.top = (top - height) + "px";
8489 lineDiv.style.borderTop = lineStyle;
8490 } else {
8491 lineDiv.style.top = top + "px";
8492 lineDiv.style.borderBottom = lineStyle;
8493 }
8494 lineDiv.style.borderLeft = lineStyle;
8495 this._lineLayer.appendChild(lineDiv);
8496 };
8497
8498 Timeline.DetailedEventPainter.prototype._paintEventIcon = function(evt, iconTrack, left, metrics, theme) {
8499 var icon = evt.getIcon();
8500 icon = icon != null ? icon : metrics.icon;
8501
8502 var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement + metrics.trackHeight / 2;
8503 var top = Math.round(middle - metrics.iconHeight / 2);
8504
8505 var img = SimileAjax.Graphics.createTranslucentImage(icon);
8506 var iconDiv = this._timeline.getDocument().createElement("div");
8507 iconDiv.style.position = "absolute";
8508 iconDiv.style.left = left + "px";
8509 iconDiv.style.top = top + "px";
8510 iconDiv.appendChild(img);
8511 iconDiv.style.cursor = "pointer";
8512
8513 if(evt._title != null)
8514 iconDiv.title = evt._title
8515
8516 this._eventLayer.appendChild(iconDiv);
8517
8518 return {
8519 left: left,
8520 top: top,
8521 width: metrics.iconWidth,
8522 height: metrics.iconHeight,
8523 elmt: iconDiv
8524 };
8525 };
8526
8527 Timeline.DetailedEventPainter.prototype._paintEventLabel = function(evt, text, left, top, width, height, theme) {
8528 var doc = this._timeline.getDocument();
8529
8530 var labelBackgroundDiv = doc.createElement("div");
8531 labelBackgroundDiv.style.position = "absolute";
8532 labelBackgroundDiv.style.left = left + "px";
8533 labelBackgroundDiv.style.width = width + "px";
8534 labelBackgroundDiv.style.top = top + "px";
8535 labelBackgroundDiv.style.height = height + "px";
8536 labelBackgroundDiv.style.backgroundColor = theme.event.label.backgroundColor;
8537 SimileAjax.Graphics.setOpacity(labelBackgroundDiv, theme.event.label.backgroundOpacity);
8538 this._eventLayer.appendChild(labelBackgroundDiv);
8539
8540 var labelDiv = doc.createElement("div");
8541 labelDiv.style.position = "absolute";
8542 labelDiv.style.left = left + "px";
8543 labelDiv.style.width = width + "px";
8544 labelDiv.style.top = top + "px";
8545 labelDiv.innerHTML = text;
8546 labelDiv.style.cursor = "pointer";
8547
8548 if(evt._title != null)
8549 labelDiv.title = evt._title;
8550
8551 var color = evt.getTextColor();
8552 if (color == null) {
8553 color = evt.getColor();
8554 }
8555 if (color != null) {
8556 labelDiv.style.color = color;
8557 }
8558
8559 this._eventLayer.appendChild(labelDiv);
8560
8561 return {
8562 left: left,
8563 top: top,
8564 width: width,
8565 height: height,
8566 elmt: labelDiv
8567 };
8568 };
8569
8570 Timeline.DetailedEventPainter.prototype._paintEventTape = function(
8571 evt, iconTrack, startPixel, endPixel, color, opacity, metrics, theme) {
8572
8573 var tapeWidth = endPixel - startPixel;
8574 var tapeHeight = theme.event.tape.height;
8575 var middle = metrics.trackOffset + iconTrack * metrics.trackIncrement + metrics.trackHeight / 2;
8576 var top = Math.round(middle - tapeHeight / 2);
8577
8578 var tapeDiv = this._timeline.getDocument().createElement("div");
8579 tapeDiv.style.position = "absolute";
8580 tapeDiv.style.left = startPixel + "px";
8581 tapeDiv.style.width = tapeWidth + "px";
8582 tapeDiv.style.top = top + "px";
8583 tapeDiv.style.height = tapeHeight + "px";
8584 tapeDiv.style.backgroundColor = color;
8585 tapeDiv.style.overflow = "hidden";
8586 tapeDiv.style.cursor = "pointer";
8587
8588 if(evt._title != null)
8589 tapeDiv.title = evt._title;
8590
8591 SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
8592
8593 this._eventLayer.appendChild(tapeDiv);
8594
8595 return {
8596 left: startPixel,
8597 top: top,
8598 width: tapeWidth,
8599 height: tapeHeight,
8600 elmt: tapeDiv
8601 };
8602 }
8603
8604 Timeline.DetailedEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
8605 if (highlightIndex >= 0) {
8606 var doc = this._timeline.getDocument();
8607 var eventTheme = theme.event;
8608
8609 var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
8610
8611 var div = doc.createElement("div");
8612 div.style.position = "absolute";
8613 div.style.overflow = "hidden";
8614 div.style.left = (dimensions.left - 2) + "px";
8615 div.style.width = (dimensions.width + 4) + "px";
8616 div.style.top = (dimensions.top - 2) + "px";
8617 div.style.height = (dimensions.height + 4) + "px";
8618 div.style.background = color;
8619
8620 this._highlightLayer.appendChild(div);
8621 }
8622 };
8623
8624 Timeline.DetailedEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
8625 var c = SimileAjax.DOM.getPageCoordinates(icon);
8626 this._showBubble(
8627 c.left + Math.ceil(icon.offsetWidth / 2),
8628 c.top + Math.ceil(icon.offsetHeight / 2),
8629 evt
8630 );
8631 this._fireOnSelect(evt.getID());
8632
8633 domEvt.cancelBubble = true;
8634 SimileAjax.DOM.cancelEvent(domEvt);
8635 return false;
8636 };
8637
8638 Timeline.DetailedEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
8639 if ("pageX" in domEvt) {
8640 var x = domEvt.pageX;
8641 var y = domEvt.pageY;
8642 } else {
8643 var c = SimileAjax.DOM.getPageCoordinates(target);
8644 var x = domEvt.offsetX + c.left;
8645 var y = domEvt.offsetY + c.top;
8646 }
8647 this._showBubble(x, y, evt);
8648 this._fireOnSelect(evt.getID());
8649
8650 domEvt.cancelBubble = true;
8651 SimileAjax.DOM.cancelEvent(domEvt);
8652 return false;
8653 };
8654
8655 Timeline.DetailedEventPainter.prototype.showBubble = function(evt) {
8656 var elmt = this._eventIdToElmt[evt.getID()];
8657 if (elmt) {
8658 var c = SimileAjax.DOM.getPageCoordinates(elmt);
8659 this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, evt);
8660 }
8661 };
8662
8663 Timeline.DetailedEventPainter.prototype._showBubble = function(x, y, evt) {
8664 var div = document.createElement("div");
8665 var themeBubble = this._params.theme.event.bubble;
8666 evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
8667
8668 SimileAjax.WindowManager.cancelPopups();
8669 SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y,
8670 themeBubble.width, null, themeBubble.maxHeight);
8671 };
8672
8673 Timeline.DetailedEventPainter.prototype._fireOnSelect = function(eventID) {
8674 for (var i = 0; i < this._onSelectListeners.length; i++) {
8675 this._onSelectListeners[i](eventID);
8676 }
8677 };
8678 /*==================================================
8679 * Overview Event Painter
8680 *==================================================
8681 */
8682
8683 Timeline.OverviewEventPainter = function(params) {
8684 this._params = params;
8685 this._onSelectListeners = [];
8686
8687 this._filterMatcher = null;
8688 this._highlightMatcher = null;
8689 };
8690
8691 Timeline.OverviewEventPainter.prototype.initialize = function(band, timeline) {
8692 this._band = band;
8693 this._timeline = timeline;
8694
8695 this._eventLayer = null;
8696 this._highlightLayer = null;
8697 };
8698
8699 Timeline.OverviewEventPainter.prototype.getType = function() {
8700 return 'overview';
8701 };
8702
8703 Timeline.OverviewEventPainter.prototype.addOnSelectListener = function(listener) {
8704 this._onSelectListeners.push(listener);
8705 };
8706
8707 Timeline.OverviewEventPainter.prototype.removeOnSelectListener = function(listener) {
8708 for (var i = 0; i < this._onSelectListeners.length; i++) {
8709 if (this._onSelectListeners[i] == listener) {
8710 this._onSelectListeners.splice(i, 1);
8711 break;
8712 }
8713 }
8714 };
8715
8716 Timeline.OverviewEventPainter.prototype.getFilterMatcher = function() {
8717 return this._filterMatcher;
8718 };
8719
8720 Timeline.OverviewEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
8721 this._filterMatcher = filterMatcher;
8722 };
8723
8724 Timeline.OverviewEventPainter.prototype.getHighlightMatcher = function() {
8725 return this._highlightMatcher;
8726 };
8727
8728 Timeline.OverviewEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
8729 this._highlightMatcher = highlightMatcher;
8730 };
8731
8732 Timeline.OverviewEventPainter.prototype.paint = function() {
8733 var eventSource = this._band.getEventSource();
8734 if (eventSource == null) {
8735 return;
8736 }
8737
8738 this._prepareForPainting();
8739
8740 var eventTheme = this._params.theme.event;
8741 var metrics = {
8742 trackOffset: eventTheme.overviewTrack.offset,
8743 trackHeight: eventTheme.overviewTrack.height,
8744 trackGap: eventTheme.overviewTrack.gap,
8745 trackIncrement: eventTheme.overviewTrack.height + eventTheme.overviewTrack.gap
8746 }
8747
8748 var minDate = this._band.getMinDate();
8749 var maxDate = this._band.getMaxDate();
8750
8751 var filterMatcher = (this._filterMatcher != null) ?
8752 this._filterMatcher :
8753 function(evt) { return true; };
8754 var highlightMatcher = (this._highlightMatcher != null) ?
8755 this._highlightMatcher :
8756 function(evt) { return -1; };
8757
8758 var iterator = eventSource.getEventReverseIterator(minDate, maxDate);
8759 while (iterator.hasNext()) {
8760 var evt = iterator.next();
8761 if (filterMatcher(evt)) {
8762 this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
8763 }
8764 }
8765
8766 this._highlightLayer.style.display = "block";
8767 this._eventLayer.style.display = "block";
8768 // update the band object for max number of tracks in this section of the ether
8769 this._band.updateEventTrackInfo(this._tracks.length, metrics.trackIncrement);
8770 };
8771
8772 Timeline.OverviewEventPainter.prototype.softPaint = function() {
8773 };
8774
8775 Timeline.OverviewEventPainter.prototype._prepareForPainting = function() {
8776 var band = this._band;
8777
8778 this._tracks = [];
8779
8780 if (this._highlightLayer != null) {
8781 band.removeLayerDiv(this._highlightLayer);
8782 }
8783 this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
8784 this._highlightLayer.style.display = "none";
8785
8786 if (this._eventLayer != null) {
8787 band.removeLayerDiv(this._eventLayer);
8788 }
8789 this._eventLayer = band.createLayerDiv(110, "timeline-band-events");
8790 this._eventLayer.style.display = "none";
8791 };
8792
8793 Timeline.OverviewEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
8794 if (evt.isInstant()) {
8795 this.paintInstantEvent(evt, metrics, theme, highlightIndex);
8796 } else {
8797 this.paintDurationEvent(evt, metrics, theme, highlightIndex);
8798 }
8799 };
8800
8801 Timeline.OverviewEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
8802 var startDate = evt.getStart();
8803 var startPixel = Math.round(this._band.dateToPixelOffset(startDate));
8804
8805 var color = evt.getColor(),
8806 klassName = evt.getClassName();
8807 if (klassName) {
8808 color = null;
8809 } else {
8810 color = color != null ? color : theme.event.duration.color;
8811 }
8812
8813 var tickElmtData = this._paintEventTick(evt, startPixel, color, 100, metrics, theme);
8814
8815 this._createHighlightDiv(highlightIndex, tickElmtData, theme);
8816 };
8817
8818 Timeline.OverviewEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
8819 var latestStartDate = evt.getLatestStart();
8820 var earliestEndDate = evt.getEarliestEnd();
8821
8822 var latestStartPixel = Math.round(this._band.dateToPixelOffset(latestStartDate));
8823 var earliestEndPixel = Math.round(this._band.dateToPixelOffset(earliestEndDate));
8824
8825 var tapeTrack = 0;
8826 for (; tapeTrack < this._tracks.length; tapeTrack++) {
8827 if (earliestEndPixel < this._tracks[tapeTrack]) {
8828 break;
8829 }
8830 }
8831 this._tracks[tapeTrack] = earliestEndPixel;
8832
8833 var color = evt.getColor(),
8834 klassName = evt.getClassName();
8835 if (klassName) {
8836 color = null;
8837 } else {
8838 color = color != null ? color : theme.event.duration.color;
8839 }
8840
8841 var tapeElmtData = this._paintEventTape(evt, tapeTrack, latestStartPixel, earliestEndPixel,
8842 color, 100, metrics, theme, klassName);
8843
8844 this._createHighlightDiv(highlightIndex, tapeElmtData, theme);
8845 };
8846
8847 Timeline.OverviewEventPainter.prototype._paintEventTape = function(
8848 evt, track, left, right, color, opacity, metrics, theme, klassName) {
8849
8850 var top = metrics.trackOffset + track * metrics.trackIncrement;
8851 var width = right - left;
8852 var height = metrics.trackHeight;
8853
8854 var tapeDiv = this._timeline.getDocument().createElement("div");
8855 tapeDiv.className = 'timeline-small-event-tape'
8856 if (klassName) {tapeDiv.className += ' small-' + klassName;}
8857 tapeDiv.style.left = left + "px";
8858 tapeDiv.style.width = width + "px";
8859 tapeDiv.style.top = top + "px";
8860 tapeDiv.style.height = height + "px";
8861
8862 if (color) {
8863 tapeDiv.style.backgroundColor = color; // set color here if defined by event. Else use css
8864 }
8865 // tapeDiv.style.overflow = "hidden"; // now set in css
8866 // tapeDiv.style.position = "absolute";
8867 if(opacity<100) SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
8868
8869 this._eventLayer.appendChild(tapeDiv);
8870
8871 return {
8872 left: left,
8873 top: top,
8874 width: width,
8875 height: height,
8876 elmt: tapeDiv
8877 };
8878 }
8879
8880 Timeline.OverviewEventPainter.prototype._paintEventTick = function(
8881 evt, left, color, opacity, metrics, theme) {
8882
8883 var height = theme.event.overviewTrack.tickHeight;
8884 var top = metrics.trackOffset - height;
8885 var width = 1;
8886
8887 var tickDiv = this._timeline.getDocument().createElement("div");
8888 tickDiv.className = 'timeline-small-event-icon'
8889 tickDiv.style.left = left + "px";
8890 tickDiv.style.top = top + "px";
8891 // tickDiv.style.width = width + "px";
8892 // tickDiv.style.position = "absolute";
8893 // tickDiv.style.height = height + "px";
8894 // tickDiv.style.backgroundColor = color;
8895 // tickDiv.style.overflow = "hidden";
8896
8897 var klassName = evt.getClassName()
8898 if (klassName) {tickDiv.className +=' small-' + klassName};
8899
8900 if(opacity<100) {SimileAjax.Graphics.setOpacity(tickDiv, opacity)};
8901
8902 this._eventLayer.appendChild(tickDiv);
8903
8904 return {
8905 left: left,
8906 top: top,
8907 width: width,
8908 height: height,
8909 elmt: tickDiv
8910 };
8911 }
8912
8913 Timeline.OverviewEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
8914 if (highlightIndex >= 0) {
8915 var doc = this._timeline.getDocument();
8916 var eventTheme = theme.event;
8917
8918 var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
8919
8920 var div = doc.createElement("div");
8921 div.style.position = "absolute";
8922 div.style.overflow = "hidden";
8923 div.style.left = (dimensions.left - 1) + "px";
8924 div.style.width = (dimensions.width + 2) + "px";
8925 div.style.top = (dimensions.top - 1) + "px";
8926 div.style.height = (dimensions.height + 2) + "px";
8927 div.style.background = color;
8928
8929 this._highlightLayer.appendChild(div);
8930 }
8931 };
8932
8933 Timeline.OverviewEventPainter.prototype.showBubble = function(evt) {
8934 // not implemented
8935 };
8936 /*==================================================
8937 * Compact Event Painter
8938 *==================================================
8939 */
8940
8941 Timeline.CompactEventPainter = function(params) {
8942 this._params = params;
8943 this._onSelectListeners = [];
8944
8945 this._filterMatcher = null;
8946 this._highlightMatcher = null;
8947 this._frc = null;
8948
8949 this._eventIdToElmt = {};
8950 };
8951
8952 Timeline.CompactEventPainter.prototype.getType = function() {
8953 return 'compact';
8954 };
8955
8956 Timeline.CompactEventPainter.prototype.initialize = function(band, timeline) {
8957 this._band = band;
8958 this._timeline = timeline;
8959
8960 this._backLayer = null;
8961 this._eventLayer = null;
8962 this._lineLayer = null;
8963 this._highlightLayer = null;
8964
8965 this._eventIdToElmt = null;
8966 };
8967
8968 Timeline.CompactEventPainter.prototype.supportsOrthogonalScrolling = function() {
8969 return true;
8970 };
8971
8972 Timeline.CompactEventPainter.prototype.addOnSelectListener = function(listener) {
8973 this._onSelectListeners.push(listener);
8974 };
8975
8976 Timeline.CompactEventPainter.prototype.removeOnSelectListener = function(listener) {
8977 for (var i = 0; i < this._onSelectListeners.length; i++) {
8978 if (this._onSelectListeners[i] == listener) {
8979 this._onSelectListeners.splice(i, 1);
8980 break;
8981 }
8982 }
8983 };
8984
8985 Timeline.CompactEventPainter.prototype.getFilterMatcher = function() {
8986 return this._filterMatcher;
8987 };
8988
8989 Timeline.CompactEventPainter.prototype.setFilterMatcher = function(filterMatcher) {
8990 this._filterMatcher = filterMatcher;
8991 };
8992
8993 Timeline.CompactEventPainter.prototype.getHighlightMatcher = function() {
8994 return this._highlightMatcher;
8995 };
8996
8997 Timeline.CompactEventPainter.prototype.setHighlightMatcher = function(highlightMatcher) {
8998 this._highlightMatcher = highlightMatcher;
8999 };
9000
9001 Timeline.CompactEventPainter.prototype.paint = function() {
9002 var eventSource = this._band.getEventSource();
9003 if (eventSource == null) {
9004 return;
9005 }
9006
9007 this._eventIdToElmt = {};
9008 this._prepareForPainting();
9009
9010 var metrics = this._computeMetrics();
9011 var minDate = this._band.getMinDate();
9012 var maxDate = this._band.getMaxDate();
9013
9014 var filterMatcher = (this._filterMatcher != null) ?
9015 this._filterMatcher :
9016 function(evt) { return true; };
9017
9018 var highlightMatcher = (this._highlightMatcher != null) ?
9019 this._highlightMatcher :
9020 function(evt) { return -1; };
9021
9022 var iterator = eventSource.getEventIterator(minDate, maxDate);
9023
9024 var stackConcurrentPreciseInstantEvents = "stackConcurrentPreciseInstantEvents" in this._params && typeof this._params.stackConcurrentPreciseInstantEvents == "object";
9025 var collapseConcurrentPreciseInstantEvents = "collapseConcurrentPreciseInstantEvents" in this._params && this._params.collapseConcurrentPreciseInstantEvents;
9026 if (collapseConcurrentPreciseInstantEvents || stackConcurrentPreciseInstantEvents) {
9027 var bufferedEvents = [];
9028 var previousInstantEvent = null;
9029
9030 while (iterator.hasNext()) {
9031 var evt = iterator.next();
9032 if (filterMatcher(evt)) {
9033 if (!evt.isInstant() || evt.isImprecise()) {
9034 this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
9035 } else if (previousInstantEvent != null &&
9036 previousInstantEvent.getStart().getTime() == evt.getStart().getTime()) {
9037 bufferedEvents[bufferedEvents.length - 1].push(evt);
9038 } else {
9039 bufferedEvents.push([ evt ]);
9040 previousInstantEvent = evt;
9041 }
9042 }
9043 }
9044
9045 for (var i = 0; i < bufferedEvents.length; i++) {
9046 var compositeEvents = bufferedEvents[i];
9047 if (compositeEvents.length == 1) {
9048 this.paintEvent(compositeEvents[0], metrics, this._params.theme, highlightMatcher(evt));
9049 } else {
9050 var match = -1;
9051 for (var j = 0; match < 0 && j < compositeEvents.length; j++) {
9052 match = highlightMatcher(compositeEvents[j]);
9053 }
9054
9055 if (stackConcurrentPreciseInstantEvents) {
9056 this.paintStackedPreciseInstantEvents(compositeEvents, metrics, this._params.theme, match);
9057 } else {
9058 this.paintCompositePreciseInstantEvents(compositeEvents, metrics, this._params.theme, match);
9059 }
9060 }
9061 }
9062 } else {
9063 while (iterator.hasNext()) {
9064 var evt = iterator.next();
9065 if (filterMatcher(evt)) {
9066 this.paintEvent(evt, metrics, this._params.theme, highlightMatcher(evt));
9067 }
9068 }
9069 }
9070
9071 this._highlightLayer.style.display = "block";
9072 this._lineLayer.style.display = "block";
9073 this._eventLayer.style.display = "block";
9074
9075 this._setOrthogonalOffset(metrics);
9076 };
9077
9078 Timeline.CompactEventPainter.prototype.softPaint = function() {
9079 this._setOrthogonalOffset(this._computeMetrics());
9080 };
9081
9082 Timeline.CompactEventPainter.prototype.getOrthogonalExtent = function() {
9083 var metrics = this._computeMetrics();
9084 return 2 * metrics.trackOffset + this._tracks.length * metrics.trackHeight;
9085 };
9086
9087 Timeline.CompactEventPainter.prototype._setOrthogonalOffset = function(metrics) {
9088 var orthogonalOffset = this._band.getViewOrthogonalOffset();
9089
9090 this._highlightLayer.style.top =
9091 this._lineLayer.style.top =
9092 this._eventLayer.style.top =
9093 orthogonalOffset + "px";
9094 };
9095
9096 Timeline.CompactEventPainter.prototype._computeMetrics = function() {
9097 var theme = this._params.theme;
9098 var eventTheme = theme.event;
9099
9100 var metrics = {
9101 trackOffset: "trackOffset" in this._params ? this._params.trackOffset : 10,
9102 trackHeight: "trackHeight" in this._params ? this._params.trackHeight : 10,
9103
9104 tapeHeight: theme.event.tape.height,
9105 tapeBottomMargin: "tapeBottomMargin" in this._params ? this._params.tapeBottomMargin : 2,
9106
9107 labelBottomMargin: "labelBottomMargin" in this._params ? this._params.labelBottomMargin : 5,
9108 labelRightMargin: "labelRightMargin" in this._params ? this._params.labelRightMargin : 5,
9109
9110 defaultIcon: eventTheme.instant.icon,
9111 defaultIconWidth: eventTheme.instant.iconWidth,
9112 defaultIconHeight: eventTheme.instant.iconHeight,
9113
9114 customIconWidth: "iconWidth" in this._params ? this._params.iconWidth : eventTheme.instant.iconWidth,
9115 customIconHeight: "iconHeight" in this._params ? this._params.iconHeight : eventTheme.instant.iconHeight,
9116
9117 iconLabelGap: "iconLabelGap" in this._params ? this._params.iconLabelGap : 2,
9118 iconBottomMargin: "iconBottomMargin" in this._params ? this._params.iconBottomMargin : 2
9119 };
9120 if ("compositeIcon" in this._params) {
9121 metrics.compositeIcon = this._params.compositeIcon;
9122 metrics.compositeIconWidth = this._params.compositeIconWidth || metrics.customIconWidth;
9123 metrics.compositeIconHeight = this._params.compositeIconHeight || metrics.customIconHeight;
9124 } else {
9125 metrics.compositeIcon = metrics.defaultIcon;
9126 metrics.compositeIconWidth = metrics.defaultIconWidth;
9127 metrics.compositeIconHeight = metrics.defaultIconHeight;
9128 }
9129 metrics.defaultStackIcon = "icon" in this._params.stackConcurrentPreciseInstantEvents ?
9130 this._params.stackConcurrentPreciseInstantEvents.icon : metrics.defaultIcon;
9131 metrics.defaultStackIconWidth = "iconWidth" in this._params.stackConcurrentPreciseInstantEvents ?
9132 this._params.stackConcurrentPreciseInstantEvents.iconWidth : metrics.defaultIconWidth;
9133 metrics.defaultStackIconHeight = "iconHeight" in this._params.stackConcurrentPreciseInstantEvents ?
9134 this._params.stackConcurrentPreciseInstantEvents.iconHeight : metrics.defaultIconHeight;
9135
9136 return metrics;
9137 };
9138
9139 Timeline.CompactEventPainter.prototype._prepareForPainting = function() {
9140 var band = this._band;
9141
9142 if (this._backLayer == null) {
9143 this._backLayer = this._band.createLayerDiv(0, "timeline-band-events");
9144 this._backLayer.style.visibility = "hidden";
9145
9146 var eventLabelPrototype = document.createElement("span");
9147 eventLabelPrototype.className = "timeline-event-label";
9148 this._backLayer.appendChild(eventLabelPrototype);
9149 this._frc = SimileAjax.Graphics.getFontRenderingContext(eventLabelPrototype);
9150 }
9151 this._frc.update();
9152 this._tracks = [];
9153
9154 if (this._highlightLayer != null) {
9155 band.removeLayerDiv(this._highlightLayer);
9156 }
9157 this._highlightLayer = band.createLayerDiv(105, "timeline-band-highlights");
9158 this._highlightLayer.style.display = "none";
9159
9160 if (this._lineLayer != null) {
9161 band.removeLayerDiv(this._lineLayer);
9162 }
9163 this._lineLayer = band.createLayerDiv(110, "timeline-band-lines");
9164 this._lineLayer.style.display = "none";
9165
9166 if (this._eventLayer != null) {
9167 band.removeLayerDiv(this._eventLayer);
9168 }
9169 this._eventLayer = band.createLayerDiv(115, "timeline-band-events");
9170 this._eventLayer.style.display = "none";
9171 };
9172
9173 Timeline.CompactEventPainter.prototype.paintEvent = function(evt, metrics, theme, highlightIndex) {
9174 if (evt.isInstant()) {
9175 this.paintInstantEvent(evt, metrics, theme, highlightIndex);
9176 } else {
9177 this.paintDurationEvent(evt, metrics, theme, highlightIndex);
9178 }
9179 };
9180
9181 Timeline.CompactEventPainter.prototype.paintInstantEvent = function(evt, metrics, theme, highlightIndex) {
9182 if (evt.isImprecise()) {
9183 this.paintImpreciseInstantEvent(evt, metrics, theme, highlightIndex);
9184 } else {
9185 this.paintPreciseInstantEvent(evt, metrics, theme, highlightIndex);
9186 }
9187 }
9188
9189 Timeline.CompactEventPainter.prototype.paintDurationEvent = function(evt, metrics, theme, highlightIndex) {
9190 if (evt.isImprecise()) {
9191 this.paintImpreciseDurationEvent(evt, metrics, theme, highlightIndex);
9192 } else {
9193 this.paintPreciseDurationEvent(evt, metrics, theme, highlightIndex);
9194 }
9195 }
9196
9197 Timeline.CompactEventPainter.prototype.paintPreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
9198 var commonData = {
9199 tooltip: evt.getProperty("tooltip") || evt.getText()
9200 };
9201
9202 var iconData = {
9203 url: evt.getIcon()
9204 };
9205 if (iconData.url == null) {
9206 iconData.url = metrics.defaultIcon;
9207 iconData.width = metrics.defaultIconWidth;
9208 iconData.height = metrics.defaultIconHeight;
9209 iconData.className = "timeline-event-icon-default";
9210 } else {
9211 iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
9212 iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
9213 }
9214
9215 var labelData = {
9216 text: evt.getText(),
9217 color: evt.getTextColor() || evt.getColor(),
9218 className: evt.getClassName()
9219 };
9220
9221 var result = this.paintTapeIconLabel(
9222 evt.getStart(),
9223 commonData,
9224 null, // no tape data
9225 iconData,
9226 labelData,
9227 metrics,
9228 theme,
9229 highlightIndex
9230 );
9231
9232 var self = this;
9233 var clickHandler = function(elmt, domEvt, target) {
9234 return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
9235 };
9236 SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
9237 SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
9238
9239 this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
9240 };
9241
9242 Timeline.CompactEventPainter.prototype.paintCompositePreciseInstantEvents = function(events, metrics, theme, highlightIndex) {
9243 var evt = events[0];
9244
9245 var tooltips = [];
9246 for (var i = 0; i < events.length; i++) {
9247 tooltips.push(events[i].getProperty("tooltip") || events[i].getText());
9248 }
9249 var commonData = {
9250 tooltip: tooltips.join("; ")
9251 };
9252
9253 var iconData = {
9254 url: metrics.compositeIcon,
9255 width: metrics.compositeIconWidth,
9256 height: metrics.compositeIconHeight,
9257 className: "timeline-event-icon-composite"
9258 };
9259
9260 var labelData = {
9261 text: String.substitute(this._params.compositeEventLabelTemplate, [ events.length ])
9262 };
9263
9264 var result = this.paintTapeIconLabel(
9265 evt.getStart(),
9266 commonData,
9267 null, // no tape data
9268 iconData,
9269 labelData,
9270 metrics,
9271 theme,
9272 highlightIndex
9273 );
9274
9275 var self = this;
9276 var clickHandler = function(elmt, domEvt, target) {
9277 return self._onClickMultiplePreciseInstantEvent(result.iconElmtData.elmt, domEvt, events);
9278 };
9279
9280 SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
9281 SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
9282
9283 for (var i = 0; i < events.length; i++) {
9284 this._eventIdToElmt[events[i].getID()] = result.iconElmtData.elmt;
9285 }
9286 };
9287
9288 Timeline.CompactEventPainter.prototype.paintStackedPreciseInstantEvents = function(events, metrics, theme, highlightIndex) {
9289 var limit = "limit" in this._params.stackConcurrentPreciseInstantEvents ?
9290 this._params.stackConcurrentPreciseInstantEvents.limit : 10;
9291 var moreMessageTemplate = "moreMessageTemplate" in this._params.stackConcurrentPreciseInstantEvents ?
9292 this._params.stackConcurrentPreciseInstantEvents.moreMessageTemplate : "%0 More Events";
9293 var showMoreMessage = limit <= events.length - 2; // We want at least 2 more events above the limit.
9294 // Otherwise we'd need the singular case of "1 More Event"
9295
9296 var band = this._band;
9297 var getPixelOffset = function(date) {
9298 return Math.round(band.dateToPixelOffset(date));
9299 };
9300 var getIconData = function(evt) {
9301 var iconData = {
9302 url: evt.getIcon()
9303 };
9304 if (iconData.url == null) {
9305 iconData.url = metrics.defaultStackIcon;
9306 iconData.width = metrics.defaultStackIconWidth;
9307 iconData.height = metrics.defaultStackIconHeight;
9308 iconData.className = "timeline-event-icon-stack timeline-event-icon-default";
9309 } else {
9310 iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
9311 iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
9312 iconData.className = "timeline-event-icon-stack";
9313 }
9314 return iconData;
9315 };
9316
9317 var firstIconData = getIconData(events[0]);
9318 var horizontalIncrement = 5;
9319 var leftIconEdge = 0;
9320 var totalLabelWidth = 0;
9321 var totalLabelHeight = 0;
9322 var totalIconHeight = 0;
9323
9324 var records = [];
9325 for (var i = 0; i < events.length && (!showMoreMessage || i < limit); i++) {
9326 var evt = events[i];
9327 var text = evt.getText();
9328 var iconData = getIconData(evt);
9329 var labelSize = this._frc.computeSize(text);
9330 var record = {
9331 text: text,
9332 iconData: iconData,
9333 labelSize: labelSize,
9334 iconLeft: firstIconData.width + i * horizontalIncrement - iconData.width
9335 };
9336 record.labelLeft = firstIconData.width + i * horizontalIncrement + metrics.iconLabelGap;
9337 record.top = totalLabelHeight;
9338 records.push(record);
9339
9340 leftIconEdge = Math.min(leftIconEdge, record.iconLeft);
9341 totalLabelHeight += labelSize.height;
9342 totalLabelWidth = Math.max(totalLabelWidth, record.labelLeft + labelSize.width);
9343 totalIconHeight = Math.max(totalIconHeight, record.top + iconData.height);
9344 }
9345 if (showMoreMessage) {
9346 var moreMessage = String.substitute(moreMessageTemplate, [ events.length - limit ]);
9347
9348 var moreMessageLabelSize = this._frc.computeSize(moreMessage);
9349 var moreMessageLabelLeft = firstIconData.width + (limit - 1) * horizontalIncrement + metrics.iconLabelGap;
9350 var moreMessageLabelTop = totalLabelHeight;
9351
9352 totalLabelHeight += moreMessageLabelSize.height;
9353 totalLabelWidth = Math.max(totalLabelWidth, moreMessageLabelLeft + moreMessageLabelSize.width);
9354 }
9355 totalLabelWidth += metrics.labelRightMargin;
9356 totalLabelHeight += metrics.labelBottomMargin;
9357 totalIconHeight += metrics.iconBottomMargin;
9358
9359 var anchorPixel = getPixelOffset(events[0].getStart());
9360 var newTracks = [];
9361
9362 var trackCount = Math.ceil(Math.max(totalIconHeight, totalLabelHeight) / metrics.trackHeight);
9363 var rightIconEdge = firstIconData.width + (events.length - 1) * horizontalIncrement;
9364 for (var i = 0; i < trackCount; i++) {
9365 newTracks.push({ start: leftIconEdge, end: rightIconEdge });
9366 }
9367 var labelTrackCount = Math.ceil(totalLabelHeight / metrics.trackHeight);
9368 for (var i = 0; i < labelTrackCount; i++) {
9369 var track = newTracks[i];
9370 track.end = Math.max(track.end, totalLabelWidth);
9371 }
9372
9373 var firstTrack = this._fitTracks(anchorPixel, newTracks);
9374 var verticalPixelOffset = firstTrack * metrics.trackHeight + metrics.trackOffset;
9375
9376 var iconStackDiv = this._timeline.getDocument().createElement("div");
9377 iconStackDiv.className = 'timeline-event-icon-stack';
9378 iconStackDiv.style.position = "absolute";
9379 iconStackDiv.style.overflow = "visible";
9380 iconStackDiv.style.left = anchorPixel + "px";
9381 iconStackDiv.style.top = verticalPixelOffset + "px";
9382 iconStackDiv.style.width = rightIconEdge + "px";
9383 iconStackDiv.style.height = totalIconHeight + "px";
9384 iconStackDiv.innerHTML = "<div style='position: relative'></div>";
9385 this._eventLayer.appendChild(iconStackDiv);
9386
9387 var self = this;
9388 var onMouseOver = function(domEvt) {
9389 try {
9390 var n = parseInt(this.getAttribute("index"));
9391 var childNodes = iconStackDiv.firstChild.childNodes;
9392 for (var i = 0; i < childNodes.length; i++) {
9393 var child = childNodes[i];
9394 if (i == n) {
9395 child.style.zIndex = childNodes.length;
9396 } else {
9397 child.style.zIndex = childNodes.length - i;
9398 }
9399 }
9400 } catch (e) {
9401 }
9402 };
9403 var paintEvent = function(index) {
9404 var record = records[index];
9405 var evt = events[index];
9406 var tooltip = evt.getProperty("tooltip") || evt.getText();
9407
9408 var labelElmtData = self._paintEventLabel(
9409 { tooltip: tooltip },
9410 { text: record.text },
9411 anchorPixel + record.labelLeft,
9412 verticalPixelOffset + record.top,
9413 record.labelSize.width,
9414 record.labelSize.height,
9415 theme
9416 );
9417 labelElmtData.elmt.setAttribute("index", index);
9418 labelElmtData.elmt.onmouseover = onMouseOver;
9419
9420 var img = SimileAjax.Graphics.createTranslucentImage(record.iconData.url);
9421 var iconDiv = self._timeline.getDocument().createElement("div");
9422 iconDiv.className = 'timeline-event-icon' + ("className" in record.iconData ? (" " + record.iconData.className) : "");
9423 iconDiv.style.left = record.iconLeft + "px";
9424 iconDiv.style.top = record.top + "px";
9425 iconDiv.style.zIndex = (records.length - index);
9426 iconDiv.appendChild(img);
9427 iconDiv.setAttribute("index", index);
9428 iconDiv.onmouseover = onMouseOver;
9429
9430 iconStackDiv.firstChild.appendChild(iconDiv);
9431
9432 var clickHandler = function(elmt, domEvt, target) {
9433 return self._onClickInstantEvent(labelElmtData.elmt, domEvt, evt);
9434 };
9435
9436 SimileAjax.DOM.registerEvent(iconDiv, "mousedown", clickHandler);
9437 SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mousedown", clickHandler);
9438
9439 self._eventIdToElmt[evt.getID()] = iconDiv;
9440 };
9441 for (var i = 0; i < records.length; i++) {
9442 paintEvent(i);
9443 }
9444
9445 if (showMoreMessage) {
9446 var moreEvents = events.slice(limit);
9447 var moreMessageLabelElmtData = this._paintEventLabel(
9448 { tooltip: moreMessage },
9449 { text: moreMessage },
9450 anchorPixel + moreMessageLabelLeft,
9451 verticalPixelOffset + moreMessageLabelTop,
9452 moreMessageLabelSize.width,
9453 moreMessageLabelSize.height,
9454 theme
9455 );
9456
9457 var moreMessageClickHandler = function(elmt, domEvt, target) {
9458 return self._onClickMultiplePreciseInstantEvent(moreMessageLabelElmtData.elmt, domEvt, moreEvents);
9459 };
9460 SimileAjax.DOM.registerEvent(moreMessageLabelElmtData.elmt, "mousedown", moreMessageClickHandler);
9461
9462 for (var i = 0; i < moreEvents.length; i++) {
9463 this._eventIdToElmt[moreEvents[i].getID()] = moreMessageLabelElmtData.elmt;
9464 }
9465 }
9466 //this._createHighlightDiv(highlightIndex, iconElmtData, theme);
9467 };
9468
9469 Timeline.CompactEventPainter.prototype.paintImpreciseInstantEvent = function(evt, metrics, theme, highlightIndex) {
9470 var commonData = {
9471 tooltip: evt.getProperty("tooltip") || evt.getText()
9472 };
9473
9474 var tapeData = {
9475 start: evt.getStart(),
9476 end: evt.getEnd(),
9477 latestStart: evt.getLatestStart(),
9478 earliestEnd: evt.getEarliestEnd(),
9479 isInstant: true
9480 };
9481
9482 var iconData = {
9483 url: evt.getIcon()
9484 };
9485 if (iconData.url == null) {
9486 iconData = null;
9487 } else {
9488 iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
9489 iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
9490 }
9491
9492 var labelData = {
9493 text: evt.getText(),
9494 color: evt.getTextColor() || evt.getColor(),
9495 className: evt.getClassName()
9496 };
9497
9498 var result = this.paintTapeIconLabel(
9499 evt.getStart(),
9500 commonData,
9501 tapeData, // no tape data
9502 iconData,
9503 labelData,
9504 metrics,
9505 theme,
9506 highlightIndex
9507 );
9508
9509 var self = this;
9510 var clickHandler = iconData != null ?
9511 function(elmt, domEvt, target) {
9512 return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
9513 } :
9514 function(elmt, domEvt, target) {
9515 return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
9516 };
9517
9518 SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
9519 SimileAjax.DOM.registerEvent(result.impreciseTapeElmtData.elmt, "mousedown", clickHandler);
9520
9521 if (iconData != null) {
9522 SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
9523 this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
9524 } else {
9525 this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
9526 }
9527 };
9528
9529 Timeline.CompactEventPainter.prototype.paintPreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
9530 var commonData = {
9531 tooltip: evt.getProperty("tooltip") || evt.getText()
9532 };
9533
9534 var tapeData = {
9535 start: evt.getStart(),
9536 end: evt.getEnd(),
9537 isInstant: false
9538 };
9539
9540 var iconData = {
9541 url: evt.getIcon()
9542 };
9543 if (iconData.url == null) {
9544 iconData = null;
9545 } else {
9546 iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
9547 iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
9548 }
9549
9550 var labelData = {
9551 text: evt.getText(),
9552 color: evt.getTextColor() || evt.getColor(),
9553 className: evt.getClassName()
9554 };
9555
9556 var result = this.paintTapeIconLabel(
9557 evt.getLatestStart(),
9558 commonData,
9559 tapeData, // no tape data
9560 iconData,
9561 labelData,
9562 metrics,
9563 theme,
9564 highlightIndex
9565 );
9566
9567 var self = this;
9568 var clickHandler = iconData != null ?
9569 function(elmt, domEvt, target) {
9570 return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
9571 } :
9572 function(elmt, domEvt, target) {
9573 return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
9574 };
9575
9576 SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
9577 SimileAjax.DOM.registerEvent(result.tapeElmtData.elmt, "mousedown", clickHandler);
9578
9579 if (iconData != null) {
9580 SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
9581 this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
9582 } else {
9583 this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
9584 }
9585 };
9586
9587 Timeline.CompactEventPainter.prototype.paintImpreciseDurationEvent = function(evt, metrics, theme, highlightIndex) {
9588 var commonData = {
9589 tooltip: evt.getProperty("tooltip") || evt.getText()
9590 };
9591
9592 var tapeData = {
9593 start: evt.getStart(),
9594 end: evt.getEnd(),
9595 latestStart: evt.getLatestStart(),
9596 earliestEnd: evt.getEarliestEnd(),
9597 isInstant: false
9598 };
9599
9600 var iconData = {
9601 url: evt.getIcon()
9602 };
9603 if (iconData.url == null) {
9604 iconData = null;
9605 } else {
9606 iconData.width = evt.getProperty("iconWidth") || metrics.customIconWidth;
9607 iconData.height = evt.getProperty("iconHeight") || metrics.customIconHeight;
9608 }
9609
9610 var labelData = {
9611 text: evt.getText(),
9612 color: evt.getTextColor() || evt.getColor(),
9613 className: evt.getClassName()
9614 };
9615
9616 var result = this.paintTapeIconLabel(
9617 evt.getLatestStart(),
9618 commonData,
9619 tapeData, // no tape data
9620 iconData,
9621 labelData,
9622 metrics,
9623 theme,
9624 highlightIndex
9625 );
9626
9627 var self = this;
9628 var clickHandler = iconData != null ?
9629 function(elmt, domEvt, target) {
9630 return self._onClickInstantEvent(result.iconElmtData.elmt, domEvt, evt);
9631 } :
9632 function(elmt, domEvt, target) {
9633 return self._onClickInstantEvent(result.labelElmtData.elmt, domEvt, evt);
9634 };
9635
9636 SimileAjax.DOM.registerEvent(result.labelElmtData.elmt, "mousedown", clickHandler);
9637 SimileAjax.DOM.registerEvent(result.tapeElmtData.elmt, "mousedown", clickHandler);
9638
9639 if (iconData != null) {
9640 SimileAjax.DOM.registerEvent(result.iconElmtData.elmt, "mousedown", clickHandler);
9641 this._eventIdToElmt[evt.getID()] = result.iconElmtData.elmt;
9642 } else {
9643 this._eventIdToElmt[evt.getID()] = result.labelElmtData.elmt;
9644 }
9645 };
9646
9647 Timeline.CompactEventPainter.prototype.paintTapeIconLabel = function(
9648 anchorDate,
9649 commonData,
9650 tapeData,
9651 iconData,
9652 labelData,
9653 metrics,
9654 theme,
9655 highlightIndex
9656 ) {
9657 var band = this._band;
9658 var getPixelOffset = function(date) {
9659 return Math.round(band.dateToPixelOffset(date));
9660 };
9661
9662 var anchorPixel = getPixelOffset(anchorDate);
9663 var newTracks = [];
9664
9665 var tapeHeightOccupied = 0; // how many pixels (vertically) the tape occupies, including bottom margin
9666 var tapeTrackCount = 0; // how many tracks the tape takes up, usually just 1
9667 var tapeLastTrackExtraSpace = 0; // on the last track that the tape occupies, how many pixels are left (for icon and label to occupy as well)
9668 if (tapeData != null) {
9669 tapeHeightOccupied = metrics.tapeHeight + metrics.tapeBottomMargin;
9670 tapeTrackCount = Math.ceil(metrics.tapeHeight / metrics.trackHeight);
9671
9672 var tapeEndPixelOffset = getPixelOffset(tapeData.end) - anchorPixel;
9673 var tapeStartPixelOffset = getPixelOffset(tapeData.start) - anchorPixel;
9674
9675 for (var t = 0; t < tapeTrackCount; t++) {
9676 newTracks.push({ start: tapeStartPixelOffset, end: tapeEndPixelOffset });
9677 }
9678
9679 tapeLastTrackExtraSpace = metrics.trackHeight - (tapeHeightOccupied % metrics.tapeHeight);
9680 }
9681
9682 var iconStartPixelOffset = 0; // where the icon starts compared to the anchor pixel;
9683 // this can be negative if the icon is center-aligned around the anchor
9684 var iconHorizontalSpaceOccupied = 0; // how many pixels the icon take up from the anchor pixel,
9685 // including the gap between the icon and the label
9686 if (iconData != null) {
9687 if ("iconAlign" in iconData && iconData.iconAlign == "center") {
9688 iconStartPixelOffset = -Math.floor(iconData.width / 2);
9689 }
9690 iconHorizontalSpaceOccupied = iconStartPixelOffset + iconData.width + metrics.iconLabelGap;
9691
9692 if (tapeTrackCount > 0) {
9693 newTracks[tapeTrackCount - 1].end = Math.max(newTracks[tapeTrackCount - 1].end, iconHorizontalSpaceOccupied);
9694 }
9695
9696 var iconHeight = iconData.height + metrics.iconBottomMargin + tapeLastTrackExtraSpace;
9697 while (iconHeight > 0) {
9698 newTracks.push({ start: iconStartPixelOffset, end: iconHorizontalSpaceOccupied });
9699 iconHeight -= metrics.trackHeight;
9700 }
9701 }
9702
9703 var text = labelData.text;
9704 var labelSize = this._frc.computeSize(text);
9705 var labelHeight = labelSize.height + metrics.labelBottomMargin + tapeLastTrackExtraSpace;
9706 var labelEndPixelOffset = iconHorizontalSpaceOccupied + labelSize.width + metrics.labelRightMargin;
9707 if (tapeTrackCount > 0) {
9708 newTracks[tapeTrackCount - 1].end = Math.max(newTracks[tapeTrackCount - 1].end, labelEndPixelOffset);
9709 }
9710 for (var i = 0; labelHeight > 0; i++) {
9711 if (tapeTrackCount + i < newTracks.length) {
9712 var track = newTracks[tapeTrackCount + i];
9713 track.end = labelEndPixelOffset;
9714 } else {
9715 newTracks.push({ start: 0, end: labelEndPixelOffset });
9716 }
9717 labelHeight -= metrics.trackHeight;
9718 }
9719
9720 /*
9721 * Try to fit the new track on top of the existing tracks, then
9722 * render the various elements.
9723 */
9724 var firstTrack = this._fitTracks(anchorPixel, newTracks);
9725 var verticalPixelOffset = firstTrack * metrics.trackHeight + metrics.trackOffset;
9726 var result = {};
9727
9728 result.labelElmtData = this._paintEventLabel(
9729 commonData,
9730 labelData,
9731 anchorPixel + iconHorizontalSpaceOccupied,
9732 verticalPixelOffset + tapeHeightOccupied,
9733 labelSize.width,
9734 labelSize.height,
9735 theme
9736 );
9737
9738 if (tapeData != null) {
9739 if ("latestStart" in tapeData || "earliestEnd" in tapeData) {
9740 result.impreciseTapeElmtData = this._paintEventTape(
9741 commonData,
9742 tapeData,
9743 metrics.tapeHeight,
9744 verticalPixelOffset,
9745 getPixelOffset(tapeData.start),
9746 getPixelOffset(tapeData.end),
9747 theme.event.duration.impreciseColor,
9748 theme.event.duration.impreciseOpacity,
9749 metrics,
9750 theme
9751 );
9752 }
9753 if (!tapeData.isInstant && "start" in tapeData && "end" in tapeData) {
9754 result.tapeElmtData = this._paintEventTape(
9755 commonData,
9756 tapeData,
9757 metrics.tapeHeight,
9758 verticalPixelOffset,
9759 anchorPixel,
9760 getPixelOffset("earliestEnd" in tapeData ? tapeData.earliestEnd : tapeData.end),
9761 tapeData.color,
9762 100,
9763 metrics,
9764 theme
9765 );
9766 }
9767 }
9768
9769 if (iconData != null) {
9770 result.iconElmtData = this._paintEventIcon(
9771 commonData,
9772 iconData,
9773 verticalPixelOffset + tapeHeightOccupied,
9774 anchorPixel + iconStartPixelOffset,
9775 metrics,
9776 theme
9777 );
9778 }
9779 //this._createHighlightDiv(highlightIndex, iconElmtData, theme);
9780
9781 return result;
9782 };
9783
9784 Timeline.CompactEventPainter.prototype._fitTracks = function(anchorPixel, newTracks) {
9785 var firstTrack;
9786 for (firstTrack = 0; firstTrack < this._tracks.length; firstTrack++) {
9787 var fit = true;
9788 for (var j = 0; j < newTracks.length && (firstTrack + j) < this._tracks.length; j++) {
9789 var existingTrack = this._tracks[firstTrack + j];
9790 var newTrack = newTracks[j];
9791 if (anchorPixel + newTrack.start < existingTrack) {
9792 fit = false;
9793 break;
9794 }
9795 }
9796
9797 if (fit) {
9798 break;
9799 }
9800 }
9801 for (var i = 0; i < newTracks.length; i++) {
9802 this._tracks[firstTrack + i] = anchorPixel + newTracks[i].end;
9803 }
9804
9805 return firstTrack;
9806 };
9807
9808
9809 Timeline.CompactEventPainter.prototype._paintEventIcon = function(commonData, iconData, top, left, metrics, theme) {
9810 var img = SimileAjax.Graphics.createTranslucentImage(iconData.url);
9811 var iconDiv = this._timeline.getDocument().createElement("div");
9812 iconDiv.className = 'timeline-event-icon' + ("className" in iconData ? (" " + iconData.className) : "");
9813 iconDiv.style.left = left + "px";
9814 iconDiv.style.top = top + "px";
9815 iconDiv.appendChild(img);
9816
9817 if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
9818 iconDiv.title = commonData.tooltip;
9819 }
9820
9821 this._eventLayer.appendChild(iconDiv);
9822
9823 return {
9824 left: left,
9825 top: top,
9826 width: metrics.iconWidth,
9827 height: metrics.iconHeight,
9828 elmt: iconDiv
9829 };
9830 };
9831
9832 Timeline.CompactEventPainter.prototype._paintEventLabel = function(commonData, labelData, left, top, width, height, theme) {
9833 var doc = this._timeline.getDocument();
9834
9835 var labelDiv = doc.createElement("div");
9836 labelDiv.className = 'timeline-event-label';
9837
9838 labelDiv.style.left = left + "px";
9839 labelDiv.style.width = (width + 1) + "px";
9840 labelDiv.style.top = top + "px";
9841 labelDiv.innerHTML = labelData.text;
9842
9843 if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
9844 labelDiv.title = commonData.tooltip;
9845 }
9846 if ("color" in labelData && typeof labelData.color == "string") {
9847 labelDiv.style.color = labelData.color;
9848 }
9849 if ("className" in labelData && typeof labelData.className == "string") {
9850 labelDiv.className += ' ' + labelData.className;
9851 }
9852
9853 this._eventLayer.appendChild(labelDiv);
9854
9855 return {
9856 left: left,
9857 top: top,
9858 width: width,
9859 height: height,
9860 elmt: labelDiv
9861 };
9862 };
9863
9864 Timeline.CompactEventPainter.prototype._paintEventTape = function(
9865 commonData, tapeData, height, top, startPixel, endPixel, color, opacity, metrics, theme) {
9866
9867 var width = endPixel - startPixel;
9868
9869 var tapeDiv = this._timeline.getDocument().createElement("div");
9870 tapeDiv.className = "timeline-event-tape"
9871
9872 tapeDiv.style.left = startPixel + "px";
9873 tapeDiv.style.top = top + "px";
9874 tapeDiv.style.width = width + "px";
9875 tapeDiv.style.height = height + "px";
9876
9877 if ("tooltip" in commonData && typeof commonData.tooltip == "string") {
9878 tapeDiv.title = commonData.tooltip;
9879 }
9880 if (color != null && typeof tapeData.color == "string") {
9881 tapeDiv.style.backgroundColor = color;
9882 }
9883
9884 if ("backgroundImage" in tapeData && typeof tapeData.backgroundImage == "string") {
9885 tapeDiv.style.backgroundImage = "url(" + backgroundImage + ")";
9886 tapeDiv.style.backgroundRepeat =
9887 ("backgroundRepeat" in tapeData && typeof tapeData.backgroundRepeat == "string")
9888 ? tapeData.backgroundRepeat : 'repeat';
9889 }
9890
9891 SimileAjax.Graphics.setOpacity(tapeDiv, opacity);
9892
9893 if ("className" in tapeData && typeof tapeData.className == "string") {
9894 tapeDiv.className += ' ' + tapeData.className;
9895 }
9896
9897 this._eventLayer.appendChild(tapeDiv);
9898
9899 return {
9900 left: startPixel,
9901 top: top,
9902 width: width,
9903 height: height,
9904 elmt: tapeDiv
9905 };
9906 }
9907
9908 Timeline.CompactEventPainter.prototype._createHighlightDiv = function(highlightIndex, dimensions, theme) {
9909 if (highlightIndex >= 0) {
9910 var doc = this._timeline.getDocument();
9911 var eventTheme = theme.event;
9912
9913 var color = eventTheme.highlightColors[Math.min(highlightIndex, eventTheme.highlightColors.length - 1)];
9914
9915 var div = doc.createElement("div");
9916 div.style.position = "absolute";
9917 div.style.overflow = "hidden";
9918 div.style.left = (dimensions.left - 2) + "px";
9919 div.style.width = (dimensions.width + 4) + "px";
9920 div.style.top = (dimensions.top - 2) + "px";
9921 div.style.height = (dimensions.height + 4) + "px";
9922 // div.style.background = color;
9923
9924 this._highlightLayer.appendChild(div);
9925 }
9926 };
9927
9928 Timeline.CompactEventPainter.prototype._onClickMultiplePreciseInstantEvent = function(icon, domEvt, events) {
9929 var c = SimileAjax.DOM.getPageCoordinates(icon);
9930 this._showBubble(
9931 c.left + Math.ceil(icon.offsetWidth / 2),
9932 c.top + Math.ceil(icon.offsetHeight / 2),
9933 events
9934 );
9935
9936 var ids = [];
9937 for (var i = 0; i < events.length; i++) {
9938 ids.push(events[i].getID());
9939 }
9940 this._fireOnSelect(ids);
9941
9942 domEvt.cancelBubble = true;
9943 SimileAjax.DOM.cancelEvent(domEvt);
9944
9945 return false;
9946 };
9947
9948 Timeline.CompactEventPainter.prototype._onClickInstantEvent = function(icon, domEvt, evt) {
9949 var c = SimileAjax.DOM.getPageCoordinates(icon);
9950 this._showBubble(
9951 c.left + Math.ceil(icon.offsetWidth / 2),
9952 c.top + Math.ceil(icon.offsetHeight / 2),
9953 [evt]
9954 );
9955 this._fireOnSelect([evt.getID()]);
9956
9957 domEvt.cancelBubble = true;
9958 SimileAjax.DOM.cancelEvent(domEvt);
9959 return false;
9960 };
9961
9962 Timeline.CompactEventPainter.prototype._onClickDurationEvent = function(target, domEvt, evt) {
9963 if ("pageX" in domEvt) {
9964 var x = domEvt.pageX;
9965 var y = domEvt.pageY;
9966 } else {
9967 var c = SimileAjax.DOM.getPageCoordinates(target);
9968 var x = domEvt.offsetX + c.left;
9969 var y = domEvt.offsetY + c.top;
9970 }
9971 this._showBubble(x, y, [evt]);
9972 this._fireOnSelect([evt.getID()]);
9973
9974 domEvt.cancelBubble = true;
9975 SimileAjax.DOM.cancelEvent(domEvt);
9976 return false;
9977 };
9978
9979 Timeline.CompactEventPainter.prototype.showBubble = function(evt) {
9980 var elmt = this._eventIdToElmt[evt.getID()];
9981 if (elmt) {
9982 var c = SimileAjax.DOM.getPageCoordinates(elmt);
9983 this._showBubble(c.left + elmt.offsetWidth / 2, c.top + elmt.offsetHeight / 2, [evt]);
9984 }
9985 };
9986
9987 Timeline.CompactEventPainter.prototype._showBubble = function(x, y, evts) {
9988 var div = document.createElement("div");
9989
9990 evts = ("fillInfoBubble" in evts) ? [evts] : evts;
9991 for (var i = 0; i < evts.length; i++) {
9992 var div2 = document.createElement("div");
9993 div.appendChild(div2);
9994
9995 evts[i].fillInfoBubble(div2, this._params.theme, this._band.getLabeller());
9996 }
9997
9998 SimileAjax.WindowManager.cancelPopups();
9999 SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, this._params.theme.event.bubble.width);
10000 };
10001
10002 Timeline.CompactEventPainter.prototype._fireOnSelect = function(eventIDs) {
10003 for (var i = 0; i < this._onSelectListeners.length; i++) {
10004 this._onSelectListeners[i](eventIDs);
10005 }
10006 };
10007 /*==================================================
10008 * Span Highlight Decorator
10009 *==================================================
10010 */
10011
10012 Timeline.SpanHighlightDecorator = function(params) {
10013 // When evaluating params, test against null. Not "p in params". Testing against
10014 // null enables caller to explicitly request the default. Testing against "in" means
10015 // that the param has to be ommitted to get the default.
10016 this._unit = params.unit != null ? params.unit : SimileAjax.NativeDateUnit;
10017 this._startDate = (typeof params.startDate == "string") ?
10018 this._unit.parseFromObject(params.startDate) : params.startDate;
10019 this._endDate = (typeof params.endDate == "string") ?
10020 this._unit.parseFromObject(params.endDate) : params.endDate;
10021 this._startLabel = params.startLabel != null ? params.startLabel : ""; // not null!
10022 this._endLabel = params.endLabel != null ? params.endLabel : ""; // not null!
10023 this._color = params.color;
10024 this._cssClass = params.cssClass != null ? params.cssClass : null;
10025 this._opacity = params.opacity != null ? params.opacity : 100;
10026 // Default z is 10, behind everything but background grid.
10027 // If inFront, then place just behind events, in front of everything else
10028 this._zIndex = (params.inFront != null && params.inFront) ? 113 : 10;
10029 };
10030
10031 Timeline.SpanHighlightDecorator.prototype.initialize = function(band, timeline) {
10032 this._band = band;
10033 this._timeline = timeline;
10034
10035 this._layerDiv = null;
10036 };
10037
10038 Timeline.SpanHighlightDecorator.prototype.paint = function() {
10039 if (this._layerDiv != null) {
10040 this._band.removeLayerDiv(this._layerDiv);
10041 }
10042 this._layerDiv = this._band.createLayerDiv(this._zIndex);
10043 this._layerDiv.setAttribute("name", "span-highlight-decorator"); // for debugging
10044 this._layerDiv.style.display = "none";
10045
10046 var minDate = this._band.getMinDate();
10047 var maxDate = this._band.getMaxDate();
10048
10049 if (this._unit.compare(this._startDate, maxDate) < 0 &&
10050 this._unit.compare(this._endDate, minDate) > 0) {
10051
10052 minDate = this._unit.later(minDate, this._startDate);
10053 maxDate = this._unit.earlier(maxDate, this._endDate);
10054
10055 var minPixel = this._band.dateToPixelOffset(minDate);
10056 var maxPixel = this._band.dateToPixelOffset(maxDate);
10057
10058 var doc = this._timeline.getDocument();
10059
10060 var createTable = function() {
10061 var table = doc.createElement("table");
10062 table.insertRow(0).insertCell(0);
10063 return table;
10064 };
10065
10066 var div = doc.createElement("div");
10067 div.className='timeline-highlight-decorator'
10068 if(this._cssClass) {
10069 div.className += ' ' + this._cssClass;
10070 }
10071 if(this._color != null) {
10072 div.style.backgroundColor = this._color;
10073 }
10074 if (this._opacity < 100) {
10075 SimileAjax.Graphics.setOpacity(div, this._opacity);
10076 }
10077 this._layerDiv.appendChild(div);
10078
10079 var tableStartLabel = createTable();
10080 tableStartLabel.className = 'timeline-highlight-label timeline-highlight-label-start'
10081 var tdStart = tableStartLabel.rows[0].cells[0]
10082 tdStart.innerHTML = this._startLabel;
10083 if (this._cssClass) {
10084 tdStart.className = 'label_' + this._cssClass;
10085 }
10086 this._layerDiv.appendChild(tableStartLabel);
10087
10088 var tableEndLabel = createTable();
10089 tableEndLabel.className = 'timeline-highlight-label timeline-highlight-label-end'
10090 var tdEnd = tableEndLabel.rows[0].cells[0]
10091 tdEnd.innerHTML = this._endLabel;
10092 if (this._cssClass) {
10093 tdEnd.className = 'label_' + this._cssClass;
10094 }
10095 this._layerDiv.appendChild(tableEndLabel);
10096
10097 if (this._timeline.isHorizontal()){
10098 div.style.left = minPixel + "px";
10099 div.style.width = (maxPixel - minPixel) + "px";
10100
10101 tableStartLabel.style.right = (this._band.getTotalViewLength() - minPixel) + "px";
10102 tableStartLabel.style.width = (this._startLabel.length) + "em";
10103
10104 tableEndLabel.style.left = maxPixel + "px";
10105 tableEndLabel.style.width = (this._endLabel.length) + "em";
10106
10107 } else {
10108 div.style.top = minPixel + "px";
10109 div.style.height = (maxPixel - minPixel) + "px";
10110
10111 tableStartLabel.style.bottom = minPixel + "px";
10112 tableStartLabel.style.height = "1.5px";
10113
10114 tableEndLabel.style.top = maxPixel + "px";
10115 tableEndLabel.style.height = "1.5px";
10116 }
10117 }
10118 this._layerDiv.style.display = "block";
10119 };
10120
10121 Timeline.SpanHighlightDecorator.prototype.softPaint = function() {
10122 };
10123
10124 /*==================================================
10125 * Point Highlight Decorator
10126 *==================================================
10127 */
10128
10129 Timeline.PointHighlightDecorator = function(params) {
10130 this._unit = params.unit != null ? params.unit : SimileAjax.NativeDateUnit;
10131 this._date = (typeof params.date == "string") ?
10132 this._unit.parseFromObject(params.date) : params.date;
10133 this._width = params.width != null ? params.width : 10;
10134 // Since the width is used to calculate placements (see minPixel, below), we
10135 // specify width here, not in css.
10136 this._color = params.color;
10137 this._cssClass = params.cssClass != null ? params.cssClass : '';
10138 this._opacity = params.opacity != null ? params.opacity : 100;
10139 };
10140
10141 Timeline.PointHighlightDecorator.prototype.initialize = function(band, timeline) {
10142 this._band = band;
10143 this._timeline = timeline;
10144 this._layerDiv = null;
10145 };
10146
10147 Timeline.PointHighlightDecorator.prototype.paint = function() {
10148 if (this._layerDiv != null) {
10149 this._band.removeLayerDiv(this._layerDiv);
10150 }
10151 this._layerDiv = this._band.createLayerDiv(10);
10152 this._layerDiv.setAttribute("name", "span-highlight-decorator"); // for debugging
10153 this._layerDiv.style.display = "none";
10154
10155 var minDate = this._band.getMinDate();
10156 var maxDate = this._band.getMaxDate();
10157
10158 if (this._unit.compare(this._date, maxDate) < 0 &&
10159 this._unit.compare(this._date, minDate) > 0) {
10160
10161 var pixel = this._band.dateToPixelOffset(this._date);
10162 var minPixel = pixel - Math.round(this._width / 2);
10163
10164 var doc = this._timeline.getDocument();
10165
10166 var div = doc.createElement("div");
10167 div.className='timeline-highlight-point-decorator';
10168 div.className += ' ' + this._cssClass;
10169
10170 if(this._color != null) {
10171 div.style.backgroundColor = this._color;
10172 }
10173 if (this._opacity < 100) {
10174 SimileAjax.Graphics.setOpacity(div, this._opacity);
10175 }
10176 this._layerDiv.appendChild(div);
10177
10178 if (this._timeline.isHorizontal()) {
10179 div.style.left = minPixel + "px";
10180 div.style.width = this._width;
10181 } else {
10182 div.style.top = minPixel + "px";
10183 div.style.height = this._width;
10184 }
10185 }
10186 this._layerDiv.style.display = "block";
10187 };
10188
10189 Timeline.PointHighlightDecorator.prototype.softPaint = function() {
10190 };
10191 /*==================================================
10192 * Default Unit
10193 *==================================================
10194 */
10195
10196 Timeline.NativeDateUnit = new Object();
10197
10198 Timeline.NativeDateUnit.createLabeller = function(locale, timeZone) {
10199 return new Timeline.GregorianDateLabeller(locale, timeZone);
10200 };
10201
10202 Timeline.NativeDateUnit.makeDefaultValue = function() {
10203 return new Date();
10204 };
10205
10206 Timeline.NativeDateUnit.cloneValue = function(v) {
10207 return new Date(v.getTime());
10208 };
10209
10210 Timeline.NativeDateUnit.getParser = function(format) {
10211 if (typeof format == "string") {
10212 format = format.toLowerCase();
10213 }
10214 return (format == "iso8601" || format == "iso 8601") ?
10215 Timeline.DateTime.parseIso8601DateTime :
10216 Timeline.DateTime.parseGregorianDateTime;
10217 };
10218
10219 Timeline.NativeDateUnit.parseFromObject = function(o) {
10220 return Timeline.DateTime.parseGregorianDateTime(o);
10221 };
10222
10223 Timeline.NativeDateUnit.toNumber = function(v) {
10224 return v.getTime();
10225 };
10226
10227 Timeline.NativeDateUnit.fromNumber = function(n) {
10228 return new Date(n);
10229 };
10230
10231 Timeline.NativeDateUnit.compare = function(v1, v2) {
10232 var n1, n2;
10233 if (typeof v1 == "object") {
10234 n1 = v1.getTime();
10235 } else {
10236 n1 = Number(v1);
10237 }
10238 if (typeof v2 == "object") {
10239 n2 = v2.getTime();
10240 } else {
10241 n2 = Number(v2);
10242 }
10243
10244 return n1 - n2;
10245 };
10246
10247 Timeline.NativeDateUnit.earlier = function(v1, v2) {
10248 return Timeline.NativeDateUnit.compare(v1, v2) < 0 ? v1 : v2;
10249 };
10250
10251 Timeline.NativeDateUnit.later = function(v1, v2) {
10252 return Timeline.NativeDateUnit.compare(v1, v2) > 0 ? v1 : v2;
10253 };
10254
10255 Timeline.NativeDateUnit.change = function(v, n) {
10256 return new Date(v.getTime() + n);
10257 };
10258
10259 /*==================================================
10260 * Simile Timeplot API
10261 *
10262 * Include Timeplot in your HTML file as follows:
10263 * <script src="http://static.simile.mit.edu/timeplot/api/1.0/timeplot-api.js" type="text/javascript"></script>
10264 *
10265 *==================================================*/
10266
10267 (function() {
10268
10269 var local = false;
10270
10271 // obtain local mode from the document URL
10272 if (document.location.search.length > 0) {
10273 var params = document.location.search.substr(1).split("&");
10274 for (var i = 0; i < params.length; i++) {
10275 if (params[i] == "local") {
10276 local = true;
10277 }
10278 }
10279 }
10280
10281 // obtain local mode from the script URL params attribute
10282 if (!local) {
10283 var heads = document.documentElement.getElementsByTagName("head");
10284 for (var h = 0; h < heads.length; h++) {
10285 var node = heads[h].firstChild;
10286 while (node != null) {
10287 if (node.nodeType == 1 && node.tagName.toLowerCase() == "script") {
10288 var url = node.src;
10289 if (url.indexOf("timeplot-complete") >= 0) {
10290 local = (url.indexOf("local") >= 0);
10291 }
10292 }
10293 node = node.nextSibling;
10294 }
10295 }
10296 }
10297
10298 // Load Timeplot if it's not already loaded (after SimileAjax and Timeline)
10299 var loadTimeplot = function() {
10300
10301 if (typeof window.Timeplot != "undefined") {
10302 return;
10303 }
10304
10305 window.Timeplot = {
10306 loaded: false,
10307 params: { bundle: true, autoCreate: true },
10308 namespace: "http://simile.mit.edu/2007/06/timeplot#",
10309 importers: {}
10310 };
10311
10312 var javascriptFiles = [
10313 "timeplot.js",
10314 "plot.js",
10315 "sources.js",
10316 "geometry.js",
10317 "color.js",
10318 "math.js",
10319 "processor.js"
10320 ];
10321 var cssFiles = [
10322 "timeplot.css"
10323 ];
10324
10325 var locales = [ "en" ];
10326
10327 var defaultClientLocales = ("language" in navigator ? navigator.language : navigator.browserLanguage).split(";");
10328 for (var l = 0; l < defaultClientLocales.length; l++) {
10329 var locale = defaultClientLocales[l];
10330 if (locale != "en") {
10331 var segments = locale.split("-");
10332 if (segments.length > 1 && segments[0] != "en") {
10333 locales.push(segments[0]);
10334 }
10335 locales.push(locale);
10336 }
10337 }
10338
10339 var paramTypes = { bundle:Boolean, js:Array, css:Array, autoCreate:Boolean };
10340 if (typeof Timeplot_urlPrefix == "string") {
10341 Timeplot.urlPrefix = Timeplot_urlPrefix;
10342 if ("Timeplot_parameters" in window) {
10343 SimileAjax.parseURLParameters(Timeplot_parameters, Timeplot.params, paramTypes);
10344 }
10345 } else {
10346 var url = SimileAjax.findScript(document, "timeplot-complete.js");
10347 if (url == null) {
10348 Timeplot.error = new Error("Failed to derive URL prefix for Simile Timeplot API code files");
10349 return;
10350 }
10351 Timeplot.urlPrefix = url.substr(0, url.indexOf("timeplot-complete.js"));
10352 SimileAjax.parseURLParameters(url, Timeplot.params, paramTypes);
10353 }
10354
10355 if (Timeplot.params.locale) { // ISO-639 language codes,
10356 // optional ISO-3166 country codes (2 characters)
10357 if (Timeplot.params.locale != "en") {
10358 var segments = Timeplot.params.locale.split("-");
10359 if (segments.length > 1 && segments[0] != "en") {
10360 locales.push(segments[0]);
10361 }
10362 locales.push(Timeplot.params.locale);
10363 }
10364 }
10365
10366 var timeplotURLPrefix = (local) ? "/timeplot/api/1.0/" : Timeplot.urlPrefix;
10367
10368 if (local && !("console" in window)) {
10369 var firebug = [ timeplotURLPrefix + "lib/firebug/firebug.js" ];
10370 SimileAjax.includeJavascriptFiles(document, "", firebug);
10371 }
10372
10373 var canvas = document.createElement("canvas");
10374
10375 if (!canvas.getContext) {
10376 var excanvas = [ timeplotURLPrefix + "lib/excanvas.js" ];
10377 SimileAjax.includeJavascriptFiles(document, "", excanvas);
10378 }
10379
10380 var scriptURLs = Timeplot.params.js || [];
10381 var cssURLs = Timeplot.params.css || [];
10382
10383 // Core scripts and styles
10384 if (Timeplot.params.bundle && !local) {
10385 scriptURLs.push(timeplotURLPrefix + "timeplot-bundle.js");
10386 cssURLs.push(timeplotURLPrefix + "timeplot-bundle.css");
10387 } else {
10388 SimileAjax.prefixURLs(scriptURLs, timeplotURLPrefix + "scripts/", javascriptFiles);
10389 SimileAjax.prefixURLs(cssURLs, timeplotURLPrefix + "styles/", cssFiles);
10390 }
10391
10392 // Localization
10393 //for (var i = 0; i < locales.length; i++) {
10394 // scriptURLs.push(Timeplot.urlPrefix + "locales/" + locales[i] + "/locale.js");
10395 //};
10396
10397 window.SimileAjax_onLoad = function() {
10398 if (local && window.console.open) window.console.open();
10399 if (Timeplot.params.callback) {
10400 eval(Timeplot.params.callback + "()");
10401 }
10402 }
10403
10404 SimileAjax.includeJavascriptFiles(document, "", scriptURLs);
10405 SimileAjax.includeCssFiles(document, "", cssURLs);
10406 Timeplot.loaded = true;
10407 };
10408
10409 // Load Timeline if it's not already loaded (after SimileAjax and before Timeplot)
10410 var loadTimeline = function() {
10411 if (typeof Timeline != "undefined") {
10412 loadTimeplot();
10413 } else {
10414 var timelineURL = (local) ? "/timeline/api-2.0/timeline-api.js?bundle=false" : "http://static.simile.mit.edu/timeline/api-2.0/timeline-api.js";
10415 window.SimileAjax_onLoad = loadTimeplot;
10416 SimileAjax.includeJavascriptFile(document, timelineURL);
10417 }
10418 };
10419
10420 // Load SimileAjax if it's not already loaded
10421 if (typeof SimileAjax == "undefined") {
10422 window.SimileAjax_onLoad = loadTimeline;
10423
10424 var url = local ?
10425 "/ajax/api-2.0/simile-ajax-api.js?bundle=false" :
10426 "http://static.simile.mit.edu/ajax/api-2.0/simile-ajax-api.js?bundle=true";
10427
10428 var createScriptElement = function() {
10429 var script = document.createElement("script");
10430 script.type = "text/javascript";
10431 script.language = "JavaScript";
10432 script.src = url;
10433 document.getElementsByTagName("head")[0].appendChild(script);
10434 }
10435
10436 if (document.body == null) {
10437 try {
10438 document.write("<script src='" + url + "' type='text/javascript'></script>");
10439 } catch (e) {
10440 createScriptElement();
10441 }
10442 } else {
10443 createScriptElement();
10444 }
10445 } else {
10446 loadTimeline();
10447 }
10448 })();
10449 /**
10450 * Timeplot
10451 *
10452 * @fileOverview Timeplot
10453 * @name Timeplot
10454 */
10455
10456 Timeline.Debug = SimileAjax.Debug; // timeline uses it's own debug system which is not as advanced
10457 var log = SimileAjax.Debug.log; // shorter name is easier to use
10458
10459 /*
10460 * This function is used to implement a raw but effective OOP-like inheritance
10461 * in various Timeplot classes.
10462 */
10463 Object.extend = function(destination, source) {
10464 for (var property in source) {
10465 destination[property] = source[property];
10466 }
10467 return destination;
10468 }
10469
10470 // ---------------------------------------------
10471
10472 /**
10473 * Create a timeplot attached to the given element and using the configuration from the given array of PlotInfos
10474 */
10475 Timeplot.create = function(elmt, plotInfos) {
10476 return new Timeplot._Impl(elmt, plotInfos);
10477 };
10478
10479 /**
10480 * Create a PlotInfo configuration from the given map of params
10481 */
10482 Timeplot.createPlotInfo = function(params) {
10483 return {
10484 id: ("id" in params) ? params.id : "p" + Math.round(Math.random() * 1000000),
10485 dataSource: ("dataSource" in params) ? params.dataSource : null,
10486 eventSource: ("eventSource" in params) ? params.eventSource : null,
10487 timeGeometry: ("timeGeometry" in params) ? params.timeGeometry : new Timeplot.DefaultTimeGeometry(),
10488 valueGeometry: ("valueGeometry" in params) ? params.valueGeometry : new Timeplot.DefaultValueGeometry(),
10489 timeZone: ("timeZone" in params) ? params.timeZone : 0,
10490 fillColor: ("fillColor" in params) ? ((params.fillColor == "string") ? new Timeplot.Color(params.fillColor) : params.fillColor) : null,
10491 fillGradient: ("fillGradient" in params) ? params.fillGradient : true,
10492 fillFrom: ("fillFrom" in params) ? params.fillFrom : Number.NEGATIVE_INFINITY,
10493 lineColor: ("lineColor" in params) ? ((params.lineColor == "string") ? new Timeplot.Color(params.lineColor) : params.lineColor) : new Timeplot.Color("#606060"),
10494 lineWidth: ("lineWidth" in params) ? params.lineWidth : 1.0,
10495 dotRadius: ("dotRadius" in params) ? params.dotRadius : 2.0,
10496 dotColor: ("dotColor" in params) ? params.dotColor : null,
10497 eventLineWidth: ("eventLineWidth" in params) ? params.eventLineWidth : 1.0,
10498 showValues: ("showValues" in params) ? params.showValues : false,
10499 roundValues: ("roundValues" in params) ? params.roundValues : true,
10500 valuesOpacity: ("valuesOpacity" in params) ? params.valuesOpacity : 75,
10501 bubbleWidth: ("bubbleWidth" in params) ? params.bubbleWidth : 300,
10502 bubbleHeight: ("bubbleHeight" in params) ? params.bubbleHeight : 200
10503 };
10504 };
10505
10506 // -------------------------------------------------------
10507
10508 /**
10509 * This is the implementation of the Timeplot object.
10510 *
10511 * @constructor
10512 */
10513 Timeplot._Impl = function(elmt, plotInfos) {
10514 this._id = "t" + Math.round(Math.random() * 1000000);
10515 this._containerDiv = elmt;
10516 this._plotInfos = plotInfos;
10517 this._painters = {
10518 background: [],
10519 foreground: []
10520 };
10521 this._painter = null;
10522 this._active = false;
10523 this._upright = false;
10524 this._initialize();
10525 };
10526
10527 Timeplot._Impl.prototype = {
10528
10529 dispose: function() {
10530 for (var i = 0; i < this._plots.length; i++) {
10531 this._plots[i].dispose();
10532 }
10533 this._plots = null;
10534 this._plotsInfos = null;
10535 this._containerDiv.innerHTML = "";
10536 },
10537
10538 /**
10539 * Returns the main container div this timeplot is operating on.
10540 */
10541 getElement: function() {
10542 return this._containerDiv;
10543 },
10544
10545 /**
10546 * Returns document this timeplot belongs to.
10547 */
10548 getDocument: function() {
10549 return this._containerDiv.ownerDocument;
10550 },
10551
10552 /**
10553 * Append the given element to the timeplot DOM
10554 */
10555 add: function(div) {
10556 this._containerDiv.appendChild(div);
10557 },
10558
10559 /**
10560 * Remove the given element to the timeplot DOM
10561 */
10562 remove: function(div) {
10563 this._containerDiv.removeChild(div);
10564 },
10565
10566 /**
10567 * Add a painter to the timeplot
10568 */
10569 addPainter: function(layerName, painter) {
10570 var layer = this._painters[layerName];
10571 if (layer) {
10572 for (var i = 0; i < layer.length; i++) {
10573 if (layer[i].context._id == painter.context._id) {
10574 return;
10575 }
10576 }
10577 layer.push(painter);
10578 }
10579 },
10580
10581 /**
10582 * Remove a painter from the timeplot
10583 */
10584 removePainter: function(layerName, painter) {
10585 var layer = this._painters[layerName];
10586 if (layer) {
10587 for (var i = 0; i < layer.length; i++) {
10588 if (layer[i].context._id == painter.context._id) {
10589 layer.splice(i, 1);
10590 break;
10591 }
10592 }
10593 }
10594 },
10595
10596 /**
10597 * Get the width in pixels of the area occupied by the entire timeplot in the page
10598 */
10599 getWidth: function() {
10600 return this._containerDiv.clientWidth;
10601 },
10602
10603 /**
10604 * Get the height in pixels of the area occupied by the entire timeplot in the page
10605 */
10606 getHeight: function() {
10607 return this._containerDiv.clientHeight;
10608 },
10609
10610 /**
10611 * Get the drawing canvas associated with this timeplot
10612 */
10613 getCanvas: function() {
10614 return this._canvas;
10615 },
10616
10617 /**
10618 * <p>Load the data from the given url into the given eventSource, using
10619 * the given separator to parse the columns and preprocess it before parsing
10620 * thru the optional filter function. The filter is useful for when
10621 * the data is row-oriented but the format is not compatible with the
10622 * one that Timeplot expects.</p>
10623 *
10624 * <p>Here is an example of a filter that changes dates in the form 'yyyy/mm/dd'
10625 * in the required 'yyyy-mm-dd' format:
10626 * <pre>var dataFilter = function(data) {
10627 * for (var i = 0; i < data.length; i++) {
10628 * var row = data[i];
10629 * row[0] = row[0].replace(/\//g,"-");
10630 * }
10631 * return data;
10632 * };</pre></p>
10633 */
10634 loadText: function(url, separator, eventSource, filter, format) {
10635 if (this._active) {
10636 var tp = this;
10637
10638 var fError = function(statusText, status, xmlhttp) {
10639 alert("Failed to load data xml from " + url + "\n" + statusText);
10640 tp.hideLoadingMessage();
10641 };
10642
10643 var fDone = function(xmlhttp) {
10644 try {
10645 eventSource.loadText(xmlhttp.responseText, separator, url, filter, format);
10646 } catch (e) {
10647 SimileAjax.Debug.exception(e);
10648 } finally {
10649 tp.hideLoadingMessage();
10650 }
10651 };
10652
10653 this.showLoadingMessage();
10654 window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
10655 }
10656 },
10657
10658 /**
10659 * Load event data from the given url into the given eventSource, using
10660 * the Timeline XML event format.
10661 */
10662 loadXML: function(url, eventSource) {
10663 if (this._active) {
10664 var tl = this;
10665
10666 var fError = function(statusText, status, xmlhttp) {
10667 alert("Failed to load data xml from " + url + "\n" + statusText);
10668 tl.hideLoadingMessage();
10669 };
10670
10671 var fDone = function(xmlhttp) {
10672 try {
10673 var xml = xmlhttp.responseXML;
10674 if (!xml.documentElement && xmlhttp.responseStream) {
10675 xml.load(xmlhttp.responseStream);
10676 }
10677 eventSource.loadXML(xml, url);
10678 } finally {
10679 tl.hideLoadingMessage();
10680 }
10681 };
10682
10683 this.showLoadingMessage();
10684 window.setTimeout(function() { SimileAjax.XmlHttp.get(url, fError, fDone); }, 0);
10685 }
10686 },
10687
10688 /**
10689 * Overlay a 'div' element filled with the given text and styles to this timeplot
10690 * This is used to implement labels since canvas does not support drawing text.
10691 */
10692 putText: function(id, text, clazz, styles) {
10693 var div = this.putDiv(id, "timeplot-div " + clazz, styles);
10694 div.innerHTML = text;
10695 return div;
10696 },
10697
10698 /**
10699 * Overlay a 'div' element, with the given class and the given styles to this timeplot.
10700 * This is used for labels and horizontal and vertical grids.
10701 */
10702 putDiv: function(id, clazz, styles) {
10703 var tid = this._id + "-" + id;
10704 var div = document.getElementById(tid);
10705 if (!div) {
10706 var container = this._containerDiv.firstChild; // get the divs container
10707 div = document.createElement("div");
10708 div.setAttribute("id",tid);
10709 container.appendChild(div);
10710 }
10711 div.setAttribute("class","timeplot-div " + clazz);
10712 div.setAttribute("className","timeplot-div " + clazz);
10713 this.placeDiv(div,styles);
10714 return div;
10715 },
10716
10717 /**
10718 * Associate the given map of styles to the given element.
10719 * In case such styles indicate position (left,right,top,bottom) correct them
10720 * with the padding information so that they align to the 'internal' area
10721 * of the timeplot.
10722 */
10723 placeDiv: function(div, styles) {
10724 if (styles) {
10725 for (style in styles) {
10726 if (style == "left") {
10727 styles[style] += this._paddingX;
10728 styles[style] += "px";
10729 } else if (style == "right") {
10730 styles[style] += this._paddingX;
10731 styles[style] += "px";
10732 } else if (style == "top") {
10733 styles[style] += this._paddingY;
10734 styles[style] += "px";
10735 } else if (style == "bottom") {
10736 styles[style] += this._paddingY;
10737 styles[style] += "px";
10738 } else if (style == "width") {
10739 if (styles[style] < 0) styles[style] = 0;
10740 styles[style] += "px";
10741 } else if (style == "height") {
10742 if (styles[style] < 0) styles[style] = 0;
10743 styles[style] += "px";
10744 }
10745 div.style[style] = styles[style];
10746 }
10747 }
10748 },
10749
10750 /**
10751 * return a {x,y} map with the location of the given element relative to the 'internal' area of the timeplot
10752 * (that is, without the container padding)
10753 */
10754 locate: function(div) {
10755 return {
10756 x: div.offsetLeft - this._paddingX,
10757 y: div.offsetTop - this._paddingY
10758 }
10759 },
10760
10761 /**
10762 * Forces timeplot to re-evaluate the various value and time geometries
10763 * associated with its plot layers and repaint accordingly. This should
10764 * be invoked after the data in any of the data sources has been
10765 * modified.
10766 */
10767 update: function() {
10768 if (this._active) {
10769 for (var i = 0; i < this._plots.length; i++) {
10770 var plot = this._plots[i];
10771 var dataSource = plot.getDataSource();
10772 if (dataSource) {
10773 var range = dataSource.getRange();
10774 if (range) {
10775 plot._valueGeometry.setRange(range);
10776 plot._timeGeometry.setRange(range);
10777 }
10778 }
10779 plot.hideValues();
10780 }
10781 this.paint();
10782 }
10783 },
10784
10785 /**
10786 * Forces timeplot to re-evaluate its own geometry, clear itself and paint.
10787 * This should be used instead of paint() when you're not sure if the
10788 * geometry of the page has changed or not.
10789 */
10790 repaint: function() {
10791 if (this._active) {
10792 this._prepareCanvas();
10793 for (var i = 0; i < this._plots.length; i++) {
10794 var plot = this._plots[i];
10795 if (plot._timeGeometry) plot._timeGeometry.reset();
10796 if (plot._valueGeometry) plot._valueGeometry.reset();
10797 }
10798 this.paint();
10799 }
10800 },
10801
10802 /**
10803 * Calls all the painters that were registered to this timeplot and makes them
10804 * paint the timeplot. This should be used only when you're sure that the geometry
10805 * of the page hasn't changed.
10806 * NOTE: painting is performed by a different thread and it's safe to call this
10807 * function in bursts (as in mousemove or during window resizing
10808 */
10809 paint: function() {
10810 if (this._active && this._painter == null) {
10811 var timeplot = this;
10812 this._painter = window.setTimeout(function() {
10813 timeplot._clearCanvas();
10814
10815 var run = function(action,context) {
10816 try {
10817 if (context.setTimeplot) context.setTimeplot(timeplot);
10818 action.apply(context,[]);
10819 } catch (e) {
10820 SimileAjax.Debug.exception(e);
10821 }
10822 }
10823
10824 var background = timeplot._painters.background;
10825 for (var i = 0; i < background.length; i++) {
10826 run(background[i].action, background[i].context);
10827 }
10828 var foreground = timeplot._painters.foreground;
10829 for (var i = 0; i < foreground.length; i++) {
10830 run(foreground[i].action, foreground[i].context);
10831 }
10832
10833 timeplot._painter = null;
10834 }, 20);
10835 }
10836 },
10837
10838 _clearCanvas: function() {
10839 var canvas = this.getCanvas();
10840 var ctx = canvas.getContext('2d');
10841 ctx.clearRect(0,0,canvas.width,canvas.height);
10842 },
10843
10844 _clearLabels: function() {
10845 var labels = this._containerDiv.firstChild;
10846 if (labels) this._containerDiv.removeChild(labels);
10847 labels = document.createElement("div");
10848 this._containerDiv.appendChild(labels);
10849 },
10850
10851 _prepareCanvas: function() {
10852 var canvas = this.getCanvas();
10853
10854 // using jQuery. note we calculate the average padding; if your
10855 // padding settings are not symmetrical, the labels will be off
10856 // since they expect to be centered on the canvas.
10857 var con = SimileAjax.jQuery(this._containerDiv);
10858 this._paddingX = (parseInt(con.css('paddingLeft')) +
10859 parseInt(con.css('paddingRight'))) / 2;
10860 this._paddingY = (parseInt(con.css('paddingTop')) +
10861 parseInt(con.css('paddingBottom'))) / 2;
10862
10863 canvas.width = this.getWidth() - (this._paddingX * 2);
10864 canvas.height = this.getHeight() - (this._paddingY * 2);
10865
10866 var ctx = canvas.getContext('2d');
10867 this._setUpright(ctx, canvas);
10868 ctx.globalCompositeOperation = 'source-over';
10869 },
10870
10871 _setUpright: function(ctx, canvas) {
10872 // excanvas+IE requires this to be done only once, ever; actual canvas
10873 // implementations reset and require this for each call to re-layout
10874 if (!SimileAjax.Platform.browser.isIE) this._upright = false;
10875 if (!this._upright) {
10876 this._upright = true;
10877 ctx.translate(0, canvas.height);
10878 ctx.scale(1,-1);
10879 }
10880 },
10881
10882 _isBrowserSupported: function(canvas) {
10883 var browser = SimileAjax.Platform.browser;
10884 if ((canvas.getContext && window.getComputedStyle) ||
10885 (browser.isIE && browser.majorVersion >= 6)) {
10886 return true;
10887 } else {
10888 return false;
10889 }
10890 },
10891
10892 _initialize: function() {
10893
10894 // initialize the window manager (used to handle the popups)
10895 // NOTE: this is a singleton and it's safe to call multiple times
10896 SimileAjax.WindowManager.initialize();
10897
10898 var containerDiv = this._containerDiv;
10899 var doc = containerDiv.ownerDocument;
10900
10901 // make sure the timeplot div has the right class
10902 containerDiv.className = "timeplot-container " + containerDiv.className;
10903
10904 // clean it up if it contains some content
10905 while (containerDiv.firstChild) {
10906 containerDiv.removeChild(containerDiv.firstChild);
10907 }
10908
10909 var canvas = doc.createElement("canvas");
10910
10911 if (this._isBrowserSupported(canvas)) {
10912 this._clearLabels();
10913
10914 this._canvas = canvas;
10915 canvas.className = "timeplot-canvas";
10916 containerDiv.appendChild(canvas);
10917 if(!canvas.getContext && G_vmlCanvasManager) {
10918 canvas = G_vmlCanvasManager.initElement(this._canvas);
10919 this._canvas = canvas;
10920 }
10921 this._prepareCanvas();
10922
10923 // inserting copyright and link to simile
10924 var elmtCopyright = SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/copyright.png");
10925 elmtCopyright.className = "timeplot-copyright";
10926 elmtCopyright.title = "Timeplot (c) SIMILE - http://simile.mit.edu/timeplot/";
10927 SimileAjax.DOM.registerEvent(elmtCopyright, "click", function() { window.location = "http://simile.mit.edu/timeplot/"; });
10928 containerDiv.appendChild(elmtCopyright);
10929
10930 var timeplot = this;
10931 var painter = {
10932 onAddMany: function() { timeplot.update(); },
10933 onClear: function() { timeplot.update(); }
10934 }
10935
10936 // creating painters
10937 this._plots = [];
10938 if (this._plotInfos) {
10939 for (var i = 0; i < this._plotInfos.length; i++) {
10940 var plot = new Timeplot.Plot(this, this._plotInfos[i]);
10941 var dataSource = plot.getDataSource();
10942 if (dataSource) {
10943 dataSource.addListener(painter);
10944 }
10945 this.addPainter("background", {
10946 context: plot.getTimeGeometry(),
10947 action: plot.getTimeGeometry().paint
10948 });
10949 this.addPainter("background", {
10950 context: plot.getValueGeometry(),
10951 action: plot.getValueGeometry().paint
10952 });
10953 this.addPainter("foreground", {
10954 context: plot,
10955 action: plot.paint
10956 });
10957 this._plots.push(plot);
10958 plot.initialize();
10959 }
10960 }
10961
10962 // creating loading UI
10963 var message = SimileAjax.Graphics.createMessageBubble(doc);
10964 message.containerDiv.className = "timeplot-message-container";
10965 containerDiv.appendChild(message.containerDiv);
10966
10967 message.contentDiv.className = "timeplot-message";
10968 message.contentDiv.innerHTML = "<img src='" + Timeplot.urlPrefix + "images/progress-running.gif' /> Loading...";
10969
10970 this.showLoadingMessage = function() { message.containerDiv.style.display = "block"; };
10971 this.hideLoadingMessage = function() { message.containerDiv.style.display = "none"; };
10972
10973 this._active = true;
10974
10975 } else {
10976
10977 this._message = SimileAjax.Graphics.createMessageBubble(doc);
10978 this._message.containerDiv.className = "timeplot-message-container";
10979 this._message.containerDiv.style.top = "15%";
10980 this._message.containerDiv.style.left = "20%";
10981 this._message.containerDiv.style.right = "20%";
10982 this._message.containerDiv.style.minWidth = "20em";
10983 this._message.contentDiv.className = "timeplot-message";
10984 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>.";
10985 this._message.containerDiv.style.display = "block";
10986
10987 containerDiv.appendChild(this._message.containerDiv);
10988
10989 }
10990 }
10991 };
10992 /**
10993 * Plot Layer
10994 *
10995 * @fileOverview Plot Layer
10996 * @name Plot
10997 */
10998
10999 /**
11000 * A plot layer is the main building block for timeplots and it's the object
11001 * that is responsible for painting the plot itself. Each plot needs to have
11002 * a time geometry, either a DataSource (for time series
11003 * plots) or an EventSource (for event plots) and a value geometry in case
11004 * of time series plots. Such parameters are passed along
11005 * in the 'plotInfo' map.
11006 *
11007 * @constructor
11008 */
11009 Timeplot.Plot = function(timeplot, plotInfo) {
11010 this._timeplot = timeplot;
11011 this._canvas = timeplot.getCanvas();
11012 this._plotInfo = plotInfo;
11013 this._id = plotInfo.id;
11014 this._timeGeometry = plotInfo.timeGeometry;
11015 this._valueGeometry = plotInfo.valueGeometry;
11016 this._theme = new Timeline.getDefaultTheme();
11017 this._dataSource = plotInfo.dataSource;
11018 this._eventSource = plotInfo.eventSource;
11019 this._bubble = null;
11020 };
11021
11022 Timeplot.Plot.prototype = {
11023
11024 /**
11025 * Initialize the plot layer
11026 */
11027 initialize: function() {
11028 if (this._dataSource && this._dataSource.getValue) {
11029 this._timeFlag = this._timeplot.putDiv("timeflag","timeplot-timeflag");
11030 this._valueFlag = this._timeplot.putDiv(this._id + "valueflag","timeplot-valueflag");
11031 this._valueFlagLineLeft = this._timeplot.putDiv(this._id + "valueflagLineLeft","timeplot-valueflag-line");
11032 this._valueFlagLineRight = this._timeplot.putDiv(this._id + "valueflagLineRight","timeplot-valueflag-line");
11033 if (!this._valueFlagLineLeft.firstChild) {
11034 this._valueFlagLineLeft.appendChild(SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/line_left.png"));
11035 this._valueFlagLineRight.appendChild(SimileAjax.Graphics.createTranslucentImage(Timeplot.urlPrefix + "images/line_right.png"));
11036 }
11037 this._valueFlagPole = this._timeplot.putDiv(this._id + "valuepole","timeplot-valueflag-pole");
11038
11039 var opacity = this._plotInfo.valuesOpacity;
11040
11041 SimileAjax.Graphics.setOpacity(this._timeFlag, opacity);
11042 SimileAjax.Graphics.setOpacity(this._valueFlag, opacity);
11043 SimileAjax.Graphics.setOpacity(this._valueFlagLineLeft, opacity);
11044 SimileAjax.Graphics.setOpacity(this._valueFlagLineRight, opacity);
11045 SimileAjax.Graphics.setOpacity(this._valueFlagPole, opacity);
11046
11047 var plot = this;
11048
11049 var mouseOverHandler = function(elmt, evt, target) {
11050 if (plot._plotInfo.showValues) {
11051 plot._valueFlag.style.display = "block";
11052 mouseMoveHandler(elmt, evt, target);
11053 }
11054 }
11055
11056 var day = 24 * 60 * 60 * 1000;
11057 var month = 30 * day;
11058
11059 var mouseMoveHandler = function(elmt, evt, target) {
11060 if (typeof SimileAjax != "undefined" && plot._plotInfo.showValues) {
11061 var c = plot._canvas;
11062 var x = Math.round(SimileAjax.DOM.getEventRelativeCoordinates(evt,plot._canvas).x);
11063 if (x > c.width) x = c.width;
11064 if (isNaN(x) || x < 0) x = 0;
11065 var t = plot._timeGeometry.fromScreen(x);
11066 if (t == 0) { // something is wrong
11067 plot._valueFlag.style.display = "none";
11068 return;
11069 }
11070
11071 var validTime = plot._dataSource.getClosestValidTime(t);
11072 x = plot._timeGeometry.toScreen(validTime);
11073 var v = plot._dataSource.getValue(validTime);
11074 if (plot._plotInfo.roundValues) v = Math.round(v);
11075 plot._valueFlag.innerHTML = new String(v);
11076 var d = new Date(validTime);
11077 var p = plot._timeGeometry.getPeriod();
11078 if (p < day) {
11079 plot._timeFlag.innerHTML = d.toLocaleTimeString();
11080 } else if (p > month) {
11081 plot._timeFlag.innerHTML = d.toLocaleDateString();
11082 } else {
11083 plot._timeFlag.innerHTML = d.toLocaleString();
11084 }
11085
11086 var tw = plot._timeFlag.clientWidth;
11087 var th = plot._timeFlag.clientHeight;
11088 var tdw = Math.round(tw / 2);
11089 var vw = plot._valueFlag.clientWidth;
11090 var vh = plot._valueFlag.clientHeight;
11091 var y = plot._valueGeometry.toScreen(v);
11092
11093 if (x + tdw > c.width) {
11094 var tx = c.width - tdw;
11095 } else if (x - tdw < 0) {
11096 var tx = tdw;
11097 } else {
11098 var tx = x;
11099 }
11100
11101 if (plot._timeGeometry._timeValuePosition == "top") {
11102 plot._timeplot.placeDiv(plot._valueFlagPole, {
11103 left: x,
11104 top: th - 5,
11105 height: c.height - y - th + 6,
11106 display: "block"
11107 });
11108 plot._timeplot.placeDiv(plot._timeFlag,{
11109 left: tx - tdw,
11110 top: -6,
11111 display: "block"
11112 });
11113 } else {
11114 plot._timeplot.placeDiv(plot._valueFlagPole, {
11115 left: x,
11116 bottom: th - 5,
11117 height: y - th + 6,
11118 display: "block"
11119 });
11120 plot._timeplot.placeDiv(plot._timeFlag,{
11121 left: tx - tdw,
11122 bottom: -6,
11123 display: "block"
11124 });
11125 }
11126
11127 if (x + vw + 14 > c.width && y + vh + 4 > c.height) {
11128 plot._valueFlagLineLeft.style.display = "none";
11129 plot._timeplot.placeDiv(plot._valueFlagLineRight,{
11130 left: x - 14,
11131 bottom: y - 14,
11132 display: "block"
11133 });
11134 plot._timeplot.placeDiv(plot._valueFlag,{
11135 left: x - vw - 13,
11136 bottom: y - vh - 13,
11137 display: "block"
11138 });
11139 } else if (x + vw + 14 > c.width && y + vh + 4 < c.height) {
11140 plot._valueFlagLineRight.style.display = "none";
11141 plot._timeplot.placeDiv(plot._valueFlagLineLeft,{
11142 left: x - 14,
11143 bottom: y,
11144 display: "block"
11145 });
11146 plot._timeplot.placeDiv(plot._valueFlag,{
11147 left: x - vw - 13,
11148 bottom: y + 13,
11149 display: "block"
11150 });
11151 } else if (x + vw + 14 < c.width && y + vh + 4 > c.height) {
11152 plot._valueFlagLineRight.style.display = "none";
11153 plot._timeplot.placeDiv(plot._valueFlagLineLeft,{
11154 left: x,
11155 bottom: y - 13,
11156 display: "block"
11157 });
11158 plot._timeplot.placeDiv(plot._valueFlag,{
11159 left: x + 13,
11160 bottom: y - 13,
11161 display: "block"
11162 });
11163 } else {
11164 plot._valueFlagLineLeft.style.display = "none";
11165 plot._timeplot.placeDiv(plot._valueFlagLineRight,{
11166 left: x,
11167 bottom: y,
11168 display: "block"
11169 });
11170 plot._timeplot.placeDiv(plot._valueFlag,{
11171 left: x + 13,
11172 bottom: y + 13,
11173 display: "block"
11174 });
11175 }
11176 }
11177 }
11178
11179 var timeplotElement = this._timeplot.getElement();
11180 SimileAjax.DOM.registerEvent(timeplotElement, "mouseover", mouseOverHandler);
11181 SimileAjax.DOM.registerEvent(timeplotElement, "mousemove", mouseMoveHandler);
11182 }
11183 },
11184
11185 /**
11186 * Dispose the plot layer and all the data sources and listeners associated to it
11187 */
11188 dispose: function() {
11189 if (this._dataSource) {
11190 this._dataSource.removeListener(this._paintingListener);
11191 this._paintingListener = null;
11192 this._dataSource.dispose();
11193 this._dataSource = null;
11194 }
11195 },
11196
11197 /**
11198 * Hide the values
11199 */
11200 hideValues: function() {
11201 if (this._valueFlag) this._valueFlag.style.display = "none";
11202 if (this._timeFlag) this._timeFlag.style.display = "none";
11203 if (this._valueFlagLineLeft) this._valueFlagLineLeft.style.display = "none";
11204 if (this._valueFlagLineRight) this._valueFlagLineRight.style.display = "none";
11205 if (this._valueFlagPole) this._valueFlagPole.style.display = "none";
11206 },
11207
11208 /**
11209 * Return the data source of this plot layer (it could be either a DataSource or an EventSource)
11210 */
11211 getDataSource: function() {
11212 return (this._dataSource) ? this._dataSource : this._eventSource;
11213 },
11214
11215 /**
11216 * Return the time geometry associated with this plot layer
11217 */
11218 getTimeGeometry: function() {
11219 return this._timeGeometry;
11220 },
11221
11222 /**
11223 * Return the value geometry associated with this plot layer
11224 */
11225 getValueGeometry: function() {
11226 return this._valueGeometry;
11227 },
11228
11229 /**
11230 * Paint this plot layer
11231 */
11232 paint: function() {
11233 var ctx = this._canvas.getContext('2d');
11234
11235 ctx.lineWidth = this._plotInfo.lineWidth;
11236 ctx.lineJoin = 'miter';
11237
11238 if (this._dataSource) {
11239 if (this._plotInfo.fillColor) {
11240 if (this._plotInfo.fillGradient) {
11241 var gradient = ctx.createLinearGradient(0,this._canvas.height,0,0);
11242 gradient.addColorStop(0,this._plotInfo.fillColor.toString());
11243 gradient.addColorStop(0.5,this._plotInfo.fillColor.toString());
11244 gradient.addColorStop(1, 'rgba(255,255,255,0)');
11245
11246 ctx.fillStyle = gradient;
11247 } else {
11248 ctx.fillStyle = this._plotInfo.fillColor.toString();
11249 }
11250
11251 ctx.beginPath();
11252 ctx.moveTo(0,0);
11253 this._plot(function(x,y) {
11254 ctx.lineTo(x,y);
11255 });
11256 if (this._plotInfo.fillFrom == Number.NEGATIVE_INFINITY) {
11257 ctx.lineTo(this._canvas.width, 0);
11258 } else if (this._plotInfo.fillFrom == Number.POSITIVE_INFINITY) {
11259 ctx.lineTo(this._canvas.width, this._canvas.height);
11260 ctx.lineTo(0, this._canvas.height);
11261 } else {
11262 ctx.lineTo(this._canvas.width, this._valueGeometry.toScreen(this._plotInfo.fillFrom));
11263 ctx.lineTo(0, this._valueGeometry.toScreen(this._plotInfo.fillFrom));
11264 }
11265 ctx.fill();
11266 }
11267
11268 if (this._plotInfo.lineColor) {
11269 ctx.strokeStyle = this._plotInfo.lineColor.toString();
11270 ctx.beginPath();
11271 var first = true;
11272 this._plot(function(x,y) {
11273 if (first) {
11274 first = false;
11275 ctx.moveTo(x,y);
11276 }
11277 ctx.lineTo(x,y);
11278 });
11279 ctx.stroke();
11280 }
11281
11282 if (this._plotInfo.dotColor) {
11283 ctx.fillStyle = this._plotInfo.dotColor.toString();
11284 var r = this._plotInfo.dotRadius;
11285 this._plot(function(x,y) {
11286 ctx.beginPath();
11287 ctx.arc(x,y,r,0,2*Math.PI,true);
11288 ctx.fill();
11289 });
11290 }
11291 }
11292
11293 if (this._eventSource) {
11294 var gradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
11295 gradient.addColorStop(1, 'rgba(255,255,255,0)');
11296
11297 ctx.strokeStyle = gradient;
11298 ctx.fillStyle = gradient;
11299 ctx.lineWidth = this._plotInfo.eventLineWidth;
11300 ctx.lineJoin = 'miter';
11301
11302 var i = this._eventSource.getAllEventIterator();
11303 while (i.hasNext()) {
11304 var event = i.next();
11305 var color = event.getColor();
11306 color = (color) ? new Timeplot.Color(color) : this._plotInfo.lineColor;
11307 var eventStart = event.getStart().getTime();
11308 var eventEnd = event.getEnd().getTime();
11309 if (eventStart == eventEnd) {
11310 var c = color.toString();
11311 gradient.addColorStop(0, c);
11312 var start = this._timeGeometry.toScreen(eventStart);
11313 start = Math.floor(start) + 0.5; // center it between two pixels (makes the rendering nicer)
11314 var end = start;
11315 ctx.beginPath();
11316 ctx.moveTo(start,0);
11317 ctx.lineTo(start,this._canvas.height);
11318 ctx.stroke();
11319 var x = start - 4;
11320 var w = 7;
11321 } else {
11322 var c = color.toString(0.5);
11323 gradient.addColorStop(0, c);
11324 var start = this._timeGeometry.toScreen(eventStart);
11325 start = Math.floor(start) + 0.5; // center it between two pixels (makes the rendering nicer)
11326 var end = this._timeGeometry.toScreen(eventEnd);
11327 end = Math.floor(end) + 0.5; // center it between two pixels (makes the rendering nicer)
11328 ctx.fillRect(start,0,end - start, this._canvas.height);
11329 var x = start;
11330 var w = end - start - 1;
11331 }
11332
11333 var div = this._timeplot.putDiv(event.getID(),"timeplot-event-box",{
11334 left: Math.round(x),
11335 width: Math.round(w),
11336 top: 0,
11337 height: this._canvas.height - 1
11338 });
11339
11340 var plot = this;
11341 var clickHandler = function(event) {
11342 return function(elmt, evt, target) {
11343 var doc = plot._timeplot.getDocument();
11344 plot._closeBubble();
11345 var coords = SimileAjax.DOM.getEventPageCoordinates(evt);
11346 var elmtCoords = SimileAjax.DOM.getPageCoordinates(elmt);
11347 plot._bubble = SimileAjax.Graphics.createBubbleForPoint(coords.x, elmtCoords.top + plot._canvas.height, plot._plotInfo.bubbleWidth, plot._plotInfo.bubbleHeight, "bottom");
11348 event.fillInfoBubble(plot._bubble.content, plot._theme, plot._timeGeometry.getLabeler());
11349 }
11350 };
11351 var mouseOverHandler = function(elmt, evt, target) {
11352 elmt.oldClass = elmt.className;
11353 elmt.className = elmt.className + " timeplot-event-box-highlight";
11354 };
11355 var mouseOutHandler = function(elmt, evt, target) {
11356 elmt.className = elmt.oldClass;
11357 elmt.oldClass = null;
11358 }
11359
11360 if (!div.instrumented) {
11361 SimileAjax.DOM.registerEvent(div, "click" , clickHandler(event));
11362 SimileAjax.DOM.registerEvent(div, "mouseover", mouseOverHandler);
11363 SimileAjax.DOM.registerEvent(div, "mouseout" , mouseOutHandler);
11364 div.instrumented = true;
11365 }
11366 }
11367 }
11368 },
11369
11370 _plot: function(f) {
11371 var data = this._dataSource.getData();
11372 if (data) {
11373 var times = data.times;
11374 var values = data.values;
11375 var T = times.length;
11376 for (var t = 0; t < T; t++) {
11377 var x = this._timeGeometry.toScreen(times[t]);
11378 var y = this._valueGeometry.toScreen(values[t]);
11379 f(x, y);
11380 }
11381 }
11382 },
11383
11384 _closeBubble: function() {
11385 if (this._bubble != null) {
11386 this._bubble.close();
11387 this._bubble = null;
11388 }
11389 }
11390
11391 }
11392 /**
11393 * Sources
11394 *
11395 * @fileOverview Sources
11396 * @name Sources
11397 */
11398
11399 /**
11400 * Timeplot.DefaultEventSource is an extension of Timeline.DefaultEventSource
11401 * and therefore reuses the exact same event loading subsystem that
11402 * Timeline uses.
11403 *
11404 * @constructor
11405 */
11406 Timeplot.DefaultEventSource = function(eventIndex) {
11407 Timeline.DefaultEventSource.apply(this, arguments);
11408 };
11409
11410 Object.extend(Timeplot.DefaultEventSource.prototype, Timeline.DefaultEventSource.prototype);
11411
11412 /**
11413 * Function used by Timeplot to load time series data from a text file.
11414 */
11415 Timeplot.DefaultEventSource.prototype.loadText = function(text, separator, url, filter, format) {
11416
11417 if (text == null) {
11418 return;
11419 }
11420
11421 this._events.maxValues = new Array();
11422 var base = this._getBaseURL(url);
11423
11424 if (!format) format = 'iso8601';
11425 var parseDateTimeFunction = this._events.getUnit().getParser(format);
11426
11427 var data = this._parseText(text, separator);
11428
11429 var added = false;
11430
11431 if (filter) {
11432 data = filter(data);
11433 }
11434
11435 if (data) {
11436 for (var i = 0; i < data.length; i++){
11437 var row = data[i];
11438 if (row.length > 1) {
11439 var dateStr = SimileAjax.jQuery.trim(row[0]);
11440 var date = parseDateTimeFunction(dateStr);
11441 if (date) {
11442 var evt = new Timeplot.DefaultEventSource.NumericEvent(date,row.slice(1));
11443 this._events.add(evt);
11444 added = true;
11445 }
11446 }
11447 }
11448 }
11449
11450 if (added) {
11451 this._fire("onAddMany", []);
11452 }
11453 }
11454
11455 /*
11456 * Parse the data file.
11457 *
11458 * Adapted from http://www.kawa.net/works/js/jkl/js/jkl-parsexml.js by Yusuke Kawasaki
11459 */
11460 Timeplot.DefaultEventSource.prototype._parseText = function (text, separator) {
11461 text = text.replace( /\r\n?/g, "\n" ); // normalize newlines
11462 var pos = 0;
11463 var len = text.length;
11464 var table = [];
11465 while (pos < len) {
11466 var line = [];
11467 if (text.charAt(pos) != '#') { // if it's not a comment, process
11468 while (pos < len) {
11469 if (text.charAt(pos) == '"') { // "..." quoted column
11470 var nextquote = text.indexOf('"', pos+1 );
11471 while (nextquote<len && nextquote > -1) {
11472 if (text.charAt(nextquote+1) != '"') {
11473 break; // end of column
11474 }
11475 nextquote = text.indexOf('"', nextquote + 2);
11476 }
11477 if ( nextquote < 0 ) {
11478 // unclosed quote
11479 } else if (text.charAt(nextquote + 1) == separator) { // end of column
11480 var quoted = text.substr(pos + 1, nextquote-pos - 1);
11481 quoted = quoted.replace(/""/g,'"');
11482 line[line.length] = quoted;
11483 pos = nextquote + 2;
11484 continue;
11485 } else if (text.charAt(nextquote + 1) == "\n" || // end of line
11486 len == nextquote + 1 ) { // end of file
11487 var quoted = text.substr(pos + 1, nextquote-pos - 1);
11488 quoted = quoted.replace(/""/g,'"');
11489 line[line.length] = quoted;
11490 pos = nextquote + 2;
11491 break;
11492 } else {
11493 // invalid column
11494 }
11495 }
11496 var nextseparator = text.indexOf(separator, pos);
11497 var nextnline = text.indexOf("\n", pos);
11498 if (nextnline < 0) nextnline = len;
11499 if (nextseparator > -1 && nextseparator < nextnline) {
11500 line[line.length] = text.substr(pos, nextseparator-pos);
11501 pos = nextseparator + 1;
11502 } else { // end of line
11503 line[line.length] = text.substr(pos, nextnline-pos);
11504 pos = nextnline + 1;
11505 break;
11506 }
11507 }
11508 } else { // if it's a comment, ignore
11509 var nextnline = text.indexOf("\n", pos);
11510 pos = (nextnline > -1) ? nextnline + 1 : cur;
11511 }
11512 if (line.length > 0) {
11513 table[table.length] = line; // push line
11514 }
11515 }
11516 if (table.length < 0) return; // null data
11517 return table;
11518 }
11519
11520 /**
11521 * Return the range of the loaded data
11522 */
11523 Timeplot.DefaultEventSource.prototype.getRange = function() {
11524 var earliestDate = this.getEarliestDate();
11525 var latestDate = this.getLatestDate();
11526 return {
11527 earliestDate: (earliestDate) ? earliestDate : null,
11528 latestDate: (latestDate) ? latestDate : null,
11529 min: 0,
11530 max: 0
11531 };
11532 }
11533
11534 // -----------------------------------------------------------------------
11535
11536 /**
11537 * A NumericEvent is an Event that also contains an array of values,
11538 * one for each columns in the loaded data file.
11539 *
11540 * @constructor
11541 */
11542 Timeplot.DefaultEventSource.NumericEvent = function(time, values) {
11543 this._id = "e" + Math.round(Math.random() * 1000000);
11544 this._time = time;
11545 this._values = values;
11546 };
11547
11548 Timeplot.DefaultEventSource.NumericEvent.prototype = {
11549 getID: function() { return this._id; },
11550 getTime: function() { return this._time; },
11551 getValues: function() { return this._values; },
11552
11553 // these are required by the EventSource
11554 getStart: function() { return this._time; },
11555 getEnd: function() { return this._time; }
11556 };
11557
11558 // -----------------------------------------------------------------------
11559
11560 /**
11561 * A DataSource represent an abstract class that represents a monodimensional time series.
11562 *
11563 * @constructor
11564 */
11565 Timeplot.DataSource = function(eventSource) {
11566 this._eventSource = eventSource;
11567 var source = this;
11568 this._processingListener = {
11569 onAddMany: function() { source._process(); },
11570 onClear: function() { source._clear(); }
11571 }
11572 this.addListener(this._processingListener);
11573 this._listeners = [];
11574 this._data = null;
11575 this._range = null;
11576 };
11577
11578 Timeplot.DataSource.prototype = {
11579
11580 _clear: function() {
11581 this._data = null;
11582 this._range = null;
11583 },
11584
11585 _process: function() {
11586 this._data = {
11587 times: new Array(),
11588 values: new Array()
11589 };
11590 this._range = {
11591 earliestDate: null,
11592 latestDate: null,
11593 min: 0,
11594 max: 0
11595 };
11596 },
11597
11598 /**
11599 * Return the range of this data source
11600 */
11601 getRange: function() {
11602 return this._range;
11603 },
11604
11605 /**
11606 * Return the actual data that this data source represents.
11607 * NOTE: _data = { times: [], values: [] }
11608 */
11609 getData: function() {
11610 return this._data;
11611 },
11612
11613 /**
11614 * Return the value associated with the given time in this time series
11615 */
11616 getValue: function(t) {
11617 if (this._data) {
11618 for (var i = 0; i < this._data.times.length; i++) {
11619 var l = this._data.times[i];
11620 if (l >= t) {
11621 return this._data.values[i];
11622 }
11623 }
11624 }
11625 return 0;
11626 },
11627
11628 /**
11629 * Return the time of the data point closest to the given time.
11630 */
11631 getClosestValidTime: function(t) {
11632 if (this._data) {
11633 for (var i = 0; i < this._data.times.length; i++) {
11634 var currentTime = this._data.times[i];
11635 if (currentTime >= t) {
11636 if (i <= 0) {
11637 return currentTime;
11638 } else {
11639 var lastTime = this._data.times[i - 1];
11640 // t must be between currentTime and lastTime.
11641 // Find the closest one.
11642 if (t - lastTime < currentTime - t) {
11643 return lastTime;
11644 } else {
11645 return currentTime;
11646 }
11647 }
11648 }
11649 }
11650 }
11651 return 0;
11652 },
11653
11654 /**
11655 * Add a listener to the underlying event source
11656 */
11657 addListener: function(listener) {
11658 this._eventSource.addListener(listener);
11659 },
11660
11661 /**
11662 * Remove a listener from the underlying event source
11663 */
11664 removeListener: function(listener) {
11665 this._eventSource.removeListener(listener);
11666 },
11667
11668 /**
11669 * Replace a listener from the underlying event source
11670 */
11671 replaceListener: function(oldListener, newListener) {
11672 this.removeListener(oldListener);
11673 this.addListener(newListener);
11674 }
11675
11676 }
11677
11678 // -----------------------------------------------------------------------
11679
11680 /**
11681 * Implementation of a DataSource that extracts the time series out of a
11682 * single column from the events
11683 *
11684 * @constructor
11685 */
11686 Timeplot.ColumnSource = function(eventSource, column) {
11687 Timeplot.DataSource.apply(this, arguments);
11688 this._column = column - 1;
11689 };
11690
11691 Object.extend(Timeplot.ColumnSource.prototype,Timeplot.DataSource.prototype);
11692
11693 Timeplot.ColumnSource.prototype.dispose = function() {
11694 this.removeListener(this._processingListener);
11695 this._clear();
11696 }
11697
11698 Timeplot.ColumnSource.prototype._process = function() {
11699 var count = this._eventSource.getCount();
11700 var times = new Array(count);
11701 var values = new Array(count);
11702 var min = Number.MAX_VALUE;
11703 var max = Number.MIN_VALUE;
11704 var i = 0;
11705
11706 var iterator = this._eventSource.getAllEventIterator();
11707 while (iterator.hasNext()) {
11708 var event = iterator.next();
11709 var time = event.getTime();
11710 times[i] = time;
11711 var value = this._getValue(event);
11712 if (!isNaN(value)) {
11713 if (value < min) {
11714 min = value;
11715 }
11716 if (value > max) {
11717 max = value;
11718 }
11719 values[i] = value;
11720 }
11721 i++;
11722 }
11723
11724 this._data = {
11725 times: times,
11726 values: values
11727 };
11728
11729 if (max == Number.MIN_VALUE) max = 1;
11730
11731 this._range = {
11732 earliestDate: this._eventSource.getEarliestDate(),
11733 latestDate: this._eventSource.getLatestDate(),
11734 min: min,
11735 max: max
11736 };
11737 }
11738
11739 Timeplot.ColumnSource.prototype._getValue = function(event) {
11740 return parseFloat(event.getValues()[this._column]);
11741 }
11742
11743 // ---------------------------------------------------------------
11744
11745 /**
11746 * Data Source that generates the time series out of the difference
11747 * between the first and the second column
11748 *
11749 * @constructor
11750 */
11751 Timeplot.ColumnDiffSource = function(eventSource, column1, column2) {
11752 Timeplot.ColumnSource.apply(this, arguments);
11753 this._column2 = column2 - 1;
11754 };
11755
11756 Object.extend(Timeplot.ColumnDiffSource.prototype,Timeplot.ColumnSource.prototype);
11757
11758 Timeplot.ColumnDiffSource.prototype._getValue = function(event) {
11759 var a = parseFloat(event.getValues()[this._column]);
11760 var b = parseFloat(event.getValues()[this._column2]);
11761 return a - b;
11762 }
11763 /**
11764 * Geometries
11765 *
11766 * @fileOverview Geometries
11767 * @name Geometries
11768 */
11769
11770 /**
11771 * This is the constructor for the default value geometry.
11772 * A value geometry is what regulates mapping of the plot values to the screen y coordinate.
11773 * If two plots share the same value geometry, they will be drawn using the same scale.
11774 * If "min" and "max" parameters are not set, the geometry will stretch itself automatically
11775 * so that the entire plot will be drawn without overflowing. The stretching happens also
11776 * when a geometry is shared between multiple plots, the one with the biggest range will
11777 * win over the others.
11778 *
11779 * @constructor
11780 */
11781 Timeplot.DefaultValueGeometry = function(params) {
11782 if (!params) params = {};
11783 this._id = ("id" in params) ? params.id : "g" + Math.round(Math.random() * 1000000);
11784 this._axisColor = ("axisColor" in params) ? ((typeof params.axisColor == "string") ? new Timeplot.Color(params.axisColor) : params.axisColor) : new Timeplot.Color("#606060"),
11785 this._gridColor = ("gridColor" in params) ? ((typeof params.gridColor == "string") ? new Timeplot.Color(params.gridColor) : params.gridColor) : null,
11786 this._gridLineWidth = ("gridLineWidth" in params) ? params.gridLineWidth : 0.5;
11787 this._axisLabelsPlacement = ("axisLabelsPlacement" in params) ? params.axisLabelsPlacement : "right";
11788 this._gridSpacing = ("gridSpacing" in params) ? params.gridStep : 50;
11789 this._gridType = ("gridType" in params) ? params.gridType : "short";
11790 this._gridShortSize = ("gridShortSize" in params) ? params.gridShortSize : 10;
11791 this._minValue = ("min" in params) ? params.min : null;
11792 this._maxValue = ("max" in params) ? params.max : null;
11793 this._linMap = {
11794 direct: function(v) {
11795 return v;
11796 },
11797 inverse: function(y) {
11798 return y;
11799 }
11800 }
11801 this._map = this._linMap;
11802 this._labels = [];
11803 this._grid = [];
11804 }
11805
11806 Timeplot.DefaultValueGeometry.prototype = {
11807
11808 /**
11809 * Since geometries can be reused across timeplots, we need to call this function
11810 * before we can paint using this geometry.
11811 */
11812 setTimeplot: function(timeplot) {
11813 this._timeplot = timeplot;
11814 this._canvas = timeplot.getCanvas();
11815 this.reset();
11816 },
11817
11818 /**
11819 * Called by all the plot layers this geometry is associated with
11820 * to update the value range. Unless min/max values are specified
11821 * in the parameters, the biggest value range will be used.
11822 */
11823 setRange: function(range) {
11824 if ((this._minValue == null) || ((this._minValue != null) && (range.min < this._minValue))) {
11825 this._minValue = range.min;
11826 }
11827 if ((this._maxValue == null) || ((this._maxValue != null) && (range.max * 1.05 > this._maxValue))) {
11828 this._maxValue = range.max * 1.05; // get a little more head room to avoid hitting the ceiling
11829 }
11830
11831 this._updateMappedValues();
11832
11833 if (!(this._minValue == 0 && this._maxValue == 0)) {
11834 this._grid = this._calculateGrid();
11835 }
11836 },
11837
11838 /**
11839 * Called after changing ranges or canvas size to reset the grid values
11840 */
11841 reset: function() {
11842 this._clearLabels();
11843 this._updateMappedValues();
11844 this._grid = this._calculateGrid();
11845 },
11846
11847 /**
11848 * Map the given value to a y screen coordinate.
11849 */
11850 toScreen: function(value) {
11851 if (this._canvas && this._maxValue) {
11852 var v = value - this._minValue;
11853 return this._canvas.height * (this._map.direct(v)) / this._mappedRange;
11854 } else {
11855 return -50;
11856 }
11857 },
11858
11859 /**
11860 * Map the given y screen coordinate to a value
11861 */
11862 fromScreen: function(y) {
11863 if (this._canvas) {
11864 return this._map.inverse(this._mappedRange * y / this._canvas.height) + this._minValue;
11865 } else {
11866 return 0;
11867 }
11868 },
11869
11870 /**
11871 * Each geometry is also a painter and paints the value grid and grid labels.
11872 */
11873 paint: function() {
11874 if (this._timeplot) {
11875 var ctx = this._canvas.getContext('2d');
11876
11877 ctx.lineJoin = 'miter';
11878
11879 // paint grid
11880 if (this._gridColor) {
11881 var gridGradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
11882 gridGradient.addColorStop(0, this._gridColor.toHexString());
11883 gridGradient.addColorStop(0.3, this._gridColor.toHexString());
11884 gridGradient.addColorStop(1, "rgba(255,255,255,0.5)");
11885
11886 ctx.lineWidth = this._gridLineWidth;
11887 ctx.strokeStyle = gridGradient;
11888
11889 for (var i = 0; i < this._grid.length; i++) {
11890 var tick = this._grid[i];
11891 var y = Math.floor(tick.y) + 0.5;
11892 if (typeof tick.label != "undefined") {
11893 if (this._axisLabelsPlacement == "left") {
11894 var div = this._timeplot.putText(this._id + "-" + i, tick.label,"timeplot-grid-label",{
11895 left: 4,
11896 bottom: y + 2,
11897 color: this._gridColor.toHexString(),
11898 visibility: "hidden"
11899 });
11900 this._labels.push(div);
11901 } else if (this._axisLabelsPlacement == "right") {
11902 var div = this._timeplot.putText(this._id + "-" + i, tick.label, "timeplot-grid-label",{
11903 right: 4,
11904 bottom: y + 2,
11905 color: this._gridColor.toHexString(),
11906 visibility: "hidden"
11907 });
11908 this._labels.push(div);
11909 }
11910 if (y + div.clientHeight < this._canvas.height + 10) {
11911 div.style.visibility = "visible"; // avoid the labels that would overflow
11912 }
11913 }
11914
11915 // draw grid
11916 ctx.beginPath();
11917 if (this._gridType == "long" || tick.label == 0) {
11918 ctx.moveTo(0, y);
11919 ctx.lineTo(this._canvas.width, y);
11920 } else if (this._gridType == "short") {
11921 if (this._axisLabelsPlacement == "left") {
11922 ctx.moveTo(0, y);
11923 ctx.lineTo(this._gridShortSize, y);
11924 } else if (this._axisLabelsPlacement == "right") {
11925 ctx.moveTo(this._canvas.width, y);
11926 ctx.lineTo(this._canvas.width - this._gridShortSize, y);
11927 }
11928 }
11929 ctx.stroke();
11930 }
11931 }
11932
11933 // paint axis
11934 var axisGradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
11935 axisGradient.addColorStop(0, this._axisColor.toString());
11936 axisGradient.addColorStop(0.5, this._axisColor.toString());
11937 axisGradient.addColorStop(1, "rgba(255,255,255,0.5)");
11938
11939 ctx.lineWidth = 1;
11940 ctx.strokeStyle = axisGradient;
11941
11942 // left axis
11943 ctx.beginPath();
11944 ctx.moveTo(0,this._canvas.height);
11945 ctx.lineTo(0,0);
11946 ctx.stroke();
11947
11948 // right axis
11949 ctx.beginPath();
11950 ctx.moveTo(this._canvas.width,0);
11951 ctx.lineTo(this._canvas.width,this._canvas.height);
11952 ctx.stroke();
11953 }
11954 },
11955
11956 /**
11957 * Removes all the labels that were added by this geometry
11958 */
11959 _clearLabels: function() {
11960 for (var i = 0; i < this._labels.length; i++) {
11961 var l = this._labels[i];
11962 var parent = l.parentNode;
11963 if (parent) parent.removeChild(l);
11964 }
11965 },
11966
11967 /*
11968 * This function calculates the grid spacing that it will be used
11969 * by this geometry to draw the grid in order to reduce clutter.
11970 */
11971 _calculateGrid: function() {
11972 var grid = [];
11973
11974 if (!this._canvas || this._valueRange == 0) return grid;
11975
11976 var power = 0;
11977 if (this._valueRange > 1) {
11978 while (Math.pow(10,power) < this._valueRange) {
11979 power++;
11980 }
11981 power--;
11982 } else {
11983 while (Math.pow(10,power) > this._valueRange) {
11984 power--;
11985 }
11986 }
11987
11988 var unit = Math.pow(10,power);
11989 var inc = unit;
11990 while (true) {
11991 var dy = this.toScreen(this._minValue + inc);
11992
11993 while (dy < this._gridSpacing) {
11994 inc += unit;
11995 dy = this.toScreen(this._minValue + inc);
11996 }
11997
11998 if (dy > 2 * this._gridSpacing) { // grids are too spaced out
11999 unit /= 10;
12000 inc = unit;
12001 } else {
12002 break;
12003 }
12004 }
12005
12006 var v = 0;
12007 var y = this.toScreen(v);
12008 if (this._minValue >= 0) {
12009 while (y < this._canvas.height) {
12010 if (y > 0) {
12011 grid.push({ y: y, label: v });
12012 }
12013 v += inc;
12014 y = this.toScreen(v);
12015 }
12016 } else if (this._maxValue <= 0) {
12017 while (y > 0) {
12018 if (y < this._canvas.height) {
12019 grid.push({ y: y, label: v });
12020 }
12021 v -= inc;
12022 y = this.toScreen(v);
12023 }
12024 } else {
12025 while (y < this._canvas.height) {
12026 if (y > 0) {
12027 grid.push({ y: y, label: v });
12028 }
12029 v += inc;
12030 y = this.toScreen(v);
12031 }
12032 v = -inc;
12033 y = this.toScreen(v);
12034 while (y > 0) {
12035 if (y < this._canvas.height) {
12036 grid.push({ y: y, label: v });
12037 }
12038 v -= inc;
12039 y = this.toScreen(v);
12040 }
12041 }
12042
12043 return grid;
12044 },
12045
12046 /*
12047 * Update the values that are used by the paint function so that
12048 * we don't have to calculate them at every repaint.
12049 */
12050 _updateMappedValues: function() {
12051 this._valueRange = Math.abs(this._maxValue - this._minValue);
12052 this._mappedRange = this._map.direct(this._valueRange);
12053 }
12054
12055 }
12056
12057 // --------------------------------------------------
12058
12059 /**
12060 * This is the constructor for a Logarithmic value geometry, which
12061 * is useful when plots have values in different magnitudes but
12062 * exhibit similar trends and such trends want to be shown on the same
12063 * plot (here a cartesian geometry would make the small magnitudes
12064 * disappear).
12065 *
12066 * NOTE: this class extends Timeplot.DefaultValueGeometry and inherits
12067 * all of the methods of that class. So refer to that class.
12068 *
12069 * @constructor
12070 */
12071 Timeplot.LogarithmicValueGeometry = function(params) {
12072 Timeplot.DefaultValueGeometry.apply(this, arguments);
12073 this._logMap = {
12074 direct: function(v) {
12075 return Math.log(v + 1) / Math.log(10);
12076 },
12077 inverse: function(y) {
12078 return Math.exp(Math.log(10) * y) - 1;
12079 }
12080 }
12081 this._mode = "log";
12082 this._map = this._logMap;
12083 this._calculateGrid = this._logarithmicCalculateGrid;
12084 };
12085
12086 Timeplot.LogarithmicValueGeometry.prototype._linearCalculateGrid = Timeplot.DefaultValueGeometry.prototype._calculateGrid;
12087
12088 Object.extend(Timeplot.LogarithmicValueGeometry.prototype,Timeplot.DefaultValueGeometry.prototype);
12089
12090 /*
12091 * This function calculates the grid spacing that it will be used
12092 * by this geometry to draw the grid in order to reduce clutter.
12093 */
12094 Timeplot.LogarithmicValueGeometry.prototype._logarithmicCalculateGrid = function() {
12095 var grid = [];
12096
12097 if (!this._canvas || this._valueRange == 0) return grid;
12098
12099 var v = 1;
12100 var y = this.toScreen(v);
12101 while (y < this._canvas.height || isNaN(y)) {
12102 if (y > 0) {
12103 grid.push({ y: y, label: v });
12104 }
12105 v *= 10;
12106 y = this.toScreen(v);
12107 }
12108
12109 return grid;
12110 };
12111
12112 /**
12113 * Turn the logarithmic scaling off.
12114 */
12115 Timeplot.LogarithmicValueGeometry.prototype.actLinear = function() {
12116 this._mode = "lin";
12117 this._map = this._linMap;
12118 this._calculateGrid = this._linearCalculateGrid;
12119 this.reset();
12120 }
12121
12122 /**
12123 * Turn the logarithmic scaling on.
12124 */
12125 Timeplot.LogarithmicValueGeometry.prototype.actLogarithmic = function() {
12126 this._mode = "log";
12127 this._map = this._logMap;
12128 this._calculateGrid = this._logarithmicCalculateGrid;
12129 this.reset();
12130 }
12131
12132 /**
12133 * Toggle logarithmic scaling seeting it to on if off and viceversa.
12134 */
12135 Timeplot.LogarithmicValueGeometry.prototype.toggle = function() {
12136 if (this._mode == "log") {
12137 this.actLinear();
12138 } else {
12139 this.actLogarithmic();
12140 }
12141 }
12142
12143 // -----------------------------------------------------
12144
12145 /**
12146 * This is the constructor for the default time geometry.
12147 *
12148 * @constructor
12149 */
12150 Timeplot.DefaultTimeGeometry = function(params) {
12151 if (!params) params = {};
12152 this._id = ("id" in params) ? params.id : "g" + Math.round(Math.random() * 1000000);
12153 this._locale = ("locale" in params) ? params.locale : "en";
12154 this._timeZone = ("timeZone" in params) ? params.timeZone : SimileAjax.DateTime.getTimezone();
12155 this._labeler = ("labeller" in params) ? params.labeller : null;
12156 this._axisColor = ("axisColor" in params) ? ((params.axisColor == "string") ? new Timeplot.Color(params.axisColor) : params.axisColor) : new Timeplot.Color("#606060"),
12157 this._gridColor = ("gridColor" in params) ? ((params.gridColor == "string") ? new Timeplot.Color(params.gridColor) : params.gridColor) : null,
12158 this._gridLineWidth = ("gridLineWidth" in params) ? params.gridLineWidth : 0.5;
12159 this._axisLabelsPlacement = ("axisLabelsPlacement" in params) ? params.axisLabelsPlacement : "bottom";
12160 this._gridStep = ("gridStep" in params) ? params.gridStep : 100;
12161 this._gridStepRange = ("gridStepRange" in params) ? params.gridStepRange : 20;
12162 this._min = ("min" in params) ? params.min : null;
12163 this._max = ("max" in params) ? params.max : null;
12164 this._timeValuePosition =("timeValuePosition" in params) ? params.timeValuePosition : "bottom";
12165 this._unit = ("unit" in params) ? params.unit : Timeline.NativeDateUnit;
12166 this._linMap = {
12167 direct: function(t) {
12168 return t;
12169 },
12170 inverse: function(x) {
12171 return x;
12172 }
12173 }
12174 this._map = this._linMap;
12175 if (!this._labeler)
12176 this._labeler = this._unit.createLabeller(this._locale, this._timeZone);
12177 var dateParser = this._unit.getParser("iso8601");
12178 if (this._min && !this._min.getTime) {
12179 this._min = dateParser(this._min);
12180 }
12181 if (this._max && !this._max.getTime) {
12182 this._max = dateParser(this._max);
12183 }
12184 this._labels = [];
12185 this._grid = [];
12186 }
12187
12188 Timeplot.DefaultTimeGeometry.prototype = {
12189
12190 /**
12191 * Since geometries can be reused across timeplots, we need to call this function
12192 * before we can paint using this geometry.
12193 */
12194 setTimeplot: function(timeplot) {
12195 this._timeplot = timeplot;
12196 this._canvas = timeplot.getCanvas();
12197 this.reset();
12198 },
12199
12200 /**
12201 * Called by all the plot layers this geometry is associated with
12202 * to update the time range. Unless min/max values are specified
12203 * in the parameters, the biggest range will be used.
12204 */
12205 setRange: function(range) {
12206 if (this._min) {
12207 this._earliestDate = this._min;
12208 } else if (range.earliestDate && ((this._earliestDate == null) || ((this._earliestDate != null) && (range.earliestDate.getTime() < this._earliestDate.getTime())))) {
12209 this._earliestDate = range.earliestDate;
12210 }
12211
12212 if (this._max) {
12213 this._latestDate = this._max;
12214 } else if (range.latestDate && ((this._latestDate == null) || ((this._latestDate != null) && (range.latestDate.getTime() > this._latestDate.getTime())))) {
12215 this._latestDate = range.latestDate;
12216 }
12217
12218 if (!this._earliestDate && !this._latestDate) {
12219 this._grid = [];
12220 } else {
12221 this.reset();
12222 }
12223 },
12224
12225 /**
12226 * Called after changing ranges or canvas size to reset the grid values
12227 */
12228 reset: function() {
12229 this._updateMappedValues();
12230 if (this._canvas) this._grid = this._calculateGrid();
12231 },
12232
12233 /**
12234 * Map the given date to a x screen coordinate.
12235 */
12236 toScreen: function(time) {
12237 if (this._canvas && this._latestDate) {
12238 var t = time - this._earliestDate.getTime();
12239 var fraction = (this._mappedPeriod > 0) ? this._map.direct(t) / this._mappedPeriod : 0;
12240 return this._canvas.width * fraction;
12241 } else {
12242 return -50;
12243 }
12244 },
12245
12246 /**
12247 * Map the given x screen coordinate to a date.
12248 */
12249 fromScreen: function(x) {
12250 if (this._canvas) {
12251 return this._map.inverse(this._mappedPeriod * x / this._canvas.width) + this._earliestDate.getTime();
12252 } else {
12253 return 0;
12254 }
12255 },
12256
12257 /**
12258 * Get a period (in milliseconds) this time geometry spans.
12259 */
12260 getPeriod: function() {
12261 return this._period;
12262 },
12263
12264 /**
12265 * Return the labeler that has been associated with this time geometry
12266 */
12267 getLabeler: function() {
12268 return this._labeler;
12269 },
12270
12271 /**
12272 * Return the time unit associated with this time geometry
12273 */
12274 getUnit: function() {
12275 return this._unit;
12276 },
12277
12278 /**
12279 * Each geometry is also a painter and paints the value grid and grid labels.
12280 */
12281 paint: function() {
12282 if (this._canvas) {
12283 var unit = this._unit;
12284 var ctx = this._canvas.getContext('2d');
12285
12286 var gradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
12287
12288 ctx.strokeStyle = gradient;
12289 ctx.lineWidth = this._gridLineWidth;
12290 ctx.lineJoin = 'miter';
12291
12292 // paint grid
12293 if (this._gridColor) {
12294 gradient.addColorStop(0, this._gridColor.toString());
12295 gradient.addColorStop(1, "rgba(255,255,255,0.9)");
12296
12297 for (var i = 0; i < this._grid.length; i++) {
12298 var tick = this._grid[i];
12299 var x = Math.floor(tick.x) + 0.5;
12300 if (this._axisLabelsPlacement == "top") {
12301 var div = this._timeplot.putText(this._id + "-" + i, tick.label,"timeplot-grid-label",{
12302 left: x + 4,
12303 top: 2,
12304 visibility: "hidden"
12305 });
12306 this._labels.push(div);
12307 } else if (this._axisLabelsPlacement == "bottom") {
12308 var div = this._timeplot.putText(this._id + "-" + i, tick.label, "timeplot-grid-label",{
12309 left: x + 4,
12310 bottom: 2,
12311 visibility: "hidden"
12312 });
12313 this._labels.push(div);
12314 }
12315 if (x + div.clientWidth < this._canvas.width + 10) {
12316 div.style.visibility = "visible"; // avoid the labels that would overflow
12317 }
12318
12319 // draw separator
12320 ctx.beginPath();
12321 ctx.moveTo(x,0);
12322 ctx.lineTo(x,this._canvas.height);
12323 ctx.stroke();
12324 }
12325 }
12326
12327 // paint axis
12328 gradient.addColorStop(0, this._axisColor.toString());
12329 gradient.addColorStop(1, "rgba(255,255,255,0.5)");
12330
12331 ctx.lineWidth = 1;
12332 gradient.addColorStop(0, this._axisColor.toString());
12333
12334 ctx.beginPath();
12335 ctx.moveTo(0,0);
12336 ctx.lineTo(this._canvas.width,0);
12337 ctx.stroke();
12338 }
12339 },
12340
12341 /*
12342 * This function calculates the grid spacing that it will be used
12343 * by this geometry to draw the grid in order to reduce clutter.
12344 */
12345 _calculateGrid: function() {
12346 var grid = [];
12347
12348 var time = SimileAjax.DateTime;
12349 var u = this._unit;
12350 var p = this._period;
12351
12352 if (p == 0) return grid;
12353
12354 // find the time units nearest to the time period
12355 if (p > time.gregorianUnitLengths[time.MILLENNIUM]) {
12356 unit = time.MILLENNIUM;
12357 } else {
12358 for (var unit = time.MILLENNIUM; unit > 0; unit--) {
12359 if (time.gregorianUnitLengths[unit-1] <= p && p < time.gregorianUnitLengths[unit]) {
12360 unit--;
12361 break;
12362 }
12363 }
12364 }
12365
12366 var t = u.cloneValue(this._earliestDate);
12367
12368 do {
12369 time.roundDownToInterval(t, unit, this._timeZone, 1, 0);
12370 var x = this.toScreen(u.toNumber(t));
12371 switch (unit) {
12372 case time.SECOND:
12373 var l = t.toLocaleTimeString();
12374 break;
12375 case time.MINUTE:
12376 var m = t.getMinutes();
12377 var l = t.getHours() + ":" + ((m < 10) ? "0" : "") + m;
12378 break;
12379 case time.HOUR:
12380 var l = t.getHours() + ":00";
12381 break;
12382 case time.DAY:
12383 case time.WEEK:
12384 case time.MONTH:
12385 var l = t.toLocaleDateString();
12386 break;
12387 case time.YEAR:
12388 case time.DECADE:
12389 case time.CENTURY:
12390 case time.MILLENNIUM:
12391 var tmpd = new Date(t.getTime());
12392 tmpd.setDate(t.getDate() + 1);
12393 var l = tmpd.getUTCFullYear();
12394 break;
12395 }
12396 if (x > 0) {
12397 grid.push({ x: x, label: l });
12398 }
12399 time.incrementByInterval(t, unit, this._timeZone);
12400 } while (t.getTime() < this._latestDate.getTime());
12401
12402 return grid;
12403 },
12404
12405 /*
12406 * Clear labels generated by this time geometry.
12407 */
12408 _clearLabels: function() {
12409 for (var i = 0; i < this._labels.length; i++) {
12410 var l = this._labels[i];
12411 var parent = l.parentNode;
12412 if (parent) parent.removeChild(l);
12413 }
12414 },
12415
12416 /*
12417 * Update the values that are used by the paint function so that
12418 * we don't have to calculate them at every repaint.
12419 */
12420 _updateMappedValues: function() {
12421 if (this._latestDate && this._earliestDate) {
12422 this._period = this._latestDate.getTime() - this._earliestDate.getTime();
12423 this._mappedPeriod = this._map.direct(this._period);
12424 } else {
12425 this._period = 0;
12426 this._mappedPeriod = 0;
12427 }
12428 }
12429
12430 }
12431
12432 // --------------------------------------------------------------
12433
12434 /**
12435 * This is the constructor for the magnifying time geometry.
12436 * Users can interact with this geometry and 'magnify' certain areas of the
12437 * plot to see the plot enlarged and resolve details that would otherwise
12438 * get lost or cluttered with a linear time geometry.
12439 *
12440 * @constructor
12441 */
12442 Timeplot.MagnifyingTimeGeometry = function(params) {
12443 Timeplot.DefaultTimeGeometry.apply(this, arguments);
12444
12445 var g = this;
12446 this._MagnifyingMap = {
12447 direct: function(t) {
12448 if (t < g._leftTimeMargin) {
12449 var x = t * g._leftRate;
12450 } else if ( g._leftTimeMargin < t && t < g._rightTimeMargin ) {
12451 var x = t * g._expandedRate + g._expandedTimeTranslation;
12452 } else {
12453 var x = t * g._rightRate + g._rightTimeTranslation;
12454 }
12455 return x;
12456 },
12457 inverse: function(x) {
12458 if (x < g._leftScreenMargin) {
12459 var t = x / g._leftRate;
12460 } else if ( g._leftScreenMargin < x && x < g._rightScreenMargin ) {
12461 var t = x / g._expandedRate + g._expandedScreenTranslation;
12462 } else {
12463 var t = x / g._rightRate + g._rightScreenTranslation;
12464 }
12465 return t;
12466 }
12467 }
12468
12469 this._mode = "lin";
12470 this._map = this._linMap;
12471 };
12472
12473 Object.extend(Timeplot.MagnifyingTimeGeometry.prototype,Timeplot.DefaultTimeGeometry.prototype);
12474
12475 /**
12476 * Initialize this geometry associating it with the given timeplot and
12477 * register the geometry event handlers to the timeplot so that it can
12478 * interact with the user.
12479 */
12480 Timeplot.MagnifyingTimeGeometry.prototype.initialize = function(timeplot) {
12481 Timeplot.DefaultTimeGeometry.prototype.initialize.apply(this, arguments);
12482
12483 if (!this._lens) {
12484 this._lens = this._timeplot.putDiv("lens","timeplot-lens");
12485 }
12486
12487 var period = 1000 * 60 * 60 * 24 * 30; // a month in the magnifying lens
12488
12489 var geometry = this;
12490
12491 var magnifyWith = function(lens) {
12492 var aperture = lens.clientWidth;
12493 var loc = geometry._timeplot.locate(lens);
12494 geometry.setMagnifyingParams(loc.x + aperture / 2, aperture, period);
12495 geometry.actMagnifying();
12496 geometry._timeplot.paint();
12497 }
12498
12499 var canvasMouseDown = function(elmt, evt, target) {
12500 geometry._canvas.startCoords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
12501 geometry._canvas.pressed = true;
12502 }
12503
12504 var canvasMouseUp = function(elmt, evt, target) {
12505 geometry._canvas.pressed = false;
12506 var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
12507 if (Timeplot.Math.isClose(coords,geometry._canvas.startCoords,5)) {
12508 geometry._lens.style.display = "none";
12509 geometry.actLinear();
12510 geometry._timeplot.paint();
12511 } else {
12512 geometry._lens.style.cursor = "move";
12513 magnifyWith(geometry._lens);
12514 }
12515 }
12516
12517 var canvasMouseMove = function(elmt, evt, target) {
12518 if (geometry._canvas.pressed) {
12519 var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
12520 if (coords.x < 0) coords.x = 0;
12521 if (coords.x > geometry._canvas.width) coords.x = geometry._canvas.width;
12522 geometry._timeplot.placeDiv(geometry._lens, {
12523 left: geometry._canvas.startCoords.x,
12524 width: coords.x - geometry._canvas.startCoords.x,
12525 bottom: 0,
12526 height: geometry._canvas.height,
12527 display: "block"
12528 });
12529 }
12530 }
12531
12532 var lensMouseDown = function(elmt, evt, target) {
12533 geometry._lens.startCoords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);;
12534 geometry._lens.pressed = true;
12535 }
12536
12537 var lensMouseUp = function(elmt, evt, target) {
12538 geometry._lens.pressed = false;
12539 }
12540
12541 var lensMouseMove = function(elmt, evt, target) {
12542 if (geometry._lens.pressed) {
12543 var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
12544 var lens = geometry._lens;
12545 var left = lens.offsetLeft + coords.x - lens.startCoords.x;
12546 if (left < geometry._timeplot._paddingX) left = geometry._timeplot._paddingX;
12547 if (left + lens.clientWidth > geometry._canvas.width - geometry._timeplot._paddingX) left = geometry._canvas.width - lens.clientWidth + geometry._timeplot._paddingX;
12548 lens.style.left = left;
12549 magnifyWith(lens);
12550 }
12551 }
12552
12553 if (!this._canvas.instrumented) {
12554 SimileAjax.DOM.registerEvent(this._canvas, "mousedown", canvasMouseDown);
12555 SimileAjax.DOM.registerEvent(this._canvas, "mousemove", canvasMouseMove);
12556 SimileAjax.DOM.registerEvent(this._canvas, "mouseup" , canvasMouseUp);
12557 SimileAjax.DOM.registerEvent(this._canvas, "mouseup" , lensMouseUp);
12558 this._canvas.instrumented = true;
12559 }
12560
12561 if (!this._lens.instrumented) {
12562 SimileAjax.DOM.registerEvent(this._lens, "mousedown", lensMouseDown);
12563 SimileAjax.DOM.registerEvent(this._lens, "mousemove", lensMouseMove);
12564 SimileAjax.DOM.registerEvent(this._lens, "mouseup" , lensMouseUp);
12565 SimileAjax.DOM.registerEvent(this._lens, "mouseup" , canvasMouseUp);
12566 this._lens.instrumented = true;
12567 }
12568 }
12569
12570 /**
12571 * Set the Magnifying parameters. c is the location in pixels where the Magnifying
12572 * center should be located in the timeplot, a is the aperture in pixel of
12573 * the Magnifying and b is the time period in milliseconds that the Magnifying
12574 * should span.
12575 */
12576 Timeplot.MagnifyingTimeGeometry.prototype.setMagnifyingParams = function(c,a,b) {
12577 a = a / 2;
12578 b = b / 2;
12579
12580 var w = this._canvas.width;
12581 var d = this._period;
12582
12583 if (c < 0) c = 0;
12584 if (c > w) c = w;
12585
12586 if (c - a < 0) a = c;
12587 if (c + a > w) a = w - c;
12588
12589 var ct = this.fromScreen(c) - this._earliestDate.getTime();
12590 if (ct - b < 0) b = ct;
12591 if (ct + b > d) b = d - ct;
12592
12593 this._centerX = c;
12594 this._centerTime = ct;
12595 this._aperture = a;
12596 this._aperturePeriod = b;
12597
12598 this._leftScreenMargin = this._centerX - this._aperture;
12599 this._rightScreenMargin = this._centerX + this._aperture;
12600 this._leftTimeMargin = this._centerTime - this._aperturePeriod;
12601 this._rightTimeMargin = this._centerTime + this._aperturePeriod;
12602
12603 this._leftRate = (c - a) / (ct - b);
12604 this._expandedRate = a / b;
12605 this._rightRate = (w - c - a) / (d - ct - b);
12606
12607 this._expandedTimeTranslation = this._centerX - this._centerTime * this._expandedRate;
12608 this._expandedScreenTranslation = this._centerTime - this._centerX / this._expandedRate;
12609 this._rightTimeTranslation = (c + a) - (ct + b) * this._rightRate;
12610 this._rightScreenTranslation = (ct + b) - (c + a) / this._rightRate;
12611
12612 this._updateMappedValues();
12613 }
12614
12615 /*
12616 * Turn magnification off.
12617 */
12618 Timeplot.MagnifyingTimeGeometry.prototype.actLinear = function() {
12619 this._mode = "lin";
12620 this._map = this._linMap;
12621 this.reset();
12622 }
12623
12624 /*
12625 * Turn magnification on.
12626 */
12627 Timeplot.MagnifyingTimeGeometry.prototype.actMagnifying = function() {
12628 this._mode = "Magnifying";
12629 this._map = this._MagnifyingMap;
12630 this.reset();
12631 }
12632
12633 /*
12634 * Toggle magnification.
12635 */
12636 Timeplot.MagnifyingTimeGeometry.prototype.toggle = function() {
12637 if (this._mode == "Magnifying") {
12638 this.actLinear();
12639 } else {
12640 this.actMagnifying();
12641 }
12642 }
12643
12644 /**
12645 * Color
12646 *
12647 * @fileOverview Color
12648 * @name Color
12649 */
12650
12651 /*
12652 * Inspired by Plotr
12653 * Copyright 2007 (c) Bas Wenneker <sabmann[a]gmail[d]com>
12654 * For use under the BSD license. <http://www.solutoire.com/plotr>
12655 */
12656
12657 /**
12658 * Create a Color object that can be used to manipulate colors programmatically.
12659 */
12660 Timeplot.Color = function(color) {
12661 this._fromHex(color);
12662 };
12663
12664 Timeplot.Color.prototype = {
12665
12666 /**
12667 * Sets the RGB values of this coor
12668 *
12669 * @param {Number} r,g,b Red green and blue values (between 0 and 255)
12670 */
12671 set: function (r,g,b,a) {
12672 this.r = r;
12673 this.g = g;
12674 this.b = b;
12675 this.a = (a) ? a : 1.0;
12676 return this.check();
12677 },
12678
12679 /**
12680 * Set the color transparency
12681 *
12682 * @param {float} a Transparency value, between 0.0 (fully transparent) and 1.0 (fully opaque).
12683 */
12684 transparency: function(a) {
12685 this.a = a;
12686 return this.check();
12687 },
12688
12689 /**
12690 * Lightens the color.
12691 *
12692 * @param {integer} level Level to lighten the color with.
12693 */
12694 lighten: function(level) {
12695 var color = new Timeplot.Color();
12696 return color.set(
12697 this.r += parseInt(level, 10),
12698 this.g += parseInt(level, 10),
12699 this.b += parseInt(level, 10)
12700 );
12701 },
12702
12703 /**
12704 * Darkens the color.
12705 *
12706 * @param {integer} level Level to darken the color with.
12707 */
12708 darken: function(level){
12709 var color = new Timeplot.Color();
12710 return color.set(
12711 this.r -= parseInt(level, 10),
12712 this.g -= parseInt(level, 10),
12713 this.b -= parseInt(level, 10)
12714 );
12715 },
12716
12717 /**
12718 * Checks and validates if the hex values r, g and b are
12719 * between 0 and 255.
12720 */
12721 check: function() {
12722 if (this.r > 255) {
12723 this.r = 255;
12724 } else if (this.r < 0){
12725 this.r = 0;
12726 }
12727 if (this.g > 255) {
12728 this.g = 255;
12729 } else if (this.g < 0) {
12730 this.g = 0;
12731 }
12732 if (this.b > 255){
12733 this.b = 255;
12734 } else if (this.b < 0){
12735 this.b = 0;
12736 }
12737 if (this.a > 1.0){
12738 this.a = 1.0;
12739 } else if (this.a < 0.0){
12740 this.a = 0.0;
12741 }
12742 return this;
12743 },
12744
12745 /**
12746 * Returns a string representation of this color.
12747 *
12748 * @param {float} alpha (optional) Transparency value, between 0.0 (fully transparent) and 1.0 (fully opaque).
12749 */
12750 toString: function(alpha) {
12751 var a = (alpha) ? alpha : ((this.a) ? this.a : 1.0);
12752 return 'rgba(' + this.r + ',' + this.g + ',' + this.b + ',' + a + ')';
12753 },
12754
12755 /**
12756 * Returns the hexadecimal representation of this color (without the alpha channel as hex colors don't support it)
12757 */
12758 toHexString: function() {
12759 return "#" + this._toHex(this.r) + this._toHex(this.g) + this._toHex(this.b);
12760 },
12761
12762 /*
12763 * Parses and stores the hex values of the input color string.
12764 *
12765 * @param {String} color Hex or rgb() css string.
12766 */
12767 _fromHex: function(color) {
12768 if(/^#?([\da-f]{3}|[\da-f]{6})$/i.test(color)){
12769 color = color.replace(/^#/, '').replace(/^([\da-f])([\da-f])([\da-f])$/i, "$1$1$2$2$3$3");
12770 this.r = parseInt(color.substr(0,2), 16);
12771 this.g = parseInt(color.substr(2,2), 16);
12772 this.b = parseInt(color.substr(4,2), 16);
12773 } else if(/^rgb *\( *\d{0,3} *, *\d{0,3} *, *\d{0,3} *\)$/i.test(color)){
12774 color = color.match(/^rgb *\( *(\d{0,3}) *, *(\d{0,3}) *, *(\d{0,3}) *\)$/i);
12775 this.r = parseInt(color[1], 10);
12776 this.g = parseInt(color[2], 10);
12777 this.b = parseInt(color[3], 10);
12778 }
12779 this.a = 1.0;
12780 return this.check();
12781 },
12782
12783 /*
12784 * Returns an hexadecimal representation of a 8 bit integer
12785 */
12786 _toHex: function(dec) {
12787 var hex = "0123456789ABCDEF"
12788 if (dec < 0) return "00";
12789 if (dec > 255) return "FF";
12790 var i = Math.floor(dec / 16);
12791 var j = dec % 16;
12792 return hex.charAt(i) + hex.charAt(j);
12793 }
12794
12795 };
12796 /**
12797 * Math Utility functions
12798 *
12799 * @fileOverview Math Utility functions
12800 * @name Math
12801 */
12802
12803 Timeplot.Math = {
12804
12805 /**
12806 * Evaluates the range (min and max values) of the given array
12807 */
12808 range: function(f) {
12809 var F = f.length;
12810 var min = Number.MAX_VALUE;
12811 var max = Number.MIN_VALUE;
12812
12813 for (var t = 0; t < F; t++) {
12814 var value = f[t];
12815 if (value < min) {
12816 min = value;
12817 }
12818 if (value > max) {
12819 max = value;
12820 }
12821 }
12822
12823 return {
12824 min: min,
12825 max: max
12826 }
12827 },
12828
12829 /**
12830 * Evaluates the windows average of a given array based on the
12831 * given window size
12832 */
12833 movingAverage: function(f, size) {
12834 var F = f.length;
12835 var g = new Array(F);
12836 for (var n = 0; n < F; n++) {
12837 var value = 0;
12838 for (var m = n - size; m < n + size; m++) {
12839 if (m < 0) {
12840 var v = f[0];
12841 } else if (m >= F) {
12842 var v = g[n-1];
12843 } else {
12844 var v = f[m];
12845 }
12846 value += v;
12847 }
12848 g[n] = value / (2 * size);
12849 }
12850 return g;
12851 },
12852
12853 /**
12854 * Returns an array with the integral of the given array
12855 */
12856 integral: function(f) {
12857 var F = f.length;
12858
12859 var g = new Array(F);
12860 var sum = 0;
12861
12862 for (var t = 0; t < F; t++) {
12863 sum += f[t];
12864 g[t] = sum;
12865 }
12866
12867 return g;
12868 },
12869
12870 /**
12871 * Normalizes an array so that its complete integral is 1.
12872 * This is useful to obtain arrays that preserve the overall
12873 * integral of a convolution.
12874 */
12875 normalize: function(f) {
12876 var F = f.length;
12877 var sum = 0.0;
12878
12879 for (var t = 0; t < F; t++) {
12880 sum += f[t];
12881 }
12882
12883 for (var t = 0; t < F; t++) {
12884 f[t] /= sum;
12885 }
12886
12887 return f;
12888 },
12889
12890 /**
12891 * Calculates the convolution between two arrays
12892 */
12893 convolution: function(f,g) {
12894 var F = f.length;
12895 var G = g.length;
12896
12897 var c = new Array(F);
12898
12899 for (var m = 0; m < F; m++) {
12900 var r = 0;
12901 var end = (m + G < F) ? m + G : F;
12902 for (var n = m; n < end; n++) {
12903 var a = f[n - G];
12904 var b = g[n - m];
12905 r += a * b;
12906 }
12907 c[m] = r;
12908 }
12909
12910 return c;
12911 },
12912
12913 // ------ Array generators -------------------------------------------------
12914 // Functions that generate arrays based on mathematical functions
12915 // Normally these are used to produce operators by convolving them with the input array
12916 // The returned arrays have the property of having
12917
12918 /**
12919 * Generate the heavyside step function of given size
12920 */
12921 heavyside: function(size) {
12922 var f = new Array(size);
12923 var value = 1 / size;
12924 for (var t = 0; t < size; t++) {
12925 f[t] = value;
12926 }
12927 return f;
12928 },
12929
12930 /**
12931 * Generate the gaussian function so that at the given 'size' it has value 'threshold'
12932 * and make sure its integral is one.
12933 */
12934 gaussian: function(size, threshold) {
12935 with (Math) {
12936 var radius = size / 2;
12937 var variance = radius * radius / log(threshold);
12938 var g = new Array(size);
12939 for (var t = 0; t < size; t++) {
12940 var l = t - radius;
12941 g[t] = exp(-variance * l * l);
12942 }
12943 }
12944
12945 return this.normalize(g);
12946 },
12947
12948 // ---- Utility Methods --------------------------------------------------
12949
12950 /**
12951 * Return x with n significant figures
12952 */
12953 round: function(x,n) {
12954 with (Math) {
12955 if (abs(x) > 1) {
12956 var l = floor(log(x)/log(10));
12957 var d = round(exp((l-n+1)*log(10)));
12958 var y = round(round(x / d) * d);
12959 return y;
12960 } else {
12961 log("FIXME(SM): still to implement for 0 < abs(x) < 1");
12962 return x;
12963 }
12964 }
12965 },
12966
12967 /**
12968 * Return the hyperbolic tangent of x
12969 */
12970 tanh: function(x) {
12971 if (x > 5) {
12972 return 1;
12973 } else if (x < 5) {
12974 return -1;
12975 } else {
12976 var expx2 = Math.exp(2 * x);
12977 return (expx2 - 1) / (expx2 + 1);
12978 }
12979 },
12980
12981 /**
12982 * Returns true if |a.x - b.x| < value && | a.y - b.y | < value
12983 */
12984 isClose: function(a,b,value) {
12985 return (a && b && Math.abs(a.x - b.x) < value && Math.abs(a.y - b.y) < value);
12986 }
12987
12988 }
12989 /**
12990 * Processing Data Source
12991 *
12992 * @fileOverview Processing Data Source and Operators
12993 * @name Processor
12994 */
12995
12996 /* -----------------------------------------------------------------------------
12997 * Operators
12998 *
12999 * These are functions that can be used directly as Timeplot.Processor operators
13000 * ----------------------------------------------------------------------------- */
13001
13002 Timeplot.Operator = {
13003
13004 /**
13005 * This is the operator used when you want to draw the cumulative sum
13006 * of a time series and not, for example, their daily values.
13007 */
13008 sum: function(data, params) {
13009 return Timeplot.Math.integral(data.values);
13010 },
13011
13012 /**
13013 * This is the operator that is used to 'smooth' a given time series
13014 * by taking the average value of a moving window centered around
13015 * each value. The size of the moving window is influenced by the 'size'
13016 * parameters in the params map.
13017 */
13018 average: function(data, params) {
13019 var size = ("size" in params) ? params.size : 30;
13020 var result = Timeplot.Math.movingAverage(data.values, size);
13021 return result;
13022 }
13023 }
13024
13025 /*==================================================
13026 * Processing Data Source
13027 *==================================================*/
13028
13029 /**
13030 * A Processor is a special DataSource that can apply an Operator
13031 * to the DataSource values and thus return a different one.
13032 *
13033 * @constructor
13034 */
13035 Timeplot.Processor = function(dataSource, operator, params) {
13036 this._dataSource = dataSource;
13037 this._operator = operator;
13038 this._params = params;
13039
13040 this._data = {
13041 times: new Array(),
13042 values: new Array()
13043 };
13044
13045 this._range = {
13046 earliestDate: null,
13047 latestDate: null,
13048 min: 0,
13049 max: 0
13050 };
13051
13052 var processor = this;
13053 this._processingListener = {
13054 onAddMany: function() { processor._process(); },
13055 onClear: function() { processor._clear(); }
13056 }
13057 this.addListener(this._processingListener);
13058 };
13059
13060 Timeplot.Processor.prototype = {
13061
13062 _clear: function() {
13063 this.removeListener(this._processingListener);
13064 this._dataSource._clear();
13065 },
13066
13067 _process: function() {
13068 // this method requires the dataSource._process() method to be
13069 // called first as to setup the data and range used below
13070 // this should be guaranteed by the order of the listener registration
13071
13072 var data = this._dataSource.getData();
13073 var range = this._dataSource.getRange();
13074
13075 var newValues = this._operator(data, this._params);
13076 var newValueRange = Timeplot.Math.range(newValues);
13077
13078 this._data = {
13079 times: data.times,
13080 values: newValues
13081 };
13082
13083 this._range = {
13084 earliestDate: range.earliestDate,
13085 latestDate: range.latestDate,
13086 min: newValueRange.min,
13087 max: newValueRange.max
13088 };
13089 },
13090
13091 getRange: function() {
13092 return this._range;
13093 },
13094
13095 getData: function() {
13096 return this._data;
13097 },
13098
13099 getValue: Timeplot.DataSource.prototype.getValue,
13100
13101 getClosestValidTime: Timeplot.DataSource.prototype.getClosestValidTime,
13102
13103 addListener: function(listener) {
13104 this._dataSource.addListener(listener);
13105 },
13106
13107 removeListener: function(listener) {
13108 this._dataSource.removeListener(listener);
13109 }
13110 }