All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.primefaces.timeline.0-timeline.js Maven / Gradle / Ivy

There is a newer version: 14.0.0-RC2
Show newest version
/**
 * @file timeline.js
 *
 * @brief
 * The Timeline is an interactive visualization chart to visualize events in
 * time, having a start and end date.
 * You can freely move and zoom in the timeline by dragging
 * and scrolling in the Timeline. Items are optionally dragable. The time
 * scale on the axis is adjusted automatically, and supports scales ranging
 * from milliseconds to years.
 *
 * Timeline is part of the CHAP Links library.
 *
 * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
 * Internet Explorer 6+.
 *
 * @license
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 * Copyright (c) 2011-2015 Almende B.V.
 *
 * @author  Jos de Jong, 
 * @date    2015-03-04
 * @version 2.9.1
 */

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

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

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


/**
 * Ensure the variable google exists
 */
if (typeof google === 'undefined') {
    google = undefined;
    // important: do not use var, as "var google = undefined;" will overwrite
    //            the existing google variable value with undefined in IE8, IE7.
}



// Internet Explorer 8 and older does not support Array.indexOf,
// so we define it here in that case
// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
if(!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(obj){
        for(var i = 0; i < this.length; i++){
            if(this[i] == obj){
                return i;
            }
        }
        return -1;
    }
}

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


/**
 * @constructor links.Timeline
 * The timeline is a visualization chart to visualize events in time.
 *
 * The timeline is developed in javascript as a Google Visualization Chart.
 *
 * @param {Element} container   The DOM element in which the Timeline will
 *                              be created. Normally a div element.
 * @param {Object} options      A name/value map containing settings for the
 *                              timeline. Optional.
 */
links.Timeline = function(container, options) {
    if (!container) {
        // this call was probably only for inheritance, no constructor-code is required
        return;
    }

    // create variables and set default values
    this.dom = {};
    this.conversion = {};
    this.eventParams = {}; // stores parameters for mouse events
    this.groups = [];
    this.groupIndexes = {};
    this.items = [];
    this.renderQueue = {
        show: [],   // Items made visible but not yet added to DOM
        hide: [],   // Items currently visible but not yet removed from DOM
        update: []  // Items with changed data but not yet adjusted DOM
    };
    this.renderedItems = [];  // Items currently rendered in the DOM
    this.clusterGenerator = new links.Timeline.ClusterGenerator(this);
    this.currentClusters = [];
    this.selection = undefined; // stores index and item which is currently selected

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

    // Initialize sizes.
    // Needed for IE (which gives an error when you try to set an undefined
    // value in a style)
    this.size = {
        'actualHeight': 0,
        'axis': {
            'characterMajorHeight': 0,
            'characterMajorWidth': 0,
            'characterMinorHeight': 0,
            'characterMinorWidth': 0,
            'height': 0,
            'labelMajorTop': 0,
            'labelMinorTop': 0,
            'line': 0,
            'lineMajorWidth': 0,
            'lineMinorHeight': 0,
            'lineMinorTop': 0,
            'lineMinorWidth': 0,
            'top': 0
        },
        'contentHeight': 0,
        'contentLeft': 0,
        'contentWidth': 0,
        'frameHeight': 0,
        'frameWidth': 0,
        'groupsLeft': 0,
        'groupsWidth': 0,
        'items': {
            'top': 0
        }
    };

    this.dom.container = container;

    //
    // Let's set the default options first
    //
    this.options = {
        'width': "100%",
        'height': "auto",
        'minHeight': 0,        // minimal height in pixels
        'groupMinHeight': 0,
        'autoHeight': true,

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

        'min': undefined,
        'max': undefined,
        'zoomMin': 10,     // milliseconds
        'zoomMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds

        'moveable': true,
        'zoomable': true,
        'selectable': true,
        'unselectable': true,
        'editable': false,
        'snapEvents': true,
        'groupsChangeable': true,
        'timeChangeable': true,

        'showCurrentTime': true, // show a red bar displaying the current time
        'showCustomTime': false, // show a blue, draggable bar displaying a custom time
        'showMajorLabels': true,
        'showMinorLabels': true,
        'showNavigation': false,
        'showButtonNew': false,
        'groupsOnRight': false,
        'groupsOrder' : true,
        'axisOnTop': false,
        'stackEvents': true,
        'animate': true,
        'animateZoom': true,
        'cluster': false,
        'clusterMaxItems': 5,
        'style': 'box',
        '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
        
        // i18n: Timeline only has built-in English text per default. Include timeline-locales.js to support more localized text.
        'locale': 'en',
        'MONTHS': ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
        'MONTHS_SHORT': ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
        'DAYS': ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
        'DAYS_SHORT': ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
        'ZOOM_IN': "Zoom in",
        'ZOOM_OUT': "Zoom out",
        'MOVE_LEFT': "Move left",
        'MOVE_RIGHT': "Move right",
        'NEW': "New",
        'CREATE_NEW_EVENT': "Create new event"
    };
    
    //
    // Now we can set the givenproperties
    //
    this.setOptions(options);

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

    // remove all elements from the container element.
    while (dom.container.hasChildNodes()) {
        dom.container.removeChild(dom.container.firstChild);
    }

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

    // add standard item types
    this.itemTypes = {
        box:           links.Timeline.ItemBox,
        range:         links.Timeline.ItemRange,
        floatingRange: links.Timeline.ItemFloatingRange,
        dot:           links.Timeline.ItemDot
    };

    // initialize data
    this.data = [];
    this.firstDraw = true;

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

    // render for the first time
    this.render();

    // fire the ready event
    var me = this;
    setTimeout(function () {
        me.trigger('ready');
    }, 0);
};


/**
 * Main drawing logic. This is the function that needs to be called
 * in the html page, to draw the timeline.
 *
 * A data table with the events must be provided, and an options table.
 *
 * @param {google.visualization.DataTable}      data
 *                                 The data containing the events for the timeline.
 *                                 Object DataTable is defined in
 *                                 google.visualization.DataTable
 * @param {Object} options         A name/value map containing settings for the
 *                                 timeline. Optional. The use of options here
 *                                 is deprecated. Pass timeline options in the
 *                                 constructor or use setOptions()
 */
links.Timeline.prototype.draw = function(data, options) {
    if (options) {
        console.log("WARNING: Passing options in draw() is deprecated. Pass options to the constructur or use setOptions() instead!");       
        this.setOptions(options);
    }

    if (this.options.selectable) {
        links.Timeline.addClassName(this.dom.frame, "timeline-selectable");
    }

    // read the data
    this.setData(data);

    if (this.firstDraw) {
        this.setVisibleChartRangeAuto();
    }

    this.firstDraw = false;
};


/**
 * Set options for the timeline.
 * Timeline must be redrawn afterwards
 * @param {Object} options A name/value map containing settings for the
 *                                 timeline. Optional.
 */
links.Timeline.prototype.setOptions = function(options) {
    if (options) {
        // retrieve parameter values
        for (var i in options) {
            if (options.hasOwnProperty(i)) {
                this.options[i] = options[i];
            }
        }

        // prepare i18n dependent on set locale
        if (typeof links.locales !== 'undefined' && this.options.locale !== 'en') {
            var localeOpts = links.locales[this.options.locale];
            if(localeOpts) {
                for (var l in localeOpts) {
                    if (localeOpts.hasOwnProperty(l)) {
                        this.options[l] = localeOpts[l];
                    }
                }
            }
        }

        // check for deprecated options
        if (options.showButtonAdd != undefined) {
            this.options.showButtonNew = options.showButtonAdd;
            console.log('WARNING: Option showButtonAdd is deprecated. Use showButtonNew instead');
        }
        if (options.intervalMin != undefined) {
            this.options.zoomMin = options.intervalMin;
            console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead');
        }
        if (options.intervalMax != undefined) {
            this.options.zoomMax = options.intervalMax;
            console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead');
        }

        if (options.scale && options.step) {
            this.step.setScale(options.scale, options.step);
        }
    }

    // validate options
    this.options.autoHeight = (this.options.height === "auto");
};

/**
 * Get options for the timeline.
 *
 * @return the options object
 */
links.Timeline.prototype.getOptions = function() {
    return this.options;
};

/**
 * Add new type of items
 * @param {String} typeName  Name of new type
 * @param {links.Timeline.Item} typeFactory Constructor of items
 */
links.Timeline.prototype.addItemType = function (typeName, typeFactory) {
    this.itemTypes[typeName] = typeFactory;
};

/**
 * Retrieve a map with the column indexes of the columns by column name.
 * For example, the method returns the map
 *     {
 *         start: 0,
 *         end: 1,
 *         content: 2,
 *         group: undefined,
 *         className: undefined
 *         editable: undefined
 *         type: undefined
 *     }
 * @param {google.visualization.DataTable} dataTable
 * @type {Object} map
 */
links.Timeline.mapColumnIds = function (dataTable) {
    var cols = {},
        colCount = dataTable.getNumberOfColumns(),
        allUndefined = true;

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

    // if no labels or ids are defined, use the default mapping
    // for start, end, content, group, className, editable, type
    if (allUndefined) {
        cols.start = 0;
        cols.end = 1;
        cols.content = 2;
        if (colCount > 3) {cols.group = 3}
        if (colCount > 4) {cols.className = 4}
        if (colCount > 5) {cols.editable = 5}
        if (colCount > 6) {cols.type = 6}
    }

    return cols;
};

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

    if (!data) {
        data = [];
    }

    // clear all data
    this.stackCancelAnimation();
    this.clearItems();
    this.data = data;
    var items = this.items;
    this.deleteGroups();

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

        // read DataTable
        for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
            items.push(this.createItem({
                'start':     ((cols.start != undefined)     ? data.getValue(row, cols.start)     : undefined),
                'end':       ((cols.end != undefined)       ? data.getValue(row, cols.end)       : undefined),
                'content':   ((cols.content != undefined)   ? data.getValue(row, cols.content)   : undefined),
                'group':     ((cols.group != undefined)     ? data.getValue(row, cols.group)     : undefined),
                'className': ((cols.className != undefined) ? data.getValue(row, cols.className) : undefined),
                'editable':  ((cols.editable != undefined)  ? data.getValue(row, cols.editable)  : undefined),
                'type':      ((cols.type != undefined)      ? data.getValue(row, cols.type)      : undefined)
            }));
        }
    }
    else if (links.Timeline.isArray(data)) {
        // read JSON array
        for (var row = 0, rows = data.length; row < rows; row++) {
            var itemData = data[row];
            var item = this.createItem(itemData);
            items.push(item);
        }
    }
    else {
        throw "Unknown data type. DataTable or Array expected.";
    }

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

    this.render({
        animate: false
    });
};

/**
 * Return the original data table.
 * @return {google.visualization.DataTable | Array} data
 */
links.Timeline.prototype.getData = function  () {
    return this.data;
};


/**
 * Update the original data with changed start, end or group.
 *
 * @param {Number} index
 * @param {Object} values   An object containing some of the following parameters:
 *                          {Date} start,
 *                          {Date} end,
 *                          {String} content,
 *                          {String} group
 */
links.Timeline.prototype.updateData = function  (index, values) {
    var data = this.data,
        prop;

    if (google && google.visualization &&
        data instanceof google.visualization.DataTable) {
        // update the original google DataTable
        var missingRows = (index + 1) - data.getNumberOfRows();
        if (missingRows > 0) {
            data.addRows(missingRows);
        }

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

        // merge all fields from the provided data into the current data
        for (prop in values) {
            if (values.hasOwnProperty(prop)) {
                var col = cols[prop];
                if (col == undefined) {
                    // create new column
                    var value = values[prop];
                    var valueType = 'string';
                    if (typeof(value) == 'number')       {valueType = 'number';}
                    else if (typeof(value) == 'boolean') {valueType = 'boolean';}
                    else if (value instanceof Date)      {valueType = 'datetime';}
                    col = data.addColumn(valueType, prop);
                }
                data.setValue(index, col, values[prop]);

                // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
            }
        }
    }
    else if (links.Timeline.isArray(data)) {
        // update the original JSON table
        var row = data[index];
        if (row == undefined) {
            row = {};
            data[index] = row;
        }

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

                // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
            }
        }
    }
    else {
        throw "Cannot update data, unknown type of data";
    }
};

/**
 * Find the item index from a given HTML element
 * If no item index is found, undefined is returned
 * @param {Element} element
 * @return {Number | undefined} index
 */
links.Timeline.prototype.getItemIndex = function(element) {
    var e = element,
        dom = this.dom,
        frame = dom.items.frame,
        items = this.items,
        index = undefined;

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

    if (e.parentNode === frame) {
        // yes! we have found the parent element of all items
        // retrieve its id from the array with items
        for (var i = 0, iMax = items.length; i < iMax; i++) {
            if (items[i].dom === e) {
                index = i;
                break;
            }
        }
    }

    return index;
};


/**
 * Find the cluster index from a given HTML element
 * If no cluster index is found, undefined is returned
 * @param {Element} element
 * @return {Number | undefined} index
 */
links.Timeline.prototype.getClusterIndex = function(element) {
    var e = element,
        dom = this.dom,
        frame = dom.items.frame,
        clusters = this.clusters,
        index = undefined;

    if (this.clusters) {
        // try to find the frame where the clusters are located in
        while (e.parentNode && e.parentNode !== frame) {
            e = e.parentNode;
        }

        if (e.parentNode === frame) {
            // yes! we have found the parent element of all clusters
            // retrieve its id from the array with clusters
            for (var i = 0, iMax = clusters.length; i < iMax; i++) {
                if (clusters[i].dom === e) {
                    index = i;
                    break;
                }
            }
        }
    }

    return index;
};

/**
 * Find all elements within the start and end range
 * If no element is found, returns an empty array
 * @param start time
 * @param end time
 * @return Array itemsInRange
 */
links.Timeline.prototype.getVisibleItems = function  (start, end) {
    var items = this.items;
    var itemsInRange = [];

    if (items) {
        for (var i = 0, iMax = items.length; i < iMax; i++) {
            var item = items[i];
            if (item.end) {
                // Time range object // NH use getLeft and getRight here
                if (start <= item.start && item.end <= end) {
                    itemsInRange.push({"row": i});
                }
            } else {
                // Point object
                if (start <= item.start && item.start <= end) {
                    itemsInRange.push({"row": i});
                }
            }
        }
    }

    //     var sel = [];
    // if (this.selection) {
    //     sel.push({"row": this.selection.index});
    // }
    // return sel;

    return itemsInRange;
};


/**
 * Set a new size for the timeline
 * @param {string} width   Width in pixels or percentage (for example "800px"
 *                         or "50%")
 * @param {string} height  Height in pixels or percentage  (for example "400px"
 *                         or "30%")
 */
links.Timeline.prototype.setSize = function(width, height) {
    if (width) {
        this.options.width = width;
        this.dom.frame.style.width = width;
    }
    if (height) {
        this.options.height = height;
        this.options.autoHeight = (this.options.height === "auto");
        if (height !==  "auto" ) {
            this.dom.frame.style.height = height;
        }
    }

    this.render({
        animate: false
    });
};


/**
 * Set a new value for the visible range int the timeline.
 * Set start undefined to include everything from the earliest date to end.
 * Set end undefined to include everything from start to the last date.
 * Example usage:
 *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
 *                                    new Date("2010-09-13"));
 * @param {Date}   start     The start date for the timeline. optional
 * @param {Date}   end       The end date for the timeline. optional
 * @param {boolean} redraw   Optional. If true (default) the Timeline is
 *                           directly redrawn
 */
links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
    var range = {};
    if (!start || !end) {
        // retrieve the date range of the items
        range = this.getDataRange(true);
    }

    if (!start) {
        if (end) {
            if (range.min && range.min.valueOf() < end.valueOf()) {
                // start of the data
                start = range.min;
            }
            else {
                // 7 days before the end
                start = new Date(end.valueOf());
                start.setDate(start.getDate() - 7);
            }
        }
        else {
            // default of 3 days ago
            start = new Date();
            start.setDate(start.getDate() - 3);
        }
    }

    if (!end) {
        if (range.max) {
            // end of the data
            end = range.max;
        }
        else {
            // 7 days after start
            end = new Date(start.valueOf());
            end.setDate(end.getDate() + 7);
        }
    }

    // prevent start Date <= end Date
    if (end <= start) {
        end = new Date(start.valueOf());
        end.setDate(end.getDate() + 7);
    }

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

    this.applyRange(start, end);

    if (redraw == undefined || redraw == true) {
        this.render({
            animate: false
        });  // TODO: optimize, no reflow needed
    }
    else {
        this.recalcConversion();
    }
};


/**
 * Change the visible chart range such that all items become visible
 */
links.Timeline.prototype.setVisibleChartRangeAuto = function() {
    if (this.options && (this.options.start || this.options.end)) {
        this.setVisibleChartRange(this.options.start, this.options.end);
    } else {
        var range = this.getDataRange(true);
        this.setVisibleChartRange(range.min, range.max);
    }
};

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

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

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


/**
 * Retrieve the current visible range in the timeline.
 * @return {Object} An object with start and end properties
 */
links.Timeline.prototype.getVisibleChartRange = function() {
    return {
        'start': new Date(this.start.valueOf()),
        'end': new Date(this.end.valueOf())
    };
};

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

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

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

            if (end != undefined) {
                max = (max != undefined) ? Math.max(max.valueOf(), end.valueOf()) : end;
            }
        }
    }

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

    return {
        'min': min != undefined ? new Date(min) : undefined,
        'max': max != undefined ? new Date(max) : undefined
    };
};

/**
 * Re-render (reflow and repaint) all components of the Timeline: frame, axis,
 * items, ...
 * @param {Object} [options]  Available options:
 *                            {boolean} renderTimesLeft   Number of times the
 *                                                        render may be repeated
 *                                                        5 times by default.
 *                            {boolean} animate           takes options.animate
 *                                                        as default value
 */
