Project

General

Profile

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-2013 Almende B.V.
31
 *
32
 * @author     Jos de Jong, <jos@almende.org>
33
 * @date    2013-08-20
34
 * @version 2.5.0
35
 */
36

    
37
/*
38
 * i18n mods by github user iktuz (https://gist.github.com/iktuz/3749287/)
39
 * added to v2.4.1 with da_DK language by @bjarkebech
40
 */
41

    
42
/*
43
 * TODO
44
 *
45
 * Add zooming with pinching on Android
46
 * 
47
 * Bug: when an item contains a javascript onclick or a link, this does not work
48
 *      when the item is not selected (when the item is being selected,
49
 *      it is redrawn, which cancels any onclick or link action)
50
 * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly
51
 * Bug: neglect items when they have no valid start/end, instead of throwing an error
52
 * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
53
 * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
54
 * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
55
 */
56

    
57
/**
58
 * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
59
 * "links"
60
 */
61
if (typeof links === 'undefined') {
62
    links = {};
63
    // important: do not use var, as "var links = {};" will overwrite 
64
    //            the existing links variable value with undefined in IE8, IE7.  
65
}
66

    
67

    
68
/**
69
 * Ensure the variable google exists
70
 */
71
if (typeof google === 'undefined') {
72
    google = undefined;
73
    // important: do not use var, as "var google = undefined;" will overwrite 
74
    //            the existing google variable value with undefined in IE8, IE7.
75
}
76

    
77

    
78

    
79
// Internet Explorer 8 and older does not support Array.indexOf,
80
// so we define it here in that case
81
// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
82
if(!Array.prototype.indexOf) {
83
    Array.prototype.indexOf = function(obj){
84
        for(var i = 0; i < this.length; i++){
85
            if(this[i] == obj){
86
                return i;
87
            }
88
        }
89
        return -1;
90
    }
91
}
92

    
93
// Internet Explorer 8 and older does not support Array.forEach,
94
// so we define it here in that case
95
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
96
if (!Array.prototype.forEach) {
97
    Array.prototype.forEach = function(fn, scope) {
98
        for(var i = 0, len = this.length; i < len; ++i) {
99
            fn.call(scope || this, this[i], i, this);
100
        }
101
    }
102
}
103

    
104

    
105
/**
106
 * @constructor links.Timeline
107
 * The timeline is a visualization chart to visualize events in time.
108
 *
109
 * The timeline is developed in javascript as a Google Visualization Chart.
110
 *
111
 * @param {Element} container   The DOM element in which the Timeline will
112
 *                                  be created. Normally a div element.
113
 */
114
links.Timeline = function(container) {
115
    if (!container) {
116
        // this call was probably only for inheritance, no constructor-code is required
117
        return;
118
    }
119

    
120
    // create variables and set default values
121
    this.dom = {};
122
    this.conversion = {};
123
    this.eventParams = {}; // stores parameters for mouse events
124
    this.groups = [];
125
    this.groupIndexes = {};
126
    this.items = [];
127
    this.renderQueue = {
128
        show: [],   // Items made visible but not yet added to DOM
129
        hide: [],   // Items currently visible but not yet removed from DOM
130
        update: []  // Items with changed data but not yet adjusted DOM
131
    };
132
    this.renderedItems = [];  // Items currently rendered in the DOM
133
    this.clusterGenerator = new links.Timeline.ClusterGenerator(this);
134
    this.currentClusters = [];
135
    this.selection = undefined; // stores index and item which is currently selected
136

    
137
    this.listeners = {}; // event listener callbacks
138

    
139
    // Initialize sizes. 
140
    // Needed for IE (which gives an error when you try to set an undefined
141
    // value in a style)
142
    this.size = {
143
        'actualHeight': 0,
144
        'axis': {
145
            'characterMajorHeight': 0,
146
            'characterMajorWidth': 0,
147
            'characterMinorHeight': 0,
148
            'characterMinorWidth': 0,
149
            'height': 0,
150
            'labelMajorTop': 0,
151
            'labelMinorTop': 0,
152
            'line': 0,
153
            'lineMajorWidth': 0,
154
            'lineMinorHeight': 0,
155
            'lineMinorTop': 0,
156
            'lineMinorWidth': 0,
157
            'top': 0
158
        },
159
        'contentHeight': 0,
160
        'contentLeft': 0,
161
        'contentWidth': 0,
162
        'frameHeight': 0,
163
        'frameWidth': 0,
164
        'groupsLeft': 0,
165
        'groupsWidth': 0,
166
        'items': {
167
            'top': 0
168
        }
169
    };
170

    
171
    this.dom.container = container;
172

    
173
    this.options = {
174
        'width': "100%",
175
        'height': "auto",
176
        'minHeight': 0,        // minimal height in pixels
177
        'autoHeight': true,
178

    
179
        'eventMargin': 10,     // minimal margin between events
180
        'eventMarginAxis': 20, // minimal margin between events and the axis
181
        'dragAreaWidth': 10,   // pixels
182

    
183
        'min': undefined,
184
        'max': undefined,
185
        'zoomMin': 10,     // milliseconds
186
        'zoomMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
187

    
188
        'moveable': true,
189
        'zoomable': true,
190
        'selectable': true,
191
        'unselectable': true,
192
        'editable': false,
193
        'snapEvents': true,
194
        'groupChangeable': true,
195

    
196
        'showCurrentTime': true, // show a red bar displaying the current time
197
        'showCustomTime': false, // show a blue, draggable bar displaying a custom time    
198
        'showMajorLabels': true,
199
        'showMinorLabels': true,
200
        'showNavigation': false,
201
        'showButtonNew': false,
202
        'groupsOnRight': false,
203
        'axisOnTop': false,
204
        'stackEvents': true,
205
        'animate': true,
206
        'animateZoom': true,
207
        'cluster': false,
208
        'style': 'box',
209
        '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
210
        
211
        // i18n: Timeline only has built-in English text per default. Include timeline-locales.js to support more localized text.
212
        'locale': 'en',
213
        'MONTHS': new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"),
214
        'MONTHS_SHORT': new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"),
215
        'DAYS': new Array("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"),
216
        'DAYS_SHORT': new Array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"),
217
        'ZOOM_IN': "Zoom in",
218
        'ZOOM_OUT': "Zoom out",
219
        'MOVE_LEFT': "Move left",
220
        'MOVE_RIGHT': "Move right",
221
        'NEW': "New",
222
        'CREATE_NEW_EVENT': "Create new event"
223
    };
224

    
225
    this.clientTimeOffset = 0;    // difference between client time and the time
226
    // set via Timeline.setCurrentTime()
227
    var dom = this.dom;
228

    
229
    // remove all elements from the container element.
230
    while (dom.container.hasChildNodes()) {
231
        dom.container.removeChild(dom.container.firstChild);
232
    }
233

    
234
    // create a step for drawing the axis
235
    this.step = new links.Timeline.StepDate();
236

    
237
    // add standard item types
238
    this.itemTypes = {
239
        box:   links.Timeline.ItemBox,
240
        range: links.Timeline.ItemRange,
241
        dot:   links.Timeline.ItemDot
242
    };
243

    
244
    // initialize data
245
    this.data = [];
246
    this.firstDraw = true;
247

    
248
    // date interval must be initialized 
249
    this.setVisibleChartRange(undefined, undefined, false);
250

    
251
    // render for the first time
252
    this.render();
253

    
254
    // fire the ready event
255
    var me = this;
256
    setTimeout(function () {
257
        me.trigger('ready');
258
    }, 0);
259
};
260

    
261

    
262
/**
263
 * Main drawing logic. This is the function that needs to be called
264
 * in the html page, to draw the timeline.
265
 *
266
 * A data table with the events must be provided, and an options table.
267
 *
268
 * @param {google.visualization.DataTable}      data
269
 *                                 The data containing the events for the timeline.
270
 *                                 Object DataTable is defined in
271
 *                                 google.visualization.DataTable
272
 * @param {Object} options         A name/value map containing settings for the
273
 *                                 timeline. Optional.
274
 */
275
links.Timeline.prototype.draw = function(data, options) {
276
    this.setOptions(options);
277
    
278
    if (this.options.selectable) {
279
        links.Timeline.addClassName(this.dom.frame, "timeline-selectable");
280
    }
281

    
282
    // read the data
283
    this.setData(data);
284

    
285
    // set timer range. this will also redraw the timeline
286
    if (options && (options.start || options.end)) {
287
        this.setVisibleChartRange(options.start, options.end);
288
    }
289
    else if (this.firstDraw) {
290
        this.setVisibleChartRangeAuto();
291
    }
292

    
293
    this.firstDraw = false;
294
};
295

    
296

    
297
/**
298
 * Set options for the timeline.
299
 * Timeline must be redrawn afterwards
300
 * @param {Object} options A name/value map containing settings for the
301
 *                                 timeline. Optional.
302
 */
303
links.Timeline.prototype.setOptions = function(options) {
304
    if (options) {
305
        // retrieve parameter values
306
        for (var i in options) {
307
            if (options.hasOwnProperty(i)) {
308
                this.options[i] = options[i];
309
            }
310
        }
311
        
312
        // prepare i18n dependent on set locale
313
        if (typeof links.locales !== 'undefined' && this.options.locale !== 'en') {
314
            var localeOpts = links.locales[this.options.locale];
315
            if(localeOpts) {
316
                for (var l in localeOpts) {
317
                    if (localeOpts.hasOwnProperty(l)) {
318
                        this.options[l] = localeOpts[l];
319
                    }
320
                }
321
            }
322
        }
323

    
324
        // check for deprecated options
325
        if (options.showButtonAdd != undefined) {
326
            this.options.showButtonNew = options.showButtonAdd;
327
            console.log('WARNING: Option showButtonAdd is deprecated. Use showButtonNew instead');
328
        }
329
        if (options.intervalMin != undefined) {
330
            this.options.zoomMin = options.intervalMin;
331
            console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead');
332
        }
333
        if (options.intervalMax != undefined) {
334
            this.options.zoomMax = options.intervalMax;
335
            console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead');
336
        }
337

    
338
        if (options.scale && options.step) {
339
            this.step.setScale(options.scale, options.step);
340
        }
341
    }
342

    
343
    // validate options
344
    this.options.autoHeight = (this.options.height === "auto");
345
};
346

    
347
/**
348
 * Add new type of items
349
 * @param {String} typeName  Name of new type
350
 * @param {links.Timeline.Item} typeFactory Constructor of items
351
 */
352
links.Timeline.prototype.addItemType = function (typeName, typeFactory) {
353
    this.itemTypes[typeName] = typeFactory;
354
};
355

    
356
/**
357
 * Retrieve a map with the column indexes of the columns by column name.
358
 * For example, the method returns the map
359
 *     {
360
 *         start: 0,
361
 *         end: 1,
362
 *         content: 2,
363
 *         group: undefined,
364
 *         className: undefined
365
 *         editable: undefined
366
 *         type: undefined
367
 *     }
368
 * @param {google.visualization.DataTable} dataTable
369
 * @type {Object} map
370
 */
371
links.Timeline.mapColumnIds = function (dataTable) {
372
    var cols = {},
373
        colCount = dataTable.getNumberOfColumns(),
374
        allUndefined = true;
375

    
376
    // loop over the columns, and map the column id's to the column indexes
377
    for (var col = 0; col < colCount; col++) {
378
        var id = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
379
        cols[id] = col;
380
        if (id == 'start' || id == 'end' || id == 'content' || id == 'group' ||
381
            id == 'className' || id == 'editable' || id == 'type') {
382
            allUndefined = false;
383
        }
384
    }
385

    
386
    // if no labels or ids are defined, use the default mapping
387
    // for start, end, content, group, className, editable, type
388
    if (allUndefined) {
389
        cols.start = 0;
390
        cols.end = 1;
391
        cols.content = 2;
392
        if (colCount >= 3) {cols.group = 3}
393
        if (colCount >= 4) {cols.className = 4}
394
        if (colCount >= 5) {cols.editable = 5}
395
        if (colCount >= 6) {cols.type = 6}
396
    }
397

    
398
    return cols;
399
};
400

    
401
/**
402
 * Set data for the timeline
403
 * @param {google.visualization.DataTable | Array} data
404
 */
405
links.Timeline.prototype.setData = function(data) {
406
    // unselect any previously selected item
407
    this.unselectItem();
408

    
409
    if (!data) {
410
        data = [];
411
    }
412

    
413
    // clear all data
414
    this.stackCancelAnimation();
415
    this.clearItems();
416
    this.data = data;
417
    var items = this.items;
418
    this.deleteGroups();
419

    
420
    if (google && google.visualization &&
421
        data instanceof google.visualization.DataTable) {
422
        // map the datatable columns
423
        var cols = links.Timeline.mapColumnIds(data);
424

    
425
        // read DataTable
426
        for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
427
            items.push(this.createItem({
428
                'start':     ((cols.start != undefined)     ? data.getValue(row, cols.start)     : undefined),
429
                'end':       ((cols.end != undefined)       ? data.getValue(row, cols.end)       : undefined),
430
                'content':   ((cols.content != undefined)   ? data.getValue(row, cols.content)   : undefined),
431
                'group':     ((cols.group != undefined)     ? data.getValue(row, cols.group)     : undefined),
432
                'className': ((cols.className != undefined) ? data.getValue(row, cols.className) : undefined),
433
                'editable':  ((cols.editable != undefined)  ? data.getValue(row, cols.editable)  : undefined),
434
                'type':      ((cols.editable != undefined)  ? data.getValue(row, cols.type)      : undefined)
435
            }));
436
        }
437
    }
438
    else if (links.Timeline.isArray(data)) {
439
        // read JSON array
440
        for (var row = 0, rows = data.length; row < rows; row++) {
441
            var itemData = data[row];
442
            var item = this.createItem(itemData);
443
            items.push(item);
444
        }
445
    }
446
    else {
447
        throw "Unknown data type. DataTable or Array expected.";
448
    }
449

    
450
    // prepare data for clustering, by filtering and sorting by type
451
    if (this.options.cluster) {
452
        this.clusterGenerator.setData(this.items);
453
    }
454

    
455
    this.render({
456
        animate: false
457
    });
458
};
459

    
460
/**
461
 * Return the original data table.
462
 * @return {google.visualization.DataTable | Array} data
463
 */
464
links.Timeline.prototype.getData = function  () {
465
    return this.data;
466
};
467

    
468

    
469
/**
470
 * Update the original data with changed start, end or group.
471
 *
472
 * @param {Number} index
473
 * @param {Object} values   An object containing some of the following parameters:
474
 *                          {Date} start,
475
 *                          {Date} end,
476
 *                          {String} content,
477
 *                          {String} group
478
 */
479
links.Timeline.prototype.updateData = function  (index, values) {
480
    var data = this.data,
481
        prop;
482

    
483
    if (google && google.visualization &&
484
        data instanceof google.visualization.DataTable) {
485
        // update the original google DataTable
486
        var missingRows = (index + 1) - data.getNumberOfRows();
487
        if (missingRows > 0) {
488
            data.addRows(missingRows);
489
        }
490

    
491
        // map the column id's by name
492
        var cols = links.Timeline.mapColumnIds(data);
493

    
494
        // merge all fields from the provided data into the current data
495
        for (prop in values) {
496
            if (values.hasOwnProperty(prop)) {
497
                var col = cols[prop];
498
                if (col == undefined) {
499
                    // create new column
500
                    var value = values[prop];
501
                    var valueType = 'string';
502
                    if (typeof(value) == 'number')       {valueType = 'number';}
503
                    else if (typeof(value) == 'boolean') {valueType = 'boolean';}
504
                    else if (value instanceof Date)      {valueType = 'datetime';}
505
                    col = data.addColumn(valueType, prop);
506
                }
507
                data.setValue(index, col, values[prop]);
508

    
509
                // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
510
            }
511
        }
512
    }
513
    else if (links.Timeline.isArray(data)) {
514
        // update the original JSON table
515
        var row = data[index];
516
        if (row == undefined) {
517
            row = {};
518
            data[index] = row;
519
        }
520

    
521
        // merge all fields from the provided data into the current data
522
        for (prop in values) {
523
            if (values.hasOwnProperty(prop)) {
524
                row[prop] = values[prop];
525

    
526
                // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
527
            }
528
        }
529
    }
530
    else {
531
        throw "Cannot update data, unknown type of data";
532
    }
533
};
534

    
535
/**
536
 * Find the item index from a given HTML element
537
 * If no item index is found, undefined is returned
538
 * @param {Element} element
539
 * @return {Number | undefined} index
540
 */
541
links.Timeline.prototype.getItemIndex = function(element) {
542
    var e = element,
543
        dom = this.dom,
544
        frame = dom.items.frame,
545
        items = this.items,
546
        index = undefined;
547

    
548
    // try to find the frame where the items are located in
549
    while (e.parentNode && e.parentNode !== frame) {
550
        e = e.parentNode;
551
    }
552

    
553
    if (e.parentNode === frame) {
554
        // yes! we have found the parent element of all items
555
        // retrieve its id from the array with items
556
        for (var i = 0, iMax = items.length; i < iMax; i++) {
557
            if (items[i].dom === e) {
558
                index = i;
559
                break;
560
            }
561
        }
562
    }
563

    
564
    return index;
565
};
566

    
567
/**
568
 * Set a new size for the timeline
569
 * @param {string} width   Width in pixels or percentage (for example "800px"
570
 *                         or "50%")
571
 * @param {string} height  Height in pixels or percentage  (for example "400px"
572
 *                         or "30%")
573
 */
574
links.Timeline.prototype.setSize = function(width, height) {
575
    if (width) {
576
        this.options.width = width;
577
        this.dom.frame.style.width = width;
578
    }
579
    if (height) {
580
        this.options.height = height;
581
        this.options.autoHeight = (this.options.height === "auto");
582
        if (height !==  "auto" ) {
583
            this.dom.frame.style.height = height;
584
        }
585
    }
586

    
587
    this.render({
588
        animate: false
589
    });
590
};
591

    
592

    
593
/**
594
 * Set a new value for the visible range int the timeline.
595
 * Set start undefined to include everything from the earliest date to end.
596
 * Set end undefined to include everything from start to the last date.
597
 * Example usage:
598
 *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
599
 *                                    new Date("2010-09-13"));
600
 * @param {Date}   start     The start date for the timeline. optional
601
 * @param {Date}   end       The end date for the timeline. optional
602
 * @param {boolean} redraw   Optional. If true (default) the Timeline is
603
 *                           directly redrawn
604
 */
605
links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
606
    var range = {};
607
    if (!start || !end) {
608
        // retrieve the date range of the items
609
        range = this.getDataRange(true);
610
    }
611

    
612
    if (!start) {
613
        if (end) {
614
            if (range.min && range.min.valueOf() < end.valueOf()) {
615
                // start of the data
616
                start = range.min;
617
            }
618
            else {
619
                // 7 days before the end
620
                start = new Date(end.valueOf());
621
                start.setDate(start.getDate() - 7);
622
            }
623
        }
624
        else {
625
            // default of 3 days ago
626
            start = new Date();
627
            start.setDate(start.getDate() - 3);
628
        }
629
    }
630

    
631
    if (!end) {
632
        if (range.max) {
633
            // end of the data
634
            end = range.max;
635
        }
636
        else {
637
            // 7 days after start
638
            end = new Date(start.valueOf());
639
            end.setDate(end.getDate() + 7);
640
        }
641
    }
642

    
643
    // prevent start Date <= end Date
644
    if (end <= start) {
645
        end = new Date(start.valueOf());
646
        end.setDate(end.getDate() + 7);
647
    }
648

    
649
    // limit to the allowed range (don't let this do by applyRange,
650
    // because that method will try to maintain the interval (end-start)
651
    var min = this.options.min ? this.options.min : undefined; // date
652
    if (min != undefined && start.valueOf() < min.valueOf()) {
653
        start = new Date(min.valueOf()); // date
654
    }
655
    var max = this.options.max ? this.options.max : undefined; // date
656
    if (max != undefined && end.valueOf() > max.valueOf()) {
657
        end = new Date(max.valueOf()); // date
658
    }
659

    
660
    this.applyRange(start, end);
661

    
662
    if (redraw == undefined || redraw == true) {
663
        this.render({
664
            animate: false
665
        });  // TODO: optimize, no reflow needed
666
    }
667
    else {
668
        this.recalcConversion();
669
    }
670
};
671

    
672

    
673
/**
674
 * Change the visible chart range such that all items become visible
675
 */
676
links.Timeline.prototype.setVisibleChartRangeAuto = function() {
677
    var range = this.getDataRange(true);
678
    this.setVisibleChartRange(range.min, range.max);
679
};
680

    
681
/**
682
 * Adjust the visible range such that the current time is located in the center
683
 * of the timeline
684
 */
685
links.Timeline.prototype.setVisibleChartRangeNow = function() {
686
    var now = new Date();
687

    
688
    var diff = (this.end.valueOf() - this.start.valueOf());
689

    
690
    var startNew = new Date(now.valueOf() - diff/2);
691
    var endNew = new Date(startNew.valueOf() + diff);
692
    this.setVisibleChartRange(startNew, endNew);
693
};
694

    
695

    
696
/**
697
 * Retrieve the current visible range in the timeline.
698
 * @return {Object} An object with start and end properties
699
 */
700
links.Timeline.prototype.getVisibleChartRange = function() {
701
    return {
702
        'start': new Date(this.start.valueOf()),
703
        'end': new Date(this.end.valueOf())
704
    };
705
};
706

    
707
/**
708
 * Get the date range of the items.
709
 * @param {boolean} [withMargin]  If true, 5% of whitespace is added to the
710
 *                                left and right of the range. Default is false.
711
 * @return {Object} range    An object with parameters min and max.
712
 *                           - {Date} min is the lowest start date of the items
713
 *                           - {Date} max is the highest start or end date of the items
714
 *                           If no data is available, the values of min and max
715
 *                           will be undefined
716
 */
717
links.Timeline.prototype.getDataRange = function (withMargin) {
718
    var items = this.items,
719
        min = undefined, // number
720
        max = undefined; // number
721

    
722
    if (items) {
723
        for (var i = 0, iMax = items.length; i < iMax; i++) {
724
            var item = items[i],
725
                start = item.start != undefined ? item.start.valueOf() : undefined,
726
                end   = item.end != undefined   ? item.end.valueOf() : start;
727

    
728
            if (start != undefined) {
729
                min = (min != undefined) ? Math.min(min.valueOf(), start.valueOf()) : start;
730
            }
731

    
732
            if (end != undefined) {
733
                max = (max != undefined) ? Math.max(max.valueOf(), end.valueOf()) : end;
734
            }
735
        }
736
    }
737

    
738
    if (min && max && withMargin) {
739
        // zoom out 5% such that you have a little white space on the left and right
740
        var diff = (max - min);
741
        min = min - diff * 0.05;
742
        max = max + diff * 0.05;
743
    }
744

    
745
    return {
746
        'min': min != undefined ? new Date(min) : undefined,
747
        'max': max != undefined ? new Date(max) : undefined
748
    };
749
};
750

    
751
/**
752
 * Re-render (reflow and repaint) all components of the Timeline: frame, axis,
753
 * items, ...
754
 * @param {Object} [options]  Available options:
755
 *                            {boolean} renderTimesLeft   Number of times the
756
 *                                                        render may be repeated
757
 *                                                        5 times by default.
758
 *                            {boolean} animate           takes options.animate
759
 *                                                        as default value
760
 */
761
links.Timeline.prototype.render = function(options) {
762
    var frameResized = this.reflowFrame();
763
    var axisResized = this.reflowAxis();
764
    var groupsResized = this.reflowGroups();
765
    var itemsResized = this.reflowItems();
766
    var resized = (frameResized || axisResized || groupsResized || itemsResized);
767

    
768
    // TODO: only stackEvents/filterItems when resized or changed. (gives a bootstrap issue).
769
    // if (resized) {
770
    var animate = this.options.animate;
771
    if (options && options.animate != undefined) {
772
        animate = options.animate;
773
    }
774

    
775
    this.recalcConversion();
776
    this.clusterItems();
777
    this.filterItems();
778
    this.stackItems(animate);
779

    
780
    this.recalcItems();
781

    
782
    // TODO: only repaint when resized or when filterItems or stackItems gave a change?
783
    var needsReflow = this.repaint();
784

    
785
    // re-render once when needed (prevent endless re-render loop)
786
    if (needsReflow) {
787
        var renderTimesLeft = options ? options.renderTimesLeft : undefined;
788
        if (renderTimesLeft == undefined) {
789
            renderTimesLeft = 5;
790
        }
791
        if (renderTimesLeft > 0) {
792
            this.render({
793
                'animate': options ? options.animate: undefined,
794
                'renderTimesLeft': (renderTimesLeft - 1)
795
            });
796
        }
797
    }
798
};
799

    
800
/**
801
 * Repaint all components of the Timeline
802
 * @return {boolean} needsReflow   Returns true if the DOM is changed such that
803
 *                                 a reflow is needed.
804
 */
805
links.Timeline.prototype.repaint = function() {
806
    var frameNeedsReflow = this.repaintFrame();
807
    var axisNeedsReflow  = this.repaintAxis();
808
    var groupsNeedsReflow  = this.repaintGroups();
809
    var itemsNeedsReflow = this.repaintItems();
810
    this.repaintCurrentTime();
811
    this.repaintCustomTime();
812

    
813
    return (frameNeedsReflow || axisNeedsReflow || groupsNeedsReflow || itemsNeedsReflow);
814
};
815

    
816
/**
817
 * Reflow the timeline frame
818
 * @return {boolean} resized    Returns true if any of the frame elements
819
 *                              have been resized.
820
 */
821
links.Timeline.prototype.reflowFrame = function() {
822
    var dom = this.dom,
823
        options = this.options,
824
        size = this.size,
825
        resized = false;
826

    
827
    // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
828
    var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
829
        frameHeight = dom.frame ? dom.frame.clientHeight : 0;
830

    
831
    resized = resized || (size.frameWidth !== frameWidth);
832
    resized = resized || (size.frameHeight !== frameHeight);
833
    size.frameWidth = frameWidth;
834
    size.frameHeight = frameHeight;
835

    
836
    return resized;
837
};
838

    
839
/**
840
 * repaint the Timeline frame
841
 * @return {boolean} needsReflow   Returns true if the DOM is changed such that
842
 *                                 a reflow is needed.
843
 */
