/**
 * @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-2012 Almende B.V.
 *
 * @author 	Jos de Jong, <jos@almende.org>
 * @date    2012-06-15
 * @version 2.1.2
 */

/*
 * 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.
}


/**
 * @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.
 */
links.Timeline = function(container) {
    // create variables and set default values
    this.dom = {};
    this.conversion = {};
    this.eventParams = {}; // stores parameters for mouse events
    this.groups = [];
    this.groupIndexes = {};
    this.items = [];
    this.visibleItems = 0;
    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,
        'dataChanged': false,
        'frameHeight': 0,
        'frameWidth': 0,
        'groupsLeft': 0,
        'groupsWidth': 0,
        'items': {
            'top': 0
        }
    };

    this.dom.container = container;

    this.options = {
        'width': "100%",
        'height': "auto",
        'minHeight': 0,       // minimal height in pixels
        'autoHeight': true,

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

        'min': undefined,
        'max': undefined,
        'intervalMin': 10,  // milliseconds
        'intervalMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds

        'moveable': true,
        'zoomable': true,
        'selectable': true,
        'editable': false,
        'snapEvents': true,
        'groupChangeable': true,
        'mousewheel': "zoom",
        'dblclick':"edit",

        'showCurrentTime': true, // show a red bar displaying the current time
        'showCustomTime': false, // show a blue, draggable bar displaying a custom time    
        'showMajorLabels': true,
        'showNavigation': false,
        'showButtonAdd': true,
        'groupsOnRight': false,
        'axisOnTop': false,
        'stackEvents': true,
        'animate': true,
        'animateZoom': true,
        'style': 'box',

        'language':'en'
    };

    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();

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

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

    // create all DOM elements
    this.redrawFrame();

    // Internet Explorer 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;
        }
    }

    // fire the ready event
    this.trigger('ready');
};


/**
 * 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.
 */
links.Timeline.prototype.draw = function(data, options) {
    this.setOptions(options);

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

    // set timer range. this will also redraw the timeline
    if (options && options.start && options.end) {
        this.setVisibleChartRange(options.start, options.end);
    }
    else 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];
            }
        }
    }

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

links.Timeline.prototype.getOptions = function(){
  return this.options;
};

/**
 * 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 = [];
    }

    this.items = [];
    this.data = data;
    var items = this.items;
    var options = this.options;

    // create groups from the data
    this.setGroups(data);

    if (google && google.visualization &&
        data instanceof google.visualization.DataTable) {
        // read DataTable
        var hasGroups = (data.getNumberOfColumns() > 3);
        for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
            items.push(this.createItem({
                'start': data.getValue(row, 0),
                'end': data.getValue(row, 1),
                'content': data.getValue(row, 2),
                'group': (hasGroups ? data.getValue(row, 3) : 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.";
    }

    // set a flag to force the recalcSize method to recalculate the 
    // heights and widths of the events
    this.size.dataChanged = true;
    this.redrawFrame();      // create the items for the new data
    this.recalcSize();       // position the items
    this.repositionItems();
    this.stackEvents(false);
    this.redrawFrame();      // redraw the items on the final positions
    this.size.dataChanged = false;
};

/**
 * Set the groups available in the given dataset
 * @param {google.visualization.DataTable | array} data
 */
links.Timeline.prototype.setGroups = function (data) {
    this.deleteGroups();
    var groups = this.groups;
    var groupIndexes = this.groupIndexes;

    if (google && google.visualization &&
        data instanceof google.visualization.DataTable) {
        // get groups from DataTable
        var hasGroups = (data.getNumberOfColumns() > 3);
        if (hasGroups) {
            var groupNames = data.getDistinctValues(3);
            for (var i = 0, iMax = groupNames.length; i < iMax; i++) {
                this.addGroup(groupNames[i]);
            }
        }
    }
    else if (links.Timeline.isArray(data)){
        // get groups from JSON Array
        for (var i = 0, iMax = data.length; i < iMax; i++) {
            var row = data[i],
                group = row.group;
            if (group) {
                this.addGroup(group);
            }
        }
    }
    else {
        throw 'Unknown data type. DataTable or Array expected.';
    }
};


/**
 * 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;

    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);
        }

        if (values.start) {
            data.setValue(index, 0, values.start);
        }
        if (values.end) {
            data.setValue(index, 1, values.end);
        }
        if (values.content) {
            data.setValue(index, 2, values.content);
        }
        if (values.group && data.getNumberOfColumns() > 3) {
            // TODO: append a column when needed?
            data.setValue(index, 3, values.group);
        }
    }
    else if (links.Timeline.isArray(data)) {
        // update the original JSON table
        var row = data[index];
        if (row == undefined) {
            row = {};
            data[index] = row;
        }

        if (values.start) {
            row.start = values.start;
        }
        if (values.end) {
            row.end = values.end;
        }
        if (values.content) {
            row.content = values.content;
        }
        if (values.group) {
            row.group = values.group;
        }
    }
    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} index
 */
links.Timeline.prototype.getItemIndex = function(element) {
    var e = element,
        dom = this.dom,
        items = this.items,
        index = undefined;

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

    if (e.parentNode === dom.items.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;
};

/**
 * 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.recalcSize();
    this.repositionItems();
    this.stackEvents(false);
    this.redrawFrame();
};


/**
 * Set a new value for the visible range int the timeline.
 * Set start to null to include everything from the earliest date to end.
 * Set end to null 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) {
    if (start == undefined) {
        // default of 3 days ago
        start = new Date();
        start.setDate(start.getDate() - 3);
    }

    if (end == undefined) {
        // default of 4 days ahead
        end = new Date();
        end.setDate(start.getDate() + 4);
    }

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

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

    this.applyRange(start, end);

    if (redraw == undefined || redraw == true) {
        this.recalcSize();
        this.repositionItems();
        this.stackEvents(false);
        this.redrawFrame();
    }
    else {
        this.recalcConversion();
    }
};


/**
 * Change the visible chart range such that all items become visible
 */
links.Timeline.prototype.setVisibleChartRangeAuto = function() {
    var items = this.items,
        startMin = undefined, // long value of a data
        endMax = undefined;   // long value of a data

    // find earliest start date from the data
    for (var i = 0, iMax = items.length; i < iMax; i++) {
        var item = items[i],
            start = item.start ? item.start.valueOf() : undefined,
            end = item.end ? item.end.valueOf() : start;

        if (startMin !== undefined && start !== undefined) {
            startMin = Math.min(startMin, start);
        }
        else {
            startMin = start;
        }
        if (endMax !== undefined && end !== undefined) {
            endMax = Math.max(endMax, end);
        }
        else {
            endMax = end;
        }
    }

    if (startMin !== undefined && endMax !== undefined) {
        // zoom out 5% such that you have a little white space on the left and right
        var center = (endMax + startMin) / 2,
            diff = (endMax - startMin);
        startMin = startMin - diff * 0.05;
        endMax = endMax + diff * 0.05;

        // adjust the start and end date
        this.setVisibleChartRange(new Date(startMin), new Date(endMax));
    }
    else {
        this.setVisibleChartRange(undefined, undefined);
    }
};

/**
 * 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.getTime() - this.start.getTime());

    var startNew = new Date(now.getTime() - diff/2);
    var endNew = new Date(startNew.getTime() + 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() {
    var range = {
        'start': new Date(this.start),
        'end': new Date(this.end)
    };
    return range;
};


/**
 * Redraw the timeline. This needs to be executed after the start and/or
 * end time are changed, or when data is added or removed dynamically.
 */
links.Timeline.prototype.redrawFrame = function() {
    var dom = this.dom,
        options = this.options,
        size = this.size;

    if (!dom.frame) {
        // the surrounding main frame
        dom.frame = document.createElement("DIV");
        dom.frame.className = "timeline-frame";
        dom.frame.style.position = "relative";
        dom.frame.style.overflow = "hidden";
        dom.container.appendChild(dom.frame);
    }

    if (options.autoHeight) {
        dom.frame.style.height = size.frameHeight + "px";
    }
    else {
        dom.frame.style.height = options.height || "100%";
    }
    dom.frame.style.width = options.width  || "100%";

    this.redrawContent();
    this.redrawGroups();
    this.redrawCurrentTime();
    this.redrawCustomTime();
    this.redrawNavigation();
};


/**
 * Redraw the content of the timeline: the axis and the items
 */
links.Timeline.prototype.redrawContent = function() {
    var dom = this.dom,
        size = this.size;

    if (!dom.content) {
        // create content box where the axis and canvas will 
        dom.content = document.createElement("DIV");
        //this.frame.className = "timeline-frame";
        dom.content.style.position = "relative";
        dom.content.style.overflow = "hidden";
        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);
        }
    }
    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.redrawAxis();
    this.redrawItems();
    this.redrawDeleteButton();
    this.redrawDragAreas();
};