links.Timeline.prototype.render = function(options) {
    var frameResized = this.reflowFrame();
    var axisResized = this.reflowAxis();
    var groupsResized = this.reflowGroups();
    var itemsResized = this.reflowItems();
    var resized = (frameResized || axisResized || groupsResized || itemsResized);

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

    this.recalcConversion();
    this.clusterItems();
    this.filterItems();
    this.stackItems(animate);
    this.recalcItems();

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

    // re-render once when needed (prevent endless re-render loop)
    if (needsReflow) {
        var renderTimesLeft = options ? options.renderTimesLeft : undefined;
        if (renderTimesLeft == undefined) {
            renderTimesLeft = 5;
        }
        if (renderTimesLeft > 0) {
            this.render({
                'animate': options ? options.animate: undefined,
                'renderTimesLeft': (renderTimesLeft - 1)
            });
        }
    }
};

/**
 * Repaint all components of the Timeline
 * @return {boolean} needsReflow   Returns true if the DOM is changed such that
 *                                 a reflow is needed.
 */
links.Timeline.prototype.repaint = function() {
    var frameNeedsReflow = this.repaintFrame();
    var axisNeedsReflow  = this.repaintAxis();
    var groupsNeedsReflow  = this.repaintGroups();
    var itemsNeedsReflow = this.repaintItems();
    this.repaintCurrentTime();
    this.repaintCustomTime();

    return (frameNeedsReflow || axisNeedsReflow || groupsNeedsReflow || itemsNeedsReflow);
};

/**
 * Reflow the timeline frame
 * @return {boolean} resized    Returns true if any of the frame elements
 *                              have been resized.
 */
links.Timeline.prototype.reflowFrame = function() {
    var dom = this.dom,
        options = this.options,
        size = this.size,
        resized = false;

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

    resized = resized || (size.frameWidth !== frameWidth);
    resized = resized || (size.frameHeight !== frameHeight);
    size.frameWidth = frameWidth;
    size.frameHeight = frameHeight;

    return resized;
};

/**
 * repaint the Timeline frame
 * @return {boolean} needsReflow   Returns true if the DOM is changed such that
 *                                 a reflow is needed.
 */
links.Timeline.prototype.repaintFrame = function() {
    var needsReflow = false,
        dom = this.dom,
        options = this.options,
        size = this.size;

    // main frame
    if (!dom.frame) {
        dom.frame = document.createElement("DIV");
        dom.frame.className = "timeline-frame ui-widget ui-widget-content ui-corner-all";
        dom.container.appendChild(dom.frame);
        needsReflow = true;
    }

    var height = options.autoHeight ?
        (size.actualHeight + "px") :
        (options.height || "100%");
    var width  = options.width || "100%";
    needsReflow = needsReflow || (dom.frame.style.height != height);
    needsReflow = needsReflow || (dom.frame.style.width != width);
    dom.frame.style.height = height;
    dom.frame.style.width = width;

    // contents
    if (!dom.content) {
        // create content box where the axis and items will be created
        dom.content = document.createElement("DIV");
        dom.content.className = "timeline-content";
        dom.frame.appendChild(dom.content);

        var timelines = document.createElement("DIV");
        timelines.style.position = "absolute";
        timelines.style.left = "0px";
        timelines.style.top = "0px";
        timelines.style.height = "100%";
        timelines.style.width = "0px";
        dom.content.appendChild(timelines);
        dom.contentTimelines = timelines;

        var params = this.eventParams,
            me = this;
        if (!params.onMouseDown) {
            params.onMouseDown = function (event) {me.onMouseDown(event);};
            links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
        }
        if (!params.onTouchStart) {
            params.onTouchStart = function (event) {me.onTouchStart(event);};
            links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
        }
        if (!params.onMouseWheel) {
            params.onMouseWheel = function (event) {me.onMouseWheel(event);};
            links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
        }
        if (!params.onDblClick) {
            params.onDblClick = function (event) {me.onDblClick(event);};
            links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
        }

        needsReflow = true;
    }
    dom.content.style.left = size.contentLeft + "px";
    dom.content.style.top = "0px";
    dom.content.style.width = size.contentWidth + "px";
    dom.content.style.height = size.frameHeight + "px";

    this.repaintNavigation();

    return needsReflow;
};

/**
 * Reflow the timeline axis. Calculate its height, width, positioning, etc...
 * @return {boolean} resized    returns true if the axis is resized
 */
links.Timeline.prototype.reflowAxis = function() {
    var resized = false,
        dom = this.dom,
        options = this.options,
        size = this.size,
        axisDom = dom.axis;

    var characterMinorWidth  = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientWidth : 0,
        characterMinorHeight = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientHeight : 0,
        characterMajorWidth  = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientWidth : 0,
        characterMajorHeight = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientHeight : 0,
        axisHeight = (options.showMinorLabels ? characterMinorHeight : 0) +
            (options.showMajorLabels ? characterMajorHeight : 0);

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

    resized = resized || (size.axis.top !== axisTop);
    resized = resized || (size.axis.line !== axisLine);
    resized = resized || (size.axis.height !== axisHeight);
    size.axis.top = axisTop;
    size.axis.line = axisLine;
    size.axis.height = axisHeight;
    size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine +
        (options.showMinorLabels ? characterMinorHeight : 0);
    size.axis.labelMinorTop = options.axisOnTop ?
        (options.showMajorLabels ? characterMajorHeight : 0) :
        axisLine;
    size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
    size.axis.lineMinorHeight = options.showMajorLabels ?
        size.frameHeight - characterMajorHeight:
        size.frameHeight;
    if (axisDom && axisDom.minorLines && axisDom.minorLines.length) {
        size.axis.lineMinorWidth = axisDom.minorLines[0].offsetWidth;
    }
    else {
        size.axis.lineMinorWidth = 1;
    }
    if (axisDom && axisDom.majorLines && axisDom.majorLines.length) {
        size.axis.lineMajorWidth = axisDom.majorLines[0].offsetWidth;
    }
    else {
        size.axis.lineMajorWidth = 1;
    }

    resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
    resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
    resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
    resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
    size.axis.characterMinorWidth  = characterMinorWidth;
    size.axis.characterMinorHeight = characterMinorHeight;
    size.axis.characterMajorWidth  = characterMajorWidth;
    size.axis.characterMajorHeight = characterMajorHeight;

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

    return resized;
};

/**
 * Redraw the timeline axis with minor and major labels
 * @return {boolean} needsReflow     Returns true if the DOM is changed such
 *                                   that a reflow is needed.
 */
links.Timeline.prototype.repaintAxis = function() {
    var needsReflow = false,
        dom = this.dom,
        options = this.options,
        size = this.size,
        step = this.step;

    var axis = dom.axis;
    if (!axis) {
        axis = {};
        dom.axis = axis;
    }
    if (!size.axis.properties) {
        size.axis.properties = {};
    }
    if (!axis.minorTexts) {
        axis.minorTexts = [];
    }
    if (!axis.minorLines) {
        axis.minorLines = [];
    }
    if (!axis.majorTexts) {
        axis.majorTexts = [];
    }
    if (!axis.majorLines) {
        axis.majorLines = [];
    }

    if (!axis.frame) {
        axis.frame = document.createElement("DIV");
        axis.frame.style.position = "absolute";
        axis.frame.style.left = "0px";
        axis.frame.style.top = "0px";
        dom.content.appendChild(axis.frame);
    }

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

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

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

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

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

    var charsNeedsReflow = this.repaintAxisCharacters();
    needsReflow = needsReflow || charsNeedsReflow;

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

    step.start();
    var xFirstMajorLabel = undefined;
    var max = 0;
    while (!step.end() && max < 1000) {
        max++;
        var cur = step.getCurrent(),
            x = this.timeToScreen(cur),
            isMajor = step.isMajor();

        if (options.showMinorLabels) {
            this.repaintAxisMinorText(x, step.getLabelMinor(options));
        }

        if (isMajor && options.showMajorLabels) {
            if (x > 0) {
                if (xFirstMajorLabel == undefined) {
                    xFirstMajorLabel = x;
                }
                this.repaintAxisMajorText(x, step.getLabelMajor(options));
            }
            this.repaintAxisMajorLine(x);
        }
        else {
            this.repaintAxisMinorLine(x);
        }

        step.next();
    }

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

        if (xFirstMajorLabel == undefined || width < xFirstMajorLabel) {
            this.repaintAxisMajorText(0, leftText, leftTime);
        }
    }

    // cleanup left over labels
    this.repaintAxisEndOverwriting();

    this.repaintAxisHorizontal();

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

    return needsReflow;
};

/**
 * Create characters used to determine the size of text on the axis
 * @return {boolean} needsReflow   Returns true if the DOM is changed such that
 *                                 a reflow is needed.
 */
links.Timeline.prototype.repaintAxisCharacters = function () {
    // calculate the width and height of a single character
    // this is used to calculate the step size, and also the positioning of the
    // axis
    var needsReflow = false,
        dom = this.dom,
        axis = dom.axis,
        text;

    if (!axis.characterMinor) {
        text = document.createTextNode("0");
        var characterMinor = document.createElement("DIV");
        characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
        characterMinor.appendChild(text);
        characterMinor.style.position = "absolute";
        characterMinor.style.visibility = "hidden";
        characterMinor.style.paddingLeft = "0px";
        characterMinor.style.paddingRight = "0px";
        axis.frame.appendChild(characterMinor);

        axis.characterMinor = characterMinor;
        needsReflow = true;
    }

    if (!axis.characterMajor) {
        text = document.createTextNode("0");
        var characterMajor = document.createElement("DIV");
        characterMajor.className = "timeline-axis-text timeline-axis-text-major";
        characterMajor.appendChild(text);
        characterMajor.style.position = "absolute";
        characterMajor.style.visibility = "hidden";
        characterMajor.style.paddingLeft = "0px";
        characterMajor.style.paddingRight = "0px";
        axis.frame.appendChild(characterMajor);

        axis.characterMajor = characterMajor;
        needsReflow = true;
    }

    return needsReflow;
};

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

    properties.minorTextNum = 0;
    properties.minorLineNum = 0;
    properties.majorTextNum = 0;
    properties.majorLineNum = 0;
};

/**
 * End of overwriting HTML DOM elements of the axis.
 * remaining elements will be removed
 */
links.Timeline.prototype.repaintAxisEndOverwriting = function () {
    var dom = this.dom,
        props = this.size.axis.properties,
        frame = this.dom.axis.frame,
        num;

    // remove leftovers
    var minorTexts = dom.axis.minorTexts;
    num = props.minorTextNum;
    while (minorTexts.length > num) {
        var minorText = minorTexts[num];
        frame.removeChild(minorText);
        minorTexts.splice(num, 1);
    }

    var minorLines = dom.axis.minorLines;
    num = props.minorLineNum;
    while (minorLines.length > num) {
        var minorLine = minorLines[num];
        frame.removeChild(minorLine);
        minorLines.splice(num, 1);
    }

    var majorTexts = dom.axis.majorTexts;
    num = props.majorTextNum;
    while (majorTexts.length > num) {
        var majorText = majorTexts[num];
        frame.removeChild(majorText);
        majorTexts.splice(num, 1);
    }

    var majorLines = dom.axis.majorLines;
    num = props.majorLineNum;
    while (majorLines.length > num) {
        var majorLine = majorLines[num];
        frame.removeChild(majorLine);
        majorLines.splice(num, 1);
    }
};

/**
 * Repaint the horizontal line and background of the axis
 */
links.Timeline.prototype.repaintAxisHorizontal = function() {
    var axis = this.dom.axis,
        size = this.size,
        options = this.options;

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

            axis.backgroundLine = backgroundLine;
        }

        if (axis.backgroundLine) {
            axis.backgroundLine.style.top = size.axis.top + "px";
            axis.backgroundLine.style.height = size.axis.height + "px";
        }
    }
    else {
        if (axis.backgroundLine) {
            axis.frame.removeChild(axis.backgroundLine);
            delete axis.backgroundLine;
        }
    }

    // line before all axis elements
    if (hasAxis) {
        if (axis.line) {
            // put this line at the end of all childs
            var line = axis.frame.removeChild(axis.line);
            axis.frame.appendChild(line);
        }
        else {
            // make the axis line
            var line = document.createElement("DIV");
            line.className = "timeline-axis";
            line.style.position = "absolute";
            line.style.left = "0px";
            line.style.width = "100%";
            line.style.height = "0px";
            axis.frame.appendChild(line);

            axis.line = line;
        }

        axis.line.style.top = size.axis.line + "px";
    }
    else {
        if (axis.line && axis.line.parentElement) {
            axis.frame.removeChild(axis.line);
            delete axis.line;
        }
    }
};

/**
 * Create a minor label for the axis at position x
 * @param {Number} x
 * @param {String} text
 */
links.Timeline.prototype.repaintAxisMinorText = function (x, text) {
    var size = this.size,
        dom = this.dom,
        props = size.axis.properties,
        frame = dom.axis.frame,
        minorTexts = dom.axis.minorTexts,
        index = props.minorTextNum,
        label;

    if (index < minorTexts.length) {
        label = minorTexts[index]
    }
    else {
        // create new label
        var content = document.createTextNode("");
        label = document.createElement("DIV");
        label.appendChild(content);
        label.className = "timeline-axis-text timeline-axis-text-minor";
        label.style.position = "absolute";

        frame.appendChild(label);

        minorTexts.push(label);
    }

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

    props.minorTextNum++;
};

/**
 * Create a minor line for the axis at position x
 * @param {Number} x
 */
links.Timeline.prototype.repaintAxisMinorLine = function (x) {
    var axis = this.size.axis,
        dom = this.dom,
        props = axis.properties,
        frame = dom.axis.frame,
        minorLines = dom.axis.minorLines,
        index = props.minorLineNum,
        line;

    if (index < minorLines.length) {
        line = minorLines[index];
    }
    else {
        // create vertical line
        line = document.createElement("DIV");
        line.className = "timeline-axis-grid timeline-axis-grid-minor";
        line.style.position = "absolute";
        line.style.width = "0px";

        frame.appendChild(line);
        minorLines.push(line);
    }

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

    props.minorLineNum++;
};

/**
 * Create a Major label for the axis at position x
 * @param {Number} x
 * @param {String} text
 */
links.Timeline.prototype.repaintAxisMajorText = function (x, text) {
    var size = this.size,
        props = size.axis.properties,
        frame = this.dom.axis.frame,
        majorTexts = this.dom.axis.majorTexts,
        index = props.majorTextNum,
        label;

    if (index < majorTexts.length) {
        label = majorTexts[index];
    }
    else {
        // create label
        var content = document.createTextNode(text);
        label = document.createElement("DIV");
        label.className = "timeline-axis-text timeline-axis-text-major";
        label.appendChild(content);
        label.style.position = "absolute";
        label.style.top = "0px";

        frame.appendChild(label);
        majorTexts.push(label);
    }

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

    props.majorTextNum ++;
};

/**
 * Create a Major line for the axis at position x
 * @param {Number} x
 */
links.Timeline.prototype.repaintAxisMajorLine = function (x) {
    var size = this.size,
        props = size.axis.properties,
        axis = this.size.axis,
        frame = this.dom.axis.frame,
        majorLines = this.dom.axis.majorLines,
        index = props.majorLineNum,
        line;

    if (index < majorLines.length) {
        line = majorLines[index];
    }
    else {
        // create vertical line
        line = document.createElement("DIV");
        line.className = "timeline-axis-grid timeline-axis-grid-major";
        line.style.position = "absolute";
        line.style.top = "0px";
        line.style.width = "0px";

        frame.appendChild(line);
        majorLines.push(line);
    }

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

    props.majorLineNum ++;
};

/**
 * Reflow all items, retrieve their actual size
 * @return {boolean} resized    returns true if any of the items is resized
 */
links.Timeline.prototype.reflowItems = function() {
    var resized = false,
        i,
        iMax,
        group,
        groups = this.groups,
        renderedItems = this.renderedItems;

    if (groups) { // TODO: need to check if labels exists?
        // loop through all groups to reset the items height
        groups.forEach(function (group) {
            group.itemsHeight = group.labelHeight || 0;
        });
    }

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

        if (domItem) {
            // TODO: move updating width and height into item.reflow
            var width = domItem ? domItem.clientWidth : 0;
            var height = domItem ? domItem.clientHeight : 0;
            resized = resized || (item.width != width);
            resized = resized || (item.height != height);
            item.width = width;
            item.height = height;
            //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
            item.reflow();
        }

        if (group) {
            group.itemsHeight = Math.max(this.options.groupMinHeight,group.itemsHeight ?
                Math.max(group.itemsHeight, item.height) :
                item.height);
        }
    }

    return resized;
};

/**
 * Recalculate item properties:
 * - the height of each group.
 * - the actualHeight, from the stacked items or the sum of the group heights
 * @return {boolean} resized    returns true if any of the items properties is
 *                              changed
 */