844
links.Timeline.prototype.repaintFrame = function() {
845
    var needsReflow = false,
846
        dom = this.dom,
847
        options = this.options,
848
        size = this.size;
849

    
850
    // main frame
851
    if (!dom.frame) {
852
        dom.frame = document.createElement("DIV");
853
        dom.frame.className = "timeline-frame ui-widget ui-widget-content ui-corner-all";
854
        dom.frame.style.position = "relative";
855
        dom.frame.style.overflow = "hidden";
856
        dom.container.appendChild(dom.frame);
857
        needsReflow = true;
858
    }
859

    
860
    var height = options.autoHeight ?
861
        (size.actualHeight + "px") :
862
        (options.height || "100%");
863
    var width  = options.width || "100%";
864
    needsReflow = needsReflow || (dom.frame.style.height != height);
865
    needsReflow = needsReflow || (dom.frame.style.width != width);
866
    dom.frame.style.height = height;
867
    dom.frame.style.width = width;
868

    
869
    // contents
870
    if (!dom.content) {
871
        // create content box where the axis and items will be created
872
        dom.content = document.createElement("DIV");
873
        dom.content.style.position = "relative";
874
        dom.content.style.overflow = "hidden";
875
        dom.frame.appendChild(dom.content);
876

    
877
        var timelines = document.createElement("DIV");
878
        timelines.style.position = "absolute";
879
        timelines.style.left = "0px";
880
        timelines.style.top = "0px";
881
        timelines.style.height = "100%";
882
        timelines.style.width = "0px";
883
        dom.content.appendChild(timelines);
884
        dom.contentTimelines = timelines;
885

    
886
        var params = this.eventParams,
887
            me = this;
888
        if (!params.onMouseDown) {
889
            params.onMouseDown = function (event) {me.onMouseDown(event);};
890
            links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
891
        }
892
        if (!params.onTouchStart) {
893
            params.onTouchStart = function (event) {me.onTouchStart(event);};
894
            links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
895
        }
896
        if (!params.onMouseWheel) {
897
            params.onMouseWheel = function (event) {me.onMouseWheel(event);};
898
            links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
899
        }
900
        if (!params.onDblClick) {
901
            params.onDblClick = function (event) {me.onDblClick(event);};
902
            links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
903
        }
904

    
905
        needsReflow = true;
906
    }
907
    dom.content.style.left = size.contentLeft + "px";
908
    dom.content.style.top = "0px";
909
    dom.content.style.width = size.contentWidth + "px";
910
    dom.content.style.height = size.frameHeight + "px";
911

    
912
    this.repaintNavigation();
913

    
914
    return needsReflow;
915
};
916

    
917
/**
918
 * Reflow the timeline axis. Calculate its height, width, positioning, etc...
919
 * @return {boolean} resized    returns true if the axis is resized
920
 */
921
links.Timeline.prototype.reflowAxis = function() {
922
    var resized = false,
923
        dom = this.dom,
924
        options = this.options,
925
        size = this.size,
926
        axisDom = dom.axis;
927

    
928
    var characterMinorWidth  = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientWidth : 0,
929
        characterMinorHeight = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientHeight : 0,
930
        characterMajorWidth  = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientWidth : 0,
931
        characterMajorHeight = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientHeight : 0,
932
        axisHeight = (options.showMinorLabels ? characterMinorHeight : 0) +
933
            (options.showMajorLabels ? characterMajorHeight : 0);
934

    
935
    var axisTop  = options.axisOnTop ? 0 : size.frameHeight - axisHeight,
936
        axisLine = options.axisOnTop ? axisHeight : axisTop;
937

    
938
    resized = resized || (size.axis.top !== axisTop);
939
    resized = resized || (size.axis.line !== axisLine);
940
    resized = resized || (size.axis.height !== axisHeight);
941
    size.axis.top = axisTop;
942
    size.axis.line = axisLine;
943
    size.axis.height = axisHeight;
944
    size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine +
945
        (options.showMinorLabels ? characterMinorHeight : 0);
946
    size.axis.labelMinorTop = options.axisOnTop ?
947
        (options.showMajorLabels ? characterMajorHeight : 0) :
948
        axisLine;
949
    size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
950
    size.axis.lineMinorHeight = options.showMajorLabels ?
951
        size.frameHeight - characterMajorHeight:
952
        size.frameHeight;
953
    if (axisDom && axisDom.minorLines && axisDom.minorLines.length) {
954
        size.axis.lineMinorWidth = axisDom.minorLines[0].offsetWidth;
955
    }
956
    else {
957
        size.axis.lineMinorWidth = 1;
958
    }
959
    if (axisDom && axisDom.majorLines && axisDom.majorLines.length) {
960
        size.axis.lineMajorWidth = axisDom.majorLines[0].offsetWidth;
961
    }
962
    else {
963
        size.axis.lineMajorWidth = 1;
964
    }
965

    
966
    resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
967
    resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
968
    resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
969
    resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
970
    size.axis.characterMinorWidth  = characterMinorWidth;
971
    size.axis.characterMinorHeight = characterMinorHeight;
972
    size.axis.characterMajorWidth  = characterMajorWidth;
973
    size.axis.characterMajorHeight = characterMajorHeight;
974

    
975
    var contentHeight = Math.max(size.frameHeight - axisHeight, 0);
976
    size.contentLeft = options.groupsOnRight ? 0 : size.groupsWidth;
977
    size.contentWidth = Math.max(size.frameWidth - size.groupsWidth, 0);
978
    size.contentHeight = contentHeight;
979

    
980
    return resized;
981
};
982

    
983
/**
984
 * Redraw the timeline axis with minor and major labels
985
 * @return {boolean} needsReflow     Returns true if the DOM is changed such
986
 *                                   that a reflow is needed.
987
 */
988
links.Timeline.prototype.repaintAxis = function() {
989
    var needsReflow = false,
990
        dom = this.dom,
991
        options = this.options,
992
        size = this.size,
993
        step = this.step;
994

    
995
    var axis = dom.axis;
996
    if (!axis) {
997
        axis = {};
998
        dom.axis = axis;
999
    }
1000
    if (!size.axis.properties) {
1001
        size.axis.properties = {};
1002
    }
1003
    if (!axis.minorTexts) {
1004
        axis.minorTexts = [];
1005
    }
1006
    if (!axis.minorLines) {
1007
        axis.minorLines = [];
1008
    }
1009
    if (!axis.majorTexts) {
1010
        axis.majorTexts = [];
1011
    }
1012
    if (!axis.majorLines) {
1013
        axis.majorLines = [];
1014
    }
1015

    
1016
    if (!axis.frame) {
1017
        axis.frame = document.createElement("DIV");
1018
        axis.frame.style.position = "absolute";
1019
        axis.frame.style.left = "0px";
1020
        axis.frame.style.top = "0px";
1021
        dom.content.appendChild(axis.frame);
1022
    }
1023

    
1024
    // take axis offline
1025
    dom.content.removeChild(axis.frame);
1026

    
1027
    axis.frame.style.width = (size.contentWidth) + "px";
1028
    axis.frame.style.height = (size.axis.height) + "px";
1029

    
1030
    // the drawn axis is more wide than the actual visual part, such that
1031
    // the axis can be dragged without having to redraw it each time again.
1032
    var start = this.screenToTime(0);
1033
    var end = this.screenToTime(size.contentWidth);
1034

    
1035
    // calculate minimum step (in milliseconds) based on character size
1036
    if (size.axis.characterMinorWidth) {
1037
        this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6) -
1038
            this.screenToTime(0);
1039

    
1040
        step.setRange(start, end, this.minimumStep);
1041
    }
1042

    
1043
    var charsNeedsReflow = this.repaintAxisCharacters();
1044
    needsReflow = needsReflow || charsNeedsReflow;
1045

    
1046
    // The current labels on the axis will be re-used (much better performance),
1047
    // therefore, the repaintAxis method uses the mechanism with
1048
    // repaintAxisStartOverwriting, repaintAxisEndOverwriting, and
1049
    // this.size.axis.properties is used.
1050
    this.repaintAxisStartOverwriting();
1051

    
1052
    step.start();
1053
    var xFirstMajorLabel = undefined;
1054
    var max = 0;
1055
    while (!step.end() && max < 1000) {
1056
        max++;
1057
        var cur = step.getCurrent(),
1058
            x = this.timeToScreen(cur),
1059
            isMajor = step.isMajor();
1060

    
1061
        if (options.showMinorLabels) {
1062
            this.repaintAxisMinorText(x, step.getLabelMinor(options));
1063
        }
1064

    
1065
        if (isMajor && options.showMajorLabels) {
1066
            if (x > 0) {
1067
                if (xFirstMajorLabel == undefined) {
1068
                    xFirstMajorLabel = x;
1069
                }
1070
                this.repaintAxisMajorText(x, step.getLabelMajor(options));
1071
            }
1072
            this.repaintAxisMajorLine(x);
1073
        }
1074
        else {
1075
            this.repaintAxisMinorLine(x);
1076
        }
1077

    
1078
        step.next();
1079
    }
1080

    
1081
    // create a major label on the left when needed
1082
    if (options.showMajorLabels) {
1083
        var leftTime = this.screenToTime(0),
1084
            leftText = this.step.getLabelMajor(options, leftTime),
1085
            width = leftText.length * size.axis.characterMajorWidth + 10; // upper bound estimation
1086

    
1087
        if (xFirstMajorLabel == undefined || width < xFirstMajorLabel) {
1088
            this.repaintAxisMajorText(0, leftText, leftTime);
1089
        }
1090
    }
1091

    
1092
    // cleanup left over labels
1093
    this.repaintAxisEndOverwriting();
1094

    
1095
    this.repaintAxisHorizontal();
1096

    
1097
    // put axis online
1098
    dom.content.insertBefore(axis.frame, dom.content.firstChild);
1099

    
1100
    return needsReflow;
1101
};
1102

    
1103
/**
1104
 * Create characters used to determine the size of text on the axis
1105
 * @return {boolean} needsReflow   Returns true if the DOM is changed such that
1106
 *                                 a reflow is needed.
1107
 */
1108
links.Timeline.prototype.repaintAxisCharacters = function () {
1109
    // calculate the width and height of a single character
1110
    // this is used to calculate the step size, and also the positioning of the
1111
    // axis
1112
    var needsReflow = false,
1113
        dom = this.dom,
1114
        axis = dom.axis,
1115
        text;
1116

    
1117
    if (!axis.characterMinor) {
1118
        text = document.createTextNode("0");
1119
        var characterMinor = document.createElement("DIV");
1120
        characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
1121
        characterMinor.appendChild(text);
1122
        characterMinor.style.position = "absolute";
1123
        characterMinor.style.visibility = "hidden";
1124
        characterMinor.style.paddingLeft = "0px";
1125
        characterMinor.style.paddingRight = "0px";
1126
        axis.frame.appendChild(characterMinor);
1127

    
1128
        axis.characterMinor = characterMinor;
1129
        needsReflow = true;
1130
    }
1131

    
1132
    if (!axis.characterMajor) {
1133
        text = document.createTextNode("0");
1134
        var characterMajor = document.createElement("DIV");
1135
        characterMajor.className = "timeline-axis-text timeline-axis-text-major";
1136
        characterMajor.appendChild(text);
1137
        characterMajor.style.position = "absolute";
1138
        characterMajor.style.visibility = "hidden";
1139
        characterMajor.style.paddingLeft = "0px";
1140
        characterMajor.style.paddingRight = "0px";
1141
        axis.frame.appendChild(characterMajor);
1142

    
1143
        axis.characterMajor = characterMajor;
1144
        needsReflow = true;
1145
    }
1146

    
1147
    return needsReflow;
1148
};
1149

    
1150
/**
1151
 * Initialize redraw of the axis. All existing labels and lines will be
1152
 * overwritten and reused.
1153
 */
1154
links.Timeline.prototype.repaintAxisStartOverwriting = function () {
1155
    var properties = this.size.axis.properties;
1156

    
1157
    properties.minorTextNum = 0;
1158
    properties.minorLineNum = 0;
1159
    properties.majorTextNum = 0;
1160
    properties.majorLineNum = 0;
1161
};
1162

    
1163
/**
1164
 * End of overwriting HTML DOM elements of the axis.
1165
 * remaining elements will be removed
1166
 */
1167
links.Timeline.prototype.repaintAxisEndOverwriting = function () {
1168
    var dom = this.dom,
1169
        props = this.size.axis.properties,
1170
        frame = this.dom.axis.frame,
1171
        num;
1172

    
1173
    // remove leftovers
1174
    var minorTexts = dom.axis.minorTexts;
1175
    num = props.minorTextNum;
1176
    while (minorTexts.length > num) {
1177
        var minorText = minorTexts[num];
1178
        frame.removeChild(minorText);
1179
        minorTexts.splice(num, 1);
1180
    }
1181

    
1182
    var minorLines = dom.axis.minorLines;
1183
    num = props.minorLineNum;
1184
    while (minorLines.length > num) {
1185
        var minorLine = minorLines[num];
1186
        frame.removeChild(minorLine);
1187
        minorLines.splice(num, 1);
1188
    }
1189

    
1190
    var majorTexts = dom.axis.majorTexts;
1191
    num = props.majorTextNum;
1192
    while (majorTexts.length > num) {
1193
        var majorText = majorTexts[num];
1194
        frame.removeChild(majorText);
1195
        majorTexts.splice(num, 1);
1196
    }
1197

    
1198
    var majorLines = dom.axis.majorLines;
1199
    num = props.majorLineNum;
1200
    while (majorLines.length > num) {
1201
        var majorLine = majorLines[num];
1202
        frame.removeChild(majorLine);
1203
        majorLines.splice(num, 1);
1204
    }
1205
};
1206

    
1207
/**
1208
 * Repaint the horizontal line and background of the axis
1209
 */
1210
links.Timeline.prototype.repaintAxisHorizontal = function() {
1211
    var axis = this.dom.axis,
1212
        size = this.size,
1213
        options = this.options;
1214

    
1215
    // line behind all axis elements (possibly having a background color)
1216
    var hasAxis = (options.showMinorLabels || options.showMajorLabels);
1217
    if (hasAxis) {
1218
        if (!axis.backgroundLine) {
1219
            // create the axis line background (for a background color or so)
1220
            var backgroundLine = document.createElement("DIV");
1221
            backgroundLine.className = "timeline-axis";
1222
            backgroundLine.style.position = "absolute";
1223
            backgroundLine.style.left = "0px";
1224
            backgroundLine.style.width = "100%";
1225
            backgroundLine.style.border = "none";
1226
            axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);
1227

    
1228
            axis.backgroundLine = backgroundLine;
1229
        }
1230

    
1231
        if (axis.backgroundLine) {
1232
            axis.backgroundLine.style.top = size.axis.top + "px";
1233
            axis.backgroundLine.style.height = size.axis.height + "px";
1234
        }
1235
    }
1236
    else {
1237
        if (axis.backgroundLine) {
1238
            axis.frame.removeChild(axis.backgroundLine);
1239
            delete axis.backgroundLine;
1240
        }
1241
    }
1242

    
1243
    // line before all axis elements
1244
    if (hasAxis) {
1245
        if (axis.line) {
1246
            // put this line at the end of all childs
1247
            var line = axis.frame.removeChild(axis.line);
1248
            axis.frame.appendChild(line);
1249
        }
1250
        else {
1251
            // make the axis line
1252
            var line = document.createElement("DIV");
1253
            line.className = "timeline-axis";
1254
            line.style.position = "absolute";
1255
            line.style.left = "0px";
1256
            line.style.width = "100%";
1257
            line.style.height = "0px";
1258
            axis.frame.appendChild(line);
1259

    
1260
            axis.line = line;
1261
        }
1262

    
1263
        axis.line.style.top = size.axis.line + "px";
1264
    }
1265
    else {
1266
        if (axis.line && axis.line.parentElement) {
1267
            axis.frame.removeChild(axis.line);
1268
            delete axis.line;
1269
        }
1270
    }
1271
};
1272

    
1273
/**
1274
 * Create a minor label for the axis at position x
1275
 * @param {Number} x
1276
 * @param {String} text
1277
 */
1278
links.Timeline.prototype.repaintAxisMinorText = function (x, text) {
1279
    var size = this.size,
1280
        dom = this.dom,
1281
        props = size.axis.properties,
1282
        frame = dom.axis.frame,
1283
        minorTexts = dom.axis.minorTexts,
1284
        index = props.minorTextNum,
1285
        label;
1286

    
1287
    if (index < minorTexts.length) {
1288
        label = minorTexts[index]
1289
    }
1290
    else {
1291
        // create new label
1292
        var content = document.createTextNode("");
1293
        label = document.createElement("DIV");
1294
        label.appendChild(content);
1295
        label.className = "timeline-axis-text timeline-axis-text-minor";
1296
        label.style.position = "absolute";
1297

    
1298
        frame.appendChild(label);
1299

    
1300
        minorTexts.push(label);
1301
    }
1302

    
1303
    label.childNodes[0].nodeValue = text;
1304
    label.style.left = x + "px";
1305
    label.style.top  = size.axis.labelMinorTop + "px";
1306
    //label.title = title;  // TODO: this is a heavy operation
1307

    
1308
    props.minorTextNum++;
1309
};
1310

    
1311
/**
1312
 * Create a minor line for the axis at position x
1313
 * @param {Number} x
1314
 */
1315
links.Timeline.prototype.repaintAxisMinorLine = function (x) {
1316
    var axis = this.size.axis,
1317
        dom = this.dom,
1318
        props = axis.properties,
1319
        frame = dom.axis.frame,
1320
        minorLines = dom.axis.minorLines,
1321
        index = props.minorLineNum,
1322
        line;
1323

    
1324
    if (index < minorLines.length) {
1325
        line = minorLines[index];
1326
    }
1327
    else {
1328
        // create vertical line
1329
        line = document.createElement("DIV");
1330
        line.className = "timeline-axis-grid timeline-axis-grid-minor";
1331
        line.style.position = "absolute";
1332
        line.style.width = "0px";
1333

    
1334
        frame.appendChild(line);
1335
        minorLines.push(line);
1336
    }
1337

    
1338
    line.style.top = axis.lineMinorTop + "px";
1339
    line.style.height = axis.lineMinorHeight + "px";
1340
    line.style.left = (x - axis.lineMinorWidth/2) + "px";
1341

    
1342
    props.minorLineNum++;
1343
};
1344

    
1345
/**
1346
 * Create a Major label for the axis at position x
1347
 * @param {Number} x
1348
 * @param {String} text
1349
 */
1350
links.Timeline.prototype.repaintAxisMajorText = function (x, text) {
1351
    var size = this.size,
1352
        props = size.axis.properties,
1353
        frame = this.dom.axis.frame,
1354
        majorTexts = this.dom.axis.majorTexts,
1355
        index = props.majorTextNum,
1356
        label;
1357

    
1358
    if (index < majorTexts.length) {
1359
        label = majorTexts[index];
1360
    }
1361
    else {
1362
        // create label
1363
        var content = document.createTextNode(text);
1364
        label = document.createElement("DIV");
1365
        label.className = "timeline-axis-text timeline-axis-text-major";
1366
        label.appendChild(content);
1367
        label.style.position = "absolute";
1368
        label.style.top = "0px";
1369

    
1370
        frame.appendChild(label);
1371
        majorTexts.push(label);
1372
    }
1373

    
1374
    label.childNodes[0].nodeValue = text;
1375
    label.style.top = size.axis.labelMajorTop + "px";
1376
    label.style.left = x + "px";
1377
    //label.title = title; // TODO: this is a heavy operation
1378

    
1379
    props.majorTextNum ++;
1380
};
1381

    
1382
/**
1383
 * Create a Major line for the axis at position x
1384
 * @param {Number} x
1385
 */
1386
links.Timeline.prototype.repaintAxisMajorLine = function (x) {
1387
    var size = this.size,
1388
        props = size.axis.properties,
1389
        axis = this.size.axis,
1390
        frame = this.dom.axis.frame,
1391
        majorLines = this.dom.axis.majorLines,
1392
        index = props.majorLineNum,
1393
        line;
1394

    
1395
    if (index < majorLines.length) {
1396
        line = majorLines[index];
1397
    }
1398
    else {
1399
        // create vertical line
1400
        line = document.createElement("DIV");
1401
        line.className = "timeline-axis-grid timeline-axis-grid-major";
1402
        line.style.position = "absolute";
1403
        line.style.top = "0px";
1404
        line.style.width = "0px";
1405

    
1406
        frame.appendChild(line);
1407
        majorLines.push(line);
1408
    }
1409

    
1410
    line.style.left = (x - axis.lineMajorWidth/2) + "px";
1411
    line.style.height = size.frameHeight + "px";
1412

    
1413
    props.majorLineNum ++;
1414
};
1415

    
1416
/**
1417
 * Reflow all items, retrieve their actual size
1418
 * @return {boolean} resized    returns true if any of the items is resized
1419
 */
1420
links.Timeline.prototype.reflowItems = function() {
1421
    var resized = false,
1422
        i,
1423
        iMax,
1424
        group,
1425
        groups = this.groups,
1426
        renderedItems = this.renderedItems;
1427

    
1428
    if (groups) { // TODO: need to check if labels exists?
1429
        // loop through all groups to reset the items height
1430
        groups.forEach(function (group) {
1431
            group.itemsHeight = 0;
1432
        });
1433
    }
1434

    
1435
    // loop through the width and height of all visible items
1436
    for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
1437
        var item = renderedItems[i],
1438
            domItem = item.dom;
1439
        group = item.group;
1440

    
1441
        if (domItem) {
1442
            // TODO: move updating width and height into item.reflow
1443
            var width = domItem ? domItem.clientWidth : 0;
1444
            var height = domItem ? domItem.clientHeight : 0;
1445
            resized = resized || (item.width != width);
1446
            resized = resized || (item.height != height);
1447
            item.width = width;
1448
            item.height = height;
1449
            //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
1450
            item.reflow();
1451
        }
1452

    
1453
        if (group) {
1454
            group.itemsHeight = group.itemsHeight ?
1455
                Math.max(group.itemsHeight, item.height) :
1456
                item.height;
1457
        }
1458
    }
1459

    
1460
    return resized;
1461
};
1462

    
1463
/**
1464
 * Recalculate item properties:
1465
 * - the height of each group.
1466
 * - the actualHeight, from the stacked items or the sum of the group heights
1467
 * @return {boolean} resized    returns true if any of the items properties is
1468
 *                              changed
1469
 */
1470
links.Timeline.prototype.recalcItems = function () {
1471
    var resized = false,
1472
        i,
1473
        iMax,
1474
        item,
1475
        finalItem,
1476
        finalItems,
1477
        group,
1478
        groups = this.groups,
1479
        size = this.size,
1480
        options = this.options,
1481
        renderedItems = this.renderedItems;
1482

    
1483
    var actualHeight = 0;
1484
    if (groups.length == 0) {
1485
        // calculate actual height of the timeline when there are no groups
1486
        // but stacked items
1487
        if (options.autoHeight || options.cluster) {
1488
            var min = 0,
1489
                max = 0;
1490

    
1491
            if (this.stack && this.stack.finalItems) {
1492
                // adjust the offset of all finalItems when the actualHeight has been changed
1493
                finalItems = this.stack.finalItems;
1494
                finalItem = finalItems[0];
1495
                if (finalItem && finalItem.top) {
1496
                    min = finalItem.top;
1497
                    max = finalItem.top + finalItem.height;
1498
                }
1499
                for (i = 1, iMax = finalItems.length; i < iMax; i++) {
1500
                    finalItem = finalItems[i];
1501
                    min = Math.min(min, finalItem.top);
1502
                    max = Math.max(max, finalItem.top + finalItem.height);
1503
                }
1504
            }
1505
            else {
1506
                item = renderedItems[0];
1507
                if (item && item.top) {
1508
                    min = item.top;
1509
                    max = item.top + item.height;
1510
                }
1511
                for (i = 1, iMax = renderedItems.length; i < iMax; i++) {
1512
                    item = renderedItems[i];
1513
                    if (item.top) {
1514
                        min = Math.min(min, item.top);
1515
                        max = Math.max(max, (item.top + item.height));
1516
                    }
1517
                }
1518
            }
1519

    
1520
            actualHeight = (max - min) + 2 * options.eventMarginAxis + size.axis.height;
1521
            if (actualHeight < options.minHeight) {
1522
                actualHeight = options.minHeight;
1523
            }
1524

    
1525
            if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
1526
                // adjust the offset of all items when the actualHeight has been changed
1527
                var diff = actualHeight - size.actualHeight;
1528
                if (this.stack && this.stack.finalItems) {
1529
                    finalItems = this.stack.finalItems;
1530
                    for (i = 0, iMax = finalItems.length; i < iMax; i++) {
1531
                        finalItems[i].top += diff;
1532
                        finalItems[i].item.top += diff;
1533
                    }
1534
                }
1535
                else {
1536
                    for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
1537
                        renderedItems[i].top += diff;
1538
                    }
1539
                }
1540
            }
1541
        }
1542
    }
1543
    else {
1544
        // loop through all groups to get the height of each group, and the
1545
        // total height
1546
        actualHeight = size.axis.height + 2 * options.eventMarginAxis;
1547
        for (i = 0, iMax = groups.length; i < iMax; i++) {
1548
            group = groups[i];
1549

    
1550
            var groupHeight = Math.max(group.labelHeight || 0, group.itemsHeight || 0);
1551
            resized = resized || (groupHeight != group.height);
1552
            group.height = groupHeight;
1553

    
1554
            actualHeight += groups[i].height + options.eventMargin;
1555
        }
1556

    
1557
        // calculate top positions of the group labels and lines
1558
        var eventMargin = options.eventMargin,
1559
            top = options.axisOnTop ?
1560
                options.eventMarginAxis + eventMargin/2 :
1561
                size.contentHeight - options.eventMarginAxis + eventMargin/ 2,
1562
            axisHeight = size.axis.height;
1563

    
1564
        for (i = 0, iMax = groups.length; i < iMax; i++) {
1565
            group = groups[i];
1566
            if (options.axisOnTop) {
1567
                group.top = top + axisHeight;
1568
                group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
1569
                group.lineTop = top + axisHeight + group.height + eventMargin/2;
1570
                top += group.height + eventMargin;
1571
            }
1572
            else {
1573
                top -= group.height + eventMargin;
1574
                group.top = top;
1575
                group.labelTop = top + (group.height - group.labelHeight) / 2;
1576
                group.lineTop = top - eventMargin/2;
1577
            }
1578
        }
1579

    
1580
        // calculate top position of the visible items
1581
        for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
1582
            item = renderedItems[i];
1583
            group = item.group;
1584

    
1585
            if (group) {
1586
                item.top = group.top;
1587
            }
1588
        }
1589

    
1590
        resized = true;
1591
    }
1592

    
1593
    if (actualHeight < options.minHeight) {
1594
        actualHeight = options.minHeight;
1595
    }
1596
    resized = resized || (actualHeight != size.actualHeight);
1597
    size.actualHeight = actualHeight;
1598

    
1599
    return resized;
1600
};
1601

    
1602
/**
1603
 * This method clears the (internal) array this.items in a safe way: neatly
1604
 * cleaning up the DOM, and accompanying arrays this.renderedItems and
1605
 * the created clusters.
1606
 */
1607
links.Timeline.prototype.clearItems = function() {
1608
    // add all visible items to the list to be hidden
1609
    var hideItems = this.renderQueue.hide;
1610
    this.renderedItems.forEach(function (item) {
1611
        hideItems.push(item);
1612
    });
1613

    
1614
    // clear the cluster generator
1615
    this.clusterGenerator.clear();
1616

    
1617
    // actually clear the items
1618
    this.items = [];
1619
};
1620

    
1621
/**
1622
 * Repaint all items
1623
 * @return {boolean} needsReflow   Returns true if the DOM is changed such that
1624
 *                                 a reflow is needed.
1625
 */
