text: /**
   * @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-2013 Almende B.V.
   *
   * @author     Jos de Jong, <jos@almende.org>
   * @date    2013-08-20
   * @version 2.5.0
   */
  
  /*
   * i18n mods by github user iktuz (https://gist.github.com/iktuz/3749287/)
   * added to v2.4.1 with da_DK language by @bjarkebech
   */
  
  /*
   * TODO
   *
   * Add zooming with pinching on Android
   * 
   * Bug: when an item contains a javascript onclick or a link, this does not work
   *      when the item is not selected (when the item is being selected,
   *      it is redrawn, which cancels any onclick or link action)
   * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly
   * Bug: neglect items when they have no valid start/end, instead of throwing an error
   * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
   * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
   * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
   */
  
  /**
   * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
   * "links"
   */
  if (typeof links === 'undefined') {
      links = {};
      // important: do not use var, as "var links = {};" will overwrite 
      //            the existing links variable value with undefined in IE8, IE7.  
  }
  
  
  /**
   * Ensure the variable google exists
   */
  if (typeof google === 'undefined') {
      google = undefined;
      // important: do not use var, as "var google = undefined;" will overwrite 
      //            the existing google variable value with undefined in IE8, IE7.
  }
  
  
  
  // Internet Explorer 8 and older does not support Array.indexOf,
  // so we define it here in that case
  // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  if(!Array.prototype.indexOf) {
      Array.prototype.indexOf = function(obj){
          for(var i = 0; i < this.length; i++){
              if(this[i] == obj){
                  return i;
              }
          }
          return -1;
      }
  }
  
  // Internet Explorer 8 and older does not support Array.forEach,
  // so we define it here in that case
  // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  if (!Array.prototype.forEach) {
      Array.prototype.forEach = function(fn, scope) {
          for(var i = 0, len = this.length; i < len; ++i) {
              fn.call(scope || this, this[i], i, this);
          }
      }
  }
  
  
  /**
   * @constructor links.Timeline
   * The timeline is a visualization chart to visualize events in time.
   *
   * The timeline is developed in javascript as a Google Visualization Chart.
   *
   * @param {Element} container   The DOM element in which the Timeline will
   *                                  be created. Normally a div element.
   */
  links.Timeline = function(container) {
      if (!container) {
          // this call was probably only for inheritance, no constructor-code is required
          return;
      }
  
      // create variables and set default values
      this.dom = {};
      this.conversion = {};
      this.eventParams = {}; // stores parameters for mouse events
      this.groups = [];
      this.groupIndexes = {};
      this.items = [];
      this.renderQueue = {
          show: [],   // Items made visible but not yet added to DOM
          hide: [],   // Items currently visible but not yet removed from DOM
          update: []  // Items with changed data but not yet adjusted DOM
      };
      this.renderedItems = [];  // Items currently rendered in the DOM
      this.clusterGenerator = new links.Timeline.ClusterGenerator(this);
      this.currentClusters = [];
      this.selection = undefined; // stores index and item which is currently selected
  
      this.listeners = {}; // event listener callbacks
  
      // Initialize sizes. 
      // Needed for IE (which gives an error when you try to set an undefined
      // value in a style)
      this.size = {
          'actualHeight': 0,
          'axis': {
              'characterMajorHeight': 0,
              'characterMajorWidth': 0,
              'characterMinorHeight': 0,
              'characterMinorWidth': 0,
              'height': 0,
              'labelMajorTop': 0,
              'labelMinorTop': 0,
              'line': 0,
              'lineMajorWidth': 0,
              'lineMinorHeight': 0,
              'lineMinorTop': 0,
              'lineMinorWidth': 0,
              'top': 0
          },
          'contentHeight': 0,
          'contentLeft': 0,
          'contentWidth': 0,
          'frameHeight': 0,
          'frameWidth': 0,
          'groupsLeft': 0,
          'groupsWidth': 0,
          'items': {
              'top': 0
          }
      };
  
      this.dom.container = container;
  
      this.options = {
          'width': "100%",
          'height': "auto",
          'minHeight': 0,        // minimal height in pixels
          'autoHeight': true,
  
          'eventMargin': 10,     // minimal margin between events
          'eventMarginAxis': 20, // minimal margin between events and the axis
          'dragAreaWidth': 10,   // pixels
  
          'min': undefined,
          'max': undefined,
          'zoomMin': 10,     // milliseconds
          'zoomMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  
          'moveable': true,
          'zoomable': true,
          'selectable': true,
          'unselectable': true,
          'editable': false,
          'snapEvents': true,
          'groupChangeable': true,
  
          'showCurrentTime': true, // show a red bar displaying the current time
          'showCustomTime': false, // show a blue, draggable bar displaying a custom time    
          'showMajorLabels': true,
          'showMinorLabels': true,
          'showNavigation': false,
          'showButtonNew': false,
          'groupsOnRight': false,
          'groupsOrder' : true,
          'axisOnTop': false,
          'stackEvents': true,
          'animate': true,
          'animateZoom': true,
          'cluster': false,
          'style': 'box',
          'customStackOrder': false, //a function(a,b) for determining stackorder amongst a group of items. Essentially a comparator, -ve value for "a before b" and vice versa
          
          // i18n: Timeline only has built-in English text per default. Include timeline-locales.js to support more localized text.
          'locale': 'en',
          'MONTHS': new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"),
          'MONTHS_SHORT': new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"),
          'DAYS': new Array("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"),
          'DAYS_SHORT': new Array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"),
          'ZOOM_IN': "Zoom in",
          'ZOOM_OUT': "Zoom out",
          'MOVE_LEFT': "Move left",
          'MOVE_RIGHT': "Move right",
          'NEW': "New",
          'CREATE_NEW_EVENT': "Create new event"
      };
  
      this.clientTimeOffset = 0;    // difference between client time and the time
      // set via Timeline.setCurrentTime()
      var dom = this.dom;
  
      // remove all elements from the container element.
      while (dom.container.hasChildNodes()) {
          dom.container.removeChild(dom.container.firstChild);
      }
  
      // create a step for drawing the axis
      this.step = new links.Timeline.StepDate();
  
      // add standard item types
      this.itemTypes = {
          box:   links.Timeline.ItemBox,
          range: links.Timeline.ItemRange,
          dot:   links.Timeline.ItemDot
      };
  
      // initialize data
      this.data = [];
      this.firstDraw = true;
  
      // date interval must be initialized 
      this.setVisibleChartRange(undefined, undefined, false);
  
      // render for the first time
      this.render();
  
      // fire the ready event
      var me = this;
      setTimeout(function () {
          me.trigger('ready');
      }, 0);
  };
  
  
  /**
   * Main drawing logic. This is the function that needs to be called
   * in the html page, to draw the timeline.
   *
   * A data table with the events must be provided, and an options table.
   *
   * @param {google.visualization.DataTable}      data
   *                                 The data containing the events for the timeline.
   *                                 Object DataTable is defined in
   *                                 google.visualization.DataTable
   * @param {Object} options         A name/value map containing settings for the
   *                                 timeline. Optional.
   */
  links.Timeline.prototype.draw = function(data, options) {
      this.setOptions(options);
      
      if (this.options.selectable) {
          links.Timeline.addClassName(this.dom.frame, "timeline-selectable");
      }
  
      // 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];
              }
          }
          
          // prepare i18n dependent on set locale
          if (typeof links.locales !== 'undefined' && this.options.locale !== 'en') {
              var localeOpts = links.locales[this.options.locale];
              if(localeOpts) {
                  for (var l in localeOpts) {
                      if (localeOpts.hasOwnProperty(l)) {
                          this.options[l] = localeOpts[l];
                      }
                  }
              }
          }
  
          // check for deprecated options
          if (options.showButtonAdd != undefined) {
              this.options.showButtonNew = options.showButtonAdd;
              console.log('WARNING: Option showButtonAdd is deprecated. Use showButtonNew instead');
          }
          if (options.intervalMin != undefined) {
              this.options.zoomMin = options.intervalMin;
              console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead');
          }
          if (options.intervalMax != undefined) {
              this.options.zoomMax = options.intervalMax;
              console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead');
          }
  
          if (options.scale && options.step) {
              this.step.setScale(options.scale, options.step);
          }
      }
  
      // validate options
      this.options.autoHeight = (this.options.height === "auto");
  };
  
  /**
   * Add new type of items
   * @param {String} typeName  Name of new type
   * @param {links.Timeline.Item} typeFactory Constructor of items
   */
  links.Timeline.prototype.addItemType = function (typeName, typeFactory) {
      this.itemTypes[typeName] = typeFactory;
  };
  
  /**
   * Retrieve a map with the column indexes of the columns by column name.
   * For example, the method returns the map
   *     {
   *         start: 0,
   *         end: 1,
   *         content: 2,
   *         group: undefined,
   *         className: undefined
   *         editable: undefined
   *         type: undefined
   *     }
   * @param {google.visualization.DataTable} dataTable
   * @type {Object} map
   */
  links.Timeline.mapColumnIds = function (dataTable) {
      var cols = {},
          colCount = dataTable.getNumberOfColumns(),
          allUndefined = true;
  
      // loop over the columns, and map the column id's to the column indexes
      for (var col = 0; col < colCount; col++) {
          var id = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
          cols[id] = col;
          if (id == 'start' || id == 'end' || id == 'content' || id == 'group' ||
              id == 'className' || id == 'editable' || id == 'type') {
              allUndefined = false;
          }
      }
  
      // if no labels or ids are defined, use the default mapping
      // for start, end, content, group, className, editable, type
      if (allUndefined) {
          cols.start = 0;
          cols.end = 1;
          cols.content = 2;
          if (colCount > 3) {cols.group = 3}
          if (colCount > 4) {cols.className = 4}
          if (colCount > 5) {cols.editable = 5}
          if (colCount > 6) {cols.type = 6}
      }
  
      return cols;
  };
  
  /**
   * Set data for the timeline
   * @param {google.visualization.DataTable | Array} data
   */
  links.Timeline.prototype.setData = function(data) {
      // unselect any previously selected item
      this.unselectItem();
  
      if (!data) {
          data = [];
      }
  
      // clear all data
      this.stackCancelAnimation();
      this.clearItems();
      this.data = data;
      var items = this.items;
      this.deleteGroups();
  
      if (google && google.visualization &&
          data instanceof google.visualization.DataTable) {
          // map the datatable columns
          var cols = links.Timeline.mapColumnIds(data);
  
          // read DataTable
          for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
              items.push(this.createItem({
                  'start':     ((cols.start != undefined)     ? data.getValue(row, cols.start)     : undefined),
                  'end':       ((cols.end != undefined)       ? data.getValue(row, cols.end)       : undefined),
                  'content':   ((cols.content != undefined)   ? data.getValue(row, cols.content)   : undefined),
                  'group':     ((cols.group != undefined)     ? data.getValue(row, cols.group)     : undefined),
                  'className': ((cols.className != undefined) ? data.getValue(row, cols.className) : undefined),
                  'editable':  ((cols.editable != undefined)  ? data.getValue(row, cols.editable)  : undefined),
                  'type':      ((cols.type != undefined)      ? data.getValue(row, cols.type)      : undefined)
              }));
          }
      }
      else if (links.Timeline.isArray(data)) {
          // read JSON array
          for (var row = 0, rows = data.length; row < rows; row++) {
              var itemData = data[row];
              var item = this.createItem(itemData);
              items.push(item);
          }
      }
      else {
          throw "Unknown data type. DataTable or Array expected.";
      }
  
      // prepare data for clustering, by filtering and sorting by type
      if (this.options.cluster) {
          this.clusterGenerator.setData(this.items);
      }
  
      this.render({
          animate: false
      });
  };
  
  /**
   * Return the original data table.
   * @return {google.visualization.DataTable | Array} data
   */
  links.Timeline.prototype.getData = function  () {
      return this.data;
  };
  
  
  /**
   * Update the original data with changed start, end or group.
   *
   * @param {Number} index
   * @param {Object} values   An object containing some of the following parameters:
   *                          {Date} start,
   *                          {Date} end,
   *                          {String} content,
   *                          {String} group
   */
  links.Timeline.prototype.updateData = function  (index, values) {
      var data = this.data,
          prop;
  
      if (google && google.visualization &&
          data instanceof google.visualization.DataTable) {
          // update the original google DataTable
          var missingRows = (index + 1) - data.getNumberOfRows();
          if (missingRows > 0) {
              data.addRows(missingRows);
          }
  
          // map the column id's by name
          var cols = links.Timeline.mapColumnIds(data);
  
          // merge all fields from the provided data into the current data
          for (prop in values) {
              if (values.hasOwnProperty(prop)) {
                  var col = cols[prop];
                  if (col == undefined) {
                      // create new column
                      var value = values[prop];
                      var valueType = 'string';
                      if (typeof(value) == 'number')       {valueType = 'number';}
                      else if (typeof(value) == 'boolean') {valueType = 'boolean';}
                      else if (value instanceof Date)      {valueType = 'datetime';}
                      col = data.addColumn(valueType, prop);
                  }
                  data.setValue(index, col, values[prop]);
  
                  // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
              }
          }
      }
      else if (links.Timeline.isArray(data)) {
          // update the original JSON table
          var row = data[index];
          if (row == undefined) {
              row = {};
              data[index] = row;
          }
  
          // merge all fields from the provided data into the current data
          for (prop in values) {
              if (values.hasOwnProperty(prop)) {
                  row[prop] = values[prop];
  
                  // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
              }
          }
      }
      else {
          throw "Cannot update data, unknown type of data";
      }
  };
  
  /**
   * Find the item index from a given HTML element
   * If no item index is found, undefined is returned
   * @param {Element} element
   * @return {Number | undefined} index
   */
  links.Timeline.prototype.getItemIndex = function(element) {
      var e = element,
          dom = this.dom,
          frame = dom.items.frame,
          items = this.items,
          index = undefined;
  
      // try to find the frame where the items are located in
      while (e.parentNode && e.parentNode !== frame) {
          e = e.parentNode;
      }
  
      if (e.parentNode === frame) {
          // yes! we have found the parent element of all items
          // retrieve its id from the array with items
          for (var i = 0, iMax = items.length; i < iMax; i++) {
              if (items[i].dom === e) {
                  index = i;
                  break;
              }
          }
      }
  
      return index;
  };
  
  /**
   * Set a new size for the timeline
   * @param {string} width   Width in pixels or percentage (for example "800px"
   *                         or "50%")
   * @param {string} height  Height in pixels or percentage  (for example "400px"
   *                         or "30%")
   */
  links.Timeline.prototype.setSize = function(width, height) {
      if (width) {
          this.options.width = width;
          this.dom.frame.style.width = width;
      }
      if (height) {
          this.options.height = height;
          this.options.autoHeight = (this.options.height === "auto");
          if (height !==  "auto" ) {
              this.dom.frame.style.height = height;
          }
      }
  
      this.render({
          animate: false
      });
  };
  
  
  /**
   * Set a new value for the visible range int the timeline.
   * Set start undefined to include everything from the earliest date to end.
   * Set end undefined to include everything from start to the last date.
   * Example usage:
   *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
   *                                    new Date("2010-09-13"));
   * @param {Date}   start     The start date for the timeline. optional
   * @param {Date}   end       The end date for the timeline. optional
   * @param {boolean} redraw   Optional. If true (default) the Timeline is
   *                           directly redrawn
   */
  links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
      var range = {};
      if (!start || !end) {
          // retrieve the date range of the items
          range = this.getDataRange(true);
      }
  
      if (!start) {
          if (end) {
              if (range.min && range.min.valueOf() < end.valueOf()) {
                  // start of the data
                  start = range.min;
              }
              else {
                  // 7 days before the end
                  start = new Date(end.valueOf());
                  start.setDate(start.getDate() - 7);
              }
          }
          else {
              // default of 3 days ago
              start = new Date();
              start.setDate(start.getDate() - 3);
          }
      }
  
      if (!end) {
          if (range.max) {
              // end of the data
              end = range.max;
          }
          else {
              // 7 days after start
              end = new Date(start.valueOf());
              end.setDate(end.getDate() + 7);
          }
      }
  
      // prevent start Date <= end Date
      if (end <= start) {
          end = new Date(start.valueOf());
          end.setDate(end.getDate() + 7);
      }
  
      // limit to the allowed range (don't let this do by applyRange,
      // because that method will try to maintain the interval (end-start)
      var min = this.options.min ? this.options.min : undefined; // date
      if (min != undefined && start.valueOf() < min.valueOf()) {
          start = new Date(min.valueOf()); // date
      }
      var max = this.options.max ? this.options.max : undefined; // date
      if (max != undefined && end.valueOf() > max.valueOf()) {
          end = new Date(max.valueOf()); // date
      }
  
      this.applyRange(start, end);
  
      if (redraw == undefined || redraw == true) {
          this.render({
              animate: false
          });  // TODO: optimize, no reflow needed
      }
      else {
          this.recalcConversion();
      }
  };
  
  
  /**
   * Change the visible chart range such that all items become visible
   */
  links.Timeline.prototype.setVisibleChartRangeAuto = function() {
      var range = this.getDataRange(true);
      this.setVisibleChartRange(range.min, range.max);
  };
  
  /**
   * Adjust the visible range such that the current time is located in the center
   * of the timeline
   */
  links.Timeline.prototype.setVisibleChartRangeNow = function() {
      var now = new Date();
  
      var diff = (this.end.valueOf() - this.start.valueOf());
  
      var startNew = new Date(now.valueOf() - diff/2);
      var endNew = new Date(startNew.valueOf() + diff);
      this.setVisibleChartRange(startNew, endNew);
  };
  
  
  /**
   * Retrieve the current visible range in the timeline.
   * @return {Object} An object with start and end properties
   */
  links.Timeline.prototype.getVisibleChartRange = function() {
      return {
          'start': new Date(this.start.valueOf()),
          'end': new Date(this.end.valueOf())
      };
  };
  
  /**
   * Get the date range of the items.
   * @param {boolean} [withMargin]  If true, 5% of whitespace is added to the
   *                                left and right of the range. Default is false.
   * @return {Object} range    An object with parameters min and max.
   *                           - {Date} min is the lowest start date of the items
   *                           - {Date} max is the highest start or end date of the items
   *                           If no data is available, the values of min and max
   *                           will be undefined
   */
  links.Timeline.prototype.getDataRange = function (withMargin) {
      var items = this.items,
          min = undefined, // number
          max = undefined; // number
  
      if (items) {
          for (var i = 0, iMax = items.length; i < iMax; i++) {
              var item = items[i],
                  start = item.start != undefined ? item.start.valueOf() : undefined,
                  end   = item.end != undefined   ? item.end.valueOf() : start;
  
              if (start != undefined) {
                  min = (min != undefined) ? Math.min(min.valueOf(), start.valueOf()) : start;
              }
  
              if (end != undefined) {
                  max = (max != undefined) ? Math.max(max.valueOf(), end.valueOf()) : end;
              }
          }
      }
  
      if (min && max && withMargin) {
          // zoom out 5% such that you have a little white space on the left and right
          var diff = (max - min);
          min = min - diff * 0.05;
          max = max + diff * 0.05;
      }
  
      return {
          'min': min != undefined ? new Date(min) : undefined,
          'max': max != undefined ? new Date(max) : undefined
      };
  };
  
  /**
   * Re-render (reflow and repaint) all components of the Timeline: frame, axis,
   * items, ...
   * @param {Object} [options]  Available options:
   *                            {boolean} renderTimesLeft   Number of times the
   *                                                        render may be repeated
   *                                                        5 times by default.
   *                            {boolean} animate           takes options.animate
   *                                                        as default value
   */
  links.Timeline.prototype.render = function(options) {
      var frameResized = this.reflowFrame();
      var axisResized = this.reflowAxis();
      var groupsResized = this.reflowGroups();
      var itemsResized = this.reflowItems();
      var resized = (frameResized || axisResized || groupsResized || itemsResized);
  
      // TODO: only stackEvents/filterItems when resized or changed. (gives a bootstrap issue).
      // if (resized) {
      var animate = this.options.animate;
      if (options && options.animate != undefined) {
          animate = options.animate;
      }
  
      this.recalcConversion();
      this.clusterItems();
      this.filterItems();
      this.stackItems(animate);
  
      this.recalcItems();
  
      // TODO: only repaint when resized or when filterItems or stackItems gave a change?
      var needsReflow = this.repaint();
  
      // re-render once when needed (prevent endless re-render loop)
      if (needsReflow) {
          var renderTimesLeft = options ? options.renderTimesLeft : undefined;
          if (renderTimesLeft == undefined) {
              renderTimesLeft = 5;
          }
          if (renderTimesLeft > 0) {
              this.render({
                  'animate': options ? options.animate: undefined,
                  'renderTimesLeft': (renderTimesLeft - 1)
              });
          }
      }
  };
  
  /**
   * Repaint all components of the Timeline
   * @return {boolean} needsReflow   Returns true if the DOM is changed such that
   *                                 a reflow is needed.
   */
  links.Timeline.prototype.repaint = function() {
      var frameNeedsReflow = this.repaintFrame();
      var axisNeedsReflow  = this.repaintAxis();
      var groupsNeedsReflow  = this.repaintGroups();
      var itemsNeedsReflow = this.repaintItems();
      this.repaintCurrentTime();
      this.repaintCustomTime();
  
      return (frameNeedsReflow || axisNeedsReflow || groupsNeedsReflow || itemsNeedsReflow);
  };
  
  /**
   * Reflow the timeline frame
   * @return {boolean} resized    Returns true if any of the frame elements
   *                              have been resized.
   */
  links.Timeline.prototype.reflowFrame = function() {
      var dom = this.dom,
          options = this.options,
          size = this.size,
          resized = false;
  
      // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
      var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
          frameHeight = dom.frame ? dom.frame.clientHeight : 0;
  
      resized = resized || (size.frameWidth !== frameWidth);
      resized = resized || (size.frameHeight !== frameHeight);
      size.frameWidth = frameWidth;
      size.frameHeight = frameHeight;
  
      return resized;
  };
  
  /**
   * repaint the Timeline frame
   * @return {boolean} needsReflow   Returns true if the DOM is changed such that
   *                                 a reflow is needed.
   */
  links.Timeline.prototype.repaintFrame = function() {
      var needsReflow = false,
          dom = this.dom,
          options = this.options,
          size = this.size;
  
      // main frame
      if (!dom.frame) {
          dom.frame = document.createElement("DIV");
          dom.frame.className = "timeline-frame ui-widget ui-widget-content ui-corner-all";
          dom.frame.style.position = "relative";
          dom.frame.style.overflow = "hidden";
          dom.container.appendChild(dom.frame);
          needsReflow = true;
      }
  
      var height = options.autoHeight ?
          (size.actualHeight + "px") :
          (options.height || "100%");
      var width  = options.width || "100%";
      needsReflow = needsReflow || (dom.frame.style.height != height);
      needsReflow = needsReflow || (dom.frame.style.width != width);
      dom.frame.style.height = height;
      dom.frame.style.width = width;
  
      // contents
      if (!dom.content) {
          // create content box where the axis and items will be created
          dom.content = document.createElement("DIV");
          dom.content.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);
          }
  
          needsReflow = true;
      }
      dom.content.style.left = size.contentLeft + "px";
      dom.content.style.top = "0px";
      dom.content.style.width = size.contentWidth + "px";
      dom.content.style.height = size.frameHeight + "px";
  
      this.repaintNavigation();
  
      return needsReflow;
  };
  
  /**
   * Reflow the timeline axis. Calculate its height, width, positioning, etc...
   * @return {boolean} resized    returns true if the axis is resized
   */
  links.Timeline.prototype.reflowAxis = function() {
      var resized = false,
          dom = this.dom,
          options = this.options,
          size = this.size,
          axisDom = dom.axis;
  
      var characterMinorWidth  = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientWidth : 0,
          characterMinorHeight = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientHeight : 0,
          characterMajorWidth  = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientWidth : 0,
          characterMajorHeight = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientHeight : 0,
          axisHeight = (options.showMinorLabels ? characterMinorHeight : 0) +
              (options.showMajorLabels ? characterMajorHeight : 0);
  
      var axisTop  = options.axisOnTop ? 0 : size.frameHeight - axisHeight,
          axisLine = options.axisOnTop ? axisHeight : axisTop;
  
      resized = resized || (size.axis.top !== axisTop);
      resized = resized || (size.axis.line !== axisLine);
      resized = resized || (size.axis.height !== axisHeight);
      size.axis.top = axisTop;
      size.axis.line = axisLine;
      size.axis.height = axisHeight;
      size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine +
          (options.showMinorLabels ? characterMinorHeight : 0);
      size.axis.labelMinorTop = options.axisOnTop ?
          (options.showMajorLabels ? characterMajorHeight : 0) :
          axisLine;
      size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
      size.axis.lineMinorHeight = options.showMajorLabels ?
          size.frameHeight - characterMajorHeight:
          size.frameHeight;
      if (axisDom && axisDom.minorLines && axisDom.minorLines.length) {
          size.axis.lineMinorWidth = axisDom.minorLines[0].offsetWidth;
      }
      else {
          size.axis.lineMinorWidth = 1;
      }
      if (axisDom && axisDom.majorLines && axisDom.majorLines.length) {
          size.axis.lineMajorWidth = axisDom.majorLines[0].offsetWidth;
      }
      else {
          size.axis.lineMajorWidth = 1;
      }
  
      resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
      resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
      resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
      resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
      size.axis.characterMinorWidth  = characterMinorWidth;
      size.axis.characterMinorHeight = characterMinorHeight;
      size.axis.characterMajorWidth  = characterMajorWidth;
      size.axis.characterMajorHeight = characterMajorHeight;
  
      var contentHeight = Math.max(size.frameHeight - axisHeight, 0);
      size.contentLeft = options.groupsOnRight ? 0 : size.groupsWidth;
      size.contentWidth = Math.max(size.frameWidth - size.groupsWidth, 0);
      size.contentHeight = contentHeight;
  
      return resized;
  };
  
  /**
   * Redraw the timeline axis with minor and major labels
   * @return {boolean} needsReflow     Returns true if the DOM is changed such
   *                                   that a reflow is needed.
   */
  links.Timeline.prototype.repaintAxis = function() {
      var needsReflow = false,
          dom = this.dom,
          options = this.options,
          size = this.size,
          step = this.step;
  
      var axis = dom.axis;
      if (!axis) {
          axis = {};
          dom.axis = axis;
      }
      if (!size.axis.properties) {
          size.axis.properties = {};
      }
      if (!axis.minorTexts) {
          axis.minorTexts = [];
      }
      if (!axis.minorLines) {
          axis.minorLines = [];
      }
      if (!axis.majorTexts) {
          axis.majorTexts = [];
      }
      if (!axis.majorLines) {
          axis.majorLines = [];
      }
  
      if (!axis.frame) {
          axis.frame = document.createElement("DIV");
          axis.frame.style.position = "absolute";
          axis.frame.style.left = "0px";
          axis.frame.style.top = "0px";
          dom.content.appendChild(axis.frame);
      }
  
      // take axis offline
      dom.content.removeChild(axis.frame);
  
      axis.frame.style.width = (size.contentWidth) + "px";
      axis.frame.style.height = (size.axis.height) + "px";
  
      // the drawn axis is more wide than the actual visual part, such that
      // the axis can be dragged without having to redraw it each time again.
      var start = this.screenToTime(0);
      var end = this.screenToTime(size.contentWidth);
  
      // calculate minimum step (in milliseconds) based on character size
      if (size.axis.characterMinorWidth) {
          this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6) -
              this.screenToTime(0);
  
          step.setRange(start, end, this.minimumStep);
      }
  
      var charsNeedsReflow = this.repaintAxisCharacters();
      needsReflow = needsReflow || charsNeedsReflow;
  
      // The current labels on the axis will be re-used (much better performance),
      // therefore, the repaintAxis method uses the mechanism with
      // repaintAxisStartOverwriting, repaintAxisEndOverwriting, and
      // this.size.axis.properties is used.
      this.repaintAxisStartOverwriting();
  
      step.start();
      var xFirstMajorLabel = undefined;
      var max = 0;
      while (!step.end() && max < 1000) {
          max++;
          var cur = step.getCurrent(),
              x = this.timeToScreen(cur),
              isMajor = step.isMajor();
  
          if (options.showMinorLabels) {
              this.repaintAxisMinorText(x, step.getLabelMinor(options));
          }
  
          if (isMajor && options.showMajorLabels) {
              if (x > 0) {
                  if (xFirstMajorLabel == undefined) {
                      xFirstMajorLabel = x;
                  }
                  this.repaintAxisMajorText(x, step.getLabelMajor(options));
              }
              this.repaintAxisMajorLine(x);
          }
          else {
              this.repaintAxisMinorLine(x);
          }
  
          step.next();
      }
  
      // create a major label on the left when needed
      if (options.showMajorLabels) {
          var leftTime = this.screenToTime(0),
              leftText = this.step.getLabelMajor(options, leftTime),
              width = leftText.length * size.axis.characterMajorWidth + 10; // upper bound estimation
  
          if (xFirstMajorLabel == undefined || width < xFirstMajorLabel) {
              this.repaintAxisMajorText(0, leftText, leftTime);
          }
      }
  
      // cleanup left over labels
      this.repaintAxisEndOverwriting();
  
      this.repaintAxisHorizontal();
  
      // put axis online
      dom.content.insertBefore(axis.frame, dom.content.firstChild);
  
      return needsReflow;
  };
  
  /**
   * Create characters used to determine the size of text on the axis
   * @return {boolean} needsReflow   Returns true if the DOM is changed such that
   *                                 a reflow is needed.
   */
  links.Timeline.prototype.repaintAxisCharacters = function () {
      // calculate the width and height of a single character
      // this is used to calculate the step size, and also the positioning of the
      // axis
      var needsReflow = false,
          dom = this.dom,
          axis = dom.axis,
          text;
  
      if (!axis.characterMinor) {
          text = document.createTextNode("0");
          var characterMinor = document.createElement("DIV");
          characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
          characterMinor.appendChild(text);
          characterMinor.style.position = "absolute";
          characterMinor.style.visibility = "hidden";
          characterMinor.style.paddingLeft = "0px";
          characterMinor.style.paddingRight = "0px";
          axis.frame.appendChild(characterMinor);
  
          axis.characterMinor = characterMinor;
          needsReflow = true;
      }
  
      if (!axis.characterMajor) {
          text = document.createTextNode("0");
          var characterMajor = document.createElement("DIV");
          characterMajor.className = "timeline-axis-text timeline-axis-text-major";
          characterMajor.appendChild(text);
          characterMajor.style.position = "absolute";
          characterMajor.style.visibility = "hidden";
          characterMajor.style.paddingLeft = "0px";
          characterMajor.style.paddingRight = "0px";
          axis.frame.appendChild(characterMajor);
  
          axis.characterMajor = characterMajor;
          needsReflow = true;
      }
  
      return needsReflow;
  };
  
  /**
   * Initialize redraw of the axis. All existing labels and lines will be
   * overwritten and reused.
   */
  links.Timeline.prototype.repaintAxisStartOverwriting = function () {
      var properties = this.size.axis.properties;
  
      properties.minorTextNum = 0;
      properties.minorLineNum = 0;
      properties.majorTextNum = 0;
      properties.majorLineNum = 0;
  };
  
  /**
   * End of overwriting HTML DOM elements of the axis.
   * remaining elements will be removed
   */
  links.Timeline.prototype.repaintAxisEndOverwriting = function () {
      var dom = this.dom,
          props = this.size.axis.properties,
          frame = this.dom.axis.frame,
          num;
  
      // remove leftovers
      var minorTexts = dom.axis.minorTexts;
      num = props.minorTextNum;
      while (minorTexts.length > num) {
          var minorText = minorTexts[num];
          frame.removeChild(minorText);
          minorTexts.splice(num, 1);
      }
  
      var minorLines = dom.axis.minorLines;
      num = props.minorLineNum;
      while (minorLines.length > num) {
          var minorLine = minorLines[num];
          frame.removeChild(minorLine);
          minorLines.splice(num, 1);
      }
  
      var majorTexts = dom.axis.majorTexts;
      num = props.majorTextNum;
      while (majorTexts.length > num) {
          var majorText = majorTexts[num];
          frame.removeChild(majorText);
          majorTexts.splice(num, 1);
      }
  
      var majorLines = dom.axis.majorLines;
      num = props.majorLineNum;
      while (majorLines.length > num) {
          var majorLine = majorLines[num];
          frame.removeChild(majorLine);
          majorLines.splice(num, 1);
      }
  };
  
  /**
   * Repaint the horizontal line and background of the axis
   */
  links.Timeline.prototype.repaintAxisHorizontal = function() {
      var axis = this.dom.axis,
          size = this.size,
          options = this.options;
  
      // line behind all axis elements (possibly having a background color)
      var hasAxis = (options.showMinorLabels || options.showMajorLabels);
      if (hasAxis) {
          if (!axis.backgroundLine) {
              // create the axis line background (for a background color or so)
              var backgroundLine = document.createElement("DIV");
              backgroundLine.className = "timeline-axis";
              backgroundLine.style.position = "absolute";
              backgroundLine.style.left = "0px";
              backgroundLine.style.width = "100%";
              backgroundLine.style.border = "none";
              axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);
  
              axis.backgroundLine = backgroundLine;
          }
  
          if (axis.backgroundLine) {
              axis.backgroundLine.style.top = size.axis.top + "px";
              axis.backgroundLine.style.height = size.axis.height + "px";
          }
      }
      else {
          if (axis.backgroundLine) {
              axis.frame.removeChild(axis.backgroundLine);
              delete axis.backgroundLine;
          }
      }
  
      // line before all axis elements
      if (hasAxis) {
          if (axis.line) {
              // put this line at the end of all childs
              var line = axis.frame.removeChild(axis.line);
              axis.frame.appendChild(line);
          }
          else {
              // make the axis line
              var line = document.createElement("DIV");
              line.className = "timeline-axis";
              line.style.position = "absolute";
              line.style.left = "0px";
              line.style.width = "100%";
              line.style.height = "0px";
              axis.frame.appendChild(line);
  
              axis.line = line;
          }
  
          axis.line.style.top = size.axis.line + "px";
      }
      else {
          if (axis.line && axis.line.parentElement) {
              axis.frame.removeChild(axis.line);
              delete axis.line;
          }
      }
  };
  
  /**
   * Create a minor label for the axis at position x
   * @param {Number} x
   * @param {String} text
   */
  links.Timeline.prototype.repaintAxisMinorText = function (x, text) {
      var size = this.size,
          dom = this.dom,
          props = size.axis.properties,
          frame = dom.axis.frame,
          minorTexts = dom.axis.minorTexts,
          index = props.minorTextNum,
          label;
  
      if (index < minorTexts.length) {
          label = minorTexts[index]
      }
      else {
          // create new label
          var content = document.createTextNode("");
          label = document.createElement("DIV");
          label.appendChild(content);
          label.className = "timeline-axis-text timeline-axis-text-minor";
          label.style.position = "absolute";
  
          frame.appendChild(label);
  
          minorTexts.push(label);
      }
  
      label.childNodes[0].nodeValue = text;
      label.style.left = x + "px";
      label.style.top  = size.axis.labelMinorTop + "px";
      //label.title = title;  // TODO: this is a heavy operation
  
      props.minorTextNum++;
  };
  
  /**
   * Create a minor line for the axis at position x
   * @param {Number} x
   */
  links.Timeline.prototype.repaintAxisMinorLine = function (x) {
      var axis = this.size.axis,
          dom = this.dom,
          props = axis.properties,
          frame = dom.axis.frame,
          minorLines = dom.axis.minorLines,
          index = props.minorLineNum,
          line;
  
      if (index < minorLines.length) {
          line = minorLines[index];
      }
      else {
          // create vertical line
          line = document.createElement("DIV");
          line.className = "timeline-axis-grid timeline-axis-grid-minor";
          line.style.position = "absolute";
          line.style.width = "0px";
  
          frame.appendChild(line);
          minorLines.push(line);
      }
  
      line.style.top = axis.lineMinorTop + "px";
      line.style.height = axis.lineMinorHeight + "px";
      line.style.left = (x - axis.lineMinorWidth/2) + "px";
  
      props.minorLineNum++;
  };
  
  /**
   * Create a Major label for the axis at position x
   * @param {Number} x
   * @param {String} text
   */
  links.Timeline.prototype.repaintAxisMajorText = function (x, text) {
      var size = this.size,
          props = size.axis.properties,
          frame = this.dom.axis.frame,
          majorTexts = this.dom.axis.majorTexts,
          index = props.majorTextNum,
          label;
  
      if (index < majorTexts.length) {
          label = majorTexts[index];
      }
      else {
          // create label
          var content = document.createTextNode(text);
          label = document.createElement("DIV");
          label.className = "timeline-axis-text timeline-axis-text-major";
          label.appendChild(content);
          label.style.position = "absolute";
          label.style.top = "0px";
  
          frame.appendChild(label);
          majorTexts.push(label);
      }
  
      label.childNodes[0].nodeValue = text;
      label.style.top = size.axis.labelMajorTop + "px";
      label.style.left = x + "px";
      //label.title = title; // TODO: this is a heavy operation
  
      props.majorTextNum ++;
  };
  
  /**
   * Create a Major line for the axis at position x
   * @param {Number} x
   */
  links.Timeline.prototype.repaintAxisMajorLine = function (x) {
      var size = this.size,
          props = size.axis.properties,
          axis = this.size.axis,
          frame = this.dom.axis.frame,
          majorLines = this.dom.axis.majorLines,
          index = props.majorLineNum,
          line;
  
      if (index < majorLines.length) {
          line = majorLines[index];
      }
      else {
          // create vertical line
          line = document.createElement("DIV");
          line.className = "timeline-axis-grid timeline-axis-grid-major";
          line.style.position = "absolute";
          line.style.top = "0px";
          line.style.width = "0px";
  
          frame.appendChild(line);
          majorLines.push(line);
      }
  
      line.style.left = (x - axis.lineMajorWidth/2) + "px";
      line.style.height = size.frameHeight + "px";
  
      props.majorLineNum ++;
  };
  
  /**
   * Reflow all items, retrieve their actual size
   * @return {boolean} resized    returns true if any of the items is resized
   */
  links.Timeline.prototype.reflowItems = function() {
      var resized = false,
          i,
          iMax,
          group,
          groups = this.groups,
          renderedItems = this.renderedItems;
  
      if (groups) { // TODO: need to check if labels exists?
          // loop through all groups to reset the items height
          groups.forEach(function (group) {
              group.itemsHeight = 0;
          });
      }
  
      // loop through the width and height of all visible items
      for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
          var item = renderedItems[i],
              domItem = item.dom;
          group = item.group;
  
          if (domItem) {
              // TODO: move updating width and height into item.reflow
              var width = domItem ? domItem.clientWidth : 0;
              var height = domItem ? domItem.clientHeight : 0;
              resized = resized || (item.width != width);
              resized = resized || (item.height != height);
              item.width = width;
              item.height = height;
              //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
              item.reflow();
          }
  
          if (group) {
              group.itemsHeight = group.itemsHeight ?
                  Math.max(group.itemsHeight, item.height) :
                  item.height;
          }
      }
  
      return resized;
  };
  
  /**
   * Recalculate item properties:
   * - the height of each group.
   * - the actualHeight, from the stacked items or the sum of the group heights
   * @return {boolean} resized    returns true if any of the items properties is
   *                              changed
   */
  links.Timeline.prototype.recalcItems = function () {
      var resized = false,
          i,
          iMax,
          item,
          finalItem,
          finalItems,
          group,
          groups = this.groups,
          size = this.size,
          options = this.options,
          renderedItems = this.renderedItems;
  
      var actualHeight = 0;
      if (groups.length == 0) {
          // calculate actual height of the timeline when there are no groups
          // but stacked items
          if (options.autoHeight || options.cluster) {
              var min = 0,
                  max = 0;
  
              if (this.stack && this.stack.finalItems) {
                  // adjust the offset of all finalItems when the actualHeight has been changed
                  finalItems = this.stack.finalItems;
                  finalItem = finalItems[0];
                  if (finalItem && finalItem.top) {
                      min = finalItem.top;
                      max = finalItem.top + finalItem.height;
                  }
                  for (i = 1, iMax = finalItems.length; i < iMax; i++) {
                      finalItem = finalItems[i];
                      min = Math.min(min, finalItem.top);
                      max = Math.max(max, finalItem.top + finalItem.height);
                  }
              }
              else {
                  item = renderedItems[0];
                  if (item && item.top) {
                      min = item.top;
                      max = item.top + item.height;
                  }
                  for (i = 1, iMax = renderedItems.length; i < iMax; i++) {
                      item = renderedItems[i];
                      if (item.top) {
                          min = Math.min(min, item.top);
                          max = Math.max(max, (item.top + item.height));
                      }
                  }
              }
  
              actualHeight = (max - min) + 2 * options.eventMarginAxis + size.axis.height;
              if (actualHeight < options.minHeight) {
                  actualHeight = options.minHeight;
              }
  
              if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
                  // adjust the offset of all items when the actualHeight has been changed
                  var diff = actualHeight - size.actualHeight;
                  if (this.stack && this.stack.finalItems) {
                      finalItems = this.stack.finalItems;
                      for (i = 0, iMax = finalItems.length; i < iMax; i++) {
                          finalItems[i].top += diff;
                          finalItems[i].item.top += diff;
                      }
                  }
                  else {
                      for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
                          renderedItems[i].top += diff;
                      }
                  }
              }
          }
      }
      else {
          // loop through all groups to get the height of each group, and the
          // total height
          actualHeight = size.axis.height + 2 * options.eventMarginAxis;
          for (i = 0, iMax = groups.length; i < iMax; i++) {
              group = groups[i];
  
              var groupHeight = Math.max(group.labelHeight || 0, group.itemsHeight || 0);
              resized = resized || (groupHeight != group.height);
              group.height = groupHeight;
  
              actualHeight += groups[i].height + options.eventMargin;
          }
  
          // calculate top positions of the group labels and lines
          var eventMargin = options.eventMargin,
              top = options.axisOnTop ?
                  options.eventMarginAxis + eventMargin/2 :
                  size.contentHeight - options.eventMarginAxis + eventMargin/ 2,
              axisHeight = size.axis.height;
  
          for (i = 0, iMax = groups.length; i < iMax; i++) {
              group = groups[i];
              if (options.axisOnTop) {
                  group.top = top + axisHeight;
                  group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
                  group.lineTop = top + axisHeight + group.height + eventMargin/2;
                  top += group.height + eventMargin;
              }
              else {
                  top -= group.height + eventMargin;
                  group.top = top;
                  group.labelTop = top + (group.height - group.labelHeight) / 2;
                  group.lineTop = top - eventMargin/2;
              }
          }
  
          // calculate top position of the visible items
          for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
              item = renderedItems[i];
              group = item.group;
  
              if (group) {
                  item.top = group.top;
              }
          }
  
          resized = true;
      }
  
      if (actualHeight < options.minHeight) {
          actualHeight = options.minHeight;
      }
      resized = resized || (actualHeight != size.actualHeight);
      size.actualHeight = actualHeight;
  
      return resized;
  };
  
  /**
   * This method clears the (internal) array this.items in a safe way: neatly
   * cleaning up the DOM, and accompanying arrays this.renderedItems and
   * the created clusters.
   */
  links.Timeline.prototype.clearItems = function() {
      // add all visible items to the list to be hidden
      var hideItems = this.renderQueue.hide;
      this.renderedItems.forEach(function (item) {
          hideItems.push(item);
      });
  
      // clear the cluster generator
      this.clusterGenerator.clear();
  
      // actually clear the items
      this.items = [];
  };
  
  /**
   * Repaint all items
   * @return {boolean} needsReflow   Returns true if the DOM is changed such that
   *                                 a reflow is needed.
   */
  links.Timeline.prototype.repaintItems = function() {
      var i, iMax, item, index;
  
      var needsReflow = false,
          dom = this.dom,
          size = this.size,
          timeline = this,
          renderedItems = this.renderedItems;
  
      if (!dom.items) {
          dom.items = {};
      }
  
      // draw the frame containing the items
      var frame = dom.items.frame;
      if (!frame) {
          frame = document.createElement("DIV");
          frame.style.position = "relative";
          dom.content.appendChild(frame);
          dom.items.frame = frame;
      }
  
      frame.style.left = "0px";
      frame.style.top = size.items.top + "px";
      frame.style.height = "0px";
  
      // Take frame offline (for faster manipulation of the DOM)
      dom.content.removeChild(frame);
  
      // process the render queue with changes
      var queue = this.renderQueue;
      var newImageUrls = [];
      needsReflow = needsReflow ||
          (queue.show.length > 0) ||
          (queue.update.length > 0) ||
          (queue.hide.length > 0);   // TODO: reflow needed on hide of items?
  
      while (item = queue.show.shift()) {
          item.showDOM(frame);
          item.getImageUrls(newImageUrls);
          renderedItems.push(item);
      }
      while (item = queue.update.shift()) {
          item.updateDOM(frame);
          item.getImageUrls(newImageUrls);
          index = this.renderedItems.indexOf(item);
          if (index == -1) {
              renderedItems.push(item);
          }
      }
      while (item = queue.hide.shift()) {
          item.hideDOM(frame);
          index = this.renderedItems.indexOf(item);
          if (index != -1) {
              renderedItems.splice(index, 1);
          }
      }
  
      // reposition all visible items
      renderedItems.forEach(function (item) {
          item.updatePosition(timeline);
      });
  
      // redraw the delete button and dragareas of the selected item (if any)
      this.repaintDeleteButton();
      this.repaintDragAreas();
  
      // put frame online again
      dom.content.appendChild(frame);
  
      if (newImageUrls.length) {
          // retrieve all image sources from the items, and set a callback once
          // all images are retrieved
          var callback = function () {
              timeline.render();
          };
          var sendCallbackWhenAlreadyLoaded = false;
          links.imageloader.loadAll(newImageUrls, callback, sendCallbackWhenAlreadyLoaded);
      }
  
      return needsReflow;
  };
  
  /**
   * Reflow the size of the groups
   * @return {boolean} resized    Returns true if any of the frame elements
   *                              have been resized.
   */
  links.Timeline.prototype.reflowGroups = function() {
      var resized = false,
          options = this.options,
          size = this.size,
          dom = this.dom;
  
      // calculate the groups width and height
      // TODO: only update when data is changed! -> use an updateSeq
      var groupsWidth = 0;
  
      // loop through all groups to get the labels width and height
      var groups = this.groups;
      var labels = this.dom.groups ? this.dom.groups.labels : [];
      for (var i = 0, iMax = groups.length; i < iMax; i++) {
          var group = groups[i];
          var label = labels[i];
          group.labelWidth  = label ? label.clientWidth : 0;
          group.labelHeight = label ? label.clientHeight : 0;
          group.width = group.labelWidth;  // TODO: group.width is redundant with labelWidth
  
          groupsWidth = Math.max(groupsWidth, group.width);
      }
  
      // limit groupsWidth to the groups width in the options
      if (options.groupsWidth !== undefined) {
          groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
      }
  
      // compensate for the border width. TODO: calculate the real border width
      groupsWidth += 1;
  
      var groupsLeft = options.groupsOnRight ? size.frameWidth - groupsWidth : 0;
      resized = resized || (size.groupsWidth !== groupsWidth);
      resized = resized || (size.groupsLeft !== groupsLeft);
      size.groupsWidth = groupsWidth;
      size.groupsLeft = groupsLeft;
  
      return resized;
  };
  
  /**
   * Redraw the group labels
   */
  links.Timeline.prototype.repaintGroups = function() {
      var dom = this.dom,
          timeline = this,
          options = this.options,
          size = this.size,
          groups = this.groups;
  
      if (dom.groups === undefined) {
          dom.groups = {};
      }
  
      var labels = dom.groups.labels;
      if (!labels) {
          labels = [];
          dom.groups.labels = labels;
      }
      var labelLines = dom.groups.labelLines;
      if (!labelLines) {
          labelLines = [];
          dom.groups.labelLines = labelLines;
      }
      var itemLines = dom.groups.itemLines;
      if (!itemLines) {
          itemLines = [];
          dom.groups.itemLines = itemLines;
      }
  
      // create the frame for holding the groups
      var frame = dom.groups.frame;
      if (!frame) {
          frame =  document.createElement("DIV");
          frame.className = "timeline-groups-axis";
          frame.style.position = "absolute";
          frame.style.overflow = "hidden";
          frame.style.top = "0px";
          frame.style.height = "100%";
  
          dom.frame.appendChild(frame);
          dom.groups.frame = frame;
      }
  
      frame.style.left = size.groupsLeft + "px";
      frame.style.width = (options.groupsWidth !== undefined) ?
          options.groupsWidth :
          size.groupsWidth + "px";
  
      // hide groups axis when there are no groups
      if (groups.length == 0) {
          frame.style.display = 'none';
      }
      else {
          frame.style.display = '';
      }
  
      // TODO: only create/update groups when data is changed.
  
      // create the items
      var current = labels.length,
          needed = groups.length;
  
      // overwrite existing group labels
      for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
          var group = groups[i];
          var label = labels[i];
          label.innerHTML = this.getGroupName(group);
          label.style.display = '';
      }
  
      // append new items when needed
      for (var i = current; i < needed; i++) {
          var group = groups[i];
  
          // create text label
          var label = document.createElement("DIV");
          label.className = "timeline-groups-text";
          label.style.position = "absolute";
          if (options.groupsWidth === undefined) {
              label.style.whiteSpace = "nowrap";
          }
          label.innerHTML = this.getGroupName(group);
          frame.appendChild(label);
          labels[i] = label;
  
          // create the grid line between the group labels
          var labelLine = document.createElement("DIV");
          labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
          labelLine.style.position = "absolute";
          labelLine.style.left = "0px";
          labelLine.style.width = "100%";
          labelLine.style.height = "0px";
          labelLine.style.borderTopStyle = "solid";
          frame.appendChild(labelLine);
          labelLines[i] = labelLine;
  
          // create the grid line between the items
          var itemLine = document.createElement("DIV");
          itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
          itemLine.style.position = "absolute";
          itemLine.style.left = "0px";
          itemLine.style.width = "100%";
          itemLine.style.height = "0px";
          itemLine.style.borderTopStyle = "solid";
          dom.content.insertBefore(itemLine, dom.content.firstChild);
          itemLines[i] = itemLine;
      }
  
      // remove redundant items from the DOM when needed
      for (var i = needed; i < current; i++) {
          var label = labels[i],
              labelLine = labelLines[i],
              itemLine = itemLines[i];
  
          frame.removeChild(label);
          frame.removeChild(labelLine);
          dom.content.removeChild(itemLine);
      }
      labels.splice(needed, current - needed);
      labelLines.splice(needed, current - needed);
      itemLines.splice(needed, current - needed);
      
      links.Timeline.addClassName(frame, options.groupsOnRight ? 'timeline-groups-axis-onright' : 'timeline-groups-axis-onleft');
  
      // position the groups
      for (var i = 0, iMax = groups.length; i < iMax; i++) {
          var group = groups[i],
              label = labels[i],
              labelLine = labelLines[i],
              itemLine = itemLines[i];
  
          label.style.top = group.labelTop + "px";
          labelLine.style.top = group.lineTop + "px";
          itemLine.style.top = group.lineTop + "px";
          itemLine.style.width = size.contentWidth + "px";
      }
  
      if (!dom.groups.background) {
          // create the axis grid line background
          var background = document.createElement("DIV");
          background.className = "timeline-axis";
          background.style.position = "absolute";
          background.style.left = "0px";
          background.style.width = "100%";
          background.style.border = "none";
  
          frame.appendChild(background);
          dom.groups.background = background;
      }
      dom.groups.background.style.top = size.axis.top + 'px';
      dom.groups.background.style.height = size.axis.height + 'px';
  
      if (!dom.groups.line) {
          // create the axis grid line
          var line = document.createElement("DIV");
          line.className = "timeline-axis";
          line.style.position = "absolute";
          line.style.left = "0px";
          line.style.width = "100%";
          line.style.height = "0px";
  
          frame.appendChild(line);
          dom.groups.line = line;
      }
      dom.groups.line.style.top = size.axis.line + 'px';
  
      // create a callback when there are images which are not yet loaded
      // TODO: more efficiently load images in the groups
      if (dom.groups.frame && groups.length) {
          var imageUrls = [];
          links.imageloader.filterImageUrls(dom.groups.frame, imageUrls);
          if (imageUrls.length) {
              // retrieve all image sources from the items, and set a callback once
              // all images are retrieved
              var callback = function () {
                  timeline.render();
              };
              var sendCallbackWhenAlreadyLoaded = false;
              links.imageloader.loadAll(imageUrls, callback, sendCallbackWhenAlreadyLoaded);
          }
      }
  };
  
  
  /**
   * Redraw the current time bar
   */
  links.Timeline.prototype.repaintCurrentTime = function() {
      var options = this.options,
          dom = this.dom,
          size = this.size;
  
      if (!options.showCurrentTime) {
          if (dom.currentTime) {
              dom.contentTimelines.removeChild(dom.currentTime);
              delete dom.currentTime;
          }
  
          return;
      }
  
      if (!dom.currentTime) {
          // create the current time bar
          var currentTime = document.createElement("DIV");
          currentTime.className = "timeline-currenttime";
          currentTime.style.position = "absolute";
          currentTime.style.top = "0px";
          currentTime.style.height = "100%";
  
          dom.contentTimelines.appendChild(currentTime);
          dom.currentTime = currentTime;
      }
  
      var now = new Date();
      var nowOffset = new Date(now.valueOf() + this.clientTimeOffset);
      var x = this.timeToScreen(nowOffset);
  
      var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
      dom.currentTime.style.display = visible ? '' : 'none';
      dom.currentTime.style.left = x + "px";
      dom.currentTime.title = "Current time: " + nowOffset;
  
      // start a timer to adjust for the new time
      if (this.currentTimeTimer != undefined) {
          clearTimeout(this.currentTimeTimer);
          delete this.currentTimeTimer;
      }
      var timeline = this;
      var onTimeout = function() {
          timeline.repaintCurrentTime();
      };
      // the time equal to the width of one pixel, divided by 2 for more smoothness
      var interval = 1 / this.conversion.factor / 2;
      if (interval < 30) interval = 30;
      this.currentTimeTimer = setTimeout(onTimeout, interval);
  };
  
  /**
   * Redraw the custom time bar
   */
  links.Timeline.prototype.repaintCustomTime = function() {
      var options = this.options,
          dom = this.dom,
          size = this.size;
  
      if (!options.showCustomTime) {
          if (dom.customTime) {
              dom.contentTimelines.removeChild(dom.customTime);
              delete dom.customTime;
          }
  
          return;
      }
  
      if (!dom.customTime) {
          var customTime = document.createElement("DIV");
          customTime.className = "timeline-customtime";
          customTime.style.position = "absolute";
          customTime.style.top = "0px";
          customTime.style.height = "100%";
  
          var drag = document.createElement("DIV");
          drag.style.position = "relative";
          drag.style.top = "0px";
          drag.style.left = "-10px";
          drag.style.height = "100%";
          drag.style.width = "20px";
          customTime.appendChild(drag);
  
          dom.contentTimelines.appendChild(customTime);
          dom.customTime = customTime;
  
          // initialize parameter
          this.customTime = new Date();
      }
  
      var x = this.timeToScreen(this.customTime),
          visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
      dom.customTime.style.display = visible ? '' : 'none';
      dom.customTime.style.left = x + "px";
      dom.customTime.title = "Time: " + this.customTime;
  };
  
  
  /**
   * Redraw the delete button, on the top right of the currently selected item
   * if there is no item selected, the button is hidden.
   */
  links.Timeline.prototype.repaintDeleteButton = function () {
      var timeline = this,
          dom = this.dom,
          frame = dom.items.frame;
  
      var deleteButton = dom.items.deleteButton;
      if (!deleteButton) {
          // create a delete button
          deleteButton = document.createElement("DIV");
          deleteButton.className = "timeline-navigation-delete";
          deleteButton.style.position = "absolute";
  
          frame.appendChild(deleteButton);
          dom.items.deleteButton = deleteButton;
      }
  
      var index = this.selection ? this.selection.index : -1,
          item = this.selection ? this.items[index] : undefined;
      if (item && item.rendered && this.isEditable(item)) {
          var right = item.getRight(this),
              top = item.top;
  
          deleteButton.style.left = right + 'px';
          deleteButton.style.top = top + 'px';
          deleteButton.style.display = '';
          frame.removeChild(deleteButton);
          frame.appendChild(deleteButton);
      }
      else {
          deleteButton.style.display = 'none';
      }
  };
  
  
  /**
   * Redraw the drag areas. When an item (ranges only) is selected,
   * it gets a drag area on the left and right side, to change its width
   */
  links.Timeline.prototype.repaintDragAreas = function () {
      var timeline = this,
          options = this.options,
          dom = this.dom,
          frame = this.dom.items.frame;
  
      // create left drag area
      var dragLeft = dom.items.dragLeft;
      if (!dragLeft) {
          dragLeft = document.createElement("DIV");
          dragLeft.className="timeline-event-range-drag-left";
          dragLeft.style.position = "absolute";
  
          frame.appendChild(dragLeft);
          dom.items.dragLeft = dragLeft;
      }
  
      // create right drag area
      var dragRight = dom.items.dragRight;
      if (!dragRight) {
          dragRight = document.createElement("DIV");
          dragRight.className="timeline-event-range-drag-right";
          dragRight.style.position = "absolute";
  
          frame.appendChild(dragRight);
          dom.items.dragRight = dragRight;
      }
  
      // reposition left and right drag area
      var index = this.selection ? this.selection.index : -1,
          item = this.selection ? this.items[index] : undefined;
      if (item && item.rendered && this.isEditable(item) &&
          (item instanceof links.Timeline.ItemRange)) {
          var 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.width = options.dragAreaWidth + "px";
          dragLeft.style.height = height + 'px';
          dragLeft.style.display = '';
          frame.removeChild(dragLeft);
          frame.appendChild(dragLeft);
  
          dragRight.style.left = (right - options.dragAreaWidth) + 'px';
          dragRight.style.top = top + 'px';
          dragRight.style.width = options.dragAreaWidth + "px";
          dragRight.style.height = height + 'px';
          dragRight.style.display = '';
          frame.removeChild(dragRight);
          frame.appendChild(dragRight);
      }
      else {
          dragLeft.style.display = 'none';
          dragRight.style.display = 'none';
      }
  };
  
  /**
   * Create the navigation buttons for zooming and moving
   */
  links.Timeline.prototype.repaintNavigation = function () {
      var timeline = this,
          options = this.options,
          dom = this.dom,
          frame = dom.frame,
          navBar = dom.navBar;
  
      if (!navBar) {
          var showButtonNew = options.showButtonNew && options.editable;
          var showNavigation = options.showNavigation && (options.zoomable || options.moveable);
          if (showNavigation || showButtonNew) {
              // create a navigation bar containing the navigation buttons
              navBar = document.createElement("DIV");
              navBar.style.position = "absolute";
              navBar.className = "timeline-navigation ui-widget ui-state-highlight ui-corner-all";
              if (options.groupsOnRight) {
                  navBar.style.left = '10px';
              }
              else {
                  navBar.style.right = '10px';
              }
              if (options.axisOnTop) {
                  navBar.style.bottom = '10px';
              }
              else {
                  navBar.style.top = '10px';
              }
              dom.navBar = navBar;
              frame.appendChild(navBar);
          }
  
          if (showButtonNew) {
              // create a new in button
              navBar.addButton = document.createElement("DIV");
              navBar.addButton.className = "timeline-navigation-new";
              navBar.addButton.title = options.CREATE_NEW_EVENT;
              var addIconSpan = document.createElement("SPAN");
              addIconSpan.className = "ui-icon ui-icon-circle-plus";            
              navBar.addButton.appendChild(addIconSpan);
              
              var onAdd = function(event) {
                  links.Timeline.preventDefault(event);
                  links.Timeline.stopPropagation(event);
  
                  // create a new event at the center of the frame
                  var w = timeline.size.contentWidth;
                  var x = w / 2;
                  var xstart = timeline.screenToTime(x - 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 = options.NEW;
                  var group = timeline.groups.length ? timeline.groups[0].content : undefined;
                  var preventRender = true;
                  timeline.addItem({
                      'start': xstart,
                      'end': xend,
                      'content': content,
                      'group': group
                  }, preventRender);
                  var index = (timeline.items.length - 1);
                  timeline.selectItem(index);
  
                  timeline.applyAdd = true;
  
                  // fire an add event.
                  // Note that the change can be canceled from within an event listener if
                  // this listener calls the method cancelAdd().
                  timeline.trigger('add');
  
                  if (timeline.applyAdd) {
                      // render and select the item
                      timeline.render({animate: false});
                      timeline.selectItem(index);
                  }
                  else {
                      // undo an add
                      timeline.deleteItem(index);
                  }
              };
              links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
              navBar.appendChild(navBar.addButton);
          }
  
          if (showButtonNew && showNavigation) {
              // create a separator line
              links.Timeline.addClassName(navBar.addButton, 'timeline-navigation-new-line');
          }
  
          if (showNavigation) {
              if (options.zoomable) {
                  // create a zoom in button
                  navBar.zoomInButton = document.createElement("DIV");
                  navBar.zoomInButton.className = "timeline-navigation-zoom-in";
                  navBar.zoomInButton.title = this.options.ZOOM_IN;
                  var ziIconSpan = document.createElement("SPAN");
                  ziIconSpan.className = "ui-icon ui-icon-circle-zoomin";
                  navBar.zoomInButton.appendChild(ziIconSpan);
                  
                  var onZoomIn = function(event) {
                      links.Timeline.preventDefault(event);
                      links.Timeline.stopPropagation(event);
                      timeline.zoom(0.4);
                      timeline.trigger("rangechange");
                      timeline.trigger("rangechanged");
                  };
                  links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
                  navBar.appendChild(navBar.zoomInButton);
  
                  // create a zoom out button
                  navBar.zoomOutButton = document.createElement("DIV");
                  navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
                  navBar.zoomOutButton.title = this.options.ZOOM_OUT;
                  var zoIconSpan = document.createElement("SPAN");
                  zoIconSpan.className = "ui-icon ui-icon-circle-zoomout";
                  navBar.zoomOutButton.appendChild(zoIconSpan);
                  
                  var onZoomOut = function(event) {
                      links.Timeline.preventDefault(event);
                      links.Timeline.stopPropagation(event);
                      timeline.zoom(-0.4);
                      timeline.trigger("rangechange");
                      timeline.trigger("rangechanged");
                  };
                  links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
                  navBar.appendChild(navBar.zoomOutButton);
              }
  
              if (options.moveable) {
                  // create a move left button
                  navBar.moveLeftButton = document.createElement("DIV");
                  navBar.moveLeftButton.className = "timeline-navigation-move-left";
                  navBar.moveLeftButton.title = this.options.MOVE_LEFT;
                  var mlIconSpan = document.createElement("SPAN");
                  mlIconSpan.className = "ui-icon ui-icon-circle-arrow-w";
                  navBar.moveLeftButton.appendChild(mlIconSpan);
                  
                  var onMoveLeft = function(event) {
                      links.Timeline.preventDefault(event);
                      links.Timeline.stopPropagation(event);
                      timeline.move(-0.2);
                      timeline.trigger("rangechange");
                      timeline.trigger("rangechanged");
                  };
                  links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
                  navBar.appendChild(navBar.moveLeftButton);
  
                  // create a move right button
                  navBar.moveRightButton = document.createElement("DIV");
                  navBar.moveRightButton.className = "timeline-navigation-move-right";
                  navBar.moveRightButton.title = this.options.MOVE_RIGHT;
                  var mrIconSpan = document.createElement("SPAN");
                  mrIconSpan.className = "ui-icon ui-icon-circle-arrow-e";
                  navBar.moveRightButton.appendChild(mrIconSpan);
                  
                  var onMoveRight = function(event) {
                      links.Timeline.preventDefault(event);
                      links.Timeline.stopPropagation(event);
                      timeline.move(0.2);
                      timeline.trigger("rangechange");
                      timeline.trigger("rangechanged");
                  };
                  links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
                  navBar.appendChild(navBar.moveRightButton);
              }
          }
      }
  };
  
  
  /**
   * Set current time. This function can be used to set the time in the client
   * timeline equal with the time on a server.
   * @param {Date} time
   */
  links.Timeline.prototype.setCurrentTime = function(time) {
      var now = new Date();
      this.clientTimeOffset = (time.valueOf() - now.valueOf());
  
      this.repaintCurrentTime();
  };
  
  /**
   * Get current time. The time can have an offset from the real time, when
   * the current time has been changed via the method setCurrentTime.
   * @return {Date} time
   */
  links.Timeline.prototype.getCurrentTime = function() {
      var now = new Date();
      return new Date(now.valueOf() + this.clientTimeOffset);
  };
  
  
  /**
   * Set custom time.
   * The custom time bar can be used to display events in past or future.
   * @param {Date} time
   */
  links.Timeline.prototype.setCustomTime = function(time) {
      this.customTime = new Date(time.valueOf());
      this.repaintCustomTime();
  };
  
  /**
   * Retrieve the current custom time.
   * @return {Date} customTime
   */
  links.Timeline.prototype.getCustomTime = function() {
      return new Date(this.customTime.valueOf());
  };
  
  /**
   * Set a custom scale. Autoscaling will be disabled.
   * For example setScale(SCALE.MINUTES, 5) will result
   * in minor steps of 5 minutes, and major steps of an hour.
   *
   * @param {links.Timeline.StepDate.SCALE} scale
   *                               A scale. Choose from SCALE.MILLISECOND,
   *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
   *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
   *                               SCALE.YEAR.
   * @param {int}        step   A step size, by default 1. Choose for
   *                               example 1, 2, 5, or 10.
   */
  links.Timeline.prototype.setScale = function(scale, step) {
      this.step.setScale(scale, step);
      this.render(); // TODO: optimize: only reflow/repaint axis
  };
  
  /**
   * Enable or disable autoscaling
   * @param {boolean} enable  If true or not defined, autoscaling is enabled.
   *                          If false, autoscaling is disabled.
   */
  links.Timeline.prototype.setAutoScale = function(enable) {
      this.step.setAutoScale(enable);
      this.render(); // TODO: optimize: only reflow/repaint axis
  };
  
  /**
   * Redraw the timeline
   * Reloads the (linked) data table and redraws the timeline when resized.
   * See also the method checkResize
   */
  links.Timeline.prototype.redraw = function() {
      this.setData(this.data);
  };
  
  
  /**
   * Check if the timeline is resized, and if so, redraw the timeline.
   * Useful when the webpage is resized.
   */
  links.Timeline.prototype.checkResize = function() {
      // TODO: re-implement the method checkResize, or better, make it redundant as this.render will be smarter
      this.render();
  };
  
  /**
   * Check whether a given item is editable
   * @param {links.Timeline.Item} item
   * @return {boolean} editable
   */
  links.Timeline.prototype.isEditable = function (item) {
      if (item) {
          if (item.editable != undefined) {
              return item.editable;
          }
          else {
              return this.options.editable;
          }
      }
      return false;
  };
  
  /**
   * Calculate the factor and offset to convert a position on screen to the
   * corresponding date and vice versa.
   * After the method calcConversionFactor is executed once, the methods screenToTime and
   * timeToScreen can be used.
   */
  links.Timeline.prototype.recalcConversion = function() {
      this.conversion.offset = this.start.valueOf();
      this.conversion.factor = this.size.contentWidth /
          (this.end.valueOf() - this.start.valueOf());
  };
  
  
  /**
   * Convert a position on screen (pixels) to a datetime
   * Before this method can be used, the method calcConversionFactor must be
   * executed once.
   * @param {int}     x    Position on the screen in pixels
   * @return {Date}   time The datetime the corresponds with given position x
   */
  links.Timeline.prototype.screenToTime = function(x) {
      var conversion = this.conversion;
      return new Date(x / conversion.factor + conversion.offset);
  };
  
  /**
   * Convert a datetime (Date object) into a position on the screen
   * Before this method can be used, the method calcConversionFactor must be
   * executed once.
   * @param {Date}   time A date
   * @return {int}   x    The position on the screen in pixels which corresponds
   *                      with the given date.
   */
  links.Timeline.prototype.timeToScreen = function(time) {
      var conversion = this.conversion;
      return (time.valueOf() - conversion.offset) * conversion.factor;
  };
  
  
  
  /**
   * Event handler for touchstart event on mobile devices
   */
  links.Timeline.prototype.onTouchStart = function(event) {
      var params = this.eventParams,
          me = this;
  
      if (params.touchDown) {
          // if already moving, return
          return;
      }
  
      params.touchDown = true;
      params.zoomed = false;
  
      this.onMouseDown(event);
  
      if (!params.onTouchMove) {
          params.onTouchMove = function (event) {me.onTouchMove(event);};
          links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
      }
      if (!params.onTouchEnd) {
          params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
          links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
      }
  
      /* TODO
       // check for double tap event
       var delta = 500; // ms
       var doubleTapStart = (new Date()).valueOf();
       var target = links.Timeline.getTarget(event);
       var doubleTapItem = this.getItemIndex(target);
       if (params.doubleTapStart &&
       (doubleTapStart - params.doubleTapStart) < delta &&
       doubleTapItem == params.doubleTapItem) {
       delete params.doubleTapStart;
       delete params.doubleTapItem;
       me.onDblClick(event);
       params.touchDown = false;
       }
       params.doubleTapStart = doubleTapStart;
       params.doubleTapItem = doubleTapItem;
       */
      // store timing for double taps
      var target = links.Timeline.getTarget(event);
      var item = this.getItemIndex(target);
      params.doubleTapStartPrev = params.doubleTapStart;
      params.doubleTapStart = (new Date()).valueOf();
      params.doubleTapItemPrev = params.doubleTapItem;
      params.doubleTapItem = item;
  
      links.Timeline.preventDefault(event);
  };
  
  /**
   * Event handler for touchmove event on mobile devices
   */
  links.Timeline.prototype.onTouchMove = function(event) {
      var params = this.eventParams;
  
      if (event.scale && event.scale !== 1) {
          params.zoomed = true;
      }
  
      if (!params.zoomed) {
          // move 
          this.onMouseMove(event);
      }
      else {
          if (this.options.zoomable) {
              // pinch
              // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
              params.zoomed = true;
  
              var scale = event.scale,
                  oldWidth = (params.end.valueOf() - params.start.valueOf()),
                  newWidth = oldWidth / scale,
                  diff = newWidth - oldWidth,
                  start = new Date(parseInt(params.start.valueOf() - diff/2)),
                  end = new Date(parseInt(params.end.valueOf() + diff/2));
  
              // TODO: determine zoom-around-date from touch positions?
  
              this.setVisibleChartRange(start, end);
              this.trigger("rangechange");
          }
      }
  
      links.Timeline.preventDefault(event);
  };
  
  /**
   * Event handler for touchend event on mobile devices
   */
  links.Timeline.prototype.onTouchEnd = function(event) {
      var params = this.eventParams;
      var me = this;
      params.touchDown = false;
  
      if (params.zoomed) {
          this.trigger("rangechanged");
      }
  
      if (params.onTouchMove) {
          links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
          delete params.onTouchMove;
  
      }
      if (params.onTouchEnd) {
          links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
          delete params.onTouchEnd;
      }
  
      this.onMouseUp(event);
  
      // check for double tap event
      var delta = 500; // ms
      var doubleTapEnd = (new Date()).valueOf();
      var target = links.Timeline.getTarget(event);
      var doubleTapItem = this.getItemIndex(target);
      if (params.doubleTapStartPrev &&
          (doubleTapEnd - params.doubleTapStartPrev) < delta &&
          params.doubleTapItem == params.doubleTapItemPrev) {
          params.touchDown = true;
          me.onDblClick(event);
          params.touchDown = false;
      }
  
      links.Timeline.preventDefault(event);
  };
  
  
  /**
   * Start a moving operation inside the provided parent element
   * @param {Event} event       The event that occurred (required for
   *                             retrieving the  mouse position)
   */
  links.Timeline.prototype.onMouseDown = function(event) {
      event = event || window.event;
  
      var params = this.eventParams,
          options = this.options,
          dom = this.dom;
  
      // only react on left mouse button down
      var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
      if (!leftButtonDown && !params.touchDown) {
          return;
      }
  
      // get mouse position
      params.mouseX = links.Timeline.getPageX(event);
      params.mouseY = links.Timeline.getPageY(event);
      params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
      params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
      params.previousLeft = 0;
      params.previousOffset = 0;
  
      params.moved = false;
      params.start = new Date(this.start.valueOf());
      params.end = new Date(this.end.valueOf());
  
      params.target = links.Timeline.getTarget(event);
      var dragLeft = (dom.items && dom.items.dragLeft) ? dom.items.dragLeft : undefined;
      var dragRight = (dom.items && dom.items.dragRight) ? dom.items.dragRight : undefined;
      params.itemDragLeft = (params.target === dragLeft);
      params.itemDragRight = (params.target === dragRight);
  
      if (params.itemDragLeft || params.itemDragRight) {
          params.itemIndex = this.selection ? this.selection.index : undefined;
      }
      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.valueOf());
          var content = options.NEW;
          var group = this.getGroupFromHeight(y);
          this.addItem({
              'start': xstart,
              'end': xend,
              'content': content,
              'group': this.getGroupName(group)
          });
          params.itemIndex = (this.items.length - 1);
          this.selectItem(params.itemIndex);
          params.itemDragRight = true;
      }
  
      var item = this.items[params.itemIndex];
      var isSelected = this.isSelected(params.itemIndex);
      params.editItem = isSelected && this.isEditable(item);
      if (params.editItem) {
          params.itemStart = item.start;
          params.itemEnd = item.end;
          params.itemGroup = item.group;
          params.itemLeft = item.start ? this.timeToScreen(item.start) : undefined;
          params.itemRight = item.end ? this.timeToScreen(item.end) : undefined;
      }
      else {
          this.dom.frame.style.cursor = 'move';
      }
      if (!params.touchDown) {
          // add event listeners to handle moving the contents
          // we store the function onmousemove and onmouseup in the timeline, so we can
          // remove the eventlisteners lateron in the function mouseUp()
          var me = this;
          if (!params.onMouseMove) {
              params.onMouseMove = function (event) {me.onMouseMove(event);};
              links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
          }
          if (!params.onMouseUp) {
              params.onMouseUp = function (event) {me.onMouseUp(event);};
              links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
          }
  
          links.Timeline.preventDefault(event);
      }
  };
  
  
  /**
   * Perform moving operating.
   * This function activated from within the funcion links.Timeline.onMouseDown().
   * @param {Event}   event  Well, eehh, the event
   */
  links.Timeline.prototype.onMouseMove = function (event) {
      event = event || window.event;
  
      var params = this.eventParams,
          size = this.size,
          dom = this.dom,
          options = this.options;
  
      // calculate change in mouse position
      var mouseX = links.Timeline.getPageX(event);
      var mouseY = links.Timeline.getPageY(event);
  
      if (params.mouseX == undefined) {
          params.mouseX = mouseX;
      }
      if (params.mouseY == undefined) {
          params.mouseY = mouseY;
      }
  
      var diffX = mouseX - params.mouseX;
      var diffY = mouseY - params.mouseY;
  
      // if mouse movement is big enough, register it as a "moved" event
      if (Math.abs(diffX) >= 1) {
          params.moved = true;
      }
  
      if (params.customTime) {
          var x = this.timeToScreen(params.customTime);
          var xnew = x + diffX;
          this.customTime = this.screenToTime(xnew);
          this.repaintCustomTime();
  
          // fire a timechange event
          this.trigger('timechange');
      }
      else if (params.editItem) {
          var item = this.items[params.itemIndex],
              left,
              right;
  
          if (params.itemDragLeft) {
              // 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);
              }
          }
  
          item.setPosition(left, right);
  
          var dragging = params.itemDragLeft || params.itemDragRight;
          if (this.groups.length && !dragging) {
              // move item from one group to another when needed
              var y = mouseY - params.frameTop;
              var group = this.getGroupFromHeight(y);
              if (options.groupsChangeable && item.group !== group) {
                  // move item to the other group
                  var index = this.items.indexOf(item);
                  this.changeItem(index, {'group': this.getGroupName(group)});
              }
              else {
                  this.repaintDeleteButton();
                  this.repaintDragAreas();
              }
          }
          else {
              // TODO: does not work well in FF, forces redraw with every mouse move it seems
              this.render(); // TODO: optimize, only redraw the items?
              // Note: when animate==true, no redraw is needed here, its done by stackItems animation
          }
      }
      else if (options.moveable) {
          var interval = (params.end.valueOf() - params.start.valueOf());
          var diffMillisecs = Math.round((-diffX) / size.contentWidth * interval);
          var newStart = new Date(params.start.valueOf() + diffMillisecs);
          var newEnd = new Date(params.end.valueOf() + diffMillisecs);
          this.applyRange(newStart, newEnd);
          // if the applied range is moved due to a fixed min or max,
          // change the diffMillisecs accordingly
          var appliedDiff = (this.start.valueOf() - newStart.valueOf());
          if (appliedDiff) {
              diffMillisecs += appliedDiff;
          }
  
          this.recalcConversion();
  
          // move the items by changing the left position of their frame.
          // this is much faster than repositioning all elements individually via the 
          // repaintFrame() function (which is done once at mouseup)
          // note that we round diffX to prevent wrong positioning on millisecond scale
          var previousLeft = params.previousLeft || 0;
          var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
          var previousOffset = params.previousOffset || 0;
          var frameOffset = previousOffset + (currentLeft - previousLeft);
          var frameLeft = -diffMillisecs / interval * size.contentWidth + frameOffset;
  
          dom.items.frame.style.left = (frameLeft) + "px";
  
          // read the left again from DOM (IE8- rounds the value)
          params.previousOffset = frameOffset;
          params.previousLeft = parseFloat(dom.items.frame.style.left) || frameLeft;
  
          this.repaintCurrentTime();
          this.repaintCustomTime();
          this.repaintAxis();
  
          // fire a rangechange event
          this.trigger('rangechange');
      }
  
      links.Timeline.preventDefault(event);
  };
  
  
  /**
   * Stop moving operating.
   * This function activated from within the funcion links.Timeline.onMouseDown().
   * @param {event}  event   The event
   */
  links.Timeline.prototype.onMouseUp = function (event) {
      var params = this.eventParams,
          options = this.options;
  
      event = event || window.event;
  
      this.dom.frame.style.cursor = 'auto';
  
      // remove event listeners here, important for Safari
      if (params.onMouseMove) {
          links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
          delete params.onMouseMove;
      }
      if (params.onMouseUp) {
          links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
          delete params.onMouseUp;
      }
      //links.Timeline.preventDefault(event);
  
      if (params.customTime) {
          // fire a timechanged event
          this.trigger('timechanged');
      }
      else if (params.editItem) {
          var item = this.items[params.itemIndex];
  
          if (params.moved || params.addItem) {
              this.applyChange = true;
              this.applyAdd = true;
  
              this.updateData(params.itemIndex, {
                  'start': item.start,
                  'end': item.end
              });
  
              // fire an add or 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': this.getGroupName(item.group)
                      });
                  }
                  else {
                      // undo an add
                      this.deleteItem(params.itemIndex);
                  }
              }
              else {
                  if (this.applyChange) {
                      this.updateData(params.itemIndex, {
                          'start': item.start,
                          'end': item.end
                      });
                  }
                  else {
                      // undo a change
                      delete this.applyChange;
                      delete this.applyAdd;
  
                      var item = this.items[params.itemIndex],
                          domItem = item.dom;
  
                      item.start = params.itemStart;
                      item.end = params.itemEnd;
                      item.group = params.itemGroup;
                      // TODO: original group should be restored too
                      item.setPosition(params.itemLeft, params.itemRight);
                  }
              }
  
              // prepare data for clustering, by filtering and sorting by type
              if (this.options.cluster) {
                  this.clusterGenerator.updateData();
              }
  
              this.render();
          }
      }
      else {
          if (!params.moved && !params.zoomed) {
              // mouse did not move -> user has selected an item
  
              if (params.target === this.dom.items.deleteButton) {
                  // delete item
                  if (this.selection) {
                      this.confirmDeleteItem(this.selection.index);
                  }
              }
              else if (options.selectable) {
                  // select/unselect item
                  if (params.itemIndex != undefined) {
                      if (!this.isSelected(params.itemIndex)) {
                          this.selectItem(params.itemIndex);
                          this.trigger('select');
                      }
                  }
                  else {
                      if (options.unselectable) {
                          this.unselectItem();
                          this.trigger('select');
                      }
                  }
              }
          }
          else {
              // timeline is moved
              // TODO: optimize: no need to reflow and cluster again?
              this.render();
  
              if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
                  // fire a rangechanged event
                  this.trigger('rangechanged');
              }
          }
      }
  };
  
  /**
   * Double click event occurred for an item
   * @param {Event}  event
   */
  links.Timeline.prototype.onDblClick = function (event) {
      var params = this.eventParams,
          options = this.options,
          dom = this.dom,
          size = this.size;
      event = event || window.event;
  
      if (params.itemIndex != undefined) {
          var item = this.items[params.itemIndex];
          if (item && this.isEditable(item)) {
              // fire the edit event
              this.trigger('edit');
          }
      }
      else {
          if (options.editable) {
              // create a new item
  
              // get mouse position
              params.mouseX = links.Timeline.getPageX(event);
              params.mouseY = links.Timeline.getPageY(event);
              var x = params.mouseX - links.Timeline.getAbsoluteLeft(dom.content);
              var y = params.mouseY - links.Timeline.getAbsoluteTop(dom.content);
  
              // create a new event at the current mouse position
              var xstart = this.screenToTime(x);
              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 = options.NEW;
              var group = this.getGroupFromHeight(y);   // (group may be undefined)
              var preventRender = true;
              this.addItem({
                  'start': xstart,
                  'end': xend,
                  'content': content,
                  'group': this.getGroupName(group)
              }, preventRender);
              params.itemIndex = (this.items.length - 1);
              this.selectItem(params.itemIndex);
  
              this.applyAdd = true;
  
              // fire an add event.
              // Note that the change can be canceled from within an event listener if
              // this listener calls the method cancelAdd().
              this.trigger('add');
  
              if (this.applyAdd) {
                  // render and select the item
                  this.render({animate: false});
                  this.selectItem(params.itemIndex);
              }
              else {
                  // undo an add
                  this.deleteItem(params.itemIndex);
              }
          }
      }
  
      links.Timeline.preventDefault(event);
  };
  
  
  /**
   * Event handler for mouse wheel event, used to zoom the timeline
   * Code from http://adomas.org/javascript-mouse-wheel/
   * @param {Event}  event   The event
   */
  links.Timeline.prototype.onMouseWheel = function(event) {
      if (!this.options.zoomable)
          return;
  
      if (!event) { /* For IE. */
          event = window.event;
      }
  
      // retrieve delta    
      var delta = 0;
      if (event.wheelDelta) { /* IE/Opera. */
          delta = event.wheelDelta/120;
      } else if (event.detail) { /* Mozilla case. */
          // In Mozilla, sign of delta is different than in IE.
          // Also, delta is multiple of 3.
          delta = -event.detail/3;
      }
  
      // If delta is nonzero, handle it.
      // Basically, delta is now positive if wheel was scrolled up,
      // and negative, if wheel was scrolled down.
      if (delta) {
          // TODO: on FireFox, the window is not redrawn within repeated scroll-events 
          // -> use a delayed redraw? Make a zoom queue?
  
          var timeline = this;
          var zoom = function () {
              // perform the zoom action. Delta is normally 1 or -1
              var zoomFactor = delta / 5.0;
              var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
              var mouseX = links.Timeline.getPageX(event);
              var zoomAroundDate =
                  (mouseX != undefined && frameLeft != undefined) ?
                      timeline.screenToTime(mouseX - frameLeft) :
                      undefined;
  
              timeline.zoom(zoomFactor, zoomAroundDate);
  
              // fire a rangechange and a rangechanged event
              timeline.trigger("rangechange");
              timeline.trigger("rangechanged");
          };
  
          var scroll = function () {
              // Scroll the timeline
              timeline.move(delta * -0.2);
              timeline.trigger("rangechange");
              timeline.trigger("rangechanged");
          };
  
          if (event.shiftKey) {
              scroll();
          }
          else {
              zoom();
          }
      }
  
      // Prevent default actions caused by mouse wheel.
      // That might be ugly, but we handle scrolls somehow
      // anyway, so don't bother here...
      links.Timeline.preventDefault(event);
  };
  
  
  /**
   * Zoom the timeline the given zoomfactor in or out. Start and end date will
   * be adjusted, and the timeline will be redrawn. You can optionally give a
   * date around which to zoom.
   * For example, try zoomfactor = 0.1 or -0.1
   * @param {Number} zoomFactor      Zooming amount. Positive value will zoom in,
   *                                 negative value will zoom out
   * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
   */
  links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
      // if zoomAroundDate is not provided, take it half between start Date and end Date
      if (zoomAroundDate == undefined) {
          zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
      }
  
      // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
      // result in a start>=end )
      if (zoomFactor >= 1) {
          zoomFactor = 0.9;
      }
      if (zoomFactor <= -1) {
          zoomFactor = -0.9;
      }
  
      // adjust a negative factor such that zooming in with 0.1 equals zooming
      // out with a factor -0.1
      if (zoomFactor < 0) {
          zoomFactor = zoomFactor / (1 + zoomFactor);
      }
  
      // zoom start Date and end Date relative to the zoomAroundDate
      var startDiff = (this.start.valueOf() - zoomAroundDate);
      var endDiff = (this.end.valueOf() - zoomAroundDate);
  
      // calculate new dates
      var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
      var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);
  
      // only zoom in when interval is larger than minimum interval (to prevent
      // sliding to left/right when having reached the minimum zoom level)
      var interval = (newEnd.valueOf() - newStart.valueOf());
      var zoomMin = Number(this.options.zoomMin) || 10;
      if (zoomMin < 10) {
          zoomMin = 10;
      }
      if (interval >= zoomMin) {
          this.applyRange(newStart, newEnd, zoomAroundDate);
          this.render({
              animate: this.options.animate && this.options.animateZoom
          });
      }
  };
  
  /**
   * Move the timeline the given movefactor to the left or right. Start and end
   * date will be adjusted, and the timeline will be redrawn.
   * For example, try moveFactor = 0.1 or -0.1
   * @param {Number}  moveFactor      Moving amount. Positive value will move right,
   *                                 negative value will move left
   */
  links.Timeline.prototype.move = function(moveFactor) {
      // zoom start Date and end Date relative to the zoomAroundDate
      var diff = (this.end.valueOf() - this.start.valueOf());
  
      // apply new dates
      var newStart = new Date(this.start.valueOf() + diff * moveFactor);
      var newEnd   = new Date(this.end.valueOf() + diff * moveFactor);
      this.applyRange(newStart, newEnd);
  
      this.render(); // TODO: optimize, no need to reflow, only to recalc conversion and repaint
  };
  
  /**
   * Apply a visible range. The range is limited to feasible maximum and minimum
   * range.
   * @param {Date} start
   * @param {Date} end
   * @param {Date}   zoomAroundDate  Optional. Date around which will be zoomed.
   */
  links.Timeline.prototype.applyRange = function (start, end, zoomAroundDate) {
      // calculate new start and end value
      var startValue = start.valueOf(); // number
      var endValue = end.valueOf();     // number
      var interval = (endValue - startValue);
  
      // determine maximum and minimum interval
      var options = this.options;
      var year = 1000 * 60 * 60 * 24 * 365;
      var zoomMin = Number(options.zoomMin) || 10;
      if (zoomMin < 10) {
          zoomMin = 10;
      }
      var zoomMax = Number(options.zoomMax) || 10000 * year;
      if (zoomMax > 10000 * year) {
          zoomMax = 10000 * year;
      }
      if (zoomMax < zoomMin) {
          zoomMax = zoomMin;
      }
  
      // determine min and max date value
      var min = options.min ? options.min.valueOf() : undefined; // number
      var max = options.max ? options.max.valueOf() : undefined; // number
      if (min != undefined && max != undefined) {
          if (min >= max) {
              // empty range
              var day = 1000 * 60 * 60 * 24;
              max = min + day;
          }
          if (zoomMax > (max - min)) {
              zoomMax = (max - min);
          }
          if (zoomMin > (max - min)) {
              zoomMin = (max - min);
          }
      }
  
      // prevent empty interval
      if (startValue >= endValue) {
          endValue += 1000 * 60 * 60 * 24;
      }
  
      // prevent too small scale
      // TODO: IE has problems with milliseconds
      if (interval < zoomMin) {
          var diff = (zoomMin - interval);
          var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
          startValue -= Math.round(diff * f);
          endValue   += Math.round(diff * (1 - f));
      }
  
      // prevent too large scale
      if (interval > zoomMax) {
          var diff = (interval - zoomMax);
          var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
          startValue += Math.round(diff * f);
          endValue   -= Math.round(diff * (1 - f));
      }
  
      // prevent to small start date
      if (min != undefined) {
          var diff = (startValue - min);
          if (diff < 0) {
              startValue -= diff;
              endValue -= diff;
          }
      }
  
      // prevent to large end date
      if (max != undefined) {
          var diff = (max - endValue);
          if (diff < 0) {
              startValue += diff;
              endValue += diff;
          }
      }
  
      // apply new dates
      this.start = new Date(startValue);
      this.end = new Date(endValue);
  };
  
  /**
   * Delete an item after a confirmation.
   * The deletion can be cancelled by executing .cancelDelete() during the
   * triggered event 'delete'.
   * @param {int} index   Index of the item to be deleted
   */
  links.Timeline.prototype.confirmDeleteItem = function(index) {
      this.applyDelete = true;
  
      // select the event to be deleted
      if (!this.isSelected(index)) {
          this.selectItem(index);
      }
  
      // fire a delete event trigger. 
      // Note that the delete event can be canceled from within an event listener if 
      // this listener calls the method cancelChange().
      this.trigger('delete');
  
      if (this.applyDelete) {
          this.deleteItem(index);
      }
  
      delete this.applyDelete;
  };
  
  /**
   * Delete an item
   * @param {int} index   Index of the item to be deleted
   * @param {boolean} [preventRender=false]   Do not re-render timeline if true
   *                                          (optimization for multiple delete)
   */
  links.Timeline.prototype.deleteItem = function(index, preventRender) {
      if (index >= this.items.length) {
          throw "Cannot delete row, index out of range";
      }
  
      if (this.selection) {
          // adjust the selection
          if (this.selection.index == index) {
              // item to be deleted is selected
              this.unselectItem();
          }
          else if (this.selection.index > index) {
              // update selection index
              this.selection.index--;
          }
      }
  
      // actually delete the item and remove it from the DOM
      var item = this.items.splice(index, 1)[0];
      this.renderQueue.hide.push(item);
  
      // delete the row in the original data table
      if (this.data) {
          if (google && google.visualization &&
              this.data instanceof google.visualization.DataTable) {
              this.data.removeRow(index);
          }
          else if (links.Timeline.isArray(this.data)) {
              this.data.splice(index, 1);
          }
          else {
              throw "Cannot delete row from data, unknown data type";
          }
      }
  
      // prepare data for clustering, by filtering and sorting by type
      if (this.options.cluster) {
          this.clusterGenerator.updateData();
      }
  
      if (!preventRender) {
          this.render();
      }
  };
  
  
  /**
   * Delete all items
   */
  links.Timeline.prototype.deleteAllItems = function() {
      this.unselectItem();
  
      // delete the loaded items
      this.clearItems();
  
      // delete the groups
      this.deleteGroups();
  
      // empty original data table
      if (this.data) {
          if (google && google.visualization &&
              this.data instanceof google.visualization.DataTable) {
              this.data.removeRows(0, this.data.getNumberOfRows());
          }
          else if (links.Timeline.isArray(this.data)) {
              this.data.splice(0, this.data.length);
          }
          else {
              throw "Cannot delete row from data, unknown data type";
          }
      }
  
      // prepare data for clustering, by filtering and sorting by type
      if (this.options.cluster) {
          this.clusterGenerator.updateData();
      }
  
      this.render();
  };
  
  
  /**
   * Find the group from a given height in the timeline
   * @param {Number} height   Height in the timeline
   * @return {Object | undefined} group   The group object, or undefined if out
   *                                      of range
   */
  links.Timeline.prototype.getGroupFromHeight = function(height) {
      var i,
          group,
          groups = this.groups;
  
      if (groups.length) {
          if (this.options.axisOnTop) {
              for (i = groups.length - 1; i >= 0; i--) {
                  group = groups[i];
                  if (height > group.top) {
                      return group;
                  }
              }
          }
          else {
              for (i = 0; i < groups.length; i++) {
                  group = groups[i];
                  if (height > group.top) {
                      return group;
                  }
              }
          }
  
          return group; // return the last group
      }
  
      return undefined;
  };
  
  /**
   * @constructor links.Timeline.Item
   * @param {Object} data       Object containing parameters start, end
   *                            content, group, type, editable.
   * @param {Object} [options]  Options to set initial property values
   *                                {Number} top
   *                                {Number} left
   *                                {Number} width
   *                                {Number} height
   */
  links.Timeline.Item = function (data, options) {
      if (data) {
          /* TODO: use parseJSONDate as soon as it is tested and working (in two directions)
           this.start = links.Timeline.parseJSONDate(data.start);
           this.end = links.Timeline.parseJSONDate(data.end);
           */
          this.start = data.start;
          this.end = data.end;
          this.content = data.content;
          this.className = data.className;
          this.editable = data.editable;
          this.group = data.group;
          this.type = data.type;
      }
      this.top = 0;
      this.left = 0;
      this.width = 0;
      this.height = 0;
      this.lineWidth = 0;
      this.dotWidth = 0;
      this.dotHeight = 0;
  
      this.rendered = false; // true when the item is draw in the Timeline DOM
  
      if (options) {
          // override the default properties
          for (var option in options) {
              if (options.hasOwnProperty(option)) {
                  this[option] = options[option];
              }
          }
      }
  
  };
  
  
  
  /**
   * Reflow the Item: retrieve its actual size from the DOM
   * @return {boolean} resized    returns true if the axis is resized
   */
  links.Timeline.Item.prototype.reflow = function () {
      // Should be implemented by sub-prototype
      return false;
  };
  
  /**
   * Append all image urls present in the items DOM to the provided array
   * @param {String[]} imageUrls
   */
  links.Timeline.Item.prototype.getImageUrls = function (imageUrls) {
      if (this.dom) {
          links.imageloader.filterImageUrls(this.dom, imageUrls);
      }
  };
  
  /**
   * Select the item
   */
  links.Timeline.Item.prototype.select = function () {
      // Should be implemented by sub-prototype
  };
  
  /**
   * Unselect the item
   */
  links.Timeline.Item.prototype.unselect = function () {
      // Should be implemented by sub-prototype
  };
  
  /**
   * Creates the DOM for the item, depending on its type
   * @return {Element | undefined}
   */
  links.Timeline.Item.prototype.createDOM = function () {
      // Should be implemented by sub-prototype
  };
  
  /**
   * Append the items DOM to the given HTML container. If items DOM does not yet
   * exist, it will be created first.
   * @param {Element} container
   */
  links.Timeline.Item.prototype.showDOM = function (container) {
      // Should be implemented by sub-prototype
  };
  
  /**
   * Remove the items DOM from the current HTML container
   * @param {Element} container
   */
  links.Timeline.Item.prototype.hideDOM = function (container) {
      // Should be implemented by sub-prototype
  };
  
  /**
   * Update the DOM of the item. This will update the content and the classes
   * of the item
   */
  links.Timeline.Item.prototype.updateDOM = function () {
      // Should be implemented by sub-prototype
  };
  
  /**
   * Reposition the item, recalculate its left, top, and width, using the current
   * range of the timeline and the timeline options.
   * @param {links.Timeline} timeline
   */
  links.Timeline.Item.prototype.updatePosition = function (timeline) {
      // Should be implemented by sub-prototype
  };
  
  /**
   * Check if the item is drawn in the timeline (i.e. the DOM of the item is
   * attached to the frame. You may also just request the parameter item.rendered
   * @return {boolean} rendered
   */
  links.Timeline.Item.prototype.isRendered = function () {
      return this.rendered;
  };
  
  /**
   * Check if the item is located in the visible area of the timeline, and
   * not part of a cluster
   * @param {Date} start
   * @param {Date} end
   * @return {boolean} visible
   */
  links.Timeline.Item.prototype.isVisible = function (start, end) {
      // Should be implemented by sub-prototype
      return false;
  };
  
  /**
   * Reposition the item
   * @param {Number} left
   * @param {Number} right
   */
  links.Timeline.Item.prototype.setPosition = function (left, right) {
      // Should be implemented by sub-prototype
  };
  
  /**
   * Calculate the right position of the item
   * @param {links.Timeline} timeline
   * @return {Number} right
   */
  links.Timeline.Item.prototype.getRight = function (timeline) {
      // Should be implemented by sub-prototype
      return 0;
  };
  
  /**
   * Calculate the width of the item
   * @param {links.Timeline} timeline
   * @return {Number} width
   */
  links.Timeline.Item.prototype.getWidth = function (timeline) {
      // Should be implemented by sub-prototype
      return this.width || 0; // last rendered width
  };
  
  
  /**
   * @constructor links.Timeline.ItemBox
   * @extends links.Timeline.Item
   * @param {Object} data       Object containing parameters start, end
   *                            content, group, type, className, editable.
   * @param {Object} [options]  Options to set initial property values
   *                                {Number} top
   *                                {Number} left
   *                                {Number} width
   *                                {Number} height
   */
  links.Timeline.ItemBox = function (data, options) {
      links.Timeline.Item.call(this, data, options);
  };
  
  links.Timeline.ItemBox.prototype = new links.Timeline.Item();
  
  /**
   * Reflow the Item: retrieve its actual size from the DOM
   * @return {boolean} resized    returns true if the axis is resized
   * @override
   */
  links.Timeline.ItemBox.prototype.reflow = function () {
      var dom = this.dom,
          dotHeight = dom.dot.offsetHeight,
          dotWidth = dom.dot.offsetWidth,
          lineWidth = dom.line.offsetWidth,
          resized = (
              (this.dotHeight != dotHeight) ||
                  (this.dotWidth != dotWidth) ||
                  (this.lineWidth != lineWidth)
              );
  
      this.dotHeight = dotHeight;
      this.dotWidth = dotWidth;
      this.lineWidth = lineWidth;
  
      return resized;
  };
  
  /**
   * Select the item
   * @override
   */
  links.Timeline.ItemBox.prototype.select = function () {
      var dom = this.dom;
      links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
      links.Timeline.addClassName(dom.line, 'timeline-event-selected ui-state-active');
      links.Timeline.addClassName(dom.dot, 'timeline-event-selected ui-state-active');
  };
  
  /**
   * Unselect the item
   * @override
   */
  links.Timeline.ItemBox.prototype.unselect = function () {
      var dom = this.dom;
      links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
      links.Timeline.removeClassName(dom.line, 'timeline-event-selected ui-state-active');
      links.Timeline.removeClassName(dom.dot, 'timeline-event-selected ui-state-active');
  };
  
  /**
   * Creates the DOM for the item, depending on its type
   * @return {Element | undefined}
   * @override
   */
  links.Timeline.ItemBox.prototype.createDOM = function () {
      // background box
      var divBox = document.createElement("DIV");
      divBox.style.position = "absolute";
      divBox.style.left = this.left + "px";
      divBox.style.top = this.top + "px";
  
      // contents box (inside the background box). used for making margins
      var divContent = document.createElement("DIV");
      divContent.className = "timeline-event-content";
      divContent.innerHTML = this.content;
      divBox.appendChild(divContent);
  
      // line to axis
      var divLine = document.createElement("DIV");
      divLine.style.position = "absolute";
      divLine.style.width = "0px";
      // important: the vertical line is added at the front of the list of elements,
      // so it will be drawn behind all boxes and ranges
      divBox.line = divLine;
  
      // dot on axis
      var divDot = document.createElement("DIV");
      divDot.style.position = "absolute";
      divDot.style.width  = "0px";
      divDot.style.height = "0px";
      divBox.dot = divDot;
  
      this.dom = divBox;
      this.updateDOM();
  
      return divBox;
  };
  
  /**
   * Append the items DOM to the given HTML container. If items DOM does not yet
   * exist, it will be created first.
   * @param {Element} container
   * @override
   */
  links.Timeline.ItemBox.prototype.showDOM = function (container) {
      var dom = this.dom;
      if (!dom) {
          dom = this.createDOM();
      }
  
      if (dom.parentNode != container) {
          if (dom.parentNode) {
              // container is changed. remove from old container
              this.hideDOM();
          }
  
          // append to this container
          container.appendChild(dom);
          container.insertBefore(dom.line, container.firstChild);
          // Note: line must be added in front of the this,
          //       such that it stays below all this
          container.appendChild(dom.dot);
          this.rendered = true;
      }
  };
  
  /**
   * Remove the items DOM from the current HTML container, but keep the DOM in
   * memory
   * @override
   */
  links.Timeline.ItemBox.prototype.hideDOM = function () {
      var dom = this.dom;
      if (dom) {
          if (dom.parentNode) {
              dom.parentNode.removeChild(dom);
          }
          if (dom.line && dom.line.parentNode) {
              dom.line.parentNode.removeChild(dom.line);
          }
          if (dom.dot && dom.dot.parentNode) {
              dom.dot.parentNode.removeChild(dom.dot);
          }
          this.rendered = false;
      }
  };
  
  /**
   * Update the DOM of the item. This will update the content and the classes
   * of the item
   * @override
   */
  links.Timeline.ItemBox.prototype.updateDOM = function () {
      var divBox = this.dom;
      if (divBox) {
          var divLine = divBox.line;
          var divDot = divBox.dot;
  
          // update contents
          divBox.firstChild.innerHTML = this.content;
  
          // update class
          divBox.className = "timeline-event timeline-event-box ui-widget ui-state-default";
          divLine.className = "timeline-event timeline-event-line ui-widget ui-state-default";
          divDot.className  = "timeline-event timeline-event-dot ui-widget ui-state-default";
  
          if (this.isCluster) {
              links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
              links.Timeline.addClassName(divLine, 'timeline-event-cluster ui-widget-header');
              links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header');
          }
  
          // add item specific class name when provided
          if (this.className) {
              links.Timeline.addClassName(divBox, this.className);
              links.Timeline.addClassName(divLine, this.className);
              links.Timeline.addClassName(divDot, this.className);
          }
  
          // TODO: apply selected className?
      }
  };
  
  /**
   * Reposition the item, recalculate its left, top, and width, using the current
   * range of the timeline and the timeline options.
   * @param {links.Timeline} timeline
   * @override
   */
  links.Timeline.ItemBox.prototype.updatePosition = function (timeline) {
      var dom = this.dom;
      if (dom) {
          var left = timeline.timeToScreen(this.start),
              axisOnTop = timeline.options.axisOnTop,
              axisTop = timeline.size.axis.top,
              axisHeight = timeline.size.axis.height,
              boxAlign = (timeline.options.box && timeline.options.box.align) ?
                  timeline.options.box.align : undefined;
  
          dom.style.top = this.top + "px";
          if (boxAlign == 'right') {
              dom.style.left = (left - this.width) + "px";
          }
          else if (boxAlign == 'left') {
              dom.style.left = (left) + "px";
          }
          else { // default or 'center'
              dom.style.left = (left - this.width/2) + "px";
          }
  
          var line = dom.line;
          var dot = dom.dot;
          line.style.left = (left - this.lineWidth/2) + "px";
          dot.style.left = (left - this.dotWidth/2) + "px";
          if (axisOnTop) {
              line.style.top = axisHeight + "px";
              line.style.height = Math.max(this.top - axisHeight, 0) + "px";
              dot.style.top = (axisHeight - this.dotHeight/2) + "px";
          }
          else {
              line.style.top = (this.top + this.height) + "px";
              line.style.height = Math.max(axisTop - this.top - this.height, 0) + "px";
              dot.style.top = (axisTop - this.dotHeight/2) + "px";
          }
      }
  };
  
  /**
   * Check if the item is visible in the timeline, and not part of a cluster
   * @param {Date} start
   * @param {Date} end
   * @return {Boolean} visible
   * @override
   */
  links.Timeline.ItemBox.prototype.isVisible = function (start, end) {
      if (this.cluster) {
          return false;
      }
  
      return (this.start > start) && (this.start < end);
  };
  
  /**
   * Reposition the item
   * @param {Number} left
   * @param {Number} right
   * @override
   */
  links.Timeline.ItemBox.prototype.setPosition = function (left, right) {
      var dom = this.dom;
  
      dom.style.left = (left - this.width / 2) + "px";
      dom.line.style.left = (left - this.lineWidth / 2) + "px";
      dom.dot.style.left = (left - this.dotWidth / 2) + "px";
  
      if (this.group) {
          this.top = this.group.top;
          dom.style.top = this.top + 'px';
      }
  };
  
  /**
   * Calculate the right position of the item
   * @param {links.Timeline} timeline
   * @return {Number} right
   * @override
   */
  links.Timeline.ItemBox.prototype.getRight = function (timeline) {
      var boxAlign = (timeline.options.box && timeline.options.box.align) ?
          timeline.options.box.align : undefined;
  
      var left = timeline.timeToScreen(this.start);
      var right;
      if (boxAlign == 'right') {
          right = left;
      }
      else if (boxAlign == 'left') {
          right = (left + this.width);
      }
      else { // default or 'center'
          right = (left + this.width / 2);
      }
  
      return right;
  };
  
  /**
   * @constructor links.Timeline.ItemRange
   * @extends links.Timeline.Item
   * @param {Object} data       Object containing parameters start, end
   *                            content, group, type, className, editable.
   * @param {Object} [options]  Options to set initial property values
   *                                {Number} top
   *                                {Number} left
   *                                {Number} width
   *                                {Number} height
   */
  links.Timeline.ItemRange = function (data, options) {
      links.Timeline.Item.call(this, data, options);
  };
  
  links.Timeline.ItemRange.prototype = new links.Timeline.Item();
  
  /**
   * Select the item
   * @override
   */
  links.Timeline.ItemRange.prototype.select = function () {
      var dom = this.dom;
      links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
  };
  
  /**
   * Unselect the item
   * @override
   */
  links.Timeline.ItemRange.prototype.unselect = function () {
      var dom = this.dom;
      links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
  };
  
  /**
   * Creates the DOM for the item, depending on its type
   * @return {Element | undefined}
   * @override
   */
  links.Timeline.ItemRange.prototype.createDOM = function () {
      // background box
      var divBox = document.createElement("DIV");
      divBox.style.position = "absolute";
  
      // contents box
      var divContent = document.createElement("DIV");
      divContent.className = "timeline-event-content";
      divBox.appendChild(divContent);
  
      this.dom = divBox;
      this.updateDOM();
  
      return divBox;
  };
  
  /**
   * Append the items DOM to the given HTML container. If items DOM does not yet
   * exist, it will be created first.
   * @param {Element} container
   * @override
   */
  links.Timeline.ItemRange.prototype.showDOM = function (container) {
      var dom = this.dom;
      if (!dom) {
          dom = this.createDOM();
      }
  
      if (dom.parentNode != container) {
          if (dom.parentNode) {
              // container changed. remove the item from the old container
              this.hideDOM();
          }
  
          // append to the new container
          container.appendChild(dom);
          this.rendered = true;
      }
  };
  
  /**
   * Remove the items DOM from the current HTML container
   * The DOM will be kept in memory
   * @override
   */
  links.Timeline.ItemRange.prototype.hideDOM = function () {
      var dom = this.dom;
      if (dom) {
          if (dom.parentNode) {
              dom.parentNode.removeChild(dom);
          }
          this.rendered = false;
      }
  };
  
  /**
   * Update the DOM of the item. This will update the content and the classes
   * of the item
   * @override
   */
  links.Timeline.ItemRange.prototype.updateDOM = function () {
      var divBox = this.dom;
      if (divBox) {
          // update contents
          divBox.firstChild.innerHTML = this.content;
  
          // update class
          divBox.className = "timeline-event timeline-event-range ui-widget ui-state-default";
  
          if (this.isCluster) {
              links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
          }
  
          // add item specific class name when provided
          if (this.className) {
              links.Timeline.addClassName(divBox, this.className);
          }
  
          // TODO: apply selected className?
      }
  };
  
  /**
   * Reposition the item, recalculate its left, top, and width, using the current
   * range of the timeline and the timeline options. *
   * @param {links.Timeline} timeline
   * @override
   */
  links.Timeline.ItemRange.prototype.updatePosition = function (timeline) {
      var dom = this.dom;
      if (dom) {
          var contentWidth = timeline.size.contentWidth,
              left = timeline.timeToScreen(this.start),
              right = timeline.timeToScreen(this.end);
  
          // limit the width of the this, as browsers cannot draw very wide divs
          if (left < -contentWidth) {
              left = -contentWidth;
          }
          if (right > 2 * contentWidth) {
              right = 2 * contentWidth;
          }
  
          dom.style.top = this.top + "px";
          dom.style.left = left + "px";
          //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth
          dom.style.width = Math.max(right - left, 1) + "px";
      }
  };
  
  /**
   * Check if the item is visible in the timeline, and not part of a cluster
   * @param {Number} start
   * @param {Number} end
   * @return {boolean} visible
   * @override
   */
  links.Timeline.ItemRange.prototype.isVisible = function (start, end) {
      if (this.cluster) {
          return false;
      }
  
      return (this.end > start)
          && (this.start < end);
  };
  
  /**
   * Reposition the item
   * @param {Number} left
   * @param {Number} right
   * @override
   */
  links.Timeline.ItemRange.prototype.setPosition = function (left, right) {
      var dom = this.dom;
  
      dom.style.left = left + 'px';
      dom.style.width = (right - left) + 'px';
  
      if (this.group) {
          this.top = this.group.top;
          dom.style.top = this.top + 'px';
      }
  };
  
  /**
   * Calculate the right position of the item
   * @param {links.Timeline} timeline
   * @return {Number} right
   * @override
   */
  links.Timeline.ItemRange.prototype.getRight = function (timeline) {
      return timeline.timeToScreen(this.end);
  };
  
  /**
   * Calculate the width of the item
   * @param {links.Timeline} timeline
   * @return {Number} width
   * @override
   */
  links.Timeline.ItemRange.prototype.getWidth = function (timeline) {
      return timeline.timeToScreen(this.end) - timeline.timeToScreen(this.start);
  };
  
  /**
   * @constructor links.Timeline.ItemDot
   * @extends links.Timeline.Item
   * @param {Object} data       Object containing parameters start, end
   *                            content, group, type, className, editable.
   * @param {Object} [options]  Options to set initial property values
   *                                {Number} top
   *                                {Number} left
   *                                {Number} width
   *                                {Number} height
   */
  links.Timeline.ItemDot = function (data, options) {
      links.Timeline.Item.call(this, data, options);
  };
  
  links.Timeline.ItemDot.prototype = new links.Timeline.Item();
  
  /**
   * Reflow the Item: retrieve its actual size from the DOM
   * @return {boolean} resized    returns true if the axis is resized
   * @override
   */
  links.Timeline.ItemDot.prototype.reflow = function () {
      var dom = this.dom,
          dotHeight = dom.dot.offsetHeight,
          dotWidth = dom.dot.offsetWidth,
          contentHeight = dom.content.offsetHeight,
          resized = (
              (this.dotHeight != dotHeight) ||
                  (this.dotWidth != dotWidth) ||
                  (this.contentHeight != contentHeight)
              );
  
      this.dotHeight = dotHeight;
      this.dotWidth = dotWidth;
      this.contentHeight = contentHeight;
  
      return resized;
  };
  
  /**
   * Select the item
   * @override
   */
  links.Timeline.ItemDot.prototype.select = function () {
      var dom = this.dom;
      links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
  };
  
  /**
   * Unselect the item
   * @override
   */
  links.Timeline.ItemDot.prototype.unselect = function () {
      var dom = this.dom;
      links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
  };
  
  /**
   * Creates the DOM for the item, depending on its type
   * @return {Element | undefined}
   * @override
   */
  links.Timeline.ItemDot.prototype.createDOM = function () {
      // background box
      var divBox = document.createElement("DIV");
      divBox.style.position = "absolute";
  
      // contents box, right from the dot
      var divContent = document.createElement("DIV");
      divContent.className = "timeline-event-content";
      divBox.appendChild(divContent);
  
      // dot at start
      var divDot = document.createElement("DIV");
      divDot.style.position = "absolute";
      divDot.style.width = "0px";
      divDot.style.height = "0px";
      divBox.appendChild(divDot);
  
      divBox.content = divContent;
      divBox.dot = divDot;
  
      this.dom = divBox;
      this.updateDOM();
  
      return divBox;
  };
  
  /**
   * Append the items DOM to the given HTML container. If items DOM does not yet
   * exist, it will be created first.
   * @param {Element} container
   * @override
   */
  links.Timeline.ItemDot.prototype.showDOM = function (container) {
      var dom = this.dom;
      if (!dom) {
          dom = this.createDOM();
      }
  
      if (dom.parentNode != container) {
          if (dom.parentNode) {
              // container changed. remove it from old container first
              this.hideDOM();
          }
  
          // append to container
          container.appendChild(dom);
          this.rendered = true;
      }
  };
  
  /**
   * Remove the items DOM from the current HTML container
   * @override
   */
  links.Timeline.ItemDot.prototype.hideDOM = function () {
      var dom = this.dom;
      if (dom) {
          if (dom.parentNode) {
              dom.parentNode.removeChild(dom);
          }
          this.rendered = false;
      }
  };
  
  /**
   * Update the DOM of the item. This will update the content and the classes
   * of the item
   * @override
   */
  links.Timeline.ItemDot.prototype.updateDOM = function () {
      if (this.dom) {
          var divBox = this.dom;
          var divDot = divBox.dot;
  
          // update contents
          divBox.firstChild.innerHTML = this.content;
  
          // update classes
          divBox.className = "timeline-event-dot-container";
          divDot.className  = "timeline-event timeline-event-dot ui-widget ui-state-default";
  
          if (this.isCluster) {
              links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
              links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header');
          }
  
          // add item specific class name when provided
          if (this.className) {
              links.Timeline.addClassName(divBox, this.className);
              links.Timeline.addClassName(divDot, this.className);
          }
  
          // TODO: apply selected className?
      }
  };
  
  /**
   * Reposition the item, recalculate its left, top, and width, using the current
   * range of the timeline and the timeline options. *
   * @param {links.Timeline} timeline
   * @override
   */
  links.Timeline.ItemDot.prototype.updatePosition = function (timeline) {
      var dom = this.dom;
      if (dom) {
          var left = timeline.timeToScreen(this.start);
  
          dom.style.top = this.top + "px";
          dom.style.left = (left - this.dotWidth / 2) + "px";
  
          dom.content.style.marginLeft = (1.5 * this.dotWidth) + "px";
          //dom.content.style.marginRight = (0.5 * this.dotWidth) + "px"; // TODO
          dom.dot.style.top = ((this.height - this.dotHeight) / 2) + "px";
      }
  };
  
  /**
   * Check if the item is visible in the timeline, and not part of a cluster.
   * @param {Date} start
   * @param {Date} end
   * @return {boolean} visible
   * @override
   */
  links.Timeline.ItemDot.prototype.isVisible = function (start, end) {
      if (this.cluster) {
          return false;
      }
  
      return (this.start > start)
          && (this.start < end);
  };
  
  /**
   * Reposition the item
   * @param {Number} left
   * @param {Number} right
   * @override
   */
  links.Timeline.ItemDot.prototype.setPosition = function (left, right) {
      var dom = this.dom;
  
      dom.style.left = (left - this.dotWidth / 2) + "px";
  
      if (this.group) {
          this.top = this.group.top;
          dom.style.top = this.top + 'px';
      }
  };
  
  /**
   * Calculate the right position of the item
   * @param {links.Timeline} timeline
   * @return {Number} right
   * @override
   */
  links.Timeline.ItemDot.prototype.getRight = function (timeline) {
      return timeline.timeToScreen(this.start) + this.width;
  };
  
  /**
   * Retrieve the properties of an item.
   * @param {Number} index
   * @return {Object} properties  Object containing item properties:<br>
   *                              {Date} start (required),
   *                              {Date} end (optional),
   *                              {String} content (required),
   *                              {String} group (optional),
   *                              {String} className (optional)
   *                              {boolean} editable (optional)
   *                              {String} type (optional)
   */
  links.Timeline.prototype.getItem = function (index) {
      if (index >= this.items.length) {
          throw "Cannot get item, index out of range";
      }
  
      var item = this.items[index];
  
      var properties = {};
      properties.start = new Date(item.start.valueOf());
      if (item.end) {
          properties.end = new Date(item.end.valueOf());
      }
      properties.content = item.content;
      if (item.group) {
          properties.group = this.getGroupName(item.group);
      }
      if (item.className) {
          properties.className = item.className;
      }
      if (typeof item.editable !== 'undefined') {
          properties.editable = item.editable;
      }
      if (item.type) {
          properties.type = item.type;
      }
  
      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)
   *                              {String} className (optional)
   *                              {Boolean} editable (optional)
   *                              {String} type (optional)
   * @param {boolean} [preventRender=false]   Do not re-render timeline if true
   */
  links.Timeline.prototype.addItem = function (itemData, preventRender) {
      var itemsData = [
          itemData
      ];
  
      this.addItems(itemsData, preventRender);
  };
  
  /**
   * Add new items.
   * @param {Array} itemsData An array containing Objects.
   *                          The objects must have the following parameters:
   *                            {Date} start,
   *                            {Date} end,
   *                            {String} content with text or HTML code,
   *                            {String} group (optional)
   *                            {String} className (optional)
   *                            {String} editable (optional)
   *                            {String} type (optional)
   * @param {boolean} [preventRender=false]   Do not re-render timeline if true
   */
  links.Timeline.prototype.addItems = function (itemsData, preventRender) {
      var timeline = this,
          items = this.items;
  
      // append the items
      itemsData.forEach(function (itemData) {
          var index = items.length;
          items.push(timeline.createItem(itemData));
          timeline.updateData(index, itemData);
  
          // note: there is no need to add the item to the renderQueue, that
          // will be done when this.render() is executed and all items are
          // filtered again.
      });
  
      // prepare data for clustering, by filtering and sorting by type
      if (this.options.cluster) {
          this.clusterGenerator.updateData();
      }
  
      if (!preventRender) {
          this.render({
              animate: false
          });
      }
  };
  
  /**
   * Create an item object, containing all needed parameters
   * @param {Object} itemData  Object containing parameters start, end
   *                           content, group.
   * @return {Object} item
   */
  links.Timeline.prototype.createItem = function(itemData) {
      var type = itemData.type || (itemData.end ? 'range' : this.options.style);
      var data = {
          start: itemData.start,
          end: itemData.end,
          content: itemData.content,
          className: itemData.className,
          editable: itemData.editable,
          order: itemData.order,
          group: this.getGroup(itemData.group, itemData.order),
          type: type
      };
      // TODO: optimize this, when creating an item, all data is copied twice...
  
      // TODO: is initialTop needed?
      var initialTop,
          options = this.options;
      if (options.axisOnTop) {
          initialTop = this.size.axis.height + options.eventMarginAxis + options.eventMargin / 2;
      }
      else {
          initialTop = this.size.contentHeight - options.eventMarginAxis - options.eventMargin / 2;
      }
  
      if (type in this.itemTypes) {
          return new this.itemTypes[type](data, {'top': initialTop})
      }
  
      console.log('ERROR: Unknown event style "' + type + '"');
      return new links.Timeline.Item(data, {
          'top': initialTop
      });
  };
  
  /**
   * 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)
   * @param {boolean} [preventRender=false]   Do not re-render timeline if true
   */
  links.Timeline.prototype.changeItem = function (index, itemData, preventRender) {
      var oldItem = this.items[index];
      if (!oldItem) {
          throw "Cannot change item, index out of range";
      }
  
      // replace item, merge the changes
      var newItem = this.createItem({
          'start':     itemData.hasOwnProperty('start') ?     itemData.start :     oldItem.start,
          'end':       itemData.hasOwnProperty('end') ?       itemData.end :       oldItem.end,
          'content':   itemData.hasOwnProperty('content') ?   itemData.content :   oldItem.content,
          'group':     itemData.hasOwnProperty('group') ?     itemData.group :     this.getGroupName(oldItem.group),
          'className': itemData.hasOwnProperty('className') ? itemData.className : oldItem.className,
          'editable':  itemData.hasOwnProperty('editable') ?  itemData.editable :  oldItem.editable,
          'type':      itemData.hasOwnProperty('type') ?      itemData.type :      oldItem.type,
          'order':     itemData.hasOwnProperty('order') ?     itemData.order :     oldItem.order
      });
      this.items[index] = newItem;
  
      // append the changes to the render queue
      this.renderQueue.hide.push(oldItem);
      this.renderQueue.show.push(newItem);
  
      // update the original data table
      this.updateData(index, itemData);
  
      // prepare data for clustering, by filtering and sorting by type
      if (this.options.cluster) {
          this.clusterGenerator.updateData();
      }
  
      if (!preventRender) {
          // redraw timeline
          this.render({
              animate: false
          });
  
          if (this.selection && this.selection.index == index) {
              newItem.select();
          }
      }
  };
  
  /**
   * Delete all groups
   */
  links.Timeline.prototype.deleteGroups = function () {
      this.groups = [];
      this.groupIndexes = {};
  };
  
  
  /**
   * Get a group by the group name. When the group does not exist,
   * it will be created.
   * @param {String} groupName   the name of the group
   * @return {Object} groupObject
   */
  links.Timeline.prototype.getGroup = function (groupName, groupOrder) {
      var groups = this.groups,
          groupIndexes = this.groupIndexes,
          groupObj = undefined;
  
      var groupIndex = groupIndexes[groupOrder];
      if (groupIndex == undefined && groupName != undefined) { // not null or undefined
          groupObj = {
              'content': groupName,
              'order': groupOrder ? groupOrder : groupName,
              'labelTop': 0,
              'lineTop': 0
              // note: this object will lateron get addition information, 
              //       such as height and width of the group         
          };
          groups.push(groupObj);
          // sort the groups
  
          if (this.options.groupsOrder == true) {
            groups = groups.sort(function (a, b) {
                if (a.order > b.order) {
                    return -1;
                }
                if (a.order < b.order) {
                    return 1;
                }
                return 0;
            });
          } else if (typeof(this.options.groupsOrder) == "function") {
            groups = groups.sort(this.options.groupsOrder)
          }
  
          // rebuilt the groupIndexes
          for (var i = 0, iMax = groups.length; i < iMax; i++) {
              groupIndexes[groups[i].order] = i;
          }
      }
      else {
          groupObj = groups[groupIndex];
      }
  
      return groupObj;
  };
  
  /**
   * Get the group name from a group object.
   * @param {Object} groupObj
   * @return {String} groupName   the name of the group, or undefined when group
   *                              was not provided
   */
  links.Timeline.prototype.getGroupName = function (groupObj) {
      return groupObj ? groupObj.content : undefined;
  };
  
  /**
   * Cancel a change item
   * This method can be called insed an event listener which catches the "change"
   * event. The changed event position will be undone.
   */
  links.Timeline.prototype.cancelChange = function () {
      this.applyChange = false;
  };
  
  /**
   * Cancel deletion of an item
   * This method can be called insed an event listener which catches the "delete"
   * event. Deletion of the event will be undone.
   */
  links.Timeline.prototype.cancelDelete = function () {
      this.applyDelete = false;
  };
  
  
  /**
   * Cancel creation of a new item
   * This method can be called insed an event listener which catches the "new"
   * event. Creation of the new the event will be undone.
   */
  links.Timeline.prototype.cancelAdd = function () {
      this.applyAdd = false;
  };
  
  
  /**
   * Select an event. The visible chart range will be moved such that the selected
   * event is placed in the middle.
   * For example selection = [{row: 5}];
   * @param {Array} selection   An array with a column row, containing the row
   *                           number (the id) of the event to be selected.
   * @return {boolean}         true if selection is succesfully set, else false.
   */
  links.Timeline.prototype.setSelection = function(selection) {
      if (selection != undefined && selection.length > 0) {
          if (selection[0].row != undefined) {
              var index = selection[0].row;
              if (this.items[index]) {
                  var item = this.items[index];
                  this.selectItem(index);
  
                  // move the visible chart range to the selected event.
                  var start = item.start;
                  var end = item.end;
                  var middle; // number
                  if (end != undefined) {
                      middle = (end.valueOf() + start.valueOf()) / 2;
                  } else {
                      middle = start.valueOf();
                  }
                  var diff = (this.end.valueOf() - this.start.valueOf()),
                      newStart = new Date(middle - diff/2),
                      newEnd = new Date(middle + diff/2);
  
                  this.setVisibleChartRange(newStart, newEnd);
  
                  return true;
              }
          }
      }
      else {
          // unselect current selection
          this.unselectItem();
      }
      return false;
  };
  
  /**
   * Retrieve the currently selected event
   * @return {Array} sel  An array with a column row, containing the row number
   *                      of the selected event. If there is no selection, an
   *                      empty array is returned.
   */
  links.Timeline.prototype.getSelection = function() {
      var sel = [];
      if (this.selection) {
          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
          };
  
          if (item && item.dom) {
              // TODO: move adjusting the domItem to the item itself
              if (this.isEditable(item)) {
                  item.dom.style.cursor = 'move';
              }
              item.select();
          }
          this.repaintDeleteButton();
          this.repaintDragAreas();
      }
  };
  
  /**
   * 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 = '';
              item.unselect();
          }
  
          this.selection = undefined;
          this.repaintDeleteButton();
          this.repaintDragAreas();
      }
  };
  
  
  /**
   * Stack the items such that they don't overlap. The items will have a minimal
   * distance equal to options.eventMargin.
   * @param {boolean | undefined} animate    if animate is true, the items are
   *                                         moved to their new position animated
   *                                         defaults to false.
   */
  links.Timeline.prototype.stackItems = function(animate) {
      if (this.groups.length > 0) {
          // under this conditions we refuse to stack the events
          // TODO: implement support for stacking items per group
          return;
      }
  
      if (animate == undefined) {
          animate = false;
      }
  
      // calculate the order and final stack position of the items
      var stack = this.stack;
      if (!stack) {
          stack = {};
          this.stack = stack;
      }
      stack.sortedItems = this.stackOrder(this.renderedItems);
      stack.finalItems = this.stackCalculateFinal(stack.sortedItems);
  
      if (animate || stack.timer) {
          // move animated to the final positions
          var timeline = this;
          var step = function () {
              var arrived = timeline.stackMoveOneStep(stack.sortedItems,
                  stack.finalItems);
  
              timeline.repaint();
  
              if (!arrived) {
                  stack.timer = setTimeout(step, 30);
              }
              else {
                  delete stack.timer;
              }
          };
  
          if (!stack.timer) {
              stack.timer = setTimeout(step, 30);
          }
      }
      else {
          // move immediately to the final positions
          this.stackMoveToFinal(stack.sortedItems, stack.finalItems);
      }
  };
  
  /**
   * Cancel any running animation
   */
  links.Timeline.prototype.stackCancelAnimation = function() {
      if (this.stack && this.stack.timer) {
          clearTimeout(this.stack.timer);
          delete this.stack.timer;
      }
  };
  
  
  /**
   * Order the items in the array this.items. The default order is determined via:
   * - Ranges go before boxes and dots.
   * - The item with the oldest start time goes first
   * If a custom function has been provided via the stackorder option, then this will be used.
   * @param {Array} items        Array with items
   * @return {Array} sortedItems Array with sorted items
   */
  links.Timeline.prototype.stackOrder = function(items) {
      // TODO: store the sorted items, to have less work later on
      var sortedItems = items.concat([]);
  
      //if a customer stack order function exists, use it. 
      var f = this.options.customStackOrder && (typeof this.options.customStackOrder === 'function') ? this.options.customStackOrder : function (a, b)
      {
          if ((a instanceof links.Timeline.ItemRange) &&
              !(b instanceof links.Timeline.ItemRange)) {
              return -1;
          }
  
          if (!(a instanceof links.Timeline.ItemRange) &&
              (b instanceof links.Timeline.ItemRange)) {
              return 1;
          }
  
          return (a.left - b.left);
      };
  
      sortedItems.sort(f);
  
      return sortedItems;
  };
  
  /**
   * Adjust vertical positions of the events such that they don't overlap each
   * other.
   * @param {timeline.Item[]} items
   * @return {Object[]} finalItems
   */
  links.Timeline.prototype.stackCalculateFinal = function(items) {
      var i,
          iMax,
          size = this.size,
          axisTop = size.axis.top,
          axisHeight = size.axis.height,
          options = this.options,
          axisOnTop = options.axisOnTop,
          eventMargin = options.eventMargin,
          eventMarginAxis = options.eventMarginAxis,
          finalItems = [];
  
      // initialize final positions
      for (i = 0, iMax = items.length; i < iMax; i++) {
          var item = items[i],
              top,
              bottom,
              height = item.height,
              width = item.getWidth(this),
              right = item.getRight(this),
              left = right - width;
  
          if (axisOnTop) {
              top = axisHeight + eventMarginAxis + eventMargin / 2;
          }
          else {
              top = axisTop - height - eventMarginAxis - eventMargin / 2;
          }
          bottom = top + height;
  
          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 (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.stackItemsCheckOverlap(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 = finalItems.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 = finalItems.length; i < iMax; i++) {
          var finalItem = finalItems[i],
              current = finalItem.item;
  
          current.left = finalItem.left;
          current.top = finalItem.top;
          current.right = finalItem.right;
          current.bottom = finalItem.bottom;
      }
  };
  
  
  
  /**
   * Check if the destiny position of given item overlaps with any
   * of the other items from index itemStart to itemEnd.
   * @param {Array} items      Array with items
   * @param {int}  itemIndex   Number of the item to be checked for overlap
   * @param {int}  itemStart   First item to be checked.
   * @param {int}  itemEnd     Last item to be checked.
   * @return {Object}          colliding item, or undefined when no collisions
   */
  links.Timeline.prototype.stackItemsCheckOverlap = function(items, itemIndex,
                                                             itemStart, itemEnd) {
      var eventMargin = this.options.eventMargin,
          collision = this.collision;
  
      // we loop from end to start, as we suppose that the chance of a 
      // collision is larger for items at the end, so check these first.
      var item1 = items[itemIndex];
      for (var i = itemEnd; i >= itemStart; i--) {
          var item2 = items[i];
          if (collision(item1, item2, eventMargin)) {
              if (i != itemIndex) {
                  return item2;
              }
          }
      }
  
      return undefined;
  };
  
  /**
   * Test if the two provided items collide
   * The items must have parameters left, right, top, and bottom.
   * @param {Element} item1       The first item
   * @param {Element} item2       The second item
   * @param {Number}              margin  A minimum required margin. Optional.
   *                              If margin is provided, the two items will be
   *                              marked colliding when they overlap or
   *                              when the margin between the two is smaller than
   *                              the requested margin.
   * @return {boolean}            true if item1 and item2 collide, else false
   */
  links.Timeline.prototype.collision = function(item1, item2, margin) {
      // set margin if not specified 
      if (margin == undefined) {
          margin = 0;
      }
  
      // calculate if there is overlap (collision)
      return (item1.left - margin < item2.right &&
          item1.right + margin > item2.left &&
          item1.top - margin < item2.bottom &&
          item1.bottom + margin > item2.top);
  };
  
  
  /**
   * fire an event
   * @param {String} event   The name of an event, for example "rangechange" or "edit"
   */
  links.Timeline.prototype.trigger = function (event) {
      // built up properties
      var properties = null;
      switch (event) {
          case 'rangechange':
          case 'rangechanged':
              properties = {
                  'start': new Date(this.start.valueOf()),
                  'end': new Date(this.end.valueOf())
              };
              break;
  
          case 'timechange':
          case 'timechanged':
              properties = {
                  'time': new Date(this.customTime.valueOf())
              };
              break;
      }
  
      // trigger the links event bus
      links.events.trigger(this, event, properties);
  
      // trigger the google event bus
      if (google && google.visualization) {
          google.visualization.events.trigger(this, event, properties);
      }
  };
  
  
  /**
   * Cluster the events
   */
  links.Timeline.prototype.clusterItems = function () {
      if (!this.options.cluster) {
          return;
      }
  
      var clusters = this.clusterGenerator.getClusters(this.conversion.factor);
      if (this.clusters != clusters) {
          // cluster level changed
          var queue = this.renderQueue;
  
          // remove the old clusters from the scene
          if (this.clusters) {
              this.clusters.forEach(function (cluster) {
                  queue.hide.push(cluster);
  
                  // unlink the items
                  cluster.items.forEach(function (item) {
                      item.cluster = undefined;
                  });
              });
          }
  
          // append the new clusters
          clusters.forEach(function (cluster) {
              // don't add to the queue.show here, will be done in .filterItems()
  
              // link all items to the cluster
              cluster.items.forEach(function (item) {
                  item.cluster = cluster;
              });
          });
  
          this.clusters = clusters;
      }
  };
  
  /**
   * Filter the visible events
   */
  links.Timeline.prototype.filterItems = function () {
      var queue = this.renderQueue,
          window = (this.end - this.start),
          start = new Date(this.start.valueOf() - window),
          end = new Date(this.end.valueOf() + window);
  
      function filter (arr) {
          arr.forEach(function (item) {
              var rendered = item.rendered;
              var visible = item.isVisible(start, end);
              if (rendered != visible) {
                  if (rendered) {
                      queue.hide.push(item); // item is rendered but no longer visible
                  }
                  if (visible && (queue.show.indexOf(item) == -1)) {
                      queue.show.push(item); // item is visible but neither rendered nor queued up to be rendered
                  }
              }
          });
      }
  
      // filter all items and all clusters
      filter(this.items);
      if (this.clusters) {
          filter(this.clusters);
      }
  };
  
  /** ------------------------------------------------------------------------ **/
  
  /**
   * @constructor links.Timeline.ClusterGenerator
   * Generator which creates clusters of items, based on the visible range in
   * the Timeline. There is a set of cluster levels which is cached.
   * @param {links.Timeline} timeline
   */
  links.Timeline.ClusterGenerator = function (timeline) {
      this.timeline = timeline;
      this.clear();
  };
  
  /**
   * Clear all cached clusters and data, and initialize all variables
   */
  links.Timeline.ClusterGenerator.prototype.clear = function () {
      // cache containing created clusters for each cluster level
      this.items = [];
      this.groups = {};
      this.clearCache();
  };
  
  /**
   * Clear the cached clusters
   */
  links.Timeline.ClusterGenerator.prototype.clearCache = function () {
      // cache containing created clusters for each cluster level
      this.cache = {};
      this.cacheLevel = -1;
      this.cache[this.cacheLevel] = [];
  };
  
  /**
   * Set the items to be clustered.
   * This will clear cached clusters.
   * @param {Item[]} items
   * @param {Object} [options]  Available options:
   *                            {boolean} applyOnChangedLevel
   *                                If true (default), the changed data is applied
   *                                as soon the cluster level changes. If false,
   *                                The changed data is applied immediately
   */
  links.Timeline.ClusterGenerator.prototype.setData = function (items, options) {
      this.items = items || [];
      this.dataChanged = true;
      this.applyOnChangedLevel = true;
      if (options && options.applyOnChangedLevel) {
          this.applyOnChangedLevel = options.applyOnChangedLevel;
      }
      // console.log('clustergenerator setData applyOnChangedLevel=' + this.applyOnChangedLevel); // TODO: cleanup
  };
  
  /**
   * Update the current data set: clear cache, and recalculate the clustering for
   * the current level
   */
  links.Timeline.ClusterGenerator.prototype.updateData = function () {
      this.dataChanged = true;
      this.applyOnChangedLevel = false;
  };
  
  /**
   * Filter the items per group.
   * @private
   */
  links.Timeline.ClusterGenerator.prototype.filterData = function () {
      // filter per group
      var items = this.items || [];
      var groups = {};
      this.groups = groups;
  
      // split the items per group
      items.forEach(function (item) {
          // put the item in the correct group
          var groupName = item.group ? item.group.content : '';
          var group = groups[groupName];
          if (!group) {
              group = [];
              groups[groupName] = group;
          }
          group.push(item);
  
          // calculate the center of the item
          if (item.start) {
              if (item.end) {
                  // range
                  item.center = (item.start.valueOf() + item.end.valueOf()) / 2;
              }
              else {
                  // box, dot
                  item.center = item.start.valueOf();
              }
          }
      });
  
      // sort the items per group
      for (var groupName in groups) {
          if (groups.hasOwnProperty(groupName)) {
              groups[groupName].sort(function (a, b) {
                  return (a.center - b.center);
              });
          }
      }
  
      this.dataChanged = false;
  };
  
  /**
   * Cluster the events which are too close together
   * @param {Number} scale     The scale of the current window,
   *                           defined as (windowWidth / (endDate - startDate))
   * @return {Item[]} clusters
   */
  links.Timeline.ClusterGenerator.prototype.getClusters = function (scale) {
      var level = -1,
          granularity = 2, // TODO: what granularity is needed for the cluster levels?
          timeWindow = 0,  // milliseconds
          maxItems = 5;    // TODO: do not hard code maxItems
  
      if (scale > 0) {
          level = Math.round(Math.log(100 / scale) / Math.log(granularity));
          timeWindow = Math.pow(granularity, level);
  
          // groups must have a larger time window, as the items will not be stacked
          if (this.timeline.groups && this.timeline.groups.length) {
              timeWindow *= 4;
          }
      }
  
      // clear the cache when and re-filter the data when needed.
      if (this.dataChanged) {
          var levelChanged = (level != this.cacheLevel);
          var applyDataNow = this.applyOnChangedLevel ? levelChanged : true;
          if (applyDataNow) {
              // TODO: currently drawn clusters should be removed! mark them as invisible?
              this.clearCache();
              this.filterData();
              // console.log('clustergenerator: cache cleared...'); // TODO: cleanup
          }
      }
  
      this.cacheLevel = level;
      var clusters = this.cache[level];
      if (!clusters) {
          // console.log('clustergenerator: create cluster level ' + level); // TODO: cleanup
          clusters = [];
  
          // TODO: spit this method, it is too large
          for (var groupName in this.groups) {
              if (this.groups.hasOwnProperty(groupName)) {
                  var items = this.groups[groupName];
                  var iMax = items.length;
                  var i = 0;
                  while (i < iMax) {
                      // find all items around current item, within the timeWindow
                      var item = items[i];
                      var neighbors = 1;  // start at 1, to include itself)
  
                      // loop through items left from the current item
                      var j = i - 1;
                      while (j >= 0 && (item.center - items[j].center) < timeWindow / 2) {
                          if (!items[j].cluster) {
                              neighbors++;
                          }
                          j--;
                      }
  
                      // loop through items right from the current item
                      var k = i + 1;
                      while (k < items.length && (items[k].center - item.center) < timeWindow / 2) {
                          neighbors++;
                          k++;
                      }
  
                      // loop through the created clusters
                      var l = clusters.length - 1;
                      while (l >= 0 && (item.center - clusters[l].center) < timeWindow / 2) {
                          if (item.group == clusters[l].group) {
                              neighbors++;
                          }
                          l--;
                      }
  
                      // aggregate until the number of items is within maxItems
                      if (neighbors > maxItems) {
                          // too busy in this window.
                          var num = neighbors - maxItems + 1;
                          var clusterItems = [];
  
                          // append the items to the cluster,
                          // and calculate the average start for the cluster
                          var avg = undefined;  // number. average of all start dates
                          var min = undefined;  // number. minimum of all start dates
                          var max = undefined;  // number. maximum of all start and end dates
                          var containsRanges = false;
                          var count = 0;
                          var m = i;
                          while (clusterItems.length < num && m < items.length) {
                              var p = items[m];
                              var start = p.start.valueOf();
                              var end = p.end ? p.end.valueOf() : p.start.valueOf();
                              clusterItems.push(p);
                              if (count) {
                                  // calculate new average (use fractions to prevent overflow)
                                  avg = (count / (count + 1)) * avg + (1 / (count + 1)) * p.center;
                              }
                              else {
                                  avg = p.center;
                              }
                              min = (min != undefined) ? Math.min(min, start) : start;
                              max = (max != undefined) ? Math.max(max, end) : end;
                              containsRanges = containsRanges || (p instanceof links.Timeline.ItemRange);
                              count++;
                              m++;
                          }
  
                          var cluster;
                          var title = 'Cluster containing ' + count +
                              ' events. Zoom in to see the individual events.';
                          var content = '<div title="' + title + '">' + count + ' events</div>';
                          var group = item.group ? item.group.content : undefined;
                          if (containsRanges) {
                              // boxes and/or ranges
                              cluster = this.timeline.createItem({
                                  'start': new Date(min),
                                  'end': new Date(max),
                                  'content': content,
                                  'group': group
                              });
                          }
                          else {
                              // boxes only
                              cluster = this.timeline.createItem({
                                  'start': new Date(avg),
                                  'content': content,
                                  'group': group
                              });
                          }
                          cluster.isCluster = true;
                          cluster.items = clusterItems;
                          cluster.items.forEach(function (item) {
                              item.cluster = cluster;
                          });
  
                          clusters.push(cluster);
                          i += num;
                      }
                      else {
                          delete item.cluster;
                          i += 1;
                      }
                  }
              }
          }
  
          this.cache[level] = clusters;
      }
  
      return clusters;
  };
  
  
  /** ------------------------------------------------------------------------ **/
  
  
  /**
   * Event listener (singleton)
   */
  links.events = links.events || {
      'listeners': [],
  
      /**
       * Find a single listener by its object
       * @param {Object} object
       * @return {Number} index  -1 when not found
       */
      'indexOf': function (object) {
          var listeners = this.listeners;
          for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
              var listener = listeners[i];
              if (listener && listener.object == object) {
                  return i;
              }
          }
          return -1;
      },
  
      /**
       * Add an event listener
       * @param {Object} object
       * @param {String} event       The name of an event, for example 'select'
       * @param {function} callback  The callback method, called when the
       *                             event takes place
       */
      'addListener': function (object, event, callback) {
          var index = this.indexOf(object);
          var listener = this.listeners[index];
          if (!listener) {
              listener = {
                  'object': object,
                  'events': {}
              };
              this.listeners.push(listener);
          }
  
          var callbacks = listener.events[event];
          if (!callbacks) {
              callbacks = [];
              listener.events[event] = callbacks;
          }
  
          // add the callback if it does not yet exist
          if (callbacks.indexOf(callback) == -1) {
              callbacks.push(callback);
          }
      },
  
      /**
       * Remove an event listener
       * @param {Object} object
       * @param {String} event       The name of an event, for example 'select'
       * @param {function} callback  The registered callback method
       */
      'removeListener': function (object, event, callback) {
          var index = this.indexOf(object);
          var listener = this.listeners[index];
          if (listener) {
              var callbacks = listener.events[event];
              if (callbacks) {
                  var index = callbacks.indexOf(callback);
                  if (index != -1) {
                      callbacks.splice(index, 1);
                  }
  
                  // remove the array when empty
                  if (callbacks.length == 0) {
                      delete listener.events[event];
                  }
              }
  
              // count the number of registered events. remove listener when empty
              var count = 0;
              var events = listener.events;
              for (var e in events) {
                  if (events.hasOwnProperty(e)) {
                      count++;
                  }
              }
              if (count == 0) {
                  delete this.listeners[index];
              }
          }
      },
  
      /**
       * Remove all registered event listeners
       */
      'removeAllListeners': function () {
          this.listeners = [];
      },
  
      /**
       * Trigger an event. All registered event handlers will be called
       * @param {Object} object
       * @param {String} event
       * @param {Object} properties (optional)
       */
      'trigger': function (object, event, properties) {
          var index = this.indexOf(object);
          var listener = this.listeners[index];
          if (listener) {
              var callbacks = listener.events[event];
              if (callbacks) {
                  for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
                      callbacks[i](properties);
                  }
              }
          }
      }
  };
  
  
  /** ------------------------------------------------------------------------ **/
  
  /**
   * @constructor  links.Timeline.StepDate
   * The class StepDate is an iterator for dates. You provide a start date and an
   * end date. The class itself determines the best scale (step size) based on the
   * provided start Date, end Date, and minimumStep.
   *
   * If minimumStep is provided, the step size is chosen as close as possible
   * to the minimumStep but larger than minimumStep. If minimumStep is not
   * provided, the scale is set to 1 DAY.
   * The minimumStep should correspond with the onscreen size of about 6 characters
   *
   * Alternatively, you can set a scale by hand.
   * After creation, you can initialize the class by executing start(). Then you
   * can iterate from the start date to the end date via next(). You can check if
   * the end date is reached with the function end(). After each step, you can
   * retrieve the current date via get().
   * The class step has scales ranging from milliseconds, seconds, minutes, hours,
   * days, to years.
   *
   * Version: 1.2
   *
   * @param {Date} start          The start date, for example new Date(2010, 9, 21)
   *                              or new Date(2010, 9, 21, 23, 45, 00)
   * @param {Date} end            The end date
   * @param {Number}  minimumStep Optional. Minimum step size in milliseconds
   */
  links.Timeline.StepDate = function(start, end, minimumStep) {
  
      // variables
      this.current = new Date();
      this._start = new Date();
      this._end = new Date();
  
      this.autoScale  = true;
      this.scale = links.Timeline.StepDate.SCALE.DAY;
      this.step = 1;
  
      // initialize the range
      this.setRange(start, end, minimumStep);
  };
  
  /// enum scale
  links.Timeline.StepDate.SCALE = {
      MILLISECOND: 1,
      SECOND: 2,
      MINUTE: 3,
      HOUR: 4,
      DAY: 5,
      WEEKDAY: 6,
      MONTH: 7,
      YEAR: 8
  };
  
  
  /**
   * Set a new range
   * If minimumStep is provided, the step size is chosen as close as possible
   * to the minimumStep but larger than minimumStep. If minimumStep is not
   * provided, the scale is set to 1 DAY.
   * The minimumStep should correspond with the onscreen size of about 6 characters
   * @param {Date} start        The start date and time.
   * @param {Date} end          The end date and time.
   * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
   */
  links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) {
      if (!(start instanceof Date) || !(end instanceof Date)) {
          //throw  "No legal start or end date in method setRange";
          return;
      }
  
      this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
      this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  
      if (this.autoScale) {
          this.setMinimumStep(minimumStep);
      }
  };
  
  /**
   * Set the step iterator to the start date.
   */
  links.Timeline.StepDate.prototype.start = function() {
      this.current = new Date(this._start.valueOf());
      this.roundToMinor();
  };
  
  /**
   * Round the current date to the first minor date value
   * This must be executed once when the current date is set to start Date
   */
  links.Timeline.StepDate.prototype.roundToMinor = function() {
      // round to floor
      // IMPORTANT: we have no breaks in this switch! (this is no bug)
      //noinspection FallthroughInSwitchStatementJS
      switch (this.scale) {
          case links.Timeline.StepDate.SCALE.YEAR:
              this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
              this.current.setMonth(0);
          case links.Timeline.StepDate.SCALE.MONTH:        this.current.setDate(1);
          case links.Timeline.StepDate.SCALE.DAY:          // intentional fall through
          case links.Timeline.StepDate.SCALE.WEEKDAY:      this.current.setHours(0);
          case links.Timeline.StepDate.SCALE.HOUR:         this.current.setMinutes(0);
          case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setSeconds(0);
          case links.Timeline.StepDate.SCALE.SECOND:       this.current.setMilliseconds(0);
          //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
      }
  
      if (this.step != 1) {
          // round down to the first minor value that is a multiple of the current step size
          switch (this.scale) {
              case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);  break;
              case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
              case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
              case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
              case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
              case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
              case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);  break;
              case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
              default: break;
          }
      }
  };
  
  /**
   * Check if the end date is reached
   * @return {boolean}  true if the current date has passed the end date
   */
  links.Timeline.StepDate.prototype.end = function () {
      return (this.current.valueOf() > this._end.valueOf());
  };
  
  /**
   * Do the next step
   */
  links.Timeline.StepDate.prototype.next = function() {
      var prev = this.current.valueOf();
  
      // Two cases, needed to prevent issues with switching daylight savings 
      // (end of March and end of October)
      if (this.current.getMonth() < 6)   {
          switch (this.scale) {
              case links.Timeline.StepDate.SCALE.MILLISECOND:
  
                  this.current = new Date(this.current.valueOf() + this.step); break;
              case links.Timeline.StepDate.SCALE.SECOND:       this.current = new Date(this.current.valueOf() + this.step * 1000); break;
              case links.Timeline.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
              case links.Timeline.StepDate.SCALE.HOUR:
                  this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
                  // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
                  var h = this.current.getHours();
                  this.current.setHours(h - (h % this.step));
                  break;
              case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
              case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
              case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
              case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
              default:                      break;
          }
      }
      else {
          switch (this.scale) {
              case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current = new Date(this.current.valueOf() + this.step); break;
              case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() + this.step); break;
              case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() + this.step); break;
              case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() + this.step); break;
              case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
              case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
              case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
              case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
              default:                      break;
          }
      }
  
      if (this.step != 1) {
          // round down to the correct major value
          switch (this.scale) {
              case links.Timeline.StepDate.SCALE.MILLISECOND:  if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);  break;
              case links.Timeline.StepDate.SCALE.SECOND:       if(this.current.getSeconds() < this.step) this.current.setSeconds(0);  break;
              case links.Timeline.StepDate.SCALE.MINUTE:       if(this.current.getMinutes() < this.step) this.current.setMinutes(0);  break;
              case links.Timeline.StepDate.SCALE.HOUR:         if(this.current.getHours() < this.step) this.current.setHours(0);  break;
              case links.Timeline.StepDate.SCALE.WEEKDAY:      // intentional fall through
              case links.Timeline.StepDate.SCALE.DAY:          if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
              case links.Timeline.StepDate.SCALE.MONTH:        if(this.current.getMonth() < this.step) this.current.setMonth(0);  break;
              case links.Timeline.StepDate.SCALE.YEAR:         break; // nothing to do for year
              default:                break;
          }
      }
  
      // safety mechanism: if current time is still unchanged, move to the end
      if (this.current.valueOf() == prev) {
          this.current = new Date(this._end.valueOf());
      }
  };
  
  
  /**
   * Get the current datetime
   * @return {Date}  current The current date
   */
  links.Timeline.StepDate.prototype.getCurrent = function() {
      return this.current;
  };
  
  /**
   * Set a custom scale. Autoscaling will be disabled.
   * For example setScale(SCALE.MINUTES, 5) will result
   * in minor steps of 5 minutes, and major steps of an hour.
   *
   * @param {links.Timeline.StepDate.SCALE} newScale
   *                               A scale. Choose from SCALE.MILLISECOND,
   *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
   *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
   *                               SCALE.YEAR.
   * @param {Number}     newStep   A step size, by default 1. Choose for
   *                               example 1, 2, 5, or 10.
   */
  links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) {
      this.scale = newScale;
  
      if (newStep > 0) {
          this.step = newStep;
      }
  
      this.autoScale = false;
  };
  
  /**
   * Enable or disable autoscaling
   * @param {boolean} enable  If true, autoascaling is set true
   */
  links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
      this.autoScale = enable;
  };
  
  
  /**
   * Automatically determine the scale that bests fits the provided minimum step
   * @param {Number} minimumStep  The minimum step size in milliseconds
   */
  links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) {
      if (minimumStep == undefined) {
          return;
      }
  
      var stepYear       = (1000 * 60 * 60 * 24 * 30 * 12);
      var stepMonth      = (1000 * 60 * 60 * 24 * 30);
      var stepDay        = (1000 * 60 * 60 * 24);
      var stepHour       = (1000 * 60 * 60);
      var stepMinute     = (1000 * 60);
      var stepSecond     = (1000);
      var stepMillisecond= (1);
  
      // find the smallest step that is larger than the provided minimumStep
      if (stepYear*1000 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1000;}
      if (stepYear*500 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 500;}
      if (stepYear*100 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 100;}
      if (stepYear*50 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 50;}
      if (stepYear*10 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 10;}
      if (stepYear*5 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 5;}
      if (stepYear > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1;}
      if (stepMonth*3 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 3;}
      if (stepMonth > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 1;}
      if (stepDay*5 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 5;}
      if (stepDay*2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 2;}
      if (stepDay > minimumStep)              {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 1;}
      if (stepDay/2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.WEEKDAY;     this.step = 1;}
      if (stepHour*4 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 4;}
      if (stepHour > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 1;}
      if (stepMinute*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 15;}
      if (stepMinute*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 10;}
      if (stepMinute*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 5;}
      if (stepMinute > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 1;}
      if (stepSecond*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 15;}
      if (stepSecond*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 10;}
      if (stepSecond*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 5;}
      if (stepSecond > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 1;}
      if (stepMillisecond*200 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
      if (stepMillisecond*100 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
      if (stepMillisecond*50 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
      if (stepMillisecond*10 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
      if (stepMillisecond*5 > minimumStep)    {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
      if (stepMillisecond > minimumStep)      {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
  };
  
  /**
   * Snap a date to a rounded value. The snap intervals are dependent on the
   * current scale and step.
   * @param {Date} date   the date to be snapped
   */
  links.Timeline.StepDate.prototype.snap = function(date) {
      if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
          var year = date.getFullYear() + Math.round(date.getMonth() / 12);
          date.setFullYear(Math.round(year / this.step) * this.step);
          date.setMonth(0);
          date.setDate(0);
          date.setHours(0);
          date.setMinutes(0);
          date.setSeconds(0);
          date.setMilliseconds(0);
      }
      else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
          if (date.getDate() > 15) {
              date.setDate(1);
              date.setMonth(date.getMonth() + 1);
              // important: first set Date to 1, after that change the month.      
          }
          else {
              date.setDate(1);
          }
  
          date.setHours(0);
          date.setMinutes(0);
          date.setSeconds(0);
          date.setMilliseconds(0);
      }
      else if (this.scale == links.Timeline.StepDate.SCALE.DAY ||
          this.scale == links.Timeline.StepDate.SCALE.WEEKDAY) {
          switch (this.step) {
              case 5:
              case 2:
                  date.setHours(Math.round(date.getHours() / 24) * 24); break;
              default:
                  date.setHours(Math.round(date.getHours() / 12) * 12); break;
          }
          date.setMinutes(0);
          date.setSeconds(0);
          date.setMilliseconds(0);
      }
      else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
          switch (this.step) {
              case 4:
                  date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
              default:
                  date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
          }
          date.setSeconds(0);
          date.setMilliseconds(0);
      } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
          switch (this.step) {
              case 15:
              case 10:
                  date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
                  date.setSeconds(0);
                  break;
              case 5:
                  date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
              default:
                  date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
          }
          date.setMilliseconds(0);
      }
      else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
          switch (this.step) {
              case 15:
              case 10:
                  date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
                  date.setMilliseconds(0);
                  break;
              case 5:
                  date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
              default:
                  date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
          }
      }
      else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
          var step = this.step > 5 ? this.step / 2 : 1;
          date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
      }
  };
  
  /**
   * Check if the current step is a major step (for example when the step
   * is DAY, a major step is each first day of the MONTH)
   * @return {boolean} true if current date is major, else false.
   */
  links.Timeline.StepDate.prototype.isMajor = function() {
      switch (this.scale) {
          case links.Timeline.StepDate.SCALE.MILLISECOND:
              return (this.current.getMilliseconds() == 0);
          case links.Timeline.StepDate.SCALE.SECOND:
              return (this.current.getSeconds() == 0);
          case links.Timeline.StepDate.SCALE.MINUTE:
              return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
          // Note: this is no bug. Major label is equal for both minute and hour scale
          case links.Timeline.StepDate.SCALE.HOUR:
              return (this.current.getHours() == 0);
          case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through
          case links.Timeline.StepDate.SCALE.DAY:
              return (this.current.getDate() == 1);
          case links.Timeline.StepDate.SCALE.MONTH:
              return (this.current.getMonth() == 0);
          case links.Timeline.StepDate.SCALE.YEAR:
              return false;
          default:
              return false;
      }
  };
  
  
  /**
   * Returns formatted text for the minor axislabel, depending on the current
   * date and the scale. For example when scale is MINUTE, the current time is
   * formatted as "hh:mm".
   * @param {Object} options
   * @param {Date} [date] custom date. if not provided, current date is taken
   */
  links.Timeline.StepDate.prototype.getLabelMinor = function(options, date) {
      if (date == undefined) {
          date = this.current;
      }
  
      switch (this.scale) {
          case links.Timeline.StepDate.SCALE.MILLISECOND:  return String(date.getMilliseconds());
          case links.Timeline.StepDate.SCALE.SECOND:       return String(date.getSeconds());
          case links.Timeline.StepDate.SCALE.MINUTE:
              return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2);
          case links.Timeline.StepDate.SCALE.HOUR:
              return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2);
          case links.Timeline.StepDate.SCALE.WEEKDAY:      return options.DAYS_SHORT[date.getDay()] + ' ' + date.getDate();
          case links.Timeline.StepDate.SCALE.DAY:          return String(date.getDate());
          case links.Timeline.StepDate.SCALE.MONTH:        return options.MONTHS_SHORT[date.getMonth()];   // month is zero based
          case links.Timeline.StepDate.SCALE.YEAR:         return String(date.getFullYear());
          default:                                         return "";
      }
  };
  
  
  /**
   * Returns formatted text for the major axislabel, depending on the current
   * date and the scale. For example when scale is MINUTE, the major scale is
   * hours, and the hour will be formatted as "hh".
   * @param {Object} options
   * @param {Date} [date] custom date. if not provided, current date is taken
   */
  links.Timeline.StepDate.prototype.getLabelMajor = function(options, date) {
      if (date == undefined) {
          date = this.current;
      }
  
      switch (this.scale) {
          case links.Timeline.StepDate.SCALE.MILLISECOND:
              return  this.addZeros(date.getHours(), 2) + ":" +
                  this.addZeros(date.getMinutes(), 2) + ":" +
                  this.addZeros(date.getSeconds(), 2);
          case links.Timeline.StepDate.SCALE.SECOND:
              return  date.getDate() + " " +
                  options.MONTHS[date.getMonth()] + " " +
                  this.addZeros(date.getHours(), 2) + ":" +
                  this.addZeros(date.getMinutes(), 2);
          case links.Timeline.StepDate.SCALE.MINUTE:
              return  options.DAYS[date.getDay()] + " " +
                  date.getDate() + " " +
                  options.MONTHS[date.getMonth()] + " " +
                  date.getFullYear();
          case links.Timeline.StepDate.SCALE.HOUR:
              return  options.DAYS[date.getDay()] + " " +
                  date.getDate() + " " +
                  options.MONTHS[date.getMonth()] + " " +
                  date.getFullYear();
          case links.Timeline.StepDate.SCALE.WEEKDAY:
          case links.Timeline.StepDate.SCALE.DAY:
              return  options.MONTHS[date.getMonth()] + " " +
                  date.getFullYear();
          case links.Timeline.StepDate.SCALE.MONTH:
              return String(date.getFullYear());
          default:
              return "";
      }
  };
  
  /**
   * Add leading zeros to the given value to match the desired length.
   * For example addZeros(123, 5) returns "00123"
   * @param {int} value   A value
   * @param {int} len     Desired final length
   * @return {string}     value with leading zeros
   */
  links.Timeline.StepDate.prototype.addZeros = function(value, len) {
      var str = "" + value;
      while (str.length < len) {
          str = "0" + str;
      }
      return str;
  };
  
  
  
  /** ------------------------------------------------------------------------ **/
  
  /**
   * Image Loader service.
   * can be used to get a callback when a certain image is loaded
   *
   */
  links.imageloader = (function () {
      var urls = {};  // the loaded urls
      var callbacks = {}; // the urls currently being loaded. Each key contains 
      // an array with callbacks
  
      /**
       * Check if an image url is loaded
       * @param {String} url
       * @return {boolean} loaded   True when loaded, false when not loaded
       *                            or when being loaded
       */
      function isLoaded (url) {
          if (urls[url] == true) {
              return true;
          }
  
          var image = new Image();
          image.src = url;
          if (image.complete) {
              return true;
          }
  
          return false;
      }
  
      /**
       * Check if an image url is being loaded
       * @param {String} url
       * @return {boolean} loading   True when being loaded, false when not loading
       *                             or when already loaded
       */
      function isLoading (url) {
          return (callbacks[url] != undefined);
      }
  
      /**
       * Load given image url
       * @param {String} url
       * @param {function} callback
       * @param {boolean} sendCallbackWhenAlreadyLoaded  optional
       */
      function load (url, callback, sendCallbackWhenAlreadyLoaded) {
          if (sendCallbackWhenAlreadyLoaded == undefined) {
              sendCallbackWhenAlreadyLoaded = true;
          }
  
          if (isLoaded(url)) {
              if (sendCallbackWhenAlreadyLoaded) {
                  callback(url);
              }
              return;
          }
  
          if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) {
              return;
          }
  
          var c = callbacks[url];
          if (!c) {
              var image = new Image();
              image.src = url;
  
              c = [];
              callbacks[url] = c;
  
              image.onload = function (event) {
                  urls[url] = true;
                  delete callbacks[url];
  
                  for (var i = 0; i < c.length; i++) {
                      c[i](url);
                  }
              }
          }
  
          if (c.indexOf(callback) == -1) {
              c.push(callback);
          }
      }
  
      /**
       * Load a set of images, and send a callback as soon as all images are
       * loaded
       * @param {String[]} urls
       * @param {function } callback
       * @param {boolean} sendCallbackWhenAlreadyLoaded
       */
      function loadAll (urls, callback, sendCallbackWhenAlreadyLoaded) {
          // list all urls which are not yet loaded
          var urlsLeft = [];
          urls.forEach(function (url) {
              if (!isLoaded(url)) {
                  urlsLeft.push(url);
              }
          });
  
          if (urlsLeft.length) {
              // there are unloaded images
              var countLeft = urlsLeft.length;
              urlsLeft.forEach(function (url) {
                  load(url, function () {
                      countLeft--;
                      if (countLeft == 0) {
                          // done!
                          callback();
                      }
                  }, sendCallbackWhenAlreadyLoaded);
              });
          }
          else {
              // we are already done!
              if (sendCallbackWhenAlreadyLoaded) {
                  callback();
              }
          }
      }
  
      /**
       * Recursively retrieve all image urls from the images located inside a given
       * HTML element
       * @param {Node} elem
       * @param {String[]} urls   Urls will be added here (no duplicates)
       */
      function filterImageUrls (elem, urls) {
          var child = elem.firstChild;
          while (child) {
              if (child.tagName == 'IMG') {
                  var url = child.src;
                  if (urls.indexOf(url) == -1) {
                      urls.push(url);
                  }
              }
  
              filterImageUrls(child, urls);
  
              child = child.nextSibling;
          }
      }
  
      return {
          'isLoaded': isLoaded,
          'isLoading': isLoading,
          'load': load,
          'loadAll': loadAll,
          'filterImageUrls': filterImageUrls
      };
  })();
  
  
  /** ------------------------------------------------------------------------ **/
  
  
  /**
   * Add and event listener. Works for all browsers
   * @param {Element} element    An html element
   * @param {string}      action     The action, for example "click",
   *                                 without the prefix "on"
   * @param {function}    listener   The callback function to be executed
   * @param {boolean}     useCapture
   */
  links.Timeline.addEventListener = function (element, action, listener, useCapture) {
      if (element.addEventListener) {
          if (useCapture === undefined)
              useCapture = false;
  
          if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
              action = "DOMMouseScroll";  // For Firefox
          }
  
          element.addEventListener(action, listener, useCapture);
      } else {
          element.attachEvent("on" + action, listener);  // IE browsers
      }
  };
  
  /**
   * Remove an event listener from an element
   * @param {Element}  element   An html dom element
   * @param {string}       action    The name of the event, for example "mousedown"
   * @param {function}     listener  The listener function
   * @param {boolean}      useCapture
   */
  links.Timeline.removeEventListener = function(element, action, listener, useCapture) {
      if (element.removeEventListener) {
          // non-IE browsers
          if (useCapture === undefined)
              useCapture = false;
  
          if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
              action = "DOMMouseScroll";  // For Firefox
          }
  
          element.removeEventListener(action, listener, useCapture);
      } else {
          // IE browsers
          element.detachEvent("on" + action, listener);
      }
  };
  
  
  /**
   * Get HTML element which is the target of the event
   * @param {Event} event
   * @return {Element} target element
   */
  links.Timeline.getTarget = function (event) {
      // code from http://www.quirksmode.org/js/events_properties.html
      if (!event) {
          event = window.event;
      }
  
      var target;
  
      if (event.target) {
          target = event.target;
      }
      else if (event.srcElement) {
          target = event.srcElement;
      }
  
      if (target.nodeType != undefined && target.nodeType == 3) {
          // defeat Safari bug
          target = target.parentNode;
      }
  
      return target;
  };
  
  /**
   * Stop event propagation
   */
  links.Timeline.stopPropagation = function (event) {
      if (!event)
          event = window.event;
  
      if (event.stopPropagation) {
          event.stopPropagation();  // non-IE browsers
      }
      else {
          event.cancelBubble = true;  // IE browsers
      }
  };
  
  
  /**
   * Cancels the event if it is cancelable, without stopping further propagation of the event.
   */
  links.Timeline.preventDefault = function (event) {
      if (!event)
          event = window.event;
  
      if (event.preventDefault) {
          event.preventDefault();  // non-IE browsers
      }
      else {
          event.returnValue = false;  // IE browsers
      }
  };
  
  
  /**
   * Retrieve the absolute left value of a DOM element
   * @param {Element} elem        A dom element, for example a div
   * @return {number} left        The absolute left position of this element
   *                              in the browser page.
   */
  links.Timeline.getAbsoluteLeft = function(elem) {
      var doc = document.documentElement;
      var body = document.body;
  
      var left = elem.offsetLeft;
      var e = elem.offsetParent;
      while (e != null && e != body && e != doc) {
          left += e.offsetLeft;
          left -= e.scrollLeft;
          e = e.offsetParent;
      }
      return left;
  };
  
  /**
   * Retrieve the absolute top value of a DOM element
   * @param {Element} elem        A dom element, for example a div
   * @return {number} top        The absolute top position of this element
   *                              in the browser page.
   */
  links.Timeline.getAbsoluteTop = function(elem) {
      var doc = document.documentElement;
      var body = document.body;
  
      var top = elem.offsetTop;
      var e = elem.offsetParent;
      while (e != null && e != body && e != doc) {
          top += e.offsetTop;
          top -= e.scrollTop;
          e = e.offsetParent;
      }
      return top;
  };
  
  /**
   * Get the absolute, vertical mouse position from an event.
   * @param {Event} event
   * @return {Number} pageY
   */
  links.Timeline.getPageY = function (event) {
      if (('targetTouches' in event) && event.targetTouches.length) {
          event = event.targetTouches[0];
      }
  
      if ('pageY' in event) {
          return event.pageY;
      }
  
      // calculate pageY from clientY
      var clientY = event.clientY;
      var doc = document.documentElement;
      var body = document.body;
      return clientY +
          ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
          ( doc && doc.clientTop || body && body.clientTop || 0 );
  };
  
  /**
   * Get the absolute, horizontal mouse position from an event.
   * @param {Event} event
   * @return {Number} pageX
   */
  links.Timeline.getPageX = function (event) {
      if (('targetTouches' in event) && event.targetTouches.length) {
          event = event.targetTouches[0];
      }
  
      if ('pageX' in event) {
          return event.pageX;
      }
  
      // calculate pageX from clientX
      var clientX = event.clientX;
      var doc = document.documentElement;
      var body = document.body;
      return clientX +
          ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
          ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  };
  
  /**
   * Adds one or more className's to the given elements style
   * @param {Element} elem
   * @param {String} className
   */
  links.Timeline.addClassName = function(elem, className) {
      var classes = elem.className.split(' ');
      var classesToAdd = className.split(' ');
      
      var added = false;
      for (var i=0; i<classesToAdd.length; i++) {
          if (classes.indexOf(classesToAdd[i]) == -1) {
              classes.push(classesToAdd[i]); // add the class to the array
              added = true;
          }
      }
      
      if (added) {
          elem.className = classes.join(' ');
      }
  };
  
  /**
   * Removes one or more className's from the given elements style
   * @param {Element} elem
   * @param {String} className
   */
  links.Timeline.removeClassName = function(elem, className) {
      var classes = elem.className.split(' ');
      var classesToRemove = className.split(' ');
      
      var removed = false;
      for (var i=0; i<classesToRemove.length; i++) {
          var index = classes.indexOf(classesToRemove[i]);
          if (index != -1) {
              classes.splice(index, 1); // remove the class from the array
              removed = true;
          }
      }
      
      if (removed) {
          elem.className = classes.join(' ');
      }
  };
  
  /**
   * 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]');
  };
  
  /**
   * parse a JSON date
   * @param {Date | String | Number} date    Date object to be parsed. Can be:
   *                                         - a Date object like new Date(),
   *                                         - a long like 1356970529389,
   *                                         an ISO String like "2012-12-31T16:16:07.213Z",
   *                                         or a .Net Date string like
   *                                         "\/Date(1356970529389)\/"
   * @return {Date} parsedDate
   */
  links.Timeline.parseJSONDate = function (date) {
      if (date == undefined) {
          return undefined;
      }
  
      //test for date
      if (date instanceof Date) {
          return date;
      }
  
      // test for MS format.
      // FIXME: will fail on a Number
      var m = date.match(/\/Date\((-?\d+)([-\+]?\d{2})?(\d{2})?\)\//i);
      if (m) {
          var offset = m[2]
              ? (3600000 * m[2]) // hrs offset
              + (60000 * m[3] * (m[2] / Math.abs(m[2]))) // mins offset
              : 0;
  
          return new Date(
              (1 * m[1]) // ticks
                  + offset
          );
      }
  
      // failing that, try to parse whatever we've got.
      return Date.parse(date);
  };
  