links.Timeline.prototype.recalcItems = function () {
    var resized = false,
        i,
        iMax,
        item,
        finalItem,
        finalItems,
        group,
        groups = this.groups,
        size = this.size,
        options = this.options,
        renderedItems = this.renderedItems;

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

            if (this.stack && this.stack.finalItems) {
                // adjust the offset of all finalItems when the actualHeight has been changed
                finalItems = this.stack.finalItems;
                finalItem = finalItems[0];
                if (finalItem && finalItem.top) {
                    min = finalItem.top;
                    max = finalItem.top + finalItem.height;
                }
                for (i = 1, iMax = finalItems.length; i < iMax; i++) {
                    finalItem = finalItems[i];
                    min = Math.min(min, finalItem.top);
                    max = Math.max(max, finalItem.top + finalItem.height);
                }
            }
            else {
                item = renderedItems[0];
                if (item && item.top) {
                    min = item.top;
                    max = item.top + item.height;
                }
                for (i = 1, iMax = renderedItems.length; i < iMax; i++) {
                    item = renderedItems[i];
                    if (item.top) {
                        min = Math.min(min, item.top);
                        max = Math.max(max, (item.top + item.height));
                    }
                }
            }

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

            if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
                // adjust the offset of all items when the actualHeight has been changed
                var diff = actualHeight - size.actualHeight;
                if (this.stack && this.stack.finalItems) {
                    finalItems = this.stack.finalItems;
                    for (i = 0, iMax = finalItems.length; i < iMax; i++) {
                        finalItems[i].top += diff;
                        finalItems[i].item.top += diff;
                    }
                }
                else {
                    for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
                        renderedItems[i].top += diff;
                    }
                }
            }
        }
    }
    else {
        // loop through all groups to get the height of each group, and the
        // total height
        actualHeight = size.axis.height + 2 * options.eventMarginAxis;
        for (i = 0, iMax = groups.length; i < iMax; i++) {
            group = groups[i];

            //
            // TODO: Do we want to apply a max height? how ?
            //
            var groupHeight = group.itemsHeight;
            resized = resized || (groupHeight != group.height);
            group.height = Math.max(groupHeight, options.groupMinHeight);

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

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

        for (i = 0, iMax = groups.length; i < iMax; i++) {
            group = groups[i];
            if (options.axisOnTop) {
                group.top = top + axisHeight;
                group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
                group.lineTop = top + axisHeight + group.height + eventMargin/2;
                top += group.height + eventMargin;
            }
            else {
                top -= group.height + eventMargin;
                group.top = top;
                group.labelTop = top + (group.height - group.labelHeight) / 2;
                group.lineTop = top - eventMargin/2;
            }
        }

        resized = true;
    }

    if (actualHeight < options.minHeight) {
        actualHeight = options.minHeight;
    }
    resized = resized || (actualHeight != size.actualHeight);
    size.actualHeight = actualHeight;

    return resized;
};

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

    // clear the cluster generator
    this.clusterGenerator.clear();

    // actually clear the items
    this.items = [];
};

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

    var needsReflow = false,
        dom = this.dom,
        size = this.size,
        timeline = this,
        renderedItems = this.renderedItems;

    if (!dom.items) {
        dom.items = {};
    }

    // draw the frame containing the items
    var frame = dom.items.frame;
    if (!frame) {
        frame = document.createElement("DIV");
        frame.style.position = "relative";
        dom.content.appendChild(frame);
        dom.items.frame = frame;
    }

    frame.style.left = "0px";
    frame.style.top = size.items.top + "px";
    frame.style.height = "0px";

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

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

    while (item = queue.show.shift()) {
        item.showDOM(frame);
        item.getImageUrls(newImageUrls);
        renderedItems.push(item);
    }
    while (item = queue.update.shift()) {
        item.updateDOM(frame);
        item.getImageUrls(newImageUrls);
        index = this.renderedItems.indexOf(item);
        if (index == -1) {
            renderedItems.push(item);
        }
    }
    while (item = queue.hide.shift()) {
        item.hideDOM(frame);
        index = this.renderedItems.indexOf(item);
        if (index != -1) {
            renderedItems.splice(index, 1);
        }
    }

    // reposition all visible items
    renderedItems.forEach(function (item) {
        item.updatePosition(timeline);
    });

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

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

    if (newImageUrls.length) {
        // retrieve all image sources from the items, and set a callback once
        // all images are retrieved
        var callback = function () {
            timeline.render();
        };
        var sendCallbackWhenAlreadyLoaded = false;
        links.imageloader.loadAll(newImageUrls, callback, sendCallbackWhenAlreadyLoaded);
    }

    return needsReflow;
};

/**
 * Reflow the size of the groups
 * @return {boolean} resized    Returns true if any of the frame elements
 *                              have been resized.
 */
links.Timeline.prototype.reflowGroups = function() {
    var resized = false,
        options = this.options,
        size = this.size,
        dom = this.dom;

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

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

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

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

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

    var groupsLeft = options.groupsOnRight ? size.frameWidth - groupsWidth : 0;
    resized = resized || (size.groupsWidth !== groupsWidth);
    resized = resized || (size.groupsLeft !== groupsLeft);
    size.groupsWidth = groupsWidth;
    size.groupsLeft = groupsLeft;

    return resized;
};

/**
 * Redraw the group labels
 */
links.Timeline.prototype.repaintGroups = function() {
    var dom = this.dom,
        timeline = this,
        options = this.options,
        size = this.size,
        groups = this.groups;

    if (dom.groups === undefined) {
        dom.groups = {};
    }

    var labels = dom.groups.labels;
    if (!labels) {
        labels = [];
        dom.groups.labels = labels;
    }
    var labelLines = dom.groups.labelLines;
    if (!labelLines) {
        labelLines = [];
        dom.groups.labelLines = labelLines;
    }
    var itemLines = dom.groups.itemLines;
    if (!itemLines) {
        itemLines = [];
        dom.groups.itemLines = itemLines;
    }

    // create the frame for holding the groups
    var frame = dom.groups.frame;
    if (!frame) {
        frame =  document.createElement("DIV");
        frame.className = "timeline-groups-axis";
        frame.style.position = "absolute";
        frame.style.overflow = "hidden";
        frame.style.top = "0px";
        frame.style.height = "100%";

        dom.frame.appendChild(frame);
        dom.groups.frame = frame;
    }

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

    // hide groups axis when there are no groups
    if (groups.length == 0) {
        frame.style.display = 'none';
    }
    else {
        frame.style.display = '';
    }

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

    // create the items
    var current = labels.length,
        needed = groups.length;

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

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

        // create text label
        var label = document.createElement("DIV");
        label.className = "timeline-groups-text";
        label.style.position = "absolute";
        if (options.groupsWidth === undefined) {
            label.style.whiteSpace = "nowrap";
        }
        label.innerHTML = this.getGroupName(group);
        frame.appendChild(label);
        labels[i] = label;

        // create the grid line between the group labels
        var labelLine = document.createElement("DIV");
        labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
        labelLine.style.position = "absolute";
        labelLine.style.left = "0px";
        labelLine.style.width = "100%";
        labelLine.style.height = "0px";
        labelLine.style.borderTopStyle = "solid";
        frame.appendChild(labelLine);
        labelLines[i] = labelLine;

        // create the grid line between the items
        var itemLine = document.createElement("DIV");
        itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
        itemLine.style.position = "absolute";
        itemLine.style.left = "0px";
        itemLine.style.width = "100%";
        itemLine.style.height = "0px";
        itemLine.style.borderTopStyle = "solid";
        dom.content.insertBefore(itemLine, dom.content.firstChild);
        itemLines[i] = itemLine;
    }

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

        frame.removeChild(label);
        frame.removeChild(labelLine);
        dom.content.removeChild(itemLine);
    }
    labels.splice(needed, current - needed);
    labelLines.splice(needed, current - needed);
    itemLines.splice(needed, current - needed);

    links.Timeline.addClassName(frame, options.groupsOnRight ? 'timeline-groups-axis-onright' : 'timeline-groups-axis-onleft');

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

        label.style.top = group.labelTop + "px";
        labelLine.style.top = group.lineTop + "px";
        itemLine.style.top = group.lineTop + "px";
        itemLine.style.width = size.contentWidth + "px";
    }

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

        frame.appendChild(background);
        dom.groups.background = background;
    }
    dom.groups.background.style.top = size.axis.top + 'px';
    dom.groups.background.style.height = size.axis.height + 'px';

    if (!dom.groups.line) {
        // create the axis grid line
        var line = document.createElement("DIV");
        line.className = "timeline-axis";
        line.style.position = "absolute";
        line.style.left = "0px";
        line.style.width = "100%";
        line.style.height = "0px";

        frame.appendChild(line);
        dom.groups.line = line;
    }
    dom.groups.line.style.top = size.axis.line + 'px';

    // create a callback when there are images which are not yet loaded
    // TODO: more efficiently load images in the groups
    if (dom.groups.frame && groups.length) {
        var imageUrls = [];
        links.imageloader.filterImageUrls(dom.groups.frame, imageUrls);
        if (imageUrls.length) {
            // retrieve all image sources from the items, and set a callback once
            // all images are retrieved
            var callback = function () {
                timeline.render();
            };
            var sendCallbackWhenAlreadyLoaded = false;
            links.imageloader.loadAll(imageUrls, callback, sendCallbackWhenAlreadyLoaded);
        }
    }
};


/**
 * Redraw the current time bar
 */
links.Timeline.prototype.repaintCurrentTime = function() {
    var options = this.options,
        dom = this.dom,
        size = this.size;

    if (!options.showCurrentTime) {
        if (dom.currentTime) {
            dom.contentTimelines.removeChild(dom.currentTime);
            delete dom.currentTime;
        }

        return;
    }

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

        dom.contentTimelines.appendChild(currentTime);
        dom.currentTime = currentTime;
    }

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

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

    // start a timer to adjust for the new time
    if (this.currentTimeTimer != undefined) {
        clearTimeout(this.currentTimeTimer);
        delete this.currentTimeTimer;
    }
    var timeline = this;
    var onTimeout = function() {
        timeline.repaintCurrentTime();
    };
    // the time equal to the width of one pixel, divided by 2 for more smoothness
    var interval = 1 / this.conversion.factor / 2;
    if (interval < 30) interval = 30;
    this.currentTimeTimer = setTimeout(onTimeout, interval);
};

/**
 * Redraw the custom time bar
 */
links.Timeline.prototype.repaintCustomTime = function() {
    var options = this.options,
        dom = this.dom,
        size = this.size;

    if (!options.showCustomTime) {
        if (dom.customTime) {
            dom.contentTimelines.removeChild(dom.customTime);
            delete dom.customTime;
        }

        return;
    }

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

        var drag = document.createElement("DIV");
        drag.style.position = "relative";
        drag.style.top = "0px";
        drag.style.left = "-10px";
        drag.style.height = "100%";
        drag.style.width = "20px";
        customTime.appendChild(drag);

        dom.contentTimelines.appendChild(customTime);
        dom.customTime = customTime;

        // initialize parameter
        this.customTime = new Date();
    }

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


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

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

        frame.appendChild(deleteButton);
        dom.items.deleteButton = deleteButton;
    }

    var index = (this.selection && this.selection.index !== undefined) ? this.selection.index : -1,
        item = (this.selection && this.selection.index !== undefined) ? this.items[index] : undefined;
    if (item && item.rendered && this.isEditable(item)) {
        var right = item.getRight(this),
            top = item.top;

        deleteButton.style.left = right + 'px';
        deleteButton.style.top = top + 'px';
        deleteButton.style.display = '';
        frame.removeChild(deleteButton);
        frame.appendChild(deleteButton);
    }
    else {
        deleteButton.style.display = 'none';
    }
};


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

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

        frame.appendChild(dragLeft);
        dom.items.dragLeft = dragLeft;
    }

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

        frame.appendChild(dragRight);
        dom.items.dragRight = dragRight;
    }

    // reposition left and right drag area
    var index = (this.selection && this.selection.index !== undefined) ? this.selection.index : -1,
        item = (this.selection && this.selection.index !== undefined) ? this.items[index] : undefined;
    if (item && item.rendered && this.isEditable(item) &&
        (item instanceof links.Timeline.ItemRange || item instanceof links.Timeline.ItemFloatingRange)) {
        var left = item.getLeft(this), // NH change to getLeft
            right = item.getRight(this), // NH change to getRight
            top = item.top,
            height = item.height;

        dragLeft.style.left = left + 'px';
        dragLeft.style.top = top + 'px';
        dragLeft.style.width = options.dragAreaWidth + "px";
        dragLeft.style.height = height + 'px';
        dragLeft.style.display = '';
        frame.removeChild(dragLeft);
        frame.appendChild(dragLeft);

        dragRight.style.left = (right - options.dragAreaWidth) + 'px';
        dragRight.style.top = top + 'px';
        dragRight.style.width = options.dragAreaWidth + "px";
        dragRight.style.height = height + 'px';
        dragRight.style.display = '';
        frame.removeChild(dragRight);
        frame.appendChild(dragRight);
    }
    else {
        dragLeft.style.display = 'none';
        dragRight.style.display = 'none';
    }
};

/**
 * Create the navigation buttons for zooming and moving
 */
links.Timeline.prototype.repaintNavigation = function () {
    var timeline = this,
        options = this.options,
        dom = this.dom,
        frame = dom.frame,
        navBar = dom.navBar;

    if (!navBar) {
        var showButtonNew = options.showButtonNew && options.editable;
        var showNavigation = options.showNavigation && (options.zoomable || options.moveable);
        if (showNavigation || showButtonNew) {
            // create a navigation bar containing the navigation buttons
            navBar = document.createElement("DIV");
            navBar.style.position = "absolute";
            navBar.className = "timeline-navigation ui-widget ui-state-highlight ui-corner-all";
            if (options.groupsOnRight) {
                navBar.style.left = '10px';
            }
            else {
                navBar.style.right = '10px';
            }
            if (options.axisOnTop) {
                navBar.style.bottom = '10px';
            }
            else {
                navBar.style.top = '10px';
            }
            dom.navBar = navBar;
            frame.appendChild(navBar);
        }

        if (showButtonNew) {
            // create a new in button
            navBar.addButton = document.createElement("DIV");
            navBar.addButton.className = "timeline-navigation-new";
            navBar.addButton.title = options.CREATE_NEW_EVENT;
            var addIconSpan = document.createElement("SPAN");
            addIconSpan.className = "ui-icon ui-icon-circle-plus";
            navBar.addButton.appendChild(addIconSpan);

            var onAdd = function(event) {
                links.Timeline.preventDefault(event);
                links.Timeline.stopPropagation(event);

                // create a new event at the center of the frame
                var w = timeline.size.contentWidth;
                var x = w / 2;
                var xstart = timeline.screenToTime(x);
                if (options.snapEvents) {
                    timeline.step.snap(xstart);
                }

                var content = options.NEW;
                var group = timeline.groups.length ? timeline.groups[0].content : undefined;
                var preventRender = true;
                timeline.addItem({
                    'start': xstart,
                    'content': content,
                    'group': group
                }, preventRender);
                var index = (timeline.items.length - 1);
                timeline.selectItem(index);

                timeline.applyAdd = true;

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

                if (timeline.applyAdd) {
                    // render and select the item
                    timeline.render({animate: false});
                    timeline.selectItem(index);
                }
                else {
                    // undo an add
                    timeline.deleteItem(index);
                }
            };
            links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
            navBar.appendChild(navBar.addButton);
        }

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

        if (showNavigation) {
            if (options.zoomable) {
                // create a zoom in button
                navBar.zoomInButton = document.createElement("DIV");
                navBar.zoomInButton.className = "timeline-navigation-zoom-in";
                navBar.zoomInButton.title = this.options.ZOOM_IN;
                var ziIconSpan = document.createElement("SPAN");
                ziIconSpan.className = "ui-icon ui-icon-circle-zoomin";
                navBar.zoomInButton.appendChild(ziIconSpan);

                var onZoomIn = function(event) {
                    links.Timeline.preventDefault(event);
                    links.Timeline.stopPropagation(event);
                    timeline.zoom(0.4);
                    timeline.trigger("rangechange");
                    timeline.trigger("rangechanged");
                };
                links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
                navBar.appendChild(navBar.zoomInButton);

                // create a zoom out button
                navBar.zoomOutButton = document.createElement("DIV");
                navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
                navBar.zoomOutButton.title = this.options.ZOOM_OUT;
                var zoIconSpan = document.createElement("SPAN");
                zoIconSpan.className = "ui-icon ui-icon-circle-zoomout";
                navBar.zoomOutButton.appendChild(zoIconSpan);

                var onZoomOut = function(event) {
                    links.Timeline.preventDefault(event);
                    links.Timeline.stopPropagation(event);
                    timeline.zoom(-0.4);
                    timeline.trigger("rangechange");
                    timeline.trigger("rangechanged");
                };
                links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
                navBar.appendChild(navBar.zoomOutButton);
            }

            if (options.moveable) {
                // create a move left button
                navBar.moveLeftButton = document.createElement("DIV");
                navBar.moveLeftButton.className = "timeline-navigation-move-left";
                navBar.moveLeftButton.title = this.options.MOVE_LEFT;
                var mlIconSpan = document.createElement("SPAN");
                mlIconSpan.className = "ui-icon ui-icon-circle-arrow-w";
                navBar.moveLeftButton.appendChild(mlIconSpan);

                var onMoveLeft = function(event) {
                    links.Timeline.preventDefault(event);
                    links.Timeline.stopPropagation(event);
                    timeline.move(-0.2);
                    timeline.trigger("rangechange");
                    timeline.trigger("rangechanged");
                };
                links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
                navBar.appendChild(navBar.moveLeftButton);

                // create a move right button
                navBar.moveRightButton = document.createElement("DIV");
                navBar.moveRightButton.className = "timeline-navigation-move-right";
                navBar.moveRightButton.title = this.options.MOVE_RIGHT;
                var mrIconSpan = document.createElement("SPAN");
                mrIconSpan.className = "ui-icon ui-icon-circle-arrow-e";
                navBar.moveRightButton.appendChild(mrIconSpan);

                var onMoveRight = function(event) {
                    links.Timeline.preventDefault(event);
                    links.Timeline.stopPropagation(event);
                    timeline.move(0.2);
                    timeline.trigger("rangechange");
                    timeline.trigger("rangechanged");
                };
                links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
                navBar.appendChild(navBar.moveRightButton);
            }
        }
    }
};


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

    this.repaintCurrentTime();
};

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


/**
 * Set custom time.
 * The custom time bar can be used to display events in past or future.
 * @param {Date} time
 */
links.Timeline.prototype.setCustomTime = function(time) {
    this.customTime = new Date(time.valueOf());
    this.repaintCustomTime();
};

/**
 * Retrieve the current custom time.
 * @return {Date} customTime
 */
links.Timeline.prototype.getCustomTime = function() {
    return new Date(this.customTime.valueOf());
};

/**
 * Set a custom scale. Autoscaling will be disabled.
 * For example setScale(SCALE.MINUTES, 5) will result
 * in minor steps of 5 minutes, and major steps of an hour.
 *
 * @param {links.Timeline.StepDate.SCALE} scale
 *                               A scale. Choose from SCALE.MILLISECOND,
 *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
 *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
 *                               SCALE.YEAR.
 * @param {int}        step   A step size, by default 1. Choose for
 *                               example 1, 2, 5, or 10.
 */