1626
links.Timeline.prototype.repaintItems = function() {
1627
    var i, iMax, item, index;
1628

    
1629
    var needsReflow = false,
1630
        dom = this.dom,
1631
        size = this.size,
1632
        timeline = this,
1633
        renderedItems = this.renderedItems;
1634

    
1635
    if (!dom.items) {
1636
        dom.items = {};
1637
    }
1638

    
1639
    // draw the frame containing the items
1640
    var frame = dom.items.frame;
1641
    if (!frame) {
1642
        frame = document.createElement("DIV");
1643
        frame.style.position = "relative";
1644
        dom.content.appendChild(frame);
1645
        dom.items.frame = frame;
1646
    }
1647

    
1648
    frame.style.left = "0px";
1649
    frame.style.top = size.items.top + "px";
1650
    frame.style.height = "0px";
1651

    
1652
    // Take frame offline (for faster manipulation of the DOM)
1653
    dom.content.removeChild(frame);
1654

    
1655
    // process the render queue with changes
1656
    var queue = this.renderQueue;
1657
    var newImageUrls = [];
1658
    needsReflow = needsReflow ||
1659
        (queue.show.length > 0) ||
1660
        (queue.update.length > 0) ||
1661
        (queue.hide.length > 0);   // TODO: reflow needed on hide of items?
1662

    
1663
    while (item = queue.show.shift()) {
1664
        item.showDOM(frame);
1665
        item.getImageUrls(newImageUrls);
1666
        renderedItems.push(item);
1667
    }
1668
    while (item = queue.update.shift()) {
1669
        item.updateDOM(frame);
1670
        item.getImageUrls(newImageUrls);
1671
        index = this.renderedItems.indexOf(item);
1672
        if (index == -1) {
1673
            renderedItems.push(item);
1674
        }
1675
    }
1676
    while (item = queue.hide.shift()) {
1677
        item.hideDOM(frame);
1678
        index = this.renderedItems.indexOf(item);
1679
        if (index != -1) {
1680
            renderedItems.splice(index, 1);
1681
        }
1682
    }
1683

    
1684
    // reposition all visible items
1685
    renderedItems.forEach(function (item) {
1686
        item.updatePosition(timeline);
1687
    });
1688

    
1689
    // redraw the delete button and dragareas of the selected item (if any)
1690
    this.repaintDeleteButton();
1691
    this.repaintDragAreas();
1692

    
1693
    // put frame online again
1694
    dom.content.appendChild(frame);
1695

    
1696
    if (newImageUrls.length) {
1697
        // retrieve all image sources from the items, and set a callback once
1698
        // all images are retrieved
1699
        var callback = function () {
1700
            timeline.render();
1701
        };
1702
        var sendCallbackWhenAlreadyLoaded = false;
1703
        links.imageloader.loadAll(newImageUrls, callback, sendCallbackWhenAlreadyLoaded);
1704
    }
1705

    
1706
    return needsReflow;
1707
};
1708

    
1709
/**
1710
 * Reflow the size of the groups
1711
 * @return {boolean} resized    Returns true if any of the frame elements
1712
 *                              have been resized.
1713
 */
1714
links.Timeline.prototype.reflowGroups = function() {
1715
    var resized = false,
1716
        options = this.options,
1717
        size = this.size,
1718
        dom = this.dom;
1719

    
1720
    // calculate the groups width and height
1721
    // TODO: only update when data is changed! -> use an updateSeq
1722
    var groupsWidth = 0;
1723

    
1724
    // loop through all groups to get the labels width and height
1725
    var groups = this.groups;
1726
    var labels = this.dom.groups ? this.dom.groups.labels : [];
1727
    for (var i = 0, iMax = groups.length; i < iMax; i++) {
1728
        var group = groups[i];
1729
        var label = labels[i];
1730
        group.labelWidth  = label ? label.clientWidth : 0;
1731
        group.labelHeight = label ? label.clientHeight : 0;
1732
        group.width = group.labelWidth;  // TODO: group.width is redundant with labelWidth
1733

    
1734
        groupsWidth = Math.max(groupsWidth, group.width);
1735
    }
1736

    
1737
    // limit groupsWidth to the groups width in the options
1738
    if (options.groupsWidth !== undefined) {
1739
        groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
1740
    }
1741

    
1742
    // compensate for the border width. TODO: calculate the real border width
1743
    groupsWidth += 1;
1744

    
1745
    var groupsLeft = options.groupsOnRight ? size.frameWidth - groupsWidth : 0;
1746
    resized = resized || (size.groupsWidth !== groupsWidth);
1747
    resized = resized || (size.groupsLeft !== groupsLeft);
1748
    size.groupsWidth = groupsWidth;
1749
    size.groupsLeft = groupsLeft;
1750

    
1751
    return resized;
1752
};
1753

    
1754
/**
1755
 * Redraw the group labels
1756
 */
1757
links.Timeline.prototype.repaintGroups = function() {
1758
    var dom = this.dom,
1759
        timeline = this,
1760
        options = this.options,
1761
        size = this.size,
1762
        groups = this.groups;
1763

    
1764
    if (dom.groups === undefined) {
1765
        dom.groups = {};
1766
    }
1767

    
1768
    var labels = dom.groups.labels;
1769
    if (!labels) {
1770
        labels = [];
1771
        dom.groups.labels = labels;
1772
    }
1773
    var labelLines = dom.groups.labelLines;
1774
    if (!labelLines) {
1775
        labelLines = [];
1776
        dom.groups.labelLines = labelLines;
1777
    }
1778
    var itemLines = dom.groups.itemLines;
1779
    if (!itemLines) {
1780
        itemLines = [];
1781
        dom.groups.itemLines = itemLines;
1782
    }
1783

    
1784
    // create the frame for holding the groups
1785
    var frame = dom.groups.frame;
1786
    if (!frame) {
1787
        frame =  document.createElement("DIV");
1788
        frame.className = "timeline-groups-axis";
1789
        frame.style.position = "absolute";
1790
        frame.style.overflow = "hidden";
1791
        frame.style.top = "0px";
1792
        frame.style.height = "100%";
1793

    
1794
        dom.frame.appendChild(frame);
1795
        dom.groups.frame = frame;
1796
    }
1797

    
1798
    frame.style.left = size.groupsLeft + "px";
1799
    frame.style.width = (options.groupsWidth !== undefined) ?
1800
        options.groupsWidth :
1801
        size.groupsWidth + "px";
1802

    
1803
    // hide groups axis when there are no groups
1804
    if (groups.length == 0) {
1805
        frame.style.display = 'none';
1806
    }
1807
    else {
1808
        frame.style.display = '';
1809
    }
1810

    
1811
    // TODO: only create/update groups when data is changed.
1812

    
1813
    // create the items
1814
    var current = labels.length,
1815
        needed = groups.length;
1816

    
1817
    // overwrite existing group labels
1818
    for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
1819
        var group = groups[i];
1820
        var label = labels[i];
1821
        label.innerHTML = this.getGroupName(group);
1822
        label.style.display = '';
1823
    }
1824

    
1825
    // append new items when needed
1826
    for (var i = current; i < needed; i++) {
1827
        var group = groups[i];
1828

    
1829
        // create text label
1830
        var label = document.createElement("DIV");
1831
        label.className = "timeline-groups-text";
1832
        label.style.position = "absolute";
1833
        if (options.groupsWidth === undefined) {
1834
            label.style.whiteSpace = "nowrap";
1835
        }
1836
        label.innerHTML = this.getGroupName(group);
1837
        frame.appendChild(label);
1838
        labels[i] = label;
1839

    
1840
        // create the grid line between the group labels
1841
        var labelLine = document.createElement("DIV");
1842
        labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1843
        labelLine.style.position = "absolute";
1844
        labelLine.style.left = "0px";
1845
        labelLine.style.width = "100%";
1846
        labelLine.style.height = "0px";
1847
        labelLine.style.borderTopStyle = "solid";
1848
        frame.appendChild(labelLine);
1849
        labelLines[i] = labelLine;
1850

    
1851
        // create the grid line between the items
1852
        var itemLine = document.createElement("DIV");
1853
        itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1854
        itemLine.style.position = "absolute";
1855
        itemLine.style.left = "0px";
1856
        itemLine.style.width = "100%";
1857
        itemLine.style.height = "0px";
1858
        itemLine.style.borderTopStyle = "solid";
1859
        dom.content.insertBefore(itemLine, dom.content.firstChild);
1860
        itemLines[i] = itemLine;
1861
    }
1862

    
1863
    // remove redundant items from the DOM when needed
1864
    for (var i = needed; i < current; i++) {
1865
        var label = labels[i],
1866
            labelLine = labelLines[i],
1867
            itemLine = itemLines[i];
1868

    
1869
        frame.removeChild(label);
1870
        frame.removeChild(labelLine);
1871
        dom.content.removeChild(itemLine);
1872
    }
1873
    labels.splice(needed, current - needed);
1874
    labelLines.splice(needed, current - needed);
1875
    itemLines.splice(needed, current - needed);
1876
    
1877
    links.Timeline.addClassName(frame, options.groupsOnRight ? 'timeline-groups-axis-onright' : 'timeline-groups-axis-onleft');
1878

    
1879
    // position the groups
1880
    for (var i = 0, iMax = groups.length; i < iMax; i++) {
1881
        var group = groups[i],
1882
            label = labels[i],
1883
            labelLine = labelLines[i],
1884
            itemLine = itemLines[i];
1885

    
1886
        label.style.top = group.labelTop + "px";
1887
        labelLine.style.top = group.lineTop + "px";
1888
        itemLine.style.top = group.lineTop + "px";
1889
        itemLine.style.width = size.contentWidth + "px";
1890
    }
1891

    
1892
    if (!dom.groups.background) {
1893
        // create the axis grid line background
1894
        var background = document.createElement("DIV");
1895
        background.className = "timeline-axis";
1896
        background.style.position = "absolute";
1897
        background.style.left = "0px";
1898
        background.style.width = "100%";
1899
        background.style.border = "none";
1900

    
1901
        frame.appendChild(background);
1902
        dom.groups.background = background;
1903
    }
1904
    dom.groups.background.style.top = size.axis.top + 'px';
1905
    dom.groups.background.style.height = size.axis.height + 'px';
1906

    
1907
    if (!dom.groups.line) {
1908
        // create the axis grid line
1909
        var line = document.createElement("DIV");
1910
        line.className = "timeline-axis";
1911
        line.style.position = "absolute";
1912
        line.style.left = "0px";
1913
        line.style.width = "100%";
1914
        line.style.height = "0px";
1915

    
1916
        frame.appendChild(line);
1917
        dom.groups.line = line;
1918
    }
1919
    dom.groups.line.style.top = size.axis.line + 'px';
1920

    
1921
    // create a callback when there are images which are not yet loaded
1922
    // TODO: more efficiently load images in the groups
1923
    if (dom.groups.frame && groups.length) {
1924
        var imageUrls = [];
1925
        links.imageloader.filterImageUrls(dom.groups.frame, imageUrls);
1926
        if (imageUrls.length) {
1927
            // retrieve all image sources from the items, and set a callback once
1928
            // all images are retrieved
1929
            var callback = function () {
1930
                timeline.render();
1931
            };
1932
            var sendCallbackWhenAlreadyLoaded = false;
1933
            links.imageloader.loadAll(imageUrls, callback, sendCallbackWhenAlreadyLoaded);
1934
        }
1935
    }
1936
};
1937

    
1938

    
1939
/**
1940
 * Redraw the current time bar
1941
 */
1942
links.Timeline.prototype.repaintCurrentTime = function() {
1943
    var options = this.options,
1944
        dom = this.dom,
1945
        size = this.size;
1946

    
1947
    if (!options.showCurrentTime) {
1948
        if (dom.currentTime) {
1949
            dom.contentTimelines.removeChild(dom.currentTime);
1950
            delete dom.currentTime;
1951
        }
1952

    
1953
        return;
1954
    }
1955

    
1956
    if (!dom.currentTime) {
1957
        // create the current time bar
1958
        var currentTime = document.createElement("DIV");
1959
        currentTime.className = "timeline-currenttime";
1960
        currentTime.style.position = "absolute";
1961
        currentTime.style.top = "0px";
1962
        currentTime.style.height = "100%";
1963

    
1964
        dom.contentTimelines.appendChild(currentTime);
1965
        dom.currentTime = currentTime;
1966
    }
1967

    
1968
    var now = new Date();
1969
    var nowOffset = new Date(now.valueOf() + this.clientTimeOffset);
1970
    var x = this.timeToScreen(nowOffset);
1971

    
1972
    var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
1973
    dom.currentTime.style.display = visible ? '' : 'none';
1974
    dom.currentTime.style.left = x + "px";
1975
    dom.currentTime.title = "Current time: " + nowOffset;
1976

    
1977
    // start a timer to adjust for the new time
1978
    if (this.currentTimeTimer != undefined) {
1979
        clearTimeout(this.currentTimeTimer);
1980
        delete this.currentTimeTimer;
1981
    }
1982
    var timeline = this;
1983
    var onTimeout = function() {
1984
        timeline.repaintCurrentTime();
1985
    };
1986
    // the time equal to the width of one pixel, divided by 2 for more smoothness
1987
    var interval = 1 / this.conversion.factor / 2;
1988
    if (interval < 30) interval = 30;
1989
    this.currentTimeTimer = setTimeout(onTimeout, interval);
1990
};
1991

    
1992
/**
1993
 * Redraw the custom time bar
1994
 */
1995
links.Timeline.prototype.repaintCustomTime = function() {
1996
    var options = this.options,
1997
        dom = this.dom,
1998
        size = this.size;
1999

    
2000
    if (!options.showCustomTime) {
2001
        if (dom.customTime) {
2002
            dom.contentTimelines.removeChild(dom.customTime);
2003
            delete dom.customTime;
2004
        }
2005

    
2006
        return;
2007
    }
2008

    
2009
    if (!dom.customTime) {
2010
        var customTime = document.createElement("DIV");
2011
        customTime.className = "timeline-customtime";
2012
        customTime.style.position = "absolute";
2013
        customTime.style.top = "0px";
2014
        customTime.style.height = "100%";
2015

    
2016
        var drag = document.createElement("DIV");
2017
        drag.style.position = "relative";
2018
        drag.style.top = "0px";
2019
        drag.style.left = "-10px";
2020
        drag.style.height = "100%";
2021
        drag.style.width = "20px";
2022
        customTime.appendChild(drag);
2023

    
2024
        dom.contentTimelines.appendChild(customTime);
2025
        dom.customTime = customTime;
2026

    
2027
        // initialize parameter
2028
        this.customTime = new Date();
2029
    }
2030

    
2031
    var x = this.timeToScreen(this.customTime),
2032
        visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
2033
    dom.customTime.style.display = visible ? '' : 'none';
2034
    dom.customTime.style.left = x + "px";
2035
    dom.customTime.title = "Time: " + this.customTime;
2036
};
2037

    
2038

    
2039
/**
2040
 * Redraw the delete button, on the top right of the currently selected item
2041
 * if there is no item selected, the button is hidden.
2042
 */
2043
links.Timeline.prototype.repaintDeleteButton = function () {
2044
    var timeline = this,
2045
        dom = this.dom,
2046
        frame = dom.items.frame;
2047

    
2048
    var deleteButton = dom.items.deleteButton;
2049
    if (!deleteButton) {
2050
        // create a delete button
2051
        deleteButton = document.createElement("DIV");
2052
        deleteButton.className = "timeline-navigation-delete";
2053
        deleteButton.style.position = "absolute";
2054

    
2055
        frame.appendChild(deleteButton);
2056
        dom.items.deleteButton = deleteButton;
2057
    }
2058

    
2059
    var index = this.selection ? this.selection.index : -1,
2060
        item = this.selection ? this.items[index] : undefined;
2061
    if (item && item.rendered && this.isEditable(item)) {
2062
        var right = item.getRight(this),
2063
            top = item.top;
2064

    
2065
        deleteButton.style.left = right + 'px';
2066
        deleteButton.style.top = top + 'px';
2067
        deleteButton.style.display = '';
2068
        frame.removeChild(deleteButton);
2069
        frame.appendChild(deleteButton);
2070
    }
2071
    else {
2072
        deleteButton.style.display = 'none';
2073
    }
2074
};
2075

    
2076

    
2077
/**
2078
 * Redraw the drag areas. When an item (ranges only) is selected,
2079
 * it gets a drag area on the left and right side, to change its width
2080
 */
2081
links.Timeline.prototype.repaintDragAreas = function () {
2082
    var timeline = this,
2083
        options = this.options,
2084
        dom = this.dom,
2085
        frame = this.dom.items.frame;
2086

    
2087
    // create left drag area
2088
    var dragLeft = dom.items.dragLeft;
2089
    if (!dragLeft) {
2090
        dragLeft = document.createElement("DIV");
2091
        dragLeft.className="timeline-event-range-drag-left";
2092
        dragLeft.style.position = "absolute";
2093

    
2094
        frame.appendChild(dragLeft);
2095
        dom.items.dragLeft = dragLeft;
2096
    }
2097

    
2098
    // create right drag area
2099
    var dragRight = dom.items.dragRight;
2100
    if (!dragRight) {
2101
        dragRight = document.createElement("DIV");
2102
        dragRight.className="timeline-event-range-drag-right";
2103
        dragRight.style.position = "absolute";
2104

    
2105
        frame.appendChild(dragRight);
2106
        dom.items.dragRight = dragRight;
2107
    }
2108

    
2109
    // reposition left and right drag area
2110
    var index = this.selection ? this.selection.index : -1,
2111
        item = this.selection ? this.items[index] : undefined;
2112
    if (item && item.rendered && this.isEditable(item) &&
2113
        (item instanceof links.Timeline.ItemRange)) {
2114
        var left = this.timeToScreen(item.start),
2115
            right = this.timeToScreen(item.end),
2116
            top = item.top,
2117
            height = item.height;
2118

    
2119
        dragLeft.style.left = left + 'px';
2120
        dragLeft.style.top = top + 'px';
2121
        dragLeft.style.width = options.dragAreaWidth + "px";
2122
        dragLeft.style.height = height + 'px';
2123
        dragLeft.style.display = '';
2124
        frame.removeChild(dragLeft);
2125
        frame.appendChild(dragLeft);
2126

    
2127
        dragRight.style.left = (right - options.dragAreaWidth) + 'px';
2128
        dragRight.style.top = top + 'px';
2129
        dragRight.style.width = options.dragAreaWidth + "px";
2130
        dragRight.style.height = height + 'px';
2131
        dragRight.style.display = '';
2132
        frame.removeChild(dragRight);
2133
        frame.appendChild(dragRight);
2134
    }
2135
    else {
2136
        dragLeft.style.display = 'none';
2137
        dragRight.style.display = 'none';
2138
    }
2139
};
2140

    
2141
/**
2142
 * Create the navigation buttons for zooming and moving
2143
 */
2144
links.Timeline.prototype.repaintNavigation = function () {
2145
    var timeline = this,
2146
        options = this.options,
2147
        dom = this.dom,
2148
        frame = dom.frame,
2149
        navBar = dom.navBar;
2150

    
2151
    if (!navBar) {
2152
        var showButtonNew = options.showButtonNew && options.editable;
2153
        var showNavigation = options.showNavigation && (options.zoomable || options.moveable);
2154
        if (showNavigation || showButtonNew) {
2155
            // create a navigation bar containing the navigation buttons
2156
            navBar = document.createElement("DIV");
2157
            navBar.style.position = "absolute";
2158
            navBar.className = "timeline-navigation ui-widget ui-state-highlight ui-corner-all";
2159
            if (options.groupsOnRight) {
2160
                navBar.style.left = '10px';
2161
            }
2162
            else {
2163
                navBar.style.right = '10px';
2164
            }
2165
            if (options.axisOnTop) {
2166
                navBar.style.bottom = '10px';
2167
            }
2168
            else {
2169
                navBar.style.top = '10px';
2170
            }
2171
            dom.navBar = navBar;
2172
            frame.appendChild(navBar);
2173
        }
2174

    
2175
        if (showButtonNew) {
2176
            // create a new in button
2177
            navBar.addButton = document.createElement("DIV");
2178
            navBar.addButton.className = "timeline-navigation-new";
2179
            navBar.addButton.title = options.CREATE_NEW_EVENT;
2180
            var addIconSpan = document.createElement("SPAN");
2181
            addIconSpan.className = "ui-icon ui-icon-circle-plus";            
2182
            navBar.addButton.appendChild(addIconSpan);
2183
            
2184
            var onAdd = function(event) {
2185
                links.Timeline.preventDefault(event);
2186
                links.Timeline.stopPropagation(event);
2187

    
2188
                // create a new event at the center of the frame
2189
                var w = timeline.size.contentWidth;
2190
                var x = w / 2;
2191
                var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width
2192
                var xend = timeline.screenToTime(x + w / 10);   // add 10% of timeline width
2193
                if (options.snapEvents) {
2194
                    timeline.step.snap(xstart);
2195
                    timeline.step.snap(xend);
2196
                }
2197

    
2198
                var content = options.NEW;
2199
                var group = timeline.groups.length ? timeline.groups[0].content : undefined;
2200
                var preventRender = true;
2201
                timeline.addItem({
2202
                    'start': xstart,
2203
                    'end': xend,
2204
                    'content': content,
2205
                    'group': group
2206
                }, preventRender);
2207
                var index = (timeline.items.length - 1);
2208
                timeline.selectItem(index);
2209

    
2210
                timeline.applyAdd = true;
2211

    
2212
                // fire an add event.
2213
                // Note that the change can be canceled from within an event listener if
2214
                // this listener calls the method cancelAdd().
2215
                timeline.trigger('add');
2216

    
2217
                if (timeline.applyAdd) {
2218
                    // render and select the item
2219
                    timeline.render({animate: false});
2220
                    timeline.selectItem(index);
2221
                }
2222
                else {
2223
                    // undo an add
2224
                    timeline.deleteItem(index);
2225
                }
2226
            };
2227
            links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
2228
            navBar.appendChild(navBar.addButton);
2229
        }
2230

    
2231
        if (showButtonNew && showNavigation) {
2232
            // create a separator line
2233
            links.Timeline.addClassName(navBar.addButton, 'timeline-navigation-new-line');
2234
        }
2235

    
2236
        if (showNavigation) {
2237
            if (options.zoomable) {
2238
                // create a zoom in button
2239
                navBar.zoomInButton = document.createElement("DIV");
2240
                navBar.zoomInButton.className = "timeline-navigation-zoom-in";
2241
                navBar.zoomInButton.title = this.options.ZOOM_IN;
2242
                var ziIconSpan = document.createElement("SPAN");
2243
                ziIconSpan.className = "ui-icon ui-icon-circle-zoomin";
2244
                navBar.zoomInButton.appendChild(ziIconSpan);
2245
                
2246
                var onZoomIn = function(event) {
2247
                    links.Timeline.preventDefault(event);
2248
                    links.Timeline.stopPropagation(event);
2249
                    timeline.zoom(0.4);
2250
                    timeline.trigger("rangechange");
2251
                    timeline.trigger("rangechanged");
2252
                };
2253
                links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
2254
                navBar.appendChild(navBar.zoomInButton);
2255

    
2256
                // create a zoom out button
2257
                navBar.zoomOutButton = document.createElement("DIV");
2258
                navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
2259
                navBar.zoomOutButton.title = this.options.ZOOM_OUT;
2260
                var zoIconSpan = document.createElement("SPAN");
2261
                zoIconSpan.className = "ui-icon ui-icon-circle-zoomout";
2262
                navBar.zoomOutButton.appendChild(zoIconSpan);
2263
                
2264
                var onZoomOut = function(event) {
2265
                    links.Timeline.preventDefault(event);
2266
                    links.Timeline.stopPropagation(event);
2267
                    timeline.zoom(-0.4);
2268
                    timeline.trigger("rangechange");
2269
                    timeline.trigger("rangechanged");
2270
                };
2271
                links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
2272
                navBar.appendChild(navBar.zoomOutButton);
2273
            }
2274

    
2275
            if (options.moveable) {
2276
                // create a move left button
2277
                navBar.moveLeftButton = document.createElement("DIV");
2278
                navBar.moveLeftButton.className = "timeline-navigation-move-left";
2279
                navBar.moveLeftButton.title = this.options.MOVE_LEFT;
2280
                var mlIconSpan = document.createElement("SPAN");
2281
                mlIconSpan.className = "ui-icon ui-icon-circle-arrow-w";
2282
                navBar.moveLeftButton.appendChild(mlIconSpan);
2283
                
2284
                var onMoveLeft = function(event) {
2285
                    links.Timeline.preventDefault(event);
2286
                    links.Timeline.stopPropagation(event);
2287
                    timeline.move(-0.2);
2288
                    timeline.trigger("rangechange");
2289
                    timeline.trigger("rangechanged");
2290
                };
2291
                links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
2292
                navBar.appendChild(navBar.moveLeftButton);
2293

    
2294
                // create a move right button
2295
                navBar.moveRightButton = document.createElement("DIV");
2296
                navBar.moveRightButton.className = "timeline-navigation-move-right";
2297
                navBar.moveRightButton.title = this.options.MOVE_RIGHT;
2298
                var mrIconSpan = document.createElement("SPAN");
2299
                mrIconSpan.className = "ui-icon ui-icon-circle-arrow-e";
2300
                navBar.moveRightButton.appendChild(mrIconSpan);
2301
                
2302
                var onMoveRight = function(event) {
2303
                    links.Timeline.preventDefault(event);
2304
                    links.Timeline.stopPropagation(event);
2305
                    timeline.move(0.2);
2306
                    timeline.trigger("rangechange");
2307
                    timeline.trigger("rangechanged");
2308
                };
2309
                links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
2310
                navBar.appendChild(navBar.moveRightButton);
2311
            }
2312
        }
2313
    }
2314
};
2315

    
2316

    
2317
/**
2318
 * Set current time. This function can be used to set the time in the client
2319
 * timeline equal with the time on a server.
2320
 * @param {Date} time
2321
 */
2322
links.Timeline.prototype.setCurrentTime = function(time) {
2323
    var now = new Date();
2324
    this.clientTimeOffset = (time.valueOf() - now.valueOf());
2325

    
2326
    this.repaintCurrentTime();
2327
};
2328

    
2329
/**
2330
 * Get current time. The time can have an offset from the real time, when
2331
 * the current time has been changed via the method setCurrentTime.
2332
 * @return {Date} time
2333
 */
2334
links.Timeline.prototype.getCurrentTime = function() {
2335
    var now = new Date();
2336
    return new Date(now.valueOf() + this.clientTimeOffset);
2337
};
2338

    
2339

    
2340
/**
2341
 * Set custom time.
2342
 * The custom time bar can be used to display events in past or future.
2343
 * @param {Date} time
2344
 */
2345
links.Timeline.prototype.setCustomTime = function(time) {
2346
    this.customTime = new Date(time.valueOf());
2347
    this.repaintCustomTime();
2348
};
2349

    
2350
/**
2351
 * Retrieve the current custom time.
2352
 * @return {Date} customTime
2353
 */
2354
links.Timeline.prototype.getCustomTime = function() {
2355
    return new Date(this.customTime.valueOf());
2356
};
2357

    
2358
/**
2359
 * Set a custom scale. Autoscaling will be disabled.
2360
 * For example setScale(SCALE.MINUTES, 5) will result
2361
 * in minor steps of 5 minutes, and major steps of an hour.
2362
 *
2363
 * @param {links.Timeline.StepDate.SCALE} scale
2364
 *                               A scale. Choose from SCALE.MILLISECOND,
2365
 *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
2366
 *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
2367
 *                               SCALE.YEAR.
2368
 * @param {int}        step   A step size, by default 1. Choose for
2369
 *                               example 1, 2, 5, or 10.
2370
 */
2371
links.Timeline.prototype.setScale = function(scale, step) {
2372
    this.step.setScale(scale, step);
2373
    this.render(); // TODO: optimize: only reflow/repaint axis
2374
};
2375

    
2376
/**
2377
 * Enable or disable autoscaling
2378
 * @param {boolean} enable  If true or not defined, autoscaling is enabled.
2379
 *                          If false, autoscaling is disabled.
2380
 */
2381
links.Timeline.prototype.setAutoScale = function(enable) {
2382
    this.step.setAutoScale(enable);
2383
    this.render(); // TODO: optimize: only reflow/repaint axis
2384
};
2385

    
2386
/**
2387
 * Redraw the timeline
2388
 * Reloads the (linked) data table and redraws the timeline when resized.
2389
 * See also the method checkResize
2390
 */
2391
links.Timeline.prototype.redraw = function() {
2392
    this.setData(this.data);
2393
};
2394

    
2395

    
2396
/**
2397
 * Check if the timeline is resized, and if so, redraw the timeline.
2398
 * Useful when the webpage is resized.
2399
 */
