From 4beee1f290177d9ad9086343b02c8a7a59b59ba9 Mon Sep 17 00:00:00 2001 From: Eric Gillingham Date: Wed, 30 Oct 2013 13:46:14 -0700 Subject: [PATCH] Create ItemRangeOverflow item type Overflow simply extends ItemRange with a new getWidth() function that determines if the content of the range is wider than the range box itself. This did require adding a getWidth() function to the Item object that always just returns this.width, and adjusting Stack.js to use the new getWidth() function instead of item.width --- src/timeline/Stack.js | 17 +- src/timeline/component/ItemSet.js | 1 + src/timeline/component/css/item.css | 16 +- src/timeline/component/item/Item.js | 8 + .../component/item/ItemRangeOverflow.js | 91 + vis.js | 559 +- vis.min.js | 15301 +++++++++++++++- 7 files changed, 15881 insertions(+), 112 deletions(-) create mode 100644 src/timeline/component/item/ItemRangeOverflow.js diff --git a/src/timeline/Stack.js b/src/timeline/Stack.js index 2464300e..e714aee4 100644 --- a/src/timeline/Stack.js +++ b/src/timeline/Stack.js @@ -185,21 +185,8 @@ Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, * @return {boolean} true if a and b collide, else false */ Stack.prototype.collision = function collision (a, b, margin) { - var a_width; - var b_width; - - if (a.props.content !== undefined && a.width < a.props.content.width) - a_width = a.props.content.width; - else - a_width = a.width; - - if (b.props.content !== undefined && b.width < b.props.content.width) - b_width = b.props.content.width; - else - b_width = b.width - - return ((a.left - margin) < (b.left + b_width) && - (a.left + a_width + margin) > b.left && + return ((a.left - margin) < (b.left + b.getWidth()) && + (a.left + a.getWidth() + margin) > b.left && (a.top - margin) < (b.top + b.height) && (a.top + a.height + margin) > b.top); }; diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index be407df4..601c07f7 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -67,6 +67,7 @@ ItemSet.prototype = new Panel(); ItemSet.types = { box: ItemBox, range: ItemRange, + rangeoverflow: ItemRangeOverflow, point: ItemPoint }; diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css index 6ec29b80..7767736f 100644 --- a/src/timeline/component/css/item.css +++ b/src/timeline/component/css/item.css @@ -42,23 +42,31 @@ } .vis.timeline .item.range { + overflow: hidden; + border-style: solid; + border-width: 1px; + border-radius: 2px; + -moz-border-radius: 2px; /* For Firefox 3.6 and older */ +} + +.vis.timeline .item.rangeoverflow { border-style: solid; border-width: 1px; border-radius: 2px; -moz-border-radius: 2px; /* For Firefox 3.6 and older */ } -.vis.timeline .item.range .drag-left { +.vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left { cursor: w-resize; z-index: 1000; } -.vis.timeline .item.range .drag-right { +.vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right { cursor: e-resize; z-index: 1000; } -.vis.timeline .item.range .content { +.vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content { position: relative; display: inline-block; } @@ -71,7 +79,7 @@ } .vis.timeline .item .content { - margin: 5px; + margin: 0; white-space: nowrap; overflow: hidden; } diff --git a/src/timeline/component/item/Item.js b/src/timeline/component/item/Item.js index 473d7621..0a5f623e 100644 --- a/src/timeline/component/item/Item.js +++ b/src/timeline/component/item/Item.js @@ -69,3 +69,11 @@ Item.prototype.reflow = function reflow() { // should be implemented by the item return false; }; + +/** + * Return the items width + * @return {Integer} width + */ +Item.prototype.getWidth = function getWidth() { + return this.width; +} diff --git a/src/timeline/component/item/ItemRangeOverflow.js b/src/timeline/component/item/ItemRangeOverflow.js new file mode 100644 index 00000000..29f4cf55 --- /dev/null +++ b/src/timeline/component/item/ItemRangeOverflow.js @@ -0,0 +1,91 @@ +/** + * @constructor ItemRangeOverflow + * @extends ItemRange + * @param {ItemSet} parent + * @param {Object} data Object containing parameters start, end + * content, className. + * @param {Object} [options] Options to set initial property values + * @param {Object} [defaultOptions] default options + * // TODO: describe available options + */ +function ItemRangeOverflow (parent, data, options, defaultOptions) { + this.props = { + content: { + left: 0, + width: 0 + } + }; + + ItemRange.call(this, parent, data, options, defaultOptions); +} + +ItemRangeOverflow.prototype = new ItemRange (null, null); + +/** + * Repaint the item + * @return {Boolean} changed + */ +ItemRangeOverflow.prototype.repaint = function repaint() { + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; + + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } + + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } + + if (!dom.box.parentNode) { + foreground.appendChild(dom.box); + changed = true; + } + + // update content + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } + + // update class + var className = this.data.className ? (' ' + this.data.className) : ''; + if (this.className != className) { + this.className = className; + dom.box.className = 'item rangeoverflow' + className; + changed = true; + } + } + + return changed; +}; + +/** + * Return the items width + * @return {Integer} width + */ +ItemRangeOverflow.prototype.getWidth = function getWidth() { + if (this.props.content !== undefined && this.width < this.props.content.width) + return this.props.content.width; + else + return this.width; +} diff --git a/vis.js b/vis.js index f4001c07..3efe6993 100644 --- a/vis.js +++ b/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 0.3.0-SNAPSHOT - * @date 2013-10-29 + * @date 2013-10-30 * * @license * Copyright (C) 2011-2013 Almende B.V, http://almende.com @@ -3769,7 +3769,17 @@ else { // Try to load dependencies from the global window object. // If not available there, load via require. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); -var Hammer = (typeof window !== 'undefined') && window['Hammer'] || require('hammerjs'); + +var Hammer; +if (typeof window !== 'undefined') { + // load hammer.js only when running in a browser (where window is available) + Hammer = window['Hammer'] || require('hammerjs'); +} +else { + Hammer = function () { + throw Error('hammer.js is only available in a browser, not in node.js.'); + } +} // Internet Explorer 8 and older does not support Array.indexOf, so we define @@ -6177,8 +6187,7 @@ TimeStep.SCALE = { */ TimeStep.prototype.setRange = function(start, end, minimumStep) { if (!(start instanceof Date) || !(end instanceof Date)) { - //throw "No legal start or end date in method setRange"; - return; + throw "No legal start or end date in method setRange"; } this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); @@ -6750,21 +6759,8 @@ Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, * @return {boolean} true if a and b collide, else false */ Stack.prototype.collision = function collision (a, b, margin) { - var a_width; - var b_width; - - if (a.props.content !== undefined && a.width < a.props.content.width) - a_width = a.props.content.width; - else - a_width = a.width; - - if (b.props.content !== undefined && b.width < b.props.content.width) - b_width = b.props.content.width; - else - b_width = b.width - - return ((a.left - margin) < (b.left + b_width) && - (a.left + a_width + margin) > b.left && + return ((a.left - margin) < (b.left + b.getWidth()) && + (a.left + a.getWidth() + margin) > b.left && (a.top - margin) < (b.top + b.height) && (a.top + a.height + margin) > b.top); }; @@ -6779,16 +6775,10 @@ Stack.prototype.collision = function collision (a, b, margin) { */ function Range(options) { this.id = util.randomUUID(); - this.start = 0; // Number - this.end = 0; // Number + this.start = null; // Number + this.end = null; // Number - // this.options = options || {}; // TODO: fix range options - this.options = { - min: null, - max: null, - zoomMin: null, - zoomMax: null - }; + this.options = options || {}; this.listeners = []; @@ -6798,8 +6788,6 @@ function Range(options) { /** * Set options for the range controller * @param {Object} options Available options: - * {Number} start Set start value of the range - * {Number} end Set end value of the range * {Number} min Minimum value for start * {Number} max Maximum value for end * {Number} zoomMin Set a minimum value for @@ -6810,8 +6798,9 @@ function Range(options) { Range.prototype.setOptions = function (options) { util.extend(this.options, options); - if (options.start != null || options.end != null) { - this.setRange(options.start, options.end); + // re-apply range with new limitations + if (this.start !== null && this.end !== null) { + this.setRange(this.start, this.end); } }; @@ -6890,8 +6879,8 @@ Range.prototype._trigger = function (event) { /** * Set a new start and end range - * @param {Number} start - * @param {Number} end + * @param {Number} [start] + * @param {Number} [end] */ Range.prototype.setRange = function(start, end) { var changed = this._applyRange(start, end); @@ -6905,21 +6894,23 @@ Range.prototype.setRange = function(start, end) { * Set a new start and end range. This method is the same as setRange, but * does not trigger a range change and range changed event, and it returns * true when the range is changed - * @param {Number} start - * @param {Number} end + * @param {Number} [start] + * @param {Number} [end] * @return {Boolean} changed * @private */ Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Number') : this.start; - var newEnd = (end != null) ? util.convert(end, 'Number') : this.end; - var diff; + var newStart = (start != null) ? util.convert(start, 'Number') : this.start, + newEnd = (end != null) ? util.convert(end, 'Number') : this.end, + max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, + min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, + diff; // check for valid number - if (isNaN(newStart)) { + if (isNaN(newStart) || newStart === null) { throw new Error('Invalid start "' + start + '"'); } - if (isNaN(newEnd)) { + if (isNaN(newEnd) || newEnd === null) { throw new Error('Invalid end "' + end + '"'); } @@ -6929,64 +6920,76 @@ Range.prototype._applyRange = function(start, end) { } // prevent start < min - if (this.options.min != null) { - var min = this.options.min.valueOf(); + if (min !== null) { if (newStart < min) { diff = (min - newStart); newStart += diff; newEnd += diff; + + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } } } // prevent end > max - if (this.options.max != null) { - var max = this.options.max.valueOf(); + if (max !== null) { if (newEnd > max) { diff = (newEnd - max); newStart -= diff; newEnd -= diff; + + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } } } - // prevent (end-start) > zoomMin - if (this.options.zoomMin != null) { - var zoomMin = this.options.zoomMin.valueOf(); + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); if (zoomMin < 0) { zoomMin = 0; } if ((newEnd - newStart) < zoomMin) { - if ((this.end - this.start) > zoomMin) { + if ((this.end - this.start) === zoomMin) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } + else { // zoom to the minimum diff = (zoomMin - (newEnd - newStart)); newStart -= diff / 2; newEnd += diff / 2; } - else { - // ingore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } } } - // prevent (end-start) > zoomMin - if (this.options.zoomMax != null) { - var zoomMax = this.options.zoomMax.valueOf(); + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); if (zoomMax < 0) { zoomMax = 0; } if ((newEnd - newStart) > zoomMax) { - if ((this.end - this.start) < zoomMax) { + if ((this.end - this.start) === zoomMax) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } + else { // zoom to the maximum diff = ((newEnd - newStart) - zoomMax); newStart += diff / 2; newEnd -= diff / 2; } - else { - // ingore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } } } @@ -8445,12 +8448,13 @@ TimeAxis.prototype.reflow = function () { // calculate range and step this._updateConversion(); - var start = util.convert(range.start, 'Date'), - end = util.convert(range.end, 'Date'), - minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0); - this.step = new TimeStep(start, end, minimumStep); - changed += update(props.range, 'start', start.valueOf()); - changed += update(props.range, 'end', end.valueOf()); + var start = util.convert(range.start, 'Number'), + end = util.convert(range.end, 'Number'), + minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf() + -this.toTime(0).valueOf(); + this.step = new TimeStep(new Date(start), new Date(end), minimumStep); + changed += update(props.range, 'start', start); + changed += update(props.range, 'end', end); changed += update(props.range, 'minimumStep', minimumStep.valueOf()); } @@ -8580,6 +8584,262 @@ CurrentTime.prototype.repaint = function () { return false; }; +/** + * A custom time bar + * @param {Component} parent + * @param {Component[]} [depends] Components on which this components depends + * (except for the parent) + * @param {Object} [options] Available parameters: + * {Boolean} [showCustomTime] + * @constructor CustomTime + * @extends Component + */ + +function CustomTime (parent, depends, options) { + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.options = options || {}; + this.defaultOptions = { + showCustomTime: false + }; + + this.listeners = []; + this.customTime = new Date(); +} + +CustomTime.prototype = new Component(); + +CustomTime.prototype.setOptions = Component.prototype.setOptions; + +/** + * Get the container element of the bar, which can be used by a child to + * add its own widgets. + * @returns {HTMLElement} container + */ +CustomTime.prototype.getContainer = function () { + return this.frame; +}; + +/** + * Repaint the component + * @return {Boolean} changed + */ +CustomTime.prototype.repaint = function () { + var bar = this.frame, + parent = this.parent, + parentContainer = parent.parent.getContainer(); + + if (!parent) { + throw new Error('Cannot repaint bar: no parent attached'); + } + + if (!parentContainer) { + throw new Error('Cannot repaint bar: parent has no container element'); + } + + if (!this.getOption('showCustomTime')) { + if (bar) { + parentContainer.removeChild(bar); + delete this.frame; + } + + return; + } + + if (!bar) { + bar = document.createElement('div'); + bar.className = 'customtime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + + parentContainer.appendChild(bar); + + 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'; + bar.appendChild(drag); + + this.frame = bar; + + this.subscribe(this, 'movetime'); + } + + if (!parent.conversion) { + parent._updateConversion(); + } + + var x = parent.toScreen(this.customTime); + + bar.style.left = x + 'px'; + bar.title = 'Time: ' + this.customTime; + + return false; +}; + +/** + * Set custom time. + * @param {Date} time + */ +CustomTime.prototype._setCustomTime = function(time) { + this.customTime = new Date(time.valueOf()); + this.repaint(); +}; + +/** + * Retrieve the current custom time. + * @return {Date} customTime + */ +CustomTime.prototype._getCustomTime = function() { + return new Date(this.customTime.valueOf()); +}; + +/** + * Add listeners for mouse and touch events to the component + * @param {Component} component + */ +CustomTime.prototype.subscribe = function (component, event) { + var me = this; + var listener = { + component: component, + event: event, + callback: function (event) { + me._onMouseDown(event, listener); + }, + params: {} + }; + + component.on('mousedown', listener.callback); + me.listeners.push(listener); + +}; + +/** + * Event handler + * @param {String} event name of the event, for example 'click', 'mousemove' + * @param {function} callback callback handler, invoked with the raw HTML Event + * as parameter. + */ +CustomTime.prototype.on = function (event, callback) { + var bar = this.frame; + if (!bar) { + throw new Error('Cannot add event listener: no parent attached'); + } + + events.addListener(this, event, callback); + util.addEventListener(bar, event, callback); +}; + +/** + * Start moving horizontally + * @param {Event} event + * @param {Object} listener Listener containing the component and params + * @private + */ +CustomTime.prototype._onMouseDown = function(event, listener) { + event = event || window.event; + var params = listener.params; + + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); + if (!leftButtonDown) { + return; + } + + // get mouse position + params.mouseX = util.getPageX(event); + params.moved = false; + + params.customTime = this.customTime; + + // add event listeners to handle moving the custom time bar + var me = this; + if (!params.onMouseMove) { + params.onMouseMove = function (event) { + me._onMouseMove(event, listener); + }; + util.addEventListener(document, 'mousemove', params.onMouseMove); + } + if (!params.onMouseUp) { + params.onMouseUp = function (event) { + me._onMouseUp(event, listener); + }; + util.addEventListener(document, 'mouseup', params.onMouseUp); + } + + util.stopPropagation(event); + util.preventDefault(event); +}; + +/** + * Perform moving operating. + * This function activated from within the funcion CustomTime._onMouseDown(). + * @param {Event} event + * @param {Object} listener + * @private + */ +CustomTime.prototype._onMouseMove = function (event, listener) { + event = event || window.event; + var params = listener.params; + var parent = this.parent; + + // calculate change in mouse position + var mouseX = util.getPageX(event); + + if (params.mouseX === undefined) { + params.mouseX = mouseX; + } + + var diff = mouseX - params.mouseX; + + // if mouse movement is big enough, register it as a "moved" event + if (Math.abs(diff) >= 1) { + params.moved = true; + } + + var x = parent.toScreen(params.customTime); + var xnew = x + diff; + var time = parent.toTime(xnew); + this._setCustomTime(time); + + // fire a timechange event + events.trigger(this, 'timechange', {customTime: this.customTime}); + + util.preventDefault(event); +}; + +/** + * Stop moving operating. + * This function activated from within the function CustomTime._onMouseDown(). + * @param {event} event + * @param {Object} listener + * @private + */ +CustomTime.prototype._onMouseUp = function (event, listener) { + event = event || window.event; + var params = listener.params; + + // remove event listeners here, important for Safari + if (params.onMouseMove) { + util.removeEventListener(document, 'mousemove', params.onMouseMove); + params.onMouseMove = null; + } + if (params.onMouseUp) { + util.removeEventListener(document, 'mouseup', params.onMouseUp); + params.onMouseUp = null; + } + + if (params.moved) { + // fire a timechanged event + events.trigger(this, 'timechanged', {customTime: this.customTime}); + } +}; + /** * An ItemSet holds a set of items and ranges which can be displayed in a * range. The width is determined by the parent of the ItemSet, and the height @@ -8649,6 +8909,7 @@ ItemSet.prototype = new Panel(); ItemSet.types = { box: ItemBox, range: ItemRange, + rangeoverflow: ItemRangeOverflow, point: ItemPoint }; @@ -9184,6 +9445,14 @@ Item.prototype.reflow = function reflow() { return false; }; +/** + * Return the items width + * @return {Integer} width + */ +Item.prototype.getWidth = function getWidth() { + return this.width; +} + /** * @constructor ItemBox * @extends Item @@ -10019,6 +10288,98 @@ ItemRange.prototype.reposition = function reposition() { } }; +/** + * @constructor ItemRangeOverflow + * @extends ItemRange + * @param {ItemSet} parent + * @param {Object} data Object containing parameters start, end + * content, className. + * @param {Object} [options] Options to set initial property values + * @param {Object} [defaultOptions] default options + * // TODO: describe available options + */ +function ItemRangeOverflow (parent, data, options, defaultOptions) { + this.props = { + content: { + left: 0, + width: 0 + } + }; + + ItemRange.call(this, parent, data, options, defaultOptions); +} + +ItemRangeOverflow.prototype = new ItemRange (null, null); + +/** + * Repaint the item + * @return {Boolean} changed + */ +ItemRangeOverflow.prototype.repaint = function repaint() { + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; + + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } + + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } + + if (!dom.box.parentNode) { + foreground.appendChild(dom.box); + changed = true; + } + + // update content + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } + + // update class + var className = this.data.className ? (' ' + this.data.className) : ''; + if (this.className != className) { + this.className = className; + dom.box.className = 'item rangeoverflow' + className; + changed = true; + } + } + + return changed; +}; + +/** + * Return the items width + * @return {Integer} width + */ +ItemRangeOverflow.prototype.getWidth = function getWidth() { + if (this.props.content !== undefined && this.width < this.props.content.width) + return this.props.content.width; + else + return this.width; +} + /** * @constructor Group * @param {GroupSet} parent @@ -10633,19 +10994,21 @@ GroupSet.prototype._toQueue = function _toQueue(ids, action) { */ function Timeline (container, items, options) { var me = this; - this.options = util.extend({ + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.options = { orientation: 'bottom', min: null, max: null, - zoomMin: 10, // milliseconds + zoomMin: 10, // milliseconds zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds // moveable: true, // TODO: option moveable // zoomable: true, // TODO: option zoomable showMinorLabels: true, showMajorLabels: true, showCurrentTime: false, + showCustomTime: false, autoResize: false - }, options); + }; // controller this.controller = new Controller(); @@ -10698,19 +11061,13 @@ function Timeline (container, items, options) { this.controller.add(this.labelPanel); // range - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.range = new Range({ - start: now.clone().add('days', -3).valueOf(), - end: now.clone().add('days', 4).valueOf() - }); - /* TODO: fix range options var rangeOptions = Object.create(this.options); this.range = new Range(rangeOptions); this.range.setRange( now.clone().add('days', -3).valueOf(), now.clone().add('days', 4).valueOf() ); - */ + // TODO: reckon with options moveable and zoomable this.range.subscribe(this.rootPanel, 'move', 'horizontal'); this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); @@ -10740,13 +11097,22 @@ function Timeline (container, items, options) { this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); this.controller.add(this.currenttime); + // custom time bar + this.customtime = new CustomTime(this.timeaxis, [], rootOptions); + this.controller.add(this.customtime); + // create itemset or groupset this.setGroups(null); this.itemsData = null; // DataSet this.groupsData = null; // DataSet - // set data + // apply options + if (options) { + this.setOptions(options); + } + + // set data (must be after options are applied) if (items) { this.setItems(items); } @@ -10757,16 +11123,33 @@ function Timeline (container, items, options) { * @param {Object} options TODO: describe the available options */ Timeline.prototype.setOptions = function (options) { - if (options) { - util.extend(this.options, options); - } + util.extend(this.options, options); - // TODO: apply range min,max + // force update of range + // options.start and options.end can be undefined + //this.range.setRange(options.start, options.end); + this.range.setRange(); this.controller.reflow(); this.controller.repaint(); }; +/** + * Set a custom time bar + * @param {Date} time + */ +Timeline.prototype.setCustomTime = function (time) { + this.customtime._setCustomTime(time); +}; + +/** + * Retrieve the current custom time. + * @return {Date} customTime + */ +Timeline.prototype.getCustomTime = function() { + return new Date(this.customtime.customTime.valueOf()); +}; + /** * Set items * @param {vis.DataSet | Array | DataTable | null} items @@ -10800,7 +11183,7 @@ Timeline.prototype.setItems = function(items) { // apply the data range as range var dataRange = this.getItemRange(); - // add 5% on both sides + // add 5% space on both sides var min = dataRange.min; var max = dataRange.max; if (min != null && max != null) { @@ -10815,10 +11198,10 @@ Timeline.prototype.setItems = function(items) { // override specified start and/or end date if (this.options.start != undefined) { - min = new Date(this.options.start.valueOf()); + min = util.convert(this.options.start, 'Date'); } if (this.options.end != undefined) { - max = new Date(this.options.end.valueOf()); + max = util.convert(this.options.end, 'Date'); } // apply range if there is a min or max available @@ -15324,7 +15707,7 @@ if (typeof window !== 'undefined') { } // inject css -util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n.vis.timeline .currenttime {\n background-color: #FF7F6E;\n width: 2px;\n z-index: 9;\n}\n"); +util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.rangeoverflow {\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 0;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n.vis.timeline .currenttime {\n background-color: #FF7F6E;\n width: 2px;\n z-index: 9;\n}\n.vis.timeline .customtime {\n background-color: #6E94FF;\n width: 2px;\n cursor: move;\n z-index: 9; \n}\n"); },{"hammerjs":1,"moment":2}]},{},[3]) (3) diff --git a/vis.min.js b/vis.min.js index f1d67de1..e93e011c 100644 --- a/vis.min.js +++ b/vis.min.js @@ -22,8 +22,15299 @@ * License for the specific language governing permissions and limitations under * the License. */ -!function(t){"object"==typeof exports?module.exports=t():"function"==typeof define&&define.amd?define(t):"undefined"!=typeof window?window.vis=t():"undefined"!=typeof global?global.vis=t():"undefined"!=typeof self&&(self.vis=t())}(function(){var t;return function e(t,i,n){function s(r,a){if(!i[r]){if(!t[r]){var h="function"==typeof require&&require;if(!a&&h)return h(r,!0);if(o)return o(r,!0);throw new Error("Cannot find module '"+r+"'")}var d=i[r]={exports:{}};t[r][0].call(d.exports,function(e){var i=t[r][1][e];return s(i?i:e)},d,d.exports,e,t,i,n)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;r0&&e==s.EVENT_END?e=s.EVENT_MOVE:l||(e=s.EVENT_END),l||null===o?o=h:h=o,i.call(s.detection,n.collectEventData(t,e,h)),s.HAS_POINTEREVENTS&&e==s.EVENT_END&&(l=s.PointerEvent.updatePointer(e,h))),l||(o=null,r=!1,a=!1,s.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=s.HAS_POINTEREVENTS?s.PointerEvent.getEvents():s.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],s.EVENT_TYPES[s.EVENT_START]=t[0],s.EVENT_TYPES[s.EVENT_MOVE]=t[1],s.EVENT_TYPES[s.EVENT_END]=t[2]},getTouchList:function(t){return s.HAS_POINTEREVENTS?s.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var n=this.getTouchList(i,e),o=s.POINTER_TOUCH;return(i.type.match(/mouse/)||s.PointerEvent.matchType(s.POINTER_MOUSE,i))&&(o=s.POINTER_MOUSE),{center:s.utils.getCenter(n),timeStamp:(new Date).getTime(),target:i.target,touches:n,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return s.detection.stopDetect()}}}},s.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==s.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[s.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==s.POINTER_MOUSE,i[s.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==s.POINTER_TOUCH,i[s.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==s.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},s.utils={extend:function(t,e,n){for(var s in e)t[s]!==i&&n||(t[s]=e[s]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],n=0,s=t.length;s>n;n++)e.push(t[n].pageX),i.push(t[n].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,n=e.pageX-t.pageX;return 180*Math.atan2(i,n)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),n=Math.abs(t.pageY-e.pageY);return i>=n?t.pageX-e.pageX>0?s.DIRECTION_LEFT:s.DIRECTION_RIGHT:t.pageY-e.pageY>0?s.DIRECTION_UP:s.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,n=e.pageY-t.pageY;return Math.sqrt(i*i+n*n)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==s.DIRECTION_UP||t==s.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,n=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var s=0;si;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==s.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=s.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,n=t.touches.length;n>i;i++)e.touches.push(s.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=s.utils.getVelocity(o,r,a);return s.utils.extend(t,{deltaTime:o,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:s.utils.getDistance(e.center,t.center),angle:s.utils.getAngle(e.center,t.center),direction:s.utils.getDirection(e.center,t.center),scale:s.utils.getScale(e.touches,t.touches),rotation:s.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),s.utils.extend(s.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},s.gestures=s.gestures||{},s.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case s.EVENT_START:clearTimeout(this.timer),s.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==s.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case s.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case s.EVENT_END:clearTimeout(this.timer)}}},s.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==s.EVENT_END){var i=s.detection.previous,n=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},s.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,void 0;if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==s.POINTER_MOUSE?(t.stopDetect(),void 0):(e.options.prevent_default&&t.preventDefault(),t.eventType==s.EVENT_START&&e.trigger(this.name,t),void 0)}},s.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==s.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=s:(t.Hammer=s,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return s}))}(this)},{}],2:[function(e,i){(function(n){function s(t,e){return function(i){return p(t.call(this,i),e)}}function o(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function r(){}function a(t){T(t),d(this,t)}function h(t){var e=v(t),i=e.year||0,n=e.month||0,s=e.week||0,o=e.day||0,r=e.hour||0,a=e.minute||0,h=e.second||0,d=e.millisecond||0;this._input=t,this._milliseconds=+d+1e3*h+6e4*a+36e5*r,this._days=+o+7*s,this._months=+n+12*i,this._data={},this._bubble()}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function l(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e){for(var i=t+"";i.lengthn;n++)(i&&t[n]!==e[n]||!i&&w(t[n])!==w(e[n]))&&r++;return r+o}function g(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=ze[t]||He[e]||e}return t}function v(t){var e,i,n={};for(i in t)t.hasOwnProperty(i)&&(e=g(i),e&&(n[e]=t[i]));return n}function y(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}se[t]=function(s,o){var r,a,h=se.fn._lang[t],d=[];if("number"==typeof s&&(o=s,s=n),a=function(t){var e=se().utc().set(i,t);return h.call(se.fn._lang,e,s||"")},null!=o)return a(o);for(r=0;e>r;r++)d.push(a(r));return d}}function w(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function _(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function b(t){return E(t)?366:365}function E(t){return 0===t%4&&0!==t%100||0===t%400}function T(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[de]<0||t._a[de]>11?de:t._a[le]<1||t._a[le]>_(t._a[he],t._a[de])?le:t._a[pe]<0||t._a[pe]>23?pe:t._a[ce]<0||t._a[ce]>59?ce:t._a[ue]<0||t._a[ue]>59?ue:t._a[fe]<0||t._a[fe]>999?fe:-1,t._pf._overflowDayOfYear&&(he>e||e>le)&&(e=le),t._pf.overflow=e)}function x(t){t._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function S(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function D(t){return t?t.toLowerCase().replace("_","-"):t}function M(t,e){return e.abbr=t,me[t]||(me[t]=new r),me[t].set(e),me[t]}function C(t){delete me[t]}function O(t){var i,n,s,o,r=0,a=function(t){if(!me[t]&&ge)try{e("./lang/"+t)}catch(i){}return me[t]};if(!t)return se.fn._lang;if(!u(t)){if(n=a(t))return n;t=[t]}for(;r0;){if(n=a(o.slice(0,i).join("-")))return n;if(s&&s.length>=i&&m(o,s,!0)>=i-1)break;i--}r++}return se.fn._lang}function N(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function L(t){var e,i,n=t.match(_e);for(e=0,i=n.length;i>e;e++)n[e]=Ve[n[e]]?Ve[n[e]]:N(n[e]);return function(s){var o="";for(e=0;i>e;e++)o+=n[e]instanceof Function?n[e].call(s,t):n[e];return o}}function k(t,e){return t.isValid()?(e=I(e,t.lang()),Ue[e]||(Ue[e]=L(e)),Ue[e](t)):t.lang().invalidDate()}function I(t,e){function i(t){return e.longDateFormat(t)||t}var n=5;for(be.lastIndex=0;n>=0&&be.test(t);)t=t.replace(be,i),be.lastIndex=0,n-=1;return t}function A(t,e){var i;switch(t){case"DDDD":return xe;case"YYYY":case"GGGG":case"gggg":return Se;case"YYYYY":case"GGGGG":case"ggggg":return De;case"S":case"SS":case"SSS":case"DDD":return Te;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ce;case"a":case"A":return O(e._l)._meridiemParse;case"X":return Le;case"Z":case"ZZ":return Oe;case"T":return Ne;case"SSSS":return Me;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"ww":case"W":case"WW":case"e":case"E":return Ee;default:return i=new RegExp(j(U(t.replace("\\","")),"i"))}}function P(t){var e=(Oe.exec(t)||[])[0],i=(e+"").match(Fe)||["-",0,0],n=+(60*i[1])+w(i[2]);return"+"===i[0]?-n:n}function F(t,e,i){var n,s=i._a;switch(t){case"M":case"MM":null!=e&&(s[de]=w(e)-1);break;case"MMM":case"MMMM":n=O(i._l).monthsParse(e),null!=n?s[de]=n:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(s[le]=w(e));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=w(e));break;case"YY":s[he]=w(e)+(w(e)>68?1900:2e3);break;case"YYYY":case"YYYYY":s[he]=w(e);break;case"a":case"A":i._isPm=O(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":s[pe]=w(e);break;case"m":case"mm":s[ce]=w(e);break;case"s":case"ss":s[ue]=w(e);break;case"S":case"SS":case"SSS":case"SSSS":s[fe]=w(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=P(e);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":t=t.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=e)}}function Y(t){var e,i,n,s,o,r,a,h,d,l,p=[];if(!t._d){for(n=z(t),t._w&&null==t._a[le]&&null==t._a[de]&&(o=function(e){return e?e.length<3?parseInt(e,10)>68?"19"+e:"20"+e:e:null==t._a[he]?se().weekYear():t._a[he]},r=t._w,null!=r.GG||null!=r.W||null!=r.E?a=J(o(r.GG),r.W||1,r.E,4,1):(h=O(t._l),d=null!=r.d?X(r.d,h):null!=r.e?parseInt(r.e,10)+h._week.dow:0,l=parseInt(r.w,10)||1,null!=r.d&&db(s)&&(t._pf._overflowDayOfYear=!0),i=q(s,0,t._dayOfYear),t._a[de]=i.getUTCMonth(),t._a[le]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=p[e]=n[e];for(;7>e;e++)t._a[e]=p[e]=null==t._a[e]?2===e?1:0:t._a[e];p[pe]+=w((t._tzm||0)/60),p[ce]+=w((t._tzm||0)%60),t._d=(t._useUTC?q:B).apply(null,p)}}function R(t){var e;t._d||(e=v(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],Y(t))}function z(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function H(t){t._a=[],t._pf.empty=!0;var e,i,n,s,o,r=O(t._l),a=""+t._i,h=a.length,d=0;for(n=I(t._f,r).match(_e)||[],e=0;e0&&t._pf.unusedInput.push(o),a=a.slice(a.indexOf(i)+i.length),d+=i.length),Ve[s]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(s),F(s,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(s);t._pf.charsLeftOver=h-d,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[pe]<12&&(t._a[pe]+=12),t._isPm===!1&&12===t._a[pe]&&(t._a[pe]=0),Y(t),T(t)}function U(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,s){return e||i||n||s})}function j(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function W(t){var e,i,n,s,o;if(0===t._f.length)return t._pf.invalidFormat=!0,t._d=new Date(0/0),void 0;for(s=0;so)&&(n=o,i=e));d(t,i||e)}function V(t){var e,i=t._i,n=ke.exec(i);if(n){for(t._pf.iso=!0,e=4;e>0;e--)if(n[e]){t._f=Ae[e-1]+(n[6]||" ");break}for(e=0;4>e;e++)if(Pe[e][1].exec(i)){t._f+=Pe[e][0];break}Oe.exec(i)&&(t._f+="Z"),H(t)}else t._d=new Date(i)}function G(t){var e=t._i,i=ve.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?V(t):u(e)?(t._a=e.slice(0),Y(t)):f(e)?t._d=new Date(+e):"object"==typeof e?R(t):t._d=new Date(e)}function B(t,e,i,n,s,o,r){var a=new Date(t,e,i,n,s,o,r);return 1970>t&&a.setFullYear(t),a}function q(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function X(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function Z(t,e,i,n,s){return s.relativeTime(e||1,!!i,t,n)}function K(t,e,i){var n=ae(Math.abs(t)/1e3),s=ae(n/60),o=ae(s/60),r=ae(o/24),a=ae(r/365),h=45>n&&["s",n]||1===s&&["m"]||45>s&&["mm",s]||1===o&&["h"]||22>o&&["hh",o]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",ae(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,Z.apply({},h)}function $(t,e,i){var n,s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),n=se(t).add("d",o),{week:Math.ceil(n.dayOfYear()/7),year:n.year()}}function J(t,e,i,n,s){var o,r,a=new Date(Date.UTC(t,0)).getUTCDay();return i=null!=i?i:s,o=s-a+(a>n?7:0),r=7*(e-1)+(i-s)+o+1,{year:r>0?t:t-1,dayOfYear:r>0?r:b(t-1)+r}}function Q(t){var e=t._i,i=t._f;return"undefined"==typeof t._pf&&x(t),null===e?se.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=O().preparse(e)),se.isMoment(e)?(t=d({},e),t._d=new Date(+e._d)):i?u(i)?W(t):H(t):G(t),new a(t))}function te(t,e){se.fn[t]=se.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),se.updateOffset(this),this):this._d["get"+i+e]()}}function ee(t){se.duration.fn[t]=function(){return this._data[t]}}function ie(t,e){se.duration.fn["as"+t]=function(){return+this/e}}function ne(t){var e=!1,i=se;"undefined"==typeof ender&&(this.moment=t?function(){return!e&&console&&console.warn&&(e=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),i.apply(null,arguments)}:se)}for(var se,oe,re="2.4.0",ae=Math.round,he=0,de=1,le=2,pe=3,ce=4,ue=5,fe=6,me={},ge="undefined"!=typeof i&&i.exports,ve=/^\/?Date\((\-?\d+)/i,ye=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,we=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,_e=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,be=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Ee=/\d\d?/,Te=/\d{1,3}/,xe=/\d{3}/,Se=/\d{1,4}/,De=/[+\-]?\d{1,6}/,Me=/\d+/,Ce=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Oe=/Z|[\+\-]\d\d:?\d\d/i,Ne=/T/i,Le=/[\+\-]?\d+(\.\d{1,3})?/,ke=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d:?\d\d|Z)?)?$/,Ie="YYYY-MM-DDTHH:mm:ssZ",Ae=["YYYY-MM-DD","GGGG-[W]WW","GGGG-[W]WW-E","YYYY-DDD"],Pe=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Fe=/([\+\-]|\d\d)/gi,Ye="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Re={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ze={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},He={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Ue={},je="DDD w W M D d".split(" "),We="M D H h m s w W".split(" "),Ve={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return p(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return w(this.milliseconds()/100)},SS:function(){return p(w(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},SSSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(w(t/60),2)+":"+p(w(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(w(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}},Ge=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];je.length;)oe=je.pop(),Ve[oe+"o"]=o(Ve[oe],oe);for(;We.length;)oe=We.pop(),Ve[oe+oe]=s(Ve[oe],2);for(Ve.DDDD=s(Ve.DDD,3),d(r.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,n;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=se.utc([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(n.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,n;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=se([2e3,1]).day(e),n="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(n.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var s=this._relativeTime[i];return"function"==typeof s?s(t,e,i,n):s.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return $(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),se=function(t,e,i,s){return"boolean"==typeof i&&(s=i,i=n),Q({_i:t,_f:e,_l:i,_strict:s,_isUTC:!1})},se.utc=function(t,e,i,s){var o;return"boolean"==typeof i&&(s=i,i=n),o=Q({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e,_strict:s}).utc()},se.unix=function(t){return se(1e3*t)},se.duration=function(t,e){var i,n,s,o=se.isDuration(t),r="number"==typeof t,a=o?t._input:r?{}:t,d=null;return r?e?a[e]=t:a.milliseconds=t:(d=ye.exec(t))?(i="-"===d[1]?-1:1,a={y:0,d:w(d[le])*i,h:w(d[pe])*i,m:w(d[ce])*i,s:w(d[ue])*i,ms:w(d[fe])*i}):(d=we.exec(t))&&(i="-"===d[1]?-1:1,s=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},a={y:s(d[2]),M:s(d[3]),d:s(d[4]),h:s(d[5]),m:s(d[6]),s:s(d[7]),w:s(d[8])}),n=new h(a),o&&t.hasOwnProperty("_lang")&&(n._lang=t._lang),n},se.version=re,se.defaultFormat=Ie,se.updateOffset=function(){},se.lang=function(t,e){var i;return t?(e?M(D(t),e):null===e?(C(t),t="en"):me[t]||O(t),i=se.duration.fn._lang=se.fn._lang=O(t),i._abbr):se.fn._lang._abbr},se.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),O(t)},se.isMoment=function(t){return t instanceof a},se.isDuration=function(t){return t instanceof h},oe=Ge.length-1;oe>=0;--oe)y(Ge[oe]);for(se.normalizeUnits=function(t){return g(t)},se.invalid=function(t){var e=se.utc(0/0);return null!=t?d(e._pf,t):e._pf.userInvalidated=!0,e},se.parseZone=function(t){return se(t).parseZone()},d(se.fn=a.prototype,{clone:function(){return se(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return k(se(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return S(this)},isDSTShifted:function(){return this._a?this.isValid()&&m(this._a,(this._isUTC?se.utc(this._a):se(this._a)).toArray())>0:!1},parsingFlags:function(){return d({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=k(this,t||se.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?se.duration(+e,t):se.duration(t,e),c(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?se.duration(+e,t):se.duration(t,e),c(this,i,-1),this},diff:function(t,e,i){var n,s,o=this._isUTC?se(t).zone(this._offset||0):se(t).local(),r=6e4*(this.zone()-o.zone());return e=g(e),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),s=12*(this.year()-o.year())+(this.month()-o.month()),s+=(this-se(this).startOf("month")-(o-se(o).startOf("month")))/n,s-=6e4*(this.zone()-se(this).startOf("month").zone()-(o.zone()-se(o).startOf("month").zone()))/n,"year"===e&&(s/=12)):(n=this-o,s="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?(n-r)/864e5:"week"===e?(n-r)/6048e5:n),i?s:l(s) -},from:function(t,e){return se.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(se(),t)},calendar:function(){var t=this.diff(se().zone(this.zone()).startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){return E(this.year())},isDST:function(){return this.zone()+se(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+se(t).startOf(e)},isSame:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)===+se(t).startOf(e)},min:function(t){return t=se.apply(null,arguments),this>t?this:t},max:function(t){return t=se.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=P(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&c(this,se.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?se(t).zone():0,0===(this.zone()-t)%60},daysInMonth:function(){return _(this.year(),this.month())},dayOfYear:function(t){var e=ae((se(this).startOf("day")-se(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=$(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=$(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=$(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},get:function(t){return t=g(t),this[t]()},set:function(t,e){return t=g(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===n?this._lang:(this._lang=O(t),this)}}),oe=0;oei;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,s;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=new Array(r),s=0;r>s;){var a,h;s in o&&(a=o[s],h=t.call(i,a,s,o),n[s]=h),s++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],s=arguments[1],o=0;i>o;o++)if(o in e){var r=e[o];t.call(s,r,o,e)&&n.push(r)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(s){if("object"!=typeof s&&"function"!=typeof s||null===s)throw new TypeError("Object.keys called on non-object");var o=[];for(var r in s)t.call(s,r)&&o.push(r);if(e)for(var a=0;n>a;a++)t.call(s,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s});var N={};N.isNumber=function(t){return t instanceof Number||"number"==typeof t},N.isString=function(t){return t instanceof String||"string"==typeof t},N.isDate=function(t){if(t instanceof Date)return!0;if(N.isString(t)){var e=L.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},N.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},N.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},N.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var s in n)n.hasOwnProperty(s)&&void 0!==n[s]&&(t[s]=n[s])}return t},N.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return String(t);case"Date":if(N.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(M.isMoment(t))return new Date(t.valueOf());if(N.isString(t))return i=L.exec(t),i?new Date(Number(i[1])):M(t).toDate();throw new Error("Cannot convert object of type "+N.getType(t)+" to type Date");case"Moment":if(N.isNumber(t))return M(t);if(t instanceof Date)return M(t.valueOf());if(M.isMoment(t))return M(t);if(N.isString(t))return i=L.exec(t),i?M(Number(i[1])):M(t);throw new Error("Cannot convert object of type "+N.getType(t)+" to type Date");case"ISODate":if(N.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(M.isMoment(t))return t.toDate().toISOString();if(N.isString(t))return i=L.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+N.getType(t)+" to type ISODate");case"ASPDate":if(N.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(N.isString(t)){i=L.exec(t);var n;return n=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+n+")/"}throw new Error("Cannot convert object of type "+N.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+N.getType(t)+' to type "'+e+'"')}};var L=/^\/?Date\((\-?\d+)/i;N.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},N.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetLeft,n-=s.scrollLeft,s=s.offsetParent;return n},N.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetTop,n-=s.scrollTop,s=s.offsetParent;return n},N.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,n=document.body;return e+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)},N.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,n=document.body;return e+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0)},N.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},N.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},N.forEach=function(t,e){var i,n;if(t instanceof Array)for(i=0,n=t.length;n>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},N.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},N.addEventListener=function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},N.removeEventListener=function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},N.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},N.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},N.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},N.option={},N.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},N.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},N.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},N.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),N.isString(t)?t:N.isNumber(t)?t+"px":e||null},N.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},N.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}};var k={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var s=e[i];if(s&&s.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];s||(s={object:t,events:{}},this.listeners.push(s));var o=s.events[e];o||(o=[],s.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];o&&(n=o.indexOf(i),-1!=n&&o.splice(n,1),0==o.length&&delete s.events[e]);var r=0,a=s.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];if(o)for(var r=0,a=o.length;a>r;r++)o[r](i)}}};s.prototype.on=function(t,e,i){var n=t instanceof RegExp?t:new RegExp(t.replace("*","\\w+")),s={id:N.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(s),s.id},s.prototype.off=function(t){for(var e=0;eo;o++)i=s._addItem(t[o]),n.push(i);else if(N.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},p=0,c=a.length;c>p;p++){var u=a[p];l[u]=t.getValue(h,p)}i=s._addItem(l),n.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=s._addItem(t),n.push(i)}return n.length&&this._trigger("add",{items:n},e),n},o.prototype.update=function(t,e){var i=[],n=[],s=this,o=s.fieldId,r=function(t){var e=t[o];s.data[e]?(e=s._updateItem(t),n.push(e)):(e=s._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(N.isDataTable(t))for(var d=this._getColumnNames(t),l=0,p=t.getNumberOfRows();p>l;l++){for(var c={},u=0,f=d.length;f>u;u++){var m=d[u];c[m]=t.getValue(l,u)}r(c)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");r(t)}return i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e),i.concat(n)},o.prototype.get=function(){var t,e,i,n,s=this,o=N.getType(arguments[0]);"String"==o||"Number"==o?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==o?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",n&&r!=N.getType(n))throw new Error('Type of parameter "data" ('+N.getType(n)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!N.isDataTable(n))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=n?"DataTable"==N.getType(n)?"DataTable":"Array":"Array";var a,h,d,l,p=i&&i.convert||this.options.convert,c=i&&i.filter,u=[];if(void 0!=t)a=s._getItem(t,p),c&&!c(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=s._getItem(e[d],p),(!c||c(a))&&u.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=s._getItem(h,p),(!c||c(a))&&u.push(a));if(i&&i.order&&void 0==t&&this._sort(u,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(d=0,l=u.length;l>d;d++)u[d]=this._filterFields(u[d],f)}if("DataTable"==r){var m=this._getColumnNames(n);if(void 0!=t)s._appendRow(n,m,a);else for(d=0,l=u.length;l>d;d++)s._appendRow(n,m,u[d]);return n}if(void 0!=t)return a;if(n){for(d=0,l=u.length;l>d;d++)n.push(u[d]);return n}return u},o.prototype.getIds=function(t){var e,i,n,s,o,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,l=[];if(a)if(h){o=[];for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&o.push(s));for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&l.push(s[this.fieldId]));else if(h){o=[];for(n in r)r.hasOwnProperty(n)&&o.push(r[n]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=r[n],l.push(s[this.fieldId]));return l},o.prototype.forEach=function(t,e){var i,n,s=e&&e.filter,o=e&&e.convert||this.options.convert,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in r)r.hasOwnProperty(n)&&(i=this._getItem(n,o),(!s||s(i))&&t(i,n))},o.prototype.map=function(t,e){var i,n=e&&e.filter,s=e&&e.convert||this.options.convert,o=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,s),(!n||n(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},o.prototype._filterFields=function(t,e){var i={};for(var n in t)t.hasOwnProperty(n)&&-1!=e.indexOf(n)&&(i[n]=t[n]);return i},o.prototype._sort=function(t,e){if(N.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],s=e[i];return n>s?1:s>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},o.prototype.remove=function(t,e){var i,n,s,o=[];if(t instanceof Array)for(i=0,n=t.length;n>i;i++)s=this._remove(t[i]),null!=s&&o.push(s);else s=this._remove(t),null!=s&&o.push(s);return o.length&&this._trigger("remove",{items:o},e),o},o.prototype._remove=function(t){if(N.isNumber(t)||N.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},o.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},o.prototype.max=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||r>n)&&(i=o,n=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||n>r)&&(i=o,n=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.convert[t],s=0;for(var o in e)if(e.hasOwnProperty(o)){for(var r=e[o],a=N.convert(r[t],n),h=!1,d=0;s>d;d++)if(i[d]==a){h=!0;break}h||(i[s]=a,s++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw new Error("Cannot add item: item with id "+e+" already exists")}else e=N.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=N.convert(t[n],s)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,n,s=this.data[t];if(!s)return null;var o={},r=this.fieldId,a=this.internalIds;if(e)for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a||(o[i]=N.convert(n,e[i])));else for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a||(o[i]=n));return o},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw new Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw new Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=N.convert(t[n],s)}return e},o.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),s=0,o=e.length;o>s;s++){var r=e[s];t.setValue(n,s,i[r])}},r.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var s in this.ids)this.ids.hasOwnProperty(s)&&e.push(s);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,n=e.length;n>i;i++)s=e[i],this.ids[s]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},r.prototype.get=function(){var t,e,i,n=this,s=N.getType(arguments[0]);"String"==s||"Number"==s||"Array"==s?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=N.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return n.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this.data&&this.data.get.apply(this.data,r)},r.prototype.getIds=function(t){var e;if(this.data){var i,n=this.options.filter;i=t&&t.filter?n?function(e){return n(e)&&t.filter(e)}:t.filter:n,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},r.prototype._onEvent=function(t,e,i){var n,s,o,r,a=e&&e.items,h=this.data,d=[],l=[],p=[];if(a&&h){switch(t){case"add":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r&&(this.ids[o]=!0,d.push(o));break;case"update":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r?this.ids[o]?l.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],p.push(o));break;case"remove":for(n=0,s=a.length;s>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],p.push(o))}d.length&&this._trigger("add",{items:d},i),l.length&&this._trigger("update",{items:l},i),p.length&&this._trigger("remove",{items:p},i) -}},r.prototype.subscribe=o.prototype.subscribe,r.prototype.unsubscribe=o.prototype.unsubscribe,r.prototype._trigger=o.prototype._trigger,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){t instanceof Date&&e instanceof Date&&(this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i))},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step)}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(this.current.getMonth()<6)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+60*1e3*this.step);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+60*60*1e3*this.step);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,n=864e5,s=36e5,o=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return M(t).format("SSS");case TimeStep.SCALE.SECOND:return M(t).format("s");case TimeStep.SCALE.MINUTE:return M(t).format("HH:mm");case TimeStep.SCALE.HOUR:return M(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return M(t).format("ddd D");case TimeStep.SCALE.DAY:return M(t).format("D");case TimeStep.SCALE.MONTH:return M(t).format("MMM");case TimeStep.SCALE.YEAR:return M(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return M(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return M(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return M(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return M(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return M(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){N.extend(this.options,t)},a.prototype.update=function(){this._order(),this._stack()},a.prototype._order=function(){var t=this.parent.items;if(!t)throw new Error("Cannot stack items: parent does not contain items");var e=[],i=0;N.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw new Error("Option order must be a function");e.sort(n),this.ordered=e},a.prototype._stack=function(){var t,e,i,n=this.ordered,s=this.options,o=s.orientation||this.defaultOptions.orientation,r="top"==o;for(i=s.margin&&void 0!==s.margin.item?s.margin.item:this.defaultOptions.margin.item,t=0,e=n.length;e>t;t++){var a=n[t],h=null;do h=this.checkOverlap(n,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},a.prototype.checkOverlap=function(t,e,i,n,s){for(var o=this.collision,r=t[e],a=n;a>=i;a--){var h=t[a];if(o(r,h,s)&&a!=e)return h}return null},a.prototype.collision=function(t,e,i){var n,s;return n=void 0!==t.props.content&&t.widthe.left&&t.top-ie.top},h.prototype.setOptions=function(t){N.extend(this.options,t),(null!=t.start||null!=t.end)&&this.setRange(t.start,t.end)},h.prototype.subscribe=function(t,e,i){var n,s=this;if("horizontal"!=i&&"vertical"!=i)throw new TypeError('Unknown direction "'+i+'". '+'Choose "horizontal" or "vertical".');if("move"==e)n={component:t,event:e,direction:i,callback:function(t){s._onMouseDown(t,n)},params:{}},t.on("mousedown",n.callback),s.listeners.push(n);else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');n={component:t,event:e,direction:i,callback:function(t){s._onMouseWheel(t,n)},params:{}},t.on("mousewheel",n.callback),s.listeners.push(n)}},h.prototype.on=function(t,e){k.addListener(this,t,e)},h.prototype._trigger=function(t){k.trigger(this,t,{start:this.start,end:this.end})},h.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},h.prototype._applyRange=function(t,e){var i,n=null!=t?N.convert(t,"Number"):this.start,s=null!=e?N.convert(e,"Number"):this.end;if(isNaN(n))throw new Error('Invalid start "'+t+'"');if(isNaN(s))throw new Error('Invalid end "'+e+'"');if(n>s&&(s=n),null!=this.options.min){var o=this.options.min.valueOf();o>n&&(i=o-n,n+=i,s+=i)}if(null!=this.options.max){var r=this.options.max.valueOf();s>r&&(i=s-r,n-=i,s-=i)}if(null!=this.options.zoomMin){var a=this.options.zoomMin.valueOf();0>a&&(a=0),a>s-n&&(this.end-this.start>a?(i=a-(s-n),n-=i/2,s+=i/2):(n=this.start,s=this.end))}if(null!=this.options.zoomMax){var h=this.options.zoomMax.valueOf();0>h&&(h=0),s-n>h&&(this.end-this.start=1&&(i.moved=!0);var h=i.end-i.start,d="horizontal"==e.direction?e.component.width:e.component.height,l=-a/d*h;this._applyRange(i.start+l,i.end+l),this._trigger("rangechange"),N.preventDefault(t)},h.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(N.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(N.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},h.prototype._onMouseWheel=function(t,e){t=t||window.event;var i=0;if(t.wheelDelta?i=t.wheelDelta/120:t.detail&&(i=-t.detail/3),i){var n=this,s=function(){var s=i/5,o=null,r=e.component.frame;if(r){var a,h;if("horizontal"==e.direction){a=e.component.width,h=n.conversion(a);var d=N.getAbsoluteLeft(r),l=N.getPageX(t);o=(l-d)/h.factor+h.offset}else{a=e.component.height,h=n.conversion(a);var p=N.getAbsoluteTop(r),c=N.getPageY(t);o=(p+a-c-p)/h.factor+h.offset}}n.zoom(s,o)};s()}N.preventDefault(t)},h.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2),t>=1&&(t=.9),-1>=t&&(t=-.9),0>t&&(t/=1+t);var i=this.start-e,n=this.end-e,s=this.start-i*t,o=this.end-n*t;this.setRange(s,o)},h.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},h.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,n=this.start-i,s=this.end-i;this.setRange(n,s)},d.prototype.add=function(t){if(void 0==t.id)throw new Error("Component has no field id");if(!(t instanceof l||t instanceof d))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},d.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},d.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},d.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},d.prototype.repaint=function A(){function A(i,n){n in e||(i.depends&&i.depends.forEach(function(t){A(t,t.id)}),i.parent&&A(i.parent,i.parent.id),t=i.repaint()||t,e[n]=!0)}var t=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var e={};N.forEach(this.components,A),t&&this.reflow()},d.prototype.reflow=function P(){function P(i,n){n in e||(i.depends&&i.depends.forEach(function(t){P(t,t.id)}),i.parent&&P(i.parent,i.parent.id),t=i.reflow()||t,e[n]=!0)}var t=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var e={};N.forEach(this.components,P),t&&this.repaint()},l.prototype.setOptions=function(t){t&&(N.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},l.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},l.prototype.getContainer=function(){return null},l.prototype.getFrame=function(){return this.frame},l.prototype.repaint=function(){return!1},l.prototype.reflow=function(){return!1},l.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},l.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},l.prototype.requestRepaint=function(){if(!this.controller)throw new Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},l.prototype.requestReflow=function(){if(!this.controller)throw new Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new l,p.prototype.setOptions=l.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=N.updateProperty,i=N.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="panel";var o=n.className;o&&("function"==typeof o?N.addClassName(s,String(o())):N.addClassName(s,String(o))),this.frame=s,t+=1}if(!s.parentNode){if(!this.parent)throw new Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw new Error("Cannot repaint panel: parent has no container element");r.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=N.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},c.prototype=new p,c.prototype.setOptions=l.prototype.setOptions,c.prototype.repaint=function(){var t=0,e=N.updateProperty,i=N.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="vis timeline rootpanel";var o=n.className;o&&N.addClassName(s,N.option.asString(o)),this.frame=s,t+=1}if(!s.parentNode){if(!this.container)throw new Error("Cannot repaint root panel: no container attached");this.container.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},c.prototype.reflow=function(){var t=0,e=N.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},c.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},c.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};N.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},c.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},c.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},c.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;N.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var s=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=s,N.addEventListener(n,i,s)}}})}},u.prototype=new l,u.prototype.setOptions=l.prototype.setOptions,u.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},u.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},u.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},u.prototype.repaint=function(){var t=0,e=N.updateProperty,i=N.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis "+s,!a.parentNode){if(!this.parent)throw new Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw new Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var l=a.nextSibling;d.removeChild(a);var p="bottom"==s&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,p)),t+=e(a.style,"left",i(n.left,"0px")),t+=e(a.style,"width",i(n.width,"100%")),t+=e(a.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var c=void 0,u=0;r.hasNext()&&1e3>u;){u++;var f=r.getCurrent(),m=this.toScreen(f),g=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,r.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==c&&(c=m),this._repaintMajorText(m,r.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),w=y.length*(o.majorCharWidth||10)+10;(void 0==c||c>w)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),l?d.insertBefore(a,l):d.appendChild(a)}return t>0},u.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},u.prototype._repaintEnd=function(){N.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},u.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var n=document.createTextNode("");i=document.createElement("div"),i.appendChild(n),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},u.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var n=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(n),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},u.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},u.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},u.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},u.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},u.prototype.reflow=function(){var t=0,e=N.updateProperty,i=this.frame,n=this.range;if(!n)throw new Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var s=this.props,o=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(s.minorCharHeight=a.clientHeight,s.minorCharWidth=a.clientWidth),h&&(s.majorCharHeight=h.clientHeight,s.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=s.parentHeight&&(s.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.minorLabelTop=0,s.majorLabelTop=s.minorLabelTop+s.minorLabelHeight,s.minorLineTop=-this.top,s.minorLineHeight=Math.max(this.top+s.majorLabelHeight,0),s.minorLineWidth=1,s.majorLineTop=-this.top,s.majorLineHeight=Math.max(this.top+s.minorLabelHeight+s.majorLabelHeight,0),s.majorLineWidth=1,s.lineTop=0;break;case"top":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.majorLabelTop=0,s.minorLabelTop=s.majorLabelTop+s.majorLabelHeight,s.minorLineTop=s.minorLabelTop,s.minorLineHeight=Math.max(d-s.majorLabelHeight-this.top),s.minorLineWidth=1,s.majorLineTop=0,s.majorLineHeight=Math.max(d-this.top),s.majorLineWidth=1,s.lineTop=s.majorLabelHeight+s.minorLabelHeight;break;default:throw new Error('Unkown orientation "'+this.getOption("orientation")+'"')}var l=s.minorLabelHeight+s.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",l),this._updateConversion();var p=N.convert(n.start,"Date"),c=N.convert(n.end,"Date"),u=this.toTime(5*(s.minorCharWidth||10))-this.toTime(0);this.step=new TimeStep(p,c,u),t+=e(s.range,"start",p.valueOf()),t+=e(s.range,"end",c.valueOf()),t+=e(s.range,"minimumStep",u.valueOf())}return t>0},u.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},f.prototype=new l,f.prototype.setOptions=l.prototype.setOptions,f.prototype.getContainer=function(){return this.frame},f.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCurrentTime"))return t&&(i.removeChild(t),delete this.frame),void 0;t||(t=document.createElement("div"),t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t),this.frame=t),e.conversion||e._updateConversion();var n=new Date,s=e.toScreen(n);t.style.left=s+"px",t.title="Current time: "+n,void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer);var o=this,r=1/e.conversion.factor/2;return 30>r&&(r=30),this.currentTimeTimer=setTimeout(function(){o.repaint()},r),!1},m.prototype=new p,m.types={box:v,range:w,point:y},m.prototype.setOptions=l.prototype.setOptions,m.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},m.prototype.repaint=function(){var t=0,e=N.updateProperty,i=N.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=n.className;a&&N.addClassName(r,N.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var l=document.createElement("div");l.className="itemset-axis",this.dom.axis=l,this.frame=r,t+=1}if(!this.parent)throw new Error("Cannot repaint itemset: no parent attached");var p=this.parent.getContainer();if(!p)throw new Error("Cannot repaint itemset: parent has no container element");r.parentNode||(p.appendChild(r),t+=1),this.dom.axis.parentNode||(p.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(n.left,"0px")),t+=e(this.dom.axis.style,"width",i(n.width,"100%")),t+="bottom"==s?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var c=this,u=this.queue,f=this.itemsData,g=this.items,v={};return Object.keys(u).forEach(function(e){var i=u[e],s=g[e];switch(i){case"add":case"update":var r=f&&f.get(e,v);if(r){var a=r.type||r.start&&r.end&&"range"||n.type||"box",h=m.types[a];if(s&&(h&&s instanceof h?(s.data=r,t++):(t+=s.hide(),s=null)),!s){if(!h)throw new TypeError('Unknown item type "'+a+'"');s=new h(c,r,n,o),t++}s.repaint(),g[e]=s}delete u[e];break;case"remove":s&&(t+=s.hide()),delete g[e],delete u[e];break;default:console.log('Error: unknown action "'+i+'"')}}),N.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},m.prototype.getForeground=function(){return this.dom.foreground},m.prototype.getBackground=function(){return this.dom.background},m.prototype.getAxis=function(){return this.dom.axis},m.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,n=e.margin&&e.margin.item||this.defaultOptions.margin.item,s=N.updateProperty,o=N.option.asNumber,r=N.option.asSize,a=this.frame;if(a){this._updateConversion(),N.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=o(e.maxHeight),l=null!=r(e.height);if(l)h=a.offsetHeight;else{var p=this.stack.ordered;if(p.length){var c=p[0].top,u=p[0].top+p[0].height;N.forEach(p,function(t){c=Math.min(c,t.top),u=Math.max(u,t.top+t.height)}),h=u-c+i+n}else h=i+n}null!=d&&(h=Math.min(h,d)),t+=s(this,"height",h),t+=s(this,"top",a.offsetTop),t+=s(this,"left",a.offsetLeft),t+=s(this,"width",a.offsetWidth)}else t+=1;return t>0},m.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t},m.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(t){if(!(t instanceof o||t instanceof r))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(n&&(N.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),this.itemsData){var s=this.id;N.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},m.prototype.getItems=function(){return this.itemsData},m.prototype._onUpdate=function(t){this._toQueue("update",t)},m.prototype._onAdd=function(t){this._toQueue("add",t)},m.prototype._onRemove=function(t){this._toQueue("remove",t)},m.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint() -},m.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},m.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},m.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},g.prototype.select=function(){this.selected=!0},g.prototype.unselect=function(){this.selected=!1},g.prototype.show=function(){return!1},g.prototype.hide=function(){return!1},g.prototype.repaint=function(){return!1},g.prototype.reflow=function(){return!1},v.prototype=new g(null,null),v.prototype.select=function(){this.selected=!0},v.prototype.unselect=function(){this.selected=!1},v.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");var n=this.parent.getBackground();if(!n)throw new Error("Cannot repaint time axis: parent has no background container element");var s=this.parent.getAxis();if(!n)throw new Error("Cannot repaint time axis: parent has no axis container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(n.appendChild(e.line),t=!0),e.dot.parentNode||(s.appendChild(e.dot),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},v.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},v.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},v.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l,p,c=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(l=this.data,p=this.parent&&this.parent.range,l&&p){var u=p.end-p.start;this.visible=l.start>p.start-u&&l.start0},v.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot")},v.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,s=t.line,o=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",s.style.left=e.line.left+"px","top"==i?(s.style.top="0px",s.style.height=this.top+"px"):(s.style.top=this.top+this.height+"px",s.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),o.style.left=e.dot.left+"px",o.style.top=e.dot.top+"px"}},y.prototype=new g(null,null),y.prototype.select=function(){this.selected=!0},y.prototype.unselect=function(){this.selected=!1},y.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}return t},y.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},y.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},y.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var p=d.end-d.start;this.visible=h.start>d.start-p&&h.start0},y.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot))},y.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},w.prototype=new g(null,null),w.prototype.select=function(){this.selected=!0},w.prototype.unselect=function(){this.selected=!1},w.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},w.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},w.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},w.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l,p,c,u,f,m,g=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw new Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,r=o.toScreen(this.data.start),a=o.toScreen(this.data.end),l=N.updateProperty,p=t.box,c=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,s=i.padding||this.defaultOptions.padding,g+=l(e.content,"width",t.content.offsetWidth),g+=l(this,"height",p.offsetHeight),-c>r&&(r=-c),a>2*c&&(a=2*c),u=0>r?Math.min(-r,a-r-e.content.width-2*s):0,g+=l(e.content,"left",u),"top"==f?(m=n,g+=l(this,"top",m)):(m=o.height-this.height-n,g+=l(this,"top",m)),g+=l(this,"left",r),g+=l(this,"width",Math.max(a-r,1))):g+=1),g>0},w.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content))},w.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},_.prototype=new l,_.prototype.setOptions=l.prototype.setOptions,_.prototype.getContainer=function(){return this.parent.getContainer()},_.prototype.setItems=function(t){if(this.itemset&&(this.itemset.hide(),this.itemset.setItems(),this.parent.controller.remove(this.itemset),this.itemset=null),t){var e=this.groupId,i=Object.create(this.options);this.itemset=new m(this,null,i),this.itemset.setRange(this.parent.range),this.view=new r(t,{filter:function(t){return t.group==e}}),this.itemset.setItems(this.view),this.parent.controller.add(this.itemset)}},_.prototype.repaint=function(){return!1},_.prototype.reflow=function(){var t=0,e=N.updateProperty;if(t+=e(this,"top",this.itemset?this.itemset.top:0),t+=e(this,"height",this.itemset?this.itemset.height:0),this.label){var i=this.label.firstChild;t+=e(this.props.label,"width",i.clientWidth),t+=e(this.props.label,"height",i.clientHeight)}else t+=e(this.props.label,"width",0),t+=e(this.props.label,"height",0);return t>0},b.prototype=new p,b.prototype.setOptions=l.prototype.setOptions,b.prototype.setRange=function(){},b.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},b.prototype.getItems=function(){return this.itemsData},b.prototype.setRange=function(t){this.range=t},b.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(N.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof o?this.groupsData=t:(this.groupsData=new o({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;N.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},b.prototype.getGroups=function(){return this.groupsData},b.prototype.repaint=function(){var t,e,i,n,s=0,o=N.updateProperty,r=N.option.asSize,a=N.option.asElement,h=this.options,d=this.dom.frame,l=this.dom.labels;if(!this.parent)throw new Error("Cannot repaint groupset: no parent attached");var p=this.parent.getContainer();if(!p)throw new Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",this.dom.frame=d;var c=h.className;c&&N.addClassName(d,N.option.asString(c)),s+=1}d.parentNode||(p.appendChild(d),s+=1);var u=a(h.labelContainer);if(!u)throw new Error('Cannot repaint groupset: option "labelContainer" not defined');l||(l=document.createElement("div"),l.className="labels",this.dom.labels=l),l.parentNode&&l.parentNode==u||(l.parentNode&&l.parentNode.removeChild(l.parentNode),u.appendChild(l)),s+=o(d.style,"height",r(h.height,this.height+"px")),s+=o(d.style,"top",r(h.top,"0px")),s+=o(d.style,"left",r(h.left,"0px")),s+=o(d.style,"width",r(h.width,"100%")),s+=o(l.style,"top",r(h.top,"0px"));var f=this,m=this.queue,g=this.groups,v=this.groupsData,y=Object.keys(m);if(y.length){y.forEach(function(t){var e=m[t],i=g[t];switch(e){case"add":case"update":if(!i){var n=Object.create(f.options);i=new _(f,t,n),i.setItems(f.itemsData),g[t]=i,f.controller.add(i)}i.data=v.get(t),delete m[t];break;case"remove":i&&(i.setItems(),delete g[t],f.controller.remove(i)),delete m[t];break;default:console.log('Error: unknown action "'+e+'"')}});var w=this.groupsData.getIds({order:this.options.groupsOrder});for(t=0;t0},b.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="label";var n=document.createElement("div");n.className="inner",i.appendChild(n);var s=e.data&&e.data.content;s instanceof Element?n.appendChild(s):void 0!=s&&(n.innerHTML=s);var o=e.data&&e.data.className;return o&&N.addClassName(i,o),e.label=i,i},b.prototype.getContainer=function(){return this.dom.frame},b.prototype.getLabelsWidth=function(){return this.props.labels.width},b.prototype.reflow=function(){var t,e,i=0,n=this.options,s=N.updateProperty,o=N.option.asNumber,r=N.option.asSize,a=this.dom.frame;if(a){var h,d=o(n.maxHeight),l=null!=r(n.height);if(l)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=s(this,"height",h),i+=s(this,"top",a.offsetTop),i+=s(this,"left",a.offsetLeft),i+=s(this,"width",a.offsetWidth)}var p=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var c=e.props&&e.props.label&&e.props.label.width||0;p=Math.max(p,c)}return i+=s(this.props.labels,"width",p),i>0},b.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},b.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},b.prototype._onUpdate=function(t){this._toQueue(t,"update")},b.prototype._onAdd=function(t){this._toQueue(t,"add")},b.prototype._onRemove=function(t){this._toQueue(t,"remove")},b.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},E.prototype.setOptions=function(t){t&&N.extend(this.options,t),this.controller.reflow(),this.controller.repaint()},E.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof o&&(e=t):e=null,t instanceof o||(e=new o({convert:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var n=this.getItemRange(),s=n.min,r=n.max;if(null!=s&&null!=r){var a=r.valueOf()-s.valueOf();0>=a&&(a=864e5),s=new Date(s.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(s=new Date(this.options.start.valueOf())),void 0!=this.options.end&&(r=new Date(this.options.end.valueOf())),(null!=s||null!=r)&&this.range.setRange(s,r)}},E.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?b:m;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var n=Object.create(this.options);N.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.itemPanel.height-e.timeaxis.height-e.content.height},left:null,width:"100%",height:function(){return e.options.height?e.itemPanel.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!N.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null},labelContainer:function(){return e.labelPanel.getContainer()}}),this.content=new i(this.itemPanel,[this.timeaxis],n),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},E.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var n=t.min("start");e=n?n.start.valueOf():null;var s=t.max("start");s&&(i=s.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},function(t){function e(t){return D=t,c()}function i(){M=0,C=D.charAt(0)}function n(){M++,C=D.charAt(M)}function s(){return D.charAt(M+1)}function o(t){return L.test(t)}function r(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t,e,i){for(var n=e.split("."),s=t;n.length;){var o=n.shift();n.length?(s[o]||(s[o]={}),s=s[o]):s[o]=i}}function h(t,e){for(var i,n,s=null,o=[t],a=t;a.parent;)o.push(a.parent),a=a.parent;if(a.nodes)for(i=0,n=a.nodes.length;n>i;i++)if(e.id===a.nodes[i].id){s=a.nodes[i];break}for(s||(s={id:e.id},t.node&&(s.attr=r(s.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(s)&&h.nodes.push(s)}e.attr&&(s.attr=r(s.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function l(t,e,i,n,s){var o={from:e,to:i,type:n};return t.edge&&(o.attr=r({},t.edge)),o.attr=r(o.attr||{},s),o}function p(){for(N=x.NULL,O="";" "==C||" "==C||"\n"==C||"\r"==C;)n();do{var t=!1;if("#"==C){for(var e=M-1;" "==D.charAt(e)||" "==D.charAt(e);)e--;if("\n"==D.charAt(e)||""==D.charAt(e)){for(;""!=C&&"\n"!=C;)n();t=!0}}if("/"==C&&"/"==s()){for(;""!=C&&"\n"!=C;)n();t=!0}if("/"==C&&"*"==s()){for(;""!=C;){if("*"==C&&"/"==s()){n(),n();break}n()}t=!0}for(;" "==C||" "==C||"\n"==C||"\r"==C;)n()}while(t);if(""==C)return N=x.DELIMITER,void 0;var i=C+s();if(S[i])return N=x.DELIMITER,O=i,n(),n(),void 0;if(S[C])return N=x.DELIMITER,O=C,n(),void 0;if(o(C)||"-"==C){for(O+=C,n();o(C);)O+=C,n();return"false"==O?O=!1:"true"==O?O=!0:isNaN(Number(O))||(O=Number(O)),N=x.IDENTIFIER,void 0}if('"'==C){for(n();""!=C&&('"'!=C||'"'==C&&'"'==s());)O+=C,'"'==C&&n(),n();if('"'!=C)throw _('End of string " expected');return n(),N=x.IDENTIFIER,void 0}for(N=x.UNKNOWN;""!=C;)O+=C,n();throw new SyntaxError('Syntax error in part "'+b(O,30)+'"')}function c(){var t={};if(i(),p(),"strict"==O&&(t.strict=!0,p()),("graph"==O||"digraph"==O)&&(t.type=O,p()),N==x.IDENTIFIER&&(t.id=O,p()),"{"!=O)throw _("Angle bracket { expected");if(p(),u(t),"}"!=O)throw _("Angle bracket } expected");if(p(),""!==O)throw _("End of file expected");return p(),delete t.node,delete t.edge,delete t.graph,t}function u(t){for(;""!==O&&"}"!=O;)f(t),";"==O&&p()}function f(t){var e=m(t);if(e)return y(t,e),void 0;var i=g(t);if(!i){if(N!=x.IDENTIFIER)throw _("Identifier expected");var n=O;if(p(),"="==O){if(p(),N!=x.IDENTIFIER)throw _("Identifier expected");t[n]=O,p()}else v(t,n)}}function m(t){var e=null;if("subgraph"==O&&(e={},e.type="subgraph",p(),N==x.IDENTIFIER&&(e.id=O,p())),"{"==O){if(p(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,u(e),"}"!=O)throw _("Angle bracket } expected");p(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function g(t){return"node"==O?(p(),t.node=w(),"node"):"edge"==O?(p(),t.edge=w(),"edge"):"graph"==O?(p(),t.graph=w(),"graph"):null}function v(t,e){var i={id:e},n=w();n&&(i.attr=n),h(t,i),y(t,e)}function y(t,e){for(;"->"==O||"--"==O;){var i,n=O;p();var s=m(t);if(s)i=s;else{if(N!=x.IDENTIFIER)throw _("Identifier or subgraph expected");i=O,h(t,{id:i}),p()}var o=w(),r=l(t,e,i,n,o);d(t,r),e=i}}function w(){for(var t=null;"["==O;){for(p(),t={};""!==O&&"]"!=O;){if(N!=x.IDENTIFIER)throw _("Attribute name expected");var e=O;if(p(),"="!=O)throw _("Equal sign = expected");if(p(),N!=x.IDENTIFIER)throw _("Attribute value expected");var i=O;a(t,e,i),p(),","==O&&p()}if("]"!=O)throw _("Bracket ] expected");p()}return t}function _(t){return new SyntaxError(t+', got "'+b(O,30)+'" (char '+M+")")}function b(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function E(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function T(t){function i(t){var e={from:t.from,to:t.to};return r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var n=e(t),s={nodes:[],edges:[],options:{}};return n.nodes&&n.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};r(e,t.attr),e.image&&(e.shape="image"),s.nodes.push(e)}),n.edges&&n.edges.forEach(function(t){var e,n;e=t.from instanceof Object?t.from.nodes:{id:t.from},n=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);s.edges.push(e)}),E(e,n,function(e,n){var o=l(s,e.id,n.id,t.type,t.attr),r=i(o);s.edges.push(r)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);s.edges.push(e)})}),n.attr&&(s.options=n.attr),s}var x={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},S={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},D="",M=0,C="",O="",N=x.NULL,L=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=T}("undefined"!=typeof N?N:n),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e-(r-o)),this.lineTo(t+s,e+o),this.lineTo(t-s,e+o),this.lineTo(t,e-(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e+(r-o)),this.lineTo(t+s,e-o),this.lineTo(t-s,e-o),this.lineTo(t,e+(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var n=0;10>n;n++){var s=0===n%2?1.3*i:.5*i;this.lineTo(t+s*Math.sin(2*n*Math.PI/10),e-s*Math.cos(2*n*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,n,s){var o=Math.PI/180;0>i-2*s&&(s=i/2),0>n-2*s&&(s=n/2),this.beginPath(),this.moveTo(t+s,e),this.lineTo(t+i-s,e),this.arc(t+i-s,e+s,s,270*o,360*o,!1),this.lineTo(t+i,e+n-s),this.arc(t+i-s,e+n-s,s,0,90*o,!1),this.lineTo(t+s,e+n),this.arc(t+s,e+n-s,s,90*o,180*o,!1),this.lineTo(t,e+s),this.arc(t+s,e+s,s,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,n){var s=.5522848,o=i/2*s,r=n/2*s,a=t+i,h=e+n,d=t+i/2,l=e+n/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,n){var s=1/3,o=i,r=n*s,a=.5522848,h=o/2*a,d=r/2*a,l=t+o,p=e+r,c=t+o/2,u=e+r/2,f=e+(n-r/2),m=e+n;this.beginPath(),this.moveTo(l,u),this.bezierCurveTo(l,u+d,c+h,p,c,p),this.bezierCurveTo(c-h,p,t,u+d,t,u),this.bezierCurveTo(t,u-d,c-h,e,c,e),this.bezierCurveTo(c+h,e,l,u-d,l,u),this.lineTo(l,f),this.bezierCurveTo(l,f+d,c+h,m,c,m),this.bezierCurveTo(c-h,m,t,f+d,t,f),this.lineTo(t,u)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,n){var s=t-n*Math.cos(i),o=e-n*Math.sin(i),r=t-.9*n*Math.cos(i),a=e-.9*n*Math.sin(i),h=s+n/3*Math.cos(i+.5*Math.PI),d=o+n/3*Math.sin(i+.5*Math.PI),l=s+n/3*Math.cos(i-.5*Math.PI),p=o+n/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(l,p),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,n,s){s||(s=[10,5]),0==c&&(c=.001);var o=s.length;this.moveTo(t,e);for(var r=i-t,a=n-e,h=a/r,d=Math.sqrt(r*r+a*a),l=0,p=!0;d>=.1;){var c=s[l++%o];c>d&&(c=d);var u=Math.sqrt(c*c/(1+h*h));0>r&&(u=-u),t+=u,e+=h*u,this[p?"lineTo":"moveTo"](t,e),d-=c,p=!p}}),T.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),this._updateMass()},T.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&this.edges.splice(e,1),this._updateMass()},T.prototype._updateMass=function(){this.mass=50+20*this.edges.length},T.prototype.setProperties=function(t,e){if(t){if(void 0!=t.id&&(this.id=t.id),void 0!=t.label&&(this.label=t.label),void 0!=t.title&&(this.title=t.title),void 0!=t.group&&(this.group=t.group),void 0!=t.x&&(this.x=t.x),void 0!=t.y&&(this.y=t.y),void 0!=t.value&&(this.value=t.value),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var n in i)i.hasOwnProperty(n)&&(this[n]=i[n])}if(void 0!=t.shape&&(this.shape=t.shape),void 0!=t.image&&(this.image=t.image),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.color&&(this.color=T.parseColor(t.color)),void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!=t.x,this.yFixed=this.yFixed||void 0!=t.y,this.radiusFixed=this.radiusFixed||void 0!=t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},T.parseColor=function(t){var e;return N.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,N.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border)),e},T.prototype.select=function(){this.selected=!0,this._reset()},T.prototype.unselect=function(){this.selected=!1,this._reset()},T.prototype._reset=function(){this.width=void 0,this.height=void 0},T.prototype.getTitle=function(){return this.title},T.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var n=this.width/2,s=this.height/2,o=Math.sin(e)*n,r=Math.cos(e)*s;return n*s/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},T.prototype._setForce=function(t,e){this.fx=t,this.fy=e},T.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},T.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i/t,this.x+=this.vx/t}if(!this.yFixed){var n=-this.damping*this.vy,s=(this.fy+n)/this.mass;this.vy+=s/t,this.y+=this.vy/t}},T.prototype.isFixed=function(){return this.xFixed&&this.yFixed},T.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t||!this.xFixed&&Math.abs(this.fx)>this.minForce||!this.yFixed&&Math.abs(this.fy)>this.minForce},T.prototype.isSelected=function(){return this.selected},T.prototype.getValue=function(){return this.value},T.prototype.getDistance=function(t,e){var i=this.x-t,n=this.y-e;return Math.sqrt(i*i+n*n)},T.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},T.prototype.draw=function(){throw"Draw method not initialized for node"},T.prototype.resize=function(){throw"Resize method not initialized for node"},T.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},T.prototype._resizeImage=function(){if(!this.width){var t,e;if(this.value){var i=this.imageObj.height/this.imageObj.width;t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e}},T.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;this.imageObj?(t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2):e=this.y,this._label(t,this.label,this.x,e,void 0,"top")},T.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},T.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},T.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=i.width+2*e;this.width=n,this.height=n}},T.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},T.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=Math.max(i.width,i.height)+2*e;this.radius=n/2,this.width=n,this.height=n}},T.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},T.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.widthl;l++)t.fillText(r[l],i,d),d+=h}},T.prototype.getTextSize=function(t){if(void 0!=this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,n=0,s=0,o=e.length;o>s;s++)n=Math.max(n,t.measureText(e[s]).width);return{width:n,height:i}}return{width:0,height:0}},x.prototype.setProperties=function(t,e){if(t)switch(void 0!=t.from&&(this.fromId=t.from),void 0!=t.to&&(this.toId=t.to),void 0!=t.id&&(this.id=t.id),void 0!=t.style&&(this.style=t.style),void 0!=t.label&&(this.label=t.label),this.label&&(this.fontSize=e.edges.fontSize,this.fontFace=e.edges.fontFace,this.fontColor=e.edges.fontColor,void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace)),void 0!=t.title&&(this.title=t.title),void 0!=t.width&&(this.width=t.width),void 0!=t.value&&(this.value=t.value),void 0!=t.length&&(this.length=t.length),t.dash&&(void 0!=t.dash.length&&(this.dash.length=t.dash.length),void 0!=t.dash.gap&&(this.dash.gap=t.dash.gap),void 0!=t.dash.altLength&&(this.dash.altLength=t.dash.altLength)),void 0!=t.color&&(this.color=t.color),this.connect(),this.widthFixed=this.widthFixed||void 0!=t.width,this.lengthFixed=this.lengthFixed||void 0!=t.length,this.stiffness=1/this.length,this.style){case"line":this.draw=this._drawLine;break;case"arrow":this.draw=this._drawArrow;break;case"arrow-center":this.draw=this._drawArrowCenter;break;case"dash-line":this.draw=this._drawDashLine;break;default:this.draw=this._drawLine}},x.prototype.connect=function(){this.disconnect(),this.from=this.graph.nodes[this.fromId]||null,this.to=this.graph.nodes[this.toId]||null,this.connected=this.from&&this.to,this.connected?(this.from.attachEdge(this),this.to.attachEdge(this)):(this.from&&this.from.detachEdge(this),this.to&&this.to.detachEdge(this))},x.prototype.disconnect=function(){this.from&&(this.from.detachEdge(this),this.from=null),this.to&&(this.to.detachEdge(this),this.to=null),this.connected=!1},x.prototype.getTitle=function(){return this.title},x.prototype.getValue=function(){return this.value},x.prototype.setValueRange=function(t,e){if(!this.widthFixed&&void 0!==this.value){var i=(this.widthMax-this.widthMin)/(e-t);this.width=(this.value-t)*i+this.widthMin}},x.prototype.draw=function(){throw"Method draw not initialized in edge"},x.prototype.isOverlappingWith=function(t){var e=10,i=this.from.x,n=this.from.y,s=this.to.x,o=this.to.y,r=t.left,a=t.top,h=x._dist(i,n,s,o,r,a);return e>h},x.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y));else{var i,n,s=this.length/4,o=this.from;o.width||o.resize(t),o.width>o.height?(i=o.x+o.width/2,n=o.y-s):(i=o.x+s,n=o.y-o.height/2),this._circle(t,i,n,s),e=this._pointOnCircle(i,n,s,.5),this._label(t,this.label,e.x,e.y)}},x.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax):this.width},x.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},x.prototype._circle=function(t,e,i,n){t.beginPath(),t.arc(e,i,n,0,2*Math.PI,!1),t.stroke()},x.prototype._label=function(t,e,i,n){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var s=t.measureText(e).width,o=this.fontSize,r=i-s/2,a=n-o/2;t.fillRect(r,a,s,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},x.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!=this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!=this.dash.length&&void 0!=this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.label){var e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}},x.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},x.prototype._pointOnCircle=function(t,e,i,n){var s=2*(n-3/8)*Math.PI;return{x:t+i*Math.cos(s),y:e-i*Math.sin(s)}},x.prototype._drawArrowCenter=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),n=10+5*this.width;e=this._pointOnLine(.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y))}else{var s,o,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(s=a.x+a.width/2,o=a.y-r):(s=a.x+r,o=a.y-a.height/2),this._circle(t,s,o,r);var i=.2*Math.PI,n=10+5*this.width;e=this._pointOnCircle(s,o,r,.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(s,o,r,.5),this._label(t,this.label,e.x,e.y))}},x.prototype._drawArrow=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var n=this.to.x-this.from.x,s=this.to.y-this.from.y,o=Math.sqrt(n*n+s*s),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,l=this.to.distanceToBorder(t,e),p=(o-l)/o,c=(1-p)*this.from.x+p*this.to.x,u=(1-p)*this.from.y+p*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(c,u),t.stroke(),i=10+5*this.width,t.arrow(c,u,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var m,g,v,y=this.from,w=this.length/4;y.width||y.resize(t),y.width>y.height?(m=y.x+y.width/2,g=y.y-w,v={x:m,y:y.y,angle:.9*Math.PI}):(m=y.x+w,g=y.y-y.height/2,v={x:y.x,y:g,angle:.6*Math.PI}),t.beginPath(),t.arc(m,g,w,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.label&&(f=this._pointOnCircle(m,g,w,.5),this._label(t,this.label,f.x,f.y))}},x._dist=function(t,e,i,n,s,o){var r=i-t,a=n-e,h=r*r+a*a,d=((s-t)*r+(o-e)*a)/h;d>1?d=1:0>d&&(d=0);var l=t+d*r,p=e+d*a,c=l-s,u=p-o;return Math.sqrt(c*c+u*u)},S.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},S.prototype.setText=function(t){this.frame.innerHTML=t},S.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,n=this.frame.parentNode.clientHeight,s=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>n&&(o=n-e-this.padding),os&&(r=s-i-this.padding),r0?s[s.length-1]:null},D.prototype._getPointer=function(t){return{x:t.pageX-I.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-I.util.getAbsoluteTop(this.frame.canvas)}},D.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.touches[0]),this.drag.pinched=!1,this.pinch.scale=this._getScale()},D.prototype._onDragStart=function(){var t=this.drag;t.selection=[],t.translation=this._getTranslation(),t.nodeId=this._getNodeAt(t.pointer);var e=this.nodes[t.nodeId];if(e){e.isSelected()||this._selectNodes([t.nodeId]);var i=this;this.selection.forEach(function(e){var n=i.nodes[e];if(n){var s={id:e,node:n,x:n.x,y:n.y,xFixed:n.xFixed,yFixed:n.yFixed};n.xFixed=!0,n.yFixed=!0,t.selection.push(s)}})}},D.prototype._onDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.touches[0]),i=this,n=this.drag,s=n.selection;if(s&&s.length){var o=e.x-n.pointer.x,r=e.y-n.pointer.y;s.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+r))}),this.moving||(this.moving=!0,this.start())}else{var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},D.prototype._onDragEnd=function(){var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},D.prototype._onTap=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];n?(this._selectNodes([i]),this.moving||this._redraw()):(this._unselectNodes(),this._redraw())},D.prototype._onHold=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];if(n){if(n.isSelected())this._unselectNodes([i]);else{var s=!0;this._selectNodes([i],s)}this.moving||this._redraw()}},D.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},D.prototype._zoom=function(t,e){var i=this._getScale();.01>t&&(t=.01),t>10&&(t=10);var n=this._getTranslation(),s=t/i,o=(1-s)*e.x+n.x*s,r=(1-s)*e.y+n.y*s;return this._setScale(t),this._setTranslation(o,r),this._redraw(),t},D.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){"mouswheelScale"in this.pinch||(this.pinch.mouswheelScale=1);var i=this.pinch.mouswheelScale,n=e/10;0>e&&(n/=1-n),i*=1+n;var s=C.event.collectEventData(this,"scroll",t),o=this._getPointer(s.center);i=this._zoom(i,o),this.pinch.mouswheelScale=i}t.preventDefault()},D.prototype._onMouseMoveTitle=function(t){var e=C.event.collectEventData(this,"mousemove",t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var n=this,s=function(){n._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.leftButtonDown||(this.popupTimer=setTimeout(s,300))},D.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},n=this.popupNode;if(void 0==this.popupNode){var s=this.nodes;for(e in s)if(s.hasOwnProperty(e)){var o=s[e];if(void 0!=o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0==this.popupNode){var r=this.edges;for(e in r)if(r.hasOwnProperty(e)){var a=r[e];if(a.connected&&void 0!=a.getTitle()&&a.isOverlappingWith(i)){this.popupNode=a;break}}}if(this.popupNode){if(this.popupNode!=n){var h=this;h.popup||(h.popup=new S(h.frame)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},D.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},D.prototype._unselectNodes=function(t,e){var i,n,s,o=!1;if(t)for(i=0,n=t.length;n>i;i++){s=t[i],this.nodes[s].unselect();for(var r=0;ri;i++)s=this.selection[i],this.nodes[s].unselect(),o=!0;this.selection=[]}return!o||1!=e&&void 0!=e||this._trigger("select"),o},D.prototype._selectNodes=function(t,e){var i,n,s=!1,o=!0;if(t.length!=this.selection.length)o=!1;else for(i=0,n=Math.min(t.length,this.selection.length);n>i;i++)if(t[i]!=this.selection[i]){o=!1;break}if(o)return s;if(void 0==e||0==e){var r=!1;s=this._unselectNodes(void 0,r)}for(i=0,n=t.length;n>i;i++){var a=t[i],h=-1!=this.selection.indexOf(a);h||(this.nodes[a].select(),this.selection.push(a),s=!0)}return s&&this._trigger("select"),s},D.prototype._getNodesOverlappingWith=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&e[n].isOverlappingWith(t)&&i.push(n);return i},D.prototype.getSelection=function(){return this.selection.concat([])},D.prototype.setSelection=function(t){var e,i,n;if(!t||void 0==t.length)throw"Selection must be an array with ids";for(e=0,i=this.selection.length;i>e;e++)n=this.selection[e],this.nodes[n].unselect();for(this.selection=[],e=0,i=t.length;i>e;e++){n=t[e];var s=this.nodes[n];if(!s)throw new RangeError('Node with id "'+n+'" not found');s.select(),this.selection.push(n)}this.redraw()},D.prototype._updateSelection=function(){for(var t=0;ti;i++)for(var s=t[i],o=s.edges,r=0,a=o.length;a>r;r++){var h=o[r],d=null;h.from==s?d=h.to:h.to==s&&(d=h.from);var l,p;if(d)for(l=0,p=t.length;p>l;l++)if(t[l]==d){d=null;break}if(d)for(l=0,p=e.length;p>l;l++)if(e[l]==d){d=null;break}d&&e.push(d)}return e}void 0==t&&(t=1);var i=[],n=this.nodes;for(var s in n)if(n.hasOwnProperty(s)){for(var o=[n[s]],r=0;t>r;r++)o=o.concat(e(o));i.push(o)}for(var a=[],h=0,d=i.length;d>h;h++)a.push(i[h].length);return a},D.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight},D.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof o||t instanceof r)this.nodesData=t;else if(t instanceof Array)this.nodesData=new o,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new o}if(e&&N.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;N.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var n=this.nodesData.getIds();this._addNodes(n)}this._updateSelection()},D.prototype._addNodes=function(t){for(var e,i=0,n=t.length;n>i;i++){e=t[i];var s=this.nodesData.get(e),o=new T(s,this.images,this.groups,this.constants);if(this.nodes[e]=o,!o.isFixed()){var r=2*this.constants.edges.length,a=t.length,h=2*Math.PI*(i/a);o.x=r*Math.cos(h),o.y=r*Math.sin(h),this.moving=!0}}this._reconnectEdges(),this._updateValueRange(this.nodes)},D.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new T(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._reconnectEdges(),this._updateValueRange(e)},D.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,n=t.length;n>i;i++){var s=t[i];delete e[s]}this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},D.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof o||t instanceof r)this.edgesData=t;else if(t instanceof Array)this.edgesData=new o,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new o}if(e&&N.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;N.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var n=this.edgesData.getIds();this._addEdges(n)}this._reconnectEdges()},D.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o];r&&r.disconnect();var a=i.get(o);e[o]=new x(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},D.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new x(r,this,this.constants),this.edges[o]=a)}this.moving=!0,this._updateValueRange(e)},D.prototype._removeEdges=function(t){for(var e=this.edges,i=0,n=t.length;n>i;i++){var s=t[i],o=e[s];o&&(o.disconnect(),delete e[s])}this.moving=!0,this._updateValueRange(e)},D.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var n=i[t];n.from=null,n.to=null,n.connect()}},D.prototype._updateValueRange=function(t){var e,i=void 0,n=void 0;for(e in t)if(t.hasOwnProperty(e)){var s=t[e].getValue();void 0!==s&&(i=void 0===i?s:Math.min(s,i),n=void 0===n?s:Math.max(s,n))}if(void 0!==i&&void 0!==n)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,n)},D.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},D.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this._drawEdges(t),this._drawNodes(t),t.restore()},D.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},D.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},D.prototype._setScale=function(t){this.scale=t},D.prototype._getScale=function(){return this.scale},D.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},D.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},D.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},D.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},D.prototype._drawNodes=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&(e[n].isSelected()?i.push(n):e[n].draw(t));for(var s=0,o=i.length;o>s;s++)e[i[s]].draw(t)},D.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];n.connected&&e[i].draw(t)}},D.prototype._doStabilize=function(){new Date;for(var t=0,e=this.constants.minVelocity,i=!1;!i&&t; + * Licensed under the MIT license */ + +(function(window, undefined) { + 'use strict'; + +/** + * Hammer + * use this to create instances + * @param {HTMLElement} element + * @param {Object} options + * @returns {Hammer.Instance} + * @constructor + */ +var Hammer = function(element, options) { + return new Hammer.Instance(element, options || {}); +}; + +// default settings +Hammer.defaults = { + // add styles and attributes to the element to prevent the browser from doing + // its native behavior. this doesnt prevent the scrolling, but cancels + // the contextmenu, tap highlighting etc + // set to false to disable this + stop_browser_behavior: { + // this also triggers onselectstart=false for IE + userSelect: 'none', + // this makes the element blocking in IE10 >, you could experiment with the value + // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 + touchAction: 'none', + touchCallout: 'none', + contentZooming: 'none', + userDrag: 'none', + tapHighlightColor: 'rgba(0,0,0,0)' + } + + // more settings are defined per gesture at gestures.js +}; + +// detect touchevents +Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; +Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); + +// dont use mouseevents on mobile devices +Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; +Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX); + +// eventtypes per touchevent (start, move, end) +// are filled by Hammer.event.determineEventTypes on setup +Hammer.EVENT_TYPES = {}; + +// direction defines +Hammer.DIRECTION_DOWN = 'down'; +Hammer.DIRECTION_LEFT = 'left'; +Hammer.DIRECTION_UP = 'up'; +Hammer.DIRECTION_RIGHT = 'right'; + +// pointer type +Hammer.POINTER_MOUSE = 'mouse'; +Hammer.POINTER_TOUCH = 'touch'; +Hammer.POINTER_PEN = 'pen'; + +// touch event defines +Hammer.EVENT_START = 'start'; +Hammer.EVENT_MOVE = 'move'; +Hammer.EVENT_END = 'end'; + +// hammer document where the base events are added at +Hammer.DOCUMENT = document; + +// plugins namespace +Hammer.plugins = {}; + +// if the window events are set... +Hammer.READY = false; + +/** + * setup events to detect gestures on the document + */ +function setup() { + if(Hammer.READY) { + return; + } + + // find what eventtypes we add listeners to + Hammer.event.determineEventTypes(); + + // Register all gestures inside Hammer.gestures + for(var name in Hammer.gestures) { + if(Hammer.gestures.hasOwnProperty(name)) { + Hammer.detection.register(Hammer.gestures[name]); + } + } + + // Add touch events on the document + Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect); + Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect); + + // Hammer is ready...! + Hammer.READY = true; +} + +/** + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * @param {HTMLElement} element + * @param {Object} [options={}] + * @returns {Hammer.Instance} + * @constructor + */ +Hammer.Instance = function(element, options) { + var self = this; + + // setup HammerJS window events and register all gestures + // this also sets up the default options + setup(); + + this.element = element; + + // start/stop detection option + this.enabled = true; + + // merge options + this.options = Hammer.utils.extend( + Hammer.utils.extend({}, Hammer.defaults), + options || {}); + + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.stop_browser_behavior) { + Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); + } + + // start detection on touchstart + Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) { + if(self.enabled) { + Hammer.detection.startDetect(self, ev); + } + }); + + // return instance + return this; +}; + + +Hammer.Instance.prototype = { + /** + * bind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {Hammer.Instance} + */ + on: function onEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t=0; t 0 && eventType == Hammer.EVENT_END) { + eventType = Hammer.EVENT_MOVE; + } + // no touches, force the end event + else if(!count_touches) { + eventType = Hammer.EVENT_END; + } + + // because touchend has no touches, and we often want to use these in our gestures, + // we send the last move event as our eventData in touchend + if(!count_touches && last_move_event !== null) { + ev = last_move_event; + } + // store the last move event + else { + last_move_event = ev; + } + + // trigger the handler + handler.call(Hammer.detection, self.collectEventData(element, eventType, ev)); + + // remove pointerevent from list + if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) { + count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); + } + } + + //debug(sourceEventType +" "+ eventType); + + // on the end we reset everything + if(!count_touches) { + last_move_event = null; + enable_detect = false; + touch_triggered = false; + Hammer.PointerEvent.reset(); + } + }); + }, + + + /** + * we have different events for each device/browser + * determine what we need and set them in the Hammer.EVENT_TYPES constant + */ + determineEventTypes: function determineEventTypes() { + // determine the eventtype we want to set + var types; + + // pointerEvents magic + if(Hammer.HAS_POINTEREVENTS) { + types = Hammer.PointerEvent.getEvents(); + } + // on Android, iOS, blackberry, windows mobile we dont want any mouseevents + else if(Hammer.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel']; + } + // for non pointer events browsers and mixed browsers, + // like chrome on windows8 touch laptop + else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup']; + } + + Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0]; + Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1]; + Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2]; + }, + + + /** + * create touchlist depending on the event + * @param {Object} ev + * @param {String} eventType used by the fakemultitouch plugin + */ + getTouchList: function getTouchList(ev/*, eventType*/) { + // get the fake pointerEvent touchlist + if(Hammer.HAS_POINTEREVENTS) { + return Hammer.PointerEvent.getTouchList(); + } + // get the touchlist + else if(ev.touches) { + return ev.touches; + } + // make fake touchlist from mouse position + else { + return [{ + identifier: 1, + pageX: ev.pageX, + pageY: ev.pageY, + target: ev.target + }]; + } + }, + + + /** + * collect event data for Hammer js + * @param {HTMLElement} element + * @param {String} eventType like Hammer.EVENT_MOVE + * @param {Object} eventData + */ + collectEventData: function collectEventData(element, eventType, ev) { + var touches = this.getTouchList(ev, eventType); + + // find out pointerType + var pointerType = Hammer.POINTER_TOUCH; + if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) { + pointerType = Hammer.POINTER_MOUSE; + } + + return { + center : Hammer.utils.getCenter(touches), + timeStamp : new Date().getTime(), + target : ev.target, + touches : touches, + eventType : eventType, + pointerType : pointerType, + srcEvent : ev, + + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + if(this.srcEvent.preventManipulation) { + this.srcEvent.preventManipulation(); + } + + if(this.srcEvent.preventDefault) { + this.srcEvent.preventDefault(); + } + }, + + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, + + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return Hammer.detection.stopDetect(); + } + }; + } +}; + +Hammer.PointerEvent = { + /** + * holds all pointers + * @type {Object} + */ + pointers: {}, + + /** + * get a list of pointers + * @returns {Array} touchlist + */ + getTouchList: function() { + var self = this; + var touchlist = []; + + // we can use forEach since pointerEvents only is in IE10 + Object.keys(self.pointers).sort().forEach(function(id) { + touchlist.push(self.pointers[id]); + }); + return touchlist; + }, + + /** + * update the position of a pointer + * @param {String} type Hammer.EVENT_END + * @param {Object} pointerEvent + */ + updatePointer: function(type, pointerEvent) { + if(type == Hammer.EVENT_END) { + this.pointers = {}; + } + else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } + + return Object.keys(this.pointers).length; + }, + + /** + * check if ev matches pointertype + * @param {String} pointerType Hammer.POINTER_MOUSE + * @param {PointerEvent} ev + */ + matchType: function(pointerType, ev) { + if(!ev.pointerType) { + return false; + } + + var types = {}; + types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE); + types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH); + types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN); + return types[pointerType]; + }, + + + /** + * get events + */ + getEvents: function() { + return [ + 'pointerdown MSPointerDown', + 'pointermove MSPointerMove', + 'pointerup pointercancel MSPointerUp MSPointerCancel' + ]; + }, + + /** + * reset the list + */ + reset: function() { + this.pointers = {}; + } +}; + + +Hammer.utils = { + /** + * extend method, + * also used for cloning when dest is an empty object + * @param {Object} dest + * @param {Object} src + * @parm {Boolean} merge do a merge + * @returns {Object} dest + */ + extend: function extend(dest, src, merge) { + for (var key in src) { + if(dest[key] !== undefined && merge) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, + + + /** + * find if a node is in the given parent + * used for event delegation tricks + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @returns {boolean} has_parent + */ + hasParent: function(node, parent) { + while(node){ + if(node == parent) { + return true; + } + node = node.parentNode; + } + return false; + }, + + + /** + * get the center of all the touches + * @param {Array} touches + * @returns {Object} center + */ + getCenter: function getCenter(touches) { + var valuesX = [], valuesY = []; + + for(var t= 0,len=touches.length; t= y) { + return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; + } + else { + return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; + } + }, + + + /** + * calculate the distance between two touches + * @param {Touch} touch1 + * @param {Touch} touch2 + * @returns {Number} distance + */ + getDistance: function getDistance(touch1, touch2) { + var x = touch2.pageX - touch1.pageX, + y = touch2.pageY - touch1.pageY; + return Math.sqrt((x*x) + (y*y)); + }, + + + /** + * calculate the scale factor between two touchLists (fingers) + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start + * @param {Array} end + * @returns {Number} scale + */ + getScale: function getScale(start, end) { + // need two fingers... + if(start.length >= 2 && end.length >= 2) { + return this.getDistance(end[0], end[1]) / + this.getDistance(start[0], start[1]); + } + return 1; + }, + + + /** + * calculate the rotation degrees between two touchLists (fingers) + * @param {Array} start + * @param {Array} end + * @returns {Number} rotation + */ + getRotation: function getRotation(start, end) { + // need two fingers + if(start.length >= 2 && end.length >= 2) { + return this.getAngle(end[1], end[0]) - + this.getAngle(start[1], start[0]); + } + return 0; + }, + + + /** + * boolean if the direction is vertical + * @param {String} direction + * @returns {Boolean} is_vertical + */ + isVertical: function isVertical(direction) { + return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN); + }, + + + /** + * stop browser default behavior with css props + * @param {HtmlElement} element + * @param {Object} css_props + */ + stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) { + var prop, + vendors = ['webkit','khtml','moz','ms','o','']; + + if(!css_props || !element.style) { + return; + } + + // with css properties for modern browsers + for(var i = 0; i < vendors.length; i++) { + for(var p in css_props) { + if(css_props.hasOwnProperty(p)) { + prop = p; + + // vender prefix at the property + if(vendors[i]) { + prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); + } + + // set the style + element.style[prop] = css_props[p]; + } + } + } + + // also the disable onselectstart + if(css_props.userSelect == 'none') { + element.onselectstart = function() { + return false; + }; + } + } +}; + +Hammer.detection = { + // contains all registred Hammer.gestures in the correct order + gestures: [], + + // data of the current Hammer.gesture detection session + current: null, + + // the previous Hammer.gesture session data + // is a full clone of the previous gesture.current object + previous: null, + + // when this becomes true, no gestures are fired + stopped: false, + + + /** + * start Hammer.gesture detection + * @param {Hammer.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a Hammer.gesture detection on an element + if(this.current) { + return; + } + + this.stopped = false; + + this.current = { + inst : inst, // reference to HammerInstance we're working for + startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent : false, // last eventData + name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; + + this.detect(eventData); + }, + + + /** + * Hammer.gesture detection + * @param {Object} eventData + * @param {Object} eventData + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return; + } + + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); + + // instance options + var inst_options = this.current.inst.options; + + // call Hammer.gesture handlers + for(var g=0,len=this.gestures.length; g b.index) { + return 1; + } + return 0; + }); + + return this.gestures; + } +}; + + +Hammer.gestures = Hammer.gestures || {}; + +/** + * Custom gestures + * ============================== + * + * Gesture object + * -------------------- + * The object structure of a gesture: + * + * { name: 'mygesture', + * index: 1337, + * defaults: { + * mygesture_option: true + * } + * handler: function(type, ev, inst) { + * // trigger gesture event + * inst.trigger(this.name, ev); + * } + * } + + * @param {String} name + * this should be the name of the gesture, lowercase + * it is also being used to disable/enable the gesture per instance config. + * + * @param {Number} [index=1000] + * the index of the gesture, where it is going to be in the stack of gestures detection + * like when you build an gesture that depends on the drag gesture, it is a good + * idea to place it after the index of the drag gesture. + * + * @param {Object} [defaults={}] + * the default settings of the gesture. these are added to the instance settings, + * and can be overruled per instance. you can also add the name of the gesture, + * but this is also added by default (and set to true). + * + * @param {Function} handler + * this handles the gesture detection of your custom gesture and receives the + * following arguments: + * + * @param {Object} eventData + * event data containing the following properties: + * timeStamp {Number} time the event occurred + * target {HTMLElement} target element + * touches {Array} touches (fingers, pointers, mouse) on the screen + * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH + * center {Object} center position of the touches. contains pageX and pageY + * deltaTime {Number} the total time of the touches in the screen + * deltaX {Number} the delta on x axis we haved moved + * deltaY {Number} the delta on y axis we haved moved + * velocityX {Number} the velocity on the x + * velocityY {Number} the velocity on y + * angle {Number} the angle we are moving + * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT + * distance {Number} the distance we haved moved + * scale {Number} scaling of the touches, needs 2 touches + * rotation {Number} rotation of the touches, needs 2 touches * + * eventType {String} matches Hammer.EVENT_START|MOVE|END + * srcEvent {Object} the source event, like TouchStart or MouseDown * + * startEvent {Object} contains the same properties as above, + * but from the first touch. this is used to calculate + * distances, deltaTime, scaling etc + * + * @param {Hammer.Instance} inst + * the instance we are doing the detection for. you can get the options from + * the inst.options object and trigger the gesture event by calling inst.trigger + * + * + * Handle gestures + * -------------------- + * inside the handler you can get/set Hammer.detection.current. This is the current + * detection session. It has the following properties + * @param {String} name + * contains the name of the gesture we have detected. it has not a real function, + * only to check in other gestures if something is detected. + * like in the drag gesture we set it to 'drag' and in the swipe gesture we can + * check if the current gesture is 'drag' by accessing Hammer.detection.current.name + * + * @readonly + * @param {Hammer.Instance} inst + * the instance we do the detection for + * + * @readonly + * @param {Object} startEvent + * contains the properties of the first gesture detection in this session. + * Used for calculations about timing, distance, etc. + * + * @readonly + * @param {Object} lastEvent + * contains all the properties of the last gesture detect in this session. + * + * after the gesture detection session has been completed (user has released the screen) + * the Hammer.detection.current object is copied into Hammer.detection.previous, + * this is usefull for gestures like doubletap, where you need to know if the + * previous gesture was a tap + * + * options that have been set by the instance can be received by calling inst.options + * + * You can trigger a gesture event by calling inst.trigger("mygesture", event). + * The first param is the name of your gesture, the second the event argument + * + * + * Register gestures + * -------------------- + * When an gesture is added to the Hammer.gestures object, it is auto registered + * at the setup of the first Hammer instance. You can also call Hammer.detection.register + * manually and pass your gesture object as a param + * + */ + +/** + * Hold + * Touch stays at the same place for x time + * @events hold + */ +Hammer.gestures.Hold = { + name: 'hold', + index: 10, + defaults: { + hold_timeout : 500, + hold_threshold : 1 + }, + timer: null, + handler: function holdGesture(ev, inst) { + switch(ev.eventType) { + case Hammer.EVENT_START: + // clear any running timers + clearTimeout(this.timer); + + // set the gesture so we can check in the timeout if it still is + Hammer.detection.current.name = this.name; + + // set timer and if after the timeout it still is hold, + // we trigger the hold event + this.timer = setTimeout(function() { + if(Hammer.detection.current.name == 'hold') { + inst.trigger('hold', ev); + } + }, inst.options.hold_timeout); + break; + + // when you move or end we clear the timer + case Hammer.EVENT_MOVE: + if(ev.distance > inst.options.hold_threshold) { + clearTimeout(this.timer); + } + break; + + case Hammer.EVENT_END: + clearTimeout(this.timer); + break; + } + } +}; + + +/** + * Tap/DoubleTap + * Quick touch at a place or double at the same place + * @events tap, doubletap + */ +Hammer.gestures.Tap = { + name: 'tap', + index: 100, + defaults: { + tap_max_touchtime : 250, + tap_max_distance : 10, + tap_always : true, + doubletap_distance : 20, + doubletap_interval : 300 + }, + handler: function tapGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + // previous gesture, for the double tap since these are two different gesture detections + var prev = Hammer.detection.previous, + did_doubletap = false; + + // when the touchtime is higher then the max touch time + // or when the moving distance is too much + if(ev.deltaTime > inst.options.tap_max_touchtime || + ev.distance > inst.options.tap_max_distance) { + return; + } + + // check if double tap + if(prev && prev.name == 'tap' && + (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval && + ev.distance < inst.options.doubletap_distance) { + inst.trigger('doubletap', ev); + did_doubletap = true; + } + + // do a single tap + if(!did_doubletap || inst.options.tap_always) { + Hammer.detection.current.name = 'tap'; + inst.trigger(Hammer.detection.current.name, ev); + } + } + } +}; + + +/** + * Swipe + * triggers swipe events when the end velocity is above the threshold + * @events swipe, swipeleft, swiperight, swipeup, swipedown + */ +Hammer.gestures.Swipe = { + name: 'swipe', + index: 40, + defaults: { + // set 0 for unlimited, but this can conflict with transform + swipe_max_touches : 1, + swipe_velocity : 0.7 + }, + handler: function swipeGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + // max touches + if(inst.options.swipe_max_touches > 0 && + ev.touches.length > inst.options.swipe_max_touches) { + return; + } + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.velocityX > inst.options.swipe_velocity || + ev.velocityY > inst.options.swipe_velocity) { + // trigger swipe events + inst.trigger(this.name, ev); + inst.trigger(this.name + ev.direction, ev); + } + } + } +}; + + +/** + * Drag + * Move with x fingers (default 1) around on the page. Blocking the scrolling when + * moving left and right is a good practice. When all the drag events are blocking + * you disable scrolling on that area. + * @events drag, drapleft, dragright, dragup, dragdown + */ +Hammer.gestures.Drag = { + name: 'drag', + index: 50, + defaults: { + drag_min_distance : 10, + // set 0 for unlimited, but this can conflict with transform + drag_max_touches : 1, + // prevent default browser behavior when dragging occurs + // be careful with it, it makes the element a blocking element + // when you are using the drag gesture, it is a good practice to set this true + drag_block_horizontal : false, + drag_block_vertical : false, + // drag_lock_to_axis keeps the drag gesture on the axis that it started on, + // It disallows vertical directions if the initial direction was horizontal, and vice versa. + drag_lock_to_axis : false, + // drag lock only kicks in when distance > drag_lock_min_distance + // This way, locking occurs only when the distance has become large enough to reliably determine the direction + drag_lock_min_distance : 25 + }, + triggered: false, + handler: function dragGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(Hammer.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } + + // max touches + if(inst.options.drag_max_touches > 0 && + ev.touches.length > inst.options.drag_max_touches) { + return; + } + + switch(ev.eventType) { + case Hammer.EVENT_START: + this.triggered = false; + break; + + case Hammer.EVENT_MOVE: + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.distance < inst.options.drag_min_distance && + Hammer.detection.current.name != this.name) { + return; + } + + // we are dragging! + Hammer.detection.current.name = this.name; + + // lock drag to axis? + if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) { + ev.drag_locked_to_axis = true; + } + var last_direction = Hammer.detection.current.lastEvent.direction; + if(ev.drag_locked_to_axis && last_direction !== ev.direction) { + // keep direction on the axis that the drag gesture started on + if(Hammer.utils.isVertical(last_direction)) { + ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; + } + else { + ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; + } + } + + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } + + // trigger normal event + inst.trigger(this.name, ev); + + // direction event, like dragdown + inst.trigger(this.name + ev.direction, ev); + + // block the browser events + if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) || + (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) { + ev.preventDefault(); + } + break; + + case Hammer.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } + + this.triggered = false; + break; + } + } +}; + + +/** + * Transform + * User want to scale or rotate with 2 fingers + * @events transform, pinch, pinchin, pinchout, rotate + */ +Hammer.gestures.Transform = { + name: 'transform', + index: 45, + defaults: { + // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 + transform_min_scale : 0.01, + // rotation in degrees + transform_min_rotation : 1, + // prevent default browser behavior when two touches are on the screen + // but it makes the element a blocking element + // when you are using the transform gesture, it is a good practice to set this true + transform_always_block : false + }, + triggered: false, + handler: function transformGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(Hammer.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } + + // atleast multitouch + if(ev.touches.length < 2) { + return; + } + + // prevent default when two fingers are on the screen + if(inst.options.transform_always_block) { + ev.preventDefault(); + } + + switch(ev.eventType) { + case Hammer.EVENT_START: + this.triggered = false; + break; + + case Hammer.EVENT_MOVE: + var scale_threshold = Math.abs(1-ev.scale); + var rotation_threshold = Math.abs(ev.rotation); + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(scale_threshold < inst.options.transform_min_scale && + rotation_threshold < inst.options.transform_min_rotation) { + return; + } + + // we are transforming! + Hammer.detection.current.name = this.name; + + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } + + inst.trigger(this.name, ev); // basic transform event + + // trigger rotate event + if(rotation_threshold > inst.options.transform_min_rotation) { + inst.trigger('rotate', ev); + } + + // trigger pinch event + if(scale_threshold > inst.options.transform_min_scale) { + inst.trigger('pinch', ev); + inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev); + } + break; + + case Hammer.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } + + this.triggered = false; + break; + } + } +}; + + +/** + * Touch + * Called as first, tells the user has touched the screen + * @events touch + */ +Hammer.gestures.Touch = { + name: 'touch', + index: -Infinity, + defaults: { + // call preventDefault at touchstart, and makes the element blocking by + // disabling the scrolling of the page, but it improves gestures like + // transforming and dragging. + // be careful with using this, it can be very annoying for users to be stuck + // on the page + prevent_default: false, + + // disable mouse events, so only touch (or pen!) input triggers events + prevent_mouseevents: false + }, + handler: function touchGesture(ev, inst) { + if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) { + ev.stopDetect(); + return; + } + + if(inst.options.prevent_default) { + ev.preventDefault(); + } + + if(ev.eventType == Hammer.EVENT_START) { + inst.trigger(this.name, ev); + } + } +}; + + +/** + * Release + * Called as last, tells the user has released the screen + * @events release + */ +Hammer.gestures.Release = { + name: 'release', + index: Infinity, + handler: function releaseGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + inst.trigger(this.name, ev); + } + } +}; + +// node export +if(typeof module === 'object' && typeof module.exports === 'object'){ + module.exports = Hammer; +} +// just window export +else { + window.Hammer = Hammer; + + // requireJS module definition + if(typeof window.define === 'function' && window.define.amd) { + window.define('hammer', [], function() { + return Hammer; + }); + } +} +})(this); + +},{}],2:[function(require,module,exports){ +//! moment.js +//! version : 2.4.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com + +(function (undefined) { + + /************************************ + Constants + ************************************/ + + var moment, + VERSION = "2.4.0", + round = Math.round, + i, + + YEAR = 0, + MONTH = 1, + DATE = 2, + HOUR = 3, + MINUTE = 4, + SECOND = 5, + MILLISECOND = 6, + + // internal storage for language config files + languages = {}, + + // check for nodeJS + hasModule = (typeof module !== 'undefined' && module.exports), + + // ASP.NET json date format regex + aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, + aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, + + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/, + + // format tokens + formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g, + localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g, + + // parsing token regexes + parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 + parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 + parseTokenThreeDigits = /\d{3}/, // 000 - 999 + parseTokenFourDigits = /\d{1,4}/, // 0 - 9999 + parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 + parseTokenDigits = /\d+/, // nonzero number of digits + parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic. + parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z + parseTokenT = /T/i, // T (ISO seperator) + parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 + + // preliminary iso regex + // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000) + isoRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d:?\d\d|Z)?)?$/, + + isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', + + isoDates = [ + 'YYYY-MM-DD', + 'GGGG-[W]WW', + 'GGGG-[W]WW-E', + 'YYYY-DDD' + ], + + // iso time formats and regexes + isoTimes = [ + ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/], + ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], + ['HH:mm', /(T| )\d\d:\d\d/], + ['HH', /(T| )\d\d/] + ], + + // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] + parseTimezoneChunker = /([\+\-]|\d\d)/gi, + + // getter and setter names + proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), + unitMillisecondFactors = { + 'Milliseconds' : 1, + 'Seconds' : 1e3, + 'Minutes' : 6e4, + 'Hours' : 36e5, + 'Days' : 864e5, + 'Months' : 2592e6, + 'Years' : 31536e6 + }, + + unitAliases = { + ms : 'millisecond', + s : 'second', + m : 'minute', + h : 'hour', + d : 'day', + D : 'date', + w : 'week', + W : 'isoWeek', + M : 'month', + y : 'year', + DDD : 'dayOfYear', + e : 'weekday', + E : 'isoWeekday', + gg: 'weekYear', + GG: 'isoWeekYear' + }, + + camelFunctions = { + dayofyear : 'dayOfYear', + isoweekday : 'isoWeekday', + isoweek : 'isoWeek', + weekyear : 'weekYear', + isoweekyear : 'isoWeekYear' + }, + + // format function strings + formatFunctions = {}, + + // tokens to ordinalize and pad + ordinalizeTokens = 'DDD w W M D d'.split(' '), + paddedTokens = 'M D H h m s w W'.split(' '), + + formatTokenFunctions = { + M : function () { + return this.month() + 1; + }, + MMM : function (format) { + return this.lang().monthsShort(this, format); + }, + MMMM : function (format) { + return this.lang().months(this, format); + }, + D : function () { + return this.date(); + }, + DDD : function () { + return this.dayOfYear(); + }, + d : function () { + return this.day(); + }, + dd : function (format) { + return this.lang().weekdaysMin(this, format); + }, + ddd : function (format) { + return this.lang().weekdaysShort(this, format); + }, + dddd : function (format) { + return this.lang().weekdays(this, format); + }, + w : function () { + return this.week(); + }, + W : function () { + return this.isoWeek(); + }, + YY : function () { + return leftZeroFill(this.year() % 100, 2); + }, + YYYY : function () { + return leftZeroFill(this.year(), 4); + }, + YYYYY : function () { + return leftZeroFill(this.year(), 5); + }, + gg : function () { + return leftZeroFill(this.weekYear() % 100, 2); + }, + gggg : function () { + return this.weekYear(); + }, + ggggg : function () { + return leftZeroFill(this.weekYear(), 5); + }, + GG : function () { + return leftZeroFill(this.isoWeekYear() % 100, 2); + }, + GGGG : function () { + return this.isoWeekYear(); + }, + GGGGG : function () { + return leftZeroFill(this.isoWeekYear(), 5); + }, + e : function () { + return this.weekday(); + }, + E : function () { + return this.isoWeekday(); + }, + a : function () { + return this.lang().meridiem(this.hours(), this.minutes(), true); + }, + A : function () { + return this.lang().meridiem(this.hours(), this.minutes(), false); + }, + H : function () { + return this.hours(); + }, + h : function () { + return this.hours() % 12 || 12; + }, + m : function () { + return this.minutes(); + }, + s : function () { + return this.seconds(); + }, + S : function () { + return toInt(this.milliseconds() / 100); + }, + SS : function () { + return leftZeroFill(toInt(this.milliseconds() / 10), 2); + }, + SSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + SSSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + Z : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2); + }, + ZZ : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(toInt(10 * a / 6), 4); + }, + z : function () { + return this.zoneAbbr(); + }, + zz : function () { + return this.zoneName(); + }, + X : function () { + return this.unix(); + } + }, + + lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin']; + + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; + } + function ordinalizeToken(func, period) { + return function (a) { + return this.lang().ordinal(func.call(this, a), period); + }; + } + + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); + } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + } + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); + + + /************************************ + Constructors + ************************************/ + + function Language() { + + } + + // Moment prototype object + function Moment(config) { + checkOverflow(config); + extend(this, config); + } + + // Duration Constructor + function Duration(duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; + + // store reference to input for deterministic cloning + this._input = duration; + + // representation for dateAddRemove + this._milliseconds = +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + + years * 12; + + this._data = {}; + + this._bubble(); + } + + /************************************ + Helpers + ************************************/ + + + function extend(a, b) { + for (var i in b) { + if (b.hasOwnProperty(i)) { + a[i] = b[i]; + } + } + + if (b.hasOwnProperty("toString")) { + a.toString = b.toString; + } + + if (b.hasOwnProperty("valueOf")) { + a.valueOf = b.valueOf; + } + + return a; + } + + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } + + // left zero fill a number + // see http://jsperf.com/left-zero-filling for performance comparison + function leftZeroFill(number, targetLength) { + var output = number + ''; + while (output.length < targetLength) { + output = '0' + output; + } + return output; + } + + // helper function for _.addTime and _.subtractTime + function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months, + minutes, + hours; + + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); + } + // store the minutes and hours so we can restore them + if (days || months) { + minutes = mom.minute(); + hours = mom.hour(); + } + if (days) { + mom.date(mom.date() + days * isAdding); + } + if (months) { + mom.month(mom.month() + months * isAdding); + } + if (milliseconds && !ignoreUpdateOffset) { + moment.updateOffset(mom); + } + // restore the minutes and hours after possibly changing dst + if (days || months) { + mom.minute(minutes); + mom.hour(hours); + } + } + + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } + + function isDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || + input instanceof Date; + } + + // compare two arrays, return the number of differences + function compareArrays(array1, array2, dontConvert) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if ((dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { + diffs++; + } + } + return diffs + lengthDiff; + } + + function normalizeUnits(units) { + if (units) { + var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); + units = unitAliases[units] || camelFunctions[lowered] || lowered; + } + return units; + } + + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop, + index; + + for (prop in inputObject) { + if (inputObject.hasOwnProperty(prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; + } + + function makeList(field) { + var count, setter; + + if (field.indexOf('week') === 0) { + count = 7; + setter = 'day'; + } + else if (field.indexOf('month') === 0) { + count = 12; + setter = 'month'; + } + else { + return; + } + + moment[field] = function (format, index) { + var i, getter, + method = moment.fn._lang[field], + results = []; + + if (typeof format === 'number') { + index = format; + format = undefined; + } + + getter = function (i) { + var m = moment().utc().set(setter, i); + return method.call(moment.fn._lang, m, format || ''); + }; + + if (index != null) { + return getter(index); + } + else { + for (i = 0; i < count; i++) { + results.push(getter(i)); + } + return results; + } + }; + } + + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } + + return value; + } + + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } + + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } + + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } + + function checkOverflow(m) { + var overflow; + if (m._a && m._pf.overflow === -2) { + overflow = + m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : + m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : + m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR : + m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : + m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : + m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + + m._pf.overflow = overflow; + } + } + + function initializeParsingFlags(config) { + config._pf = { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso: false + }; + } + + function isValid(m) { + if (m._isValid == null) { + m._isValid = !isNaN(m._d.getTime()) && + m._pf.overflow < 0 && + !m._pf.empty && + !m._pf.invalidMonth && + !m._pf.nullInput && + !m._pf.invalidFormat && + !m._pf.userInvalidated; + + if (m._strict) { + m._isValid = m._isValid && + m._pf.charsLeftOver === 0 && + m._pf.unusedTokens.length === 0; + } + } + return m._isValid; + } + + function normalizeLanguage(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } + + /************************************ + Languages + ************************************/ + + + extend(Language.prototype, { + + set : function (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + }, + + _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), + months : function (m) { + return this._months[m.month()]; + }, + + _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), + monthsShort : function (m) { + return this._monthsShort[m.month()]; + }, + + monthsParse : function (monthName) { + var i, mom, regex; + + if (!this._monthsParse) { + this._monthsParse = []; + } + + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + if (!this._monthsParse[i]) { + mom = moment.utc([2000, i]); + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._monthsParse[i].test(monthName)) { + return i; + } + } + }, + + _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), + weekdays : function (m) { + return this._weekdays[m.day()]; + }, + + _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), + weekdaysShort : function (m) { + return this._weekdaysShort[m.day()]; + }, + + _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), + weekdaysMin : function (m) { + return this._weekdaysMin[m.day()]; + }, + + weekdaysParse : function (weekdayName) { + var i, mom, regex; + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + if (!this._weekdaysParse[i]) { + mom = moment([2000, 1]).day(i); + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } + }, + + _longDateFormat : { + LT : "h:mm A", + L : "MM/DD/YYYY", + LL : "MMMM D YYYY", + LLL : "MMMM D YYYY LT", + LLLL : "dddd, MMMM D YYYY LT" + }, + longDateFormat : function (key) { + var output = this._longDateFormat[key]; + if (!output && this._longDateFormat[key.toUpperCase()]) { + output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + this._longDateFormat[key] = output; + } + return output; + }, + + isPM : function (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + }, + + _meridiemParse : /[ap]\.?m?\.?/i, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, + + _calendar : { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }, + calendar : function (key, mom) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.apply(mom) : output; + }, + + _relativeTime : { + future : "in %s", + past : "%s ago", + s : "a few seconds", + m : "a minute", + mm : "%d minutes", + h : "an hour", + hh : "%d hours", + d : "a day", + dd : "%d days", + M : "a month", + MM : "%d months", + y : "a year", + yy : "%d years" + }, + relativeTime : function (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (typeof output === 'function') ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); + }, + pastFuture : function (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + }, + + ordinal : function (number) { + return this._ordinal.replace("%d", number); + }, + _ordinal : "%d", + + preparse : function (string) { + return string; + }, + + postformat : function (string) { + return string; + }, + + week : function (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + }, + + _week : { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + }, + + _invalidDate: 'Invalid date', + invalidDate: function () { + return this._invalidDate; + } + }); + + // Loads a language definition into the `languages` cache. The function + // takes a key and optionally values. If not in the browser and no values + // are provided, it will load the language file module. As a convenience, + // this function also returns the language values. + function loadLang(key, values) { + values.abbr = key; + if (!languages[key]) { + languages[key] = new Language(); + } + languages[key].set(values); + return languages[key]; + } + + // Remove a language from the `languages` cache. Mostly useful in tests. + function unloadLang(key) { + delete languages[key]; + } + + // Determines which language definition to use and returns it. + // + // With no parameters, it will return the global language. If you + // pass in a language key, such as 'en', it will return the + // definition for 'en', so long as 'en' has already been loaded using + // moment.lang. + function getLangDefinition(key) { + var i = 0, j, lang, next, split, + get = function (k) { + if (!languages[k] && hasModule) { + try { + require('./lang/' + k); + } catch (e) { } + } + return languages[k]; + }; + + if (!key) { + return moment.fn._lang; + } + + if (!isArray(key)) { + //short-circuit everything else + lang = get(key); + if (lang) { + return lang; + } + key = [key]; + } + + //pick the language from the array + //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + while (i < key.length) { + split = normalizeLanguage(key[i]).split('-'); + j = split.length; + next = normalizeLanguage(key[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + lang = get(split.slice(0, j).join('-')); + if (lang) { + return lang; + } + if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; + } + return moment.fn._lang; + } + + /************************************ + Formatting + ************************************/ + + + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ""); + } + return input.replace(/\\/g, ""); + } + + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = ""; + for (i = 0; i < length; i++) { + output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; + } + return output; + }; + } + + // format date using native date object + function formatMoment(m, format) { + + if (!m.isValid()) { + return m.lang().invalidDate(); + } + + format = expandFormat(format, m.lang()); + + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } + + return formatFunctions[format](m); + } + + function expandFormat(format, lang) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return lang.longDateFormat(input) || input; + } + + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } + + return format; + } + + + /************************************ + Parsing + ************************************/ + + + // get the regex to find the next token + function getParseRegexForToken(token, config) { + var a; + switch (token) { + case 'DDDD': + return parseTokenThreeDigits; + case 'YYYY': + case 'GGGG': + case 'gggg': + return parseTokenFourDigits; + case 'YYYYY': + case 'GGGGG': + case 'ggggg': + return parseTokenSixDigits; + case 'S': + case 'SS': + case 'SSS': + case 'DDD': + return parseTokenOneToThreeDigits; + case 'MMM': + case 'MMMM': + case 'dd': + case 'ddd': + case 'dddd': + return parseTokenWord; + case 'a': + case 'A': + return getLangDefinition(config._l)._meridiemParse; + case 'X': + return parseTokenTimestampMs; + case 'Z': + case 'ZZ': + return parseTokenTimezone; + case 'T': + return parseTokenT; + case 'SSSS': + return parseTokenDigits; + case 'MM': + case 'DD': + case 'YY': + case 'GG': + case 'gg': + case 'HH': + case 'hh': + case 'mm': + case 'ss': + case 'M': + case 'D': + case 'd': + case 'H': + case 'h': + case 'm': + case 's': + case 'w': + case 'ww': + case 'W': + case 'WW': + case 'e': + case 'E': + return parseTokenOneOrTwoDigits; + default : + a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i")); + return a; + } + } + + function timezoneMinutesFromString(string) { + var tzchunk = (parseTokenTimezone.exec(string) || [])[0], + parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0], + minutes = +(parts[1] * 60) + toInt(parts[2]); + + return parts[0] === '+' ? -minutes : minutes; + } + + // function to convert string input to date + function addTimeToArrayFromToken(token, input, config) { + var a, datePartArray = config._a; + + switch (token) { + // MONTH + case 'M' : // fall through to MM + case 'MM' : + if (input != null) { + datePartArray[MONTH] = toInt(input) - 1; + } + break; + case 'MMM' : // fall through to MMMM + case 'MMMM' : + a = getLangDefinition(config._l).monthsParse(input); + // if we didn't find a month name, mark the date as invalid. + if (a != null) { + datePartArray[MONTH] = a; + } else { + config._pf.invalidMonth = input; + } + break; + // DAY OF MONTH + case 'D' : // fall through to DD + case 'DD' : + if (input != null) { + datePartArray[DATE] = toInt(input); + } + break; + // DAY OF YEAR + case 'DDD' : // fall through to DDDD + case 'DDDD' : + if (input != null) { + config._dayOfYear = toInt(input); + } + + break; + // YEAR + case 'YY' : + datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + break; + case 'YYYY' : + case 'YYYYY' : + datePartArray[YEAR] = toInt(input); + break; + // AM / PM + case 'a' : // fall through to A + case 'A' : + config._isPm = getLangDefinition(config._l).isPM(input); + break; + // 24 HOUR + case 'H' : // fall through to hh + case 'HH' : // fall through to hh + case 'h' : // fall through to hh + case 'hh' : + datePartArray[HOUR] = toInt(input); + break; + // MINUTE + case 'm' : // fall through to mm + case 'mm' : + datePartArray[MINUTE] = toInt(input); + break; + // SECOND + case 's' : // fall through to ss + case 'ss' : + datePartArray[SECOND] = toInt(input); + break; + // MILLISECOND + case 'S' : + case 'SS' : + case 'SSS' : + case 'SSSS' : + datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000); + break; + // UNIX TIMESTAMP WITH MS + case 'X': + config._d = new Date(parseFloat(input) * 1000); + break; + // TIMEZONE + case 'Z' : // fall through to ZZ + case 'ZZ' : + config._useUTC = true; + config._tzm = timezoneMinutesFromString(input); + break; + case 'w': + case 'ww': + case 'W': + case 'WW': + case 'd': + case 'dd': + case 'ddd': + case 'dddd': + case 'e': + case 'E': + token = token.substr(0, 1); + /* falls through */ + case 'gg': + case 'gggg': + case 'GG': + case 'GGGG': + case 'GGGGG': + token = token.substr(0, 2); + if (input) { + config._w = config._w || {}; + config._w[token] = input; + } + break; + } + } + + // convert an array to a date. + // the array should mirror the parameters below + // note: all values past the year are optional and will default to the lowest possible value. + // [year, month, day , hour, minute, second, millisecond] + function dateFromConfig(config) { + var i, date, input = [], currentDate, + yearToUse, fixYear, w, temp, lang, weekday, week; + + if (config._d) { + return; + } + + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + fixYear = function (val) { + return val ? + (val.length < 3 ? (parseInt(val, 10) > 68 ? '19' + val : '20' + val) : val) : + (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]); + }; + + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1); + } + else { + lang = getLangDefinition(config._l); + weekday = w.d != null ? parseWeekday(w.d, lang) : + (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0); + + week = parseInt(w.w, 10) || 1; + + //if we're parsing 'd', then the low day numbers may be next week + if (w.d != null && weekday < lang._week.dow) { + week++; + } + + temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow); + } + + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } + + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR]; + + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } + + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } + + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; + } + + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { + config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; + } + + // add the offsets to the time to be parsed so that we can have a clean array for checking isValid + input[HOUR] += toInt((config._tzm || 0) / 60); + input[MINUTE] += toInt((config._tzm || 0) % 60); + + config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + } + + function dateFromObject(config) { + var normalizedInput; + + if (config._d) { + return; + } + + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; + + dateFromConfig(config); + } + + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [ + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + ]; + } else { + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } + } + + // date from string and format string + function makeDateFromStringAndFormat(config) { + + config._a = []; + config._pf.empty = true; + + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var lang = getLangDefinition(config._l), + string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; + + tokens = expandFormat(config._f, lang).match(formattingTokens) || []; + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + parsedInput = (getParseRegexForToken(token, config).exec(string) || [])[0]; + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + config._pf.unusedInput.push(skipped); + } + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + config._pf.empty = false; + } + else { + config._pf.unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + config._pf.unusedTokens.push(token); + } + } + + // add remaining unparsed input length to the string + config._pf.charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + config._pf.unusedInput.push(string); + } + + // handle am pm + if (config._isPm && config._a[HOUR] < 12) { + config._a[HOUR] += 12; + } + // if is 12 am, change hours to 0 + if (config._isPm === false && config._a[HOUR] === 12) { + config._a[HOUR] = 0; + } + + dateFromConfig(config); + checkOverflow(config); + } + + function unescapeFormat(s) { + return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }); + } + + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function regexpEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + + // date from string and array of format strings + function makeDateFromStringAndArray(config) { + var tempConfig, + bestMoment, + + scoreToBeat, + i, + currentScore; + + if (config._f.length === 0) { + config._pf.invalidFormat = true; + config._d = new Date(NaN); + return; + } + + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = extend({}, config); + initializeParsingFlags(tempConfig); + tempConfig._f = config._f[i]; + makeDateFromStringAndFormat(tempConfig); + + if (!isValid(tempConfig)) { + continue; + } + + // if there is any input that was not parsed add a penalty for that format + currentScore += tempConfig._pf.charsLeftOver; + + //or tokens + currentScore += tempConfig._pf.unusedTokens.length * 10; + + tempConfig._pf.score = currentScore; + + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } + + extend(config, bestMoment || tempConfig); + } + + // date from iso format + function makeDateFromString(config) { + var i, + string = config._i, + match = isoRegex.exec(string); + + if (match) { + config._pf.iso = true; + for (i = 4; i > 0; i--) { + if (match[i]) { + // match[5] should be "T" or undefined + config._f = isoDates[i - 1] + (match[6] || " "); + break; + } + } + for (i = 0; i < 4; i++) { + if (isoTimes[i][1].exec(string)) { + config._f += isoTimes[i][0]; + break; + } + } + if (parseTokenTimezone.exec(string)) { + config._f += "Z"; + } + makeDateFromStringAndFormat(config); + } + else { + config._d = new Date(string); + } + } + + function makeDateFromInput(config) { + var input = config._i, + matched = aspNetJsonRegex.exec(input); + + if (input === undefined) { + config._d = new Date(); + } else if (matched) { + config._d = new Date(+matched[1]); + } else if (typeof input === 'string') { + makeDateFromString(config); + } else if (isArray(input)) { + config._a = input.slice(0); + dateFromConfig(config); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof(input) === 'object') { + dateFromObject(config); + } else { + config._d = new Date(input); + } + } + + function makeDate(y, m, d, h, M, s, ms) { + //can't just apply() to create a date: + //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply + var date = new Date(y, m, d, h, M, s, ms); + + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } + + function makeUTCDate(y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } + + function parseWeekday(input, language) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = language.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } + } + return input; + } + + /************************************ + Relative Time + ************************************/ + + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { + return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function relativeTime(milliseconds, withoutSuffix, lang) { + var seconds = round(Math.abs(milliseconds) / 1000), + minutes = round(seconds / 60), + hours = round(minutes / 60), + days = round(hours / 24), + years = round(days / 365), + args = seconds < 45 && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < 45 && ['mm', minutes] || + hours === 1 && ['h'] || + hours < 22 && ['hh', hours] || + days === 1 && ['d'] || + days <= 25 && ['dd', days] || + days <= 45 && ['M'] || + days < 345 && ['MM', round(days / 30)] || + years === 1 && ['y'] || ['yy', years]; + args[2] = withoutSuffix; + args[3] = milliseconds > 0; + args[4] = lang; + return substituteTimeAgo.apply({}, args); + } + + + /************************************ + Week of Year + ************************************/ + + + // firstDayOfWeek 0 = sun, 6 = sat + // the day of the week that starts the week + // (usually sunday or monday) + // firstDayOfWeekOfYear 0 = sun, 6 = sat + // the first week is the week that contains the first + // of this day of the week + // (eg. ISO weeks use thursday (4)) + function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { + var end = firstDayOfWeekOfYear - firstDayOfWeek, + daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), + adjustedMoment; + + + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } + + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } + + adjustedMoment = moment(mom).add('d', daysToDayOfWeek); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; + } + + //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { + var d = new Date(Date.UTC(year, 0)).getUTCDay(), + daysToAdd, dayOfYear; + + weekday = weekday != null ? weekday : firstDayOfWeek; + daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0); + dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; + + return { + year: dayOfYear > 0 ? year : year - 1, + dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } + + /************************************ + Top Level Functions + ************************************/ + + function makeMoment(config) { + var input = config._i, + format = config._f; + + if (typeof config._pf === 'undefined') { + initializeParsingFlags(config); + } + + if (input === null) { + return moment.invalid({nullInput: true}); + } + + if (typeof input === 'string') { + config._i = input = getLangDefinition().preparse(input); + } + + if (moment.isMoment(input)) { + config = extend({}, input); + + config._d = new Date(+input._d); + } else if (format) { + if (isArray(format)) { + makeDateFromStringAndArray(config); + } else { + makeDateFromStringAndFormat(config); + } + } else { + makeDateFromInput(config); + } + + return new Moment(config); + } + + moment = function (input, format, lang, strict) { + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + return makeMoment({ + _i : input, + _f : format, + _l : lang, + _strict : strict, + _isUTC : false + }); + }; + + // creating with utc + moment.utc = function (input, format, lang, strict) { + var m; + + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + m = makeMoment({ + _useUTC : true, + _isUTC : true, + _l : lang, + _i : input, + _f : format, + _strict : strict + }).utc(); + + return m; + }; + + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; + + // duration + moment.duration = function (input, key) { + var isDuration = moment.isDuration(input), + isNumber = (typeof input === 'number'), + duration = (isDuration ? input._input : (isNumber ? {} : input)), + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + parseIso, + timeEmpty, + dateTimeEmpty; + + if (isNumber) { + if (key) { + duration[key] = input; + } else { + duration.milliseconds = input; + } + } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) { + sign = (match[1] === "-") ? -1 : 1; + duration = { + y: 0, + d: toInt(match[DATE]) * sign, + h: toInt(match[HOUR]) * sign, + m: toInt(match[MINUTE]) * sign, + s: toInt(match[SECOND]) * sign, + ms: toInt(match[MILLISECOND]) * sign + }; + } else if (!!(match = isoDurationRegex.exec(input))) { + sign = (match[1] === "-") ? -1 : 1; + parseIso = function (inp) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + }; + duration = { + y: parseIso(match[2]), + M: parseIso(match[3]), + d: parseIso(match[4]), + h: parseIso(match[5]), + m: parseIso(match[6]), + s: parseIso(match[7]), + w: parseIso(match[8]) + }; + } + + ret = new Duration(duration); + + if (isDuration && input.hasOwnProperty('_lang')) { + ret._lang = input._lang; + } + + return ret; + }; + + // version number + moment.version = VERSION; + + // default format + moment.defaultFormat = isoFormat; + + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + moment.updateOffset = function () {}; + + // This function will load languages and then set the global language. If + // no arguments are passed in, it will simply return the current global + // language key. + moment.lang = function (key, values) { + var r; + if (!key) { + return moment.fn._lang._abbr; + } + if (values) { + loadLang(normalizeLanguage(key), values); + } else if (values === null) { + unloadLang(key); + key = 'en'; + } else if (!languages[key]) { + getLangDefinition(key); + } + r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); + return r._abbr; + }; + + // returns language data + moment.langData = function (key) { + if (key && key._lang && key._lang._abbr) { + key = key._lang._abbr; + } + return getLangDefinition(key); + }; + + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment; + }; + + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; + + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); + } + + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; + + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; + } + + return m; + }; + + moment.parseZone = function (input) { + return moment(input).parseZone(); + }; + + /************************************ + Moment Prototype + ************************************/ + + + extend(moment.fn = Moment.prototype, { + + clone : function () { + return moment(this); + }, + + valueOf : function () { + return +this._d + ((this._offset || 0) * 60000); + }, + + unix : function () { + return Math.floor(+this / 1000); + }, + + toString : function () { + return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + }, + + toDate : function () { + return this._offset ? new Date(+this) : this._d; + }, + + toISOString : function () { + return formatMoment(moment(this).utc(), 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + }, + + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, + + isValid : function () { + return isValid(this); + }, + + isDSTShifted : function () { + + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + } + + return false; + }, + + parsingFlags : function () { + return extend({}, this._pf); + }, + + invalidAt: function () { + return this._pf.overflow; + }, + + utc : function () { + return this.zone(0); + }, + + local : function () { + this.zone(0); + this._isUTC = false; + return this; + }, + + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.lang().postformat(output); + }, + + add : function (input, val) { + var dur; + // switch args to support add('s', 1) and add(1, 's') + if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, 1); + return this; + }, + + subtract : function (input, val) { + var dur; + // switch args to support subtract('s', 1) and subtract(1, 's') + if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, -1); + return this; + }, + + diff : function (input, units, asFloat) { + var that = this._isUTC ? moment(input).zone(this._offset || 0) : moment(input).local(), + zoneDiff = (this.zone() - that.zone()) * 6e4, + diff, output; + + units = normalizeUnits(units); + + if (units === 'year' || units === 'month') { + // average number of days in the months in the given dates + diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 + // difference in months + output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); + // adjust by taking difference in days, average number of days + // and dst in the given months. + output += ((this - moment(this).startOf('month')) - + (that - moment(that).startOf('month'))) / diff; + // same as above but with zones, to negate all dst + output -= ((this.zone() - moment(this).startOf('month').zone()) - + (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff; + if (units === 'year') { + output = output / 12; + } + } else { + diff = (this - that); + output = units === 'second' ? diff / 1e3 : // 1000 + units === 'minute' ? diff / 6e4 : // 1000 * 60 + units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + diff; + } + return asFloat ? output : absRound(output); + }, + + from : function (time, withoutSuffix) { + return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix); + }, + + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, + + calendar : function () { + var diff = this.diff(moment().zone(this.zone()).startOf('day'), 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + return this.format(this.lang().calendar(format, this)); + }, + + isLeapYear : function () { + return isLeapYear(this.year()); + }, + + isDST : function () { + return (this.zone() < this.clone().month(0).zone() || + this.zone() < this.clone().month(5).zone()); + }, + + day : function (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.lang()); + return this.add({ d : input - day }); + } else { + return day; + } + }, + + month : function (input) { + var utc = this._isUTC ? 'UTC' : '', + dayOfMonth; + + if (input != null) { + if (typeof input === 'string') { + input = this.lang().monthsParse(input); + if (typeof input !== 'number') { + return this; + } + } + + dayOfMonth = this.date(); + this.date(1); + this._d['set' + utc + 'Month'](input); + this.date(Math.min(dayOfMonth, this.daysInMonth())); + + moment.updateOffset(this); + return this; + } else { + return this._d['get' + utc + 'Month'](); + } + }, + + startOf: function (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + /* falls through */ + } + + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } + + return this; + }, + + endOf: function (units) { + units = normalizeUnits(units); + return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1); + }, + + isAfter: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) > +moment(input).startOf(units); + }, + + isBefore: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) < +moment(input).startOf(units); + }, + + isSame: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) === +moment(input).startOf(units); + }, + + min: function (other) { + other = moment.apply(null, arguments); + return other < this ? this : other; + }, + + max: function (other) { + other = moment.apply(null, arguments); + return other > this ? this : other; + }, + + zone : function (input) { + var offset = this._offset || 0; + if (input != null) { + if (typeof input === "string") { + input = timezoneMinutesFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + this._offset = input; + this._isUTC = true; + if (offset !== input) { + addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true); + } + } else { + return this._isUTC ? offset : this._d.getTimezoneOffset(); + } + return this; + }, + + zoneAbbr : function () { + return this._isUTC ? "UTC" : ""; + }, + + zoneName : function () { + return this._isUTC ? "Coordinated Universal Time" : ""; + }, + + parseZone : function () { + if (typeof this._i === 'string') { + this.zone(this._i); + } + return this; + }, + + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; + } + else { + input = moment(input).zone(); + } + + return (this.zone() - input) % 60 === 0; + }, + + daysInMonth : function () { + return daysInMonth(this.year(), this.month()); + }, + + dayOfYear : function (input) { + var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add("d", (input - dayOfYear)); + }, + + weekYear : function (input) { + var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year; + return input == null ? year : this.add("y", (input - year)); + }, + + isoWeekYear : function (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add("y", (input - year)); + }, + + week : function (input) { + var week = this.lang().week(this); + return input == null ? week : this.add("d", (input - week) * 7); + }, + + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add("d", (input - week) * 7); + }, + + weekday : function (input) { + var weekday = (this.day() + 7 - this.lang()._week.dow) % 7; + return input == null ? weekday : this.add("d", input - weekday); + }, + + isoWeekday : function (input) { + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + }, + + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, + + set : function (units, value) { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + this[units](value); + } + return this; + }, + + // If passed a language key, it will set the language for this + // instance. Otherwise, it will return the language configuration + // variables for this instance. + lang : function (key) { + if (key === undefined) { + return this._lang; + } else { + this._lang = getLangDefinition(key); + return this; + } + } + }); + + // helper for adding shortcuts + function makeGetterAndSetter(name, key) { + moment.fn[name] = moment.fn[name + 's'] = function (input) { + var utc = this._isUTC ? 'UTC' : ''; + if (input != null) { + this._d['set' + utc + key](input); + moment.updateOffset(this); + return this; + } else { + return this._d['get' + utc + key](); + } + }; + } + + // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) + for (i = 0; i < proxyGettersAndSetters.length; i ++) { + makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]); + } + + // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') + makeGetterAndSetter('year', 'FullYear'); + + // add plural methods + moment.fn.days = moment.fn.day; + moment.fn.months = moment.fn.month; + moment.fn.weeks = moment.fn.week; + moment.fn.isoWeeks = moment.fn.isoWeek; + + // add aliased format methods + moment.fn.toJSON = moment.fn.toISOString; + + /************************************ + Duration Prototype + ************************************/ + + + extend(moment.duration.fn = Duration.prototype, { + + _bubble : function () { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, minutes, hours, years; + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absRound(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absRound(seconds / 60); + data.minutes = minutes % 60; + + hours = absRound(minutes / 60); + data.hours = hours % 24; + + days += absRound(hours / 24); + data.days = days % 30; + + months += absRound(days / 30); + data.months = months % 12; + + years = absRound(months / 12); + data.years = years; + }, + + weeks : function () { + return absRound(this.days() / 7); + }, + + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6; + }, + + humanize : function (withSuffix) { + var difference = +this, + output = relativeTime(difference, !withSuffix, this.lang()); + + if (withSuffix) { + output = this.lang().pastFuture(difference, output); + } + + return this.lang().postformat(output); + }, + + add : function (input, val) { + // supports only 2.0-style add(1, 's') or add(moment) + var dur = moment.duration(input, val); + + this._milliseconds += dur._milliseconds; + this._days += dur._days; + this._months += dur._months; + + this._bubble(); + + return this; + }, + + subtract : function (input, val) { + var dur = moment.duration(input, val); + + this._milliseconds -= dur._milliseconds; + this._days -= dur._days; + this._months -= dur._months; + + this._bubble(); + + return this; + }, + + get : function (units) { + units = normalizeUnits(units); + return this[units.toLowerCase() + 's'](); + }, + + as : function (units) { + units = normalizeUnits(units); + return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's'](); + }, + + lang : moment.fn.lang, + + toIsoString : function () { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var years = Math.abs(this.years()), + months = Math.abs(this.months()), + days = Math.abs(this.days()), + hours = Math.abs(this.hours()), + minutes = Math.abs(this.minutes()), + seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); + + if (!this.asSeconds()) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + return (this.asSeconds() < 0 ? '-' : '') + + 'P' + + (years ? years + 'Y' : '') + + (months ? months + 'M' : '') + + (days ? days + 'D' : '') + + ((hours || minutes || seconds) ? 'T' : '') + + (hours ? hours + 'H' : '') + + (minutes ? minutes + 'M' : '') + + (seconds ? seconds + 'S' : ''); + } + }); + + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; + } + + function makeDurationAsGetter(name, factor) { + moment.duration.fn['as' + name] = function () { + return +this / factor; + }; + } + + for (i in unitMillisecondFactors) { + if (unitMillisecondFactors.hasOwnProperty(i)) { + makeDurationAsGetter(i, unitMillisecondFactors[i]); + makeDurationGetter(i.toLowerCase()); + } + } + + makeDurationAsGetter('Weeks', 6048e5); + moment.duration.fn.asMonths = function () { + return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12; + }; + + + /************************************ + Default Lang + ************************************/ + + + // Set default language, other languages will inherit from English. + moment.lang('en', { + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } + }); + + /* EMBED_LANGUAGES */ + + /************************************ + Exposing Moment + ************************************/ + + function makeGlobal(deprecate) { + var warned = false, local_moment = moment; + /*global ender:false */ + if (typeof ender !== 'undefined') { + return; + } + // here, `this` means `window` in the browser, or `global` on the server + // add `moment` as a global object via a string identifier, + // for Closure Compiler "advanced" mode + if (deprecate) { + this.moment = function () { + if (!warned && console && console.warn) { + warned = true; + console.warn( + "Accessing Moment through the global scope is " + + "deprecated, and will be removed in an upcoming " + + "release."); + } + return local_moment.apply(null, arguments); + }; + } else { + this['moment'] = moment; + } + } + + // CommonJS module is defined + if (hasModule) { + module.exports = moment; + makeGlobal(true); + } else if (typeof define === "function" && define.amd) { + define("moment", function (require, exports, module) { + if (module.config().noGlobal !== true) { + // If user provided noGlobal, he is aware of global + makeGlobal(module.config().noGlobal === undefined); + } + + return moment; + }); + } else { + makeGlobal(); + } +}).call(this); + +},{}],3:[function(require,module,exports){ +/** + * vis.js module imports + */ + +// Try to load dependencies from the global window object. +// If not available there, load via require. +var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); +var Hammer = (typeof window !== 'undefined') && window['Hammer'] || require('hammerjs'); + + +// 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; + }; + + try { + console.log("Warning: Ancient browser detected. Please update your browser"); + } + catch (err) { + } +} + +// 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); + } + } +} + +// Internet Explorer 8 and older does not support Array.map, so we define it +// here in that case. +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map +// Production steps of ECMA-262, Edition 5, 15.4.4.19 +// Reference: http://es5.github.com/#x15.4.4.19 +if (!Array.prototype.map) { + Array.prototype.map = function(callback, thisArg) { + + var T, A, k; + + if (this == null) { + throw new TypeError(" this is null or not defined"); + } + + // 1. Let O be the result of calling ToObject passing the |this| value as the argument. + var O = Object(this); + + // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". + // 3. Let len be ToUint32(lenValue). + var len = O.length >>> 0; + + // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + if (typeof callback !== "function") { + throw new TypeError(callback + " is not a function"); + } + + // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (thisArg) { + T = thisArg; + } + + // 6. Let A be a new array created as if by the expression new Array(len) where Array is + // the standard built-in constructor with that name and len is the value of len. + A = new Array(len); + + // 7. Let k be 0 + k = 0; + + // 8. Repeat, while k < len + while(k < len) { + + var kValue, mappedValue; + + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + if (k in O) { + + // i. Let kValue be the result of calling the Get internal method of O with argument Pk. + kValue = O[ k ]; + + // ii. Let mappedValue be the result of calling the Call internal method of callback + // with T as the this value and argument list containing kValue, k, and O. + mappedValue = callback.call(T, kValue, k, O); + + // iii. Call the DefineOwnProperty internal method of A with arguments + // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true}, + // and false. + + // In browsers that support Object.defineProperty, use the following: + // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true }); + + // For best browser support, use the following: + A[ k ] = mappedValue; + } + // d. Increase k by 1. + k++; + } + + // 9. return A + return A; + }; +} + +// Internet Explorer 8 and older does not support Array.filter, so we define it +// here in that case. +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter +if (!Array.prototype.filter) { + Array.prototype.filter = function(fun /*, thisp */) { + "use strict"; + + if (this == null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun != "function") { + throw new TypeError(); + } + + var res = []; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; // in case fun mutates this + if (fun.call(thisp, val, i, t)) + res.push(val); + } + } + + return res; + }; +} + + +// Internet Explorer 8 and older does not support Object.keys, so we define it +// here in that case. +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys +if (!Object.keys) { + Object.keys = (function () { + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = []; + + for (var prop in obj) { + if (hasOwnProperty.call(obj, prop)) result.push(prop); + } + + if (hasDontEnumBug) { + for (var i=0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); + } + } + return result; + } + })() +} + +// Internet Explorer 8 and older does not support Array.isArray, +// so we define it here in that case. +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray +if(!Array.isArray) { + Array.isArray = function (vArg) { + return Object.prototype.toString.call(vArg) === "[object Array]"; + }; +} + +// Internet Explorer 8 and older does not support Function.bind, +// so we define it here in that case. +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind +if (!Function.prototype.bind) { + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} + +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create +if (!Object.create) { + Object.create = function (o) { + if (arguments.length > 1) { + throw new Error('Object.create implementation only accepts the first parameter.'); + } + function F() {} + F.prototype = o; + return new F(); + }; +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind +if (!Function.prototype.bind) { + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} + +/** + * utility functions + */ +var util = {}; + +/** + * Test whether given object is a number + * @param {*} object + * @return {Boolean} isNumber + */ +util.isNumber = function isNumber(object) { + return (object instanceof Number || typeof object == 'number'); +}; + +/** + * Test whether given object is a string + * @param {*} object + * @return {Boolean} isString + */ +util.isString = function isString(object) { + return (object instanceof String || typeof object == 'string'); +}; + +/** + * Test whether given object is a Date, or a String containing a Date + * @param {Date | String} object + * @return {Boolean} isDate + */ +util.isDate = function isDate(object) { + if (object instanceof Date) { + return true; + } + else if (util.isString(object)) { + // test whether this string contains a date + var match = ASPDateRegex.exec(object); + if (match) { + return true; + } + else if (!isNaN(Date.parse(object))) { + return true; + } + } + + return false; +}; + +/** + * Test whether given object is an instance of google.visualization.DataTable + * @param {*} object + * @return {Boolean} isDataTable + */ +util.isDataTable = function isDataTable(object) { + return (typeof (google) !== 'undefined') && + (google.visualization) && + (google.visualization.DataTable) && + (object instanceof google.visualization.DataTable); +}; + +/** + * Create a semi UUID + * source: http://stackoverflow.com/a/105074/1262753 + * @return {String} uuid + */ +util.randomUUID = function randomUUID () { + var S4 = function () { + return Math.floor( + Math.random() * 0x10000 /* 65536 */ + ).toString(16); + }; + + return ( + S4() + S4() + '-' + + S4() + '-' + + S4() + '-' + + S4() + '-' + + S4() + S4() + S4() + ); +}; + +/** + * Extend object a with the properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Object} a + * @param {... Object} b + * @return {Object} a + */ +util.extend = function (a, b) { + for (var i = 1, len = arguments.length; i < len; i++) { + var other = arguments[i]; + for (var prop in other) { + if (other.hasOwnProperty(prop) && other[prop] !== undefined) { + a[prop] = other[prop]; + } + } + } + + return a; +}; + +/** + * Convert an object to another type + * @param {Boolean | Number | String | Date | Moment | Null | undefined} object + * @param {String | undefined} type Name of the type. Available types: + * 'Boolean', 'Number', 'String', + * 'Date', 'Moment', ISODate', 'ASPDate'. + * @return {*} object + * @throws Error + */ +util.convert = function convert(object, type) { + var match; + + if (object === undefined) { + return undefined; + } + if (object === null) { + return null; + } + + if (!type) { + return object; + } + if (!(typeof type === 'string') && !(type instanceof String)) { + throw new Error('Type must be a string'); + } + + //noinspection FallthroughInSwitchStatementJS + switch (type) { + case 'boolean': + case 'Boolean': + return Boolean(object); + + case 'number': + case 'Number': + return Number(object.valueOf()); + + case 'string': + case 'String': + return String(object); + + case 'Date': + if (util.isNumber(object)) { + return new Date(object); + } + if (object instanceof Date) { + return new Date(object.valueOf()); + } + else if (moment.isMoment(object)) { + return new Date(object.valueOf()); + } + if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])); // parse number + } + else { + return moment(object).toDate(); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type Date'); + } + + case 'Moment': + if (util.isNumber(object)) { + return moment(object); + } + if (object instanceof Date) { + return moment(object.valueOf()); + } + else if (moment.isMoment(object)) { + return moment(object); + } + if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return moment(Number(match[1])); // parse number + } + else { + return moment(object); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type Date'); + } + + case 'ISODate': + if (util.isNumber(object)) { + return new Date(object); + } + else if (object instanceof Date) { + return object.toISOString(); + } + else if (moment.isMoment(object)) { + return object.toDate().toISOString(); + } + else if (util.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])).toISOString(); // parse number + } + else { + return new Date(object).toISOString(); // parse string + } + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type ISODate'); + } + + case 'ASPDate': + if (util.isNumber(object)) { + return '/Date(' + object + ')/'; + } + else if (object instanceof Date) { + return '/Date(' + object.valueOf() + ')/'; + } + else if (util.isString(object)) { + match = ASPDateRegex.exec(object); + var value; + if (match) { + // object is an ASP date + value = new Date(Number(match[1])).valueOf(); // parse number + } + else { + value = new Date(object).valueOf(); // parse string + } + return '/Date(' + value + ')/'; + } + else { + throw new Error( + 'Cannot convert object of type ' + util.getType(object) + + ' to type ASPDate'); + } + + default: + throw new Error('Cannot convert object of type ' + util.getType(object) + + ' to type "' + type + '"'); + } +}; + +// parse ASP.Net Date pattern, +// for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' +// code from http://momentjs.com/ +var ASPDateRegex = /^\/?Date\((\-?\d+)/i; + +/** + * Get the type of an object, for example util.getType([]) returns 'Array' + * @param {*} object + * @return {String} type + */ +util.getType = function getType(object) { + var type = typeof object; + + if (type == 'object') { + if (object == null) { + return 'null'; + } + if (object instanceof Boolean) { + return 'Boolean'; + } + if (object instanceof Number) { + return 'Number'; + } + if (object instanceof String) { + return 'String'; + } + if (object instanceof Array) { + return 'Array'; + } + if (object instanceof Date) { + return 'Date'; + } + return 'Object'; + } + else if (type == 'number') { + return 'Number'; + } + else if (type == 'boolean') { + return 'Boolean'; + } + else if (type == 'string') { + return 'String'; + } + + return type; +}; + +/** + * 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. + */ +util.getAbsoluteLeft = function getAbsoluteLeft (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. + */ +util.getAbsoluteTop = function getAbsoluteTop (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 + */ +util.getPageY = function getPageY (event) { + if ('pageY' in event) { + return event.pageY; + } + else { + var clientY; + if (('targetTouches' in event) && event.targetTouches.length) { + clientY = event.targetTouches[0].clientY; + } + else { + 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 + */ +util.getPageX = function getPageX (event) { + if ('pageY' in event) { + return event.pageX; + } + else { + var clientX; + if (('targetTouches' in event) && event.targetTouches.length) { + clientX = event.targetTouches[0].clientX; + } + else { + 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 ); + } +}; + +/** + * add a className to the given elements style + * @param {Element} elem + * @param {String} className + */ +util.addClassName = function addClassName(elem, className) { + var classes = elem.className.split(' '); + if (classes.indexOf(className) == -1) { + classes.push(className); // add the class to the array + elem.className = classes.join(' '); + } +}; + +/** + * add a className to the given elements style + * @param {Element} elem + * @param {String} className + */ +util.removeClassName = function removeClassname(elem, className) { + var classes = elem.className.split(' '); + var index = classes.indexOf(className); + if (index != -1) { + classes.splice(index, 1); // remove the class from the array + elem.className = classes.join(' '); + } +}; + +/** + * For each method for both arrays and objects. + * In case of an array, the built-in Array.forEach() is applied. + * In case of an Object, the method loops over all properties of the object. + * @param {Object | Array} object An Object or Array + * @param {function} callback Callback method, called for each item in + * the object or array with three parameters: + * callback(value, index, object) + */ +util.forEach = function forEach (object, callback) { + var i, + len; + if (object instanceof Array) { + // array + for (i = 0, len = object.length; i < len; i++) { + callback(object[i], i, object); + } + } + else { + // object + for (i in object) { + if (object.hasOwnProperty(i)) { + callback(object[i], i, object); + } + } + } +}; + +/** + * Update a property in an object + * @param {Object} object + * @param {String} key + * @param {*} value + * @return {Boolean} changed + */ +util.updateProperty = function updateProp (object, key, value) { + if (object[key] !== value) { + object[key] = value; + return true; + } + else { + return false; + } +}; + +/** + * 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] + */ +util.addEventListener = function addEventListener(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] + */ +util.removeEventListener = function removeEventListener(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 + */ +util.getTarget = function getTarget(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 + */ +util.stopPropagation = function stopPropagation(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. + */ +util.preventDefault = function preventDefault (event) { + if (!event) + event = window.event; + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } +}; + + +util.option = {}; + +/** + * Convert a value into a boolean + * @param {Boolean | function | undefined} value + * @param {Boolean} [defaultValue] + * @returns {Boolean} bool + */ +util.option.asBoolean = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } + + if (value != null) { + return (value != false); + } + + return defaultValue || null; +}; + +/** + * Convert a value into a number + * @param {Boolean | function | undefined} value + * @param {Number} [defaultValue] + * @returns {Number} number + */ +util.option.asNumber = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } + + if (value != null) { + return Number(value) || defaultValue || null; + } + + return defaultValue || null; +}; + +/** + * Convert a value into a string + * @param {String | function | undefined} value + * @param {String} [defaultValue] + * @returns {String} str + */ +util.option.asString = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } + + if (value != null) { + return String(value); + } + + return defaultValue || null; +}; + +/** + * Convert a size or location into a string with pixels or a percentage + * @param {String | Number | function | undefined} value + * @param {String} [defaultValue] + * @returns {String} size + */ +util.option.asSize = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } + + if (util.isString(value)) { + return value; + } + else if (util.isNumber(value)) { + return value + 'px'; + } + else { + return defaultValue || null; + } +}; + +/** + * Convert a value into a DOM element + * @param {HTMLElement | function | undefined} value + * @param {HTMLElement} [defaultValue] + * @returns {HTMLElement | null} dom + */ +util.option.asElement = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } + + return value || defaultValue || null; +}; + +/** + * load css from text + * @param {String} css Text containing css + */ +util.loadCss = function (css) { + if (typeof document === 'undefined') { + return; + } + + // get the script location, and built the css file name from the js file name + // http://stackoverflow.com/a/2161748/1262753 + // var scripts = document.getElementsByTagName('script'); + // var jsFile = scripts[scripts.length-1].src.split('?')[0]; + // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css'; + + // inject css + // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript + var style = document.createElement('style'); + style.type = 'text/css'; + if (style.styleSheet){ + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + document.getElementsByTagName('head')[0].appendChild(style); +}; + +/** + * Event listener (singleton) + */ +// TODO: replace usage of the event listener for the EventBus +var 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) { + 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); + } + } + } + } +}; + +/** + * An event bus can be used to emit events, and to subscribe to events + * @constructor EventBus + */ +function EventBus() { + this.subscriptions = []; +} + +/** + * Subscribe to an event + * @param {String | RegExp} event The event can be a regular expression, or + * a string with wildcards, like 'server.*'. + * @param {function} callback. Callback are called with three parameters: + * {String} event, {*} [data], {*} [source] + * @param {*} [target] + * @returns {String} id A subscription id + */ +EventBus.prototype.on = function (event, callback, target) { + var regexp = (event instanceof RegExp) ? + event : + new RegExp(event.replace('*', '\\w+')); + + var subscription = { + id: util.randomUUID(), + event: event, + regexp: regexp, + callback: (typeof callback === 'function') ? callback : null, + target: target + }; + + this.subscriptions.push(subscription); + + return subscription.id; +}; + +/** + * Unsubscribe from an event + * @param {String | Object} filter Filter for subscriptions to be removed + * Filter can be a string containing a + * subscription id, or an object containing + * one or more of the fields id, event, + * callback, and target. + */ +EventBus.prototype.off = function (filter) { + var i = 0; + while (i < this.subscriptions.length) { + var subscription = this.subscriptions[i]; + + var match = true; + if (filter instanceof Object) { + // filter is an object. All fields must match + for (var prop in filter) { + if (filter.hasOwnProperty(prop)) { + if (filter[prop] !== subscription[prop]) { + match = false; + } + } + } + } + else { + // filter is a string, filter on id + match = (subscription.id == filter); + } + + if (match) { + this.subscriptions.splice(i, 1); + } + else { + i++; + } + } +}; + +/** + * Emit an event + * @param {String} event + * @param {*} [data] + * @param {*} [source] + */ +EventBus.prototype.emit = function (event, data, source) { + for (var i =0; i < this.subscriptions.length; i++) { + var subscription = this.subscriptions[i]; + if (subscription.regexp.test(event)) { + if (subscription.callback) { + subscription.callback(event, data, source); + } + } + } +}; + +/** + * DataSet + * + * Usage: + * var dataSet = new DataSet({ + * fieldId: '_id', + * convert: { + * // ... + * } + * }); + * + * dataSet.add(item); + * dataSet.add(data); + * dataSet.update(item); + * dataSet.update(data); + * dataSet.remove(id); + * dataSet.remove(ids); + * var data = dataSet.get(); + * var data = dataSet.get(id); + * var data = dataSet.get(ids); + * var data = dataSet.get(ids, options, data); + * dataSet.clear(); + * + * A data set can: + * - add/remove/update data + * - gives triggers upon changes in the data + * - can import/export data in various data formats + * + * @param {Object} [options] Available options: + * {String} fieldId Field name of the id in the + * items, 'id' by default. + * {Object.} [convert] + * {String[]} [fields] field names to be returned + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * {Array | DataTable} [data] If provided, items will be appended to this + * array or table. Required in case of Google + * DataTable. + * + * @throws Error + */ +DataSet.prototype.get = function (args) { + var me = this; + + // parse the arguments + var id, ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number') { + // get(id [, options] [, data]) + id = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } + else if (firstType == 'Array') { + // get(ids [, options] [, data]) + ids = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } + else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } + + // determine the return type + var type; + if (options && options.type) { + type = (options.type == 'DataTable') ? 'DataTable' : 'Array'; + + if (data && (type != util.getType(data))) { + throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' + + 'does not correspond with specified options.type (' + options.type + ')'); + } + if (type == 'DataTable' && !util.isDataTable(data)) { + throw new Error('Parameter "data" must be a DataTable ' + + 'when options.type is "DataTable"'); + } + } + else if (data) { + type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; + } + else { + type = 'Array'; + } + + // build options + var convert = options && options.convert || this.options.convert; + var filter = options && options.filter; + var items = [], item, itemId, i, len; + + // convert items + if (id != undefined) { + // return a single item + item = me._getItem(id, convert); + if (filter && !filter(item)) { + item = null; + } + } + else if (ids != undefined) { + // return a subset of items + for (i = 0, len = ids.length; i < len; i++) { + item = me._getItem(ids[i], convert); + if (!filter || filter(item)) { + items.push(item); + } + } + } + else { + // return all items + for (itemId in this.data) { + if (this.data.hasOwnProperty(itemId)) { + item = me._getItem(itemId, convert); + if (!filter || filter(item)) { + items.push(item); + } + } + } + } + + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } + + // filter fields of the items + if (options && options.fields) { + var fields = options.fields; + if (id != undefined) { + item = this._filterFields(item, fields); + } + else { + for (i = 0, len = items.length; i < len; i++) { + items[i] = this._filterFields(items[i], fields); + } + } + } + + // return the results + if (type == 'DataTable') { + var columns = this._getColumnNames(data); + if (id != undefined) { + // append a single item to the data table + me._appendRow(data, columns, item); + } + else { + // copy the items to the provided data table + for (i = 0, len = items.length; i < len; i++) { + me._appendRow(data, columns, items[i]); + } + } + return data; + } + else { + // return an array + if (id != undefined) { + // a single item + return item; + } + else { + // multiple items + if (data) { + // copy the items to the provided array + for (i = 0, len = items.length; i < len; i++) { + data.push(items[i]); + } + return data; + } + else { + // just return our array + return items; + } + } + } +}; + +/** + * Get ids of all items or from a filtered set of items. + * @param {Object} [options] An Object with options. Available options: + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Array} ids + */ +DataSet.prototype.getIds = function (options) { + var data = this.data, + filter = options && options.filter, + order = options && options.order, + convert = options && options.convert || this.options.convert, + i, + len, + id, + item, + items, + ids = []; + + if (filter) { + // get filtered items + if (order) { + // create ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (filter(item)) { + items.push(item); + } + } + } + + this._sort(items, order); + + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this.fieldId]; + } + } + else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (filter(item)) { + ids.push(item[this.fieldId]); + } + } + } + } + } + else { + // get all items + if (order) { + // create an ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + items.push(data[id]); + } + } + + this._sort(items, order); + + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this.fieldId]; + } + } + else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = data[id]; + ids.push(item[this.fieldId]); + } + } + } + } + + return ids; +}; + +/** + * Execute a callback function for every item in the dataset. + * The order of the items is not determined. + * @param {function} callback + * @param {Object} [options] Available options: + * {Object.} [convert] + * {String[]} [fields] filter fields + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + */ +DataSet.prototype.forEach = function (callback, options) { + var filter = options && options.filter, + convert = options && options.convert || this.options.convert, + data = this.data, + item, + id; + + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); + + for (var i = 0, len = items.length; i < len; i++) { + item = items[i]; + id = item[this.fieldId]; + callback(item, id); + } + } + else { + // unordered + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (!filter || filter(item)) { + callback(item, id); + } + } + } + } +}; + +/** + * Map every item in the dataset. + * @param {function} callback + * @param {Object} [options] Available options: + * {Object.} [convert] + * {String[]} [fields] filter fields + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Object[]} mappedItems + */ +DataSet.prototype.map = function (callback, options) { + var filter = options && options.filter, + convert = options && options.convert || this.options.convert, + mappedItems = [], + data = this.data, + item; + + // convert and filter items + for (var id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, convert); + if (!filter || filter(item)) { + mappedItems.push(callback(item, id)); + } + } + } + + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); + } + + return mappedItems; +}; + +/** + * Filter the fields of an item + * @param {Object} item + * @param {String[]} fields Field names + * @return {Object} filteredItem + * @private + */ +DataSet.prototype._filterFields = function (item, fields) { + var filteredItem = {}; + + for (var field in item) { + if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { + filteredItem[field] = item[field]; + } + } + + return filteredItem; +}; + +/** + * Sort the provided array with items + * @param {Object[]} items + * @param {String | function} order A field name or custom sort function. + * @private + */ +DataSet.prototype._sort = function (items, order) { + if (util.isString(order)) { + // order by provided field name + var name = order; // field name + items.sort(function (a, b) { + var av = a[name]; + var bv = b[name]; + return (av > bv) ? 1 : ((av < bv) ? -1 : 0); + }); + } + else if (typeof order === 'function') { + // order by sort function + items.sort(order); + } + // TODO: extend order by an Object {field:String, direction:String} + // where direction can be 'asc' or 'desc' + else { + throw new TypeError('Order must be a function or a string'); + } +}; + +/** + * Remove an object by pointer or by id + * @param {String | Number | Object | Array} id Object or id, or an array with + * objects or ids to be removed + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds + */ +DataSet.prototype.remove = function (id, senderId) { + var removedIds = [], + i, len, removedId; + + if (id instanceof Array) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); + } + } + } + else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); + } + } + + if (removedIds.length) { + this._trigger('remove', {items: removedIds}, senderId); + } + + return removedIds; +}; + +/** + * Remove an item by its id + * @param {Number | String | Object} id id or item + * @returns {Number | String | null} id + * @private + */ +DataSet.prototype._remove = function (id) { + if (util.isNumber(id) || util.isString(id)) { + if (this.data[id]) { + delete this.data[id]; + delete this.internalIds[id]; + return id; + } + } + else if (id instanceof Object) { + var itemId = id[this.fieldId]; + if (itemId && this.data[itemId]) { + delete this.data[itemId]; + delete this.internalIds[itemId]; + return itemId; + } + } + return null; +}; + +/** + * Clear the data + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds The ids of all removed items + */ +DataSet.prototype.clear = function (senderId) { + var ids = Object.keys(this.data); + + this.data = {}; + this.internalIds = {}; + + this._trigger('remove', {items: ids}, senderId); + + return ids; +}; + +/** + * Find the item with maximum value of a specified field + * @param {String} field + * @return {Object | null} item Item containing max value, or null if no items + */ +DataSet.prototype.max = function (field) { + var data = this.data, + max = null, + maxField = null; + + for (var id in data) { + if (data.hasOwnProperty(id)) { + var item = data[id]; + var itemField = item[field]; + if (itemField != null && (!max || itemField > maxField)) { + max = item; + maxField = itemField; + } + } + } + + return max; +}; + +/** + * Find the item with minimum value of a specified field + * @param {String} field + * @return {Object | null} item Item containing max value, or null if no items + */ +DataSet.prototype.min = function (field) { + var data = this.data, + min = null, + minField = null; + + for (var id in data) { + if (data.hasOwnProperty(id)) { + var item = data[id]; + var itemField = item[field]; + if (itemField != null && (!min || itemField < minField)) { + min = item; + minField = itemField; + } + } + } + + return min; +}; + +/** + * Find all distinct values of a specified field + * @param {String} field + * @return {Array} values Array containing all distinct values. If the data + * items do not contain the specified field, an array + * containing a single value undefined is returned. + * The returned array is unordered. + */ +DataSet.prototype.distinct = function (field) { + var data = this.data, + values = [], + fieldType = this.options.convert[field], + count = 0; + + for (var prop in data) { + if (data.hasOwnProperty(prop)) { + var item = data[prop]; + var value = util.convert(item[field], fieldType); + var exists = false; + for (var i = 0; i < count; i++) { + if (values[i] == value) { + exists = true; + break; + } + } + if (!exists) { + values[count] = value; + count++; + } + } + } + + return values; +}; + +/** + * Add a single item. Will fail when an item with the same id already exists. + * @param {Object} item + * @return {String} id + * @private + */ +DataSet.prototype._addItem = function (item) { + var id = item[this.fieldId]; + + if (id != undefined) { + // check whether this id is already taken + if (this.data[id]) { + // item already exists + throw new Error('Cannot add item: item with id ' + id + ' already exists'); + } + } + else { + // generate an id + id = util.randomUUID(); + item[this.fieldId] = id; + this.internalIds[id] = item; + } + + var d = {}; + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this.convert[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + this.data[id] = d; + + return id; +}; + +/** + * Get an item. Fields can be converted to a specific type + * @param {String} id + * @param {Object.} [convert] field types to convert + * @return {Object | null} item + * @private + */ +DataSet.prototype._getItem = function (id, convert) { + var field, value; + + // get the item from the dataset + var raw = this.data[id]; + if (!raw) { + return null; + } + + // convert the items field types + var converted = {}, + fieldId = this.fieldId, + internalIds = this.internalIds; + if (convert) { + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + // output all fields, except internal ids + if ((field != fieldId) || !(value in internalIds)) { + converted[field] = util.convert(value, convert[field]); + } + } + } + } + else { + // no field types specified, no converting needed + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + // output all fields, except internal ids + if ((field != fieldId) || !(value in internalIds)) { + converted[field] = value; + } + } + } + } + + return converted; +}; + +/** + * Update a single item: merge with existing item. + * Will fail when the item has no id, or when there does not exist an item + * with the same id. + * @param {Object} item + * @return {String} id + * @private + */ +DataSet.prototype._updateItem = function (item) { + var id = item[this.fieldId]; + if (id == undefined) { + throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); + } + var d = this.data[id]; + if (!d) { + // item doesn't exist + throw new Error('Cannot update item: no item with id ' + id + ' found'); + } + + // merge with current item + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this.convert[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + + return id; +}; + +/** + * Get an array with the column names of a Google DataTable + * @param {DataTable} dataTable + * @return {String[]} columnNames + * @private + */ +DataSet.prototype._getColumnNames = function (dataTable) { + var columns = []; + for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) { + columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); + } + return columns; +}; + +/** + * Append an item as a row to the dataTable + * @param dataTable + * @param columns + * @param item + * @private + */ +DataSet.prototype._appendRow = function (dataTable, columns, item) { + var row = dataTable.addRow(); + + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + dataTable.setValue(row, col, item[field]); + } +}; + +/** + * DataView + * + * a dataview offers a filtered view on a dataset or an other dataview. + * + * @param {DataSet | DataView} data + * @param {Object} [options] Available options: see method get + * + * @constructor DataView + */ +function DataView (data, options) { + this.id = util.randomUUID(); + + this.data = null; + this.ids = {}; // ids of the items currently in memory (just contains a boolean true) + this.options = options || {}; + this.fieldId = 'id'; // name of the field containing id + this.subscribers = {}; // event subscribers + + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); + }; + + this.setData(data); +} + +/** + * Set a data source for the view + * @param {DataSet | DataView} data + */ +DataView.prototype.setData = function (data) { + var ids, dataItems, i, len; + + if (this.data) { + // unsubscribe from current dataset + if (this.data.unsubscribe) { + this.data.unsubscribe('*', this.listener); + } + + // trigger a remove of all items in memory + ids = []; + for (var id in this.ids) { + if (this.ids.hasOwnProperty(id)) { + ids.push(id); + } + } + this.ids = {}; + this._trigger('remove', {items: ids}); + } + + this.data = data; + + if (this.data) { + // update fieldId + this.fieldId = this.options.fieldId || + (this.data && this.data.options && this.data.options.fieldId) || + 'id'; + + // trigger an add of all added items + ids = this.data.getIds({filter: this.options && this.options.filter}); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this.ids[id] = true; + } + this._trigger('add', {items: ids}); + + // subscribe to new dataset + if (this.data.subscribe) { + this.data.subscribe('*', this.listener); + } + } +}; + +/** + * Get data from the data view + * + * Usage: + * + * get() + * get(options: Object) + * get(options: Object, data: Array | DataTable) + * + * get(id: Number) + * get(id: Number, options: Object) + * get(id: Number, options: Object, data: Array | DataTable) + * + * get(ids: Number[]) + * get(ids: Number[], options: Object) + * get(ids: Number[], options: Object, data: Array | DataTable) + * + * Where: + * + * {Number | String} id The id of an item + * {Number[] | String{}} ids An array with ids of items + * {Object} options An Object with options. Available options: + * {String} [type] Type of data to be returned. Can + * be 'DataTable' or 'Array' (default) + * {Object.} [convert] + * {String[]} [fields] field names to be returned + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * {Array | DataTable} [data] If provided, items will be appended to this + * array or table. Required in case of Google + * DataTable. + * @param args + */ +DataView.prototype.get = function (args) { + var me = this; + + // parse the arguments + var ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { + // get(id(s) [, options] [, data]) + ids = arguments[0]; // can be a single id or an array with ids + options = arguments[1]; + data = arguments[2]; + } + else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } + + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this.options, options); + + // create a combined filter method when needed + if (this.options.filter && options && options.filter) { + viewOptions.filter = function (item) { + return me.options.filter(item) && options.filter(item); + } + } + + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); + } + getArguments.push(viewOptions); + getArguments.push(data); + + return this.data && this.data.get.apply(this.data, getArguments); +}; + +/** + * Get ids of all items or from a filtered set of items. + * @param {Object} [options] An Object with options. Available options: + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Array} ids + */ +DataView.prototype.getIds = function (options) { + var ids; + + if (this.data) { + var defaultFilter = this.options.filter; + var filter; + + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + } + } + else { + filter = options.filter; + } + } + else { + filter = defaultFilter; + } + + ids = this.data.getIds({ + filter: filter, + order: options && options.order + }); + } + else { + ids = []; + } + + return ids; +}; + +/** + * Event listener. Will propagate all events from the connected data set to + * the subscribers of the DataView, but will filter the items and only trigger + * when there are changes in the filtered data set. + * @param {String} event + * @param {Object | null} params + * @param {String} senderId + * @private + */ +DataView.prototype._onEvent = function (event, params, senderId) { + var i, len, id, item, + ids = params && params.items, + data = this.data, + added = [], + updated = [], + removed = []; + + if (ids && data) { + switch (event) { + case 'add': + // filter the ids of the added items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + if (item) { + this.ids[id] = true; + added.push(id); + } + } + + break; + + case 'update': + // determine the event from the views viewpoint: an updated + // item can be added, updated, or removed from this view. + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + + if (item) { + if (this.ids[id]) { + updated.push(id); + } + else { + this.ids[id] = true; + added.push(id); + } + } + else { + if (this.ids[id]) { + delete this.ids[id]; + removed.push(id); + } + else { + // nothing interesting for me :-( + } + } + } + + break; + + case 'remove': + // filter the ids of the removed items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + if (this.ids[id]) { + delete this.ids[id]; + removed.push(id); + } + } + + break; + } + + if (added.length) { + this._trigger('add', {items: added}, senderId); + } + if (updated.length) { + this._trigger('update', {items: updated}, senderId); + } + if (removed.length) { + this._trigger('remove', {items: removed}, senderId); + } + } +}; + +// copy subscription functionality from DataSet +DataView.prototype.subscribe = DataSet.prototype.subscribe; +DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe; +DataView.prototype._trigger = DataSet.prototype._trigger; + +/** + * @constructor TimeStep + * The class TimeStep 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 first(). 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 hasNext(). After each step, you can + * retrieve the current date via getCurrent(). + * The TimeStep 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 + */ +TimeStep = function(start, end, minimumStep) { + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); + + this.autoScale = true; + this.scale = TimeStep.SCALE.DAY; + this.step = 1; + + // initialize the range + this.setRange(start, end, minimumStep); +}; + +/// enum scale +TimeStep.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 + */ +TimeStep.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 range iterator to the start date. + */ +TimeStep.prototype.first = 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 + */ +TimeStep.prototype.roundToMinor = function() { + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.YEAR: + this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); + this.current.setMonth(0); + case TimeStep.SCALE.MONTH: this.current.setDate(1); + case TimeStep.SCALE.DAY: // intentional fall through + case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); + case TimeStep.SCALE.HOUR: this.current.setMinutes(0); + case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); + case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); + //case TimeStep.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 TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; + default: break; + } + } +}; + +/** + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date + */ +TimeStep.prototype.hasNext = function () { + return (this.current.valueOf() <= this._end.valueOf()); +}; + +/** + * Do the next step + */ +TimeStep.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 TimeStep.SCALE.MILLISECOND: + + this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; + case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; + case TimeStep.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 TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; + } + } + else { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.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 TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; + case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; + case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; + case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; + case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; + case TimeStep.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 + */ +TimeStep.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 {TimeStep.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. + */ +TimeStep.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 + */ +TimeStep.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 + */ +TimeStep.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 = TimeStep.SCALE.YEAR; this.step = 1000;} + if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} + if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} + if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} + if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} + if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} + if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} + if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} + if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} + if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} + if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} + if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} + if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} + if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} + if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} + if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} + if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} + if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} + if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} + if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} + if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} + if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} + if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} + if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} + if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} + if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} + if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} + if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} + if (stepMillisecond > minimumStep) {this.scale = TimeStep.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 + */ +TimeStep.prototype.snap = function(date) { + if (this.scale == TimeStep.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 == TimeStep.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 == TimeStep.SCALE.DAY || + this.scale == TimeStep.SCALE.WEEKDAY) { + //noinspection FallthroughInSwitchStatementJS + 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 == TimeStep.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 == TimeStep.SCALE.MINUTE) { + //noinspection FallthroughInSwitchStatementJS + 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 == TimeStep.SCALE.SECOND) { + //noinspection FallthroughInSwitchStatementJS + 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 == TimeStep.SCALE.MILLISECOND) { + var step = this.step > 5 ? this.step / 2 : 1; + date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); + } +}; + +/** + * Check if the current value is a major value (for example when the step + * is DAY, a major value is each first day of the MONTH) + * @return {boolean} true if current date is major, else false. + */ +TimeStep.prototype.isMajor = function() { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: + return (this.current.getMilliseconds() == 0); + case TimeStep.SCALE.SECOND: + return (this.current.getSeconds() == 0); + case TimeStep.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 TimeStep.SCALE.HOUR: + return (this.current.getHours() == 0); + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: + return (this.current.getDate() == 1); + case TimeStep.SCALE.MONTH: + return (this.current.getMonth() == 0); + case TimeStep.SCALE.YEAR: + return false; + default: + return false; + } +}; + + +/** + * Returns formatted text for the minor axislabel, depending on the current + * date and the scale. For example when scale is MINUTE, the current time is + * formatted as "hh:mm". + * @param {Date} [date] custom date. if not provided, current date is taken + */ +TimeStep.prototype.getLabelMinor = function(date) { + if (date == undefined) { + date = this.current; + } + + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); + case TimeStep.SCALE.SECOND: return moment(date).format('s'); + case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); + case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); + case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); + case TimeStep.SCALE.DAY: return moment(date).format('D'); + case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); + case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); + default: return ''; + } +}; + + +/** + * Returns formatted text for the major axis label, depending on the current + * date and the scale. For example when scale is MINUTE, the major scale is + * hours, and the hour will be formatted as "hh". + * @param {Date} [date] custom date. if not provided, current date is taken + */ +TimeStep.prototype.getLabelMajor = function(date) { + if (date == undefined) { + date = this.current; + } + + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); + case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); + case TimeStep.SCALE.MINUTE: + case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); + case TimeStep.SCALE.WEEKDAY: + case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); + case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); + case TimeStep.SCALE.YEAR: return ''; + default: return ''; + } +}; + +/** + * @constructor Stack + * Stacks items on top of each other. + * @param {ItemSet} parent + * @param {Object} [options] + */ +function Stack (parent, options) { + this.parent = parent; + + this.options = options || {}; + this.defaultOptions = { + order: function (a, b) { + //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup + // Order: ranges over non-ranges, ranged ordered by width, and + // lastly ordered by start. + if (a instanceof ItemRange) { + if (b instanceof ItemRange) { + var aInt = (a.data.end - a.data.start); + var bInt = (b.data.end - b.data.start); + return (aInt - bInt) || (a.data.start - b.data.start); + } + else { + return -1; + } + } + else { + if (b instanceof ItemRange) { + return 1; + } + else { + return (a.data.start - b.data.start); + } + } + }, + margin: { + item: 10 + } + }; + + this.ordered = []; // ordered items +} + +/** + * Set options for the stack + * @param {Object} options Available options: + * {ItemSet} parent + * {Number} margin + * {function} order Stacking order + */ +Stack.prototype.setOptions = function setOptions (options) { + util.extend(this.options, options); + + // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately +}; + +/** + * Stack the items such that they don't overlap. The items will have a minimal + * distance equal to options.margin.item. + */ +Stack.prototype.update = function update() { + this._order(); + this._stack(); +}; + +/** + * Order the items. The items are ordered by width first, and by left position + * second. + * If a custom order function has been provided via the options, then this will + * be used. + * @private + */ +Stack.prototype._order = function _order () { + var items = this.parent.items; + if (!items) { + throw new Error('Cannot stack items: parent does not contain items'); + } + + // TODO: store the sorted items, to have less work later on + var ordered = []; + var index = 0; + // items is a map (no array) + util.forEach(items, function (item) { + if (item.visible) { + ordered[index] = item; + index++; + } + }); + + //if a customer stack order function exists, use it. + var order = this.options.order || this.defaultOptions.order; + if (!(typeof order === 'function')) { + throw new Error('Option order must be a function'); + } + + ordered.sort(order); + + this.ordered = ordered; +}; + +/** + * Adjust vertical positions of the events such that they don't overlap each + * other. + * @private + */ +Stack.prototype._stack = function _stack () { + var i, + iMax, + ordered = this.ordered, + options = this.options, + orientation = options.orientation || this.defaultOptions.orientation, + axisOnTop = (orientation == 'top'), + margin; + + if (options.margin && options.margin.item !== undefined) { + margin = options.margin.item; + } + else { + margin = this.defaultOptions.margin.item + } + + // calculate new, non-overlapping positions + for (i = 0, iMax = ordered.length; i < iMax; i++) { + var item = ordered[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.checkOverlap(ordered, i, 0, i - 1, margin); + if (collidingItem != null) { + // There is a collision. Reposition the event above the colliding element + if (axisOnTop) { + item.top = collidingItem.top + collidingItem.height + margin; + } + else { + item.top = collidingItem.top - item.height - margin; + } + } + } while (collidingItem); + } +}; + +/** + * 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 | null} colliding item, or undefined when no collisions + * @param {Number} margin A minimum required margin. + * 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. + */ +Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, + itemStart, itemEnd, margin) { + var 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 a = items[itemIndex]; + for (var i = itemEnd; i >= itemStart; i--) { + var b = items[i]; + if (collision(a, b, margin)) { + if (i != itemIndex) { + return b; + } + } + } + + return null; +}; + +/** + * Test if the two provided items collide + * The items must have parameters left, width, top, and height. + * @param {Component} a The first item + * @param {Component} b The second item + * @param {Number} margin A minimum required margin. + * 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 a and b collide, else false + */ +Stack.prototype.collision = function collision (a, b, margin) { + return ((a.left - margin) < (b.left + b.width) && + (a.left + a.width + margin) > b.left && + (a.top - margin) < (b.top + b.height) && + (a.top + a.height + margin) > b.top); +}; + +/** + * @constructor Range + * A Range controls a numeric range with a start and end value. + * The Range adjusts the range based on mouse events or programmatic changes, + * and triggers events when the range is changing or has been changed. + * @param {Object} [options] See description at Range.setOptions + * @extends Controller + */ +function Range(options) { + this.id = util.randomUUID(); + this.start = 0; // Number + this.end = 0; // Number + + // this.options = options || {}; // TODO: fix range options + this.options = { + min: null, + max: null, + zoomMin: null, + zoomMax: null + }; + + this.listeners = []; + + this.setOptions(options); +} + +/** + * Set options for the range controller + * @param {Object} options Available options: + * {Number} start Set start value of the range + * {Number} end Set end value of the range + * {Number} min Minimum value for start + * {Number} max Maximum value for end + * {Number} zoomMin Set a minimum value for + * (end - start). + * {Number} zoomMax Set a maximum value for + * (end - start). + */ +Range.prototype.setOptions = function (options) { + util.extend(this.options, options); + + if (options.start != null || options.end != null) { + this.setRange(options.start, options.end); + } +}; + +/** + * Add listeners for mouse and touch events to the component + * @param {Component} component + * @param {String} event Available events: 'move', 'zoom' + * @param {String} direction Available directions: 'horizontal', 'vertical' + */ +Range.prototype.subscribe = function (component, event, direction) { + var me = this; + var listener; + + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + + 'Choose "horizontal" or "vertical".'); + } + + //noinspection FallthroughInSwitchStatementJS + if (event == 'move') { + listener = { + component: component, + event: event, + direction: direction, + callback: function (event) { + me._onMouseDown(event, listener); + }, + params: {} + }; + + component.on('mousedown', listener.callback); + me.listeners.push(listener); + } + else if (event == 'zoom') { + listener = { + component: component, + event: event, + direction: direction, + callback: function (event) { + me._onMouseWheel(event, listener); + }, + params: {} + }; + + component.on('mousewheel', listener.callback); + me.listeners.push(listener); + } + else { + throw new TypeError('Unknown event "' + event + '". ' + + 'Choose "move" or "zoom".'); + } +}; + +/** + * Event handler + * @param {String} event name of the event, for example 'click', 'mousemove' + * @param {function} callback callback handler, invoked with the raw HTML Event + * as parameter. + */ +Range.prototype.on = function (event, callback) { + events.addListener(this, event, callback); +}; + +/** + * Trigger an event + * @param {String} event name of the event, available events: 'rangechange', + * 'rangechanged' + * @private + */ +Range.prototype._trigger = function (event) { + events.trigger(this, event, { + start: this.start, + end: this.end + }); +}; + +/** + * Set a new start and end range + * @param {Number} start + * @param {Number} end + */ +Range.prototype.setRange = function(start, end) { + var changed = this._applyRange(start, end); + if (changed) { + this._trigger('rangechange'); + this._trigger('rangechanged'); + } +}; + +/** + * Set a new start and end range. This method is the same as setRange, but + * does not trigger a range change and range changed event, and it returns + * true when the range is changed + * @param {Number} start + * @param {Number} end + * @return {Boolean} changed + * @private + */ +Range.prototype._applyRange = function(start, end) { + var newStart = (start != null) ? util.convert(start, 'Number') : this.start; + var newEnd = (end != null) ? util.convert(end, 'Number') : this.end; + var diff; + + // check for valid number + if (isNaN(newStart)) { + throw new Error('Invalid start "' + start + '"'); + } + if (isNaN(newEnd)) { + throw new Error('Invalid end "' + end + '"'); + } + + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } + + // prevent start < min + if (this.options.min != null) { + var min = this.options.min.valueOf(); + if (newStart < min) { + diff = (min - newStart); + newStart += diff; + newEnd += diff; + } + } + + // prevent end > max + if (this.options.max != null) { + var max = this.options.max.valueOf(); + if (newEnd > max) { + diff = (newEnd - max); + newStart -= diff; + newEnd -= diff; + } + } + + // prevent (end-start) > zoomMin + if (this.options.zoomMin != null) { + var zoomMin = this.options.zoomMin.valueOf(); + if (zoomMin < 0) { + zoomMin = 0; + } + if ((newEnd - newStart) < zoomMin) { + if ((this.end - this.start) > zoomMin) { + // zoom to the minimum + diff = (zoomMin - (newEnd - newStart)); + newStart -= diff / 2; + newEnd += diff / 2; + } + else { + // ingore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } + } + } + + // prevent (end-start) > zoomMin + if (this.options.zoomMax != null) { + var zoomMax = this.options.zoomMax.valueOf(); + if (zoomMax < 0) { + zoomMax = 0; + } + if ((newEnd - newStart) > zoomMax) { + if ((this.end - this.start) < zoomMax) { + // zoom to the maximum + diff = ((newEnd - newStart) - zoomMax); + newStart += diff / 2; + newEnd -= diff / 2; + } + else { + // ingore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } + } + } + + var changed = (this.start != newStart || this.end != newEnd); + + this.start = newStart; + this.end = newEnd; + + return changed; +}; + +/** + * Retrieve the current range. + * @return {Object} An object with start and end properties + */ +Range.prototype.getRange = function() { + return { + start: this.start, + end: this.end + }; +}; + +/** + * Calculate the conversion offset and factor for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, factor: number}} conversion + */ +Range.prototype.conversion = function (width) { + var start = this.start; + var end = this.end; + + return Range.conversion(this.start, this.end, width); +}; + +/** + * Static method to calculate the conversion offset and factor for a range, + * based on the provided start, end, and width + * @param {Number} start + * @param {Number} end + * @param {Number} width + * @returns {{offset: number, factor: number}} conversion + */ +Range.conversion = function (start, end, width) { + if (width != 0 && (end - start != 0)) { + return { + offset: start, + factor: width / (end - start) + } + } + else { + return { + offset: 0, + factor: 1 + }; + } +}; + +/** + * Start moving horizontally or vertically + * @param {Event} event + * @param {Object} listener Listener containing the component and params + * @private + */ +Range.prototype._onMouseDown = function(event, listener) { + event = event || window.event; + var params = listener.params; + + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); + if (!leftButtonDown) { + return; + } + + // get mouse position + params.mouseX = util.getPageX(event); + params.mouseY = util.getPageY(event); + params.previousLeft = 0; + params.previousOffset = 0; + + params.moved = false; + params.start = this.start; + params.end = this.end; + + var frame = listener.component.frame; + if (frame) { + frame.style.cursor = 'move'; + } + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the timeaxis, + // so we can remove the eventlisteners lateron in the function onmouseup + var me = this; + if (!params.onMouseMove) { + params.onMouseMove = function (event) { + me._onMouseMove(event, listener); + }; + util.addEventListener(document, "mousemove", params.onMouseMove); + } + if (!params.onMouseUp) { + params.onMouseUp = function (event) { + me._onMouseUp(event, listener); + }; + util.addEventListener(document, "mouseup", params.onMouseUp); + } + + util.preventDefault(event); +}; + +/** + * Perform moving operating. + * This function activated from within the funcion TimeAxis._onMouseDown(). + * @param {Event} event + * @param {Object} listener + * @private + */ +Range.prototype._onMouseMove = function (event, listener) { + event = event || window.event; + + var params = listener.params; + + // calculate change in mouse position + var mouseX = util.getPageX(event); + var mouseY = util.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; + var diff = (listener.direction == 'horizontal') ? diffX : diffY; + + // if mouse movement is big enough, register it as a "moved" event + if (Math.abs(diff) >= 1) { + params.moved = true; + } + + var interval = (params.end - params.start); + var width = (listener.direction == 'horizontal') ? + listener.component.width : listener.component.height; + var diffRange = -diff / width * interval; + this._applyRange(params.start + diffRange, params.end + diffRange); + + // fire a rangechange event + this._trigger('rangechange'); + + util.preventDefault(event); +}; + +/** + * Stop moving operating. + * This function activated from within the function Range._onMouseDown(). + * @param {event} event + * @param {Object} listener + * @private + */ +Range.prototype._onMouseUp = function (event, listener) { + event = event || window.event; + + var params = listener.params; + + if (listener.component.frame) { + listener.component.frame.style.cursor = 'auto'; + } + + // remove event listeners here, important for Safari + if (params.onMouseMove) { + util.removeEventListener(document, "mousemove", params.onMouseMove); + params.onMouseMove = null; + } + if (params.onMouseUp) { + util.removeEventListener(document, "mouseup", params.onMouseUp); + params.onMouseUp = null; + } + //util.preventDefault(event); + + if (params.moved) { + // fire a rangechanged event + this._trigger('rangechanged'); + } +}; + +/** + * Event handler for mouse wheel event, used to zoom + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {Event} event + * @param {Object} listener + * @private + */ +Range.prototype._onMouseWheel = function(event, listener) { + event = 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) { + var me = this; + var zoom = function () { + // perform the zoom action. Delta is normally 1 or -1 + var zoomFactor = delta / 5.0; + var zoomAround = null; + var frame = listener.component.frame; + if (frame) { + var size, conversion; + if (listener.direction == 'horizontal') { + size = listener.component.width; + conversion = me.conversion(size); + var frameLeft = util.getAbsoluteLeft(frame); + var mouseX = util.getPageX(event); + zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset; + } + else { + size = listener.component.height; + conversion = me.conversion(size); + var frameTop = util.getAbsoluteTop(frame); + var mouseY = util.getPageY(event); + zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset; + } + } + + me.zoom(zoomFactor, zoomAround); + }; + + zoom(); + } + + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here... + util.preventDefault(event); +}; + + +/** + * Zoom the range 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 {Number} zoomAround Value around which will be zoomed. Optional + */ +Range.prototype.zoom = function(zoomFactor, zoomAround) { + // if zoomAroundDate is not provided, take it half between start Date and end Date + if (zoomAround == null) { + zoomAround = (this.start + this.end) / 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 and end relative to the zoomAround value + var startDiff = (this.start - zoomAround); + var endDiff = (this.end - zoomAround); + + // calculate new start and end + var newStart = this.start - startDiff * zoomFactor; + var newEnd = this.end - endDiff * zoomFactor; + + this.setRange(newStart, newEnd); +}; + +/** + * Move the range with a given factor to the left or right. Start and end + * value will be adjusted. For example, try moveFactor = 0.1 or -0.1 + * @param {Number} moveFactor Moving amount. Positive value will move right, + * negative value will move left + */ +Range.prototype.move = function(moveFactor) { + // zoom start Date and end Date relative to the zoomAroundDate + var diff = (this.end - this.start); + + // apply new values + var newStart = this.start + diff * moveFactor; + var newEnd = this.end + diff * moveFactor; + + // TODO: reckon with min and max range + + this.start = newStart; + this.end = newEnd; +}; + +/** + * Move the range to a new center point + * @param {Number} moveTo New center point of the range + */ +Range.prototype.moveTo = function(moveTo) { + var center = (this.start + this.end) / 2; + + var diff = center - moveTo; + + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; + + this.setRange(newStart, newEnd); +} + +/** + * @constructor Controller + * + * A Controller controls the reflows and repaints of all visual components + */ +function Controller () { + this.id = util.randomUUID(); + this.components = {}; + + this.repaintTimer = undefined; + this.reflowTimer = undefined; +} + +/** + * Add a component to the controller + * @param {Component} component + */ +Controller.prototype.add = function add(component) { + // validate the component + if (component.id == undefined) { + throw new Error('Component has no field id'); + } + if (!(component instanceof Component) && !(component instanceof Controller)) { + throw new TypeError('Component must be an instance of ' + + 'prototype Component or Controller'); + } + + // add the component + component.controller = this; + this.components[component.id] = component; +}; + +/** + * Remove a component from the controller + * @param {Component | String} component + */ +Controller.prototype.remove = function remove(component) { + var id; + for (id in this.components) { + if (this.components.hasOwnProperty(id)) { + if (id == component || this.components[id] == component) { + break; + } + } + } + + if (id) { + delete this.components[id]; + } +}; + +/** + * Request a reflow. The controller will schedule a reflow + * @param {Boolean} [force] If true, an immediate reflow is forced. Default + * is false. + */ +Controller.prototype.requestReflow = function requestReflow(force) { + if (force) { + this.reflow(); + } + else { + if (!this.reflowTimer) { + var me = this; + this.reflowTimer = setTimeout(function () { + me.reflowTimer = undefined; + me.reflow(); + }, 0); + } + } +}; + +/** + * Request a repaint. The controller will schedule a repaint + * @param {Boolean} [force] If true, an immediate repaint is forced. Default + * is false. + */ +Controller.prototype.requestRepaint = function requestRepaint(force) { + if (force) { + this.repaint(); + } + else { + if (!this.repaintTimer) { + var me = this; + this.repaintTimer = setTimeout(function () { + me.repaintTimer = undefined; + me.repaint(); + }, 0); + } + } +}; + +/** + * Repaint all components + */ +Controller.prototype.repaint = function repaint() { + var changed = false; + + // cancel any running repaint request + if (this.repaintTimer) { + clearTimeout(this.repaintTimer); + this.repaintTimer = undefined; + } + + var done = {}; + + function repaint(component, id) { + if (!(id in done)) { + // first repaint the components on which this component is dependent + if (component.depends) { + component.depends.forEach(function (dep) { + repaint(dep, dep.id); + }); + } + if (component.parent) { + repaint(component.parent, component.parent.id); + } + + // repaint the component itself and mark as done + changed = component.repaint() || changed; + done[id] = true; + } + } + + util.forEach(this.components, repaint); + + // immediately reflow when needed + if (changed) { + this.reflow(); + } + // TODO: limit the number of nested reflows/repaints, prevent loop +}; + +/** + * Reflow all components + */ +Controller.prototype.reflow = function reflow() { + var resized = false; + + // cancel any running repaint request + if (this.reflowTimer) { + clearTimeout(this.reflowTimer); + this.reflowTimer = undefined; + } + + var done = {}; + + function reflow(component, id) { + if (!(id in done)) { + // first reflow the components on which this component is dependent + if (component.depends) { + component.depends.forEach(function (dep) { + reflow(dep, dep.id); + }); + } + if (component.parent) { + reflow(component.parent, component.parent.id); + } + + // reflow the component itself and mark as done + resized = component.reflow() || resized; + done[id] = true; + } + } + + util.forEach(this.components, reflow); + + // immediately repaint when needed + if (resized) { + this.repaint(); + } + // TODO: limit the number of nested reflows/repaints, prevent loop +}; + +/** + * Prototype for visual components + */ +function Component () { + this.id = null; + this.parent = null; + this.depends = null; + this.controller = null; + this.options = null; + + this.frame = null; // main DOM element + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; +} + +/** + * Set parameters for the frame. Parameters will be merged in current parameter + * set. + * @param {Object} options Available parameters: + * {String | function} [className] + * {EventBus} [eventBus] + * {String | Number | function} [left] + * {String | Number | function} [top] + * {String | Number | function} [width] + * {String | Number | function} [height] + */ +Component.prototype.setOptions = function setOptions(options) { + if (options) { + util.extend(this.options, options); + + if (this.controller) { + this.requestRepaint(); + this.requestReflow(); + } + } +}; + +/** + * Get an option value by name + * The function will first check this.options object, and else will check + * this.defaultOptions. + * @param {String} name + * @return {*} value + */ +Component.prototype.getOption = function getOption(name) { + var value; + if (this.options) { + value = this.options[name]; + } + if (value === undefined && this.defaultOptions) { + value = this.defaultOptions[name]; + } + return value; +}; + +/** + * Get the container element of the component, which can be used by a child to + * add its own widgets. Not all components do have a container for childs, in + * that case null is returned. + * @returns {HTMLElement | null} container + */ +// TODO: get rid of the getContainer and getFrame methods, provide these via the options +Component.prototype.getContainer = function getContainer() { + // should be implemented by the component + return null; +}; + +/** + * Get the frame element of the component, the outer HTML DOM element. + * @returns {HTMLElement | null} frame + */ +Component.prototype.getFrame = function getFrame() { + return this.frame; +}; + +/** + * Repaint the component + * @return {Boolean} changed + */ +Component.prototype.repaint = function repaint() { + // should be implemented by the component + return false; +}; + +/** + * Reflow the component + * @return {Boolean} resized + */ +Component.prototype.reflow = function reflow() { + // should be implemented by the component + return false; +}; + +/** + * Hide the component from the DOM + * @return {Boolean} changed + */ +Component.prototype.hide = function hide() { + if (this.frame && this.frame.parentNode) { + this.frame.parentNode.removeChild(this.frame); + return true; + } + else { + return false; + } +}; + +/** + * Show the component in the DOM (when not already visible). + * A repaint will be executed when the component is not visible + * @return {Boolean} changed + */ +Component.prototype.show = function show() { + if (!this.frame || !this.frame.parentNode) { + return this.repaint(); + } + else { + return false; + } +}; + +/** + * Request a repaint. The controller will schedule a repaint + */ +Component.prototype.requestRepaint = function requestRepaint() { + if (this.controller) { + this.controller.requestRepaint(); + } + else { + throw new Error('Cannot request a repaint: no controller configured'); + // TODO: just do a repaint when no parent is configured? + } +}; + +/** + * Request a reflow. The controller will schedule a reflow + */ +Component.prototype.requestReflow = function requestReflow() { + if (this.controller) { + this.controller.requestReflow(); + } + else { + throw new Error('Cannot request a reflow: no controller configured'); + // TODO: just do a reflow when no parent is configured? + } +}; + +/** + * A panel can contain components + * @param {Component} [parent] + * @param {Component[]} [depends] Components on which this components depends + * (except for the parent) + * @param {Object} [options] Available parameters: + * {String | Number | function} [left] + * {String | Number | function} [top] + * {String | Number | function} [width] + * {String | Number | function} [height] + * {String | function} [className] + * @constructor Panel + * @extends Component + */ +function Panel(parent, depends, options) { + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.options = options || {}; +} + +Panel.prototype = new Component(); + +/** + * Set options. Will extend the current options. + * @param {Object} [options] Available parameters: + * {String | function} [className] + * {String | Number | function} [left] + * {String | Number | function} [top] + * {String | Number | function} [width] + * {String | Number | function} [height] + */ +Panel.prototype.setOptions = Component.prototype.setOptions; + +/** + * Get the container element of the panel, which can be used by a child to + * add its own widgets. + * @returns {HTMLElement} container + */ +Panel.prototype.getContainer = function () { + return this.frame; +}; + +/** + * Repaint the component + * @return {Boolean} changed + */ +Panel.prototype.repaint = function () { + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + frame = this.frame; + if (!frame) { + frame = document.createElement('div'); + frame.className = 'panel'; + + var className = options.className; + if (className) { + if (typeof className == 'function') { + util.addClassName(frame, String(className())); + } + else { + util.addClassName(frame, String(className)); + } + } + + this.frame = frame; + changed += 1; + } + if (!frame.parentNode) { + if (!this.parent) { + throw new Error('Cannot repaint panel: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint panel: parent has no container element'); + } + parentContainer.appendChild(frame); + changed += 1; + } + + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, '100%')); + + return (changed > 0); +}; + +/** + * Reflow the component + * @return {Boolean} resized + */ +Panel.prototype.reflow = function () { + var changed = 0, + update = util.updateProperty, + frame = this.frame; + + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', frame.offsetHeight); + } + else { + changed += 1; + } + + return (changed > 0); +}; + +/** + * A root panel can hold components. The root panel must be initialized with + * a DOM element as container. + * @param {HTMLElement} container + * @param {Object} [options] Available parameters: see RootPanel.setOptions. + * @constructor RootPanel + * @extends Panel + */ +function RootPanel(container, options) { + this.id = util.randomUUID(); + this.container = container; + + this.options = options || {}; + this.defaultOptions = { + autoResize: true + }; + + this.listeners = {}; // event listeners +} + +RootPanel.prototype = new Panel(); + +/** + * Set options. Will extend the current options. + * @param {Object} [options] Available parameters: + * {String | function} [className] + * {String | Number | function} [left] + * {String | Number | function} [top] + * {String | Number | function} [width] + * {String | Number | function} [height] + * {Boolean | function} [autoResize] + */ +RootPanel.prototype.setOptions = Component.prototype.setOptions; + +/** + * Repaint the component + * @return {Boolean} changed + */ +RootPanel.prototype.repaint = function () { + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + frame = this.frame; + + if (!frame) { + frame = document.createElement('div'); + frame.className = 'vis timeline rootpanel'; + + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); + } + + this.frame = frame; + + changed += 1; + } + if (!frame.parentNode) { + if (!this.container) { + throw new Error('Cannot repaint root panel: no container attached'); + } + this.container.appendChild(frame); + changed += 1; + } + + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, '100%')); + + this._updateEventEmitters(); + this._updateWatch(); + + return (changed > 0); +}; + +/** + * Reflow the component + * @return {Boolean} resized + */ +RootPanel.prototype.reflow = function () { + var changed = 0, + update = util.updateProperty, + frame = this.frame; + + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', frame.offsetHeight); + } + else { + changed += 1; + } + + return (changed > 0); +}; + +/** + * Update watching for resize, depending on the current option + * @private + */ +RootPanel.prototype._updateWatch = function () { + var autoResize = this.getOption('autoResize'); + if (autoResize) { + this._watch(); + } + else { + this._unwatch(); + } +}; + +/** + * Watch for changes in the size of the frame. On resize, the Panel will + * automatically redraw itself. + * @private + */ +RootPanel.prototype._watch = function () { + var me = this; + + this._unwatch(); + + var checkSize = function () { + var autoResize = me.getOption('autoResize'); + if (!autoResize) { + // stop watching when the option autoResize is changed to false + me._unwatch(); + return; + } + + if (me.frame) { + // check whether the frame is resized + if ((me.frame.clientWidth != me.width) || + (me.frame.clientHeight != me.height)) { + me.requestReflow(); + } + } + }; + + // TODO: automatically cleanup the event listener when the frame is deleted + util.addEventListener(window, 'resize', checkSize); + + this.watchTimer = setInterval(checkSize, 1000); +}; + +/** + * Stop watching for a resize of the frame. + * @private + */ +RootPanel.prototype._unwatch = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; + } + + // TODO: remove event listener on window.resize +}; + +/** + * Event handler + * @param {String} event name of the event, for example 'click', 'mousemove' + * @param {function} callback callback handler, invoked with the raw HTML Event + * as parameter. + */ +RootPanel.prototype.on = function (event, callback) { + // register the listener at this component + var arr = this.listeners[event]; + if (!arr) { + arr = []; + this.listeners[event] = arr; + } + arr.push(callback); + + this._updateEventEmitters(); +}; + +/** + * Update the event listeners for all event emitters + * @private + */ +RootPanel.prototype._updateEventEmitters = function () { + if (this.listeners) { + var me = this; + util.forEach(this.listeners, function (listeners, event) { + if (!me.emitters) { + me.emitters = {}; + } + if (!(event in me.emitters)) { + // create event + var frame = me.frame; + if (frame) { + //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging + var callback = function(event) { + listeners.forEach(function (listener) { + // TODO: filter on event target! + listener(event); + }); + }; + me.emitters[event] = callback; + util.addEventListener(frame, event, callback); + } + } + }); + + // TODO: be able to delete event listeners + // TODO: be able to move event listeners to a parent when available + } +}; + +/** + * A horizontal time axis + * @param {Component} parent + * @param {Component[]} [depends] Components on which this components depends + * (except for the parent) + * @param {Object} [options] See TimeAxis.setOptions for the available + * options. + * @constructor TimeAxis + * @extends Component + */ +function TimeAxis (parent, depends, options) { + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.dom = { + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [], + redundant: { + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [] + } + }; + this.props = { + range: { + start: 0, + end: 0, + minimumStep: 0 + }, + lineTop: 0 + }; + + this.options = options || {}; + this.defaultOptions = { + orientation: 'bottom', // supported: 'top', 'bottom' + // TODO: implement timeaxis orientations 'left' and 'right' + showMinorLabels: true, + showMajorLabels: true + }; + + this.conversion = null; + this.range = null; +} + +TimeAxis.prototype = new Component(); + +// TODO: comment options +TimeAxis.prototype.setOptions = Component.prototype.setOptions; + +/** + * Set a range (start and end) + * @param {Range | Object} range A Range or an object containing start and end. + */ +TimeAxis.prototype.setRange = function (range) { + if (!(range instanceof Range) && (!range || !range.start || !range.end)) { + throw new TypeError('Range must be an instance of Range, ' + + 'or an object containing start and end.'); + } + this.range = range; +}; + +/** + * Convert a position on screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + */ +TimeAxis.prototype.toTime = 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 + * @param {Date} time A date + * @return {int} x The position on the screen in pixels which corresponds + * with the given date. + * @private + */ +TimeAxis.prototype.toScreen = function(time) { + var conversion = this.conversion; + return (time.valueOf() - conversion.offset) * conversion.factor; +}; + +/** + * Repaint the component + * @return {Boolean} changed + */ +TimeAxis.prototype.repaint = function () { + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + orientation = this.getOption('orientation'), + props = this.props, + step = this.step; + + var frame = this.frame; + if (!frame) { + frame = document.createElement('div'); + this.frame = frame; + changed += 1; + } + frame.className = 'axis ' + orientation; + // TODO: custom className? + + if (!frame.parentNode) { + if (!this.parent) { + throw new Error('Cannot repaint time axis: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint time axis: parent has no container element'); + } + parentContainer.appendChild(frame); + + changed += 1; + } + + var parent = frame.parentNode; + if (parent) { + var beforeChild = frame.nextSibling; + parent.removeChild(frame); // take frame offline while updating (is almost twice as fast) + + var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ? + (this.props.parentHeight - this.height) + 'px' : + '0px'; + changed += update(frame.style, 'top', asSize(options.top, defaultTop)); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + + // get characters width and height + this._repaintMeasureChars(); + + if (this.step) { + this._repaintStart(); + + step.first(); + var xFirstMajorLabel = undefined; + var max = 0; + while (step.hasNext() && max < 1000) { + max++; + var cur = step.getCurrent(), + x = this.toScreen(cur), + isMajor = step.isMajor(); + + // TODO: lines must have a width, such that we can create css backgrounds + + if (this.getOption('showMinorLabels')) { + this._repaintMinorText(x, step.getLabelMinor()); + } + + if (isMajor && this.getOption('showMajorLabels')) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; + } + this._repaintMajorText(x, step.getLabelMajor()); + } + this._repaintMajorLine(x); + } + else { + this._repaintMinorLine(x); + } + + step.next(); + } + + // create a major label on the left when needed + if (this.getOption('showMajorLabels')) { + var leftTime = this.toTime(0), + leftText = step.getLabelMajor(leftTime), + widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation + + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText); + } + } + + this._repaintEnd(); + } + + this._repaintLine(); + + // put frame online again + if (beforeChild) { + parent.insertBefore(frame, beforeChild); + } + else { + parent.appendChild(frame) + } + } + + return (changed > 0); +}; + +/** + * Start a repaint. Move all DOM elements to a redundant list, where they + * can be picked for re-use, or can be cleaned up in the end + * @private + */ +TimeAxis.prototype._repaintStart = function () { + var dom = this.dom, + redundant = dom.redundant; + + redundant.majorLines = dom.majorLines; + redundant.majorTexts = dom.majorTexts; + redundant.minorLines = dom.minorLines; + redundant.minorTexts = dom.minorTexts; + + dom.majorLines = []; + dom.majorTexts = []; + dom.minorLines = []; + dom.minorTexts = []; +}; + +/** + * End a repaint. Cleanup leftover DOM elements in the redundant list + * @private + */ +TimeAxis.prototype._repaintEnd = function () { + util.forEach(this.dom.redundant, function (arr) { + while (arr.length) { + var elem = arr.pop(); + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + }); +}; + + +/** + * Create a minor label for the axis at position x + * @param {Number} x + * @param {String} text + * @private + */ +TimeAxis.prototype._repaintMinorText = function (x, text) { + // reuse redundant label + var label = this.dom.redundant.minorTexts.shift(); + + if (!label) { + // create new label + var content = document.createTextNode(''); + label = document.createElement('div'); + label.appendChild(content); + label.className = 'text minor'; + this.frame.appendChild(label); + } + this.dom.minorTexts.push(label); + + label.childNodes[0].nodeValue = text; + label.style.left = x + 'px'; + label.style.top = this.props.minorLabelTop + 'px'; + //label.title = title; // TODO: this is a heavy operation +}; + +/** + * Create a Major label for the axis at position x + * @param {Number} x + * @param {String} text + * @private + */ +TimeAxis.prototype._repaintMajorText = function (x, text) { + // reuse redundant label + var label = this.dom.redundant.majorTexts.shift(); + + if (!label) { + // create label + var content = document.createTextNode(text); + label = document.createElement('div'); + label.className = 'text major'; + label.appendChild(content); + this.frame.appendChild(label); + } + this.dom.majorTexts.push(label); + + label.childNodes[0].nodeValue = text; + label.style.top = this.props.majorLabelTop + 'px'; + label.style.left = x + 'px'; + //label.title = title; // TODO: this is a heavy operation +}; + +/** + * Create a minor line for the axis at position x + * @param {Number} x + * @private + */ +TimeAxis.prototype._repaintMinorLine = function (x) { + // reuse redundant line + var line = this.dom.redundant.minorLines.shift(); + + if (!line) { + // create vertical line + line = document.createElement('div'); + line.className = 'grid vertical minor'; + this.frame.appendChild(line); + } + this.dom.minorLines.push(line); + + var props = this.props; + line.style.top = props.minorLineTop + 'px'; + line.style.height = props.minorLineHeight + 'px'; + line.style.left = (x - props.minorLineWidth / 2) + 'px'; +}; + +/** + * Create a Major line for the axis at position x + * @param {Number} x + * @private + */ +TimeAxis.prototype._repaintMajorLine = function (x) { + // reuse redundant line + var line = this.dom.redundant.majorLines.shift(); + + if (!line) { + // create vertical line + line = document.createElement('DIV'); + line.className = 'grid vertical major'; + this.frame.appendChild(line); + } + this.dom.majorLines.push(line); + + var props = this.props; + line.style.top = props.majorLineTop + 'px'; + line.style.left = (x - props.majorLineWidth / 2) + 'px'; + line.style.height = props.majorLineHeight + 'px'; +}; + + +/** + * Repaint the horizontal line for the axis + * @private + */ +TimeAxis.prototype._repaintLine = function() { + var line = this.dom.line, + frame = this.frame, + options = this.options; + + // line before all axis elements + if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) { + if (line) { + // put this line at the end of all childs + frame.removeChild(line); + frame.appendChild(line); + } + else { + // create the axis line + line = document.createElement('div'); + line.className = 'grid horizontal major'; + frame.appendChild(line); + this.dom.line = line; + } + + line.style.top = this.props.lineTop + 'px'; + } + else { + if (line && axis.parentElement) { + frame.removeChild(axis.line); + delete this.dom.line; + } + } +}; + +/** + * Create characters used to determine the size of text on the axis + * @private + */ +TimeAxis.prototype._repaintMeasureChars = function () { + // calculate the width and height of a single character + // this is used to calculate the step size, and also the positioning of the + // axis + var dom = this.dom, + text; + + if (!dom.measureCharMinor) { + text = document.createTextNode('0'); + var measureCharMinor = document.createElement('DIV'); + measureCharMinor.className = 'text minor measure'; + measureCharMinor.appendChild(text); + this.frame.appendChild(measureCharMinor); + + dom.measureCharMinor = measureCharMinor; + } + + if (!dom.measureCharMajor) { + text = document.createTextNode('0'); + var measureCharMajor = document.createElement('DIV'); + measureCharMajor.className = 'text major measure'; + measureCharMajor.appendChild(text); + this.frame.appendChild(measureCharMajor); + + dom.measureCharMajor = measureCharMajor; + } +}; + +/** + * Reflow the component + * @return {Boolean} resized + */ +TimeAxis.prototype.reflow = function () { + var changed = 0, + update = util.updateProperty, + frame = this.frame, + range = this.range; + + if (!range) { + throw new Error('Cannot repaint time axis: no range configured'); + } + + if (frame) { + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + + // calculate size of a character + var props = this.props, + showMinorLabels = this.getOption('showMinorLabels'), + showMajorLabels = this.getOption('showMajorLabels'), + measureCharMinor = this.dom.measureCharMinor, + measureCharMajor = this.dom.measureCharMajor; + if (measureCharMinor) { + props.minorCharHeight = measureCharMinor.clientHeight; + props.minorCharWidth = measureCharMinor.clientWidth; + } + if (measureCharMajor) { + props.majorCharHeight = measureCharMajor.clientHeight; + props.majorCharWidth = measureCharMajor.clientWidth; + } + + var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0; + if (parentHeight != props.parentHeight) { + props.parentHeight = parentHeight; + changed += 1; + } + switch (this.getOption('orientation')) { + case 'bottom': + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + + props.minorLabelTop = 0; + props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight; + + props.minorLineTop = -this.top; + props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0); + props.minorLineWidth = 1; // TODO: really calculate width + + props.majorLineTop = -this.top; + props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0); + props.majorLineWidth = 1; // TODO: really calculate width + + props.lineTop = 0; + + break; + + case 'top': + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + + props.majorLabelTop = 0; + props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight; + + props.minorLineTop = props.minorLabelTop; + props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top); + props.minorLineWidth = 1; // TODO: really calculate width + + props.majorLineTop = 0; + props.majorLineHeight = Math.max(parentHeight - this.top); + props.majorLineWidth = 1; // TODO: really calculate width + + props.lineTop = props.majorLabelHeight + props.minorLabelHeight; + + break; + + default: + throw new Error('Unkown orientation "' + this.getOption('orientation') + '"'); + } + + var height = props.minorLabelHeight + props.majorLabelHeight; + changed += update(this, 'width', frame.offsetWidth); + changed += update(this, 'height', height); + + // calculate range and step + this._updateConversion(); + + var start = util.convert(range.start, 'Date'), + end = util.convert(range.end, 'Date'), + minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0); + this.step = new TimeStep(start, end, minimumStep); + changed += update(props.range, 'start', start.valueOf()); + changed += update(props.range, 'end', end.valueOf()); + changed += update(props.range, 'minimumStep', minimumStep.valueOf()); + } + + return (changed > 0); +}; + +/** + * Calculate the factor and offset to convert a position on screen to the + * corresponding date and vice versa. + * After the method _updateConversion is executed once, the methods toTime + * and toScreen can be used. + * @private + */ +TimeAxis.prototype._updateConversion = function() { + var range = this.range; + if (!range) { + throw new Error('No range configured'); + } + + if (range.conversion) { + this.conversion = range.conversion(this.width); + } + else { + this.conversion = Range.conversion(range.start, range.end, this.width); + } +}; + +/** + * A current time bar + * @param {Component} parent + * @param {Component[]} [depends] Components on which this components depends + * (except for the parent) + * @param {Object} [options] Available parameters: + * {Boolean} [showCurrentTime] + * @constructor CurrentTime + * @extends Component + */ + +function CurrentTime (parent, depends, options) { + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.options = options || {}; + this.defaultOptions = { + showCurrentTime: false + }; +} + +CurrentTime.prototype = new Component(); + +CurrentTime.prototype.setOptions = Component.prototype.setOptions; + +/** + * Get the container element of the bar, which can be used by a child to + * add its own widgets. + * @returns {HTMLElement} container + */ +CurrentTime.prototype.getContainer = function () { + return this.frame; +}; + +/** + * Repaint the component + * @return {Boolean} changed + */ +CurrentTime.prototype.repaint = function () { + var bar = this.frame, + parent = this.parent, + parentContainer = parent.parent.getContainer(); + + if (!parent) { + throw new Error('Cannot repaint bar: no parent attached'); + } + + if (!parentContainer) { + throw new Error('Cannot repaint bar: parent has no container element'); + } + + if (!this.getOption('showCurrentTime')) { + if (bar) { + parentContainer.removeChild(bar); + delete this.frame; + } + + return; + } + + if (!bar) { + bar = document.createElement('div'); + bar.className = 'currenttime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + + parentContainer.appendChild(bar); + this.frame = bar; + } + + if (!parent.conversion) { + parent._updateConversion(); + } + + var now = new Date(); + var x = parent.toScreen(now); + + bar.style.left = x + 'px'; + bar.title = 'Current time: ' + now; + + // start a timer to adjust for the new time + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; + } + + var timeline = this; + var interval = 1 / parent.conversion.factor / 2; + + if (interval < 30) { + interval = 30; + } + + this.currentTimeTimer = setTimeout(function() { + timeline.repaint(); + }, interval); + + return false; +}; + +/** + * An ItemSet holds a set of items and ranges which can be displayed in a + * range. The width is determined by the parent of the ItemSet, and the height + * is determined by the size of the items. + * @param {Component} parent + * @param {Component[]} [depends] Components on which this components depends + * (except for the parent) + * @param {Object} [options] See ItemSet.setOptions for the available + * options. + * @constructor ItemSet + * @extends Panel + */ +// TODO: improve performance by replacing all Array.forEach with a for loop +function ItemSet(parent, depends, options) { + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + // one options object is shared by this itemset and all its items + this.options = options || {}; + this.defaultOptions = { + type: 'box', + align: 'center', + orientation: 'bottom', + margin: { + axis: 20, + item: 10 + }, + padding: 5 + }; + + this.dom = {}; + + var me = this; + this.itemsData = null; // DataSet + this.range = null; // Range or Object {start: number, end: number} + + this.listeners = { + 'add': function (event, params, senderId) { + if (senderId != me.id) { + me._onAdd(params.items); + } + }, + 'update': function (event, params, senderId) { + if (senderId != me.id) { + me._onUpdate(params.items); + } + }, + 'remove': function (event, params, senderId) { + if (senderId != me.id) { + me._onRemove(params.items); + } + } + }; + + this.items = {}; // object with an Item for every data item + this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' + this.stack = new Stack(this, Object.create(this.options)); + this.conversion = null; + + // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis +} + +ItemSet.prototype = new Panel(); + +// available item types will be registered here +ItemSet.types = { + box: ItemBox, + range: ItemRange, + point: ItemPoint +}; + +/** + * Set options for the ItemSet. Existing options will be extended/overwritten. + * @param {Object} [options] The following options are available: + * {String | function} [className] + * class name for the itemset + * {String} [type] + * Default type for the items. Choose from 'box' + * (default), 'point', or 'range'. The default + * Style can be overwritten by individual items. + * {String} align + * Alignment for the items, only applicable for + * ItemBox. Choose 'center' (default), 'left', or + * 'right'. + * {String} orientation + * Orientation of the item set. Choose 'top' or + * 'bottom' (default). + * {Number} margin.axis + * Margin between the axis and the items in pixels. + * Default is 20. + * {Number} margin.item + * Margin between items in pixels. Default is 10. + * {Number} padding + * Padding of the contents of an item in pixels. + * Must correspond with the items css. Default is 5. + */ +ItemSet.prototype.setOptions = Component.prototype.setOptions; + +/** + * Set range (start and end). + * @param {Range | Object} range A Range or an object containing start and end. + */ +ItemSet.prototype.setRange = function setRange(range) { + if (!(range instanceof Range) && (!range || !range.start || !range.end)) { + throw new TypeError('Range must be an instance of Range, ' + + 'or an object containing start and end.'); + } + this.range = range; +}; + +/** + * Repaint the component + * @return {Boolean} changed + */ +ItemSet.prototype.repaint = function repaint() { + var changed = 0, + update = util.updateProperty, + asSize = util.option.asSize, + options = this.options, + orientation = this.getOption('orientation'), + defaultOptions = this.defaultOptions, + frame = this.frame; + + if (!frame) { + frame = document.createElement('div'); + frame.className = 'itemset'; + + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); + } + + // create background panel + var background = document.createElement('div'); + background.className = 'background'; + frame.appendChild(background); + this.dom.background = background; + + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; + + // create axis panel + var axis = document.createElement('div'); + axis.className = 'itemset-axis'; + //frame.appendChild(axis); + this.dom.axis = axis; + + this.frame = frame; + changed += 1; + } + + if (!this.parent) { + throw new Error('Cannot repaint itemset: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint itemset: parent has no container element'); + } + if (!frame.parentNode) { + parentContainer.appendChild(frame); + changed += 1; + } + if (!this.dom.axis.parentNode) { + parentContainer.appendChild(this.dom.axis); + changed += 1; + } + + // reposition frame + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + + // reposition axis + changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px')); + changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%')); + if (orientation == 'bottom') { + changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px'); + } + else { // orientation == 'top' + changed += update(this.dom.axis.style, 'top', this.top + 'px'); + } + + this._updateConversion(); + + var me = this, + queue = this.queue, + itemsData = this.itemsData, + items = this.items, + dataOptions = { + // TODO: cleanup + // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className'] + }; + + // show/hide added/changed/removed items + Object.keys(queue).forEach(function (id) { + //var entry = queue[id]; + var action = queue[id]; + var item = items[id]; + //var item = entry.item; + //noinspection FallthroughInSwitchStatementJS + switch (action) { + case 'add': + case 'update': + var itemData = itemsData && itemsData.get(id, dataOptions); + + if (itemData) { + var type = itemData.type || + (itemData.start && itemData.end && 'range') || + options.type || + 'box'; + var constructor = ItemSet.types[type]; + + // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, hide and delete the item + changed += item.hide(); + item = null; + } + else { + item.data = itemData; // TODO: create a method item.setData ? + changed++; + } + } + + if (!item) { + // create item + if (constructor) { + item = new constructor(me, itemData, options, defaultOptions); + changed++; + } + else { + throw new TypeError('Unknown item type "' + type + '"'); + } + } + + // force a repaint (not only a reposition) + item.repaint(); + + items[id] = item; + } + + // update queue + delete queue[id]; + break; + + case 'remove': + if (item) { + // remove DOM of the item + changed += item.hide(); + } + + // update lists + delete items[id]; + delete queue[id]; + break; + + default: + console.log('Error: unknown action "' + action + '"'); + } + }); + + // reposition all items. Show items only when in the visible area + util.forEach(this.items, function (item) { + if (item.visible) { + changed += item.show(); + item.reposition(); + } + else { + changed += item.hide(); + } + }); + + return (changed > 0); +}; + +/** + * Get the foreground container element + * @return {HTMLElement} foreground + */ +ItemSet.prototype.getForeground = function getForeground() { + return this.dom.foreground; +}; + +/** + * Get the background container element + * @return {HTMLElement} background + */ +ItemSet.prototype.getBackground = function getBackground() { + return this.dom.background; +}; + +/** + * Get the axis container element + * @return {HTMLElement} axis + */ +ItemSet.prototype.getAxis = function getAxis() { + return this.dom.axis; +}; + +/** + * Reflow the component + * @return {Boolean} resized + */ +ItemSet.prototype.reflow = function reflow () { + var changed = 0, + options = this.options, + marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis, + marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item, + update = util.updateProperty, + asNumber = util.option.asNumber, + asSize = util.option.asSize, + frame = this.frame; + + if (frame) { + this._updateConversion(); + + util.forEach(this.items, function (item) { + changed += item.reflow(); + }); + + // TODO: stack.update should be triggered via an event, in stack itself + // TODO: only update the stack when there are changed items + this.stack.update(); + + var maxHeight = asNumber(options.maxHeight); + var fixedHeight = (asSize(options.height) != null); + var height; + if (fixedHeight) { + height = frame.offsetHeight; + } + else { + // height is not specified, determine the height from the height and positioned items + var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items + if (visibleItems.length) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, (item.top + item.height)); + }); + height = (max - min) + marginAxis + marginItem; + } + else { + height = marginAxis + marginItem; + } + } + if (maxHeight != null) { + height = Math.min(height, maxHeight); + } + changed += update(this, 'height', height); + + // calculate height from items + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + } + else { + changed += 1; + } + + return (changed > 0); +}; + +/** + * Hide this component from the DOM + * @return {Boolean} changed + */ +ItemSet.prototype.hide = function hide() { + var changed = false; + + // remove the DOM + if (this.frame && this.frame.parentNode) { + this.frame.parentNode.removeChild(this.frame); + changed = true; + } + if (this.dom.axis && this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + changed = true; + } + + return changed; +}; + +/** + * Set items + * @param {vis.DataSet | null} items + */ +ItemSet.prototype.setItems = function setItems(items) { + var me = this, + ids, + oldItemsData = this.itemsData; + + // replace the dataset + if (!items) { + this.itemsData = null; + } + else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } + else { + throw new TypeError('Data must be an instance of DataSet'); + } + + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.listeners, function (callback, event) { + oldItemsData.unsubscribe(event, callback); + }); + + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } + + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.listeners, function (callback, event) { + me.itemsData.subscribe(event, callback, id); + }); + + // draw all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + } +}; + +/** + * Get the current items items + * @returns {vis.DataSet | null} + */ +ItemSet.prototype.getItems = function getItems() { + return this.itemsData; +}; + +/** + * Handle updated items + * @param {Number[]} ids + * @private + */ +ItemSet.prototype._onUpdate = function _onUpdate(ids) { + this._toQueue('update', ids); +}; + +/** + * Handle changed items + * @param {Number[]} ids + * @private + */ +ItemSet.prototype._onAdd = function _onAdd(ids) { + this._toQueue('add', ids); +}; + +/** + * Handle removed items + * @param {Number[]} ids + * @private + */ +ItemSet.prototype._onRemove = function _onRemove(ids) { + this._toQueue('remove', ids); +}; + +/** + * Put items in the queue to be added/updated/remove + * @param {String} action can be 'add', 'update', 'remove' + * @param {Number[]} ids + */ +ItemSet.prototype._toQueue = function _toQueue(action, ids) { + var queue = this.queue; + ids.forEach(function (id) { + queue[id] = action; + }); + + if (this.controller) { + //this.requestReflow(); + this.requestRepaint(); + } +}; + +/** + * Calculate the factor and offset to convert a position on screen to the + * corresponding date and vice versa. + * After the method _updateConversion is executed once, the methods toTime + * and toScreen can be used. + * @private + */ +ItemSet.prototype._updateConversion = function _updateConversion() { + var range = this.range; + if (!range) { + throw new Error('No range configured'); + } + + if (range.conversion) { + this.conversion = range.conversion(this.width); + } + else { + this.conversion = Range.conversion(range.start, range.end, this.width); + } +}; + +/** + * Convert a position on screen (pixels) to a datetime + * Before this method can be used, the method _updateConversion must be + * executed once. + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + */ +ItemSet.prototype.toTime = function toTime(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 _updateConversion 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. + */ +ItemSet.prototype.toScreen = function toScreen(time) { + var conversion = this.conversion; + return (time.valueOf() - conversion.offset) * conversion.factor; +}; + +/** + * @constructor Item + * @param {ItemSet} parent + * @param {Object} data Object containing (optional) parameters type, + * start, end, content, group, className. + * @param {Object} [options] Options to set initial property values + * @param {Object} [defaultOptions] default options + * // TODO: describe available options + */ +function Item (parent, data, options, defaultOptions) { + this.parent = parent; + this.data = data; + this.dom = null; + this.options = options || {}; + this.defaultOptions = defaultOptions || {}; + + this.selected = false; + this.visible = false; + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; +} + +/** + * Select current item + */ +Item.prototype.select = function select() { + this.selected = true; +}; + +/** + * Unselect current item + */ +Item.prototype.unselect = function unselect() { + this.selected = false; +}; + +/** + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed + */ +Item.prototype.show = function show() { + return false; +}; + +/** + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed + */ +Item.prototype.hide = function hide() { + return false; +}; + +/** + * Repaint the item + * @return {Boolean} changed + */ +Item.prototype.repaint = function repaint() { + // should be implemented by the item + return false; +}; + +/** + * Reflow the item + * @return {Boolean} resized + */ +Item.prototype.reflow = function reflow() { + // should be implemented by the item + return false; +}; + +/** + * @constructor ItemBox + * @extends Item + * @param {ItemSet} parent + * @param {Object} data Object containing parameters start + * content, className. + * @param {Object} [options] Options to set initial property values + * @param {Object} [defaultOptions] default options + * // TODO: describe available options + */ +function ItemBox (parent, data, options, defaultOptions) { + this.props = { + dot: { + left: 0, + top: 0, + width: 0, + height: 0 + }, + line: { + top: 0, + left: 0, + width: 0, + height: 0 + } + }; + + Item.call(this, parent, data, options, defaultOptions); +} + +ItemBox.prototype = new Item (null, null); + +/** + * Select the item + * @override + */ +ItemBox.prototype.select = function select() { + this.selected = true; + // TODO: select and unselect +}; + +/** + * Unselect the item + * @override + */ +ItemBox.prototype.unselect = function unselect() { + this.selected = false; + // TODO: select and unselect +}; + +/** + * Repaint the item + * @return {Boolean} changed + */ +ItemBox.prototype.repaint = function repaint() { + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; + + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } + + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } + var background = this.parent.getBackground(); + if (!background) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no background container element'); + } + var axis = this.parent.getAxis(); + if (!background) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no axis container element'); + } + + if (!dom.box.parentNode) { + foreground.appendChild(dom.box); + changed = true; + } + if (!dom.line.parentNode) { + background.appendChild(dom.line); + changed = true; + } + if (!dom.dot.parentNode) { + axis.appendChild(dom.dot); + changed = true; + } + + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } + + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.box.className = 'item box' + className; + dom.line.className = 'item line' + className; + dom.dot.className = 'item dot' + className; + changed = true; + } + } + + return changed; +}; + +/** + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. + * @return {Boolean} changed + */ +ItemBox.prototype.show = function show() { + if (!this.dom || !this.dom.box.parentNode) { + return this.repaint(); + } + else { + return false; + } +}; + +/** + * Hide the item from the DOM (when visible) + * @return {Boolean} changed + */ +ItemBox.prototype.hide = function hide() { + var changed = false, + dom = this.dom; + if (dom) { + if (dom.box.parentNode) { + dom.box.parentNode.removeChild(dom.box); + changed = true; + } + if (dom.line.parentNode) { + dom.line.parentNode.removeChild(dom.line); + } + if (dom.dot.parentNode) { + dom.dot.parentNode.removeChild(dom.dot); + } + } + return changed; +}; + +/** + * Reflow the item: calculate its actual size and position from the DOM + * @return {boolean} resized returns true if the axis is resized + * @override + */ +ItemBox.prototype.reflow = function reflow() { + var changed = 0, + update, + dom, + props, + options, + margin, + start, + align, + orientation, + top, + left, + data, + range; + + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } + + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item + var interval = (range.end - range.start); + this.visible = (data.start > range.start - interval) && (data.start < range.end + interval); + } + else { + this.visible = false; + } + + if (this.visible) { + dom = this.dom; + if (dom) { + update = util.updateProperty; + props = this.props; + options = this.options; + start = this.parent.toScreen(this.data.start); + align = options.align || this.defaultOptions.align; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + orientation = options.orientation || this.defaultOptions.orientation; + + changed += update(props.dot, 'height', dom.dot.offsetHeight); + changed += update(props.dot, 'width', dom.dot.offsetWidth); + changed += update(props.line, 'width', dom.line.offsetWidth); + changed += update(props.line, 'height', dom.line.offsetHeight); + changed += update(props.line, 'top', dom.line.offsetTop); + changed += update(this, 'width', dom.box.offsetWidth); + changed += update(this, 'height', dom.box.offsetHeight); + if (align == 'right') { + left = start - this.width; + } + else if (align == 'left') { + left = start; + } + else { + // default or 'center' + left = start - this.width / 2; + } + changed += update(this, 'left', left); + + changed += update(props.line, 'left', start - props.line.width / 2); + changed += update(props.dot, 'left', start - props.dot.width / 2); + changed += update(props.dot, 'top', -props.dot.height / 2); + if (orientation == 'top') { + top = margin; + + changed += update(this, 'top', top); + } + else { + // default or 'bottom' + var parentHeight = this.parent.height; + top = parentHeight - this.height - margin; + + changed += update(this, 'top', top); + } + } + else { + changed += 1; + } + } + + return (changed > 0); +}; + +/** + * Create an items DOM + * @private + */ +ItemBox.prototype._create = function _create() { + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + + // create the box + dom.box = document.createElement('DIV'); + // className is updated in repaint() + + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'line'; + + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'dot'; + } +}; + +/** + * Reposition the item, recalculate its left, top, and width, using the current + * range and size of the items itemset + * @override + */ +ItemBox.prototype.reposition = function reposition() { + var dom = this.dom, + props = this.props, + orientation = this.options.orientation || this.defaultOptions.orientation; + + if (dom) { + var box = dom.box, + line = dom.line, + dot = dom.dot; + + box.style.left = this.left + 'px'; + box.style.top = this.top + 'px'; + + line.style.left = props.line.left + 'px'; + if (orientation == 'top') { + line.style.top = 0 + 'px'; + line.style.height = this.top + 'px'; + } + else { + // orientation 'bottom' + line.style.top = (this.top + this.height) + 'px'; + line.style.height = Math.max(this.parent.height - this.top - this.height + + this.props.dot.height / 2, 0) + 'px'; + } + + dot.style.left = props.dot.left + 'px'; + dot.style.top = props.dot.top + 'px'; + } +}; + +/** + * @constructor ItemPoint + * @extends Item + * @param {ItemSet} parent + * @param {Object} data Object containing parameters start + * content, className. + * @param {Object} [options] Options to set initial property values + * @param {Object} [defaultOptions] default options + * // TODO: describe available options + */ +function ItemPoint (parent, data, options, defaultOptions) { + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; + + Item.call(this, parent, data, options, defaultOptions); +} + +ItemPoint.prototype = new Item (null, null); + +/** + * Select the item + * @override + */ +ItemPoint.prototype.select = function select() { + this.selected = true; + // TODO: select and unselect +}; + +/** + * Unselect the item + * @override + */ +ItemPoint.prototype.unselect = function unselect() { + this.selected = false; + // TODO: select and unselect +}; + +/** + * Repaint the item + * @return {Boolean} changed + */ +ItemPoint.prototype.repaint = function repaint() { + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; + + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } + + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } + + if (!dom.point.parentNode) { + foreground.appendChild(dom.point); + foreground.appendChild(dom.point); + changed = true; + } + + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } + + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.point.className = 'item point' + className; + changed = true; + } + } + + return changed; +}; + +/** + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. + * @return {Boolean} changed + */ +ItemPoint.prototype.show = function show() { + if (!this.dom || !this.dom.point.parentNode) { + return this.repaint(); + } + else { + return false; + } +}; + +/** + * Hide the item from the DOM (when visible) + * @return {Boolean} changed + */ +ItemPoint.prototype.hide = function hide() { + var changed = false, + dom = this.dom; + if (dom) { + if (dom.point.parentNode) { + dom.point.parentNode.removeChild(dom.point); + changed = true; + } + } + return changed; +}; + +/** + * Reflow the item: calculate its actual size from the DOM + * @return {boolean} resized returns true if the axis is resized + * @override + */ +ItemPoint.prototype.reflow = function reflow() { + var changed = 0, + update, + dom, + props, + options, + margin, + orientation, + start, + top, + data, + range; + + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } + + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item + var interval = (range.end - range.start); + this.visible = (data.start > range.start - interval) && (data.start < range.end); + } + else { + this.visible = false; + } + + if (this.visible) { + dom = this.dom; + if (dom) { + update = util.updateProperty; + props = this.props; + options = this.options; + orientation = options.orientation || this.defaultOptions.orientation; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + start = this.parent.toScreen(this.data.start); + + changed += update(this, 'width', dom.point.offsetWidth); + changed += update(this, 'height', dom.point.offsetHeight); + changed += update(props.dot, 'width', dom.dot.offsetWidth); + changed += update(props.dot, 'height', dom.dot.offsetHeight); + changed += update(props.content, 'height', dom.content.offsetHeight); + + if (orientation == 'top') { + top = margin; + } + else { + // default or 'bottom' + var parentHeight = this.parent.height; + top = Math.max(parentHeight - this.height - margin, 0); + } + changed += update(this, 'top', top); + changed += update(this, 'left', start - props.dot.width / 2); + changed += update(props.content, 'marginLeft', 1.5 * props.dot.width); + //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO + + changed += update(props.dot, 'top', (this.height - props.dot.height) / 2); + } + else { + changed += 1; + } + } + + return (changed > 0); +}; + +/** + * Create an items DOM + * @private + */ +ItemPoint.prototype._create = function _create() { + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + + // background box + dom.point = document.createElement('div'); + // className is updated in repaint() + + // contents box, right from the dot + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.point.appendChild(dom.content); + + // dot at start + dom.dot = document.createElement('div'); + dom.dot.className = 'dot'; + dom.point.appendChild(dom.dot); + } +}; + +/** + * Reposition the item, recalculate its left, top, and width, using the current + * range and size of the items itemset + * @override + */ +ItemPoint.prototype.reposition = function reposition() { + var dom = this.dom, + props = this.props; + + if (dom) { + dom.point.style.top = this.top + 'px'; + dom.point.style.left = this.left + 'px'; + + dom.content.style.marginLeft = props.content.marginLeft + 'px'; + //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO + + dom.dot.style.top = props.dot.top + 'px'; + } +}; + +/** + * @constructor ItemRange + * @extends Item + * @param {ItemSet} parent + * @param {Object} data Object containing parameters start, end + * content, className. + * @param {Object} [options] Options to set initial property values + * @param {Object} [defaultOptions] default options + * // TODO: describe available options + */ +function ItemRange (parent, data, options, defaultOptions) { + this.props = { + content: { + left: 0, + width: 0 + } + }; + + Item.call(this, parent, data, options, defaultOptions); +} + +ItemRange.prototype = new Item (null, null); + +/** + * Select the item + * @override + */ +ItemRange.prototype.select = function select() { + this.selected = true; + // TODO: select and unselect +}; + +/** + * Unselect the item + * @override + */ +ItemRange.prototype.unselect = function unselect() { + this.selected = false; + // TODO: select and unselect +}; + +/** + * Repaint the item + * @return {Boolean} changed + */ +ItemRange.prototype.repaint = function repaint() { + // TODO: make an efficient repaint + var changed = false; + var dom = this.dom; + + if (!dom) { + this._create(); + dom = this.dom; + changed = true; + } + + if (dom) { + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); + } + var foreground = this.parent.getForeground(); + if (!foreground) { + throw new Error('Cannot repaint time axis: ' + + 'parent has no foreground container element'); + } + + if (!dom.box.parentNode) { + foreground.appendChild(dom.box); + changed = true; + } + + // update content + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } + changed = true; + } + + // update class + var className = this.data.className ? (' ' + this.data.className) : ''; + if (this.className != className) { + this.className = className; + dom.box.className = 'item range' + className; + changed = true; + } + } + + return changed; +}; + +/** + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. + * @return {Boolean} changed + */ +ItemRange.prototype.show = function show() { + if (!this.dom || !this.dom.box.parentNode) { + return this.repaint(); + } + else { + return false; + } +}; + +/** + * Hide the item from the DOM (when visible) + * @return {Boolean} changed + */ +ItemRange.prototype.hide = function hide() { + var changed = false, + dom = this.dom; + if (dom) { + if (dom.box.parentNode) { + dom.box.parentNode.removeChild(dom.box); + changed = true; + } + } + return changed; +}; + +/** + * Reflow the item: calculate its actual size from the DOM + * @return {boolean} resized returns true if the axis is resized + * @override + */ +ItemRange.prototype.reflow = function reflow() { + var changed = 0, + dom, + props, + options, + margin, + padding, + parent, + start, + end, + data, + range, + update, + box, + parentWidth, + contentLeft, + orientation, + top; + + if (this.data.start == undefined) { + throw new Error('Property "start" missing in item ' + this.data.id); + } + if (this.data.end == undefined) { + throw new Error('Property "end" missing in item ' + this.data.id); + } + + data = this.data; + range = this.parent && this.parent.range; + if (data && range) { + // TODO: account for the width of the item. Take some margin + this.visible = (data.start < range.end) && (data.end > range.start); + } + else { + this.visible = false; + } + + if (this.visible) { + dom = this.dom; + if (dom) { + props = this.props; + options = this.options; + parent = this.parent; + start = parent.toScreen(this.data.start); + end = parent.toScreen(this.data.end); + update = util.updateProperty; + box = dom.box; + parentWidth = parent.width; + orientation = options.orientation || this.defaultOptions.orientation; + margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; + padding = options.padding || this.defaultOptions.padding; + + changed += update(props.content, 'width', dom.content.offsetWidth); + + changed += update(this, 'height', box.offsetHeight); + + // limit the width of the this, as browsers cannot draw very wide divs + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } + + // when range exceeds left of the window, position the contents at the left of the visible area + if (start < 0) { + contentLeft = Math.min(-start, + (end - start - props.content.width - 2 * padding)); + // TODO: remove the need for options.padding. it's terrible. + } + else { + contentLeft = 0; + } + changed += update(props.content, 'left', contentLeft); + + if (orientation == 'top') { + top = margin; + changed += update(this, 'top', top); + } + else { + // default or 'bottom' + top = parent.height - this.height - margin; + changed += update(this, 'top', top); + } + + changed += update(this, 'left', start); + changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width; + } + else { + changed += 1; + } + } + + return (changed > 0); +}; + +/** + * Create an items DOM + * @private + */ +ItemRange.prototype._create = function _create() { + var dom = this.dom; + if (!dom) { + this.dom = dom = {}; + // background box + dom.box = document.createElement('div'); + // className is updated in repaint() + + // contents box + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + } +}; + +/** + * Reposition the item, recalculate its left, top, and width, using the current + * range and size of the items itemset + * @override + */ +ItemRange.prototype.reposition = function reposition() { + var dom = this.dom, + props = this.props; + + if (dom) { + dom.box.style.top = this.top + 'px'; + dom.box.style.left = this.left + 'px'; + dom.box.style.width = this.width + 'px'; + + dom.content.style.left = props.content.left + 'px'; + } +}; + +/** + * @constructor Group + * @param {GroupSet} parent + * @param {Number | String} groupId + * @param {Object} [options] Options to set initial property values + * // TODO: describe available options + * @extends Component + */ +function Group (parent, groupId, options) { + this.id = util.randomUUID(); + this.parent = parent; + + this.groupId = groupId; + this.itemset = null; // ItemSet + this.options = options || {}; + this.options.top = 0; + + this.props = { + label: { + width: 0, + height: 0 + } + }; + + this.top = 0; + this.left = 0; + this.width = 0; + this.height = 0; +} + +Group.prototype = new Component(); + +// TODO: comment +Group.prototype.setOptions = Component.prototype.setOptions; + +/** + * Get the container element of the panel, which can be used by a child to + * add its own widgets. + * @returns {HTMLElement} container + */ +Group.prototype.getContainer = function () { + return this.parent.getContainer(); +}; + +/** + * Set item set for the group. The group will create a view on the itemset, + * filtered by the groups id. + * @param {DataSet | DataView} items + */ +Group.prototype.setItems = function setItems(items) { + if (this.itemset) { + // remove current item set + this.itemset.hide(); + this.itemset.setItems(); + + this.parent.controller.remove(this.itemset); + this.itemset = null; + } + + if (items) { + var groupId = this.groupId; + + var itemsetOptions = Object.create(this.options); + this.itemset = new ItemSet(this, null, itemsetOptions); + this.itemset.setRange(this.parent.range); + + this.view = new DataView(items, { + filter: function (item) { + return item.group == groupId; + } + }); + this.itemset.setItems(this.view); + + this.parent.controller.add(this.itemset); + } +}; + +/** + * Repaint the item + * @return {Boolean} changed + */ +Group.prototype.repaint = function repaint() { + return false; +}; + +/** + * Reflow the item + * @return {Boolean} resized + */ +Group.prototype.reflow = function reflow() { + var changed = 0, + update = util.updateProperty; + + changed += update(this, 'top', this.itemset ? this.itemset.top : 0); + changed += update(this, 'height', this.itemset ? this.itemset.height : 0); + + // TODO: reckon with the height of the group label + + if (this.label) { + var inner = this.label.firstChild; + changed += update(this.props.label, 'width', inner.clientWidth); + changed += update(this.props.label, 'height', inner.clientHeight); + } + else { + changed += update(this.props.label, 'width', 0); + changed += update(this.props.label, 'height', 0); + } + + return (changed > 0); +}; + +/** + * An GroupSet holds a set of groups + * @param {Component} parent + * @param {Component[]} [depends] Components on which this components depends + * (except for the parent) + * @param {Object} [options] See GroupSet.setOptions for the available + * options. + * @constructor GroupSet + * @extends Panel + */ +function GroupSet(parent, depends, options) { + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.options = options || {}; + + this.range = null; // Range or Object {start: number, end: number} + this.itemsData = null; // DataSet with items + this.groupsData = null; // DataSet with groups + + this.groups = {}; // map with groups + + this.dom = {}; + this.props = { + labels: { + width: 0 + } + }; + + // TODO: implement right orientation of the labels + + // changes in groups are queued key/value map containing id/action + this.queue = {}; + + var me = this; + this.listeners = { + 'add': function (event, params) { + me._onAdd(params.items); + }, + 'update': function (event, params) { + me._onUpdate(params.items); + }, + 'remove': function (event, params) { + me._onRemove(params.items); + } + }; +} + +GroupSet.prototype = new Panel(); + +/** + * Set options for the GroupSet. Existing options will be extended/overwritten. + * @param {Object} [options] The following options are available: + * {String | function} groupsOrder + * TODO: describe options + */ +GroupSet.prototype.setOptions = Component.prototype.setOptions; + +GroupSet.prototype.setRange = function (range) { + // TODO: implement setRange +}; + +/** + * Set items + * @param {vis.DataSet | null} items + */ +GroupSet.prototype.setItems = function setItems(items) { + this.itemsData = items; + + for (var id in this.groups) { + if (this.groups.hasOwnProperty(id)) { + var group = this.groups[id]; + group.setItems(items); + } + } +}; + +/** + * Get items + * @return {vis.DataSet | null} items + */ +GroupSet.prototype.getItems = function getItems() { + return this.itemsData; +}; + +/** + * Set range (start and end). + * @param {Range | Object} range A Range or an object containing start and end. + */ +GroupSet.prototype.setRange = function setRange(range) { + this.range = range; +}; + +/** + * Set groups + * @param {vis.DataSet} groups + */ +GroupSet.prototype.setGroups = function setGroups(groups) { + var me = this, + ids; + + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.listeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); + + // remove all drawn groups + ids = this.groupsData.getIds(); + this._onRemove(ids); + } + + // replace the dataset + if (!groups) { + this.groupsData = null; + } + else if (groups instanceof DataSet) { + this.groupsData = groups; + } + else { + this.groupsData = new DataSet({ + convert: { + start: 'Date', + end: 'Date' + } + }); + this.groupsData.add(groups); + } + + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.listeners, function (callback, event) { + me.groupsData.subscribe(event, callback, id); + }); + + // draw all new groups + ids = this.groupsData.getIds(); + this._onAdd(ids); + } +}; + +/** + * Get groups + * @return {vis.DataSet | null} groups + */ +GroupSet.prototype.getGroups = function getGroups() { + return this.groupsData; +}; + +/** + * Repaint the component + * @return {Boolean} changed + */ +GroupSet.prototype.repaint = function repaint() { + var changed = 0, + i, id, group, label, + update = util.updateProperty, + asSize = util.option.asSize, + asElement = util.option.asElement, + options = this.options, + frame = this.dom.frame, + labels = this.dom.labels; + + // create frame + if (!this.parent) { + throw new Error('Cannot repaint groupset: no parent attached'); + } + var parentContainer = this.parent.getContainer(); + if (!parentContainer) { + throw new Error('Cannot repaint groupset: parent has no container element'); + } + if (!frame) { + frame = document.createElement('div'); + frame.className = 'groupset'; + this.dom.frame = frame; + + var className = options.className; + if (className) { + util.addClassName(frame, util.option.asString(className)); + } + + changed += 1; + } + if (!frame.parentNode) { + parentContainer.appendChild(frame); + changed += 1; + } + + // create labels + var labelContainer = asElement(options.labelContainer); + if (!labelContainer) { + throw new Error('Cannot repaint groupset: option "labelContainer" not defined'); + } + if (!labels) { + labels = document.createElement('div'); + labels.className = 'labels'; + //frame.appendChild(labels); + this.dom.labels = labels; + } + if (!labels.parentNode || labels.parentNode != labelContainer) { + if (labels.parentNode) { + labels.parentNode.removeChild(labels.parentNode); + } + labelContainer.appendChild(labels); + } + + // reposition frame + changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + changed += update(frame.style, 'top', asSize(options.top, '0px')); + changed += update(frame.style, 'left', asSize(options.left, '0px')); + changed += update(frame.style, 'width', asSize(options.width, '100%')); + + // reposition labels + changed += update(labels.style, 'top', asSize(options.top, '0px')); + + var me = this, + queue = this.queue, + groups = this.groups, + groupsData = this.groupsData; + + // show/hide added/changed/removed groups + var ids = Object.keys(queue); + if (ids.length) { + ids.forEach(function (id) { + var action = queue[id]; + var group = groups[id]; + + //noinspection FallthroughInSwitchStatementJS + switch (action) { + case 'add': + case 'update': + if (!group) { + var groupOptions = Object.create(me.options); + group = new Group(me, id, groupOptions); + group.setItems(me.itemsData); // attach items data + groups[id] = group; + + me.controller.add(group); + } + + // TODO: update group data + group.data = groupsData.get(id); + + delete queue[id]; + break; + + case 'remove': + if (group) { + group.setItems(); // detach items data + delete groups[id]; + + me.controller.remove(group); + } + + // update lists + delete queue[id]; + break; + + default: + console.log('Error: unknown action "' + action + '"'); + } + }); + + // the groupset depends on each of the groups + //this.depends = this.groups; // TODO: gives a circular reference through the parent + + // TODO: apply dependencies of the groupset + + // update the top positions of the groups in the correct order + var orderedGroups = this.groupsData.getIds({ + order: this.options.groupsOrder + }); + for (i = 0; i < orderedGroups.length; i++) { + (function (group, prevGroup) { + var top = 0; + if (prevGroup) { + top = function () { + // TODO: top must reckon with options.maxHeight + return prevGroup.top + prevGroup.height; + } + } + group.setOptions({ + top: top + }); + })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]); + } + + // (re)create the labels + while (labels.firstChild) { + labels.removeChild(labels.firstChild); + } + for (i = 0; i < orderedGroups.length; i++) { + id = orderedGroups[i]; + label = this._createLabel(id); + labels.appendChild(label); + } + + changed++; + } + + // reposition the labels + // TODO: labels are not displayed correctly when orientation=='top' + // TODO: width of labelPanel is not immediately updated on a change in groups + for (id in groups) { + if (groups.hasOwnProperty(id)) { + group = groups[id]; + label = group.label; + if (label) { + label.style.top = group.top + 'px'; + label.style.height = group.height + 'px'; + } + } + } + + return (changed > 0); +}; + +/** + * Create a label for group with given id + * @param {Number} id + * @return {Element} label + * @private + */ +GroupSet.prototype._createLabel = function(id) { + var group = this.groups[id]; + var label = document.createElement('div'); + label.className = 'label'; + var inner = document.createElement('div'); + inner.className = 'inner'; + label.appendChild(inner); + + var content = group.data && group.data.content; + if (content instanceof Element) { + inner.appendChild(content); + } + else if (content != undefined) { + inner.innerHTML = content; + } + + var className = group.data && group.data.className; + if (className) { + util.addClassName(label, className); + } + + group.label = label; // TODO: not so nice, parking labels in the group this way!!! + + return label; +}; + +/** + * Get container element + * @return {HTMLElement} container + */ +GroupSet.prototype.getContainer = function getContainer() { + return this.dom.frame; +}; + +/** + * Get the width of the group labels + * @return {Number} width + */ +GroupSet.prototype.getLabelsWidth = function getContainer() { + return this.props.labels.width; +}; + +/** + * Reflow the component + * @return {Boolean} resized + */ +GroupSet.prototype.reflow = function reflow() { + var changed = 0, + id, group, + options = this.options, + update = util.updateProperty, + asNumber = util.option.asNumber, + asSize = util.option.asSize, + frame = this.dom.frame; + + if (frame) { + var maxHeight = asNumber(options.maxHeight); + var fixedHeight = (asSize(options.height) != null); + var height; + if (fixedHeight) { + height = frame.offsetHeight; + } + else { + // height is not specified, calculate the sum of the height of all groups + height = 0; + + for (id in this.groups) { + if (this.groups.hasOwnProperty(id)) { + group = this.groups[id]; + height += group.height; + } + } + } + if (maxHeight != null) { + height = Math.min(height, maxHeight); + } + changed += update(this, 'height', height); + + changed += update(this, 'top', frame.offsetTop); + changed += update(this, 'left', frame.offsetLeft); + changed += update(this, 'width', frame.offsetWidth); + } + + // calculate the maximum width of the labels + var width = 0; + for (id in this.groups) { + if (this.groups.hasOwnProperty(id)) { + group = this.groups[id]; + var labelWidth = group.props && group.props.label && group.props.label.width || 0; + width = Math.max(width, labelWidth); + } + } + changed += update(this.props.labels, 'width', width); + + return (changed > 0); +}; + +/** + * Hide the component from the DOM + * @return {Boolean} changed + */ +GroupSet.prototype.hide = function hide() { + if (this.dom.frame && this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + return true; + } + else { + return false; + } +}; + +/** + * Show the component in the DOM (when not already visible). + * A repaint will be executed when the component is not visible + * @return {Boolean} changed + */ +GroupSet.prototype.show = function show() { + if (!this.dom.frame || !this.dom.frame.parentNode) { + return this.repaint(); + } + else { + return false; + } +}; + +/** + * Handle updated groups + * @param {Number[]} ids + * @private + */ +GroupSet.prototype._onUpdate = function _onUpdate(ids) { + this._toQueue(ids, 'update'); +}; + +/** + * Handle changed groups + * @param {Number[]} ids + * @private + */ +GroupSet.prototype._onAdd = function _onAdd(ids) { + this._toQueue(ids, 'add'); +}; + +/** + * Handle removed groups + * @param {Number[]} ids + * @private + */ +GroupSet.prototype._onRemove = function _onRemove(ids) { + this._toQueue(ids, 'remove'); +}; + +/** + * Put groups in the queue to be added/updated/remove + * @param {Number[]} ids + * @param {String} action can be 'add', 'update', 'remove' + */ +GroupSet.prototype._toQueue = function _toQueue(ids, action) { + var queue = this.queue; + ids.forEach(function (id) { + queue[id] = action; + }); + + if (this.controller) { + //this.requestReflow(); + this.requestRepaint(); + } +}; + +/** + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | DataTable} [items] + * @param {Object} [options] See Timeline.setOptions for the available options. + * @constructor + */ +function Timeline (container, items, options) { + var me = this; + this.options = util.extend({ + orientation: 'bottom', + min: null, + max: null, + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds + // moveable: true, // TODO: option moveable + // zoomable: true, // TODO: option zoomable + showMinorLabels: true, + showMajorLabels: true, + showCurrentTime: false, + autoResize: false + }, options); + + // controller + this.controller = new Controller(); + + // root panel + if (!container) { + throw new Error('No container element provided'); + } + var rootOptions = Object.create(this.options); + rootOptions.height = function () { + if (me.options.height) { + // fixed height + return me.options.height; + } + else { + // auto height + return me.timeaxis.height + me.content.height; + } + }; + this.rootPanel = new RootPanel(container, rootOptions); + this.controller.add(this.rootPanel); + + // item panel + var itemOptions = Object.create(this.options); + itemOptions.left = function () { + return me.labelPanel.width; + }; + itemOptions.width = function () { + return me.rootPanel.width - me.labelPanel.width; + }; + itemOptions.top = null; + itemOptions.height = null; + this.itemPanel = new Panel(this.rootPanel, [], itemOptions); + this.controller.add(this.itemPanel); + + // label panel + var labelOptions = Object.create(this.options); + labelOptions.top = null; + labelOptions.left = null; + labelOptions.height = null; + labelOptions.width = function () { + if (me.content && typeof me.content.getLabelsWidth === 'function') { + return me.content.getLabelsWidth(); + } + else { + return 0; + } + }; + this.labelPanel = new Panel(this.rootPanel, [], labelOptions); + this.controller.add(this.labelPanel); + + // range + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.range = new Range({ + start: now.clone().add('days', -3).valueOf(), + end: now.clone().add('days', 4).valueOf() + }); + /* TODO: fix range options + var rangeOptions = Object.create(this.options); + this.range = new Range(rangeOptions); + this.range.setRange( + now.clone().add('days', -3).valueOf(), + now.clone().add('days', 4).valueOf() + ); + */ + // TODO: reckon with options moveable and zoomable + this.range.subscribe(this.rootPanel, 'move', 'horizontal'); + this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); + this.range.on('rangechange', function () { + var force = true; + me.controller.requestReflow(force); + }); + this.range.on('rangechanged', function () { + var force = true; + me.controller.requestReflow(force); + }); + + // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable + + // time axis + var timeaxisOptions = Object.create(rootOptions); + timeaxisOptions.range = this.range; + timeaxisOptions.left = null; + timeaxisOptions.top = null; + timeaxisOptions.width = '100%'; + timeaxisOptions.height = null; + this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions); + this.timeaxis.setRange(this.range); + this.controller.add(this.timeaxis); + + // current time bar + this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); + this.controller.add(this.currenttime); + + // create itemset or groupset + this.setGroups(null); + + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // set data + if (items) { + this.setItems(items); + } +} + +/** + * Set options + * @param {Object} options TODO: describe the available options + */ +Timeline.prototype.setOptions = function (options) { + if (options) { + util.extend(this.options, options); + } + + // TODO: apply range min,max + + this.controller.reflow(); + this.controller.repaint(); +}; + +/** + * Set items + * @param {vis.DataSet | Array | DataTable | null} items + */ +Timeline.prototype.setItems = function(items) { + var initialLoad = (this.itemsData == null); + + // convert to type DataSet when needed + var newItemSet; + if (!items) { + newItemSet = null; + } + else if (items instanceof DataSet) { + newItemSet = items; + } + if (!(items instanceof DataSet)) { + newItemSet = new DataSet({ + convert: { + start: 'Date', + end: 'Date' + } + }); + newItemSet.add(items); + } + + // set items + this.itemsData = newItemSet; + this.content.setItems(newItemSet); + + if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { + // apply the data range as range + var dataRange = this.getItemRange(); + + // add 5% on both sides + var min = dataRange.min; + var max = dataRange.max; + if (min != null && max != null) { + var interval = (max.valueOf() - min.valueOf()); + if (interval <= 0) { + // prevent an empty interval + interval = 24 * 60 * 60 * 1000; // 1 day + } + min = new Date(min.valueOf() - interval * 0.05); + max = new Date(max.valueOf() + interval * 0.05); + } + + // override specified start and/or end date + if (this.options.start != undefined) { + min = new Date(this.options.start.valueOf()); + } + if (this.options.end != undefined) { + max = new Date(this.options.end.valueOf()); + } + + // apply range if there is a min or max available + if (min != null || max != null) { + this.range.setRange(min, max); + } + } +}; + +/** + * Set groups + * @param {vis.DataSet | Array | DataTable} groups + */ +Timeline.prototype.setGroups = function(groups) { + var me = this; + this.groupsData = groups; + + // switch content type between ItemSet or GroupSet when needed + var type = this.groupsData ? GroupSet : ItemSet; + if (!(this.content instanceof type)) { + // remove old content set + if (this.content) { + this.content.hide(); + if (this.content.setItems) { + this.content.setItems(); // disconnect from items + } + if (this.content.setGroups) { + this.content.setGroups(); // disconnect from groups + } + this.controller.remove(this.content); + } + + // create new content set + var options = Object.create(this.options); + util.extend(options, { + top: function () { + if (me.options.orientation == 'top') { + return me.timeaxis.height; + } + else { + return me.itemPanel.height - me.timeaxis.height - me.content.height; + } + }, + left: null, + width: '100%', + height: function () { + if (me.options.height) { + return me.itemPanel.height - me.timeaxis.height; + } + else { + return null; + } + }, + maxHeight: function () { + if (me.options.maxHeight) { + if (!util.isNumber(me.options.maxHeight)) { + throw new TypeError('Number expected for property maxHeight'); + } + return me.options.maxHeight - me.timeaxis.height; + } + else { + return null; + } + }, + labelContainer: function () { + return me.labelPanel.getContainer(); + } + }); + this.content = new type(this.itemPanel, [this.timeaxis], options); + if (this.content.setRange) { + this.content.setRange(this.range); + } + if (this.content.setItems) { + this.content.setItems(this.itemsData); + } + if (this.content.setGroups) { + this.content.setGroups(this.groupsData); + } + this.controller.add(this.content); + } +}; + +/** + * Get the data range of the item set. + * @returns {{min: Date, max: Date}} range A range with a start and end Date. + * When no minimum is found, min==null + * When no maximum is found, max==null + */ +Timeline.prototype.getItemRange = function getItemRange() { + // calculate min from start filed + var itemsData = this.itemsData, + min = null, + max = null; + + if (itemsData) { + // calculate the minimum value of the field 'start' + var minItem = itemsData.min('start'); + min = minItem ? minItem.start.valueOf() : null; + + // calculate maximum value of fields 'start' and 'end' + var maxStartItem = itemsData.max('start'); + if (maxStartItem) { + max = maxStartItem.start.valueOf(); + } + var maxEndItem = itemsData.max('end'); + if (maxEndItem) { + if (max == null) { + max = maxEndItem.end.valueOf(); + } + else { + max = Math.max(max, maxEndItem.end.valueOf()); + } + } + } + + return { + min: (min != null) ? new Date(min) : null, + max: (max != null) ? new Date(max) : null + }; +}; + +(function(exports) { + /** + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. + * + * DOT language reference: http://www.graphviz.org/doc/info/lang.html + * + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graph An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges + */ + function parseDOT (data) { + dot = data; + return parseGraph(); + } + + // token types enumeration + var TOKENTYPE = { + NULL : 0, + DELIMITER : 1, + IDENTIFIER: 2, + UNKNOWN : 3 + }; + + // map with all delimiters + var DELIMITERS = { + '{': true, + '}': true, + '[': true, + ']': true, + ';': true, + '=': true, + ',': true, + + '->': true, + '--': true + }; + + var dot = ''; // current dot file + var index = 0; // current index in dot file + var c = ''; // current token character in expr + var token = ''; // current token + var tokenType = TOKENTYPE.NULL; // type of the token + + /** + * Get the first character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function first() { + index = 0; + c = dot.charAt(0); + } + + /** + * Get the next character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function next() { + index++; + c = dot.charAt(index); + } + + /** + * Preview the next character from the dot file. + * @return {String} cNext + */ + function nextPreview() { + return dot.charAt(index + 1); + } + + /** + * Test whether given character is alphabetic or numeric + * @param {String} c + * @return {Boolean} isAlphaNumeric + */ + var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; + function isAlphaNumeric(c) { + return regexAlphaNumeric.test(c); + } + + /** + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + function merge (a, b) { + if (!a) { + a = {}; + } + + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + return a; + } + + /** + * Set a value in an object, where the provided parameter name can be a + * path with nested parameters. For example: + * + * var obj = {a: 2}; + * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} + * + * @param {Object} obj + * @param {String} path A parameter name or dot-separated parameter path, + * like "color.highlight.border". + * @param {*} value + */ + function setValue(obj, path, value) { + var keys = path.split('.'); + var o = obj; + while (keys.length) { + var key = keys.shift(); + if (keys.length) { + // this isn't the end point + if (!o[key]) { + o[key] = {}; + } + o = o[key]; + } + else { + // this is the end point + o[key] = value; + } + } + } + + /** + * Add a node to a graph object. If there is already a node with + * the same id, their attributes will be merged. + * @param {Object} graph + * @param {Object} node + */ + function addNode(graph, node) { + var i, len; + var current = null; + + // find root graph (in case of subgraph) + var graphs = [graph]; // list with all graphs from current graph to root graph + var root = graph; + while (root.parent) { + graphs.push(root.parent); + root = root.parent; + } + + // find existing node (at root level) by its id + if (root.nodes) { + for (i = 0, len = root.nodes.length; i < len; i++) { + if (node.id === root.nodes[i].id) { + current = root.nodes[i]; + break; + } + } + } + + if (!current) { + // this is a new node + current = { + id: node.id + }; + if (graph.node) { + // clone default attributes + current.attr = merge(current.attr, graph.node); + } + } + + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; + + if (!g.nodes) { + g.nodes = []; + } + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); + } + } + + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); + } + } + + /** + * Add an edge to a graph object + * @param {Object} graph + * @param {Object} edge + */ + function addEdge(graph, edge) { + if (!graph.edges) { + graph.edges = []; + } + graph.edges.push(edge); + if (graph.edge) { + var attr = merge({}, graph.edge); // clone default attributes + edge.attr = merge(attr, edge.attr); // merge attributes + } + } + + /** + * Create an edge to a graph object + * @param {Object} graph + * @param {String | Number | Object} from + * @param {String | Number | Object} to + * @param {String} type + * @param {Object | null} attr + * @return {Object} edge + */ + function createEdge(graph, from, to, type, attr) { + var edge = { + from: from, + to: to, + type: type + }; + + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes + } + edge.attr = merge(edge.attr || {}, attr); // merge attributes + + return edge; + } + + /** + * Get next token in the current dot file. + * The token and token type are available as token and tokenType + */ + function getToken() { + tokenType = TOKENTYPE.NULL; + token = ''; + + // skip over whitespaces + while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter + next(); + } + + do { + var isComment = false; + + // skip comment + if (c == '#') { + // find the previous non-space character + var i = index - 1; + while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { + i--; + } + if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { + // the # is at the start of a line, this is indeed a line comment + while (c != '' && c != '\n') { + next(); + } + isComment = true; + } + } + if (c == '/' && nextPreview() == '/') { + // skip line comment + while (c != '' && c != '\n') { + next(); + } + isComment = true; + } + if (c == '/' && nextPreview() == '*') { + // skip block comment + while (c != '') { + if (c == '*' && nextPreview() == '/') { + // end of block comment found. skip these last two characters + next(); + next(); + break; + } + else { + next(); + } + } + isComment = true; + } + + // skip over whitespaces + while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter + next(); + } + } + while (isComment); + + // check for end of dot file + if (c == '') { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; + } + + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; + } + + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; + } + + // check for an identifier (number or string) + // TODO: more precise parsing of numbers/strings (and the port separator ':') + if (isAlphaNumeric(c) || c == '-') { + token += c; + next(); + + while (isAlphaNumeric(c)) { + token += c; + next(); + } + if (token == 'false') { + token = false; // convert to boolean + } + else if (token == 'true') { + token = true; // convert to boolean + } + else if (!isNaN(Number(token))) { + token = Number(token); // convert to number + } + tokenType = TOKENTYPE.IDENTIFIER; + return; + } + + // check for a string enclosed by double quotes + if (c == '"') { + next(); + while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { + token += c; + if (c == '"') { // skip the escape character + next(); + } + next(); + } + if (c != '"') { + throw newSyntaxError('End of string " expected'); + } + next(); + tokenType = TOKENTYPE.IDENTIFIER; + return; + } + + // something unknown is found, wrong characters, a syntax error + tokenType = TOKENTYPE.UNKNOWN; + while (c != '') { + token += c; + next(); + } + throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); + } + + /** + * Parse a graph. + * @returns {Object} graph + */ + function parseGraph() { + var graph = {}; + + first(); + getToken(); + + // optional strict keyword + if (token == 'strict') { + graph.strict = true; + getToken(); + } + + // graph or digraph keyword + if (token == 'graph' || token == 'digraph') { + graph.type = token; + getToken(); + } + + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); + } + + // open angle bracket + if (token != '{') { + throw newSyntaxError('Angle bracket { expected'); + } + getToken(); + + // statements + parseStatements(graph); + + // close angle bracket + if (token != '}') { + throw newSyntaxError('Angle bracket } expected'); + } + getToken(); + + // end of file + if (token !== '') { + throw newSyntaxError('End of file expected'); + } + getToken(); + + // remove temporary default properties + delete graph.node; + delete graph.edge; + delete graph.graph; + + return graph; + } + + /** + * Parse a list with statements. + * @param {Object} graph + */ + function parseStatements (graph) { + while (token !== '' && token != '}') { + parseStatement(graph); + if (token == ';') { + getToken(); + } + } + } + + /** + * Parse a single statement. Can be a an attribute statement, node + * statement, a series of node statements and edge statements, or a + * parameter. + * @param {Object} graph + */ + function parseStatement(graph) { + // parse subgraph + var subgraph = parseSubgraph(graph); + if (subgraph) { + // edge statements + parseEdge(graph, subgraph); + + return; + } + + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; + } + + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier expected'); + } + var id = token; // id can be a string or a number + getToken(); + + if (token == '=') { + // id statement + getToken(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier expected'); + } + graph[id] = token; + getToken(); + // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " + } + else { + parseNodeStatement(graph, id); + } + } + + /** + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph + */ + function parseSubgraph (graph) { + var subgraph = null; + + // optional subgraph keyword + if (token == 'subgraph') { + subgraph = {}; + subgraph.type = 'subgraph'; + getToken(); + + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); + } + } + + // open angle bracket + if (token == '{') { + getToken(); + + if (!subgraph) { + subgraph = {}; + } + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; + + // statements + parseStatements(subgraph); + + // close angle bracket + if (token != '}') { + throw newSyntaxError('Angle bracket } expected'); + } + getToken(); + + // remove temporary default properties + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; + + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; + } + graph.subgraphs.push(subgraph); + } + + return subgraph; + } + + /** + * parse an attribute statement like "node [shape=circle fontSize=16]". + * Available keywords are 'node', 'edge', 'graph'. + * The previous list with default attributes will be replaced + * @param {Object} graph + * @returns {String | null} keyword Returns the name of the parsed attribute + * (node, edge, graph), or null if nothing + * is parsed. + */ + function parseAttributeStatement (graph) { + // attribute statements + if (token == 'node') { + getToken(); + + // node attributes + graph.node = parseAttributeList(); + return 'node'; + } + else if (token == 'edge') { + getToken(); + + // edge attributes + graph.edge = parseAttributeList(); + return 'edge'; + } + else if (token == 'graph') { + getToken(); + + // graph attributes + graph.graph = parseAttributeList(); + return 'graph'; + } + + return null; + } + + /** + * parse a node statement + * @param {Object} graph + * @param {String | Number} id + */ + function parseNodeStatement(graph, id) { + // node statement + var node = { + id: id + }; + var attr = parseAttributeList(); + if (attr) { + node.attr = attr; + } + addNode(graph, node); + + // edge statements + parseEdge(graph, id); + } + + /** + * Parse an edge or a series of edges + * @param {Object} graph + * @param {String | Number} from Id of the from node + */ + function parseEdge(graph, from) { + while (token == '->' || token == '--') { + var to; + var type = token; + getToken(); + + var subgraph = parseSubgraph(graph); + if (subgraph) { + to = subgraph; + } + else { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier or subgraph expected'); + } + to = token; + addNode(graph, { + id: to + }); + getToken(); + } + + // parse edge attributes + var attr = parseAttributeList(); + + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); + + from = to; + } + } + + /** + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr + */ + function parseAttributeList() { + var attr = null; + + while (token == '[') { + getToken(); + attr = {}; + while (token !== '' && token != ']') { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Attribute name expected'); + } + var name = token; + + getToken(); + if (token != '=') { + throw newSyntaxError('Equal sign = expected'); + } + getToken(); + + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Attribute value expected'); + } + var value = token; + setValue(attr, name, value); // name can be a path + + getToken(); + if (token ==',') { + getToken(); + } + } + + if (token != ']') { + throw newSyntaxError('Bracket ] expected'); + } + getToken(); + } + + return attr; + } + + /** + * Create a syntax error with extra information on current token and index. + * @param {String} message + * @returns {SyntaxError} err + */ + function newSyntaxError(message) { + return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); + } + + /** + * Chop off text after a maximum length + * @param {String} text + * @param {Number} maxLength + * @returns {String} + */ + function chop (text, maxLength) { + return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); + } + + /** + * Execute a function fn for each pair of elements in two arrays + * @param {Array | *} array1 + * @param {Array | *} array2 + * @param {function} fn + */ + function forEach2(array1, array2, fn) { + if (array1 instanceof Array) { + array1.forEach(function (elem1) { + if (array2 instanceof Array) { + array2.forEach(function (elem2) { + fn(elem1, elem2); + }); + } + else { + fn(elem1, array2); + } + }); + } + else { + if (array2 instanceof Array) { + array2.forEach(function (elem2) { + fn(array1, elem2); + }); + } + else { + fn(array1, array2); + } + } + } + + /** + * Convert a string containing a graph in DOT language into a map containing + * with nodes and edges in the format of graph. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graphData + */ + function DOTToGraph (data) { + // parse the DOT file + var dotData = parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: {} + }; + + // copy the nodes + if (dotData.nodes) { + dotData.nodes.forEach(function (dotNode) { + var graphNode = { + id: dotNode.id, + label: String(dotNode.label || dotNode.id) + }; + merge(graphNode, dotNode.attr); + if (graphNode.image) { + graphNode.shape = 'image'; + } + graphData.nodes.push(graphNode); + }); + } + + // copy the edges + if (dotData.edges) { + /** + * Convert an edge in DOT format to an edge with VisGraph format + * @param {Object} dotEdge + * @returns {Object} graphEdge + */ + function convertEdge(dotEdge) { + var graphEdge = { + from: dotEdge.from, + to: dotEdge.to + }; + merge(graphEdge, dotEdge.attr); + graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; + return graphEdge; + } + + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; + } + else { + from = { + id: dotEdge.from + } + } + + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; + } + else { + to = { + id: dotEdge.to + } + } + + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + + forEach2(from, to, function (from, to) { + var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + }); + } + + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; + } + + return graphData; + } + + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; + +})(typeof util !== 'undefined' ? util : exports); + +/** + * Canvas shapes used by the Graph + */ +if (typeof CanvasRenderingContext2D !== 'undefined') { + + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function(x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2*Math.PI, false); + }; + + /** + * Draw a square shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r size, width and height of the square + */ + CanvasRenderingContext2D.prototype.square = function(x, y, r) { + this.beginPath(); + this.rect(x - r, y - r, r * 2, r * 2); + }; + + /** + * Draw a triangle shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); + }; + + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; + + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.star = function(x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); + + for (var n = 0; n < 10; n++) { + var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; + this.lineTo( + x + radius * Math.sin(n * 2 * Math.PI / 10), + y - radius * Math.cos(n * 2 * Math.PI / 10) + ); + } + + this.closePath(); + }; + + /** + * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas + */ + CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { + var r2d = Math.PI/180; + if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x + if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y + this.beginPath(); + this.moveTo(x+r,y); + this.lineTo(x+w-r,y); + this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); + this.lineTo(x+w,y+h-r); + this.arc(x+w-r,y+h-r,r,0,r2d*90,false); + this.lineTo(x+r,y+h); + this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); + this.lineTo(x,y+r); + this.arc(x+r,y+r,r,r2d*180,r2d*270,false); + }; + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { + var kappa = .5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + this.beginPath(); + this.moveTo(x, ym); + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + }; + + + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { + var f = 1/3; + var wEllipse = w; + var hEllipse = h * f; + + var kappa = .5522848, + ox = (wEllipse / 2) * kappa, // control point offset horizontal + oy = (hEllipse / 2) * kappa, // control point offset vertical + xe = x + wEllipse, // x-end + ye = y + hEllipse, // y-end + xm = x + wEllipse / 2, // x-middle + ym = y + hEllipse / 2, // y-middle + ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse + + this.beginPath(); + this.moveTo(xe, ym); + + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + + this.lineTo(xe, ymb); + + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + + this.lineTo(x, ym); + }; + + + /** + * Draw an arrow point (no line) + */ + CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { + // tail + var xt = x - length * Math.cos(angle); + var yt = y - length * Math.sin(angle); + + // inner tail + // TODO: allow to customize different shapes + var xi = x - length * 0.9 * Math.cos(angle); + var yi = y - length * 0.9 * Math.sin(angle); + + // left + var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); + var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); + + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; + + /** + * Sets up the dashedLine functionality for drawing + * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas + * @author David Jordan + * @date 2012-08-08 + */ + CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){ + if (!dashArray) dashArray=[10,5]; + if (dashLength==0) dashLength = 0.001; // Hack for Safari + var dashCount = dashArray.length; + this.moveTo(x, y); + var dx = (x2-x), dy = (y2-y); + var slope = dy/dx; + var distRemaining = Math.sqrt( dx*dx + dy*dy ); + var dashIndex=0, draw=true; + while (distRemaining>=0.1){ + var dashLength = dashArray[dashIndex++%dashCount]; + if (dashLength > distRemaining) dashLength = distRemaining; + var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); + if (dx<0) xStep = -xStep; + x += xStep; + y += slope*xStep; + this[draw ? 'lineTo' : 'moveTo'](x,y); + distRemaining -= dashLength; + draw = !draw; + } + }; + + // TODO: add diamond shape +} + +/** + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} properties An object containing properties for the node. All + * properties are optional, except for the id. + * {number} id Id of the node. Required + * {string} label Text label for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} shape Node shape, available: + * "database", "circle", "ellipse", + * "box", "image", "text", "dot", + * "star", "triangle", "triangleDown", + * "square" + * {string} image An image url + * {string} title An title text, can be HTML + * {anytype} group A group name or number + * @param {Graph.Images} imagelist A list with images. Only needed + * when the node has an image + * @param {Graph.Groups} grouplist A list with groups. Needed for + * retrieving group properties + * @param {Object} constants An object with default values for + * example for the color + */ +function Node(properties, imagelist, grouplist, constants) { + this.selected = false; + + this.edges = []; // all edges connected to this node + this.group = constants.nodes.group; + + this.fontSize = constants.nodes.fontSize; + this.fontFace = constants.nodes.fontFace; + this.fontColor = constants.nodes.fontColor; + + this.color = constants.nodes.color; + + // set defaults for the properties + this.id = undefined; + this.shape = constants.nodes.shape; + this.image = constants.nodes.image; + this.x = 0; + this.y = 0; + this.xFixed = false; + this.yFixed = false; + this.radius = constants.nodes.radius; + this.radiusFixed = false; + this.radiusMin = constants.nodes.radiusMin; + this.radiusMax = constants.nodes.radiusMax; + + this.imagelist = imagelist; + this.grouplist = grouplist; + + this.setProperties(properties, constants); + + // mass, force, velocity + this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.fx = 0.0; // external force x + this.fy = 0.0; // external force y + this.vx = 0.0; // velocity x + this.vy = 0.0; // velocity y + this.minForce = constants.minForce; + this.damping = 0.9; // damping factor +}; + +/** + * Attach a edge to the node + * @param {Edge} edge + */ +Node.prototype.attachEdge = function(edge) { + if (this.edges.indexOf(edge) == -1) { + this.edges.push(edge); + } + this._updateMass(); +}; + +/** + * Detach a edge from the node + * @param {Edge} edge + */ +Node.prototype.detachEdge = function(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + this._updateMass(); +}; + +/** + * Update the nodes mass, which is determined by the number of edges connecting + * to it (more edges -> heavier node). + * @private + */ +Node.prototype._updateMass = function() { + this.mass = 50 + 20 * this.edges.length; // kg +}; + +/** + * Set or overwrite properties for the node + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Node.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // basic properties + if (properties.id != undefined) {this.id = properties.id;} + if (properties.label != undefined) {this.label = properties.label;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.group != undefined) {this.group = properties.group;} + if (properties.x != undefined) {this.x = properties.x;} + if (properties.y != undefined) {this.y = properties.y;} + if (properties.value != undefined) {this.value = properties.value;} + + if (this.id === undefined) { + throw "Node must have an id"; + } + + // copy group properties + if (this.group) { + var groupObj = this.grouplist.get(this.group); + for (var prop in groupObj) { + if (groupObj.hasOwnProperty(prop)) { + this[prop] = groupObj[prop]; + } + } + } + + // individual shape properties + if (properties.shape != undefined) {this.shape = properties.shape;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.color != undefined) {this.color = Node.parseColor(properties.color);} + + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + + + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + this.xFixed = this.xFixed || (properties.x != undefined); + this.yFixed = this.yFixed || (properties.y != undefined); + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + + if (this.shape == 'image') { + this.radiusMin = constants.nodes.widthMin; + this.radiusMax = constants.nodes.widthMax; + } + + // choose draw method depending on the shape + switch (this.shape) { + case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; + case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break; + case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; + case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; + // TODO: add diamond shape + case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; + case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; + case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; + case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; + case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; + case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; + case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; + default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; + } + + // reset the size of the node, this can be changed + this._reset(); +}; + +/** + * Parse a color property into an object with border, background, and + * hightlight colors + * @param {Object | String} color + * @return {Object} colorObject + */ +Node.parseColor = function(color) { + var c; + if (util.isString(color)) { + c = { + border: color, + background: color, + highlight: { + border: color, + background: color + } + }; + // TODO: automatically generate a nice highlight color + } + else { + c = {}; + c.background = color.background || 'white'; + c.border = color.border || c.background; + if (util.isString(color.highlight)) { + c.highlight = { + border: color.highlight, + background: color.highlight + } + } + else { + c.highlight = {}; + c.highlight.background = color.highlight && color.highlight.background || c.background; + c.highlight.border = color.highlight && color.highlight.border || c.border; + } + } + return c; +}; + +/** + * select this node + */ +Node.prototype.select = function() { + this.selected = true; + this._reset(); +}; + +/** + * unselect this node + */ +Node.prototype.unselect = function() { + this.selected = false; + this._reset(); +}; + +/** + * Reset the calculated size of the node, forces it to recalculate its size + * @private + */ +Node.prototype._reset = function() { + this.width = undefined; + this.height = undefined; +}; + +/** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ +Node.prototype.getTitle = function() { + return this.title; +}; + +/** + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels + */ +Node.prototype.distanceToBorder = function (ctx, angle) { + var borderWidth = 1; + + if (!this.width) { + this.resize(ctx); + } + + //noinspection FallthroughInSwitchStatementJS + switch (this.shape) { + case 'circle': + case 'dot': + return this.radius + borderWidth; + + case 'ellipse': + var a = this.width / 2; + var b = this.height / 2; + var w = (Math.sin(angle) * a); + var h = (Math.cos(angle) * b); + return a * b / Math.sqrt(w * w + h * h); + + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown + + case 'box': + case 'image': + case 'text': + default: + if (this.width) { + return Math.min( + Math.abs(this.width / 2 / Math.cos(angle)), + Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + // TODO: reckon with border radius too in case of box + } + else { + return 0; + } + + } + + // TODO: implement calculation of distance to border for all shapes +}; + +/** + * Set forces acting on the node + * @param {number} fx Force in horizontal direction + * @param {number} fy Force in vertical direction + */ +Node.prototype._setForce = function(fx, fy) { + this.fx = fx; + this.fy = fy; +}; + +/** + * Add forces acting on the node + * @param {number} fx Force in horizontal direction + * @param {number} fy Force in vertical direction + * @private + */ +Node.prototype._addForce = function(fx, fy) { + this.fx += fx; + this.fy += fy; +}; + +/** + * Perform one discrete step for the node + * @param {number} interval Time interval in seconds + */ +Node.prototype.discreteStep = function(interval) { + if (!this.xFixed) { + var dx = -this.damping * this.vx; // damping force + var ax = (this.fx + dx) / this.mass; // acceleration + this.vx += ax / interval; // velocity + this.x += this.vx / interval; // position + } + + if (!this.yFixed) { + var dy = -this.damping * this.vy; // damping force + var ay = (this.fy + dy) / this.mass; // acceleration + this.vy += ay / interval; // velocity + this.y += this.vy / interval; // position + } +}; + + +/** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ +Node.prototype.isFixed = function() { + return (this.xFixed && this.yFixed); +}; + +/** + * Check if this node is moving + * @param {number} vmin the minimum velocity considered as "moving" + * @return {boolean} true if moving, false if it has no velocity + */ +// TODO: replace this method with calculating the kinetic energy +Node.prototype.isMoving = function(vmin) { + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || + (!this.xFixed && Math.abs(this.fx) > this.minForce) || + (!this.yFixed && Math.abs(this.fy) > this.minForce)); +}; + +/** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ +Node.prototype.isSelected = function() { + return this.selected; +}; + +/** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ +Node.prototype.getValue = function() { + return this.value; +}; + +/** + * Calculate the distance from the nodes location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ +Node.prototype.getDistance = function(x, y) { + var dx = this.x - x, + dy = this.y - y; + return Math.sqrt(dx * dx + dy * dy); +}; + + +/** + * Adjust the value range of the node. The node will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Node.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + if (max == min) { + this.radius = (this.radiusMin + this.radiusMax) / 2; + } + else { + var scale = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * scale + this.radiusMin; + } + } +}; + +/** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Node.prototype.draw = function(ctx) { + throw "Draw method not initialized for node"; +}; + +/** + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Node.prototype.resize = function(ctx) { + throw "Resize method not initialized for node"; +}; + +/** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ +Node.prototype.isOverlappingWith = function(obj) { + return (this.left < obj.right && + this.left + this.width > obj.left && + this.top < obj.bottom && + this.top + this.height > obj.top); +}; + +Node.prototype._resizeImage = function (ctx) { + // TODO: pre calculate the image size + if (!this.width) { // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + this.width = width; + this.height = height; + } +}; + +Node.prototype._drawImage = function (ctx) { + this._resizeImage(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + var yLabel; + if (this.imageObj) { + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + yLabel = this.y + this.height / 2; + } + else { + // image still loading... just draw the label for now + yLabel = this.y; + } + + this._label(ctx, this.label, this.x, yLabel, undefined, "top"); +}; + + +Node.prototype._resizeBox = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } +}; + +Node.prototype._drawBox = function (ctx) { + this._resizeBox(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); + ctx.fill(); + ctx.stroke(); + + this._label(ctx, this.label, this.x, this.y); +}; + + +Node.prototype._resizeDatabase = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; + } +}; + +Node.prototype._drawDatabase = function (ctx) { + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); + + this._label(ctx, this.label, this.x, this.y); +}; + + +Node.prototype._resizeCircle = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.radius = diameter / 2; + + this.width = diameter; + this.height = diameter; + } +}; + +Node.prototype._drawCircle = function (ctx) { + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.circle(this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + this._label(ctx, this.label, this.x, this.y); +}; + +Node.prototype._resizeEllipse = function (ctx) { + if (!this.width) { + var textSize = this.getTextSize(ctx); + + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; + } + } +}; + +Node.prototype._drawEllipse = function (ctx) { + this._resizeEllipse(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.ellipse(this.left, this.top, this.width, this.height); + ctx.fill(); + ctx.stroke(); + + this._label(ctx, this.label, this.x, this.y); +}; + +Node.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Node.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Node.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Node.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Node.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Node.prototype._resizeShape = function (ctx) { + if (!this.width) { + var size = 2 * this.radius; + this.width = size; + this.height = size; + } +}; + +Node.prototype._drawShape = function (ctx, shape) { + this._resizeShape(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + + ctx[shape](this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + if (this.label) { + this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top'); + } +}; + +Node.prototype._resizeText = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } +}; + +Node.prototype._drawText = function (ctx) { + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + this._label(ctx, this.label, this.x, this.y); +}; + + +Node.prototype._label = function (ctx, text, x, y, align, baseline) { + if (text) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = align || "center"; + ctx.textBaseline = baseline || "middle"; + + var lines = text.split('\n'), + lineCount = lines.length, + fontSize = (this.fontSize + 4), + yLine = y + (1 - lineCount) / 2 * fontSize; + + for (var i = 0; i < lineCount; i++) { + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; + } + } +}; + + +Node.prototype.getTextSize = function(ctx) { + if (this.label != undefined) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + + var lines = this.label.split('\n'), + height = (this.fontSize + 4) * lines.length, + width = 0; + + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); + } + + return {"width": width, "height": height}; + } + else { + return {"width": 0, "height": 0}; + } +}; + +/** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Graph} graph A graph object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color + */ +function Edge (properties, graph, constants) { + if (!graph) { + throw "No graph provided"; + } + this.graph = graph; + + // initialize constants + this.widthMin = constants.edges.widthMin; + this.widthMax = constants.edges.widthMax; + + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.style = constants.edges.style; + this.title = undefined; + this.width = constants.edges.width; + this.value = undefined; + this.length = constants.edges.length; + + this.from = null; // a node + this.to = null; // a node + this.connected = false; + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength + + this.stiffness = undefined; // depends on the length of the edge + this.color = constants.edges.color; + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties, constants); +} + +/** + * Set or overwrite properties for the edge + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Edge.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + if (properties.from != undefined) {this.fromId = properties.from;} + if (properties.to != undefined) {this.toId = properties.to;} + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.label != undefined) {this.label = properties.label;} + if (this.label) { + this.fontSize = constants.edges.fontSize; + this.fontFace = constants.edges.fontFace; + this.fontColor = constants.edges.fontColor; + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + } + if (properties.title != undefined) {this.title = properties.title;} + if (properties.width != undefined) {this.width = properties.width;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.length != undefined) {this.length = properties.length;} + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (properties.dash) { + if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;} + if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;} + if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;} + } + + if (properties.color != undefined) {this.color = properties.color;} + + // A node is connected when it has a from and to node. + this.connect(); + + this.widthFixed = this.widthFixed || (properties.width != undefined); + this.lengthFixed = this.lengthFixed || (properties.length != undefined); + this.stiffness = 1 / this.length; + + // set draw method based on style + switch (this.style) { + case 'line': this.draw = this._drawLine; break; + case 'arrow': this.draw = this._drawArrow; break; + case 'arrow-center': this.draw = this._drawArrowCenter; break; + case 'dash-line': this.draw = this._drawDashLine; break; + default: this.draw = this._drawLine; break; + } +}; + +/** + * Connect an edge to its nodes + */ +Edge.prototype.connect = function () { + this.disconnect(); + + this.from = this.graph.nodes[this.fromId] || null; + this.to = this.graph.nodes[this.toId] || null; + this.connected = (this.from && this.to); + + if (this.connected) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } + else { + if (this.from) { + this.from.detachEdge(this); + } + if (this.to) { + this.to.detachEdge(this); + } + } +}; + +/** + * Disconnect an edge from its nodes + */ +Edge.prototype.disconnect = function () { + if (this.from) { + this.from.detachEdge(this); + this.from = null; + } + if (this.to) { + this.to.detachEdge(this); + this.to = null; + } + + this.connected = false; +}; + +/** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ +Edge.prototype.getTitle = function() { + return this.title; +}; + + +/** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ +Edge.prototype.getValue = function() { + return this.value; +}; + +/** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Edge.prototype.setValueRange = function(min, max) { + if (!this.widthFixed && this.value !== undefined) { + var factor = (this.widthMax - this.widthMin) / (max - min); + this.width = (this.value - min) * factor + this.widthMin; + } +}; + +/** + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Edge.prototype.draw = function(ctx) { + throw "Method draw not initialized in edge"; +}; + +/** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge + */ +Edge.prototype.isOverlappingWith = function(obj) { + var distMax = 10; + + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; + + + var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return (dist < distMax); +}; + + +/** + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Edge.prototype._drawLine = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + var point; + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw label + if (this.label) { + point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + var x, y; + var radius = this.length / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } +}; + +/** + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width + * @private + */ +Edge.prototype._getLineWidth = function() { + if (this.from.selected || this.to.selected) { + return Math.min(this.width * 2, this.widthMax); + } + else { + return this.width; + } +}; + +/** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Edge.prototype._line = function (ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); +}; + +/** + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private + */ +Edge.prototype._circle = function (ctx, x, y, radius) { + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); +}; + +/** + * Draw label with white background and with the middle at (x, y) + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} x + * @param {Number} y + * @private + */ +Edge.prototype._label = function (ctx, text, x, y) { + if (text) { + // TODO: cache the calculated size + ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = 'white'; + var width = ctx.measureText(text).width; + var height = this.fontSize; + var left = x - width / 2; + var top = y - height / 2; + + ctx.fillRect(left, top, width, height); + + // draw text + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + ctx.fillText(text, left, top); + } +}; + +/** + * Redraw a edge as a dashed line + * Draw this edge in the given canvas + * @author David Jordan + * @date 2012-08-08 + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Edge.prototype._drawDashLine = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw dashed line + ctx.beginPath(); + ctx.lineCap = 'round'; + if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, + [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]); + } + else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, + [this.dash.length,this.dash.gap]); + } + else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + + // draw label + if (this.label) { + var point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } +}; + +/** + * Get a point on a line + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ +Edge.prototype._pointOnLine = function (percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + } +}; + +/** + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ +Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { + var angle = (percentage - 3/8) * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + } +}; + +/** + * Redraw a edge as a line with an arrow halfway the line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Edge.prototype._drawArrowCenter = function(ctx) { + var point; + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw an arrow halfway the line + var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var length = 10 + 5 * this.width; // TODO: make customizable? + point = this._pointOnLine(0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + // draw circle + var x, y; + var radius = this.length / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); + + // draw all arrows + var angle = 0.2 * Math.PI; + var length = 10 + 5 * this.width; // TODO: make customizable? + point = this._pointOnCircle(x, y, radius, 0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } + } +}; + + + +/** + * Redraw a edge as a line with an arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Edge.prototype._drawArrow = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var angle, length; + if (this.from != this.to) { + // calculate length and angle of the line + angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var dx = (this.to.x - this.from.x); + var dy = (this.to.y - this.from.y); + var lEdge = Math.sqrt(dx * dx + dy * dy); + + var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI); + var pFrom = (lEdge - lFrom) / lEdge; + var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x; + var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y; + + var lTo = this.to.distanceToBorder(ctx, angle); + var pTo = (lEdge - lTo) / lEdge; + var xTo = (1 - pTo) * this.from.x + pTo * this.to.x; + var yTo = (1 - pTo) * this.from.y + pTo * this.to.y; + + ctx.beginPath(); + ctx.moveTo(xFrom, yFrom); + ctx.lineTo(xTo, yTo); + ctx.stroke(); + + // draw arrow at the end of the line + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(xTo, yTo, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + var point = this._pointOnLine(0.5); + this._label(ctx, this.label, point.x, point.y); + } + } + else { + // draw circle + var node = this.from; + var x, y, arrow; + var radius = this.length / 4; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + arrow = { + x: x, + y: node.y, + angle: 0.9 * Math.PI + }; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + arrow = { + x: node.x, + y: y, + angle: 0.6 * Math.PI + }; + } + ctx.beginPath(); + // TODO: do not draw a circle, but an arc + // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); + + // draw all arrows + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(arrow.x, arrow.y, arrow.angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } + } +}; + + + +/** + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 + * @private + */ +Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; + + if (u > 1) { + u = 1; + } + else if (u < 0) { + u = 0; + } + + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; + + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance + + return Math.sqrt(dx*dx + dy*dy); +}; + +/** + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} [x] + * @param {Number} [y] + * @param {String} [text] + */ +function Popup(container, x, y, text) { + if (container) { + this.container = container; + } + else { + this.container = document.body; + } + this.x = 0; + this.y = 0; + this.padding = 5; + + if (x !== undefined && y !== undefined ) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } + + // create the frame + this.frame = document.createElement("div"); + var style = this.frame.style; + style.position = "absolute"; + style.visibility = "hidden"; + style.border = "1px solid #666"; + style.color = "black"; + style.padding = this.padding + "px"; + style.backgroundColor = "#FFFFC6"; + style.borderRadius = "3px"; + style.MozBorderRadius = "3px"; + style.WebkitBorderRadius = "3px"; + style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; + style.whiteSpace = "nowrap"; + this.container.appendChild(this.frame); +}; + +/** + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window + */ +Popup.prototype.setPosition = function(x, y) { + this.x = parseInt(x); + this.y = parseInt(y); +}; + +/** + * Set the text for the popup window. This can be HTML code + * @param {string} text + */ +Popup.prototype.setText = function(text) { + this.frame.innerHTML = text; +}; + +/** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ +Popup.prototype.show = function (show) { + if (show === undefined) { + show = true; + } + + if (show) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; + + var top = (this.y - height); + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } + + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; + } + if (left < this.padding) { + left = this.padding; + } + + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + } + else { + this.hide(); + } +}; + +/** + * Hide the popup window + */ +Popup.prototype.hide = function () { + this.frame.style.visibility = "hidden"; +}; + +/** + * @class Groups + * This class can store groups and properties specific for groups. + */ +Groups = function () { + this.clear(); + this.defaultIndex = 0; +}; + + +/** + * default constants for group colors + */ +Groups.DEFAULT = [ + {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue + {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow + {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red + {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green + {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta + {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple + {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange + {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue + {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink + {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint +]; + + +/** + * Clear all groups + */ +Groups.prototype.clear = function () { + this.groups = {}; + this.groups.length = function() + { + var i = 0; + for ( var p in this ) { + if (this.hasOwnProperty(p)) { + i++; + } + } + return i; + } +}; + + +/** + * get group properties of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group properties + */ +Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + + if (group == undefined) { + // create new group + var index = this.defaultIndex % Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.color = Groups.DEFAULT[index]; + this.groups[groupname] = group; + } + + return group; +}; + +/** + * Add a custom group style + * @param {String} groupname + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ +Groups.prototype.add = function (groupname, style) { + this.groups[groupname] = style; + if (style.color) { + style.color = Node.parseColor(style.color); + } + return style; +}; + +/** + * @class Images + * This class loads images and keeps them stored. + */ +Images = function () { + this.images = {}; + + this.callback = undefined; +}; + +/** + * Set an onload callback function. This will be called each time an image + * is loaded + * @param {function} callback + */ +Images.prototype.setOnloadCallback = function(callback) { + this.callback = callback; +}; + +/** + * + * @param {string} url Url of the image + * @return {Image} img The image object + */ +Images.prototype.load = function(url) { + var img = this.images[url]; + if (img == undefined) { + // create the image + var images = this; + img = new Image(); + this.images[url] = img; + img.onload = function() { + if (images.callback) { + images.callback(this); + } + }; + img.src = url; + } + + return img; +}; + +/** + * @constructor Graph + * Create a graph visualization, displaying nodes and edges. + * + * @param {Element} container The DOM element in which the Graph will + * be created. Normally a div element. + * @param {Object} data An object containing parameters + * {Array} nodes + * {Array} edges + * @param {Object} options Options + */ +function Graph (container, data, options) { + // create variables and set default values + this.containerElement = container; + this.width = '100%'; + this.height = '100%'; + this.refreshRate = 50; // milliseconds + this.stabilize = true; // stabilize before displaying the graph + this.selectable = true; + + // set constant values + this.constants = { + nodes: { + radiusMin: 5, + radiusMax: 20, + radius: 5, + distance: 100, // px + shape: 'ellipse', + image: undefined, + widthMin: 16, // px + widthMax: 64, // px + fontColor: 'black', + fontSize: 14, // px + //fontFace: verdana, + fontFace: 'arial', + color: { + border: '#2B7CE9', + background: '#97C2FC', + highlight: { + border: '#2B7CE9', + background: '#D2E5FF' + } + }, + borderColor: '#2B7CE9', + backgroundColor: '#97C2FC', + highlightColor: '#D2E5FF', + group: undefined + }, + edges: { + widthMin: 1, + widthMax: 15, + width: 1, + style: 'line', + color: '#343434', + fontColor: '#343434', + fontSize: 14, // px + fontFace: 'arial', + //distance: 100, //px + length: 100, // px + dash: { + length: 10, + gap: 5, + altLength: undefined + } + }, + minForce: 0.05, + minVelocity: 0.02, // px/s + maxIterations: 1000 // maximum number of iteration to stabilize + }; + + var graph = this; + this.nodes = {}; // object with Node objects + this.edges = {}; // object with Edge objects + // TODO: create a counter to keep track on the number of nodes having values + // TODO: create a counter to keep track on the number of nodes currently moving + // TODO: create a counter to keep track on the number of edges having values + + this.nodesData = null; // A DataSet or DataView + this.edgesData = null; // A DataSet or DataView + + // create event listeners used to subscribe on the DataSets of the nodes and edges + var me = this; + this.nodesListeners = { + 'add': function (event, params) { + me._addNodes(params.items); + me.start(); + }, + 'update': function (event, params) { + me._updateNodes(params.items); + me.start(); + }, + 'remove': function (event, params) { + me._removeNodes(params.items); + me.start(); + } + }; + this.edgesListeners = { + 'add': function (event, params) { + me._addEdges(params.items); + me.start(); + }, + 'update': function (event, params) { + me._updateEdges(params.items); + me.start(); + }, + 'remove': function (event, params) { + me._removeEdges(params.items); + me.start(); + } + }; + + this.groups = new Groups(); // object with groups + this.images = new Images(); // object with images + this.images.setOnloadCallback(function () { + graph._redraw(); + }); + + // properties of the data + this.moving = false; // True if any of the nodes have an undefined position + + this.selection = []; + this.timer = undefined; + + // create a frame and canvas + this._create(); + + // apply options + this.setOptions(options); + + // draw data + this.setData(data); +} + +/** + * Set nodes and edges, and optionally options as well. + * + * @param {Object} data Object containing parameters: + * {Array | DataSet | DataView} [nodes] Array with nodes + * {Array | DataSet | DataView} [edges] Array with edges + * {String} [dot] String containing data in DOT format + * {Options} [options] Object with options + */ +Graph.prototype.setData = function(data) { + if (data && data.dot && (data.nodes || data.edges)) { + throw new SyntaxError('Data must contain either parameter "dot" or ' + + ' parameter pair "nodes" and "edges", but not both.'); + } + + // set options + this.setOptions(data && data.options); + + // set all data + if (data && data.dot) { + // parse DOT file + if(data && data.dot) { + var dotData = vis.util.DOTToGraph(data.dot); + this.setData(dotData); + return; + } + } + else { + this._setNodes(data && data.nodes); + this._setEdges(data && data.edges); + } + + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.start(); +}; + +/** + * Set options + * @param {Object} options + */ +Graph.prototype.setOptions = function (options) { + if (options) { + // retrieve parameter values + if (options.width != undefined) {this.width = options.width;} + if (options.height != undefined) {this.height = options.height;} + if (options.stabilize != undefined) {this.stabilize = options.stabilize;} + if (options.selectable != undefined) {this.selectable = options.selectable;} + + // TODO: work out these options and document them + if (options.edges) { + for (var prop in options.edges) { + if (options.edges.hasOwnProperty(prop)) { + this.constants.edges[prop] = options.edges[prop]; + } + } + + if (options.edges.length != undefined && + options.nodes && options.nodes.distance == undefined) { + this.constants.edges.length = options.edges.length; + this.constants.nodes.distance = options.edges.length * 1.25; + } + + if (!options.edges.fontColor) { + this.constants.edges.fontColor = options.edges.color; + } + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (options.edges.dash) { + if (options.edges.dash.length != undefined) { + this.constants.edges.dash.length = options.edges.dash.length; + } + if (options.edges.dash.gap != undefined) { + this.constants.edges.dash.gap = options.edges.dash.gap; + } + if (options.edges.dash.altLength != undefined) { + this.constants.edges.dash.altLength = options.edges.dash.altLength; + } + } + } + + if (options.nodes) { + for (prop in options.nodes) { + if (options.nodes.hasOwnProperty(prop)) { + this.constants.nodes[prop] = options.nodes[prop]; + } + } + + if (options.nodes.color) { + this.constants.nodes.color = Node.parseColor(options.nodes.color); + } + + /* + if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; + if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; + */ + } + + if (options.groups) { + for (var groupname in options.groups) { + if (options.groups.hasOwnProperty(groupname)) { + var group = options.groups[groupname]; + this.groups.add(groupname, group); + } + } + } + } + + this.setSize(this.width, this.height); + this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); + this._setScale(1); +}; + +/** + * fire an event + * @param {String} event The name of an event, for example 'select' + * @param {Object} params Optional object with event parameters + * @private + */ +Graph.prototype._trigger = function (event, params) { + events.trigger(this, event, params); +}; + + +/** + * Create the main frame for the Graph. + * This function is executed once when a Graph object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + * @private + */ +Graph.prototype._create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } + + this.frame = document.createElement('div'); + this.frame.className = 'graph-frame'; + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; + + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement( 'canvas' ); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement( 'DIV' ); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold' ; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); + } + + var me = this; + this.drag = {}; + this.pinch = {}; + this.hammer = Hammer(this.frame.canvas, { + prevent_default: true + }); + this.hammer.on('tap', me._onTap.bind(me) ); + this.hammer.on('hold', me._onHold.bind(me) ); + this.hammer.on('pinch', me._onPinch.bind(me) ); + this.hammer.on('touch', me._onTouch.bind(me) ); + this.hammer.on('dragstart', me._onDragStart.bind(me) ); + this.hammer.on('drag', me._onDrag.bind(me) ); + this.hammer.on('dragend', me._onDragEnd.bind(me) ); + this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); + this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF + this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); + + // add the frame to the container element + this.containerElement.appendChild(this.frame); +}; + +/** + * + * @param {{x: Number, y: Number}} pointer + * @return {Number | null} node + * @private + */ +Graph.prototype._getNodeAt = function (pointer) { + var x = this._canvasToX(pointer.x); + var y = this._canvasToY(pointer.y); + + var obj = { + left: x, + top: y, + right: x, + bottom: y + }; + + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + var overlappingNodes = this._getNodesOverlappingWith(obj); + return (overlappingNodes.length > 0) ? + overlappingNodes[overlappingNodes.length - 1] : null; +}; + +/** + * Get the pointer location from a touch location + * @param {{pageX: Number, pageY: Number}} touch + * @return {{x: Number, y: Number}} pointer + * @private + */ +Graph.prototype._getPointer = function (touch) { + return { + x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas), + y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas) + }; +}; + +/** + * On start of a touch gesture, store the pointer + * @param event + * @private + */ +Graph.prototype._onTouch = function (event) { + this.drag.pointer = this._getPointer(event.gesture.touches[0]); + this.drag.pinched = false; + this.pinch.scale = this._getScale(); +}; + +/** + * handle drag start event + * @private + */ +Graph.prototype._onDragStart = function () { + var drag = this.drag; + + drag.selection = []; + drag.translation = this._getTranslation(); + drag.nodeId = this._getNodeAt(drag.pointer); + // note: drag.pointer is set in _onTouch to get the initial touch location + + var node = this.nodes[drag.nodeId]; + if (node) { + // select the clicked node if not yet selected + if (!node.isSelected()) { + this._selectNodes([drag.nodeId]); + } + + // create an array with the selected nodes and their original location and status + var me = this; + this.selection.forEach(function (id) { + var node = me.nodes[id]; + if (node) { + var s = { + id: id, + node: node, + + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: node.x, + y: node.y, + xFixed: node.xFixed, + yFixed: node.yFixed + }; + + node.xFixed = true; + node.yFixed = true; + + drag.selection.push(s); + } + }); + + } +}; + +/** + * handle drag event + * @private + */ +Graph.prototype._onDrag = function (event) { + if (this.drag.pinched) { + return; + } + + var pointer = this._getPointer(event.gesture.touches[0]); + + var me = this, + drag = this.drag, + selection = drag.selection; + if (selection && selection.length) { + // calculate delta's and new location + var deltaX = pointer.x - drag.pointer.x, + deltaY = pointer.y - drag.pointer.y; + + // update position of all selected nodes + selection.forEach(function (s) { + var node = s.node; + + if (!s.xFixed) { + node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX); + } + + if (!s.yFixed) { + node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY); + } + }); + + // start animation if not yet running + if (!this.moving) { + this.moving = true; + this.start(); + } + } + else { + // move the graph + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; + + this._setTranslation( + this.drag.translation.x + diffX, + this.drag.translation.y + diffY); + this._redraw(); + + this.moved = true; + } +}; + +/** + * handle drag start event + * @private + */ +Graph.prototype._onDragEnd = function () { + var selection = this.drag.selection; + if (selection) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.xFixed = s.xFixed; + s.node.yFixed = s.yFixed; + }); + } +}; + +/** + * handle tap/click event: select/unselect a node + * @private + */ +Graph.prototype._onTap = function (event) { + var pointer = this._getPointer(event.gesture.touches[0]); + + var nodeId = this._getNodeAt(pointer); + var node = this.nodes[nodeId]; + if (node) { + // select this node + this._selectNodes([nodeId]); + + if (!this.moving) { + this._redraw(); + } + } + else { + // remove selection + this._unselectNodes(); + this._redraw(); + } +}; + +/** + * handle long tap event: multi select nodes + * @private + */ +Graph.prototype._onHold = function (event) { + var pointer = this._getPointer(event.gesture.touches[0]); + var nodeId = this._getNodeAt(pointer); + var node = this.nodes[nodeId]; + if (node) { + if (!node.isSelected()) { + // select this node, keep previous selection + var append = true; + this._selectNodes([nodeId], append); + } + else { + this._unselectNodes([nodeId]); + } + + if (!this.moving) { + this._redraw(); + } + } + else { + // Do nothing + } +}; + +/** + * Handle pinch event + * @param event + * @private + */ +Graph.prototype._onPinch = function (event) { + var pointer = this._getPointer(event.gesture.center); + + this.drag.pinched = true; + if (!('scale' in this.pinch)) { + this.pinch.scale = 1; + } + + // TODO: enable moving while pinching? + var scale = this.pinch.scale * event.gesture.scale; + this._zoom(scale, pointer) +}; + +/** + * Zoom the graph in or out + * @param {Number} scale a number around 1, and between 0.01 and 10 + * @param {{x: Number, y: Number}} pointer + * @return {Number} appliedScale scale is limited within the boundaries + * @private + */ +Graph.prototype._zoom = function(scale, pointer) { + var scaleOld = this._getScale(); + if (scale < 0.01) { + scale = 0.01; + } + if (scale > 10) { + scale = 10; + } + + var translation = this._getTranslation(); + var scaleFrac = scale / scaleOld; + var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + + this._setScale(scale); + this._setTranslation(tx, ty); + this._redraw(); + + return scale; +}; + +/** + * Event handler for mouse wheel event, used to zoom the timeline + * See http://adomas.org/javascript-mouse-wheel/ + * https://github.com/EightMedia/hammer.js/issues/256 + * @param {MouseEvent} event + * @private + */ +Graph.prototype._onMouseWheel = function(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) { + if (!('mouswheelScale' in this.pinch)) { + this.pinch.mouswheelScale = 1; + } + + // calculate the new scale + var scale = this.pinch.mouswheelScale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= (1 + zoom); + + // calculate the pointer location + var gesture = Hammer.event.collectEventData(this, 'scroll', event); + var pointer = this._getPointer(gesture.center); + + // apply the new scale + scale = this._zoom(scale, pointer); + + // store the new, applied scale + this.pinch.mouswheelScale = scale; + } + + // Prevent default actions caused by mouse wheel. + event.preventDefault(); +}; + + +/** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ +Graph.prototype._onMouseMoveTitle = function (event) { + var gesture = Hammer.event.collectEventData(this, 'mousemove', event); + var pointer = this._getPointer(gesture.center); + + // check if the previously selected node is still selected + if (this.popupNode) { + this._checkHidePopup(pointer); + } + + // start a timeout that will check if the mouse is positioned above + // an element + var me = this; + var checkShow = function() { + me._checkShowPopup(pointer); + }; + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running timer + } + if (!this.leftButtonDown) { + this.popupTimer = setTimeout(checkShow, 300); + } +}; + +/** + * Check if there is an element on the given position in the graph + * (a node or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {{x:Number, y:Number}} pointer + * @private + */ +Graph.prototype._checkShowPopup = function (pointer) { + var obj = { + left: this._canvasToX(pointer.x), + top: this._canvasToY(pointer.y), + right: this._canvasToX(pointer.x), + bottom: this._canvasToY(pointer.y) + }; + + var id; + var lastPopupNode = this.popupNode; + + if (this.popupNode == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.nodes; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { + this.popupNode = node; + break; + } + } + } + } + + if (this.popupNode == undefined) { + // search the edges for overlap + var edges = this.edges; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected && (edge.getTitle() != undefined) && + edge.isOverlappingWith(obj)) { + this.popupNode = edge; + break; + } + } + } + } + + if (this.popupNode) { + // show popup message window + if (this.popupNode != lastPopupNode) { + var me = this; + if (!me.popup) { + me.popup = new Popup(me.frame); + } + + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + me.popup.setPosition(pointer.x - 3, pointer.y - 3); + me.popup.setText(me.popupNode.getTitle()); + me.popup.show(); + } + } + else { + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Check if the popup must be hided, which is the case when the mouse is no + * longer hovering on the object + * @param {{x:Number, y:Number}} pointer + * @private + */ +Graph.prototype._checkHidePopup = function (pointer) { + if (!this.popupNode || !this._getNodeAt(pointer) ) { + this.popupNode = undefined; + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Unselect selected nodes. If no selection array is provided, all nodes + * are unselected + * @param {Object[]} selection Array with selection objects, each selection + * object has a parameter row. Optional + * @param {Boolean} triggerSelect If true (default), the select event + * is triggered when nodes are unselected + * @return {Boolean} changed True if the selection is changed + * @private + */ +Graph.prototype._unselectNodes = function(selection, triggerSelect) { + var changed = false; + var i, iMax, id; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + this.nodes[id].unselect(); + + var j = 0; + while (j < this.selection.length) { + if (this.selection[j] == id) { + this.selection.splice(j, 1); + changed = true; + } + else { + j++; + } + } + } + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + id = this.selection[i]; + this.nodes[id].unselect(); + changed = true; + } + this.selection = []; + } + + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this._trigger('select'); + } + + return changed; +}; + +/** + * select all nodes on given location x, y + * @param {Array} selection an array with node ids + * @param {boolean} append If true, the new selection will be appended to the + * current selection (except for duplicate entries) + * @return {Boolean} changed True if the selection is changed + * @private + */ +Graph.prototype._selectNodes = function(selection, append) { + var changed = false; + var i, iMax; + + // TODO: the selectNodes method is a little messy, rework this + + // check if the current selection equals the desired selection + var selectionAlreadyThere = true; + if (selection.length != this.selection.length) { + selectionAlreadyThere = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i] != this.selection[i]) { + selectionAlreadyThere = false; + break; + } + } + } + if (selectionAlreadyThere) { + return changed; + } + + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } + + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var id = selection[i]; + var isDuplicate = (this.selection.indexOf(id) != -1); + if (!isDuplicate) { + this.nodes[id].select(); + this.selection.push(id); + changed = true; + } + } + + if (changed) { + // fire the select event + this._trigger('select'); + } + + return changed; +}; + +/** + * retrieve all nodes overlapping with given object + * @param {Object} obj An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ +Graph.prototype._getNodesOverlappingWith = function (obj) { + var nodes = this.nodes, + overlappingNodes = []; + + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].isOverlappingWith(obj)) { + overlappingNodes.push(id); + } + } + } + + return overlappingNodes; +}; + +/** + * retrieve the currently selected nodes + * @return {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ +Graph.prototype.getSelection = function() { + return this.selection.concat([]); +}; + +/** + * select zero or more nodes + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ +Graph.prototype.setSelection = function(selection) { + var i, iMax, id; + + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + id = this.selection[i]; + this.nodes[id].unselect(); + } + + this.selection = []; + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); + } + node.select(); + this.selection.push(id); + } + + this.redraw(); +}; + +/** + * Validate the selection: remove ids of nodes which no longer exist + * @private + */ +Graph.prototype._updateSelection = function () { + var i = 0; + while (i < this.selection.length) { + var id = this.selection[i]; + if (!this.nodes[id]) { + this.selection.splice(i, 1); + } + else { + i++; + } + } +}; + +/** + * Temporary method to test calculating a hub value for the nodes + * @param {number} level Maximum number edges between two nodes in order + * to call them connected. Optional, 1 by default + * @return {Number[]} connectioncount array with the connection count + * for each node + * @private + */ +Graph.prototype._getConnectionCount = function(level) { + if (level == undefined) { + level = 1; + } + + // get the nodes connected to given nodes + function getConnectedNodes(nodes) { + var connectedNodes = []; + + for (var j = 0, jMax = nodes.length; j < jMax; j++) { + var node = nodes[j]; + + // find all nodes connected to this node + var edges = node.edges; + for (var i = 0, iMax = edges.length; i < iMax; i++) { + var edge = edges[i]; + var other = null; + + // check if connected + if (edge.from == node) + other = edge.to; + else if (edge.to == node) + other = edge.from; + + // check if the other node is not already in the list with nodes + var k, kMax; + if (other) { + for (k = 0, kMax = nodes.length; k < kMax; k++) { + if (nodes[k] == other) { + other = null; + break; + } + } + } + if (other) { + for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { + if (connectedNodes[k] == other) { + other = null; + break; + } + } + } + + if (other) + connectedNodes.push(other); + } + } + + return connectedNodes; + } + + var connections = []; + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + var c = [nodes[id]]; + for (var l = 0; l < level; l++) { + c = c.concat(getConnectedNodes(c)); + } + connections.push(c); + } + } + + var hubs = []; + for (var i = 0, len = connections.length; i < len; i++) { + hubs.push(connections[i].length); + } + + return hubs; +}; + + +/** + * Set a new size for the graph + * @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%') + */ +Graph.prototype.setSize = function(width, height) { + this.frame.style.width = width; + this.frame.style.height = height; + + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; + + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; +}; + +/** + * Set a data set with nodes for the graph + * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * @private + */ +Graph.prototype._setNodes = function(nodes) { + var oldNodesData = this.nodesData; + + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.nodesData = nodes; + } + else if (nodes instanceof Array) { + this.nodesData = new DataSet(); + this.nodesData.add(nodes); + } + else if (!nodes) { + this.nodesData = new DataSet(); + } + else { + throw new TypeError('Array or DataSet expected'); + } + + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.unsubscribe(event, callback); + }); + } + + // remove drawn nodes + this.nodes = {}; + + if (this.nodesData) { + // subscribe to new dataset + var me = this; + util.forEach(this.nodesListeners, function (callback, event) { + me.nodesData.subscribe(event, callback); + }); + + // draw all new nodes + var ids = this.nodesData.getIds(); + this._addNodes(ids); + } + + this._updateSelection(); +}; + +/** + * Add nodes + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._addNodes = function(ids) { + var id; + for (var i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + var data = this.nodesData.get(id); + var node = new Node(data, this.images, this.groups, this.constants); + this.nodes[id] = node; // note: this may replace an existing node + + if (!node.isFixed()) { + // TODO: position new nodes in a smarter way! + var radius = this.constants.edges.length * 2; + var count = ids.length; + var angle = 2 * Math.PI * (i / count); + node.x = radius * Math.cos(angle); + node.y = radius * Math.sin(angle); + + // note: no not use node.isMoving() here, as that gives the current + // velocity of the node, which is zero after creation of the node. + this.moving = true; + } + } + + this._reconnectEdges(); + this._updateValueRange(this.nodes); +}; + +/** + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._updateNodes = function(ids) { + var nodes = this.nodes, + nodesData = this.nodesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = nodesData.get(id); + if (node) { + // update node + node.setProperties(data, this.constants); + } + else { + // create node + node = new Node(properties, this.images, this.groups, this.constants); + nodes[id] = node; + + if (!node.isFixed()) { + this.moving = true; + } + } + } + + this._reconnectEdges(); + this._updateValueRange(nodes); +}; + +/** + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._removeNodes = function(ids) { + var nodes = this.nodes; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + delete nodes[id]; + } + + this._reconnectEdges(); + this._updateSelection(); + this._updateValueRange(nodes); +}; + +/** + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private + * @private + */ +Graph.prototype._setEdges = function(edges) { + var oldEdgesData = this.edgesData; + + if (edges instanceof DataSet || edges instanceof DataView) { + this.edgesData = edges; + } + else if (edges instanceof Array) { + this.edgesData = new DataSet(); + this.edgesData.add(edges); + } + else if (!edges) { + this.edgesData = new DataSet(); + } + else { + throw new TypeError('Array or DataSet expected'); + } + + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.unsubscribe(event, callback); + }); + } + + // remove drawn edges + this.edges = {}; + + if (this.edgesData) { + // subscribe to new dataset + var me = this; + util.forEach(this.edgesListeners, function (callback, event) { + me.edgesData.subscribe(event, callback); + }); + + // draw all new nodes + var ids = this.edgesData.getIds(); + this._addEdges(ids); + } + + this._reconnectEdges(); +}; + +/** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._addEdges = function (ids) { + var edges = this.edges, + edgesData = this.edgesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); + } + + var data = edgesData.get(id); + edges[id] = new Edge(data, this, this.constants); + } + + this.moving = true; + this._updateValueRange(edges); +}; + +/** + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._updateEdges = function (ids) { + var edges = this.edges, + edgesData = this.edgesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + + var data = edgesData.get(id); + var edge = edges[id]; + if (edge) { + // update edge + edge.disconnect(); + edge.setProperties(data, this.constants); + edge.connect(); + } + else { + // create edge + edge = new Edge(data, this, this.constants); + this.edges[id] = edge; + } + } + + this.moving = true; + this._updateValueRange(edges); +}; + +/** + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._removeEdges = function (ids) { + var edges = this.edges; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge) { + edge.disconnect(); + delete edges[id]; + } + } + + this.moving = true; + this._updateValueRange(edges); +}; + +/** + * Reconnect all edges + * @private + */ +Graph.prototype._reconnectEdges = function() { + var id, + nodes = this.nodes, + edges = this.edges; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } + } + + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); + } + } +}; + +/** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Object} obj An object containing a set of Edges or Nodes + * The objects must have a method getValue() and + * setValueRange(min, max). + * @private + */ +Graph.prototype._updateValueRange = function(obj) { + var id; + + // determine the range of the objects + var valueMin = undefined; + var valueMax = undefined; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + var value = obj[id].getValue(); + if (value !== undefined) { + valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); + valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); + } + } + } + + // adjust the range of all objects + if (valueMin !== undefined && valueMax !== undefined) { + for (id in obj) { + if (obj.hasOwnProperty(id)) { + obj[id].setValueRange(valueMin, valueMax); + } + } + } +}; + +/** + * Redraw the graph with the current data + * chart will be resized too. + */ +Graph.prototype.redraw = function() { + this.setSize(this.width, this.height); + + this._redraw(); +}; + +/** + * Redraw the graph with the current data + * @private + */ +Graph.prototype._redraw = function() { + var ctx = this.frame.canvas.getContext('2d'); + + // clear the canvas + var w = this.frame.canvas.width; + var h = this.frame.canvas.height; + ctx.clearRect(0, 0, w, h); + + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); + + this._drawEdges(ctx); + this._drawNodes(ctx); + + // restore original scaling and translation + ctx.restore(); +}; + +/** + * Set the translation of the graph + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset + * @private + */ +Graph.prototype._setTranslation = function(offsetX, offsetY) { + if (this.translation === undefined) { + this.translation = { + x: 0, + y: 0 + }; + } + + if (offsetX !== undefined) { + this.translation.x = offsetX; + } + if (offsetY !== undefined) { + this.translation.y = offsetY; + } +}; + +/** + * Get the translation of the graph + * @return {Object} translation An object with parameters x and y, both a number + * @private + */ +Graph.prototype._getTranslation = function() { + return { + x: this.translation.x, + y: this.translation.y + }; +}; + +/** + * Scale the graph + * @param {Number} scale Scaling factor 1.0 is unscaled + * @private + */ +Graph.prototype._setScale = function(scale) { + this.scale = scale; +}; +/** + * Get the current scale of the graph + * @return {Number} scale Scaling factor 1.0 is unscaled + * @private + */ +Graph.prototype._getScale = function() { + return this.scale; +}; + +/** + * Convert a horizontal point on the HTML canvas to the x-value of the model + * @param {number} x + * @returns {number} + * @private + */ +Graph.prototype._canvasToX = function(x) { + return (x - this.translation.x) / this.scale; +}; + +/** + * Convert an x-value in the model to a horizontal point on the HTML canvas + * @param {number} x + * @returns {number} + * @private + */ +Graph.prototype._xToCanvas = function(x) { + return x * this.scale + this.translation.x; +}; + +/** + * Convert a vertical point on the HTML canvas to the y-value of the model + * @param {number} y + * @returns {number} + * @private + */ +Graph.prototype._canvasToY = function(y) { + return (y - this.translation.y) / this.scale; +}; + +/** + * Convert an y-value in the model to a vertical point on the HTML canvas + * @param {number} y + * @returns {number} + * @private + */ +Graph.prototype._yToCanvas = function(y) { + return y * this.scale + this.translation.y ; +}; + +/** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Graph.prototype._drawNodes = function(ctx) { + // first draw the unselected nodes + var nodes = this.nodes; + var selected = []; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].isSelected()) { + selected.push(id); + } + else { + nodes[id].draw(ctx); + } + } + } + + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + nodes[selected[s]].draw(ctx); + } +}; + +/** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Graph.prototype._drawEdges = function(ctx) { + var edges = this.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected) { + edges[id].draw(ctx); + } + } + } +}; + +/** + * Find a stable position for all nodes + * @private + */ +Graph.prototype._doStabilize = function() { + var start = new Date(); + + // find stable position + var count = 0; + var vmin = this.constants.minVelocity; + var stable = false; + while (!stable && count < this.constants.maxIterations) { + this._calculateForces(); + this._discreteStepNodes(); + stable = !this._isMoving(vmin); + count++; + } + + var end = new Date(); + + // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup +}; + +/** + * Calculate the external forces acting on the nodes + * Forces are caused by: edges, repulsing forces between nodes, gravity + * @private + */ +Graph.prototype._calculateForces = function() { + // create a local edge to the nodes and edges, that is faster + var id, dx, dy, angle, distance, fx, fy, + repulsingForce, springForce, length, edgeLength, + nodes = this.nodes, + edges = this.edges; + + // gravity, add a small constant force to pull the nodes towards the center of + // the graph + // Also, the forces are reset to zero in this loop by using _setForce instead + // of _addForce + var gravity = 0.01, + gx = this.frame.canvas.clientWidth / 2, + gy = this.frame.canvas.clientHeight / 2; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + dx = gx - node.x; + dy = gy - node.y; + angle = Math.atan2(dy, dx); + fx = Math.cos(angle) * gravity; + fy = Math.sin(angle) * gravity; + + node._setForce(fx, fy); + } + } + + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + + for (var id1 in nodes) { + if (nodes.hasOwnProperty(id1)) { + var node1 = nodes[id1]; + for (var id2 in nodes) { + if (nodes.hasOwnProperty(id2)) { + var node2 = nodes[id2]; + // calculate normally distributed force + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); + + // TODO: correct factor for repulsing force + //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; + + node1._addForce(-fx, -fy); + node2._addForce(fx, fy); + } + } + } + } + + /* TODO: re-implement repulsion of edges + for (var n = 0; n < nodes.length; n++) { + for (var l = 0; l < edges.length; l++) { + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + + // calculate normally distributed force + dx = nodes[n].x - lx, + dy = nodes[n].y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + nodes[n]._addForce(fx, fy); + edges[l].from._addForce(-fx/2,-fy/2); + edges[l].to._addForce(-fx/2,-fy/2); + } + } + */ + + // forces caused by the edges, modelled as springs + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; + edgeLength = edge.length; + length = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); + + springForce = edge.stiffness * (edgeLength - length); + + fx = Math.cos(angle) * springForce; + fy = Math.sin(angle) * springForce; + + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } + } + } + + /* TODO: re-implement repulsion of edges + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } + */ +}; + + +/** + * Check if any of the nodes is still moving + * @param {number} vmin the minimum velocity considered as 'moving' + * @return {boolean} true if moving, false if non of the nodes is moving + * @private + */ +Graph.prototype._isMoving = function(vmin) { + // TODO: ismoving does not work well: should check the kinetic energy, not its velocity + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { + return true; + } + } + return false; +}; + + +/** + * Perform one discrete step for all nodes + * @private + */ +Graph.prototype._discreteStepNodes = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].discreteStep(interval); + } + } +}; + +/** + * Start animating nodes and edges + */ +Graph.prototype.start = function() { + if (this.moving) { + this._calculateForces(); + this._discreteStepNodes(); + + var vmin = this.constants.minVelocity; + this.moving = this._isMoving(vmin); + } + + if (this.moving) { + // start animation. only start timer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + graph.start(); + graph._redraw(); + }, this.refreshRate); + } + } + else { + this._redraw(); + } +}; + +/** + * Stop animating nodes and edges. + */ +Graph.prototype.stop = function () { + if (this.timer) { + window.clearInterval(this.timer); + this.timer = undefined; + } +}; + +/** + * vis.js module exports + */ +var vis = { + util: util, + events: events, + + Controller: Controller, + DataSet: DataSet, + DataView: DataView, + Range: Range, + Stack: Stack, + TimeStep: TimeStep, + EventBus: EventBus, + + components: { + items: { + Item: Item, + ItemBox: ItemBox, + ItemPoint: ItemPoint, + ItemRange: ItemRange + }, + + Component: Component, + Panel: Panel, + RootPanel: RootPanel, + ItemSet: ItemSet, + TimeAxis: TimeAxis + }, + + graph: { + Node: Node, + Edge: Edge, + Popup: Popup, + Groups: Groups, + Images: Images + }, + + Timeline: Timeline, + Graph: Graph +}; + +/** + * CommonJS module exports + */ +if (typeof exports !== 'undefined') { + exports = vis; +} +if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = vis; +} + +/** + * AMD module exports + */ +if (typeof(define) === 'function') { + define(function () { + return vis; + }); +} + +/** + * Window exports + */ +if (typeof window !== 'undefined') { + // attach the module to the window, load as a regular javascript file + window['vis'] = vis; +} + +// inject css +util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n.vis.timeline .currenttime {\n background-color: #FF7F6E;\n width: 2px;\n z-index: 9;\n}\n"); + +},{"hammerjs":1,"moment":2}]},{},[3]) +(3) +}); +; \ No newline at end of file