/**
 * Redraw the timeline axis with minor and major labels
 */
links.Timeline.prototype.redrawAxis = function() {
    var 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 === undefined) {
        size.axis.properties = {};
    }
    if (axis.minorTexts === undefined) {
        axis.minorTexts = [];
    }
    if (axis.minorLines === undefined) {
        axis.minorLines = [];
    }
    if (axis.majorTexts === undefined) {
        axis.majorTexts = [];
    }
    if (axis.majorLines === undefined) {
        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";
        axis.frame.className = "axis";
        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);
    var width = size.contentWidth;

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

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

    this.redrawAxisCharacters();

    this.redrawAxisStartOverwriting();

    step.start();
    var xFirstMajorLabel = undefined;
    while (!step.end()) {
        var cur = step.getCurrent(),
            x = this.timeToScreen(cur),
            isMajor = step.isMajor();

        this.redrawAxisMinorText(x, step.getLabelMinor(undefined, options.language));

        if (isMajor && options.showMajorLabels) {
            if (x > 0) {
                if (xFirstMajorLabel === undefined) {
                    xFirstMajorLabel = x;
                }
                this.redrawAxisMajorText(x, step.getLabelMajor(undefined, options.language));
            }
            this.redrawAxisMajorLine(x);
        }
        else {
            this.redrawAxisMinorLine(x);
        }

        step.next();
    }

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

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

    this.redrawAxisHorizontal();

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

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

/**
 * Create characters used to determine the size of text on the axis
 */
links.Timeline.prototype.redrawAxisCharacters = 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 dom = this.dom,
        axis = dom.axis;

    if (!axis.characterMinor) {
        var 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;
    }

    if (!axis.characterMajor) {
        var 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;
    }
};

/**
 * Initialize redraw of the axis. All existing labels and lines will be
 * overwritten and reused.
 */
links.Timeline.prototype.redrawAxisStartOverwriting = 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.redrawAxisEndOverwriting = function () {
    var dom = this.dom,
        props = this.size.axis.properties,
        frame = this.dom.axis.frame;

    // 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);
    }
};

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

    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;
    }
    axis.backgroundLine.style.top = size.axis.top + "px";
    axis.backgroundLine.style.height = size.axis.height + "px";

    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";
};

/**
 * Create a minor label for the axis at position x
 * @param {Number} x
 * @param {String} text
 */
links.Timeline.prototype.redrawAxisMinorText = 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.redrawAxisMinorLine = 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.redrawAxisMajorText = 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.redrawAxisMajorLine = 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) {
        var 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 ++;
};

/**
 * Redraw all items
 */
links.Timeline.prototype.redrawItems = function() {
    var dom = this.dom,
        options = this.options,
        boxAlign = (options.box && options.box.align) ? options.box.align : undefined,
        size = this.size,
        contentWidth = size.contentWidth,
        items = this.items,
        visibleItems = [];
    
    // console.log("redrawItems");
    
    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.width = "0px";
    frame.style.top = size.items.top + "px";
    frame.style.height = (size.frameHeight - size.axis.height) + "px";

    // initialize arrarys for storing the items
    var ranges = dom.items.ranges;
    if (!ranges) {
        ranges = [];
        dom.items.ranges = ranges;
    }
    var boxes = dom.items.boxes;
    if (!boxes) {
        boxes = [];
        dom.items.boxes = boxes;
    }
    var dots = dom.items.dots;
    if (!dots) {
        dots = [];
        dom.items.dots = dots;
    }

    // Take frame offline
    dom.content.removeChild(frame);

    if (size.dataChanged) {
        // create the items
        var rangesCreated = ranges.length,
            boxesCreated = boxes.length,
            dotsCreated = dots.length,
            rangesUsed = 0,
            boxesUsed = 0,
            dotsUsed = 0,
            itemsLength = items.length;

        for (var i = 0, iMax = items.length; i < iMax; i++) {
            var item = items[i];
            switch (item.type) {
                case 'range':
                    if (rangesUsed < rangesCreated) {
                        // reuse existing range
                        var domItem = ranges[rangesUsed];
                        domItem.firstChild.innerHTML = item.content;
                        domItem.style.display = '';
                        item.dom = domItem;
                        rangesUsed++;
                    }
                    else {
                        // create a new range
                        var domItem = this.createEventRange(item.content);
                        ranges[rangesUsed] = domItem;
                        frame.appendChild(domItem);
                        item.dom = domItem;
                        rangesUsed++;
                        rangesCreated++;
                    }
                    break;

                case 'box':
                    if (boxesUsed < boxesCreated) {
                        // reuse existing box
                        var domItem = boxes[boxesUsed];
                        domItem.firstChild.innerHTML = item.content;
                        domItem.style.display = '';
                        item.dom = domItem;
                        boxesUsed++;
                    }
                    else {
                        // create a new box
                        var domItem = this.createEventBox(item.content);
                        boxes[boxesUsed] = domItem;
                        frame.appendChild(domItem);
                        frame.insertBefore(domItem.line, frame.firstChild);
                        // Note: line must be added in front of the items, 
                        //       such that it stays below all items
                        frame.appendChild(domItem.dot);
                        item.dom = domItem;
                        boxesUsed++;
                        boxesCreated++;
                    }
                    break;

                case 'dot':
                    if (dotsUsed < dotsCreated) {
                        // reuse existing box
                        var domItem = dots[dotsUsed];
                        domItem.firstChild.innerHTML = item.content;
                        domItem.style.display = '';
                        item.dom = domItem;
                        dotsUsed++;
                    }
                    else {
                        // create a new box
                        var domItem = this.createEventDot(item.content);
                        dots[dotsUsed] = domItem;
                        frame.appendChild(domItem);
                        item.dom = domItem;
                        dotsUsed++;
                        dotsCreated++;
                    }
                    break;

                default:
                    // do nothing
                    break;
            }
        }

        // remove redundant items when needed
        for (var i = rangesUsed; i < rangesCreated; i++) {
            frame.removeChild(ranges[i]);
        }
        ranges.splice(rangesUsed, rangesCreated - rangesUsed);
        for (var i = boxesUsed; i < boxesCreated; i++) {
            var box = boxes[i];
            frame.removeChild(box.line);
            frame.removeChild(box.dot);
            frame.removeChild(box);
        }
        boxes.splice(boxesUsed, boxesCreated - boxesUsed);
        for (var i = dotsUsed; i < dotsCreated; i++) {
            frame.removeChild(dots[i]);
        }
        dots.splice(dotsUsed, dotsCreated - dotsUsed);
    }
    
    for (var i = 0, iMax = this.items.length; i < iMax; i++) {
      item = this.items[i];
      domItem = item.dom;

      switch (item.type) {
          case 'range':
              domItem.style.top = item.top + "px";
              domItem.style.left = item.left + "px";
              //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO: borderWidth
              domItem.style.width = Math.max(item.right - item.left, 1) + "px";
              break;

          case 'box':
              var axisOnTop = options.axisOnTop,
                  axisHeight = size.axis.height,
                  axisTop = size.axis.top;

              domItem.style.top = item.top + "px";
              if (boxAlign == 'right') {
                  domItem.style.left = (item.left - item.width) + "px";
              }
              else if (boxAlign == 'left') {
                  domItem.style.left = (item.left) + "px";
              }
              else { // default or 'center'
                  domItem.style.left = (item.left - item.width/2) + "px";
              }
              
              var line = domItem.line;
              line.style.left = (item.left - item.lineWidth/2) + "px";
              if (axisOnTop) {
                  line.style.top = "0px";
                  line.style.height = Math.max(item.top, 0) + "px";
              }
              else {
                  line.style.top = (item.top + item.height) + "px";
                  line.style.height = Math.max(axisTop - item.top - item.height, 0) + "px";
              }
              
              var dot = domItem.dot;
              dot.style.left = (item.left - item.dotWidth/2) + "px";
              dot.style.top = (axisTop - item.dotHeight/2) + "px";
              break;

          case 'dot':
              domItem.style.top = item.top + "px";
              domItem.style.left = (item.left - item.dotWidth / 2) + "px";
              
              domItem.content.style.marginLeft = (1.5 * item.dotWidth) + "px";
              //domItem.content.style.marginRight = (0.5 * item.dotWidth) + "px"; // TODO
              domItem.dot.style.top = ((item.height - item.dotHeight) / 2) + "px";
              break;

          default:
              // do nothing
              break;
      }
    };
    
    // ! ! ! !    here there was the all items repositionig ! ! ! !
    
    // move selected item to the end, to ensure that it is always on top
    if (this.selection) {
        var item = this.selection.item;
        frame.removeChild(item);
        frame.appendChild(item);
    }

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

    /* TODO
     // retrieve all image sources from the items, and set a callback once 
     // all images are retrieved
     var urls = [];
     var timeline = this;
     links.Timeline.filterImageUrls(frame, urls);
     if (urls.length) {
     for (var i = 0; i < urls.length; i++) {
     var url = urls[i];
     var callback = function (url) {
     timeline.redraw();
     };
     var sendCallbackWhenAlreadyLoaded = false;
     links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
     }
     }    
     */
};