2400
links.Timeline.prototype.checkResize = function() {
2401
    // TODO: re-implement the method checkResize, or better, make it redundant as this.render will be smarter
2402
    this.render();
2403
};
2404

    
2405
/**
2406
 * Check whether a given item is editable
2407
 * @param {links.Timeline.Item} item
2408
 * @return {boolean} editable
2409
 */
2410
links.Timeline.prototype.isEditable = function (item) {
2411
    if (item) {
2412
        if (item.editable != undefined) {
2413
            return item.editable;
2414
        }
2415
        else {
2416
            return this.options.editable;
2417
        }
2418
    }
2419
    return false;
2420
};
2421

    
2422
/**
2423
 * Calculate the factor and offset to convert a position on screen to the
2424
 * corresponding date and vice versa.
2425
 * After the method calcConversionFactor is executed once, the methods screenToTime and
2426
 * timeToScreen can be used.
2427
 */
2428
links.Timeline.prototype.recalcConversion = function() {
2429
    this.conversion.offset = this.start.valueOf();
2430
    this.conversion.factor = this.size.contentWidth /
2431
        (this.end.valueOf() - this.start.valueOf());
2432
};
2433

    
2434

    
2435
/**
2436
 * Convert a position on screen (pixels) to a datetime
2437
 * Before this method can be used, the method calcConversionFactor must be
2438
 * executed once.
2439
 * @param {int}     x    Position on the screen in pixels
2440
 * @return {Date}   time The datetime the corresponds with given position x
2441
 */
2442
links.Timeline.prototype.screenToTime = function(x) {
2443
    var conversion = this.conversion;
2444
    return new Date(x / conversion.factor + conversion.offset);
2445
};
2446

    
2447
/**
2448
 * Convert a datetime (Date object) into a position on the screen
2449
 * Before this method can be used, the method calcConversionFactor must be
2450
 * executed once.
2451
 * @param {Date}   time A date
2452
 * @return {int}   x    The position on the screen in pixels which corresponds
2453
 *                      with the given date.
2454
 */
2455
links.Timeline.prototype.timeToScreen = function(time) {
2456
    var conversion = this.conversion;
2457
    return (time.valueOf() - conversion.offset) * conversion.factor;
2458
};
2459

    
2460

    
2461

    
2462
/**
2463
 * Event handler for touchstart event on mobile devices
2464
 */
2465
links.Timeline.prototype.onTouchStart = function(event) {
2466
    var params = this.eventParams,
2467
        me = this;
2468

    
2469
    if (params.touchDown) {
2470
        // if already moving, return
2471
        return;
2472
    }
2473

    
2474
    params.touchDown = true;
2475
    params.zoomed = false;
2476

    
2477
    this.onMouseDown(event);
2478

    
2479
    if (!params.onTouchMove) {
2480
        params.onTouchMove = function (event) {me.onTouchMove(event);};
2481
        links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
2482
    }
2483
    if (!params.onTouchEnd) {
2484
        params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
2485
        links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
2486
    }
2487

    
2488
    /* TODO
2489
     // check for double tap event
2490
     var delta = 500; // ms
2491
     var doubleTapStart = (new Date()).valueOf();
2492
     var target = links.Timeline.getTarget(event);
2493
     var doubleTapItem = this.getItemIndex(target);
2494
     if (params.doubleTapStart &&
2495
     (doubleTapStart - params.doubleTapStart) < delta &&
2496
     doubleTapItem == params.doubleTapItem) {
2497
     delete params.doubleTapStart;
2498
     delete params.doubleTapItem;
2499
     me.onDblClick(event);
2500
     params.touchDown = false;
2501
     }
2502
     params.doubleTapStart = doubleTapStart;
2503
     params.doubleTapItem = doubleTapItem;
2504
     */
2505
    // store timing for double taps
2506
    var target = links.Timeline.getTarget(event);
2507
    var item = this.getItemIndex(target);
2508
    params.doubleTapStartPrev = params.doubleTapStart;
2509
    params.doubleTapStart = (new Date()).valueOf();
2510
    params.doubleTapItemPrev = params.doubleTapItem;
2511
    params.doubleTapItem = item;
2512

    
2513
    links.Timeline.preventDefault(event);
2514
};
2515

    
2516
/**
2517
 * Event handler for touchmove event on mobile devices
2518
 */
2519
links.Timeline.prototype.onTouchMove = function(event) {
2520
    var params = this.eventParams;
2521

    
2522
    if (event.scale && event.scale !== 1) {
2523
        params.zoomed = true;
2524
    }
2525

    
2526
    if (!params.zoomed) {
2527
        // move 
2528
        this.onMouseMove(event);
2529
    }
2530
    else {
2531
        if (this.options.zoomable) {
2532
            // pinch
2533
            // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
2534
            params.zoomed = true;
2535

    
2536
            var scale = event.scale,
2537
                oldWidth = (params.end.valueOf() - params.start.valueOf()),
2538
                newWidth = oldWidth / scale,
2539
                diff = newWidth - oldWidth,
2540
                start = new Date(parseInt(params.start.valueOf() - diff/2)),
2541
                end = new Date(parseInt(params.end.valueOf() + diff/2));
2542

    
2543
            // TODO: determine zoom-around-date from touch positions?
2544

    
2545
            this.setVisibleChartRange(start, end);
2546
            this.trigger("rangechange");
2547
        }
2548
    }
2549

    
2550
    links.Timeline.preventDefault(event);
2551
};
2552

    
2553
/**
2554
 * Event handler for touchend event on mobile devices
2555
 */
2556
links.Timeline.prototype.onTouchEnd = function(event) {
2557
    var params = this.eventParams;
2558
    var me = this;
2559
    params.touchDown = false;
2560

    
2561
    if (params.zoomed) {
2562
        this.trigger("rangechanged");
2563
    }
2564

    
2565
    if (params.onTouchMove) {
2566
        links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
2567
        delete params.onTouchMove;
2568

    
2569
    }
2570
    if (params.onTouchEnd) {
2571
        links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
2572
        delete params.onTouchEnd;
2573
    }
2574

    
2575
    this.onMouseUp(event);
2576

    
2577
    // check for double tap event
2578
    var delta = 500; // ms
2579
    var doubleTapEnd = (new Date()).valueOf();
2580
    var target = links.Timeline.getTarget(event);
2581
    var doubleTapItem = this.getItemIndex(target);
2582
    if (params.doubleTapStartPrev &&
2583
        (doubleTapEnd - params.doubleTapStartPrev) < delta &&
2584
        params.doubleTapItem == params.doubleTapItemPrev) {
2585
        params.touchDown = true;
2586
        me.onDblClick(event);
2587
        params.touchDown = false;
2588
    }
2589

    
2590
    links.Timeline.preventDefault(event);
2591
};
2592

    
2593

    
2594
/**
2595
 * Start a moving operation inside the provided parent element
2596
 * @param {Event} event       The event that occurred (required for
2597
 *                             retrieving the  mouse position)
2598
 */
2599
links.Timeline.prototype.onMouseDown = function(event) {
2600
    event = event || window.event;
2601

    
2602
    var params = this.eventParams,
2603
        options = this.options,
2604
        dom = this.dom;
2605

    
2606
    // only react on left mouse button down
2607
    var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
2608
    if (!leftButtonDown && !params.touchDown) {
2609
        return;
2610
    }
2611

    
2612
    // get mouse position
2613
    params.mouseX = links.Timeline.getPageX(event);
2614
    params.mouseY = links.Timeline.getPageY(event);
2615
    params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
2616
    params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
2617
    params.previousLeft = 0;
2618
    params.previousOffset = 0;
2619

    
2620
    params.moved = false;
2621
    params.start = new Date(this.start.valueOf());
2622
    params.end = new Date(this.end.valueOf());
2623

    
2624
    params.target = links.Timeline.getTarget(event);
2625
    var dragLeft = (dom.items && dom.items.dragLeft) ? dom.items.dragLeft : undefined;
2626
    var dragRight = (dom.items && dom.items.dragRight) ? dom.items.dragRight : undefined;
2627
    params.itemDragLeft = (params.target === dragLeft);
2628
    params.itemDragRight = (params.target === dragRight);
2629

    
2630
    if (params.itemDragLeft || params.itemDragRight) {
2631
        params.itemIndex = this.selection ? this.selection.index : undefined;
2632
    }
2633
    else {
2634
        params.itemIndex = this.getItemIndex(params.target);
2635
    }
2636

    
2637
    params.customTime = (params.target === dom.customTime ||
2638
        params.target.parentNode === dom.customTime) ?
2639
        this.customTime :
2640
        undefined;
2641

    
2642
    params.addItem = (options.editable && event.ctrlKey);
2643
    if (params.addItem) {
2644
        // create a new event at the current mouse position
2645
        var x = params.mouseX - params.frameLeft;
2646
        var y = params.mouseY - params.frameTop;
2647

    
2648
        var xstart = this.screenToTime(x);
2649
        if (options.snapEvents) {
2650
            this.step.snap(xstart);
2651
        }
2652
        var xend = new Date(xstart.valueOf());
2653
        var content = options.NEW;
2654
        var group = this.getGroupFromHeight(y);
2655
        this.addItem({
2656
            'start': xstart,
2657
            'end': xend,
2658
            'content': content,
2659
            'group': this.getGroupName(group)
2660
        });
2661
        params.itemIndex = (this.items.length - 1);
2662
        this.selectItem(params.itemIndex);
2663
        params.itemDragRight = true;
2664
    }
2665

    
2666
    var item = this.items[params.itemIndex];
2667
    var isSelected = this.isSelected(params.itemIndex);
2668
    params.editItem = isSelected && this.isEditable(item);
2669
    if (params.editItem) {
2670
        params.itemStart = item.start;
2671
        params.itemEnd = item.end;
2672
        params.itemGroup = item.group;
2673
        params.itemLeft = item.start ? this.timeToScreen(item.start) : undefined;
2674
        params.itemRight = item.end ? this.timeToScreen(item.end) : undefined;
2675
    }
2676
    else {
2677
        this.dom.frame.style.cursor = 'move';
2678
    }
2679
    if (!params.touchDown) {
2680
        // add event listeners to handle moving the contents
2681
        // we store the function onmousemove and onmouseup in the timeline, so we can
2682
        // remove the eventlisteners lateron in the function mouseUp()
2683
        var me = this;
2684
        if (!params.onMouseMove) {
2685
            params.onMouseMove = function (event) {me.onMouseMove(event);};
2686
            links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
2687
        }
2688
        if (!params.onMouseUp) {
2689
            params.onMouseUp = function (event) {me.onMouseUp(event);};
2690
            links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
2691
        }
2692

    
2693
        links.Timeline.preventDefault(event);
2694
    }
2695
};
2696

    
2697

    
2698
/**
2699
 * Perform moving operating.
2700
 * This function activated from within the funcion links.Timeline.onMouseDown().
2701
 * @param {Event}   event  Well, eehh, the event
2702
 */
2703
links.Timeline.prototype.onMouseMove = function (event) {
2704
    event = event || window.event;
2705

    
2706
    var params = this.eventParams,
2707
        size = this.size,
2708
        dom = this.dom,
2709
        options = this.options;
2710

    
2711
    // calculate change in mouse position
2712
    var mouseX = links.Timeline.getPageX(event);
2713
    var mouseY = links.Timeline.getPageY(event);
2714

    
2715
    if (params.mouseX == undefined) {
2716
        params.mouseX = mouseX;
2717
    }
2718
    if (params.mouseY == undefined) {
2719
        params.mouseY = mouseY;
2720
    }
2721

    
2722
    var diffX = mouseX - params.mouseX;
2723
    var diffY = mouseY - params.mouseY;
2724

    
2725
    // if mouse movement is big enough, register it as a "moved" event
2726
    if (Math.abs(diffX) >= 1) {
2727
        params.moved = true;
2728
    }
2729

    
2730
    if (params.customTime) {
2731
        var x = this.timeToScreen(params.customTime);
2732
        var xnew = x + diffX;
2733
        this.customTime = this.screenToTime(xnew);
2734
        this.repaintCustomTime();
2735

    
2736
        // fire a timechange event
2737
        this.trigger('timechange');
2738
    }
2739
    else if (params.editItem) {
2740
        var item = this.items[params.itemIndex],
2741
            left,
2742
            right;
2743

    
2744
        if (params.itemDragLeft) {
2745
            // move the start of the item
2746
            left = params.itemLeft + diffX;
2747
            right = params.itemRight;
2748

    
2749
            item.start = this.screenToTime(left);
2750
            if (options.snapEvents) {
2751
                this.step.snap(item.start);
2752
                left = this.timeToScreen(item.start);
2753
            }
2754

    
2755
            if (left > right) {
2756
                left = right;
2757
                item.start = this.screenToTime(left);
2758
            }
2759
        }
2760
        else if (params.itemDragRight) {
2761
            // move the end of the item
2762
            left = params.itemLeft;
2763
            right = params.itemRight + diffX;
2764

    
2765
            item.end = this.screenToTime(right);
2766
            if (options.snapEvents) {
2767
                this.step.snap(item.end);
2768
                right = this.timeToScreen(item.end);
2769
            }
2770

    
2771
            if (right < left) {
2772
                right = left;
2773
                item.end = this.screenToTime(right);
2774
            }
2775
        }
2776
        else {
2777
            // move the item
2778
            left = params.itemLeft + diffX;
2779
            item.start = this.screenToTime(left);
2780
            if (options.snapEvents) {
2781
                this.step.snap(item.start);
2782
                left = this.timeToScreen(item.start);
2783
            }
2784

    
2785
            if (item.end) {
2786
                right = left + (params.itemRight - params.itemLeft);
2787
                item.end = this.screenToTime(right);
2788
            }
2789
        }
2790

    
2791
        item.setPosition(left, right);
2792

    
2793
        var dragging = params.itemDragLeft || params.itemDragRight;
2794
        if (this.groups.length && !dragging) {
2795
            // move item from one group to another when needed
2796
            var y = mouseY - params.frameTop;
2797
            var group = this.getGroupFromHeight(y);
2798
            if (options.groupsChangeable && item.group !== group) {
2799
                // move item to the other group
2800
                var index = this.items.indexOf(item);
2801
                this.changeItem(index, {'group': this.getGroupName(group)});
2802
            }
2803
            else {
2804
                this.repaintDeleteButton();
2805
                this.repaintDragAreas();
2806
            }
2807
        }
2808
        else {
2809
            // TODO: does not work well in FF, forces redraw with every mouse move it seems
2810
            this.render(); // TODO: optimize, only redraw the items?
2811
            // Note: when animate==true, no redraw is needed here, its done by stackItems animation
2812
        }
2813
    }
2814
    else if (options.moveable) {
2815
        var interval = (params.end.valueOf() - params.start.valueOf());
2816
        var diffMillisecs = Math.round((-diffX) / size.contentWidth * interval);
2817
        var newStart = new Date(params.start.valueOf() + diffMillisecs);
2818
        var newEnd = new Date(params.end.valueOf() + diffMillisecs);
2819
        this.applyRange(newStart, newEnd);
2820
        // if the applied range is moved due to a fixed min or max,
2821
        // change the diffMillisecs accordingly
2822
        var appliedDiff = (this.start.valueOf() - newStart.valueOf());
2823
        if (appliedDiff) {
2824
            diffMillisecs += appliedDiff;
2825
        }
2826

    
2827
        this.recalcConversion();
2828

    
2829
        // move the items by changing the left position of their frame.
2830
        // this is much faster than repositioning all elements individually via the 
2831
        // repaintFrame() function (which is done once at mouseup)
2832
        // note that we round diffX to prevent wrong positioning on millisecond scale
2833
        var previousLeft = params.previousLeft || 0;
2834
        var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
2835
        var previousOffset = params.previousOffset || 0;
2836
        var frameOffset = previousOffset + (currentLeft - previousLeft);
2837
        var frameLeft = -diffMillisecs / interval * size.contentWidth + frameOffset;
2838

    
2839
        dom.items.frame.style.left = (frameLeft) + "px";
2840

    
2841
        // read the left again from DOM (IE8- rounds the value)
2842
        params.previousOffset = frameOffset;
2843
        params.previousLeft = parseFloat(dom.items.frame.style.left) || frameLeft;
2844

    
2845
        this.repaintCurrentTime();
2846
        this.repaintCustomTime();
2847
        this.repaintAxis();
2848

    
2849
        // fire a rangechange event
2850
        this.trigger('rangechange');
2851
    }
2852

    
2853
    links.Timeline.preventDefault(event);
2854
};
2855

    
2856

    
2857
/**
2858
 * Stop moving operating.
2859
 * This function activated from within the funcion links.Timeline.onMouseDown().
2860
 * @param {event}  event   The event
2861
 */
2862
links.Timeline.prototype.onMouseUp = function (event) {
2863
    var params = this.eventParams,
2864
        options = this.options;
2865

    
2866
    event = event || window.event;
2867

    
2868
    this.dom.frame.style.cursor = 'auto';
2869

    
2870
    // remove event listeners here, important for Safari
2871
    if (params.onMouseMove) {
2872
        links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
2873
        delete params.onMouseMove;
2874
    }
2875
    if (params.onMouseUp) {
2876
        links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
2877
        delete params.onMouseUp;
2878
    }
2879
    //links.Timeline.preventDefault(event);
2880

    
2881
    if (params.customTime) {
2882
        // fire a timechanged event
2883
        this.trigger('timechanged');
2884
    }
2885
    else if (params.editItem) {
2886
        var item = this.items[params.itemIndex];
2887

    
2888
        if (params.moved || params.addItem) {
2889
            this.applyChange = true;
2890
            this.applyAdd = true;
2891

    
2892
            this.updateData(params.itemIndex, {
2893
                'start': item.start,
2894
                'end': item.end
2895
            });
2896

    
2897
            // fire an add or change event. 
2898
            // Note that the change can be canceled from within an event listener if 
2899
            // this listener calls the method cancelChange().
2900
            this.trigger(params.addItem ? 'add' : 'change');
2901

    
2902
            if (params.addItem) {
2903
                if (this.applyAdd) {
2904
                    this.updateData(params.itemIndex, {
2905
                        'start': item.start,
2906
                        'end': item.end,
2907
                        'content': item.content,
2908
                        'group': this.getGroupName(item.group)
2909
                    });
2910
                }
2911
                else {
2912
                    // undo an add
2913
                    this.deleteItem(params.itemIndex);
2914
                }
2915
            }
2916
            else {
2917
                if (this.applyChange) {
2918
                    this.updateData(params.itemIndex, {
2919
                        'start': item.start,
2920
                        'end': item.end
2921
                    });
2922
                }
2923
                else {
2924
                    // undo a change
2925
                    delete this.applyChange;
2926
                    delete this.applyAdd;
2927

    
2928
                    var item = this.items[params.itemIndex],
2929
                        domItem = item.dom;
2930

    
2931
                    item.start = params.itemStart;
2932
                    item.end = params.itemEnd;
2933
                    item.group = params.itemGroup;
2934
                    // TODO: original group should be restored too
2935
                    item.setPosition(params.itemLeft, params.itemRight);
2936
                }
2937
            }
2938

    
2939
            // prepare data for clustering, by filtering and sorting by type
2940
            if (this.options.cluster) {
2941
                this.clusterGenerator.updateData();
2942
            }
2943

    
2944
            this.render();
2945
        }
2946
    }
2947
    else {
2948
        if (!params.moved && !params.zoomed) {
2949
            // mouse did not move -> user has selected an item
2950

    
2951
            if (params.target === this.dom.items.deleteButton) {
2952
                // delete item
2953
                if (this.selection) {
2954
                    this.confirmDeleteItem(this.selection.index);
2955
                }
2956
            }
2957
            else if (options.selectable) {
2958
                // select/unselect item
2959
                if (params.itemIndex != undefined) {
2960
                    if (!this.isSelected(params.itemIndex)) {
2961
                        this.selectItem(params.itemIndex);
2962
                        this.trigger('select');
2963
                    }
2964
                }
2965
                else {
2966
                    if (options.unselectable) {
2967
                        this.unselectItem();
2968
                        this.trigger('select');
2969
                    }
2970
                }
2971
            }
2972
        }
2973
        else {
2974
            // timeline is moved
2975
            // TODO: optimize: no need to reflow and cluster again?
2976
            this.render();
2977

    
2978
            if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
2979
                // fire a rangechanged event
2980
                this.trigger('rangechanged');
2981
            }
2982
        }
2983
    }
2984
};
2985

    
2986
/**
2987
 * Double click event occurred for an item
2988
 * @param {Event}  event
2989
 */
2990
links.Timeline.prototype.onDblClick = function (event) {
2991
    var params = this.eventParams,
2992
        options = this.options,
2993
        dom = this.dom,
2994
        size = this.size;
2995
    event = event || window.event;
2996

    
2997
    if (params.itemIndex != undefined) {
2998
        var item = this.items[params.itemIndex];
2999
        if (item && this.isEditable(item)) {
3000
            // fire the edit event
3001
            this.trigger('edit');
3002
        }
3003
    }
3004
    else {
3005
        if (options.editable) {
3006
            // create a new item
3007

    
3008
            // get mouse position
3009
            params.mouseX = links.Timeline.getPageX(event);
3010
            params.mouseY = links.Timeline.getPageY(event);
3011
            var x = params.mouseX - links.Timeline.getAbsoluteLeft(dom.content);
3012
            var y = params.mouseY - links.Timeline.getAbsoluteTop(dom.content);
3013

    
3014
            // create a new event at the current mouse position
3015
            var xstart = this.screenToTime(x);
3016
            var xend = this.screenToTime(x  + size.frameWidth / 10); // add 10% of timeline width
3017
            if (options.snapEvents) {
3018
                this.step.snap(xstart);
3019
                this.step.snap(xend);
3020
            }
3021

    
3022
            var content = options.NEW;
3023
            var group = this.getGroupFromHeight(y);   // (group may be undefined)
3024
            var preventRender = true;
3025
            this.addItem({
3026
                'start': xstart,
3027
                'end': xend,
3028
                'content': content,
3029
                'group': this.getGroupName(group)
3030
            }, preventRender);
3031
            params.itemIndex = (this.items.length - 1);
3032
            this.selectItem(params.itemIndex);
3033

    
3034
            this.applyAdd = true;
3035

    
3036
            // fire an add event.
3037
            // Note that the change can be canceled from within an event listener if
3038
            // this listener calls the method cancelAdd().
3039
            this.trigger('add');
3040

    
3041
            if (this.applyAdd) {
3042
                // render and select the item
3043
                this.render({animate: false});
3044
                this.selectItem(params.itemIndex);
3045
            }
3046
            else {
3047
                // undo an add
3048
                this.deleteItem(params.itemIndex);
3049
            }
3050
        }
3051
    }
3052

    
3053
    links.Timeline.preventDefault(event);
3054
};
3055

    
3056

    
3057
/**
3058
 * Event handler for mouse wheel event, used to zoom the timeline
3059
 * Code from http://adomas.org/javascript-mouse-wheel/
3060
 * @param {Event}  event   The event
3061
 */
3062
links.Timeline.prototype.onMouseWheel = function(event) {
3063
    if (!this.options.zoomable)
3064
        return;
3065

    
3066
    if (!event) { /* For IE. */
3067
        event = window.event;
3068
    }
3069

    
3070
    // retrieve delta    
3071
    var delta = 0;
3072
    if (event.wheelDelta) { /* IE/Opera. */
3073
        delta = event.wheelDelta/120;
3074
    } else if (event.detail) { /* Mozilla case. */
3075
        // In Mozilla, sign of delta is different than in IE.
3076
        // Also, delta is multiple of 3.
3077
        delta = -event.detail/3;
3078
    }
3079

    
3080
    // If delta is nonzero, handle it.
3081
    // Basically, delta is now positive if wheel was scrolled up,
3082
    // and negative, if wheel was scrolled down.
3083
    if (delta) {
3084
        // TODO: on FireFox, the window is not redrawn within repeated scroll-events 
3085
        // -> use a delayed redraw? Make a zoom queue?
3086

    
3087
        var timeline = this;
3088
        var zoom = function () {
3089
            // perform the zoom action. Delta is normally 1 or -1
3090
            var zoomFactor = delta / 5.0;
3091
            var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
3092
            var mouseX = links.Timeline.getPageX(event);
3093
            var zoomAroundDate =
3094
                (mouseX != undefined && frameLeft != undefined) ?
3095
                    timeline.screenToTime(mouseX - frameLeft) :
3096
                    undefined;
3097

    
3098
            timeline.zoom(zoomFactor, zoomAroundDate);
3099

    
3100
            // fire a rangechange and a rangechanged event
3101
            timeline.trigger("rangechange");
3102
            timeline.trigger("rangechanged");
3103
        };
3104

    
3105
        var scroll = function () {
3106
            // Scroll the timeline
3107
            timeline.move(delta * -0.2);
3108
            timeline.trigger("rangechange");
3109
            timeline.trigger("rangechanged");
3110
        };
3111

    
3112
        if (event.shiftKey) {
3113
            scroll();
3114
        }
3115
        else {
3116
            zoom();
3117
        }
3118
    }
3119

    
3120
    // Prevent default actions caused by mouse wheel.
3121
    // That might be ugly, but we handle scrolls somehow
3122
    // anyway, so don't bother here...
3123
    links.Timeline.preventDefault(event);
3124
};
3125

    
3126

    
3127
/**
3128
 * Zoom the timeline the given zoomfactor in or out. Start and end date will
3129
 * be adjusted, and the timeline will be redrawn. You can optionally give a
3130
 * date around which to zoom.
3131
 * For example, try zoomfactor = 0.1 or -0.1
3132
 * @param {Number} zoomFactor      Zooming amount. Positive value will zoom in,
3133
 *                                 negative value will zoom out
3134
 * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
3135
 */
3136
links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
3137
    // if zoomAroundDate is not provided, take it half between start Date and end Date
3138
    if (zoomAroundDate == undefined) {
3139
        zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
3140
    }
3141

    
3142
    // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
3143
    // result in a start>=end )
3144
    if (zoomFactor >= 1) {
3145
        zoomFactor = 0.9;
3146
    }
3147
    if (zoomFactor <= -1) {
3148
        zoomFactor = -0.9;
3149
    }
3150

    
3151
    // adjust a negative factor such that zooming in with 0.1 equals zooming
3152
    // out with a factor -0.1
3153
    if (zoomFactor < 0) {
3154
        zoomFactor = zoomFactor / (1 + zoomFactor);
3155
    }
3156

    
3157
    // zoom start Date and end Date relative to the zoomAroundDate
3158
    var startDiff = (this.start.valueOf() - zoomAroundDate);
3159
    var endDiff = (this.end.valueOf() - zoomAroundDate);
3160

    
3161
    // calculate new dates
3162
    var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
3163
    var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);
3164

    
3165
    // only zoom in when interval is larger than minimum interval (to prevent
3166
    // sliding to left/right when having reached the minimum zoom level)
3167
    var interval = (newEnd.valueOf() - newStart.valueOf());
3168
    var zoomMin = Number(this.options.zoomMin) || 10;
3169
    if (zoomMin < 10) {
3170
        zoomMin = 10;
3171
    }
3172
    if (interval >= zoomMin) {
3173
        this.applyRange(newStart, newEnd, zoomAroundDate);
3174
        this.render({
3175
            animate: this.options.animate && this.options.animateZoom
3176
        });
3177
    }
3178
};
3179

    
3180
/**
3181
 * Move the timeline the given movefactor to the left or right. Start and end
3182
 * date will be adjusted, and the timeline will be redrawn.
3183
 * For example, try moveFactor = 0.1 or -0.1
3184
 * @param {Number}  moveFactor      Moving amount. Positive value will move right,
3185
 *                                 negative value will move left
3186
 */