links.Timeline.prototype.setScale = function(scale, step) {
    this.step.setScale(scale, step);
    this.render(); // TODO: optimize: only reflow/repaint axis
};

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

/**
 * Redraw the timeline
 * Reloads the (linked) data table and redraws the timeline when resized.
 * See also the method checkResize
 */
links.Timeline.prototype.redraw = function() {
    this.setData(this.data);
};


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

/**
 * Check whether a given item is editable
 * @param {links.Timeline.Item} item
 * @return {boolean} editable
 */
links.Timeline.prototype.isEditable = function (item) {
    if (item) {
        if (item.editable != undefined) {
            return item.editable;
        }
        else {
            return this.options.editable;
        }
    }
    return false;
};

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


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

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



/**
 * Event handler for touchstart event on mobile devices
 */
links.Timeline.prototype.onTouchStart = function(event) {
    var params = this.eventParams,
        me = this;

    if (params.touchDown) {
        // if already moving, return
        return;
    }

    params.touchDown = true;
    params.zoomed = false;

    this.onMouseDown(event);

    if (!params.onTouchMove) {
        params.onTouchMove = function (event) {me.onTouchMove(event);};
        links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
    }
    if (!params.onTouchEnd) {
        params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
        links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
    }

    /* TODO
     // check for double tap event
     var delta = 500; // ms
     var doubleTapStart = (new Date()).valueOf();
     var target = links.Timeline.getTarget(event);
     var doubleTapItem = this.getItemIndex(target);
     if (params.doubleTapStart &&
     (doubleTapStart - params.doubleTapStart) < delta &&
     doubleTapItem == params.doubleTapItem) {
     delete params.doubleTapStart;
     delete params.doubleTapItem;
     me.onDblClick(event);
     params.touchDown = false;
     }
     params.doubleTapStart = doubleTapStart;
     params.doubleTapItem = doubleTapItem;
     */
    // store timing for double taps
    var target = links.Timeline.getTarget(event);
    var item = this.getItemIndex(target);
    params.doubleTapStartPrev = params.doubleTapStart;
    params.doubleTapStart = (new Date()).valueOf();
    params.doubleTapItemPrev = params.doubleTapItem;
    params.doubleTapItem = item;

    links.Timeline.preventDefault(event);
};

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

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

    if (!params.zoomed) {
        // move
        this.onMouseMove(event);
    }
    else {
        if (this.options.zoomable) {
            // pinch
            // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
            params.zoomed = true;

            var scale = event.scale,
                oldWidth = (params.end.valueOf() - params.start.valueOf()),
                newWidth = oldWidth / scale,
                diff = newWidth - oldWidth,
                start = new Date(parseInt(params.start.valueOf() - diff/2)),
                end = new Date(parseInt(params.end.valueOf() + diff/2));

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

            this.setVisibleChartRange(start, end);
            this.trigger("rangechange");
        }
    }

    links.Timeline.preventDefault(event);
};

/**
 * Event handler for touchend event on mobile devices
 */
links.Timeline.prototype.onTouchEnd = function(event) {
    var params = this.eventParams;
    var me = this;
    params.touchDown = false;

    if (params.zoomed) {
        this.trigger("rangechanged");
    }

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

    }
    if (params.onTouchEnd) {
        links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
        delete params.onTouchEnd;
    }

    this.onMouseUp(event);

    // check for double tap event
    var delta = 500; // ms
    var doubleTapEnd = (new Date()).valueOf();
    var target = links.Timeline.getTarget(event);
    var doubleTapItem = this.getItemIndex(target);
    if (params.doubleTapStartPrev &&
        (doubleTapEnd - params.doubleTapStartPrev) < delta &&
        params.doubleTapItem == params.doubleTapItemPrev) {
        params.touchDown = true;
        me.onDblClick(event);
        params.touchDown = false;
    }

    links.Timeline.preventDefault(event);
};


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

    var params = this.eventParams,
        options = this.options,
        dom = this.dom;

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

    // get mouse position
    params.mouseX = links.Timeline.getPageX(event);
    params.mouseY = links.Timeline.getPageY(event);
    params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
    params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
    params.previousLeft = 0;
    params.previousOffset = 0;

    params.moved = false;
    params.start = new Date(this.start.valueOf());
    params.end = new Date(this.end.valueOf());

    params.target = links.Timeline.getTarget(event);
    var dragLeft = (dom.items && dom.items.dragLeft) ? dom.items.dragLeft : undefined;
    var dragRight = (dom.items && dom.items.dragRight) ? dom.items.dragRight : undefined;
    params.itemDragLeft = (params.target === dragLeft);
    params.itemDragRight = (params.target === dragRight);

    if (params.itemDragLeft || params.itemDragRight) {
        params.itemIndex = (this.selection && this.selection.index !== undefined) ? this.selection.index : undefined;
        delete params.clusterIndex;
    }
    else {
        params.itemIndex = this.getItemIndex(params.target);
        params.clusterIndex = this.getClusterIndex(params.target);
    }

    params.customTime = (params.target === dom.customTime ||
        params.target.parentNode === dom.customTime) ?
        this.customTime :
        undefined;

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

        var xstart = this.screenToTime(x);
        if (options.snapEvents) {
            this.step.snap(xstart);
        }
        var xend = new Date(xstart.valueOf());
        var content = options.NEW;
        var group = this.getGroupFromHeight(y);
        this.addItem({
            'start': xstart,
            'end': xend,
            'content': content,
            'group': this.getGroupName(group)
        });
        params.itemIndex = (this.items.length - 1);
        delete params.clusterIndex;
        this.selectItem(params.itemIndex);
        params.itemDragRight = true;
    }

    var item = this.items[params.itemIndex];
    var isSelected = this.isSelected(params.itemIndex);
    params.editItem = isSelected && this.isEditable(item);
    if (params.editItem) {
        params.itemStart = item.start;
        params.itemEnd = item.end;
        params.itemGroup = item.group;
        params.itemLeft = item.getLeft(this); // NH Use item.getLeft here
        params.itemRight = item.getRight(this); // NH Use item.getRight here
    }
    else {
        this.dom.frame.style.cursor = 'move';
    }
    if (!params.touchDown) {
        // add event listeners to handle moving the contents
        // we store the function onmousemove and onmouseup in the timeline, so we can
        // remove the eventlisteners lateron in the function mouseUp()
        var me = this;
        if (!params.onMouseMove) {
            params.onMouseMove = function (event) {me.onMouseMove(event);};
            links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
        }
        if (!params.onMouseUp) {
            params.onMouseUp = function (event) {me.onMouseUp(event);};
            links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
        }

        links.Timeline.preventDefault(event);
    }
};


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

    var params = this.eventParams,
        size = this.size,
        dom = this.dom,
        options = this.options;

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

    if (params.mouseX == undefined) {
        params.mouseX = mouseX;
    }
    if (params.mouseY == undefined) {
        params.mouseY = mouseY;
    }

    var diffX = mouseX - params.mouseX;
    var diffY = mouseY - params.mouseY;

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

    if (params.customTime) {
        var x = this.timeToScreen(params.customTime);
        var xnew = x + diffX;
        this.customTime = this.screenToTime(xnew);
        this.repaintCustomTime();

        // fire a timechange event
        this.trigger('timechange');
    }
    else if (params.editItem) {
        var item = this.items[params.itemIndex],
            left,
            right;

        if (params.itemDragLeft && options.timeChangeable) {
            // move the start of the item
            left = params.itemLeft + diffX;
            right = params.itemRight;

            item.start = this.screenToTime(left);
            if (options.snapEvents) {
                this.step.snap(item.start);
                left = this.timeToScreen(item.start);
            }

            if (left > right) {
                left = right;
                item.start = this.screenToTime(left);
            }
          this.trigger('change');
        }
        else if (params.itemDragRight && options.timeChangeable) {
            // move the end of the item
            left = params.itemLeft;
            right = params.itemRight + diffX;

            item.end = this.screenToTime(right);
            if (options.snapEvents) {
                this.step.snap(item.end);
                right = this.timeToScreen(item.end);
            }

            if (right < left) {
                right = left;
                item.end = this.screenToTime(right);
            }
          this.trigger('change');
        }
        else if (options.timeChangeable) {
            // move the item
            left = params.itemLeft + diffX;
            item.start = this.screenToTime(left);
            if (options.snapEvents) {
                this.step.snap(item.start);
                left = this.timeToScreen(item.start);
            }

            if (item.end) {
                right = left + (params.itemRight - params.itemLeft);
                item.end = this.screenToTime(right);
            }
            this.trigger('change');
        }

        item.setPosition(left, right);

        var dragging = params.itemDragLeft || params.itemDragRight;
        if (this.groups.length && !dragging) {
            // move item from one group to another when needed
            var y = mouseY - params.frameTop;
            var group = this.getGroupFromHeight(y);
            if (options.groupsChangeable && item.group !== group) {
                // move item to the other group
                var index = this.items.indexOf(item);
                this.changeItem(index, {'group': this.getGroupName(group)});
            }
            else {
                this.repaintDeleteButton();
                this.repaintDragAreas();
            }
        }
        else {
            // TODO: does not work well in FF, forces redraw with every mouse move it seems
            this.render(); // TODO: optimize, only redraw the items?
            // Note: when animate==true, no redraw is needed here, its done by stackItems animation
        }
    }
    else if (options.moveable) {
        var interval = (params.end.valueOf() - params.start.valueOf());
        var diffMillisecs = Math.round((-diffX) / size.contentWidth * interval);
        var newStart = new Date(params.start.valueOf() + diffMillisecs);
        var newEnd = new Date(params.end.valueOf() + diffMillisecs);
        this.applyRange(newStart, newEnd);
        // if the applied range is moved due to a fixed min or max,
        // change the diffMillisecs accordingly
        var appliedDiff = (this.start.valueOf() - newStart.valueOf());
        if (appliedDiff) {
            diffMillisecs += appliedDiff;
        }

        this.recalcConversion();

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

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

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

        this.repaintCurrentTime();
        this.repaintCustomTime();
        this.repaintAxis();

        // fire a rangechange event
        this.trigger('rangechange');
    }

    links.Timeline.preventDefault(event);
};


/**
 * Stop moving operating.
 * This function activated from within the funcion links.Timeline.onMouseDown().
 * @param {event}  event   The event
 */
links.Timeline.prototype.onMouseUp = function (event) {
    var params = this.eventParams,
        options = this.options;

    event = event || window.event;

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

    // remove event listeners here, important for Safari
    if (params.onMouseMove) {
        links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
        delete params.onMouseMove;
    }
    if (params.onMouseUp) {
        links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
        delete params.onMouseUp;
    }
    //links.Timeline.preventDefault(event);

    if (params.customTime) {
        // fire a timechanged event
        this.trigger('timechanged');
    }
    else if (params.editItem) {
        var item = this.items[params.itemIndex];

        if (params.moved || params.addItem) {
            this.applyChange = true;
            this.applyAdd = true;

            this.updateData(params.itemIndex, {
                'start': item.start,
                'end': item.end
            });

            // fire an add or changed event.
            // Note that the change can be canceled from within an event listener if
            // this listener calls the method cancelChange().
            this.trigger(params.addItem ? 'add' : 'changed');
            
            //retrieve item data again to include changes made to it in the triggered event handlers
            item = this.items[params.itemIndex];

            if (params.addItem) {
                if (this.applyAdd) {
                    this.updateData(params.itemIndex, {
                        'start': item.start,
                        'end': item.end,
                        'content': item.content,
                        'group': this.getGroupName(item.group)
                    });
                }
                else {
                    // undo an add
                    this.deleteItem(params.itemIndex);
                }
            }
            else {
                if (this.applyChange) {
                    this.updateData(params.itemIndex, {
                        'start': item.start,
                        'end': item.end
                    });
                }
                else {
                    // undo a change
                    delete this.applyChange;
                    delete this.applyAdd;

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

                    item.start = params.itemStart;
                    item.end = params.itemEnd;
                    item.group = params.itemGroup;
                    // TODO: original group should be restored too
                    item.setPosition(params.itemLeft, params.itemRight);

                    this.updateData(params.itemIndex, {
                        'start': params.itemStart,
                        'end': params.itemEnd
                    });
                }
            }

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

            this.render();
        }
    }
    else {
        if (!params.moved && !params.zoomed) {
            // mouse did not move -> user has selected an item

            if (params.target === this.dom.items.deleteButton) {
                // delete item
                if (this.selection && this.selection.index !== undefined) {
                    this.confirmDeleteItem(this.selection.index);
                }
            }
            else if (options.selectable) {
                // select/unselect item
                if (params.itemIndex != undefined) {
                    if (!this.isSelected(params.itemIndex)) {
                        this.selectItem(params.itemIndex);
                        this.trigger('select');
                    }
                }
                else if(params.clusterIndex != undefined) {
                    this.selectCluster(params.clusterIndex);
                    this.trigger('select');
                }
                else {
                    if (options.unselectable) {
                        this.unselectItem();
                        this.trigger('select');
                    }
                }
            }
        }
        else {
            // timeline is moved
            // TODO: optimize: no need to reflow and cluster again?
            this.render();

            if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
                // fire a rangechanged event
                this.trigger('rangechanged');
            }
        }
    }
};

/**
 * Double click event occurred for an item
 * @param {Event}  event
 */
links.Timeline.prototype.onDblClick = function (event) {
    var params = this.eventParams,
        options = this.options,
        dom = this.dom,
        size = this.size;
    event = event || window.event;

    if (params.itemIndex != undefined) {
        var item = this.items[params.itemIndex];
        if (item && this.isEditable(item)) {
            // fire the edit event
            this.trigger('edit');
        }
    }
    else {
        if (options.editable) {
            // create a new item

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

            // create a new event at the current mouse position
            var xstart = this.screenToTime(x);
            if (options.snapEvents) {
                this.step.snap(xstart);
            }

            var content = options.NEW;
            var group = this.getGroupFromHeight(y);   // (group may be undefined)
            var preventRender = true;
            this.addItem({
                'start': xstart,
                'content': content,
                'group': this.getGroupName(group)
            }, preventRender);
            params.itemIndex = (this.items.length - 1);
            this.selectItem(params.itemIndex);

            this.applyAdd = true;

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

            if (this.applyAdd) {
                // render and select the item
                this.render({animate: false});
                this.selectItem(params.itemIndex);
            }
            else {
                // undo an add
                this.deleteItem(params.itemIndex);
            }
        }
    }

    links.Timeline.preventDefault(event);
};


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

    if (!event) { /* For IE. */
        event = window.event;
    }

    // retrieve delta
    var delta = 0;
    if (event.wheelDelta) { /* IE/Opera. */
        delta = event.wheelDelta/120;
    } else if (event.detail) { /* Mozilla case. */
        // In Mozilla, sign of delta is different than in IE.
        // Also, delta is multiple of 3.
        delta = -event.detail/3;
    }

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

        var timeline = this;
        var zoom = function () {
            // perform the zoom action. Delta is normally 1 or -1
            var zoomFactor = delta / 5.0;
            var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
            var mouseX = links.Timeline.getPageX(event);
            var zoomAroundDate =
                (mouseX != undefined && frameLeft != undefined) ?
                    timeline.screenToTime(mouseX - frameLeft) :
                    undefined;

            timeline.zoom(zoomFactor, zoomAroundDate);

            // fire a rangechange and a rangechanged event
            timeline.trigger("rangechange");
            timeline.trigger("rangechanged");
        };

        var scroll = function () {
            // Scroll the timeline
            timeline.move(delta * -0.2);
            timeline.trigger("rangechange");
            timeline.trigger("rangechanged");
        };

        if (event.shiftKey) {
            scroll();
        }
        else {
            zoom();
        }
    }

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


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

    // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
    // result in a start>=end )
    if (zoomFactor >= 1) {
        zoomFactor = 0.9;
    }
    if (zoomFactor <= -1) {
        zoomFactor = -0.9;
    }

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

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

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

    // only zoom in when interval is larger than minimum interval (to prevent
    // sliding to left/right when having reached the minimum zoom level)
    var interval = (newEnd.valueOf() - newStart.valueOf());
    var zoomMin = Number(this.options.zoomMin) || 10;
    if (zoomMin < 10) {
        zoomMin = 10;
    }
    if (interval >= zoomMin) {
        this.applyRange(newStart, newEnd, zoomAroundDate);
        this.render({
            animate: this.options.animate && this.options.animateZoom
        });
    }
};

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

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

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

/**
 * Apply a visible range. The range is limited to feasible maximum and minimum
 * range.
 * @param {Date} start
 * @param {Date} end
 * @param {Date}   zoomAroundDate  Optional. Date around which will be zoomed.
 */
