      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