3187
links.Timeline.prototype.move = function(moveFactor) {
3188
    // zoom start Date and end Date relative to the zoomAroundDate
3189
    var diff = (this.end.valueOf() - this.start.valueOf());
3190

    
3191
    // apply new dates
3192
    var newStart = new Date(this.start.valueOf() + diff * moveFactor);
3193
    var newEnd   = new Date(this.end.valueOf() + diff * moveFactor);
3194
    this.applyRange(newStart, newEnd);
3195

    
3196
    this.render(); // TODO: optimize, no need to reflow, only to recalc conversion and repaint
3197
};
3198

    
3199
/**
3200
 * Apply a visible range. The range is limited to feasible maximum and minimum
3201
 * range.
3202
 * @param {Date} start
3203
 * @param {Date} end
3204
 * @param {Date}   zoomAroundDate  Optional. Date around which will be zoomed.
3205
 */
3206
links.Timeline.prototype.applyRange = function (start, end, zoomAroundDate) {
3207
    // calculate new start and end value
3208
    var startValue = start.valueOf(); // number
3209
    var endValue = end.valueOf();     // number
3210
    var interval = (endValue - startValue);
3211

    
3212
    // determine maximum and minimum interval
3213
    var options = this.options;
3214
    var year = 1000 * 60 * 60 * 24 * 365;
3215
    var zoomMin = Number(options.zoomMin) || 10;
3216
    if (zoomMin < 10) {
3217
        zoomMin = 10;
3218
    }
3219
    var zoomMax = Number(options.zoomMax) || 10000 * year;
3220
    if (zoomMax > 10000 * year) {
3221
        zoomMax = 10000 * year;
3222
    }
3223
    if (zoomMax < zoomMin) {
3224
        zoomMax = zoomMin;
3225
    }
3226

    
3227
    // determine min and max date value
3228
    var min = options.min ? options.min.valueOf() : undefined; // number
3229
    var max = options.max ? options.max.valueOf() : undefined; // number
3230
    if (min != undefined && max != undefined) {
3231
        if (min >= max) {
3232
            // empty range
3233
            var day = 1000 * 60 * 60 * 24;
3234
            max = min + day;
3235
        }
3236
        if (zoomMax > (max - min)) {
3237
            zoomMax = (max - min);
3238
        }
3239
        if (zoomMin > (max - min)) {
3240
            zoomMin = (max - min);
3241
        }
3242
    }
3243

    
3244
    // prevent empty interval
3245
    if (startValue >= endValue) {
3246
        endValue += 1000 * 60 * 60 * 24;
3247
    }
3248

    
3249
    // prevent too small scale
3250
    // TODO: IE has problems with milliseconds
3251
    if (interval < zoomMin) {
3252
        var diff = (zoomMin - interval);
3253
        var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
3254
        startValue -= Math.round(diff * f);
3255
        endValue   += Math.round(diff * (1 - f));
3256
    }
3257

    
3258
    // prevent too large scale
3259
    if (interval > zoomMax) {
3260
        var diff = (interval - zoomMax);
3261
        var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
3262
        startValue += Math.round(diff * f);
3263
        endValue   -= Math.round(diff * (1 - f));
3264
    }
3265

    
3266
    // prevent to small start date
3267
    if (min != undefined) {
3268
        var diff = (startValue - min);
3269
        if (diff < 0) {
3270
            startValue -= diff;
3271
            endValue -= diff;
3272
        }
3273
    }
3274

    
3275
    // prevent to large end date
3276
    if (max != undefined) {
3277
        var diff = (max - endValue);
3278
        if (diff < 0) {
3279
            startValue += diff;
3280
            endValue += diff;
3281
        }
3282
    }
3283

    
3284
    // apply new dates
3285
    this.start = new Date(startValue);
3286
    this.end = new Date(endValue);
3287
};
3288

    
3289
/**
3290
 * Delete an item after a confirmation.
3291
 * The deletion can be cancelled by executing .cancelDelete() during the
3292
 * triggered event 'delete'.
3293
 * @param {int} index   Index of the item to be deleted
3294
 */
3295
links.Timeline.prototype.confirmDeleteItem = function(index) {
3296
    this.applyDelete = true;
3297

    
3298
    // select the event to be deleted
3299
    if (!this.isSelected(index)) {
3300
        this.selectItem(index);
3301
    }
3302

    
3303
    // fire a delete event trigger. 
3304
    // Note that the delete event can be canceled from within an event listener if 
3305
    // this listener calls the method cancelChange().
3306
    this.trigger('delete');
3307

    
3308
    if (this.applyDelete) {
3309
        this.deleteItem(index);
3310
    }
3311

    
3312
    delete this.applyDelete;
3313
};
3314

    
3315
/**
3316
 * Delete an item
3317
 * @param {int} index   Index of the item to be deleted
3318
 * @param {boolean} [preventRender=false]   Do not re-render timeline if true
3319
 *                                          (optimization for multiple delete)
3320
 */
3321
links.Timeline.prototype.deleteItem = function(index, preventRender) {
3322
    if (index >= this.items.length) {
3323
        throw "Cannot delete row, index out of range";
3324
    }
3325

    
3326
    if (this.selection) {
3327
        // adjust the selection
3328
        if (this.selection.index == index) {
3329
            // item to be deleted is selected
3330
            this.unselectItem();
3331
        }
3332
        else if (this.selection.index > index) {
3333
            // update selection index
3334
            this.selection.index--;
3335
        }
3336
    }
3337

    
3338
    // actually delete the item and remove it from the DOM
3339
    var item = this.items.splice(index, 1)[0];
3340
    this.renderQueue.hide.push(item);
3341

    
3342
    // delete the row in the original data table
3343
    if (this.data) {
3344
        if (google && google.visualization &&
3345
            this.data instanceof google.visualization.DataTable) {
3346
            this.data.removeRow(index);
3347
        }
3348
        else if (links.Timeline.isArray(this.data)) {
3349
            this.data.splice(index, 1);
3350
        }
3351
        else {
3352
            throw "Cannot delete row from data, unknown data type";
3353
        }
3354
    }
3355

    
3356
    // prepare data for clustering, by filtering and sorting by type
3357
    if (this.options.cluster) {
3358
        this.clusterGenerator.updateData();
3359
    }
3360

    
3361
    if (!preventRender) {
3362
        this.render();
3363
    }
3364
};
3365

    
3366

    
3367
/**
3368
 * Delete all items
3369
 */
3370
links.Timeline.prototype.deleteAllItems = function() {
3371
    this.unselectItem();
3372

    
3373
    // delete the loaded items
3374
    this.clearItems();
3375

    
3376
    // delete the groups
3377
    this.deleteGroups();
3378

    
3379
    // empty original data table
3380
    if (this.data) {
3381
        if (google && google.visualization &&
3382
            this.data instanceof google.visualization.DataTable) {
3383
            this.data.removeRows(0, this.data.getNumberOfRows());
3384
        }
3385
        else if (links.Timeline.isArray(this.data)) {
3386
            this.data.splice(0, this.data.length);
3387
        }
3388
        else {
3389
            throw "Cannot delete row from data, unknown data type";
3390
        }
3391
    }
3392

    
3393
    // prepare data for clustering, by filtering and sorting by type
3394
    if (this.options.cluster) {
3395
        this.clusterGenerator.updateData();
3396
    }
3397

    
3398
    this.render();
3399
};
3400

    
3401

    
3402
/**
3403
 * Find the group from a given height in the timeline
3404
 * @param {Number} height   Height in the timeline
3405
 * @return {Object | undefined} group   The group object, or undefined if out
3406
 *                                      of range
3407
 */
3408
links.Timeline.prototype.getGroupFromHeight = function(height) {
3409
    var i,
3410
        group,
3411
        groups = this.groups;
3412

    
3413
    if (groups.length) {
3414
        if (this.options.axisOnTop) {
3415
            for (i = groups.length - 1; i >= 0; i--) {
3416
                group = groups[i];
3417
                if (height > group.top) {
3418
                    return group;
3419
                }
3420
            }
3421
        }
3422
        else {
3423
            for (i = 0; i < groups.length; i++) {
3424
                group = groups[i];
3425
                if (height > group.top) {
3426
                    return group;
3427
                }
3428
            }
3429
        }
3430

    
3431
        return group; // return the last group
3432
    }
3433

    
3434
    return undefined;
3435
};
3436

    
3437
/**
3438
 * @constructor links.Timeline.Item
3439
 * @param {Object} data       Object containing parameters start, end
3440
 *                            content, group, type, editable.
3441
 * @param {Object} [options]  Options to set initial property values
3442
 *                                {Number} top
3443
 *                                {Number} left
3444
 *                                {Number} width
3445
 *                                {Number} height
3446
 */
3447
links.Timeline.Item = function (data, options) {
3448
    if (data) {
3449
        /* TODO: use parseJSONDate as soon as it is tested and working (in two directions)
3450
         this.start = links.Timeline.parseJSONDate(data.start);
3451
         this.end = links.Timeline.parseJSONDate(data.end);
3452
         */
3453
        this.start = data.start;
3454
        this.end = data.end;
3455
        this.content = data.content;
3456
        this.className = data.className;
3457
        this.editable = data.editable;
3458
        this.group = data.group;
3459
        this.type = data.type;
3460
    }
3461
    this.top = 0;
3462
    this.left = 0;
3463
    this.width = 0;
3464
    this.height = 0;
3465
    this.lineWidth = 0;
3466
    this.dotWidth = 0;
3467
    this.dotHeight = 0;
3468

    
3469
    this.rendered = false; // true when the item is draw in the Timeline DOM
3470

    
3471
    if (options) {
3472
        // override the default properties
3473
        for (var option in options) {
3474
            if (options.hasOwnProperty(option)) {
3475
                this[option] = options[option];
3476
            }
3477
        }
3478
    }
3479

    
3480
};
3481

    
3482

    
3483

    
3484
/**
3485
 * Reflow the Item: retrieve its actual size from the DOM
3486
 * @return {boolean} resized    returns true if the axis is resized
3487
 */
3488
links.Timeline.Item.prototype.reflow = function () {
3489
    // Should be implemented by sub-prototype
3490
    return false;
3491
};
3492

    
3493
/**
3494
 * Append all image urls present in the items DOM to the provided array
3495
 * @param {String[]} imageUrls
3496
 */
3497
links.Timeline.Item.prototype.getImageUrls = function (imageUrls) {
3498
    if (this.dom) {
3499
        links.imageloader.filterImageUrls(this.dom, imageUrls);
3500
    }
3501
};
3502

    
3503
/**
3504
 * Select the item
3505
 */
3506
links.Timeline.Item.prototype.select = function () {
3507
    // Should be implemented by sub-prototype
3508
};
3509

    
3510
/**
3511
 * Unselect the item
3512
 */
3513
links.Timeline.Item.prototype.unselect = function () {
3514
    // Should be implemented by sub-prototype
3515
};
3516

    
3517
/**
3518
 * Creates the DOM for the item, depending on its type
3519
 * @return {Element | undefined}
3520
 */
3521
links.Timeline.Item.prototype.createDOM = function () {
3522
    // Should be implemented by sub-prototype
3523
};
3524

    
3525
/**
3526
 * Append the items DOM to the given HTML container. If items DOM does not yet
3527
 * exist, it will be created first.
3528
 * @param {Element} container
3529
 */
3530
links.Timeline.Item.prototype.showDOM = function (container) {
3531
    // Should be implemented by sub-prototype
3532
};
3533

    
3534
/**
3535
 * Remove the items DOM from the current HTML container
3536
 * @param {Element} container
3537
 */
3538
links.Timeline.Item.prototype.hideDOM = function (container) {
3539
    // Should be implemented by sub-prototype
3540
};
3541

    
3542
/**
3543
 * Update the DOM of the item. This will update the content and the classes
3544
 * of the item
3545
 */
3546
links.Timeline.Item.prototype.updateDOM = function () {
3547
    // Should be implemented by sub-prototype
3548
};
3549

    
3550
/**
3551
 * Reposition the item, recalculate its left, top, and width, using the current
3552
 * range of the timeline and the timeline options.
3553
 * @param {links.Timeline} timeline
3554
 */
3555
links.Timeline.Item.prototype.updatePosition = function (timeline) {
3556
    // Should be implemented by sub-prototype
3557
};
3558

    
3559
/**
3560
 * Check if the item is drawn in the timeline (i.e. the DOM of the item is
3561
 * attached to the frame. You may also just request the parameter item.rendered
3562
 * @return {boolean} rendered
3563
 */
3564
links.Timeline.Item.prototype.isRendered = function () {
3565
    return this.rendered;
3566
};
3567

    
3568
/**
3569
 * Check if the item is located in the visible area of the timeline, and
3570
 * not part of a cluster
3571
 * @param {Date} start
3572
 * @param {Date} end
3573
 * @return {boolean} visible
3574
 */
3575
links.Timeline.Item.prototype.isVisible = function (start, end) {
3576
    // Should be implemented by sub-prototype
3577
    return false;
3578
};
3579

    
3580
/**
3581
 * Reposition the item
3582
 * @param {Number} left
3583
 * @param {Number} right
3584
 */
3585
links.Timeline.Item.prototype.setPosition = function (left, right) {
3586
    // Should be implemented by sub-prototype
3587
};
3588

    
3589
/**
3590
 * Calculate the right position of the item
3591
 * @param {links.Timeline} timeline
3592
 * @return {Number} right
3593
 */
3594
links.Timeline.Item.prototype.getRight = function (timeline) {
3595
    // Should be implemented by sub-prototype
3596
    return 0;
3597
};
3598

    
3599
/**
3600
 * Calculate the width of the item
3601
 * @param {links.Timeline} timeline
3602
 * @return {Number} width
3603
 */
3604
links.Timeline.Item.prototype.getWidth = function (timeline) {
3605
    // Should be implemented by sub-prototype
3606
    return this.width || 0; // last rendered width
3607
};
3608

    
3609

    
3610
/**
3611
 * @constructor links.Timeline.ItemBox
3612
 * @extends links.Timeline.Item
3613
 * @param {Object} data       Object containing parameters start, end
3614
 *                            content, group, type, className, editable.
3615
 * @param {Object} [options]  Options to set initial property values
3616
 *                                {Number} top
3617
 *                                {Number} left
3618
 *                                {Number} width
3619
 *                                {Number} height
3620
 */
3621
links.Timeline.ItemBox = function (data, options) {
3622
    links.Timeline.Item.call(this, data, options);
3623
};
3624

    
3625
links.Timeline.ItemBox.prototype = new links.Timeline.Item();
3626

    
3627
/**
3628
 * Reflow the Item: retrieve its actual size from the DOM
3629
 * @return {boolean} resized    returns true if the axis is resized
3630
 * @override
3631
 */
3632
links.Timeline.ItemBox.prototype.reflow = function () {
3633
    var dom = this.dom,
3634
        dotHeight = dom.dot.offsetHeight,
3635
        dotWidth = dom.dot.offsetWidth,
3636
        lineWidth = dom.line.offsetWidth,
3637
        resized = (
3638
            (this.dotHeight != dotHeight) ||
3639
                (this.dotWidth != dotWidth) ||
3640
                (this.lineWidth != lineWidth)
3641
            );
3642

    
3643
    this.dotHeight = dotHeight;
3644
    this.dotWidth = dotWidth;
3645
    this.lineWidth = lineWidth;
3646

    
3647
    return resized;
3648
};
3649

    
3650
/**
3651
 * Select the item
3652
 * @override
3653
 */
3654
links.Timeline.ItemBox.prototype.select = function () {
3655
    var dom = this.dom;
3656
    links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
3657
    links.Timeline.addClassName(dom.line, 'timeline-event-selected ui-state-active');
3658
    links.Timeline.addClassName(dom.dot, 'timeline-event-selected ui-state-active');
3659
};
3660

    
3661
/**
3662
 * Unselect the item
3663
 * @override
3664
 */
3665
links.Timeline.ItemBox.prototype.unselect = function () {
3666
    var dom = this.dom;
3667
    links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
3668
    links.Timeline.removeClassName(dom.line, 'timeline-event-selected ui-state-active');
3669
    links.Timeline.removeClassName(dom.dot, 'timeline-event-selected ui-state-active');
3670
};
3671

    
3672
/**
3673
 * Creates the DOM for the item, depending on its type
3674
 * @return {Element | undefined}
3675
 * @override
3676
 */
3677
links.Timeline.ItemBox.prototype.createDOM = function () {
3678
    // background box
3679
    var divBox = document.createElement("DIV");
3680
    divBox.style.position = "absolute";
3681
    divBox.style.left = this.left + "px";
3682
    divBox.style.top = this.top + "px";
3683

    
3684
    // contents box (inside the background box). used for making margins
3685
    var divContent = document.createElement("DIV");
3686
    divContent.className = "timeline-event-content";
3687
    divContent.innerHTML = this.content;
3688
    divBox.appendChild(divContent);
3689

    
3690
    // line to axis
3691
    var divLine = document.createElement("DIV");
3692
    divLine.style.position = "absolute";
3693
    divLine.style.width = "0px";
3694
    // important: the vertical line is added at the front of the list of elements,
3695
    // so it will be drawn behind all boxes and ranges
3696
    divBox.line = divLine;
3697

    
3698
    // dot on axis
3699
    var divDot = document.createElement("DIV");
3700
    divDot.style.position = "absolute";
3701
    divDot.style.width  = "0px";
3702
    divDot.style.height = "0px";
3703
    divBox.dot = divDot;
3704

    
3705
    this.dom = divBox;
3706
    this.updateDOM();
3707

    
3708
    return divBox;
3709
};
3710

    
3711
/**
3712
 * Append the items DOM to the given HTML container. If items DOM does not yet
3713
 * exist, it will be created first.
3714
 * @param {Element} container
3715
 * @override
3716
 */
3717
links.Timeline.ItemBox.prototype.showDOM = function (container) {
3718
    var dom = this.dom;
3719
    if (!dom) {
3720
        dom = this.createDOM();
3721
    }
3722

    
3723
    if (dom.parentNode != container) {
3724
        if (dom.parentNode) {
3725
            // container is changed. remove from old container
3726
            this.hideDOM();
3727
        }
3728

    
3729
        // append to this container
3730
        container.appendChild(dom);
3731
        container.insertBefore(dom.line, container.firstChild);
3732
        // Note: line must be added in front of the this,
3733
        //       such that it stays below all this
3734
        container.appendChild(dom.dot);
3735
        this.rendered = true;
3736
    }
3737
};
3738

    
3739
/**
3740
 * Remove the items DOM from the current HTML container, but keep the DOM in
3741
 * memory
3742
 * @override
3743
 */
3744
links.Timeline.ItemBox.prototype.hideDOM = function () {
3745
    var dom = this.dom;
3746
    if (dom) {
3747
        if (dom.parentNode) {
3748
            dom.parentNode.removeChild(dom);
3749
        }
3750
        if (dom.line && dom.line.parentNode) {
3751
            dom.line.parentNode.removeChild(dom.line);
3752
        }
3753
        if (dom.dot && dom.dot.parentNode) {
3754
            dom.dot.parentNode.removeChild(dom.dot);
3755
        }
3756
        this.rendered = false;
3757
    }
3758
};
3759

    
3760
/**
3761
 * Update the DOM of the item. This will update the content and the classes
3762
 * of the item
3763
 * @override
3764
 */
3765
links.Timeline.ItemBox.prototype.updateDOM = function () {
3766
    var divBox = this.dom;
3767
    if (divBox) {
3768
        var divLine = divBox.line;
3769
        var divDot = divBox.dot;
3770

    
3771
        // update contents
3772
        divBox.firstChild.innerHTML = this.content;
3773

    
3774
        // update class
3775
        divBox.className = "timeline-event timeline-event-box ui-widget ui-state-default";
3776
        divLine.className = "timeline-event timeline-event-line ui-widget ui-state-default";
3777
        divDot.className  = "timeline-event timeline-event-dot ui-widget ui-state-default";
3778

    
3779
        if (this.isCluster) {
3780
            links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
3781
            links.Timeline.addClassName(divLine, 'timeline-event-cluster ui-widget-header');
3782
            links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header');
3783
        }
3784

    
3785
        // add item specific class name when provided
3786
        if (this.className) {
3787
            links.Timeline.addClassName(divBox, this.className);
3788
            links.Timeline.addClassName(divLine, this.className);
3789
            links.Timeline.addClassName(divDot, this.className);
3790
        }
3791

    
3792
        // TODO: apply selected className?
3793
    }
3794
};
3795

    
3796
/**
3797
 * Reposition the item, recalculate its left, top, and width, using the current
3798
 * range of the timeline and the timeline options.
3799
 * @param {links.Timeline} timeline
3800
 * @override
3801
 */
3802
links.Timeline.ItemBox.prototype.updatePosition = function (timeline) {
3803
    var dom = this.dom;
3804
    if (dom) {
3805
        var left = timeline.timeToScreen(this.start),
3806
            axisOnTop = timeline.options.axisOnTop,
3807
            axisTop = timeline.size.axis.top,
3808
            axisHeight = timeline.size.axis.height,
3809
            boxAlign = (timeline.options.box && timeline.options.box.align) ?
3810
                timeline.options.box.align : undefined;
3811

    
3812
        dom.style.top = this.top + "px";
3813
        if (boxAlign == 'right') {
3814
            dom.style.left = (left - this.width) + "px";
3815
        }
3816
        else if (boxAlign == 'left') {
3817
            dom.style.left = (left) + "px";
3818
        }
3819
        else { // default or 'center'
3820
            dom.style.left = (left - this.width/2) + "px";
3821
        }
3822

    
3823
        var line = dom.line;
3824
        var dot = dom.dot;
3825
        line.style.left = (left - this.lineWidth/2) + "px";
3826
        dot.style.left = (left - this.dotWidth/2) + "px";
3827
        if (axisOnTop) {
3828
            line.style.top = axisHeight + "px";
3829
            line.style.height = Math.max(this.top - axisHeight, 0) + "px";
3830
            dot.style.top = (axisHeight - this.dotHeight/2) + "px";
3831
        }
3832
        else {
3833
            line.style.top = (this.top + this.height) + "px";
3834
            line.style.height = Math.max(axisTop - this.top - this.height, 0) + "px";
3835
            dot.style.top = (axisTop - this.dotHeight/2) + "px";
3836
        }
3837
    }
3838
};
3839

    
3840
/**
3841
 * Check if the item is visible in the timeline, and not part of a cluster
3842
 * @param {Date} start
3843
 * @param {Date} end
3844
 * @return {Boolean} visible
3845
 * @override
3846
 */
3847
links.Timeline.ItemBox.prototype.isVisible = function (start, end) {
3848
    if (this.cluster) {
3849
        return false;
3850
    }
3851

    
3852
    return (this.start > start) && (this.start < end);
3853
};
3854

    
3855
/**
3856
 * Reposition the item
3857
 * @param {Number} left
3858
 * @param {Number} right
3859
 * @override
3860
 */
3861
links.Timeline.ItemBox.prototype.setPosition = function (left, right) {
3862
    var dom = this.dom;
3863

    
3864
    dom.style.left = (left - this.width / 2) + "px";
3865
    dom.line.style.left = (left - this.lineWidth / 2) + "px";
3866
    dom.dot.style.left = (left - this.dotWidth / 2) + "px";
3867

    
3868
    if (this.group) {
3869
        this.top = this.group.top;
3870
        dom.style.top = this.top + 'px';
3871
    }
3872
};
3873

    
3874
/**
3875
 * Calculate the right position of the item
3876
 * @param {links.Timeline} timeline
3877
 * @return {Number} right
3878
 * @override
3879
 */
3880
links.Timeline.ItemBox.prototype.getRight = function (timeline) {
3881
    var boxAlign = (timeline.options.box && timeline.options.box.align) ?
3882
        timeline.options.box.align : undefined;
3883

    
3884
    var left = timeline.timeToScreen(this.start);
3885
    var right;
3886
    if (boxAlign == 'right') {
3887
        right = left;
3888
    }
3889
    else if (boxAlign == 'left') {
3890
        right = (left + this.width);
3891
    }
3892
    else { // default or 'center'
3893
        right = (left + this.width / 2);
3894
    }
3895

    
3896
    return right;
3897
};
3898

    
3899
/**
3900
 * @constructor links.Timeline.ItemRange
3901
 * @extends links.Timeline.Item
3902
 * @param {Object} data       Object containing parameters start, end
3903
 *                            content, group, type, className, editable.
3904
 * @param {Object} [options]  Options to set initial property values
3905
 *                                {Number} top
3906
 *                                {Number} left
3907
 *                                {Number} width
3908
 *                                {Number} height
3909
 */
3910
links.Timeline.ItemRange = function (data, options) {
3911
    links.Timeline.Item.call(this, data, options);
3912
};
3913

    
3914
links.Timeline.ItemRange.prototype = new links.Timeline.Item();
3915

    
3916
/**
3917
 * Select the item
3918
 * @override
3919
 */
3920
links.Timeline.ItemRange.prototype.select = function () {
3921
    var dom = this.dom;
3922
    links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
3923
};
3924

    
3925
/**
3926
 * Unselect the item
3927
 * @override
3928
 */
3929
links.Timeline.ItemRange.prototype.unselect = function () {
3930
    var dom = this.dom;
3931
    links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
3932
};
3933

    
3934
/**
3935
 * Creates the DOM for the item, depending on its type
3936
 * @return {Element | undefined}
3937
 * @override
3938
 */
3939
links.Timeline.ItemRange.prototype.createDOM = function () {
3940
    // background box
3941
    var divBox = document.createElement("DIV");
3942
    divBox.style.position = "absolute";
3943

    
3944
    // contents box
3945
    var divContent = document.createElement("DIV");
3946
    divContent.className = "timeline-event-content";
3947
    divBox.appendChild(divContent);
3948

    
3949
    this.dom = divBox;
3950
    this.updateDOM();
3951

    
3952
    return divBox;
3953
};
3954

    
3955
/**
3956
 * Append the items DOM to the given HTML container. If items DOM does not yet
3957
 * exist, it will be created first.
3958
 * @param {Element} container
3959
 * @override
3960
 */
3961
links.Timeline.ItemRange.prototype.showDOM = function (container) {
3962
    var dom = this.dom;
3963
    if (!dom) {
3964
        dom = this.createDOM();
3965
    }
3966

    
3967
    if (dom.parentNode != container) {
3968
        if (dom.parentNode) {
3969
            // container changed. remove the item from the old container
3970
            this.hideDOM();
3971
        }
3972

    
3973
        // append to the new container
3974
        container.appendChild(dom);
3975
        this.rendered = true;
3976
    }
3977
};
3978

    
3979
/**
3980
 * Remove the items DOM from the current HTML container
3981
 * The DOM will be kept in memory
3982
 * @override
3983
 */
3984
links.Timeline.ItemRange.prototype.hideDOM = function () {
3985
    var dom = this.dom;
3986
    if (dom) {
3987
        if (dom.parentNode) {
3988
            dom.parentNode.removeChild(dom);
3989
        }
3990
        this.rendered = false;
3991
    }
3992
};
3993

    
3994
/**
3995
 * Update the DOM of the item. This will update the content and the classes
3996
 * of the item
3997
 * @override
3998
 */