/**
 * Create an event in the timeline, with (optional) formatting: inside a box
 * with rounded corners, and a vertical line+dot to the axis.
 * @param {string} content    The content for the event. This can be plain text
 *                            or HTML code.
 */
links.Timeline.prototype.createEventBox = function(content) {
    // background box
    var divBox = document.createElement("DIV");
    divBox.style.position = "absolute";
    divBox.style.left  = "0px";
    divBox.style.top = "0px";
    divBox.className  = "timeline-event timeline-event-box";

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

    // line to axis
    var divLine = document.createElement("DIV");
    divLine.style.position = "absolute";
    divLine.style.width = "0px";
    divLine.className = "timeline-event timeline-event-line";
    // 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";
    divDot.className  = "timeline-event timeline-event-dot";
    divBox.dot = divDot;

    return divBox;
};


/**
 * Create an event in the timeline: a dot, followed by the content.
 * @param {string} content    The content for the event. This can be plain text
 *                            or HTML code.
 */
links.Timeline.prototype.createEventDot = function(content) {
    // 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";
    divContent.innerHTML = content;
    divBox.appendChild(divContent);

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

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

    return divBox;
};


/**
 * Create an event range as a beam in the timeline.
 * @param {string}  content    The content for the event. This can be plain text
 *                             or HTML code.
 */
links.Timeline.prototype.createEventRange = function(content) {
    // background box
    var divBox = document.createElement("DIV");
    divBox.style.position = "absolute";
    divBox.className = "timeline-event timeline-event-range";

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

    return divBox;
};

/**
 * Redraw the group labels
 */
links.Timeline.prototype.redrawGroups = function() {
    var dom = this.dom,
        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 = '';
    }

    if (size.dataChanged) {
        // create the items
        var current = labels.length,
            needed = groups.length;

        // overwrite existing items
        for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
            var group = groups[i];
            var label = labels[i];
            label.innerHTML = group.content;
            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 = group.content;
            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);

        frame.style.borderStyle = options.groupsOnRight ?
            "none none none solid" :
            "none solid none none";
    }

    // 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';
};


/**
 * Redraw the current time bar
 */