links.Timeline.prototype.applyRange = function (start, end, zoomAroundDate) {
    // calculate new start and end value
    var startValue = start.valueOf(); // number
    var endValue = end.valueOf();     // number
    var interval = (endValue - startValue);

    // determine maximum and minimum interval
    var options = this.options;
    var year = 1000 * 60 * 60 * 24 * 365;
    var zoomMin = Number(options.zoomMin) || 10;
    if (zoomMin < 10) {
        zoomMin = 10;
    }
    var zoomMax = Number(options.zoomMax) || 10000 * year;
    if (zoomMax > 10000 * year) {
        zoomMax = 10000 * year;
    }
    if (zoomMax < zoomMin) {
        zoomMax = zoomMin;
    }

    // determine min and max date value
    var min = options.min ? options.min.valueOf() : undefined; // number
    var max = options.max ? options.max.valueOf() : undefined; // number
    if (min != undefined && max != undefined) {
        if (min >= max) {
            // empty range
            var day = 1000 * 60 * 60 * 24;
            max = min + day;
        }
        if (zoomMax > (max - min)) {
            zoomMax = (max - min);
        }
        if (zoomMin > (max - min)) {
            zoomMin = (max - min);
        }
    }

    // prevent empty interval
    if (startValue >= endValue) {
        endValue += 1000 * 60 * 60 * 24;
    }

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

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

    // prevent to small start date
    if (min != undefined) {
        var diff = (startValue - min);
        if (diff < 0) {
            startValue -= diff;
            endValue -= diff;
        }
    }

    // prevent to large end date
    if (max != undefined) {
        var diff = (max - endValue);
        if (diff < 0) {
            startValue += diff;
            endValue += diff;
        }
    }

    // apply new dates
    this.start = new Date(startValue);
    this.end = new Date(endValue);
};

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

    // select the event to be deleted
    if (!this.isSelected(index)) {
        this.selectItem(index);
    }

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

    if (this.applyDelete) {
        this.deleteItem(index);
    }

    delete this.applyDelete;
};

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

    if (this.selection && this.selection.index !== undefined) {
        // adjust the selection
        if (this.selection.index == index) {
            // item to be deleted is selected
            this.unselectItem();
        }
        else if (this.selection.index > index) {
            // update selection index
            this.selection.index--;
        }
    }

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

    // delete the row in the original data table
    if (this.data) {
        if (google && google.visualization &&
            this.data instanceof google.visualization.DataTable) {
            this.data.removeRow(index);
        }
        else if (links.Timeline.isArray(this.data)) {
            this.data.splice(index, 1);
        }
        else {
            throw "Cannot delete row from data, unknown data type";
        }
    }

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

    if (!preventRender) {
        this.render();
    }
};


/**
 * Delete all items
 */
links.Timeline.prototype.deleteAllItems = function() {
    this.unselectItem();

    // delete the loaded items
    this.clearItems();

    // delete the groups
    this.deleteGroups();

    // empty original data table
    if (this.data) {
        if (google && google.visualization &&
            this.data instanceof google.visualization.DataTable) {
            this.data.removeRows(0, this.data.getNumberOfRows());
        }
        else if (links.Timeline.isArray(this.data)) {
            this.data.splice(0, this.data.length);
        }
        else {
            throw "Cannot delete row from data, unknown data type";
        }
    }

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

    this.render();
};


/**
 * Find the group from a given height in the timeline
 * @param {Number} height   Height in the timeline
 * @return {Object | undefined} group   The group object, or undefined if out
 *                                      of range
 */
links.Timeline.prototype.getGroupFromHeight = function(height) {
    var i,
        group,
        groups = this.groups;

    if (groups.length) {
        if (this.options.axisOnTop) {
            for (i = groups.length - 1; i >= 0; i--) {
                group = groups[i];
                if (height > group.top) {
                    return group;
                }
            }
        }
        else {
            for (i = 0; i < groups.length; i++) {
                group = groups[i];
                if (height > group.top) {
                    return group;
                }
            }
        }

        return group; // return the last group
    }

    return undefined;
};

/**
 * @constructor links.Timeline.Item
 * @param {Object} data       Object containing parameters start, end
 *                            content, group, type, editable.
 * @param {Object} [options]  Options to set initial property values
 *                                {Number} top
 *                                {Number} left
 *                                {Number} width
 *                                {Number} height
 */
links.Timeline.Item = function (data, options) {
    if (data) {
        /* TODO: use parseJSONDate as soon as it is tested and working (in two directions)
         this.start = links.Timeline.parseJSONDate(data.start);
         this.end = links.Timeline.parseJSONDate(data.end);
         */
        this.start = data.start;
        this.end = data.end;
        this.content = data.content;
        this.className = data.className;
        this.editable = data.editable;
        this.group = data.group;
        this.type = data.type;
    }
    this.top = 0;
    this.left = 0;
    this.width = 0;
    this.height = 0;
    this.lineWidth = 0;
    this.dotWidth = 0;
    this.dotHeight = 0;

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

    if (options) {
        // override the default properties
        for (var option in options) {
            if (options.hasOwnProperty(option)) {
                this[option] = options[option];
            }
        }
    }

};



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

/**
 * Append all image urls present in the items DOM to the provided array
 * @param {String[]} imageUrls
 */
links.Timeline.Item.prototype.getImageUrls = function (imageUrls) {
    if (this.dom) {
        links.imageloader.filterImageUrls(this.dom, imageUrls);
    }
};

/**
 * Select the item
 */
links.Timeline.Item.prototype.select = function () {
    // Should be implemented by sub-prototype
};

/**
 * Unselect the item
 */
links.Timeline.Item.prototype.unselect = function () {
    // Should be implemented by sub-prototype
};

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

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

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

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

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

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

/**
 * Check if the item is located in the visible area of the timeline, and
 * not part of a cluster
 * @param {Date} start
 * @param {Date} end
 * @return {boolean} visible
 */
links.Timeline.Item.prototype.isVisible = function (start, end) {
    // Should be implemented by sub-prototype
    return false;
};

/**
 * Reposition the item
 * @param {Number} left
 * @param {Number} right
 */
links.Timeline.Item.prototype.setPosition = function (left, right) {
    // Should be implemented by sub-prototype
};

/**
 * Calculate the left position of the item
 * @param {links.Timeline} timeline
 * @return {Number} left
 */
links.Timeline.Item.prototype.getLeft = function (timeline) {
    // Should be implemented by sub-prototype
    return 0;
};

/**
 * Calculate the right position of the item
 * @param {links.Timeline} timeline
 * @return {Number} right
 */
links.Timeline.Item.prototype.getRight = function (timeline) {
    // Should be implemented by sub-prototype
    return 0;
};

/**
 * Calculate the width of the item
 * @param {links.Timeline} timeline
 * @return {Number} width
 */
links.Timeline.Item.prototype.getWidth = function (timeline) {
    // Should be implemented by sub-prototype
    return this.width || 0; // last rendered width
};


/**
 * @constructor links.Timeline.ItemBox
 * @extends links.Timeline.Item
 * @param {Object} data       Object containing parameters start, end
 *                            content, group, type, className, editable.
 * @param {Object} [options]  Options to set initial property values
 *                                {Number} top
 *                                {Number} left
 *                                {Number} width
 *                                {Number} height
 */
links.Timeline.ItemBox = function (data, options) {
    links.Timeline.Item.call(this, data, options);
};

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

/**
 * Reflow the Item: retrieve its actual size from the DOM
 * @return {boolean} resized    returns true if the axis is resized
 * @override
 */
links.Timeline.ItemBox.prototype.reflow = function () {
    var dom = this.dom,
        dotHeight = dom.dot.offsetHeight,
        dotWidth = dom.dot.offsetWidth,
        lineWidth = dom.line.offsetWidth,
        resized = (
            (this.dotHeight != dotHeight) ||
                (this.dotWidth != dotWidth) ||
                (this.lineWidth != lineWidth)
            );

    this.dotHeight = dotHeight;
    this.dotWidth = dotWidth;
    this.lineWidth = lineWidth;

    return resized;
};

/**
 * Select the item
 * @override
 */
links.Timeline.ItemBox.prototype.select = function () {
    var dom = this.dom;
    links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
    links.Timeline.addClassName(dom.line, 'timeline-event-selected ui-state-active');
    links.Timeline.addClassName(dom.dot, 'timeline-event-selected ui-state-active');
};

/**
 * Unselect the item
 * @override
 */
links.Timeline.ItemBox.prototype.unselect = function () {
    var dom = this.dom;
    links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
    links.Timeline.removeClassName(dom.line, 'timeline-event-selected ui-state-active');
    links.Timeline.removeClassName(dom.dot, 'timeline-event-selected ui-state-active');
};

/**
 * Creates the DOM for the item, depending on its type
 * @return {Element | undefined}
 * @override
 */
links.Timeline.ItemBox.prototype.createDOM = function () {
    // background box
    var divBox = document.createElement("DIV");
    divBox.style.position = "absolute";
    divBox.style.left = this.left + "px";
    divBox.style.top = this.top + "px";

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

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

    // dot on axis
    var divDot = document.createElement("DIV");
    divDot.style.position = "absolute";
    divDot.style.width  = "0px";
    divDot.style.height = "0px";
    divBox.dot = divDot;

    this.dom = divBox;
    this.updateDOM();

    return divBox;
};

/**
 * Append the items DOM to the given HTML container. If items DOM does not yet
 * exist, it will be created first.
 * @param {Element} container
 * @override
 */
links.Timeline.ItemBox.prototype.showDOM = function (container) {
    var dom = this.dom;
    if (!dom) {
        dom = this.createDOM();
    }

    if (dom.parentNode != container) {
        if (dom.parentNode) {
            // container is changed. remove from old container
            this.hideDOM();
        }

        // append to this container
        container.appendChild(dom);
        container.insertBefore(dom.line, container.firstChild);
        // Note: line must be added in front of the this,
        //       such that it stays below all this
        container.appendChild(dom.dot);
        this.rendered = true;
    }
};

/**
 * Remove the items DOM from the current HTML container, but keep the DOM in
 * memory
 * @override
 */
links.Timeline.ItemBox.prototype.hideDOM = function () {
    var dom = this.dom;
    if (dom) {
        if (dom.parentNode) {
            dom.parentNode.removeChild(dom);
        }
        if (dom.line && dom.line.parentNode) {
            dom.line.parentNode.removeChild(dom.line);
        }
        if (dom.dot && dom.dot.parentNode) {
            dom.dot.parentNode.removeChild(dom.dot);
        }
        this.rendered = false;
    }
};

/**
 * Update the DOM of the item. This will update the content and the classes
 * of the item
 * @override
 */
links.Timeline.ItemBox.prototype.updateDOM = function () {
    var divBox = this.dom;
    if (divBox) {
        var divLine = divBox.line;
        var divDot = divBox.dot;

        // update contents
        divBox.firstChild.innerHTML = this.content;

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

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

        // add item specific class name when provided
        if (this.className) {
            links.Timeline.addClassName(divBox, this.className);
            links.Timeline.addClassName(divLine, this.className);
            links.Timeline.addClassName(divDot, this.className);
        }

        // TODO: apply selected className?
    }
};

/**
 * Reposition the item, recalculate its left, top, and width, using the current
 * range of the timeline and the timeline options.
 * @param {links.Timeline} timeline
 * @override
 */
links.Timeline.ItemBox.prototype.updatePosition = function (timeline) {
    var dom = this.dom;
    if (dom) {
        var left = timeline.timeToScreen(this.start),
            axisOnTop = timeline.options.axisOnTop,
            axisTop = timeline.size.axis.top,
            axisHeight = timeline.size.axis.height,
            boxAlign = (timeline.options.box && timeline.options.box.align) ?
                timeline.options.box.align : undefined;

        dom.style.top = this.top + "px";
        if (boxAlign == 'right') {
            dom.style.left = (left - this.width) + "px";
        }
        else if (boxAlign == 'left') {
            dom.style.left = (left) + "px";
        }
        else { // default or 'center'
            dom.style.left = (left - this.width/2) + "px";
        }

        var line = dom.line;
        var dot = dom.dot;
        line.style.left = (left - this.lineWidth/2) + "px";
        dot.style.left = (left - this.dotWidth/2) + "px";
        if (axisOnTop) {
            line.style.top = axisHeight + "px";
            line.style.height = Math.max(this.top - axisHeight, 0) + "px";
            dot.style.top = (axisHeight - this.dotHeight/2) + "px";
        }
        else {
            line.style.top = (this.top + this.height) + "px";
            line.style.height = Math.max(axisTop - this.top - this.height, 0) + "px";
            dot.style.top = (axisTop - this.dotHeight/2) + "px";
        }
    }
};

/**
 * Check if the item is visible in the timeline, and not part of a cluster
 * @param {Date} start
 * @param {Date} end
 * @return {Boolean} visible
 * @override
 */
links.Timeline.ItemBox.prototype.isVisible = function (start, end) {
    if (this.cluster) {
        return false;
    }

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

/**
 * Reposition the item
 * @param {Number} left
 * @param {Number} right
 * @override
 */
links.Timeline.ItemBox.prototype.setPosition = function (left, right) {
    var dom = this.dom;

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

    if (this.group) {
        this.top = this.group.top;
        dom.style.top = this.top + 'px';
    }
};

/**
 * Calculate the left position of the item
 * @param {links.Timeline} timeline
 * @return {Number} left
 * @override
 */
links.Timeline.ItemBox.prototype.getLeft = function (timeline) {
    var boxAlign = (timeline.options.box && timeline.options.box.align) ?
        timeline.options.box.align : undefined;

    var left = timeline.timeToScreen(this.start);
    if (boxAlign == 'right') {
        left = left - width;
    }
    else { // default or 'center'
        left = (left - this.width / 2);
    }

    return left;
};

/**
 * Calculate the right position of the item
 * @param {links.Timeline} timeline
 * @return {Number} right
 * @override
 */
links.Timeline.ItemBox.prototype.getRight = function (timeline) {
    var boxAlign = (timeline.options.box && timeline.options.box.align) ?
        timeline.options.box.align : undefined;

    var left = timeline.timeToScreen(this.start);
    var right;
    if (boxAlign == 'right') {
        right = left;
    }
    else if (boxAlign == 'left') {
        right = (left + this.width);
    }
    else { // default or 'center'
        right = (left + this.width / 2);
    }

    return right;
};

/**
 * @constructor links.Timeline.ItemRange
 * @extends links.Timeline.Item
 * @param {Object} data       Object containing parameters start, end
 *                            content, group, type, className, editable.
 * @param {Object} [options]  Options to set initial property values
 *                                {Number} top
 *                                {Number} left
 *                                {Number} width
 *                                {Number} height
 */
links.Timeline.ItemRange = function (data, options) {
    links.Timeline.Item.call(this, data, options);
};

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

/**
 * Select the item
 * @override
 */
links.Timeline.ItemRange.prototype.select = function () {
    var dom = this.dom;
    links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
};

/**
 * Unselect the item
 * @override
 */
links.Timeline.ItemRange.prototype.unselect = function () {
    var dom = this.dom;
    links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
};

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

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

    this.dom = divBox;
    this.updateDOM();

    return divBox;
};

/**
 * Append the items DOM to the given HTML container. If items DOM does not yet
 * exist, it will be created first.
 * @param {Element} container
 * @override
 */
links.Timeline.ItemRange.prototype.showDOM = function (container) {
    var dom = this.dom;
    if (!dom) {
        dom = this.createDOM();
    }

    if (dom.parentNode != container) {
        if (dom.parentNode) {
            // container changed. remove the item from the old container
            this.hideDOM();
        }

        // append to the new container
        container.appendChild(dom);
        this.rendered = true;
    }
};

/**
 * Remove the items DOM from the current HTML container
 * The DOM will be kept in memory
 * @override
 */
links.Timeline.ItemRange.prototype.hideDOM = function () {
    var dom = this.dom;
    if (dom) {
        if (dom.parentNode) {
            dom.parentNode.removeChild(dom);
        }
        this.rendered = false;
    }
};

/**
 * Update the DOM of the item. This will update the content and the classes
 * of the item
 * @override
 */
links.Timeline.ItemRange.prototype.updateDOM = function () {
    var divBox = this.dom;
    if (divBox) {
        // update contents
        divBox.firstChild.innerHTML = this.content;

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

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

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

        // TODO: apply selected className?
    }
};

/**
 * Reposition the item, recalculate its left, top, and width, using the current
 * range of the timeline and the timeline options. *
 * @param {links.Timeline} timeline
 * @override
 */
links.Timeline.ItemRange.prototype.updatePosition = function (timeline) {
    var dom = this.dom;
    if (dom) {
        var contentWidth = timeline.size.contentWidth,
            left = timeline.timeToScreen(this.start),
            right = timeline.timeToScreen(this.end);

        // limit the width of the this, as browsers cannot draw very wide divs
        if (left < -contentWidth) {
            left = -contentWidth;
        }
        if (right > 2 * contentWidth) {
            right = 2 * contentWidth;
        }

        dom.style.top = this.top + "px";
        dom.style.left = left + "px";
        //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth
        dom.style.width = Math.max(right - left, 1) + "px";
    }
};

/**
 * Check if the item is visible in the timeline, and not part of a cluster
 * @param {Number} start
 * @param {Number} end
 * @return {boolean} visible
 * @override
 */