3999
links.Timeline.ItemRange.prototype.updateDOM = function () {
4000
    var divBox = this.dom;
4001
    if (divBox) {
4002
        // update contents
4003
        divBox.firstChild.innerHTML = this.content;
4004

    
4005
        // update class
4006
        divBox.className = "timeline-event timeline-event-range ui-widget ui-state-default";
4007

    
4008
        if (this.isCluster) {
4009
            links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
4010
        }
4011

    
4012
        // add item specific class name when provided
4013
        if (this.className) {
4014
            links.Timeline.addClassName(divBox, this.className);
4015
        }
4016

    
4017
        // TODO: apply selected className?
4018
    }
4019
};
4020

    
4021
/**
4022
 * Reposition the item, recalculate its left, top, and width, using the current
4023
 * range of the timeline and the timeline options. *
4024
 * @param {links.Timeline} timeline
4025
 * @override
4026
 */
4027
links.Timeline.ItemRange.prototype.updatePosition = function (timeline) {
4028
    var dom = this.dom;
4029
    if (dom) {
4030
        var contentWidth = timeline.size.contentWidth,
4031
            left = timeline.timeToScreen(this.start),
4032
            right = timeline.timeToScreen(this.end);
4033

    
4034
        // limit the width of the this, as browsers cannot draw very wide divs
4035
        if (left < -contentWidth) {
4036
            left = -contentWidth;
4037
        }
4038
        if (right > 2 * contentWidth) {
4039
            right = 2 * contentWidth;
4040
        }
4041

    
4042
        dom.style.top = this.top + "px";
4043
        dom.style.left = left + "px";
4044
        //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth
4045
        dom.style.width = Math.max(right - left, 1) + "px";
4046
    }
4047
};
4048

    
4049
/**
4050
 * Check if the item is visible in the timeline, and not part of a cluster
4051
 * @param {Number} start
4052
 * @param {Number} end
4053
 * @return {boolean} visible
4054
 * @override
4055
 */
4056
links.Timeline.ItemRange.prototype.isVisible = function (start, end) {
4057
    if (this.cluster) {
4058
        return false;
4059
    }
4060

    
4061
    return (this.end > start)
4062
        && (this.start < end);
4063
};
4064

    
4065
/**
4066
 * Reposition the item
4067
 * @param {Number} left
4068
 * @param {Number} right
4069
 * @override
4070
 */
4071
links.Timeline.ItemRange.prototype.setPosition = function (left, right) {
4072
    var dom = this.dom;
4073

    
4074
    dom.style.left = left + 'px';
4075
    dom.style.width = (right - left) + 'px';
4076

    
4077
    if (this.group) {
4078
        this.top = this.group.top;
4079
        dom.style.top = this.top + 'px';
4080
    }
4081
};
4082

    
4083
/**
4084
 * Calculate the right position of the item
4085
 * @param {links.Timeline} timeline
4086
 * @return {Number} right
4087
 * @override
4088
 */
4089
links.Timeline.ItemRange.prototype.getRight = function (timeline) {
4090
    return timeline.timeToScreen(this.end);
4091
};
4092

    
4093
/**
4094
 * Calculate the width of the item
4095
 * @param {links.Timeline} timeline
4096
 * @return {Number} width
4097
 * @override
4098
 */
4099
links.Timeline.ItemRange.prototype.getWidth = function (timeline) {
4100
    return timeline.timeToScreen(this.end) - timeline.timeToScreen(this.start);
4101
};
4102

    
4103
/**
4104
 * @constructor links.Timeline.ItemDot
4105
 * @extends links.Timeline.Item
4106
 * @param {Object} data       Object containing parameters start, end
4107
 *                            content, group, type, className, editable.
4108
 * @param {Object} [options]  Options to set initial property values
4109
 *                                {Number} top
4110
 *                                {Number} left
4111
 *                                {Number} width
4112
 *                                {Number} height
4113
 */
4114
links.Timeline.ItemDot = function (data, options) {
4115
    links.Timeline.Item.call(this, data, options);
4116
};
4117

    
4118
links.Timeline.ItemDot.prototype = new links.Timeline.Item();
4119

    
4120
/**
4121
 * Reflow the Item: retrieve its actual size from the DOM
4122
 * @return {boolean} resized    returns true if the axis is resized
4123
 * @override
4124
 */
4125
links.Timeline.ItemDot.prototype.reflow = function () {
4126
    var dom = this.dom,
4127
        dotHeight = dom.dot.offsetHeight,
4128
        dotWidth = dom.dot.offsetWidth,
4129
        contentHeight = dom.content.offsetHeight,
4130
        resized = (
4131
            (this.dotHeight != dotHeight) ||
4132
                (this.dotWidth != dotWidth) ||
4133
                (this.contentHeight != contentHeight)
4134
            );
4135

    
4136
    this.dotHeight = dotHeight;
4137
    this.dotWidth = dotWidth;
4138
    this.contentHeight = contentHeight;
4139

    
4140
    return resized;
4141
};
4142

    
4143
/**
4144
 * Select the item
4145
 * @override
4146
 */
4147
links.Timeline.ItemDot.prototype.select = function () {
4148
    var dom = this.dom;
4149
    links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
4150
};
4151

    
4152
/**
4153
 * Unselect the item
4154
 * @override
4155
 */
4156
links.Timeline.ItemDot.prototype.unselect = function () {
4157
    var dom = this.dom;
4158
    links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
4159
};
4160

    
4161
/**
4162
 * Creates the DOM for the item, depending on its type
4163
 * @return {Element | undefined}
4164
 * @override
4165
 */
4166
links.Timeline.ItemDot.prototype.createDOM = function () {
4167
    // background box
4168
    var divBox = document.createElement("DIV");
4169
    divBox.style.position = "absolute";
4170

    
4171
    // contents box, right from the dot
4172
    var divContent = document.createElement("DIV");
4173
    divContent.className = "timeline-event-content";
4174
    divBox.appendChild(divContent);
4175

    
4176
    // dot at start
4177
    var divDot = document.createElement("DIV");
4178
    divDot.style.position = "absolute";
4179
    divDot.style.width = "0px";
4180
    divDot.style.height = "0px";
4181
    divBox.appendChild(divDot);
4182

    
4183
    divBox.content = divContent;
4184
    divBox.dot = divDot;
4185

    
4186
    this.dom = divBox;
4187
    this.updateDOM();
4188

    
4189
    return divBox;
4190
};
4191

    
4192
/**
4193
 * Append the items DOM to the given HTML container. If items DOM does not yet
4194
 * exist, it will be created first.
4195
 * @param {Element} container
4196
 * @override
4197
 */
4198
links.Timeline.ItemDot.prototype.showDOM = function (container) {
4199
    var dom = this.dom;
4200
    if (!dom) {
4201
        dom = this.createDOM();
4202
    }
4203

    
4204
    if (dom.parentNode != container) {
4205
        if (dom.parentNode) {
4206
            // container changed. remove it from old container first
4207
            this.hideDOM();
4208
        }
4209

    
4210
        // append to container
4211
        container.appendChild(dom);
4212
        this.rendered = true;
4213
    }
4214
};
4215

    
4216
/**
4217
 * Remove the items DOM from the current HTML container
4218
 * @override
4219
 */
4220
links.Timeline.ItemDot.prototype.hideDOM = function () {
4221
    var dom = this.dom;
4222
    if (dom) {
4223
        if (dom.parentNode) {
4224
            dom.parentNode.removeChild(dom);
4225
        }
4226
        this.rendered = false;
4227
    }
4228
};
4229

    
4230
/**
4231
 * Update the DOM of the item. This will update the content and the classes
4232
 * of the item
4233
 * @override
4234
 */
4235
links.Timeline.ItemDot.prototype.updateDOM = function () {
4236
    if (this.dom) {
4237
        var divBox = this.dom;
4238
        var divDot = divBox.dot;
4239

    
4240
        // update contents
4241
        divBox.firstChild.innerHTML = this.content;
4242

    
4243
        // update classes
4244
        divBox.className = "timeline-event-dot-container";
4245
        divDot.className  = "timeline-event timeline-event-dot ui-widget ui-state-default";
4246

    
4247
        if (this.isCluster) {
4248
            links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
4249
            links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header');
4250
        }
4251

    
4252
        // add item specific class name when provided
4253
        if (this.className) {
4254
            links.Timeline.addClassName(divBox, this.className);
4255
            links.Timeline.addClassName(divDot, this.className);
4256
        }
4257

    
4258
        // TODO: apply selected className?
4259
    }
4260
};
4261

    
4262
/**
4263
 * Reposition the item, recalculate its left, top, and width, using the current
4264
 * range of the timeline and the timeline options. *
4265
 * @param {links.Timeline} timeline
4266
 * @override
4267
 */
4268
links.Timeline.ItemDot.prototype.updatePosition = function (timeline) {
4269
    var dom = this.dom;
4270
    if (dom) {
4271
        var left = timeline.timeToScreen(this.start);
4272

    
4273
        dom.style.top = this.top + "px";
4274
        dom.style.left = (left - this.dotWidth / 2) + "px";
4275

    
4276
        dom.content.style.marginLeft = (1.5 * this.dotWidth) + "px";
4277
        //dom.content.style.marginRight = (0.5 * this.dotWidth) + "px"; // TODO
4278
        dom.dot.style.top = ((this.height - this.dotHeight) / 2) + "px";
4279
    }
4280
};
4281

    
4282
/**
4283
 * Check if the item is visible in the timeline, and not part of a cluster.
4284
 * @param {Date} start
4285
 * @param {Date} end
4286
 * @return {boolean} visible
4287
 * @override
4288
 */
4289
links.Timeline.ItemDot.prototype.isVisible = function (start, end) {
4290
    if (this.cluster) {
4291
        return false;
4292
    }
4293

    
4294
    return (this.start > start)
4295
        && (this.start < end);
4296
};
4297

    
4298
/**
4299
 * Reposition the item
4300
 * @param {Number} left
4301
 * @param {Number} right
4302
 * @override
4303
 */
4304
links.Timeline.ItemDot.prototype.setPosition = function (left, right) {
4305
    var dom = this.dom;
4306

    
4307
    dom.style.left = (left - this.dotWidth / 2) + "px";
4308

    
4309
    if (this.group) {
4310
        this.top = this.group.top;
4311
        dom.style.top = this.top + 'px';
4312
    }
4313
};
4314

    
4315
/**
4316
 * Calculate the right position of the item
4317
 * @param {links.Timeline} timeline
4318
 * @return {Number} right
4319
 * @override
4320
 */
4321
links.Timeline.ItemDot.prototype.getRight = function (timeline) {
4322
    return timeline.timeToScreen(this.start) + this.width;
4323
};
4324

    
4325
/**
4326
 * Retrieve the properties of an item.
4327
 * @param {Number} index
4328
 * @return {Object} properties  Object containing item properties:<br>
4329
 *                              {Date} start (required),
4330
 *                              {Date} end (optional),
4331
 *                              {String} content (required),
4332
 *                              {String} group (optional),
4333
 *                              {String} className (optional)
4334
 *                              {boolean} editable (optional)
4335
 *                              {String} type (optional)
4336
 */
4337
links.Timeline.prototype.getItem = function (index) {
4338
    if (index >= this.items.length) {
4339
        throw "Cannot get item, index out of range";
4340
    }
4341

    
4342
    var item = this.items[index];
4343

    
4344
    var properties = {};
4345
    properties.start = new Date(item.start.valueOf());
4346
    if (item.end) {
4347
        properties.end = new Date(item.end.valueOf());
4348
    }
4349
    properties.content = item.content;
4350
    if (item.group) {
4351
        properties.group = this.getGroupName(item.group);
4352
    }
4353
    if ('className' in item) {
4354
        properties.className = this.getGroupName(item.className);
4355
    }
4356
    if (item.hasOwnProperty('editable') && (typeof item.editable != 'undefined')) {
4357
        properties.editable = item.editable;
4358
    }
4359
    if (item.type) {
4360
        properties.type = item.type;
4361
    }
4362

    
4363
    return properties;
4364
};
4365

    
4366
/**
4367
 * Add a new item.
4368
 * @param {Object} itemData     Object containing item properties:<br>
4369
 *                              {Date} start (required),
4370
 *                              {Date} end (optional),
4371
 *                              {String} content (required),
4372
 *                              {String} group (optional)
4373
 *                              {String} className (optional)
4374
 *                              {Boolean} editable (optional)
4375
 *                              {String} type (optional)
4376
 * @param {boolean} [preventRender=false]   Do not re-render timeline if true
4377
 */
4378
links.Timeline.prototype.addItem = function (itemData, preventRender) {
4379
    var itemsData = [
4380
        itemData
4381
    ];
4382

    
4383
    this.addItems(itemsData, preventRender);
4384
};
4385

    
4386
/**
4387
 * Add new items.
4388
 * @param {Array} itemsData An array containing Objects.
4389
 *                          The objects must have the following parameters:
4390
 *                            {Date} start,
4391
 *                            {Date} end,
4392
 *                            {String} content with text or HTML code,
4393
 *                            {String} group (optional)
4394
 *                            {String} className (optional)
4395
 *                            {String} editable (optional)
4396
 *                            {String} type (optional)
4397
 * @param {boolean} [preventRender=false]   Do not re-render timeline if true
4398
 */
4399
links.Timeline.prototype.addItems = function (itemsData, preventRender) {
4400
    var timeline = this,
4401
        items = this.items;
4402

    
4403
    // append the items
4404
    itemsData.forEach(function (itemData) {
4405
        var index = items.length;
4406
        items.push(timeline.createItem(itemData));
4407
        timeline.updateData(index, itemData);
4408

    
4409
        // note: there is no need to add the item to the renderQueue, that
4410
        // will be done when this.render() is executed and all items are
4411
        // filtered again.
4412
    });
4413

    
4414
    // prepare data for clustering, by filtering and sorting by type
4415
    if (this.options.cluster) {
4416
        this.clusterGenerator.updateData();
4417
    }
4418

    
4419
    if (!preventRender) {
4420
        this.render({
4421
            animate: false
4422
        });
4423
    }
4424
};
4425

    
4426
/**
4427
 * Create an item object, containing all needed parameters
4428
 * @param {Object} itemData  Object containing parameters start, end
4429
 *                           content, group.
4430
 * @return {Object} item
4431
 */
4432
links.Timeline.prototype.createItem = function(itemData) {
4433
    var type = itemData.type || (itemData.end ? 'range' : this.options.style);
4434
    var data = {
4435
        start: itemData.start,
4436
        end: itemData.end,
4437
        content: itemData.content,
4438
        className: itemData.className,
4439
        editable: itemData.editable,
4440
        group: this.getGroup(itemData.group),
4441
        type: type
4442
    };
4443
    // TODO: optimize this, when creating an item, all data is copied twice...
4444

    
4445
    // TODO: is initialTop needed?
4446
    var initialTop,
4447
        options = this.options;
4448
    if (options.axisOnTop) {
4449
        initialTop = this.size.axis.height + options.eventMarginAxis + options.eventMargin / 2;
4450
    }
4451
    else {
4452
        initialTop = this.size.contentHeight - options.eventMarginAxis - options.eventMargin / 2;
4453
    }
4454

    
4455
    if (type in this.itemTypes) {
4456
        return new this.itemTypes[type](data, {'top': initialTop})
4457
    }
4458

    
4459
    console.log('ERROR: Unknown event style "' + type + '"');
4460
    return new links.Timeline.Item(data, {
4461
        'top': initialTop
4462
    });
4463
};
4464

    
4465
/**
4466
 * Edit an item
4467
 * @param {Number} index
4468
 * @param {Object} itemData     Object containing item properties:<br>
4469
 *                              {Date} start (required),
4470
 *                              {Date} end (optional),
4471
 *                              {String} content (required),
4472
 *                              {String} group (optional)
4473
 * @param {boolean} [preventRender=false]   Do not re-render timeline if true
4474
 */
4475
links.Timeline.prototype.changeItem = function (index, itemData, preventRender) {
4476
    var oldItem = this.items[index];
4477
    if (!oldItem) {
4478
        throw "Cannot change item, index out of range";
4479
    }
4480

    
4481
    // replace item, merge the changes
4482
    var newItem = this.createItem({
4483
        'start':     itemData.hasOwnProperty('start') ?     itemData.start :     oldItem.start,
4484
        'end':       itemData.hasOwnProperty('end') ?       itemData.end :       oldItem.end,
4485
        'content':   itemData.hasOwnProperty('content') ?   itemData.content :   oldItem.content,
4486
        'group':     itemData.hasOwnProperty('group') ?     itemData.group :     this.getGroupName(oldItem.group),
4487
        'className': itemData.hasOwnProperty('className') ? itemData.className : oldItem.className,
4488
        'editable':  itemData.hasOwnProperty('editable') ?  itemData.editable :  oldItem.editable,
4489
        'type':      itemData.hasOwnProperty('type') ?      itemData.type :      oldItem.type
4490
    });
4491
    this.items[index] = newItem;
4492

    
4493
    // append the changes to the render queue
4494
    this.renderQueue.hide.push(oldItem);
4495
    this.renderQueue.show.push(newItem);
4496

    
4497
    // update the original data table
4498
    this.updateData(index, itemData);
4499

    
4500
    // prepare data for clustering, by filtering and sorting by type
4501
    if (this.options.cluster) {
4502
        this.clusterGenerator.updateData();
4503
    }
4504

    
4505
    if (!preventRender) {
4506
        // redraw timeline
4507
        this.render({
4508
            animate: false
4509
        });
4510

    
4511
        if (this.selection && this.selection.index == index) {
4512
            newItem.select();
4513
        }
4514
    }
4515
};
4516

    
4517
/**
4518
 * Delete all groups
4519
 */
4520
links.Timeline.prototype.deleteGroups = function () {
4521
    this.groups = [];
4522
    this.groupIndexes = {};
4523
};
4524

    
4525

    
4526
/**
4527
 * Get a group by the group name. When the group does not exist,
4528
 * it will be created.
4529
 * @param {String} groupName   the name of the group
4530
 * @return {Object} groupObject
4531
 */
4532
links.Timeline.prototype.getGroup = function (groupName) {
4533
    var groups = this.groups,
4534
        groupIndexes = this.groupIndexes,
4535
        groupObj = undefined;
4536

    
4537
    var groupIndex = groupIndexes[groupName];
4538
    if (groupIndex == undefined && groupName != undefined) { // not null or undefined
4539
        groupObj = {
4540
            'content': groupName,
4541
            'labelTop': 0,
4542
            'lineTop': 0
4543
            // note: this object will lateron get addition information, 
4544
            //       such as height and width of the group         
4545
        };
4546
        groups.push(groupObj);
4547
        // sort the groups
4548
        groups = groups.sort(function (a, b) {
4549
            if (a.content > b.content) {
4550
                return 1;
4551
            }
4552
            if (a.content < b.content) {
4553
                return -1;
4554
            }
4555
            return 0;
4556
        });
4557

    
4558
        // rebuilt the groupIndexes
4559
        for (var i = 0, iMax = groups.length; i < iMax; i++) {
4560
            groupIndexes[groups[i].content] = i;
4561
        }
4562
    }
4563
    else {
4564
        groupObj = groups[groupIndex];
4565
    }
4566

    
4567
    return groupObj;
4568
};
4569

    
4570
/**
4571
 * Get the group name from a group object.
4572
 * @param {Object} groupObj
4573
 * @return {String} groupName   the name of the group, or undefined when group
4574
 *                              was not provided
4575
 */
4576
links.Timeline.prototype.getGroupName = function (groupObj) {
4577
    return groupObj ? groupObj.content : undefined;
4578
};
4579

    
4580
/**
4581
 * Cancel a change item
4582
 * This method can be called insed an event listener which catches the "change"
4583
 * event. The changed event position will be undone.
4584
 */
4585
links.Timeline.prototype.cancelChange = function () {
4586
    this.applyChange = false;
4587
};
4588

    
4589
/**
4590
 * Cancel deletion of an item
4591
 * This method can be called insed an event listener which catches the "delete"
4592
 * event. Deletion of the event will be undone.
4593
 */
4594
links.Timeline.prototype.cancelDelete = function () {
4595
    this.applyDelete = false;
4596
};
4597

    
4598

    
4599
/**
4600
 * Cancel creation of a new item
4601
 * This method can be called insed an event listener which catches the "new"
4602
 * event. Creation of the new the event will be undone.
4603
 */
4604
links.Timeline.prototype.cancelAdd = function () {
4605
    this.applyAdd = false;
4606
};
4607

    
4608

    
4609
/**
4610
 * Select an event. The visible chart range will be moved such that the selected
4611
 * event is placed in the middle.
4612
 * For example selection = [{row: 5}];
4613
 * @param {Array} selection   An array with a column row, containing the row
4614
 *                           number (the id) of the event to be selected.
4615
 * @return {boolean}         true if selection is succesfully set, else false.
4616
 */
4617
links.Timeline.prototype.setSelection = function(selection) {
4618
    if (selection != undefined && selection.length > 0) {
4619
        if (selection[0].row != undefined) {
4620
            var index = selection[0].row;
4621
            if (this.items[index]) {
4622
                var item = this.items[index];
4623
                this.selectItem(index);
4624

    
4625
                // move the visible chart range to the selected event.
4626
                var start = item.start;
4627
                var end = item.end;
4628
                var middle; // number
4629
                if (end != undefined) {
4630
                    middle = (end.valueOf() + start.valueOf()) / 2;
4631
                } else {
4632
                    middle = start.valueOf();
4633
                }
4634
                var diff = (this.end.valueOf() - this.start.valueOf()),
4635
                    newStart = new Date(middle - diff/2),
4636
                    newEnd = new Date(middle + diff/2);
4637

    
4638
                this.setVisibleChartRange(newStart, newEnd);
4639

    
4640
                return true;
4641
            }
4642
        }
4643
    }
4644
    else {
4645
        // unselect current selection
4646
        this.unselectItem();
4647
    }
4648
    return false;
4649
};
4650

    
4651
/**
4652
 * Retrieve the currently selected event
4653
 * @return {Array} sel  An array with a column row, containing the row number
4654
 *                      of the selected event. If there is no selection, an
4655
 *                      empty array is returned.
4656
 */
4657
links.Timeline.prototype.getSelection = function() {
4658
    var sel = [];
4659
    if (this.selection) {
4660
        sel.push({"row": this.selection.index});
4661
    }
4662
    return sel;
4663
};
4664

    
4665

    
4666
/**
4667
 * Select an item by its index
4668
 * @param {Number} index
4669
 */
4670
links.Timeline.prototype.selectItem = function(index) {
4671
    this.unselectItem();
4672

    
4673
    this.selection = undefined;
4674

    
4675
    if (this.items[index] != undefined) {
4676
        var item = this.items[index],
4677
            domItem = item.dom;
4678

    
4679
        this.selection = {
4680
            'index': index
4681
        };
4682

    
4683
        if (item && item.dom) {
4684
            // TODO: move adjusting the domItem to the item itself
4685
            if (this.isEditable(item)) {
4686
                item.dom.style.cursor = 'move';
4687
            }
4688
            item.select();
4689
        }
4690
        this.repaintDeleteButton();
4691
        this.repaintDragAreas();
4692
    }
4693
};
4694

    
4695
/**
4696
 * Check if an item is currently selected
4697
 * @param {Number} index
4698
 * @return {boolean} true if row is selected, else false
4699
 */
4700
links.Timeline.prototype.isSelected = function (index) {
4701
    return (this.selection && this.selection.index == index);
4702
};
4703

    
4704
/**
4705
 * Unselect the currently selected event (if any)
4706
 */
4707
links.Timeline.prototype.unselectItem = function() {
4708
    if (this.selection) {
4709
        var item = this.items[this.selection.index];
4710

    
4711
        if (item && item.dom) {
4712
            var domItem = item.dom;
4713
            domItem.style.cursor = '';
4714
            item.unselect();
4715
        }
4716

    
4717
        this.selection = undefined;
4718
        this.repaintDeleteButton();
4719
        this.repaintDragAreas();
4720
    }
4721
};
4722

    
4723

    
4724
/**
4725
 * Stack the items such that they don't overlap. The items will have a minimal
4726
 * distance equal to options.eventMargin.
4727
 * @param {boolean | undefined} animate    if animate is true, the items are
4728
 *                                         moved to their new position animated
4729
 *                                         defaults to false.
4730
 */
4731
links.Timeline.prototype.stackItems = function(animate) {
4732
    if (this.groups.length > 0) {
4733
        // under this conditions we refuse to stack the events
4734
        // TODO: implement support for stacking items per group
4735
        return;
4736
    }
4737

    
4738
    if (animate == undefined) {
4739
        animate = false;
4740
    }
4741

    
4742
    // calculate the order and final stack position of the items
4743
    var stack = this.stack;
4744
    if (!stack) {
4745
        stack = {};
4746
        this.stack = stack;
4747
    }
4748
    stack.sortedItems = this.stackOrder(this.renderedItems);
4749
    stack.finalItems = this.stackCalculateFinal(stack.sortedItems);
4750

    
4751
    if (animate || stack.timer) {
4752
        // move animated to the final positions
4753
        var timeline = this;
4754
        var step = function () {
4755
            var arrived = timeline.stackMoveOneStep(stack.sortedItems,
4756
                stack.finalItems);
4757

    
4758
            timeline.repaint();
4759

    
4760
            if (!arrived) {
4761
                stack.timer = setTimeout(step, 30);
4762
            }
4763
            else {
4764
                delete stack.timer;
4765
            }
4766
        };
4767

    
4768
        if (!stack.timer) {
4769
            stack.timer = setTimeout(step, 30);
4770
        }
4771
    }
4772
    else {
4773
        // move immediately to the final positions
4774
        this.stackMoveToFinal(stack.sortedItems, stack.finalItems);
4775
    }
4776
};
4777

    
4778
/**
4779
 * Cancel any running animation
4780
 */
4781
links.Timeline.prototype.stackCancelAnimation = function() {
4782
    if (this.stack && this.stack.timer) {
4783
        clearTimeout(this.stack.timer);
4784
        delete this.stack.timer;
4785
    }
4786
};
4787

    
4788

    
4789
/**
4790
 * Order the items in the array this.items. The default order is determined via:
4791
 * - Ranges go before boxes and dots.
4792
 * - The item with the oldest start time goes first
4793
 * If a custom function has been provided via the stackorder option, then this will be used.
4794
 * @param {Array} items        Array with items
4795
 * @return {Array} sortedItems Array with sorted items
4796
 */
4797
links.Timeline.prototype.stackOrder = function(items) {
4798
    // TODO: store the sorted items, to have less work later on
4799
    var sortedItems = items.concat([]);
4800

    
4801
    //if a customer stack order function exists, use it. 
4802
    var f = this.options.customStackOrder && (typeof this.options.customStackOrder === 'function') ? this.options.customStackOrder : function (a, b)
4803
    {
4804
        if ((a instanceof links.Timeline.ItemRange) &&
4805
            !(b instanceof links.Timeline.ItemRange)) {
4806
            return -1;
4807
        }
4808

    
4809
        if (!(a instanceof links.Timeline.ItemRange) &&
4810
            (b instanceof links.Timeline.ItemRange)) {
4811
            return 1;
4812
        }
4813

    
4814
        return (a.left - b.left);
4815
    };
4816

    
4817
    sortedItems.sort(f);
4818

    
4819
    return sortedItems;
4820
};
4821

    
4822
/**
4823
 * Adjust vertical positions of the events such that they don't overlap each
4824
 * other.
4825
 * @param {timeline.Item[]} items
4826
 * @return {Object[]} finalItems
4827
 */