links.Timeline.prototype.redrawCurrentTime = 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.getTime() + 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.redrawCurrentTime();
    };
    // 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.redrawCustomTime = 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.redrawDeleteButton = function () {
    var timeline = this,
        options = this.options,
        dom = this.dom,
        size = this.size,
        frame = dom.items.frame;

    if (!options.editable) {
        return;
    }

    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;
    }

    if (this.selection) {
        var index = this.selection.index,
            item = this.items[index],
            domItem = this.selection.item,
            right,
            top = item.top;

        switch (item.type) {
            case 'range':
                right = this.timeToScreen(item.end);
                break;

            case 'box':
                //right = this.timeToScreen(item.start) + item.width / 2 + item.borderWidth; // TODO: borderWidth
                right = this.timeToScreen(item.start) + item.width / 2;
                break;

            case 'dot':
                right = this.timeToScreen(item.start) + item.width;
                break;
        }

        // limit the position
        if (right < -size.contentWidth) {
            right = -size.contentWidth;
        }
        if (right > 2 * size.contentWidth) {
            right = 2 * size.contentWidth;
        }

        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.redrawDragAreas = function () {
    var timeline = this,
        options = this.options,
        dom = this.dom,
        size = this.size,
        frame = this.dom.items.frame;

    if (!options.editable) {
        return;
    }

    // create left drag area
    var dragLeft = dom.items.dragLeft;
    if (!dragLeft) {
        dragLeft = document.createElement("DIV");
        dragLeft.className="timeline-event-range-drag-left";
        dragLeft.style.width = options.dragAreaWidth + "px";
        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.width = options.dragAreaWidth + "px";
        dragRight.style.position = "absolute";

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

    // reposition left and right drag area
    if (this.selection) {
        var index = this.selection.index,
            item = this.items[index];

        if (item.type == 'range') {
            var domItem = item.dom,
                left = this.timeToScreen(item.start),
                right = this.timeToScreen(item.end),
                top = item.top,
                height = item.height;

            dragLeft.style.left = left + 'px';
            dragLeft.style.top = top + '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.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.redrawNavigation = function () {
    var timeline = this,
        options = this.options,
        dom = this.dom,
        frame = dom.frame,
        navBar = dom.navBar;

    if (!navBar) {
        if (options.editable || options.showNavigation) {
            // create a navigation bar containing the navigation buttons
            navBar = document.createElement("DIV");
            navBar.style.position = "absolute";
            navBar.className = "timeline-navigation";
            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 (options.editable && options.showButtonAdd) {
            // create a new in button
            navBar.addButton = document.createElement("DIV");
            navBar.addButton.className = "timeline-navigation-new";

            navBar.addButton.title = "Create new event";
            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 - w / 10); // subtract 10% of timeline width
                var xend = timeline.screenToTime(x + w / 10); // add 10% of timeline width
                if (options.snapEvents) {
                    timeline.step.snap(xstart);
                    timeline.step.snap(xend);
                }

                var content = "New";
                var group = timeline.groups.length ? timeline.groups[0].content : undefined;

                timeline.addItem({
                    'start': xstart,
                    'end': xend,
                    'content': content,
                    'group': group
                });
                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) {
                    // undo an add
                    timeline.deleteItem(index);
                }
                timeline.redrawDeleteButton();
                timeline.redrawDragAreas();
            };
            links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
            navBar.appendChild(navBar.addButton);
        }

        if (options.editable && options.showButtonAdd && options.showNavigation) {
            // create a separator line
            navBar.addButton.style.borderRightWidth = "1px";
            navBar.addButton.style.borderRightStyle = "solid";
        }

        if (options.showNavigation) {
            // create a zoom out button
            navBar.zoomOutButton = document.createElement("DIV");
            navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
            navBar.zoomOutButton.title = "Zoom out";
            var onZoomOut = function(event) {
                links.Timeline.preventDefault(event);
                links.Timeline.stopPropagation(event);
                timeline.zoom(-0.7);
                timeline.trigger("rangechange");
                timeline.trigger("rangechanged");
            };
            links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
            navBar.appendChild(navBar.zoomOutButton);
            
            // create a zoom in button
            navBar.zoomInButton = document.createElement("DIV");
            navBar.zoomInButton.className = "timeline-navigation-zoom-in";
            navBar.zoomInButton.title = "Zoom in";
            var onZoomIn = function(event) {
                links.Timeline.preventDefault(event);
                links.Timeline.stopPropagation(event);
                timeline.zoom(0.7);
                timeline.trigger("rangechange");
                timeline.trigger("rangechanged");
            };
            links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
            navBar.appendChild(navBar.zoomInButton);

            // create a move left button
            navBar.moveLeftButton = document.createElement("DIV");
            navBar.moveLeftButton.className = "timeline-navigation-move-left";
            navBar.moveLeftButton.title = "Move left";
            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 = "Move right";
            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.getTime() - now.getTime();

    this.redrawCurrentTime();
};

/**
 * 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.getTime() + 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);
    this.redrawCustomTime();
};

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

/**
 * 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.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.redrawFrame();
};

/**
 * 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.redrawFrame();
};

/**
 * 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() {
    var resized = this.recalcSize();
    resized = this.repositionItems();
    if (resized) {
        this.redrawFrame();
    }
};

/**
 * Recursively retrieve all image urls from the images located inside a given
 * HTML element
 * @param {HTMLElement} elem
 * @param {String[]} urls   Urls will be added here (no duplicates)
 */
links.Timeline.filterImageUrls = function(elem, urls) {
    var child = elem.firstChild;
    while (child) {
        if (child.tagName == 'IMG') {
            var url = child.src;
            if (urls.indexOf(url) == -1) {
                urls.push(url);
            }
        }

        links.Timeline.filterImageUrls(child, urls);

        child = child.nextSibling;
    }
};

/**
 * Recalculate the sizes of all frames, groups, items, axis
 * After recalcSize() is executed, the Timeline should be redrawn normally
 *
 * @return {boolean} resized   Returns true when the timeline has been resized
 */
links.Timeline.prototype.recalcSize = function() {
    var resized = false;

    var timeline = this,
        size = this.size,
        options = this.options,
        axisOnTop = options.axisOnTop,
        dom = this.dom,
        axis = dom.axis,
        groups = this.groups,
        labels = dom.groups.labels,
        items = this.items;

    var groupsWidth = size.groupsWidth,
        characterMinorWidth  = axis.characterMinor ? axis.characterMinor.clientWidth : 0,
        characterMinorHeight = axis.characterMinor ? axis.characterMinor.clientHeight : 0,
        characterMajorWidth  = axis.characterMajor ? axis.characterMajor.clientWidth : 0,
        characterMajorHeight = axis.characterMajor ? axis.characterMajor.clientHeight : 0,
        axisHeight = characterMinorHeight + (options.showMajorLabels ? characterMajorHeight : 0),
        actualHeight = size.actualHeight || axisHeight;

    // TODO: move checking for loaded items when creating the dom
    if (size.dataChanged) {
        // retrieve all image sources from the items, and set a callback once 
        // all images are retrieved
        var urls = [];
        for (var i = 0, iMax = items.length; i < iMax; i++) {
            var item = items[i],
                domItem = item.dom;

            if (domItem) {
                links.Timeline.filterImageUrls(domItem, urls);
            }
        }
        if (urls.length) {
            for (var i = 0; i < urls.length; i++) {
                var url = urls[i];
                var callback = function (url) {
                    timeline.redraw();
                };
                var sendCallbackWhenAlreadyLoaded = false;
                links.imageloader.load(url, callback, sendCallbackWhenAlreadyLoaded);
            }
        }
    }

    // check sizes of the items and groups (width and height) when the data is changed
    if (size.dataChanged) { // TODO: always calculate the size of an item?
        //if (true) {
        groupsWidth = 0;

        // loop through all groups to get the maximum width and the heights
        for (var i = 0, iMax = labels.length; i < iMax; i++) {
            var group = groups[i];
            group.width = labels[i].clientWidth;
            group.height = labels[i].clientHeight;
            group.labelHeight = group.height;

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

        var resized = this.recalcSizeItems();
        // calculate the actual height of the timeline (needed for auto sizing
        // the timeline)
        actualHeight = axisHeight + 2 * options.eventMarginAxis;
        for (var i = 0, iMax = groups.length; i < iMax; i++) {
            actualHeight += groups[i].height + options.eventMargin;
        }
    }

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

        if (this.animation && this.animation.finalItems) {
            // adjust the offset of all finalItems when the actualHeight has been changed
            var finalItems = this.animation.finalItems,
                finalItem = finalItems[0];
            if (finalItem && finalItem.top) {
                min = finalItem.top,
                    max = finalItem.top + finalItem.height;
            }
            for (var 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 {
            var item = items[0];
            if (item && item.top) {
                min = item.top,
                    max = item.top + item.height;
            }
            for (var i = 1, iMax = items.length; i < iMax; i++) {
                var item = items[i];
                if (item.top) {
                    min = Math.min(min, item.top);
                    max = Math.max(max, (item.top + item.height));
                }
            }
        }

        actualHeight = (max - min) + 2 * options.eventMarginAxis + axisHeight;

        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.animation && this.animation.finalItems) {
                var finalItems = this.animation.finalItems;
                for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
                    finalItems[i].top += diff;
                    finalItems[i].item.top += diff;
                }
            }
            else {
                for (var i = 0, iMax = items.length; i < iMax; i++) {
                    items[i].top += diff;
                }
            }
        }
    }

    // now the heights of the elements are known, we can calculate the the 
    // width and height of frame and axis and content 
    // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
    var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
        frameHeight = Math.max(options.autoHeight ?
            actualHeight : (dom.frame ? dom.frame.clientHeight : 0),
            options.minHeight),
        axisTop  = axisOnTop ? 0 : frameHeight - axisHeight,
        axisLine = axisOnTop ? axisHeight : axisTop,
        itemsTop = axisOnTop ? axisHeight : 0,
        contentHeight = Math.max(frameHeight - axisHeight, 0);

    if (options.groupsWidth !== undefined) {
        groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
    }
    var groupsLeft = options.groupsOnRight ? frameWidth - groupsWidth : 0;

    if (size.dataChanged) {
        // calculate top positions of the group labels and lines
        var eventMargin = options.eventMargin,
            top = axisOnTop ?
                options.eventMarginAxis + eventMargin/2 :
                contentHeight - options.eventMarginAxis + eventMargin/2;

        for (var i = 0, iMax = groups.length; i < iMax; i++) {
            var group = groups[i];
            if (axisOnTop) {
                group.top = top;
                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;
            }
        }

        // calculate top position of the items
        for (var i = 0, iMax = items.length; i < iMax; i++) {
            var item = items[i],
                group = item.group;

            if (group) {
                item.top = group.top;
            }
        }

        resized = true;
    }

    resized = resized || (size.groupsWidth !== groupsWidth);
    resized = resized || (size.groupsLeft !== groupsLeft);
    resized = resized || (size.actualHeight !== actualHeight);
    size.groupsWidth = groupsWidth;
    size.groupsLeft = groupsLeft;
    size.actualHeight = actualHeight;

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

    resized = resized || (size.groupsWidth !== groupsWidth);
    size.groupsWidth = groupsWidth;
    size.contentLeft = options.groupsOnRight ? 0 : groupsWidth;
    size.contentWidth = Math.max(frameWidth - groupsWidth, 0);
    size.contentHeight = contentHeight;

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

    size.items.top = itemsTop;

    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;

    // conversion factors can be changed when width of the Timeline is changed,
    // and when start or end are changed
    this.recalcConversion();

    return resized;
};


/**
*
*
*/
links.Timeline.prototype.recalcSizeItems = function(resized) {
  // loop through the width and height of all items
  resized = resized || false;
  
  for (var i = 0, iMax = this.items.length; i < iMax; i++) {
      var item = this.items[i],
          domItem = item.dom,
          group = item.group;

      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

      switch (item.type) {
          case 'range':
              break;

          case 'box':
              item.dotHeight = domItem.dot.offsetHeight;
              item.dotWidth = domItem.dot.offsetWidth;
              item.lineWidth = domItem.line.offsetWidth;
              break;

          case 'dot':
              item.dotHeight = domItem.dot.offsetHeight;
              item.dotWidth = domItem.dot.offsetWidth;
              item.contentHeight = domItem.content.offsetHeight;
              break;
      }

      if (group) {
          group.height = group.height ? Math.max(group.height, item.height) : item.height;
      }
  }
  
  return resized;
  
}

/**
 * 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 = parseFloat(this.start.valueOf());
    this.conversion.factor = parseFloat(this.size.contentWidth) /
        parseFloat(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,
        time = new Date(parseFloat(x) / conversion.factor + conversion.offset);
    return time;
};

/**
 * 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;
    var x = (time.valueOf() - conversion.offset) * conversion.factor;
    return x;
};



/**
 * Event handler for touchstart event on mobile devices
 */
links.Timeline.prototype.onTouchStart = function(event) {
    var params = this.eventParams,
        dom = this.dom,
        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);
    }
};

/**
 * 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);
            timeline.trigger("rangechange");

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

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

    if (params.zoomed) {
        timeline.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);
};


/**
 * 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;
    }

    // check if frame is not resized (causing a mismatch with the end Date) 
    this.recalcSize();

    // get mouse position
    if (!params.touchDown) {
        params.mouseX = event.clientX;
        params.mouseY = event.clientY;
    }
    else {
        params.mouseX = event.targetTouches[0].clientX;
        params.mouseY = event.targetTouches[0].clientY;
    }
    if (params.mouseX === undefined) {params.mouseX = 0;}
    if (params.mouseY === undefined) {params.mouseY = 0;}
    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);
    params.end = new Date(this.end);

    params.target = links.Timeline.getTarget(event);
    params.itemDragLeft = (params.target === this.dom.items.dragLeft);
    params.itemDragRight = (params.target === this.dom.items.dragRight);

    if (params.itemDragLeft || params.itemDragRight) {
        params.itemIndex = this.selection ? this.selection.index : undefined;
    }
    else {
        params.itemIndex = this.getItemIndex(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);
        var content = "New";
        var group = this.getGroupFromHeight(y);
        this.addItem({
            'start': xstart,
            'end': xend,
            'content': content,
            'group': group.content
        });
        params.itemIndex = (this.items.length - 1);
        this.selectItem(params.itemIndex);
        params.itemDragRight = true;
    }

    params.editItem = options.editable ? this.isSelected(params.itemIndex) : undefined;
    if (params.editItem) {
        var item = this.items[params.itemIndex];
        params.itemStart = item.start;
        params.itemEnd = item.end;
        params.itemType = item.type;
        if (params.itemType == 'range') {
            params.itemLeft = this.timeToScreen(item.start);
            params.itemRight = this.timeToScreen(item.end);
        }
        else {
            params.itemLeft = this.timeToScreen(item.start);
        }
    }
    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, mouseY;
    if (!params.touchDown) {
        mouseX = event.clientX;
        mouseY = event.clientY;
    }
    else {
        mouseX = event.targetTouches[0].clientX;
        mouseY = event.targetTouches[0].clientY;
    }
    if (mouseX === undefined) {mouseX = 0;}
    if (mouseY === undefined) {mouseY = 0;}

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

    var diffX = parseFloat(mouseX) - params.mouseX;
    var diffY = parseFloat(mouseY) - params.mouseY;

    params.moved = true;

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

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

        if (params.itemDragLeft) {
            // 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);
            }
        }
        else if (params.itemDragRight) {
            // 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);
            }
        }
        else {
            // 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.repositionItem(item, left, right);

        if (this.groups.length == 0) {
            // TODO: does not work well in FF, forces redraw with every mouse move it seems
            this.stackEvents(options.animate);
            if (!options.animate) {
                this.redrawFrame();
            }
            // Note: when animate==true, no redraw is needed here, its done by stackEvents animation
        }
        else {
            // move item from one group to another when needed
            if (options.groupsChangeable) {
                var y = mouseY - params.frameTop;
                var group = this.getGroupFromHeight(y);
                if (item.group !== group) {
                    // move item to the other group

                    //item.group = group;
                    var index = this.items.indexOf(item);
                    this.changeItem(index, {'group': group.content});

                    item.top = group.top;
                    this.repositionItem(item);
                }
            }
        }

        this.redrawDeleteButton();
        this.redrawDragAreas();
    }
    else if (options.moveable) {
      this.moveFrame(diffX);
    }

    links.Timeline.preventDefault(event);
};


links.Timeline.prototype.moveFrame = function(diffX){
  var params = this.eventParams,
      size = this.size,
      dom = this.dom,
      options = this.options;

  var interval = (params.end.valueOf() - params.start.valueOf());
  var diffMillisecs = Math.round(parseFloat(-diffX) / size.contentWidth * interval);
  var newStart = new Date(params.start.valueOf() + diffMillisecs);
  var newEnd = new Date(params.end.valueOf() + diffMillisecs);
  this.applyRange(newStart, newEnd);
  // console.log('interval = '+interval+'\ndiffMillisecs = '+diffMillisecs);//+'\nnewStart = '+newStart+'\nnewEnd = '+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 
  // redrawFrame() 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 = Math.round(-diffMillisecs / interval * size.contentWidth + frameOffset);
  params.previousOffset = frameOffset;
  params.previousLeft = frameLeft;
  // console.log('previousLeft = '+previousLeft+' \ncurrentLeft = '+currentLeft+'\npreviousOffset = '+previousOffset+'\nframeOffset = '+frameOffset+'\nframeLeft = '+frameLeft);

  dom.items.frame.style.left = (frameLeft) + "px";
  
  this.redrawCurrentTime();
  this.redrawCustomTime();
  this.redrawAxis();

  // fire a rangechange event
  this.trigger('rangechange');
  // console.log('dom.items.frame.style.left = '+dom.items.frame.style.left);
}

/**
 * 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 change 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' : 'change');
            if (params.addItem) {
                if (this.applyAdd) {
                    this.updateData(params.itemIndex, {
                        'start': item.start,
                        'end': item.end,
                        'content': item.content,
                        'group': item.group ? item.group.content : undefined
                    });
                }
                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;
                    this.repositionItem(item, params.itemLeft, params.itemRight);
                }
            }

            this.recalcSize();
            this.repositionItems();
            this.stackEvents(options.animate);
            if (!options.animate) {
                this.redrawFrame();
            }
            this.redrawDeleteButton();
            this.redrawDragAreas();
        }
    }
    else {
      this.endMoveFrame();
            // mouse did not move -> user has selected an item
    }
};

links.Timeline.prototype.endMoveFrame = function(){
  //console.log('endMoveFrame');
  var params = this.eventParams,
      options = this.options;
  
  if (!params.moved && !params.zoomed) {
      // mouse did not move -> user has selected an item

      if (options.editable && (params.target === this.dom.items.deleteButton)) {
          // delete item
          if (this.selection) {
              this.confirmDeleteItem(this.selection.index);
          }

          this.redrawFrame();
      }
      else if (options.selectable) {
          // select/unselect item
          if (params.itemIndex !== undefined) {
              if (!this.isSelected(params.itemIndex)) {
                  this.selectItem(params.itemIndex);
                  this.redrawDeleteButton();
                  this.redrawDragAreas();
                  this.trigger('select');
              }
          }
          else {
              this.unselectItem();
              this.redrawDeleteButton();
              this.redrawDragAreas();
          }
      }
  }
  else {

      // timeline is moved 
      this.repositionItems();
      this.stackEvents(options.animate);
      if (!options.animate) {

          this.redrawFrame();
      }
      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 (!options.editable && !options.zoomable) {
        return;
    }
    
    /*
      TODO WRONG here 
      detect dblclick on editable element or on timeline background
    */
    switch(options.dblclick){
      case "edit":
        if(!options.editable)
          return;
          
        if (params.itemIndex !== undefined) {
            // fire the edit event
            this.trigger('edit');
        } else {
            // create a new item    
            var x = event.clientX - links.Timeline.getAbsoluteLeft(dom.content);
            var y = event.clientY - links.Timeline.getAbsoluteTop(dom.content);

            // create a new event at the current mouse position
            var xstart = this.screenToTime(x);
            var xend = this.screenToTime(x  + size.frameWidth / 10); // add 10% of timeline width
            if (options.snapEvents) {
                this.step.snap(xstart);
                this.step.snap(xend);
            }
            var content = "New";
            var group = this.getGroupFromHeight(y);   // (group may be undefined)
            this.addItem({
                'start': xstart,
                'end': xend,
                'content': content,
                'group': group.content
            });
            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) {
                // undo an add
                this.deleteItem(params.itemIndex);
            }

            this.redrawDeleteButton();
            this.redrawDragAreas();
        }
      
      break;
      case "zoom":
        if(!options.zoomable)
          return;

        // this.zoom(0.7);
        var x = event.clientX - links.Timeline.getAbsoluteLeft(dom.content),
            range = size.frameWidth / 5,
            xstart = this.screenToTime(x - range),
            xend = this.screenToTime(x  + range);
        
        this.setVisibleChartRange(xstart, xend);
      break;
    }

    links.Timeline.preventDefault(event);
};

