1 /**
  2  * @file timeline.js
  3  *
  4  * @brief
  5  * The Timeline is an interactive visualization chart to visualize events in
  6  * time, having a start and end date.
  7  * You can freely move and zoom in the timeline by dragging
  8  * and scrolling in the Timeline. Items are optionally dragable. The time
  9  * scale on the axis is adjusted automatically, and supports scales ranging
 10  * from milliseconds to years.
 11  *
 12  * Timeline is part of the CHAP Links library.
 13  *
 14  * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
 15  * Internet Explorer 6+.
 16  *
 17  * @license
 18  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 19  * use this file except in compliance with the License. You may obtain a copy
 20  * of the License at
 21  *
 22  * http://www.apache.org/licenses/LICENSE-2.0
 23  *
 24  * Unless required by applicable law or agreed to in writing, software
 25  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 26  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 27  * License for the specific language governing permissions and limitations under
 28  * the License.
 29  *
 30  * Copyright (c) 2011-2012 Almende B.V.
 31  *
 32  * @author 	Jos de Jong, <jos@almende.org>
 33  * @date    2012-06-15
 34  * @version 2.1.2
 35  */
 36 
 37 /*
 38  * TODO
 39  *
 40  * Add zooming with pinching on Android
 41  * 
 42  * Bug: when an item contains a javascript onclick or a link, this does not work
 43  *      when the item is not selected (when the item is being selected,
 44  *      it is redrawn, which cancels any onclick or link action)
 45  * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly
 46  * Bug: neglect items when they have no valid start/end, instead of throwing an error
 47  * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
 48  * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
 49  * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
 50  */
 51 
 52 /**
 53  * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 54  * "links"
 55  */
 56 if (typeof links === 'undefined') {
 57     links = {};
 58     // important: do not use var, as "var links = {};" will overwrite 
 59     //            the existing links variable value with undefined in IE8, IE7.  
 60 }
 61 
 62 
 63 /**
 64  * Ensure the variable google exists
 65  */
 66 if (typeof google === 'undefined') {
 67     google = undefined;
 68     // important: do not use var, as "var google = undefined;" will overwrite 
 69     //            the existing google variable value with undefined in IE8, IE7.
 70 }
 71 
 72 
 73 /**
 74  * @constructor links.Timeline
 75  * The timeline is a visualization chart to visualize events in time.
 76  *
 77  * The timeline is developed in javascript as a Google Visualization Chart.
 78  *
 79  * @param {Element} container   The DOM element in which the Timeline will
 80  *                                  be created. Normally a div element.
 81  */
 82 links.Timeline = function(container) {
 83     // create variables and set default values
 84     this.dom = {};
 85     this.conversion = {};
 86     this.eventParams = {}; // stores parameters for mouse events
 87     this.groups = [];
 88     this.groupIndexes = {};
 89     this.items = [];
 90     this.selection = undefined; // stores index and item which is currently selected
 91 
 92     this.listeners = {}; // event listener callbacks
 93 
 94     // Initialize sizes. 
 95     // Needed for IE (which gives an error when you try to set an undefined
 96     // value in a style)
 97     this.size = {
 98         'actualHeight': 0,
 99         'axis': {
100             'characterMajorHeight': 0,
101             'characterMajorWidth': 0,
102             'characterMinorHeight': 0,
103             'characterMinorWidth': 0,
104             'height': 0,
105             'labelMajorTop': 0,
106             'labelMinorTop': 0,
107             'line': 0,
108             'lineMajorWidth': 0,
109             'lineMinorHeight': 0,
110             'lineMinorTop': 0,
111             'lineMinorWidth': 0,
112             'top': 0
113         },
114         'contentHeight': 0,
115         'contentLeft': 0,
116         'contentWidth': 0,
117         'dataChanged': false,
118         'frameHeight': 0,
119         'frameWidth': 0,
120         'groupsLeft': 0,
121         'groupsWidth': 0,
122         'items': {
123             'top': 0
124         }
125     };
126 
127     this.dom.container = container;
128 
129     this.options = {
130         'width': "100%",
131         'height': "auto",
132         'minHeight': 0,       // minimal height in pixels
133         'autoHeight': true,
134 
135         'eventMargin': 10,    // minimal margin between events 
136         'eventMarginAxis': 20, // minimal margin beteen events and the axis
137         'dragAreaWidth': 10, // pixels
138 
139         'min': undefined,
140         'max': undefined,
141         'intervalMin': 10,  // milliseconds
142         'intervalMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
143 
144         'moveable': true,
145         'zoomable': true,
146         'selectable': true,
147         'editable': false,
148         'snapEvents': true,
149         'groupChangeable': true,
150 
151         'showCurrentTime': true, // show a red bar displaying the current time
152         'showCustomTime': false, // show a blue, draggable bar displaying a custom time    
153         'showMajorLabels': true,
154         'showNavigation': false,
155         'showButtonAdd': true,
156         'groupsOnRight': false,
157         'axisOnTop': false,
158         'stackEvents': true,
159         'animate': true,
160         'animateZoom': true,
161         'style': 'box'
162     };
163 
164     this.clientTimeOffset = 0;    // difference between client time and the time
165     // set via Timeline.setCurrentTime()
166     var dom = this.dom;
167 
168     // remove all elements from the container element.
169     while (dom.container.hasChildNodes()) {
170         dom.container.removeChild(dom.container.firstChild);
171     }
172 
173     // create a step for drawing the axis
174     this.step = new links.Timeline.StepDate();
175 
176     // initialize data
177     this.data = [];
178     this.firstDraw = true;
179 
180     // date interval must be initialized 
181     this.setVisibleChartRange(undefined, undefined, false);
182 
183     // create all DOM elements
184     this.redrawFrame();
185 
186     // Internet Explorer does not support Array.indexof, 
187     // so we define it here in that case
188     // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
189     if(!Array.prototype.indexOf) {
190         Array.prototype.indexOf = function(obj){
191             for(var i = 0; i < this.length; i++){
192                 if(this[i] == obj){
193                     return i;
194                 }
195             }
196             return -1;
197         }
198     }
199 
200     // fire the ready event
201     this.trigger('ready');
202 };
203 
204 
205 /**
206  * Main drawing logic. This is the function that needs to be called
207  * in the html page, to draw the timeline.
208  *
209  * A data table with the events must be provided, and an options table.
210  *
211  * @param {google.visualization.DataTable}      data
212  *                                 The data containing the events for the timeline.
213  *                                 Object DataTable is defined in
214  *                                 google.visualization.DataTable
215  * @param {Object} options         A name/value map containing settings for the
216  *                                 timeline. Optional.
217  */
218 links.Timeline.prototype.draw = function(data, options) {
219     this.setOptions(options);
220 
221     // read the data
222     this.setData(data);
223 
224     // set timer range. this will also redraw the timeline
225     if (options && options.start && options.end) {
226         this.setVisibleChartRange(options.start, options.end);
227     }
228     else if (this.firstDraw) {
229         this.setVisibleChartRangeAuto();
230     }
231 
232     this.firstDraw = false;
233 };
234 
235 
236 /**
237  * Set options for the timeline.
238  * Timeline must be redrawn afterwards
239  * @param {Object} options A name/value map containing settings for the
240  *                                 timeline. Optional.
241  */
242 links.Timeline.prototype.setOptions = function(options) {
243     if (options) {
244         // retrieve parameter values
245         for (var i in options) {
246             if (options.hasOwnProperty(i)) {
247                 this.options[i] = options[i];
248             }
249         }
250     }
251 
252     // validate options
253     this.options.autoHeight = (this.options.height === "auto");
254 };
255 
256 /**
257  * Set data for the timeline
258  * @param {google.visualization.DataTable | array} data
259  */
260 links.Timeline.prototype.setData = function(data) {
261     // unselect any previously selected item
262     this.unselectItem();
263 
264     if (!data) {
265         data = [];
266     }
267 
268     this.items = [];
269     this.data = data;
270     var items = this.items;
271     var options = this.options;
272 
273     // create groups from the data
274     this.setGroups(data);
275 
276     if (google && google.visualization &&
277         data instanceof google.visualization.DataTable) {
278         // read DataTable
279         var hasGroups = (data.getNumberOfColumns() > 3);
280         for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
281             items.push(this.createItem({
282                 'start': data.getValue(row, 0),
283                 'end': data.getValue(row, 1),
284                 'content': data.getValue(row, 2),
285                 'group': (hasGroups ? data.getValue(row, 3) : undefined)
286             }));
287         }
288     }
289     else if (links.Timeline.isArray(data)) {
290         // read JSON array
291         for (var row = 0, rows = data.length; row < rows; row++) {
292             var itemData = data[row]
293             var item = this.createItem(itemData);
294             items.push(item);
295         }
296     }
297     else {
298         throw "Unknown data type. DataTable or Array expected.";
299     }
300 
301     // set a flag to force the recalcSize method to recalculate the 
302     // heights and widths of the events
303     this.size.dataChanged = true;
304     this.redrawFrame();      // create the items for the new data
305     this.recalcSize();       // position the items
306     this.stackEvents(false);
307     this.redrawFrame();      // redraw the items on the final positions
308     this.size.dataChanged = false;
309 };
310 
311 /**
312  * Set the groups available in the given dataset
313  * @param {google.visualization.DataTable | array} data
314  */
315 links.Timeline.prototype.setGroups = function (data) {
316     this.deleteGroups();
317     var groups = this.groups;
318     var groupIndexes = this.groupIndexes;
319 
320     if (google && google.visualization &&
321         data instanceof google.visualization.DataTable) {
322         // get groups from DataTable
323         var hasGroups = (data.getNumberOfColumns() > 3);
324         if (hasGroups) {
325             var groupNames = data.getDistinctValues(3);
326             for (var i = 0, iMax = groupNames.length; i < iMax; i++) {
327                 this.addGroup(groupNames[i]);
328             }
329         }
330     }
331     else if (links.Timeline.isArray(data)){
332         // get groups from JSON Array
333         for (var i = 0, iMax = data.length; i < iMax; i++) {
334             var row = data[i],
335                 group = row.group;
336             if (group) {
337                 this.addGroup(group);
338             }
339         }
340     }
341     else {
342         throw 'Unknown data type. DataTable or Array expected.';
343     }
344 };
345 
346 
347 /**
348  * Return the original data table.
349  * @return {google.visualization.DataTable | Array} data
350  */
351 links.Timeline.prototype.getData = function  () {
352     return this.data;
353 };
354 
355 
356 /**
357  * Update the original data with changed start, end or group.
358  *
359  * @param {Number} index
360  * @param {Object} values   An object containing some of the following parameters:
361  *                          {Date} start,
362  *                          {Date} end,
363  *                          {String} content,
364  *                          {String} group
365  */
366 links.Timeline.prototype.updateData = function  (index, values) {
367     var data = this.data;
368 
369     if (google && google.visualization &&
370         data instanceof google.visualization.DataTable) {
371         // update the original google DataTable
372         var missingRows = (index + 1) - data.getNumberOfRows();
373         if (missingRows > 0) {
374             data.addRows(missingRows);
375         }
376 
377         if (values.start) {
378             data.setValue(index, 0, values.start);
379         }
380         if (values.end) {
381             data.setValue(index, 1, values.end);
382         }
383         if (values.content) {
384             data.setValue(index, 2, values.content);
385         }
386         if (values.group && data.getNumberOfColumns() > 3) {
387             // TODO: append a column when needed?
388             data.setValue(index, 3, values.group);
389         }
390     }
391     else if (links.Timeline.isArray(data)) {
392         // update the original JSON table
393         var row = data[index];
394         if (row == undefined) {
395             row = {};
396             data[index] = row;
397         }
398 
399         if (values.start) {
400             row.start = values.start;
401         }
402         if (values.end) {
403             row.end = values.end;
404         }
405         if (values.content) {
406             row.content = values.content;
407         }
408         if (values.group) {
409             row.group = values.group;
410         }
411     }
412     else {
413         throw "Cannot update data, unknown type of data";
414     }
415 };
416 
417 /**
418  * Find the item index from a given HTML element
419  * If no item index is found, undefined is returned
420  * @param {Element} element
421  * @return {Number} index
422  */
423 links.Timeline.prototype.getItemIndex = function(element) {
424     var e = element,
425         dom = this.dom,
426         items = this.items,
427         index = undefined;
428 
429     // try to find the frame where the items are located in
430     while (e.parentNode && e.parentNode !== dom.items.frame) {
431         e = e.parentNode;
432     }
433 
434     if (e.parentNode === dom.items.frame) {
435         // yes! we have found the parent element of all items
436         // retrieve its id from the array with items
437         for (var i = 0, iMax = items.length; i < iMax; i++) {
438             if (items[i].dom === e) {
439                 index = i;
440                 break;
441             }
442         }
443     }
444 
445     return index;
446 };
447 
448 /**
449  * Set a new size for the timeline
450  * @param {string} width   Width in pixels or percentage (for example "800px"
451  *                         or "50%")
452  * @param {string} height  Height in pixels or percentage  (for example "400px"
453  *                         or "30%")
454  */
455 links.Timeline.prototype.setSize = function(width, height) {
456     if (width) {
457         this.options.width = width;
458         this.dom.frame.style.width = width;
459     }
460     if (height) {
461         this.options.height = height;
462         this.options.autoHeight = (this.options.height === "auto");
463         if (height !==  "auto" ) {
464             this.dom.frame.style.height = height;
465         }
466     }
467 
468     this.recalcSize();
469     this.stackEvents(false);
470     this.redrawFrame();
471 };
472 
473 
474 /**
475  * Set a new value for the visible range int the timeline.
476  * Set start to null to include everything from the earliest date to end.
477  * Set end to null to include everything from start to the last date.
478  * Example usage:
479  *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
480  *                                    new Date("2010-09-13"));
481  * @param {Date}   start     The start date for the timeline. optional
482  * @param {Date}   end       The end date for the timeline. optional
483  * @param {boolean} redraw   Optional. If true (default) the Timeline is
484  *                           directly redrawn
485  */
486 links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
487     if (start == undefined) {
488         // default of 3 days ago
489         start = new Date();
490         start.setDate(start.getDate() - 3);
491     }
492 
493     if (end == undefined) {
494         // default of 4 days ahead
495         end = new Date();
496         end.setDate(start.getDate() + 4);
497     }
498 
499     // prevent start Date <= end Date
500     if (end.valueOf() <= start.valueOf()) {
501         end = new Date(start);
502         end.setDate(end.getDate() + 7);
503     }
504 
505     // limit to the allowed range (dont let this do by applyRange,
506     // because that method will try to maintain the interval (end-start)
507     var min = this.options.min ? this.options.min.valueOf() : undefined;
508     if (min && start.valueOf() < min) {
509         start = new Date(min);
510     }
511     var max = this.options.max ? this.options.max.valueOf() : undefined;
512     if (max && end.valueOf() > max) {
513         end = new Date(max);
514     }
515 
516     this.applyRange(start, end);
517 
518     if (redraw == undefined || redraw == true) {
519         this.recalcSize();
520         this.stackEvents(false);
521         this.redrawFrame();
522     }
523     else {
524         this.recalcConversion();
525     }
526 };
527 
528 
529 /**
530  * Change the visible chart range such that all items become visible
531  */
532 links.Timeline.prototype.setVisibleChartRangeAuto = function() {
533     var items = this.items,
534         startMin = undefined, // long value of a data
535         endMax = undefined;   // long value of a data
536 
537     // find earliest start date from the data
538     for (var i = 0, iMax = items.length; i < iMax; i++) {
539         var item = items[i],
540             start = item.start ? item.start.valueOf() : undefined,
541             end = item.end ? item.end.valueOf() : start;
542 
543         if (startMin !== undefined && start !== undefined) {
544             startMin = Math.min(startMin, start);
545         }
546         else {
547             startMin = start;
548         }
549         if (endMax !== undefined && end !== undefined) {
550             endMax = Math.max(endMax, end);
551         }
552         else {
553             endMax = end;
554         }
555     }
556 
557     if (startMin !== undefined && endMax !== undefined) {
558         // zoom out 5% such that you have a little white space on the left and right
559         var center = (endMax + startMin) / 2,
560             diff = (endMax - startMin);
561         startMin = startMin - diff * 0.05;
562         endMax = endMax + diff * 0.05;
563 
564         // adjust the start and end date
565         this.setVisibleChartRange(new Date(startMin), new Date(endMax));
566     }
567     else {
568         this.setVisibleChartRange(undefined, undefined);
569     }
570 };
571 
572 /**
573  * Adjust the visible range such that the current time is located in the center
574  * of the timeline
575  */
576 links.Timeline.prototype.setVisibleChartRangeNow = function() {
577     var now = new Date();
578 
579     var diff = (this.end.getTime() - this.start.getTime());
580 
581     var startNew = new Date(now.getTime() - diff/2);
582     var endNew = new Date(startNew.getTime() + diff);
583     this.setVisibleChartRange(startNew, endNew);
584 };
585 
586 
587 /**
588  * Retrieve the current visible range in the timeline.
589  * @return {Object} An object with start and end properties
590  */
591 links.Timeline.prototype.getVisibleChartRange = function() {
592     var range = {
593         'start': new Date(this.start),
594         'end': new Date(this.end)
595     };
596     return range;
597 };
598 
599 
600 /**
601  * Redraw the timeline. This needs to be executed after the start and/or
602  * end time are changed, or when data is added or removed dynamically.
603  */
604 links.Timeline.prototype.redrawFrame = function() {
605     var dom = this.dom,
606         options = this.options,
607         size = this.size;
608 
609     if (!dom.frame) {
610         // the surrounding main frame
611         dom.frame = document.createElement("DIV");
612         dom.frame.className = "timeline-frame";
613         dom.frame.style.position = "relative";
614         dom.frame.style.overflow = "hidden";
615         dom.container.appendChild(dom.frame);
616     }
617 
618     if (options.autoHeight) {
619         dom.frame.style.height = size.frameHeight + "px";
620     }
621     else {
622         dom.frame.style.height = options.height || "100%";
623     }
624     dom.frame.style.width = options.width  || "100%";
625 
626     this.redrawContent();
627     this.redrawGroups();
628     this.redrawCurrentTime();
629     this.redrawCustomTime();
630     this.redrawNavigation();
631 };
632 
633 
634 /**
635  * Redraw the content of the timeline: the axis and the items
636  */
637 links.Timeline.prototype.redrawContent = function() {
638     var dom = this.dom,
639         size = this.size;
640 
641     if (!dom.content) {
642         // create content box where the axis and canvas will 
643         dom.content = document.createElement("DIV");
644         //this.frame.className = "timeline-frame";
645         dom.content.style.position = "relative";
646         dom.content.style.overflow = "hidden";
647         dom.frame.appendChild(dom.content);
648 
649         var timelines = document.createElement("DIV");
650         timelines.style.position = "absolute";
651         timelines.style.left = "0px";
652         timelines.style.top = "0px";
653         timelines.style.height = "100%";
654         timelines.style.width = "0px";
655         dom.content.appendChild(timelines);
656         dom.contentTimelines = timelines;
657 
658         var params = this.eventParams,
659             me = this;
660         if (!params.onMouseDown) {
661             params.onMouseDown = function (event) {me.onMouseDown(event);};
662             links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
663         }
664         if (!params.onTouchStart) {
665             params.onTouchStart = function (event) {me.onTouchStart(event);};
666             links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
667         }
668         if (!params.onMouseWheel) {
669             params.onMouseWheel = function (event) {me.onMouseWheel(event);};
670             links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
671         }
672         if (!params.onDblClick) {
673             params.onDblClick = function (event) {me.onDblClick(event);};
674             links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
675         }
676     }
677     dom.content.style.left = size.contentLeft + "px";
678     dom.content.style.top = "0px";
679     dom.content.style.width = size.contentWidth + "px";
680     dom.content.style.height = size.frameHeight + "px";
681 
682     this.redrawAxis();
683     this.redrawItems();
684     this.redrawDeleteButton();
685     this.redrawDragAreas();
686 };
687 
688 /**
689  * Redraw the timeline axis with minor and major labels
690  */
691 links.Timeline.prototype.redrawAxis = function() {
692     var dom = this.dom,
693         options = this.options,
694         size = this.size,
695         step = this.step;
696 
697     var axis = dom.axis;
698     if (!axis) {
699         axis = {};
700         dom.axis = axis;
701     }
702     if (size.axis.properties === undefined) {
703         size.axis.properties = {};
704     }
705     if (axis.minorTexts === undefined) {
706         axis.minorTexts = [];
707     }
708     if (axis.minorLines === undefined) {
709         axis.minorLines = [];
710     }
711     if (axis.majorTexts === undefined) {
712         axis.majorTexts = [];
713     }
714     if (axis.majorLines === undefined) {
715         axis.majorLines = [];
716     }
717 
718     if (!axis.frame) {
719         axis.frame = document.createElement("DIV");
720         axis.frame.style.position = "absolute";
721         axis.frame.style.left = "0px";
722         axis.frame.style.top = "0px";
723         dom.content.appendChild(axis.frame);
724     }
725 
726     // take axis offline
727     dom.content.removeChild(axis.frame);
728 
729     axis.frame.style.width = (size.contentWidth) + "px";
730     axis.frame.style.height = (size.axis.height) + "px";
731 
732     // the drawn axis is more wide than the actual visual part, such that
733     // the axis can be dragged without having to redraw it each time again.
734     var start = this.screenToTime(0);
735     var end = this.screenToTime(size.contentWidth);
736     var width = size.contentWidth;
737 
738     // calculate minimum step (in milliseconds) based on character size
739     this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6).valueOf() -
740         this.screenToTime(0).valueOf();
741 
742     step.setRange(start, end, this.minimumStep);
743 
744     this.redrawAxisCharacters();
745 
746     this.redrawAxisStartOverwriting();
747 
748     step.start();
749     var xFirstMajorLabel = undefined;
750     while (!step.end()) {
751         var cur = step.getCurrent(),
752             x = this.timeToScreen(cur),
753             isMajor = step.isMajor();
754 
755         this.redrawAxisMinorText(x, step.getLabelMinor());
756 
757         if (isMajor && options.showMajorLabels) {
758             if (x > 0) {
759                 if (xFirstMajorLabel === undefined) {
760                     xFirstMajorLabel = x;
761                 }
762                 this.redrawAxisMajorText(x, step.getLabelMajor());
763             }
764             this.redrawAxisMajorLine(x);
765         }
766         else {
767             this.redrawAxisMinorLine(x);
768         }
769 
770         step.next();
771     }
772 
773     // create a major label on the left when needed
774     if (options.showMajorLabels) {
775         var leftTime = this.screenToTime(0),
776             leftText = this.step.getLabelMajor(leftTime),
777             width = leftText.length * size.axis.characterMajorWidth + 10;// estimation
778 
779         if (xFirstMajorLabel === undefined || width < xFirstMajorLabel) {
780             this.redrawAxisMajorText(0, leftText, leftTime);
781         }
782     }
783 
784     this.redrawAxisHorizontal();
785 
786     // cleanup left over labels
787     this.redrawAxisEndOverwriting();
788 
789     // put axis online
790     dom.content.insertBefore(axis.frame, dom.content.firstChild);
791 };
792 
793 /**
794  * Create characters used to determine the size of text on the axis
795  */
796 links.Timeline.prototype.redrawAxisCharacters = function () {
797     // calculate the width and height of a single character
798     // this is used to calculate the step size, and also the positioning of the
799     // axis
800     var dom = this.dom,
801         axis = dom.axis;
802 
803     if (!axis.characterMinor) {
804         var text = document.createTextNode("0");
805         var characterMinor = document.createElement("DIV");
806         characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
807         characterMinor.appendChild(text);
808         characterMinor.style.position = "absolute";
809         characterMinor.style.visibility = "hidden";
810         characterMinor.style.paddingLeft = "0px";
811         characterMinor.style.paddingRight = "0px";
812         axis.frame.appendChild(characterMinor);
813 
814         axis.characterMinor = characterMinor;
815     }
816 
817     if (!axis.characterMajor) {
818         var text = document.createTextNode("0");
819         var characterMajor = document.createElement("DIV");
820         characterMajor.className = "timeline-axis-text timeline-axis-text-major";
821         characterMajor.appendChild(text);
822         characterMajor.style.position = "absolute";
823         characterMajor.style.visibility = "hidden";
824         characterMajor.style.paddingLeft = "0px";
825         characterMajor.style.paddingRight = "0px";
826         axis.frame.appendChild(characterMajor);
827 
828         axis.characterMajor = characterMajor;
829     }
830 };
831 
832 /**
833  * Initialize redraw of the axis. All existing labels and lines will be
834  * overwritten and reused.
835  */
836 links.Timeline.prototype.redrawAxisStartOverwriting = function () {
837     var properties = this.size.axis.properties;
838 
839     properties.minorTextNum = 0;
840     properties.minorLineNum = 0;
841     properties.majorTextNum = 0;
842     properties.majorLineNum = 0;
843 };
844 
845 /**
846  * End of overwriting HTML DOM elements of the axis.
847  * remaining elements will be removed
848  */
849 links.Timeline.prototype.redrawAxisEndOverwriting = function () {
850     var dom = this.dom,
851         props = this.size.axis.properties,
852         frame = this.dom.axis.frame;
853 
854     // remove leftovers
855     var minorTexts = dom.axis.minorTexts,
856         num = props.minorTextNum;
857     while (minorTexts.length > num) {
858         var minorText = minorTexts[num];
859         frame.removeChild(minorText);
860         minorTexts.splice(num, 1);
861     }
862 
863     var minorLines = dom.axis.minorLines,
864         num = props.minorLineNum;
865     while (minorLines.length > num) {
866         var minorLine = minorLines[num];
867         frame.removeChild(minorLine);
868         minorLines.splice(num, 1);
869     }
870 
871     var majorTexts = dom.axis.majorTexts,
872         num = props.majorTextNum;
873     while (majorTexts.length > num) {
874         var majorText = majorTexts[num];
875         frame.removeChild(majorText);
876         majorTexts.splice(num, 1);
877     }
878 
879     var majorLines = dom.axis.majorLines,
880         num = props.majorLineNum;
881     while (majorLines.length > num) {
882         var majorLine = majorLines[num];
883         frame.removeChild(majorLine);
884         majorLines.splice(num, 1);
885     }
886 };
887 
888 /**
889  * Redraw the horizontal line and background of the axis
890  */
891 links.Timeline.prototype.redrawAxisHorizontal = function() {
892     var axis = this.dom.axis,
893         size = this.size;
894 
895     if (!axis.backgroundLine) {
896         // create the axis line background (for a background color or so)
897         var backgroundLine = document.createElement("DIV");
898         backgroundLine.className = "timeline-axis";
899         backgroundLine.style.position = "absolute";
900         backgroundLine.style.left = "0px";
901         backgroundLine.style.width = "100%";
902         backgroundLine.style.border = "none";
903         axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);
904 
905         axis.backgroundLine = backgroundLine;
906     }
907     axis.backgroundLine.style.top = size.axis.top + "px";
908     axis.backgroundLine.style.height = size.axis.height + "px";
909 
910     if (axis.line) {
911         // put this line at the end of all childs
912         var line = axis.frame.removeChild(axis.line);
913         axis.frame.appendChild(line);
914     }
915     else {
916         // make the axis line
917         var line = document.createElement("DIV");
918         line.className = "timeline-axis";
919         line.style.position = "absolute";
920         line.style.left = "0px";
921         line.style.width = "100%";
922         line.style.height = "0px";
923         axis.frame.appendChild(line);
924 
925         axis.line = line;
926     }
927     axis.line.style.top = size.axis.line + "px";
928 };
929 
930 /**
931  * Create a minor label for the axis at position x
932  * @param {Number} x
933  * @param {String} text
934  */
935 links.Timeline.prototype.redrawAxisMinorText = function (x, text) {
936     var size = this.size,
937         dom = this.dom,
938         props = size.axis.properties,
939         frame = dom.axis.frame,
940         minorTexts = dom.axis.minorTexts,
941         index = props.minorTextNum,
942         label;
943 
944     if (index < minorTexts.length) {
945         label = minorTexts[index]
946     }
947     else {
948         // create new label
949         var content = document.createTextNode(""),
950             label = document.createElement("DIV");
951         label.appendChild(content);
952         label.className = "timeline-axis-text timeline-axis-text-minor";
953         label.style.position = "absolute";
954 
955         frame.appendChild(label);
956 
957         minorTexts.push(label);
958     }
959 
960     label.childNodes[0].nodeValue = text;
961     label.style.left = x + "px";
962     label.style.top  = size.axis.labelMinorTop + "px";
963     //label.title = title;  // TODO: this is a heavy operation
964 
965     props.minorTextNum++;
966 };
967 
968 /**
969  * Create a minor line for the axis at position x
970  * @param {Number} x
971  */
972 links.Timeline.prototype.redrawAxisMinorLine = function (x) {
973     var axis = this.size.axis,
974         dom = this.dom,
975         props = axis.properties,
976         frame = dom.axis.frame,
977         minorLines = dom.axis.minorLines,
978         index = props.minorLineNum,
979         line;
980 
981     if (index < minorLines.length) {
982         line = minorLines[index];
983     }
984     else {
985         // create vertical line
986         line = document.createElement("DIV");
987         line.className = "timeline-axis-grid timeline-axis-grid-minor";
988         line.style.position = "absolute";
989         line.style.width = "0px";
990 
991         frame.appendChild(line);
992         minorLines.push(line);
993     }
994 
995     line.style.top = axis.lineMinorTop + "px";
996     line.style.height = axis.lineMinorHeight + "px";
997     line.style.left = (x - axis.lineMinorWidth/2) + "px";
998 
999     props.minorLineNum++;
1000 };
1001 
1002 /**
1003  * Create a Major label for the axis at position x
1004  * @param {Number} x
1005  * @param {String} text
1006  */
1007 links.Timeline.prototype.redrawAxisMajorText = function (x, text) {
1008     var size = this.size,
1009         props = size.axis.properties,
1010         frame = this.dom.axis.frame,
1011         majorTexts = this.dom.axis.majorTexts,
1012         index = props.majorTextNum,
1013         label;
1014 
1015     if (index < majorTexts.length) {
1016         label = majorTexts[index];
1017     }
1018     else {
1019         // create label
1020         var content = document.createTextNode(text);
1021         label = document.createElement("DIV");
1022         label.className = "timeline-axis-text timeline-axis-text-major";
1023         label.appendChild(content);
1024         label.style.position = "absolute";
1025         label.style.top = "0px";
1026 
1027         frame.appendChild(label);
1028         majorTexts.push(label);
1029     }
1030 
1031     label.childNodes[0].nodeValue = text;
1032     label.style.top = size.axis.labelMajorTop + "px";
1033     label.style.left = x + "px";
1034     //label.title = title; // TODO: this is a heavy operation
1035 
1036     props.majorTextNum ++;
1037 };
1038 
1039 /**
1040  * Create a Major line for the axis at position x
1041  * @param {Number} x
1042  */
1043 links.Timeline.prototype.redrawAxisMajorLine = function (x) {
1044     var size = this.size,
1045         props = size.axis.properties,
1046         axis = this.size.axis,
1047         frame = this.dom.axis.frame,
1048         majorLines = this.dom.axis.majorLines,
1049         index = props.majorLineNum,
1050         line;
1051 
1052     if (index < majorLines.length) {
1053         var line = majorLines[index];
1054     }
1055     else {
1056         // create vertical line
1057         line = document.createElement("DIV");
1058         line.className = "timeline-axis-grid timeline-axis-grid-major";
1059         line.style.position = "absolute";
1060         line.style.top = "0px";
1061         line.style.width = "0px";
1062 
1063         frame.appendChild(line);
1064         majorLines.push(line);
1065     }
1066 
1067     line.style.left = (x - axis.lineMajorWidth/2) + "px";
1068     line.style.height = size.frameHeight + "px";
1069 
1070     props.majorLineNum ++;
1071 };
1072 
1073 /**
1074  * Redraw all items
1075  */
1076 links.Timeline.prototype.redrawItems = function() {
1077     var dom = this.dom,
1078         options = this.options,
1079         boxAlign = (options.box && options.box.align) ? options.box.align : undefined,
1080         size = this.size,
1081         contentWidth = size.contentWidth,
1082         items = this.items;
1083 
1084     if (!dom.items) {
1085         dom.items = {};
1086     }
1087 
1088     // draw the frame containing the items
1089     var frame = dom.items.frame;
1090     if (!frame) {
1091         frame = document.createElement("DIV");
1092         frame.style.position = "relative";
1093         dom.content.appendChild(frame);
1094         dom.items.frame = frame;
1095     }
1096 
1097     frame.style.left = "0px";
1098     //frame.style.width = "0px";
1099     frame.style.top = size.items.top + "px";
1100     frame.style.height = (size.frameHeight - size.axis.height) + "px";
1101 
1102     // initialize arrarys for storing the items
1103     var ranges = dom.items.ranges;
1104     if (!ranges) {
1105         ranges = [];
1106         dom.items.ranges = ranges;
1107     }
1108     var boxes = dom.items.boxes;
1109     if (!boxes) {
1110         boxes = [];
1111         dom.items.boxes = boxes;
1112     }
1113     var dots = dom.items.dots;
1114     if (!dots) {
1115         dots = [];
1116         dom.items.dots = dots;
1117     }
1118 
1119     // Take frame offline
1120     dom.content.removeChild(frame);
1121 
1122     if (size.dataChanged) {
1123         // create the items
1124         var rangesCreated = ranges.length,
1125             boxesCreated = boxes.length,
1126             dotsCreated = dots.length,
1127             rangesUsed = 0,
1128             boxesUsed = 0,
1129             dotsUsed = 0,
1130             itemsLength = items.length;
1131 
1132         for (var i = 0, iMax = items.length; i < iMax; i++) {
1133             var item = items[i];
1134             switch (item.type) {
1135                 case 'range':
1136                     if (rangesUsed < rangesCreated) {
1137                         // reuse existing range
1138                         var domItem = ranges[rangesUsed];
1139                         domItem.firstChild.innerHTML = item.content;
1140                         domItem.style.display = '';
1141                         item.dom = domItem;
1142                         rangesUsed++;
1143                     }
1144                     else {
1145                         // create a new range
1146                         var domItem = this.createEventRange(item.content);
1147                         ranges[rangesUsed] = domItem;
1148                         frame.appendChild(domItem);
1149                         item.dom = domItem;
1150                         rangesUsed++;
1151                         rangesCreated++;
1152                     }
1153                     break;
1154 
1155                 case 'box':
1156                     if (boxesUsed < boxesCreated) {
1157                         // reuse existing box
1158                         var domItem = boxes[boxesUsed];
1159                         domItem.firstChild.innerHTML = item.content;
1160                         domItem.style.display = '';
1161                         item.dom = domItem;
1162                         boxesUsed++;
1163                     }
1164                     else {
1165                         // create a new box
1166                         var domItem = this.createEventBox(item.content);
1167                         boxes[boxesUsed] = domItem;
1168                         frame.appendChild(domItem);
1169                         frame.insertBefore(domItem.line, frame.firstChild);
1170                         // Note: line must be added in front of the items, 
1171                         //       such that it stays below all items
1172                         frame.appendChild(domItem.dot);
1173                         item.dom = domItem;
1174                         boxesUsed++;
1175                         boxesCreated++;
1176                     }
1177                     break;
1178 
1179                 case 'dot':
1180                     if (dotsUsed < dotsCreated) {
1181                         // reuse existing box
1182                         var domItem = dots[dotsUsed];
1183                         domItem.firstChild.innerHTML = item.content;
1184                         domItem.style.display = '';
1185                         item.dom = domItem;
1186                         dotsUsed++;
1187                     }
1188                     else {
1189                         // create a new box
1190                         var domItem = this.createEventDot(item.content);
1191                         dots[dotsUsed] = domItem;
1192                         frame.appendChild(domItem);
1193                         item.dom = domItem;
1194                         dotsUsed++;
1195                         dotsCreated++;
1196                     }
1197                     break;
1198 
1199                 default:
1200                     // do nothing
1201                     break;
1202             }
1203         }
1204 
1205         // remove redundant items when needed
1206         for (var i = rangesUsed; i < rangesCreated; i++) {
1207             frame.removeChild(ranges[i]);
1208         }
1209         ranges.splice(rangesUsed, rangesCreated - rangesUsed);
1210         for (var i = boxesUsed; i < boxesCreated; i++) {
1211             var box = boxes[i];
1212             frame.removeChild(box.line);
1213             frame.removeChild(box.dot);
1214             frame.removeChild(box);
1215         }
1216         boxes.splice(boxesUsed, boxesCreated - boxesUsed);
1217         for (var i = dotsUsed; i < dotsCreated; i++) {
1218             frame.removeChild(dots[i]);
1219         }
1220         dots.splice(dotsUsed, dotsCreated - dotsUsed);
1221     }
1222 
1223     // reposition all items
1224     for (var i = 0, iMax = items.length; i < iMax; i++) {
1225         var item = items[i],
1226             domItem = item.dom;
1227 
1228         switch (item.type) {
1229             case 'range':
1230                 var left = this.timeToScreen(item.start),
1231                     right = this.timeToScreen(item.end);
1232 
1233                 // limit the width of the item, as browsers cannot draw very wide divs
1234                 if (left < -contentWidth) {
1235                     left = -contentWidth;
1236                 }
1237                 if (right > 2 * contentWidth) {
1238                     right = 2 * contentWidth;
1239                 }
1240 
1241                 var visible = right > -contentWidth && left < 2 * contentWidth;
1242                 if (visible || size.dataChanged) {
1243                     // when data is changed, all items must be kept visible, as their heights must be measured
1244                     if (item.hidden) {
1245                         item.hidden = false;
1246                         domItem.style.display = '';
1247                     }
1248                     domItem.style.top = item.top + "px";
1249                     domItem.style.left = left + "px";
1250                     //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO: borderWidth
1251                     domItem.style.width = Math.max(right - left, 1) + "px";
1252                 }
1253                 else {
1254                     // hide when outside of the current window
1255                     if (!item.hidden) {
1256                         domItem.style.display = 'none';
1257                         item.hidden = true;
1258                     }
1259                 }
1260 
1261                 break;
1262 
1263             case 'box':
1264                 var left = this.timeToScreen(item.start);
1265 
1266                 var axisOnTop = options.axisOnTop,
1267                     axisHeight = size.axis.height,
1268                     axisTop = size.axis.top;
1269                 var visible = ((left + item.width/2 > -contentWidth) &&
1270                     (left - item.width/2 < 2 * contentWidth));
1271                 if (visible || size.dataChanged) {
1272                     // when data is changed, all items must be kept visible, as their heights must be measured
1273                     if (item.hidden) {
1274                         item.hidden = false;
1275                         domItem.style.display = '';
1276                         domItem.line.style.display = '';
1277                         domItem.dot.style.display = '';
1278                     }
1279                     domItem.style.top = item.top + "px";
1280                     if (boxAlign == 'right') {
1281                         domItem.style.left = (left - item.width) + "px";
1282                     }
1283                     else if (boxAlign == 'left') {
1284                         domItem.style.left = (left) + "px";
1285                     }
1286                     else { // default or 'center'
1287                         domItem.style.left = (left - item.width/2) + "px";
1288                     }
1289 
1290                     var line = domItem.line;
1291                     line.style.left = (left - item.lineWidth/2) + "px";
1292                     if (axisOnTop) {
1293                         line.style.top = "0px";
1294                         line.style.height = Math.max(item.top, 0) + "px";
1295                     }
1296                     else {
1297                         line.style.top = (item.top + item.height) + "px";
1298                         line.style.height = Math.max(axisTop - item.top - item.height, 0) + "px";
1299                     }
1300 
1301                     var dot = domItem.dot;
1302                     dot.style.left = (left - item.dotWidth/2) + "px";
1303                     dot.style.top = (axisTop - item.dotHeight/2) + "px";
1304                 }
1305                 else {
1306                     // hide when outside of the current window
1307                     if (!item.hidden) {
1308                         domItem.style.display = 'none';
1309                         domItem.line.style.display = 'none';
1310                         domItem.dot.style.display = 'none';
1311                         item.hidden = true;
1312                     }
1313                 }
1314                 break;
1315 
1316             case 'dot':
1317                 var left = this.timeToScreen(item.start);
1318 
1319                 var axisOnTop = options.axisOnTop,
1320                     axisHeight = size.axis.height,
1321                     axisTop = size.axis.top;
1322                 var visible = (left + item.width > -contentWidth) && (left < 2 * contentWidth);
1323                 if (visible || size.dataChanged) {
1324                     // when data is changed, all items must be kept visible, as their heights must be measured
1325                     if (item.hidden) {
1326                         item.hidden = false;
1327                         domItem.style.display = '';
1328                     }
1329                     domItem.style.top = item.top + "px";
1330                     domItem.style.left = (left - item.dotWidth / 2) + "px";
1331 
1332                     domItem.content.style.marginLeft = (1.5 * item.dotWidth) + "px";
1333                     //domItem.content.style.marginRight = (0.5 * item.dotWidth) + "px"; // TODO
1334                     domItem.dot.style.top = ((item.height - item.dotHeight) / 2) + "px";
1335                 }
1336                 else {
1337                     // hide when outside of the current window
1338                     if (!item.hidden) {
1339                         domItem.style.display = 'none';
1340                         item.hidden = true;
1341                     }
1342                 }
1343                 break;
1344 
1345             default:
1346                 // do nothing
1347                 break;
1348         }
1349     }
1350 
1351     // move selected item to the end, to ensure that it is always on top
1352     if (this.selection) {
1353         var item = this.selection.item;
1354         frame.removeChild(item);
1355         frame.appendChild(item);
1356     }
1357 
1358     // put frame online again
1359     dom.content.appendChild(frame);
1360 
1361     /* TODO
1362      // retrieve all image sources from the items, and set a callback once 
1363      // all images are retrieved
1364      var urls = [];
1365      var timeline = this;
1366      links.Timeline.filterImageUrls(frame, urls);
1367      if (urls.length) {
1368      for (var i = 0; i < urls.length; i++) {
1369      var url = urls[i];
1370      var callback = function (url) {
1371      timeline.redraw();
1372      };
1373      var sendCallbackWhenAlreadyLoaded = false;
1374      links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
1375      }
1376      }    
1377      */
1378 };
1379 
1380 
1381 /**
1382  * Create an event in the timeline, with (optional) formatting: inside a box
1383  * with rounded corners, and a vertical line+dot to the axis.
1384  * @param {string} content    The content for the event. This can be plain text
1385  *                            or HTML code.
1386  */
1387 links.Timeline.prototype.createEventBox = function(content) {
1388     // background box
1389     var divBox = document.createElement("DIV");
1390     divBox.style.position = "absolute";
1391     divBox.style.left  = "0px";
1392     divBox.style.top = "0px";
1393     divBox.className  = "timeline-event timeline-event-box";
1394 
1395     // contents box (inside the background box). used for making margins
1396     var divContent = document.createElement("DIV");
1397     divContent.className = "timeline-event-content";
1398     divContent.innerHTML = content;
1399     divBox.appendChild(divContent);
1400 
1401     // line to axis
1402     var divLine = document.createElement("DIV");
1403     divLine.style.position = "absolute";
1404     divLine.style.width = "0px";
1405     divLine.className = "timeline-event timeline-event-line";
1406     // important: the vertical line is added at the front of the list of elements,
1407     // so it will be drawn behind all boxes and ranges
1408     divBox.line = divLine;
1409 
1410     // dot on axis
1411     var divDot = document.createElement("DIV");
1412     divDot.style.position = "absolute";
1413     divDot.style.width  = "0px";
1414     divDot.style.height = "0px";
1415     divDot.className  = "timeline-event timeline-event-dot";
1416     divBox.dot = divDot;
1417 
1418     return divBox;
1419 };
1420 
1421 
1422 /**
1423  * Create an event in the timeline: a dot, followed by the content.
1424  * @param {string} content    The content for the event. This can be plain text
1425  *                            or HTML code.
1426  */
1427 links.Timeline.prototype.createEventDot = function(content) {
1428     // background box
1429     var divBox = document.createElement("DIV");
1430     divBox.style.position = "absolute";
1431 
1432     // contents box, right from the dot
1433     var divContent = document.createElement("DIV");
1434     divContent.className = "timeline-event-content";
1435     divContent.innerHTML = content;
1436     divBox.appendChild(divContent);
1437 
1438     // dot at start
1439     var divDot = document.createElement("DIV");
1440     divDot.style.position = "absolute";
1441     divDot.className = "timeline-event timeline-event-dot";
1442     divDot.style.width = "0px";
1443     divDot.style.height = "0px";
1444     divBox.appendChild(divDot);
1445 
1446     divBox.content = divContent;
1447     divBox.dot = divDot;
1448 
1449     return divBox;
1450 };
1451 
1452 
1453 /**
1454  * Create an event range as a beam in the timeline.
1455  * @param {string}  content    The content for the event. This can be plain text
1456  *                             or HTML code.
1457  */
1458 links.Timeline.prototype.createEventRange = function(content) {
1459     // background box
1460     var divBox = document.createElement("DIV");
1461     divBox.style.position = "absolute";
1462     divBox.className = "timeline-event timeline-event-range";
1463 
1464     // contents box
1465     var divContent = document.createElement("DIV");
1466     divContent.className = "timeline-event-content";
1467     divContent.innerHTML = content;
1468     divBox.appendChild(divContent);
1469 
1470     return divBox;
1471 };
1472 
1473 /**
1474  * Redraw the group labels
1475  */
1476 links.Timeline.prototype.redrawGroups = function() {
1477     var dom = this.dom,
1478         options = this.options,
1479         size = this.size,
1480         groups = this.groups;
1481 
1482     if (dom.groups === undefined) {
1483         dom.groups = {};
1484     }
1485 
1486     var labels = dom.groups.labels;
1487     if (!labels) {
1488         labels = [];
1489         dom.groups.labels = labels;
1490     }
1491     var labelLines = dom.groups.labelLines;
1492     if (!labelLines) {
1493         labelLines = [];
1494         dom.groups.labelLines = labelLines;
1495     }
1496     var itemLines = dom.groups.itemLines;
1497     if (!itemLines) {
1498         itemLines = [];
1499         dom.groups.itemLines = itemLines;
1500     }
1501 
1502     // create the frame for holding the groups
1503     var frame = dom.groups.frame;
1504     if (!frame) {
1505         frame =  document.createElement("DIV");
1506         frame.className = "timeline-groups-axis";
1507         frame.style.position = "absolute";
1508         frame.style.overflow = "hidden";
1509         frame.style.top = "0px";
1510         frame.style.height = "100%";
1511 
1512         dom.frame.appendChild(frame);
1513         dom.groups.frame = frame;
1514     }
1515 
1516     frame.style.left = size.groupsLeft + "px";
1517     frame.style.width = (options.groupsWidth !== undefined) ?
1518         options.groupsWidth :
1519         size.groupsWidth + "px";
1520 
1521     // hide groups axis when there are no groups
1522     if (groups.length == 0) {
1523         frame.style.display = 'none';
1524     }
1525     else {
1526         frame.style.display = '';
1527     }
1528 
1529     if (size.dataChanged) {
1530         // create the items
1531         var current = labels.length,
1532             needed = groups.length;
1533 
1534         // overwrite existing items
1535         for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
1536             var group = groups[i];
1537             var label = labels[i];
1538             label.innerHTML = group.content;
1539             label.style.display = '';
1540         }
1541 
1542         // append new items when needed
1543         for (var i = current; i < needed; i++) {
1544             var group = groups[i];
1545 
1546             // create text label
1547             var label = document.createElement("DIV");
1548             label.className = "timeline-groups-text";
1549             label.style.position = "absolute";
1550             if (options.groupsWidth === undefined) {
1551                 label.style.whiteSpace = "nowrap";
1552             }
1553             label.innerHTML = group.content;
1554             frame.appendChild(label);
1555             labels[i] = label;
1556 
1557             // create the grid line between the group labels
1558             var labelLine = document.createElement("DIV");
1559             labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1560             labelLine.style.position = "absolute";
1561             labelLine.style.left = "0px";
1562             labelLine.style.width = "100%";
1563             labelLine.style.height = "0px";
1564             labelLine.style.borderTopStyle = "solid";
1565             frame.appendChild(labelLine);
1566             labelLines[i] = labelLine;
1567 
1568             // create the grid line between the items
1569             var itemLine = document.createElement("DIV");
1570             itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1571             itemLine.style.position = "absolute";
1572             itemLine.style.left = "0px";
1573             itemLine.style.width = "100%";
1574             itemLine.style.height = "0px";
1575             itemLine.style.borderTopStyle = "solid";
1576             dom.content.insertBefore(itemLine, dom.content.firstChild);
1577             itemLines[i] = itemLine;
1578         }
1579 
1580         // remove redundant items from the DOM when needed
1581         for (var i = needed; i < current; i++) {
1582             var label = labels[i],
1583                 labelLine = labelLines[i],
1584                 itemLine = itemLines[i];
1585 
1586             frame.removeChild(label);
1587             frame.removeChild(labelLine);
1588             dom.content.removeChild(itemLine);
1589         }
1590         labels.splice(needed, current - needed);
1591         labelLines.splice(needed, current - needed);
1592         itemLines.splice(needed, current - needed);
1593 
1594         frame.style.borderStyle = options.groupsOnRight ?
1595             "none none none solid" :
1596             "none solid none none";
1597     }
1598 
1599     // position the groups
1600     for (var i = 0, iMax = groups.length; i < iMax; i++) {
1601         var group = groups[i],
1602             label = labels[i],
1603             labelLine = labelLines[i],
1604             itemLine = itemLines[i];
1605 
1606         label.style.top = group.labelTop + "px";
1607         labelLine.style.top = group.lineTop + "px";
1608         itemLine.style.top = group.lineTop + "px";
1609         itemLine.style.width = size.contentWidth + "px";
1610     }
1611 
1612     if (!dom.groups.background) {
1613         // create the axis grid line background
1614         var background = document.createElement("DIV");
1615         background.className = "timeline-axis";
1616         background.style.position = "absolute";
1617         background.style.left = "0px";
1618         background.style.width = "100%";
1619         background.style.border = "none";
1620 
1621         frame.appendChild(background);
1622         dom.groups.background = background;
1623     }
1624     dom.groups.background.style.top = size.axis.top + 'px';
1625     dom.groups.background.style.height = size.axis.height + 'px';
1626 
1627     if (!dom.groups.line) {
1628         // create the axis grid line
1629         var line = document.createElement("DIV");
1630         line.className = "timeline-axis";
1631         line.style.position = "absolute";
1632         line.style.left = "0px";
1633         line.style.width = "100%";
1634         line.style.height = "0px";
1635 
1636         frame.appendChild(line);
1637         dom.groups.line = line;
1638     }
1639     dom.groups.line.style.top = size.axis.line + 'px';
1640 };
1641 
1642 
1643 /**
1644  * Redraw the current time bar
1645  */
1646 links.Timeline.prototype.redrawCurrentTime = function() {
1647     var options = this.options,
1648         dom = this.dom,
1649         size = this.size;
1650 
1651     if (!options.showCurrentTime) {
1652         if (dom.currentTime) {
1653             dom.contentTimelines.removeChild(dom.currentTime);
1654             delete dom.currentTime;
1655         }
1656 
1657         return;
1658     }
1659 
1660     if (!dom.currentTime) {
1661         // create the current time bar
1662         var currentTime = document.createElement("DIV");
1663         currentTime.className = "timeline-currenttime";
1664         currentTime.style.position = "absolute";
1665         currentTime.style.top = "0px";
1666         currentTime.style.height = "100%";
1667 
1668         dom.contentTimelines.appendChild(currentTime);
1669         dom.currentTime = currentTime;
1670     }
1671 
1672     var now = new Date();
1673     var nowOffset = new Date(now.getTime() + this.clientTimeOffset);
1674     var x = this.timeToScreen(nowOffset);
1675 
1676     var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
1677     dom.currentTime.style.display = visible ? '' : 'none';
1678     dom.currentTime.style.left = x + "px";
1679     dom.currentTime.title = "Current time: " + nowOffset;
1680 
1681     // start a timer to adjust for the new time
1682     if (this.currentTimeTimer != undefined) {
1683         clearTimeout(this.currentTimeTimer);
1684         delete this.currentTimeTimer;
1685     }
1686     var timeline = this;
1687     var onTimeout = function() {
1688         timeline.redrawCurrentTime();
1689     };
1690     // the time equal to the width of one pixel, divided by 2 for more smoothness
1691     var interval = 1 / this.conversion.factor / 2;
1692     if (interval < 30) interval = 30;
1693     this.currentTimeTimer = setTimeout(onTimeout, interval);
1694 };
1695 
1696 /**
1697  * Redraw the custom time bar
1698  */
1699 links.Timeline.prototype.redrawCustomTime = function() {
1700     var options = this.options,
1701         dom = this.dom,
1702         size = this.size;
1703 
1704     if (!options.showCustomTime) {
1705         if (dom.customTime) {
1706             dom.contentTimelines.removeChild(dom.customTime);
1707             delete dom.customTime;
1708         }
1709 
1710         return;
1711     }
1712 
1713     if (!dom.customTime) {
1714         var customTime = document.createElement("DIV");
1715         customTime.className = "timeline-customtime";
1716         customTime.style.position = "absolute";
1717         customTime.style.top = "0px";
1718         customTime.style.height = "100%";
1719 
1720         var drag = document.createElement("DIV");
1721         drag.style.position = "relative";
1722         drag.style.top = "0px";
1723         drag.style.left = "-10px";
1724         drag.style.height = "100%";
1725         drag.style.width = "20px";
1726         customTime.appendChild(drag);
1727 
1728         dom.contentTimelines.appendChild(customTime);
1729         dom.customTime = customTime;
1730 
1731         // initialize parameter
1732         this.customTime = new Date();
1733     }
1734 
1735     var x = this.timeToScreen(this.customTime),
1736         visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
1737     dom.customTime.style.display = visible ? '' : 'none';
1738     dom.customTime.style.left = x + "px";
1739     dom.customTime.title = "Time: " + this.customTime;
1740 };
1741 
1742 
1743 /**
1744  * Redraw the delete button, on the top right of the currently selected item
1745  * if there is no item selected, the button is hidden.
1746  */
1747 links.Timeline.prototype.redrawDeleteButton = function () {
1748     var timeline = this,
1749         options = this.options,
1750         dom = this.dom,
1751         size = this.size,
1752         frame = dom.items.frame;
1753 
1754     if (!options.editable) {
1755         return;
1756     }
1757 
1758     var deleteButton = dom.items.deleteButton;
1759     if (!deleteButton) {
1760         // create a delete button
1761         deleteButton = document.createElement("DIV");
1762         deleteButton.className = "timeline-navigation-delete";
1763         deleteButton.style.position = "absolute";
1764 
1765         frame.appendChild(deleteButton);
1766         dom.items.deleteButton = deleteButton;
1767     }
1768 
1769     if (this.selection) {
1770         var index = this.selection.index,
1771             item = this.items[index],
1772             domItem = this.selection.item,
1773             right,
1774             top = item.top;
1775 
1776         switch (item.type) {
1777             case 'range':
1778                 right = this.timeToScreen(item.end);
1779                 break;
1780 
1781             case 'box':
1782                 //right = this.timeToScreen(item.start) + item.width / 2 + item.borderWidth; // TODO: borderWidth
1783                 right = this.timeToScreen(item.start) + item.width / 2;
1784                 break;
1785 
1786             case 'dot':
1787                 right = this.timeToScreen(item.start) + item.width;
1788                 break;
1789         }
1790 
1791         // limit the position
1792         if (right < -size.contentWidth) {
1793             right = -size.contentWidth;
1794         }
1795         if (right > 2 * size.contentWidth) {
1796             right = 2 * size.contentWidth;
1797         }
1798 
1799         deleteButton.style.left = right + 'px';
1800         deleteButton.style.top = top + 'px';
1801         deleteButton.style.display = '';
1802         frame.removeChild(deleteButton);
1803         frame.appendChild(deleteButton);
1804     }
1805     else {
1806         deleteButton.style.display = 'none';
1807     }
1808 };
1809 
1810 
1811 /**
1812  * Redraw the drag areas. When an item (ranges only) is selected,
1813  * it gets a drag area on the left and right side, to change its width
1814  */
1815 links.Timeline.prototype.redrawDragAreas = function () {
1816     var timeline = this,
1817         options = this.options,
1818         dom = this.dom,
1819         size = this.size,
1820         frame = this.dom.items.frame;
1821 
1822     if (!options.editable) {
1823         return;
1824     }
1825 
1826     // create left drag area
1827     var dragLeft = dom.items.dragLeft;
1828     if (!dragLeft) {
1829         dragLeft = document.createElement("DIV");
1830         dragLeft.className="timeline-event-range-drag-left";
1831         dragLeft.style.width = options.dragAreaWidth + "px";
1832         dragLeft.style.position = "absolute";
1833 
1834         frame.appendChild(dragLeft);
1835         dom.items.dragLeft = dragLeft;
1836     }
1837 
1838     // create right drag area
1839     var dragRight = dom.items.dragRight;
1840     if (!dragRight) {
1841         dragRight = document.createElement("DIV");
1842         dragRight.className="timeline-event-range-drag-right";
1843         dragRight.style.width = options.dragAreaWidth + "px";
1844         dragRight.style.position = "absolute";
1845 
1846         frame.appendChild(dragRight);
1847         dom.items.dragRight = dragRight;
1848     }
1849 
1850     // reposition left and right drag area
1851     if (this.selection) {
1852         var index = this.selection.index,
1853             item = this.items[index];
1854 
1855         if (item.type == 'range') {
1856             var domItem = item.dom,
1857                 left = this.timeToScreen(item.start),
1858                 right = this.timeToScreen(item.end),
1859                 top = item.top,
1860                 height = item.height;
1861 
1862             dragLeft.style.left = left + 'px';
1863             dragLeft.style.top = top + 'px';
1864             dragLeft.style.height = height + 'px';
1865             dragLeft.style.display = '';
1866             frame.removeChild(dragLeft);
1867             frame.appendChild(dragLeft);
1868 
1869             dragRight.style.left = (right - options.dragAreaWidth) + 'px';
1870             dragRight.style.top = top + 'px';
1871             dragRight.style.height = height + 'px';
1872             dragRight.style.display = '';
1873             frame.removeChild(dragRight);
1874             frame.appendChild(dragRight);
1875         }
1876     }
1877     else {
1878         dragLeft.style.display = 'none';
1879         dragRight.style.display = 'none';
1880     }
1881 };
1882 
1883 
1884 
1885 /**
1886  * Create the navigation buttons for zooming and moving
1887  */
1888 links.Timeline.prototype.redrawNavigation = function () {
1889     var timeline = this,
1890         options = this.options,
1891         dom = this.dom,
1892         frame = dom.frame,
1893         navBar = dom.navBar;
1894 
1895     if (!navBar) {
1896         if (options.editable || options.showNavigation) {
1897             // create a navigation bar containing the navigation buttons
1898             navBar = document.createElement("DIV");
1899             navBar.style.position = "absolute";
1900             navBar.className = "timeline-navigation";
1901             if (options.groupsOnRight) {
1902                 navBar.style.left = '10px';
1903             }
1904             else {
1905                 navBar.style.right = '10px';
1906             }
1907             if (options.axisOnTop) {
1908                 navBar.style.bottom = '10px';
1909             }
1910             else {
1911                 navBar.style.top = '10px';
1912             }
1913             dom.navBar = navBar;
1914             frame.appendChild(navBar);
1915         }
1916 
1917         if (options.editable && options.showButtonAdd) {
1918             // create a new in button
1919             navBar.addButton = document.createElement("DIV");
1920             navBar.addButton.className = "timeline-navigation-new";
1921 
1922             navBar.addButton.title = "Create new event";
1923             var onAdd = function(event) {
1924                 links.Timeline.preventDefault(event);
1925                 links.Timeline.stopPropagation(event);
1926 
1927                 // create a new event at the center of the frame
1928                 var w = timeline.size.contentWidth;
1929                 var x = w / 2;
1930                 var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width
1931                 var xend = timeline.screenToTime(x + w / 10); // add 10% of timeline width
1932                 if (options.snapEvents) {
1933                     timeline.step.snap(xstart);
1934                     timeline.step.snap(xend);
1935                 }
1936 
1937                 var content = "New";
1938                 var group = timeline.groups.length ? timeline.groups[0].content : undefined;
1939 
1940                 timeline.addItem({
1941                     'start': xstart,
1942                     'end': xend,
1943                     'content': content,
1944                     'group': group
1945                 });
1946                 var index = (timeline.items.length - 1);
1947                 timeline.selectItem(index);
1948 
1949                 timeline.applyAdd = true;
1950 
1951                 // fire an add event. 
1952                 // Note that the change can be canceled from within an event listener if 
1953                 // this listener calls the method cancelAdd().
1954                 timeline.trigger('add');
1955 
1956                 if (!timeline.applyAdd) {
1957                     // undo an add
1958                     timeline.deleteItem(index);
1959                 }
1960                 timeline.redrawDeleteButton();
1961                 timeline.redrawDragAreas();
1962             };
1963             links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
1964             navBar.appendChild(navBar.addButton);
1965         }
1966 
1967         if (options.editable && options.showButtonAdd && options.showNavigation) {
1968             // create a separator line
1969             navBar.addButton.style.borderRightWidth = "1px";
1970             navBar.addButton.style.borderRightStyle = "solid";
1971         }
1972 
1973         if (options.showNavigation) {
1974             // create a zoom in button
1975             navBar.zoomInButton = document.createElement("DIV");
1976             navBar.zoomInButton.className = "timeline-navigation-zoom-in";
1977             navBar.zoomInButton.title = "Zoom in";
1978             var onZoomIn = function(event) {
1979                 links.Timeline.preventDefault(event);
1980                 links.Timeline.stopPropagation(event);
1981                 timeline.zoom(0.4);
1982                 timeline.trigger("rangechange");
1983                 timeline.trigger("rangechanged");
1984             };
1985             links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
1986             navBar.appendChild(navBar.zoomInButton);
1987 
1988             // create a zoom out button
1989             navBar.zoomOutButton = document.createElement("DIV");
1990             navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
1991             navBar.zoomOutButton.title = "Zoom out";
1992             var onZoomOut = function(event) {
1993                 links.Timeline.preventDefault(event);
1994                 links.Timeline.stopPropagation(event);
1995                 timeline.zoom(-0.4);
1996                 timeline.trigger("rangechange");
1997                 timeline.trigger("rangechanged");
1998             };
1999             links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
2000             navBar.appendChild(navBar.zoomOutButton);
2001 
2002             // create a move left button
2003             navBar.moveLeftButton = document.createElement("DIV");
2004             navBar.moveLeftButton.className = "timeline-navigation-move-left";
2005             navBar.moveLeftButton.title = "Move left";
2006             var onMoveLeft = function(event) {
2007                 links.Timeline.preventDefault(event);
2008                 links.Timeline.stopPropagation(event);
2009                 timeline.move(-0.2);
2010                 timeline.trigger("rangechange");
2011                 timeline.trigger("rangechanged");
2012             };
2013             links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
2014             navBar.appendChild(navBar.moveLeftButton);
2015 
2016             // create a move right button
2017             navBar.moveRightButton = document.createElement("DIV");
2018             navBar.moveRightButton.className = "timeline-navigation-move-right";
2019             navBar.moveRightButton.title = "Move right";
2020             var onMoveRight = function(event) {
2021                 links.Timeline.preventDefault(event);
2022                 links.Timeline.stopPropagation(event);
2023                 timeline.move(0.2);
2024                 timeline.trigger("rangechange");
2025                 timeline.trigger("rangechanged");
2026             };
2027             links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
2028             navBar.appendChild(navBar.moveRightButton);
2029         }
2030     }
2031 };
2032 
2033 
2034 /**
2035  * Set current time. This function can be used to set the time in the client
2036  * timeline equal with the time on a server.
2037  * @param {Date} time
2038  */
2039 links.Timeline.prototype.setCurrentTime = function(time) {
2040     var now = new Date();
2041     this.clientTimeOffset = time.getTime() - now.getTime();
2042 
2043     this.redrawCurrentTime();
2044 };
2045 
2046 /**
2047  * Get current time. The time can have an offset from the real time, when
2048  * the current time has been changed via the method setCurrentTime.
2049  * @return {Date} time
2050  */
2051 links.Timeline.prototype.getCurrentTime = function() {
2052     var now = new Date();
2053     return new Date(now.getTime() + this.clientTimeOffset);
2054 };
2055 
2056 
2057 /**
2058  * Set custom time.
2059  * The custom time bar can be used to display events in past or future.
2060  * @param {Date} time
2061  */
2062 links.Timeline.prototype.setCustomTime = function(time) {
2063     this.customTime = new Date(time);
2064     this.redrawCustomTime();
2065 };
2066 
2067 /**
2068  * Retrieve the current custom time.
2069  * @return {Date} customTime
2070  */
2071 links.Timeline.prototype.getCustomTime = function() {
2072     return new Date(this.customTime);
2073 };
2074 
2075 /**
2076  * Set a custom scale. Autoscaling will be disabled.
2077  * For example setScale(SCALE.MINUTES, 5) will result
2078  * in minor steps of 5 minutes, and major steps of an hour.
2079  *
2080  * @param {links.Timeline.StepDate.SCALE} scale
2081  *                               A scale. Choose from SCALE.MILLISECOND,
2082  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
2083  *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
2084  * @param {int}        step   A step size, by default 1. Choose for
2085  *                               example 1, 2, 5, or 10.
2086  */
2087 links.Timeline.prototype.setScale = function(scale, step) {
2088     this.step.setScale(scale, step);
2089     this.redrawFrame();
2090 };
2091 
2092 /**
2093  * Enable or disable autoscaling
2094  * @param {boolean} enable  If true or not defined, autoscaling is enabled.
2095  *                          If false, autoscaling is disabled.
2096  */
2097 links.Timeline.prototype.setAutoScale = function(enable) {
2098     this.step.setAutoScale(enable);
2099     this.redrawFrame();
2100 };
2101 
2102 /**
2103  * Redraw the timeline
2104  * Reloads the (linked) data table and redraws the timeline when resized.
2105  * See also the method checkResize
2106  */
2107 links.Timeline.prototype.redraw = function() {
2108     this.setData(this.data);
2109 };
2110 
2111 
2112 /**
2113  * Check if the timeline is resized, and if so, redraw the timeline.
2114  * Useful when the webpage is resized.
2115  */
2116 links.Timeline.prototype.checkResize = function() {
2117     var resized = this.recalcSize();
2118     if (resized) {
2119         this.redrawFrame();
2120     }
2121 };
2122 
2123 /**
2124  * Recursively retrieve all image urls from the images located inside a given
2125  * HTML element
2126  * @param {HTMLElement} elem
2127  * @param {String[]} urls   Urls will be added here (no duplicates)
2128  */
2129 links.Timeline.filterImageUrls = function(elem, urls) {
2130     var child = elem.firstChild;
2131     while (child) {
2132         if (child.tagName == 'IMG') {
2133             var url = child.src;
2134             if (urls.indexOf(url) == -1) {
2135                 urls.push(url);
2136             }
2137         }
2138 
2139         links.Timeline.filterImageUrls(child, urls);
2140 
2141         child = child.nextSibling;
2142     }
2143 };
2144 
2145 /**
2146  * Recalculate the sizes of all frames, groups, items, axis
2147  * After recalcSize() is executed, the Timeline should be redrawn normally
2148  *
2149  * @return {boolean} resized   Returns true when the timeline has been resized
2150  */
2151 links.Timeline.prototype.recalcSize = function() {
2152     var resized = false;
2153 
2154     var timeline = this,
2155         size = this.size,
2156         options = this.options,
2157         axisOnTop = options.axisOnTop,
2158         dom = this.dom,
2159         axis = dom.axis,
2160         groups = this.groups,
2161         labels = dom.groups.labels,
2162         items = this.items;
2163 
2164     var groupsWidth = size.groupsWidth,
2165         characterMinorWidth  = axis.characterMinor ? axis.characterMinor.clientWidth : 0,
2166         characterMinorHeight = axis.characterMinor ? axis.characterMinor.clientHeight : 0,
2167         characterMajorWidth  = axis.characterMajor ? axis.characterMajor.clientWidth : 0,
2168         characterMajorHeight = axis.characterMajor ? axis.characterMajor.clientHeight : 0,
2169         axisHeight = characterMinorHeight + (options.showMajorLabels ? characterMajorHeight : 0),
2170         actualHeight = size.actualHeight || axisHeight;
2171 
2172     // TODO: move checking for loaded items when creating the dom
2173     if (size.dataChanged) {
2174         // retrieve all image sources from the items, and set a callback once 
2175         // all images are retrieved
2176         var urls = [];
2177         for (var i = 0, iMax = items.length; i < iMax; i++) {
2178             var item = items[i],
2179                 domItem = item.dom;
2180 
2181             if (domItem) {
2182                 links.Timeline.filterImageUrls(domItem, urls);
2183             }
2184         }
2185         if (urls.length) {
2186             for (var i = 0; i < urls.length; i++) {
2187                 var url = urls[i];
2188                 var callback = function (url) {
2189                     timeline.redraw();
2190                 };
2191                 var sendCallbackWhenAlreadyLoaded = false;
2192                 links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
2193             }
2194         }
2195     }
2196 
2197     // check sizes of the items and groups (width and height) when the data is changed
2198     if (size.dataChanged) { // TODO: always calculate the size of an item?
2199         //if (true) {
2200         groupsWidth = 0;
2201 
2202         // loop through all groups to get the maximum width and the heights
2203         for (var i = 0, iMax = labels.length; i < iMax; i++) {
2204             var group = groups[i];
2205             group.width = labels[i].clientWidth;
2206             group.height = labels[i].clientHeight;
2207             group.labelHeight = group.height;
2208 
2209             groupsWidth = Math.max(groupsWidth, group.width);
2210         }
2211 
2212         // loop through the width and height of all items
2213         for (var i = 0, iMax = items.length; i < iMax; i++) {
2214             var item = items[i],
2215                 domItem = item.dom,
2216                 group = item.group;
2217 
2218             var width = domItem ? domItem.clientWidth : 0;
2219             var height = domItem ? domItem.clientHeight : 0;
2220             resized = resized || (item.width != width);
2221             resized = resized || (item.height != height);
2222             item.width = width;
2223             item.height = height;
2224             //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
2225 
2226             switch (item.type) {
2227                 case 'range':
2228                     break;
2229 
2230                 case 'box':
2231                     item.dotHeight = domItem.dot.offsetHeight;
2232                     item.dotWidth = domItem.dot.offsetWidth;
2233                     item.lineWidth = domItem.line.offsetWidth;
2234                     break;
2235 
2236                 case 'dot':
2237                     item.dotHeight = domItem.dot.offsetHeight;
2238                     item.dotWidth = domItem.dot.offsetWidth;
2239                     item.contentHeight = domItem.content.offsetHeight;
2240                     break;
2241             }
2242 
2243             if (group) {
2244                 group.height = group.height ? Math.max(group.height, item.height) : item.height;
2245             }
2246         }
2247 
2248         // calculate the actual height of the timeline (needed for auto sizing
2249         // the timeline)
2250         actualHeight = axisHeight + 2 * options.eventMarginAxis;
2251         for (var i = 0, iMax = groups.length; i < iMax; i++) {
2252             actualHeight += groups[i].height + options.eventMargin;
2253         }
2254     }
2255 
2256     // calculate actual height of the timeline when there are no groups
2257     // but stacked items
2258     if (groups.length == 0 && options.autoHeight) {
2259         var min = 0,
2260             max = 0;
2261 
2262         if (this.animation && this.animation.finalItems) {
2263             // adjust the offset of all finalItems when the actualHeight has been changed
2264             var finalItems = this.animation.finalItems,
2265                 finalItem = finalItems[0];
2266             if (finalItem && finalItem.top) {
2267                 min = finalItem.top,
2268                     max = finalItem.top + finalItem.height;
2269             }
2270             for (var i = 1, iMax = finalItems.length; i < iMax; i++) {
2271                 finalItem = finalItems[i];
2272                 min = Math.min(min, finalItem.top);
2273                 max = Math.max(max, finalItem.top + finalItem.height);
2274             }
2275         }
2276         else {
2277             var item = items[0];
2278             if (item && item.top) {
2279                 min = item.top,
2280                     max = item.top + item.height;
2281             }
2282             for (var i = 1, iMax = items.length; i < iMax; i++) {
2283                 var item = items[i];
2284                 if (item.top) {
2285                     min = Math.min(min, item.top);
2286                     max = Math.max(max, (item.top + item.height));
2287                 }
2288             }
2289         }
2290 
2291         actualHeight = (max - min) + 2 * options.eventMarginAxis + axisHeight;
2292 
2293         if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
2294             // adjust the offset of all items when the actualHeight has been changed
2295             var diff = actualHeight - size.actualHeight;
2296             if (this.animation && this.animation.finalItems) {
2297                 var finalItems = this.animation.finalItems;
2298                 for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
2299                     finalItems[i].top += diff;
2300                     finalItems[i].item.top += diff;
2301                 }
2302             }
2303             else {
2304                 for (var i = 0, iMax = items.length; i < iMax; i++) {
2305                     items[i].top += diff;
2306                 }
2307             }
2308         }
2309     }
2310 
2311     // now the heights of the elements are known, we can calculate the the 
2312     // width and height of frame and axis and content 
2313     // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
2314     var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
2315         frameHeight = Math.max(options.autoHeight ?
2316             actualHeight : (dom.frame ? dom.frame.clientHeight : 0),
2317             options.minHeight),
2318         axisTop  = axisOnTop ? 0 : frameHeight - axisHeight,
2319         axisLine = axisOnTop ? axisHeight : axisTop,
2320         itemsTop = axisOnTop ? axisHeight : 0,
2321         contentHeight = Math.max(frameHeight - axisHeight, 0);
2322 
2323     if (options.groupsWidth !== undefined) {
2324         groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
2325     }
2326     var groupsLeft = options.groupsOnRight ? frameWidth - groupsWidth : 0;
2327 
2328     if (size.dataChanged) {
2329         // calculate top positions of the group labels and lines
2330         var eventMargin = options.eventMargin,
2331             top = axisOnTop ?
2332                 options.eventMarginAxis + eventMargin/2 :
2333                 contentHeight - options.eventMarginAxis + eventMargin/2;
2334 
2335         for (var i = 0, iMax = groups.length; i < iMax; i++) {
2336             var group = groups[i];
2337             if (axisOnTop) {
2338                 group.top = top;
2339                 group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
2340                 group.lineTop = top + axisHeight + group.height + eventMargin/2;
2341                 top += group.height + eventMargin;
2342             }
2343             else {
2344                 top -= group.height + eventMargin;
2345                 group.top = top;
2346                 group.labelTop = top + (group.height - group.labelHeight) / 2;
2347                 group.lineTop = top - eventMargin/2;
2348             }
2349         }
2350 
2351         // calculate top position of the items
2352         for (var i = 0, iMax = items.length; i < iMax; i++) {
2353             var item = items[i],
2354                 group = item.group;
2355 
2356             if (group) {
2357                 item.top = group.top;
2358             }
2359         }
2360 
2361         resized = true;
2362     }
2363 
2364     resized = resized || (size.groupsWidth !== groupsWidth);
2365     resized = resized || (size.groupsLeft !== groupsLeft);
2366     resized = resized || (size.actualHeight !== actualHeight);
2367     size.groupsWidth = groupsWidth;
2368     size.groupsLeft = groupsLeft;
2369     size.actualHeight = actualHeight;
2370 
2371     resized = resized || (size.frameWidth !== frameWidth);
2372     resized = resized || (size.frameHeight !== frameHeight);
2373     size.frameWidth = frameWidth;
2374     size.frameHeight = frameHeight;
2375 
2376     resized = resized || (size.groupsWidth !== groupsWidth);
2377     size.groupsWidth = groupsWidth;
2378     size.contentLeft = options.groupsOnRight ? 0 : groupsWidth;
2379     size.contentWidth = Math.max(frameWidth - groupsWidth, 0);
2380     size.contentHeight = contentHeight;
2381 
2382     resized = resized || (size.axis.top !== axisTop);
2383     resized = resized || (size.axis.line !== axisLine);
2384     resized = resized || (size.axis.height !== axisHeight);
2385     resized = resized || (size.items.top !== itemsTop);
2386     size.axis.top = axisTop;
2387     size.axis.line = axisLine;
2388     size.axis.height = axisHeight;
2389     size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine + characterMinorHeight;
2390     size.axis.labelMinorTop = options.axisOnTop ?
2391         (options.showMajorLabels ? characterMajorHeight : 0) :
2392         axisLine;
2393     size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
2394     size.axis.lineMinorHeight = options.showMajorLabels ?
2395         frameHeight - characterMajorHeight:
2396         frameHeight;
2397     size.axis.lineMinorWidth = dom.axis.minorLines.length ?
2398         dom.axis.minorLines[0].offsetWidth : 1;
2399     size.axis.lineMajorWidth = dom.axis.majorLines.length ?
2400         dom.axis.majorLines[0].offsetWidth : 1;
2401 
2402     size.items.top = itemsTop;
2403 
2404     resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
2405     resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
2406     resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
2407     resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
2408     size.axis.characterMinorWidth  = characterMinorWidth;
2409     size.axis.characterMinorHeight = characterMinorHeight;
2410     size.axis.characterMajorWidth  = characterMajorWidth;
2411     size.axis.characterMajorHeight = characterMajorHeight;
2412 
2413     // conversion factors can be changed when width of the Timeline is changed,
2414     // and when start or end are changed
2415     this.recalcConversion();
2416 
2417     return resized;
2418 };
2419 
2420 
2421 
2422 /**
2423  * Calculate the factor and offset to convert a position on screen to the
2424  * corresponding date and vice versa.
2425  * After the method calcConversionFactor is executed once, the methods screenToTime and
2426  * timeToScreen can be used.
2427  */
2428 links.Timeline.prototype.recalcConversion = function() {
2429     this.conversion.offset = parseFloat(this.start.valueOf());
2430     this.conversion.factor = parseFloat(this.size.contentWidth) /
2431         parseFloat(this.end.valueOf() - this.start.valueOf());
2432 };
2433 
2434 
2435 /**
2436  * Convert a position on screen (pixels) to a datetime
2437  * Before this method can be used, the method calcConversionFactor must be
2438  * executed once.
2439  * @param {int}     x    Position on the screen in pixels
2440  * @return {Date}   time The datetime the corresponds with given position x
2441  */
2442 links.Timeline.prototype.screenToTime = function(x) {
2443     var conversion = this.conversion,
2444         time = new Date(parseFloat(x) / conversion.factor + conversion.offset);
2445     return time;
2446 };
2447 
2448 /**
2449  * Convert a datetime (Date object) into a position on the screen
2450  * Before this method can be used, the method calcConversionFactor must be
2451  * executed once.
2452  * @param {Date}   time A date
2453  * @return {int}   x    The position on the screen in pixels which corresponds
2454  *                      with the given date.
2455  */
2456 links.Timeline.prototype.timeToScreen = function(time) {
2457     var conversion = this.conversion;
2458     var x = (time.valueOf() - conversion.offset) * conversion.factor;
2459     return x;
2460 };
2461 
2462 
2463 
2464 /**
2465  * Event handler for touchstart event on mobile devices
2466  */
2467 links.Timeline.prototype.onTouchStart = function(event) {
2468     var params = this.eventParams,
2469         dom = this.dom,
2470         me = this;
2471 
2472     if (params.touchDown) {
2473         // if already moving, return
2474         return;
2475     }
2476 
2477     params.touchDown = true;
2478     params.zoomed = false;
2479 
2480     this.onMouseDown(event);
2481 
2482     if (!params.onTouchMove) {
2483         params.onTouchMove = function (event) {me.onTouchMove(event);};
2484         links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
2485     }
2486     if (!params.onTouchEnd) {
2487         params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
2488         links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
2489     }
2490 };
2491 
2492 /**
2493  * Event handler for touchmove event on mobile devices
2494  */
2495 links.Timeline.prototype.onTouchMove = function(event) {
2496     var params = this.eventParams;
2497 
2498     if (event.scale && event.scale !== 1) {
2499         params.zoomed = true;
2500     }
2501 
2502     if (!params.zoomed) {
2503         // move 
2504         this.onMouseMove(event);
2505     }
2506     else {
2507         if (this.options.zoomable) {
2508             // pinch
2509             // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
2510             params.zoomed = true;
2511 
2512             var scale = event.scale,
2513                 oldWidth = (params.end.valueOf() - params.start.valueOf()),
2514                 newWidth = oldWidth / scale,
2515                 diff = newWidth - oldWidth,
2516                 start = new Date(parseInt(params.start.valueOf() - diff/2)),
2517                 end = new Date(parseInt(params.end.valueOf() + diff/2));
2518 
2519             // TODO: determine zoom-around-date from touch positions?
2520 
2521             this.setVisibleChartRange(start, end);
2522             timeline.trigger("rangechange");
2523 
2524             links.Timeline.preventDefault(event);
2525         }
2526     }
2527 };
2528 
2529 /**
2530  * Event handler for touchend event on mobile devices
2531  */
2532 links.Timeline.prototype.onTouchEnd = function(event) {
2533     var params = this.eventParams;
2534     params.touchDown = false;
2535 
2536     if (params.zoomed) {
2537         timeline.trigger("rangechanged");
2538     }
2539 
2540     if (params.onTouchMove) {
2541         links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
2542         delete params.onTouchMove;
2543 
2544     }
2545     if (params.onTouchEnd) {
2546         links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
2547         delete params.onTouchEnd;
2548     }
2549 
2550     this.onMouseUp(event);
2551 };
2552 
2553 
2554 /**
2555  * Start a moving operation inside the provided parent element
2556  * @param {event} event       The event that occurred (required for
2557  *                             retrieving the  mouse position)
2558  */
2559 links.Timeline.prototype.onMouseDown = function(event) {
2560     event = event || window.event;
2561 
2562     var params = this.eventParams,
2563         options = this.options,
2564         dom = this.dom;
2565 
2566     // only react on left mouse button down
2567     var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
2568     if (!leftButtonDown && !params.touchDown) {
2569         return;
2570     }
2571 
2572     // check if frame is not resized (causing a mismatch with the end Date) 
2573     this.recalcSize();
2574 
2575     // get mouse position
2576     if (!params.touchDown) {
2577         params.mouseX = event.clientX;
2578         params.mouseY = event.clientY;
2579     }
2580     else {
2581         params.mouseX = event.targetTouches[0].clientX;
2582         params.mouseY = event.targetTouches[0].clientY;
2583     }
2584     if (params.mouseX === undefined) {params.mouseX = 0;}
2585     if (params.mouseY === undefined) {params.mouseY = 0;}
2586     params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
2587     params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
2588     params.previousLeft = 0;
2589     params.previousOffset = 0;
2590 
2591     params.moved = false;
2592     params.start = new Date(this.start);
2593     params.end = new Date(this.end);
2594 
2595     params.target = links.Timeline.getTarget(event);
2596     params.itemDragLeft = (params.target === this.dom.items.dragLeft);
2597     params.itemDragRight = (params.target === this.dom.items.dragRight);
2598 
2599     if (params.itemDragLeft || params.itemDragRight) {
2600         params.itemIndex = this.selection ? this.selection.index : undefined;
2601     }
2602     else {
2603         params.itemIndex = this.getItemIndex(params.target);
2604     }
2605 
2606     params.customTime = (params.target === dom.customTime ||
2607         params.target.parentNode === dom.customTime) ?
2608         this.customTime :
2609         undefined;
2610 
2611     params.addItem = (options.editable && event.ctrlKey);
2612     if (params.addItem) {
2613         // create a new event at the current mouse position
2614         var x = params.mouseX - params.frameLeft;
2615         var y = params.mouseY - params.frameTop;
2616 
2617         var xstart = this.screenToTime(x);
2618         if (options.snapEvents) {
2619             this.step.snap(xstart);
2620         }
2621         var xend = new Date(xstart);
2622         var content = "New";
2623         var group = this.getGroupFromHeight(y);
2624         this.addItem({
2625             'start': xstart,
2626             'end': xend,
2627             'content': content,
2628             'group': group.content
2629         });
2630         params.itemIndex = (this.items.length - 1);
2631         this.selectItem(params.itemIndex);
2632         params.itemDragRight = true;
2633     }
2634 
2635     params.editItem = options.editable ? this.isSelected(params.itemIndex) : undefined;
2636     if (params.editItem) {
2637         var item = this.items[params.itemIndex];
2638         params.itemStart = item.start;
2639         params.itemEnd = item.end;
2640         params.itemType = item.type;
2641         if (params.itemType == 'range') {
2642             params.itemLeft = this.timeToScreen(item.start);
2643             params.itemRight = this.timeToScreen(item.end);
2644         }
2645         else {
2646             params.itemLeft = this.timeToScreen(item.start);
2647         }
2648     }
2649     else {
2650         this.dom.frame.style.cursor = 'move';
2651     }
2652     if (!params.touchDown) {
2653         // add event listeners to handle moving the contents
2654         // we store the function onmousemove and onmouseup in the timeline, so we can
2655         // remove the eventlisteners lateron in the function mouseUp()
2656         var me = this;
2657         if (!params.onMouseMove) {
2658             params.onMouseMove = function (event) {me.onMouseMove(event);};
2659             links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
2660         }
2661         if (!params.onMouseUp) {
2662             params.onMouseUp = function (event) {me.onMouseUp(event);};
2663             links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
2664         }
2665 
2666         links.Timeline.preventDefault(event);
2667     }
2668 };
2669 
2670 
2671 /**
2672  * Perform moving operating.
2673  * This function activated from within the funcion links.Timeline.onMouseDown().
2674  * @param {event}   event  Well, eehh, the event
2675  */
2676 links.Timeline.prototype.onMouseMove = function (event) {
2677     event = event || window.event;
2678 
2679     var params = this.eventParams,
2680         size = this.size,
2681         dom = this.dom,
2682         options = this.options;
2683 
2684     // calculate change in mouse position
2685     var mouseX, mouseY;
2686     if (!params.touchDown) {
2687         mouseX = event.clientX;
2688         mouseY = event.clientY;
2689     }
2690     else {
2691         mouseX = event.targetTouches[0].clientX;
2692         mouseY = event.targetTouches[0].clientY;
2693     }
2694     if (mouseX === undefined) {mouseX = 0;}
2695     if (mouseY === undefined) {mouseY = 0;}
2696 
2697     if (params.mouseX === undefined) {
2698         params.mouseX = mouseX;
2699     }
2700     if (params.mouseY === undefined) {
2701         params.mouseY = mouseY;
2702     }
2703 
2704     var diffX = parseFloat(mouseX) - params.mouseX;
2705     var diffY = parseFloat(mouseY) - params.mouseY;
2706 
2707     params.moved = true;
2708 
2709     if (params.customTime) {
2710         var x = this.timeToScreen(params.customTime);
2711         var xnew = x + diffX;
2712         this.customTime = this.screenToTime(xnew);
2713         this.redrawCustomTime();
2714 
2715         // fire a timechange event
2716         this.trigger('timechange');
2717     }
2718     else if (params.editItem) {
2719         var item = this.items[params.itemIndex],
2720             domItem = item.dom,
2721             left,
2722             right;
2723 
2724         if (params.itemDragLeft) {
2725             // move the start of the item
2726             left = params.itemLeft + diffX;
2727             right = params.itemRight;
2728 
2729             item.start = this.screenToTime(left);
2730             if (options.snapEvents) {
2731                 this.step.snap(item.start);
2732                 left = this.timeToScreen(item.start);
2733             }
2734 
2735             if (left > right) {
2736                 left = right;
2737                 item.start = this.screenToTime(left);
2738             }
2739         }
2740         else if (params.itemDragRight) {
2741             // move the end of the item
2742             left = params.itemLeft;
2743             right = params.itemRight + diffX;
2744 
2745             item.end = this.screenToTime(right);
2746             if (options.snapEvents) {
2747                 this.step.snap(item.end);
2748                 right = this.timeToScreen(item.end);
2749             }
2750 
2751             if (right < left) {
2752                 right = left;
2753                 item.end = this.screenToTime(right);
2754             }
2755         }
2756         else {
2757             // move the item
2758             left = params.itemLeft + diffX;
2759             item.start = this.screenToTime(left);
2760             if (options.snapEvents) {
2761                 this.step.snap(item.start);
2762                 left = this.timeToScreen(item.start);
2763             }
2764 
2765             if (item.end) {
2766                 right = left + (params.itemRight - params.itemLeft);
2767                 item.end = this.screenToTime(right);
2768             }
2769         }
2770 
2771         this.repositionItem(item, left, right);
2772 
2773         if (this.groups.length == 0) {
2774             // TODO: does not work well in FF, forces redraw with every mouse move it seems
2775             this.stackEvents(options.animate);
2776             if (!options.animate) {
2777                 this.redrawFrame();
2778             }
2779             // Note: when animate==true, no redraw is needed here, its done by stackEvents animation
2780         }
2781         else {
2782             // move item from one group to another when needed
2783             if (options.groupsChangeable) {
2784                 var y = mouseY - params.frameTop;
2785                 var group = this.getGroupFromHeight(y);
2786                 if (item.group !== group) {
2787                     // move item to the other group
2788 
2789                     //item.group = group;
2790                     var index = this.items.indexOf(item);
2791                     this.changeItem(index, {'group': group.content});
2792 
2793                     item.top = group.top;
2794                     this.repositionItem(item);
2795                 }
2796             }
2797         }
2798 
2799         this.redrawDeleteButton();
2800         this.redrawDragAreas();
2801     }
2802     else if (options.moveable) {
2803         var interval = (params.end.valueOf() - params.start.valueOf());
2804         var diffMillisecs = Math.round(parseFloat(-diffX) / size.contentWidth * interval);
2805         var newStart = new Date(params.start.valueOf() + diffMillisecs);
2806         var newEnd = new Date(params.end.valueOf() + diffMillisecs);
2807         this.applyRange(newStart, newEnd);
2808 
2809         // if the applied range is moved due to a fixed min or max, 
2810         // change the diffMillisecs accordingly
2811         var appliedDiff = (this.start.valueOf() - newStart.valueOf());
2812         if (appliedDiff) {
2813             diffMillisecs += appliedDiff;
2814         }
2815 
2816         this.recalcConversion();
2817 
2818         // move the items by changing the left position of their frame.
2819         // this is much faster than repositioning all elements individually via the 
2820         // redrawFrame() function (which is done once at mouseup)
2821         // note that we round diffX to prevent wrong positioning on millisecond scale
2822         var previousLeft = params.previousLeft || 0;
2823         var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
2824         var previousOffset = params.previousOffset || 0;
2825         var frameOffset = previousOffset + (currentLeft - previousLeft);
2826         var frameLeft = -diffMillisecs / interval * size.contentWidth + frameOffset;
2827         params.previousOffset = frameOffset;
2828         params.previousLeft = frameLeft;
2829 
2830         dom.items.frame.style.left = (frameLeft) + "px";
2831 
2832         this.redrawCurrentTime();
2833         this.redrawCustomTime();
2834         this.redrawAxis();
2835 
2836         // fire a rangechange event
2837         this.trigger('rangechange');
2838     }
2839 
2840     links.Timeline.preventDefault(event);
2841 };
2842 
2843 
2844 /**
2845  * Stop moving operating.
2846  * This function activated from within the funcion links.Timeline.onMouseDown().
2847  * @param {event}  event   The event
2848  */
2849 links.Timeline.prototype.onMouseUp = function (event) {
2850     var params = this.eventParams,
2851         options = this.options;
2852 
2853     event = event || window.event;
2854 
2855     this.dom.frame.style.cursor = 'auto';
2856 
2857     // remove event listeners here, important for Safari
2858     if (params.onMouseMove) {
2859         links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
2860         delete params.onMouseMove;
2861     }
2862     if (params.onMouseUp) {
2863         links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
2864         delete params.onMouseUp;
2865     }
2866     //links.Timeline.preventDefault(event);
2867 
2868     if (params.customTime) {
2869         // fire a timechanged event
2870         this.trigger('timechanged');
2871     }
2872     else if (params.editItem) {
2873         var item = this.items[params.itemIndex];
2874 
2875         if (params.moved || params.addItem) {
2876             this.applyChange = true;
2877             this.applyAdd = true;
2878 
2879             this.updateData(params.itemIndex, {
2880                 'start': item.start,
2881                 'end': item.end
2882             });
2883 
2884             // fire an add or change event. 
2885             // Note that the change can be canceled from within an event listener if 
2886             // this listener calls the method cancelChange().
2887             this.trigger(params.addItem ? 'add' : 'change');
2888 
2889             if (params.addItem) {
2890                 if (this.applyAdd) {
2891                     this.updateData(params.itemIndex, {
2892                         'start': item.start,
2893                         'end': item.end,
2894                         'content': item.content,
2895                         'group': item.group ? item.group.content : undefined
2896                     });
2897                 }
2898                 else {
2899                     // undo an add
2900                     this.deleteItem(params.itemIndex);
2901                 }
2902             }
2903             else {
2904                 if (this.applyChange) {
2905                     this.updateData(params.itemIndex, {
2906                         'start': item.start,
2907                         'end': item.end
2908                     });
2909                 }
2910                 else {
2911                     // undo a change
2912                     delete this.applyChange;
2913                     delete this.applyAdd;
2914 
2915                     var item = this.items[params.itemIndex],
2916                         domItem = item.dom;
2917 
2918                     item.start = params.itemStart;
2919                     item.end = params.itemEnd;
2920                     this.repositionItem(item, params.itemLeft, params.itemRight);
2921                 }
2922             }
2923 
2924             this.recalcSize();
2925             this.stackEvents(options.animate);
2926             if (!options.animate) {
2927                 this.redrawFrame();
2928             }
2929             this.redrawDeleteButton();
2930             this.redrawDragAreas();
2931         }
2932     }
2933     else {
2934         if (!params.moved && !params.zoomed) {
2935             // mouse did not move -> user has selected an item
2936 
2937             if (options.editable && (params.target === this.dom.items.deleteButton)) {
2938                 // delete item
2939                 if (this.selection) {
2940                     this.confirmDeleteItem(this.selection.index);
2941                 }
2942                 this.redrawFrame();
2943             }
2944             else if (options.selectable) {
2945                 // select/unselect item
2946                 if (params.itemIndex !== undefined) {
2947                     if (!this.isSelected(params.itemIndex)) {
2948                         this.selectItem(params.itemIndex);
2949                         this.redrawDeleteButton();
2950                         this.redrawDragAreas();
2951                         this.trigger('select');
2952                     }
2953                 }
2954                 else {
2955                     this.unselectItem();
2956                     this.redrawDeleteButton();
2957                     this.redrawDragAreas();
2958                 }
2959             }
2960         }
2961         else {
2962             // timeline is moved 
2963             this.redrawFrame();
2964 
2965             if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
2966                 // fire a rangechanged event
2967                 this.trigger('rangechanged');
2968             }
2969         }
2970     }
2971 };
2972 
2973 /**
2974  * Double click event occurred for an item
2975  * @param {event}  event
2976  */
2977 links.Timeline.prototype.onDblClick = function (event) {
2978     var params = this.eventParams,
2979         options = this.options,
2980         dom = this.dom,
2981         size = this.size;
2982     event = event || window.event;
2983 
2984     if (!options.editable) {
2985         return;
2986     }
2987 
2988     if (params.itemIndex !== undefined) {
2989         // fire the edit event
2990         this.trigger('edit');
2991     }
2992     else {
2993         // create a new item    
2994         var x = event.clientX - links.Timeline.getAbsoluteLeft(dom.content);
2995         var y = event.clientY - links.Timeline.getAbsoluteTop(dom.content);
2996 
2997         // create a new event at the current mouse position
2998         var xstart = this.screenToTime(x);
2999         var xend = this.screenToTime(x  + size.frameWidth / 10); // add 10% of timeline width
3000         if (options.snapEvents) {
3001             this.step.snap(xstart);
3002             this.step.snap(xend);
3003         }
3004 
3005         var content = "New";
3006         var group = this.getGroupFromHeight(y);   // (group may be undefined)
3007         this.addItem({
3008             'start': xstart,
3009             'end': xend,
3010             'content': content,
3011             'group': group.content
3012         });
3013         params.itemIndex = (this.items.length - 1);
3014         this.selectItem(params.itemIndex);
3015 
3016         this.applyAdd = true;
3017 
3018         // fire an add event. 
3019         // Note that the change can be canceled from within an event listener if 
3020         // this listener calls the method cancelAdd().
3021         this.trigger('add');
3022 
3023         if (!this.applyAdd) {
3024             // undo an add
3025             this.deleteItem(params.itemIndex);
3026         }
3027 
3028         this.redrawDeleteButton();
3029         this.redrawDragAreas();
3030     }
3031 
3032     links.Timeline.preventDefault(event);
3033 };
3034 
3035 
3036 /**
3037  * Event handler for mouse wheel event, used to zoom the timeline
3038  * Code from http://adomas.org/javascript-mouse-wheel/
3039  * @param {event}  event   The event
3040  */
3041 links.Timeline.prototype.onMouseWheel = function(event) {
3042     if (!this.options.zoomable)
3043         return;
3044 
3045     if (!event) { /* For IE. */
3046         event = window.event;
3047     }
3048 
3049     // retrieve delta    
3050     var delta = 0;
3051     if (event.wheelDelta) { /* IE/Opera. */
3052         delta = event.wheelDelta/120;
3053     } else if (event.detail) { /* Mozilla case. */
3054         // In Mozilla, sign of delta is different than in IE.
3055         // Also, delta is multiple of 3.
3056         delta = -event.detail/3;
3057     }
3058 
3059     // If delta is nonzero, handle it.
3060     // Basically, delta is now positive if wheel was scrolled up,
3061     // and negative, if wheel was scrolled down.
3062     if (delta) {
3063         // TODO: on FireFox, the window is not redrawn within repeated scroll-events 
3064         // -> use a delayed redraw? Make a zoom queue?
3065 
3066         var timeline = this;
3067         var zoom = function () {
3068             // check if frame is not resized (causing a mismatch with the end date) 
3069             timeline.recalcSize();
3070 
3071             // perform the zoom action. Delta is normally 1 or -1
3072             var zoomFactor = delta / 5.0;
3073             var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
3074             var zoomAroundDate =
3075                 (event.clientX != undefined && frameLeft != undefined) ?
3076                     timeline.screenToTime(event.clientX - frameLeft) :
3077                     undefined;
3078 
3079             timeline.zoom(zoomFactor, zoomAroundDate);
3080 
3081             // fire a rangechange and a rangechanged event
3082             timeline.trigger("rangechange");
3083             timeline.trigger("rangechanged");
3084 
3085             /* TODO: smooth scrolling on FF
3086              timeline.zooming = false;
3087 
3088              if (timeline.zoomingQueue) {
3089              setTimeout(timeline.zoomingQueue, 100);
3090              timeline.zoomingQueue = undefined;
3091              }
3092 
3093              timeline.zoomCount = (timeline.zoomCount || 0) + 1;
3094              console.log('zoomCount', timeline.zoomCount)
3095              */
3096         };
3097 
3098         zoom();
3099 
3100         /* TODO: smooth scrolling on FF
3101          if (!timeline.zooming || true) {
3102 
3103          timeline.zooming = true;
3104          setTimeout(zoom, 100);
3105          }
3106          else {
3107          timeline.zoomingQueue = zoom;
3108          }
3109          //*/
3110     }
3111 
3112     // Prevent default actions caused by mouse wheel.
3113     // That might be ugly, but we handle scrolls somehow
3114     // anyway, so don't bother here...
3115     links.Timeline.preventDefault(event);
3116 };
3117 
3118 
3119 /**
3120  * Zoom the timeline the given zoomfactor in or out. Start and end date will
3121  * be adjusted, and the timeline will be redrawn. You can optionally give a
3122  * date around which to zoom.
3123  * For example, try zoomfactor = 0.1 or -0.1
3124  * @param {Number} zoomFactor      Zooming amount. Positive value will zoom in,
3125  *                                 negative value will zoom out
3126  * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
3127  */
3128 links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
3129     // if zoomAroundDate is not provided, take it half between start Date and end Date
3130     if (zoomAroundDate == undefined) {
3131         zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
3132     }
3133 
3134     // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
3135     // result in a start>=end )
3136     if (zoomFactor >= 1) {
3137         zoomFactor = 0.9;
3138     }
3139     if (zoomFactor <= -1) {
3140         zoomFactor = -0.9;
3141     }
3142 
3143     // adjust a negative factor such that zooming in with 0.1 equals zooming
3144     // out with a factor -0.1
3145     if (zoomFactor < 0) {
3146         zoomFactor = zoomFactor / (1 + zoomFactor);
3147     }
3148 
3149     // zoom start Date and end Date relative to the zoomAroundDate
3150     var startDiff = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf());
3151     var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf());
3152 
3153     // calculate new dates
3154     var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
3155     var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);
3156 
3157     this.applyRange(newStart, newEnd, zoomAroundDate);
3158 
3159     this.recalcSize();
3160     var animate = this.options.animate ? this.options.animateZoom : false;
3161     this.stackEvents(animate);
3162     if (!animate || this.groups.length > 0) {
3163         this.redrawFrame();
3164     }
3165     /* TODO
3166      else {
3167      this.redrawFrame();
3168      this.recalcSize();
3169      this.stackEvents(animate);
3170      this.redrawFrame();
3171      }*/
3172 };
3173 
3174 
3175 /**
3176  * Move the timeline the given movefactor to the left or right. Start and end
3177  * date will be adjusted, and the timeline will be redrawn.
3178  * For example, try moveFactor = 0.1 or -0.1
3179  * @param {Number}  moveFactor      Moving amount. Positive value will move right,
3180  *                                 negative value will move left
3181  */
3182 links.Timeline.prototype.move = function(moveFactor) {
3183     // zoom start Date and end Date relative to the zoomAroundDate
3184     var diff = parseFloat(this.end.valueOf() - this.start.valueOf());
3185 
3186     // apply new dates
3187     var newStart = new Date(this.start.valueOf() + diff * moveFactor);
3188     var newEnd   = new Date(this.end.valueOf() + diff * moveFactor);
3189     this.applyRange(newStart, newEnd);
3190 
3191     this.recalcConversion();
3192     this.redrawFrame();
3193 };
3194 
3195 /**
3196  * Reposition given item
3197  * @param {Object} item
3198  * @param {Number} left
3199  * @param {Number} right
3200  */
3201 links.Timeline.prototype.repositionItem = function (item, left, right) {
3202     var domItem = item.dom;
3203     switch(item.type) {
3204         case 'range':
3205             domItem.style.left = left + "px";
3206             //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px";  // TODO: borderwidth
3207             domItem.style.width = Math.max(right - left, 1) + "px";
3208             break;
3209 
3210         case 'box':
3211             domItem.style.left = (left - item.width / 2) + "px";
3212             domItem.line.style.left = (left - item.lineWidth / 2) + "px";
3213             domItem.dot.style.left = (left - item.dotWidth / 2) + "px";
3214             break;
3215 
3216         case 'dot':
3217             domItem.style.left = (left - item.dotWidth / 2) + "px";
3218             break;
3219     }
3220 
3221     if (this.groups.length > 0) {
3222         domItem.style.top = item.top + 'px';
3223     }
3224 };
3225 
3226 /**
3227  * Apply a visible range. The range is limited to feasible maximum and minimum
3228  * range.
3229  * @param {Date} start
3230  * @param {Date} end
3231  * @param {Date}   zoomAroundDate  Optional. Date around which will be zoomed.
3232  */
3233 links.Timeline.prototype.applyRange = function (start, end, zoomAroundDate) {
3234     // calculate new start and end value
3235     var startValue = start.valueOf();
3236     var endValue = end.valueOf();
3237     var interval = (endValue - startValue);
3238 
3239     // determine maximum and minimum interval
3240     var options = this.options;
3241     var year = 1000 * 60 * 60 * 24 * 365;
3242     var intervalMin = Number(options.intervalMin) || 10;
3243     if (intervalMin < 10) {
3244         intervalMin = 10;
3245     }
3246     var intervalMax = Number(options.intervalMax) || 10000 * year;
3247     if (intervalMax > 10000 * year) {
3248         intervalMax = 10000 * year;
3249     }
3250     if (intervalMax < intervalMin) {
3251         intervalMax = intervalMin;
3252     }
3253 
3254     // determine min and max date value
3255     var min = options.min ? options.min.valueOf() : undefined;
3256     var max = options.max ? options.max.valueOf() : undefined;
3257     if (min && max) {
3258         if (min >= max) {
3259             // empty range
3260             var day = 1000 * 60 * 60 * 24;
3261             max = min + day;
3262         }
3263         if (intervalMax > (max - min)) {
3264             intervalMax = (max - min);
3265         }
3266         if (intervalMin > (max - min)) {
3267             intervalMin = (max - min);
3268         }
3269     }
3270 
3271     // prevent empty interval
3272     if (startValue >= endValue) {
3273         endValue += 1000 * 60 * 60 * 24;
3274     }
3275 
3276     // prevent too small scale
3277     // TODO: IE has problems with milliseconds
3278     if (interval < intervalMin) {
3279         var diff = (intervalMin - interval);
3280         var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
3281         startValue -= Math.round(diff * f);
3282         endValue   += Math.round(diff * (1 - f));
3283     }
3284 
3285     // prevent too large scale
3286     if (interval > intervalMax) {
3287         var diff = (interval - intervalMax);
3288         var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
3289         startValue += Math.round(diff * f);
3290         endValue   -= Math.round(diff * (1 - f));
3291     }
3292 
3293     // prevent to small start date
3294     if (min) {
3295         var diff = (startValue - min);
3296         if (diff < 0) {
3297             startValue -= diff;
3298             endValue -= diff;
3299         }
3300     }
3301 
3302     // prevent to large end date
3303     if (max) {
3304         var diff = (max - endValue);
3305         if (diff < 0) {
3306             startValue += diff;
3307             endValue += diff;
3308         }
3309     }
3310 
3311     // apply new dates
3312     this.start = new Date(startValue);
3313     this.end = new Date(endValue);
3314 };
3315 
3316 /**
3317  * Delete an item after a confirmation.
3318  * The deletion can be cancelled by executing .cancelDelete() during the
3319  * triggered event 'delete'.
3320  * @param {int} index   Index of the item to be deleted
3321  */
3322 links.Timeline.prototype.confirmDeleteItem = function(index) {
3323     this.applyDelete = true;
3324 
3325     // select the event to be deleted
3326     if (!this.isSelected(index)) {
3327         this.selectItem(index);
3328     }
3329 
3330     // fire a delete event trigger. 
3331     // Note that the delete event can be canceled from within an event listener if 
3332     // this listener calls the method cancelChange().
3333     this.trigger('delete');
3334 
3335     if (this.applyDelete) {
3336         this.deleteItem(index);
3337     }
3338 
3339     delete this.applyDelete;
3340 };
3341 
3342 /**
3343  * Delete an item
3344  * @param {int} index   Index of the item to be deleted
3345  */
3346 links.Timeline.prototype.deleteItem = function(index) {
3347     if (index >= this.items.length) {
3348         throw "Cannot delete row, index out of range";
3349     }
3350 
3351     this.unselectItem();
3352 
3353     // actually delete the item
3354     this.items.splice(index, 1);
3355 
3356     // delete the row in the original data table
3357     if (this.data) {
3358         if (google && google.visualization &&
3359             this.data instanceof google.visualization.DataTable) {
3360             this.data.removeRow(index);
3361         }
3362         else if (links.Timeline.isArray(this.data)) {
3363             this.data.splice(index, 1);
3364         }
3365         else {
3366             throw "Cannot delete row from data, unknown data type";
3367         }
3368     }
3369 
3370     this.size.dataChanged = true;
3371     this.redrawFrame();
3372     this.recalcSize();
3373     this.stackEvents(this.options.animate);
3374     if (!this.options.animate) {
3375         this.redrawFrame();
3376     }
3377     this.size.dataChanged = false;
3378 };
3379 
3380 
3381 /**
3382  * Delete all items
3383  */
3384 links.Timeline.prototype.deleteAllItems = function() {
3385     this.unselectItem();
3386 
3387     // delete the loaded data
3388     this.items = [];
3389 
3390     // delete the groups
3391     this.deleteGroups();
3392 
3393     // empty original data table
3394     if (this.data) {
3395         if (google && google.visualization &&
3396             this.data instanceof google.visualization.DataTable) {
3397             this.data.removeRows(0, this.data.getNumberOfRows());
3398         }
3399         else if (links.Timeline.isArray(this.data)) {
3400             this.data.splice(0, this.data.length);
3401         }
3402         else {
3403             throw "Cannot delete row from data, unknown data type";
3404         }
3405     }
3406 
3407     this.size.dataChanged = true;
3408     this.redrawFrame();
3409     this.recalcSize();
3410     this.stackEvents(this.options.animate);
3411     if (!this.options.animate) {
3412         this.redrawFrame();
3413     }
3414     this.size.dataChanged = false;
3415 };
3416 
3417 
3418 /**
3419  * Find the group from a given height in the timeline
3420  * @param {Number} height   Height in the timeline
3421  * @param {boolean}
3422     * @return {Object} group   The group object, or undefined if out of range
3423  */
3424 links.Timeline.prototype.getGroupFromHeight = function(height) {
3425     var groups = this.groups,
3426         options = this.options,
3427         size = this.size,
3428         y = height - (options.axisOnTop ? size.axis.height : 0);
3429 
3430     if (groups) {
3431         var group;
3432         /* TODO: cleanup
3433          for (var i = 0, iMax = groups.length; i < iMax; i++) {
3434          group = groups[i];
3435          if (y > group.top && y < group.top + group.height) {
3436          return group;
3437          }
3438          }*/
3439         for (var i = groups.length - 1; i >= 0; i--) {
3440             group = groups[i];
3441             if (y > group.top) {
3442                 return group;
3443             }
3444         }
3445 
3446         return group; // return the last group
3447     }
3448 
3449     return undefined;
3450 };
3451 
3452 /**
3453  * Retrieve the properties of an item.
3454  * @param {Number} index
3455  * @return {Object} properties   Object containing item properties:<br>
3456  *                              {Date} start (required),
3457  *                              {Date} end (optional),
3458  *                              {String} content (required),
3459  *                              {String} group (optional)
3460  */
3461 links.Timeline.prototype.getItem = function (index) {
3462     if (index >= this.items.length) {
3463         throw "Cannot get item, index out of range";
3464     }
3465 
3466     var item = this.items[index];
3467 
3468     var properties = {};
3469     properties.start = new Date(item.start);
3470     if (item.end) {
3471         properties.end = new Date(item.end);
3472     }
3473     properties.content = item.content;
3474     if (item.group) {
3475         properties.group = item.group.content;
3476     }
3477 
3478     return properties;
3479 };
3480 
3481 /**
3482  * Add a new item.
3483  * @param {Object} itemData     Object containing item properties:<br>
3484  *                              {Date} start (required),
3485  *                              {Date} end (optional),
3486  *                              {String} content (required),
3487  *                              {String} group (optional)
3488  */
3489 links.Timeline.prototype.addItem = function (itemData) {
3490     var items = [
3491         itemData
3492     ];
3493 
3494     this.addItems(items);
3495 };
3496 
3497 /**
3498  * Add new items.
3499  * @param {Array} items  An array containing Objects.
3500  *                       The objects must have the following parameters:
3501  *                         {Date} start,
3502  *                         {Date} end,
3503  *                         {String} content with text or HTML code,
3504  *                         {String} group
3505  */
3506 links.Timeline.prototype.addItems = function (items) {
3507     var newItems = items,
3508         curItems = this.items;
3509 
3510     // append the items
3511     for (var i = 0, iMax = newItems.length; i < iMax; i++) {
3512         var itemData = items[i];
3513 
3514         this.addGroup(itemData.group);
3515 
3516         curItems.push(this.createItem(itemData));
3517 
3518         var index = curItems.length - 1;
3519         this.updateData(index, itemData);
3520     }
3521 
3522     // redraw timeline
3523     this.size.dataChanged = true;
3524     this.redrawFrame();
3525     this.recalcSize();
3526     this.stackEvents(false);
3527     this.redrawFrame();
3528     this.size.dataChanged = false;
3529 };
3530 
3531 /**
3532  * Create an item object, containing all needed parameters
3533  * @param {Object} itemData  Object containing parameters start, end
3534  *                           content, group.
3535  * @return {Object} item
3536  */
3537 links.Timeline.prototype.createItem = function(itemData) {
3538     var item = {
3539         'start': itemData.start,
3540         'end': itemData.end,
3541         'content': itemData.content,
3542         'type': itemData.end ? 'range' : this.options.style,
3543         'group': this.findGroup(itemData.group),
3544         'top': 0,
3545         'left': 0,
3546         'width': 0,
3547         'height': 0,
3548         'lineWidth' : 0,
3549         'dotWidth': 0,
3550         'dotHeight': 0
3551     };
3552     return item;
3553 };
3554 
3555 /**
3556  * Edit an item
3557  * @param {Number} index
3558  * @param {Object} itemData     Object containing item properties:<br>
3559  *                              {Date} start (required),
3560  *                              {Date} end (optional),
3561  *                              {String} content (required),
3562  *                              {String} group (optional)
3563  */
3564 links.Timeline.prototype.changeItem = function (index, itemData) {
3565     if (index >= this.items.length) {
3566         throw "Cannot change item, index out of range";
3567     }
3568 
3569     var style = this.options.style;
3570     var item = this.items[index];
3571 
3572     // edit the item
3573     if (itemData.start) {
3574         item.start = itemData.start;
3575     }
3576     if (itemData.end) {
3577         item.end = itemData.end;
3578     }
3579     if (itemData.content) {
3580         item.content = itemData.content;
3581     }
3582     if (itemData.group) {
3583         item.group = this.addGroup(itemData.group);
3584     }
3585 
3586     // update the original data table
3587     this.updateData(index, itemData);
3588 
3589     // redraw timeline
3590     this.size.dataChanged = true;
3591     this.redrawFrame();
3592     this.recalcSize();
3593     this.stackEvents(false);
3594     this.redrawFrame();
3595     this.size.dataChanged = false;
3596 };
3597 
3598 
3599 /**
3600  * Find a group by its name.
3601  * @param {String} group
3602  * @return {Object} a group object or undefined when group is not found
3603  */
3604 links.Timeline.prototype.findGroup = function (group) {
3605     var index = this.groupIndexes[group];
3606     return (index != undefined) ? this.groups[index] : undefined;
3607 };
3608 
3609 /**
3610  * Delete all groups
3611  */
3612 links.Timeline.prototype.deleteGroups = function () {
3613     this.groups = [];
3614     this.groupIndexes = {};
3615 };
3616 
3617 
3618 /**
3619  * Add a group. When the group already exists, no new group is created
3620  * but the existing group is returned.
3621  * @param {String} groupName   the name of the group
3622  * @return {Object} groupObject
3623  */
3624 links.Timeline.prototype.addGroup = function (groupName) {
3625     var groups = this.groups,
3626         groupIndexes = this.groupIndexes,
3627         groupObj = undefined;
3628 
3629     var groupIndex = groupIndexes[groupName];
3630     if (groupIndex === undefined && groupName !== undefined) {
3631         groupObj = {
3632             'content': groupName,
3633             'labelTop': 0,
3634             'lineTop': 0
3635             // note: this object will lateron get addition information, 
3636             //       such as height and width of the group         
3637         };
3638         groups.push(groupObj);
3639         // sort the groups
3640         groups = groups.sort(function (a, b) {
3641             if (a.content > b.content) {
3642                 return 1;
3643             }
3644             if (a.content < b.content) {
3645                 return -1;
3646             }
3647             return 0;
3648         });
3649 
3650         // rebuilt the groupIndexes
3651         for (var i = 0, iMax = groups.length; i < iMax; i++) {
3652             groupIndexes[groups[i].content] = i;
3653         }
3654     }
3655     else {
3656         groupObj = groups[groupIndex];
3657     }
3658 
3659     return groupObj;
3660 };
3661 
3662 /**
3663  * Cancel a change item
3664  * This method can be called insed an event listener which catches the "change"
3665  * event. The changed event position will be undone.
3666  */
3667 links.Timeline.prototype.cancelChange = function () {
3668     this.applyChange = false;
3669 };
3670 
3671 /**
3672  * Cancel deletion of an item
3673  * This method can be called insed an event listener which catches the "delete"
3674  * event. Deletion of the event will be undone.
3675  */
3676 links.Timeline.prototype.cancelDelete = function () {
3677     this.applyDelete = false;
3678 };
3679 
3680 
3681 /**
3682  * Cancel creation of a new item
3683  * This method can be called insed an event listener which catches the "new"
3684  * event. Creation of the new the event will be undone.
3685  */
3686 links.Timeline.prototype.cancelAdd = function () {
3687     this.applyAdd = false;
3688 };
3689 
3690 
3691 /**
3692  * Select an event. The visible chart range will be moved such that the selected
3693  * event is placed in the middle.
3694  * For example selection = [{row: 5}];
3695  * @param {Array} selection   An array with a column row, containing the row
3696  *                           number (the id) of the event to be selected.
3697  * @return {boolean}         true if selection is succesfully set, else false.
3698  */
3699 links.Timeline.prototype.setSelection = function(selection) {
3700     if (selection != undefined && selection.length > 0) {
3701         if (selection[0].row != undefined) {
3702             var index = selection[0].row;
3703             if (this.items[index]) {
3704                 var item = this.items[index];
3705                 this.selectItem(index);
3706 
3707                 // move the visible chart range to the selected event.
3708                 var start = item.start;
3709                 var end = item.end;
3710                 var middle;
3711                 if (end != undefined) {
3712                     middle = new Date((end.valueOf() + start.valueOf()) / 2);
3713                 } else {
3714                     middle = new Date(start);
3715                 }
3716                 var diff = (this.end.valueOf() - this.start.valueOf()),
3717                     newStart = new Date(middle.valueOf() - diff/2),
3718                     newEnd = new Date(middle.valueOf() + diff/2);
3719 
3720                 this.setVisibleChartRange(newStart, newEnd);
3721 
3722                 return true;
3723             }
3724         }
3725     }
3726     else {
3727         // unselect current selection
3728         this.unselectItem();
3729     }
3730     return false;
3731 };
3732 
3733 /**
3734  * Retrieve the currently selected event
3735  * @return {Array} sel  An array with a column row, containing the row number
3736  *                      of the selected event. If there is no selection, an
3737  *                      empty array is returned.
3738  */
3739 links.Timeline.prototype.getSelection = function() {
3740     var sel = [];
3741     if (this.selection) {
3742         sel.push({"row": this.selection.index});
3743     }
3744     return sel;
3745 };
3746 
3747 
3748 /**
3749  * Select an item by its index
3750  * @param {Number} index
3751  */
3752 links.Timeline.prototype.selectItem = function(index) {
3753     this.unselectItem();
3754 
3755     this.selection = undefined;
3756 
3757     if (this.items[index] !== undefined) {
3758         var item = this.items[index],
3759             domItem = item.dom;
3760 
3761         this.selection = {
3762             'index': index,
3763             'item': domItem
3764         };
3765 
3766         if (this.options.editable) {
3767             domItem.style.cursor = 'move';
3768         }
3769         switch (item.type) {
3770             case 'range':
3771                 domItem.className = "timeline-event timeline-event-selected timeline-event-range";
3772                 break;
3773             case 'box':
3774                 domItem.className = "timeline-event timeline-event-selected timeline-event-box";
3775                 domItem.line.className = "timeline-event timeline-event-selected timeline-event-line";
3776                 domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
3777                 break;
3778             case 'dot':
3779                 domItem.className = "timeline-event timeline-event-selected";
3780                 domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
3781                 break;
3782         }
3783 
3784         /* TODO: cleanup this cannot work as this breaks any javscript action inside the item
3785          // move the item to the end, such that it will be displayed on top of the other items
3786          var parent = domItem.parentNode;
3787          if (parent) {
3788          parent.removeChild(domItem);
3789          parent.appendChild(domItem);
3790          }
3791          */
3792     }
3793 };
3794 
3795 /**
3796  * Check if an item is currently selected
3797  * @param {Number} index
3798  * @return {boolean} true if row is selected, else false
3799  */
3800 links.Timeline.prototype.isSelected = function (index) {
3801     return (this.selection && this.selection.index === index);
3802 };
3803 
3804 /**
3805  * Unselect the currently selected event (if any)
3806  */
3807 links.Timeline.prototype.unselectItem = function() {
3808     if (this.selection) {
3809         var item = this.items[this.selection.index];
3810 
3811         if (item && item.dom) {
3812             var domItem = item.dom;
3813             domItem.style.cursor = '';
3814             switch (item.type) {
3815                 case 'range':
3816                     domItem.className = "timeline-event timeline-event-range";
3817                     break;
3818                 case 'box':
3819                     domItem.className = "timeline-event timeline-event-box";
3820                     domItem.line.className = "timeline-event timeline-event-line";
3821                     domItem.dot.className = "timeline-event timeline-event-dot";
3822                     break;
3823                 case 'dot':
3824                     domItem.className = "";
3825                     domItem.dot.className = "timeline-event timeline-event-dot";
3826                     break;
3827             }
3828         }
3829     }
3830 
3831     this.selection = undefined;
3832 };
3833 
3834 
3835 /**
3836  * Stack the items such that they don't overlap. The items will have a minimal
3837  * distance equal to options.eventMargin.
3838  * @param {boolean} animate     if animate is true, the items are moved to
3839  *                              their new position animated
3840  */
3841 links.Timeline.prototype.stackEvents = function(animate) {
3842     if (this.groups.length > 0) {
3843         // under this conditions we refuse to stack the events
3844         return;
3845     }
3846 
3847     if (animate == undefined) {
3848         animate = false;
3849     }
3850 
3851     var sortedItems = this.stackOrder(this.items);
3852     var finalItems = this.stackCalculateFinal(sortedItems, animate);
3853 
3854     if (animate) {
3855         // move animated to the final positions
3856         var animation = this.animation;
3857         if (!animation) {
3858             animation = {};
3859             this.animation = animation;
3860         }
3861         animation.finalItems = finalItems;
3862 
3863         var timeline = this;
3864         var step = function () {
3865             var arrived = timeline.stackMoveOneStep(sortedItems, animation.finalItems);
3866 
3867             timeline.recalcSize();
3868             timeline.redrawFrame();
3869 
3870             if (!arrived) {
3871                 animation.timer = setTimeout(step, 30);
3872             }
3873             else {
3874                 delete animation.finalItems;
3875                 delete animation.timer;
3876             }
3877         };
3878 
3879         if (!animation.timer) {
3880             animation.timer = setTimeout(step, 30);
3881         }
3882     }
3883     else {
3884         this.stackMoveToFinal(sortedItems, finalItems);
3885         this.recalcSize();
3886     }
3887 };
3888 
3889 
3890 /**
3891  * Order the items in the array this.items. The order is determined via:
3892  * - Ranges go before boxes and dots.
3893  * - The item with the left most location goes first
3894  * @param {Array} items        Array with items
3895  * @return {Array} sortedItems Array with sorted items
3896  */
3897 links.Timeline.prototype.stackOrder = function(items) {
3898     // TODO: store the sorted items, to have less work later on
3899     var sortedItems = items.concat([]);
3900 
3901     var f = function (a, b) {
3902         if (a.type == 'range' && b.type != 'range') {
3903             return -1;
3904         }
3905 
3906         if (a.type != 'range' && b.type == 'range') {
3907             return 1;
3908         }
3909 
3910         return (a.left - b.left);
3911     };
3912 
3913     sortedItems.sort(f);
3914 
3915     return sortedItems;
3916 };
3917 
3918 /**
3919  * Adjust vertical positions of the events such that they don't overlap each
3920  * other.
3921  */
3922 links.Timeline.prototype.stackCalculateFinal = function(items) {
3923     var size = this.size,
3924         axisTop = size.axis.top,
3925         options = this.options,
3926         axisOnTop = options.axisOnTop,
3927         eventMargin = options.eventMargin,
3928         eventMarginAxis = options.eventMarginAxis,
3929         finalItems = [];
3930 
3931     // initialize final positions
3932     for (var i = 0, iMax = items.length; i < iMax; i++) {
3933         var item = items[i],
3934             top,
3935             left,
3936             right,
3937             bottom,
3938             height = item.height,
3939             width = item.width;
3940 
3941         if (axisOnTop) {
3942             top = axisTop + eventMarginAxis + eventMargin / 2;
3943         }
3944         else {
3945             top = axisTop - height - eventMarginAxis - eventMargin / 2;
3946         }
3947         bottom = top + height;
3948 
3949         switch (item.type) {
3950             case 'range':
3951             case 'dot':
3952                 left = this.timeToScreen(item.start);
3953                 right = item.end ? this.timeToScreen(item.end) : left + width;
3954                 break;
3955 
3956             case 'box':
3957                 left = this.timeToScreen(item.start) - width / 2;
3958                 right = left + width;
3959                 break;
3960         }
3961 
3962         finalItems[i] = {
3963             'left': left,
3964             'top': top,
3965             'right': right,
3966             'bottom': bottom,
3967             'height': height,
3968             'item': item
3969         };
3970     }
3971 
3972     if (this.options.stackEvents) {
3973         // calculate new, non-overlapping positions
3974         //var items = sortedItems;
3975         for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
3976             //for (var i = finalItems.length - 1; i >= 0; i--) {
3977             var finalItem = finalItems[i];
3978             var collidingItem = null;
3979             do {
3980                 // TODO: optimize checking for overlap. when there is a gap without items,
3981                 //  you only need to check for items from the next item on, not from zero
3982                 collidingItem = this.stackEventsCheckOverlap(finalItems, i, 0, i-1);
3983                 if (collidingItem != null) {
3984                     // There is a collision. Reposition the event above the colliding element
3985                     if (axisOnTop) {
3986                         finalItem.top = collidingItem.top + collidingItem.height + eventMargin;
3987                     }
3988                     else {
3989                         finalItem.top = collidingItem.top - finalItem.height - eventMargin;
3990                     }
3991                     finalItem.bottom = finalItem.top + finalItem.height;
3992                 }
3993             } while (collidingItem);
3994         }
3995     }
3996 
3997     return finalItems;
3998 };
3999 
4000 
4001 /**
4002  * Move the events one step in the direction of their final positions
4003  * @param {Array} currentItems   Array with the real items and their current
4004  *                               positions
4005  * @param {Array} finalItems     Array with objects containing the final
4006  *                               positions of the items
4007  * @return {boolean} arrived     True if all items have reached their final
4008  *                               location, else false
4009  */
4010 links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) {
4011     var arrived = true;
4012 
4013     // apply new positions animated
4014     for (i = 0, iMax = currentItems.length; i < iMax; i++) {
4015         var finalItem = finalItems[i],
4016             item = finalItem.item;
4017 
4018         var topNow = parseInt(item.top);
4019         var topFinal = parseInt(finalItem.top);
4020         var diff = (topFinal - topNow);
4021         if (diff) {
4022             var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1);
4023             if (Math.abs(diff) > 4) step = diff / 4;
4024             var topNew = parseInt(topNow + step);
4025 
4026             if (topNew != topFinal) {
4027                 arrived = false;
4028             }
4029 
4030             item.top = topNew;
4031             item.bottom = item.top + item.height;
4032         }
4033         else {
4034             item.top = finalItem.top;
4035             item.bottom = finalItem.bottom;
4036         }
4037 
4038         item.left = finalItem.left;
4039         item.right = finalItem.right;
4040     }
4041 
4042     return arrived;
4043 };
4044 
4045 
4046 
4047 /**
4048  * Move the events from their current position to the final position
4049  * @param {Array} currentItems   Array with the real items and their current
4050  *                               positions
4051  * @param {Array} finalItems     Array with objects containing the final
4052  *                               positions of the items
4053  */
4054 links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) {
4055     // Put the events directly at there final position
4056     for (i = 0, iMax = currentItems.length; i < iMax; i++) {
4057         var current = currentItems[i],
4058             finalItem = finalItems[i];
4059 
4060         current.left = finalItem.left;
4061         current.top = finalItem.top;
4062         current.right = finalItem.right;
4063         current.bottom = finalItem.bottom;
4064     }
4065 };
4066 
4067 
4068 
4069 /**
4070  * Check if the destiny position of given item overlaps with any
4071  * of the other items from index itemStart to itemEnd.
4072  * @param {Array} items      Array with items
4073  * @param {int}  itemIndex   Number of the item to be checked for overlap
4074  * @param {int}  itemStart   First item to be checked.
4075  * @param {int}  itemEnd     Last item to be checked.
4076  * @return {Object}          colliding item, or undefined when no collisions
4077  */
4078 links.Timeline.prototype.stackEventsCheckOverlap = function(items, itemIndex,
4079                                                             itemStart, itemEnd) {
4080     var eventMargin = this.options.eventMargin,
4081         collision = this.collision;
4082 
4083     // we loop from end to start, as we suppose that the chance of a 
4084     // collision is larger for items at the end, so check these first.
4085     var item1 = items[itemIndex];
4086     for (var i = itemEnd; i >= itemStart; i--) {
4087         var item2 = items[i];
4088         if (collision(item1, item2, eventMargin)) {
4089             if (i != itemIndex) {
4090                 return item2;
4091             }
4092         }
4093     }
4094 
4095     return undefined;
4096 };
4097 
4098 /**
4099  * Test if the two provided items collide
4100  * The items must have parameters left, right, top, and bottom.
4101  * @param {Element} item1       The first item
4102  * @param {Element} item2       The second item
4103  * @param {Number}              margin  A minimum required margin. Optional.
4104  *                              If margin is provided, the two items will be
4105  *                              marked colliding when they overlap or
4106  *                              when the margin between the two is smaller than
4107  *                              the requested margin.
4108  * @return {boolean}            true if item1 and item2 collide, else false
4109  */
4110 links.Timeline.prototype.collision = function(item1, item2, margin) {
4111     // set margin if not specified 
4112     if (margin == undefined) {
4113         margin = 0;
4114     }
4115 
4116     // calculate if there is overlap (collision)
4117     return (item1.left - margin < item2.right &&
4118         item1.right + margin > item2.left &&
4119         item1.top - margin < item2.bottom &&
4120         item1.bottom + margin > item2.top);
4121 };
4122 
4123 
4124 /**
4125  * fire an event
4126  * @param {String} event   The name of an event, for example "rangechange" or "edit"
4127  */
4128 links.Timeline.prototype.trigger = function (event) {
4129     // built up properties
4130     var properties = null;
4131     switch (event) {
4132         case 'rangechange':
4133         case 'rangechanged':
4134             properties = {
4135                 'start': new Date(this.start),
4136                 'end': new Date(this.end)
4137             };
4138             break;
4139 
4140         case 'timechange':
4141         case 'timechanged':
4142             properties = {
4143                 'time': new Date(this.customTime)
4144             };
4145             break;
4146     }
4147 
4148     // trigger the links event bus
4149     links.events.trigger(this, event, properties);
4150 
4151     // trigger the google event bus
4152     if (google && google.visualization) {
4153         google.visualization.events.trigger(this, event, properties);
4154     }
4155 };
4156 
4157 
4158 
4159 /** ------------------------------------------------------------------------ **/
4160 
4161 
4162 /**
4163  * Event listener (singleton)
4164  */
4165 links.events = links.events || {
4166     'listeners': [],
4167 
4168     /**
4169      * Find a single listener by its object
4170      * @param {Object} object
4171      * @return {Number} index  -1 when not found
4172      */
4173     'indexOf': function (object) {
4174         var listeners = this.listeners;
4175         for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
4176             var listener = listeners[i];
4177             if (listener && listener.object == object) {
4178                 return i;
4179             }
4180         }
4181         return -1;
4182     },
4183 
4184     /**
4185      * Add an event listener
4186      * @param {Object} object
4187      * @param {String} event       The name of an event, for example 'select'
4188      * @param {function} callback  The callback method, called when the
4189      *                             event takes place
4190      */
4191     'addListener': function (object, event, callback) {
4192         var index = this.indexOf(object);
4193         var listener = this.listeners[index];
4194         if (!listener) {
4195             listener = {
4196                 'object': object,
4197                 'events': {}
4198             };
4199             this.listeners.push(listener);
4200         }
4201 
4202         var callbacks = listener.events[event];
4203         if (!callbacks) {
4204             callbacks = [];
4205             listener.events[event] = callbacks;
4206         }
4207 
4208         // add the callback if it does not yet exist
4209         if (callbacks.indexOf(callback) == -1) {
4210             callbacks.push(callback);
4211         }
4212     },
4213 
4214     /**
4215      * Remove an event listener
4216      * @param {Object} object
4217      * @param {String} event       The name of an event, for example 'select'
4218      * @param {function} callback  The registered callback method
4219      */
4220     'removeListener': function (object, event, callback) {
4221         var index = this.indexOf(object);
4222         var listener = this.listeners[index];
4223         if (listener) {
4224             var callbacks = listener.events[event];
4225             if (callbacks) {
4226                 var index = callbacks.indexOf(callback);
4227                 if (index != -1) {
4228                     callbacks.splice(index, 1);
4229                 }
4230 
4231                 // remove the array when empty
4232                 if (callbacks.length == 0) {
4233                     delete listener.events[event];
4234                 }
4235             }
4236 
4237             // count the number of registered events. remove listener when empty
4238             var count = 0;
4239             var events = listener.events;
4240             for (var e in events) {
4241                 if (events.hasOwnProperty(e)) {
4242                     count++;
4243                 }
4244             }
4245             if (count == 0) {
4246                 delete this.listeners[index];
4247             }
4248         }
4249     },
4250 
4251     /**
4252      * Remove all registered event listeners
4253      */
4254     'removeAllListeners': function () {
4255         this.listeners = [];
4256     },
4257 
4258     /**
4259      * Trigger an event. All registered event handlers will be called
4260      * @param {Object} object
4261      * @param {String} event
4262      * @param {Object} properties (optional)
4263      */
4264     'trigger': function (object, event, properties) {
4265         var index = this.indexOf(object);
4266         var listener = this.listeners[index];
4267         if (listener) {
4268             var callbacks = listener.events[event];
4269             if (callbacks) {
4270                 for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
4271                     callbacks[i](properties);
4272                 }
4273             }
4274         }
4275     }
4276 };
4277 
4278 
4279 /** ------------------------------------------------------------------------ **/
4280 
4281 /**
4282  * @constructor  links.Timeline.StepDate
4283  * The class StepDate is an iterator for dates. You provide a start date and an
4284  * end date. The class itself determines the best scale (step size) based on the
4285  * provided start Date, end Date, and minimumStep.
4286  *
4287  * If minimumStep is provided, the step size is chosen as close as possible
4288  * to the minimumStep but larger than minimumStep. If minimumStep is not
4289  * provided, the scale is set to 1 DAY.
4290  * The minimumStep should correspond with the onscreen size of about 6 characters
4291  *
4292  * Alternatively, you can set a scale by hand.
4293  * After creation, you can initialize the class by executing start(). Then you
4294  * can iterate from the start date to the end date via next(). You can check if
4295  * the end date is reached with the function end(). After each step, you can
4296  * retrieve the current date via get().
4297  * The class step has scales ranging from milliseconds, seconds, minutes, hours,
4298  * days, to years.
4299  *
4300  * Version: 1.0
4301  *
4302  * @param {Date} start        The start date, for example new Date(2010, 9, 21)
4303  *                            or new Date(2010, 9,21,23,45,00)
4304  * @param {Date} end          The end date
4305  * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
4306  */
4307 links.Timeline.StepDate = function(start, end, minimumStep) {
4308 
4309     // variables
4310     this.current = new Date();
4311     this._start = new Date();
4312     this._end = new Date();
4313 
4314     this.autoScale  = true;
4315     this.scale = links.Timeline.StepDate.SCALE.DAY;
4316     this.step = 1;
4317 
4318     // initialize the range
4319     this.setRange(start, end, minimumStep);
4320 };
4321 
4322 /// enum scale
4323 links.Timeline.StepDate.SCALE = { MILLISECOND : 1,
4324     SECOND : 2,
4325     MINUTE : 3,
4326     HOUR : 4,
4327     DAY : 5,
4328     MONTH : 6,
4329     YEAR : 7};
4330 
4331 
4332 /**
4333  * Set a new range
4334  * If minimumStep is provided, the step size is chosen as close as possible
4335  * to the minimumStep but larger than minimumStep. If minimumStep is not
4336  * provided, the scale is set to 1 DAY.
4337  * The minimumStep should correspond with the onscreen size of about 6 characters
4338  * @param {Date} start        The start date and time.
4339  * @param {Date} end          The end date and time.
4340  * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
4341  */
4342 links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) {
4343     if (isNaN(start) || isNaN(end)) {
4344         //throw  "No legal start or end date in method setRange";
4345         return;
4346     }
4347 
4348     this._start      = (start != undefined)  ? new Date(start) : new Date();
4349     this._end        = (end != undefined)    ? new Date(end) : new Date();
4350 
4351     if (this.autoScale) {
4352         this.setMinimumStep(minimumStep);
4353     }
4354 };
4355 
4356 /**
4357  * Set the step iterator to the start date.
4358  */
4359 links.Timeline.StepDate.prototype.start = function() {
4360     this.current = new Date(this._start);
4361     this.roundToMinor();
4362 };
4363 
4364 /**
4365  * Round the current date to the first minor date value
4366  * This must be executed once when the current date is set to start Date
4367  */
4368 links.Timeline.StepDate.prototype.roundToMinor = function() {
4369     // round to floor
4370     // IMPORTANT: we have no breaks in this switch! (this is no bug)
4371     switch (this.scale) {
4372         case links.Timeline.StepDate.SCALE.YEAR:
4373             this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
4374             this.current.setMonth(0);
4375         case links.Timeline.StepDate.SCALE.MONTH:        this.current.setDate(1);
4376         case links.Timeline.StepDate.SCALE.DAY:          this.current.setHours(0);
4377         case links.Timeline.StepDate.SCALE.HOUR:         this.current.setMinutes(0);
4378         case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setSeconds(0);
4379         case links.Timeline.StepDate.SCALE.SECOND:       this.current.setMilliseconds(0);
4380         //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
4381     }
4382 
4383     if (this.step != 1) {
4384         // round down to the first minor value that is a multiple of the current step size
4385         switch (this.scale) {
4386             case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);  break;
4387             case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);  break;
4388             case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);  break;
4389             case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);  break;
4390             case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1);  break;
4391             case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);  break;
4392             case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
4393             default:                      break;
4394         }
4395     }
4396 };
4397 
4398 /**
4399  * Check if the end date is reached
4400  * @return {boolean}  true if the current date has passed the end date
4401  */
4402 links.Timeline.StepDate.prototype.end = function () {
4403     return (this.current.getTime() > this._end.getTime());
4404 };
4405 
4406 /**
4407  * Do the next step
4408  */
4409 links.Timeline.StepDate.prototype.next = function() {
4410     var prev = this.current.getTime();
4411 
4412     // Two cases, needed to prevent issues with switching daylight savings 
4413     // (end of March and end of October)
4414     if (this.current.getMonth() < 6)   {
4415         switch (this.scale)
4416         {
4417             case links.Timeline.StepDate.SCALE.MILLISECOND:
4418 
4419                 this.current = new Date(this.current.getTime() + this.step); break;
4420             case links.Timeline.StepDate.SCALE.SECOND:       this.current = new Date(this.current.getTime() + this.step * 1000); break;
4421             case links.Timeline.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.getTime() + this.step * 1000 * 60); break;
4422             case links.Timeline.StepDate.SCALE.HOUR:
4423                 this.current = new Date(this.current.getTime() + this.step * 1000 * 60 * 60);
4424                 // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
4425                 var h = this.current.getHours();
4426                 this.current.setHours(h - (h % this.step));
4427                 break;
4428             case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
4429             case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
4430             case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
4431             default:                      break;
4432         }
4433     }
4434     else {
4435         switch (this.scale)
4436         {
4437             case links.Timeline.StepDate.SCALE.MILLISECOND:
4438 
4439                 this.current = new Date(this.current.getTime() + this.step); break;
4440             case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() + this.step); break;
4441             case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() + this.step); break;
4442             case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() + this.step); break;
4443             case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
4444             case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
4445             case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
4446             default:                      break;
4447         }
4448     }
4449 
4450     if (this.step != 1) {
4451         // round down to the correct major value
4452         switch (this.scale) {
4453             case links.Timeline.StepDate.SCALE.MILLISECOND:  if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);  break;
4454             case links.Timeline.StepDate.SCALE.SECOND:       if(this.current.getSeconds() < this.step) this.current.setSeconds(0);  break;
4455             case links.Timeline.StepDate.SCALE.MINUTE:       if(this.current.getMinutes() < this.step) this.current.setMinutes(0);  break;
4456             case links.Timeline.StepDate.SCALE.HOUR:         if(this.current.getHours() < this.step) this.current.setHours(0);  break;
4457             case links.Timeline.StepDate.SCALE.DAY:          if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
4458             case links.Timeline.StepDate.SCALE.MONTH:        if(this.current.getMonth() < this.step) this.current.setMonth(0);  break;
4459             case links.Timeline.StepDate.SCALE.YEAR:         break; // nothing to do for year
4460             default:                break;
4461         }
4462     }
4463 
4464     // safety mechanism: if current time is still unchanged, move to the end
4465     if (this.current.getTime() == prev) {
4466         this.current = new Date(this._end);
4467     }
4468 };
4469 
4470 
4471 /**
4472  * Get the current datetime
4473  * @return {Date}  current The current date
4474  */
4475 links.Timeline.StepDate.prototype.getCurrent = function() {
4476     return this.current;
4477 };
4478 
4479 /**
4480  * Set a custom scale. Autoscaling will be disabled.
4481  * For example setScale(SCALE.MINUTES, 5) will result
4482  * in minor steps of 5 minutes, and major steps of an hour.
4483  *
4484  * @param {links.Timeline.StepDate.SCALE} newScale
4485  *                               A scale. Choose from SCALE.MILLISECOND,
4486  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
4487  *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
4488  * @param {int}        newStep   A step size, by default 1. Choose for
4489  *                               example 1, 2, 5, or 10.
4490  */
4491 links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) {
4492     this.scale = newScale;
4493 
4494     if (newStep > 0)
4495         this.step = newStep;
4496 
4497     this.autoScale = false;
4498 };
4499 
4500 /**
4501  * Enable or disable autoscaling
4502  * @param {boolean} enable  If true, autoascaling is set true
4503  */
4504 links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
4505     this.autoScale = enable;
4506 };
4507 
4508 
4509 /**
4510  * Automatically determine the scale that bests fits the provided minimum step
4511  * @param {int} minimumStep  The minimum step size in milliseconds
4512  */
4513 links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) {
4514     if (minimumStep == undefined)
4515         return;
4516 
4517     var stepYear       = (1000 * 60 * 60 * 24 * 30 * 12);
4518     var stepMonth      = (1000 * 60 * 60 * 24 * 30);
4519     var stepDay        = (1000 * 60 * 60 * 24);
4520     var stepHour       = (1000 * 60 * 60);
4521     var stepMinute     = (1000 * 60);
4522     var stepSecond     = (1000);
4523     var stepMillisecond= (1);
4524 
4525     // find the smallest step that is larger than the provided minimumStep
4526     if (stepYear*1000 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1000;}
4527     if (stepYear*500 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 500;}
4528     if (stepYear*100 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 100;}
4529     if (stepYear*50 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 50;}
4530     if (stepYear*10 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 10;}
4531     if (stepYear*5 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 5;}
4532     if (stepYear > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1;}
4533     if (stepMonth*3 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 3;}
4534     if (stepMonth > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 1;}
4535     if (stepDay*5 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 5;}
4536     if (stepDay*2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 2;}
4537     if (stepDay > minimumStep)              {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 1;}
4538     if (stepHour*4 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 4;}
4539     if (stepHour > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 1;}
4540     if (stepMinute*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 15;}
4541     if (stepMinute*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 10;}
4542     if (stepMinute*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 5;}
4543     if (stepMinute > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 1;}
4544     if (stepSecond*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 15;}
4545     if (stepSecond*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 10;}
4546     if (stepSecond*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 5;}
4547     if (stepSecond > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 1;}
4548     if (stepMillisecond*200 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
4549     if (stepMillisecond*100 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
4550     if (stepMillisecond*50 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
4551     if (stepMillisecond*10 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
4552     if (stepMillisecond*5 > minimumStep)    {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
4553     if (stepMillisecond > minimumStep)      {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
4554 };
4555 
4556 /**
4557  * Snap a date to a rounded value. The snap intervals are dependent on the
4558  * current scale and step.
4559  * @param {Date} date   the date to be snapped
4560  */
4561 links.Timeline.StepDate.prototype.snap = function(date) {
4562     if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
4563         var year = date.getFullYear() + Math.round(date.getMonth() / 12);
4564         date.setFullYear(Math.round(year / this.step) * this.step);
4565         date.setMonth(0);
4566         date.setDate(0);
4567         date.setHours(0);
4568         date.setMinutes(0);
4569         date.setSeconds(0);
4570         date.setMilliseconds(0);
4571     }
4572     else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
4573         if (date.getDate() > 15) {
4574             date.setDate(1);
4575             date.setMonth(date.getMonth() + 1);
4576             // important: first set Date to 1, after that change the month.      
4577         }
4578         else {
4579             date.setDate(1);
4580         }
4581 
4582         date.setHours(0);
4583         date.setMinutes(0);
4584         date.setSeconds(0);
4585         date.setMilliseconds(0);
4586     }
4587     else if (this.scale == links.Timeline.StepDate.SCALE.DAY) {
4588         switch (this.step) {
4589             case 5:
4590             case 2:
4591                 date.setHours(Math.round(date.getHours() / 24) * 24); break;
4592             default:
4593                 date.setHours(Math.round(date.getHours() / 12) * 12); break;
4594         }
4595         date.setMinutes(0);
4596         date.setSeconds(0);
4597         date.setMilliseconds(0);
4598     }
4599     else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
4600         switch (this.step) {
4601             case 4:
4602                 date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
4603             default:
4604                 date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
4605         }
4606         date.setSeconds(0);
4607         date.setMilliseconds(0);
4608     } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
4609         switch (this.step) {
4610             case 15:
4611             case 10:
4612                 date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
4613                 date.setSeconds(0);
4614                 break;
4615             case 5:
4616                 date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
4617             default:
4618                 date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
4619         }
4620         date.setMilliseconds(0);
4621     }
4622     else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
4623         switch (this.step) {
4624             case 15:
4625             case 10:
4626                 date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
4627                 date.setMilliseconds(0);
4628                 break;
4629             case 5:
4630                 date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
4631             default:
4632                 date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
4633         }
4634     }
4635     else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
4636         var step = this.step > 5 ? this.step / 2 : 1;
4637         date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
4638     }
4639 };
4640 
4641 /**
4642  * Check if the current step is a major step (for example when the step
4643  * is DAY, a major step is each first day of the MONTH)
4644  * @return true if current date is major, else false.
4645  */
4646 links.Timeline.StepDate.prototype.isMajor = function() {
4647     switch (this.scale)
4648     {
4649         case links.Timeline.StepDate.SCALE.MILLISECOND:
4650             return (this.current.getMilliseconds() == 0);
4651         case links.Timeline.StepDate.SCALE.SECOND:
4652             return (this.current.getSeconds() == 0);
4653         case links.Timeline.StepDate.SCALE.MINUTE:
4654             return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
4655         // Note: this is no bug. Major label is equal for both minute and hour scale
4656         case links.Timeline.StepDate.SCALE.HOUR:
4657             return (this.current.getHours() == 0);
4658         case links.Timeline.StepDate.SCALE.DAY:
4659             return (this.current.getDate() == 1);
4660         case links.Timeline.StepDate.SCALE.MONTH:
4661             return (this.current.getMonth() == 0);
4662         case links.Timeline.StepDate.SCALE.YEAR:
4663             return false;
4664         default:
4665             return false;
4666     }
4667 };
4668 
4669 
4670 /**
4671  * Returns formatted text for the minor axislabel, depending on the current
4672  * date and the scale. For example when scale is MINUTE, the current time is
4673  * formatted as "hh:mm".
4674  * @param {Date} [date] custom date. if not provided, current date is taken
4675  * @return {string}     minor axislabel
4676  */
4677 links.Timeline.StepDate.prototype.getLabelMinor = function(date) {
4678     var MONTHS_SHORT = new Array("Jan", "Feb", "Mar",
4679         "Apr", "May", "Jun",
4680         "Jul", "Aug", "Sep",
4681         "Oct", "Nov", "Dec");
4682 
4683     if (date == undefined) {
4684         date = this.current;
4685     }
4686 
4687     switch (this.scale)
4688     {
4689         case links.Timeline.StepDate.SCALE.MILLISECOND:  return String(date.getMilliseconds());
4690         case links.Timeline.StepDate.SCALE.SECOND:       return String(date.getSeconds());
4691         case links.Timeline.StepDate.SCALE.MINUTE:       return this.addZeros(date.getHours(), 2) + ":" +
4692             this.addZeros(date.getMinutes(), 2);
4693         case links.Timeline.StepDate.SCALE.HOUR:         return this.addZeros(date.getHours(), 2) + ":" +
4694             this.addZeros(date.getMinutes(), 2);
4695         case links.Timeline.StepDate.SCALE.DAY:          return String(date.getDate());
4696         case links.Timeline.StepDate.SCALE.MONTH:        return MONTHS_SHORT[date.getMonth()];   // month is zero based
4697         case links.Timeline.StepDate.SCALE.YEAR:         return String(date.getFullYear());
4698         default:                                         return "";
4699     }
4700 };
4701 
4702 
4703 /**
4704  * Returns formatted text for the major axislabel, depending on the current
4705  * date and the scale. For example when scale is MINUTE, the major scale is
4706  * hours, and the hour will be formatted as "hh".
4707  * @param {Date} [date] custom date. if not provided, current date is taken
4708  * @return {string}     major axislabel
4709  */
4710 links.Timeline.StepDate.prototype.getLabelMajor = function(date) {
4711     var MONTHS = new Array("January", "February", "March",
4712         "April", "May", "June",
4713         "July", "August", "September",
4714         "October", "November", "December");
4715     var DAYS = new Array("Sunday", "Monday", "Tuesday",
4716         "Wednesday", "Thursday", "Friday", "Saturday");
4717 
4718     if (date == undefined) {
4719         date = this.current;
4720     }
4721 
4722     switch (this.scale) {
4723         case links.Timeline.StepDate.SCALE.MILLISECOND:
4724             return  this.addZeros(date.getHours(), 2) + ":" +
4725                 this.addZeros(date.getMinutes(), 2) + ":" +
4726                 this.addZeros(date.getSeconds(), 2);
4727         case links.Timeline.StepDate.SCALE.SECOND:
4728             return  date.getDate() + " " +
4729                 MONTHS[date.getMonth()] + " " +
4730                 this.addZeros(date.getHours(), 2) + ":" +
4731                 this.addZeros(date.getMinutes(), 2);
4732         case links.Timeline.StepDate.SCALE.MINUTE:
4733             return  DAYS[date.getDay()] + " " +
4734                 date.getDate() + " " +
4735                 MONTHS[date.getMonth()] + " " +
4736                 date.getFullYear();
4737         case links.Timeline.StepDate.SCALE.HOUR:
4738             return  DAYS[date.getDay()] + " " +
4739                 date.getDate() + " " +
4740                 MONTHS[date.getMonth()] + " " +
4741                 date.getFullYear();
4742         case links.Timeline.StepDate.SCALE.DAY:
4743             return  MONTHS[date.getMonth()] + " " +
4744                 date.getFullYear();
4745         case links.Timeline.StepDate.SCALE.MONTH:
4746             return String(date.getFullYear());
4747         default:
4748             return "";
4749     }
4750 };
4751 
4752 /**
4753  * Add leading zeros to the given value to match the desired length.
4754  * For example addZeros(123, 5) returns "00123"
4755  * @param {int} value   A value
4756  * @param {int} len     Desired final length
4757  * @return {string}     value with leading zeros
4758  */
4759 links.Timeline.StepDate.prototype.addZeros = function(value, len) {
4760     var str = "" + value;
4761     while (str.length < len) {
4762         str = "0" + str;
4763     }
4764     return str;
4765 };
4766 
4767 
4768 
4769 /** ------------------------------------------------------------------------ **/
4770 
4771 /**
4772  * Image Loader service.
4773  * can be used to get a callback when a certain image is loaded
4774  *
4775  */
4776 links.imageloader = (function () {
4777     var urls = {};  // the loaded urls
4778     var callbacks = {}; // the urls currently being loaded. Each key contains 
4779     // an array with callbacks
4780 
4781     /**
4782      * Check if an image url is loaded
4783      * @param {String} url
4784      * @return {boolean} loaded   True when loaded, false when not loaded
4785      *                            or when being loaded
4786      */
4787     function isLoaded (url) {
4788         if (urls[url] == true) {
4789             return true;
4790         }
4791 
4792         var image = new Image();
4793         image.src = url;
4794         if (image.complete) {
4795             return true;
4796         }
4797 
4798         return false;
4799     }
4800 
4801     /**
4802      * Check if an image url is being loaded
4803      * @param {String} url
4804      * @return {boolean} loading   True when being loaded, false when not loading
4805      *                             or when already loaded
4806      */
4807     function isLoading (url) {
4808         return (callbacks[url] != undefined);
4809     }
4810 
4811     /**
4812      * Load given image url
4813      * @param {String} url
4814      * @param {function} callback
4815      * @param {boolean} sendCallbackWhenAlreadyLoaded  optional
4816      */
4817     function load (url, callback, sendCallbackWhenAlreadyLoaded) {
4818         if (sendCallbackWhenAlreadyLoaded == undefined) {
4819             sendCallbackWhenAlreadyLoaded = true;
4820         }
4821 
4822         if (isLoaded(url)) {
4823             if (sendCallbackWhenAlreadyLoaded) {
4824                 callback(url);
4825             }
4826             return;
4827         }
4828 
4829         if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) {
4830             return;
4831         }
4832 
4833         var c = callbacks[url];
4834         if (!c) {
4835             var image = new Image();
4836             image.src = url;
4837 
4838             c = [];
4839             callbacks[url] = c;
4840 
4841             image.onload = function (event) {
4842                 urls[url] = true;
4843                 delete callbacks[url];
4844 
4845                 for (var i = 0; i < c.length; i++) {
4846                     c[i](url);
4847                 }
4848             }
4849         }
4850 
4851         if (c.indexOf(callback) == -1) {
4852             c.push(callback);
4853         }
4854     }
4855 
4856     return {
4857         'isLoaded': isLoaded,
4858         'isLoading': isLoading,
4859         'load': load
4860     };
4861 })();
4862 
4863 
4864 /** ------------------------------------------------------------------------ **/
4865 
4866 
4867 /**
4868  * Add and event listener. Works for all browsers
4869  * @param {Element} element    An html element
4870  * @param {string}      action     The action, for example "click",
4871  *                                 without the prefix "on"
4872  * @param {function}    listener   The callback function to be executed
4873  * @param {boolean}     useCapture
4874  */
4875 links.Timeline.addEventListener = function (element, action, listener, useCapture) {
4876     if (element.addEventListener) {
4877         if (useCapture === undefined)
4878             useCapture = false;
4879 
4880         if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
4881             action = "DOMMouseScroll";  // For Firefox
4882         }
4883 
4884         element.addEventListener(action, listener, useCapture);
4885     } else {
4886         element.attachEvent("on" + action, listener);  // IE browsers
4887     }
4888 };
4889 
4890 /**
4891  * Remove an event listener from an element
4892  * @param {Element}  element   An html dom element
4893  * @param {string}       action    The name of the event, for example "mousedown"
4894  * @param {function}     listener  The listener function
4895  * @param {boolean}      useCapture
4896  */
4897 links.Timeline.removeEventListener = function(element, action, listener, useCapture) {
4898     if (element.removeEventListener) {
4899         // non-IE browsers
4900         if (useCapture === undefined)
4901             useCapture = false;
4902 
4903         if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
4904             action = "DOMMouseScroll";  // For Firefox
4905         }
4906 
4907         element.removeEventListener(action, listener, useCapture);
4908     } else {
4909         // IE browsers
4910         element.detachEvent("on" + action, listener);
4911     }
4912 };
4913 
4914 
4915 /**
4916  * Get HTML element which is the target of the event
4917  * @param {MouseEvent} event
4918  * @return {Element} target element
4919  */
4920 links.Timeline.getTarget = function (event) {
4921     // code from http://www.quirksmode.org/js/events_properties.html
4922     if (!event) {
4923         event = window.event;
4924     }
4925 
4926     var target;
4927 
4928     if (event.target) {
4929         target = event.target;
4930     }
4931     else if (event.srcElement) {
4932         target = event.srcElement;
4933     }
4934 
4935     if (target.nodeType !== undefined && target.nodeType == 3) {
4936         // defeat Safari bug
4937         target = target.parentNode;
4938     }
4939 
4940     return target;
4941 };
4942 
4943 /**
4944  * Stop event propagation
4945  */
4946 links.Timeline.stopPropagation = function (event) {
4947     if (!event)
4948         event = window.event;
4949 
4950     if (event.stopPropagation) {
4951         event.stopPropagation();  // non-IE browsers
4952     }
4953     else {
4954         event.cancelBubble = true;  // IE browsers
4955     }
4956 };
4957 
4958 
4959 /**
4960  * Cancels the event if it is cancelable, without stopping further propagation of the event.
4961  */
4962 links.Timeline.preventDefault = function (event) {
4963     if (!event)
4964         event = window.event;
4965 
4966     if (event.preventDefault) {
4967         event.preventDefault();  // non-IE browsers
4968     }
4969     else {
4970         event.returnValue = false;  // IE browsers
4971     }
4972 };
4973 
4974 
4975 /**
4976  * Retrieve the absolute left value of a DOM element
4977  * @param {Element} elem        A dom element, for example a div
4978  * @return {number} left        The absolute left position of this element
4979  *                              in the browser page.
4980  */
4981 links.Timeline.getAbsoluteLeft = function(elem) {
4982     var left = 0;
4983     while( elem != null ) {
4984         left += elem.offsetLeft;
4985         left -= elem.scrollLeft;
4986         elem = elem.offsetParent;
4987     }
4988     if (!document.body.scrollLeft && window.pageXOffset) {
4989         // FF
4990         left -= window.pageXOffset;
4991     }
4992     return left;
4993 };
4994 
4995 /**
4996  * Retrieve the absolute top value of a DOM element
4997  * @param {Element} elem        A dom element, for example a div
4998  * @return {number} top        The absolute top position of this element
4999  *                              in the browser page.
5000  */
5001 links.Timeline.getAbsoluteTop = function(elem) {
5002     var top = 0;
5003     while( elem != null ) {
5004         top += elem.offsetTop;
5005         top -= elem.scrollTop;
5006         elem = elem.offsetParent;
5007     }
5008     if (!document.body.scrollTop && window.pageYOffset) {
5009         // FF
5010         top -= window.pageYOffset;
5011     }
5012     return top;
5013 };
5014 
5015 /**
5016  * Check if given object is a Javascript Array
5017  * @param {*} obj
5018  * @return {Boolean} isArray    true if the given object is an array
5019  */
5020 // See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
5021 links.Timeline.isArray = function (obj) {
5022     if (obj instanceof Array) {
5023         return true;
5024     }
5025     return (Object.prototype.toString.call(obj) === '[object Array]');
5026 };
5027