4828
links.Timeline.prototype.stackCalculateFinal = function(items) {
4829
    var i,
4830
        iMax,
4831
        size = this.size,
4832
        axisTop = size.axis.top,
4833
        axisHeight = size.axis.height,
4834
        options = this.options,
4835
        axisOnTop = options.axisOnTop,
4836
        eventMargin = options.eventMargin,
4837
        eventMarginAxis = options.eventMarginAxis,
4838
        finalItems = [];
4839

    
4840
    // initialize final positions
4841
    for (i = 0, iMax = items.length; i < iMax; i++) {
4842
        var item = items[i],
4843
            top,
4844
            bottom,
4845
            height = item.height,
4846
            width = item.getWidth(this),
4847
            right = item.getRight(this),
4848
            left = right - width;
4849

    
4850
        if (axisOnTop) {
4851
            top = axisHeight + eventMarginAxis + eventMargin / 2;
4852
        }
4853
        else {
4854
            top = axisTop - height - eventMarginAxis - eventMargin / 2;
4855
        }
4856
        bottom = top + height;
4857

    
4858
        finalItems[i] = {
4859
            'left': left,
4860
            'top': top,
4861
            'right': right,
4862
            'bottom': bottom,
4863
            'height': height,
4864
            'item': item
4865
        };
4866
    }
4867

    
4868
    if (this.options.stackEvents) {
4869
        // calculate new, non-overlapping positions
4870
        //var items = sortedItems;
4871
        for (i = 0, iMax = finalItems.length; i < iMax; i++) {
4872
            //for (var i = finalItems.length - 1; i >= 0; i--) {
4873
            var finalItem = finalItems[i];
4874
            var collidingItem = null;
4875
            do {
4876
                // TODO: optimize checking for overlap. when there is a gap without items,
4877
                //  you only need to check for items from the next item on, not from zero
4878
                collidingItem = this.stackItemsCheckOverlap(finalItems, i, 0, i-1);
4879
                if (collidingItem != null) {
4880
                    // There is a collision. Reposition the event above the colliding element
4881
                    if (axisOnTop) {
4882
                        finalItem.top = collidingItem.top + collidingItem.height + eventMargin;
4883
                    }
4884
                    else {
4885
                        finalItem.top = collidingItem.top - finalItem.height - eventMargin;
4886
                    }
4887
                    finalItem.bottom = finalItem.top + finalItem.height;
4888
                }
4889
            } while (collidingItem);
4890
        }
4891
    }
4892

    
4893
    return finalItems;
4894
};
4895

    
4896

    
4897
/**
4898
 * Move the events one step in the direction of their final positions
4899
 * @param {Array} currentItems   Array with the real items and their current
4900
 *                               positions
4901
 * @param {Array} finalItems     Array with objects containing the final
4902
 *                               positions of the items
4903
 * @return {boolean} arrived     True if all items have reached their final
4904
 *                               location, else false
4905
 */
4906
links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) {
4907
    var arrived = true;
4908

    
4909
    // apply new positions animated
4910
    for (i = 0, iMax = finalItems.length; i < iMax; i++) {
4911
        var finalItem = finalItems[i],
4912
            item = finalItem.item;
4913

    
4914
        var topNow = parseInt(item.top);
4915
        var topFinal = parseInt(finalItem.top);
4916
        var diff = (topFinal - topNow);
4917
        if (diff) {
4918
            var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1);
4919
            if (Math.abs(diff) > 4) step = diff / 4;
4920
            var topNew = parseInt(topNow + step);
4921

    
4922
            if (topNew != topFinal) {
4923
                arrived = false;
4924
            }
4925

    
4926
            item.top = topNew;
4927
            item.bottom = item.top + item.height;
4928
        }
4929
        else {
4930
            item.top = finalItem.top;
4931
            item.bottom = finalItem.bottom;
4932
        }
4933

    
4934
        item.left = finalItem.left;
4935
        item.right = finalItem.right;
4936
    }
4937

    
4938
    return arrived;
4939
};
4940

    
4941

    
4942

    
4943
/**
4944
 * Move the events from their current position to the final position
4945
 * @param {Array} currentItems   Array with the real items and their current
4946
 *                               positions
4947
 * @param {Array} finalItems     Array with objects containing the final
4948
 *                               positions of the items
4949
 */
4950
links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) {
4951
    // Put the events directly at there final position
4952
    for (i = 0, iMax = finalItems.length; i < iMax; i++) {
4953
        var finalItem = finalItems[i],
4954
            current = finalItem.item;
4955

    
4956
        current.left = finalItem.left;
4957
        current.top = finalItem.top;
4958
        current.right = finalItem.right;
4959
        current.bottom = finalItem.bottom;
4960
    }
4961
};
4962

    
4963

    
4964

    
4965
/**
4966
 * Check if the destiny position of given item overlaps with any
4967
 * of the other items from index itemStart to itemEnd.
4968
 * @param {Array} items      Array with items
4969
 * @param {int}  itemIndex   Number of the item to be checked for overlap
4970
 * @param {int}  itemStart   First item to be checked.
4971
 * @param {int}  itemEnd     Last item to be checked.
4972
 * @return {Object}          colliding item, or undefined when no collisions
4973
 */
4974
links.Timeline.prototype.stackItemsCheckOverlap = function(items, itemIndex,
4975
                                                           itemStart, itemEnd) {
4976
    var eventMargin = this.options.eventMargin,
4977
        collision = this.collision;
4978

    
4979
    // we loop from end to start, as we suppose that the chance of a 
4980
    // collision is larger for items at the end, so check these first.
4981
    var item1 = items[itemIndex];
4982
    for (var i = itemEnd; i >= itemStart; i--) {
4983
        var item2 = items[i];
4984
        if (collision(item1, item2, eventMargin)) {
4985
            if (i != itemIndex) {
4986
                return item2;
4987
            }
4988
        }
4989
    }
4990

    
4991
    return undefined;
4992
};
4993

    
4994
/**
4995
 * Test if the two provided items collide
4996
 * The items must have parameters left, right, top, and bottom.
4997
 * @param {Element} item1       The first item
4998
 * @param {Element} item2       The second item
4999
 * @param {Number}              margin  A minimum required margin. Optional.
5000
 *                              If margin is provided, the two items will be
5001
 *                              marked colliding when they overlap or
5002
 *                              when the margin between the two is smaller than
5003
 *                              the requested margin.
5004
 * @return {boolean}            true if item1 and item2 collide, else false
5005
 */
5006
links.Timeline.prototype.collision = function(item1, item2, margin) {
5007
    // set margin if not specified 
5008
    if (margin == undefined) {
5009
        margin = 0;
5010
    }
5011

    
5012
    // calculate if there is overlap (collision)
5013
    return (item1.left - margin < item2.right &&
5014
        item1.right + margin > item2.left &&
5015
        item1.top - margin < item2.bottom &&
5016
        item1.bottom + margin > item2.top);
5017
};
5018

    
5019

    
5020
/**
5021
 * fire an event
5022
 * @param {String} event   The name of an event, for example "rangechange" or "edit"
5023
 */
5024
links.Timeline.prototype.trigger = function (event) {
5025
    // built up properties
5026
    var properties = null;
5027
    switch (event) {
5028
        case 'rangechange':
5029
        case 'rangechanged':
5030
            properties = {
5031
                'start': new Date(this.start.valueOf()),
5032
                'end': new Date(this.end.valueOf())
5033
            };
5034
            break;
5035

    
5036
        case 'timechange':
5037
        case 'timechanged':
5038
            properties = {
5039
                'time': new Date(this.customTime.valueOf())
5040
            };
5041
            break;
5042
    }
5043

    
5044
    // trigger the links event bus
5045
    links.events.trigger(this, event, properties);
5046

    
5047
    // trigger the google event bus
5048
    if (google && google.visualization) {
5049
        google.visualization.events.trigger(this, event, properties);
5050
    }
5051
};
5052

    
5053

    
5054
/**
5055
 * Cluster the events
5056
 */
5057
links.Timeline.prototype.clusterItems = function () {
5058
    if (!this.options.cluster) {
5059
        return;
5060
    }
5061

    
5062
    var clusters = this.clusterGenerator.getClusters(this.conversion.factor);
5063
    if (this.clusters != clusters) {
5064
        // cluster level changed
5065
        var queue = this.renderQueue;
5066

    
5067
        // remove the old clusters from the scene
5068
        if (this.clusters) {
5069
            this.clusters.forEach(function (cluster) {
5070
                queue.hide.push(cluster);
5071

    
5072
                // unlink the items
5073
                cluster.items.forEach(function (item) {
5074
                    item.cluster = undefined;
5075
                });
5076
            });
5077
        }
5078

    
5079
        // append the new clusters
5080
        clusters.forEach(function (cluster) {
5081
            // don't add to the queue.show here, will be done in .filterItems()
5082

    
5083
            // link all items to the cluster
5084
            cluster.items.forEach(function (item) {
5085
                item.cluster = cluster;
5086
            });
5087
        });
5088

    
5089
        this.clusters = clusters;
5090
    }
5091
};
5092

    
5093
/**
5094
 * Filter the visible events
5095
 */
5096
links.Timeline.prototype.filterItems = function () {
5097
    var queue = this.renderQueue,
5098
        window = (this.end - this.start),
5099
        start = new Date(this.start.valueOf() - window),
5100
        end = new Date(this.end.valueOf() + window);
5101

    
5102
    function filter (arr) {
5103
        arr.forEach(function (item) {
5104
            var rendered = item.rendered;
5105
            var visible = item.isVisible(start, end);
5106
            if (rendered != visible) {
5107
                if (rendered) {
5108
                    queue.hide.push(item); // item is rendered but no longer visible
5109
                }
5110
                if (visible && (queue.show.indexOf(item) == -1)) {
5111
                    queue.show.push(item); // item is visible but neither rendered nor queued up to be rendered
5112
                }
5113
            }
5114
        });
5115
    }
5116

    
5117
    // filter all items and all clusters
5118
    filter(this.items);
5119
    if (this.clusters) {
5120
        filter(this.clusters);
5121
    }
5122
};
5123

    
5124
/** ------------------------------------------------------------------------ **/
5125

    
5126
/**
5127
 * @constructor links.Timeline.ClusterGenerator
5128
 * Generator which creates clusters of items, based on the visible range in
5129
 * the Timeline. There is a set of cluster levels which is cached.
5130
 * @param {links.Timeline} timeline
5131
 */
5132
links.Timeline.ClusterGenerator = function (timeline) {
5133
    this.timeline = timeline;
5134
    this.clear();
5135
};
5136

    
5137
/**
5138
 * Clear all cached clusters and data, and initialize all variables
5139
 */
5140
links.Timeline.ClusterGenerator.prototype.clear = function () {
5141
    // cache containing created clusters for each cluster level
5142
    this.items = [];
5143
    this.groups = {};
5144
    this.clearCache();
5145
};
5146

    
5147
/**
5148
 * Clear the cached clusters
5149
 */
5150
links.Timeline.ClusterGenerator.prototype.clearCache = function () {
5151
    // cache containing created clusters for each cluster level
5152
    this.cache = {};
5153
    this.cacheLevel = -1;
5154
    this.cache[this.cacheLevel] = [];
5155
};
5156

    
5157
/**
5158
 * Set the items to be clustered.
5159
 * This will clear cached clusters.
5160
 * @param {Item[]} items
5161
 * @param {Object} [options]  Available options:
5162
 *                            {boolean} applyOnChangedLevel
5163
 *                                If true (default), the changed data is applied
5164
 *                                as soon the cluster level changes. If false,
5165
 *                                The changed data is applied immediately
5166
 */
5167
links.Timeline.ClusterGenerator.prototype.setData = function (items, options) {
5168
    this.items = items || [];
5169
    this.dataChanged = true;
5170
    this.applyOnChangedLevel = true;
5171
    if (options && options.applyOnChangedLevel) {
5172
        this.applyOnChangedLevel = options.applyOnChangedLevel;
5173
    }
5174
    // console.log('clustergenerator setData applyOnChangedLevel=' + this.applyOnChangedLevel); // TODO: cleanup
5175
};
5176

    
5177
/**
5178
 * Update the current data set: clear cache, and recalculate the clustering for
5179
 * the current level
5180
 */
5181
links.Timeline.ClusterGenerator.prototype.updateData = function () {
5182
    this.dataChanged = true;
5183
    this.applyOnChangedLevel = false;
5184
};
5185

    
5186
/**
5187
 * Filter the items per group.
5188
 * @private
5189
 */
5190
links.Timeline.ClusterGenerator.prototype.filterData = function () {
5191
    // filter per group
5192
    var items = this.items || [];
5193
    var groups = {};
5194
    this.groups = groups;
5195

    
5196
    // split the items per group
5197
    items.forEach(function (item) {
5198
        // put the item in the correct group
5199
        var groupName = item.group ? item.group.content : '';
5200
        var group = groups[groupName];
5201
        if (!group) {
5202
            group = [];
5203
            groups[groupName] = group;
5204
        }
5205
        group.push(item);
5206

    
5207
        // calculate the center of the item
5208
        if (item.start) {
5209
            if (item.end) {
5210
                // range
5211
                item.center = (item.start.valueOf() + item.end.valueOf()) / 2;
5212
            }
5213
            else {
5214
                // box, dot
5215
                item.center = item.start.valueOf();
5216
            }
5217
        }
5218
    });
5219

    
5220
    // sort the items per group
5221
    for (var groupName in groups) {
5222
        if (groups.hasOwnProperty(groupName)) {
5223
            groups[groupName].sort(function (a, b) {
5224
                return (a.center - b.center);
5225
            });
5226
        }
5227
    }
5228

    
5229
    this.dataChanged = false;
5230
};
5231

    
5232
/**
5233
 * Cluster the events which are too close together
5234
 * @param {Number} scale     The scale of the current window,
5235
 *                           defined as (windowWidth / (endDate - startDate))
5236
 * @return {Item[]} clusters
5237
 */
5238
links.Timeline.ClusterGenerator.prototype.getClusters = function (scale) {
5239
    var level = -1,
5240
        granularity = 2, // TODO: what granularity is needed for the cluster levels?
5241
        timeWindow = 0,  // milliseconds
5242
        maxItems = 5;    // TODO: do not hard code maxItems
5243

    
5244
    if (scale > 0) {
5245
        level = Math.round(Math.log(100 / scale) / Math.log(granularity));
5246
        timeWindow = Math.pow(granularity, level);
5247

    
5248
        // groups must have a larger time window, as the items will not be stacked
5249
        if (this.timeline.groups && this.timeline.groups.length) {
5250
            timeWindow *= 4;
5251
        }
5252
    }
5253

    
5254
    // clear the cache when and re-filter the data when needed.
5255
    if (this.dataChanged) {
5256
        var levelChanged = (level != this.cacheLevel);
5257
        var applyDataNow = this.applyOnChangedLevel ? levelChanged : true;
5258
        if (applyDataNow) {
5259
            // TODO: currently drawn clusters should be removed! mark them as invisible?
5260
            this.clearCache();
5261
            this.filterData();
5262
            // console.log('clustergenerator: cache cleared...'); // TODO: cleanup
5263
        }
5264
    }
5265

    
5266
    this.cacheLevel = level;
5267
    var clusters = this.cache[level];
5268
    if (!clusters) {
5269
        // console.log('clustergenerator: create cluster level ' + level); // TODO: cleanup
5270
        clusters = [];
5271

    
5272
        // TODO: spit this method, it is too large
5273
        for (var groupName in this.groups) {
5274
            if (this.groups.hasOwnProperty(groupName)) {
5275
                var items = this.groups[groupName];
5276
                var iMax = items.length;
5277
                var i = 0;
5278
                while (i < iMax) {
5279
                    // find all items around current item, within the timeWindow
5280
                    var item = items[i];
5281
                    var neighbors = 1;  // start at 1, to include itself)
5282

    
5283
                    // loop through items left from the current item
5284
                    var j = i - 1;
5285
                    while (j >= 0 && (item.center - items[j].center) < timeWindow / 2) {
5286
                        if (!items[j].cluster) {
5287
                            neighbors++;
5288
                        }
5289
                        j--;
5290
                    }
5291

    
5292
                    // loop through items right from the current item
5293
                    var k = i + 1;
5294
                    while (k < items.length && (items[k].center - item.center) < timeWindow / 2) {
5295
                        neighbors++;
5296
                        k++;
5297
                    }
5298

    
5299
                    // loop through the created clusters
5300
                    var l = clusters.length - 1;
5301
                    while (l >= 0 && (item.center - clusters[l].center) < timeWindow / 2) {
5302
                        if (item.group == clusters[l].group) {
5303
                            neighbors++;
5304
                        }
5305
                        l--;
5306
                    }
5307

    
5308
                    // aggregate until the number of items is within maxItems
5309
                    if (neighbors > maxItems) {
5310
                        // too busy in this window.
5311
                        var num = neighbors - maxItems + 1;
5312
                        var clusterItems = [];
5313

    
5314
                        // append the items to the cluster,
5315
                        // and calculate the average start for the cluster
5316
                        var avg = undefined;  // number. average of all start dates
5317
                        var min = undefined;  // number. minimum of all start dates
5318
                        var max = undefined;  // number. maximum of all start and end dates
5319
                        var containsRanges = false;
5320
                        var count = 0;
5321
                        var m = i;
5322
                        while (clusterItems.length < num && m < items.length) {
5323
                            var p = items[m];
5324
                            var start = p.start.valueOf();
5325
                            var end = p.end ? p.end.valueOf() : p.start.valueOf();
5326
                            clusterItems.push(p);
5327
                            if (count) {
5328
                                // calculate new average (use fractions to prevent overflow)
5329
                                avg = (count / (count + 1)) * avg + (1 / (count + 1)) * p.center;
5330
                            }
5331
                            else {
5332
                                avg = p.center;
5333
                            }
5334
                            min = (min != undefined) ? Math.min(min, start) : start;
5335
                            max = (max != undefined) ? Math.max(max, end) : end;
5336
                            containsRanges = containsRanges || (p instanceof links.Timeline.ItemRange);
5337
                            count++;
5338
                            m++;
5339
                        }
5340

    
5341
                        var cluster;
5342
                        var title = 'Cluster containing ' + count +
5343
                            ' events. Zoom in to see the individual events.';
5344
                        var content = '<div title="' + title + '">' + count + ' events</div>';
5345
                        var group = item.group ? item.group.content : undefined;
5346
                        if (containsRanges) {
5347
                            // boxes and/or ranges
5348
                            cluster = this.timeline.createItem({
5349
                                'start': new Date(min),
5350
                                'end': new Date(max),
5351
                                'content': content,
5352
                                'group': group
5353
                            });
5354
                        }
5355
                        else {
5356
                            // boxes only
5357
                            cluster = this.timeline.createItem({
5358
                                'start': new Date(avg),
5359
                                'content': content,
5360
                                'group': group
5361
                            });
5362
                        }
5363
                        cluster.isCluster = true;
5364
                        cluster.items = clusterItems;
5365
                        cluster.items.forEach(function (item) {
5366
                            item.cluster = cluster;
5367
                        });
5368

    
5369
                        clusters.push(cluster);
5370
                        i += num;
5371
                    }
5372
                    else {
5373
                        delete item.cluster;
5374
                        i += 1;
5375
                    }
5376
                }
5377
            }
5378
        }
5379

    
5380
        this.cache[level] = clusters;
5381
    }
5382

    
5383
    return clusters;
5384
};
5385

    
5386

    
5387
/** ------------------------------------------------------------------------ **/
5388

    
5389

    
5390
/**
5391
 * Event listener (singleton)
5392
 */
5393
links.events = links.events || {
5394
    'listeners': [],
5395

    
5396
    /**
5397
     * Find a single listener by its object
5398
     * @param {Object} object
5399
     * @return {Number} index  -1 when not found
5400
     */
5401
    'indexOf': function (object) {
5402
        var listeners = this.listeners;
5403
        for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
5404
            var listener = listeners[i];
5405
            if (listener && listener.object == object) {
5406
                return i;
5407
            }
5408
        }
5409
        return -1;
5410
    },
5411

    
5412
    /**
5413
     * Add an event listener
5414
     * @param {Object} object
5415
     * @param {String} event       The name of an event, for example 'select'
5416
     * @param {function} callback  The callback method, called when the
5417
     *                             event takes place
5418
     */
5419
    'addListener': function (object, event, callback) {
5420
        var index = this.indexOf(object);
5421
        var listener = this.listeners[index];
5422
        if (!listener) {
5423
            listener = {
5424
                'object': object,
5425
                'events': {}
5426
            };
5427
            this.listeners.push(listener);
5428
        }
5429

    
5430
        var callbacks = listener.events[event];
5431
        if (!callbacks) {
5432
            callbacks = [];
5433
            listener.events[event] = callbacks;
5434
        }
5435

    
5436
        // add the callback if it does not yet exist
5437
        if (callbacks.indexOf(callback) == -1) {
5438
            callbacks.push(callback);
5439
        }
5440
    },
5441

    
5442
    /**
5443
     * Remove an event listener
5444
     * @param {Object} object
5445
     * @param {String} event       The name of an event, for example 'select'
5446
     * @param {function} callback  The registered callback method
5447
     */
5448
    'removeListener': function (object, event, callback) {
5449
        var index = this.indexOf(object);
5450
        var listener = this.listeners[index];
5451
        if (listener) {
5452
            var callbacks = listener.events[event];
5453
            if (callbacks) {
5454
                var index = callbacks.indexOf(callback);
5455
                if (index != -1) {
5456
                    callbacks.splice(index, 1);
5457
                }
5458

    
5459
                // remove the array when empty
5460
                if (callbacks.length == 0) {
5461
                    delete listener.events[event];
5462
                }
5463
            }
5464

    
5465
            // count the number of registered events. remove listener when empty
5466
            var count = 0;
5467
            var events = listener.events;
5468
            for (var e in events) {
5469
                if (events.hasOwnProperty(e)) {
5470
                    count++;
5471
                }
5472
            }
5473
            if (count == 0) {
5474
                delete this.listeners[index];
5475
            }
5476
        }
5477
    },
5478

    
5479
    /**
5480
     * Remove all registered event listeners
5481
     */
5482
    'removeAllListeners': function () {
5483
        this.listeners = [];
5484
    },
5485

    
5486
    /**
5487
     * Trigger an event. All registered event handlers will be called
5488
     * @param {Object} object
5489
     * @param {String} event
5490
     * @param {Object} properties (optional)
5491
     */
5492
    'trigger': function (object, event, properties) {
5493
        var index = this.indexOf(object);
5494
        var listener = this.listeners[index];
5495
        if (listener) {
5496
            var callbacks = listener.events[event];
5497
            if (callbacks) {
5498
                for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
5499
                    callbacks[i](properties);
5500
                }
5501
            }
5502
        }
5503
    }
5504
};
5505

    
5506

    
5507
/** ------------------------------------------------------------------------ **/
5508

    
5509
/**
5510
 * @constructor  links.Timeline.StepDate
5511
 * The class StepDate is an iterator for dates. You provide a start date and an
5512
 * end date. The class itself determines the best scale (step size) based on the
5513
 * provided start Date, end Date, and minimumStep.
5514
 *
5515
 * If minimumStep is provided, the step size is chosen as close as possible
5516
 * to the minimumStep but larger than minimumStep. If minimumStep is not
5517
 * provided, the scale is set to 1 DAY.
5518
 * The minimumStep should correspond with the onscreen size of about 6 characters
5519
 *
5520
 * Alternatively, you can set a scale by hand.
5521
 * After creation, you can initialize the class by executing start(). Then you
5522
 * can iterate from the start date to the end date via next(). You can check if
5523
 * the end date is reached with the function end(). After each step, you can
5524
 * retrieve the current date via get().
5525
 * The class step has scales ranging from milliseconds, seconds, minutes, hours,
5526
 * days, to years.
5527
 *
5528
 * Version: 1.2
5529
 *
5530
 * @param {Date} start          The start date, for example new Date(2010, 9, 21)
5531
 *                              or new Date(2010, 9, 21, 23, 45, 00)
5532
 * @param {Date} end            The end date
5533
 * @param {Number}  minimumStep Optional. Minimum step size in milliseconds
5534
 */
5535
links.Timeline.StepDate = function(start, end, minimumStep) {
5536

    
5537
    // variables
5538
    this.current = new Date();
5539
    this._start = new Date();
5540
    this._end = new Date();
5541

    
5542
    this.autoScale  = true;
5543
    this.scale = links.Timeline.StepDate.SCALE.DAY;
5544
    this.step = 1;
5545

    
5546
    // initialize the range
5547
    this.setRange(start, end, minimumStep);
5548
};
5549

    
5550
/// enum scale
5551
links.Timeline.StepDate.SCALE = {
5552
    MILLISECOND: 1,
5553
    SECOND: 2,
5554
    MINUTE: 3,
5555
    HOUR: 4,
5556
    DAY: 5,
5557
    WEEKDAY: 6,
5558
    MONTH: 7,
5559
    YEAR: 8
5560
};
5561

    
5562

    
5563
/**
5564
 * Set a new range
5565
 * If minimumStep is provided, the step size is chosen as close as possible
5566
 * to the minimumStep but larger than minimumStep. If minimumStep is not
5567
 * provided, the scale is set to 1 DAY.
5568
 * The minimumStep should correspond with the onscreen size of about 6 characters
5569
 * @param {Date} start        The start date and time.
5570
 * @param {Date} end          The end date and time.
5571
 * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
5572
 */
5573
links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) {
5574
    if (!(start instanceof Date) || !(end instanceof Date)) {
5575
        //throw  "No legal start or end date in method setRange";
5576
        return;
5577
    }
5578

    
5579
    this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
5580
    this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
5581

    
5582
    if (this.autoScale) {
5583
        this.setMinimumStep(minimumStep);
5584
    }
5585
};
5586

    
5587
/**
5588
 * Set the step iterator to the start date.
5589
 */
5590
links.Timeline.StepDate.prototype.start = function() {
5591
    this.current = new Date(this._start.valueOf());
5592
    this.roundToMinor();
5593
};
5594

    
5595
/**
5596
 * Round the current date to the first minor date value
5597
 * This must be executed once when the current date is set to start Date
5598
 */
5599
links.Timeline.StepDate.prototype.roundToMinor = function() {
5600
    // round to floor
5601
    // IMPORTANT: we have no breaks in this switch! (this is no bug)
5602
    //noinspection FallthroughInSwitchStatementJS
5603
    switch (this.scale) {
5604
        case links.Timeline.StepDate.SCALE.YEAR:
5605
            this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
5606
            this.current.setMonth(0);
5607
        case links.Timeline.StepDate.SCALE.MONTH:        this.current.setDate(1);
5608
        case links.Timeline.StepDate.SCALE.DAY:          // intentional fall through
5609
        case links.Timeline.StepDate.SCALE.WEEKDAY:      this.current.setHours(0);
5610
        case links.Timeline.StepDate.SCALE.HOUR:         this.current.setMinutes(0);
5611
        case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setSeconds(0);
5612
        case links.Timeline.StepDate.SCALE.SECOND:       this.current.setMilliseconds(0);
5613
        //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
5614
    }
5615

    
5616
    if (this.step != 1) {
5617
        // round down to the first minor value that is a multiple of the current step size
5618
        switch (this.scale) {
5619
            case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);  break;
5620
            case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
5621
            case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
5622
            case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
5623
            case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
5624
            case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
5625
            case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);  break;
5626
            case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
5627
            default: break;
5628
        }
5629
    }
5630
};
5631

    
5632
/**
5633
 * Check if the end date is reached
5634
 * @return {boolean}  true if the current date has passed the end date
5635
 */
5636
links.Timeline.StepDate.prototype.end = function () {
5637
    return (this.current.valueOf() > this._end.valueOf());
5638
};
5639

    
5640
/**
5641
 * Do the next step
5642
 */