/**
 * Event handler for mouse wheel event
 * @param {event}  event   The event
 */
links.Timeline.prototype.onMouseWheel = function(event) {
  
  if (!this.options.zoomable && !this.options.moveable)
      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) {
    switch(this.options.mousewheel){
      case 'zoom':
        this.onMouseWheelZoom(event, delta);
        break;
      case 'move':
        this.onMouseWheelMove(event, delta);
        break;
      default:
      break;
    }
  }  
  // 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);
}

/**
 * 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.onMouseWheelZoom = function(event, delta) {
    if (!this.options.zoomable)
        return;

        // 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 () {
            // check if frame is not resized (causing a mismatch with the end date) 
            timeline.recalcSize();

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

            timeline.zoom(zoomFactor, zoomAroundDate);

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

            /* TODO: smooth scrolling on FF
             timeline.zooming = false;

             if (timeline.zoomingQueue) {
             setTimeout(timeline.zoomingQueue, 100);
             timeline.zoomingQueue = undefined;
             }

             timeline.zoomCount = (timeline.zoomCount || 0) + 1;
             //console.log('zoomCount', timeline.zoomCount)
             */
        };

        zoom();

        /* TODO: smooth scrolling on FF
         if (!timeline.zooming || true) {

         timeline.zooming = true;
         setTimeout(zoom, 100);
         }
         else {
         timeline.zoomingQueue = zoom;
         }
         //*/

};

