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