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-2014 Almende B.V.
 31  *
 32  * @author  Jos de Jong, <jos@almende.org>
 33  * @date    2014-07-28
 34  * @version 2.9.0
 35  */
 36 
 37 /*
 38  * i18n mods by github user iktuz (https://gist.github.com/iktuz/3749287/)
 39  * added to v2.4.1 with da_DK language by @bjarkebech
 40  */
 41 
 42 /*
 43  * TODO
 44  *
 45  * Add zooming with pinching on Android
 46  *
 47  * Bug: when an item contains a javascript onclick or a link, this does not work
 48  *      when the item is not selected (when the item is being selected,
 49  *      it is redrawn, which cancels any onclick or link action)
 50  * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly
 51  * Bug: neglect items when they have no valid start/end, instead of throwing an error
 52  * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
 53  * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
 54  * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
 55  */
 56 
 57 /**
 58  * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 59  * "links"
 60  */
 61 if (typeof links === 'undefined') {
 62     links = {};
 63     // important: do not use var, as "var links = {};" will overwrite
 64     //            the existing links variable value with undefined in IE8, IE7.
 65 }
 66 
 67 
 68 /**
 69  * Ensure the variable google exists
 70  */
 71 if (typeof google === 'undefined') {
 72     google = undefined;
 73     // important: do not use var, as "var google = undefined;" will overwrite
 74     //            the existing google variable value with undefined in IE8, IE7.
 75 }
 76 
 77 
 78 
 79 // Internet Explorer 8 and older does not support Array.indexOf,
 80 // so we define it here in that case
 81 // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
 82 if(!Array.prototype.indexOf) {
 83     Array.prototype.indexOf = function(obj){
 84         for(var i = 0; i < this.length; i++){
 85             if(this[i] == obj){
 86                 return i;
 87             }
 88         }
 89         return -1;
 90     }
 91 }
 92 
 93 // Internet Explorer 8 and older does not support Array.forEach,
 94 // so we define it here in that case
 95 // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
 96 if (!Array.prototype.forEach) {
 97     Array.prototype.forEach = function(fn, scope) {
 98         for(var i = 0, len = this.length; i < len; ++i) {
 99             fn.call(scope || this, this[i], i, this);
100         }
101     }
102 }
103 
104 
105 /**
106  * @constructor links.Timeline
107  * The timeline is a visualization chart to visualize events in time.
108  *
109  * The timeline is developed in javascript as a Google Visualization Chart.
110  *
111  * @param {Element} container   The DOM element in which the Timeline will
112  *                              be created. Normally a div element.
113  * @param {Object} options      A name/value map containing settings for the
114  *                              timeline. Optional.
115  */
116 links.Timeline = function(container, options) {
117     if (!container) {
118         // this call was probably only for inheritance, no constructor-code is required
119         return;
120     }
121 
122     // create variables and set default values
123     this.dom = {};
124     this.conversion = {};
125     this.eventParams = {}; // stores parameters for mouse events
126     this.groups = [];
127     this.groupIndexes = {};
128     this.items = [];
129     this.renderQueue = {
130         show: [],   // Items made visible but not yet added to DOM
131         hide: [],   // Items currently visible but not yet removed from DOM
132         update: []  // Items with changed data but not yet adjusted DOM
133     };
134     this.renderedItems = [];  // Items currently rendered in the DOM
135     this.clusterGenerator = new links.Timeline.ClusterGenerator(this);
136     this.currentClusters = [];
137     this.selection = undefined; // stores index and item which is currently selected
138 
139     this.listeners = {}; // event listener callbacks
140 
141     // Initialize sizes.
142     // Needed for IE (which gives an error when you try to set an undefined
143     // value in a style)
144     this.size = {
145         'actualHeight': 0,
146         'axis': {
147             'characterMajorHeight': 0,
148             'characterMajorWidth': 0,
149             'characterMinorHeight': 0,
150             'characterMinorWidth': 0,
151             'height': 0,
152             'labelMajorTop': 0,
153             'labelMinorTop': 0,
154             'line': 0,
155             'lineMajorWidth': 0,
156             'lineMinorHeight': 0,
157             'lineMinorTop': 0,
158             'lineMinorWidth': 0,
159             'top': 0
160         },
161         'contentHeight': 0,
162         'contentLeft': 0,
163         'contentWidth': 0,
164         'frameHeight': 0,
165         'frameWidth': 0,
166         'groupsLeft': 0,
167         'groupsWidth': 0,
168         'items': {
169             'top': 0
170         }
171     };
172 
173     this.dom.container = container;
174 
175     //
176     // Let's set the default options first
177     //
178     this.options = {
179         'width': "100%",
180         'height': "auto",
181         'minHeight': 0,        // minimal height in pixels
182         'groupMinHeight': 0,
183         'autoHeight': true,
184 
185         'eventMargin': 10,     // minimal margin between events
186         'eventMarginAxis': 20, // minimal margin between events and the axis
187         'dragAreaWidth': 10,   // pixels
188 
189         'min': undefined,
190         'max': undefined,
191         'zoomMin': 10,     // milliseconds
192         'zoomMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
193 
194         'moveable': true,
195         'zoomable': true,
196         'selectable': true,
197         'unselectable': true,
198         'editable': false,
199         'snapEvents': true,
200         'groupChangeable': true,
201         'timeChangeable': true,
202 
203         'showCurrentTime': true, // show a red bar displaying the current time
204         'showCustomTime': false, // show a blue, draggable bar displaying a custom time
205         'showMajorLabels': true,
206         'showMinorLabels': true,
207         'showNavigation': false,
208         'showButtonNew': false,
209         'groupsOnRight': false,
210         'groupsOrder' : true,
211         'axisOnTop': false,
212         'stackEvents': true,
213         'animate': true,
214         'animateZoom': true,
215         'cluster': false,
216         'clusterMaxItems': 5,
217         'style': 'box',
218         'customStackOrder': false, //a function(a,b) for determining stackorder amongst a group of items. Essentially a comparator, -ve value for "a before b" and vice versa
219         
220         // i18n: Timeline only has built-in English text per default. Include timeline-locales.js to support more localized text.
221         'locale': 'en',
222         'MONTHS': ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
223         'MONTHS_SHORT': ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
224         'DAYS': ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
225         'DAYS_SHORT': ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
226         'ZOOM_IN': "Zoom in",
227         'ZOOM_OUT': "Zoom out",
228         'MOVE_LEFT': "Move left",
229         'MOVE_RIGHT': "Move right",
230         'NEW': "New",
231         'CREATE_NEW_EVENT': "Create new event"
232     };
233     
234     //
235     // Now we can set the givenproperties
236     //
237     this.setOptions(options);
238 
239     this.clientTimeOffset = 0;    // difference between client time and the time
240     // set via Timeline.setCurrentTime()
241     var dom = this.dom;
242 
243     // remove all elements from the container element.
244     while (dom.container.hasChildNodes()) {
245         dom.container.removeChild(dom.container.firstChild);
246     }
247 
248     // create a step for drawing the axis
249     this.step = new links.Timeline.StepDate();
250 
251     // add standard item types
252     this.itemTypes = {
253         box:           links.Timeline.ItemBox,
254         range:         links.Timeline.ItemRange,
255         floatingRange: links.Timeline.ItemFloatingRange,
256         dot:           links.Timeline.ItemDot
257     };
258 
259     // initialize data
260     this.data = [];
261     this.firstDraw = true;
262 
263     // date interval must be initialized
264     this.setVisibleChartRange(undefined, undefined, false);
265 
266     // render for the first time
267     this.render();
268 
269     // fire the ready event
270     var me = this;
271     setTimeout(function () {
272         me.trigger('ready');
273     }, 0);
274 };
275 
276 
277 /**
278  * Main drawing logic. This is the function that needs to be called
279  * in the html page, to draw the timeline.
280  *
281  * A data table with the events must be provided, and an options table.
282  *
283  * @param {google.visualization.DataTable}      data
284  *                                 The data containing the events for the timeline.
285  *                                 Object DataTable is defined in
286  *                                 google.visualization.DataTable
287  * @param {Object} options         A name/value map containing settings for the
288  *                                 timeline. Optional. The use of options here
289  *                                 is deprecated. Pass timeline options in the
290  *                                 constructor or use setOptions()
291  */
292 links.Timeline.prototype.draw = function(data, options) {
293     if (options) {
294         console.log("WARNING: Passing options in draw() is deprecated. Pass options to the constructur or use setOptions() instead!");
295     }
296     this.setOptions(options);
297 
298     if (this.options.selectable) {
299         links.Timeline.addClassName(this.dom.frame, "timeline-selectable");
300     }
301 
302     // read the data
303     this.setData(data);
304 
305     // set timer range. this will also redraw the timeline
306     if (options && (options.start || options.end)) {
307         this.setVisibleChartRange(options.start, options.end);
308     }
309     else if (this.firstDraw) {
310         this.setVisibleChartRangeAuto();
311     }
312 
313     this.firstDraw = false;
314 };
315 
316 
317 /**
318  * Set options for the timeline.
319  * Timeline must be redrawn afterwards
320  * @param {Object} options A name/value map containing settings for the
321  *                                 timeline. Optional.
322  */
323 links.Timeline.prototype.setOptions = function(options) {
324     if (options) {
325         // retrieve parameter values
326         for (var i in options) {
327             if (options.hasOwnProperty(i)) {
328                 this.options[i] = options[i];
329             }
330         }
331 
332         // prepare i18n dependent on set locale
333         if (typeof links.locales !== 'undefined' && this.options.locale !== 'en') {
334             var localeOpts = links.locales[this.options.locale];
335             if(localeOpts) {
336                 for (var l in localeOpts) {
337                     if (localeOpts.hasOwnProperty(l)) {
338                         this.options[l] = localeOpts[l];
339                     }
340                 }
341             }
342         }
343 
344         // check for deprecated options
345         if (options.showButtonAdd != undefined) {
346             this.options.showButtonNew = options.showButtonAdd;
347             console.log('WARNING: Option showButtonAdd is deprecated. Use showButtonNew instead');
348         }
349         if (options.intervalMin != undefined) {
350             this.options.zoomMin = options.intervalMin;
351             console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead');
352         }
353         if (options.intervalMax != undefined) {
354             this.options.zoomMax = options.intervalMax;
355             console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead');
356         }
357 
358         if (options.scale && options.step) {
359             this.step.setScale(options.scale, options.step);
360         }
361     }
362 
363     // validate options
364     this.options.autoHeight = (this.options.height === "auto");
365 };
366 
367 /**
368  * Get options for the timeline.
369  *
370  * @return the options object
371  */
372 links.Timeline.prototype.getOptions = function() {
373     return this.options;
374 };
375 
376 /**
377  * Add new type of items
378  * @param {String} typeName  Name of new type
379  * @param {links.Timeline.Item} typeFactory Constructor of items
380  */
381 links.Timeline.prototype.addItemType = function (typeName, typeFactory) {
382     this.itemTypes[typeName] = typeFactory;
383 };
384 
385 /**
386  * Retrieve a map with the column indexes of the columns by column name.
387  * For example, the method returns the map
388  *     {
389  *         start: 0,
390  *         end: 1,
391  *         content: 2,
392  *         group: undefined,
393  *         className: undefined
394  *         editable: undefined
395  *         type: undefined
396  *     }
397  * @param {google.visualization.DataTable} dataTable
398  * @type {Object} map
399  */
400 links.Timeline.mapColumnIds = function (dataTable) {
401     var cols = {},
402         colCount = dataTable.getNumberOfColumns(),
403         allUndefined = true;
404 
405     // loop over the columns, and map the column id's to the column indexes
406     for (var col = 0; col < colCount; col++) {
407         var id = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
408         cols[id] = col;
409         if (id == 'start' || id == 'end' || id == 'content' || id == 'group' ||
410             id == 'className' || id == 'editable' || id == 'type') {
411             allUndefined = false;
412         }
413     }
414 
415     // if no labels or ids are defined, use the default mapping
416     // for start, end, content, group, className, editable, type
417     if (allUndefined) {
418         cols.start = 0;
419         cols.end = 1;
420         cols.content = 2;
421         if (colCount > 3) {cols.group = 3}
422         if (colCount > 4) {cols.className = 4}
423         if (colCount > 5) {cols.editable = 5}
424         if (colCount > 6) {cols.type = 6}
425     }
426 
427     return cols;
428 };
429 
430 /**
431  * Set data for the timeline
432  * @param {google.visualization.DataTable | Array} data
433  */
434 links.Timeline.prototype.setData = function(data) {
435     // unselect any previously selected item
436     this.unselectItem();
437 
438     if (!data) {
439         data = [];
440     }
441 
442     // clear all data
443     this.stackCancelAnimation();
444     this.clearItems();
445     this.data = data;
446     var items = this.items;
447     this.deleteGroups();
448 
449     if (google && google.visualization &&
450         data instanceof google.visualization.DataTable) {
451         // map the datatable columns
452         var cols = links.Timeline.mapColumnIds(data);
453 
454         // read DataTable
455         for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
456             items.push(this.createItem({
457                 'start':     ((cols.start != undefined)     ? data.getValue(row, cols.start)     : undefined),
458                 'end':       ((cols.end != undefined)       ? data.getValue(row, cols.end)       : undefined),
459                 'content':   ((cols.content != undefined)   ? data.getValue(row, cols.content)   : undefined),
460                 'group':     ((cols.group != undefined)     ? data.getValue(row, cols.group)     : undefined),
461                 'className': ((cols.className != undefined) ? data.getValue(row, cols.className) : undefined),
462                 'editable':  ((cols.editable != undefined)  ? data.getValue(row, cols.editable)  : undefined),
463                 'type':      ((cols.type != undefined)      ? data.getValue(row, cols.type)      : undefined)
464             }));
465         }
466     }
467     else if (links.Timeline.isArray(data)) {
468         // read JSON array
469         for (var row = 0, rows = data.length; row < rows; row++) {
470             var itemData = data[row];
471             var item = this.createItem(itemData);
472             items.push(item);
473         }
474     }
475     else {
476         throw "Unknown data type. DataTable or Array expected.";
477     }
478 
479     // prepare data for clustering, by filtering and sorting by type
480     if (this.options.cluster) {
481         this.clusterGenerator.setData(this.items);
482     }
483 
484     this.render({
485         animate: false
486     });
487 };
488 
489 /**
490  * Return the original data table.
491  * @return {google.visualization.DataTable | Array} data
492  */
493 links.Timeline.prototype.getData = function  () {
494     return this.data;
495 };
496 
497 
498 /**
499  * Update the original data with changed start, end or group.
500  *
501  * @param {Number} index
502  * @param {Object} values   An object containing some of the following parameters:
503  *                          {Date} start,
504  *                          {Date} end,
505  *                          {String} content,
506  *                          {String} group
507  */
508 links.Timeline.prototype.updateData = function  (index, values) {
509     var data = this.data,
510         prop;
511 
512     if (google && google.visualization &&
513         data instanceof google.visualization.DataTable) {
514         // update the original google DataTable
515         var missingRows = (index + 1) - data.getNumberOfRows();
516         if (missingRows > 0) {
517             data.addRows(missingRows);
518         }
519 
520         // map the column id's by name
521         var cols = links.Timeline.mapColumnIds(data);
522 
523         // merge all fields from the provided data into the current data
524         for (prop in values) {
525             if (values.hasOwnProperty(prop)) {
526                 var col = cols[prop];
527                 if (col == undefined) {
528                     // create new column
529                     var value = values[prop];
530                     var valueType = 'string';
531                     if (typeof(value) == 'number')       {valueType = 'number';}
532                     else if (typeof(value) == 'boolean') {valueType = 'boolean';}
533                     else if (value instanceof Date)      {valueType = 'datetime';}
534                     col = data.addColumn(valueType, prop);
535                 }
536                 data.setValue(index, col, values[prop]);
537 
538                 // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
539             }
540         }
541     }
542     else if (links.Timeline.isArray(data)) {
543         // update the original JSON table
544         var row = data[index];
545         if (row == undefined) {
546             row = {};
547             data[index] = row;
548         }
549 
550         // merge all fields from the provided data into the current data
551         for (prop in values) {
552             if (values.hasOwnProperty(prop)) {
553                 row[prop] = values[prop];
554 
555                 // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
556             }
557         }
558     }
559     else {
560         throw "Cannot update data, unknown type of data";
561     }
562 };
563 
564 /**
565  * Find the item index from a given HTML element
566  * If no item index is found, undefined is returned
567  * @param {Element} element
568  * @return {Number | undefined} index
569  */
570 links.Timeline.prototype.getItemIndex = function(element) {
571     var e = element,
572         dom = this.dom,
573         frame = dom.items.frame,
574         items = this.items,
575         index = undefined;
576 
577     // try to find the frame where the items are located in
578     while (e.parentNode && e.parentNode !== frame) {
579         e = e.parentNode;
580     }
581 
582     if (e.parentNode === frame) {
583         // yes! we have found the parent element of all items
584         // retrieve its id from the array with items
585         for (var i = 0, iMax = items.length; i < iMax; i++) {
586             if (items[i].dom === e) {
587                 index = i;
588                 break;
589             }
590         }
591     }
592 
593     return index;
594 };
595 
596 
597 /**
598  * Find the cluster index from a given HTML element
599  * If no cluster index is found, undefined is returned
600  * @param {Element} element
601  * @return {Number | undefined} index
602  */
603 links.Timeline.prototype.getClusterIndex = function(element) {
604     var e = element,
605         dom = this.dom,
606         frame = dom.items.frame,
607         clusters = this.clusters,
608         index = undefined;
609 
610     if (this.clusters) {
611         // try to find the frame where the clusters are located in
612         while (e.parentNode && e.parentNode !== frame) {
613             e = e.parentNode;
614         }
615 
616         if (e.parentNode === frame) {
617             // yes! we have found the parent element of all clusters
618             // retrieve its id from the array with clusters
619             for (var i = 0, iMax = clusters.length; i < iMax; i++) {
620                 if (clusters[i].dom === e) {
621                     index = i;
622                     break;
623                 }
624             }
625         }
626     }
627 
628     return index;
629 };
630 
631 /**
632  * Find all elements within the start and end range
633  * If no element is found, returns an empty array
634  * @param start time
635  * @param end time
636  * @return Array itemsInRange
637  */
638 links.Timeline.prototype.getVisibleItems = function  (start, end) {
639     var items = this.items;
640     var itemsInRange = [];
641 
642     if (items) {
643         for (var i = 0, iMax = items.length; i < iMax; i++) {
644             var item = items[i];
645             if (item.end) {
646                 // Time range object // NH use getLeft and getRight here
647                 if (start <= item.start && item.end <= end) {
648                     itemsInRange.push({"row": i});
649                 }
650             } else {
651                 // Point object
652                 if (start <= item.start && item.start <= end) {
653                     itemsInRange.push({"row": i});
654                 }
655             }
656         }
657     }
658 
659     //     var sel = [];
660     // if (this.selection) {
661     //     sel.push({"row": this.selection.index});
662     // }
663     // return sel;
664 
665     return itemsInRange;
666 };
667 
668 
669 /**
670  * Set a new size for the timeline
671  * @param {string} width   Width in pixels or percentage (for example "800px"
672  *                         or "50%")
673  * @param {string} height  Height in pixels or percentage  (for example "400px"
674  *                         or "30%")
675  */
676 links.Timeline.prototype.setSize = function(width, height) {
677     if (width) {
678         this.options.width = width;
679         this.dom.frame.style.width = width;
680     }
681     if (height) {
682         this.options.height = height;
683         this.options.autoHeight = (this.options.height === "auto");
684         if (height !==  "auto" ) {
685             this.dom.frame.style.height = height;
686         }
687     }
688 
689     this.render({
690         animate: false
691     });
692 };
693 
694 
695 /**
696  * Set a new value for the visible range int the timeline.
697  * Set start undefined to include everything from the earliest date to end.
698  * Set end undefined to include everything from start to the last date.
699  * Example usage:
700  *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
701  *                                    new Date("2010-09-13"));
702  * @param {Date}   start     The start date for the timeline. optional
703  * @param {Date}   end       The end date for the timeline. optional
704  * @param {boolean} redraw   Optional. If true (default) the Timeline is
705  *                           directly redrawn
706  */
707 links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
708     var range = {};
709     if (!start || !end) {
710         // retrieve the date range of the items
711         range = this.getDataRange(true);
712     }
713 
714     if (!start) {
715         if (end) {
716             if (range.min && range.min.valueOf() < end.valueOf()) {
717                 // start of the data
718                 start = range.min;
719             }
720             else {
721                 // 7 days before the end
722                 start = new Date(end.valueOf());
723                 start.setDate(start.getDate() - 7);
724             }
725         }
726         else {
727             // default of 3 days ago
728             start = new Date();
729             start.setDate(start.getDate() - 3);
730         }
731     }
732 
733     if (!end) {
734         if (range.max) {
735             // end of the data
736             end = range.max;
737         }
738         else {
739             // 7 days after start
740             end = new Date(start.valueOf());
741             end.setDate(end.getDate() + 7);
742         }
743     }
744 
745     // prevent start Date <= end Date
746     if (end <= start) {
747         end = new Date(start.valueOf());
748         end.setDate(end.getDate() + 7);
749     }
750 
751     // limit to the allowed range (don't let this do by applyRange,
752     // because that method will try to maintain the interval (end-start)
753     var min = this.options.min ? this.options.min : undefined; // date
754     if (min != undefined && start.valueOf() < min.valueOf()) {
755         start = new Date(min.valueOf()); // date
756     }
757     var max = this.options.max ? this.options.max : undefined; // date
758     if (max != undefined && end.valueOf() > max.valueOf()) {
759         end = new Date(max.valueOf()); // date
760     }
761 
762     this.applyRange(start, end);
763 
764     if (redraw == undefined || redraw == true) {
765         this.render({
766             animate: false
767         });  // TODO: optimize, no reflow needed
768     }
769     else {
770         this.recalcConversion();
771     }
772 };
773 
774 
775 /**
776  * Change the visible chart range such that all items become visible
777  */
778 links.Timeline.prototype.setVisibleChartRangeAuto = function() {
779     var range = this.getDataRange(true);
780     this.setVisibleChartRange(range.min, range.max);
781 };
782 
783 /**
784  * Adjust the visible range such that the current time is located in the center
785  * of the timeline
786  */
787 links.Timeline.prototype.setVisibleChartRangeNow = function() {
788     var now = new Date();
789 
790     var diff = (this.end.valueOf() - this.start.valueOf());
791 
792     var startNew = new Date(now.valueOf() - diff/2);
793     var endNew = new Date(startNew.valueOf() + diff);
794     this.setVisibleChartRange(startNew, endNew);
795 };
796 
797 
798 /**
799  * Retrieve the current visible range in the timeline.
800  * @return {Object} An object with start and end properties
801  */
802 links.Timeline.prototype.getVisibleChartRange = function() {
803     return {
804         'start': new Date(this.start.valueOf()),
805         'end': new Date(this.end.valueOf())
806     };
807 };
808 
809 /**
810  * Get the date range of the items.
811  * @param {boolean} [withMargin]  If true, 5% of whitespace is added to the
812  *                                left and right of the range. Default is false.
813  * @return {Object} range    An object with parameters min and max.
814  *                           - {Date} min is the lowest start date of the items
815  *                           - {Date} max is the highest start or end date of the items
816  *                           If no data is available, the values of min and max
817  *                           will be undefined
818  */
819 links.Timeline.prototype.getDataRange = function (withMargin) {
820     var items = this.items,
821         min = undefined, // number
822         max = undefined; // number
823 
824     if (items) {
825         for (var i = 0, iMax = items.length; i < iMax; i++) {
826             var item = items[i],
827                 start = item.start != undefined ? item.start.valueOf() : undefined,
828                 end   = item.end != undefined   ? item.end.valueOf() : start;
829 
830             if (start != undefined) {
831                 min = (min != undefined) ? Math.min(min.valueOf(), start.valueOf()) : start;
832             }
833 
834             if (end != undefined) {
835                 max = (max != undefined) ? Math.max(max.valueOf(), end.valueOf()) : end;
836             }
837         }
838     }
839 
840     if (min && max && withMargin) {
841         // zoom out 5% such that you have a little white space on the left and right
842         var diff = (max - min);
843         min = min - diff * 0.05;
844         max = max + diff * 0.05;
845     }
846 
847     return {
848         'min': min != undefined ? new Date(min) : undefined,
849         'max': max != undefined ? new Date(max) : undefined
850     };
851 };
852 
853 /**
854  * Re-render (reflow and repaint) all components of the Timeline: frame, axis,
855  * items, ...
856  * @param {Object} [options]  Available options:
857  *                            {boolean} renderTimesLeft   Number of times the
858  *                                                        render may be repeated
859  *                                                        5 times by default.
860  *                            {boolean} animate           takes options.animate
861  *                                                        as default value
862  */
863 links.Timeline.prototype.render = function(options) {
864     var frameResized = this.reflowFrame();
865     var axisResized = this.reflowAxis();
866     var groupsResized = this.reflowGroups();
867     var itemsResized = this.reflowItems();
868     var resized = (frameResized || axisResized || groupsResized || itemsResized);
869 
870     // TODO: only stackEvents/filterItems when resized or changed. (gives a bootstrap issue).
871     // if (resized) {
872     var animate = this.options.animate;
873     if (options && options.animate != undefined) {
874         animate = options.animate;
875     }
876 
877     this.recalcConversion();
878     this.clusterItems();
879     this.filterItems();
880     this.stackItems(animate);
881     this.recalcItems();
882 
883     // TODO: only repaint when resized or when filterItems or stackItems gave a change?
884     var needsReflow = this.repaint();
885 
886     // re-render once when needed (prevent endless re-render loop)
887     if (needsReflow) {
888         var renderTimesLeft = options ? options.renderTimesLeft : undefined;
889         if (renderTimesLeft == undefined) {
890             renderTimesLeft = 5;
891         }
892         if (renderTimesLeft > 0) {
893             this.render({
894                 'animate': options ? options.animate: undefined,
895                 'renderTimesLeft': (renderTimesLeft - 1)
896             });
897         }
898     }
899 };
900 
901 /**
902  * Repaint all components of the Timeline
903  * @return {boolean} needsReflow   Returns true if the DOM is changed such that
904  *                                 a reflow is needed.
905  */
906 links.Timeline.prototype.repaint = function() {
907     var frameNeedsReflow = this.repaintFrame();
908     var axisNeedsReflow  = this.repaintAxis();
909     var groupsNeedsReflow  = this.repaintGroups();
910     var itemsNeedsReflow = this.repaintItems();
911     this.repaintCurrentTime();
912     this.repaintCustomTime();
913 
914     return (frameNeedsReflow || axisNeedsReflow || groupsNeedsReflow || itemsNeedsReflow);
915 };
916 
917 /**
918  * Reflow the timeline frame
919  * @return {boolean} resized    Returns true if any of the frame elements
920  *                              have been resized.
921  */
922 links.Timeline.prototype.reflowFrame = function() {
923     var dom = this.dom,
924         options = this.options,
925         size = this.size,
926         resized = false;
927 
928     // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
929     var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
930         frameHeight = dom.frame ? dom.frame.clientHeight : 0;
931 
932     resized = resized || (size.frameWidth !== frameWidth);
933     resized = resized || (size.frameHeight !== frameHeight);
934     size.frameWidth = frameWidth;
935     size.frameHeight = frameHeight;
936 
937     return resized;
938 };
939 
940 /**
941  * repaint the Timeline frame
942  * @return {boolean} needsReflow   Returns true if the DOM is changed such that
943  *                                 a reflow is needed.
944  */
945 links.Timeline.prototype.repaintFrame = function() {
946     var needsReflow = false,
947         dom = this.dom,
948         options = this.options,
949         size = this.size;
950 
951     // main frame
952     if (!dom.frame) {
953         dom.frame = document.createElement("DIV");
954         dom.frame.className = "timeline-frame ui-widget ui-widget-content ui-corner-all";
955         dom.container.appendChild(dom.frame);
956         needsReflow = true;
957     }
958 
959     var height = options.autoHeight ?
960         (size.actualHeight + "px") :
961         (options.height || "100%");
962     var width  = options.width || "100%";
963     needsReflow = needsReflow || (dom.frame.style.height != height);
964     needsReflow = needsReflow || (dom.frame.style.width != width);
965     dom.frame.style.height = height;
966     dom.frame.style.width = width;
967 
968     // contents
969     if (!dom.content) {
970         // create content box where the axis and items will be created
971         dom.content = document.createElement("DIV");
972         dom.content.className = "timeline-content";
973         dom.frame.appendChild(dom.content);
974 
975         var timelines = document.createElement("DIV");
976         timelines.style.position = "absolute";
977         timelines.style.left = "0px";
978         timelines.style.top = "0px";
979         timelines.style.height = "100%";
980         timelines.style.width = "0px";
981         dom.content.appendChild(timelines);
982         dom.contentTimelines = timelines;
983 
984         var params = this.eventParams,
985             me = this;
986         if (!params.onMouseDown) {
987             params.onMouseDown = function (event) {me.onMouseDown(event);};
988             links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
989         }
990         if (!params.onTouchStart) {
991             params.onTouchStart = function (event) {me.onTouchStart(event);};
992             links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
993         }
994         if (!params.onMouseWheel) {
995             params.onMouseWheel = function (event) {me.onMouseWheel(event);};
996             links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
997         }
998         if (!params.onDblClick) {
999             params.onDblClick = function (event) {me.onDblClick(event);};
1000             links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
1001         }
1002 
1003         needsReflow = true;
1004     }
1005     dom.content.style.left = size.contentLeft + "px";
1006     dom.content.style.top = "0px";
1007     dom.content.style.width = size.contentWidth + "px";
1008     dom.content.style.height = size.frameHeight + "px";
1009 
1010     this.repaintNavigation();
1011 
1012     return needsReflow;
1013 };
1014 
1015 /**
1016  * Reflow the timeline axis. Calculate its height, width, positioning, etc...
1017  * @return {boolean} resized    returns true if the axis is resized
1018  */
1019 links.Timeline.prototype.reflowAxis = function() {
1020     var resized = false,
1021         dom = this.dom,
1022         options = this.options,
1023         size = this.size,
1024         axisDom = dom.axis;
1025 
1026     var characterMinorWidth  = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientWidth : 0,
1027         characterMinorHeight = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientHeight : 0,
1028         characterMajorWidth  = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientWidth : 0,
1029         characterMajorHeight = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientHeight : 0,
1030         axisHeight = (options.showMinorLabels ? characterMinorHeight : 0) +
1031             (options.showMajorLabels ? characterMajorHeight : 0);
1032 
1033     var axisTop  = options.axisOnTop ? 0 : size.frameHeight - axisHeight,
1034         axisLine = options.axisOnTop ? axisHeight : axisTop;
1035 
1036     resized = resized || (size.axis.top !== axisTop);
1037     resized = resized || (size.axis.line !== axisLine);
1038     resized = resized || (size.axis.height !== axisHeight);
1039     size.axis.top = axisTop;
1040     size.axis.line = axisLine;
1041     size.axis.height = axisHeight;
1042     size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine +
1043         (options.showMinorLabels ? characterMinorHeight : 0);
1044     size.axis.labelMinorTop = options.axisOnTop ?
1045         (options.showMajorLabels ? characterMajorHeight : 0) :
1046         axisLine;
1047     size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
1048     size.axis.lineMinorHeight = options.showMajorLabels ?
1049         size.frameHeight - characterMajorHeight:
1050         size.frameHeight;
1051     if (axisDom && axisDom.minorLines && axisDom.minorLines.length) {
1052         size.axis.lineMinorWidth = axisDom.minorLines[0].offsetWidth;
1053     }
1054     else {
1055         size.axis.lineMinorWidth = 1;
1056     }
1057     if (axisDom && axisDom.majorLines && axisDom.majorLines.length) {
1058         size.axis.lineMajorWidth = axisDom.majorLines[0].offsetWidth;
1059     }
1060     else {
1061         size.axis.lineMajorWidth = 1;
1062     }
1063 
1064     resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
1065     resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
1066     resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
1067     resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
1068     size.axis.characterMinorWidth  = characterMinorWidth;
1069     size.axis.characterMinorHeight = characterMinorHeight;
1070     size.axis.characterMajorWidth  = characterMajorWidth;
1071     size.axis.characterMajorHeight = characterMajorHeight;
1072 
1073     var contentHeight = Math.max(size.frameHeight - axisHeight, 0);
1074     size.contentLeft = options.groupsOnRight ? 0 : size.groupsWidth;
1075     size.contentWidth = Math.max(size.frameWidth - size.groupsWidth, 0);
1076     size.contentHeight = contentHeight;
1077 
1078     return resized;
1079 };
1080 
1081 /**
1082  * Redraw the timeline axis with minor and major labels
1083  * @return {boolean} needsReflow     Returns true if the DOM is changed such
1084  *                                   that a reflow is needed.
1085  */
1086 links.Timeline.prototype.repaintAxis = function() {
1087     var needsReflow = false,
1088         dom = this.dom,
1089         options = this.options,
1090         size = this.size,
1091         step = this.step;
1092 
1093     var axis = dom.axis;
1094     if (!axis) {
1095         axis = {};
1096         dom.axis = axis;
1097     }
1098     if (!size.axis.properties) {
1099         size.axis.properties = {};
1100     }
1101     if (!axis.minorTexts) {
1102         axis.minorTexts = [];
1103     }
1104     if (!axis.minorLines) {
1105         axis.minorLines = [];
1106     }
1107     if (!axis.majorTexts) {
1108         axis.majorTexts = [];
1109     }
1110     if (!axis.majorLines) {
1111         axis.majorLines = [];
1112     }
1113 
1114     if (!axis.frame) {
1115         axis.frame = document.createElement("DIV");
1116         axis.frame.style.position = "absolute";
1117         axis.frame.style.left = "0px";
1118         axis.frame.style.top = "0px";
1119         dom.content.appendChild(axis.frame);
1120     }
1121 
1122     // take axis offline
1123     dom.content.removeChild(axis.frame);
1124 
1125     axis.frame.style.width = (size.contentWidth) + "px";
1126     axis.frame.style.height = (size.axis.height) + "px";
1127 
1128     // the drawn axis is more wide than the actual visual part, such that
1129     // the axis can be dragged without having to redraw it each time again.
1130     var start = this.screenToTime(0);
1131     var end = this.screenToTime(size.contentWidth);
1132 
1133     // calculate minimum step (in milliseconds) based on character size
1134     if (size.axis.characterMinorWidth) {
1135         this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6) -
1136             this.screenToTime(0);
1137 
1138         step.setRange(start, end, this.minimumStep);
1139     }
1140 
1141     var charsNeedsReflow = this.repaintAxisCharacters();
1142     needsReflow = needsReflow || charsNeedsReflow;
1143 
1144     // The current labels on the axis will be re-used (much better performance),
1145     // therefore, the repaintAxis method uses the mechanism with
1146     // repaintAxisStartOverwriting, repaintAxisEndOverwriting, and
1147     // this.size.axis.properties is used.
1148     this.repaintAxisStartOverwriting();
1149 
1150     step.start();
1151     var xFirstMajorLabel = undefined;
1152     var max = 0;
1153     while (!step.end() && max < 1000) {
1154         max++;
1155         var cur = step.getCurrent(),
1156             x = this.timeToScreen(cur),
1157             isMajor = step.isMajor();
1158 
1159         if (options.showMinorLabels) {
1160             this.repaintAxisMinorText(x, step.getLabelMinor(options));
1161         }
1162 
1163         if (isMajor && options.showMajorLabels) {
1164             if (x > 0) {
1165                 if (xFirstMajorLabel == undefined) {
1166                     xFirstMajorLabel = x;
1167                 }
1168                 this.repaintAxisMajorText(x, step.getLabelMajor(options));
1169             }
1170             this.repaintAxisMajorLine(x);
1171         }
1172         else {
1173             this.repaintAxisMinorLine(x);
1174         }
1175 
1176         step.next();
1177     }
1178 
1179     // create a major label on the left when needed
1180     if (options.showMajorLabels) {
1181         var leftTime = this.screenToTime(0),
1182             leftText = this.step.getLabelMajor(options, leftTime),
1183             width = leftText.length * size.axis.characterMajorWidth + 10; // upper bound estimation
1184 
1185         if (xFirstMajorLabel == undefined || width < xFirstMajorLabel) {
1186             this.repaintAxisMajorText(0, leftText, leftTime);
1187         }
1188     }
1189 
1190     // cleanup left over labels
1191     this.repaintAxisEndOverwriting();
1192 
1193     this.repaintAxisHorizontal();
1194 
1195     // put axis online
1196     dom.content.insertBefore(axis.frame, dom.content.firstChild);
1197 
1198     return needsReflow;
1199 };
1200 
1201 /**
1202  * Create characters used to determine the size of text on the axis
1203  * @return {boolean} needsReflow   Returns true if the DOM is changed such that
1204  *                                 a reflow is needed.
1205  */
1206 links.Timeline.prototype.repaintAxisCharacters = function () {
1207     // calculate the width and height of a single character
1208     // this is used to calculate the step size, and also the positioning of the
1209     // axis
1210     var needsReflow = false,
1211         dom = this.dom,
1212         axis = dom.axis,
1213         text;
1214 
1215     if (!axis.characterMinor) {
1216         text = document.createTextNode("0");
1217         var characterMinor = document.createElement("DIV");
1218         characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
1219         characterMinor.appendChild(text);
1220         characterMinor.style.position = "absolute";
1221         characterMinor.style.visibility = "hidden";
1222         characterMinor.style.paddingLeft = "0px";
1223         characterMinor.style.paddingRight = "0px";
1224         axis.frame.appendChild(characterMinor);
1225 
1226         axis.characterMinor = characterMinor;
1227         needsReflow = true;
1228     }
1229 
1230     if (!axis.characterMajor) {
1231         text = document.createTextNode("0");
1232         var characterMajor = document.createElement("DIV");
1233         characterMajor.className = "timeline-axis-text timeline-axis-text-major";
1234         characterMajor.appendChild(text);
1235         characterMajor.style.position = "absolute";
1236         characterMajor.style.visibility = "hidden";
1237         characterMajor.style.paddingLeft = "0px";
1238         characterMajor.style.paddingRight = "0px";
1239         axis.frame.appendChild(characterMajor);
1240 
1241         axis.characterMajor = characterMajor;
1242         needsReflow = true;
1243     }
1244 
1245     return needsReflow;
1246 };
1247 
1248 /**
1249  * Initialize redraw of the axis. All existing labels and lines will be
1250  * overwritten and reused.
1251  */
1252 links.Timeline.prototype.repaintAxisStartOverwriting = function () {
1253     var properties = this.size.axis.properties;
1254 
1255     properties.minorTextNum = 0;
1256     properties.minorLineNum = 0;
1257     properties.majorTextNum = 0;
1258     properties.majorLineNum = 0;
1259 };
1260 
1261 /**
1262  * End of overwriting HTML DOM elements of the axis.
1263  * remaining elements will be removed
1264  */
1265 links.Timeline.prototype.repaintAxisEndOverwriting = function () {
1266     var dom = this.dom,
1267         props = this.size.axis.properties,
1268         frame = this.dom.axis.frame,
1269         num;
1270 
1271     // remove leftovers
1272     var minorTexts = dom.axis.minorTexts;
1273     num = props.minorTextNum;
1274     while (minorTexts.length > num) {
1275         var minorText = minorTexts[num];
1276         frame.removeChild(minorText);
1277         minorTexts.splice(num, 1);
1278     }
1279 
1280     var minorLines = dom.axis.minorLines;
1281     num = props.minorLineNum;
1282     while (minorLines.length > num) {
1283         var minorLine = minorLines[num];
1284         frame.removeChild(minorLine);
1285         minorLines.splice(num, 1);
1286     }
1287 
1288     var majorTexts = dom.axis.majorTexts;
1289     num = props.majorTextNum;
1290     while (majorTexts.length > num) {
1291         var majorText = majorTexts[num];
1292         frame.removeChild(majorText);
1293         majorTexts.splice(num, 1);
1294     }
1295 
1296     var majorLines = dom.axis.majorLines;
1297     num = props.majorLineNum;
1298     while (majorLines.length > num) {
1299         var majorLine = majorLines[num];
1300         frame.removeChild(majorLine);
1301         majorLines.splice(num, 1);
1302     }
1303 };
1304 
1305 /**
1306  * Repaint the horizontal line and background of the axis
1307  */
1308 links.Timeline.prototype.repaintAxisHorizontal = function() {
1309     var axis = this.dom.axis,
1310         size = this.size,
1311         options = this.options;
1312 
1313     // line behind all axis elements (possibly having a background color)
1314     var hasAxis = (options.showMinorLabels || options.showMajorLabels);
1315     if (hasAxis) {
1316         if (!axis.backgroundLine) {
1317             // create the axis line background (for a background color or so)
1318             var backgroundLine = document.createElement("DIV");
1319             backgroundLine.className = "timeline-axis";
1320             backgroundLine.style.position = "absolute";
1321             backgroundLine.style.left = "0px";
1322             backgroundLine.style.width = "100%";
1323             backgroundLine.style.border = "none";
1324             axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);
1325 
1326             axis.backgroundLine = backgroundLine;
1327         }
1328 
1329         if (axis.backgroundLine) {
1330             axis.backgroundLine.style.top = size.axis.top + "px";
1331             axis.backgroundLine.style.height = size.axis.height + "px";
1332         }
1333     }
1334     else {
1335         if (axis.backgroundLine) {
1336             axis.frame.removeChild(axis.backgroundLine);
1337             delete axis.backgroundLine;
1338         }
1339     }
1340 
1341     // line before all axis elements
1342     if (hasAxis) {
1343         if (axis.line) {
1344             // put this line at the end of all childs
1345             var line = axis.frame.removeChild(axis.line);
1346             axis.frame.appendChild(line);
1347         }
1348         else {
1349             // make the axis line
1350             var line = document.createElement("DIV");
1351             line.className = "timeline-axis";
1352             line.style.position = "absolute";
1353             line.style.left = "0px";
1354             line.style.width = "100%";
1355             line.style.height = "0px";
1356             axis.frame.appendChild(line);
1357 
1358             axis.line = line;
1359         }
1360 
1361         axis.line.style.top = size.axis.line + "px";
1362     }
1363     else {
1364         if (axis.line && axis.line.parentElement) {
1365             axis.frame.removeChild(axis.line);
1366             delete axis.line;
1367         }
1368     }
1369 };
1370 
1371 /**
1372  * Create a minor label for the axis at position x
1373  * @param {Number} x
1374  * @param {String} text
1375  */
1376 links.Timeline.prototype.repaintAxisMinorText = function (x, text) {
1377     var size = this.size,
1378         dom = this.dom,
1379         props = size.axis.properties,
1380         frame = dom.axis.frame,
1381         minorTexts = dom.axis.minorTexts,
1382         index = props.minorTextNum,
1383         label;
1384 
1385     if (index < minorTexts.length) {
1386         label = minorTexts[index]
1387     }
1388     else {
1389         // create new label
1390         var content = document.createTextNode("");
1391         label = document.createElement("DIV");
1392         label.appendChild(content);
1393         label.className = "timeline-axis-text timeline-axis-text-minor";
1394         label.style.position = "absolute";
1395 
1396         frame.appendChild(label);
1397 
1398         minorTexts.push(label);
1399     }
1400 
1401     label.childNodes[0].nodeValue = text;
1402     label.style.left = x + "px";
1403     label.style.top  = size.axis.labelMinorTop + "px";
1404     //label.title = title;  // TODO: this is a heavy operation
1405 
1406     props.minorTextNum++;
1407 };
1408 
1409 /**
1410  * Create a minor line for the axis at position x
1411  * @param {Number} x
1412  */
1413 links.Timeline.prototype.repaintAxisMinorLine = function (x) {
1414     var axis = this.size.axis,
1415         dom = this.dom,
1416         props = axis.properties,
1417         frame = dom.axis.frame,
1418         minorLines = dom.axis.minorLines,
1419         index = props.minorLineNum,
1420         line;
1421 
1422     if (index < minorLines.length) {
1423         line = minorLines[index];
1424     }
1425     else {
1426         // create vertical line
1427         line = document.createElement("DIV");
1428         line.className = "timeline-axis-grid timeline-axis-grid-minor";
1429         line.style.position = "absolute";
1430         line.style.width = "0px";
1431 
1432         frame.appendChild(line);
1433         minorLines.push(line);
1434     }
1435 
1436     line.style.top = axis.lineMinorTop + "px";
1437     line.style.height = axis.lineMinorHeight + "px";
1438     line.style.left = (x - axis.lineMinorWidth/2) + "px";
1439 
1440     props.minorLineNum++;
1441 };
1442 
1443 /**
1444  * Create a Major label for the axis at position x
1445  * @param {Number} x
1446  * @param {String} text
1447  */
1448 links.Timeline.prototype.repaintAxisMajorText = function (x, text) {
1449     var size = this.size,
1450         props = size.axis.properties,
1451         frame = this.dom.axis.frame,
1452         majorTexts = this.dom.axis.majorTexts,
1453         index = props.majorTextNum,
1454         label;
1455 
1456     if (index < majorTexts.length) {
1457         label = majorTexts[index];
1458     }
1459     else {
1460         // create label
1461         var content = document.createTextNode(text);
1462         label = document.createElement("DIV");
1463         label.className = "timeline-axis-text timeline-axis-text-major";
1464         label.appendChild(content);
1465         label.style.position = "absolute";
1466         label.style.top = "0px";
1467 
1468         frame.appendChild(label);
1469         majorTexts.push(label);
1470     }
1471 
1472     label.childNodes[0].nodeValue = text;
1473     label.style.top = size.axis.labelMajorTop + "px";
1474     label.style.left = x + "px";
1475     //label.title = title; // TODO: this is a heavy operation
1476 
1477     props.majorTextNum ++;
1478 };
1479 
1480 /**
1481  * Create a Major line for the axis at position x
1482  * @param {Number} x
1483  */
1484 links.Timeline.prototype.repaintAxisMajorLine = function (x) {
1485     var size = this.size,
1486         props = size.axis.properties,
1487         axis = this.size.axis,
1488         frame = this.dom.axis.frame,
1489         majorLines = this.dom.axis.majorLines,
1490         index = props.majorLineNum,
1491         line;
1492 
1493     if (index < majorLines.length) {
1494         line = majorLines[index];
1495     }
1496     else {
1497         // create vertical line
1498         line = document.createElement("DIV");
1499         line.className = "timeline-axis-grid timeline-axis-grid-major";
1500         line.style.position = "absolute";
1501         line.style.top = "0px";
1502         line.style.width = "0px";
1503 
1504         frame.appendChild(line);
1505         majorLines.push(line);
1506     }
1507 
1508     line.style.left = (x - axis.lineMajorWidth/2) + "px";
1509     line.style.height = size.frameHeight + "px";
1510 
1511     props.majorLineNum ++;
1512 };
1513 
1514 /**
1515  * Reflow all items, retrieve their actual size
1516  * @return {boolean} resized    returns true if any of the items is resized
1517  */
1518 links.Timeline.prototype.reflowItems = function() {
1519     var resized = false,
1520         i,
1521         iMax,
1522         group,
1523         groups = this.groups,
1524         renderedItems = this.renderedItems;
1525 
1526     if (groups) { // TODO: need to check if labels exists?
1527         // loop through all groups to reset the items height
1528         groups.forEach(function (group) {
1529             group.itemsHeight = 0;
1530         });
1531     }
1532 
1533     // loop through the width and height of all visible items
1534     for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
1535         var item = renderedItems[i],
1536             domItem = item.dom;
1537         group = item.group;
1538 
1539         if (domItem) {
1540             // TODO: move updating width and height into item.reflow
1541             var width = domItem ? domItem.clientWidth : 0;
1542             var height = domItem ? domItem.clientHeight : 0;
1543             resized = resized || (item.width != width);
1544             resized = resized || (item.height != height);
1545             item.width = width;
1546             item.height = height;
1547             //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
1548             item.reflow();
1549         }
1550 
1551         if (group) {
1552             group.itemsHeight = Math.max(this.options.groupMinHeight,group.itemsHeight ?
1553                 Math.max(group.itemsHeight, item.height) :
1554                 item.height);
1555         }
1556     }
1557 
1558     return resized;
1559 };
1560 
1561 /**
1562  * Recalculate item properties:
1563  * - the height of each group.
1564  * - the actualHeight, from the stacked items or the sum of the group heights
1565  * @return {boolean} resized    returns true if any of the items properties is
1566  *                              changed
1567  */
1568 links.Timeline.prototype.recalcItems = function () {
1569     var resized = false,
1570         i,
1571         iMax,
1572         item,
1573         finalItem,
1574         finalItems,
1575         group,
1576         groups = this.groups,
1577         size = this.size,
1578         options = this.options,
1579         renderedItems = this.renderedItems;
1580 
1581     var actualHeight = 0;
1582     if (groups.length == 0) {
1583         // calculate actual height of the timeline when there are no groups
1584         // but stacked items
1585         if (options.autoHeight || options.cluster) {
1586             var min = 0,
1587                 max = 0;
1588 
1589             if (this.stack && this.stack.finalItems) {
1590                 // adjust the offset of all finalItems when the actualHeight has been changed
1591                 finalItems = this.stack.finalItems;
1592                 finalItem = finalItems[0];
1593                 if (finalItem && finalItem.top) {
1594                     min = finalItem.top;
1595                     max = finalItem.top + finalItem.height;
1596                 }
1597                 for (i = 1, iMax = finalItems.length; i < iMax; i++) {
1598                     finalItem = finalItems[i];
1599                     min = Math.min(min, finalItem.top);
1600                     max = Math.max(max, finalItem.top + finalItem.height);
1601                 }
1602             }
1603             else {
1604                 item = renderedItems[0];
1605                 if (item && item.top) {
1606                     min = item.top;
1607                     max = item.top + item.height;
1608                 }
1609                 for (i = 1, iMax = renderedItems.length; i < iMax; i++) {
1610                     item = renderedItems[i];
1611                     if (item.top) {
1612                         min = Math.min(min, item.top);
1613                         max = Math.max(max, (item.top + item.height));
1614                     }
1615                 }
1616             }
1617 
1618             actualHeight = (max - min) + 2 * options.eventMarginAxis + size.axis.height;
1619             if (actualHeight < options.minHeight) {
1620                 actualHeight = options.minHeight;
1621             }
1622 
1623             if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
1624                 // adjust the offset of all items when the actualHeight has been changed
1625                 var diff = actualHeight - size.actualHeight;
1626                 if (this.stack && this.stack.finalItems) {
1627                     finalItems = this.stack.finalItems;
1628                     for (i = 0, iMax = finalItems.length; i < iMax; i++) {
1629                         finalItems[i].top += diff;
1630                         finalItems[i].item.top += diff;
1631                     }
1632                 }
1633                 else {
1634                     for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
1635                         renderedItems[i].top += diff;
1636                     }
1637                 }
1638             }
1639         }
1640     }
1641     else {
1642         // loop through all groups to get the height of each group, and the
1643         // total height
1644         actualHeight = size.axis.height + 2 * options.eventMarginAxis;
1645         for (i = 0, iMax = groups.length; i < iMax; i++) {
1646             group = groups[i];
1647 
1648             //
1649             // TODO: Do we want to apply a max height? how ?
1650             //
1651             var groupHeight = group.itemsHeight;
1652             resized = resized || (groupHeight != group.height);
1653             group.height = Math.max(groupHeight, options.groupMinHeight);
1654 
1655             actualHeight += groups[i].height + options.eventMargin;
1656         }
1657 
1658         // calculate top positions of the group labels and lines
1659         var eventMargin = options.eventMargin,
1660             top = options.axisOnTop ?
1661                 options.eventMarginAxis + eventMargin/2 :
1662                 size.contentHeight - options.eventMarginAxis + eventMargin/ 2,
1663             axisHeight = size.axis.height;
1664 
1665         for (i = 0, iMax = groups.length; i < iMax; i++) {
1666             group = groups[i];
1667             if (options.axisOnTop) {
1668                 group.top = top + axisHeight;
1669                 group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
1670                 group.lineTop = top + axisHeight + group.height + eventMargin/2;
1671                 top += group.height + eventMargin;
1672             }
1673             else {
1674                 top -= group.height + eventMargin;
1675                 group.top = top;
1676                 group.labelTop = top + (group.height - group.labelHeight) / 2;
1677                 group.lineTop = top - eventMargin/2;
1678             }
1679         }
1680 
1681         resized = true;
1682     }
1683 
1684     if (actualHeight < options.minHeight) {
1685         actualHeight = options.minHeight;
1686     }
1687     resized = resized || (actualHeight != size.actualHeight);
1688     size.actualHeight = actualHeight;
1689 
1690     return resized;
1691 };
1692 
1693 /**
1694  * This method clears the (internal) array this.items in a safe way: neatly
1695  * cleaning up the DOM, and accompanying arrays this.renderedItems and
1696  * the created clusters.
1697  */
1698 links.Timeline.prototype.clearItems = function() {
1699     // add all visible items to the list to be hidden
1700     var hideItems = this.renderQueue.hide;
1701     this.renderedItems.forEach(function (item) {
1702         hideItems.push(item);
1703     });
1704 
1705     // clear the cluster generator
1706     this.clusterGenerator.clear();
1707 
1708     // actually clear the items
1709     this.items = [];
1710 };
1711 
1712 /**
1713  * Repaint all items
1714  * @return {boolean} needsReflow   Returns true if the DOM is changed such that
1715  *                                 a reflow is needed.
1716  */
1717 links.Timeline.prototype.repaintItems = function() {
1718     var i, iMax, item, index;
1719 
1720     var needsReflow = false,
1721         dom = this.dom,
1722         size = this.size,
1723         timeline = this,
1724         renderedItems = this.renderedItems;
1725 
1726     if (!dom.items) {
1727         dom.items = {};
1728     }
1729 
1730     // draw the frame containing the items
1731     var frame = dom.items.frame;
1732     if (!frame) {
1733         frame = document.createElement("DIV");
1734         frame.style.position = "relative";
1735         dom.content.appendChild(frame);
1736         dom.items.frame = frame;
1737     }
1738 
1739     frame.style.left = "0px";
1740     frame.style.top = size.items.top + "px";
1741     frame.style.height = "0px";
1742 
1743     // Take frame offline (for faster manipulation of the DOM)
1744     dom.content.removeChild(frame);
1745 
1746     // process the render queue with changes
1747     var queue = this.renderQueue;
1748     var newImageUrls = [];
1749     needsReflow = needsReflow ||
1750         (queue.show.length > 0) ||
1751         (queue.update.length > 0) ||
1752         (queue.hide.length > 0);   // TODO: reflow needed on hide of items?
1753 
1754     while (item = queue.show.shift()) {
1755         item.showDOM(frame);
1756         item.getImageUrls(newImageUrls);
1757         renderedItems.push(item);
1758     }
1759     while (item = queue.update.shift()) {
1760         item.updateDOM(frame);
1761         item.getImageUrls(newImageUrls);
1762         index = this.renderedItems.indexOf(item);
1763         if (index == -1) {
1764             renderedItems.push(item);
1765         }
1766     }
1767     while (item = queue.hide.shift()) {
1768         item.hideDOM(frame);
1769         index = this.renderedItems.indexOf(item);
1770         if (index != -1) {
1771             renderedItems.splice(index, 1);
1772         }
1773     }
1774 
1775     // reposition all visible items
1776     renderedItems.forEach(function (item) {
1777         item.updatePosition(timeline);
1778     });
1779 
1780     // redraw the delete button and dragareas of the selected item (if any)
1781     this.repaintDeleteButton();
1782     this.repaintDragAreas();
1783 
1784     // put frame online again
1785     dom.content.appendChild(frame);
1786 
1787     if (newImageUrls.length) {
1788         // retrieve all image sources from the items, and set a callback once
1789         // all images are retrieved
1790         var callback = function () {
1791             timeline.render();
1792         };
1793         var sendCallbackWhenAlreadyLoaded = false;
1794         links.imageloader.loadAll(newImageUrls, callback, sendCallbackWhenAlreadyLoaded);
1795     }
1796 
1797     return needsReflow;
1798 };
1799 
1800 /**
1801  * Reflow the size of the groups
1802  * @return {boolean} resized    Returns true if any of the frame elements
1803  *                              have been resized.
1804  */
1805 links.Timeline.prototype.reflowGroups = function() {
1806     var resized = false,
1807         options = this.options,
1808         size = this.size,
1809         dom = this.dom;
1810 
1811     // calculate the groups width and height
1812     // TODO: only update when data is changed! -> use an updateSeq
1813     var groupsWidth = 0;
1814 
1815     // loop through all groups to get the labels width and height
1816     var groups = this.groups;
1817     var labels = this.dom.groups ? this.dom.groups.labels : [];
1818     for (var i = 0, iMax = groups.length; i < iMax; i++) {
1819         var group = groups[i];
1820         var label = labels[i];
1821         group.labelWidth  = label ? label.clientWidth : 0;
1822         group.labelHeight = label ? label.clientHeight : 0;
1823         group.width = group.labelWidth;  // TODO: group.width is redundant with labelWidth
1824 
1825         groupsWidth = Math.max(groupsWidth, group.width);
1826     }
1827 
1828     // limit groupsWidth to the groups width in the options
1829     if (options.groupsWidth !== undefined) {
1830         groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
1831     }
1832 
1833     // compensate for the border width. TODO: calculate the real border width
1834     groupsWidth += 1;
1835 
1836     var groupsLeft = options.groupsOnRight ? size.frameWidth - groupsWidth : 0;
1837     resized = resized || (size.groupsWidth !== groupsWidth);
1838     resized = resized || (size.groupsLeft !== groupsLeft);
1839     size.groupsWidth = groupsWidth;
1840     size.groupsLeft = groupsLeft;
1841 
1842     return resized;
1843 };
1844 
1845 /**
1846  * Redraw the group labels
1847  */
1848 links.Timeline.prototype.repaintGroups = function() {
1849     var dom = this.dom,
1850         timeline = this,
1851         options = this.options,
1852         size = this.size,
1853         groups = this.groups;
1854 
1855     if (dom.groups === undefined) {
1856         dom.groups = {};
1857     }
1858 
1859     var labels = dom.groups.labels;
1860     if (!labels) {
1861         labels = [];
1862         dom.groups.labels = labels;
1863     }
1864     var labelLines = dom.groups.labelLines;
1865     if (!labelLines) {
1866         labelLines = [];
1867         dom.groups.labelLines = labelLines;
1868     }
1869     var itemLines = dom.groups.itemLines;
1870     if (!itemLines) {
1871         itemLines = [];
1872         dom.groups.itemLines = itemLines;
1873     }
1874 
1875     // create the frame for holding the groups
1876     var frame = dom.groups.frame;
1877     if (!frame) {
1878         frame =  document.createElement("DIV");
1879         frame.className = "timeline-groups-axis";
1880         frame.style.position = "absolute";
1881         frame.style.overflow = "hidden";
1882         frame.style.top = "0px";
1883         frame.style.height = "100%";
1884 
1885         dom.frame.appendChild(frame);
1886         dom.groups.frame = frame;
1887     }
1888 
1889     frame.style.left = size.groupsLeft + "px";
1890     frame.style.width = (options.groupsWidth !== undefined) ?
1891         options.groupsWidth :
1892         size.groupsWidth + "px";
1893 
1894     // hide groups axis when there are no groups
1895     if (groups.length == 0) {
1896         frame.style.display = 'none';
1897     }
1898     else {
1899         frame.style.display = '';
1900     }
1901 
1902     // TODO: only create/update groups when data is changed.
1903 
1904     // create the items
1905     var current = labels.length,
1906         needed = groups.length;
1907 
1908     // overwrite existing group labels
1909     for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
1910         var group = groups[i];
1911         var label = labels[i];
1912         label.innerHTML = this.getGroupName(group);
1913         label.style.display = '';
1914     }
1915 
1916     // append new items when needed
1917     for (var i = current; i < needed; i++) {
1918         var group = groups[i];
1919 
1920         // create text label
1921         var label = document.createElement("DIV");
1922         label.className = "timeline-groups-text";
1923         label.style.position = "absolute";
1924         if (options.groupsWidth === undefined) {
1925             label.style.whiteSpace = "nowrap";
1926         }
1927         label.innerHTML = this.getGroupName(group);
1928         frame.appendChild(label);
1929         labels[i] = label;
1930 
1931         // create the grid line between the group labels
1932         var labelLine = document.createElement("DIV");
1933         labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1934         labelLine.style.position = "absolute";
1935         labelLine.style.left = "0px";
1936         labelLine.style.width = "100%";
1937         labelLine.style.height = "0px";
1938         labelLine.style.borderTopStyle = "solid";
1939         frame.appendChild(labelLine);
1940         labelLines[i] = labelLine;
1941 
1942         // create the grid line between the items
1943         var itemLine = document.createElement("DIV");
1944         itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1945         itemLine.style.position = "absolute";
1946         itemLine.style.left = "0px";
1947         itemLine.style.width = "100%";
1948         itemLine.style.height = "0px";
1949         itemLine.style.borderTopStyle = "solid";
1950         dom.content.insertBefore(itemLine, dom.content.firstChild);
1951         itemLines[i] = itemLine;
1952     }
1953 
1954     // remove redundant items from the DOM when needed
1955     for (var i = needed; i < current; i++) {
1956         var label = labels[i],
1957             labelLine = labelLines[i],
1958             itemLine = itemLines[i];
1959 
1960         frame.removeChild(label);
1961         frame.removeChild(labelLine);
1962         dom.content.removeChild(itemLine);
1963     }
1964     labels.splice(needed, current - needed);
1965     labelLines.splice(needed, current - needed);
1966     itemLines.splice(needed, current - needed);
1967 
1968     links.Timeline.addClassName(frame, options.groupsOnRight ? 'timeline-groups-axis-onright' : 'timeline-groups-axis-onleft');
1969 
1970     // position the groups
1971     for (var i = 0, iMax = groups.length; i < iMax; i++) {
1972         var group = groups[i],
1973             label = labels[i],
1974             labelLine = labelLines[i],
1975             itemLine = itemLines[i];
1976 
1977         label.style.top = group.labelTop + "px";
1978         labelLine.style.top = group.lineTop + "px";
1979         itemLine.style.top = group.lineTop + "px";
1980         itemLine.style.width = size.contentWidth + "px";
1981     }
1982 
1983     if (!dom.groups.background) {
1984         // create the axis grid line background
1985         var background = document.createElement("DIV");
1986         background.className = "timeline-axis";
1987         background.style.position = "absolute";
1988         background.style.left = "0px";
1989         background.style.width = "100%";
1990         background.style.border = "none";
1991 
1992         frame.appendChild(background);
1993         dom.groups.background = background;
1994     }
1995     dom.groups.background.style.top = size.axis.top + 'px';
1996     dom.groups.background.style.height = size.axis.height + 'px';
1997 
1998     if (!dom.groups.line) {
1999         // create the axis grid line
2000         var line = document.createElement("DIV");
2001         line.className = "timeline-axis";
2002         line.style.position = "absolute";
2003         line.style.left = "0px";
2004         line.style.width = "100%";
2005         line.style.height = "0px";
2006 
2007         frame.appendChild(line);
2008         dom.groups.line = line;
2009     }
2010     dom.groups.line.style.top = size.axis.line + 'px';
2011 
2012     // create a callback when there are images which are not yet loaded
2013     // TODO: more efficiently load images in the groups
2014     if (dom.groups.frame && groups.length) {
2015         var imageUrls = [];
2016         links.imageloader.filterImageUrls(dom.groups.frame, imageUrls);
2017         if (imageUrls.length) {
2018             // retrieve all image sources from the items, and set a callback once
2019             // all images are retrieved
2020             var callback = function () {
2021                 timeline.render();
2022             };
2023             var sendCallbackWhenAlreadyLoaded = false;
2024             links.imageloader.loadAll(imageUrls, callback, sendCallbackWhenAlreadyLoaded);
2025         }
2026     }
2027 };
2028 
2029 
2030 /**
2031  * Redraw the current time bar
2032  */
2033 links.Timeline.prototype.repaintCurrentTime = function() {
2034     var options = this.options,
2035         dom = this.dom,
2036         size = this.size;
2037 
2038     if (!options.showCurrentTime) {
2039         if (dom.currentTime) {
2040             dom.contentTimelines.removeChild(dom.currentTime);
2041             delete dom.currentTime;
2042         }
2043 
2044         return;
2045     }
2046 
2047     if (!dom.currentTime) {
2048         // create the current time bar
2049         var currentTime = document.createElement("DIV");
2050         currentTime.className = "timeline-currenttime";
2051         currentTime.style.position = "absolute";
2052         currentTime.style.top = "0px";
2053         currentTime.style.height = "100%";
2054 
2055         dom.contentTimelines.appendChild(currentTime);
2056         dom.currentTime = currentTime;
2057     }
2058 
2059     var now = new Date();
2060     var nowOffset = new Date(now.valueOf() + this.clientTimeOffset);
2061     var x = this.timeToScreen(nowOffset);
2062 
2063     var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
2064     dom.currentTime.style.display = visible ? '' : 'none';
2065     dom.currentTime.style.left = x + "px";
2066     dom.currentTime.title = "Current time: " + nowOffset;
2067 
2068     // start a timer to adjust for the new time
2069     if (this.currentTimeTimer != undefined) {
2070         clearTimeout(this.currentTimeTimer);
2071         delete this.currentTimeTimer;
2072     }
2073     var timeline = this;
2074     var onTimeout = function() {
2075         timeline.repaintCurrentTime();
2076     };
2077     // the time equal to the width of one pixel, divided by 2 for more smoothness
2078     var interval = 1 / this.conversion.factor / 2;
2079     if (interval < 30) interval = 30;
2080     this.currentTimeTimer = setTimeout(onTimeout, interval);
2081 };
2082 
2083 /**
2084  * Redraw the custom time bar
2085  */
2086 links.Timeline.prototype.repaintCustomTime = function() {
2087     var options = this.options,
2088         dom = this.dom,
2089         size = this.size;
2090 
2091     if (!options.showCustomTime) {
2092         if (dom.customTime) {
2093             dom.contentTimelines.removeChild(dom.customTime);
2094             delete dom.customTime;
2095         }
2096 
2097         return;
2098     }
2099 
2100     if (!dom.customTime) {
2101         var customTime = document.createElement("DIV");
2102         customTime.className = "timeline-customtime";
2103         customTime.style.position = "absolute";
2104         customTime.style.top = "0px";
2105         customTime.style.height = "100%";
2106 
2107         var drag = document.createElement("DIV");
2108         drag.style.position = "relative";
2109         drag.style.top = "0px";
2110         drag.style.left = "-10px";
2111         drag.style.height = "100%";
2112         drag.style.width = "20px";
2113         customTime.appendChild(drag);
2114 
2115         dom.contentTimelines.appendChild(customTime);
2116         dom.customTime = customTime;
2117 
2118         // initialize parameter
2119         this.customTime = new Date();
2120     }
2121 
2122     var x = this.timeToScreen(this.customTime),
2123         visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
2124     dom.customTime.style.display = visible ? '' : 'none';
2125     dom.customTime.style.left = x + "px";
2126     dom.customTime.title = "Time: " + this.customTime;
2127 };
2128 
2129 
2130 /**
2131  * Redraw the delete button, on the top right of the currently selected item
2132  * if there is no item selected, the button is hidden.
2133  */
2134 links.Timeline.prototype.repaintDeleteButton = function () {
2135     var timeline = this,
2136         dom = this.dom,
2137         frame = dom.items.frame;
2138 
2139     var deleteButton = dom.items.deleteButton;
2140     if (!deleteButton) {
2141         // create a delete button
2142         deleteButton = document.createElement("DIV");
2143         deleteButton.className = "timeline-navigation-delete";
2144         deleteButton.style.position = "absolute";
2145 
2146         frame.appendChild(deleteButton);
2147         dom.items.deleteButton = deleteButton;
2148     }
2149 
2150     var index = (this.selection && this.selection.index !== undefined) ? this.selection.index : -1,
2151         item = (this.selection && this.selection.index !== undefined) ? this.items[index] : undefined;
2152     if (item && item.rendered && this.isEditable(item)) {
2153         var right = item.getRight(this),
2154             top = item.top;
2155 
2156         deleteButton.style.left = right + 'px';
2157         deleteButton.style.top = top + 'px';
2158         deleteButton.style.display = '';
2159         frame.removeChild(deleteButton);
2160         frame.appendChild(deleteButton);
2161     }
2162     else {
2163         deleteButton.style.display = 'none';
2164     }
2165 };
2166 
2167 
2168 /**
2169  * Redraw the drag areas. When an item (ranges only) is selected,
2170  * it gets a drag area on the left and right side, to change its width
2171  */
2172 links.Timeline.prototype.repaintDragAreas = function () {
2173     var timeline = this,
2174         options = this.options,
2175         dom = this.dom,
2176         frame = this.dom.items.frame;
2177 
2178     // create left drag area
2179     var dragLeft = dom.items.dragLeft;
2180     if (!dragLeft) {
2181         dragLeft = document.createElement("DIV");
2182         dragLeft.className="timeline-event-range-drag-left";
2183         dragLeft.style.position = "absolute";
2184 
2185         frame.appendChild(dragLeft);
2186         dom.items.dragLeft = dragLeft;
2187     }
2188 
2189     // create right drag area
2190     var dragRight = dom.items.dragRight;
2191     if (!dragRight) {
2192         dragRight = document.createElement("DIV");
2193         dragRight.className="timeline-event-range-drag-right";
2194         dragRight.style.position = "absolute";
2195 
2196         frame.appendChild(dragRight);
2197         dom.items.dragRight = dragRight;
2198     }
2199 
2200     // reposition left and right drag area
2201     var index = (this.selection && this.selection.index !== undefined) ? this.selection.index : -1,
2202         item = (this.selection && this.selection.index !== undefined) ? this.items[index] : undefined;
2203     if (item && item.rendered && this.isEditable(item) &&
2204         (item instanceof links.Timeline.ItemRange || item instanceof links.Timeline.ItemFloatingRange)) {
2205         var left = item.getLeft(this), // NH change to getLeft
2206             right = item.getRight(this), // NH change to getRight
2207             top = item.top,
2208             height = item.height;
2209 
2210         dragLeft.style.left = left + 'px';
2211         dragLeft.style.top = top + 'px';
2212         dragLeft.style.width = options.dragAreaWidth + "px";
2213         dragLeft.style.height = height + 'px';
2214         dragLeft.style.display = '';
2215         frame.removeChild(dragLeft);
2216         frame.appendChild(dragLeft);
2217 
2218         dragRight.style.left = (right - options.dragAreaWidth) + 'px';
2219         dragRight.style.top = top + 'px';
2220         dragRight.style.width = options.dragAreaWidth + "px";
2221         dragRight.style.height = height + 'px';
2222         dragRight.style.display = '';
2223         frame.removeChild(dragRight);
2224         frame.appendChild(dragRight);
2225     }
2226     else {
2227         dragLeft.style.display = 'none';
2228         dragRight.style.display = 'none';
2229     }
2230 };
2231 
2232 /**
2233  * Create the navigation buttons for zooming and moving
2234  */
2235 links.Timeline.prototype.repaintNavigation = function () {
2236     var timeline = this,
2237         options = this.options,
2238         dom = this.dom,
2239         frame = dom.frame,
2240         navBar = dom.navBar;
2241 
2242     if (!navBar) {
2243         var showButtonNew = options.showButtonNew && options.editable;
2244         var showNavigation = options.showNavigation && (options.zoomable || options.moveable);
2245         if (showNavigation || showButtonNew) {
2246             // create a navigation bar containing the navigation buttons
2247             navBar = document.createElement("DIV");
2248             navBar.style.position = "absolute";
2249             navBar.className = "timeline-navigation ui-widget ui-state-highlight ui-corner-all";
2250             if (options.groupsOnRight) {
2251                 navBar.style.left = '10px';
2252             }
2253             else {
2254                 navBar.style.right = '10px';
2255             }
2256             if (options.axisOnTop) {
2257                 navBar.style.bottom = '10px';
2258             }
2259             else {
2260                 navBar.style.top = '10px';
2261             }
2262             dom.navBar = navBar;
2263             frame.appendChild(navBar);
2264         }
2265 
2266         if (showButtonNew) {
2267             // create a new in button
2268             navBar.addButton = document.createElement("DIV");
2269             navBar.addButton.className = "timeline-navigation-new";
2270             navBar.addButton.title = options.CREATE_NEW_EVENT;
2271             var addIconSpan = document.createElement("SPAN");
2272             addIconSpan.className = "ui-icon ui-icon-circle-plus";
2273             navBar.addButton.appendChild(addIconSpan);
2274 
2275             var onAdd = function(event) {
2276                 links.Timeline.preventDefault(event);
2277                 links.Timeline.stopPropagation(event);
2278 
2279                 // create a new event at the center of the frame
2280                 var w = timeline.size.contentWidth;
2281                 var x = w / 2;
2282                 var xstart = timeline.screenToTime(x);
2283                 if (options.snapEvents) {
2284                     timeline.step.snap(xstart);
2285                 }
2286 
2287                 var content = options.NEW;
2288                 var group = timeline.groups.length ? timeline.groups[0].content : undefined;
2289                 var preventRender = true;
2290                 timeline.addItem({
2291                     'start': xstart,
2292                     'content': content,
2293                     'group': group
2294                 }, preventRender);
2295                 var index = (timeline.items.length - 1);
2296                 timeline.selectItem(index);
2297 
2298                 timeline.applyAdd = true;
2299 
2300                 // fire an add event.
2301                 // Note that the change can be canceled from within an event listener if
2302                 // this listener calls the method cancelAdd().
2303                 timeline.trigger('add');
2304 
2305                 if (timeline.applyAdd) {
2306                     // render and select the item
2307                     timeline.render({animate: false});
2308                     timeline.selectItem(index);
2309                 }
2310                 else {
2311                     // undo an add
2312                     timeline.deleteItem(index);
2313                 }
2314             };
2315             links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
2316             navBar.appendChild(navBar.addButton);
2317         }
2318 
2319         if (showButtonNew && showNavigation) {
2320             // create a separator line
2321             links.Timeline.addClassName(navBar.addButton, 'timeline-navigation-new-line');
2322         }
2323 
2324         if (showNavigation) {
2325             if (options.zoomable) {
2326                 // create a zoom in button
2327                 navBar.zoomInButton = document.createElement("DIV");
2328                 navBar.zoomInButton.className = "timeline-navigation-zoom-in";
2329                 navBar.zoomInButton.title = this.options.ZOOM_IN;
2330                 var ziIconSpan = document.createElement("SPAN");
2331                 ziIconSpan.className = "ui-icon ui-icon-circle-zoomin";
2332                 navBar.zoomInButton.appendChild(ziIconSpan);
2333 
2334                 var onZoomIn = function(event) {
2335                     links.Timeline.preventDefault(event);
2336                     links.Timeline.stopPropagation(event);
2337                     timeline.zoom(0.4);
2338                     timeline.trigger("rangechange");
2339                     timeline.trigger("rangechanged");
2340                 };
2341                 links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
2342                 navBar.appendChild(navBar.zoomInButton);
2343 
2344                 // create a zoom out button
2345                 navBar.zoomOutButton = document.createElement("DIV");
2346                 navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
2347                 navBar.zoomOutButton.title = this.options.ZOOM_OUT;
2348                 var zoIconSpan = document.createElement("SPAN");
2349                 zoIconSpan.className = "ui-icon ui-icon-circle-zoomout";
2350                 navBar.zoomOutButton.appendChild(zoIconSpan);
2351 
2352                 var onZoomOut = function(event) {
2353                     links.Timeline.preventDefault(event);
2354                     links.Timeline.stopPropagation(event);
2355                     timeline.zoom(-0.4);
2356                     timeline.trigger("rangechange");
2357                     timeline.trigger("rangechanged");
2358                 };
2359                 links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
2360                 navBar.appendChild(navBar.zoomOutButton);
2361             }
2362 
2363             if (options.moveable) {
2364                 // create a move left button
2365                 navBar.moveLeftButton = document.createElement("DIV");
2366                 navBar.moveLeftButton.className = "timeline-navigation-move-left";
2367                 navBar.moveLeftButton.title = this.options.MOVE_LEFT;
2368                 var mlIconSpan = document.createElement("SPAN");
2369                 mlIconSpan.className = "ui-icon ui-icon-circle-arrow-w";
2370                 navBar.moveLeftButton.appendChild(mlIconSpan);
2371 
2372                 var onMoveLeft = function(event) {
2373                     links.Timeline.preventDefault(event);
2374                     links.Timeline.stopPropagation(event);
2375                     timeline.move(-0.2);
2376                     timeline.trigger("rangechange");
2377                     timeline.trigger("rangechanged");
2378                 };
2379                 links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
2380                 navBar.appendChild(navBar.moveLeftButton);
2381 
2382                 // create a move right button
2383                 navBar.moveRightButton = document.createElement("DIV");
2384                 navBar.moveRightButton.className = "timeline-navigation-move-right";
2385                 navBar.moveRightButton.title = this.options.MOVE_RIGHT;
2386                 var mrIconSpan = document.createElement("SPAN");
2387                 mrIconSpan.className = "ui-icon ui-icon-circle-arrow-e";
2388                 navBar.moveRightButton.appendChild(mrIconSpan);
2389 
2390                 var onMoveRight = function(event) {
2391                     links.Timeline.preventDefault(event);
2392                     links.Timeline.stopPropagation(event);
2393                     timeline.move(0.2);
2394                     timeline.trigger("rangechange");
2395                     timeline.trigger("rangechanged");
2396                 };
2397                 links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
2398                 navBar.appendChild(navBar.moveRightButton);
2399             }
2400         }
2401     }
2402 };
2403 
2404 
2405 /**
2406  * Set current time. This function can be used to set the time in the client
2407  * timeline equal with the time on a server.
2408  * @param {Date} time
2409  */
2410 links.Timeline.prototype.setCurrentTime = function(time) {
2411     var now = new Date();
2412     this.clientTimeOffset = (time.valueOf() - now.valueOf());
2413 
2414     this.repaintCurrentTime();
2415 };
2416 
2417 /**
2418  * Get current time. The time can have an offset from the real time, when
2419  * the current time has been changed via the method setCurrentTime.
2420  * @return {Date} time
2421  */
2422 links.Timeline.prototype.getCurrentTime = function() {
2423     var now = new Date();
2424     return new Date(now.valueOf() + this.clientTimeOffset);
2425 };
2426 
2427 
2428 /**
2429  * Set custom time.
2430  * The custom time bar can be used to display events in past or future.
2431  * @param {Date} time
2432  */
2433 links.Timeline.prototype.setCustomTime = function(time) {
2434     this.customTime = new Date(time.valueOf());
2435     this.repaintCustomTime();
2436 };
2437 
2438 /**
2439  * Retrieve the current custom time.
2440  * @return {Date} customTime
2441  */
2442 links.Timeline.prototype.getCustomTime = function() {
2443     return new Date(this.customTime.valueOf());
2444 };
2445 
2446 /**
2447  * Set a custom scale. Autoscaling will be disabled.
2448  * For example setScale(SCALE.MINUTES, 5) will result
2449  * in minor steps of 5 minutes, and major steps of an hour.
2450  *
2451  * @param {links.Timeline.StepDate.SCALE} scale
2452  *                               A scale. Choose from SCALE.MILLISECOND,
2453  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
2454  *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
2455  *                               SCALE.YEAR.
2456  * @param {int}        step   A step size, by default 1. Choose for
2457  *                               example 1, 2, 5, or 10.
2458  */
2459 links.Timeline.prototype.setScale = function(scale, step) {
2460     this.step.setScale(scale, step);
2461     this.render(); // TODO: optimize: only reflow/repaint axis
2462 };
2463 
2464 /**
2465  * Enable or disable autoscaling
2466  * @param {boolean} enable  If true or not defined, autoscaling is enabled.
2467  *                          If false, autoscaling is disabled.
2468  */
2469 links.Timeline.prototype.setAutoScale = function(enable) {
2470     this.step.setAutoScale(enable);
2471     this.render(); // TODO: optimize: only reflow/repaint axis
2472 };
2473 
2474 /**
2475  * Redraw the timeline
2476  * Reloads the (linked) data table and redraws the timeline when resized.
2477  * See also the method checkResize
2478  */
2479 links.Timeline.prototype.redraw = function() {
2480     this.setData(this.data);
2481 };
2482 
2483 
2484 /**
2485  * Check if the timeline is resized, and if so, redraw the timeline.
2486  * Useful when the webpage is resized.
2487  */
2488 links.Timeline.prototype.checkResize = function() {
2489     // TODO: re-implement the method checkResize, or better, make it redundant as this.render will be smarter
2490     this.render();
2491 };
2492 
2493 /**
2494  * Check whether a given item is editable
2495  * @param {links.Timeline.Item} item
2496  * @return {boolean} editable
2497  */
2498 links.Timeline.prototype.isEditable = function (item) {
2499     if (item) {
2500         if (item.editable != undefined) {
2501             return item.editable;
2502         }
2503         else {
2504             return this.options.editable;
2505         }
2506     }
2507     return false;
2508 };
2509 
2510 /**
2511  * Calculate the factor and offset to convert a position on screen to the
2512  * corresponding date and vice versa.
2513  * After the method calcConversionFactor is executed once, the methods screenToTime and
2514  * timeToScreen can be used.
2515  */
2516 links.Timeline.prototype.recalcConversion = function() {
2517     this.conversion.offset = this.start.valueOf();
2518     this.conversion.factor = this.size.contentWidth /
2519         (this.end.valueOf() - this.start.valueOf());
2520 };
2521 
2522 
2523 /**
2524  * Convert a position on screen (pixels) to a datetime
2525  * Before this method can be used, the method calcConversionFactor must be
2526  * executed once.
2527  * @param {int}     x    Position on the screen in pixels
2528  * @return {Date}   time The datetime the corresponds with given position x
2529  */
2530 links.Timeline.prototype.screenToTime = function(x) {
2531     var conversion = this.conversion;
2532     return new Date(x / conversion.factor + conversion.offset);
2533 };
2534 
2535 /**
2536  * Convert a datetime (Date object) into a position on the screen
2537  * Before this method can be used, the method calcConversionFactor must be
2538  * executed once.
2539  * @param {Date}   time A date
2540  * @return {int}   x    The position on the screen in pixels which corresponds
2541  *                      with the given date.
2542  */
2543 links.Timeline.prototype.timeToScreen = function(time) {
2544     var conversion = this.conversion;
2545     return (time.valueOf() - conversion.offset) * conversion.factor;
2546 };
2547 
2548 
2549 
2550 /**
2551  * Event handler for touchstart event on mobile devices
2552  */
2553 links.Timeline.prototype.onTouchStart = function(event) {
2554     var params = this.eventParams,
2555         me = this;
2556 
2557     if (params.touchDown) {
2558         // if already moving, return
2559         return;
2560     }
2561 
2562     params.touchDown = true;
2563     params.zoomed = false;
2564 
2565     this.onMouseDown(event);
2566 
2567     if (!params.onTouchMove) {
2568         params.onTouchMove = function (event) {me.onTouchMove(event);};
2569         links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
2570     }
2571     if (!params.onTouchEnd) {
2572         params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
2573         links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
2574     }
2575 
2576     /* TODO
2577      // check for double tap event
2578      var delta = 500; // ms
2579      var doubleTapStart = (new Date()).valueOf();
2580      var target = links.Timeline.getTarget(event);
2581      var doubleTapItem = this.getItemIndex(target);
2582      if (params.doubleTapStart &&
2583      (doubleTapStart - params.doubleTapStart) < delta &&
2584      doubleTapItem == params.doubleTapItem) {
2585      delete params.doubleTapStart;
2586      delete params.doubleTapItem;
2587      me.onDblClick(event);
2588      params.touchDown = false;
2589      }
2590      params.doubleTapStart = doubleTapStart;
2591      params.doubleTapItem = doubleTapItem;
2592      */
2593     // store timing for double taps
2594     var target = links.Timeline.getTarget(event);
2595     var item = this.getItemIndex(target);
2596     params.doubleTapStartPrev = params.doubleTapStart;
2597     params.doubleTapStart = (new Date()).valueOf();
2598     params.doubleTapItemPrev = params.doubleTapItem;
2599     params.doubleTapItem = item;
2600 
2601     links.Timeline.preventDefault(event);
2602 };
2603 
2604 /**
2605  * Event handler for touchmove event on mobile devices
2606  */
2607 links.Timeline.prototype.onTouchMove = function(event) {
2608     var params = this.eventParams;
2609 
2610     if (event.scale && event.scale !== 1) {
2611         params.zoomed = true;
2612     }
2613 
2614     if (!params.zoomed) {
2615         // move
2616         this.onMouseMove(event);
2617     }
2618     else {
2619         if (this.options.zoomable) {
2620             // pinch
2621             // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
2622             params.zoomed = true;
2623 
2624             var scale = event.scale,
2625                 oldWidth = (params.end.valueOf() - params.start.valueOf()),
2626                 newWidth = oldWidth / scale,
2627                 diff = newWidth - oldWidth,
2628                 start = new Date(parseInt(params.start.valueOf() - diff/2)),
2629                 end = new Date(parseInt(params.end.valueOf() + diff/2));
2630 
2631             // TODO: determine zoom-around-date from touch positions?
2632 
2633             this.setVisibleChartRange(start, end);
2634             this.trigger("rangechange");
2635         }
2636     }
2637 
2638     links.Timeline.preventDefault(event);
2639 };
2640 
2641 /**
2642  * Event handler for touchend event on mobile devices
2643  */
2644 links.Timeline.prototype.onTouchEnd = function(event) {
2645     var params = this.eventParams;
2646     var me = this;
2647     params.touchDown = false;
2648 
2649     if (params.zoomed) {
2650         this.trigger("rangechanged");
2651     }
2652 
2653     if (params.onTouchMove) {
2654         links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
2655         delete params.onTouchMove;
2656 
2657     }
2658     if (params.onTouchEnd) {
2659         links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
2660         delete params.onTouchEnd;
2661     }
2662 
2663     this.onMouseUp(event);
2664 
2665     // check for double tap event
2666     var delta = 500; // ms
2667     var doubleTapEnd = (new Date()).valueOf();
2668     var target = links.Timeline.getTarget(event);
2669     var doubleTapItem = this.getItemIndex(target);
2670     if (params.doubleTapStartPrev &&
2671         (doubleTapEnd - params.doubleTapStartPrev) < delta &&
2672         params.doubleTapItem == params.doubleTapItemPrev) {
2673         params.touchDown = true;
2674         me.onDblClick(event);
2675         params.touchDown = false;
2676     }
2677 
2678     links.Timeline.preventDefault(event);
2679 };
2680 
2681 
2682 /**
2683  * Start a moving operation inside the provided parent element
2684  * @param {Event} event       The event that occurred (required for
2685  *                             retrieving the  mouse position)
2686  */
2687 links.Timeline.prototype.onMouseDown = function(event) {
2688     event = event || window.event;
2689 
2690     var params = this.eventParams,
2691         options = this.options,
2692         dom = this.dom;
2693 
2694     // only react on left mouse button down
2695     var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
2696     if (!leftButtonDown && !params.touchDown) {
2697         return;
2698     }
2699 
2700     // get mouse position
2701     params.mouseX = links.Timeline.getPageX(event);
2702     params.mouseY = links.Timeline.getPageY(event);
2703     params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
2704     params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
2705     params.previousLeft = 0;
2706     params.previousOffset = 0;
2707 
2708     params.moved = false;
2709     params.start = new Date(this.start.valueOf());
2710     params.end = new Date(this.end.valueOf());
2711 
2712     params.target = links.Timeline.getTarget(event);
2713     var dragLeft = (dom.items && dom.items.dragLeft) ? dom.items.dragLeft : undefined;
2714     var dragRight = (dom.items && dom.items.dragRight) ? dom.items.dragRight : undefined;
2715     params.itemDragLeft = (params.target === dragLeft);
2716     params.itemDragRight = (params.target === dragRight);
2717 
2718     if (params.itemDragLeft || params.itemDragRight) {
2719         params.itemIndex = (this.selection && this.selection.index !== undefined) ? this.selection.index : undefined;
2720         delete params.clusterIndex;
2721     }
2722     else {
2723         params.itemIndex = this.getItemIndex(params.target);
2724         params.clusterIndex = this.getClusterIndex(params.target);
2725     }
2726 
2727     params.customTime = (params.target === dom.customTime ||
2728         params.target.parentNode === dom.customTime) ?
2729         this.customTime :
2730         undefined;
2731 
2732     params.addItem = (options.editable && event.ctrlKey);
2733     if (params.addItem) {
2734         // create a new event at the current mouse position
2735         var x = params.mouseX - params.frameLeft;
2736         var y = params.mouseY - params.frameTop;
2737 
2738         var xstart = this.screenToTime(x);
2739         if (options.snapEvents) {
2740             this.step.snap(xstart);
2741         }
2742         var xend = new Date(xstart.valueOf());
2743         var content = options.NEW;
2744         var group = this.getGroupFromHeight(y);
2745         this.addItem({
2746             'start': xstart,
2747             'end': xend,
2748             'content': content,
2749             'group': this.getGroupName(group)
2750         });
2751         params.itemIndex = (this.items.length - 1);
2752         delete params.clusterIndex;
2753         this.selectItem(params.itemIndex);
2754         params.itemDragRight = true;
2755     }
2756 
2757     var item = this.items[params.itemIndex];
2758     var isSelected = this.isSelected(params.itemIndex);
2759     params.editItem = isSelected && this.isEditable(item);
2760     if (params.editItem) {
2761         params.itemStart = item.start;
2762         params.itemEnd = item.end;
2763         params.itemGroup = item.group;
2764         params.itemLeft = item.getLeft(this); // NH Use item.getLeft here
2765         params.itemRight = item.getRight(this); // NH Use item.getRight here
2766     }
2767     else {
2768         this.dom.frame.style.cursor = 'move';
2769     }
2770     if (!params.touchDown) {
2771         // add event listeners to handle moving the contents
2772         // we store the function onmousemove and onmouseup in the timeline, so we can
2773         // remove the eventlisteners lateron in the function mouseUp()
2774         var me = this;
2775         if (!params.onMouseMove) {
2776             params.onMouseMove = function (event) {me.onMouseMove(event);};
2777             links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
2778         }
2779         if (!params.onMouseUp) {
2780             params.onMouseUp = function (event) {me.onMouseUp(event);};
2781             links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
2782         }
2783 
2784         links.Timeline.preventDefault(event);
2785     }
2786 };
2787 
2788 
2789 /**
2790  * Perform moving operating.
2791  * This function activated from within the funcion links.Timeline.onMouseDown().
2792  * @param {Event}   event  Well, eehh, the event
2793  */
2794 links.Timeline.prototype.onMouseMove = function (event) {
2795     event = event || window.event;
2796 
2797     var params = this.eventParams,
2798         size = this.size,
2799         dom = this.dom,
2800         options = this.options;
2801 
2802     // calculate change in mouse position
2803     var mouseX = links.Timeline.getPageX(event);
2804     var mouseY = links.Timeline.getPageY(event);
2805 
2806     if (params.mouseX == undefined) {
2807         params.mouseX = mouseX;
2808     }
2809     if (params.mouseY == undefined) {
2810         params.mouseY = mouseY;
2811     }
2812 
2813     var diffX = mouseX - params.mouseX;
2814     var diffY = mouseY - params.mouseY;
2815 
2816     // if mouse movement is big enough, register it as a "moved" event
2817     if (Math.abs(diffX) >= 1) {
2818         params.moved = true;
2819     }
2820 
2821     if (params.customTime) {
2822         var x = this.timeToScreen(params.customTime);
2823         var xnew = x + diffX;
2824         this.customTime = this.screenToTime(xnew);
2825         this.repaintCustomTime();
2826 
2827         // fire a timechange event
2828         this.trigger('timechange');
2829     }
2830     else if (params.editItem) {
2831         var item = this.items[params.itemIndex],
2832             left,
2833             right;
2834 
2835         if (params.itemDragLeft && options.timeChangeable) {
2836             // move the start of the item
2837             left = params.itemLeft + diffX;
2838             right = params.itemRight;
2839 
2840             item.start = this.screenToTime(left);
2841             if (options.snapEvents) {
2842                 this.step.snap(item.start);
2843                 left = this.timeToScreen(item.start);
2844             }
2845 
2846             if (left > right) {
2847                 left = right;
2848                 item.start = this.screenToTime(left);
2849             }
2850           this.trigger('change');
2851         }
2852         else if (params.itemDragRight && options.timeChangeable) {
2853             // move the end of the item
2854             left = params.itemLeft;
2855             right = params.itemRight + diffX;
2856 
2857             item.end = this.screenToTime(right);
2858             if (options.snapEvents) {
2859                 this.step.snap(item.end);
2860                 right = this.timeToScreen(item.end);
2861             }
2862 
2863             if (right < left) {
2864                 right = left;
2865                 item.end = this.screenToTime(right);
2866             }
2867           this.trigger('change');
2868         }
2869         else if (options.timeChangeable) {
2870             // move the item
2871             left = params.itemLeft + diffX;
2872             item.start = this.screenToTime(left);
2873             if (options.snapEvents) {
2874                 this.step.snap(item.start);
2875                 left = this.timeToScreen(item.start);
2876             }
2877 
2878             if (item.end) {
2879                 right = left + (params.itemRight - params.itemLeft);
2880                 item.end = this.screenToTime(right);
2881             }
2882             this.trigger('change');
2883         }
2884 
2885         item.setPosition(left, right);
2886 
2887         var dragging = params.itemDragLeft || params.itemDragRight;
2888         if (this.groups.length && !dragging) {
2889             // move item from one group to another when needed
2890             var y = mouseY - params.frameTop;
2891             var group = this.getGroupFromHeight(y);
2892             if (options.groupsChangeable && item.group !== group) {
2893                 // move item to the other group
2894                 var index = this.items.indexOf(item);
2895                 this.changeItem(index, {'group': this.getGroupName(group)});
2896             }
2897             else {
2898                 this.repaintDeleteButton();
2899                 this.repaintDragAreas();
2900             }
2901         }
2902         else {
2903             // TODO: does not work well in FF, forces redraw with every mouse move it seems
2904             this.render(); // TODO: optimize, only redraw the items?
2905             // Note: when animate==true, no redraw is needed here, its done by stackItems animation
2906         }
2907     }
2908     else if (options.moveable) {
2909         var interval = (params.end.valueOf() - params.start.valueOf());
2910         var diffMillisecs = Math.round((-diffX) / size.contentWidth * interval);
2911         var newStart = new Date(params.start.valueOf() + diffMillisecs);
2912         var newEnd = new Date(params.end.valueOf() + diffMillisecs);
2913         this.applyRange(newStart, newEnd);
2914         // if the applied range is moved due to a fixed min or max,
2915         // change the diffMillisecs accordingly
2916         var appliedDiff = (this.start.valueOf() - newStart.valueOf());
2917         if (appliedDiff) {
2918             diffMillisecs += appliedDiff;
2919         }
2920 
2921         this.recalcConversion();
2922 
2923         // move the items by changing the left position of their frame.
2924         // this is much faster than repositioning all elements individually via the
2925         // repaintFrame() function (which is done once at mouseup)
2926         // note that we round diffX to prevent wrong positioning on millisecond scale
2927         var previousLeft = params.previousLeft || 0;
2928         var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
2929         var previousOffset = params.previousOffset || 0;
2930         var frameOffset = previousOffset + (currentLeft - previousLeft);
2931         var frameLeft = -diffMillisecs / interval * size.contentWidth + frameOffset;
2932 
2933         dom.items.frame.style.left = (frameLeft) + "px";
2934 
2935         // read the left again from DOM (IE8- rounds the value)
2936         params.previousOffset = frameOffset;
2937         params.previousLeft = parseFloat(dom.items.frame.style.left) || frameLeft;
2938 
2939         this.repaintCurrentTime();
2940         this.repaintCustomTime();
2941         this.repaintAxis();
2942 
2943         // fire a rangechange event
2944         this.trigger('rangechange');
2945     }
2946 
2947     links.Timeline.preventDefault(event);
2948 };
2949 
2950 
2951 /**
2952  * Stop moving operating.
2953  * This function activated from within the funcion links.Timeline.onMouseDown().
2954  * @param {event}  event   The event
2955  */
2956 links.Timeline.prototype.onMouseUp = function (event) {
2957     var params = this.eventParams,
2958         options = this.options;
2959 
2960     event = event || window.event;
2961 
2962     this.dom.frame.style.cursor = 'auto';
2963 
2964     // remove event listeners here, important for Safari
2965     if (params.onMouseMove) {
2966         links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
2967         delete params.onMouseMove;
2968     }
2969     if (params.onMouseUp) {
2970         links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
2971         delete params.onMouseUp;
2972     }
2973     //links.Timeline.preventDefault(event);
2974 
2975     if (params.customTime) {
2976         // fire a timechanged event
2977         this.trigger('timechanged');
2978     }
2979     else if (params.editItem) {
2980         var item = this.items[params.itemIndex];
2981 
2982         if (params.moved || params.addItem) {
2983             this.applyChange = true;
2984             this.applyAdd = true;
2985 
2986             this.updateData(params.itemIndex, {
2987                 'start': item.start,
2988                 'end': item.end
2989             });
2990 
2991             // fire an add or changed event.
2992             // Note that the change can be canceled from within an event listener if
2993             // this listener calls the method cancelChange().
2994             this.trigger(params.addItem ? 'add' : 'changed');
2995             
2996             //retrieve item data again to include changes made to it in the triggered event handlers
2997             item = this.items[params.itemIndex];
2998 
2999             if (params.addItem) {
3000                 if (this.applyAdd) {
3001                     this.updateData(params.itemIndex, {
3002                         'start': item.start,
3003                         'end': item.end,
3004                         'content': item.content,
3005                         'group': this.getGroupName(item.group)
3006                     });
3007                 }
3008                 else {
3009                     // undo an add
3010                     this.deleteItem(params.itemIndex);
3011                 }
3012             }
3013             else {
3014                 if (this.applyChange) {
3015                     this.updateData(params.itemIndex, {
3016                         'start': item.start,
3017                         'end': item.end
3018                     });
3019                 }
3020                 else {
3021                     // undo a change
3022                     delete this.applyChange;
3023                     delete this.applyAdd;
3024 
3025                     var item = this.items[params.itemIndex],
3026                         domItem = item.dom;
3027 
3028                     item.start = params.itemStart;
3029                     item.end = params.itemEnd;
3030                     item.group = params.itemGroup;
3031                     // TODO: original group should be restored too
3032                     item.setPosition(params.itemLeft, params.itemRight);
3033 
3034                     this.updateData(params.itemIndex, {
3035                         'start': params.itemStart,
3036                         'end': params.itemEnd
3037                     });
3038                 }
3039             }
3040 
3041             // prepare data for clustering, by filtering and sorting by type
3042             if (this.options.cluster) {
3043                 this.clusterGenerator.updateData();
3044             }
3045 
3046             this.render();
3047         }
3048     }
3049     else {
3050         if (!params.moved && !params.zoomed) {
3051             // mouse did not move -> user has selected an item
3052 
3053             if (params.target === this.dom.items.deleteButton) {
3054                 // delete item
3055                 if (this.selection && this.selection.index !== undefined) {
3056                     this.confirmDeleteItem(this.selection.index);
3057                 }
3058             }
3059             else if (options.selectable) {
3060                 // select/unselect item
3061                 if (params.itemIndex != undefined) {
3062                     if (!this.isSelected(params.itemIndex)) {
3063                         this.selectItem(params.itemIndex);
3064                         this.trigger('select');
3065                     }
3066                 }
3067                 else if(params.clusterIndex != undefined) {
3068                     this.selectCluster(params.clusterIndex);
3069                     this.trigger('select');
3070                 }
3071                 else {
3072                     if (options.unselectable) {
3073                         this.unselectItem();
3074                         this.trigger('select');
3075                     }
3076                 }
3077             }
3078         }
3079         else {
3080             // timeline is moved
3081             // TODO: optimize: no need to reflow and cluster again?
3082             this.render();
3083 
3084             if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
3085                 // fire a rangechanged event
3086                 this.trigger('rangechanged');
3087             }
3088         }
3089     }
3090 };
3091 
3092 /**
3093  * Double click event occurred for an item
3094  * @param {Event}  event
3095  */
3096 links.Timeline.prototype.onDblClick = function (event) {
3097     var params = this.eventParams,
3098         options = this.options,
3099         dom = this.dom,
3100         size = this.size;
3101     event = event || window.event;
3102 
3103     if (params.itemIndex != undefined) {
3104         var item = this.items[params.itemIndex];
3105         if (item && this.isEditable(item)) {
3106             // fire the edit event
3107             this.trigger('edit');
3108         }
3109     }
3110     else {
3111         if (options.editable) {
3112             // create a new item
3113 
3114             // get mouse position
3115             params.mouseX = links.Timeline.getPageX(event);
3116             params.mouseY = links.Timeline.getPageY(event);
3117             var x = params.mouseX - links.Timeline.getAbsoluteLeft(dom.content);
3118             var y = params.mouseY - links.Timeline.getAbsoluteTop(dom.content);
3119 
3120             // create a new event at the current mouse position
3121             var xstart = this.screenToTime(x);
3122             if (options.snapEvents) {
3123                 this.step.snap(xstart);
3124             }
3125 
3126             var content = options.NEW;
3127             var group = this.getGroupFromHeight(y);   // (group may be undefined)
3128             var preventRender = true;
3129             this.addItem({
3130                 'start': xstart,
3131                 'content': content,
3132                 'group': this.getGroupName(group)
3133             }, preventRender);
3134             params.itemIndex = (this.items.length - 1);
3135             this.selectItem(params.itemIndex);
3136 
3137             this.applyAdd = true;
3138 
3139             // fire an add event.
3140             // Note that the change can be canceled from within an event listener if
3141             // this listener calls the method cancelAdd().
3142             this.trigger('add');
3143 
3144             if (this.applyAdd) {
3145                 // render and select the item
3146                 this.render({animate: false});
3147                 this.selectItem(params.itemIndex);
3148             }
3149             else {
3150                 // undo an add
3151                 this.deleteItem(params.itemIndex);
3152             }
3153         }
3154     }
3155 
3156     links.Timeline.preventDefault(event);
3157 };
3158 
3159 
3160 /**
3161  * Event handler for mouse wheel event, used to zoom the timeline
3162  * Code from http://adomas.org/javascript-mouse-wheel/
3163  * @param {Event}  event   The event
3164  */
3165 links.Timeline.prototype.onMouseWheel = function(event) {
3166     if (!this.options.zoomable)
3167         return;
3168 
3169     if (!event) { /* For IE. */
3170         event = window.event;
3171     }
3172 
3173     // retrieve delta
3174     var delta = 0;
3175     if (event.wheelDelta) { /* IE/Opera. */
3176         delta = event.wheelDelta/120;
3177     } else if (event.detail) { /* Mozilla case. */
3178         // In Mozilla, sign of delta is different than in IE.
3179         // Also, delta is multiple of 3.
3180         delta = -event.detail/3;
3181     }
3182 
3183     // If delta is nonzero, handle it.
3184     // Basically, delta is now positive if wheel was scrolled up,
3185     // and negative, if wheel was scrolled down.
3186     if (delta) {
3187         // TODO: on FireFox, the window is not redrawn within repeated scroll-events
3188         // -> use a delayed redraw? Make a zoom queue?
3189 
3190         var timeline = this;
3191         var zoom = function () {
3192             // perform the zoom action. Delta is normally 1 or -1
3193             var zoomFactor = delta / 5.0;
3194             var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
3195             var mouseX = links.Timeline.getPageX(event);
3196             var zoomAroundDate =
3197                 (mouseX != undefined && frameLeft != undefined) ?
3198                     timeline.screenToTime(mouseX - frameLeft) :
3199                     undefined;
3200 
3201             timeline.zoom(zoomFactor, zoomAroundDate);
3202 
3203             // fire a rangechange and a rangechanged event
3204             timeline.trigger("rangechange");
3205             timeline.trigger("rangechanged");
3206         };
3207 
3208         var scroll = function () {
3209             // Scroll the timeline
3210             timeline.move(delta * -0.2);
3211             timeline.trigger("rangechange");
3212             timeline.trigger("rangechanged");
3213         };
3214 
3215         if (event.shiftKey) {
3216             scroll();
3217         }
3218         else {
3219             zoom();
3220         }
3221     }
3222 
3223     // Prevent default actions caused by mouse wheel.
3224     // That might be ugly, but we handle scrolls somehow
3225     // anyway, so don't bother here...
3226     links.Timeline.preventDefault(event);
3227 };
3228 
3229 
3230 /**
3231  * Zoom the timeline the given zoomfactor in or out. Start and end date will
3232  * be adjusted, and the timeline will be redrawn. You can optionally give a
3233  * date around which to zoom.
3234  * For example, try zoomfactor = 0.1 or -0.1
3235  * @param {Number} zoomFactor      Zooming amount. Positive value will zoom in,
3236  *                                 negative value will zoom out
3237  * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
3238  */
3239 links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
3240     // if zoomAroundDate is not provided, take it half between start Date and end Date
3241     if (zoomAroundDate == undefined) {
3242         zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
3243     }
3244 
3245     // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
3246     // result in a start>=end )
3247     if (zoomFactor >= 1) {
3248         zoomFactor = 0.9;
3249     }
3250     if (zoomFactor <= -1) {
3251         zoomFactor = -0.9;
3252     }
3253 
3254     // adjust a negative factor such that zooming in with 0.1 equals zooming
3255     // out with a factor -0.1
3256     if (zoomFactor < 0) {
3257         zoomFactor = zoomFactor / (1 + zoomFactor);
3258     }
3259 
3260     // zoom start Date and end Date relative to the zoomAroundDate
3261     var startDiff = (this.start.valueOf() - zoomAroundDate);
3262     var endDiff = (this.end.valueOf() - zoomAroundDate);
3263 
3264     // calculate new dates
3265     var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
3266     var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);
3267 
3268     // only zoom in when interval is larger than minimum interval (to prevent
3269     // sliding to left/right when having reached the minimum zoom level)
3270     var interval = (newEnd.valueOf() - newStart.valueOf());
3271     var zoomMin = Number(this.options.zoomMin) || 10;
3272     if (zoomMin < 10) {
3273         zoomMin = 10;
3274     }
3275     if (interval >= zoomMin) {
3276         this.applyRange(newStart, newEnd, zoomAroundDate);
3277         this.render({
3278             animate: this.options.animate && this.options.animateZoom
3279         });
3280     }
3281 };
3282 
3283 /**
3284  * Move the timeline the given movefactor to the left or right. Start and end
3285  * date will be adjusted, and the timeline will be redrawn.
3286  * For example, try moveFactor = 0.1 or -0.1
3287  * @param {Number}  moveFactor      Moving amount. Positive value will move right,
3288  *                                 negative value will move left
3289  */
3290 links.Timeline.prototype.move = function(moveFactor) {
3291     // zoom start Date and end Date relative to the zoomAroundDate
3292     var diff = (this.end.valueOf() - this.start.valueOf());
3293 
3294     // apply new dates
3295     var newStart = new Date(this.start.valueOf() + diff * moveFactor);
3296     var newEnd   = new Date(this.end.valueOf() + diff * moveFactor);
3297     this.applyRange(newStart, newEnd);
3298 
3299     this.render(); // TODO: optimize, no need to reflow, only to recalc conversion and repaint
3300 };
3301 
3302 /**
3303  * Apply a visible range. The range is limited to feasible maximum and minimum
3304  * range.
3305  * @param {Date} start
3306  * @param {Date} end
3307  * @param {Date}   zoomAroundDate  Optional. Date around which will be zoomed.
3308  */
3309 links.Timeline.prototype.applyRange = function (start, end, zoomAroundDate) {
3310     // calculate new start and end value
3311     var startValue = start.valueOf(); // number
3312     var endValue = end.valueOf();     // number
3313     var interval = (endValue - startValue);
3314 
3315     // determine maximum and minimum interval
3316     var options = this.options;
3317     var year = 1000 * 60 * 60 * 24 * 365;
3318     var zoomMin = Number(options.zoomMin) || 10;
3319     if (zoomMin < 10) {
3320         zoomMin = 10;
3321     }
3322     var zoomMax = Number(options.zoomMax) || 10000 * year;
3323     if (zoomMax > 10000 * year) {
3324         zoomMax = 10000 * year;
3325     }
3326     if (zoomMax < zoomMin) {
3327         zoomMax = zoomMin;
3328     }
3329 
3330     // determine min and max date value
3331     var min = options.min ? options.min.valueOf() : undefined; // number
3332     var max = options.max ? options.max.valueOf() : undefined; // number
3333     if (min != undefined && max != undefined) {
3334         if (min >= max) {
3335             // empty range
3336             var day = 1000 * 60 * 60 * 24;
3337             max = min + day;
3338         }
3339         if (zoomMax > (max - min)) {
3340             zoomMax = (max - min);
3341         }
3342         if (zoomMin > (max - min)) {
3343             zoomMin = (max - min);
3344         }
3345     }
3346 
3347     // prevent empty interval
3348     if (startValue >= endValue) {
3349         endValue += 1000 * 60 * 60 * 24;
3350     }
3351 
3352     // prevent too small scale
3353     // TODO: IE has problems with milliseconds
3354     if (interval < zoomMin) {
3355         var diff = (zoomMin - interval);
3356         var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
3357         startValue -= Math.round(diff * f);
3358         endValue   += Math.round(diff * (1 - f));
3359     }
3360 
3361     // prevent too large scale
3362     if (interval > zoomMax) {
3363         var diff = (interval - zoomMax);
3364         var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
3365         startValue += Math.round(diff * f);
3366         endValue   -= Math.round(diff * (1 - f));
3367     }
3368 
3369     // prevent to small start date
3370     if (min != undefined) {
3371         var diff = (startValue - min);
3372         if (diff < 0) {
3373             startValue -= diff;
3374             endValue -= diff;
3375         }
3376     }
3377 
3378     // prevent to large end date
3379     if (max != undefined) {
3380         var diff = (max - endValue);
3381         if (diff < 0) {
3382             startValue += diff;
3383             endValue += diff;
3384         }
3385     }
3386 
3387     // apply new dates
3388     this.start = new Date(startValue);
3389     this.end = new Date(endValue);
3390 };
3391 
3392 /**
3393  * Delete an item after a confirmation.
3394  * The deletion can be cancelled by executing .cancelDelete() during the
3395  * triggered event 'delete'.
3396  * @param {int} index   Index of the item to be deleted
3397  */
3398 links.Timeline.prototype.confirmDeleteItem = function(index) {
3399     this.applyDelete = true;
3400 
3401     // select the event to be deleted
3402     if (!this.isSelected(index)) {
3403         this.selectItem(index);
3404     }
3405 
3406     // fire a delete event trigger.
3407     // Note that the delete event can be canceled from within an event listener if
3408     // this listener calls the method cancelChange().
3409     this.trigger('delete');
3410 
3411     if (this.applyDelete) {
3412         this.deleteItem(index);
3413     }
3414 
3415     delete this.applyDelete;
3416 };
3417 
3418 /**
3419  * Delete an item
3420  * @param {int} index   Index of the item to be deleted
3421  * @param {boolean} [preventRender=false]   Do not re-render timeline if true
3422  *                                          (optimization for multiple delete)
3423  */
3424 links.Timeline.prototype.deleteItem = function(index, preventRender) {
3425     if (index >= this.items.length) {
3426         throw "Cannot delete row, index out of range";
3427     }
3428 
3429     if (this.selection && this.selection.index !== undefined) {
3430         // adjust the selection
3431         if (this.selection.index == index) {
3432             // item to be deleted is selected
3433             this.unselectItem();
3434         }
3435         else if (this.selection.index > index) {
3436             // update selection index
3437             this.selection.index--;
3438         }
3439     }
3440 
3441     // actually delete the item and remove it from the DOM
3442     var item = this.items.splice(index, 1)[0];
3443     this.renderQueue.hide.push(item);
3444 
3445     // delete the row in the original data table
3446     if (this.data) {
3447         if (google && google.visualization &&
3448             this.data instanceof google.visualization.DataTable) {
3449             this.data.removeRow(index);
3450         }
3451         else if (links.Timeline.isArray(this.data)) {
3452             this.data.splice(index, 1);
3453         }
3454         else {
3455             throw "Cannot delete row from data, unknown data type";
3456         }
3457     }
3458 
3459     // prepare data for clustering, by filtering and sorting by type
3460     if (this.options.cluster) {
3461         this.clusterGenerator.updateData();
3462     }
3463 
3464     if (!preventRender) {
3465         this.render();
3466     }
3467 };
3468 
3469 
3470 /**
3471  * Delete all items
3472  */
3473 links.Timeline.prototype.deleteAllItems = function() {
3474     this.unselectItem();
3475 
3476     // delete the loaded items
3477     this.clearItems();
3478 
3479     // delete the groups
3480     this.deleteGroups();
3481 
3482     // empty original data table
3483     if (this.data) {
3484         if (google && google.visualization &&
3485             this.data instanceof google.visualization.DataTable) {
3486             this.data.removeRows(0, this.data.getNumberOfRows());
3487         }
3488         else if (links.Timeline.isArray(this.data)) {
3489             this.data.splice(0, this.data.length);
3490         }
3491         else {
3492             throw "Cannot delete row from data, unknown data type";
3493         }
3494     }
3495 
3496     // prepare data for clustering, by filtering and sorting by type
3497     if (this.options.cluster) {
3498         this.clusterGenerator.updateData();
3499     }
3500 
3501     this.render();
3502 };
3503 
3504 
3505 /**
3506  * Find the group from a given height in the timeline
3507  * @param {Number} height   Height in the timeline
3508  * @return {Object | undefined} group   The group object, or undefined if out
3509  *                                      of range
3510  */
3511 links.Timeline.prototype.getGroupFromHeight = function(height) {
3512     var i,
3513         group,
3514         groups = this.groups;
3515 
3516     if (groups.length) {
3517         if (this.options.axisOnTop) {
3518             for (i = groups.length - 1; i >= 0; i--) {
3519                 group = groups[i];
3520                 if (height > group.top) {
3521                     return group;
3522                 }
3523             }
3524         }
3525         else {
3526             for (i = 0; i < groups.length; i++) {
3527                 group = groups[i];
3528                 if (height > group.top) {
3529                     return group;
3530                 }
3531             }
3532         }
3533 
3534         return group; // return the last group
3535     }
3536 
3537     return undefined;
3538 };
3539 
3540 /**
3541  * @constructor links.Timeline.Item
3542  * @param {Object} data       Object containing parameters start, end
3543  *                            content, group, type, editable.
3544  * @param {Object} [options]  Options to set initial property values
3545  *                                {Number} top
3546  *                                {Number} left
3547  *                                {Number} width
3548  *                                {Number} height
3549  */
3550 links.Timeline.Item = function (data, options) {
3551     if (data) {
3552         /* TODO: use parseJSONDate as soon as it is tested and working (in two directions)
3553          this.start = links.Timeline.parseJSONDate(data.start);
3554          this.end = links.Timeline.parseJSONDate(data.end);
3555          */
3556         this.start = data.start;
3557         this.end = data.end;
3558         this.content = data.content;
3559         this.className = data.className;
3560         this.editable = data.editable;
3561         this.group = data.group;
3562         this.type = data.type;
3563     }
3564     this.top = 0;
3565     this.left = 0;
3566     this.width = 0;
3567     this.height = 0;
3568     this.lineWidth = 0;
3569     this.dotWidth = 0;
3570     this.dotHeight = 0;
3571 
3572     this.rendered = false; // true when the item is draw in the Timeline DOM
3573 
3574     if (options) {
3575         // override the default properties
3576         for (var option in options) {
3577             if (options.hasOwnProperty(option)) {
3578                 this[option] = options[option];
3579             }
3580         }
3581     }
3582 
3583 };
3584 
3585 
3586 
3587 /**
3588  * Reflow the Item: retrieve its actual size from the DOM
3589  * @return {boolean} resized    returns true if the axis is resized
3590  */
3591 links.Timeline.Item.prototype.reflow = function () {
3592     // Should be implemented by sub-prototype
3593     return false;
3594 };
3595 
3596 /**
3597  * Append all image urls present in the items DOM to the provided array
3598  * @param {String[]} imageUrls
3599  */
3600 links.Timeline.Item.prototype.getImageUrls = function (imageUrls) {
3601     if (this.dom) {
3602         links.imageloader.filterImageUrls(this.dom, imageUrls);
3603     }
3604 };
3605 
3606 /**
3607  * Select the item
3608  */
3609 links.Timeline.Item.prototype.select = function () {
3610     // Should be implemented by sub-prototype
3611 };
3612 
3613 /**
3614  * Unselect the item
3615  */
3616 links.Timeline.Item.prototype.unselect = function () {
3617     // Should be implemented by sub-prototype
3618 };
3619 
3620 /**
3621  * Creates the DOM for the item, depending on its type
3622  * @return {Element | undefined}
3623  */
3624 links.Timeline.Item.prototype.createDOM = function () {
3625     // Should be implemented by sub-prototype
3626 };
3627 
3628 /**
3629  * Append the items DOM to the given HTML container. If items DOM does not yet
3630  * exist, it will be created first.
3631  * @param {Element} container
3632  */
3633 links.Timeline.Item.prototype.showDOM = function (container) {
3634     // Should be implemented by sub-prototype
3635 };
3636 
3637 /**
3638  * Remove the items DOM from the current HTML container
3639  * @param {Element} container
3640  */
3641 links.Timeline.Item.prototype.hideDOM = function (container) {
3642     // Should be implemented by sub-prototype
3643 };
3644 
3645 /**
3646  * Update the DOM of the item. This will update the content and the classes
3647  * of the item
3648  */
3649 links.Timeline.Item.prototype.updateDOM = function () {
3650     // Should be implemented by sub-prototype
3651 };
3652 
3653 /**
3654  * Reposition the item, recalculate its left, top, and width, using the current
3655  * range of the timeline and the timeline options.
3656  * @param {links.Timeline} timeline
3657  */
3658 links.Timeline.Item.prototype.updatePosition = function (timeline) {
3659     // Should be implemented by sub-prototype
3660 };
3661 
3662 /**
3663  * Check if the item is drawn in the timeline (i.e. the DOM of the item is
3664  * attached to the frame. You may also just request the parameter item.rendered
3665  * @return {boolean} rendered
3666  */
3667 links.Timeline.Item.prototype.isRendered = function () {
3668     return this.rendered;
3669 };
3670 
3671 /**
3672  * Check if the item is located in the visible area of the timeline, and
3673  * not part of a cluster
3674  * @param {Date} start
3675  * @param {Date} end
3676  * @return {boolean} visible
3677  */
3678 links.Timeline.Item.prototype.isVisible = function (start, end) {
3679     // Should be implemented by sub-prototype
3680     return false;
3681 };
3682 
3683 /**
3684  * Reposition the item
3685  * @param {Number} left
3686  * @param {Number} right
3687  */
3688 links.Timeline.Item.prototype.setPosition = function (left, right) {
3689     // Should be implemented by sub-prototype
3690 };
3691 
3692 /**
3693  * Calculate the left position of the item
3694  * @param {links.Timeline} timeline
3695  * @return {Number} left
3696  */
3697 links.Timeline.Item.prototype.getLeft = function (timeline) {
3698     // Should be implemented by sub-prototype
3699     return 0;
3700 };
3701 
3702 /**
3703  * Calculate the right position of the item
3704  * @param {links.Timeline} timeline
3705  * @return {Number} right
3706  */
3707 links.Timeline.Item.prototype.getRight = function (timeline) {
3708     // Should be implemented by sub-prototype
3709     return 0;
3710 };
3711 
3712 /**
3713  * Calculate the width of the item
3714  * @param {links.Timeline} timeline
3715  * @return {Number} width
3716  */
3717 links.Timeline.Item.prototype.getWidth = function (timeline) {
3718     // Should be implemented by sub-prototype
3719     return this.width || 0; // last rendered width
3720 };
3721 
3722 
3723 /**
3724  * @constructor links.Timeline.ItemBox
3725  * @extends links.Timeline.Item
3726  * @param {Object} data       Object containing parameters start, end
3727  *                            content, group, type, className, editable.
3728  * @param {Object} [options]  Options to set initial property values
3729  *                                {Number} top
3730  *                                {Number} left
3731  *                                {Number} width
3732  *                                {Number} height
3733  */
3734 links.Timeline.ItemBox = function (data, options) {
3735     links.Timeline.Item.call(this, data, options);
3736 };
3737 
3738 links.Timeline.ItemBox.prototype = new links.Timeline.Item();
3739 
3740 /**
3741  * Reflow the Item: retrieve its actual size from the DOM
3742  * @return {boolean} resized    returns true if the axis is resized
3743  * @override
3744  */
3745 links.Timeline.ItemBox.prototype.reflow = function () {
3746     var dom = this.dom,
3747         dotHeight = dom.dot.offsetHeight,
3748         dotWidth = dom.dot.offsetWidth,
3749         lineWidth = dom.line.offsetWidth,
3750         resized = (
3751             (this.dotHeight != dotHeight) ||
3752                 (this.dotWidth != dotWidth) ||
3753                 (this.lineWidth != lineWidth)
3754             );
3755 
3756     this.dotHeight = dotHeight;
3757     this.dotWidth = dotWidth;
3758     this.lineWidth = lineWidth;
3759 
3760     return resized;
3761 };
3762 
3763 /**
3764  * Select the item
3765  * @override
3766  */
3767 links.Timeline.ItemBox.prototype.select = function () {
3768     var dom = this.dom;
3769     links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
3770     links.Timeline.addClassName(dom.line, 'timeline-event-selected ui-state-active');
3771     links.Timeline.addClassName(dom.dot, 'timeline-event-selected ui-state-active');
3772 };
3773 
3774 /**
3775  * Unselect the item
3776  * @override
3777  */
3778 links.Timeline.ItemBox.prototype.unselect = function () {
3779     var dom = this.dom;
3780     links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
3781     links.Timeline.removeClassName(dom.line, 'timeline-event-selected ui-state-active');
3782     links.Timeline.removeClassName(dom.dot, 'timeline-event-selected ui-state-active');
3783 };
3784 
3785 /**
3786  * Creates the DOM for the item, depending on its type
3787  * @return {Element | undefined}
3788  * @override
3789  */
3790 links.Timeline.ItemBox.prototype.createDOM = function () {
3791     // background box
3792     var divBox = document.createElement("DIV");
3793     divBox.style.position = "absolute";
3794     divBox.style.left = this.left + "px";
3795     divBox.style.top = this.top + "px";
3796 
3797     // contents box (inside the background box). used for making margins
3798     var divContent = document.createElement("DIV");
3799     divContent.className = "timeline-event-content";
3800     divContent.innerHTML = this.content;
3801     divBox.appendChild(divContent);
3802 
3803     // line to axis
3804     var divLine = document.createElement("DIV");
3805     divLine.style.position = "absolute";
3806     divLine.style.width = "0px";
3807     // important: the vertical line is added at the front of the list of elements,
3808     // so it will be drawn behind all boxes and ranges
3809     divBox.line = divLine;
3810 
3811     // dot on axis
3812     var divDot = document.createElement("DIV");
3813     divDot.style.position = "absolute";
3814     divDot.style.width  = "0px";
3815     divDot.style.height = "0px";
3816     divBox.dot = divDot;
3817 
3818     this.dom = divBox;
3819     this.updateDOM();
3820 
3821     return divBox;
3822 };
3823 
3824 /**
3825  * Append the items DOM to the given HTML container. If items DOM does not yet
3826  * exist, it will be created first.
3827  * @param {Element} container
3828  * @override
3829  */
3830 links.Timeline.ItemBox.prototype.showDOM = function (container) {
3831     var dom = this.dom;
3832     if (!dom) {
3833         dom = this.createDOM();
3834     }
3835 
3836     if (dom.parentNode != container) {
3837         if (dom.parentNode) {
3838             // container is changed. remove from old container
3839             this.hideDOM();
3840         }
3841 
3842         // append to this container
3843         container.appendChild(dom);
3844         container.insertBefore(dom.line, container.firstChild);
3845         // Note: line must be added in front of the this,
3846         //       such that it stays below all this
3847         container.appendChild(dom.dot);
3848         this.rendered = true;
3849     }
3850 };
3851 
3852 /**
3853  * Remove the items DOM from the current HTML container, but keep the DOM in
3854  * memory
3855  * @override
3856  */
3857 links.Timeline.ItemBox.prototype.hideDOM = function () {
3858     var dom = this.dom;
3859     if (dom) {
3860         if (dom.parentNode) {
3861             dom.parentNode.removeChild(dom);
3862         }
3863         if (dom.line && dom.line.parentNode) {
3864             dom.line.parentNode.removeChild(dom.line);
3865         }
3866         if (dom.dot && dom.dot.parentNode) {
3867             dom.dot.parentNode.removeChild(dom.dot);
3868         }
3869         this.rendered = false;
3870     }
3871 };
3872 
3873 /**
3874  * Update the DOM of the item. This will update the content and the classes
3875  * of the item
3876  * @override
3877  */
3878 links.Timeline.ItemBox.prototype.updateDOM = function () {
3879     var divBox = this.dom;
3880     if (divBox) {
3881         var divLine = divBox.line;
3882         var divDot = divBox.dot;
3883 
3884         // update contents
3885         divBox.firstChild.innerHTML = this.content;
3886 
3887         // update class
3888         divBox.className = "timeline-event timeline-event-box ui-widget ui-state-default";
3889         divLine.className = "timeline-event timeline-event-line ui-widget ui-state-default";
3890         divDot.className  = "timeline-event timeline-event-dot ui-widget ui-state-default";
3891 
3892         if (this.isCluster) {
3893             links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
3894             links.Timeline.addClassName(divLine, 'timeline-event-cluster ui-widget-header');
3895             links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header');
3896         }
3897 
3898         // add item specific class name when provided
3899         if (this.className) {
3900             links.Timeline.addClassName(divBox, this.className);
3901             links.Timeline.addClassName(divLine, this.className);
3902             links.Timeline.addClassName(divDot, this.className);
3903         }
3904 
3905         // TODO: apply selected className?
3906     }
3907 };
3908 
3909 /**
3910  * Reposition the item, recalculate its left, top, and width, using the current
3911  * range of the timeline and the timeline options.
3912  * @param {links.Timeline} timeline
3913  * @override
3914  */
3915 links.Timeline.ItemBox.prototype.updatePosition = function (timeline) {
3916     var dom = this.dom;
3917     if (dom) {
3918         var left = timeline.timeToScreen(this.start),
3919             axisOnTop = timeline.options.axisOnTop,
3920             axisTop = timeline.size.axis.top,
3921             axisHeight = timeline.size.axis.height,
3922             boxAlign = (timeline.options.box && timeline.options.box.align) ?
3923                 timeline.options.box.align : undefined;
3924 
3925         dom.style.top = this.top + "px";
3926         if (boxAlign == 'right') {
3927             dom.style.left = (left - this.width) + "px";
3928         }
3929         else if (boxAlign == 'left') {
3930             dom.style.left = (left) + "px";
3931         }
3932         else { // default or 'center'
3933             dom.style.left = (left - this.width/2) + "px";
3934         }
3935 
3936         var line = dom.line;
3937         var dot = dom.dot;
3938         line.style.left = (left - this.lineWidth/2) + "px";
3939         dot.style.left = (left - this.dotWidth/2) + "px";
3940         if (axisOnTop) {
3941             line.style.top = axisHeight + "px";
3942             line.style.height = Math.max(this.top - axisHeight, 0) + "px";
3943             dot.style.top = (axisHeight - this.dotHeight/2) + "px";
3944         }
3945         else {
3946             line.style.top = (this.top + this.height) + "px";
3947             line.style.height = Math.max(axisTop - this.top - this.height, 0) + "px";
3948             dot.style.top = (axisTop - this.dotHeight/2) + "px";
3949         }
3950     }
3951 };
3952 
3953 /**
3954  * Check if the item is visible in the timeline, and not part of a cluster
3955  * @param {Date} start
3956  * @param {Date} end
3957  * @return {Boolean} visible
3958  * @override
3959  */
3960 links.Timeline.ItemBox.prototype.isVisible = function (start, end) {
3961     if (this.cluster) {
3962         return false;
3963     }
3964 
3965     return (this.start > start) && (this.start < end);
3966 };
3967 
3968 /**
3969  * Reposition the item
3970  * @param {Number} left
3971  * @param {Number} right
3972  * @override
3973  */
3974 links.Timeline.ItemBox.prototype.setPosition = function (left, right) {
3975     var dom = this.dom;
3976 
3977     dom.style.left = (left - this.width / 2) + "px";
3978     dom.line.style.left = (left - this.lineWidth / 2) + "px";
3979     dom.dot.style.left = (left - this.dotWidth / 2) + "px";
3980 
3981     if (this.group) {
3982         this.top = this.group.top;
3983         dom.style.top = this.top + 'px';
3984     }
3985 };
3986 
3987 /**
3988  * Calculate the left position of the item
3989  * @param {links.Timeline} timeline
3990  * @return {Number} left
3991  * @override
3992  */
3993 links.Timeline.ItemBox.prototype.getLeft = function (timeline) {
3994     var boxAlign = (timeline.options.box && timeline.options.box.align) ?
3995         timeline.options.box.align : undefined;
3996 
3997     var left = timeline.timeToScreen(this.start);
3998     if (boxAlign == 'right') {
3999         left = left - width;
4000     }
4001     else { // default or 'center'
4002         left = (left - this.width / 2);
4003     }
4004 
4005     return left;
4006 };
4007 
4008 /**
4009  * Calculate the right position of the item
4010  * @param {links.Timeline} timeline
4011  * @return {Number} right
4012  * @override
4013  */
4014 links.Timeline.ItemBox.prototype.getRight = function (timeline) {
4015     var boxAlign = (timeline.options.box && timeline.options.box.align) ?
4016         timeline.options.box.align : undefined;
4017 
4018     var left = timeline.timeToScreen(this.start);
4019     var right;
4020     if (boxAlign == 'right') {
4021         right = left;
4022     }
4023     else if (boxAlign == 'left') {
4024         right = (left + this.width);
4025     }
4026     else { // default or 'center'
4027         right = (left + this.width / 2);
4028     }
4029 
4030     return right;
4031 };
4032 
4033 /**
4034  * @constructor links.Timeline.ItemRange
4035  * @extends links.Timeline.Item
4036  * @param {Object} data       Object containing parameters start, end
4037  *                            content, group, type, className, editable.
4038  * @param {Object} [options]  Options to set initial property values
4039  *                                {Number} top
4040  *                                {Number} left
4041  *                                {Number} width
4042  *                                {Number} height
4043  */
4044 links.Timeline.ItemRange = function (data, options) {
4045     links.Timeline.Item.call(this, data, options);
4046 };
4047 
4048 links.Timeline.ItemRange.prototype = new links.Timeline.Item();
4049 
4050 /**
4051  * Select the item
4052  * @override
4053  */
4054 links.Timeline.ItemRange.prototype.select = function () {
4055     var dom = this.dom;
4056     links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
4057 };
4058 
4059 /**
4060  * Unselect the item
4061  * @override
4062  */
4063 links.Timeline.ItemRange.prototype.unselect = function () {
4064     var dom = this.dom;
4065     links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
4066 };
4067 
4068 /**
4069  * Creates the DOM for the item, depending on its type
4070  * @return {Element | undefined}
4071  * @override
4072  */
4073 links.Timeline.ItemRange.prototype.createDOM = function () {
4074     // background box
4075     var divBox = document.createElement("DIV");
4076     divBox.style.position = "absolute";
4077 
4078     // contents box
4079     var divContent = document.createElement("DIV");
4080     divContent.className = "timeline-event-content";
4081     divBox.appendChild(divContent);
4082 
4083     this.dom = divBox;
4084     this.updateDOM();
4085 
4086     return divBox;
4087 };
4088 
4089 /**
4090  * Append the items DOM to the given HTML container. If items DOM does not yet
4091  * exist, it will be created first.
4092  * @param {Element} container
4093  * @override
4094  */
4095 links.Timeline.ItemRange.prototype.showDOM = function (container) {
4096     var dom = this.dom;
4097     if (!dom) {
4098         dom = this.createDOM();
4099     }
4100 
4101     if (dom.parentNode != container) {
4102         if (dom.parentNode) {
4103             // container changed. remove the item from the old container
4104             this.hideDOM();
4105         }
4106 
4107         // append to the new container
4108         container.appendChild(dom);
4109         this.rendered = true;
4110     }
4111 };
4112 
4113 /**
4114  * Remove the items DOM from the current HTML container
4115  * The DOM will be kept in memory
4116  * @override
4117  */
4118 links.Timeline.ItemRange.prototype.hideDOM = function () {
4119     var dom = this.dom;
4120     if (dom) {
4121         if (dom.parentNode) {
4122             dom.parentNode.removeChild(dom);
4123         }
4124         this.rendered = false;
4125     }
4126 };
4127 
4128 /**
4129  * Update the DOM of the item. This will update the content and the classes
4130  * of the item
4131  * @override
4132  */
4133 links.Timeline.ItemRange.prototype.updateDOM = function () {
4134     var divBox = this.dom;
4135     if (divBox) {
4136         // update contents
4137         divBox.firstChild.innerHTML = this.content;
4138 
4139         // update class
4140         divBox.className = "timeline-event timeline-event-range ui-widget ui-state-default";
4141 
4142         if (this.isCluster) {
4143             links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
4144         }
4145 
4146         // add item specific class name when provided
4147         if (this.className) {
4148             links.Timeline.addClassName(divBox, this.className);
4149         }
4150 
4151         // TODO: apply selected className?
4152     }
4153 };
4154 
4155 /**
4156  * Reposition the item, recalculate its left, top, and width, using the current
4157  * range of the timeline and the timeline options. *
4158  * @param {links.Timeline} timeline
4159  * @override
4160  */
4161 links.Timeline.ItemRange.prototype.updatePosition = function (timeline) {
4162     var dom = this.dom;
4163     if (dom) {
4164         var contentWidth = timeline.size.contentWidth,
4165             left = timeline.timeToScreen(this.start),
4166             right = timeline.timeToScreen(this.end);
4167 
4168         // limit the width of the this, as browsers cannot draw very wide divs
4169         if (left < -contentWidth) {
4170             left = -contentWidth;
4171         }
4172         if (right > 2 * contentWidth) {
4173             right = 2 * contentWidth;
4174         }
4175 
4176         dom.style.top = this.top + "px";
4177         dom.style.left = left + "px";
4178         //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth
4179         dom.style.width = Math.max(right - left, 1) + "px";
4180     }
4181 };
4182 
4183 /**
4184  * Check if the item is visible in the timeline, and not part of a cluster
4185  * @param {Number} start
4186  * @param {Number} end
4187  * @return {boolean} visible
4188  * @override
4189  */
4190 links.Timeline.ItemRange.prototype.isVisible = function (start, end) {
4191     if (this.cluster) {
4192         return false;
4193     }
4194 
4195     return (this.end > start)
4196         && (this.start < end);
4197 };
4198 
4199 /**
4200  * Reposition the item
4201  * @param {Number} left
4202  * @param {Number} right
4203  * @override
4204  */
4205 links.Timeline.ItemRange.prototype.setPosition = function (left, right) {
4206     var dom = this.dom;
4207 
4208     dom.style.left = left + 'px';
4209     dom.style.width = (right - left) + 'px';
4210 
4211     if (this.group) {
4212         this.top = this.group.top;
4213         dom.style.top = this.top + 'px';
4214     }
4215 };
4216 
4217 /**
4218  * Calculate the left position of the item
4219  * @param {links.Timeline} timeline
4220  * @return {Number} left
4221  * @override
4222  */
4223 links.Timeline.ItemRange.prototype.getLeft = function (timeline) {
4224     return timeline.timeToScreen(this.start);
4225 };
4226 
4227 /**
4228  * Calculate the right position of the item
4229  * @param {links.Timeline} timeline
4230  * @return {Number} right
4231  * @override
4232  */
4233 links.Timeline.ItemRange.prototype.getRight = function (timeline) {
4234     return timeline.timeToScreen(this.end);
4235 };
4236 
4237 /**
4238  * Calculate the width of the item
4239  * @param {links.Timeline} timeline
4240  * @return {Number} width
4241  * @override
4242  */
4243 links.Timeline.ItemRange.prototype.getWidth = function (timeline) {
4244     return timeline.timeToScreen(this.end) - timeline.timeToScreen(this.start);
4245 };
4246 
4247 /**
4248  * @constructor links.Timeline.ItemFloatingRange
4249  * @extends links.Timeline.Item
4250  * @param {Object} data       Object containing parameters start, end
4251  *                            content, group, type, className, editable.
4252  * @param {Object} [options]  Options to set initial property values
4253  *                                {Number} top
4254  *                                {Number} left
4255  *                                {Number} width
4256  *                                {Number} height
4257  */
4258 links.Timeline.ItemFloatingRange = function (data, options) {
4259     links.Timeline.Item.call(this, data, options);
4260 };
4261 
4262 links.Timeline.ItemFloatingRange.prototype = new links.Timeline.Item();
4263 
4264 /**
4265  * Select the item
4266  * @override
4267  */
4268 links.Timeline.ItemFloatingRange.prototype.select = function () {
4269     var dom = this.dom;
4270     links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
4271 };
4272 
4273 /**
4274  * Unselect the item
4275  * @override
4276  */
4277 links.Timeline.ItemFloatingRange.prototype.unselect = function () {
4278     var dom = this.dom;
4279     links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
4280 };
4281 
4282 /**
4283  * Creates the DOM for the item, depending on its type
4284  * @return {Element | undefined}
4285  * @override
4286  */
4287 links.Timeline.ItemFloatingRange.prototype.createDOM = function () {
4288     // background box
4289     var divBox = document.createElement("DIV");
4290     divBox.style.position = "absolute";
4291 
4292     // contents box
4293     var divContent = document.createElement("DIV");
4294     divContent.className = "timeline-event-content";
4295     divBox.appendChild(divContent);
4296 
4297     this.dom = divBox;
4298     this.updateDOM();
4299 
4300     return divBox;
4301 };
4302 
4303 /**
4304  * Append the items DOM to the given HTML container. If items DOM does not yet
4305  * exist, it will be created first.
4306  * @param {Element} container
4307  * @override
4308  */
4309 links.Timeline.ItemFloatingRange.prototype.showDOM = function (container) {
4310     var dom = this.dom;
4311     if (!dom) {
4312         dom = this.createDOM();
4313     }
4314 
4315     if (dom.parentNode != container) {
4316         if (dom.parentNode) {
4317             // container changed. remove the item from the old container
4318             this.hideDOM();
4319         }
4320 
4321         // append to the new container
4322         container.appendChild(dom);
4323         this.rendered = true;
4324     }
4325 };
4326 
4327 /**
4328  * Remove the items DOM from the current HTML container
4329  * The DOM will be kept in memory
4330  * @override
4331  */
4332 links.Timeline.ItemFloatingRange.prototype.hideDOM = function () {
4333     var dom = this.dom;
4334     if (dom) {
4335         if (dom.parentNode) {
4336             dom.parentNode.removeChild(dom);
4337         }
4338         this.rendered = false;
4339     }
4340 };
4341 
4342 /**
4343  * Update the DOM of the item. This will update the content and the classes
4344  * of the item
4345  * @override
4346  */
4347 links.Timeline.ItemFloatingRange.prototype.updateDOM = function () {
4348     var divBox = this.dom;
4349     if (divBox) {
4350         // update contents
4351         divBox.firstChild.innerHTML = this.content;
4352 
4353         // update class
4354         divBox.className = "timeline-event timeline-event-range ui-widget ui-state-default";
4355 
4356         if (this.isCluster) {
4357             links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
4358         }
4359 
4360         // add item specific class name when provided
4361         if (this.className) {
4362             links.Timeline.addClassName(divBox, this.className);
4363         }
4364 
4365         // TODO: apply selected className?
4366     }
4367 };
4368 
4369 /**
4370  * Reposition the item, recalculate its left, top, and width, using the current
4371  * range of the timeline and the timeline options. *
4372  * @param {links.Timeline} timeline
4373  * @override
4374  */
4375 links.Timeline.ItemFloatingRange.prototype.updatePosition = function (timeline) {
4376     var dom = this.dom;
4377     if (dom) {
4378         var contentWidth = timeline.size.contentWidth,
4379             left = this.getLeft(timeline), // NH use getLeft
4380             right = this.getRight(timeline); // NH use getRight;
4381 
4382         // limit the width of the this, as browsers cannot draw very wide divs
4383         if (left < -contentWidth) {
4384             left = -contentWidth;
4385         }
4386         if (right > 2 * contentWidth) {
4387             right = 2 * contentWidth;
4388         }
4389 
4390         dom.style.top = this.top + "px";
4391         dom.style.left = left + "px";
4392         //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth
4393         dom.style.width = Math.max(right - left, 1) + "px";
4394     }
4395 };
4396 
4397 /**
4398  * Check if the item is visible in the timeline, and not part of a cluster
4399  * @param {Number} start
4400  * @param {Number} end
4401  * @return {boolean} visible
4402  * @override
4403  */
4404 links.Timeline.ItemFloatingRange.prototype.isVisible = function (start, end) {
4405     if (this.cluster) {
4406         return false;
4407     }
4408 
4409 	// NH check for no end value
4410 	if (this.end && this.start) {
4411 		return (this.end > start)
4412 			&& (this.start < end);
4413 	} else if (this.start) {
4414 		return (this.start < end);
4415 	} else if (this.end) {
4416         return (this.end > start);
4417     } else {return true;}
4418 };
4419 
4420 /**
4421  * Reposition the item
4422  * @param {Number} left
4423  * @param {Number} right
4424  * @override
4425  */
4426 links.Timeline.ItemFloatingRange.prototype.setPosition = function (left, right) {
4427     var dom = this.dom;
4428 
4429     dom.style.left = left + 'px';
4430     dom.style.width = (right - left) + 'px';
4431 
4432     if (this.group) {
4433         this.top = this.group.top;
4434         dom.style.top = this.top + 'px';
4435     }
4436 };
4437 
4438 /**
4439  * Calculate the left position of the item
4440  * @param {links.Timeline} timeline
4441  * @return {Number} left
4442  * @override
4443  */
4444 links.Timeline.ItemFloatingRange.prototype.getLeft = function (timeline) {
4445     // NH check for no start value
4446 	if (this.start) {
4447 		return timeline.timeToScreen(this.start);
4448 	} else {
4449 		return 0;
4450 	}
4451 };
4452 
4453 /**
4454  * Calculate the right position of the item
4455  * @param {links.Timeline} timeline
4456  * @return {Number} right
4457  * @override
4458  */
4459 links.Timeline.ItemFloatingRange.prototype.getRight = function (timeline) {
4460     // NH check for no end value
4461 	if (this.end) {
4462 		return timeline.timeToScreen(this.end);
4463 	} else {
4464 		return timeline.size.contentWidth;
4465 	}
4466 };
4467 
4468 /**
4469  * Calculate the width of the item
4470  * @param {links.Timeline} timeline
4471  * @return {Number} width
4472  * @override
4473  */
4474 links.Timeline.ItemFloatingRange.prototype.getWidth = function (timeline) {
4475     return this.getRight(timeline) - this.getLeft(timeline);
4476 };
4477 
4478 /**
4479  * @constructor links.Timeline.ItemDot
4480  * @extends links.Timeline.Item
4481  * @param {Object} data       Object containing parameters start, end
4482  *                            content, group, type, className, editable.
4483  * @param {Object} [options]  Options to set initial property values
4484  *                                {Number} top
4485  *                                {Number} left
4486  *                                {Number} width
4487  *                                {Number} height
4488  */
4489 links.Timeline.ItemDot = function (data, options) {
4490     links.Timeline.Item.call(this, data, options);
4491 };
4492 
4493 links.Timeline.ItemDot.prototype = new links.Timeline.Item();
4494 
4495 /**
4496  * Reflow the Item: retrieve its actual size from the DOM
4497  * @return {boolean} resized    returns true if the axis is resized
4498  * @override
4499  */
4500 links.Timeline.ItemDot.prototype.reflow = function () {
4501     var dom = this.dom,
4502         dotHeight = dom.dot.offsetHeight,
4503         dotWidth = dom.dot.offsetWidth,
4504         contentHeight = dom.content.offsetHeight,
4505         resized = (
4506             (this.dotHeight != dotHeight) ||
4507                 (this.dotWidth != dotWidth) ||
4508                 (this.contentHeight != contentHeight)
4509             );
4510 
4511     this.dotHeight = dotHeight;
4512     this.dotWidth = dotWidth;
4513     this.contentHeight = contentHeight;
4514 
4515     return resized;
4516 };
4517 
4518 /**
4519  * Select the item
4520  * @override
4521  */
4522 links.Timeline.ItemDot.prototype.select = function () {
4523     var dom = this.dom;
4524     links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
4525 };
4526 
4527 /**
4528  * Unselect the item
4529  * @override
4530  */
4531 links.Timeline.ItemDot.prototype.unselect = function () {
4532     var dom = this.dom;
4533     links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
4534 };
4535 
4536 /**
4537  * Creates the DOM for the item, depending on its type
4538  * @return {Element | undefined}
4539  * @override
4540  */
4541 links.Timeline.ItemDot.prototype.createDOM = function () {
4542     // background box
4543     var divBox = document.createElement("DIV");
4544     divBox.style.position = "absolute";
4545 
4546     // contents box, right from the dot
4547     var divContent = document.createElement("DIV");
4548     divContent.className = "timeline-event-content";
4549     divBox.appendChild(divContent);
4550 
4551     // dot at start
4552     var divDot = document.createElement("DIV");
4553     divDot.style.position = "absolute";
4554     divDot.style.width = "0px";
4555     divDot.style.height = "0px";
4556     divBox.appendChild(divDot);
4557 
4558     divBox.content = divContent;
4559     divBox.dot = divDot;
4560 
4561     this.dom = divBox;
4562     this.updateDOM();
4563 
4564     return divBox;
4565 };
4566 
4567 /**
4568  * Append the items DOM to the given HTML container. If items DOM does not yet
4569  * exist, it will be created first.
4570  * @param {Element} container
4571  * @override
4572  */
4573 links.Timeline.ItemDot.prototype.showDOM = function (container) {
4574     var dom = this.dom;
4575     if (!dom) {
4576         dom = this.createDOM();
4577     }
4578 
4579     if (dom.parentNode != container) {
4580         if (dom.parentNode) {
4581             // container changed. remove it from old container first
4582             this.hideDOM();
4583         }
4584 
4585         // append to container
4586         container.appendChild(dom);
4587         this.rendered = true;
4588     }
4589 };
4590 
4591 /**
4592  * Remove the items DOM from the current HTML container
4593  * @override
4594  */
4595 links.Timeline.ItemDot.prototype.hideDOM = function () {
4596     var dom = this.dom;
4597     if (dom) {
4598         if (dom.parentNode) {
4599             dom.parentNode.removeChild(dom);
4600         }
4601         this.rendered = false;
4602     }
4603 };
4604 
4605 /**
4606  * Update the DOM of the item. This will update the content and the classes
4607  * of the item
4608  * @override
4609  */
4610 links.Timeline.ItemDot.prototype.updateDOM = function () {
4611     if (this.dom) {
4612         var divBox = this.dom;
4613         var divDot = divBox.dot;
4614 
4615         // update contents
4616         divBox.firstChild.innerHTML = this.content;
4617 
4618         // update classes
4619         divBox.className = "timeline-event-dot-container";
4620         divDot.className  = "timeline-event timeline-event-dot ui-widget ui-state-default";
4621 
4622         if (this.isCluster) {
4623             links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
4624             links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header');
4625         }
4626 
4627         // add item specific class name when provided
4628         if (this.className) {
4629             links.Timeline.addClassName(divBox, this.className);
4630             links.Timeline.addClassName(divDot, this.className);
4631         }
4632 
4633         // TODO: apply selected className?
4634     }
4635 };
4636 
4637 /**
4638  * Reposition the item, recalculate its left, top, and width, using the current
4639  * range of the timeline and the timeline options. *
4640  * @param {links.Timeline} timeline
4641  * @override
4642  */
4643 links.Timeline.ItemDot.prototype.updatePosition = function (timeline) {
4644     var dom = this.dom;
4645     if (dom) {
4646         var left = timeline.timeToScreen(this.start);
4647 
4648         dom.style.top = this.top + "px";
4649         dom.style.left = (left - this.dotWidth / 2) + "px";
4650 
4651         dom.content.style.marginLeft = (1.5 * this.dotWidth) + "px";
4652         //dom.content.style.marginRight = (0.5 * this.dotWidth) + "px"; // TODO
4653         dom.dot.style.top = ((this.height - this.dotHeight) / 2) + "px";
4654     }
4655 };
4656 
4657 /**
4658  * Check if the item is visible in the timeline, and not part of a cluster.
4659  * @param {Date} start
4660  * @param {Date} end
4661  * @return {boolean} visible
4662  * @override
4663  */
4664 links.Timeline.ItemDot.prototype.isVisible = function (start, end) {
4665     if (this.cluster) {
4666         return false;
4667     }
4668 
4669     return (this.start > start)
4670         && (this.start < end);
4671 };
4672 
4673 /**
4674  * Reposition the item
4675  * @param {Number} left
4676  * @param {Number} right
4677  * @override
4678  */
4679 links.Timeline.ItemDot.prototype.setPosition = function (left, right) {
4680     var dom = this.dom;
4681 
4682     dom.style.left = (left - this.dotWidth / 2) + "px";
4683 
4684     if (this.group) {
4685         this.top = this.group.top;
4686         dom.style.top = this.top + 'px';
4687     }
4688 };
4689 
4690 /**
4691  * Calculate the left position of the item
4692  * @param {links.Timeline} timeline
4693  * @return {Number} left
4694  * @override
4695  */
4696 links.Timeline.ItemDot.prototype.getLeft = function (timeline) {
4697     return timeline.timeToScreen(this.start);
4698 };
4699 
4700 /**
4701  * Calculate the right position of the item
4702  * @param {links.Timeline} timeline
4703  * @return {Number} right
4704  * @override
4705  */
4706 links.Timeline.ItemDot.prototype.getRight = function (timeline) {
4707     return timeline.timeToScreen(this.start) + this.width;
4708 };
4709 
4710 /**
4711  * Retrieve the properties of an item.
4712  * @param {Number} index
4713  * @return {Object} itemData    Object containing item properties:<br>
4714  *                              {Date} start (required),
4715  *                              {Date} end (optional),
4716  *                              {String} content (required),
4717  *                              {String} group (optional),
4718  *                              {String} className (optional)
4719  *                              {boolean} editable (optional)
4720  *                              {String} type (optional)
4721  */
4722 links.Timeline.prototype.getItem = function (index) {
4723     if (index >= this.items.length) {
4724         throw "Cannot get item, index out of range";
4725     }
4726 
4727     // take the original data as start, includes foreign fields
4728     var data = this.data,
4729         itemData;
4730     if (google && google.visualization &&
4731         data instanceof google.visualization.DataTable) {
4732         // map the datatable columns
4733         var cols = links.Timeline.mapColumnIds(data);
4734 
4735         itemData = {};
4736         for (var col in cols) {
4737             if (cols.hasOwnProperty(col)) {
4738                 itemData[col] = this.data.getValue(index, cols[col]);
4739             }
4740         }
4741     }
4742     else if (links.Timeline.isArray(this.data)) {
4743         // read JSON array
4744         itemData = links.Timeline.clone(this.data[index]);
4745     }
4746     else {
4747         throw "Unknown data type. DataTable or Array expected.";
4748     }
4749 
4750     // override the data with current settings of the item (should be the same)
4751     var item = this.items[index];
4752 
4753     itemData.start = new Date(item.start.valueOf());
4754     if (item.end) {
4755         itemData.end = new Date(item.end.valueOf());
4756     }
4757     itemData.content = item.content;
4758     if (item.group) {
4759         itemData.group = this.getGroupName(item.group);
4760     }
4761     if (item.className) {
4762         itemData.className = item.className;
4763     }
4764     if (typeof item.editable !== 'undefined') {
4765         itemData.editable = item.editable;
4766     }
4767     if (item.type) {
4768         itemData.type = item.type;
4769     }
4770 
4771     return itemData;
4772 };
4773 
4774 
4775 /**
4776  * Retrieve the properties of a cluster.
4777  * @param {Number} index
4778  * @return {Object} clusterdata    Object containing cluster properties:<br>
4779  *                              {Date} start (required),
4780  *                              {String} type (optional)
4781  *                              {Array} array with item data as is in getItem()
4782  */
4783 links.Timeline.prototype.getCluster = function (index) {
4784     if (index >= this.clusters.length) {
4785         throw "Cannot get cluster, index out of range";
4786     }
4787 
4788     var clusterData = {},
4789         cluster = this.clusters[index],
4790         clusterItems = cluster.items;
4791     
4792     clusterData.start = new Date(cluster.start.valueOf());
4793     if (cluster.type) {
4794         clusterData.type = cluster.type;
4795     }
4796 
4797     // push cluster item data
4798     clusterData.items = [];
4799     for(var i = 0; i < clusterItems.length; i++){
4800         for(var j = 0; j < this.items.length; j++){
4801             // TODO could be nicer to be able to have the item index into the cluster
4802             if(this.items[j] == clusterItems[i])
4803             {
4804                 clusterData.items.push(this.getItem(j));
4805                 break;
4806             }
4807 
4808         }
4809     }
4810 
4811     return clusterData;
4812 };
4813 
4814 /**
4815  * Add a new item.
4816  * @param {Object} itemData     Object containing item properties:<br>
4817  *                              {Date} start (required),
4818  *                              {Date} end (optional),
4819  *                              {String} content (required),
4820  *                              {String} group (optional)
4821  *                              {String} className (optional)
4822  *                              {Boolean} editable (optional)
4823  *                              {String} type (optional)
4824  * @param {boolean} [preventRender=false]   Do not re-render timeline if true
4825  */
4826 links.Timeline.prototype.addItem = function (itemData, preventRender) {
4827     var itemsData = [
4828         itemData
4829     ];
4830 
4831     this.addItems(itemsData, preventRender);
4832 };
4833 
4834 /**
4835  * Add new items.
4836  * @param {Array} itemsData An array containing Objects.
4837  *                          The objects must have the following parameters:
4838  *                            {Date} start,
4839  *                            {Date} end,
4840  *                            {String} content with text or HTML code,
4841  *                            {String} group (optional)
4842  *                            {String} className (optional)
4843  *                            {String} editable (optional)
4844  *                            {String} type (optional)
4845  * @param {boolean} [preventRender=false]   Do not re-render timeline if true
4846  */
4847 links.Timeline.prototype.addItems = function (itemsData, preventRender) {
4848     var timeline = this,
4849         items = this.items;
4850 
4851     // append the items
4852     itemsData.forEach(function (itemData) {
4853         var index = items.length;
4854         items.push(timeline.createItem(itemData));
4855         timeline.updateData(index, itemData);
4856 
4857         // note: there is no need to add the item to the renderQueue, that
4858         // will be done when this.render() is executed and all items are
4859         // filtered again.
4860     });
4861 
4862     // prepare data for clustering, by filtering and sorting by type
4863     if (this.options.cluster) {
4864         this.clusterGenerator.updateData();
4865     }
4866 
4867     if (!preventRender) {
4868         this.render({
4869             animate: false
4870         });
4871     }
4872 };
4873 
4874 /**
4875  * Create an item object, containing all needed parameters
4876  * @param {Object} itemData  Object containing parameters start, end
4877  *                           content, group.
4878  * @return {Object} item
4879  */
4880 links.Timeline.prototype.createItem = function(itemData) {
4881     var type = itemData.type || (itemData.end ? 'range' : this.options.style);
4882     var data = links.Timeline.clone(itemData);
4883     data.type = type;
4884     data.group = this.getGroup(itemData.group);
4885     // TODO: optimize this, when creating an item, all data is copied twice...
4886 
4887     // TODO: is initialTop needed?
4888     var initialTop,
4889         options = this.options;
4890     if (options.axisOnTop) {
4891         initialTop = this.size.axis.height + options.eventMarginAxis + options.eventMargin / 2;
4892     }
4893     else {
4894         initialTop = this.size.contentHeight - options.eventMarginAxis - options.eventMargin / 2;
4895     }
4896 
4897     if (type in this.itemTypes) {
4898         return new this.itemTypes[type](data, {'top': initialTop})
4899     }
4900 
4901     console.log('ERROR: Unknown event type "' + type + '"');
4902     return new links.Timeline.Item(data, {
4903         'top': initialTop
4904     });
4905 };
4906 
4907 /**
4908  * Edit an item
4909  * @param {Number} index
4910  * @param {Object} itemData     Object containing item properties:<br>
4911  *                              {Date} start (required),
4912  *                              {Date} end (optional),
4913  *                              {String} content (required),
4914  *                              {String} group (optional)
4915  * @param {boolean} [preventRender=false]   Do not re-render timeline if true
4916  */
4917 links.Timeline.prototype.changeItem = function (index, itemData, preventRender) {
4918     var oldItem = this.items[index];
4919     if (!oldItem) {
4920         throw "Cannot change item, index out of range";
4921     }
4922 
4923     // replace item, merge the changes
4924     var newItem = this.createItem({
4925         'start':   itemData.hasOwnProperty('start') ?   itemData.start :   oldItem.start,
4926         'end':     itemData.hasOwnProperty('end') ?     itemData.end :     oldItem.end,
4927         'content': itemData.hasOwnProperty('content') ? itemData.content : oldItem.content,
4928         'group':   itemData.hasOwnProperty('group') ?   itemData.group :   this.getGroupName(oldItem.group),
4929         'className': itemData.hasOwnProperty('className') ? itemData.className : oldItem.className,
4930         'editable':  itemData.hasOwnProperty('editable') ?  itemData.editable :  oldItem.editable,
4931         'type':      itemData.hasOwnProperty('type') ?      itemData.type :      oldItem.type
4932     });
4933     this.items[index] = newItem;
4934 
4935     // append the changes to the render queue
4936     this.renderQueue.hide.push(oldItem);
4937     this.renderQueue.show.push(newItem);
4938 
4939     // update the original data table
4940     this.updateData(index, itemData);
4941 
4942     // prepare data for clustering, by filtering and sorting by type
4943     if (this.options.cluster) {
4944         this.clusterGenerator.updateData();
4945     }
4946 
4947     if (!preventRender) {
4948         // redraw timeline
4949         this.render({
4950             animate: false
4951         });
4952 
4953         if (this.selection && this.selection.index == index) {
4954             newItem.select();
4955         }
4956     }
4957 };
4958 
4959 /**
4960  * Delete all groups
4961  */
4962 links.Timeline.prototype.deleteGroups = function () {
4963     this.groups = [];
4964     this.groupIndexes = {};
4965 };
4966 
4967 
4968 /**
4969  * Get a group by the group name. When the group does not exist,
4970  * it will be created.
4971  * @param {String} groupName   the name of the group
4972  * @return {Object} groupObject
4973  */
4974 links.Timeline.prototype.getGroup = function (groupName) {
4975     var groups = this.groups,
4976         groupIndexes = this.groupIndexes,
4977         groupObj = undefined;
4978 
4979     var groupIndex = groupIndexes[groupName];
4980     if (groupIndex == undefined && groupName != undefined) { // not null or undefined
4981         groupObj = {
4982             'content': groupName,
4983             'labelTop': 0,
4984             'lineTop': 0
4985             // note: this object will lateron get addition information,
4986             //       such as height and width of the group
4987         };
4988         groups.push(groupObj);
4989         // sort the groups
4990         if (this.options.groupsOrder == true) {
4991             groups = groups.sort(function (a, b) {
4992                 if (a.content > b.content) {
4993                     return 1;
4994 		        }
4995 		        if (a.content < b.content) {
4996 		            return -1;
4997 		        }
4998 		        return 0;
4999         	});
5000         } else if (typeof(this.options.groupsOrder) == "function") {
5001         	groups = groups.sort(this.options.groupsOrder)
5002         }
5003 
5004         // rebuilt the groupIndexes
5005         for (var i = 0, iMax = groups.length; i < iMax; i++) {
5006             groupIndexes[groups[i].content] = i;
5007         }
5008     }
5009     else {
5010         groupObj = groups[groupIndex];
5011     }
5012 
5013     return groupObj;
5014 };
5015 
5016 /**
5017  * Get the group name from a group object.
5018  * @param {Object} groupObj
5019  * @return {String} groupName   the name of the group, or undefined when group
5020  *                              was not provided
5021  */
5022 links.Timeline.prototype.getGroupName = function (groupObj) {
5023     return groupObj ? groupObj.content : undefined;
5024 };
5025 
5026 /**
5027  * Cancel a change item
5028  * This method can be called insed an event listener which catches the "change"
5029  * event. The changed event position will be undone.
5030  */
5031 links.Timeline.prototype.cancelChange = function () {
5032     this.applyChange = false;
5033 };
5034 
5035 /**
5036  * Cancel deletion of an item
5037  * This method can be called insed an event listener which catches the "delete"
5038  * event. Deletion of the event will be undone.
5039  */
5040 links.Timeline.prototype.cancelDelete = function () {
5041     this.applyDelete = false;
5042 };
5043 
5044 
5045 /**
5046  * Cancel creation of a new item
5047  * This method can be called insed an event listener which catches the "new"
5048  * event. Creation of the new the event will be undone.
5049  */
5050 links.Timeline.prototype.cancelAdd = function () {
5051     this.applyAdd = false;
5052 };
5053 
5054 
5055 /**
5056  * Select an event. The visible chart range will be moved such that the selected
5057  * event is placed in the middle.
5058  * For example selection = [{row: 5}];
5059  * @param {Array} selection   An array with a column row, containing the row
5060  *                           number (the id) of the event to be selected.
5061  * @return {boolean}         true if selection is succesfully set, else false.
5062  */
5063 links.Timeline.prototype.setSelection = function(selection) {
5064     if (selection != undefined && selection.length > 0) {
5065         if (selection[0].row != undefined) {
5066             var index = selection[0].row;
5067             if (this.items[index]) {
5068                 var item = this.items[index];
5069                 this.selectItem(index);
5070 
5071                 // move the visible chart range to the selected event.
5072                 var start = item.start;
5073                 var end = item.end;
5074                 var middle; // number
5075                 if (end != undefined) {
5076                     middle = (end.valueOf() + start.valueOf()) / 2;
5077                 } else {
5078                     middle = start.valueOf();
5079                 }
5080                 var diff = (this.end.valueOf() - this.start.valueOf()),
5081                     newStart = new Date(middle - diff/2),
5082                     newEnd = new Date(middle + diff/2);
5083 
5084                 this.setVisibleChartRange(newStart, newEnd);
5085 
5086                 return true;
5087             }
5088         }
5089     }
5090     else {
5091         // unselect current selection
5092         this.unselectItem();
5093     }
5094     return false;
5095 };
5096 
5097 /**
5098  * Retrieve the currently selected event
5099  * @return {Array} sel  An array with a column row, containing the row number
5100  *                      of the selected event. If there is no selection, an
5101  *                      empty array is returned.
5102  */
5103 links.Timeline.prototype.getSelection = function() {
5104     var sel = [];
5105     if (this.selection) {
5106         if(this.selection.index !== undefined)
5107         {
5108             sel.push({"row": this.selection.index});
5109         } else {
5110             sel.push({"cluster": this.selection.cluster});
5111         }
5112     }
5113     return sel;
5114 };
5115 
5116 
5117 /**
5118  * Select an item by its index
5119  * @param {Number} index
5120  */
5121 links.Timeline.prototype.selectItem = function(index) {
5122     this.unselectItem();
5123 
5124     this.selection = undefined;
5125 
5126     if (this.items[index] != undefined) {
5127         var item = this.items[index],
5128             domItem = item.dom;
5129 
5130         this.selection = {
5131             'index': index
5132         };
5133 
5134         if (item && item.dom) {
5135             // TODO: move adjusting the domItem to the item itself
5136             if (this.isEditable(item)) {
5137                 item.dom.style.cursor = 'move';
5138             }
5139             item.select();
5140         }
5141         this.repaintDeleteButton();
5142         this.repaintDragAreas();
5143     }
5144 };
5145 
5146 /**
5147  * Select an cluster by its index
5148  * @param {Number} index
5149  */
5150 links.Timeline.prototype.selectCluster = function(index) {
5151     this.unselectItem();
5152 
5153     this.selection = undefined;
5154 
5155     if (this.clusters[index] != undefined) {
5156         this.selection = {
5157             'cluster': index
5158         };
5159         this.repaintDeleteButton();
5160         this.repaintDragAreas();
5161     }
5162 };
5163 
5164 /**
5165  * Check if an item is currently selected
5166  * @param {Number} index
5167  * @return {boolean} true if row is selected, else false
5168  */
5169 links.Timeline.prototype.isSelected = function (index) {
5170     return (this.selection && this.selection.index == index);
5171 };
5172 
5173 /**
5174  * Unselect the currently selected event (if any)
5175  */
5176 links.Timeline.prototype.unselectItem = function() {
5177     if (this.selection && this.selection.index !== undefined) {
5178         var item = this.items[this.selection.index];
5179 
5180         if (item && item.dom) {
5181             var domItem = item.dom;
5182             domItem.style.cursor = '';
5183             item.unselect();
5184         }
5185 
5186         this.selection = undefined;
5187         this.repaintDeleteButton();
5188         this.repaintDragAreas();
5189     }
5190 };
5191 
5192 
5193 /**
5194  * Stack the items such that they don't overlap. The items will have a minimal
5195  * distance equal to options.eventMargin.
5196  * @param {boolean | undefined} animate    if animate is true, the items are
5197  *                                         moved to their new position animated
5198  *                                         defaults to false.
5199  */
5200 links.Timeline.prototype.stackItems = function(animate) {
5201     if (animate == undefined) {
5202         animate = false;
5203     }
5204 
5205     // calculate the order and final stack position of the items
5206     var stack = this.stack;
5207     if (!stack) {
5208         stack = {};
5209         this.stack = stack;
5210     }
5211     stack.sortedItems = this.stackOrder(this.renderedItems);
5212     stack.finalItems = this.stackCalculateFinal(stack.sortedItems);
5213 
5214     if (animate || stack.timer) {
5215         // move animated to the final positions
5216         var timeline = this;
5217         var step = function () {
5218             var arrived = timeline.stackMoveOneStep(stack.sortedItems,
5219                 stack.finalItems);
5220 
5221             timeline.repaint();
5222 
5223             if (!arrived) {
5224                 stack.timer = setTimeout(step, 30);
5225             }
5226             else {
5227                 delete stack.timer;
5228             }
5229         };
5230 
5231         if (!stack.timer) {
5232             stack.timer = setTimeout(step, 30);
5233         }
5234     }
5235     else {
5236         // move immediately to the final positions
5237         this.stackMoveToFinal(stack.sortedItems, stack.finalItems);
5238     }
5239 };
5240 
5241 /**
5242  * Cancel any running animation
5243  */
5244 links.Timeline.prototype.stackCancelAnimation = function() {
5245     if (this.stack && this.stack.timer) {
5246         clearTimeout(this.stack.timer);
5247         delete this.stack.timer;
5248     }
5249 };
5250 
5251 links.Timeline.prototype.getItemsByGroup = function(items) {
5252     var itemsByGroup = {};
5253     for (var i = 0; i < items.length; ++i) {
5254         var item = items[i];
5255         var group = "undefined";
5256 
5257         if (item.group) {
5258             if (item.group.content) {
5259                 group = item.group.content;
5260             } else {
5261                 group = item.group;
5262             }
5263         }
5264 
5265         if (!itemsByGroup[group]) {
5266             itemsByGroup[group] = [];
5267         }
5268 
5269         itemsByGroup[group].push(item);
5270     }
5271 
5272     return itemsByGroup;
5273 };
5274 
5275 /**
5276  * Order the items in the array this.items. The default order is determined via:
5277  * - Ranges go before boxes and dots.
5278  * - The item with the oldest start time goes first
5279  * If a custom function has been provided via the stackorder option, then this will be used.
5280  * @param {Array} items        Array with items
5281  * @return {Array} sortedItems Array with sorted items
5282  */
5283 links.Timeline.prototype.stackOrder = function(items) {
5284     // TODO: store the sorted items, to have less work later on
5285     var sortedItems = items.concat([]);
5286 
5287     //if a customer stack order function exists, use it.
5288     var f = this.options.customStackOrder && (typeof this.options.customStackOrder === 'function') ? this.options.customStackOrder : function (a, b)
5289     {
5290         if ((a instanceof links.Timeline.ItemRange || a instanceof links.Timeline.ItemFloatingRange) &&
5291             !(b instanceof links.Timeline.ItemRange || b instanceof links.Timeline.ItemFloatingRange)) {
5292             return -1;
5293         }
5294 
5295         if (!(a instanceof links.Timeline.ItemRange || a instanceof links.Timeline.ItemFloatingRange) &&
5296             (b instanceof links.Timeline.ItemRange || b instanceof links.Timeline.ItemFloatingRange)) {
5297             return 1;
5298         }
5299 
5300         return (a.left - b.left);
5301     };
5302 
5303     sortedItems.sort(f);
5304 
5305     return sortedItems;
5306 };
5307 
5308 /**
5309  * Adjust vertical positions of the events such that they don't overlap each
5310  * other.
5311  * @param {timeline.Item[]} items
5312  * @return {Object[]} finalItems
5313  */
5314 links.Timeline.prototype.stackCalculateFinal = function(items) {
5315     var size = this.size,
5316         options = this.options,
5317         axisOnTop = options.axisOnTop,
5318         eventMargin = options.eventMargin,
5319         eventMarginAxis = options.eventMarginAxis,
5320         groupBase = (axisOnTop)
5321                   ? size.axis.height + eventMarginAxis + eventMargin/2
5322                   : size.contentHeight - eventMarginAxis - eventMargin/2,
5323         groupedItems, groupFinalItems, finalItems = [];
5324 
5325     groupedItems = this.getItemsByGroup(items);
5326 
5327     //
5328     // groupedItems contains all items by group, plus it may contain an
5329     // additional "undefined" group which contains all items with no group. We
5330     // first process the grouped items, and then the ungrouped
5331     //
5332     for (j = 0; j<this.groups.length; ++j) {
5333         var group = this.groups[j];
5334 
5335         if (!groupedItems[group.content]) {
5336             if (axisOnTop) {
5337                 groupBase += options.groupMinHeight + eventMargin;
5338             } else {
5339                 groupBase -= (options.groupMinHeight + eventMargin);
5340             }
5341             continue;
5342         }
5343 
5344         // initialize final positions and fill finalItems
5345         groupFinalItems = this.finalItemsPosition(groupedItems[group.content], groupBase, group);
5346         groupFinalItems.forEach(function(item) {
5347            finalItems.push(item);
5348         });
5349 
5350         if (axisOnTop) {
5351             groupBase += group.itemsHeight + eventMargin;
5352         } else {
5353             groupBase -= (group.itemsHeight + eventMargin);
5354         }
5355     }
5356 
5357     //
5358     // Ungrouped items' turn now!
5359     //
5360     if (groupedItems["undefined"]) {
5361         // initialize final positions and fill finalItems
5362         groupFinalItems = this.finalItemsPosition(groupedItems["undefined"], groupBase);
5363         groupFinalItems.forEach(function(item) {
5364            finalItems.push(item);
5365         });
5366     }
5367 
5368     return finalItems;
5369 };
5370 
5371 links.Timeline.prototype.finalItemsPosition = function(items, groupBase, group) {
5372     var i,
5373         iMax,
5374         options = this.options,
5375         axisOnTop = options.axisOnTop,
5376         eventMargin = options.eventMargin,
5377         groupFinalItems;
5378 
5379     // initialize final positions and fill finalItems
5380     groupFinalItems = this.initialItemsPosition(items, groupBase);
5381 
5382     // calculate new, non-overlapping positions
5383     for (i = 0, iMax = groupFinalItems.length; i < iMax; i++) {
5384         var finalItem = groupFinalItems[i];
5385         var collidingItem = null;
5386 
5387         if (this.options.stackEvents) {
5388             do {
5389                 // TODO: optimize checking for overlap. when there is a gap without items,
5390                 //  you only need to check for items from the next item on, not from zero
5391                 collidingItem = this.stackItemsCheckOverlap(groupFinalItems, i, 0, i-1);
5392                 if (collidingItem != null) {
5393                     // There is a collision. Reposition the event above the colliding element
5394                     if (axisOnTop) {
5395                         finalItem.top = collidingItem.top + collidingItem.height + eventMargin;
5396                     }
5397                     else {
5398                         finalItem.top = collidingItem.top - finalItem.height - eventMargin;
5399                     }
5400                     finalItem.bottom = finalItem.top + finalItem.height;
5401                 }
5402             } while (collidingItem);
5403         }
5404 
5405         if (group) {
5406             if (axisOnTop) {
5407                 group.itemsHeight = (group.itemsHeight)
5408                                   ? Math.max(group.itemsHeight, finalItem.bottom - groupBase)
5409                                   : finalItem.height + eventMargin;
5410             } else {
5411                 group.itemsHeight = (group.itemsHeight)
5412                                   ? Math.max(group.itemsHeight, groupBase - finalItem.top)
5413                                   : finalItem.height + eventMargin;
5414             }
5415         }
5416     }
5417 
5418     return groupFinalItems;
5419 };
5420 
5421 links.Timeline.prototype.initialItemsPosition = function(items, groupBase) {
5422     var options = this.options,
5423         axisOnTop = options.axisOnTop,
5424         finalItems = [];
5425 
5426     for (var i = 0, iMax = items.length; i < iMax; ++i) {
5427         var item = items[i],
5428             top,
5429             bottom,
5430             height = item.height,
5431             width = item.getWidth(this),
5432             right = item.getRight(this),
5433             left = right - width;
5434 
5435         top = (axisOnTop) ? groupBase
5436                           : groupBase - height;
5437 
5438         bottom = top + height;
5439 
5440         finalItems.push({
5441             'left': left,
5442             'top': top,
5443             'right': right,
5444             'bottom': bottom,
5445             'height': height,
5446             'item': item
5447         });
5448     }
5449 
5450     return finalItems;
5451 };
5452 
5453 /**
5454  * Move the events one step in the direction of their final positions
5455  * @param {Array} currentItems   Array with the real items and their current
5456  *                               positions
5457  * @param {Array} finalItems     Array with objects containing the final
5458  *                               positions of the items
5459  * @return {boolean} arrived     True if all items have reached their final
5460  *                               location, else false
5461  */
5462 links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) {
5463     var arrived = true;
5464 
5465     // apply new positions animated
5466     for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
5467         var finalItem = finalItems[i],
5468             item = finalItem.item;
5469 
5470         var topNow = parseInt(item.top);
5471         var topFinal = parseInt(finalItem.top);
5472         var diff = (topFinal - topNow);
5473         if (diff) {
5474             var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1);
5475             if (Math.abs(diff) > 4) step = diff / 4;
5476             var topNew = parseInt(topNow + step);
5477 
5478             if (topNew != topFinal) {
5479                 arrived = false;
5480             }
5481 
5482             item.top = topNew;
5483             item.bottom = item.top + item.height;
5484         }
5485         else {
5486             item.top = finalItem.top;
5487             item.bottom = finalItem.bottom;
5488         }
5489 
5490         item.left = finalItem.left;
5491         item.right = finalItem.right;
5492     }
5493 
5494     return arrived;
5495 };
5496 
5497 
5498 
5499 /**
5500  * Move the events from their current position to the final position
5501  * @param {Array} currentItems   Array with the real items and their current
5502  *                               positions
5503  * @param {Array} finalItems     Array with objects containing the final
5504  *                               positions of the items
5505  */
5506 links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) {
5507     // Put the events directly at there final position
5508     for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
5509         var finalItem = finalItems[i],
5510             current = finalItem.item;
5511 
5512         current.left = finalItem.left;
5513         current.top = finalItem.top;
5514         current.right = finalItem.right;
5515         current.bottom = finalItem.bottom;
5516     }
5517 };
5518 
5519 
5520 
5521 /**
5522  * Check if the destiny position of given item overlaps with any
5523  * of the other items from index itemStart to itemEnd.
5524  * @param {Array} items      Array with items
5525  * @param {int}  itemIndex   Number of the item to be checked for overlap
5526  * @param {int}  itemStart   First item to be checked.
5527  * @param {int}  itemEnd     Last item to be checked.
5528  * @return {Object}          colliding item, or undefined when no collisions
5529  */
5530 links.Timeline.prototype.stackItemsCheckOverlap = function(items, itemIndex,
5531                                                            itemStart, itemEnd) {
5532     var eventMargin = this.options.eventMargin,
5533         collision = this.collision;
5534 
5535     // we loop from end to start, as we suppose that the chance of a
5536     // collision is larger for items at the end, so check these first.
5537     var item1 = items[itemIndex];
5538     for (var i = itemEnd; i >= itemStart; i--) {
5539         var item2 = items[i];
5540         if (collision(item1, item2, eventMargin)) {
5541             if (i != itemIndex) {
5542                 return item2;
5543             }
5544         }
5545     }
5546 
5547     return undefined;
5548 };
5549 
5550 /**
5551  * Test if the two provided items collide
5552  * The items must have parameters left, right, top, and bottom.
5553  * @param {Element} item1       The first item
5554  * @param {Element} item2       The second item
5555  * @param {Number}              margin  A minimum required margin. Optional.
5556  *                              If margin is provided, the two items will be
5557  *                              marked colliding when they overlap or
5558  *                              when the margin between the two is smaller than
5559  *                              the requested margin.
5560  * @return {boolean}            true if item1 and item2 collide, else false
5561  */
5562 links.Timeline.prototype.collision = function(item1, item2, margin) {
5563     // set margin if not specified
5564     if (margin == undefined) {
5565         margin = 0;
5566     }
5567 
5568     // calculate if there is overlap (collision)
5569     return (item1.left - margin < item2.right &&
5570         item1.right + margin > item2.left &&
5571         item1.top - margin < item2.bottom &&
5572         item1.bottom + margin > item2.top);
5573 };
5574 
5575 
5576 /**
5577  * fire an event
5578  * @param {String} event   The name of an event, for example "rangechange" or "edit"
5579  */
5580 links.Timeline.prototype.trigger = function (event) {
5581     // built up properties
5582     var properties = null;
5583     switch (event) {
5584         case 'rangechange':
5585         case 'rangechanged':
5586             properties = {
5587                 'start': new Date(this.start.valueOf()),
5588                 'end': new Date(this.end.valueOf())
5589             };
5590             break;
5591 
5592         case 'timechange':
5593         case 'timechanged':
5594             properties = {
5595                 'time': new Date(this.customTime.valueOf())
5596             };
5597             break;
5598     }
5599 
5600     // trigger the links event bus
5601     links.events.trigger(this, event, properties);
5602 
5603     // trigger the google event bus
5604     if (google && google.visualization) {
5605         google.visualization.events.trigger(this, event, properties);
5606     }
5607 };
5608 
5609 
5610 /**
5611  * Cluster the events
5612  */
5613 links.Timeline.prototype.clusterItems = function () {
5614     if (!this.options.cluster) {
5615         return;
5616     }
5617 
5618     var clusters = this.clusterGenerator.getClusters(this.conversion.factor, this.options.clusterMaxItems);
5619     if (this.clusters != clusters) {
5620         // cluster level changed
5621         var queue = this.renderQueue;
5622 
5623         // remove the old clusters from the scene
5624         if (this.clusters) {
5625             this.clusters.forEach(function (cluster) {
5626                 queue.hide.push(cluster);
5627 
5628                 // unlink the items
5629                 cluster.items.forEach(function (item) {
5630                     item.cluster = undefined;
5631                 });
5632             });
5633         }
5634 
5635         // append the new clusters
5636         clusters.forEach(function (cluster) {
5637             // don't add to the queue.show here, will be done in .filterItems()
5638 
5639             // link all items to the cluster
5640             cluster.items.forEach(function (item) {
5641                 item.cluster = cluster;
5642             });
5643         });
5644 
5645         this.clusters = clusters;
5646     }
5647 };
5648 
5649 /**
5650  * Filter the visible events
5651  */
5652 links.Timeline.prototype.filterItems = function () {
5653     var queue = this.renderQueue,
5654         window = (this.end - this.start),
5655         start = new Date(this.start.valueOf() - window),
5656         end = new Date(this.end.valueOf() + window);
5657 
5658     function filter (arr) {
5659         arr.forEach(function (item) {
5660             var rendered = item.rendered;
5661             var visible = item.isVisible(start, end);
5662             if (rendered != visible) {
5663                 if (rendered) {
5664                     queue.hide.push(item); // item is rendered but no longer visible
5665                 }
5666                 if (visible && (queue.show.indexOf(item) == -1)) {
5667                     queue.show.push(item); // item is visible but neither rendered nor queued up to be rendered
5668                 }
5669             }
5670         });
5671     }
5672 
5673     // filter all items and all clusters
5674     filter(this.items);
5675     if (this.clusters) {
5676         filter(this.clusters);
5677     }
5678 };
5679 
5680 /** ------------------------------------------------------------------------ **/
5681 
5682 /**
5683  * @constructor links.Timeline.ClusterGenerator
5684  * Generator which creates clusters of items, based on the visible range in
5685  * the Timeline. There is a set of cluster levels which is cached.
5686  * @param {links.Timeline} timeline
5687  */
5688 links.Timeline.ClusterGenerator = function (timeline) {
5689     this.timeline = timeline;
5690     this.clear();
5691 };
5692 
5693 /**
5694  * Clear all cached clusters and data, and initialize all variables
5695  */
5696 links.Timeline.ClusterGenerator.prototype.clear = function () {
5697     // cache containing created clusters for each cluster level
5698     this.items = [];
5699     this.groups = {};
5700     this.clearCache();
5701 };
5702 
5703 /**
5704  * Clear the cached clusters
5705  */
5706 links.Timeline.ClusterGenerator.prototype.clearCache = function () {
5707     // cache containing created clusters for each cluster level
5708     this.cache = {};
5709     this.cacheLevel = -1;
5710     this.cache[this.cacheLevel] = [];
5711 };
5712 
5713 /**
5714  * Set the items to be clustered.
5715  * This will clear cached clusters.
5716  * @param {Item[]} items
5717  * @param {Object} [options]  Available options:
5718  *                            {boolean} applyOnChangedLevel
5719  *                                If true (default), the changed data is applied
5720  *                                as soon the cluster level changes. If false,
5721  *                                The changed data is applied immediately
5722  */
5723 links.Timeline.ClusterGenerator.prototype.setData = function (items, options) {
5724     this.items = items || [];
5725     this.dataChanged = true;
5726     this.applyOnChangedLevel = true;
5727     if (options && options.applyOnChangedLevel) {
5728         this.applyOnChangedLevel = options.applyOnChangedLevel;
5729     }
5730     // console.log('clustergenerator setData applyOnChangedLevel=' + this.applyOnChangedLevel); // TODO: cleanup
5731 };
5732 
5733 /**
5734  * Update the current data set: clear cache, and recalculate the clustering for
5735  * the current level
5736  */
5737 links.Timeline.ClusterGenerator.prototype.updateData = function () {
5738     this.dataChanged = true;
5739     this.applyOnChangedLevel = false;
5740 };
5741 
5742 /**
5743  * Filter the items per group.
5744  * @private
5745  */
5746 links.Timeline.ClusterGenerator.prototype.filterData = function () {
5747     // filter per group
5748     var items = this.items || [];
5749     var groups = {};
5750     this.groups = groups;
5751 
5752     // split the items per group
5753     items.forEach(function (item) {
5754         // put the item in the correct group
5755         var groupName = item.group ? item.group.content : '';
5756         var group = groups[groupName];
5757         if (!group) {
5758             group = [];
5759             groups[groupName] = group;
5760         }
5761         group.push(item);
5762 
5763         // calculate the center of the item
5764         if (item.start) {
5765             if (item.end) {
5766                 // range
5767                 item.center = (item.start.valueOf() + item.end.valueOf()) / 2;
5768             }
5769             else {
5770                 // box, dot
5771                 item.center = item.start.valueOf();
5772             }
5773         }
5774     });
5775 
5776     // sort the items per group
5777     for (var groupName in groups) {
5778         if (groups.hasOwnProperty(groupName)) {
5779             groups[groupName].sort(function (a, b) {
5780                 return (a.center - b.center);
5781             });
5782         }
5783     }
5784 
5785     this.dataChanged = false;
5786 };
5787 
5788 /**
5789  * Cluster the events which are too close together
5790  * @param {Number} scale     The scale of the current window,
5791  *                           defined as (windowWidth / (endDate - startDate))
5792  * @return {Item[]} clusters
5793  */
5794 links.Timeline.ClusterGenerator.prototype.getClusters = function (scale, maxItems) {
5795     var level = -1,
5796         granularity = 2, // TODO: what granularity is needed for the cluster levels?
5797         timeWindow = 0;  // milliseconds
5798 
5799     if (scale > 0) {
5800         level = Math.round(Math.log(100 / scale) / Math.log(granularity));
5801         timeWindow = Math.pow(granularity, level);
5802     }
5803 
5804     // clear the cache when and re-filter the data when needed.
5805     if (this.dataChanged) {
5806         var levelChanged = (level != this.cacheLevel);
5807         var applyDataNow = this.applyOnChangedLevel ? levelChanged : true;
5808         if (applyDataNow) {
5809             // TODO: currently drawn clusters should be removed! mark them as invisible?
5810             this.clearCache();
5811             this.filterData();
5812             // console.log('clustergenerator: cache cleared...'); // TODO: cleanup
5813         }
5814     }
5815 
5816     this.cacheLevel = level;
5817     var clusters = this.cache[level];
5818     if (!clusters) {
5819         // console.log('clustergenerator: create cluster level ' + level); // TODO: cleanup
5820         clusters = [];
5821 
5822         // TODO: spit this method, it is too large
5823         for (var groupName in this.groups) {
5824             if (this.groups.hasOwnProperty(groupName)) {
5825                 var items = this.groups[groupName];
5826                 var iMax = items.length;
5827                 var i = 0;
5828                 while (i < iMax) {
5829                     // find all items around current item, within the timeWindow
5830                     var item = items[i];
5831                     var neighbors = 1;  // start at 1, to include itself)
5832 
5833                     // loop through items left from the current item
5834                     var j = i - 1;
5835                     while (j >= 0 && (item.center - items[j].center) < timeWindow / 2) {
5836                         if (!items[j].cluster) {
5837                             neighbors++;
5838                         }
5839                         j--;
5840                     }
5841 
5842                     // loop through items right from the current item
5843                     var k = i + 1;
5844                     while (k < items.length && (items[k].center - item.center) < timeWindow / 2) {
5845                         neighbors++;
5846                         k++;
5847                     }
5848 
5849                     // loop through the created clusters
5850                     var l = clusters.length - 1;
5851                     while (l >= 0 && (item.center - clusters[l].center) < timeWindow / 2) {
5852                         if (item.group == clusters[l].group) {
5853                             neighbors++;
5854                         }
5855                         l--;
5856                     }
5857 
5858                     // aggregate until the number of items is within maxItems
5859                     if (neighbors > maxItems) {
5860                         // too busy in this window.
5861                         var num = neighbors - maxItems + 1;
5862                         var clusterItems = [];
5863 
5864                         // append the items to the cluster,
5865                         // and calculate the average start for the cluster
5866                         var avg = undefined;  // number. average of all start dates
5867                         var min = undefined;  // number. minimum of all start dates
5868                         var max = undefined;  // number. maximum of all start and end dates
5869                         var containsRanges = false;
5870                         var count = 0;
5871                         var m = i;
5872                         while (clusterItems.length < num && m < items.length) {
5873                             var p = items[m];
5874                             var start = p.start.valueOf();
5875                             var end = p.end ? p.end.valueOf() : p.start.valueOf();
5876                             clusterItems.push(p);
5877                             if (count) {
5878                                 // calculate new average (use fractions to prevent overflow)
5879                                 avg = (count / (count + 1)) * avg + (1 / (count + 1)) * p.center;
5880                             }
5881                             else {
5882                                 avg = p.center;
5883                             }
5884                             min = (min != undefined) ? Math.min(min, start) : start;
5885                             max = (max != undefined) ? Math.max(max, end) : end;
5886                             containsRanges = containsRanges || (p instanceof links.Timeline.ItemRange || p instanceof links.Timeline.ItemFloatingRange);
5887                             count++;
5888                             m++;
5889                         }
5890 
5891                         var cluster;
5892                         var title = 'Cluster containing ' + count +
5893                             ' events. Zoom in to see the individual events.';
5894                         var content = '<div title="' + title + '">' + count + ' events</div>';
5895                         var group = item.group ? item.group.content : undefined;
5896                         if (containsRanges) {
5897                             // boxes and/or ranges
5898                             cluster = this.timeline.createItem({
5899                                 'start': new Date(min),
5900                                 'end': new Date(max),
5901                                 'content': content,
5902                                 'group': group
5903                             });
5904                         }
5905                         else {
5906                             // boxes only
5907                             cluster = this.timeline.createItem({
5908                                 'start': new Date(avg),
5909                                 'content': content,
5910                                 'group': group
5911                             });
5912                         }
5913                         cluster.isCluster = true;
5914                         cluster.items = clusterItems;
5915                         cluster.items.forEach(function (item) {
5916                             item.cluster = cluster;
5917                         });
5918 
5919                         clusters.push(cluster);
5920                         i += num;
5921                     }
5922                     else {
5923                         delete item.cluster;
5924                         i += 1;
5925                     }
5926                 }
5927             }
5928         }
5929 
5930         this.cache[level] = clusters;
5931     }
5932 
5933     return clusters;
5934 };
5935 
5936 
5937 /** ------------------------------------------------------------------------ **/
5938 
5939 
5940 /**
5941  * Event listener (singleton)
5942  */
5943 links.events = links.events || {
5944     'listeners': [],
5945 
5946     /**
5947      * Find a single listener by its object
5948      * @param {Object} object
5949      * @return {Number} index  -1 when not found
5950      */
5951     'indexOf': function (object) {
5952         var listeners = this.listeners;
5953         for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
5954             var listener = listeners[i];
5955             if (listener && listener.object == object) {
5956                 return i;
5957             }
5958         }
5959         return -1;
5960     },
5961 
5962     /**
5963      * Add an event listener
5964      * @param {Object} object
5965      * @param {String} event       The name of an event, for example 'select'
5966      * @param {function} callback  The callback method, called when the
5967      *                             event takes place
5968      */
5969     'addListener': function (object, event, callback) {
5970         var index = this.indexOf(object);
5971         var listener = this.listeners[index];
5972         if (!listener) {
5973             listener = {
5974                 'object': object,
5975                 'events': {}
5976             };
5977             this.listeners.push(listener);
5978         }
5979 
5980         var callbacks = listener.events[event];
5981         if (!callbacks) {
5982             callbacks = [];
5983             listener.events[event] = callbacks;
5984         }
5985 
5986         // add the callback if it does not yet exist
5987         if (callbacks.indexOf(callback) == -1) {
5988             callbacks.push(callback);
5989         }
5990     },
5991 
5992     /**
5993      * Remove an event listener
5994      * @param {Object} object
5995      * @param {String} event       The name of an event, for example 'select'
5996      * @param {function} callback  The registered callback method
5997      */
5998     'removeListener': function (object, event, callback) {
5999         var index = this.indexOf(object);
6000         var listener = this.listeners[index];
6001         if (listener) {
6002             var callbacks = listener.events[event];
6003             if (callbacks) {
6004                 var index = callbacks.indexOf(callback);
6005                 if (index != -1) {
6006                     callbacks.splice(index, 1);
6007                 }
6008 
6009                 // remove the array when empty
6010                 if (callbacks.length == 0) {
6011                     delete listener.events[event];
6012                 }
6013             }
6014 
6015             // count the number of registered events. remove listener when empty
6016             var count = 0;
6017             var events = listener.events;
6018             for (var e in events) {
6019                 if (events.hasOwnProperty(e)) {
6020                     count++;
6021                 }
6022             }
6023             if (count == 0) {
6024                 delete this.listeners[index];
6025             }
6026         }
6027     },
6028 
6029     /**
6030      * Remove all registered event listeners
6031      */
6032     'removeAllListeners': function () {
6033         this.listeners = [];
6034     },
6035 
6036     /**
6037      * Trigger an event. All registered event handlers will be called
6038      * @param {Object} object
6039      * @param {String} event
6040      * @param {Object} properties (optional)
6041      */
6042     'trigger': function (object, event, properties) {
6043         var index = this.indexOf(object);
6044         var listener = this.listeners[index];
6045         if (listener) {
6046             var callbacks = listener.events[event];
6047             if (callbacks) {
6048                 for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
6049                     callbacks[i](properties);
6050                 }
6051             }
6052         }
6053     }
6054 };
6055 
6056 
6057 /** ------------------------------------------------------------------------ **/
6058 
6059 /**
6060  * @constructor  links.Timeline.StepDate
6061  * The class StepDate is an iterator for dates. You provide a start date and an
6062  * end date. The class itself determines the best scale (step size) based on the
6063  * provided start Date, end Date, and minimumStep.
6064  *
6065  * If minimumStep is provided, the step size is chosen as close as possible
6066  * to the minimumStep but larger than minimumStep. If minimumStep is not
6067  * provided, the scale is set to 1 DAY.
6068  * The minimumStep should correspond with the onscreen size of about 6 characters
6069  *
6070  * Alternatively, you can set a scale by hand.
6071  * After creation, you can initialize the class by executing start(). Then you
6072  * can iterate from the start date to the end date via next(). You can check if
6073  * the end date is reached with the function end(). After each step, you can
6074  * retrieve the current date via get().
6075  * The class step has scales ranging from milliseconds, seconds, minutes, hours,
6076  * days, to years.
6077  *
6078  * Version: 1.2
6079  *
6080  * @param {Date} start          The start date, for example new Date(2010, 9, 21)
6081  *                              or new Date(2010, 9, 21, 23, 45, 00)
6082  * @param {Date} end            The end date
6083  * @param {Number}  minimumStep Optional. Minimum step size in milliseconds
6084  */
6085 links.Timeline.StepDate = function(start, end, minimumStep) {
6086 
6087     // variables
6088     this.current = new Date();
6089     this._start = new Date();
6090     this._end = new Date();
6091 
6092     this.autoScale  = true;
6093     this.scale = links.Timeline.StepDate.SCALE.DAY;
6094     this.step = 1;
6095 
6096     // initialize the range
6097     this.setRange(start, end, minimumStep);
6098 };
6099 
6100 /// enum scale
6101 links.Timeline.StepDate.SCALE = {
6102     MILLISECOND: 1,
6103     SECOND: 2,
6104     MINUTE: 3,
6105     HOUR: 4,
6106     DAY: 5,
6107     WEEKDAY: 6,
6108     MONTH: 7,
6109     YEAR: 8
6110 };
6111 
6112 
6113 /**
6114  * Set a new range
6115  * If minimumStep is provided, the step size is chosen as close as possible
6116  * to the minimumStep but larger than minimumStep. If minimumStep is not
6117  * provided, the scale is set to 1 DAY.
6118  * The minimumStep should correspond with the onscreen size of about 6 characters
6119  * @param {Date} start        The start date and time.
6120  * @param {Date} end          The end date and time.
6121  * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
6122  */
6123 links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) {
6124     if (!(start instanceof Date) || !(end instanceof Date)) {
6125         //throw  "No legal start or end date in method setRange";
6126         return;
6127     }
6128 
6129     this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
6130     this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
6131 
6132     if (this.autoScale) {
6133         this.setMinimumStep(minimumStep);
6134     }
6135 };
6136 
6137 /**
6138  * Set the step iterator to the start date.
6139  */
6140 links.Timeline.StepDate.prototype.start = function() {
6141     this.current = new Date(this._start.valueOf());
6142     this.roundToMinor();
6143 };
6144 
6145 /**
6146  * Round the current date to the first minor date value
6147  * This must be executed once when the current date is set to start Date
6148  */
6149 links.Timeline.StepDate.prototype.roundToMinor = function() {
6150     // round to floor
6151     // IMPORTANT: we have no breaks in this switch! (this is no bug)
6152     //noinspection FallthroughInSwitchStatementJS
6153     switch (this.scale) {
6154         case links.Timeline.StepDate.SCALE.YEAR:
6155             this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
6156             this.current.setMonth(0);
6157         case links.Timeline.StepDate.SCALE.MONTH:        this.current.setDate(1);
6158         case links.Timeline.StepDate.SCALE.DAY:          // intentional fall through
6159         case links.Timeline.StepDate.SCALE.WEEKDAY:      this.current.setHours(0);
6160         case links.Timeline.StepDate.SCALE.HOUR:         this.current.setMinutes(0);
6161         case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setSeconds(0);
6162         case links.Timeline.StepDate.SCALE.SECOND:       this.current.setMilliseconds(0);
6163         //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
6164     }
6165 
6166     if (this.step != 1) {
6167         // round down to the first minor value that is a multiple of the current step size
6168         switch (this.scale) {
6169             case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);  break;
6170             case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
6171             case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
6172             case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
6173             case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
6174             case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
6175             case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);  break;
6176             case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
6177             default: break;
6178         }
6179     }
6180 };
6181 
6182 /**
6183  * Check if the end date is reached
6184  * @return {boolean}  true if the current date has passed the end date
6185  */
6186 links.Timeline.StepDate.prototype.end = function () {
6187     return (this.current.valueOf() > this._end.valueOf());
6188 };
6189 
6190 /**
6191  * Do the next step
6192  */
6193 links.Timeline.StepDate.prototype.next = function() {
6194     var prev = this.current.valueOf();
6195 
6196     // Two cases, needed to prevent issues with switching daylight savings
6197     // (end of March and end of October)
6198     if (this.current.getMonth() < 6)   {
6199         switch (this.scale) {
6200             case links.Timeline.StepDate.SCALE.MILLISECOND:
6201 
6202                 this.current = new Date(this.current.valueOf() + this.step); break;
6203             case links.Timeline.StepDate.SCALE.SECOND:       this.current = new Date(this.current.valueOf() + this.step * 1000); break;
6204             case links.Timeline.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
6205             case links.Timeline.StepDate.SCALE.HOUR:
6206                 this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
6207                 // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
6208                 var h = this.current.getHours();
6209                 this.current.setHours(h - (h % this.step));
6210                 break;
6211             case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
6212             case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
6213             case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
6214             case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
6215             default:                      break;
6216         }
6217     }
6218     else {
6219         switch (this.scale) {
6220             case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current = new Date(this.current.valueOf() + this.step); break;
6221             case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() + this.step); break;
6222             case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() + this.step); break;
6223             case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() + this.step); break;
6224             case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
6225             case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
6226             case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
6227             case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
6228             default:                      break;
6229         }
6230     }
6231 
6232     if (this.step != 1) {
6233         // round down to the correct major value
6234         switch (this.scale) {
6235             case links.Timeline.StepDate.SCALE.MILLISECOND:  if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);  break;
6236             case links.Timeline.StepDate.SCALE.SECOND:       if(this.current.getSeconds() < this.step) this.current.setSeconds(0);  break;
6237             case links.Timeline.StepDate.SCALE.MINUTE:       if(this.current.getMinutes() < this.step) this.current.setMinutes(0);  break;
6238             case links.Timeline.StepDate.SCALE.HOUR:         if(this.current.getHours() < this.step) this.current.setHours(0);  break;
6239             case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
6240             case links.Timeline.StepDate.SCALE.DAY:          if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
6241             case links.Timeline.StepDate.SCALE.MONTH:        if(this.current.getMonth() < this.step) this.current.setMonth(0);  break;
6242             case links.Timeline.StepDate.SCALE.YEAR:         break; // nothing to do for year
6243             default:                break;
6244         }
6245     }
6246 
6247     // safety mechanism: if current time is still unchanged, move to the end
6248     if (this.current.valueOf() == prev) {
6249         this.current = new Date(this._end.valueOf());
6250     }
6251 };
6252 
6253 
6254 /**
6255  * Get the current datetime
6256  * @return {Date}  current The current date
6257  */
6258 links.Timeline.StepDate.prototype.getCurrent = function() {
6259     return this.current;
6260 };
6261 
6262 /**
6263  * Set a custom scale. Autoscaling will be disabled.
6264  * For example setScale(SCALE.MINUTES, 5) will result
6265  * in minor steps of 5 minutes, and major steps of an hour.
6266  *
6267  * @param {links.Timeline.StepDate.SCALE} newScale
6268  *                               A scale. Choose from SCALE.MILLISECOND,
6269  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
6270  *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
6271  *                               SCALE.YEAR.
6272  * @param {Number}     newStep   A step size, by default 1. Choose for
6273  *                               example 1, 2, 5, or 10.
6274  */
6275 links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) {
6276     this.scale = newScale;
6277 
6278     if (newStep > 0) {
6279         this.step = newStep;
6280     }
6281 
6282     this.autoScale = false;
6283 };
6284 
6285 /**
6286  * Enable or disable autoscaling
6287  * @param {boolean} enable  If true, autoascaling is set true
6288  */
6289 links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
6290     this.autoScale = enable;
6291 };
6292 
6293 
6294 /**
6295  * Automatically determine the scale that bests fits the provided minimum step
6296  * @param {Number} minimumStep  The minimum step size in milliseconds
6297  */
6298 links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) {
6299     if (minimumStep == undefined) {
6300         return;
6301     }
6302 
6303     var stepYear       = (1000 * 60 * 60 * 24 * 30 * 12);
6304     var stepMonth      = (1000 * 60 * 60 * 24 * 30);
6305     var stepDay        = (1000 * 60 * 60 * 24);
6306     var stepHour       = (1000 * 60 * 60);
6307     var stepMinute     = (1000 * 60);
6308     var stepSecond     = (1000);
6309     var stepMillisecond= (1);
6310 
6311     // find the smallest step that is larger than the provided minimumStep
6312     if (stepYear*1000 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1000;}
6313     if (stepYear*500 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 500;}
6314     if (stepYear*100 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 100;}
6315     if (stepYear*50 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 50;}
6316     if (stepYear*10 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 10;}
6317     if (stepYear*5 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 5;}
6318     if (stepYear > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1;}
6319     if (stepMonth*3 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 3;}
6320     if (stepMonth > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 1;}
6321     if (stepDay*5 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 5;}
6322     if (stepDay*2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 2;}
6323     if (stepDay > minimumStep)              {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 1;}
6324     if (stepDay/2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.WEEKDAY;     this.step = 1;}
6325     if (stepHour*4 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 4;}
6326     if (stepHour > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 1;}
6327     if (stepMinute*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 15;}
6328     if (stepMinute*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 10;}
6329     if (stepMinute*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 5;}
6330     if (stepMinute > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 1;}
6331     if (stepSecond*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 15;}
6332     if (stepSecond*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 10;}
6333     if (stepSecond*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 5;}
6334     if (stepSecond > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 1;}
6335     if (stepMillisecond*200 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
6336     if (stepMillisecond*100 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
6337     if (stepMillisecond*50 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
6338     if (stepMillisecond*10 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
6339     if (stepMillisecond*5 > minimumStep)    {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
6340     if (stepMillisecond > minimumStep)      {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
6341 };
6342 
6343 /**
6344  * Snap a date to a rounded value. The snap intervals are dependent on the
6345  * current scale and step.
6346  * @param {Date} date   the date to be snapped
6347  */
6348 links.Timeline.StepDate.prototype.snap = function(date) {
6349     if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
6350         var year = date.getFullYear() + Math.round(date.getMonth() / 12);
6351         date.setFullYear(Math.round(year / this.step) * this.step);
6352         date.setMonth(0);
6353         date.setDate(0);
6354         date.setHours(0);
6355         date.setMinutes(0);
6356         date.setSeconds(0);
6357         date.setMilliseconds(0);
6358     }
6359     else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
6360         if (date.getDate() > 15) {
6361             date.setDate(1);
6362             date.setMonth(date.getMonth() + 1);
6363             // important: first set Date to 1, after that change the month.
6364         }
6365         else {
6366             date.setDate(1);
6367         }
6368 
6369         date.setHours(0);
6370         date.setMinutes(0);
6371         date.setSeconds(0);
6372         date.setMilliseconds(0);
6373     }
6374     else if (this.scale == links.Timeline.StepDate.SCALE.DAY ||
6375         this.scale == links.Timeline.StepDate.SCALE.WEEKDAY) {
6376         switch (this.step) {
6377             case 5:
6378             case 2:
6379                 date.setHours(Math.round(date.getHours() / 24) * 24); break;
6380             default:
6381                 date.setHours(Math.round(date.getHours() / 12) * 12); break;
6382         }
6383         date.setMinutes(0);
6384         date.setSeconds(0);
6385         date.setMilliseconds(0);
6386     }
6387     else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
6388         switch (this.step) {
6389             case 4:
6390                 date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
6391             default:
6392                 date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
6393         }
6394         date.setSeconds(0);
6395         date.setMilliseconds(0);
6396     } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
6397         switch (this.step) {
6398             case 15:
6399             case 10:
6400                 date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
6401                 date.setSeconds(0);
6402                 break;
6403             case 5:
6404                 date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
6405             default:
6406                 date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
6407         }
6408         date.setMilliseconds(0);
6409     }
6410     else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
6411         switch (this.step) {
6412             case 15:
6413             case 10:
6414                 date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
6415                 date.setMilliseconds(0);
6416                 break;
6417             case 5:
6418                 date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
6419             default:
6420                 date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
6421         }
6422     }
6423     else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
6424         var step = this.step > 5 ? this.step / 2 : 1;
6425         date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
6426     }
6427 };
6428 
6429 /**
6430  * Check if the current step is a major step (for example when the step
6431  * is DAY, a major step is each first day of the MONTH)
6432  * @return {boolean} true if current date is major, else false.
6433  */
6434 links.Timeline.StepDate.prototype.isMajor = function() {
6435     switch (this.scale) {
6436         case links.Timeline.StepDate.SCALE.MILLISECOND:
6437             return (this.current.getMilliseconds() == 0);
6438         case links.Timeline.StepDate.SCALE.SECOND:
6439             return (this.current.getSeconds() == 0);
6440         case links.Timeline.StepDate.SCALE.MINUTE:
6441             return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
6442         // Note: this is no bug. Major label is equal for both minute and hour scale
6443         case links.Timeline.StepDate.SCALE.HOUR:
6444             return (this.current.getHours() == 0);
6445         case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through
6446         case links.Timeline.StepDate.SCALE.DAY:
6447             return (this.current.getDate() == 1);
6448         case links.Timeline.StepDate.SCALE.MONTH:
6449             return (this.current.getMonth() == 0);
6450         case links.Timeline.StepDate.SCALE.YEAR:
6451             return false;
6452         default:
6453             return false;
6454     }
6455 };
6456 
6457 
6458 /**
6459  * Returns formatted text for the minor axislabel, depending on the current
6460  * date and the scale. For example when scale is MINUTE, the current time is
6461  * formatted as "hh:mm".
6462  * @param {Object} options
6463  * @param {Date} [date] custom date. if not provided, current date is taken
6464  */
6465 links.Timeline.StepDate.prototype.getLabelMinor = function(options, date) {
6466     if (date == undefined) {
6467         date = this.current;
6468     }
6469 
6470     switch (this.scale) {
6471         case links.Timeline.StepDate.SCALE.MILLISECOND:  return String(date.getMilliseconds());
6472         case links.Timeline.StepDate.SCALE.SECOND:       return String(date.getSeconds());
6473         case links.Timeline.StepDate.SCALE.MINUTE:
6474             return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2);
6475         case links.Timeline.StepDate.SCALE.HOUR:
6476             return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2);
6477         case links.Timeline.StepDate.SCALE.WEEKDAY:      return options.DAYS_SHORT[date.getDay()] + ' ' + date.getDate();
6478         case links.Timeline.StepDate.SCALE.DAY:          return String(date.getDate());
6479         case links.Timeline.StepDate.SCALE.MONTH:        return options.MONTHS_SHORT[date.getMonth()];   // month is zero based
6480         case links.Timeline.StepDate.SCALE.YEAR:         return String(date.getFullYear());
6481         default:                                         return "";
6482     }
6483 };
6484 
6485 
6486 /**
6487  * Returns formatted text for the major axislabel, depending on the current
6488  * date and the scale. For example when scale is MINUTE, the major scale is
6489  * hours, and the hour will be formatted as "hh".
6490  * @param {Object} options
6491  * @param {Date} [date] custom date. if not provided, current date is taken
6492  */
6493 links.Timeline.StepDate.prototype.getLabelMajor = function(options, date) {
6494     if (date == undefined) {
6495         date = this.current;
6496     }
6497 
6498     switch (this.scale) {
6499         case links.Timeline.StepDate.SCALE.MILLISECOND:
6500             return  this.addZeros(date.getHours(), 2) + ":" +
6501                 this.addZeros(date.getMinutes(), 2) + ":" +
6502                 this.addZeros(date.getSeconds(), 2);
6503         case links.Timeline.StepDate.SCALE.SECOND:
6504             return  date.getDate() + " " +
6505                 options.MONTHS[date.getMonth()] + " " +
6506                 this.addZeros(date.getHours(), 2) + ":" +
6507                 this.addZeros(date.getMinutes(), 2);
6508         case links.Timeline.StepDate.SCALE.MINUTE:
6509             return  options.DAYS[date.getDay()] + " " +
6510                 date.getDate() + " " +
6511                 options.MONTHS[date.getMonth()] + " " +
6512                 date.getFullYear();
6513         case links.Timeline.StepDate.SCALE.HOUR:
6514             return  options.DAYS[date.getDay()] + " " +
6515                 date.getDate() + " " +
6516                 options.MONTHS[date.getMonth()] + " " +
6517                 date.getFullYear();
6518         case links.Timeline.StepDate.SCALE.WEEKDAY:
6519         case links.Timeline.StepDate.SCALE.DAY:
6520             return  options.MONTHS[date.getMonth()] + " " +
6521                 date.getFullYear();
6522         case links.Timeline.StepDate.SCALE.MONTH:
6523             return String(date.getFullYear());
6524         default:
6525             return "";
6526     }
6527 };
6528 
6529 /**
6530  * Add leading zeros to the given value to match the desired length.
6531  * For example addZeros(123, 5) returns "00123"
6532  * @param {int} value   A value
6533  * @param {int} len     Desired final length
6534  * @return {string}     value with leading zeros
6535  */
6536 links.Timeline.StepDate.prototype.addZeros = function(value, len) {
6537     var str = "" + value;
6538     while (str.length < len) {
6539         str = "0" + str;
6540     }
6541     return str;
6542 };
6543 
6544 
6545 
6546 /** ------------------------------------------------------------------------ **/
6547 
6548 /**
6549  * Image Loader service.
6550  * can be used to get a callback when a certain image is loaded
6551  *
6552  */
6553 links.imageloader = (function () {
6554     var urls = {};  // the loaded urls
6555     var callbacks = {}; // the urls currently being loaded. Each key contains
6556     // an array with callbacks
6557 
6558     /**
6559      * Check if an image url is loaded
6560      * @param {String} url
6561      * @return {boolean} loaded   True when loaded, false when not loaded
6562      *                            or when being loaded
6563      */
6564     function isLoaded (url) {
6565         if (urls[url] == true) {
6566             return true;
6567         }
6568 
6569         var image = new Image();
6570         image.src = url;
6571         if (image.complete) {
6572             return true;
6573         }
6574 
6575         return false;
6576     }
6577 
6578     /**
6579      * Check if an image url is being loaded
6580      * @param {String} url
6581      * @return {boolean} loading   True when being loaded, false when not loading
6582      *                             or when already loaded
6583      */
6584     function isLoading (url) {
6585         return (callbacks[url] != undefined);
6586     }
6587 
6588     /**
6589      * Load given image url
6590      * @param {String} url
6591      * @param {function} callback
6592      * @param {boolean} sendCallbackWhenAlreadyLoaded  optional
6593      */
6594     function load (url, callback, sendCallbackWhenAlreadyLoaded) {
6595         if (sendCallbackWhenAlreadyLoaded == undefined) {
6596             sendCallbackWhenAlreadyLoaded = true;
6597         }
6598 
6599         if (isLoaded(url)) {
6600             if (sendCallbackWhenAlreadyLoaded) {
6601                 callback(url);
6602             }
6603             return;
6604         }
6605 
6606         if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) {
6607             return;
6608         }
6609 
6610         var c = callbacks[url];
6611         if (!c) {
6612             var image = new Image();
6613             image.src = url;
6614 
6615             c = [];
6616             callbacks[url] = c;
6617 
6618             image.onload = function (event) {
6619                 urls[url] = true;
6620                 delete callbacks[url];
6621 
6622                 for (var i = 0; i < c.length; i++) {
6623                     c[i](url);
6624                 }
6625             }
6626         }
6627 
6628         if (c.indexOf(callback) == -1) {
6629             c.push(callback);
6630         }
6631     }
6632 
6633     /**
6634      * Load a set of images, and send a callback as soon as all images are
6635      * loaded
6636      * @param {String[]} urls
6637      * @param {function } callback
6638      * @param {boolean} sendCallbackWhenAlreadyLoaded
6639      */
6640     function loadAll (urls, callback, sendCallbackWhenAlreadyLoaded) {
6641         // list all urls which are not yet loaded
6642         var urlsLeft = [];
6643         urls.forEach(function (url) {
6644             if (!isLoaded(url)) {
6645                 urlsLeft.push(url);
6646             }
6647         });
6648 
6649         if (urlsLeft.length) {
6650             // there are unloaded images
6651             var countLeft = urlsLeft.length;
6652             urlsLeft.forEach(function (url) {
6653                 load(url, function () {
6654                     countLeft--;
6655                     if (countLeft == 0) {
6656                         // done!
6657                         callback();
6658                     }
6659                 }, sendCallbackWhenAlreadyLoaded);
6660             });
6661         }
6662         else {
6663             // we are already done!
6664             if (sendCallbackWhenAlreadyLoaded) {
6665                 callback();
6666             }
6667         }
6668     }
6669 
6670     /**
6671      * Recursively retrieve all image urls from the images located inside a given
6672      * HTML element
6673      * @param {Node} elem
6674      * @param {String[]} urls   Urls will be added here (no duplicates)
6675      */
6676     function filterImageUrls (elem, urls) {
6677         var child = elem.firstChild;
6678         while (child) {
6679             if (child.tagName == 'IMG') {
6680                 var url = child.src;
6681                 if (urls.indexOf(url) == -1) {
6682                     urls.push(url);
6683                 }
6684             }
6685 
6686             filterImageUrls(child, urls);
6687 
6688             child = child.nextSibling;
6689         }
6690     }
6691 
6692     return {
6693         'isLoaded': isLoaded,
6694         'isLoading': isLoading,
6695         'load': load,
6696         'loadAll': loadAll,
6697         'filterImageUrls': filterImageUrls
6698     };
6699 })();
6700 
6701 
6702 /** ------------------------------------------------------------------------ **/
6703 
6704 
6705 /**
6706  * Add and event listener. Works for all browsers
6707  * @param {Element} element    An html element
6708  * @param {string}      action     The action, for example "click",
6709  *                                 without the prefix "on"
6710  * @param {function}    listener   The callback function to be executed
6711  * @param {boolean}     useCapture
6712  */
6713 links.Timeline.addEventListener = function (element, action, listener, useCapture) {
6714     if (element.addEventListener) {
6715         if (useCapture === undefined)
6716             useCapture = false;
6717 
6718         if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
6719             action = "DOMMouseScroll";  // For Firefox
6720         }
6721 
6722         element.addEventListener(action, listener, useCapture);
6723     } else {
6724         element.attachEvent("on" + action, listener);  // IE browsers
6725     }
6726 };
6727 
6728 /**
6729  * Remove an event listener from an element
6730  * @param {Element}  element   An html dom element
6731  * @param {string}       action    The name of the event, for example "mousedown"
6732  * @param {function}     listener  The listener function
6733  * @param {boolean}      useCapture
6734  */
6735 links.Timeline.removeEventListener = function(element, action, listener, useCapture) {
6736     if (element.removeEventListener) {
6737         // non-IE browsers
6738         if (useCapture === undefined)
6739             useCapture = false;
6740 
6741         if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
6742             action = "DOMMouseScroll";  // For Firefox
6743         }
6744 
6745         element.removeEventListener(action, listener, useCapture);
6746     } else {
6747         // IE browsers
6748         element.detachEvent("on" + action, listener);
6749     }
6750 };
6751 
6752 
6753 /**
6754  * Get HTML element which is the target of the event
6755  * @param {Event} event
6756  * @return {Element} target element
6757  */
6758 links.Timeline.getTarget = function (event) {
6759     // code from http://www.quirksmode.org/js/events_properties.html
6760     if (!event) {
6761         event = window.event;
6762     }
6763 
6764     var target;
6765 
6766     if (event.target) {
6767         target = event.target;
6768     }
6769     else if (event.srcElement) {
6770         target = event.srcElement;
6771     }
6772 
6773     if (target.nodeType != undefined && target.nodeType == 3) {
6774         // defeat Safari bug
6775         target = target.parentNode;
6776     }
6777 
6778     return target;
6779 };
6780 
6781 /**
6782  * Stop event propagation
6783  */
6784 links.Timeline.stopPropagation = function (event) {
6785     if (!event)
6786         event = window.event;
6787 
6788     if (event.stopPropagation) {
6789         event.stopPropagation();  // non-IE browsers
6790     }
6791     else {
6792         event.cancelBubble = true;  // IE browsers
6793     }
6794 };
6795 
6796 
6797 /**
6798  * Cancels the event if it is cancelable, without stopping further propagation of the event.
6799  */
6800 links.Timeline.preventDefault = function (event) {
6801     if (!event)
6802         event = window.event;
6803 
6804     if (event.preventDefault) {
6805         event.preventDefault();  // non-IE browsers
6806     }
6807     else {
6808         event.returnValue = false;  // IE browsers
6809     }
6810 };
6811 
6812 
6813 /**
6814  * Retrieve the absolute left value of a DOM element
6815  * @param {Element} elem        A dom element, for example a div
6816  * @return {number} left        The absolute left position of this element
6817  *                              in the browser page.
6818  */
6819 links.Timeline.getAbsoluteLeft = function(elem) {
6820     var doc = document.documentElement;
6821     var body = document.body;
6822 
6823     var left = elem.offsetLeft;
6824     var e = elem.offsetParent;
6825     while (e != null && e != body && e != doc) {
6826         left += e.offsetLeft;
6827         left -= e.scrollLeft;
6828         e = e.offsetParent;
6829     }
6830     return left;
6831 };
6832 
6833 /**
6834  * Retrieve the absolute top value of a DOM element
6835  * @param {Element} elem        A dom element, for example a div
6836  * @return {number} top        The absolute top position of this element
6837  *                              in the browser page.
6838  */
6839 links.Timeline.getAbsoluteTop = function(elem) {
6840     var doc = document.documentElement;
6841     var body = document.body;
6842 
6843     var top = elem.offsetTop;
6844     var e = elem.offsetParent;
6845     while (e != null && e != body && e != doc) {
6846         top += e.offsetTop;
6847         top -= e.scrollTop;
6848         e = e.offsetParent;
6849     }
6850     return top;
6851 };
6852 
6853 /**
6854  * Get the absolute, vertical mouse position from an event.
6855  * @param {Event} event
6856  * @return {Number} pageY
6857  */
6858 links.Timeline.getPageY = function (event) {
6859     if (('targetTouches' in event) && event.targetTouches.length) {
6860         event = event.targetTouches[0];
6861     }
6862 
6863     if ('pageY' in event) {
6864         return event.pageY;
6865     }
6866 
6867     // calculate pageY from clientY
6868     var clientY = event.clientY;
6869     var doc = document.documentElement;
6870     var body = document.body;
6871     return clientY +
6872         ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
6873         ( doc && doc.clientTop || body && body.clientTop || 0 );
6874 };
6875 
6876 /**
6877  * Get the absolute, horizontal mouse position from an event.
6878  * @param {Event} event
6879  * @return {Number} pageX
6880  */
6881 links.Timeline.getPageX = function (event) {
6882     if (('targetTouches' in event) && event.targetTouches.length) {
6883         event = event.targetTouches[0];
6884     }
6885 
6886     if ('pageX' in event) {
6887         return event.pageX;
6888     }
6889 
6890     // calculate pageX from clientX
6891     var clientX = event.clientX;
6892     var doc = document.documentElement;
6893     var body = document.body;
6894     return clientX +
6895         ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
6896         ( doc && doc.clientLeft || body && body.clientLeft || 0 );
6897 };
6898 
6899 /**
6900  * Adds one or more className's to the given elements style
6901  * @param {Element} elem
6902  * @param {String} className
6903  */
6904 links.Timeline.addClassName = function(elem, className) {
6905     var classes = elem.className.split(' ');
6906     var classesToAdd = className.split(' ');
6907 
6908     var added = false;
6909     for (var i=0; i<classesToAdd.length; i++) {
6910         if (classes.indexOf(classesToAdd[i]) == -1) {
6911             classes.push(classesToAdd[i]); // add the class to the array
6912             added = true;
6913         }
6914     }
6915 
6916     if (added) {
6917         elem.className = classes.join(' ');
6918     }
6919 };
6920 
6921 /**
6922  * Removes one or more className's from the given elements style
6923  * @param {Element} elem
6924  * @param {String} className
6925  */
6926 links.Timeline.removeClassName = function(elem, className) {
6927     var classes = elem.className.split(' ');
6928     var classesToRemove = className.split(' ');
6929 
6930     var removed = false;
6931     for (var i=0; i<classesToRemove.length; i++) {
6932         var index = classes.indexOf(classesToRemove[i]);
6933         if (index != -1) {
6934             classes.splice(index, 1); // remove the class from the array
6935             removed = true;
6936         }
6937     }
6938 
6939     if (removed) {
6940         elem.className = classes.join(' ');
6941     }
6942 };
6943 
6944 /**
6945  * Check if given object is a Javascript Array
6946  * @param {*} obj
6947  * @return {Boolean} isArray    true if the given object is an array
6948  */
6949 // See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
6950 links.Timeline.isArray = function (obj) {
6951     if (obj instanceof Array) {
6952         return true;
6953     }
6954     return (Object.prototype.toString.call(obj) === '[object Array]');
6955 };
6956 
6957 /**
6958  * Shallow clone an object
6959  * @param {Object} object
6960  * @return {Object} clone
6961  */
6962 links.Timeline.clone = function (object) {
6963     var clone = {};
6964     for (var prop in object) {
6965         if (object.hasOwnProperty(prop)) {
6966             clone[prop] = object[prop];
6967         }
6968     }
6969     return clone;
6970 };
6971 
6972 /**
6973  * parse a JSON date
6974  * @param {Date | String | Number} date    Date object to be parsed. Can be:
6975  *                                         - a Date object like new Date(),
6976  *                                         - a long like 1356970529389,
6977  *                                         an ISO String like "2012-12-31T16:16:07.213Z",
6978  *                                         or a .Net Date string like
6979  *                                         "\/Date(1356970529389)\/"
6980  * @return {Date} parsedDate
6981  */
6982 links.Timeline.parseJSONDate = function (date) {
6983     if (date == undefined) {
6984         return undefined;
6985     }
6986 
6987     //test for date
6988     if (date instanceof Date) {
6989         return date;
6990     }
6991 
6992     // test for MS format.
6993     // FIXME: will fail on a Number
6994     var m = date.match(/\/Date\((-?\d+)([-\+]?\d{2})?(\d{2})?\)\//i);
6995     if (m) {
6996         var offset = m[2]
6997             ? (3600000 * m[2]) // hrs offset
6998             + (60000 * m[3] * (m[2] / Math.abs(m[2]))) // mins offset
6999             : 0;
7000 
7001         return new Date(
7002             (1 * m[1]) // ticks
7003                 + offset
7004         );
7005     }
7006 
7007     // failing that, try to parse whatever we've got.
7008     return Date.parse(date);
7009 };
7010