5643
links.Timeline.StepDate.prototype.next = function() {
5644
    var prev = this.current.valueOf();
5645

    
5646
    // Two cases, needed to prevent issues with switching daylight savings 
5647
    // (end of March and end of October)
5648
    if (this.current.getMonth() < 6)   {
5649
        switch (this.scale) {
5650
            case links.Timeline.StepDate.SCALE.MILLISECOND:
5651

    
5652
                this.current = new Date(this.current.valueOf() + this.step); break;
5653
            case links.Timeline.StepDate.SCALE.SECOND:       this.current = new Date(this.current.valueOf() + this.step * 1000); break;
5654
            case links.Timeline.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
5655
            case links.Timeline.StepDate.SCALE.HOUR:
5656
                this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
5657
                // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
5658
                var h = this.current.getHours();
5659
                this.current.setHours(h - (h % this.step));
5660
                break;
5661
            case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
5662
            case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
5663
            case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
5664
            case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
5665
            default:                      break;
5666
        }
5667
    }
5668
    else {
5669
        switch (this.scale) {
5670
            case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current = new Date(this.current.valueOf() + this.step); break;
5671
            case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() + this.step); break;
5672
            case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() + this.step); break;
5673
            case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() + this.step); break;
5674
            case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
5675
            case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
5676
            case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
5677
            case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
5678
            default:                      break;
5679
        }
5680
    }
5681

    
5682
    if (this.step != 1) {
5683
        // round down to the correct major value
5684
        switch (this.scale) {
5685
            case links.Timeline.StepDate.SCALE.MILLISECOND:  if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);  break;
5686
            case links.Timeline.StepDate.SCALE.SECOND:       if(this.current.getSeconds() < this.step) this.current.setSeconds(0);  break;
5687
            case links.Timeline.StepDate.SCALE.MINUTE:       if(this.current.getMinutes() < this.step) this.current.setMinutes(0);  break;
5688
            case links.Timeline.StepDate.SCALE.HOUR:         if(this.current.getHours() < this.step) this.current.setHours(0);  break;
5689
            case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
5690
            case links.Timeline.StepDate.SCALE.DAY:          if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
5691
            case links.Timeline.StepDate.SCALE.MONTH:        if(this.current.getMonth() < this.step) this.current.setMonth(0);  break;
5692
            case links.Timeline.StepDate.SCALE.YEAR:         break; // nothing to do for year
5693
            default:                break;
5694
        }
5695
    }
5696

    
5697
    // safety mechanism: if current time is still unchanged, move to the end
5698
    if (this.current.valueOf() == prev) {
5699
        this.current = new Date(this._end.valueOf());
5700
    }
5701
};
5702

    
5703

    
5704
/**
5705
 * Get the current datetime
5706
 * @return {Date}  current The current date
5707
 */
5708
links.Timeline.StepDate.prototype.getCurrent = function() {
5709
    return this.current;
5710
};
5711

    
5712
/**
5713
 * Set a custom scale. Autoscaling will be disabled.
5714
 * For example setScale(SCALE.MINUTES, 5) will result
5715
 * in minor steps of 5 minutes, and major steps of an hour.
5716
 *
5717
 * @param {links.Timeline.StepDate.SCALE} newScale
5718
 *                               A scale. Choose from SCALE.MILLISECOND,
5719
 *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
5720
 *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
5721
 *                               SCALE.YEAR.
5722
 * @param {Number}     newStep   A step size, by default 1. Choose for
5723
 *                               example 1, 2, 5, or 10.
5724
 */
5725
links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) {
5726
    this.scale = newScale;
5727

    
5728
    if (newStep > 0) {
5729
        this.step = newStep;
5730
    }
5731

    
5732
    this.autoScale = false;
5733
};
5734

    
5735
/**
5736
 * Enable or disable autoscaling
5737
 * @param {boolean} enable  If true, autoascaling is set true
5738
 */
5739
links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
5740
    this.autoScale = enable;
5741
};
5742

    
5743

    
5744
/**
5745
 * Automatically determine the scale that bests fits the provided minimum step
5746
 * @param {Number} minimumStep  The minimum step size in milliseconds
5747
 */
5748
links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) {
5749
    if (minimumStep == undefined) {
5750
        return;
5751
    }
5752

    
5753
    var stepYear       = (1000 * 60 * 60 * 24 * 30 * 12);
5754
    var stepMonth      = (1000 * 60 * 60 * 24 * 30);
5755
    var stepDay        = (1000 * 60 * 60 * 24);
5756
    var stepHour       = (1000 * 60 * 60);
5757
    var stepMinute     = (1000 * 60);
5758
    var stepSecond     = (1000);
5759
    var stepMillisecond= (1);
5760

    
5761
    // find the smallest step that is larger than the provided minimumStep
5762
    if (stepYear*1000 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1000;}
5763
    if (stepYear*500 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 500;}
5764
    if (stepYear*100 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 100;}
5765
    if (stepYear*50 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 50;}
5766
    if (stepYear*10 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 10;}
5767
    if (stepYear*5 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 5;}
5768
    if (stepYear > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1;}
5769
    if (stepMonth*3 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 3;}
5770
    if (stepMonth > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 1;}
5771
    if (stepDay*5 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 5;}
5772
    if (stepDay*2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 2;}
5773
    if (stepDay > minimumStep)              {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 1;}
5774
    if (stepDay/2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.WEEKDAY;     this.step = 1;}
5775
    if (stepHour*4 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 4;}
5776
    if (stepHour > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 1;}
5777
    if (stepMinute*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 15;}
5778
    if (stepMinute*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 10;}
5779
    if (stepMinute*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 5;}
5780
    if (stepMinute > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 1;}
5781
    if (stepSecond*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 15;}
5782
    if (stepSecond*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 10;}
5783
    if (stepSecond*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 5;}
5784
    if (stepSecond > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 1;}
5785
    if (stepMillisecond*200 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
5786
    if (stepMillisecond*100 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
5787
    if (stepMillisecond*50 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
5788
    if (stepMillisecond*10 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
5789
    if (stepMillisecond*5 > minimumStep)    {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
5790
    if (stepMillisecond > minimumStep)      {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
5791
};
5792

    
5793
/**
5794
 * Snap a date to a rounded value. The snap intervals are dependent on the
5795
 * current scale and step.
5796
 * @param {Date} date   the date to be snapped
5797
 */
5798
links.Timeline.StepDate.prototype.snap = function(date) {
5799
    if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
5800
        var year = date.getFullYear() + Math.round(date.getMonth() / 12);
5801
        date.setFullYear(Math.round(year / this.step) * this.step);
5802
        date.setMonth(0);
5803
        date.setDate(0);
5804
        date.setHours(0);
5805
        date.setMinutes(0);
5806
        date.setSeconds(0);
5807
        date.setMilliseconds(0);
5808
    }
5809
    else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
5810
        if (date.getDate() > 15) {
5811
            date.setDate(1);
5812
            date.setMonth(date.getMonth() + 1);
5813
            // important: first set Date to 1, after that change the month.      
5814
        }
5815
        else {
5816
            date.setDate(1);
5817
        }
5818

    
5819
        date.setHours(0);
5820
        date.setMinutes(0);
5821
        date.setSeconds(0);
5822
        date.setMilliseconds(0);
5823
    }
5824
    else if (this.scale == links.Timeline.StepDate.SCALE.DAY ||
5825
        this.scale == links.Timeline.StepDate.SCALE.WEEKDAY) {
5826
        switch (this.step) {
5827
            case 5:
5828
            case 2:
5829
                date.setHours(Math.round(date.getHours() / 24) * 24); break;
5830
            default:
5831
                date.setHours(Math.round(date.getHours() / 12) * 12); break;
5832
        }
5833
        date.setMinutes(0);
5834
        date.setSeconds(0);
5835
        date.setMilliseconds(0);
5836
    }
5837
    else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
5838
        switch (this.step) {
5839
            case 4:
5840
                date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
5841
            default:
5842
                date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
5843
        }
5844
        date.setSeconds(0);
5845
        date.setMilliseconds(0);
5846
    } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
5847
        switch (this.step) {
5848
            case 15:
5849
            case 10:
5850
                date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
5851
                date.setSeconds(0);
5852
                break;
5853
            case 5:
5854
                date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
5855
            default:
5856
                date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
5857
        }
5858
        date.setMilliseconds(0);
5859
    }
5860
    else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
5861
        switch (this.step) {
5862
            case 15:
5863
            case 10:
5864
                date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
5865
                date.setMilliseconds(0);
5866
                break;
5867
            case 5:
5868
                date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
5869
            default:
5870
                date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
5871
        }
5872
    }
5873
    else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
5874
        var step = this.step > 5 ? this.step / 2 : 1;
5875
        date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
5876
    }
5877
};
5878

    
5879
/**
5880
 * Check if the current step is a major step (for example when the step
5881
 * is DAY, a major step is each first day of the MONTH)
5882
 * @return {boolean} true if current date is major, else false.
5883
 */
5884
links.Timeline.StepDate.prototype.isMajor = function() {
5885
    switch (this.scale) {
5886
        case links.Timeline.StepDate.SCALE.MILLISECOND:
5887
            return (this.current.getMilliseconds() == 0);
5888
        case links.Timeline.StepDate.SCALE.SECOND:
5889
            return (this.current.getSeconds() == 0);
5890
        case links.Timeline.StepDate.SCALE.MINUTE:
5891
            return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
5892
        // Note: this is no bug. Major label is equal for both minute and hour scale
5893
        case links.Timeline.StepDate.SCALE.HOUR:
5894
            return (this.current.getHours() == 0);
5895
        case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through
5896
        case links.Timeline.StepDate.SCALE.DAY:
5897
            return (this.current.getDate() == 1);
5898
        case links.Timeline.StepDate.SCALE.MONTH:
5899
            return (this.current.getMonth() == 0);
5900
        case links.Timeline.StepDate.SCALE.YEAR:
5901
            return false;
5902
        default:
5903
            return false;
5904
    }
5905
};
5906

    
5907

    
5908
/**
5909
 * Returns formatted text for the minor axislabel, depending on the current
5910
 * date and the scale. For example when scale is MINUTE, the current time is
5911
 * formatted as "hh:mm".
5912
 * @param {Object} options
5913
 * @param {Date} [date] custom date. if not provided, current date is taken
5914
 */
5915
links.Timeline.StepDate.prototype.getLabelMinor = function(options, date) {
5916
    if (date == undefined) {
5917
        date = this.current;
5918
    }
5919

    
5920
    switch (this.scale) {
5921
        case links.Timeline.StepDate.SCALE.MILLISECOND:  return String(date.getMilliseconds());
5922
        case links.Timeline.StepDate.SCALE.SECOND:       return String(date.getSeconds());
5923
        case links.Timeline.StepDate.SCALE.MINUTE:
5924
            return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2);
5925
        case links.Timeline.StepDate.SCALE.HOUR:
5926
            return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2);
5927
        case links.Timeline.StepDate.SCALE.WEEKDAY:      return options.DAYS_SHORT[date.getDay()] + ' ' + date.getDate();
5928
        case links.Timeline.StepDate.SCALE.DAY:          return String(date.getDate());
5929
        case links.Timeline.StepDate.SCALE.MONTH:        return options.MONTHS_SHORT[date.getMonth()];   // month is zero based
5930
        case links.Timeline.StepDate.SCALE.YEAR:         return String(date.getFullYear());
5931
        default:                                         return "";
5932
    }
5933
};
5934

    
5935

    
5936
/**
5937
 * Returns formatted text for the major axislabel, depending on the current
5938
 * date and the scale. For example when scale is MINUTE, the major scale is
5939
 * hours, and the hour will be formatted as "hh".
5940
 * @param {Object} options
5941
 * @param {Date} [date] custom date. if not provided, current date is taken
5942
 */
5943
links.Timeline.StepDate.prototype.getLabelMajor = function(options, date) {
5944
    if (date == undefined) {
5945
        date = this.current;
5946
    }
5947

    
5948
    switch (this.scale) {
5949
        case links.Timeline.StepDate.SCALE.MILLISECOND:
5950
            return  this.addZeros(date.getHours(), 2) + ":" +
5951
                this.addZeros(date.getMinutes(), 2) + ":" +
5952
                this.addZeros(date.getSeconds(), 2);
5953
        case links.Timeline.StepDate.SCALE.SECOND:
5954
            return  date.getDate() + " " +
5955
                options.MONTHS[date.getMonth()] + " " +
5956
                this.addZeros(date.getHours(), 2) + ":" +
5957
                this.addZeros(date.getMinutes(), 2);
5958
        case links.Timeline.StepDate.SCALE.MINUTE:
5959
            return  options.DAYS[date.getDay()] + " " +
5960
                date.getDate() + " " +
5961
                options.MONTHS[date.getMonth()] + " " +
5962
                date.getFullYear();
5963
        case links.Timeline.StepDate.SCALE.HOUR:
5964
            return  options.DAYS[date.getDay()] + " " +
5965
                date.getDate() + " " +
5966
                options.MONTHS[date.getMonth()] + " " +
5967
                date.getFullYear();
5968
        case links.Timeline.StepDate.SCALE.WEEKDAY:
5969
        case links.Timeline.StepDate.SCALE.DAY:
5970
            return  options.MONTHS[date.getMonth()] + " " +
5971
                date.getFullYear();
5972
        case links.Timeline.StepDate.SCALE.MONTH:
5973
            return String(date.getFullYear());
5974
        default:
5975
            return "";
5976
    }
5977
};
5978

    
5979
/**
5980
 * Add leading zeros to the given value to match the desired length.
5981
 * For example addZeros(123, 5) returns "00123"
5982
 * @param {int} value   A value
5983
 * @param {int} len     Desired final length
5984
 * @return {string}     value with leading zeros
5985
 */
5986
links.Timeline.StepDate.prototype.addZeros = function(value, len) {
5987
    var str = "" + value;
5988
    while (str.length < len) {
5989
        str = "0" + str;
5990
    }
5991
    return str;
5992
};
5993

    
5994

    
5995

    
5996
/** ------------------------------------------------------------------------ **/
5997

    
5998
/**
5999
 * Image Loader service.
6000
 * can be used to get a callback when a certain image is loaded
6001
 *
6002
 */
6003
links.imageloader = (function () {
6004
    var urls = {};  // the loaded urls
6005
    var callbacks = {}; // the urls currently being loaded. Each key contains 
6006
    // an array with callbacks
6007

    
6008
    /**
6009
     * Check if an image url is loaded
6010
     * @param {String} url
6011
     * @return {boolean} loaded   True when loaded, false when not loaded
6012
     *                            or when being loaded
6013
     */
6014
    function isLoaded (url) {
6015
        if (urls[url] == true) {
6016
            return true;
6017
        }
6018

    
6019
        var image = new Image();
6020
        image.src = url;
6021
        if (image.complete) {
6022
            return true;
6023
        }
6024

    
6025
        return false;
6026
    }
6027

    
6028
    /**
6029
     * Check if an image url is being loaded
6030
     * @param {String} url
6031
     * @return {boolean} loading   True when being loaded, false when not loading
6032
     *                             or when already loaded
6033
     */
6034
    function isLoading (url) {
6035
        return (callbacks[url] != undefined);
6036
    }
6037

    
6038
    /**
6039
     * Load given image url
6040
     * @param {String} url
6041
     * @param {function} callback
6042
     * @param {boolean} sendCallbackWhenAlreadyLoaded  optional
6043
     */
6044
    function load (url, callback, sendCallbackWhenAlreadyLoaded) {
6045
        if (sendCallbackWhenAlreadyLoaded == undefined) {
6046
            sendCallbackWhenAlreadyLoaded = true;
6047
        }
6048

    
6049
        if (isLoaded(url)) {
6050
            if (sendCallbackWhenAlreadyLoaded) {
6051
                callback(url);
6052
            }
6053
            return;
6054
        }
6055

    
6056
        if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) {
6057
            return;
6058
        }
6059

    
6060
        var c = callbacks[url];
6061
        if (!c) {
6062
            var image = new Image();
6063
            image.src = url;
6064

    
6065
            c = [];
6066
            callbacks[url] = c;
6067

    
6068
            image.onload = function (event) {
6069
                urls[url] = true;
6070
                delete callbacks[url];
6071

    
6072
                for (var i = 0; i < c.length; i++) {
6073
                    c[i](url);
6074
                }
6075
            }
6076
        }
6077

    
6078
        if (c.indexOf(callback) == -1) {
6079
            c.push(callback);
6080
        }
6081
    }
6082

    
6083
    /**
6084
     * Load a set of images, and send a callback as soon as all images are
6085
     * loaded
6086
     * @param {String[]} urls
6087
     * @param {function } callback
6088
     * @param {boolean} sendCallbackWhenAlreadyLoaded
6089
     */
6090
    function loadAll (urls, callback, sendCallbackWhenAlreadyLoaded) {
6091
        // list all urls which are not yet loaded
6092
        var urlsLeft = [];
6093
        urls.forEach(function (url) {
6094
            if (!isLoaded(url)) {
6095
                urlsLeft.push(url);
6096
            }
6097
        });
6098

    
6099
        if (urlsLeft.length) {
6100
            // there are unloaded images
6101
            var countLeft = urlsLeft.length;
6102
            urlsLeft.forEach(function (url) {
6103
                load(url, function () {
6104
                    countLeft--;
6105
                    if (countLeft == 0) {
6106
                        // done!
6107
                        callback();
6108
                    }
6109
                }, sendCallbackWhenAlreadyLoaded);
6110
            });
6111
        }
6112
        else {
6113
            // we are already done!
6114
            if (sendCallbackWhenAlreadyLoaded) {
6115
                callback();
6116
            }
6117
        }
6118
    }
6119

    
6120
    /**
6121
     * Recursively retrieve all image urls from the images located inside a given
6122
     * HTML element
6123
     * @param {Node} elem
6124
     * @param {String[]} urls   Urls will be added here (no duplicates)
6125
     */
6126
    function filterImageUrls (elem, urls) {
6127
        var child = elem.firstChild;
6128
        while (child) {
6129
            if (child.tagName == 'IMG') {
6130
                var url = child.src;
6131
                if (urls.indexOf(url) == -1) {
6132
                    urls.push(url);
6133
                }
6134
            }
6135

    
6136
            filterImageUrls(child, urls);
6137

    
6138
            child = child.nextSibling;
6139
        }
6140
    }
6141

    
6142
    return {
6143
        'isLoaded': isLoaded,
6144
        'isLoading': isLoading,
6145
        'load': load,
6146
        'loadAll': loadAll,
6147
        'filterImageUrls': filterImageUrls
6148
    };
6149
})();
6150

    
6151

    
6152
/** ------------------------------------------------------------------------ **/
6153

    
6154

    
6155
/**
6156
 * Add and event listener. Works for all browsers
6157
 * @param {Element} element    An html element
6158
 * @param {string}      action     The action, for example "click",
6159
 *                                 without the prefix "on"
6160
 * @param {function}    listener   The callback function to be executed
6161
 * @param {boolean}     useCapture
6162
 */
6163
links.Timeline.addEventListener = function (element, action, listener, useCapture) {
6164
    if (element.addEventListener) {
6165
        if (useCapture === undefined)
6166
            useCapture = false;
6167

    
6168
        if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
6169
            action = "DOMMouseScroll";  // For Firefox
6170
        }
6171

    
6172
        element.addEventListener(action, listener, useCapture);
6173
    } else {
6174
        element.attachEvent("on" + action, listener);  // IE browsers
6175
    }
6176
};
6177

    
6178
/**
6179
 * Remove an event listener from an element
6180
 * @param {Element}  element   An html dom element
6181
 * @param {string}       action    The name of the event, for example "mousedown"
6182
 * @param {function}     listener  The listener function
6183
 * @param {boolean}      useCapture
6184
 */
6185
links.Timeline.removeEventListener = function(element, action, listener, useCapture) {
6186
    if (element.removeEventListener) {
6187
        // non-IE browsers
6188
        if (useCapture === undefined)
6189
            useCapture = false;
6190

    
6191
        if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
6192
            action = "DOMMouseScroll";  // For Firefox
6193
        }
6194

    
6195
        element.removeEventListener(action, listener, useCapture);
6196
    } else {
6197
        // IE browsers
6198
        element.detachEvent("on" + action, listener);
6199
    }
6200
};
6201

    
6202

    
6203
/**
6204
 * Get HTML element which is the target of the event
6205
 * @param {Event} event
6206
 * @return {Element} target element
6207
 */
6208
links.Timeline.getTarget = function (event) {
6209
    // code from http://www.quirksmode.org/js/events_properties.html
6210
    if (!event) {
6211
        event = window.event;
6212
    }
6213

    
6214
    var target;
6215

    
6216
    if (event.target) {
6217
        target = event.target;
6218
    }
6219
    else if (event.srcElement) {
6220
        target = event.srcElement;
6221
    }
6222

    
6223
    if (target.nodeType != undefined && target.nodeType == 3) {
6224
        // defeat Safari bug
6225
        target = target.parentNode;
6226
    }
6227

    
6228
    return target;
6229
};
6230

    
6231
/**
6232
 * Stop event propagation
6233
 */
6234
links.Timeline.stopPropagation = function (event) {
6235
    if (!event)
6236
        event = window.event;
6237

    
6238
    if (event.stopPropagation) {
6239
        event.stopPropagation();  // non-IE browsers
6240
    }
6241
    else {
6242
        event.cancelBubble = true;  // IE browsers
6243
    }
6244
};
6245

    
6246

    
6247
/**
6248
 * Cancels the event if it is cancelable, without stopping further propagation of the event.
6249
 */
6250
links.Timeline.preventDefault = function (event) {
6251
    if (!event)
6252
        event = window.event;
6253

    
6254
    if (event.preventDefault) {
6255
        event.preventDefault();  // non-IE browsers
6256
    }
6257
    else {
6258
        event.returnValue = false;  // IE browsers
6259
    }
6260
};
6261

    
6262

    
6263
/**
6264
 * Retrieve the absolute left value of a DOM element
6265
 * @param {Element} elem        A dom element, for example a div
6266
 * @return {number} left        The absolute left position of this element
6267
 *                              in the browser page.
6268
 */
6269
links.Timeline.getAbsoluteLeft = function(elem) {
6270
    var doc = document.documentElement;
6271
    var body = document.body;
6272

    
6273
    var left = elem.offsetLeft;
6274
    var e = elem.offsetParent;
6275
    while (e != null && e != body && e != doc) {
6276
        left += e.offsetLeft;
6277
        left -= e.scrollLeft;
6278
        e = e.offsetParent;
6279
    }
6280
    return left;
6281
};
6282

    
6283
/**
6284
 * Retrieve the absolute top value of a DOM element
6285
 * @param {Element} elem        A dom element, for example a div
6286
 * @return {number} top        The absolute top position of this element
6287
 *                              in the browser page.
6288
 */
6289
links.Timeline.getAbsoluteTop = function(elem) {
6290
    var doc = document.documentElement;
6291
    var body = document.body;
6292

    
6293
    var top = elem.offsetTop;
6294
    var e = elem.offsetParent;
6295
    while (e != null && e != body && e != doc) {
6296
        top += e.offsetTop;
6297
        top -= e.scrollTop;
6298
        e = e.offsetParent;
6299
    }
6300
    return top;
6301
};
6302

    
6303
/**
6304
 * Get the absolute, vertical mouse position from an event.
6305
 * @param {Event} event
6306
 * @return {Number} pageY
6307
 */
6308
links.Timeline.getPageY = function (event) {
6309
    if (('targetTouches' in event) && event.targetTouches.length) {
6310
        event = event.targetTouches[0];
6311
    }
6312

    
6313
    if ('pageY' in event) {
6314
        return event.pageY;
6315
    }
6316

    
6317
    // calculate pageY from clientY
6318
    var clientY = event.clientY;
6319
    var doc = document.documentElement;
6320
    var body = document.body;
6321
    return clientY +
6322
        ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
6323
        ( doc && doc.clientTop || body && body.clientTop || 0 );
6324
};
6325

    
6326
/**
6327
 * Get the absolute, horizontal mouse position from an event.
6328
 * @param {Event} event
6329
 * @return {Number} pageX
6330
 */
6331
links.Timeline.getPageX = function (event) {
6332
    if (('targetTouches' in event) && event.targetTouches.length) {
6333
        event = event.targetTouches[0];
6334
    }
6335

    
6336
    if ('pageX' in event) {
6337
        return event.pageX;
6338
    }
6339

    
6340
    // calculate pageX from clientX
6341
    var clientX = event.clientX;
6342
    var doc = document.documentElement;
6343
    var body = document.body;
6344
    return clientX +
6345
        ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
6346
        ( doc && doc.clientLeft || body && body.clientLeft || 0 );
6347
};
6348

    
6349
/**
6350
 * Adds one or more className's to the given elements style
6351
 * @param {Element} elem
6352
 * @param {String} className
6353
 */
6354
links.Timeline.addClassName = function(elem, className) {
6355
    var classes = elem.className.split(' ');
6356
    var classesToAdd = className.split(' ');
6357
    
6358
    var added = false;
6359
    for (var i=0; i<classesToAdd.length; i++) {
6360
        if (classes.indexOf(classesToAdd[i]) == -1) {
6361
            classes.push(classesToAdd[i]); // add the class to the array
6362
            added = true;
6363
        }
6364
    }
6365
    
6366
    if (added) {
6367
        elem.className = classes.join(' ');
6368
    }
6369
};
6370

    
6371
/**
6372
 * Removes one or more className's from the given elements style
6373
 * @param {Element} elem
6374
 * @param {String} className
6375
 */
6376
links.Timeline.removeClassName = function(elem, className) {
6377
    var classes = elem.className.split(' ');
6378
    var classesToRemove = className.split(' ');
6379
    
6380
    var removed = false;
6381
    for (var i=0; i<classesToRemove.length; i++) {
6382
        var index = classes.indexOf(classesToRemove[i]);
6383
        if (index != -1) {
6384
            classes.splice(index, 1); // remove the class from the array
6385
            removed = true;
6386
        }
6387
    }
6388
    
6389
    if (removed) {
6390
        elem.className = classes.join(' ');
6391
    }
6392
};
6393

    
6394
/**
6395
 * Check if given object is a Javascript Array
6396
 * @param {*} obj
6397
 * @return {Boolean} isArray    true if the given object is an array
6398
 */
6399
// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
6400
links.Timeline.isArray = function (obj) {
6401
    if (obj instanceof Array) {
6402
        return true;
6403
    }
6404
    return (Object.prototype.toString.call(obj) === '[object Array]');
6405
};
6406

    
6407
/**
6408
 * parse a JSON date
6409
 * @param {Date | String | Number} date    Date object to be parsed. Can be:
6410
 *                                         - a Date object like new Date(),
6411
 *                                         - a long like 1356970529389,
6412
 *                                         an ISO String like "2012-12-31T16:16:07.213Z",
6413
 *                                         or a .Net Date string like
6414
 *                                         "\/Date(1356970529389)\/"
6415
 * @return {Date} parsedDate
6416
 */
6417
links.Timeline.parseJSONDate = function (date) {
6418
    if (date == undefined) {
6419
        return undefined;
6420
    }
6421

    
6422
    //test for date
6423
    if (date instanceof Date) {
6424
        return date;
6425
    }
6426

    
6427
    // test for MS format.
6428
    // FIXME: will fail on a Number
6429
    var m = date.match(/\/Date\((-?\d+)([-\+]?\d{2})?(\d{2})?\)\//i);
6430
    if (m) {
6431
        var offset = m[2]
6432
            ? (3600000 * m[2]) // hrs offset
6433
            + (60000 * m[3] * (m[2] / Math.abs(m[2]))) // mins offset
6434
            : 0;
6435

    
6436
        return new Date(
6437
            (1 * m[1]) // ticks
6438
                + offset
6439
        );
6440
    }
6441

    
6442
    // failing that, try to parse whatever we've got.
6443
    return Date.parse(date);
6444
};
(28-28/34)