links.Timeline.ItemRange.prototype.isVisible = function (start, end) {
    if (this.cluster) {
        return false;
    }

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

/**
 * Reposition the item
 * @param {Number} left
 * @param {Number} right
 * @override
 */
links.Timeline.ItemRange.prototype.setPosition = function (left, right) {
    var dom = this.dom;

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

    if (this.group) {
        this.top = this.group.top;
        dom.style.top = this.top + 'px';
    }
};

/**
 * Calculate the left position of the item
 * @param {links.Timeline} timeline
 * @return {Number} left
 * @override
 */
links.Timeline.ItemRange.prototype.getLeft = function (timeline) {
    return timeline.timeToScreen(this.start);
};

/**
 * Calculate the right position of the item
 * @param {links.Timeline} timeline
 * @return {Number} right
 * @override
 */
links.Timeline.ItemRange.prototype.getRight = function (timeline) {
    return timeline.timeToScreen(this.end);
};

/**
 * Calculate the width of the item
 * @param {links.Timeline} timeline
 * @return {Number} width
 * @override
 */
links.Timeline.ItemRange.prototype.getWidth = function (timeline) {
    return timeline.timeToScreen(this.end) - timeline.timeToScreen(this.start);
};

/**
 * @constructor links.Timeline.ItemFloatingRange
 * @extends links.Timeline.Item
 * @param {Object} data       Object containing parameters start, end
 *                            content, group, type, className, editable.
 * @param {Object} [options]  Options to set initial property values
 *                                {Number} top
 *                                {Number} left
 *                                {Number} width
 *                                {Number} height
 */
links.Timeline.ItemFloatingRange = function (data, options) {
    links.Timeline.Item.call(this, data, options);
};

links.Timeline.ItemFloatingRange.prototype = new links.Timeline.Item();

/**
 * Select the item
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.select = function () {
    var dom = this.dom;
    links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
};

/**
 * Unselect the item
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.unselect = function () {
    var dom = this.dom;
    links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
};

/**
 * Creates the DOM for the item, depending on its type
 * @return {Element | undefined}
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.createDOM = function () {
    // background box
    var divBox = document.createElement("DIV");
    divBox.style.position = "absolute";

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

    this.dom = divBox;
    this.updateDOM();

    return divBox;
};

/**
 * Append the items DOM to the given HTML container. If items DOM does not yet
 * exist, it will be created first.
 * @param {Element} container
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.showDOM = function (container) {
    var dom = this.dom;
    if (!dom) {
        dom = this.createDOM();
    }

    if (dom.parentNode != container) {
        if (dom.parentNode) {
            // container changed. remove the item from the old container
            this.hideDOM();
        }

        // append to the new container
        container.appendChild(dom);
        this.rendered = true;
    }
};

/**
 * Remove the items DOM from the current HTML container
 * The DOM will be kept in memory
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.hideDOM = function () {
    var dom = this.dom;
    if (dom) {
        if (dom.parentNode) {
            dom.parentNode.removeChild(dom);
        }
        this.rendered = false;
    }
};

/**
 * Update the DOM of the item. This will update the content and the classes
 * of the item
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.updateDOM = function () {
    var divBox = this.dom;
    if (divBox) {
        // update contents
        divBox.firstChild.innerHTML = this.content;

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

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

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

        // TODO: apply selected className?
    }
};

/**
 * Reposition the item, recalculate its left, top, and width, using the current
 * range of the timeline and the timeline options. *
 * @param {links.Timeline} timeline
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.updatePosition = function (timeline) {
    var dom = this.dom;
    if (dom) {
        var contentWidth = timeline.size.contentWidth,
            left = this.getLeft(timeline), // NH use getLeft
            right = this.getRight(timeline); // NH use getRight;

        // limit the width of the this, as browsers cannot draw very wide divs
        if (left < -contentWidth) {
            left = -contentWidth;
        }
        if (right > 2 * contentWidth) {
            right = 2 * contentWidth;
        }

        dom.style.top = this.top + "px";
        dom.style.left = left + "px";
        //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth
        dom.style.width = Math.max(right - left, 1) + "px";
    }
};

/**
 * Check if the item is visible in the timeline, and not part of a cluster
 * @param {Number} start
 * @param {Number} end
 * @return {boolean} visible
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.isVisible = function (start, end) {
    if (this.cluster) {
        return false;
    }

	// NH check for no end value
	if (this.end && this.start) {
		return (this.end > start)
			&& (this.start < end);
	} else if (this.start) {
		return (this.start < end);
	} else if (this.end) {
        return (this.end > start);
    } else {return true;}
};

/**
 * Reposition the item
 * @param {Number} left
 * @param {Number} right
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.setPosition = function (left, right) {
    var dom = this.dom;

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

    if (this.group) {
        this.top = this.group.top;
        dom.style.top = this.top + 'px';
    }
};

/**
 * Calculate the left position of the item
 * @param {links.Timeline} timeline
 * @return {Number} left
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.getLeft = function (timeline) {
    // NH check for no start value
	if (this.start) {
		return timeline.timeToScreen(this.start);
	} else {
		return 0;
	}
};

/**
 * Calculate the right position of the item
 * @param {links.Timeline} timeline
 * @return {Number} right
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.getRight = function (timeline) {
    // NH check for no end value
	if (this.end) {
		return timeline.timeToScreen(this.end);
	} else {
		return timeline.size.contentWidth;
	}
};

/**
 * Calculate the width of the item
 * @param {links.Timeline} timeline
 * @return {Number} width
 * @override
 */
links.Timeline.ItemFloatingRange.prototype.getWidth = function (timeline) {
    return this.getRight(timeline) - this.getLeft(timeline);
};

/**
 * @constructor links.Timeline.ItemDot
 * @extends links.Timeline.Item
 * @param {Object} data       Object containing parameters start, end
 *                            content, group, type, className, editable.
 * @param {Object} [options]  Options to set initial property values
 *                                {Number} top
 *                                {Number} left
 *                                {Number} width
 *                                {Number} height
 */
links.Timeline.ItemDot = function (data, options) {
    links.Timeline.Item.call(this, data, options);
};

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

/**
 * Reflow the Item: retrieve its actual size from the DOM
 * @return {boolean} resized    returns true if the axis is resized
 * @override
 */
links.Timeline.ItemDot.prototype.reflow = function () {
    var dom = this.dom,
        dotHeight = dom.dot.offsetHeight,
        dotWidth = dom.dot.offsetWidth,
        contentHeight = dom.content.offsetHeight,
        resized = (
            (this.dotHeight != dotHeight) ||
                (this.dotWidth != dotWidth) ||
                (this.contentHeight != contentHeight)
            );

    this.dotHeight = dotHeight;
    this.dotWidth = dotWidth;
    this.contentHeight = contentHeight;

    return resized;
};

/**
 * Select the item
 * @override
 */
links.Timeline.ItemDot.prototype.select = function () {
    var dom = this.dom;
    links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
};

/**
 * Unselect the item
 * @override
 */
links.Timeline.ItemDot.prototype.unselect = function () {
    var dom = this.dom;
    links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
};

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

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

    // dot at start
    var divDot = document.createElement("DIV");
    divDot.style.position = "absolute";
    divDot.style.width = "0px";
    divDot.style.height = "0px";
    divBox.appendChild(divDot);

    divBox.content = divContent;
    divBox.dot = divDot;

    this.dom = divBox;
    this.updateDOM();

    return divBox;
};

/**
 * Append the items DOM to the given HTML container. If items DOM does not yet
 * exist, it will be created first.
 * @param {Element} container
 * @override
 */
links.Timeline.ItemDot.prototype.showDOM = function (container) {
    var dom = this.dom;
    if (!dom) {
        dom = this.createDOM();
    }

    if (dom.parentNode != container) {
        if (dom.parentNode) {
            // container changed. remove it from old container first
            this.hideDOM();
        }

        // append to container
        container.appendChild(dom);
        this.rendered = true;
    }
};

/**
 * Remove the items DOM from the current HTML container
 * @override
 */
links.Timeline.ItemDot.prototype.hideDOM = function () {
    var dom = this.dom;
    if (dom) {
        if (dom.parentNode) {
            dom.parentNode.removeChild(dom);
        }
        this.rendered = false;
    }
};

/**
 * Update the DOM of the item. This will update the content and the classes
 * of the item
 * @override
 */
links.Timeline.ItemDot.prototype.updateDOM = function () {
    if (this.dom) {
        var divBox = this.dom;
        var divDot = divBox.dot;

        // update contents
        divBox.firstChild.innerHTML = this.content;

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

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

        // add item specific class name when provided
        if (this.className) {
            links.Timeline.addClassName(divBox, this.className);
            links.Timeline.addClassName(divDot, this.className);
        }

        // TODO: apply selected className?
    }
};

/**
 * Reposition the item, recalculate its left, top, and width, using the current
 * range of the timeline and the timeline options. *
 * @param {links.Timeline} timeline
 * @override
 */