/**
 * Event handler for mouse wheel event, used to move the timeline
 * Code from http://adomas.org/javascript-mouse-wheel/
 * @param {event}  event   The event
 */
links.Timeline.prototype.onMouseWheelMove = function(event, delta) {

    if (!this.options.moveable)
        return;

    var params = this.eventParams;
    
    if(params.scrolled === undefined || !params.scrolled){
      params.start = new Date(this.start);
      params.end = new Date(this.end);
      params.scrolled = true;
      params.wheelScrolledX = 0;
      params.previousLeft = 0;
      params.previousOffset = 0;
      params.dif
    }

    params.wheelScrolledX += Math.round(delta*11);
        
    this.moveFrame(params.wheelScrolledX);
    
    
    /* SET TIME OUT FOR END SCROLLING EVENT */
    if(this.eventParams.wheelEnderTimer !== undefined)
      clearTimeout(this.eventParams.wheelEnderTimer);

    /*      
    var me = this;
    function endScorlling(){    
      me.repositionItems();
      me.stackEvents(me.options.animate);
      if (!me.options.animate) {
        me.redrawFrame();
      }
      if (params.scrolled && me.options.moveable) {
        params.scrolled = false;
        // fire a rangechanged event
        me.trigger('rangechanged');
      }
    };
    */    
    
    var me = this;
    this.eventParams.wheelEnderTimer = setTimeout(function(){
      params.scrolled = false;
      me.endMoveFrame();
    }, 100);
};


/**
 * 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 = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf());
    var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf());

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

    this.applyRange(newStart, newEnd, zoomAroundDate);

    this.recalcSize();
    var animate = this.options.animate ? this.options.animateZoom : false;
    this.repositionItems();
    this.stackEvents(animate);
    if (!animate || this.groups.length > 0) {
        this.redrawFrame();
    }
    /* TODO
     else {
     this.redrawFrame();
     this.recalcSize();
     this.stackEvents(animate);
     this.redrawFrame();
     }*/
};


/**
 * 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 = parseFloat(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.recalcConversion();
    this.repositionItems();
    this.stackEvents(this.options.animate);
    if (!this.options.animate) {
        this.redrawFrame();
    }
    
};

/**
 * Reposition given item
 * @param {Object} item
 * @param {Number} left
 * @param {Number} right
 */
links.Timeline.prototype.repositionItem = function (item, left, right) {
    var domItem = item.dom;
    switch(item.type) {
        case 'range':
            domItem.style.left = left + "px";
            //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px";  // TODO: borderwidth
            domItem.style.width = Math.max(right - left, 1) + "px";
            break;

        case 'box':
            domItem.style.left = (left - item.width / 2) + "px";
            domItem.line.style.left = (left - item.lineWidth / 2) + "px";
            domItem.dot.style.left = (left - item.dotWidth / 2) + "px";
            break;

        case 'dot':
            domItem.style.left = (left - item.dotWidth / 2) + "px";
            break;
    }

    if (this.groups.length > 0) {
        domItem.style.top = item.top + 'px';
    }
};

/**
 * 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();
    var endValue = end.valueOf();
    var interval = (endValue - startValue);

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

    // determine min and max date value
    var min = options.min ? options.min.valueOf() : undefined;
    var max = options.max ? options.max.valueOf() : undefined;
    if (min && max) {
        if (min >= max) {
            // empty range
            var day = 1000 * 60 * 60 * 24;
            max = min + day;
        }
        if (intervalMax > (max - min)) {
            intervalMax = (max - min);
        }
        if (intervalMin > (max - min)) {
            intervalMin = (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 < intervalMin) {
        var diff = (intervalMin - 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 > intervalMax) {
        var diff = (interval - intervalMax);
        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) {
        var diff = (startValue - min);
        if (diff < 0) {
            startValue -= diff;
            endValue -= diff;
        }
    }

    // prevent to large end date
    if (max) {
        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
 */
links.Timeline.prototype.deleteItem = function(index) {
    if (index >= this.items.length) {
        throw "Cannot delete row, index out of range";
    }

    this.unselectItem();

    // actually delete the item
    this.items.splice(index, 1);

    // 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";
        }
    }

    this.size.dataChanged = true;
    this.redrawFrame();
    this.recalcSize();
    this.repositionItems();
    this.stackEvents(this.options.animate);
    if (!this.options.animate) {
        this.redrawFrame();
    }
    this.size.dataChanged = false;
};


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

    // delete the loaded data
    this.items = [];

    // 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";
        }
    }

    this.size.dataChanged = true;
    this.redrawFrame();
    this.recalcSize();
    this.repositionItems();
    this.stackEvents(this.options.animate);
    if (!this.options.animate) {
        this.redrawFrame();
    }
    this.size.dataChanged = false;
};


/**
 * Find the group from a given height in the timeline
 * @param {Number} height   Height in the timeline
 * @param {boolean}
    * @return {Object} group   The group object, or undefined if out of range
 */
links.Timeline.prototype.getGroupFromHeight = function(height) {
    var groups = this.groups,
        options = this.options,
        size = this.size,
        y = height - (options.axisOnTop ? size.axis.height : 0);

    if (groups) {
        var group;
        /* TODO: cleanup
         for (var i = 0, iMax = groups.length; i < iMax; i++) {
         group = groups[i];
         if (y > group.top && y < group.top + group.height) {
         return group;
         }
         }*/
        for (var i = groups.length - 1; i >= 0; i--) {
            group = groups[i];
            if (y > group.top) {
                return group;
            }
        }

        return group; // return the last group
    }

    return undefined;
};

/**
 * Retrieve the properties of an item.
 * @param {Number} index
 * @return {Object} properties   Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.getItem = function (index) {
    if (index >= this.items.length) {
        throw "Cannot get item, index out of range";
    }

    var item = this.items[index];

    var properties = {};
    properties.start = new Date(item.start);
    if (item.end) {
        properties.end = new Date(item.end);
    }
    properties.content = item.content;
    if (item.group) {
        properties.group = item.group.content;
    }

    return properties;
};

/**
 * Add a new item.
 * @param {Object} itemData     Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.addItem = function (itemData) {
    var items = [
        itemData
    ];

    this.addItems(items);
};

/**
 * Add new items.
 * @param {Array} items  An array containing Objects.
 *                       The objects must have the following parameters:
 *                         {Date} start,
 *                         {Date} end,
 *                         {String} content with text or HTML code,
 *                         {String} group
 */
