1/*==================================================
2 *  Default Event Source
3 *==================================================
4 */
5Timeline.DefaultEventSource = function(eventIndex) {
6    this._events = (eventIndex instanceof Object) ? eventIndex : new SimileAjax.EventIndex();
7    this._listeners = [];
8};
9
10Timeline.DefaultEventSource.prototype.addListener = function(listener) {
11    this._listeners.push(listener);
12};
13
14Timeline.DefaultEventSource.prototype.removeListener = function(listener) {
15    for (var i = 0; i < this._listeners.length; i++) {
16        if (this._listeners[i] == listener) {
17            this._listeners.splice(i, 1);
18            break;
19        }
20    }
21};
22
23Timeline.DefaultEventSource.prototype.loadXML = function(xml, url) {
24    var base = this._getBaseURL(url);
25
26    var wikiURL = xml.documentElement.getAttribute("wiki-url");
27    var wikiSection = xml.documentElement.getAttribute("wiki-section");
28
29    var dateTimeFormat = xml.documentElement.getAttribute("date-time-format");
30    var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
31
32    var node = xml.documentElement.firstChild;
33    var added = false;
34    while (node != null) {
35        if (node.nodeType == 1) {
36            var description = "";
37            if (node.firstChild != null && node.firstChild.nodeType == 3) {
38//                description = node.firstChild.nodeValue;
39                description = this.concatChildValues(node)            }
40            // instant event: default is true. Or use values from isDuration or durationEvent
41            var instant = (node.getAttribute("isDuration")    === null &&
42                           node.getAttribute("durationEvent") === null) ||
43                          node.getAttribute("isDuration") == "false" ||
44                          node.getAttribute("durationEvent") == "false";
45
46            var evt = new Timeline.DefaultEventSource.Event( {
47                          id: node.getAttribute("id"),
48                       start: parseDateTimeFunction(node.getAttribute("start")),
49                         end: parseDateTimeFunction(node.getAttribute("end")),
50                 latestStart: parseDateTimeFunction(node.getAttribute("latestStart")),
51                 earliestEnd: parseDateTimeFunction(node.getAttribute("earliestEnd")),
52                     instant: instant,
53                        text: node.getAttribute("title"),
54                 description: description,
55                       image: this._resolveRelativeURL(node.getAttribute("image"), base),
56                        link: this._resolveRelativeURL(node.getAttribute("link") , base),
57                        icon: this._resolveRelativeURL(node.getAttribute("icon") , base),
58                       color: node.getAttribute("color"),
59                   textColor: node.getAttribute("textColor"),
60                   hoverText: node.getAttribute("hoverText"),
61                   classname: node.getAttribute("classname"),
62                   tapeImage: node.getAttribute("tapeImage"),
63                  tapeRepeat: node.getAttribute("tapeRepeat"),
64                     caption: node.getAttribute("caption"),
65                     eventID: node.getAttribute("eventID"),
66                    trackNum: node.getAttribute("trackNum")
67            });
68
69            evt._node = node;
70            evt.getProperty = function(name) {
71                return this._node.getAttribute(name);
72            };
73            evt.setWikiInfo(wikiURL, wikiSection);
74
75            this._events.add(evt);
76
77            added = true;
78        }
79        node = node.nextSibling;
80    }
81
82    if (added) {
83        this._fire("onAddMany", []);
84    }
85};
86
87
88Timeline.DefaultEventSource.prototype.loadJSON = function(data, url) {
89    var base = this._getBaseURL(url);
90    var added = false;
91    if (data && data.events){
92        var wikiURL = ("wikiURL" in data) ? data.wikiURL : null;
93        var wikiSection = ("wikiSection" in data) ? data.wikiSection : null;
94
95        var dateTimeFormat = ("dateTimeFormat" in data) ? data.dateTimeFormat : null;
96        var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
97
98        for (var i=0; i < data.events.length; i++){
99            var evnt = data.events[i];
100
101            // New feature: attribute synonyms. The following attribute names are interchangable.
102            // The shorter names enable smaller load files.
103            //    eid -- eventID
104            //      s -- start
105            //      e -- end
106            //     ls -- latestStart
107            //     ee -- earliestEnd
108            //      d -- description
109            //     de -- durationEvent
110            //      t -- title,
111            //      c -- classname
112
113            // Fixing issue 33:
114            // instant event: default (for JSON only) is false. Or use values from isDuration or durationEvent
115            // isDuration was negated (see issue 33, so keep that interpretation
116            var instant = evnt.isDuration ||
117                          (('durationEvent' in evnt) && !evnt.durationEvent) ||
118                          (('de' in evnt) && !evnt.de);
119            var evt = new Timeline.DefaultEventSource.Event({
120                          id: ("id" in evnt) ? evnt.id : undefined,
121                       start: parseDateTimeFunction(evnt.start || evnt.s),
122                         end: parseDateTimeFunction(evnt.end || evnt.e),
123                 latestStart: parseDateTimeFunction(evnt.latestStart || evnt.ls),
124                 earliestEnd: parseDateTimeFunction(evnt.earliestEnd || evnt.ee),
125                     instant: instant,
126                        text: evnt.title || evnt.t,
127                 description: evnt.description || evnt.d,
128                       image: this._resolveRelativeURL(evnt.image, base),
129                        link: this._resolveRelativeURL(evnt.link , base),
130                        icon: this._resolveRelativeURL(evnt.icon , base),
131                       color: evnt.color,
132                   textColor: evnt.textColor,
133                   hoverText: evnt.hoverText,
134                   classname: evnt.classname || evnt.c,
135                   tapeImage: evnt.tapeImage,
136                  tapeRepeat: evnt.tapeRepeat,
137                     caption: evnt.caption,
138                     eventID: evnt.eventID  || evnt.eid,
139                    trackNum: evnt.trackNum
140            });
141            evt._obj = evnt;
142            evt.getProperty = function(name) {
143                return this._obj[name];
144            };
145            evt.setWikiInfo(wikiURL, wikiSection);
146
147            this._events.add(evt);
148            added = true;
149        }
150    }
151
152    if (added) {
153        this._fire("onAddMany", []);
154    }
155};
156
157/*
158 *  Contributed by Morten Frederiksen, http://www.wasab.dk/morten/
159 */
160Timeline.DefaultEventSource.prototype.loadSPARQL = function(xml, url) {
161    var base = this._getBaseURL(url);
162
163    var dateTimeFormat = 'iso8601';
164    var parseDateTimeFunction = this._events.getUnit().getParser(dateTimeFormat);
165
166    if (xml == null) {
167        return;
168    }
169
170    /*
171     *  Find <results> tag
172     */
173    var node = xml.documentElement.firstChild;
174    while (node != null && (node.nodeType != 1 || node.nodeName != 'results')) {
175        node = node.nextSibling;
176    }
177
178    var wikiURL = null;
179    var wikiSection = null;
180    if (node != null) {
181        wikiURL = node.getAttribute("wiki-url");
182        wikiSection = node.getAttribute("wiki-section");
183
184        node = node.firstChild;
185    }
186
187    var added = false;
188    while (node != null) {
189        if (node.nodeType == 1) {
190            var bindings = { };
191            var binding = node.firstChild;
192            while (binding != null) {
193                if (binding.nodeType == 1 &&
194                    binding.firstChild != null &&
195                    binding.firstChild.nodeType == 1 &&
196                    binding.firstChild.firstChild != null &&
197                    binding.firstChild.firstChild.nodeType == 3) {
198                    bindings[binding.getAttribute('name')] = binding.firstChild.firstChild.nodeValue;
199                }
200                binding = binding.nextSibling;
201            }
202
203            if (bindings["start"] == null && bindings["date"] != null) {
204                bindings["start"] = bindings["date"];
205            }
206
207            // instant event: default is true. Or use values from isDuration or durationEvent
208            var instant = (bindings["isDuration"]    === null &&
209                           bindings["durationEvent"] === null) ||
210                          bindings["isDuration"] == "false" ||
211                          bindings["durationEvent"] == "false";
212
213            var evt = new Timeline.DefaultEventSource.Event({
214                          id: bindings["id"],
215                       start: parseDateTimeFunction(bindings["start"]),
216                         end: parseDateTimeFunction(bindings["end"]),
217                 latestStart: parseDateTimeFunction(bindings["latestStart"]),
218                 earliestEnd: parseDateTimeFunction(bindings["earliestEnd"]),
219                     instant: instant, // instant
220                        text: bindings["title"], // text
221                 description: bindings["description"],
222                       image: this._resolveRelativeURL(bindings["image"], base),
223                        link: this._resolveRelativeURL(bindings["link"] , base),
224                        icon: this._resolveRelativeURL(bindings["icon"] , base),
225                       color: bindings["color"],
226                   textColor: bindings["textColor"],
227                   hoverText: bindings["hoverText"],
228                     caption: bindings["caption"],
229                   classname: bindings["classname"],
230                   tapeImage: bindings["tapeImage"],
231                  tapeRepeat: bindings["tapeRepeat"],
232                     eventID: bindings["eventID"],
233                    trackNum: bindings["trackNum"]
234            });
235            evt._bindings = bindings;
236            evt.getProperty = function(name) {
237                return this._bindings[name];
238            };
239            evt.setWikiInfo(wikiURL, wikiSection);
240
241            this._events.add(evt);
242            added = true;
243        }
244        node = node.nextSibling;
245    }
246
247    if (added) {
248        this._fire("onAddMany", []);
249    }
250};
251
252Timeline.DefaultEventSource.prototype.add = function(evt) {
253    this._events.add(evt);
254    this._fire("onAddOne", [evt]);
255};
256
257Timeline.DefaultEventSource.prototype.addMany = function(events) {
258    for (var i = 0; i < events.length; i++) {
259        this._events.add(events[i]);
260    }
261    this._fire("onAddMany", []);
262};
263
264Timeline.DefaultEventSource.prototype.clear = function() {
265    this._events.removeAll();
266    this._fire("onClear", []);
267};
268
269Timeline.DefaultEventSource.prototype.getEvent = function(id) {
270    return this._events.getEvent(id);
271};
272
273Timeline.DefaultEventSource.prototype.getEventIterator = function(startDate, endDate) {
274    return this._events.getIterator(startDate, endDate);
275};
276
277Timeline.DefaultEventSource.prototype.getEventReverseIterator = function(startDate, endDate) {
278    return this._events.getReverseIterator(startDate, endDate);
279};
280
281Timeline.DefaultEventSource.prototype.getAllEventIterator = function() {
282    return this._events.getAllIterator();
283};
284
285Timeline.DefaultEventSource.prototype.getCount = function() {
286    return this._events.getCount();
287};
288
289Timeline.DefaultEventSource.prototype.getEarliestDate = function() {
290    return this._events.getEarliestDate();
291};
292
293Timeline.DefaultEventSource.prototype.getLatestDate = function() {
294    return this._events.getLatestDate();
295};
296
297Timeline.DefaultEventSource.prototype._fire = function(handlerName, args) {
298    for (var i = 0; i < this._listeners.length; i++) {
299        var listener = this._listeners[i];
300        if (handlerName in listener) {
301            try {
302                listener[handlerName].apply(listener, args);
303            } catch (e) {
304                SimileAjax.Debug.exception(e);
305            }
306        }
307    }
308};
309
310Timeline.DefaultEventSource.prototype._getBaseURL = function(url) {
311    if (url.indexOf("://") < 0) {
312        var url2 = this._getBaseURL(document.location.href);
313        if (url.substr(0,1) == "/") {
314            url = url2.substr(0, url2.indexOf("/", url2.indexOf("://") + 3)) + url;
315        } else {
316            url = url2 + url;
317        }
318    }
319
320    var i = url.lastIndexOf("/");
321    if (i < 0) {
322        return "";
323    } else {
324        return url.substr(0, i+1);
325    }
326};
327
328Timeline.DefaultEventSource.prototype._resolveRelativeURL = function(url, base) {
329    if (url == null || url == "") {
330        return url;
331    } else if (url.indexOf("://") > 0) {
332        return url;
333    } else if (url.substr(0,1) == "/") {
334        return base.substr(0, base.indexOf("/", base.indexOf("://") + 3)) + url;
335    } else {
336        return base + url;
337    }
338};
339
340
341Timeline.DefaultEventSource.Event = function(args) {
342  //
343  // Attention developers!
344  // If you add a new event attribute, please be sure to add it to
345  // all three load functions: loadXML, loadSPARCL, loadJSON.
346  // Thanks!
347  //
348  // args is a hash/object. It supports the following keys. Most are optional
349  //   id            -- an internal id. Really shouldn't be used by events.
350  //                    Timeline library clients should use eventID
351  //   eventID       -- For use by library client when writing custom painters or
352  //                    custom fillInfoBubble
353  //   start
354  //   end
355  //   latestStart
356  //   earliestEnd
357  //   instant      -- boolean. Controls precise/non-precise logic & duration/instant issues
358  //   text         -- event source attribute 'title' -- used as the label on Timelines and in bubbles.
359  //   description  -- used in bubbles
360  //   image        -- used in bubbles
361  //   link         -- used in bubbles
362  //   icon         -- on the Timeline
363  //   color        -- Timeline label and tape color
364  //   textColor    -- Timeline label color, overrides color attribute
365  //   hoverText    -- deprecated, here for backwards compatibility.
366  //                   Superceeded by caption
367  //   caption      -- tooltip-like caption on the Timeline. Uses HTML title attribute
368  //   classname    -- used to set classname in Timeline. Enables better CSS selector rules
369  //   tapeImage    -- background image of the duration event's tape div on the Timeline
370  //   tapeRepeat   -- repeat attribute for tapeImage. {repeat | repeat-x | repeat-y }
371
372  function cleanArg(arg) {
373      // clean up an arg
374      return (args[arg] != null && args[arg] != "") ? args[arg] : null;
375  }
376
377  var id = args.id ? args.id.trim() : "";
378  this._id = id.length > 0 ? id : Timeline.EventUtils.getNewEventID();
379
380  this._instant = args.instant || (args.end == null);
381
382  this._start = args.start;
383  this._end = (args.end != null) ? args.end : args.start;
384
385  this._latestStart = (args.latestStart != null) ?
386                       args.latestStart : (args.instant ? this._end : this._start);
387  this._earliestEnd = (args.earliestEnd != null) ? args.earliestEnd : this._end;
388
389  // check sanity of dates since incorrect dates will later cause calculation errors
390  // when painting
391  var err=[];
392  if (this._start > this._latestStart) {
393          this._latestStart = this._start;
394          err.push("start is > latestStart");}
395  if (this._start > this._earliestEnd) {
396          this._earliestEnd = this._latestStart;
397          err.push("start is > earliestEnd");}
398  if (this._start > this._end) {
399          this._end = this._earliestEnd;
400          err.push("start is > end");}
401  if (this._latestStart > this._earliestEnd) {
402          this._earliestEnd = this._latestStart;
403          err.push("latestStart is > earliestEnd");}
404  if (this._latestStart > this._end) {
405          this._end = this._earliestEnd;
406          err.push("latestStart is > end");}
407  if (this._earliestEnd > this._end) {
408          this._end = this._earliestEnd;
409          err.push("earliestEnd is > end");}
410
411  this._eventID = cleanArg('eventID');
412  this._text = (args.text != null) ? SimileAjax.HTML.deEntify(args.text) : ""; // Change blank titles to ""
413  if (err.length > 0) {
414          this._text += " PROBLEM: " + err.join(", ");
415  }
416
417  this._description = SimileAjax.HTML.deEntify(args.description);
418  this._image = cleanArg('image');
419  this._link =  cleanArg('link');
420  this._title = cleanArg('hoverText');
421  this._title = cleanArg('caption');
422
423  this._icon = cleanArg('icon');
424  this._color = cleanArg('color');
425  this._textColor = cleanArg('textColor');
426  this._classname = cleanArg('classname');
427  this._tapeImage = cleanArg('tapeImage');
428  this._tapeRepeat = cleanArg('tapeRepeat');
429  this._trackNum = cleanArg('trackNum');
430  if (this._trackNum != null) {
431      this._trackNum = parseInt(this._trackNum);
432  }
433
434  this._wikiURL = null;
435  this._wikiSection = null;
436};
437
438Timeline.DefaultEventSource.Event.prototype = {
439    getID:          function() { return this._id; },
440
441    isInstant:      function() { return this._instant; },
442    isImprecise:    function() { return this._start != this._latestStart || this._end != this._earliestEnd; },
443
444    getStart:       function() { return this._start; },
445    getEnd:         function() { return this._end; },
446    getLatestStart: function() { return this._latestStart; },
447    getEarliestEnd: function() { return this._earliestEnd; },
448
449    getEventID:     function() { return this._eventID; },
450    getText:        function() { return this._text; }, // title
451    getDescription: function() { return this._description; },
452    getImage:       function() { return this._image; },
453    getLink:        function() { return this._link; },
454
455    getIcon:        function() { return this._icon; },
456    getColor:       function() { return this._color; },
457    getTextColor:   function() { return this._textColor; },
458    getClassName:   function() { return this._classname; },
459    getTapeImage:   function() { return this._tapeImage; },
460    getTapeRepeat:  function() { return this._tapeRepeat; },
461    getTrackNum:    function() { return this._trackNum; },
462
463    getProperty:    function(name) { return null; },
464
465    getWikiURL:     function() { return this._wikiURL; },
466    getWikiSection: function() { return this._wikiSection; },
467    setWikiInfo: function(wikiURL, wikiSection) {
468        this._wikiURL = wikiURL;
469        this._wikiSection = wikiSection;
470    },
471
472    fillDescription: function(elmt) {
473        if (this._description) {
474            elmt.innerHTML = this._description;
475        }
476    },
477    fillWikiInfo: function(elmt) {
478        // Many bubbles will not support a wiki link.
479        //
480        // Strategy: assume no wiki link. If we do have
481        // enough parameters for one, then create it.
482        elmt.style.display = "none"; // default
483
484        if (this._wikiURL == null || this._wikiSection == null) {
485          return; // EARLY RETURN
486        }
487
488        // create the wikiID from the property or from the event text (the title)
489        var wikiID = this.getProperty("wikiID");
490        if (wikiID == null || wikiID.length == 0) {
491            wikiID = this.getText(); // use the title as the backup wiki id
492        }
493
494        if (wikiID == null || wikiID.length == 0) {
495          return; // No wikiID. Thus EARLY RETURN
496        }
497
498        // ready to go...
499        elmt.style.display = "inline";
500        wikiID = wikiID.replace(/\s/g, "_");
501        var url = this._wikiURL + this._wikiSection.replace(/\s/g, "_") + "/" + wikiID;
502        var a = document.createElement("a");
503        a.href = url;
504        a.target = "new";
505        a.innerHTML = Timeline.strings[Timeline.clientLocale].wikiLinkLabel;
506
507        elmt.appendChild(document.createTextNode("["));
508        elmt.appendChild(a);
509        elmt.appendChild(document.createTextNode("]"));
510    },
511
512    fillTime: function(elmt, labeller) {
513        if (this._instant) {
514            if (this.isImprecise()) {
515                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
516                elmt.appendChild(elmt.ownerDocument.createElement("br"));
517                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
518            } else {
519                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
520            }
521        } else {
522            if (this.isImprecise()) {
523                elmt.appendChild(elmt.ownerDocument.createTextNode(
524                    labeller.labelPrecise(this._start) + " ~ " + labeller.labelPrecise(this._latestStart)));
525                elmt.appendChild(elmt.ownerDocument.createElement("br"));
526                elmt.appendChild(elmt.ownerDocument.createTextNode(
527                    labeller.labelPrecise(this._earliestEnd) + " ~ " + labeller.labelPrecise(this._end)));
528            } else {
529                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._start)));
530                elmt.appendChild(elmt.ownerDocument.createElement("br"));
531                elmt.appendChild(elmt.ownerDocument.createTextNode(labeller.labelPrecise(this._end)));
532            }
533        }
534    },
535
536    fillInfoBubble: function(elmt, theme, labeller) {
537        var doc = elmt.ownerDocument;
538
539        var title = this.getText();
540        var link = this.getLink();
541        var image = this.getImage();
542
543        if (image != null) {
544            var img = doc.createElement("img");
545            img.src = image;
546
547            theme.event.bubble.imageStyler(img);
548            elmt.appendChild(img);
549        }
550
551        var divTitle = doc.createElement("div");
552        var textTitle = doc.createTextNode(title);
553        if (link != null) {
554            var a = doc.createElement("a");
555            a.href = link;
556            a.appendChild(textTitle);
557            divTitle.appendChild(a);
558        } else {
559            divTitle.appendChild(textTitle);
560        }
561        theme.event.bubble.titleStyler(divTitle);
562        elmt.appendChild(divTitle);
563
564        var divBody = doc.createElement("div");
565        this.fillDescription(divBody);
566        theme.event.bubble.bodyStyler(divBody);
567        elmt.appendChild(divBody);
568
569        var divTime = doc.createElement("div");
570        this.fillTime(divTime, labeller);
571        theme.event.bubble.timeStyler(divTime);
572        elmt.appendChild(divTime);
573
574        var divWiki = doc.createElement("div");
575        this.fillWikiInfo(divWiki);
576        theme.event.bubble.wikiStyler(divWiki);
577        elmt.appendChild(divWiki);
578    }
579};
580
581
582