links.Timeline.ItemDot.prototype.updatePosition = function (timeline) {
    var dom = this.dom;
    if (dom) {
        var left = timeline.timeToScreen(this.start);

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

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

/**
 * Check if the item is visible in the timeline, and not part of a cluster.
 * @param {Date} start
 * @param {Date} end
 * @return {boolean} visible
 * @override
 */
links.Timeline.ItemDot.prototype.isVisible = function (start, end) {
    if (this.cluster) {
        return false;
    }

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

/**
 * Reposition the item
 * @param {Number} left
 * @param {Number} right
 * @override
 */
links.Timeline.ItemDot.prototype.setPosition = function (left, right) {
    var dom = this.dom;

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

    if (this.group) {
        this.top = this.group.top;
        dom.style.top = this.top + 'px';
    }
};

/**
 * Calculate the left position of the item
 * @param {links.Timeline} timeline
 * @return {Number} left
 * @override
 */
links.Timeline.ItemDot.prototype.getLeft = function (timeline) {
    return timeline.timeToScreen(this.start);
};

/**
 * Calculate the right position of the item
 * @param {links.Timeline} timeline
 * @return {Number} right
 * @override
 */
links.Timeline.ItemDot.prototype.getRight = function (timeline) {
    return timeline.timeToScreen(this.start) + this.width;
};

/**
 * Retrieve the properties of an item.
 * @param {Number} index
 * @return {Object} itemData    Object containing item properties:
* {Date} start (required), * {Date} end (optional), * {String} content (required), * {String} group (optional), * {String} className (optional) * {boolean} editable (optional) * {String} type (optional) */ links.Timeline.prototype.getItem = function (index) { if (index >= this.items.length) { throw "Cannot get item, index out of range"; } // take the original data as start, includes foreign fields var data = this.data, itemData; if (google && google.visualization && data instanceof google.visualization.DataTable) { // map the datatable columns var cols = links.Timeline.mapColumnIds(data); itemData = {}; for (var col in cols) { if (cols.hasOwnProperty(col)) { itemData[col] = this.data.getValue(index, cols[col]); } } } else if (links.Timeline.isArray(this.data)) { // read JSON array itemData = links.Timeline.clone(this.data[index]); } else { throw "Unknown data type. DataTable or Array expected."; } // override the data with current settings of the item (should be the same) var item = this.items[index]; itemData.start = new Date(item.start.valueOf()); if (item.end) { itemData.end = new Date(item.end.valueOf()); } itemData.content = item.content; if (item.group) { itemData.group = this.getGroupName(item.group); } if (item.className) { itemData.className = item.className; } if (typeof item.editable !== 'undefined') { itemData.editable = item.editable; } if (item.type) { itemData.type = item.type; } return itemData; }; /** * Retrieve the properties of a cluster. * @param {Number} index * @return {Object} clusterdata Object containing cluster properties:
* {Date} start (required), * {String} type (optional) * {Array} array with item data as is in getItem() */ links.Timeline.prototype.getCluster = function (index) { if (index >= this.clusters.length) { throw "Cannot get cluster, index out of range"; } var clusterData = {}, cluster = this.clusters[index], clusterItems = cluster.items; clusterData.start = new Date(cluster.start.valueOf()); if (cluster.type) { clusterData.type = cluster.type; } // push cluster item data clusterData.items = []; for(var i = 0; i < clusterItems.length; i++){ for(var j = 0; j < this.items.length; j++){ // TODO could be nicer to be able to have the item index into the cluster if(this.items[j] == clusterItems[i]) { clusterData.items.push(this.getItem(j)); break; } } } return clusterData; }; /** * Add a new item. * @param {Object} itemData Object containing item properties:
* {Date} start (required), * {Date} end (optional), * {String} content (required), * {String} group (optional) * {String} className (optional) * {Boolean} editable (optional) * {String} type (optional) * @param {boolean} [preventRender=false] Do not re-render timeline if true */ links.Timeline.prototype.addItem = function (itemData, preventRender) { var itemsData = [ itemData ]; this.addItems(itemsData, preventRender); }; /** * Add new items. * @param {Array} itemsData An array containing Objects. * The objects must have the following parameters: * {Date} start, * {Date} end, * {String} content with text or HTML code, * {String} group (optional) * {String} className (optional) * {String} editable (optional) * {String} type (optional) * @param {boolean} [preventRender=false] Do not re-render timeline if true */ links.Timeline.prototype.addItems = function (itemsData, preventRender) { var timeline = this, items = this.items; // append the items itemsData.forEach(function (itemData) { var index = items.length; items.push(timeline.createItem(itemData)); timeline.updateData(index, itemData); // note: there is no need to add the item to the renderQueue, that // will be done when this.render() is executed and all items are // filtered again. }); // prepare data for clustering, by filtering and sorting by type if (this.options.cluster) { this.clusterGenerator.updateData(); } if (!preventRender) { this.render({ animate: false }); } }; /** * Create an item object, containing all needed parameters * @param {Object} itemData Object containing parameters start, end * content, group. * @return {Object} item */ links.Timeline.prototype.createItem = function(itemData) { var type = itemData.type || (itemData.end ? 'range' : this.options.style); var data = links.Timeline.clone(itemData); data.type = type; data.group = this.getGroup(itemData.group); // TODO: optimize this, when creating an item, all data is copied twice... // TODO: is initialTop needed? var initialTop, options = this.options; if (options.axisOnTop) { initialTop = this.size.axis.height + options.eventMarginAxis + options.eventMargin / 2; } else { initialTop = this.size.contentHeight - options.eventMarginAxis - options.eventMargin / 2; } if (type in this.itemTypes) { return new this.itemTypes[type](data, {'top': initialTop}) } console.log('ERROR: Unknown event type "' + type + '"'); return new links.Timeline.Item(data, { 'top': initialTop }); }; /** * Edit an item * @param {Number} index * @param {Object} itemData Object containing item properties:
* {Date} start (required), * {Date} end (optional), * {String} content (required), * {String} group (optional) * @param {boolean} [preventRender=false] Do not re-render timeline if true */ links.Timeline.prototype.changeItem = function (index, itemData, preventRender) { var oldItem = this.items[index]; if (!oldItem) { throw "Cannot change item, index out of range"; } // replace item, merge the changes var newItem = this.createItem({ 'start': itemData.hasOwnProperty('start') ? itemData.start : oldItem.start, 'end': itemData.hasOwnProperty('end') ? itemData.end : oldItem.end, 'content': itemData.hasOwnProperty('content') ? itemData.content : oldItem.content, 'group': itemData.hasOwnProperty('group') ? itemData.group : this.getGroupName(oldItem.group), 'className': itemData.hasOwnProperty('className') ? itemData.className : oldItem.className, 'editable': itemData.hasOwnProperty('editable') ? itemData.editable : oldItem.editable, 'type': itemData.hasOwnProperty('type') ? itemData.type : (itemData.end ? 'range' : 'box') }); this.items[index] = newItem; // append the changes to the render queue this.renderQueue.hide.push(oldItem); this.renderQueue.show.push(newItem); // update the original data table this.updateData(index, itemData); // prepare data for clustering, by filtering and sorting by type if (this.options.cluster) { this.clusterGenerator.updateData(); } if (!preventRender) { // redraw timeline this.render({ animate: false }); if (this.selection && this.selection.index == index) { newItem.select(); } } }; /** * Delete all groups */ links.Timeline.prototype.deleteGroups = function () { this.groups = []; this.groupIndexes = {}; }; /** * Get a group by the group name. When the group does not exist, * it will be created. * @param {String} groupName the name of the group * @return {Object} groupObject */ links.Timeline.prototype.getGroup = function (groupName) { var groups = this.groups, groupIndexes = this.groupIndexes, groupObj = undefined; var groupIndex = groupIndexes[groupName]; if (groupIndex == undefined && groupName != undefined) { // not null or undefined groupObj = { 'content': groupName, 'labelTop': 0, 'lineTop': 0 // note: this object will lateron get addition information, // such as height and width of the group }; groups.push(groupObj); // sort the groups if (this.options.groupsOrder == true) { groups = groups.sort(function (a, b) { if (a.content > b.content) { return 1; } if (a.content < b.content) { return -1; } return 0; }); } else if (typeof(this.options.groupsOrder) == "function") { groups = groups.sort(this.options.groupsOrder) } // rebuilt the groupIndexes for (var i = 0, iMax = groups.length; i < iMax; i++) { groupIndexes[groups[i].content] = i; } } else { groupObj = groups[groupIndex]; } return groupObj; }; /** * Get the group name from a group object. * @param {Object} groupObj * @return {String} groupName the name of the group, or undefined when group * was not provided */ links.Timeline.prototype.getGroupName = function (groupObj) { return groupObj ? groupObj.content : undefined; }; /** * Cancel a change item * This method can be called insed an event listener which catches the "change" * event. The changed event position will be undone. */ links.Timeline.prototype.cancelChange = function () { this.applyChange = false; }; /** * Cancel deletion of an item * This method can be called insed an event listener which catches the "delete" * event. Deletion of the event will be undone. */ links.Timeline.prototype.cancelDelete = function () { this.applyDelete = false; }; /** * Cancel creation of a new item * This method can be called insed an event listener which catches the "new" * event. Creation of the new the event will be undone. */ links.Timeline.prototype.cancelAdd = function () { this.applyAdd = false; }; /** * Select an event. The visible chart range will be moved such that the selected * event is placed in the middle. * For example selection = [{row: 5}]; * @param {Array} selection An array with a column row, containing the row * number (the id) of the event to be selected. * @return {boolean} true if selection is succesfully set, else false. */ links.Timeline.prototype.setSelection = function(selection) { if (selection != undefined && selection.length > 0) { if (selection[0].row != undefined) { var index = selection[0].row; if (this.items[index]) { var item = this.items[index]; this.selectItem(index); // move the visible chart range to the selected event. var start = item.start; var end = item.end; var middle; // number if (end != undefined) { middle = (end.valueOf() + start.valueOf()) / 2; } else { middle = start.valueOf(); } var diff = (this.end.valueOf() - this.start.valueOf()), newStart = new Date(middle - diff/2), newEnd = new Date(middle + diff/2); this.setVisibleChartRange(newStart, newEnd); return true; } } } else { // unselect current selection this.unselectItem(); } return false; }; /** * Retrieve the currently selected event * @return {Array} sel An array with a column row, containing the row number * of the selected event. If there is no selection, an * empty array is returned. */ links.Timeline.prototype.getSelection = function() { var sel = []; if (this.selection) { if(this.selection.index !== undefined) { sel.push({"row": this.selection.index}); } else { sel.push({"cluster": this.selection.cluster}); } } return sel; }; /** * Select an item by its index * @param {Number} index */ links.Timeline.prototype.selectItem = function(index) { this.unselectItem(); this.selection = undefined; if (this.items[index] != undefined) { var item = this.items[index], domItem = item.dom; this.selection = { 'index': index }; if (item && item.dom) { // TODO: move adjusting the domItem to the item itself if (this.isEditable(item)) { item.dom.style.cursor = 'move'; } item.select(); } this.repaintDeleteButton(); this.repaintDragAreas(); } }; /** * Select an cluster by its index * @param {Number} index */ links.Timeline.prototype.selectCluster = function(index) { this.unselectItem(); this.selection = undefined; if (this.clusters[index] != undefined) { this.selection = { 'cluster': index }; this.repaintDeleteButton(); this.repaintDragAreas(); } }; /** * Check if an item is currently selected * @param {Number} index * @return {boolean} true if row is selected, else false */ links.Timeline.prototype.isSelected = function (index) { return (this.selection && this.selection.index == index); }; /** * Unselect the currently selected event (if any) */ links.Timeline.prototype.unselectItem = function() { if (this.selection && this.selection.index !== undefined) { var item = this.items[this.selection.index]; if (item && item.dom) { var domItem = item.dom; domItem.style.cursor = ''; item.unselect(); } this.selection = undefined; this.repaintDeleteButton(); this.repaintDragAreas(); } }; /** * Stack the items such that they don't overlap. The items will have a minimal * distance equal to options.eventMargin. * @param {boolean | undefined} animate if animate is true, the items are * moved to their new position animated * defaults to false. */ links.Timeline.prototype.stackItems = function(animate) { if (animate == undefined) { animate = false; } // calculate the order and final stack position of the items var stack = this.stack; if (!stack) { stack = {}; this.stack = stack; } stack.sortedItems = this.stackOrder(this.renderedItems); stack.finalItems = this.stackCalculateFinal(stack.sortedItems); if (animate || stack.timer) { // move animated to the final positions var timeline = this; var step = function () { var arrived = timeline.stackMoveOneStep(stack.sortedItems, stack.finalItems); timeline.repaint(); if (!arrived) { stack.timer = setTimeout(step, 30); } else { delete stack.timer; } }; if (!stack.timer) { stack.timer = setTimeout(step, 30); } } else { // move immediately to the final positions this.stackMoveToFinal(stack.sortedItems, stack.finalItems); } }; /** * Cancel any running animation */ links.Timeline.prototype.stackCancelAnimation = function() { if (this.stack && this.stack.timer) { clearTimeout(this.stack.timer); delete this.stack.timer; } }; links.Timeline.prototype.getItemsByGroup = function(items) { var itemsByGroup = {}; for (var i = 0; i < items.length; ++i) { var item = items[i]; var group = "undefined"; if (item.group) { if (item.group.content) { group = item.group.content; } else { group = item.group; } } if (!itemsByGroup[group]) { itemsByGroup[group] = []; } itemsByGroup[group].push(item); } return itemsByGroup; }; /** * Order the items in the array this.items. The default order is determined via: * - Ranges go before boxes and dots. * - The item with the oldest start time goes first * If a custom function has been provided via the stackorder option, then this will be used. * @param {Array} items Array with items * @return {Array} sortedItems Array with sorted items */ links.Timeline.prototype.stackOrder = function(items) { // TODO: store the sorted items, to have less work later on var sortedItems = items.concat([]); //if a customer stack order function exists, use it. var f = this.options.customStackOrder && (typeof this.options.customStackOrder === 'function') ? this.options.customStackOrder : function (a, b) { if ((a instanceof links.Timeline.ItemRange || a instanceof links.Timeline.ItemFloatingRange) && !(b instanceof links.Timeline.ItemRange || b instanceof links.Timeline.ItemFloatingRange)) { return -1; } if (!(a instanceof links.Timeline.ItemRange || a instanceof links.Timeline.ItemFloatingRange) && (b instanceof links.Timeline.ItemRange || b instanceof links.Timeline.ItemFloatingRange)) { return 1; } return (a.left - b.left); }; sortedItems.sort(f); return sortedItems; }; /** * Adjust vertical positions of the events such that they don't overlap each * other. * @param {timeline.Item[]} items * @return {Object[]} finalItems */ links.Timeline.prototype.stackCalculateFinal = function(items) { var size = this.size, options = this.options, axisOnTop = options.axisOnTop, eventMargin = options.eventMargin, eventMarginAxis = options.eventMarginAxis, groupBase = (axisOnTop) ? size.axis.height + eventMarginAxis + eventMargin/2 : size.contentHeight - eventMarginAxis - eventMargin/2, groupedItems, groupFinalItems, finalItems = []; groupedItems = this.getItemsByGroup(items); // // groupedItems contains all items by group, plus it may contain an // additional "undefined" group which contains all items with no group. We // first process the grouped items, and then the ungrouped // for (j = 0; j topNow) ? 1 : -1); if (Math.abs(diff) > 4) step = diff / 4; var topNew = parseInt(topNow + step); if (topNew != topFinal) { arrived = false; } item.top = topNew; item.bottom = item.top + item.height; } else { item.top = finalItem.top; item.bottom = finalItem.bottom; } item.left = finalItem.left; item.right = finalItem.right; } return arrived; }; /** * Move the events from their current position to the final position * @param {Array} currentItems Array with the real items and their current * positions * @param {Array} finalItems Array with objects containing the final * positions of the items */ links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) { // Put the events directly at there final position for (var i = 0, iMax = finalItems.length; i < iMax; i++) { var finalItem = finalItems[i], current = finalItem.item; current.left = finalItem.left; current.top = finalItem.top; current.right = finalItem.right; current.bottom = finalItem.bottom; } }; /** * Check if the destiny position of given item overlaps with any * of the other items from index itemStart to itemEnd. * @param {Array} items Array with items * @param {int} itemIndex Number of the item to be checked for overlap * @param {int} itemStart First item to be checked. * @param {int} itemEnd Last item to be checked. * @return {Object} colliding item, or undefined when no collisions */ links.Timeline.prototype.stackItemsCheckOverlap = function(items, itemIndex, itemStart, itemEnd) { var eventMargin = this.options.eventMargin, collision = this.collision; // we loop from end to start, as we suppose that the chance of a // collision is larger for items at the end, so check these first. var item1 = items[itemIndex]; for (var i = itemEnd; i >= itemStart; i--) { var item2 = items[i]; if (collision(item1, item2, eventMargin)) { if (i != itemIndex) { return item2; } } } return undefined; }; /** * Test if the two provided items collide * The items must have parameters left, right, top, and bottom. * @param {Element} item1 The first item * @param {Element} item2 The second item * @param {Number} margin A minimum required margin. Optional. * If margin is provided, the two items will be * marked colliding when they overlap or * when the margin between the two is smaller than * the requested margin. * @return {boolean} true if item1 and item2 collide, else false */ links.Timeline.prototype.collision = function(item1, item2, margin) { // set margin if not specified if (margin == undefined) { margin = 0; } // calculate if there is overlap (collision) return (item1.left - margin < item2.right && item1.right + margin > item2.left && item1.top - margin < item2.bottom && item1.bottom + margin > item2.top); }; /** * fire an event * @param {String} event The name of an event, for example "rangechange" or "edit" */ links.Timeline.prototype.trigger = function (event) { // built up properties var properties = null; switch (event) { case 'rangechange': case 'rangechanged': properties = { 'start': new Date(this.start.valueOf()), 'end': new Date(this.end.valueOf()) }; break; case 'timechange': case 'timechanged': properties = { 'time': new Date(this.customTime.valueOf()) }; break; } // trigger the links event bus links.events.trigger(this, event, properties); // trigger the google event bus if (google && google.visualization) { google.visualization.events.trigger(this, event, properties); } }; /** * Cluster the events */ links.Timeline.prototype.clusterItems = function () { if (!this.options.cluster) { return; } var clusters = this.clusterGenerator.getClusters(this.conversion.factor, this.options.clusterMaxItems); if (this.clusters != clusters) { // cluster level changed var queue = this.renderQueue; // remove the old clusters from the scene if (this.clusters) { this.clusters.forEach(function (cluster) { queue.hide.push(cluster); // unlink the items cluster.items.forEach(function (item) { item.cluster = undefined; }); }); } // append the new clusters clusters.forEach(function (cluster) { // don't add to the queue.show here, will be done in .filterItems() // link all items to the cluster cluster.items.forEach(function (item) { item.cluster = cluster; }); }); this.clusters = clusters; } }; /** * Filter the visible events */ links.Timeline.prototype.filterItems = function () { var queue = this.renderQueue, window = (this.end - this.start), start = new Date(this.start.valueOf() - window), end = new Date(this.end.valueOf() + window); function filter (arr) { arr.forEach(function (item) { var rendered = item.rendered; var visible = item.isVisible(start, end); if (rendered != visible) { if (rendered) { queue.hide.push(item); // item is rendered but no longer visible } if (visible && (queue.show.indexOf(item) == -1)) { queue.show.push(item); // item is visible but neither rendered nor queued up to be rendered } } }); } // filter all items and all clusters filter(this.items); if (this.clusters) { filter(this.clusters); } }; /** ------------------------------------------------------------------------ **/ /** * @constructor links.Timeline.ClusterGenerator * Generator which creates clusters of items, based on the visible range in * the Timeline. There is a set of cluster levels which is cached. * @param {links.Timeline} timeline */ links.Timeline.ClusterGenerator = function (timeline) { this.timeline = timeline; this.clear(); }; /** * Clear all cached clusters and data, and initialize all variables */ links.Timeline.ClusterGenerator.prototype.clear = function () { // cache containing created clusters for each cluster level this.items = []; this.groups = {}; this.clearCache(); }; /** * Clear the cached clusters */ links.Timeline.ClusterGenerator.prototype.clearCache = function () { // cache containing created clusters for each cluster level this.cache = {}; this.cacheLevel = -1; this.cache[this.cacheLevel] = []; }; /** * Set the items to be clustered. * This will clear cached clusters. * @param {Item[]} items * @param {Object} [options] Available options: * {boolean} applyOnChangedLevel * If true (default), the changed data is applied * as soon the cluster level changes. If false, * The changed data is applied immediately */ links.Timeline.ClusterGenerator.prototype.setData = function (items, options) { this.items = items || []; this.dataChanged = true; this.applyOnChangedLevel = true; if (options && options.applyOnChangedLevel) { this.applyOnChangedLevel = options.applyOnChangedLevel; } // console.log('clustergenerator setData applyOnChangedLevel=' + this.applyOnChangedLevel); // TODO: cleanup }; /** * Update the current data set: clear cache, and recalculate the clustering for * the current level */ links.Timeline.ClusterGenerator.prototype.updateData = function () { this.dataChanged = true; this.applyOnChangedLevel = false; }; /** * Filter the items per group. * @private */ links.Timeline.ClusterGenerator.prototype.filterData = function () { // filter per group var items = this.items || []; var groups = {}; this.groups = groups; // split the items per group items.forEach(function (item) { // put the item in the correct group var groupName = item.group ? item.group.content : ''; var group = groups[groupName]; if (!group) { group = []; groups[groupName] = group; } group.push(item); // calculate the center of the item if (item.start) { if (item.end) { // range item.center = (item.start.valueOf() + item.end.valueOf()) / 2; } else { // box, dot item.center = item.start.valueOf(); } } }); // sort the items per group for (var groupName in groups) { if (groups.hasOwnProperty(groupName)) { groups[groupName].sort(function (a, b) { return (a.center - b.center); }); } } this.dataChanged = false; }; /** * Cluster the events which are too close together * @param {Number} scale The scale of the current window, * defined as (windowWidth / (endDate - startDate)) * @return {Item[]} clusters */ links.Timeline.ClusterGenerator.prototype.getClusters = function (scale, maxItems) { var level = -1, granularity = 2, // TODO: what granularity is needed for the cluster levels? timeWindow = 0; // milliseconds if (scale > 0) { level = Math.round(Math.log(100 / scale) / Math.log(granularity)); timeWindow = Math.pow(granularity, level); } // clear the cache when and re-filter the data when needed. if (this.dataChanged) { var levelChanged = (level != this.cacheLevel); var applyDataNow = this.applyOnChangedLevel ? levelChanged : true; if (applyDataNow) { // TODO: currently drawn clusters should be removed! mark them as invisible? this.clearCache(); this.filterData(); // console.log('clustergenerator: cache cleared...'); // TODO: cleanup } } this.cacheLevel = level; var clusters = this.cache[level]; if (!clusters) { // console.log('clustergenerator: create cluster level ' + level); // TODO: cleanup clusters = []; // TODO: spit this method, it is too large for (var groupName in this.groups) { if (this.groups.hasOwnProperty(groupName)) { var items = this.groups[groupName]; var iMax = items.length; var i = 0; while (i < iMax) { // find all items around current item, within the timeWindow var item = items[i]; var neighbors = 1; // start at 1, to include itself) // loop through items left from the current item var j = i - 1; while (j >= 0 && (item.center - items[j].center) < timeWindow / 2) { if (!items[j].cluster) { neighbors++; } j--; } // loop through items right from the current item var k = i + 1; while (k < items.length && (items[k].center - item.center) < timeWindow / 2) { neighbors++; k++; } // loop through the created clusters var l = clusters.length - 1; while (l >= 0 && (item.center - clusters[l].center) < timeWindow / 2) { if (item.group == clusters[l].group) { neighbors++; } l--; } // aggregate until the number of items is within maxItems if (neighbors > maxItems) { // too busy in this window. var num = neighbors - maxItems + 1; var clusterItems = []; // append the items to the cluster, // and calculate the average start for the cluster var avg = undefined; // number. average of all start dates var min = undefined; // number. minimum of all start dates var max = undefined; // number. maximum of all start and end dates var containsRanges = false; var count = 0; var m = i; while (clusterItems.length < num && m < items.length) { var p = items[m]; var start = p.start.valueOf(); var end = p.end ? p.end.valueOf() : p.start.valueOf(); clusterItems.push(p); if (count) { // calculate new average (use fractions to prevent overflow) avg = (count / (count + 1)) * avg + (1 / (count + 1)) * p.center; } else { avg = p.center; } min = (min != undefined) ? Math.min(min, start) : start; max = (max != undefined) ? Math.max(max, end) : end; containsRanges = containsRanges || (p instanceof links.Timeline.ItemRange || p instanceof links.Timeline.ItemFloatingRange); count++; m++; } var cluster; var title = 'Cluster containing ' + count + ' events. Zoom in to see the individual events.'; var content = '
' + count + ' events
'; var group = item.group ? item.group.content : undefined; if (containsRanges) { // boxes and/or ranges cluster = this.timeline.createItem({ 'start': new Date(min), 'end': new Date(max), 'content': content, 'group': group }); } else { // boxes only cluster = this.timeline.createItem({ 'start': new Date(avg), 'content': content, 'group': group }); } cluster.isCluster = true; cluster.items = clusterItems; cluster.items.forEach(function (item) { item.cluster = cluster; }); clusters.push(cluster); i += num; } else { delete item.cluster; i += 1; } } } } this.cache[level] = clusters; } return clusters; }; /** ------------------------------------------------------------------------ **/ /** * Event listener (singleton) */ links.events = links.events || { 'listeners': [], /** * Find a single listener by its object * @param {Object} object * @return {Number} index -1 when not found */ 'indexOf': function (object) { var listeners = this.listeners; for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { var listener = listeners[i]; if (listener && listener.object == object) { return i; } } return -1; }, /** * Add an event listener * @param {Object} object * @param {String} event The name of an event, for example 'select' * @param {function} callback The callback method, called when the * event takes place */ 'addListener': function (object, event, callback) { var index = this.indexOf(object); var listener = this.listeners[index]; if (!listener) { listener = { 'object': object, 'events': {} }; this.listeners.push(listener); } var callbacks = listener.events[event]; if (!callbacks) { callbacks = []; listener.events[event] = callbacks; } // add the callback if it does not yet exist if (callbacks.indexOf(callback) == -1) { callbacks.push(callback); } }, /** * Remove an event listener * @param {Object} object * @param {String} event The name of an event, for example 'select' * @param {function} callback The registered callback method */ 'removeListener': function (object, event, callback) { var index = this.indexOf(object); var listener = this.listeners[index]; if (listener) { var callbacks = listener.events[event]; if (callbacks) { var index = callbacks.indexOf(callback); if (index != -1) { callbacks.splice(index, 1); } // remove the array when empty if (callbacks.length == 0) { delete listener.events[event]; } } // count the number of registered events. remove listener when empty var count = 0; var events = listener.events; for (var e in events) { if (events.hasOwnProperty(e)) { count++; } } if (count == 0) { delete this.listeners[index]; } } }, /** * Remove all registered event listeners */ 'removeAllListeners': function () { this.listeners = []; }, /** * Trigger an event. All registered event handlers will be called * @param {Object} object * @param {String} event * @param {Object} properties (optional) */ 'trigger': function (object, event, properties) { var index = this.indexOf(object); var listener = this.listeners[index]; if (listener) { var callbacks = listener.events[event]; if (callbacks) { for (var i = 0, iMax = callbacks.length; i < iMax; i++) { callbacks[i](properties); } } } } }; /** ------------------------------------------------------------------------ **/ /** * @constructor links.Timeline.StepDate * The class StepDate is an iterator for dates. You provide a start date and an * end date. The class itself determines the best scale (step size) based on the * provided start Date, end Date, and minimumStep. * * If minimumStep is provided, the step size is chosen as close as possible * to the minimumStep but larger than minimumStep. If minimumStep is not * provided, the scale is set to 1 DAY. * The minimumStep should correspond with the onscreen size of about 6 characters * * Alternatively, you can set a scale by hand. * After creation, you can initialize the class by executing start(). Then you * can iterate from the start date to the end date via next(). You can check if * the end date is reached with the function end(). After each step, you can * retrieve the current date via get(). * The class step has scales ranging from milliseconds, seconds, minutes, hours, * days, to years. * * Version: 1.2 * * @param {Date} start The start date, for example new Date(2010, 9, 21) * or new Date(2010, 9, 21, 23, 45, 00) * @param {Date} end The end date * @param {Number} minimumStep Optional. Minimum step size in milliseconds */ links.Timeline.StepDate = function(start, end, minimumStep) { // variables this.current = new Date(); this._start = new Date(); this._end = new Date(); this.autoScale = true; this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 1; // initialize the range this.setRange(start, end, minimumStep); }; /// enum scale links.Timeline.StepDate.SCALE = { MILLISECOND: 1, SECOND: 2, MINUTE: 3, HOUR: 4, DAY: 5, WEEKDAY: 6, MONTH: 7, YEAR: 8 }; /** * Set a new range * If minimumStep is provided, the step size is chosen as close as possible * to the minimumStep but larger than minimumStep. If minimumStep is not * provided, the scale is set to 1 DAY. * The minimumStep should correspond with the onscreen size of about 6 characters * @param {Date} start The start date and time. * @param {Date} end The end date and time. * @param {int} minimumStep Optional. Minimum step size in milliseconds */ links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) { if (!(start instanceof Date) || !(end instanceof Date)) { //throw "No legal start or end date in method setRange"; return; } this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); if (this.autoScale) { this.setMinimumStep(minimumStep); } }; /** * Set the step iterator to the start date. */ links.Timeline.StepDate.prototype.start = function() { this.current = new Date(this._start.valueOf()); this.roundToMinor(); }; /** * Round the current date to the first minor date value * This must be executed once when the current date is set to start Date */ links.Timeline.StepDate.prototype.roundToMinor = function() { // round to floor // IMPORTANT: we have no breaks in this switch! (this is no bug) //noinspection FallthroughInSwitchStatementJS switch (this.scale) { case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); this.current.setMonth(0); case links.Timeline.StepDate.SCALE.MONTH: this.current.setDate(1); case links.Timeline.StepDate.SCALE.DAY: // intentional fall through case links.Timeline.StepDate.SCALE.WEEKDAY: this.current.setHours(0); case links.Timeline.StepDate.SCALE.HOUR: this.current.setMinutes(0); case links.Timeline.StepDate.SCALE.MINUTE: this.current.setSeconds(0); case links.Timeline.StepDate.SCALE.SECOND: this.current.setMilliseconds(0); //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds } if (this.step != 1) { // round down to the first minor value that is a multiple of the current step size switch (this.scale) { case links.Timeline.StepDate.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through case links.Timeline.StepDate.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; default: break; } } }; /** * Check if the end date is reached * @return {boolean} true if the current date has passed the end date */ links.Timeline.StepDate.prototype.end = function () { return (this.current.valueOf() > this._end.valueOf()); }; /** * Do the next step */ links.Timeline.StepDate.prototype.next = function() { var prev = this.current.valueOf(); // Two cases, needed to prevent issues with switching daylight savings // (end of March and end of October) if (this.current.getMonth() < 6) { switch (this.scale) { case links.Timeline.StepDate.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; case links.Timeline.StepDate.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; case links.Timeline.StepDate.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; case links.Timeline.StepDate.SCALE.HOUR: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) var h = this.current.getHours(); this.current.setHours(h - (h % this.step)); break; case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; default: break; } } else { switch (this.scale) { case links.Timeline.StepDate.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; default: break; } } if (this.step != 1) { // round down to the correct major value switch (this.scale) { case links.Timeline.StepDate.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; case links.Timeline.StepDate.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; case links.Timeline.StepDate.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; case links.Timeline.StepDate.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through case links.Timeline.StepDate.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; case links.Timeline.StepDate.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; case links.Timeline.StepDate.SCALE.YEAR: break; // nothing to do for year default: break; } } // safety mechanism: if current time is still unchanged, move to the end if (this.current.valueOf() == prev) { this.current = new Date(this._end.valueOf()); } }; /** * Get the current datetime * @return {Date} current The current date */ links.Timeline.StepDate.prototype.getCurrent = function() { return this.current; }; /** * Set a custom scale. Autoscaling will be disabled. * For example setScale(SCALE.MINUTES, 5) will result * in minor steps of 5 minutes, and major steps of an hour. * * @param {links.Timeline.StepDate.SCALE} newScale * A scale. Choose from SCALE.MILLISECOND, * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, * SCALE.YEAR. * @param {Number} newStep A step size, by default 1. Choose for * example 1, 2, 5, or 10. */ links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) { this.scale = newScale; if (newStep > 0) { this.step = newStep; } this.autoScale = false; }; /** * Enable or disable autoscaling * @param {boolean} enable If true, autoascaling is set true */ links.Timeline.StepDate.prototype.setAutoScale = function (enable) { this.autoScale = enable; }; /** * Automatically determine the scale that bests fits the provided minimum step * @param {Number} minimumStep The minimum step size in milliseconds */ links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) { if (minimumStep == undefined) { return; } var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); var stepMonth = (1000 * 60 * 60 * 24 * 30); var stepDay = (1000 * 60 * 60 * 24); var stepHour = (1000 * 60 * 60); var stepMinute = (1000 * 60); var stepSecond = (1000); var stepMillisecond= (1); // find the smallest step that is larger than the provided minimumStep if (stepYear*1000 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1000;} if (stepYear*500 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 500;} if (stepYear*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 100;} if (stepYear*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 50;} if (stepYear*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 10;} if (stepYear*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 5;} if (stepYear > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1;} if (stepMonth*3 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 3;} if (stepMonth > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 1;} if (stepDay*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 5;} if (stepDay*2 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 2;} if (stepDay > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 1;} if (stepDay/2 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.WEEKDAY; this.step = 1;} if (stepHour*4 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 4;} if (stepHour > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 1;} if (stepMinute*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 15;} if (stepMinute*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 10;} if (stepMinute*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 5;} if (stepMinute > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 1;} if (stepSecond*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 15;} if (stepSecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 10;} if (stepSecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 5;} if (stepSecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 1;} if (stepMillisecond*200 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;} if (stepMillisecond*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;} if (stepMillisecond*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;} if (stepMillisecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;} if (stepMillisecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;} if (stepMillisecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;} }; /** * Snap a date to a rounded value. The snap intervals are dependent on the * current scale and step. * @param {Date} date the date to be snapped */ links.Timeline.StepDate.prototype.snap = function(date) { if (this.scale == links.Timeline.StepDate.SCALE.YEAR) { var year = date.getFullYear() + Math.round(date.getMonth() / 12); date.setFullYear(Math.round(year / this.step) * this.step); date.setMonth(0); date.setDate(0); date.setHours(0); date.setMinutes(0); date.setSeconds(0); date.setMilliseconds(0); } else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) { if (date.getDate() > 15) { date.setDate(1); date.setMonth(date.getMonth() + 1); // important: first set Date to 1, after that change the month. } else { date.setDate(1); } date.setHours(0); date.setMinutes(0); date.setSeconds(0); date.setMilliseconds(0); } else if (this.scale == links.Timeline.StepDate.SCALE.DAY || this.scale == links.Timeline.StepDate.SCALE.WEEKDAY) { switch (this.step) { case 5: case 2: date.setHours(Math.round(date.getHours() / 24) * 24); break; default: date.setHours(Math.round(date.getHours() / 12) * 12); break; } date.setMinutes(0); date.setSeconds(0); date.setMilliseconds(0); } else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) { switch (this.step) { case 4: date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; default: date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; } date.setSeconds(0); date.setMilliseconds(0); } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) { switch (this.step) { case 15: case 10: date.setMinutes(Math.round(date.getMinutes() / 5) * 5); date.setSeconds(0); break; case 5: date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; default: date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; } date.setMilliseconds(0); } else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) { switch (this.step) { case 15: case 10: date.setSeconds(Math.round(date.getSeconds() / 5) * 5); date.setMilliseconds(0); break; case 5: date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; default: date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; } } else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) { var step = this.step > 5 ? this.step / 2 : 1; date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); } }; /** * Check if the current step is a major step (for example when the step * is DAY, a major step is each first day of the MONTH) * @return {boolean} true if current date is major, else false. */ links.Timeline.StepDate.prototype.isMajor = function() { switch (this.scale) { case links.Timeline.StepDate.SCALE.MILLISECOND: return (this.current.getMilliseconds() == 0); case links.Timeline.StepDate.SCALE.SECOND: return (this.current.getSeconds() == 0); case links.Timeline.StepDate.SCALE.MINUTE: return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); // Note: this is no bug. Major label is equal for both minute and hour scale case links.Timeline.StepDate.SCALE.HOUR: return (this.current.getHours() == 0); case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through case links.Timeline.StepDate.SCALE.DAY: return (this.current.getDate() == 1); case links.Timeline.StepDate.SCALE.MONTH: return (this.current.getMonth() == 0); case links.Timeline.StepDate.SCALE.YEAR: return false; default: return false; } }; /** * Returns formatted text for the minor axislabel, depending on the current * date and the scale. For example when scale is MINUTE, the current time is * formatted as "hh:mm". * @param {Object} options * @param {Date} [date] custom date. if not provided, current date is taken */ links.Timeline.StepDate.prototype.getLabelMinor = function(options, date) { if (date == undefined) { date = this.current; } switch (this.scale) { case links.Timeline.StepDate.SCALE.MILLISECOND: return String(date.getMilliseconds()); case links.Timeline.StepDate.SCALE.SECOND: return String(date.getSeconds()); case links.Timeline.StepDate.SCALE.MINUTE: return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); case links.Timeline.StepDate.SCALE.HOUR: return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); case links.Timeline.StepDate.SCALE.WEEKDAY: return options.DAYS_SHORT[date.getDay()] + ' ' + date.getDate(); case links.Timeline.StepDate.SCALE.DAY: return String(date.getDate()); case links.Timeline.StepDate.SCALE.MONTH: return options.MONTHS_SHORT[date.getMonth()]; // month is zero based case links.Timeline.StepDate.SCALE.YEAR: return String(date.getFullYear()); default: return ""; } }; /** * Returns formatted text for the major axislabel, depending on the current * date and the scale. For example when scale is MINUTE, the major scale is * hours, and the hour will be formatted as "hh". * @param {Object} options * @param {Date} [date] custom date. if not provided, current date is taken */ links.Timeline.StepDate.prototype.getLabelMajor = function(options, date) { if (date == undefined) { date = this.current; } switch (this.scale) { case links.Timeline.StepDate.SCALE.MILLISECOND: return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2) + ":" + this.addZeros(date.getSeconds(), 2); case links.Timeline.StepDate.SCALE.SECOND: return date.getDate() + " " + options.MONTHS[date.getMonth()] + " " + this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); case links.Timeline.StepDate.SCALE.MINUTE: return options.DAYS[date.getDay()] + " " + date.getDate() + " " + options.MONTHS[date.getMonth()] + " " + date.getFullYear(); case links.Timeline.StepDate.SCALE.HOUR: return options.DAYS[date.getDay()] + " " + date.getDate() + " " + options.MONTHS[date.getMonth()] + " " + date.getFullYear(); case links.Timeline.StepDate.SCALE.WEEKDAY: case links.Timeline.StepDate.SCALE.DAY: return options.MONTHS[date.getMonth()] + " " + date.getFullYear(); case links.Timeline.StepDate.SCALE.MONTH: return String(date.getFullYear()); default: return ""; } }; /** * Add leading zeros to the given value to match the desired length. * For example addZeros(123, 5) returns "00123" * @param {int} value A value * @param {int} len Desired final length * @return {string} value with leading zeros */ links.Timeline.StepDate.prototype.addZeros = function(value, len) { var str = "" + value; while (str.length < len) { str = "0" + str; } return str; }; /** ------------------------------------------------------------------------ **/ /** * Image Loader service. * can be used to get a callback when a certain image is loaded * */ links.imageloader = (function () { var urls = {}; // the loaded urls var callbacks = {}; // the urls currently being loaded. Each key contains // an array with callbacks /** * Check if an image url is loaded * @param {String} url * @return {boolean} loaded True when loaded, false when not loaded * or when being loaded */ function isLoaded (url) { if (urls[url] == true) { return true; } var image = new Image(); image.src = url; if (image.complete) { return true; } return false; } /** * Check if an image url is being loaded * @param {String} url * @return {boolean} loading True when being loaded, false when not loading * or when already loaded */ function isLoading (url) { return (callbacks[url] != undefined); } /** * Load given image url * @param {String} url * @param {function} callback * @param {boolean} sendCallbackWhenAlreadyLoaded optional */ function load (url, callback, sendCallbackWhenAlreadyLoaded) { if (sendCallbackWhenAlreadyLoaded == undefined) { sendCallbackWhenAlreadyLoaded = true; } if (isLoaded(url)) { if (sendCallbackWhenAlreadyLoaded) { callback(url); } return; } if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) { return; } var c = callbacks[url]; if (!c) { var image = new Image(); image.src = url; c = []; callbacks[url] = c; image.onload = function (event) { urls[url] = true; delete callbacks[url]; for (var i = 0; i < c.length; i++) { c[i](url); } } } if (c.indexOf(callback) == -1) { c.push(callback); } } /** * Load a set of images, and send a callback as soon as all images are * loaded * @param {String[]} urls * @param {function } callback * @param {boolean} sendCallbackWhenAlreadyLoaded */ function loadAll (urls, callback, sendCallbackWhenAlreadyLoaded) { // list all urls which are not yet loaded var urlsLeft = []; urls.forEach(function (url) { if (!isLoaded(url)) { urlsLeft.push(url); } }); if (urlsLeft.length) { // there are unloaded images var countLeft = urlsLeft.length; urlsLeft.forEach(function (url) { load(url, function () { countLeft--; if (countLeft == 0) { // done! callback(); } }, sendCallbackWhenAlreadyLoaded); }); } else { // we are already done! if (sendCallbackWhenAlreadyLoaded) { callback(); } } } /** * Recursively retrieve all image urls from the images located inside a given * HTML element * @param {Node} elem * @param {String[]} urls Urls will be added here (no duplicates) */ function filterImageUrls (elem, urls) { var child = elem.firstChild; while (child) { if (child.tagName == 'IMG') { var url = child.src; if (urls.indexOf(url) == -1) { urls.push(url); } } filterImageUrls(child, urls); child = child.nextSibling; } } return { 'isLoaded': isLoaded, 'isLoading': isLoading, 'load': load, 'loadAll': loadAll, 'filterImageUrls': filterImageUrls }; })(); /** ------------------------------------------------------------------------ **/ /** * Add and event listener. Works for all browsers * @param {Element} element An html element * @param {string} action The action, for example "click", * without the prefix "on" * @param {function} listener The callback function to be executed * @param {boolean} useCapture */ links.Timeline.addEventListener = function (element, action, listener, useCapture) { if (element.addEventListener) { if (useCapture === undefined) useCapture = false; if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { action = "DOMMouseScroll"; // For Firefox } element.addEventListener(action, listener, useCapture); } else { element.attachEvent("on" + action, listener); // IE browsers } }; /** * Remove an event listener from an element * @param {Element} element An html dom element * @param {string} action The name of the event, for example "mousedown" * @param {function} listener The listener function * @param {boolean} useCapture */ links.Timeline.removeEventListener = function(element, action, listener, useCapture) { if (element.removeEventListener) { // non-IE browsers if (useCapture === undefined) useCapture = false; if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { action = "DOMMouseScroll"; // For Firefox } element.removeEventListener(action, listener, useCapture); } else { // IE browsers element.detachEvent("on" + action, listener); } }; /** * Get HTML element which is the target of the event * @param {Event} event * @return {Element} target element */ links.Timeline.getTarget = function (event) { // code from http://www.quirksmode.org/js/events_properties.html if (!event) { event = window.event; } var target; if (event.target) { target = event.target; } else if (event.srcElement) { target = event.srcElement; } if (target.nodeType != undefined && target.nodeType == 3) { // defeat Safari bug target = target.parentNode; } return target; }; /** * Stop event propagation */ links.Timeline.stopPropagation = function (event) { if (!event) event = window.event; if (event.stopPropagation) { event.stopPropagation(); // non-IE browsers } else { event.cancelBubble = true; // IE browsers } }; /** * Cancels the event if it is cancelable, without stopping further propagation of the event. */ links.Timeline.preventDefault = function (event) { if (!event) event = window.event; if (event.preventDefault) { event.preventDefault(); // non-IE browsers } else { event.returnValue = false; // IE browsers } }; /** * Retrieve the absolute left value of a DOM element * @param {Element} elem A dom element, for example a div * @return {number} left The absolute left position of this element * in the browser page. */ links.Timeline.getAbsoluteLeft = function(elem) { var doc = document.documentElement; var body = document.body; var left = elem.offsetLeft; var e = elem.offsetParent; while (e != null && e != body && e != doc) { left += e.offsetLeft; left -= e.scrollLeft; e = e.offsetParent; } return left; }; /** * Retrieve the absolute top value of a DOM element * @param {Element} elem A dom element, for example a div * @return {number} top The absolute top position of this element * in the browser page. */ links.Timeline.getAbsoluteTop = function(elem) { var doc = document.documentElement; var body = document.body; var top = elem.offsetTop; var e = elem.offsetParent; while (e != null && e != body && e != doc) { top += e.offsetTop; top -= e.scrollTop; e = e.offsetParent; } return top; }; /** * Get the absolute, vertical mouse position from an event. * @param {Event} event * @return {Number} pageY */ links.Timeline.getPageY = function (event) { if (('targetTouches' in event) && event.targetTouches.length) { event = event.targetTouches[0]; } if ('pageY' in event) { return event.pageY; } // calculate pageY from clientY var clientY = event.clientY; var doc = document.documentElement; var body = document.body; return clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); }; /** * Get the absolute, horizontal mouse position from an event. * @param {Event} event * @return {Number} pageX */ links.Timeline.getPageX = function (event) { if (('targetTouches' in event) && event.targetTouches.length) { event = event.targetTouches[0]; } if ('pageX' in event) { return event.pageX; } // calculate pageX from clientX var clientX = event.clientX; var doc = document.documentElement; var body = document.body; return clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); }; /** * Adds one or more className's to the given elements style * @param {Element} elem * @param {String} className */ links.Timeline.addClassName = function(elem, className) { var classes = elem.className.split(' '); var classesToAdd = className.split(' '); var added = false; for (var i=0; i




© 2015 - 2024 Weber Informatics LLC | Privacy Policy