links.Timeline.prototype.addItems = function (items) {
    var newItems = items,
        curItems = this.items;

    // append the items
    for (var i = 0, iMax = newItems.length; i < iMax; i++) {
        var itemData = items[i];

        this.addGroup(itemData.group);

        curItems.push(this.createItem(itemData));

        var index = curItems.length - 1;
        this.updateData(index, itemData);
    }

    // redraw timeline
    this.size.dataChanged = true;
    this.redrawFrame();
    this.recalcSize();
    this.repositionItems();
    this.stackEvents(false);
    this.redrawFrame();
    this.size.dataChanged = 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 item = {
        'start': itemData.start,
        'end': itemData.end,
        'content': itemData.content,
        'type': itemData.end ? 'range' : this.options.style,
        'group': this.findGroup(itemData.group),
        'top': 0,
        'left': 0,
        'width': 0,
        'height': 0,
        'lineWidth' : 0,
        'dotWidth': 0,
        'dotHeight': 0
    };
    return item;
};

/**
 * Edit an item
 * @param {Number} index
 * @param {Object} itemData     Object containing item properties:<br>
 *                              {Date} start (required),
 *                              {Date} end (optional),
 *                              {String} content (required),
 *                              {String} group (optional)
 */
links.Timeline.prototype.changeItem = function (index, itemData) {
    if (index >= this.items.length) {
        throw "Cannot change item, index out of range";
    }

    var style = this.options.style;
    var item = this.items[index];

    // edit the item
    if (itemData.start) {
        item.start = itemData.start;
    }
    if (itemData.end) {
        item.end = itemData.end;
    }
    if (itemData.content) {
        item.content = itemData.content;
    }
    if (itemData.group) {
        item.group = this.addGroup(itemData.group);
    }

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

    // redraw timeline
    this.size.dataChanged = true;
    this.redrawFrame();
    this.recalcSize();
    this.repositionItems();
    this.stackEvents(false);
    this.redrawFrame();
    this.size.dataChanged = false;
};


/**
 * Find a group by its name.
 * @param {String} group
 * @return {Object} a group object or undefined when group is not found
 */
links.Timeline.prototype.findGroup = function (group) {
    var index = this.groupIndexes[group];
    return (index != undefined) ? this.groups[index] : undefined;
};

/**
 * Delete all groups
 */
links.Timeline.prototype.deleteGroups = function () {
    this.groups = [];
    this.groupIndexes = {};
};


/**
 * Add a group. When the group already exists, no new group is created
 * but the existing group is returned.
 * @param {String} groupName   the name of the group
 * @return {Object} groupObject
 */
links.Timeline.prototype.addGroup = function (groupName) {
    var groups = this.groups,
        groupIndexes = this.groupIndexes,
        groupObj = undefined;

    var groupIndex = groupIndexes[groupName];
    if (groupIndex === undefined && groupName !== 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
        groups = groups.sort(function (a, b) {
            if (a.content > b.content) {
                return 1;
            }
            if (a.content < b.content) {
                return -1;
            }
            return 0;
        });

        // rebuilt the groupIndexes
        for (var i = 0, iMax = groups.length; i < iMax; i++) {
            groupIndexes[groups[i].content] = i;
        }
    }
    else {
        groupObj = groups[groupIndex];
    }

    return groupObj;
};

/**
 * 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;
                if (end != undefined) {
                    middle = new Date((end.valueOf() + start.valueOf()) / 2);
                } else {
                    middle = new Date(start);
                }
                var diff = (this.end.valueOf() - this.start.valueOf()),
                    newStart = new Date(middle.valueOf() - diff/2),
                    newEnd = new Date(middle.valueOf() + 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) {
        sel.push({"row": this.selection.index});
    }
    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,
            'item': domItem
        };

        if (this.options.editable) {
            domItem.style.cursor = 'move';
        }
        switch (item.type) {
            case 'range':
                domItem.className = "timeline-event timeline-event-selected timeline-event-range";
                break;
            case 'box':
                domItem.className = "timeline-event timeline-event-selected timeline-event-box";
                domItem.line.className = "timeline-event timeline-event-selected timeline-event-line";
                domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
                break;
            case 'dot':
                domItem.className = "timeline-event timeline-event-selected";
                domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
                break;
        }

        /* TODO: cleanup this cannot work as this breaks any javscript action inside the item
         // move the item to the end, such that it will be displayed on top of the other items
         var parent = domItem.parentNode;
         if (parent) {
         parent.removeChild(domItem);
         parent.appendChild(domItem);
         }
         */
    }
};

/**
 * 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) {
        var item = this.items[this.selection.index];

        if (item && item.dom) {
            var domItem = item.dom;
            domItem.style.cursor = '';
            switch (item.type) {
                case 'range':
                    domItem.className = "timeline-event timeline-event-range";
                    break;
                case 'box':
                    domItem.className = "timeline-event timeline-event-box";
                    domItem.line.className = "timeline-event timeline-event-line";
                    domItem.dot.className = "timeline-event timeline-event-dot";
                    break;
                case 'dot':
                    domItem.className = "";
                    domItem.dot.className = "timeline-event timeline-event-dot";
                    break;
            }
        }
    }

    this.selection = undefined;
};


/**
*
*
*
*/

links.Timeline.prototype.repositionItems = function(){
  var size = this.size,
  contentWidth = size.contentWidth,
  item, domItem,
  visibleItems = [];

  // reposition all items
  for (var i = 0, iMax = this.items.length; i < iMax; i++) {
      item = this.items[i];
      domItem = item.dom;

      switch (item.type) {
          case 'range':
              item.left = this.timeToScreen(item.start);
              item.right = this.timeToScreen(item.end);

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

              var visible = item.right > -contentWidth && item.left < 2 * contentWidth;
              if (visible || size.dataChanged) {
                  // when data is changed, all items must be kept visible, as their heights must be measured
                  if (item.hidden) {
                      item.hidden = false;
                      domItem.style.display = '';
                  }
              }
              else {
                  // hide when outside of the current window
                  if (!item.hidden) {
                      domItem.style.display = 'none';
                      item.hidden = true;
                  }
              }

              break;

          case 'box':
              item.left = this.timeToScreen(item.start);

              var visible = ((item.left + item.width/2 > -contentWidth) &&
                  (item.left - item.width/2 < 2 * contentWidth));
              if (visible || size.dataChanged) {
                  //when data is changed, all items must be kept visible, as their heights must be measured
                  if (item.hidden) {
                      item.hidden = false;
                      domItem.style.display = '';
                      domItem.line.style.display = '';
                      domItem.dot.style.display = '';
                  }
              }
              else {
                  // hide when outside of the current window
                  if (!item.hidden) {
                      domItem.style.display = 'none';
                      domItem.line.style.display = 'none';
                      domItem.dot.style.display = 'none';
                      item.hidden = true;
                  }
              }
              break;

          case 'dot':
              item.left = this.timeToScreen(item.start);
              var visible = (item.left + item.width > -contentWidth) && (item.left < 2 * contentWidth);
              if (visible || size.dataChanged) {
                  // when data is changed, all items must be kept visible, as their heights must be measured
                  if (item.hidden) {
                      item.hidden = false;
                      domItem.style.display = '';
                  }
              }
              else {
                  // hide when outside of the current window
                  if (!item.hidden) {
                      domItem.style.display = 'none';
                      item.hidden = true;
                  }
              }
              break;

          default:
              // do nothing
              break;
      }

      if(!item.hidden)
        visibleItems.push(item);
        
      
      this.items[i] = item;
      

  }
  
  
  if(this.visibleItems.length != visibleItems.length){
    this.visibleItemsChanged = true;
  }else{
    this.visibleItemsChanged = false;
  }
  this.visibleItems = visibleItems;
  
  this.itemsSizeChanged = false;  
}

/**
 * Stack the items such that they don't overlap. The items will have a minimal
 * distance equal to options.eventMargin.
 * @param {boolean} animate     if animate is true, the items are moved to
 *                              their new position animated
 */
links.Timeline.prototype.stackEvents = function(animate) {
    
    this.trigger('on-before-stack-events');
    
    if(this.itemsSizeChanged)
      this.recalcSizeItems();
    
    if (this.groups.length > 0) {
        // under this conditions we refuse to stack the events
        return;
    }

    if (animate == undefined) {
        animate = false;
    }

    var sortedItems = this.stackOrder(this.items);
    var finalItems = this.stackCalculateFinal(sortedItems, animate);

    if (animate) {
        // move animated to the final positions
        var animation = this.animation;
        if (!animation) {
            animation = {};
            this.animation = animation;
        }
        animation.finalItems = finalItems;

        var timeline = this;
        var step = function () {
            var arrived = timeline.stackMoveOneStep(sortedItems, animation.finalItems);

            timeline.recalcSize();
            timeline.repositionItems();
            timeline.redrawFrame();

            if (!arrived) {
                animation.timer = setTimeout(step, 30);
            }
            else {
                delete animation.finalItems;
                delete animation.timer;
            }
        };

        if (!animation.timer) {
            animation.timer = setTimeout(step, 30);
        }
    }
    else {
        this.stackMoveToFinal(sortedItems, finalItems);
        this.recalcSize();
    }
};


/**
 * Order the items in the array this.items. The order is determined via:
 * - Ranges go before boxes and dots.
 * - The item with the left most location goes first
 * @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([]);

    var f = function (a, b) {
        if (a.type == 'range' && b.type != 'range') {
            return -1;
        }

        if (a.type != 'range' && b.type == 'range') {
            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.
 */
links.Timeline.prototype.stackCalculateFinal = function(items) {
    var size = this.size,
        axisTop = size.axis.top,
        options = this.options,
        axisOnTop = options.axisOnTop,
        eventMargin = options.eventMargin,
        eventMarginAxis = options.eventMarginAxis,
        finalItems = [];

    // initialize final positions
    for (var i = 0, iMax = items.length; i < iMax; i++) {
        var item = items[i],
            top,
            left,
            right,
            bottom,
            height = item.height,
            width = item.width;

        if (axisOnTop) {
            top = axisTop + eventMarginAxis + eventMargin / 2;
        }
        else {
            top = axisTop - height - eventMarginAxis - eventMargin / 2;
        }
        bottom = top + height;

        switch (item.type) {
            case 'range':
            case 'dot':
                left = this.timeToScreen(item.start);
                right = item.end ? this.timeToScreen(item.end) : left + width;
                break;

            case 'box':
                left = this.timeToScreen(item.start) - width / 2;
                right = left + width;
                break;
        }

        finalItems[i] = {
            'left': left,
            'top': top,
            'right': right,
            'bottom': bottom,
            'height': height,
            'item': item
        };
    }

    if (this.options.stackEvents) {
        // calculate new, non-overlapping positions
        //var items = sortedItems;
        for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
            //for (var i = finalItems.length - 1; i >= 0; i--) {
            var finalItem = finalItems[i];
            var collidingItem = null;
            do {
                // TODO: optimize checking for overlap. when there is a gap without items,
                //  you only need to check for items from the next item on, not from zero
                collidingItem = this.stackEventsCheckOverlap(finalItems, i, 0, i-1);
                if (collidingItem != null) {
                    // There is a collision. Reposition the event above the colliding element
                    if (axisOnTop) {
                        finalItem.top = collidingItem.top + collidingItem.height + eventMargin;
                    }
                    else {
                        finalItem.top = collidingItem.top - finalItem.height - eventMargin;
                    }
                    finalItem.bottom = finalItem.top + finalItem.height;
                }
            } while (collidingItem);
        }
    }

    return finalItems;
};


/**
 * Move the events one step in the direction of their final positions
 * @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
 * @return {boolean} arrived     True if all items have reached their final
 *                               location, else false
 */
links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) {
    var arrived = true;

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

        var topNow = parseInt(item.top);
        var topFinal = parseInt(finalItem.top);
        var diff = (topFinal - topNow);
        if (diff) {
            var step = (topFinal == topNow) ? 0 : ((topFinal > 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 (i = 0, iMax = currentItems.length; i < iMax; i++) {
        var current = currentItems[i],
            finalItem = finalItems[i];

        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.stackEventsCheckOverlap = 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),
                'end': new Date(this.end)
            };
            break;

        case 'timechange':
        case 'timechanged':
            properties = {
                'time': new Date(this.customTime)
            };
            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);
    }
};



/** ------------------------------------------------------------------------ **/


/**
 * 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.0
 *
 * @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 {int}  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,
    MONTH : 6,
    YEAR : 7};


/**
 * 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 (isNaN(start) || isNaN(end)) {
        //throw  "No legal start or end date in method setRange";
        return;
    }

    this._start      = (start != undefined)  ? new Date(start) : new Date();
    this._end        = (end != undefined)    ? new Date(end) : 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);
    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)
    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:          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.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.getTime() > this._end.getTime());
};

/**
 * Do the next step
 */
links.Timeline.StepDate.prototype.next = function() {
    var prev = this.current.getTime();

    // 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.getTime() + this.step); break;
            case links.Timeline.StepDate.SCALE.SECOND:       this.current = new Date(this.current.getTime() + this.step * 1000); break;
            case links.Timeline.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.getTime() + this.step * 1000 * 60); break;
            case links.Timeline.StepDate.SCALE.HOUR:
                this.current = new Date(this.current.getTime() + 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.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.getTime() + 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.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.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.getTime() == prev) {
        this.current = new Date(this._end);
    }
};


/**
 * 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.DAY, SCALE.MONTH, SCALE.YEAR.
 * @param {int}        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 {int} 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 (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) {
        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 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.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 {Date} [date] custom date. if not provided, current date is taken
 * @return {string}     minor axislabel
 */
links.Timeline.StepDate.prototype.getLabelMinor = function(date, language) {
    // console.log('getLabelMinor', language);
    switch(language){
        case 'fr':
        var MONTHS_SHORT = new Array("Jan", "Fev", "Mar", "Avr", "Mai", "Juin", "Juil", "Aout", "Sep", "Oct", "Nov", "Dec");
        break;
        case 'en':
        default:
        var MONTHS_SHORT = new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
        break;
    }
    

    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.DAY:          return String(date.getDate());
        case links.Timeline.StepDate.SCALE.MONTH:        return 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 {Date} [date] custom date. if not provided, current date is taken
 * @return {string}     major axislabel
 */
links.Timeline.StepDate.prototype.getLabelMajor = function(date, language) {
    // console.log('getLabelMajor',language);
    switch(language){
        case 'fr':
        var MONTHS = new Array("Janvier", "Février", "Mars","Avril", "Mai", "Juin","Juillet", "Aout", "Septembre","Octobre", "Novembre", "Decembre");
        var DAYS = new Array("Dimanche", "Lundi", "Mardi","mercredi", "Jeudi", "Vendredi", "Samedi");
        break;
        case 'en':
        default:
        var MONTHS = new Array("January", "February", "March","April", "May", "June","July", "August", "September","October", "November", "December");
        var DAYS = new Array("Sunday", "Monday", "Tuesday","Wednesday", "Thursday", "Friday", "Saturday");
        break;
    }
    

    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() + " " +
                MONTHS[date.getMonth()] + " " +
                this.addZeros(date.getHours(), 2) + ":" +
                this.addZeros(date.getMinutes(), 2);
        case links.Timeline.StepDate.SCALE.MINUTE:
            return  DAYS[date.getDay()] + " " +
                date.getDate() + " " +
                MONTHS[date.getMonth()] + " " +
                date.getFullYear();
        case links.Timeline.StepDate.SCALE.HOUR:
            return  DAYS[date.getDay()] + " " +
                date.getDate() + " " +
                MONTHS[date.getMonth()] + " " +
                date.getFullYear();
        case links.Timeline.StepDate.SCALE.DAY:
            return  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);
        }
    }

    return {
        'isLoaded': isLoaded,
        'isLoading': isLoading,
        'load': load
    };
})();


/** ------------------------------------------------------------------------ **/


/**
 * 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 {MouseEvent} 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 left = 0;
    while( elem != null ) {
        left += elem.offsetLeft;
        left -= elem.scrollLeft;
        elem = elem.offsetParent;
    }
    if (!document.body.scrollLeft && window.pageXOffset) {
        // FF
        left -= window.pageXOffset;
    }
    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 top = 0;
    while( elem != null ) {
        top += elem.offsetTop;
        top -= elem.scrollTop;
        elem = elem.offsetParent;
    }
    if (!document.body.scrollTop && window.pageYOffset) {
        // FF
        top -= window.pageYOffset;
    }
    return top;
};

/**
 * Check if given object is a Javascript Array
 * @param {*} obj
 * @return {Boolean} isArray    true if the given object is an array
 */
// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
links.Timeline.isArray = function (obj) {
    if (obj instanceof Array) {
        return true;
    }
    return (Object.prototype.toString.call(obj) === '[object Array]');
};
