diff --git a/.gitignore b/.gitignore index b6a487c8..df8fb4e6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules .settings/org.eclipse.wst.jsdt.ui.superType.container .settings/org.eclipse.wst.jsdt.ui.superType.name npm-debug.log +dist/ diff --git a/Jakefile.js b/Jakefile.js index bb43714b..a36a934c 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -82,11 +82,15 @@ task('build', {async: true}, function () { './src/graph/Popup.js', './src/graph/Groups.js', './src/graph/Images.js', - './src/graph/manipulationMixin.js', - './src/graph/SectorsMixin.js', - './src/graph/ClusterMixin.js', - './src/graph/SelectionMixin.js', - './src/graph/NavigationMixin.js', + './src/graph/graphMixins/physics/PhysicsMixin.js', + './src/graph/graphMixins/physics/BarnesHut.js', + './src/graph/graphMixins/physics/Repulsion.js', + './src/graph/graphMixins/ManipulationMixin.js', + './src/graph/graphMixins/SectorsMixin.js', + './src/graph/graphMixins/ClusterMixin.js', + './src/graph/graphMixins/SelectionMixin.js', + './src/graph/graphMixins/NavigationMixin.js', + './src/graph/graphMixins/MixinLoader.js', './src/graph/Graph.js', './src/module/exports.js' @@ -99,6 +103,10 @@ task('build', {async: true}, function () { wrench.copyDirSyncRecursive('./src/graph/img', DIST+ '/img', { forceDelete: true }); + // copy css + wrench.copyDirSyncRecursive('./src/graph/css', DIST+ '/css', { + forceDelete: true + }); var timeStart = Date.now(); // bundle the concatenated script and dependencies into one file diff --git a/dist/vis.js b/dist/vis.js deleted file mode 100644 index 462af842..00000000 --- a/dist/vis.js +++ /dev/null @@ -1,21020 +0,0 @@ -/** - * vis.js - * https://github.com/almende/vis - * - * A dynamic, browser-based visualization library. - * - * @version 0.5.0-SNAPSHOT - * @date 2014-02-06 - * - * @license - * Copyright (C) 2011-2014 Almende B.V, http://almende.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy - * of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -!function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.vis=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o>> 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 - } -}; - -/** - * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent - * @param {Element} element - * @param {Event} event - */ -util.fakeGesture = function fakeGesture (element, event) { - var eventType = null; - - // for hammer.js 1.0.5 - return Hammer.event.collectEventData(this, eventType, event); - - // for hammer.js 1.0.6 - //var touches = Hammer.event.getTouchList(event, eventType); - //return Hammer.event.collectEventData(this, eventType, touches, event); -}; - -/** - * 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; -}; - - - -util.GiveDec = function GiveDec(Hex) -{ - if(Hex == "A") - Value = 10; - else - if(Hex == "B") - Value = 11; - else - if(Hex == "C") - Value = 12; - else - if(Hex == "D") - Value = 13; - else - if(Hex == "E") - Value = 14; - else - if(Hex == "F") - Value = 15; - else - Value = eval(Hex) - return Value; -} - -util.GiveHex = function GiveHex(Dec) -{ - if(Dec == 10) - Value = "A"; - else - if(Dec == 11) - Value = "B"; - else - if(Dec == 12) - Value = "C"; - else - if(Dec == 13) - Value = "D"; - else - if(Dec == 14) - Value = "E"; - else - if(Dec == 15) - Value = "F"; - else - Value = "" + Dec; - return Value; -} - -/** - * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php - * - * @param {String} hex - * @returns {{r: *, g: *, b: *}} - */ -util.hexToRGB = function hexToRGB(hex) { - hex = hex.replace("#","").toUpperCase(); - - var a = util.GiveDec(hex.substring(0, 1)); - var b = util.GiveDec(hex.substring(1, 2)); - var c = util.GiveDec(hex.substring(2, 3)); - var d = util.GiveDec(hex.substring(3, 4)); - var e = util.GiveDec(hex.substring(4, 5)); - var f = util.GiveDec(hex.substring(5, 6)); - - var r = (a * 16) + b; - var g = (c * 16) + d; - var b = (e * 16) + f; - - return {r:r,g:g,b:b}; -}; - -util.RGBToHex = function RGBToHex(red,green,blue) { - var a = util.GiveHex(Math.floor(red / 16)); - var b = util.GiveHex(red % 16); - var c = util.GiveHex(Math.floor(green / 16)); - var d = util.GiveHex(green % 16); - var e = util.GiveHex(Math.floor(blue / 16)); - var f = util.GiveHex(blue % 16); - - var hex = a + b + c + d + e + f; - return "#" + hex; -}; - - -/** - * http://www.javascripter.net/faq/rgb2hsv.htm - * - * @param red - * @param green - * @param blue - * @returns {*} - * @constructor - */ -util.RGBToHSV = function RGBToHSV (red,green,blue) { - red=red/255; green=green/255; blue=blue/255; - var minRGB = Math.min(red,Math.min(green,blue)); - var maxRGB = Math.max(red,Math.max(green,blue)); - - // Black-gray-white - if (minRGB == maxRGB) { - return {h:0,s:0,v:minRGB}; - } - - // Colors other than black-gray-white: - var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red); - var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5); - var hue = 60*(h - d/(maxRGB - minRGB))/360; - var saturation = (maxRGB - minRGB)/maxRGB; - var value = maxRGB; - return {h:hue,s:saturation,v:value}; -}; - - -/** - * https://gist.github.com/mjijackson/5311256 - * @param hue - * @param saturation - * @param value - * @returns {{r: number, g: number, b: number}} - * @constructor - */ -util.HSVToRGB = function HSVToRGB(h, s, v) { - var r, g, b; - - var i = Math.floor(h * 6); - var f = h * 6 - i; - var p = v * (1 - s); - var q = v * (1 - f * s); - var t = v * (1 - (1 - f) * s); - - switch (i % 6) { - case 0: r = v, g = t, b = p; break; - case 1: r = q, g = v, b = p; break; - case 2: r = p, g = v, b = t; break; - case 3: r = p, g = q, b = v; break; - case 4: r = t, g = p, b = v; break; - case 5: r = v, g = p, b = q; break; - } - - return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) }; -}; - -util.HSVToHex = function HSVToHex(h,s,v) { - var rgb = util.HSVToRGB(h,s,v); - return util.RGBToHex(rgb.r,rgb.g,rgb.b); -} - -util.hexToHSV = function hexToHSV(hex) { - var rgb = util.hexToRGB(hex); - return util.RGBToHSV(rgb.r,rgb.g,rgb.b); -} -/** - * 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; - var globalShowInternalIds = this.showInternalIds; - - // 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'; - } - - // we allow the setting of this value for a single get request. - if (options != undefined) { - if (options.showInternalIds != undefined) { - this.showInternalIds = options.showInternalIds; - } - } - - // 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); - } - } - } - } - - // restore the global value of showInternalIds - this.showInternalIds = globalShowInternalIds; - - // 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) || this.showInternalIds)) { - 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) || this.showInternalIds)) { - 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; -}; - -/** - * check if an id is an internal or external id - * @param id - * @returns {boolean} - * @private - */ -DataSet.prototype.isInternalId = function(id) { - return (id in this.internalIds); -}; - - -/** - * 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); -} - -// TODO: implement a function .config() to dynamically update things like configured filter -// and trigger changes accordingly - -/** - * 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"; - } - - 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.getWidth()) && - (a.left + a.getWidth() + 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 = null; // Number - this.end = null; // Number - - this.options = options || {}; - - this.setOptions(options); -} - -/** - * Set options for the range controller - * @param {Object} options Available options: - * {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); - - // re-apply range with new limitations - if (this.start !== null && this.end !== null) { - this.setRange(this.start, this.end); - } -}; - -/** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' - */ -function validateDirection (direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + - 'Choose "horizontal" or "vertical".'); - } -} - -/** - * Add listeners for mouse and touch events to the component - * @param {Controller} controller - * @param {Component} component Should be a rootpanel - * @param {String} event Available events: 'move', 'zoom' - * @param {String} direction Available directions: 'horizontal', 'vertical' - */ -Range.prototype.subscribe = function (controller, component, event, direction) { - var me = this; - - if (event == 'move') { - // drag start listener - controller.on('dragstart', function (event) { - me._onDragStart(event, component); - }); - - // drag listener - controller.on('drag', function (event) { - me._onDrag(event, component, direction); - }); - - // drag end listener - controller.on('dragend', function (event) { - me._onDragEnd(event, component); - }); - } - else if (event == 'zoom') { - // mouse wheel - function mousewheel (event) { - me._onMouseWheel(event, component, direction); - } - controller.on('mousewheel', mousewheel); - controller.on('DOMMouseScroll', mousewheel); // For FF - - // pinch - controller.on('touch', function (event) { - me._onTouch(); - }); - controller.on('pinch', function (event) { - me._onPinch(event, component, direction); - }); - } - else { - throw new TypeError('Unknown event "' + event + '". ' + - 'Choose "move" or "zoom".'); - } -}; - -/** - * Add event listener - * @param {String} event Name of the event. - * Available events: 'rangechange', 'rangechanged' - * @param {function} callback Callback function, invoked as callback({start: Date, end: Date}) - */ -Range.prototype.on = function on (event, callback) { - var available = ['rangechange', 'rangechanged']; - - if (available.indexOf(event) == -1) { - throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); - } - - events.addListener(this, event, callback); -}; - -/** - * Remove an event listener - * @param {String} event name of the event - * @param {function} callback callback handler - */ -Range.prototype.off = function off (event, callback) { - events.removeListener(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, 'Date').valueOf() : this.start, - newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : 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) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); - } - - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; - } - - // prevent start < min - 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 (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 = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; - } - if ((newEnd - newStart) < 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; - } - } - } - - // 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) { - // 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; - } - } - } - - 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 scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ -Range.prototype.conversion = function (width) { - return Range.conversion(this.start, this.end, width); -}; - -/** - * Static method to calculate the conversion offset and scale for a range, - * based on the provided start, end, and width - * @param {Number} start - * @param {Number} end - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ -Range.conversion = function (start, end, width) { - if (width != 0 && (end - start != 0)) { - return { - offset: start, - scale: width / (end - start) - } - } - else { - return { - offset: 0, - scale: 1 - }; - } -}; - -// global (private) object to store drag params -var touchParams = {}; - -/** - * Start dragging horizontally or vertically - * @param {Event} event - * @param {Object} component - * @private - */ -Range.prototype._onDragStart = function(event, component) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; - - touchParams.start = this.start; - touchParams.end = this.end; - - var frame = component.frame; - if (frame) { - frame.style.cursor = 'move'; - } -}; - -/** - * Perform dragging operating. - * @param {Event} event - * @param {Component} component - * @param {String} direction 'horizontal' or 'vertical' - * @private - */ -Range.prototype._onDrag = function (event, component, direction) { - validateDirection(direction); - - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; - - var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, - interval = (touchParams.end - touchParams.start), - width = (direction == 'horizontal') ? component.width : component.height, - diffRange = -delta / width * interval; - - this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange); - - // fire a rangechange event - this._trigger('rangechange'); -}; - -/** - * Stop dragging operating. - * @param {event} event - * @param {Component} component - * @private - */ -Range.prototype._onDragEnd = function (event, component) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; - - if (component.frame) { - component.frame.style.cursor = 'auto'; - } - - // 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 {Component} component - * @param {String} direction 'horizontal' or 'vertical' - * @private - */ -Range.prototype._onMouseWheel = function(event, component, direction) { - validateDirection(direction); - - // 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) { - // perform the zoom action. Delta is normally 1 or -1 - - // adjust a negative delta such that zooming in with delta 0.1 - // equals zooming out with a delta -0.1 - var scale; - if (delta < 0) { - scale = 1 - (delta / 5); - } - else { - scale = 1 / (1 + (delta / 5)) ; - } - - // calculate center, the date to zoom around - var gesture = util.fakeGesture(this, event), - pointer = getPointer(gesture.touches[0], component.frame), - pointerDate = this._pointerToDate(component, direction, pointer); - - this.zoom(scale, pointerDate); - } - - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - util.preventDefault(event); -}; - -/** - * On start of a touch gesture, initialize scale to 1 - * @private - */ -Range.prototype._onTouch = function () { - touchParams.start = this.start; - touchParams.end = this.end; - touchParams.pinching = false; - touchParams.center = null; -}; - -/** - * Handle pinch event - * @param {Event} event - * @param {Component} component - * @param {String} direction 'horizontal' or 'vertical' - * @private - */ -Range.prototype._onPinch = function (event, component, direction) { - touchParams.pinching = true; - - if (event.gesture.touches.length > 1) { - if (!touchParams.center) { - touchParams.center = getPointer(event.gesture.center, component.frame); - } - - var scale = 1 / event.gesture.scale, - initDate = this._pointerToDate(component, direction, touchParams.center), - center = getPointer(event.gesture.center, component.frame), - date = this._pointerToDate(component, direction, center), - delta = date - initDate; // TODO: utilize delta - - // calculate new start and end - var newStart = parseInt(initDate + (touchParams.start - initDate) * scale); - var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale); - - // apply new range - this.setRange(newStart, newEnd); - } -}; - -/** - * Helper function to calculate the center date for zooming - * @param {Component} component - * @param {{x: Number, y: Number}} pointer - * @param {String} direction 'horizontal' or 'vertical' - * @return {number} date - * @private - */ -Range.prototype._pointerToDate = function (component, direction, pointer) { - var conversion; - if (direction == 'horizontal') { - var width = component.width; - conversion = this.conversion(width); - return pointer.x / conversion.scale + conversion.offset; - } - else { - var height = component.height; - conversion = this.conversion(height); - return pointer.y / conversion.scale + conversion.offset; - } -}; - -/** - * Get the pointer location relative to the location of the dom element - * @param {{pageX: Number, pageY: Number}} touch - * @param {Element} element HTML DOM element - * @return {{x: Number, y: Number}} pointer - * @private - */ -function getPointer (touch, element) { - return { - x: touch.pageX - vis.util.getAbsoluteLeft(element), - y: touch.pageY - vis.util.getAbsoluteTop(element) - }; -} - -/** - * Zoom the range the given scale 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 scale = 0.9 or 1.1 - * @param {Number} scale Scaling factor. Values above 1 will zoom out, - * values below 1 will zoom in. - * @param {Number} [center] Value representing a date around which will - * be zoomed. - */ -Range.prototype.zoom = function(scale, center) { - // if centerDate is not provided, take it half between start Date and end Date - if (center == null) { - center = (this.start + this.end) / 2; - } - - // calculate new start and end - var newStart = center + (this.start - center) * scale; - var newEnd = center + (this.end - center) * scale; - - this.setRange(newStart, newEnd); -}; - -/** - * Move the range with a given delta to the left or right. Start and end - * value will be adjusted. For example, try delta = 0.1 or -0.1 - * @param {Number} delta Moving amount. Positive value will move right, - * negative value will move left - */ -Range.prototype.move = function(delta) { - // zoom start Date and end Date relative to the centerDate - var diff = (this.end - this.start); - - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; - - // 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; -} - -// Extend controller with Emitter mixin -Emitter(Controller.prototype); - -/** - * 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.setController(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) { - // unregister the controller (gives the component the ability to unregister - // event listeners and clean up other stuff) - this.components[id].setController(null); - - 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; -}; - -/** - * Set controller for this component, or remove current controller by passing - * null as parameter value. - * @param {Controller | null} controller - */ -Component.prototype.setController = function setController (controller) { - this.controller = controller || null; -}; - -/** - * Get controller of this component - * @return {Controller} controller - */ -Component.prototype.getController = function getController () { - return this.controller; -}; - -/** - * 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; - - // create functions to be used as DOM event listeners - var me = this; - this.hammer = null; - - // create listeners for all interesting events, these events will be emitted - // via the controller - var events = [ - 'touch', 'pinch', 'tap', 'hold', - 'dragstart', 'drag', 'dragend', - 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox - ]; - this.listeners = {}; - events.forEach(function (event) { - me.listeners[event] = function () { - var args = [event].concat(Array.prototype.slice.call(arguments, 0)); - me.controller.emit.apply(me.controller, args); - }; - }); - - this.options = options || {}; - this.defaultOptions = { - autoResize: true - }; -} - -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'); - - this.frame = frame; - - this._registerListeners(); - - changed += 1; - } - if (!frame.parentNode) { - if (!this.container) { - throw new Error('Cannot repaint root panel: no container attached'); - } - this.container.appendChild(frame); - changed += 1; - } - - frame.className = 'vis timeline rootpanel ' + options.orientation; - var className = options.className; - if (className) { - util.addClassName(frame, util.option.asString(className)); - } - - 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._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 -}; - -/** - * Set controller for this component, or remove current controller by passing - * null as parameter value. - * @param {Controller | null} controller - */ -RootPanel.prototype.setController = function setController (controller) { - this.controller = controller || null; - - if (this.controller) { - this._registerListeners(); - } - else { - this._unregisterListeners(); - } -}; - -/** - * Register event emitters emitted by the rootpanel - * @private - */ -RootPanel.prototype._registerListeners = function () { - if (this.frame && this.controller && !this.hammer) { - this.hammer = Hammer(this.frame, { - prevent_default: true - }); - - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - this.hammer.on(event, this.listeners[event]); - } - } - } -}; - -/** - * Unregister event emitters from the rootpanel - * @private - */ -RootPanel.prototype._unregisterListeners = function () { - if (this.hammer) { - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - this.hammer.off(event, this.listeners[event]); - } - } - - this.hammer = null; - } -}; - -/** - * 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.scale + 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.scale; -}; - -/** - * 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'; - // 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 && line.parentElement) { - frame.removeChild(line.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, '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()); - } - - return (changed > 0); -}; - -/** - * Calculate the scale 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.scale / 2; - - if (interval < 30) { - interval = 30; - } - - this.currentTimeTimer = setTimeout(function() { - timeline.repaint(); - }, interval); - - 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 - * 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; - - // event listeners - this.listeners = { - dragstart: this._onDragStart.bind(this), - drag: this._onDrag.bind(this), - dragend: this._onDragEnd.bind(this) - }; - - // 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.selection = []; // list with the ids of all selected nodes - 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, - rangeoverflow: ItemRangeOverflow, - 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 controller for this component - * @param {Controller | null} controller - */ -ItemSet.prototype.setController = function setController (controller) { - var event; - - // unregister old event listeners - if (this.controller) { - for (event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - this.controller.off(event, this.listeners[event]); - } - } - } - - this.controller = controller || null; - - // register new event listeners - if (this.controller) { - for (event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - this.controller.on(event, this.listeners[event]); - } - } - } -}; - -// attach event listeners for dragging items to the controller -(function (me) { - var _controller = null; - var _onDragStart = null; - var _onDrag = null; - var _onDragEnd = null; - - Object.defineProperty(me, 'controller', { - get: function () { - return _controller; - }, - - set: function (controller) { - - } - }); -}) (this); - - -/** - * 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; -}; - -/** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {Array} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. - */ -ItemSet.prototype.setSelection = function setSelection(ids) { - var i, ii, id, item, selection; - - if (ids) { - if (!Array.isArray(ids)) { - throw new TypeError('Array expected'); - } - - // unselect currently selected items - for (i = 0, ii = this.selection.length; i < ii; i++) { - id = this.selection[i]; - item = this.items[id]; - if (item) item.unselect(); - } - - // select items - this.selection = []; - for (i = 0, ii = ids.length; i < ii; i++) { - id = ids[i]; - item = this.items[id]; - if (item) { - this.selection.push(id); - item.select(); - } - } - - // trigger a select event - selection = this.selection.concat([]); - events.trigger(this, 'select', { - ids: selection - }); - - if (this.controller) { - this.requestRepaint(); - } - } -}; - -/** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items - */ -ItemSet.prototype.getSelection = function getSelection() { - return this.selection.concat([]); -}; - -/** - * Deselect a selected item - * @param {String | Number} id - * @private - */ -ItemSet.prototype._deselect = function _deselect(id) { - var selection = this.selection; - for (var i = 0, ii = selection.length; i < ii; i++) { - if (selection[i] == id) { // non-strict comparison! - selection.splice(i, 1); - break; - } - } -}; - -/** - * 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 - for (var id in queue) { - if (queue.hasOwnProperty(id)) { - var entry = queue[id], - item = items[id], - action = entry.action; - - //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); - item.id = entry.id; // we take entry.id, as id itself is stringified - 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 the item from the set selected items - if (item.selected) { - me._deselect(id); - } - - // 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] = { - id: id, - action: action - }; - }); - - if (this.controller) { - //this.requestReflow(); - this.requestRepaint(); - } -}; - -/** - * Calculate the scale 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.scale + 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.scale; -}; - -// global (private) object to store drag params -var touchParams = {}; - -/** - * Start dragging the selected events - * @param {Event} event - * @private - */ -// TODO: move this function to ItemSet -ItemSet.prototype._onDragStart = function (event) { - var item = this._itemFromTarget(event); -console.log('_onDragStart', event) - if (item && item.selected) { - touchParams.items = [item]; - //touchParams.items = this.getSelection(); // TODO: use the current selection - touchParams.itemsLeft = touchParams.items.map(function (item) { - return item.left; - }); - console.log('_onDragStart', touchParams) - event.stopPropagation(); - } -}; - -/** - * Drag selected items - * @param {Event} event - * @private - */ -// TODO: move this function to ItemSet -ItemSet.prototype._onDrag = function (event) { - if (touchParams.items) { - var deltaX = event.gesture.deltaX; - - touchParams.items.forEach(function (item, i) { - item.left = touchParams.itemsLeft[i] + deltaX; - item.reposition(); - }); - - event.stopPropagation(); - } -}; - -/** - * End of dragging selected items - * @param {Event} event - * @private - */ -// TODO: move this function to ItemSet -ItemSet.prototype._onDragEnd = function (event) { - if (touchParams.items) { - // actually apply the new locations - - touchParams.items = null; - - event.stopPropagation(); - } -}; - -/** - * Find an item from an event target: - * searches for the attribute 'timeline-item' in the event target's element tree - * @param {Event} event - * @return {Item | null| item - * @private - */ -ItemSet.prototype._itemFromTarget = function _itemFromTarget (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-item']; - } - target = target.parentNode; - } - - return null; -}; -/** - * @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; - if (this.visible) this.repaint(); -}; - -/** - * Unselect current item - */ -Item.prototype.unselect = function unselect() { - this.selected = false; - if (this.visible) this.repaint(); -}; - -/** - * 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; -}; - -/** - * Return the items width - * @return {Number} width - */ -Item.prototype.getWidth = function getWidth() { - return this.width; -}; - -/** - * @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); - -/** - * 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'); - } - - if (!dom.box.parentNode) { - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } - foreground.appendChild(dom.box); - changed = true; - } - - if (!dom.line.parentNode) { - var background = this.parent.getBackground(); - if (!background) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no background container element'); - } - background.appendChild(dom.line); - changed = true; - } - - if (!dom.dot.parentNode) { - var axis = this.parent.getAxis(); - if (!background) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no axis container element'); - } - 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'; - - // attach this item as attribute - dom.box['timeline-item'] = this; - } -}; - -/** - * 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); - -/** - * 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); - - // attach this item as attribute - dom.point['timeline-item'] = this; - } -}; - -/** - * 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); - -/** - * 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 : '') + - (this.selected ? ' selected' : ''); - 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); - - // attach this item as attribute - dom.box['timeline-item'] = this; - } -}; - -/** - * 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 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 {Number} 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 - * @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); - } -}; - -/** - * Set selected items by their id. Replaces the current selection. - * Unknown id's are silently ignored. - * @param {Array} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. - */ -Group.prototype.setSelection = function setSelection(ids) { - if (this.itemset) this.itemset.setSelection(ids); -}; - -/** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items - */ -Group.prototype.getSelection = function getSelection() { - return this.itemset ? this.itemset.getSelection() : []; -}; - -/** - * 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; -}; - -/** - * Set selected items by their id. Replaces the current selection. - * Unknown id's are silently ignored. - * @param {Array} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. - */ -GroupSet.prototype.setSelection = function setSelection(ids) { - var selection = [], - groups = this.groups; - - // iterate over each of the groups - for (var id in groups) { - if (groups.hasOwnProperty(id)) { - var group = groups[id]; - group.setSelection(ids); - } - } - - return selection; -}; - -/** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items - */ -GroupSet.prototype.getSelection = function getSelection() { - var selection = [], - groups = this.groups; - - // iterate over each of the groups - for (var id in groups) { - if (groups.hasOwnProperty(id)) { - var group = groups[id]; - selection = selection.concat(group.getSelection()); - } - } - - return selection; -}; - -/** - * 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, - labelSet = this.dom.labelSet; - - // 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'; - this.dom.labels = labels; - } - if (!labelSet) { - labelSet = document.createElement('div'); - labelSet.className = 'label-set'; - labels.appendChild(labelSet); - this.dom.labelSet = labelSet; - } - 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(labelSet.style, 'top', asSize(options.top, '0px')); - changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px')); - - 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); - util.extend(groupOptions, { - height: null, - maxHeight: null - }); - - 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.groupOrder - }); - 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 (labelSet.firstChild) { - labelSet.removeChild(labelSet.firstChild); - } - for (i = 0; i < orderedGroups.length; i++) { - id = orderedGroups[i]; - label = this._createLabel(id); - labelSet.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 | google.visualization.DataTable} [items] - * @param {Object} [options] See Timeline.setOptions for the available options. - * @constructor - */ -function Timeline (container, items, options) { - var me = this; - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.options = { - 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, - showCustomTime: false, - autoResize: false - }; - - // 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 () { - // TODO: change to height - if (me.options.height) { - // fixed height - return me.options.height; - } - else { - // auto height - return (me.timeaxis.height + me.content.height) + 'px'; - } - }; - this.rootPanel = new RootPanel(container, rootOptions); - this.controller.add(this.rootPanel); - - // single select (or unselect) when tapping an item - // TODO: implement ctrl+click - this.controller.on('tap', this._onSelectItem.bind(this)); - - // multi select when holding mouse/touch, or on ctrl+click - this.controller.on('hold', this._onMultiSelectItem.bind(this)); - - // 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 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 - // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable - // TODO: enable moving again - this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal'); - this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal'); - this.range.on('rangechange', function (properties) { - var force = true; - me.controller.requestReflow(force); - me.emit('rangechange', properties); - }); - this.range.on('rangechanged', function (properties) { - var force = true; - me.controller.requestReflow(force); - me.emit('rangechanged', properties); - }); - - // 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); - - // custom time bar - this.customtime = new CustomTime(this.timeaxis, [], rootOptions); - this.controller.add(this.customtime); - - // create groupset - this.setGroups(null); - - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - - // apply options - if (options) { - this.setOptions(options); - } - - // create itemset and groupset - if (items) { - this.setItems(items); - } -} - -// extend Timeline with the Emitter mixin -Emitter(Timeline.prototype); - -/** - * Set options - * @param {Object} options TODO: describe the available options - */ -Timeline.prototype.setOptions = function (options) { - util.extend(this.options, options); - - // force update of range (apply new min/max etc.) - // both start and end are optional - this.range.setRange(options.start, options.end); - - 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 | google.visualization.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% space on both sides - var start = dataRange.min; - var end = dataRange.max; - if (start != null && end != null) { - var interval = (end.valueOf() - start.valueOf()); - if (interval <= 0) { - // prevent an empty interval - interval = 24 * 60 * 60 * 1000; // 1 day - } - start = new Date(start.valueOf() - interval * 0.05); - end = new Date(end.valueOf() + interval * 0.05); - } - - // override specified start and/or end date - if (this.options.start != undefined) { - start = util.convert(this.options.start, 'Date'); - } - if (this.options.end != undefined) { - end = util.convert(this.options.end, 'Date'); - } - - // apply range if there is a min or max available - if (start != null || end != null) { - this.range.setRange(start, end); - } - } -}; - -/** - * Set groups - * @param {vis.DataSet | Array | google.visualization.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) { - // fixed height - return me.itemPanel.height - me.timeaxis.height; - } - else { - // auto height - return null; - } - }, - maxHeight: function () { - // TODO: change maxHeight to be a css string like '100%' or '300px' - 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 - }; -}; - -/** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {Array} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. - */ -Timeline.prototype.setSelection = function setSelection (ids) { - if (this.content) this.content.setSelection(ids); -}; - -/** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items - */ -Timeline.prototype.getSelection = function getSelection() { - return this.content ? this.content.getSelection() : []; -}; - -/** - * Handle selecting/deselecting an item when tapping it - * @param {Event} event - * @private - */ -// TODO: move this function to ItemSet -Timeline.prototype._onSelectItem = function (event) { - var item = this._itemFromTarget(event); - - var selection = item ? [item.id] : []; - this.setSelection(selection); - - this.emit('select', { - items: this.getSelection() - }); - - event.stopPropagation(); -}; - -/** - * Handle selecting/deselecting multiple items when holding an item - * @param {Event} event - * @private - */ -// TODO: move this function to ItemSet -Timeline.prototype._onMultiSelectItem = function (event) { - var selection, - item = this._itemFromTarget(event); - - if (!item) { - // do nothing... - return; - } - - selection = this.getSelection(); // current selection - var index = selection.indexOf(item.id); - if (index == -1) { - // item is not yet selected -> select it - selection.push(item.id); - } - else { - // item is already selected -> deselect it - selection.splice(index, 1); - } - this.setSelection(selection); - - this.emit('select', { - items: this.getSelection() - }); - - event.stopPropagation(); -}; - -/** - * Find an item from an event target: - * searches for the attribute 'timeline-item' in the event target's element tree - * @param {Event} event - * @return {Item | null| item - * @private - */ -// TODO: move this function to ItemSet -Timeline.prototype._itemFromTarget = function _itemFromTarget (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-item']; - } - target = target.parentNode; - } - - return 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.dynamicEdges = []; - this.reroutedEdges = {}; - 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.horizontalAlignLeft = true; // these are for the navigation controls - this.verticalAlignTop = true; // these are for the navigation controls - this.radius = constants.nodes.radius; - this.baseRadiusValue = 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); - - // creating the variables for clustering - this.resetCluster(); - this.dynamicEdgesLength = 0; - this.clusterSession = 0; - this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width; - this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height; - this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius; - - // mass, force, velocity - this.mass = 1; // 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; - this.dampingFactor = 75; - - this.graphScaleInv = 1; - this.canvasTopLeft = {"x": -300, "y": -300}; - this.canvasBottomRight = {"x": 300, "y": 300}; -} - -/** - * (re)setting the clustering variables and objects - */ -Node.prototype.resetCluster = function() { - // clustering variables - this.formationScale = undefined; // this is used to determine when to open the cluster - this.clusterSize = 1; // this signifies the total amount of nodes in this cluster - this.containedNodes = {}; - this.containedEdges = {}; - this.clusterSessions = []; -}; - -/** - * Attach a edge to the node - * @param {Edge} edge - */ -Node.prototype.attachEdge = function(edge) { - if (this.edges.indexOf(edge) == -1) { - this.edges.push(edge); - } - if (this.dynamicEdges.indexOf(edge) == -1) { - this.dynamicEdges.push(edge); - } - this.dynamicEdgesLength = this.dynamicEdges.length; - 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.dynamicEdges.splice(index, 1); - } - this.dynamicEdgesLength = this.dynamicEdges.length; - 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 = 1 + 0.6 * 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; - } - this.originalLabel = undefined; - // basic properties - if (properties.id !== undefined) {this.id = properties.id;} - if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = 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;} - - // navigation controls properties - if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} - if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} - if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;} - - 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 && properties.fixed); - this.yFixed = this.yFixed || (properties.y !== undefined && properties.fixed); - 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 - */ -Node.prototype.clearSizeCache = function() { - 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) { - - if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) { -// console.log(vmin,this.vx,this.vy); - return true; - } - else { - this.vx = 0; this.vy = 0; - return false; - } - //return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin); -}; - -/** - * 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; - } - } - this.baseRadiusValue = this.radius; -}; - -/** - * 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 || !this.height) { // undefined or 0 - var width, height; - if (this.value) { - this.radius = this.baseRadiusValue; - var scale = this.imageObj.height / this.imageObj.width; - if (scale !== undefined) { - width = this.radius || this.imageObj.width; - height = this.radius * scale || this.imageObj.height; - } - else { - width = 0; - height = 0; - } - } - else { - width = this.imageObj.width; - height = this.imageObj.height; - } - this.width = width; - this.height = height; - - if (this.width > 0 && this.height > 0) { - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; - } - } - -}; - -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.width != 0 ) { - // draw the shade - if (this.clusterSize > 1) { - var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0); - lineWidth *= this.graphScaleInv; - lineWidth = Math.min(0.2 * this.width,lineWidth); - - ctx.globalAlpha = 0.5; - ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth); - } - - // draw the image - ctx.globalAlpha = 1.0; - 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; - - this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; -// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; - } -}; - -Node.prototype._drawBox = function (ctx) { - this._resizeBox(ctx); - - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var selectionLineWidth = 2; - - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - - 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; - - // scaling used for clustering - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; - } -}; - -Node.prototype._drawDatabase = function (ctx) { - this._resizeDatabase(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var selectionLineWidth = 2; - - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - 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; - - // scaling used for clustering -// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; -// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; - } -}; - -Node.prototype._drawCircle = function (ctx) { - this._resizeCircle(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var selectionLineWidth = 2; - - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - 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; - } - - // scaling used for clustering - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; - } -}; - -Node.prototype._drawEllipse = function (ctx) { - this._resizeEllipse(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var selectionLineWidth = 2; - - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - - 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) { - this.radius = this.baseRadiusValue; - var size = 2 * this.radius; - this.width = size; - this.height = size; - - // scaling used for clustering - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; - } -}; - -Node.prototype._drawShape = function (ctx, shape) { - this._resizeShape(ctx); - - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var selectionLineWidth = 2; - var radiusMultiplier = 2; - - // choose draw method depending on the shape - switch (shape) { - case 'dot': radiusMultiplier = 2; break; - case 'square': radiusMultiplier = 2; break; - case 'triangle': radiusMultiplier = 3; break; - case 'triangleDown': radiusMultiplier = 3; break; - case 'star': radiusMultiplier = 4; break; - } - - ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; - - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth); - ctx.stroke(); - } - ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.graphScaleInv; - ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); - - ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - - 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; - - // scaling used for clustering - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; - } -}; - -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}; - } -}; - -/** - * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn. - * there is a safety margin of 0.3 * width; - * - * @returns {boolean} - */ -Node.prototype.inArea = function() { - if (this.width !== undefined) { - return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x && - this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x && - this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y && - this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y); - } - else { - return true; - } -} - -/** - * checks if the core of the node is in the display area, this is used for opening clusters around zoom - * @returns {boolean} - */ -Node.prototype.inView = function() { - return (this.x >= this.canvasTopLeft.x && - this.x < this.canvasBottomRight.x && - this.y >= this.canvasTopLeft.y && - this.y < this.canvasBottomRight.y); -} - -/** - * This allows the zoom level of the graph to influence the rendering - * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas - * - * @param scale - * @param canvasTopLeft - * @param canvasBottomRight - */ -Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { - this.graphScaleInv = 1.0/scale; - this.canvasTopLeft = canvasTopLeft; - this.canvasBottomRight = canvasBottomRight; -}; - - -/** - * This allows the zoom level of the graph to influence the rendering - * - * @param scale - */ -Node.prototype.setScale = function(scale) { - this.graphScaleInv = 1.0/scale; -}; - -/** - * This function updates the damping parameter for clusters, based ont he - * - * @param {Number} numberOfNodes - */ -Node.prototype.updateDamping = function(numberOfNodes) { - this.damping = (0.8 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2))); - this.damping *= this.dampingFactor; -}; - - -/** - * set the velocity at 0. Is called when this node is contained in another during clustering - */ -Node.prototype.clearVelocity = function() { - this.vx = 0; - this.vy = 0; -}; - - -/** - * Basic preservation of (kinectic) energy - * - * @param massBeforeClustering - */ -Node.prototype.updateVelocity = function(massBeforeClustering) { - var energyBefore = this.vx * this.vx * massBeforeClustering; - this.vx = Math.sqrt(energyBefore/this.mass); - energyBefore = this.vy * this.vy * massBeforeClustering; - this.vy = Math.sqrt(energyBefore/this.mass); -}; - - -/** - * @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.selected = false; - - this.from = null; // a node - this.to = null; // a node - - // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster - // by storing the original information we can revert to the original connection when the cluser is opened. - this.originalFromId = []; - this.originalToId = []; - - 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 scale = (this.widthMax - this.widthMin) / (max - min); - this.width = (this.value - min) * scale + 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.selected == true) { - return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv; - } - else { - return this.width*this.graphScaleInv; - } -}; - -/** - * 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); -}; - - - -/** - * This allows the zoom level of the graph to influence the rendering - * - * @param scale - */ -Edge.prototype.setScale = function(scale) { - this.graphScaleInv = 1.0/scale; -}; - - -Edge.prototype.select = function() { - this.selected = true; -} - -Edge.prototype.unselect = function() { - this.selected = false; -} -/** - * 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; -}; - -/** - * Created by Alex on 2/4/14. - */ - -var manipulationMixin = { - - /** - * clears the toolbar div element of children - * - * @private - */ - _clearManipulatorBar : function() { - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } - }, - - - /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private - */ - _createManipulatorBar : function() { - // remove bound functions - this.off('select', this.boundFunction); - - // reset global variables - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false - - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } - // add the icons to the manipulator div - this.manipulationDiv.innerHTML = "" + - "Add Node" + - "
" + - "Edit Selected" + - "
" + - "Connect Node" + - "
" + - "Delete selected"; - - // bind the icons - var addButton = document.getElementById("manipulate-addNode"); - addButton.onclick = this._createAddToolbar.bind(this); - var editButton = document.getElementById("manipulate-editNode"); - editButton.onclick = this._createEditToolbar.bind(this); - var connectButton = document.getElementById("manipulate-connectNode"); - connectButton.onclick = this._createConnectToolbar.bind(this); - var deleteButton = document.getElementById("manipulate-delete"); - deleteButton.onclick = this._createDeletionToolbar.bind(this); - }, - - - /** - * Create the toolbar for adding Nodes - * - * @private - */ - _createAddToolbar : function() { - // clear the toolbar - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Back" + - "
" + - "Click in an empty space to place a new node"; - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._addNode.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * Create the toolbar to edit nodes or edges. - * TODO: edges not implemented yet, unsure what to edit. - * - * @private - */ - _createEditToolbar : function() { - // clear the toolbar - this.blockConnectingEdgeSelection = false; - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - - var message = ""; - if (this._selectionIsEmpty()) { - message = "Select a node or edge to edit."; - } - else { - if (this._getSelectedObjectCount() > 1) { - message = "Select a single node or edge to edit." - this._unselectAll(true); - } - else { - if (this._clusterInSelection()) { - message = "You cannot edit a cluster." - this._unselectAll(true); - } - else { - if (this._getSelectedNodeCount() > 0) { // the selected item is a node - this._createEditNodeToolbar(); - } - else { // the selected item is an edge - this._createEditEdgeToolbar(); - } - } - } - } - - if (message != "") { - this.blockConnectingEdgeSelection = true; - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Back" + - "
" + - ""+message+""; - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createEditToolbar.bind(this); - this.on('select', this.boundFunction); - } - }, - - - /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. - * TODO: change shape or group? - * - * @private - */ - _createEditNodeToolbar : function() { - // clear the toolbar - this.blockConnectingEdgeSelection = false; - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - var editObject = this._getEditObject(); - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Cancel" + - "
" + - "label: " + - "
" + - "color: " + - "
" + - "" - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - var saveButton = document.getElementById("manipulator-obj-save"); - saveButton.onclick = this._saveNodeData.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * save the changes in the node data - * - * @private - */ - _saveNodeData : function() { - var editObjectId = this._getEditObject().id; - var label = document.getElementById('manipulator-obj-label').value; - - var definedColor = document.getElementById('manipulator-obj-color').value; - var hsv = util.hexToHSV(definedColor); - - var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)}; - var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6}; - var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v); - var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v); - - var updatedSettings = {id:editObjectId, - label: label, - color: { - background:definedColor, - border:darkerColorHex, - highlight: { - background:lighterColorHex, - border:darkerColorHex - } - }}; - this.nodesData.update(updatedSettings); - this._createManipulatorBar(); - }, - - - /** - * creating the toolbar to edit edges. - * - * @private - */ - _createEditEdgeToolbar : function() { - // clear the toolbar - this.blockConnectingEdgeSelection = false; - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Back" + - "
" + - "Currently only nodes can be edited."; - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * create the toolbar to connect nodes - * - * @private - */ - _createConnectToolbar : function() { - // clear the toolbar - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - this._unselectAll(); - this.forceAppendSelection = false; - this.blockConnectingEdgeSelection = true; - - this.manipulationDiv.innerHTML = "" + - "Back" + - "
" + - "Select the node you want to connect to other nodes."; - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._handleConnect.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * create the toolbar for deleting selected objects. User has to be sure. - * - * @private - */ - _createDeletionToolbar : function() { - // clear the toolbar - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - if (this._selectionIsEmpty()) { - this.manipulationDiv.innerHTML = "" + - "Cannot delete an empty selection."; - var graph = this; - window.setTimeout (function() {graph._createManipulatorBar()},1500); - } - else { - this.manipulationDiv.innerHTML = "" + - "Back" + - "
" + - "Are you sure? This cannot be undone." + - "
" + - "Yes."; - - // bind the buttons - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - var acceptDeleteButton = document.getElementById("manipulate-acceptDelete"); - acceptDeleteButton.onclick = this._deleteSelected.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - } - }, - - - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - _handleConnect : function() { - this.forceAppendSelection = false; - if (this._clusterInSelection()) { - this._unselectClusters(true); - if (!this._selectionIsEmpty()) { - this._setManipulationMessage("You cannot connect a node to a cluster."); - this.forceAppendSelection = true; - } - else { - this._setManipulationMessage("You cannot connect anything to a cluster."); - } - } - else if (!this._selectionIsEmpty()) { - if (this._getSelectedNodeCount() == 2) { - this._connectNodes(); - this._restoreSourceNode(); - this._setManipulationMessage("Click on another node you want to connect this node to or go back."); - } - else { - this._setManipulationMessage("Click on the node you want to connect this node."); - this._setSourceNode(); - this.forceAppendSelection = true; - } - } - else { - this._setManipulationMessage("Select the node you want to connect to other nodes."); - } - }, - - - /** - * returns the object that is selected - * - * @returns {*} - * @private - */ - _getEditObject : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - return this.selectionObj[objectId]; - } - } - return null; - }, - - - /** - * stores the first selected node for the connecting process as the source node. This allows us to remember the direction - * - * @private - */ - _setSourceNode : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - this.manipulationSourceNode = this.selectionObj[objectId]; - } - } - } - }, - - - /** - * gets the node the source connects to. - * - * @returns {*} - * @private - */ - _getTargetNode : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (this.manipulationSourceNode.id != this.selectionObj[objectId].id) { - return this.selectionObj[objectId]; - } - } - } - } - return null; - }, - - - /** - * restore the selection back to only the sourcenode - * - * @private - */ - _restoreSourceNode : function() { - this._unselectAll(true); - this._selectObject(this.manipulationSourceNode); - }, - - - /** - * change the description message on the toolbar - * - * @param message - * @private - */ - _setManipulationMessage : function(message) { - var messageSpan = document.getElementById('manipulatorLabel'); - messageSpan.innerHTML = message; - }, - - - /** - * Adds a node on the specified location - * - * @param {Object} pointer - */ - _addNode : function() { - if (this._selectionIsEmpty()) { - var positionObject = this._pointerToPositionObject(this.pointerPosition); - this.createNodeOnClick = true; - this.nodesData.add({id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",fixed:false}); - this.createNodeOnClick = false; - this.moving = true; - this.start(); - } - }, - - - /** - * connect two nodes with a new edge. - * - * @private - */ - _connectNodes : function() { - var targetNode = this._getTargetNode(); - var sourceNode = this.manipulationSourceNode; - this.edgesData.add({from:sourceNode.id, to:targetNode.id}) - this.moving = true; - this.start(); - }, - - - /** - * delete everything in the selection - * - * @private - */ - _deleteSelected : function() { - if (!this._clusterInSelection()) { - var selectedNodes = this.getSelectedNodes(); - var selectedEdges = this.getSelectedEdges(); - this._removeEdges(selectedEdges); - this._removeNodes(selectedNodes); - this.moving = true; - this.start(); - } - else { - alert("Clusters cannot be deleted.") - } - } - - -} -/** - * Creation of the SectorMixin var. - * - * This contains all the functions the Graph object can use to employ the sector system. - * The sector system is always used by Graph, though the benefits only apply to the use of clustering. - * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. - * - * Alex de Mulder - * 21-01-2013 - */ -var SectorMixin = { - - /** - * This function is only called by the setData function of the Graph object. - * This loads the global references into the active sector. This initializes the sector. - * - * @private - */ - _putDataInSector : function() { - this.sectors["active"][this._sector()].nodes = this.nodes; - this.sectors["active"][this._sector()].edges = this.edges; - this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; - }, - - - /** - * /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied (active) sector. If a type is defined, do the specific type - * - * @param {String} sectorId - * @param {String} [sectorType] | "active" or "frozen" - * @private - */ - _switchToSector : function(sectorId, sectorType) { - if (sectorType === undefined || sectorType == "active") { - this._switchToActiveSector(sectorId); - } - else { - this._switchToFrozenSector(sectorId); - } - }, - - - /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied active sector. - * - * @param sectorId - * @private - */ - _switchToActiveSector : function(sectorId) { - this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["active"][sectorId]["nodes"]; - this.edges = this.sectors["active"][sectorId]["edges"]; - }, - - - /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied frozen sector. - * - * @param sectorId - * @private - */ - _switchToFrozenSector : function(sectorId) { - this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["frozen"][sectorId]["nodes"]; - this.edges = this.sectors["frozen"][sectorId]["edges"]; - }, - - - /** - * This function sets the global references to nodes, edges and nodeIndices to - * those of the navigation controls sector. - * - * @private - */ - _switchToNavigationSector : function() { - this.nodeIndices = this.sectors["navigation"]["nodeIndices"]; - this.nodes = this.sectors["navigation"]["nodes"]; - this.edges = this.sectors["navigation"]["edges"]; - }, - - - /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the currently active sector. - * - * @private - */ - _loadLatestSector : function() { - this._switchToSector(this._sector()); - }, - - - /** - * This function returns the currently active sector Id - * - * @returns {String} - * @private - */ - _sector : function() { - return this.activeSector[this.activeSector.length-1]; - }, - - - /** - * This function returns the previously active sector Id - * - * @returns {String} - * @private - */ - _previousSector : function() { - if (this.activeSector.length > 1) { - return this.activeSector[this.activeSector.length-2]; - } - else { - throw new TypeError('there are not enough sectors in the this.activeSector array.'); - } - }, - - - /** - * We add the active sector at the end of the this.activeSector array - * This ensures it is the currently active sector returned by _sector() and it reaches the top - * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. - * - * @param newId - * @private - */ - _setActiveSector : function(newId) { - this.activeSector.push(newId); - }, - - - /** - * We remove the currently active sector id from the active sector stack. This happens when - * we reactivate the previously active sector - * - * @private - */ - _forgetLastSector : function() { - this.activeSector.pop(); - }, - - - /** - * This function creates a new active sector with the supplied newId. This newId - * is the expanding node id. - * - * @param {String} newId | Id of the new active sector - * @private - */ - _createNewSector : function(newId) { - // create the new sector - this.sectors["active"][newId] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": this.scale, - "drawingNode": undefined}; - - // create the new sector render node. This gives visual feedback that you are in a new sector. - this.sectors["active"][newId]['drawingNode'] = new Node( - {id:newId, - color: { - background: "#eaefef", - border: "495c5e" - } - },{},{},this.constants); - this.sectors["active"][newId]['drawingNode'].clusterSize = 2; - }, - - - /** - * This function removes the currently active sector. This is called when we create a new - * active sector. - * - * @param {String} sectorId | Id of the active sector that will be removed - * @private - */ - _deleteActiveSector : function(sectorId) { - delete this.sectors["active"][sectorId]; - }, - - - /** - * This function removes the currently active sector. This is called when we reactivate - * the previously active sector. - * - * @param {String} sectorId | Id of the active sector that will be removed - * @private - */ - _deleteFrozenSector : function(sectorId) { - delete this.sectors["frozen"][sectorId]; - }, - - - /** - * Freezing an active sector means moving it from the "active" object to the "frozen" object. - * We copy the references, then delete the active entree. - * - * @param sectorId - * @private - */ - _freezeSector : function(sectorId) { - // we move the set references from the active to the frozen stack. - this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; - - // we have moved the sector data into the frozen set, we now remove it from the active set - this._deleteActiveSector(sectorId); - }, - - - /** - * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" - * object to the "active" object. - * - * @param sectorId - * @private - */ - _activateSector : function(sectorId) { - // we move the set references from the frozen to the active stack. - this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; - - // we have moved the sector data into the active set, we now remove it from the frozen stack - this._deleteFrozenSector(sectorId); - }, - - - /** - * This function merges the data from the currently active sector with a frozen sector. This is used - * in the process of reverting back to the previously active sector. - * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it - * upon the creation of a new active sector. - * - * @param sectorId - * @private - */ - _mergeThisWithFrozen : function(sectorId) { - // copy all nodes - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; - } - } - - // copy all edges (if not fully clustered, else there are no edges) - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; - } - } - - // merge the nodeIndices - for (var i = 0; i < this.nodeIndices.length; i++) { - this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); - } - }, - - - /** - * This clusters the sector to one cluster. It was a single cluster before this process started so - * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. - * - * @private - */ - _collapseThisToSingleCluster : function() { - this.clusterToFit(1,false); - }, - - - /** - * We create a new active sector from the node that we want to open. - * - * @param node - * @private - */ - _addSector : function(node) { - // this is the currently active sector - var sector = this._sector(); - -// // this should allow me to select nodes from a frozen set. -// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { -// console.log("the node is part of the active sector"); -// } -// else { -// console.log("I dont know what the fuck happened!!"); -// } - - // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. - delete this.nodes[node.id]; - - var unqiueIdentifier = util.randomUUID(); - - // we fully freeze the currently active sector - this._freezeSector(sector); - - // we create a new active sector. This sector has the Id of the node to ensure uniqueness - this._createNewSector(unqiueIdentifier); - - // we add the active sector to the sectors array to be able to revert these steps later on - this._setActiveSector(unqiueIdentifier); - - // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier - this._switchToSector(this._sector()); - - // finally we add the node we removed from our previous active sector to the new active sector - this.nodes[node.id] = node; - }, - - - /** - * We close the sector that is currently open and revert back to the one before. - * If the active sector is the "default" sector, nothing happens. - * - * @private - */ - _collapseSector : function() { - // the currently active sector - var sector = this._sector(); - - // we cannot collapse the default sector - if (sector != "default") { - if ((this.nodeIndices.length == 1) || - (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || - (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { - var previousSector = this._previousSector(); - - // we collapse the sector back to a single cluster - this._collapseThisToSingleCluster(); - - // we move the remaining nodes, edges and nodeIndices to the previous sector. - // This previous sector is the one we will reactivate - this._mergeThisWithFrozen(previousSector); - - // the previously active (frozen) sector now has all the data from the currently active sector. - // we can now delete the active sector. - this._deleteActiveSector(sector); - - // we activate the previously active (and currently frozen) sector. - this._activateSector(previousSector); - - // we load the references from the newly active sector into the global references - this._switchToSector(previousSector); - - // we forget the previously active sector because we reverted to the one before - this._forgetLastSector(); - - // finally, we update the node index list. - this._updateNodeIndexList(); - } - } - }, - - - /** - * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). - * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we dont pass the function itself because then the "this" is the window object - * | instead of the Graph object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - _doInAllActiveSectors : function(runFunction,argument) { - if (argument === undefined) { - for (var sector in this.sectors["active"]) { - if (this.sectors["active"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToActiveSector(sector); - this[runFunction](); - } - } - } - else { - for (var sector in this.sectors["active"]) { - if (this.sectors["active"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToActiveSector(sector); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - } - } - // we revert the global references back to our active sector - this._loadLatestSector(); - }, - - - /** - * This runs a function in all frozen sectors. This is used in the _redraw(). - * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Graph object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - _doInAllFrozenSectors : function(runFunction,argument) { - if (argument === undefined) { - for (var sector in this.sectors["frozen"]) { - if (this.sectors["frozen"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToFrozenSector(sector); - this[runFunction](); - } - } - } - else { - for (var sector in this.sectors["frozen"]) { - if (this.sectors["frozen"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToFrozenSector(sector); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - } - } - this._loadLatestSector(); - }, - - - /** - * This runs a function in the navigation controls sector. - * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Graph object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - _doInNavigationSector : function(runFunction,argument) { - this._switchToNavigationSector(); - if (argument === undefined) { - this[runFunction](); - } - else { - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - this._loadLatestSector(); - }, - - - /** - * This runs a function in all sectors. This is used in the _redraw(). - * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Graph object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - _doInAllSectors : function(runFunction,argument) { - var args = Array.prototype.splice.call(arguments, 1); - if (argument === undefined) { - this._doInAllActiveSectors(runFunction); - this._doInAllFrozenSectors(runFunction); - } - else { - if (args.length > 1) { - this._doInAllActiveSectors(runFunction,args[0],args[1]); - this._doInAllFrozenSectors(runFunction,args[0],args[1]); - } - else { - this._doInAllActiveSectors(runFunction,argument); - this._doInAllFrozenSectors(runFunction,argument); - } - } - - }, - - - /** - * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the - * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. - * - * @private - */ - _clearNodeIndexList : function() { - var sector = this._sector(); - this.sectors["active"][sector]["nodeIndices"] = []; - this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; - }, - - - /** - * Draw the encompassing sector node - * - * @param ctx - * @param sectorType - * @private - */ - _drawSectorNodes : function(ctx,sectorType) { - var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; - for (var sector in this.sectors[sectorType]) { - if (this.sectors[sectorType].hasOwnProperty(sector)) { - if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { - - this._switchToSector(sector,sectorType); - - minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.resize(ctx); - if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} - if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} - if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} - if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} - } - } - node = this.sectors[sectorType][sector]["drawingNode"]; - node.x = 0.5 * (maxX + minX); - node.y = 0.5 * (maxY + minY); - node.width = 2 * (node.x - minX); - node.height = 2 * (node.y - minY); - node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); - node.setScale(this.scale); - node._drawCircle(ctx); - } - } - } - }, - - _drawAllSectorNodes : function(ctx) { - this._drawSectorNodes(ctx,"frozen"); - this._drawSectorNodes(ctx,"active"); - this._loadLatestSector(); - } -}; -/** - * Creation of the ClusterMixin var. - * - * This contains all the functions the Graph object can use to employ clustering - * - * Alex de Mulder - * 21-01-2013 - */ -var ClusterMixin = { - -/** - * This is only called in the constructor of the graph object - * */ - startWithClustering : function() { - // cluster if the data set is big - this.clusterToFit(this.constants.clustering.initialMaxNodes, true); - - // updates the lables after clustering - this.updateLabels(); - - // this is called here because if clusterin is disabled, the start and stabilize are called in - // the setData function. - if (this.stabilize) { - this._doStabilize(); - } - this.start(); - }, - - /** - * This function clusters until the initialMaxNodes has been reached - * - * @param {Number} maxNumberOfNodes - * @param {Boolean} reposition - */ - clusterToFit : function(maxNumberOfNodes, reposition) { - var numberOfNodes = this.nodeIndices.length; - - var maxLevels = 50; - var level = 0; - - // we first cluster the hubs, then we pull in the outliers, repeat - while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { - if (level % 3 == 0) { - this.forceAggregateHubs(); - } - else { - this.increaseClusterLevel(); - } - numberOfNodes = this.nodeIndices.length; - level += 1; - } - - // after the clustering we reposition the nodes to reduce the initial chaos - if (level > 1 && reposition == true) { - this.repositionNodes(); - } - }, - - /** - * This function can be called to open up a specific cluster. It is only called by - * It will unpack the cluster back one level. - * - * @param node | Node object: cluster to open. - */ - openCluster : function(node) { - var isMovingBeforeClustering = this.moving; - if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && - !(this._sector() == "default" && this.nodeIndices.length == 1)) { - this._addSector(node); - var level = 0; - while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { - this.decreaseClusterLevel(); - level += 1; - } - } - else { - this._expandClusterNode(node,false,true); - - // update the index list, dynamic edges and labels - this._updateNodeIndexList(); - this._updateDynamicEdges(); - this.updateLabels(); - } - - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); - } - }, - - /** - * This calls the updateClustes with default arguments - */ - updateClustersDefault : function() { - if (this.constants.clustering.enabled == true) { - this.updateClusters(0,false,false); - } - }, - - /** - * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will - * be clustered with their connected node. This can be repeated as many times as needed. - * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. - */ - increaseClusterLevel : function() { - this.updateClusters(-1,false,true); - }, - - - - /** - * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will - * be unpacked if they are a cluster. This can be repeated as many times as needed. - * This can be called externally (by a key-bind for instance) to look into clusters without zooming. - */ - decreaseClusterLevel : function() { - this.updateClusters(1,false,true); - }, - - - /** - * This is the main clustering function. It clusters and declusters on zoom or forced - * This function clusters on zoom, it can be called with a predefined zoom direction - * If out, check if we can form clusters, if in, check if we can open clusters. - * This function is only called from _zoom() - * - * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn - * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters - * @param {Boolean} force | enabled or disable forcing - * - */ - updateClusters : function(zoomDirection,recursive,force) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; - - // on zoom out collapse the sector if the scale is at the level the sector was made - if (this.previousScale > this.scale && zoomDirection == 0) { - this._collapseSector(); - } - - // check if we zoom in or out - if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out - // forming clusters when forced pulls outliers in. When not forced, the edge length of the - // outer nodes determines if it is being clustered - this._formClusters(force); - } - else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in - if (force == true) { - // _openClusters checks for each node if the formationScale of the cluster is smaller than - // the current scale and if so, declusters. When forced, all clusters are reduced by one step - this._openClusters(recursive,force); - } - else { - // if a cluster takes up a set percentage of the active window - this._openClustersBySize(); - } - } - this._updateNodeIndexList(); - - // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs - if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { - this._aggregateHubs(force); - this._updateNodeIndexList(); - } - - // we now reduce chains. - if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out - this.handleChains(); - this._updateNodeIndexList(); - } - - this.previousScale = this.scale; - - // rest of the update the index list, dynamic edges and labels - this._updateDynamicEdges(); - this.updateLabels(); - - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place - this.clusterSession += 1; - } - - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); - } - }, - - /** - * This function handles the chains. It is called on every updateClusters(). - */ - handleChains : function() { - // after clustering we check how many chains there are - var chainPercentage = this._getChainFraction(); - if (chainPercentage > this.constants.clustering.chainThreshold) { - this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage) - - } - }, - - /** - * this functions starts clustering by hubs - * The minimum hub threshold is set globally - * - * @private - */ - _aggregateHubs : function(force) { - this._getHubSize(); - this._formClustersByHub(force,false); - }, - - - /** - * This function is fired by keypress. It forces hubs to form. - * - */ - forceAggregateHubs : function() { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; - - this._aggregateHubs(true); - - // update the index list, dynamic edges and labels - this._updateNodeIndexList(); - this._updateDynamicEdges(); - this.updateLabels(); - - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; - } - - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); - } - }, - - /** - * If a cluster takes up more than a set percentage of the screen, open the cluster - * - * @private - */ - _openClustersBySize : function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.inView() == true) { - if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || - (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { - this.openCluster(node); - } - } - } - } - }, - - - /** - * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it - * has to be opened based on the current zoom level. - * - * @private - */ - _openClusters : function(recursive,force) { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - this._expandClusterNode(node,recursive,force); - } - }, - - /** - * This function checks if a node has to be opened. This is done by checking the zoom level. - * If the node contains child nodes, this function is recursively called on the child nodes as well. - * This recursive behaviour is optional and can be set by the recursive argument. - * - * @param {Node} parentNode | to check for cluster and expand - * @param {Boolean} recursive | enabled or disable recursive calling - * @param {Boolean} force | enabled or disable forcing - * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released - * @private - */ - _expandClusterNode : function(parentNode, recursive, force, openAll) { - // first check if node is a cluster - if (parentNode.clusterSize > 1) { - // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 - if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) { - openAll = true; - } - recursive = openAll ? true : recursive; - - // if the last child has been added on a smaller scale than current scale decluster - if (parentNode.formationScale < this.scale || force == true) { - // we will check if any of the contained child nodes should be removed from the cluster - for (var containedNodeId in parentNode.containedNodes) { - if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) { - var childNode = parentNode.containedNodes[containedNodeId]; - - // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that - // the largest cluster is the one that comes from outside - if (force == true) { - if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] - || openAll) { - this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); - } - } - else { - if (this._nodeInActiveArea(parentNode)) { - this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); - } - } - } - } - } - } - }, - - /** - * ONLY CALLED FROM _expandClusterNode - * - * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove - * the child node from the parent contained_node object and put it back into the global nodes object. - * The same holds for the edge that was connected to the child node. It is moved back into the global edges object. - * - * @param {Node} parentNode | the parent node - * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node - * @param {Boolean} recursive | This will also check if the child needs to be expanded. - * With force and recursive both true, the entire cluster is unpacked - * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent - * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released - * @private - */ - _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) { - var childNode = parentNode.containedNodes[containedNodeId]; - - // if child node has been added on smaller scale than current, kick out - if (childNode.formationScale < this.scale || force == true) { - // remove the selection, first remove the selection from the connected edges - this._unselectConnectedEdges(parentNode); - parentNode.unselect(); - - // put the child node back in the global nodes object - this.nodes[containedNodeId] = childNode; - - // release the contained edges from this childNode back into the global edges - this._releaseContainedEdges(parentNode,childNode); - - // reconnect rerouted edges to the childNode - this._connectEdgeBackToChild(parentNode,childNode); - - // validate all edges in dynamicEdges - this._validateEdges(parentNode); - - // undo the changes from the clustering operation on the parent node - parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass; - parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; - parentNode.clusterSize -= childNode.clusterSize; - parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; - - // place the child node near the parent, not at the exact same location to avoid chaos in the system - childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; - childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; - - // remove node from the list - delete parentNode.containedNodes[containedNodeId]; - - // check if there are other childs with this clusterSession in the parent. - var othersPresent = false; - for (var childNodeId in parentNode.containedNodes) { - if (parentNode.containedNodes.hasOwnProperty(childNodeId)) { - if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) { - othersPresent = true; - break; - } - } - } - // if there are no others, remove the cluster session from the list - if (othersPresent == false) { - parentNode.clusterSessions.pop(); - } - - // remove the clusterSession from the child node - childNode.clusterSession = 0; - - // restart the simulation to reorganise all nodes - this.moving = true; - - // recalculate the size of the node on the next time the node is rendered - parentNode.clearSizeCache(); - - // this unselects the rest of the edges - this._unselectConnectedEdges(parentNode); - } - - // check if a further expansion step is possible if recursivity is enabled - if (recursive == true) { - this._expandClusterNode(childNode,recursive,force,openAll); - } - }, - - - /** - * This function checks if any nodes at the end of their trees have edges below a threshold length - * This function is called only from updateClusters() - * forceLevelCollapse ignores the length of the edge and collapses one level - * This means that a node with only one edge will be clustered with its connected node - * - * @private - * @param {Boolean} force - */ - _formClusters : function(force) { - if (force == false) { - this._formClustersByZoom(); - } - else { - this._forceClustersByZoom(); - } - }, - - /** - * This function handles the clustering by zooming out, this is based on a minimum edge distance - * - * @private - */ - _formClustersByZoom : function() { - var dx,dy,length, - minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; - - // check if any edges are shorter than minLength and start the clustering - // the clustering favours the node with the larger mass - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - var edge = this.edges[edgeId]; - if (edge.connected) { - if (edge.toId != edge.fromId) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - length = Math.sqrt(dx * dx + dy * dy); - - - if (length < minLength) { - // first check which node is larger - var parentNode = edge.from; - var childNode = edge.to; - if (edge.to.mass > edge.from.mass) { - parentNode = edge.to; - childNode = edge.from; - } - - if (childNode.dynamicEdgesLength == 1) { - this._addToCluster(parentNode,childNode,false); - } - else if (parentNode.dynamicEdgesLength == 1) { - this._addToCluster(childNode,parentNode,false); - } - } - } - } - } - } - }, - - /** - * This function forces the graph to cluster all nodes with only one connecting edge to their - * connected node. - * - * @private - */ - _forceClustersByZoom : function() { - for (var nodeId in this.nodes) { - // another node could have absorbed this child. - if (this.nodes.hasOwnProperty(nodeId)) { - var childNode = this.nodes[nodeId]; - - // the edges can be swallowed by another decrease - if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) { - var edge = childNode.dynamicEdges[0]; - var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; - - // group to the largest node - if (childNode.id != parentNode.id) { - if (parentNode.mass > childNode.mass) { - this._addToCluster(parentNode,childNode,true); - } - else { - this._addToCluster(childNode,parentNode,true); - } - } - } - } - } - }, - - - - /** - * This function forms clusters from hubs, it loops over all nodes - * - * @param {Boolean} force | Disregard zoom level - * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges - * @private - */ - _formClustersByHub : function(force, onlyEqual) { - // we loop over all nodes in the list - for (var nodeId in this.nodes) { - // we check if it is still available since it can be used by the clustering in this loop - if (this.nodes.hasOwnProperty(nodeId)) { - this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); - } - } - }, - - /** - * This function forms a cluster from a specific preselected hub node - * - * @param {Node} hubNode | the node we will cluster as a hub - * @param {Boolean} force | Disregard zoom level - * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges - * @param {Number} [absorptionSizeOffset] | - * @private - */ - _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) { - if (absorptionSizeOffset === undefined) { - absorptionSizeOffset = 0; - } - // we decide if the node is a hub - if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || - (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { - // initialize variables - var dx,dy,length; - var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; - var allowCluster = false; - - // we create a list of edges because the dynamicEdges change over the course of this loop - var edgesIdarray = []; - var amountOfInitialEdges = hubNode.dynamicEdges.length; - for (var j = 0; j < amountOfInitialEdges; j++) { - edgesIdarray.push(hubNode.dynamicEdges[j].id); - } - - // if the hub clustering is not forces, we check if one of the edges connected - // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold - if (force == false) { - allowCluster = false; - for (j = 0; j < amountOfInitialEdges; j++) { - var edge = this.edges[edgesIdarray[j]]; - if (edge !== undefined) { - if (edge.connected) { - if (edge.toId != edge.fromId) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - length = Math.sqrt(dx * dx + dy * dy); - - if (length < minLength) { - allowCluster = true; - break; - } - } - } - } - } - } - - // start the clustering if allowed - if ((!force && allowCluster) || force) { - // we loop over all edges INITIALLY connected to this hub - for (j = 0; j < amountOfInitialEdges; j++) { - edge = this.edges[edgesIdarray[j]]; - // the edge can be clustered by this function in a previous loop - if (edge !== undefined) { - var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; - // we do not want hubs to merge with other hubs nor do we want to cluster itself. - if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) && - (childNode.id != hubNode.id)) { - this._addToCluster(hubNode,childNode,force); - } - } - } - } - } - }, - - - - /** - * This function adds the child node to the parent node, creating a cluster if it is not already. - * - * @param {Node} parentNode | this is the node that will house the child node - * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node - * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse - * @private - */ - _addToCluster : function(parentNode, childNode, force) { - // join child node in the parent node - parentNode.containedNodes[childNode.id] = childNode; - - // manage all the edges connected to the child and parent nodes - for (var i = 0; i < childNode.dynamicEdges.length; i++) { - var edge = childNode.dynamicEdges[i]; - if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode - this._addToContainedEdges(parentNode,childNode,edge); - } - else { - this._connectEdgeToCluster(parentNode,childNode,edge); - } - } - // a contained node has no dynamic edges. - childNode.dynamicEdges = []; - - // remove circular edges from clusters - this._containCircularEdgesFromNode(parentNode,childNode); - - - // remove the childNode from the global nodes object - delete this.nodes[childNode.id]; - - // update the properties of the child and parent - var massBefore = parentNode.mass; - childNode.clusterSession = this.clusterSession; - parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass; - parentNode.clusterSize += childNode.clusterSize; - parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; - - // keep track of the clustersessions so we can open the cluster up as it has been formed. - if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { - parentNode.clusterSessions.push(this.clusterSession); - } - - // forced clusters only open from screen size and double tap - if (force == true) { - // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); - parentNode.formationScale = 0; - } - else { - parentNode.formationScale = this.scale; // The latest child has been added on this scale - } - - // recalculate the size of the node on the next time the node is rendered - parentNode.clearSizeCache(); - - // set the pop-out scale for the childnode - parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale; - - // nullify the movement velocity of the child, this is to avoid hectic behaviour - childNode.clearVelocity(); - - // the mass has altered, preservation of energy dictates the velocity to be updated - parentNode.updateVelocity(massBefore); - - // restart the simulation to reorganise all nodes - this.moving = true; - }, - - - /** - * This function will apply the changes made to the remainingEdges during the formation of the clusters. - * This is a seperate function to allow for level-wise collapsing of the node tree. - * It has to be called if a level is collapsed. It is called by _formClusters(). - * @private - */ - _updateDynamicEdges : function() { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - node.dynamicEdgesLength = node.dynamicEdges.length; - - // this corrects for multiple edges pointing at the same other node - var correction = 0; - if (node.dynamicEdgesLength > 1) { - for (var j = 0; j < node.dynamicEdgesLength - 1; j++) { - var edgeToId = node.dynamicEdges[j].toId; - var edgeFromId = node.dynamicEdges[j].fromId; - for (var k = j+1; k < node.dynamicEdgesLength; k++) { - if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) || - (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) { - correction += 1; - } - } - } - } - node.dynamicEdgesLength -= correction; - } - }, - - - /** - * This adds an edge from the childNode to the contained edges of the parent node - * - * @param parentNode | Node object - * @param childNode | Node object - * @param edge | Edge object - * @private - */ - _addToContainedEdges : function(parentNode, childNode, edge) { - // create an array object if it does not yet exist for this childNode - if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) { - parentNode.containedEdges[childNode.id] = [] - } - // add this edge to the list - parentNode.containedEdges[childNode.id].push(edge); - - // remove the edge from the global edges object - delete this.edges[edge.id]; - - // remove the edge from the parent object - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - if (parentNode.dynamicEdges[i].id == edge.id) { - parentNode.dynamicEdges.splice(i,1); - break; - } - } - }, - - /** - * This function connects an edge that was connected to a child node to the parent node. - * It keeps track of which nodes it has been connected to with the originalId array. - * - * @param {Node} parentNode | Node object - * @param {Node} childNode | Node object - * @param {Edge} edge | Edge object - * @private - */ - _connectEdgeToCluster : function(parentNode, childNode, edge) { - // handle circular edges - if (edge.toId == edge.fromId) { - this._addToContainedEdges(parentNode, childNode, edge); - } - else { - if (edge.toId == childNode.id) { // edge connected to other node on the "to" side - edge.originalToId.push(childNode.id); - edge.to = parentNode; - edge.toId = parentNode.id; - } - else { // edge connected to other node with the "from" side - - edge.originalFromId.push(childNode.id); - edge.from = parentNode; - edge.fromId = parentNode.id; - } - - this._addToReroutedEdges(parentNode,childNode,edge); - } - }, - - - _containCircularEdgesFromNode : function(parentNode, childNode) { - // manage all the edges connected to the child and parent nodes - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - var edge = parentNode.dynamicEdges[i]; - // handle circular edges - if (edge.toId == edge.fromId) { - this._addToContainedEdges(parentNode, childNode, edge); - } - } - }, - - - /** - * This adds an edge from the childNode to the rerouted edges of the parent node - * - * @param parentNode | Node object - * @param childNode | Node object - * @param edge | Edge object - * @private - */ - _addToReroutedEdges : function(parentNode, childNode, edge) { - // create an array object if it does not yet exist for this childNode - // we store the edge in the rerouted edges so we can restore it when the cluster pops open - if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) { - parentNode.reroutedEdges[childNode.id] = []; - } - parentNode.reroutedEdges[childNode.id].push(edge); - - // this edge becomes part of the dynamicEdges of the cluster node - parentNode.dynamicEdges.push(edge); - }, - - - - /** - * This function connects an edge that was connected to a cluster node back to the child node. - * - * @param parentNode | Node object - * @param childNode | Node object - * @private - */ - _connectEdgeBackToChild : function(parentNode, childNode) { - if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) { - for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) { - var edge = parentNode.reroutedEdges[childNode.id][i]; - if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) { - edge.originalFromId.pop(); - edge.fromId = childNode.id; - edge.from = childNode; - } - else { - edge.originalToId.pop(); - edge.toId = childNode.id; - edge.to = childNode; - } - - // append this edge to the list of edges connecting to the childnode - childNode.dynamicEdges.push(edge); - - // remove the edge from the parent object - for (var j = 0; j < parentNode.dynamicEdges.length; j++) { - if (parentNode.dynamicEdges[j].id == edge.id) { - parentNode.dynamicEdges.splice(j,1); - break; - } - } - } - // remove the entry from the rerouted edges - delete parentNode.reroutedEdges[childNode.id]; - } - }, - - - /** - * When loops are clustered, an edge can be both in the rerouted array and the contained array. - * This function is called last to verify that all edges in dynamicEdges are in fact connected to the - * parentNode - * - * @param parentNode | Node object - * @private - */ - _validateEdges : function(parentNode) { - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - var edge = parentNode.dynamicEdges[i]; - if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { - parentNode.dynamicEdges.splice(i,1); - } - } - }, - - - /** - * This function released the contained edges back into the global domain and puts them back into the - * dynamic edges of both parent and child. - * - * @param {Node} parentNode | - * @param {Node} childNode | - * @private - */ - _releaseContainedEdges : function(parentNode, childNode) { - for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) { - var edge = parentNode.containedEdges[childNode.id][i]; - - // put the edge back in the global edges object - this.edges[edge.id] = edge; - - // put the edge back in the dynamic edges of the child and parent - childNode.dynamicEdges.push(edge); - parentNode.dynamicEdges.push(edge); - } - // remove the entry from the contained edges - delete parentNode.containedEdges[childNode.id]; - - }, - - - - - // ------------------- UTILITY FUNCTIONS ---------------------------- // - - - /** - * This updates the node labels for all nodes (for debugging purposes) - */ - updateLabels : function() { - var nodeId; - // update node labels - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.clusterSize > 1) { - node.label = "[".concat(String(node.clusterSize),"]"); - } - } - } - - // update node labels - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.clusterSize == 1) { - if (node.originalLabel !== undefined) { - node.label = node.originalLabel; - } - else { - node.label = String(node.id); - } - } - } - } - - /* Debug Override */ - // for (nodeId in this.nodes) { - // if (this.nodes.hasOwnProperty(nodeId)) { - // node = this.nodes[nodeId]; - // node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale)); - // } - // } - - }, - - - /** - * This function determines if the cluster we want to decluster is in the active area - * this means around the zoom center - * - * @param {Node} node - * @returns {boolean} - * @private - */ - _nodeInActiveArea : function(node) { - return ( - Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale - && - Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale - ) - }, - - - /** - * This is an adaptation of the original repositioning function. This is called if the system is clustered initially - * It puts large clusters away from the center and randomizes the order. - * - */ - repositionNodes : function() { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - if (!node.isFixed()) { - var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize); - var angle = 2 * Math.PI * Math.random(); - node.x = radius * Math.cos(angle); - node.y = radius * Math.sin(angle); - } - } - }, - - - - - - /** - * We determine how many connections denote an important hub. - * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) - * - * @private - */ - _getHubSize : function() { - var average = 0; - var averageSquared = 0; - var hubCounter = 0; - var largestHub = 0; - - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - if (node.dynamicEdgesLength > largestHub) { - largestHub = node.dynamicEdgesLength; - } - average += node.dynamicEdgesLength; - averageSquared += Math.pow(node.dynamicEdgesLength,2); - hubCounter += 1; - } - average = average / hubCounter; - averageSquared = averageSquared / hubCounter; - - var variance = averageSquared - Math.pow(average,2); - - var standardDeviation = Math.sqrt(variance); - - this.hubThreshold = Math.floor(average + 2*standardDeviation); - - // always have at least one to cluster - if (this.hubThreshold > largestHub) { - this.hubThreshold = largestHub; - } - - // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); - // console.log("hubThreshold:",this.hubThreshold); - }, - - - /** - * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods - * with this amount we can cluster specifically on these chains. - * - * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce - * @private - */ - _reduceAmountOfChains : function(fraction) { - this.hubThreshold = 2; - var reduceAmount = Math.floor(this.nodeIndices.length * fraction); - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { - if (reduceAmount > 0) { - this._formClusterFromHub(this.nodes[nodeId],true,true,1); - reduceAmount -= 1; - } - } - } - } - }, - - /** - * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods - * with this amount we can cluster specifically on these chains. - * - * @private - */ - _getChainFraction : function() { - var chains = 0; - var total = 0; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { - chains += 1; - } - total += 1; - } - } - return chains/total; - } -}; - - -var SelectionMixin = { - - /** - * This function can be called from the _doInAllSectors function - * - * @param object - * @param overlappingNodes - * @private - */ - _getNodesOverlappingWith : function(object, overlappingNodes) { - var nodes = this.nodes; - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); - } - } - } - }, - - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - _getAllNodesOverlappingWith : function (object) { - var overlappingNodes = []; - this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); - return overlappingNodes; - }, - - - /** - * retrieve all nodes in the navigation controls overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - _getAllNavigationNodesOverlappingWith : function (object) { - var overlappingNodes = []; - this._doInNavigationSector("_getNodesOverlappingWith",object,overlappingNodes); - return overlappingNodes; - }, - - /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private - */ - _pointerToPositionObject : function(pointer) { - var x = this._canvasToX(pointer.x); - var y = this._canvasToY(pointer.y); - - return {left: x, - top: y, - right: x, - bottom: y}; - }, - - /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private - */ - _pointerToScreenPositionObject : function(pointer) { - var x = pointer.x; - var y = pointer.y; - - return {left: x, - top: y, - right: x, - bottom: y}; - }, - - - /** - * Get the top navigation controls node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node - * @private - */ - _getNavigationNodeAt : function (pointer) { - var screenPositionObject = this._pointerToScreenPositionObject(pointer); - var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject); - if (overlappingNodes.length > 0) { - return this.sectors["navigation"]["nodes"][overlappingNodes[overlappingNodes.length - 1]]; - } - else { - return null; - } - }, - - - /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node - * @private - */ - _getNodeAt : function (pointer) { - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } - else { - return null; - } - }, - - - /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - _getEdgesOverlappingWith : function (object, overlappingEdges) { - var edges = this.edges; - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); - } - } - } - }, - - - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - _getAllEdgesOverlappingWith : function (object) { - var overlappingEdges = []; - this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); - return overlappingEdges; - }, - - /** - * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call - * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. - * - * @param pointer - * @returns {null} - * @private - */ - _getEdgeAt : function(pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - - if (overlappingEdges.length > 0) { - return this.edges[overlappingEdges[overlappingEdges.length - 1]]; - } - else { - return null; - } - }, - - - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - _addToSelection : function(obj) { - this.selectionObj[obj.id] = obj; - }, - - - /** - * Remove a single option from selection. - * - * @param {Object} obj - * @private - */ - _removeFromSelection : function(obj) { - delete this.selectionObj[obj.id]; - }, - - - /** - * Unselect all. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - _unselectAll : function(doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - this.selectionObj[objectId].unselect(); - } - } - this.selectionObj = {}; - - if (doNotTrigger == false) { - this._trigger('select', { - nodes: this.getSelection() - }); - } - }, - - /** - * Unselect all clusters. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - _unselectClusters : function(doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (this.selectionObj[objectId].clusterSize > 1) { - this.selectionObj[objectId].unselect(); - this._removeFromSelection(this.selectionObj[objectId]); - } - } - } - } - - if (doNotTrigger == false) { - this._trigger('select', { - nodes: this.getSelection() - }); - } - }, - - - /** - * return the number of selected nodes - * - * @returns {number} - * @private - */ - _getSelectedNodeCount : function() { - var count = 0; - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - count += 1; - } - } - } - return count; - }, - - - /** - * return the number of selected edges - * - * @returns {number} - * @private - */ - _getSelectedEdgeCount : function() { - var count = 0; - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Edge) { - count += 1; - } - } - } - return count; - }, - - - /** - * return the number of selected objects. - * - * @returns {number} - * @private - */ - _getSelectedObjectCount : function() { - var count = 0; - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - count += 1; - } - } - return count; - }, - - /** - * Check if anything is selected - * - * @returns {boolean} - * @private - */ - _selectionIsEmpty : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - return false; - } - } - return true; - }, - - - /** - * check if one of the selected nodes is a cluster. - * - * @returns {boolean} - * @private - */ - _clusterInSelection : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (this.selectionObj[objectId].clusterSize > 1) { - return true; - } - } - } - } - return false; - }, - - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - _selectConnectedEdges : function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.select(); - this._addToSelection(edge); - } - }, - - - /** - * unselect the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - _unselectConnectedEdges : function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.unselect(); - this._removeFromSelection(edge); - } - }, - - - - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @param {Boolean} append - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - _selectObject : function(object, append, doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - - if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { - this._unselectAll(true); - } - - if (object.selected == false) { - object.select(); - this._addToSelection(object); - if (object instanceof Node && this.blockConnectingEdgeSelection == false) { - this._selectConnectedEdges(object); - } - } - else { - object.unselect(); - this._removeFromSelection(object); - } - if (doNotTrigger == false) { - this._trigger('select', { - nodes: this.getSelection() - }); - } - }, - - - /** - * handles the selection part of the touch, only for navigation controls elements; - * Touch is triggered before tap, also before hold. Hold triggers after a while. - * This is the most responsive solution - * - * @param {Object} pointer - * @private - */ - _handleTouch : function(pointer) { - if (this.constants.navigation.enabled == true) { - this.pointerPosition = pointer; - var node = this._getNavigationNodeAt(pointer); - if (node != null) { - if (this[node.triggerFunction] !== undefined) { - this[node.triggerFunction](); - } - } - } - }, - - - /** - * handles the selection part of the tap; - * - * @param {Object} pointer - * @private - */ - _handleTap : function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node,false); - } - else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge,false); - } - else { - this._unselectAll(); - } - } - this._redraw(); - }, - - - /** - * handles the selection part of the double tap and opens a cluster if needed - * - * @param {Object} pointer - * @private - */ - _handleDoubleTap : function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null && node !== undefined) { - // we reset the areaCenter here so the opening of the node will occur - this.areaCenter = {"x" : this._canvasToX(pointer.x), - "y" : this._canvasToY(pointer.y)}; - this.openCluster(node); - } - }, - - - /** - * Handle the onHold selection part - * - * @param pointer - * @private - */ - _handleOnHold : function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node,true); - } - else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge,true); - } - } - this._redraw(); - }, - - - /** - * handle the onRelease event. These functions are here for the navigation controls module. - * - * @private - */ - _handleOnRelease : function() { - this.xIncrement = 0; - this.yIncrement = 0; - this.zoomIncrement = 0; - this._unHighlightAll(); - }, - - - - /** - * - * retrieve the currently selected objects - * @return {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - getSelection : function() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return {nodes:nodeIds, edges:edgeIds}; - }, - - /** - * - * retrieve the currently selected nodes - * @return {String} selection An array with the ids of the - * selected nodes. - */ - getSelectedNodes : function() { - var idArray = []; - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - idArray.push(objectId); - } - } - } - return idArray - }, - - /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. - */ - getSelectedEdges : function() { - var idArray = []; - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Edge) { - idArray.push(objectId); - } - } - } - return idArray - }, - - - /** - * select zero or more nodes - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - 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 - this._unselectAll(true); - - 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'); - } - this._selectObject(node,true,true); - } - this.redraw(); - }, - - - /** - * Validate the selection: remove ids of nodes which no longer exist - * @private - */ - _updateSelection : function () { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (!this.nodes.hasOwnProperty(objectId)) { - delete this.selectionObj[objectId]; - } - } - else { // assuming only edges and nodes are selected - if (!this.edges.hasOwnProperty(objectId)) { - delete this.selectionObj[objectId]; - } - } - } - } - } - -} -/** - * 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 - */ -/* _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; ->>>>>>> develop - } - } - } - } - - -<<<<<<< HEAD -======= - if (changed) { - // fire the select event - this._trigger('select', { - nodes: this.getSelection() - }); - } ->>>>>>> develop - -}; - - - - -/** - * Created by Alex on 1/22/14. - */ - -var NavigationMixin = { - - /** - * This function moves the navigation controls if the canvas size has been changed. If the arugments - * verticaAlignTop and horizontalAlignLeft are false, the correction will be made - * - * @private - */ - _relocateNavigation : function() { - if (this.sectors !== undefined) { - var xOffset = this.navigationClientWidth - this.frame.canvas.clientWidth; - var yOffset = this.navigationClientHeight - this.frame.canvas.clientHeight; - this.navigationClientWidth = this.frame.canvas.clientWidth; - this.navigationClientHeight = this.frame.canvas.clientHeight; - var node = null; - - for (var nodeId in this.sectors["navigation"]["nodes"]) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(nodeId)) { - node = this.sectors["navigation"]["nodes"][nodeId]; - if (!node.horizontalAlignLeft) { - node.x -= xOffset; - } - if (!node.verticalAlignTop) { - node.y -= yOffset; - } - } - } - } - }, - - - /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. - * - * @private - */ - _loadNavigationElements : function() { - var DIR = this.constants.navigation.iconPath; - this.navigationClientWidth = this.frame.canvas.clientWidth; - this.navigationClientHeight = this.frame.canvas.clientHeight; - if (this.navigationClientWidth === undefined) { - this.navigationClientWidth = 0; - this.navigationClientHeight = 0; - } - var offset = 15; - var intermediateOffset = 7; - var navigationNodes = [ - {id: 'navigation_up', shape: 'image', image: DIR + '/uparrow.png', triggerFunction: "_moveUp", - verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 45 - offset - intermediateOffset}, - {id: 'navigation_down', shape: 'image', image: DIR + '/downarrow.png', triggerFunction: "_moveDown", - verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_left', shape: 'image', image: DIR + '/leftarrow.png', triggerFunction: "_moveLeft", - verticalAlignTop: false, x: 15 + offset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_right', shape: 'image', image: DIR + '/rightarrow.png',triggerFunction: "_moveRight", - verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - - {id: 'navigation_plus', shape: 'image', image: DIR + '/plus.png', triggerFunction: "_zoomIn", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 45 - offset - intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_min', shape: 'image', image: DIR + '/minus.png', triggerFunction: "_zoomOut", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_zoomExtends', shape: 'image', image: DIR + '/zoomExtends.png', triggerFunction: "zoomToFit", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 45 - offset - intermediateOffset} - ]; - - var nodeObj = null; - for (var i = 0; i < navigationNodes.length; i++) { - nodeObj = this.sectors["navigation"]['nodes']; - nodeObj[navigationNodes[i]['id']] = new Node(navigationNodes[i], this.images, this.groups, this.constants); - } - }, - - - /** - * By setting the clustersize to be larger than 1, we use the clustering drawing method - * to illustrate the buttons are presed. We call this highlighting. - * - * @param {String} elementId - * @private - */ - _highlightNavigationElement : function(elementId) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { - this.sectors["navigation"]["nodes"][elementId].clusterSize = 2; - } - }, - - - /** - * Reverting back to a normal button - * - * @param {String} elementId - * @private - */ - _unHighlightNavigationElement : function(elementId) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { - this.sectors["navigation"]["nodes"][elementId].clusterSize = 1; - } - }, - - /** - * un-highlight (for lack of a better term) all navigation controls elements - * @private - */ - _unHighlightAll : function() { - for (var nodeId in this.sectors['navigation']['nodes']) { - if (this.sectors['navigation']['nodes'].hasOwnProperty(nodeId)) { - this._unHighlightNavigationElement(nodeId); - } - } - }, - - - _preventDefault : function(event) { - if (event !== undefined) { - if (event.preventDefault) { - event.preventDefault(); - } else { - event.returnValue = false; - } - } - }, - - - /** - * move the screen up - * By using the increments, instead of adding a fixed number to the translation, we keep fluent and - * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently - * To avoid this behaviour, we do the translation in the start loop. - * - * @private - */ - _moveUp : function(event) { - this._highlightNavigationElement("navigation_up"); - this.yIncrement = this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * move the screen down - * @private - */ - _moveDown : function(event) { - this._highlightNavigationElement("navigation_down"); - this.yIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * move the screen left - * @private - */ - _moveLeft : function(event) { - this._highlightNavigationElement("navigation_left"); - this.xIncrement = this.constants.keyboard.speed.x; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * move the screen right - * @private - */ - _moveRight : function(event) { - this._highlightNavigationElement("navigation_right"); - this.xIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * Zoom in, using the same method as the movement. - * @private - */ - _zoomIn : function(event) { - this._highlightNavigationElement("navigation_plus"); - this.zoomIncrement = this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * Zoom out - * @private - */ - _zoomOut : function() { - this._highlightNavigationElement("navigation_min"); - this.zoomIncrement = -this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * Stop zooming and unhighlight the zoom controls - * @private - */ - _stopZoom : function() { - this._unHighlightNavigationElement("navigation_plus"); - this._unHighlightNavigationElement("navigation_min"); - - this.zoomIncrement = 0; - }, - - - /** - * Stop moving in the Y direction and unHighlight the up and down - * @private - */ - _yStopMoving : function() { - this._unHighlightNavigationElement("navigation_up"); - this._unHighlightNavigationElement("navigation_down"); - - this.yIncrement = 0; - }, - - - /** - * Stop moving in the X direction and unHighlight left and right. - * @private - */ - _xStopMoving : function() { - this._unHighlightNavigationElement("navigation_left"); - this._unHighlightNavigationElement("navigation_right"); - - this.xIncrement = 0; - } - - -}; - -/** - * @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%'; - // to give everything a nice fluidity, we seperate the rendering and calculating of the forces - this.renderRefreshRate = 60; // hz (fps) - this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on - this.stabilize = true; // stabilize before displaying the graph - this.selectable = true; - - this.forceFactor = 50000; - - // 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 - } - }, - clustering: { // Per Node in Cluster = PNiC - enabled: false, // (Boolean) | global on/off switch for clustering. - initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold. - clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes - reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this - chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains). - clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered. - sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. - screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node. - fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px). - forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). - distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). - edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. - nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster. - height: 10, // (px PNiC) | growth of the height per node in cluster. - radius: 10}, // (px PNiC) | growth of the radius per node in cluster. - activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open. - massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass - }, - navigation: { - enabled: false, - iconPath: this._getScriptPath() + '/img' - }, - keyboard: { - enabled: false, - speed: {x: 10, y: 10, zoom: 0.02} - }, - dataManipulationToolbar: { - enabled: false - }, - minVelocity: 2, // px/s - maxIterations: 1000 // maximum number of iteration to stabilize - }; - - // Node variables - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function () { - graph._redraw(); - }); - - // navigation variables - this.xIncrement = 0; - this.yIncrement = 0; - this.zoomIncrement = 0; - - // create a frame and canvas - this._create(); - - // load the sector system. (mandatory, fully integrated with Graph) - this._loadSectorSystem(); - - // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) - this._loadClusterSystem(); - - // load the selection system. (mandatory, required by Graph) - this._loadSelectionSystem(); - - // apply options - this.setOptions(options); - - // other vars - var graph = this; - this.freezeSimulation = false;// freeze the simulation - - this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation - this.nodes = {}; // object with Node objects - this.edges = {}; // object with Edge objects - - this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. - this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - - this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action - this.scale = 1; // defining the global scale variable in the constructor - this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out - // 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(); - } - }; - - // properties of the data - this.moving = false; // True if any of the nodes have an undefined position - this.timer = undefined; - - // load data (the disable start variable will be the same as the enabled clustering) - this.setData(data,this.constants.clustering.enabled); - - // zoom so all data will fit on the screen - this.zoomToFit(true); - - // if clustering is disabled, the simulation will have started in the setData function - if (this.constants.clustering.enabled) { - this.startWithClustering(); - } -} - -/** - * Get the script path where the vis.js library is located - * - * @returns {string | null} path Path or null when not found. Path does not - * end with a slash. - * @private - */ -Graph.prototype._getScriptPath = function() { - var scripts = document.getElementsByTagName( 'script' ); - - // find script named vis.js or vis.min.js - for (var i = 0; i < scripts.length; i++) { - var src = scripts[i].src; - var match = src && /\/?vis(.min)?\.js$/.exec(src); - if (match) { - // return path without the script name - return src.substring(0, src.length - match[0].length); - } - } - - return null; -}; - - -/** - * Find the center position of the graph - * @private - */ -Graph.prototype._getRange = function() { - var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; - for (var i = 0; i < this.nodeIndices.length; i++) { - node = this.nodes[this.nodeIndices[i]]; - if (minX > (node.x - node.width)) {minX = node.x - node.width;} - if (maxX < (node.x + node.width)) {maxX = node.x + node.width;} - if (minY > (node.y - node.height)) {minY = node.y - node.height;} - if (maxY < (node.y + node.height)) {maxY = node.y + node.height;} - } - return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; -}; - - -/** - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - * @returns {{x: number, y: number}} - * @private - */ -Graph.prototype._findCenter = function(range) { - var center = {x: (0.5 * (range.maxX + range.minX)), - y: (0.5 * (range.maxY + range.minY))}; - return center; -}; - - -/** - * center the graph - * - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - */ -Graph.prototype._centerGraph = function(range) { - var center = this._findCenter(range); - - center.x *= this.scale; - center.y *= this.scale; - center.x -= 0.5 * this.frame.canvas.clientWidth; - center.y -= 0.5 * this.frame.canvas.clientHeight; - - this._setTranslation(-center.x,-center.y); // set at 0,0 -}; - - -/** - * This function zooms out to fit all data on screen based on amount of nodes - * - * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; - */ -Graph.prototype.zoomToFit = function(initialZoom) { - if (initialZoom === undefined) { - initialZoom = false; - } - - var numberOfNodes = this.nodeIndices.length; - var range = this._getRange(); - - if (initialZoom == true) { - if (this.constants.clustering.enabled == true && - numberOfNodes >= this.constants.clustering.initialMaxNodes) { - var zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - else { - var zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - } - else { - var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1; - var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1; - - var xZoomLevel = this.frame.canvas.clientWidth / xDistance; - var yZoomLevel = this.frame.canvas.clientHeight / yDistance; - - zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; - } - - if (zoomLevel > 1.0) { - zoomLevel = 1.0; - } - - this.pinch.mousewheelScale = zoomLevel; - this._setScale(zoomLevel); - this._centerGraph(range); - this.start(); -}; - - -/** - * Update the this.nodeIndices with the most recent node index list - * @private - */ -Graph.prototype._updateNodeIndexList = function() { - this._clearNodeIndexList(); - for (var idx in this.nodes) { - if (this.nodes.hasOwnProperty(idx)) { - this.nodeIndices.push(idx); - } - } -}; - - -/** - * 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 - * @param {Boolean} [disableStart] | optional: disable the calling of the start function. - */ -Graph.prototype.setData = function(data, disableStart) { - if (disableStart === undefined) { - disableStart = false; - } - - 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); - } - - this._putDataInSector(); - - if (!disableStart) { - // find a stable position or start animating to a stable position - if (this.stabilize) { - this._doStabilize(); - } - this.moving = true; - 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;} - - if (options.clustering) { - this.constants.clustering.enabled = true; - for (var prop in options.clustering) { - if (options.clustering.hasOwnProperty(prop)) { - this.constants.clustering[prop] = options.clustering[prop]; - } - } - } - else if (options.clustering !== undefined) { - this.constants.clustering.enabled = false; - } - - if (options.navigation) { - this.constants.navigation.enabled = true; - for (var prop in options.navigation) { - if (options.navigation.hasOwnProperty(prop)) { - this.constants.navigation[prop] = options.navigation[prop]; - } - } - } - else if (options.navigation !== undefined) { - this.constants.navigation.enabled = false; - } - - if (options.keyboard) { - this.constants.keyboard.enabled = true; - for (var prop in options.keyboard) { - if (options.keyboard.hasOwnProperty(prop)) { - this.constants.keyboard[prop] = options.keyboard[prop]; - } - } - } - else if (options.keyboard !== undefined) { - this.constants.keyboard.enabled = false; - } - - if (options.dataManipulationToolbar) { - this.constants.dataManipulationToolbar.enabled = true; - for (var prop in options.dataManipulationToolbar) { - if (options.dataManipulationToolbar.hasOwnProperty(prop)) { - this.constants.dataManipulationToolbar[prop] = options.dataManipulationToolbar[prop]; - } - } - } - else if (options.dataManipulationToolbar !== undefined) { - this.constants.dataManipulationToolbar.enabled = false; - } - - // TODO: work out these options and document them - if (options.edges) { - for (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); - } - } - } - } - - // load the navigation system. - this._loadNavigationControls(); - - // load the data manipulation system - this._loadManipulationSystem(); - - // bind keys. If disabled, this will not do anything; - this._createKeyBinds(); - - this.setSize(this.width, this.height); - this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); - this._setScale(1); - this._redraw(); -}; - -/** - * Add event listener - * @param {String} event Event name. Available events: - * 'select' - * @param {function} callback Callback function, invoked as callback(properties) - * where properties is an optional object containing - * event specific properties. - */ -Graph.prototype.on = function on (event, callback) { - var available = ['select']; - - if (available.indexOf(event) == -1) { - throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); - } - - events.addListener(this, event, callback); -}; - -/** - * Remove an event listener - * @param {String} event Event name - * @param {function} callback Callback function - */ -Graph.prototype.off = function off (event, callback) { - events.removeListener(this, event, callback); -}; - -/** - * 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'; - this.frame.style.zIndex = "1"; - - // 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('doubletap', me._onDoubleTap.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('release', me._onRelease.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); - -}; - - -/** - * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin - * @private - */ -Graph.prototype._createKeyBinds = function() { - var me = this; - this.mousetrap = mousetrap; - - this.mousetrap.reset(); - - if (this.constants.keyboard.enabled == true) { - this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown"); - this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup"); - this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown"); - this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup"); - this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown"); - this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup"); - this.mousetrap.bind("right",this._moveRight.bind(me), "keydown"); - this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup"); - this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown"); - this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown"); - this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown"); - this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown"); - this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown"); - this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); - this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); - } - - if (this.constants.dataManipulationToolbar.enabled == true) { - this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); - } -} - -/** - * 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(); - - this._handleTouch(this.drag.pointer); -}; - -/** - * handle drag start event - * @private - */ -Graph.prototype._onDragStart = function () { - var drag = this.drag; - var node = this._getNodeAt(drag.pointer); - // note: drag.pointer is set in _onTouch to get the initial touch location - - drag.dragging = true; - drag.selection = []; - drag.translation = this._getTranslation(); - drag.nodeId = null; - - if (node != null) { - drag.nodeId = node.id; - // select the clicked node if not yet selected - if (!node.isSelected()) { - this._selectObject(node,false); - } - - // create an array with the selected nodes and their original location and status - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - var object = this.selectionObj[objectId]; - if (object instanceof Node) { - var s = { - id: object.id, - node: object, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.xFixed, - yFixed: object.yFixed - }; - - object.xFixed = true; - object.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 () { - this.drag.dragging = false; - 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]); - this.pointerPosition = pointer; - this._handleTap(pointer); - -}; - - -/** - * handle doubletap event - * @private - */ -Graph.prototype._onDoubleTap = function (event) { - var pointer = this._getPointer(event.gesture.touches[0]); - this._handleDoubleTap(pointer); - -}; - - -/** - * handle long tap event: multi select nodes - * @private - */ -Graph.prototype._onHold = function (event) { - var pointer = this._getPointer(event.gesture.touches[0]); - this.pointerPosition = pointer; - this._handleOnHold(pointer); -}; - -/** - * handle the release of the screen - * - * @param event - * @private - */ -Graph.prototype._onRelease = function (event) { - this._handleOnRelease(); -}; - -/** - * 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: enabled 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 Position on screen - * @return {Number} appliedScale scale is limited within the boundaries - * @private - */ -Graph.prototype._zoom = function(scale, pointer) { - var scaleOld = this._getScale(); - if (scale < 0.00001) { - scale = 0.00001; - } - if (scale > 10) { - scale = 10; - } -// + this.frame.canvas.clientHeight / 2 - 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.areaCenter = {"x" : this._canvasToX(pointer.x), - "y" : this._canvasToY(pointer.y)}; - - // this.areaCenter = {"x" : pointer.x,"y" : pointer.y }; -// console.log(translation.x,translation.y,pointer.x,pointer.y,scale); - this.pinch.mousewheelScale = scale; - this._setScale(scale); - this._setTranslation(tx, ty); - this.updateClustersDefault(); - 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 (!('mousewheelScale' in this.pinch)) { - this.pinch.mousewheelScale = 1; - } - - // calculate the new scale - var scale = this.pinch.mousewheelScale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= (1 + zoom); - - // calculate the pointer location - var gesture = util.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); - - // apply the new scale - scale = this._zoom(scale, pointer); - - // store the new, applied scale -- this is now done in _zoom -// this.pinch.mousewheelScale = 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 = util.fakeGesture(this, 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 calculationTimer - } - if (!this.drag.dragging) { - 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(); - } - } -}; - - -/** - * 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; - - if (this.manipulationDiv !== undefined) { - this.manipulationDiv.style.width = this.frame.canvas.clientWidth; - } - - if (this.constants.navigation.enabled == true) { - this._relocateNavigation(); - } -}; - -/** - * 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() && this.createNodeOnClick != true) { - // 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._updateNodeIndexList(); - this._reconnectEdges(); - this._updateValueRange(this.nodes); - this.updateLabels(); -}; - -/** - * 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._updateNodeIndexList(); - 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._updateNodeIndexList(); - 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, {"showInternalIds" : true}); - 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.canvasTopLeft = { - "x": this._canvasToX(0), - "y": this._canvasToY(0) - }; - this.canvasBottomRight = { - "x": this._canvasToX(this.frame.canvas.clientWidth), - "y": this._canvasToY(this.frame.canvas.clientHeight) - }; - - this._doInAllSectors("_drawAllSectorNodes",ctx); - this._doInAllSectors("_drawEdges",ctx); - this._doInAllSectors("_drawNodes",ctx,true); - - // restore original scaling and translation - ctx.restore(); - - if (this.constants.navigation.enabled == true) { - this._doInNavigationSector("_drawNodes",ctx,true); - } -}; - -/** - * 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 - * @param {Boolean} [alwaysShow] - * @private - */ -Graph.prototype._drawNodes = function(ctx,alwaysShow) { - if (alwaysShow === undefined) { - alwaysShow = false; - } - - // first draw the unselected nodes - var nodes = this.nodes; - var selected = []; - - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight); - if (nodes[id].isSelected()) { - selected.push(id); - } - else { - if (nodes[id].inArea() || alwaysShow) { - nodes[id].draw(ctx); - } - } - } - } - - // draw the selected nodes on top - for (var s = 0, sMax = selected.length; s < sMax; s++) { - if (nodes[selected[s]].inArea() || alwaysShow) { - 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]; - edge.setScale(this.scale); - 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._initializeForceCalculation(); - this._discreteStepNodes(); - stable = !this._isMoving(vmin); - count++; - } - this.zoomToFit(); - - // var end = new Date(); - - // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup -}; - - -/** - * Before calculating the forces, we check if we need to cluster to keep up performance and we check - * if there is more than one node. If it is just one node, we dont calculate anything. - * - * @private - */ -Graph.prototype._initializeForceCalculation = function() { - // stop calculation if there is only one node - if (this.nodeIndices.length == 1) { - this.nodes[this.nodeIndices[0]]._setForce(0,0); - } - else { - // if there are too many nodes on screen, we cluster without repositioning - if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { - this.clusterToFit(this.constants.clustering.reduceToNodes, false); - } - - // we now start the force calculation - this._calculateForces(); - } -}; - - -/** - * Calculate the external forces acting on the nodes - * Forces are caused by: edges, repulsing forces between nodes, gravity - * @private - */ -Graph.prototype._calculateForces = function() { -// var screenCenterPos = {"x":(0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x)), -// "y":(0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y))} - // create a local edge to the nodes and edges, that is faster - var dx, dy, angle, distance, fx, fy, - repulsingForce, springForce, length, edgeLength, - node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter; - var clusterSize; - var nodes = this.nodes; - var edges = this.edges; - - // Gravity is required to keep separated groups from floating off - // the forces are reset to zero in this loop by using _setForce instead - // of _addForce - var gravity = 0.08 * this.forceFactor; - for (i = 0; i < this.nodeIndices.length; i++) { - node = nodes[this.nodeIndices[i]]; - // gravity does not apply when we are in a pocket sector - if (this._sector() == "default") { - dx = -node.x;// + screenCenterPos.x; - dy = -node.y;// + screenCenterPos.y; - - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; - } - else { - fx = 0; - fy = 0; - } - node._setForce(fx, fy); - - node.updateDamping(this.nodeIndices.length); - } - - // repulsing forces between nodes - var minimumDistance = this.constants.nodes.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - var a_base = (-2/3); var b = 4/3; - for (i = 0; i < this.nodeIndices.length-1; i++) { - node1 = nodes[this.nodeIndices[i]]; - for (j = i+1; j < this.nodeIndices.length; j++) { - node2 = nodes[this.nodeIndices[j]]; - clusterSize = (node1.clusterSize + node2.clusterSize - 2); - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - - // clusters have a larger region of influence - minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); - var a = a_base / minimumDistance; - if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 - angle = Math.atan2(dy, dx); - - if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 - repulsingForce = 1.0; - } - else { - // 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 - repulsingForce = a * distance + b; // TODO: test the approximation of the function above - } - // amplify the repulsion for clusters. - repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; - repulsingForce *= this.forceFactor; - - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce ; - - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); - } - } - } - -/* - // repulsion of the edges on the nodes and - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - node = nodes[nodeId]; - for(var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - - // get the center of the edge - xCenter = edge.from.x+(edge.to.x - edge.from.x)/2; - yCenter = edge.from.y+(edge.to.y - edge.from.y)/2; - - // calculate normally distributed force - dx = node.x - xCenter; - dy = node.y - yCenter; - distance = Math.sqrt(dx * dx + dy * dy); - if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 - angle = Math.atan2(dy, dx); - - if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 - repulsingForce = 1.0; - } - else { - // 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; - node._addForce(fx, fy); - edge.from._addForce(-fx/2,-fy/2); - edge.to._addForce(-fx/2,-fy/2); - } - } - } - } - } -*/ - - // forces caused by the edges, modelled as springs - for (edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - if (edge.connected) { - // only calculate forces if nodes are in the same sector - if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { - clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); - 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; - // this implies that the edges between big clusters are longer - edgeLength += clusterSize * this.constants.clustering.edgeGrowth; - length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); - - springForce = edge.stiffness * (edgeLength - length) * this.forceFactor; - - 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) { - var vminCorrected = vmin / this.scale; - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) { - return true; - } - } - return false; -}; - - -/** - * /** - * Perform one discrete step for all nodes - * - * @param interval - * @private - */ -Graph.prototype._discreteStepNodes = function() { - var interval = 0.01; - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].discreteStep(interval); - } - } - - var vmin = this.constants.minVelocity; - this.moving = this._isMoving(vmin); -}; - - - -/** - * Start animating nodes and edges - * - * @poram {Boolean} runCalculationStep - */ -Graph.prototype.start = function() { - if (!this.freezeSimulation) { - - if (this.moving) { - this._doInAllActiveSectors("_initializeForceCalculation"); - this._doInAllActiveSectors("_discreteStepNodes"); - this._findCenter(this._getRange()) - } - - if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { - // start animation. only start calculationTimer if it is not already running - if (!this.timer) { - var graph = this; - this.timer = window.setTimeout(function () { - graph.timer = undefined; - - // keyboad movement - if (graph.xIncrement != 0 || graph.yIncrement != 0) { - var translation = graph._getTranslation(); - graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement); - } - if (graph.zoomIncrement != 0) { - var center = { - x: graph.frame.canvas.clientWidth / 2, - y: graph.frame.canvas.clientHeight / 2 - }; - graph._zoom(graph.scale*(1 + graph.zoomIncrement), center); - } - - graph.start(); - graph._redraw(); - - //this.end = window.performance.now(); - //this.time = this.end - this.startTime; - //console.log('refresh time: ' + this.time); - //this.startTime = window.performance.now(); - - }, this.renderTimestep); - } - } - else { - this._redraw(); - } - } -}; - -/** - * Debug function, does one step of the graph - */ -Graph.prototype.singleStep = function() { - if (this.moving) { - this._initializeForceCalculation(); - this._discreteStepNodes(); - - var vmin = this.constants.minVelocity; - this.moving = this._isMoving(vmin); - this._redraw(); - } -}; - - - -/** - * Freeze the animation - */ -Graph.prototype.toggleFreeze = function() { - if (this.freezeSimulation == false) { - this.freezeSimulation = true; - } - else { - this.freezeSimulation = false; - this.start(); - } -}; - -/** - * Mixin the cluster system and initialize the parameters required. - * - * @private - */ -Graph.prototype._loadClusterSystem = function() { - this.clusterSession = 0; - this.hubThreshold = 5; - - for (var mixinFunction in ClusterMixin) { - if (ClusterMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction]; - } - } -} - -/** - * Mixin the sector system and initialize the parameters required - * - * @private - */ -Graph.prototype._loadSectorSystem = function() { - this.sectors = {}; - this.activeSector = ["default"]; - this.sectors["active"] = {}; - this.sectors["active"]["default"] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": 1.0, - "drawingNode": undefined}; - this.sectors["frozen"] = {}; - this.sectors["navigation"] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": 1.0, - "drawingNode": undefined}; - - this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields - for (var mixinFunction in SectorMixin) { - if (SectorMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = SectorMixin[mixinFunction]; - } - } -}; - - -/** - * Mixin the selection system and initialize the parameters required - * - * @private - */ -Graph.prototype._loadSelectionSystem = function() { - this.selectionObj = {}; - - for (var mixinFunction in SelectionMixin) { - if (SelectionMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = SelectionMixin[mixinFunction]; - } - } -} - - - -/** - * Mixin the navigationUI (User Interface) system and initialize the parameters required - * - * @private - */ -Graph.prototype._loadManipulationSystem = function() { - // reset global variables -- these are used by the selection of nodes and edges. - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false - - - if (this.constants.dataManipulationToolbar.enabled == true) { - // load the manipulator HTML elements. All styling done in css. - if (this.manipulationDiv === undefined) { - this.manipulationDiv = document.createElement('div'); - this.manipulationDiv.className = 'graph-manipulationDiv'; - this.containerElement.insertBefore(this.manipulationDiv, this.frame); - } - // load the manipulation functions - for (var mixinFunction in manipulationMixin) { - if (manipulationMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = manipulationMixin[mixinFunction]; - } - } - - // create the manipulator toolbar - this._createManipulatorBar(); - } -} - -/** - * Mixin the navigation (User Interface) system and initialize the parameters required - * - * @private - */ -Graph.prototype._loadNavigationControls = function() { - for (var mixinFunction in NavigationMixin) { - if (NavigationMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction]; - } - } - - if (this.constants.navigation.enabled == true) { - this._loadNavigationElements(); - } -} - -/** - * this function exists to avoid errors when not loading the navigation system - */ -Graph.prototype._relocateNavigation = function() { - // empty, is overloaded by navigation system -} - -/** - * * this function exists to avoid errors when not loading the navigation system - */ -Graph.prototype._unHighlightAll = function() { - // empty, is overloaded by the navigation system -} - - - - - - - - - - - - - - - - - - - - - - - - - -/** - * 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; -} - - -},{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){ - -/** - * Expose `Emitter`. - */ - -module.exports = Emitter; - -/** - * Initialize a new `Emitter`. - * - * @api public - */ - -function Emitter(obj) { - if (obj) return mixin(obj); -}; - -/** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private - */ - -function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; - } - return obj; -} - -/** - * Listen on the given `event` with `fn`. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.on = -Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; -}; - -/** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; - - function on() { - self.off(event, on); - fn.apply(this, arguments); - } - - on.fn = fn; - this.on(event, on); - return this; -}; - -/** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.off = -Emitter.prototype.removeListener = -Emitter.prototype.removeAllListeners = -Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; - } - - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; - - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; - } - - // remove specific handler - var cb; - for (var i = 0; i < callbacks.length; i++) { - cb = callbacks[i]; - if (cb === fn || cb.fn === fn) { - callbacks.splice(i, 1); - break; - } - } - return this; -}; - -/** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} - */ - -Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; - - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); - } - } - - return this; -}; - -/** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public - */ - -Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; -}; - -/** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public - */ - -Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; -}; - -},{}],3:[function(require,module,exports){ -/*! Hammer.JS - v1.0.5 - 2013-04-07 - * http://eightmedia.github.com/hammer.js - * - * Copyright (c) 2013 Jorik Tangelder ; - * 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); -},{}],4:[function(require,module,exports){ -//! moment.js -//! version : 2.5.1 -//! authors : Tim Wood, Iskren Chernev, Moment.js contributors -//! license : MIT -//! momentjs.com - -(function (undefined) { - - /************************************ - Constants - ************************************/ - - var moment, - VERSION = "2.5.1", - global = this, - 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 = {}, - - // moment internal properties - momentProperties = { - _isAMomentObject: null, - _i : null, - _f : null, - _l : null, - _strict : null, - _isUTC : null, - _offset : null, // optional. Combine with _isUTC - _pf : null, - _lang : null // optional - }, - - // check for nodeJS - hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'), - - // 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]?|YYYYYY|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 - parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999 - parseTokenOneToSixDigits = /[+\-]?\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/gi, // +00:00 -00:00 +0000 -0000 or Z - parseTokenT = /T/i, // T (ISO separator) - parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 - - //strict parsing regexes - parseTokenOneDigit = /\d/, // 0 - 9 - parseTokenTwoDigits = /\d\d/, // 00 - 99 - parseTokenThreeDigits = /\d{3}/, // 000 - 999 - parseTokenFourDigits = /\d{4}/, // 0000 - 9999 - parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 - parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf - - // iso 8601 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 or +00) - isoRegex = /^\s*(?:[+-]\d{6}|\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)?|\s*Z)?)?$/, - - isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', - - isoDates = [ - ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], - ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], - ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], - ['GGGG-[W]WW', /\d{4}-W\d{2}/], - ['YYYY-DDD', /\d{4}-\d{3}/] - ], - - // 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); - }, - YYYYYY : function () { - var y = this.year(), sign = y >= 0 ? '+' : '-'; - return sign + leftZeroFill(Math.abs(y), 6); - }, - gg : function () { - return leftZeroFill(this.weekYear() % 100, 2); - }, - gggg : function () { - return leftZeroFill(this.weekYear(), 4); - }, - ggggg : function () { - return leftZeroFill(this.weekYear(), 5); - }, - GG : function () { - return leftZeroFill(this.isoWeekYear() % 100, 2); - }, - GGGG : function () { - return leftZeroFill(this.isoWeekYear(), 4); - }, - 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(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2); - }, - z : function () { - return this.zoneAbbr(); - }, - zz : function () { - return this.zoneName(); - }, - X : function () { - return this.unix(); - }, - Q : function () { - return this.quarter(); - } - }, - - lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin']; - - function defaultParsingFlags() { - // We need to deep clone this object, and es5 standard is not very - // helpful. - return { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso: false - }; - } - - 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; - - // 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 cloneMoment(m) { - var result = {}, i; - for (i in m) { - if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) { - result[i] = m[i]; - } - } - - return result; - } - - 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, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; - - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + 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; - - 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 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; - } - - // Return a moment from input, that is local/utc/zone equivalent to model. - function makeAs(input, model) { - return model._isUTC ? moment(input).zone(model._offset || 0) : - moment(input).local(); - } - - /************************************ - 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, strict = config._strict; - switch (token) { - case 'DDDD': - return parseTokenThreeDigits; - case 'YYYY': - case 'GGGG': - case 'gggg': - return strict ? parseTokenFourDigits : parseTokenOneToFourDigits; - case 'Y': - case 'G': - case 'g': - return parseTokenSignedNumber; - case 'YYYYYY': - case 'YYYYY': - case 'GGGGG': - case 'ggggg': - return strict ? parseTokenSixDigits : parseTokenOneToSixDigits; - case 'S': - if (strict) { return parseTokenOneDigit; } - /* falls through */ - case 'SS': - if (strict) { return parseTokenTwoDigits; } - /* falls through */ - case 'SSS': - if (strict) { return parseTokenThreeDigits; } - /* falls through */ - 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 'ww': - case 'WW': - return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits; - case 'M': - case 'D': - case 'd': - case 'H': - case 'h': - case 'm': - case 's': - case 'w': - case 'W': - case 'e': - case 'E': - return parseTokenOneOrTwoDigits; - default : - a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i")); - return a; - } - } - - function timezoneMinutesFromString(string) { - string = string || ""; - var possibleTzMatches = (string.match(parseTokenTimezone) || []), - tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [], - 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' : - case 'YYYYYY' : - 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) { - var int_val = parseInt(val, 10); - return val ? - (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_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 = (string.match(getParseRegexForToken(token, config)) || [])[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); - tempConfig._pf = defaultParsingFlags(); - 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, l, - string = config._i, - match = isoRegex.exec(string); - - if (match) { - config._pf.iso = true; - for (i = 0, l = isoDates.length; i < l; i++) { - if (isoDates[i][1].exec(string)) { - // match[5] should be "T" or undefined - config._f = isoDates[i][0] + (match[6] || " "); - break; - } - } - for (i = 0, l = isoTimes.length; i < l; i++) { - if (isoTimes[i][1].exec(string)) { - config._f += isoTimes[i][0]; - break; - } - } - if (string.match(parseTokenTimezone)) { - 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 = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear; - - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 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 (input === null) { - return moment.invalid({nullInput: true}); - } - - if (typeof input === 'string') { - config._i = input = getLangDefinition().preparse(input); - } - - if (moment.isMoment(input)) { - config = cloneMoment(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) { - var c; - - if (typeof(lang) === "boolean") { - strict = lang; - lang = undefined; - } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c = {}; - c._isAMomentObject = true; - c._i = input; - c._f = format; - c._l = lang; - c._strict = strict; - c._isUTC = false; - c._pf = defaultParsingFlags(); - - return makeMoment(c); - }; - - // creating with utc - moment.utc = function (input, format, lang, strict) { - var c; - - if (typeof(lang) === "boolean") { - strict = lang; - lang = undefined; - } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c = {}; - c._isAMomentObject = true; - c._useUTC = true; - c._isUTC = true; - c._l = lang; - c._i = input; - c._f = format; - c._strict = strict; - c._pf = defaultParsingFlags(); - - return makeMoment(c).utc(); - }; - - // creating with unix timestamp (in seconds) - moment.unix = function (input) { - return moment(input * 1000); - }; - - // duration - moment.duration = function (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - parseIso; - - if (moment.isDuration(input)) { - duration = { - ms: input._milliseconds, - d: input._days, - M: input._months - }; - } else if (typeof input === 'number') { - duration = {}; - 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 (moment.isDuration(input) && 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 || - (obj != null && obj.hasOwnProperty('_isAMomentObject')); - }; - - // 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 () { - var m = moment(this).utc(); - if (0 < m.year() && m.year() <= 9999) { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } else { - return formatMoment(m, 'YYYYYY-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 = makeAs(input, this), - 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 () { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're zone'd or not. - var sod = makeAs(moment(), this).startOf('day'), - diff = this.diff(sod, '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 = units || 'ms'; - return +this.clone().startOf(units) === +makeAs(input, this).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 (this._tzm) { - this.zone(this._tzm); - } else 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)); - }, - - quarter : function () { - return Math.ceil((this.month() + 1.0) / 3.0); - }, - - 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) { - global.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); - }; - extend(global.moment, local_moment); - } else { - global['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 && module.config() && module.config().noGlobal !== true) { - // If user provided noGlobal, he is aware of global - makeGlobal(module.config().noGlobal === undefined); - } - - return moment; - }); - } else { - makeGlobal(); - } -}).call(this); - -},{}],5:[function(require,module,exports){ -/** - * Copyright 2012 Craig Campbell - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Mousetrap is a simple keyboard shortcut library for Javascript with - * no external dependencies - * - * @version 1.1.2 - * @url craig.is/killing/mice - */ - - /** - * mapping of special keycodes to their corresponding keys - * - * everything in this dictionary cannot use keypress events - * so it has to be here to map to the correct keycodes for - * keyup/keydown events - * - * @type {Object} - */ - var _MAP = { - 8: 'backspace', - 9: 'tab', - 13: 'enter', - 16: 'shift', - 17: 'ctrl', - 18: 'alt', - 20: 'capslock', - 27: 'esc', - 32: 'space', - 33: 'pageup', - 34: 'pagedown', - 35: 'end', - 36: 'home', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down', - 45: 'ins', - 46: 'del', - 91: 'meta', - 93: 'meta', - 224: 'meta' - }, - - /** - * mapping for special characters so they can support - * - * this dictionary is only used incase you want to bind a - * keyup or keydown event to one of these keys - * - * @type {Object} - */ - _KEYCODE_MAP = { - 106: '*', - 107: '+', - 109: '-', - 110: '.', - 111 : '/', - 186: ';', - 187: '=', - 188: ',', - 189: '-', - 190: '.', - 191: '/', - 192: '`', - 219: '[', - 220: '\\', - 221: ']', - 222: '\'' - }, - - /** - * this is a mapping of keys that require shift on a US keypad - * back to the non shift equivelents - * - * this is so you can use keyup events with these keys - * - * note that this will only work reliably on US keyboards - * - * @type {Object} - */ - _SHIFT_MAP = { - '~': '`', - '!': '1', - '@': '2', - '#': '3', - '$': '4', - '%': '5', - '^': '6', - '&': '7', - '*': '8', - '(': '9', - ')': '0', - '_': '-', - '+': '=', - ':': ';', - '\"': '\'', - '<': ',', - '>': '.', - '?': '/', - '|': '\\' - }, - - /** - * this is a list of special strings you can use to map - * to modifier keys when you specify your keyboard shortcuts - * - * @type {Object} - */ - _SPECIAL_ALIASES = { - 'option': 'alt', - 'command': 'meta', - 'return': 'enter', - 'escape': 'esc' - }, - - /** - * variable to store the flipped version of _MAP from above - * needed to check if we should use keypress or not when no action - * is specified - * - * @type {Object|undefined} - */ - _REVERSE_MAP, - - /** - * a list of all the callbacks setup via Mousetrap.bind() - * - * @type {Object} - */ - _callbacks = {}, - - /** - * direct map of string combinations to callbacks used for trigger() - * - * @type {Object} - */ - _direct_map = {}, - - /** - * keeps track of what level each sequence is at since multiple - * sequences can start out with the same sequence - * - * @type {Object} - */ - _sequence_levels = {}, - - /** - * variable to store the setTimeout call - * - * @type {null|number} - */ - _reset_timer, - - /** - * temporary state where we will ignore the next keyup - * - * @type {boolean|string} - */ - _ignore_next_keyup = false, - - /** - * are we currently inside of a sequence? - * type of action ("keyup" or "keydown" or "keypress") or false - * - * @type {boolean|string} - */ - _inside_sequence = false; - - /** - * loop through the f keys, f1 to f19 and add them to the map - * programatically - */ - for (var i = 1; i < 20; ++i) { - _MAP[111 + i] = 'f' + i; - } - - /** - * loop through to map numbers on the numeric keypad - */ - for (i = 0; i <= 9; ++i) { - _MAP[i + 96] = i; - } - - /** - * cross browser add event method - * - * @param {Element|HTMLDocument} object - * @param {string} type - * @param {Function} callback - * @returns void - */ - function _addEvent(object, type, callback) { - if (object.addEventListener) { - return object.addEventListener(type, callback, false); - } - - object.attachEvent('on' + type, callback); - } - - /** - * takes the event and returns the key character - * - * @param {Event} e - * @return {string} - */ - function _characterFromEvent(e) { - - // for keypress events we should return the character as is - if (e.type == 'keypress') { - return String.fromCharCode(e.which); - } - - // for non keypress events the special maps are needed - if (_MAP[e.which]) { - return _MAP[e.which]; - } - - if (_KEYCODE_MAP[e.which]) { - return _KEYCODE_MAP[e.which]; - } - - // if it is not in the special map - return String.fromCharCode(e.which).toLowerCase(); - } - - /** - * should we stop this event before firing off callbacks - * - * @param {Event} e - * @return {boolean} - */ - function _stop(e) { - var element = e.target || e.srcElement, - tag_name = element.tagName; - - // if the element has the class "mousetrap" then no need to stop - if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { - return false; - } - - // stop for input, select, and textarea - return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true'); - } - - /** - * checks if two arrays are equal - * - * @param {Array} modifiers1 - * @param {Array} modifiers2 - * @returns {boolean} - */ - function _modifiersMatch(modifiers1, modifiers2) { - return modifiers1.sort().join(',') === modifiers2.sort().join(','); - } - - /** - * resets all sequence counters except for the ones passed in - * - * @param {Object} do_not_reset - * @returns void - */ - function _resetSequences(do_not_reset) { - do_not_reset = do_not_reset || {}; - - var active_sequences = false, - key; - - for (key in _sequence_levels) { - if (do_not_reset[key]) { - active_sequences = true; - continue; - } - _sequence_levels[key] = 0; - } - - if (!active_sequences) { - _inside_sequence = false; - } - } - - /** - * finds all callbacks that match based on the keycode, modifiers, - * and action - * - * @param {string} character - * @param {Array} modifiers - * @param {string} action - * @param {boolean=} remove - should we remove any matches - * @param {string=} combination - * @returns {Array} - */ - function _getMatches(character, modifiers, action, remove, combination) { - var i, - callback, - matches = []; - - // if there are no events related to this keycode - if (!_callbacks[character]) { - return []; - } - - // if a modifier key is coming up on its own we should allow it - if (action == 'keyup' && _isModifier(character)) { - modifiers = [character]; - } - - // loop through all callbacks for the key that was pressed - // and see if any of them match - for (i = 0; i < _callbacks[character].length; ++i) { - callback = _callbacks[character][i]; - - // if this is a sequence but it is not at the right level - // then move onto the next match - if (callback.seq && _sequence_levels[callback.seq] != callback.level) { - continue; - } - - // if the action we are looking for doesn't match the action we got - // then we should keep going - if (action != callback.action) { - continue; - } - - // if this is a keypress event that means that we need to only - // look at the character, otherwise check the modifiers as - // well - if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) { - - // remove is used so if you change your mind and call bind a - // second time with a new function the first one is overwritten - if (remove && callback.combo == combination) { - _callbacks[character].splice(i, 1); - } - - matches.push(callback); - } - } - - return matches; - } - - /** - * takes a key event and figures out what the modifiers are - * - * @param {Event} e - * @returns {Array} - */ - function _eventModifiers(e) { - var modifiers = []; - - if (e.shiftKey) { - modifiers.push('shift'); - } - - if (e.altKey) { - modifiers.push('alt'); - } - - if (e.ctrlKey) { - modifiers.push('ctrl'); - } - - if (e.metaKey) { - modifiers.push('meta'); - } - - return modifiers; - } - - /** - * actually calls the callback function - * - * if your callback function returns false this will use the jquery - * convention - prevent default and stop propogation on the event - * - * @param {Function} callback - * @param {Event} e - * @returns void - */ - function _fireCallback(callback, e) { - if (callback(e) === false) { - if (e.preventDefault) { - e.preventDefault(); - } - - if (e.stopPropagation) { - e.stopPropagation(); - } - - e.returnValue = false; - e.cancelBubble = true; - } - } - - /** - * handles a character key event - * - * @param {string} character - * @param {Event} e - * @returns void - */ - function _handleCharacter(character, e) { - - // if this event should not happen stop here - if (_stop(e)) { - return; - } - - var callbacks = _getMatches(character, _eventModifiers(e), e.type), - i, - do_not_reset = {}, - processed_sequence_callback = false; - - // loop through matching callbacks for this key event - for (i = 0; i < callbacks.length; ++i) { - - // fire for all sequence callbacks - // this is because if for example you have multiple sequences - // bound such as "g i" and "g t" they both need to fire the - // callback for matching g cause otherwise you can only ever - // match the first one - if (callbacks[i].seq) { - processed_sequence_callback = true; - - // keep a list of which sequences were matches for later - do_not_reset[callbacks[i].seq] = 1; - _fireCallback(callbacks[i].callback, e); - continue; - } - - // if there were no sequence matches but we are still here - // that means this is a regular match so we should fire that - if (!processed_sequence_callback && !_inside_sequence) { - _fireCallback(callbacks[i].callback, e); - } - } - - // if you are inside of a sequence and the key you are pressing - // is not a modifier key then we should reset all sequences - // that were not matched by this key event - if (e.type == _inside_sequence && !_isModifier(character)) { - _resetSequences(do_not_reset); - } - } - - /** - * handles a keydown event - * - * @param {Event} e - * @returns void - */ - function _handleKey(e) { - - // normalize e.which for key events - // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion - e.which = typeof e.which == "number" ? e.which : e.keyCode; - - var character = _characterFromEvent(e); - - // no character found then stop - if (!character) { - return; - } - - if (e.type == 'keyup' && _ignore_next_keyup == character) { - _ignore_next_keyup = false; - return; - } - - _handleCharacter(character, e); - } - - /** - * determines if the keycode specified is a modifier key or not - * - * @param {string} key - * @returns {boolean} - */ - function _isModifier(key) { - return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; - } - - /** - * called to set a 1 second timeout on the specified sequence - * - * this is so after each key press in the sequence you have 1 second - * to press the next key before you have to start over - * - * @returns void - */ - function _resetSequenceTimer() { - clearTimeout(_reset_timer); - _reset_timer = setTimeout(_resetSequences, 1000); - } - - /** - * reverses the map lookup so that we can look for specific keys - * to see what can and can't use keypress - * - * @return {Object} - */ - function _getReverseMap() { - if (!_REVERSE_MAP) { - _REVERSE_MAP = {}; - for (var key in _MAP) { - - // pull out the numeric keypad from here cause keypress should - // be able to detect the keys from the character - if (key > 95 && key < 112) { - continue; - } - - if (_MAP.hasOwnProperty(key)) { - _REVERSE_MAP[_MAP[key]] = key; - } - } - } - return _REVERSE_MAP; - } - - /** - * picks the best action based on the key combination - * - * @param {string} key - character for key - * @param {Array} modifiers - * @param {string=} action passed in - */ - function _pickBestAction(key, modifiers, action) { - - // if no action was picked in we should try to pick the one - // that we think would work best for this key - if (!action) { - action = _getReverseMap()[key] ? 'keydown' : 'keypress'; - } - - // modifier keys don't work as expected with keypress, - // switch to keydown - if (action == 'keypress' && modifiers.length) { - action = 'keydown'; - } - - return action; - } - - /** - * binds a key sequence to an event - * - * @param {string} combo - combo specified in bind call - * @param {Array} keys - * @param {Function} callback - * @param {string=} action - * @returns void - */ - function _bindSequence(combo, keys, callback, action) { - - // start off by adding a sequence level record for this combination - // and setting the level to 0 - _sequence_levels[combo] = 0; - - // if there is no action pick the best one for the first key - // in the sequence - if (!action) { - action = _pickBestAction(keys[0], []); - } - - /** - * callback to increase the sequence level for this sequence and reset - * all other sequences that were active - * - * @param {Event} e - * @returns void - */ - var _increaseSequence = function(e) { - _inside_sequence = action; - ++_sequence_levels[combo]; - _resetSequenceTimer(); - }, - - /** - * wraps the specified callback inside of another function in order - * to reset all sequence counters as soon as this sequence is done - * - * @param {Event} e - * @returns void - */ - _callbackAndReset = function(e) { - _fireCallback(callback, e); - - // we should ignore the next key up if the action is key down - // or keypress. this is so if you finish a sequence and - // release the key the final key will not trigger a keyup - if (action !== 'keyup') { - _ignore_next_keyup = _characterFromEvent(e); - } - - // weird race condition if a sequence ends with the key - // another sequence begins with - setTimeout(_resetSequences, 10); - }, - i; - - // loop through keys one at a time and bind the appropriate callback - // function. for any key leading up to the final one it should - // increase the sequence. after the final, it should reset all sequences - for (i = 0; i < keys.length; ++i) { - _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i); - } - } - - /** - * binds a single keyboard combination - * - * @param {string} combination - * @param {Function} callback - * @param {string=} action - * @param {string=} sequence_name - name of sequence if part of sequence - * @param {number=} level - what part of the sequence the command is - * @returns void - */ - function _bindSingle(combination, callback, action, sequence_name, level) { - - // make sure multiple spaces in a row become a single space - combination = combination.replace(/\s+/g, ' '); - - var sequence = combination.split(' '), - i, - key, - keys, - modifiers = []; - - // if this pattern is a sequence of keys then run through this method - // to reprocess each pattern one key at a time - if (sequence.length > 1) { - return _bindSequence(combination, sequence, callback, action); - } - - // take the keys from this pattern and figure out what the actual - // pattern is all about - keys = combination === '+' ? ['+'] : combination.split('+'); - - for (i = 0; i < keys.length; ++i) { - key = keys[i]; - - // normalize key names - if (_SPECIAL_ALIASES[key]) { - key = _SPECIAL_ALIASES[key]; - } - - // if this is not a keypress event then we should - // be smart about using shift keys - // this will only work for US keyboards however - if (action && action != 'keypress' && _SHIFT_MAP[key]) { - key = _SHIFT_MAP[key]; - modifiers.push('shift'); - } - - // if this key is a modifier then add it to the list of modifiers - if (_isModifier(key)) { - modifiers.push(key); - } - } - - // depending on what the key combination is - // we will try to pick the best event for it - action = _pickBestAction(key, modifiers, action); - - // make sure to initialize array if this is the first time - // a callback is added for this key - if (!_callbacks[key]) { - _callbacks[key] = []; - } - - // remove an existing match if there is one - _getMatches(key, modifiers, action, !sequence_name, combination); - - // add this call back to the array - // if it is a sequence put it at the beginning - // if not put it at the end - // - // this is important because the way these are processed expects - // the sequence ones to come first - _callbacks[key][sequence_name ? 'unshift' : 'push']({ - callback: callback, - modifiers: modifiers, - action: action, - seq: sequence_name, - level: level, - combo: combination - }); - } - - /** - * binds multiple combinations to the same callback - * - * @param {Array} combinations - * @param {Function} callback - * @param {string|undefined} action - * @returns void - */ - function _bindMultiple(combinations, callback, action) { - for (var i = 0; i < combinations.length; ++i) { - _bindSingle(combinations[i], callback, action); - } - } - - // start! - _addEvent(document, 'keypress', _handleKey); - _addEvent(document, 'keydown', _handleKey); - _addEvent(document, 'keyup', _handleKey); - - var mousetrap = { - - /** - * binds an event to mousetrap - * - * can be a single key, a combination of keys separated with +, - * a comma separated list of keys, an array of keys, or - * a sequence of keys separated by spaces - * - * be sure to list the modifier keys first to make sure that the - * correct key ends up getting bound (the last key in the pattern) - * - * @param {string|Array} keys - * @param {Function} callback - * @param {string=} action - 'keypress', 'keydown', or 'keyup' - * @returns void - */ - bind: function(keys, callback, action) { - _bindMultiple(keys instanceof Array ? keys : [keys], callback, action); - _direct_map[keys + ':' + action] = callback; - return this; - }, - - /** - * unbinds an event to mousetrap - * - * the unbinding sets the callback function of the specified key combo - * to an empty function and deletes the corresponding key in the - * _direct_map dict. - * - * the keycombo+action has to be exactly the same as - * it was defined in the bind method - * - * TODO: actually remove this from the _callbacks dictionary instead - * of binding an empty function - * - * @param {string|Array} keys - * @param {string} action - * @returns void - */ - unbind: function(keys, action) { - if (_direct_map[keys + ':' + action]) { - delete _direct_map[keys + ':' + action]; - this.bind(keys, function() {}, action); - } - return this; - }, - - /** - * triggers an event that has already been bound - * - * @param {string} keys - * @param {string=} action - * @returns void - */ - trigger: function(keys, action) { - _direct_map[keys + ':' + action](); - return this; - }, - - /** - * resets the library back to its initial state. this is useful - * if you want to clear out the current keyboard shortcuts and bind - * new ones - for example if you switch to another page - * - * @returns void - */ - reset: function() { - _callbacks = {}; - _direct_map = {}; - return this; - } - }; - -module.exports = mousetrap; - - -},{}]},{},[1]) -(1) -}); \ No newline at end of file diff --git a/dist/vis.min.js b/dist/vis.min.js deleted file mode 100644 index 3da5db1c..00000000 --- a/dist/vis.min.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * vis.js - * https://github.com/almende/vis - * - * A dynamic, browser-based visualization library. - * - * @version 0.5.0-SNAPSHOT - * @date 2014-02-05 - * - * @license - * Copyright (C) 2011-2014 Almende B.V, http://almende.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy - * of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -!function(t){if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.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 c=i[r]={exports:{}};t[r][0].call(c.exports,function(e){var i=t[r][1][e];return s(i?i:e)},c,c.exports,e,t,i,n)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;ri;++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 z={};z.isNumber=function(t){return t instanceof Number||"number"==typeof t},z.isString=function(t){return t instanceof String||"string"==typeof t},z.isDate=function(t){if(t instanceof Date)return!0;if(z.isString(t)){var e=P.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},z.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},z.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},z.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},z.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(z.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(L.isMoment(t))return new Date(t.valueOf());if(z.isString(t))return i=P.exec(t),i?new Date(Number(i[1])):L(t).toDate();throw new Error("Cannot convert object of type "+z.getType(t)+" to type Date");case"Moment":if(z.isNumber(t))return L(t);if(t instanceof Date)return L(t.valueOf());if(L.isMoment(t))return L(t);if(z.isString(t))return i=P.exec(t),L(i?Number(i[1]):t);throw new Error("Cannot convert object of type "+z.getType(t)+" to type Date");case"ISODate":if(z.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(L.isMoment(t))return t.toDate().toISOString();if(z.isString(t))return i=P.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+z.getType(t)+" to type ISODate");case"ASPDate":if(z.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(z.isString(t)){i=P.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 "+z.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+z.getType(t)+' to type "'+e+'"')}};var P=/^\/?Date\((\-?\d+)/i;z.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},z.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},z.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},z.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)},z.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)},z.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},z.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},z.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)},z.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},z.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)},z.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)},z.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},z.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},z.fakeGesture=function(t,e){var i=null;return N.event.collectEventData(this,i,e)},z.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},z.option={},z.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},z.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},z.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},z.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),z.isString(t)?t:z.isNumber(t)?t+"px":e||null},z.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},utli.hexToRGB=function(t){var e=GiveDec(t.substring(0,1)),i=GiveDec(t.substring(1,2)),n=GiveDec(t.substring(2,3)),s=GiveDec(t.substring(3,4)),o=GiveDec(t.substring(4,5)),r=GiveDec(t.substring(5,6)),a=16*e+i,h=16*n+s,i=16*o+r;return{r:a,g:h,b:i}},utli.RGBToHex=function(t,e,i){var n=GiveHex(Math.floor(t/16)),s=GiveHex(t%16),o=GiveHex(Math.floor(e/16)),r=GiveHex(e%16),a=GiveHex(Math.floor(i/16)),h=GiveHex(i%16),c=n+s+o+r+a+h;return c},utli.RGBToHSV=function(t,e,i){t/=255,e/=255,i/=255;var n=Math.min(t,Math.min(e,i)),s=Math.max(t,Math.max(e,i));if(n==s)return{h:0,s:0,v:n};var o=t==n?e-i:i==n?t-e:i-t,r=t==n?3:i==n?1:5,a=60*(r-o/(s-n)),h=(s-n)/s,c=s;return{h:a,s:h,v:c}},z.HSVToRGB=function(t,e,i){var n,s,o,r,a,h,c,d;switch(t&&void 0===e&&void 0===i&&(e=t.s,i=t.v,t=t.h),r=Math.floor(6*t),a=6*t-r,h=i*(1-e),c=i*(1-a*e),d=i*(1-(1-a)*e),r%6){case 0:n=i,s=d,o=h;break;case 1:n=c,s=i,o=h;break;case 2:n=h,s=i,o=d;break;case 3:n=h,s=c,o=i;break;case 4:n=d,s=h,o=i;break;case 5:n=i,s=h,o=c}return{r:Math.floor(255*n),g:Math.floor(255*s),b:Math.floor(255*o)}};var F={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:z.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(z.isDataTable(t))for(var a=this._getColumnNames(t),h=0,c=t.getNumberOfRows();c>h;h++){for(var d={},l=0,u=a.length;u>l;l++){var p=a[l];d[p]=t.getValue(h,l)}i=s._addItem(d),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(z.isDataTable(t))for(var c=this._getColumnNames(t),d=0,l=t.getNumberOfRows();l>d;d++){for(var u={},p=0,f=c.length;f>p;p++){var g=c[p];u[g]=t.getValue(d,p)}r(u)}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=this.showInternalIds,r=z.getType(arguments[0]);"String"==r||"Number"==r?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==r?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var a;if(i&&i.type){if(a="DataTable"==i.type?"DataTable":"Array",n&&a!=z.getType(n))throw new Error('Type of parameter "data" ('+z.getType(n)+") does not correspond with specified options.type ("+i.type+")");if("DataTable"==a&&!z.isDataTable(n))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else a=n?"DataTable"==z.getType(n)?"DataTable":"Array":"Array";void 0!=i&&void 0!=i.showInternalIds&&(this.showInternalIds=i.showInternalIds);var h,c,d,l,u=i&&i.convert||this.options.convert,p=i&&i.filter,f=[];if(void 0!=t)h=s._getItem(t,u),p&&!p(h)&&(h=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)h=s._getItem(e[d],u),(!p||p(h))&&f.push(h);else for(c in this.data)this.data.hasOwnProperty(c)&&(h=s._getItem(c,u),(!p||p(h))&&f.push(h));if(this.showInternalIds=o,i&&i.order&&void 0==t&&this._sort(f,i.order),i&&i.fields){var g=i.fields;if(void 0!=t)h=this._filterFields(h,g);else for(d=0,l=f.length;l>d;d++)f[d]=this._filterFields(f[d],g)}if("DataTable"==a){var m=this._getColumnNames(n);if(void 0!=t)s._appendRow(n,m,h);else for(d=0,l=f.length;l>d;d++)s._appendRow(n,m,f[d]);return n}if(void 0!=t)return h;if(n){for(d=0,l=f.length;l>d;d++)n.push(f[d]);return n}return f},o.prototype.getIds=function(t){var e,i,n,s,o,r=this.data,a=t&&t.filter,h=t&&t.order,c=t&&t.convert||this.options.convert,d=[];if(a)if(h){o=[];for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,c),a(s)&&o.push(s));for(this._sort(o,h),e=0,i=o.length;i>e;e++)d[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,c),a(s)&&d.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++)d[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=r[n],d.push(s[this.fieldId]));return d},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,c=a.length;c>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(z.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(z.isNumber(t)||z.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=z.convert(r[t],n),h=!1,c=0;s>c;c++)if(i[c]==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=z.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]=z.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&&!this.showInternalIds||(o[i]=z.convert(n,e[i])));else for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a&&!this.showInternalIds||(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]=z.convert(t[n],s)}return e},o.prototype.isInternalId=function(t){return t in this.internalIds},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=z.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=z.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,c=[],d=[],l=[];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,c.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]?d.push(o):(this.ids[o]=!0,c.push(o)):this.ids[o]&&(delete this.ids[o],l.push(o));break;case"remove":for(n=0,s=a.length;s>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],l.push(o))}c.length&&this._trigger("add",{items:c},i),d.length&&this._trigger("update",{items:d},i),l.length&&this._trigger("remove",{items:l},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){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";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()+1e3*this.step*60);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+1e3*this.step*60*60);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 L(t).format("SSS");case TimeStep.SCALE.SECOND:return L(t).format("s");case TimeStep.SCALE.MINUTE:return L(t).format("HH:mm");case TimeStep.SCALE.HOUR:return L(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return L(t).format("ddd D");case TimeStep.SCALE.DAY:return L(t).format("D");case TimeStep.SCALE.MONTH:return L(t).format("MMM");case TimeStep.SCALE.YEAR:return L(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return L(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return L(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return L(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return L(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return L(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){z.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;z.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){return t.left-ie.left&&t.top-ie.top},h.prototype.setOptions=function(t){z.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},h.prototype.subscribe=function(t,e,i){function n(e){s._onMouseWheel(e,t,i)}var s=this;if("move"==e)t.on("dragstart",function(e){s._onDragStart(e,t)}),t.on("drag",function(e){s._onDrag(e,t,i)}),t.on("dragend",function(e){s._onDragEnd(e,t)});else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". Choose "move" or "zoom".');t.on("mousewheel",n),t.on("DOMMouseScroll",n),t.on("touch",function(){s._onTouch()}),t.on("pinch",function(e){s._onPinch(e,t,i)})}},h.prototype.on=function(t,e){var i=["rangechange","rangechanged"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},h.prototype.off=function(t,e){F.removeListener(this,t,e)},h.prototype._trigger=function(t){F.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?z.convert(t,"Date").valueOf():this.start,s=null!=e?z.convert(e,"Date").valueOf():this.end,o=null!=this.options.max?z.convert(this.options.max,"Date").valueOf():null,r=null!=this.options.min?z.convert(this.options.min,"Date").valueOf():null;if(isNaN(n)||null===n)throw new Error('Invalid start "'+t+'"');if(isNaN(s)||null===s)throw new Error('Invalid end "'+e+'"');if(n>s&&(s=n),null!==r&&r>n&&(i=r-n,n+=i,s+=i,null!=o&&s>o&&(s=o)),null!==o&&s>o&&(i=s-o,n-=i,s-=i,null!=r&&r>n&&(n=r)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>s-n&&(this.end-this.start===a?(n=this.start,s=this.end):(i=a-(s-n),n-=i/2,s+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),s-n>h&&(this.end-this.start===h?(n=this.start,s=this.end):(i=s-n-h,n+=i/2,s-=i/2))}var c=this.start!=n||this.end!=s;return this.start=n,this.end=s,c},h.prototype.getRange=function(){return{start:this.start,end:this.end}},h.prototype.conversion=function(t){return h.conversion(this.start,this.end,t)},h.conversion=function(t,e,i){return 0!=i&&e-t!=0?{offset:t,scale:i/(e-t)}:{offset:0,scale:1}};var Y={};h.prototype._onDragStart=function(t,e){if(!Y.pinching){Y.start=this.start,Y.end=this.end;var i=e.frame;i&&(i.style.cursor="move")}},h.prototype._onDrag=function(t,e,i){if(c(i),!Y.pinching){var n="horizontal"==i?t.gesture.deltaX:t.gesture.deltaY,s=Y.end-Y.start,o="horizontal"==i?e.width:e.height,r=-n/o*s;this._applyRange(Y.start+r,Y.end+r),this._trigger("rangechange")}},h.prototype._onDragEnd=function(t,e){Y.pinching||(e.frame&&(e.frame.style.cursor="auto"),this._trigger("rangechanged"))},h.prototype._onMouseWheel=function(t,e,i){c(i);var n=0;if(t.wheelDelta?n=t.wheelDelta/120:t.detail&&(n=-t.detail/3),n){var s;s=0>n?1-n/5:1/(1+n/5);var o=z.fakeGesture(this,t),r=d(o.touches[0],e.frame),a=this._pointerToDate(e,i,r);this.zoom(s,a)}z.preventDefault(t)},h.prototype._onTouch=function(){Y.start=this.start,Y.end=this.end,Y.pinching=!1,Y.center=null},h.prototype._onPinch=function(t,e,i){if(Y.pinching=!0,t.gesture.touches.length>1){Y.center||(Y.center=d(t.gesture.center,e.frame));var n=1/t.gesture.scale,s=this._pointerToDate(e,i,Y.center),o=d(t.gesture.center,e.frame),r=(this._pointerToDate(e,i,o),parseInt(s+(Y.start-s)*n)),a=parseInt(s+(Y.end-s)*n);this.setRange(r,a)}},h.prototype._pointerToDate=function(t,e,i){var n;if("horizontal"==e){var s=t.width;return n=this.conversion(s),i.x/n.scale+n.offset}var o=t.height;return n=this.conversion(o),i.y/n.scale+n.offset},h.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2);var i=e+(this.start-e)*t,n=e+(this.end-e)*t;this.setRange(i,n)},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)},l.prototype.add=function(t){if(void 0==t.id)throw new Error("Component has no field id");if(!(t instanceof u||t instanceof l))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},l.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]},l.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)}},l.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)}},l.prototype.repaint=function V(){function V(i,n){n in e||(i.depends&&i.depends.forEach(function(t){V(t,t.id)}),i.parent&&V(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={};z.forEach(this.components,V),t&&this.reflow()},l.prototype.reflow=function G(){function G(i,n){n in e||(i.depends&&i.depends.forEach(function(t){G(t,t.id)}),i.parent&&G(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={};z.forEach(this.components,G),t&&this.repaint()},u.prototype.setOptions=function(t){t&&(z.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},u.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},u.prototype.getContainer=function(){return null},u.prototype.getFrame=function(){return this.frame},u.prototype.repaint=function(){return!1},u.prototype.reflow=function(){return!1},u.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},u.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},u.prototype.requestRepaint=function(){if(!this.controller)throw new Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},u.prototype.requestReflow=function(){if(!this.controller)throw new Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new u,p.prototype.setOptions=u.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.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?z.addClassName(s,String(o())):z.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=z.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},f.prototype=new p,f.prototype.setOptions=u.prototype.setOptions,f.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.frame;if(s||(s=document.createElement("div"),this.frame=s,t+=1),!s.parentNode){if(!this.container)throw new Error("Cannot repaint root panel: no container attached");this.container.appendChild(s),t+=1}s.className="vis timeline rootpanel "+n.orientation;var o=n.className;return o&&z.addClassName(s,z.option.asString(o)),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},f.prototype.reflow=function(){var t=0,e=z.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},f.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},f.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?void(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow()):void t._unwatch()};z.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},f.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},f.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},f.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;z.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,t.hammer||(t.hammer=N(n,{prevent_default:!0})),t.hammer.on(i,s)}}})}},g.prototype=new u,g.prototype.setOptions=u.prototype.setOptions,g.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},g.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},g.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},g.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.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",!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 c=a.parentNode;if(c){var d=a.nextSibling;c.removeChild(a);var l="bottom"==s&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,l)),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 u=void 0,p=0;r.hasNext()&&1e3>p;){p++;var f=r.getCurrent(),g=this.toScreen(f),m=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(g,r.getLabelMinor()),m&&this.getOption("showMajorLabels")?(g>0&&(void 0==u&&(u=g),this._repaintMajorText(g,r.getLabelMajor())),this._repaintMajorLine(g)):this._repaintMinorLine(g),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),_=y.length*(o.majorCharWidth||10)+10;(void 0==u||u>_)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),d?c.insertBefore(a,d):c.appendChild(a)}return t>0},g.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=[]},g.prototype._repaintEnd=function(){z.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},g.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"},g.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"},g.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"},g.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"},g.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&&t.parentElement&&(e.removeChild(t.line),delete this.dom.line)},g.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}},g.prototype.reflow=function(){var t=0,e=z.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 c=i.parentNode?i.parentNode.offsetHeight:0;switch(c!=s.parentHeight&&(s.parentHeight=c,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(c-s.majorLabelHeight-this.top),s.minorLineWidth=1,s.majorLineTop=0,s.majorLineHeight=Math.max(c-this.top),s.majorLineWidth=1,s.lineTop=s.majorLabelHeight+s.minorLabelHeight;break;default:throw new Error('Unkown orientation "'+this.getOption("orientation")+'"')}var d=s.minorLabelHeight+s.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",d),this._updateConversion();var l=z.convert(n.start,"Number"),u=z.convert(n.end,"Number"),p=this.toTime(5*(s.minorCharWidth||10)).valueOf()-this.toTime(0).valueOf();this.step=new TimeStep(new Date(l),new Date(u),p),t+=e(s.range,"start",l),t+=e(s.range,"end",u),t+=e(s.range,"minimumStep",p.valueOf())}return t>0},g.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=new u,m.prototype.setOptions=u.prototype.setOptions,m.prototype.getContainer=function(){return this.frame},m.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 void(t&&(i.removeChild(t),delete this.frame));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.scale/2;return 30>r&&(r=30),this.currentTimeTimer=setTimeout(function(){o.repaint()},r),!1},v.prototype=new u,v.prototype.setOptions=u.prototype.setOptions,v.prototype.getContainer=function(){return this.frame},v.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("showCustomTime"))return void(t&&(i.removeChild(t),delete this.frame));if(!t){t=document.createElement("div"),t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t);var n=document.createElement("div");n.style.position="relative",n.style.top="0px",n.style.left="-10px",n.style.height="100%",n.style.width="20px",t.appendChild(n),this.frame=t,this.subscribe(this,"movetime")}e.conversion||e._updateConversion();var s=e.toScreen(this.customTime);return t.style.left=s+"px",t.title="Time: "+this.customTime,!1},v.prototype._setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},v.prototype._getCustomTime=function(){return new Date(this.customTime.valueOf())},v.prototype.subscribe=function(t,e){var i=this,n={component:t,event:e,callback:function(t){i._onMouseDown(t,n)},params:{}};t.on("mousedown",n.callback),i.listeners.push(n)},v.prototype.on=function(t,e){var i=this.frame;if(!i)throw new Error("Cannot add event listener: no parent attached");F.addListener(this,t,e),z.addEventListener(i,t,e)},v.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=z.getPageX(t),i.moved=!1,i.customTime=this.customTime;var s=this;i.onMouseMove||(i.onMouseMove=function(t){s._onMouseMove(t,e)},z.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){s._onMouseUp(t,e)},z.addEventListener(document,"mouseup",i.onMouseUp)),z.stopPropagation(t),z.preventDefault(t)}},v.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=this.parent,s=z.getPageX(t);void 0===i.mouseX&&(i.mouseX=s);var o=s-i.mouseX;Math.abs(o)>=1&&(i.moved=!0);var r=n.toScreen(i.customTime),a=r+o,h=n.toTime(a);this._setCustomTime(h),F.trigger(this,"timechange",{customTime:this.customTime}),z.preventDefault(t)},v.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;i.onMouseMove&&(z.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(z.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&F.trigger(this,"timechanged",{customTime:this.customTime})},y.prototype=new p,y.types={box:w,range:S,rangeoverflow:E,point:b},y.prototype.setOptions=u.prototype.setOptions,y.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},y.prototype.setSelection=function(t){var e,i,n,s,o;if(t){if(!Array.isArray(t))throw new TypeError("Array expected");for(e=0,i=this.selection.length;i>e;e++)n=this.selection[e],s=this.items[n],s&&s.unselect();for(this.selection=[],e=0,i=t.length;i>e;e++)n=t[e],s=this.items[n],s&&(this.selection.push(n),s.select());o=this.selection.concat([]),F.trigger(this,"select",{ids:o}),this.controller&&this.requestRepaint()}},y.prototype.getSelection=function(){return this.selection.concat([])},y.prototype._deselect=function(t){for(var e=this.selection,i=0,n=e.length;n>i;i++)if(e[i]==t){e.splice(i,1);break}},y.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.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&&z.addClassName(r,z.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var c=document.createElement("div");c.className="foreground",r.appendChild(c),this.dom.foreground=c;var d=document.createElement("div");d.className="itemset-axis",this.dom.axis=d,this.frame=r,t+=1}if(!this.parent)throw new Error("Cannot repaint itemset: no parent attached");var l=this.parent.getContainer();if(!l)throw new Error("Cannot repaint itemset: parent has no container element");r.parentNode||(l.appendChild(r),t+=1),this.dom.axis.parentNode||(l.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 u=this,p=this.queue,f=this.itemsData,g=this.items,m={};for(var v in p)if(p.hasOwnProperty(v)){var _=p[v],w=g[v],b=_.action;switch(b){case"add":case"update":var S=f&&f.get(v,m);if(S){var E=S.type||S.start&&S.end&&"range"||n.type||"box",T=y.types[E];if(w&&(T&&w instanceof T?(w.data=S,t++):(t+=w.hide(),w=null)),!w){if(!T)throw new TypeError('Unknown item type "'+E+'"');w=new T(u,S,n,o),w.id=_.id,t++}w.repaint(),g[v]=w}delete p[v];break;case"remove":w&&(w.selected&&u._deselect(v),t+=w.hide()),delete g[v],delete p[v];break;default:console.log('Error: unknown action "'+b+'"')}}return z.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},y.prototype.getForeground=function(){return this.dom.foreground -},y.prototype.getBackground=function(){return this.dom.background},y.prototype.getAxis=function(){return this.dom.axis},y.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=z.updateProperty,o=z.option.asNumber,r=z.option.asSize,a=this.frame;if(a){this._updateConversion(),z.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,c=o(e.maxHeight),d=null!=r(e.height);if(d)h=a.offsetHeight;else{var l=this.stack.ordered;if(l.length){var u=l[0].top,p=l[0].top+l[0].height;z.forEach(l,function(t){u=Math.min(u,t.top),p=Math.max(p,t.top+t.height)}),h=p-u+i+n}else h=i+n}null!=c&&(h=Math.min(h,c)),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},y.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},y.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&&(z.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),this.itemsData){var s=this.id;z.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},y.prototype.getItems=function(){return this.itemsData},y.prototype._onUpdate=function(t){this._toQueue("update",t)},y.prototype._onAdd=function(t){this._toQueue("add",t)},y.prototype._onRemove=function(t){this._toQueue("remove",t)},y.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]={id:e,action:t}}),this.controller&&this.requestRepaint()},y.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)},y.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},y.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},_.prototype.select=function(){this.selected=!0,this.visible&&this.repaint()},_.prototype.unselect=function(){this.selected=!1,this.visible&&this.repaint()},_.prototype.show=function(){return!1},_.prototype.hide=function(){return!1},_.prototype.repaint=function(){return!1},_.prototype.reflow=function(){return!1},_.prototype.getWidth=function(){return this.width},w.prototype=new _(null,null),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");if(!e.box.parentNode){var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");i.appendChild(e.box),t=!0}if(!e.line.parentNode){var n=this.parent.getBackground();if(!n)throw new Error("Cannot repaint time axis: parent has no background container element");n.appendChild(e.line),t=!0}if(!e.dot.parentNode){var s=this.parent.getAxis();if(!n)throw new Error("Cannot repaint time axis: parent has no axis container element");s.appendChild(e.dot),t=!0}if(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},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),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},w.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,c,d,l,u=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(d=this.data,l=this.parent&&this.parent.range,d&&l){var p=l.end-l.start;this.visible=d.start>l.start-p&&d.start0},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),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot",t.box["timeline-item"]=this)},w.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"}},b.prototype=new _(null,null),b.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},b.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},b.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},b.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,c,d=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(h=this.data,c=this.parent&&this.parent.range,h&&c){var l=c.end-c.start;this.visible=h.start>c.start-l&&h.start0},b.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),t.point["timeline-item"]=this)},b.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")},S.prototype=new _(null,null),S.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.selected?" selected":"");this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},S.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},S.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},S.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,c,d,l,u,p,f,g,m=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,c=this.parent&&this.parent.range,this.visible=h&&c?h.startc.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),d=z.updateProperty,l=t.box,u=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,s=i.padding||this.defaultOptions.padding,m+=d(e.content,"width",t.content.offsetWidth),m+=d(this,"height",l.offsetHeight),-u>r&&(r=-u),a>2*u&&(a=2*u),p=0>r?Math.min(-r,a-r-e.content.width-2*s):0,m+=d(e.content,"left",p),"top"==f?(g=n,m+=d(this,"top",g)):(g=o.height-this.height-n,m+=d(this,"top",g)),m+=d(this,"left",r),m+=d(this,"width",Math.max(a-r,1))):m+=1),m>0},S.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.box["timeline-item"]=this)},S.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")},E.prototype=new S(null,null),E.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 rangeoverflow"+n,t=!0)}return t},E.prototype.getWidth=function(){return void 0!==this.props.content&&this.width0},x.prototype=new p,x.prototype.setOptions=u.prototype.setOptions,x.prototype.setRange=function(){},x.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)}},x.prototype.getItems=function(){return this.itemsData},x.prototype.setRange=function(t){this.range=t},x.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(z.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;z.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},x.prototype.getGroups=function(){return this.groupsData},x.prototype.setSelection=function(t){var e=[],i=this.groups;for(var n in i)if(i.hasOwnProperty(n)){var s=i[n];s.setSelection(t)}return e},x.prototype.getSelection=function(){var t=[],e=this.groups;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];t=t.concat(n.getSelection())}return t},x.prototype.repaint=function(){var t,e,i,n,s=0,o=z.updateProperty,r=z.option.asSize,a=z.option.asElement,h=this.options,c=this.dom.frame,d=this.dom.labels,l=this.dom.labelSet;if(!this.parent)throw new Error("Cannot repaint groupset: no parent attached");var u=this.parent.getContainer();if(!u)throw new Error("Cannot repaint groupset: parent has no container element");if(!c){c=document.createElement("div"),c.className="groupset",this.dom.frame=c;var p=h.className;p&&z.addClassName(c,z.option.asString(p)),s+=1}c.parentNode||(u.appendChild(c),s+=1);var f=a(h.labelContainer);if(!f)throw new Error('Cannot repaint groupset: option "labelContainer" not defined');d||(d=document.createElement("div"),d.className="labels",this.dom.labels=d),l||(l=document.createElement("div"),l.className="label-set",d.appendChild(l),this.dom.labelSet=l),d.parentNode&&d.parentNode==f||(d.parentNode&&d.parentNode.removeChild(d.parentNode),f.appendChild(d)),s+=o(c.style,"height",r(h.height,this.height+"px")),s+=o(c.style,"top",r(h.top,"0px")),s+=o(c.style,"left",r(h.left,"0px")),s+=o(c.style,"width",r(h.width,"100%")),s+=o(l.style,"top",r(h.top,"0px")),s+=o(l.style,"height",r(h.height,this.height+"px"));var g=this,m=this.queue,v=this.groups,y=this.groupsData,_=Object.keys(m);if(_.length){_.forEach(function(t){var e=m[t],i=v[t];switch(e){case"add":case"update":if(!i){var n=Object.create(g.options);z.extend(n,{height:null,maxHeight:null}),i=new T(g,t,n),i.setItems(g.itemsData),v[t]=i,g.controller.add(i)}i.data=y.get(t),delete m[t];break;case"remove":i&&(i.setItems(),delete v[t],g.controller.remove(i)),delete m[t];break;default:console.log('Error: unknown action "'+e+'"')}});var w=this.groupsData.getIds({order:this.options.groupOrder});for(t=0;t0},x.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&&z.addClassName(i,o),e.label=i,i},x.prototype.getContainer=function(){return this.dom.frame},x.prototype.getLabelsWidth=function(){return this.props.labels.width},x.prototype.reflow=function(){var t,e,i=0,n=this.options,s=z.updateProperty,o=z.option.asNumber,r=z.option.asSize,a=this.dom.frame;if(a){var h,c=o(n.maxHeight),d=null!=r(n.height);if(d)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=c&&(h=Math.min(h,c)),i+=s(this,"height",h),i+=s(this,"top",a.offsetTop),i+=s(this,"left",a.offsetLeft),i+=s(this,"width",a.offsetWidth)}var l=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var u=e.props&&e.props.label&&e.props.label.width||0;l=Math.max(l,u)}return i+=s(this.props.labels,"width",l),i>0},x.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},x.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},x.prototype._onUpdate=function(t){this._toQueue(t,"update")},x.prototype._onAdd=function(t){this._toQueue(t,"add")},x.prototype._onRemove=function(t){this._toQueue(t,"remove")},x.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},C.prototype.setOptions=function(t){z.extend(this.options,t),this.range.setRange(t.start,t.end),this.controller.reflow(),this.controller.repaint()},C.prototype.setCustomTime=function(t){this.customtime._setCustomTime(t)},C.prototype.getCustomTime=function(){return new Date(this.customtime.customTime.valueOf())},C.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=z.convert(this.options.start,"Date")),void 0!=this.options.end&&(r=z.convert(this.options.end,"Date")),(null!=s||null!=r)&&this.range.setRange(s,r)}},C.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?x:y;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);z.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(!z.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)}},C.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}},C.prototype.setSelection=function(t){this.content&&this.content.setSelection(t)},C.prototype.getSelection=function(){return this.content?this.content.getSelection():[]},C.prototype.on=function(t,e){var i=["rangechange","rangechanged","select"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},C.prototype.off=function(t,e){F.removeListener(this,t,e)},C.prototype._trigger=function(t,e){F.trigger(this,t,e||{})},C.prototype._onSelectItem=function(t){var e=this._itemFromTarget(t),i=e?[e.id]:[];this.setSelection(i),this._trigger("select",{items:this.getSelection()}),t.stopPropagation()},C.prototype._onMultiSelectItem=function(t){var e,i=this._itemFromTarget(t);if(i){e=this.getSelection();var n=e.indexOf(i.id);-1==n?e.push(i.id):e.splice(n,1),this.setSelection(e),this._trigger("select",{items:this.getSelection()}),t.stopPropagation()}},C.prototype._itemFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-item"))return e["timeline-item"];e=e.parentNode}return null},function(t){function e(t){return C=t,u()}function i(){M=0,D=C.charAt(0)}function n(){M++,D=C.charAt(M)}function s(){return C.charAt(M+1)}function o(t){return N.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 c(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 d(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 l(){for(I=T.NULL,O="";" "==D||" "==D||"\n"==D||"\r"==D;)n();do{var t=!1;if("#"==D){for(var e=M-1;" "==C.charAt(e)||" "==C.charAt(e);)e--;if("\n"==C.charAt(e)||""==C.charAt(e)){for(;""!=D&&"\n"!=D;)n();t=!0}}if("/"==D&&"/"==s()){for(;""!=D&&"\n"!=D;)n();t=!0}if("/"==D&&"*"==s()){for(;""!=D;){if("*"==D&&"/"==s()){n(),n();break}n()}t=!0}for(;" "==D||" "==D||"\n"==D||"\r"==D;)n()}while(t);if(""==D)return void(I=T.DELIMITER);var i=D+s();if(x[i])return I=T.DELIMITER,O=i,n(),void n();if(x[D])return I=T.DELIMITER,O=D,void n();if(o(D)||"-"==D){for(O+=D,n();o(D);)O+=D,n();return"false"==O?O=!1:"true"==O?O=!0:isNaN(Number(O))||(O=Number(O)),void(I=T.IDENTIFIER)}if('"'==D){for(n();""!=D&&('"'!=D||'"'==D&&'"'==s());)O+=D,'"'==D&&n(),n();if('"'!=D)throw w('End of string " expected');return n(),void(I=T.IDENTIFIER)}for(I=T.UNKNOWN;""!=D;)O+=D,n();throw new SyntaxError('Syntax error in part "'+b(O,30)+'"')}function u(){var t={};if(i(),l(),"strict"==O&&(t.strict=!0,l()),("graph"==O||"digraph"==O)&&(t.type=O,l()),I==T.IDENTIFIER&&(t.id=O,l()),"{"!=O)throw w("Angle bracket { expected");if(l(),p(t),"}"!=O)throw w("Angle bracket } expected");if(l(),""!==O)throw w("End of file expected");return l(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==O&&"}"!=O;)f(t),";"==O&&l()}function f(t){var e=g(t);if(e)return void y(t,e);var i=m(t);if(!i){if(I!=T.IDENTIFIER)throw w("Identifier expected");var n=O;if(l(),"="==O){if(l(),I!=T.IDENTIFIER)throw w("Identifier expected");t[n]=O,l()}else v(t,n)}}function g(t){var e=null;if("subgraph"==O&&(e={},e.type="subgraph",l(),I==T.IDENTIFIER&&(e.id=O,l())),"{"==O){if(l(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=O)throw w("Angle bracket } expected");l(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function m(t){return"node"==O?(l(),t.node=_(),"node"):"edge"==O?(l(),t.edge=_(),"edge"):"graph"==O?(l(),t.graph=_(),"graph"):null}function v(t,e){var i={id:e},n=_();n&&(i.attr=n),h(t,i),y(t,e)}function y(t,e){for(;"->"==O||"--"==O;){var i,n=O;l();var s=g(t);if(s)i=s;else{if(I!=T.IDENTIFIER)throw w("Identifier or subgraph expected");i=O,h(t,{id:i}),l()}var o=_(),r=d(t,e,i,n,o);c(t,r),e=i}}function _(){for(var t=null;"["==O;){for(l(),t={};""!==O&&"]"!=O;){if(I!=T.IDENTIFIER)throw w("Attribute name expected");var e=O;if(l(),"="!=O)throw w("Equal sign = expected");if(l(),I!=T.IDENTIFIER)throw w("Attribute value expected");var i=O;a(t,e,i),l(),","==O&&l()}if("]"!=O)throw w("Bracket ] expected");l()}return t}function w(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 S(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 E(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)}),S(e,n,function(e,n){var o=d(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 T={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},x={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},C="",M=0,D="",O="",I=T.NULL,N=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=E}("undefined"!=typeof z?z: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=n%2===0?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,c=t+i/2,d=e+n/2;this.beginPath(),this.moveTo(t,d),this.bezierCurveTo(t,d-r,c-o,e,c,e),this.bezierCurveTo(c+o,e,a,d-r,a,d),this.bezierCurveTo(a,d+r,c+o,h,c,h),this.bezierCurveTo(c-o,h,t,d+r,t,d)},CanvasRenderingContext2D.prototype.database=function(t,e,i,n){var s=1/3,o=i,r=n*s,a=.5522848,h=o/2*a,c=r/2*a,d=t+o,l=e+r,u=t+o/2,p=e+r/2,f=e+(n-r/2),g=e+n;this.beginPath(),this.moveTo(d,p),this.bezierCurveTo(d,p+c,u+h,l,u,l),this.bezierCurveTo(u-h,l,t,p+c,t,p),this.bezierCurveTo(t,p-c,u-h,e,u,e),this.bezierCurveTo(u+h,e,d,p-c,d,p),this.lineTo(d,f),this.bezierCurveTo(d,f+c,u+h,g,u,g),this.bezierCurveTo(u-h,g,t,f+c,t,f),this.lineTo(t,p)},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),c=o+n/3*Math.sin(i+.5*Math.PI),d=s+n/3*Math.cos(i-.5*Math.PI),l=o+n/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,c),this.lineTo(r,a),this.lineTo(d,l),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,n,s){s||(s=[10,5]),0==u&&(u=.001);var o=s.length;this.moveTo(t,e);for(var r=i-t,a=n-e,h=a/r,c=Math.sqrt(r*r+a*a),d=0,l=!0;c>=.1;){var u=s[d++%o];u>c&&(u=c);var p=Math.sqrt(u*u/(1+h*h));0>r&&(p=-p),t+=p,e+=h*p,this[l?"lineTo":"moveTo"](t,e),c-=u,l=!l}}),M.prototype.resetCluster=function(){this.formationScale=void 0,this.clusterSize=1,this.containedNodes={},this.containedEdges={},this.clusterSessions=[]},M.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),-1==this.dynamicEdges.indexOf(t)&&this.dynamicEdges.push(t),this.dynamicEdgesLength=this.dynamicEdges.length,this._updateMass()},M.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&(this.edges.splice(e,1),this.dynamicEdges.splice(e,1)),this.dynamicEdgesLength=this.dynamicEdges.length,this._updateMass()},M.prototype._updateMass=function(){this.mass=1+.6*this.edges.length},M.prototype.setProperties=function(t,e){if(t){if(this.originalLabel=void 0,void 0!==t.id&&(this.id=t.id),void 0!==t.label&&(this.label=t.label,this.originalLabel=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!==t.horizontalAlignLeft&&(this.horizontalAlignLeft=t.horizontalAlignLeft),void 0!==t.verticalAlignTop&&(this.verticalAlignTop=t.verticalAlignTop),void 0!==t.triggerFunction&&(this.triggerFunction=t.triggerFunction),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=M.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&&t.fixed,this.yFixed=this.yFixed||void 0!==t.y&&t.fixed,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()}},M.parseColor=function(t){var e;return z.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,z.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},M.prototype.select=function(){this.selected=!0,this._reset()},M.prototype.unselect=function(){this.selected=!1,this._reset()},M.prototype.clearSizeCache=function(){this._reset()},M.prototype._reset=function(){this.width=void 0,this.height=void 0},M.prototype.getTitle=function(){return this.title},M.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}},M.prototype._setForce=function(t,e){this.fx=t,this.fy=e},M.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},M.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}},M.prototype.isFixed=function(){return this.xFixed&&this.yFixed},M.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t?!0:(this.vx=0,this.vy=0,!1)},M.prototype.isSelected=function(){return this.selected},M.prototype.getValue=function(){return this.value},M.prototype.getDistance=function(t,e){var i=this.x-t,n=this.y-e;return Math.sqrt(i*i+n*n)},M.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}this.baseRadiusValue=this.radius},M.prototype.draw=function(){throw"Draw method not initialized for node"},M.prototype.resize=function(){throw"Resize method not initialized for node"},M.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},M.prototype._resizeImage=function(){if(!this.width||!this.height){var t,e;if(this.value){this.radius=this.baseRadiusValue;var i=this.imageObj.height/this.imageObj.width;void 0!==i?(t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height):(t=0,e=0)}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e,this.width>0&&this.height>0&&(this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=(this.clusterSize-1)*this.clusterSizeRadiusFactor)}},M.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;if(0!=this.imageObj.width){if(this.clusterSize>1){var i=this.clusterSize>1?10:0;i*=this.graphScaleInv,i=Math.min(.2*this.width,i),t.globalAlpha=.5,t.drawImage(this.imageObj,this.left-i,this.top-i,this.width+2*i,this.height+2*i)}t.globalAlpha=1,t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2}else e=this.y;this._label(t,this.label,this.x,e,void 0,"top")},M.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,this.width+=.5*(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=.5*(this.clusterSize-1)*this.clusterSizeHeightFactor}},M.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.roundRect(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth,this.radius),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,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)},M.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,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.database(this.x-this.width/2-2*t.lineWidth,this.y-.5*this.height-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,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)},M.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,this.radius+=.5*(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.circle(this.x,this.y,this.radius+2*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.width1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.ellipse(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.ellipse(this.left,this.top,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._drawDot=function(t){this._drawShape(t,"circle")},M.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},M.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},M.prototype._drawSquare=function(t){this._drawShape(t,"square")},M.prototype._drawStar=function(t){this._drawShape(t,"star")},M.prototype._resizeShape=function(){if(!this.width){this.radius=this.baseRadiusValue;var t=2*this.radius;this.width=t,this.height=t,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=.5*(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawShape=function(t,e){this._resizeShape(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var i=2.5,n=2,s=2;switch(e){case"dot":s=2;break;case"square":s=2;break;case"triangle":s=3;break;case"triangleDown":s=3;break;case"star":s=4}t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?n:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t[e](this.x,this.y,this.radius+s*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?n:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t[e](this.x,this.y,this.radius),t.fill(),t.stroke(),this.label&&this._label(t,this.label,this.x,this.y+this.height/2,void 0,"top")},M.prototype._resizeText=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawText=function(t){this._resizeText(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,this._label(t,this.label,this.x,this.y)},M.prototype._label=function(t,e,i,n,s,o){if(e){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=s||"center",t.textBaseline=o||"middle";for(var r=e.split("\n"),a=r.length,h=this.fontSize+4,c=n+(1-a)/2*h,d=0;a>d;d++)t.fillText(r[d],i,c),c+=h}},M.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}},M.prototype.inArea=function(){return void 0!==this.width?this.x+this.width*this.graphScaleInv>=this.canvasTopLeft.x&&this.x-this.width*this.graphScaleInv=this.canvasTopLeft.y&&this.y-this.height*this.graphScaleInv=this.canvasTopLeft.x&&this.x=this.canvasTopLeft.y&&this.yh},D.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)}},D.prototype._getLineWidth=function(){return 1==this.selected?Math.min(2*this.width,this.widthMax)*this.graphScaleInv:this.width*this.graphScaleInv},D.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},D.prototype._circle=function(t,e,i,n){t.beginPath(),t.arc(e,i,n,0,2*Math.PI,!1),t.stroke()},D.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)}},D.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)}},D.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}},D.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)}},D.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))}},D.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,c=a*this.from.y+(1-a)*this.to.y,d=this.to.distanceToBorder(t,e),l=(o-d)/o,u=(1-l)*this.from.x+l*this.to.x,p=(1-l)*this.from.y+l*this.to.y;if(t.beginPath(),t.moveTo(h,c),t.lineTo(u,p),t.stroke(),i=10+5*this.width,t.arrow(u,p,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var g,m,v,y=this.from,_=this.length/4;y.width||y.resize(t),y.width>y.height?(g=y.x+y.width/2,m=y.y-_,v={x:g,y:y.y,angle:.9*Math.PI}):(g=y.x+_,m=y.y-y.height/2,v={x:y.x,y:m,angle:.6*Math.PI}),t.beginPath(),t.arc(g,m,_,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(g,m,_,.5),this._label(t,this.label,f.x,f.y))}},D._dist=function(t,e,i,n,s,o){var r=i-t,a=n-e,h=r*r+a*a,c=((s-t)*r+(o-e)*a)/h;c>1?c=1:0>c&&(c=0);var d=t+c*r,l=e+c*a,u=d-s,p=l-o;return Math.sqrt(u*u+p*p)},D.prototype.setScale=function(t){this.graphScaleInv=1/t},D.prototype.select=function(){this.selected=!0},D.prototype.unselect=function(){this.selected=!1},O.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},O.prototype.setText=function(t){this.frame.innerHTML=t},O.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),rAdd Node
Edit Selected
Connect Node
Delete selected";var t=document.getElementById("manipulate-addNode");t.onclick=this._createAddToolbar.bind(this);var e=document.getElementById("manipulate-editNode");e.onclick=this._createEditToolbar.bind(this);var i=document.getElementById("manipulate-connectNode");i.onclick=this._createConnectToolbar.bind(this);var n=document.getElementById("manipulate-delete");n.onclick=this._deleteSelected.bind(this)},_createAddToolbar:function(){this._clearManipulatorBar(),this.off("select",this.boundFunction),this.manipulationDiv.innerHTML="Back
Click in an empty space to place a new node";var t=document.getElementById("manipulate-back");t.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._addNode.bind(this),this.on("select",this.boundFunction)},_createEditToolbar:function(){this.selectForEdit=!1,this._clearManipulatorBar(),this.off("select",this.boundFunction);var t="";if(this._selectionIsEmpty()?t="Select a node or edge to edit.":this._getSelectedObjectCount()>1?(t="Select a single node or edge to edit.",this._unselectAll()):this._getSelectedNodeCount()>0?this._createEditNodeToolbar():this._createEditEdgeToolbar(),""!=t){this.selectForEdit=!0,this.manipulationDiv.innerHTML="Back
"+t+"";var e=document.getElementById("manipulate-back");e.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._createEditToolbar.bind(this),this.on("select",this.boundFunction)}},_createEditNodeToolbar:function(){console.log("here)"),this.selectForEdit=!1,this._clearManipulatorBar(),this.off("select",this.boundFunction);var t=this._getEditObject();this.manipulationDiv.innerHTML="Back
label:
color:
";var e=document.getElementById("manipulate-back");e.onclick=this._createManipulatorBar.bind(this);var i=document.getElementById("manipulator-obj-save");i.onclick=this._saveNodeData.bind(this),this.boundFunction=this._createManipulatorBar.bind(this),this.on("select",this.boundFunction)},_saveNodeData:function(){var t=this._getEditObject();t.label=document.getElementById("manipulator-obj-label").innerHTML;document.getElementById("manipulator-obj-label").innerHTML;t.color.background=document.getElementById("manipulator-obj-label").innerHTML},_createEditEdgeToolbar:function(){},_createConnectToolbar:function(){this._clearManipulatorBar(),this.off("select",this.boundFunction),this._unselectAll(),this.selectingForConnection=!1,this.manipulationDiv.innerHTML="Back
Select the node you want to connect to other nodes.";var t=document.getElementById("manipulate-back");t.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._handleConnect.bind(this),this.on("select",this.boundFunction)},_handleConnect:function(){this.selectingForConnection=!1,this._clusterInSelection()?(this._unselectClusters(!0),this._setManipulationMessage(this._selectionIsEmpty()?"Select the node you want to connect to other nodes (Clusters are not allowed).":"Click on the node you want to connect this node to (Clusters are not allowed).")):this._selectionIsEmpty()?this._setManipulationMessage("Select the node you want to connect to other nodes."):2==this._getSelectedNodeCount()?(this._connectNodes(),this._restoreSourceNode(),this._setManipulationMessage("Click on another node you want to connect this node to or go back.")):(this._setManipulationMessage("Click on the node you want to connect this node."),this._setSourceNode(),this.selectingForConnection=!0)},_getEditObject:function(){for(var t in this.selectionObj)if(this.selectionObj.hasOwnProperty(t))return this.selectionObj[t];return null},_setSourceNode:function(){for(var t in this.selectionObj)this.selectionObj.hasOwnProperty(t)&&this.selectionObj[t]instanceof M&&(this.manipulationSourceNode=this.selectionObj[t])},_getTargetNode:function(){for(var t in this.selectionObj)if(this.selectionObj.hasOwnProperty(t)&&this.selectionObj[t]instanceof M&&this.manipulationSourceNode.id!=this.selectionObj[t].id)return this.selectionObj[t];return null},_restoreSourceNode:function(){this._unselectAll(!0),this._selectObject(this.manipulationSourceNode)},_setManipulationMessage:function(t){var e=document.getElementById("manipulatorLabel");e.innerHTML=t},_addNode:function(){if(this._selectionIsEmpty()){var t=this._pointerToPositionObject(this.pointerPosition);this.createNodeOnClick=!0,this.nodesData.add({id:z.randomUUID(),x:t.left,y:t.top,label:"new",fixed:!1}),this.createNodeOnClick=!1,this.moving=!0,this.start()}},_connectNodes:function(){var t=this._getTargetNode(),e=this.manipulationSourceNode;this.edgesData.add({from:e.id,to:t.id}),this.moving=!0,this.start()},_deleteSelected:function(){if(this._clusterInSelection())alert("Clusters cannot be deleted.");else{var t=this.getSelectedNodes(),e=this.getSelectedEdges();this._removeEdges(e),this._removeNodes(t),this._redraw()}}},R={_putDataInSector:function(){this.sectors.active[this._sector()].nodes=this.nodes,this.sectors.active[this._sector()].edges=this.edges,this.sectors.active[this._sector()].nodeIndices=this.nodeIndices},_switchToSector:function(t,e){void 0===e||"active"==e?this._switchToActiveSector(t):this._switchToFrozenSector(t)},_switchToActiveSector:function(t){this.nodeIndices=this.sectors.active[t].nodeIndices,this.nodes=this.sectors.active[t].nodes,this.edges=this.sectors.active[t].edges},_switchToFrozenSector:function(t){this.nodeIndices=this.sectors.frozen[t].nodeIndices,this.nodes=this.sectors.frozen[t].nodes,this.edges=this.sectors.frozen[t].edges},_switchToNavigationSector:function(){this.nodeIndices=this.sectors.navigation.nodeIndices,this.nodes=this.sectors.navigation.nodes,this.edges=this.sectors.navigation.edges},_loadLatestSector:function(){this._switchToSector(this._sector())},_sector:function(){return this.activeSector[this.activeSector.length-1]},_previousSector:function(){if(this.activeSector.length>1)return this.activeSector[this.activeSector.length-2];throw new TypeError("there are not enough sectors in the this.activeSector array.")},_setActiveSector:function(t){this.activeSector.push(t)},_forgetLastSector:function(){this.activeSector.pop()},_createNewSector:function(t){this.sectors.active[t]={nodes:{},edges:{},nodeIndices:[],formationScale:this.scale,drawingNode:void 0},this.sectors.active[t].drawingNode=new M({id:t,color:{background:"#eaefef",border:"495c5e"}},{},{},this.constants),this.sectors.active[t].drawingNode.clusterSize=2},_deleteActiveSector:function(t){delete this.sectors.active[t]},_deleteFrozenSector:function(t){delete this.sectors.frozen[t]},_freezeSector:function(t){this.sectors.frozen[t]=this.sectors.active[t],this._deleteActiveSector(t)},_activateSector:function(t){this.sectors.active[t]=this.sectors.frozen[t],this._deleteFrozenSector(t)},_mergeThisWithFrozen:function(t){for(var e in this.nodes)this.nodes.hasOwnProperty(e)&&(this.sectors.frozen[t].nodes[e]=this.nodes[e]);for(var i in this.edges)this.edges.hasOwnProperty(i)&&(this.sectors.frozen[t].edges[i]=this.edges[i]);for(var n=0;n1?this[t](n[0],n[1]):this[t](e)}this._loadLatestSector()},_doInAllFrozenSectors:function(t,e){if(void 0===e)for(var i in this.sectors.frozen)this.sectors.frozen.hasOwnProperty(i)&&(this._switchToFrozenSector(i),this[t]());else for(var i in this.sectors.frozen)if(this.sectors.frozen.hasOwnProperty(i)){this._switchToFrozenSector(i);var n=Array.prototype.splice.call(arguments,1);n.length>1?this[t](n[0],n[1]):this[t](e)}this._loadLatestSector()},_doInNavigationSector:function(t,e){if(this._switchToNavigationSector(),void 0===e)this[t]();else{var i=Array.prototype.splice.call(arguments,1);i.length>1?this[t](i[0],i[1]):this[t](e)}this._loadLatestSector()},_doInAllSectors:function(t,e){var i=Array.prototype.splice.call(arguments,1);void 0===e?(this._doInAllActiveSectors(t),this._doInAllFrozenSectors(t)):i.length>1?(this._doInAllActiveSectors(t,i[0],i[1]),this._doInAllFrozenSectors(t,i[0],i[1])):(this._doInAllActiveSectors(t,e),this._doInAllFrozenSectors(t,e))},_clearNodeIndexList:function(){var t=this._sector();this.sectors.active[t].nodeIndices=[],this.nodeIndices=this.sectors.active[t].nodeIndices},_drawSectorNodes:function(t,e){var i,n=1e9,s=-1e9,o=1e9,r=-1e9; -for(var a in this.sectors[e])if(this.sectors[e].hasOwnProperty(a)&&void 0!==this.sectors[e][a].drawingNode){this._switchToSector(a,e),n=1e9,s=-1e9,o=1e9,r=-1e9;for(var h in this.nodes)this.nodes.hasOwnProperty(h)&&(i=this.nodes[h],i.resize(t),o>i.x-.5*i.width&&(o=i.x-.5*i.width),ri.y-.5*i.height&&(n=i.y-.5*i.height),st&&n>s;)s%3==0?this.forceAggregateHubs():this.increaseClusterLevel(),i=this.nodeIndices.length,s+=1;s>1&&1==e&&this.repositionNodes()},openCluster:function(t){var e=this.moving;if(t.clusterSize>this.constants.clustering.sectorThreshold&&this._nodeInActiveArea(t)&&("default"!=this._sector()||1!=this.nodeIndices.length)){this._addSector(t);for(var i=0;this.nodeIndices.lengthi;)this.decreaseClusterLevel(),i+=1}else this._expandClusterNode(t,!1,!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels();this.moving!=e&&this.start()},updateClustersDefault:function(){1==this.constants.clustering.enabled&&this.updateClusters(0,!1,!1)},increaseClusterLevel:function(){this.updateClusters(-1,!1,!0)},decreaseClusterLevel:function(){this.updateClusters(1,!1,!0)},updateClusters:function(t,e,i){var n=this.moving,s=this.nodeIndices.length;this.previousScale>this.scale&&0==t&&this._collapseSector(),this.previousScale>this.scale||-1==t?this._formClusters(i):(this.previousScalethis.scale||-1==t)&&(this._aggregateHubs(i),this._updateNodeIndexList()),(this.previousScale>this.scale||-1==t)&&(this.handleChains(),this._updateNodeIndexList()),this.previousScale=this.scale,this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.lengththis.constants.clustering.chainThreshold&&this._reduceAmountOfChains(1-this.constants.clustering.chainThreshold/t)},_aggregateHubs:function(t){this._getHubSize(),this._formClustersByHub(t,!1)},forceAggregateHubs:function(){var t=this.moving,e=this.nodeIndices.length;this._aggregateHubs(!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.length!=e&&(this.clusterSession+=1),this.moving!=t&&this.start()},_openClustersBySize:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];1==e.inView()&&(e.width*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientWidth||e.height*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientHeight)&&this.openCluster(e)}},_openClusters:function(t,e){for(var i=0;i1&&(t.clusterSizei)){var r=o.from,a=o.to;o.to.mass>o.from.mass&&(r=o.to,a=o.from),1==a.dynamicEdgesLength?this._addToCluster(r,a,!1):1==r.dynamicEdgesLength&&this._addToCluster(a,r,!1)}}},_forceClustersByZoom:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];if(1==e.dynamicEdgesLength&&0!=e.dynamicEdges.length){var i=e.dynamicEdges[0],n=i.toId==e.id?this.nodes[i.fromId]:this.nodes[i.toId];e.id!=n.id&&(n.mass>e.mass?this._addToCluster(n,e,!0):this._addToCluster(e,n,!0))}}},_formClustersByHub:function(t,e){for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&this._formClusterFromHub(this.nodes[i],t,e)},_formClusterFromHub:function(t,e,i,n){if(void 0===n&&(n=0),t.dynamicEdgesLength>=this.hubThreshold&&0==i||t.dynamicEdgesLength==this.hubThreshold&&1==i){for(var s,o,r,a=this.constants.clustering.clusterEdgeThreshold/this.scale,h=!1,c=[],d=t.dynamicEdges.length,l=0;d>l;l++)c.push(t.dynamicEdges[l].id);if(0==e)for(h=!1,l=0;d>l;l++){var u=this.edges[c[l]];if(void 0!==u&&u.connected&&u.toId!=u.fromId&&(s=u.to.x-u.from.x,o=u.to.y-u.from.y,r=Math.sqrt(s*s+o*o),a>r)){h=!0;break}}if(!e&&h||e)for(l=0;d>l;l++)if(u=this.edges[c[l]],void 0!==u){var p=this.nodes[u.fromId==t.id?u.toId:u.fromId];p.dynamicEdges.length<=this.hubThreshold+n&&p.id!=t.id&&this._addToCluster(t,p,e)}}},_addToCluster:function(t,e,i){t.containedNodes[e.id]=e;for(var n=0;n1)for(var n=0;n1&&(e.label="[".concat(String(e.clusterSize),"]"))}for(t in this.nodes)this.nodes.hasOwnProperty(t)&&(e=this.nodes[t],1==e.clusterSize&&(e.label=void 0!==e.originalLabel?e.originalLabel:String(e.id)))},_nodeInActiveArea:function(t){return Math.abs(t.x-this.areaCenter.x)<=this.constants.clustering.activeAreaBoxSize/this.scale&&Math.abs(t.y-this.areaCenter.y)<=this.constants.clustering.activeAreaBoxSize/this.scale},repositionNodes:function(){for(var t=0;tn&&(n=o.dynamicEdgesLength),t+=o.dynamicEdgesLength,e+=Math.pow(o.dynamicEdgesLength,2),i+=1}t/=i,e/=i;var r=e-Math.pow(t,2),a=Math.sqrt(r);this.hubThreshold=Math.floor(t+2*a),this.hubThreshold>n&&(this.hubThreshold=n)},_reduceAmountOfChains:function(t){this.hubThreshold=2;var e=Math.floor(this.nodeIndices.length*t);for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&e>0&&(this._formClusterFromHub(this.nodes[i],!0,!0,1),e-=1)},_getChainFraction:function(){var t=0,e=0;for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&(2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&(t+=1),e+=1);return t/e}},j={_getNodesOverlappingWith:function(t,e){var i=this.nodes;for(var n in i)i.hasOwnProperty(n)&&i[n].isOverlappingWith(t)&&e.push(n)},_getAllNodesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getNodesOverlappingWith",t,e),e},_getAllNavigationNodesOverlappingWith:function(t){var e=[];return this._doInNavigationSector("_getNodesOverlappingWith",t,e),e},_pointerToPositionObject:function(t){var e=this._canvasToX(t.x),i=this._canvasToY(t.y);return{left:e,top:i,right:e,bottom:i}},_pointerToScreenPositionObject:function(t){var e=t.x,i=t.y;return{left:e,top:i,right:e,bottom:i}},_getNavigationNodeAt:function(t){var e=this._pointerToScreenPositionObject(t),i=this._getAllNavigationNodesOverlappingWith(e);return i.length>0?this.sectors.navigation.nodes[i[i.length-1]]:null},_getNodeAt:function(t){var e=this._pointerToPositionObject(t),i=this._getAllNodesOverlappingWith(e);return i.length>0?this.nodes[i[i.length-1]]:null},_getEdgesOverlappingWith:function(t,e){var i=this.edges;for(var n in i)i.hasOwnProperty(n)&&i[n].isOverlappingWith(t)&&e.push(n)},_getAllEdgesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getEdgesOverlappingWith",t,e),e},_getEdgeAt:function(t){var e=this._pointerToPositionObject(t),i=this._getAllEdgesOverlappingWith(e);return i.length>0?this.edges[i[i.length-1]]:null},_addToSelection:function(t){this.selectionObj[t.id]=t},_removeFromSelection:function(t){delete this.selectionObj[t.id]},_unselectAll:function(t){void 0===t&&(t=!1);for(var e in this.selectionObj)this.selectionObj.hasOwnProperty(e)&&this.selectionObj[e].unselect();this.selectionObj={},0==t&&this._trigger("select",{nodes:this.getSelection()})},_unselectClusters:function(t){void 0===t&&(t=!1);for(var e in this.selectionObj)this.selectionObj.hasOwnProperty(e)&&this.selectionObj[e]instanceof M&&this.selectionObj[e].clusterSize>1&&this.selectionObj[e].unselect();0==t&&this._trigger("select",{nodes:this.getSelection()})},_getSelectedNodeCount:function(){var t=0;for(var e in this.selectionObj)this.selectionObj.hasOwnProperty(e)&&this.selectionObj[e]instanceof M&&(t+=1);return t},_getSelectedEdgeCount:function(){var t=0;for(var e in this.selectionObj)this.selectionObj.hasOwnProperty(e)&&this.selectionObj[e]instanceof D&&(t+=1);return t},_getSelectedObjectCount:function(){var t=0;for(var e in this.selectionObj)this.selectionObj.hasOwnProperty(e)&&(t+=1);return t},_selectionIsEmpty:function(){for(var t in this.selectionObj)if(this.selectionObj.hasOwnProperty(t))return!1;return!0},_clusterInSelection:function(){for(var t in this.selectionObj)if(this.selectionObj.hasOwnProperty(t)&&this.selectionObj[t]instanceof M&&this.selectionObj[t].clusterSize>1)return!0;return!1},_selectConnectedEdges:function(t){for(var e=0;ee;e++){n=t[e];var s=this.nodes[n];if(!s)throw new RangeError('Node with id "'+n+'" not found');this._selectObject(s,!0,!0)}this.redraw()},_updateSelection:function(){for(var t in this.selectionObj)this.selectionObj.hasOwnProperty(t)&&(this.selectionObj[t]instanceof M?this.nodes.hasOwnProperty(t)||delete this.selectionObj[t]:this.edges.hasOwnProperty(t)||delete this.selectionObj[t])}},U={_relocateNavigation:function(){if(void 0!==this.sectors){var t=this.navigationClientWidth-this.frame.canvas.clientWidth,e=this.navigationClientHeight-this.frame.canvas.clientHeight;this.navigationClientWidth=this.frame.canvas.clientWidth,this.navigationClientHeight=this.frame.canvas.clientHeight;var i=null;for(var n in this.sectors.navigation.nodes)this.sectors.navigation.nodes.hasOwnProperty(n)&&(i=this.sectors.navigation.nodes[n],i.horizontalAlignLeft||(i.x-=t),i.verticalAlignTop||(i.y-=e))}},_loadNavigationElements:function(){var t=this.constants.navigation.iconPath;this.navigationClientWidth=this.frame.canvas.clientWidth,this.navigationClientHeight=this.frame.canvas.clientHeight,void 0===this.navigationClientWidth&&(this.navigationClientWidth=0,this.navigationClientHeight=0);for(var e=15,i=7,n=[{id:"navigation_up",shape:"image",image:t+"/uparrow.png",triggerFunction:"_moveUp",verticalAlignTop:!1,x:45+e+i,y:this.navigationClientHeight-45-e-i},{id:"navigation_down",shape:"image",image:t+"/downarrow.png",triggerFunction:"_moveDown",verticalAlignTop:!1,x:45+e+i,y:this.navigationClientHeight-15-e},{id:"navigation_left",shape:"image",image:t+"/leftarrow.png",triggerFunction:"_moveLeft",verticalAlignTop:!1,x:15+e,y:this.navigationClientHeight-15-e},{id:"navigation_right",shape:"image",image:t+"/rightarrow.png",triggerFunction:"_moveRight",verticalAlignTop:!1,x:75+e+2*i,y:this.navigationClientHeight-15-e},{id:"navigation_plus",shape:"image",image:t+"/plus.png",triggerFunction:"_zoomIn",verticalAlignTop:!1,horizontalAlignLeft:!1,x:this.navigationClientWidth-45-e-i,y:this.navigationClientHeight-15-e},{id:"navigation_min",shape:"image",image:t+"/minus.png",triggerFunction:"_zoomOut",verticalAlignTop:!1,horizontalAlignLeft:!1,x:this.navigationClientWidth-15-e,y:this.navigationClientHeight-15-e},{id:"navigation_zoomExtends",shape:"image",image:t+"/zoomExtends.png",triggerFunction:"zoomToFit",verticalAlignTop:!1,horizontalAlignLeft:!1,x:this.navigationClientWidth-15-e,y:this.navigationClientHeight-45-e-i}],s=null,o=0;ot.x-t.width&&(n=t.x-t.width),st.y-t.height&&(e=t.y-t.height),i=this.constants.clustering.initialMaxNodes)var n=38.8467/(e-14.50184)+.0116;else var n=42.54117319/(e+39.31966387)+.1944405;else{var s=1.1*(Math.abs(i.minX)+Math.abs(i.maxX)),o=1.1*(Math.abs(i.minY)+Math.abs(i.maxY)),r=this.frame.canvas.clientWidth/s,a=this.frame.canvas.clientHeight/o;n=a>=r?r:a}n>1&&(n=1),this.pinch.mousewheelScale=n,this._setScale(n),this._centerGraph(i),this.start()},I.prototype._updateNodeIndexList=function(){this._clearNodeIndexList();for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodeIndices.push(t)},I.prototype.setData=function(t,e){if(void 0===e&&(e=!1),t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var i=B.util.DOTToGraph(t.dot);return void this.setData(i)}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this._putDataInSector(),e||(this.stabilize&&this._doStabilize(),this.moving=!0,this.start())},I.prototype.setOptions=function(t){if(t){if(void 0!==t.width&&(this.width=t.width),void 0!==t.height&&(this.height=t.height),void 0!==t.stabilize&&(this.stabilize=t.stabilize),void 0!==t.selectable&&(this.selectable=t.selectable),t.clustering){this.constants.clustering.enabled=!0;for(var e in t.clustering)t.clustering.hasOwnProperty(e)&&(this.constants.clustering[e]=t.clustering[e])}else void 0!==t.clustering&&(this.constants.clustering.enabled=!1);if(t.navigation){this.constants.navigation.enabled=!0;for(var e in t.navigation)t.navigation.hasOwnProperty(e)&&(this.constants.navigation[e]=t.navigation[e])}else void 0!==t.navigation&&(this.constants.navigation.enabled=!1);if(t.keyboard){this.constants.keyboard.enabled=!0;for(var e in t.keyboard)t.keyboard.hasOwnProperty(e)&&(this.constants.keyboard[e]=t.keyboard[e])}else void 0!==t.keyboard&&(this.constants.keyboard.enabled=!1);if(t.edges){for(e in t.edges)t.edges.hasOwnProperty(e)&&(this.constants.edges[e]=t.edges[e]);void 0!==t.edges.length&&t.nodes&&void 0===t.nodes.distance&&(this.constants.edges.length=t.edges.length,this.constants.nodes.distance=1.25*t.edges.length),t.edges.fontColor||(this.constants.edges.fontColor=t.edges.color),t.edges.dash&&(void 0!==t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!==t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!==t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=M.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var n=t.groups[i];this.groups.add(i,n)}}this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1),this._loadNavigationControls(),this._createKeyBinds(),this._redraw()},I.prototype.on=function(t,e){var i=["select"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},I.prototype.off=function(t,e){F.removeListener(this,t,e)},I.prototype._trigger=function(t,e){F.trigger(this,t,e)},I.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.style.zIndex="1",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this;this.drag={},this.pinch={},this.hammer=N(this.frame.canvas,{prevent_default:!0}),this.hammer.on("tap",e._onTap.bind(e)),this.hammer.on("doubletap",e._onDoubleTap.bind(e)),this.hammer.on("hold",e._onHold.bind(e)),this.hammer.on("pinch",e._onPinch.bind(e)),this.hammer.on("touch",e._onTouch.bind(e)),this.hammer.on("dragstart",e._onDragStart.bind(e)),this.hammer.on("drag",e._onDrag.bind(e)),this.hammer.on("dragend",e._onDragEnd.bind(e)),this.hammer.on("release",e._onRelease.bind(e)),this.hammer.on("mousewheel",e._onMouseWheel.bind(e)),this.hammer.on("DOMMouseScroll",e._onMouseWheel.bind(e)),this.hammer.on("mousemove",e._onMouseMoveTitle.bind(e)),this.containerElement.appendChild(this.frame)},I.prototype._createKeyBinds=function(){var t=this;this.mousetrap=k,this.mousetrap.reset(),1==this.constants.keyboard.enabled&&(this.mousetrap.bind("up",this._moveUp.bind(t),"keydown"),this.mousetrap.bind("up",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("down",this._moveDown.bind(t),"keydown"),this.mousetrap.bind("down",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("left",this._moveLeft.bind(t),"keydown"),this.mousetrap.bind("left",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("right",this._moveRight.bind(t),"keydown"),this.mousetrap.bind("right",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("=",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("=",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("-",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("-",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("[",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("[",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("]",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("]",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pageup",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("pageup",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pagedown",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("pagedown",this._stopZoom.bind(t),"keyup"))},I.prototype._getPointer=function(t){return{x:t.pageX-B.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-B.util.getAbsoluteTop(this.frame.canvas)}},I.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.touches[0]),this.drag.pinched=!1,this.pinch.scale=this._getScale(),this._handleTouch(this.drag.pointer)},I.prototype._onDragStart=function(){var t=this.drag,e=this._getNodeAt(t.pointer);if(t.dragging=!0,t.selection=[],t.translation=this._getTranslation(),t.nodeId=null,null!=e){t.nodeId=e.id,e.isSelected()||this._selectObject(e,!1);for(var i in this.selectionObj)if(this.selectionObj.hasOwnProperty(i)){var n=this.selectionObj[i];if(n instanceof M){var s={id:n.id,node:n,x:n.x,y:n.y,xFixed:n.xFixed,yFixed:n.yFixed};n.xFixed=!0,n.yFixed=!0,t.selection.push(s)}}}},I.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}}},I.prototype._onDragEnd=function(){this.drag.dragging=!1;var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},I.prototype._onTap=function(t){var e=this._getPointer(t.gesture.touches[0]);this.pointerPosition=e,this._handleTap(e)},I.prototype._onDoubleTap=function(t){var e=this._getPointer(t.gesture.touches[0]);this._handleDoubleTap(e)},I.prototype._onHold=function(t){var e=this._getPointer(t.gesture.touches[0]);this.pointerPosition=e,this._handleOnHold(e)},I.prototype._onRelease=function(){this._handleOnRelease()},I.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)},I.prototype._zoom=function(t,e){var i=this._getScale();1e-5>t&&(t=1e-5),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.areaCenter={x:this._canvasToX(e.x),y:this._canvasToY(e.y)},this.pinch.mousewheelScale=t,this._setScale(t),this._setTranslation(o,r),this.updateClustersDefault(),this._redraw(),t},I.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){"mousewheelScale"in this.pinch||(this.pinch.mousewheelScale=1);var i=this.pinch.mousewheelScale,n=e/10;0>e&&(n/=1-n),i*=1+n;var s=z.fakeGesture(this,t),o=this._getPointer(s.center);i=this._zoom(i,o)}t.preventDefault()},I.prototype._onMouseMoveTitle=function(t){var e=z.fakeGesture(this,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.drag.dragging||(this.popupTimer=setTimeout(s,300))},I.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 O(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()},I.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},I.prototype._getConnectionCount=function(t){function e(t){for(var e=[],i=0,n=t.length;n>i;i++)for(var s=t[i],o=s.edges,r=0,a=o.length;a>r;r++){var h=o[r],c=null;h.from==s?c=h.to:h.to==s&&(c=h.from);var d,l;if(c)for(d=0,l=t.length;l>d;d++)if(t[d]==c){c=null;break}if(c)for(d=0,l=e.length;l>d;d++)if(e[d]==c){c=null;break}c&&e.push(c)}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,c=i.length;c>h;h++)a.push(i[h].length);return a},I.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,void 0!==this.manipulationDiv&&(this.manipulationDiv.style.width=this.frame.canvas.clientWidth),1==this.constants.navigation.enabled&&this._relocateNavigation() -},I.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&&z.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;z.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var n=this.nodesData.getIds();this._addNodes(n)}this._updateSelection()},I.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 M(s,this.images,this.groups,this.constants);if(this.nodes[e]=o,!o.isFixed()&&1!=this.createNodeOnClick){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._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(this.nodes),this.updateLabels()},I.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 M(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(e)},I.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._updateNodeIndexList(),this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},I.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&&z.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;z.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var n=this.edgesData.getIds();this._addEdges(n)}this._reconnectEdges()},I.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,{showInternalIds:!0});e[o]=new D(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},I.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 D(r,this,this.constants),this.edges[o]=a)}this.moving=!0,this._updateValueRange(e)},I.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)},I.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()}},I.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)},I.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},I.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.canvasTopLeft={x:this._canvasToX(0),y:this._canvasToY(0)},this.canvasBottomRight={x:this._canvasToX(this.frame.canvas.clientWidth),y:this._canvasToY(this.frame.canvas.clientHeight)},this._doInAllSectors("_drawAllSectorNodes",t),this._doInAllSectors("_drawEdges",t),this._doInAllSectors("_drawNodes",t,!0),t.restore(),1==this.constants.navigation.enabled&&this._doInNavigationSector("_drawNodes",t,!0)},I.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)},I.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},I.prototype._setScale=function(t){this.scale=t},I.prototype._getScale=function(){return this.scale},I.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},I.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},I.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},I.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},I.prototype._drawNodes=function(t,e){void 0===e&&(e=!1);var i=this.nodes,n=[];for(var s in i)i.hasOwnProperty(s)&&(i[s].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight),i[s].isSelected()?n.push(s):(i[s].inArea()||e)&&i[s].draw(t));for(var o=0,r=n.length;r>o;o++)(i[n[o]].inArea()||e)&&i[n[o]].draw(t)},I.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];n.setScale(this.scale),n.connected&&e[i].draw(t)}},I.prototype._doStabilize=function(){for(var t=0,e=this.constants.minVelocity,i=!1;!i&&tthis.constants.clustering.clusterThreshold&&1==this.constants.clustering.enabled&&this.clusterToFit(this.constants.clustering.reduceToNodes,!1),this._calculateForces())},I.prototype._calculateForces=function(){var t,e,i,n,s,o,r,a,h,c,d,l,u,p,f,g,m,v,y=this.nodes,_=this.edges,w=.08*this.forceFactor;for(g=0;gn&&(i=Math.atan2(e,t),r=.5*b>n?1:T*n+E,r*=0==v?1:1+v*this.constants.clustering.forceAmplification,r*=this.forceFactor,s=Math.cos(i)*r,o=Math.sin(i)*r,l._addForce(-s,-o),u._addForce(s,o))}for(f in _)_.hasOwnProperty(f)&&(p=_[f],p.connected&&this.nodes.hasOwnProperty(p.toId)&&this.nodes.hasOwnProperty(p.fromId)&&(v=p.to.clusterSize+p.from.clusterSize-2,t=p.to.x-p.from.x,e=p.to.y-p.from.y,c=p.length,c+=v*this.constants.clustering.edgeGrowth,h=Math.sqrt(t*t+e*e),i=Math.atan2(e,t),a=p.stiffness*(c-h)*this.forceFactor,s=Math.cos(i)*a,o=Math.sin(i)*a,p.from._addForce(-s,-o),p.to._addForce(s,o)))},I.prototype._isMoving=function(t){var e=t/this.scale,i=this.nodes;for(var n in i)if(i.hasOwnProperty(n)&&i[n].isMoving(e))return!0;return!1},I.prototype._discreteStepNodes=function(){var t=.01,e=this.nodes;for(var i in e)e.hasOwnProperty(i)&&e[i].discreteStep(t);var n=this.constants.minVelocity;this.moving=this._isMoving(n)},I.prototype.start=function(){if(!this.freezeSimulation)if(this.moving&&(this._doInAllActiveSectors("_initializeForceCalculation"),this._doInAllActiveSectors("_discreteStepNodes"),this._findCenter(this._getRange())),this.moving||0!=this.xIncrement||0!=this.yIncrement||0!=this.zoomIncrement){if(!this.timer){var t=this;this.timer=window.setTimeout(function(){if(t.timer=void 0,0!=t.xIncrement||0!=t.yIncrement){var e=t._getTranslation();t._setTranslation(e.x+t.xIncrement,e.y+t.yIncrement)}if(0!=t.zoomIncrement){var i={x:t.frame.canvas.clientWidth/2,y:t.frame.canvas.clientHeight/2};t._zoom(t.scale*(1+t.zoomIncrement),i)}t.start(),t._redraw()},this.renderTimestep)}}else this._redraw()},I.prototype.singleStep=function(){if(this.moving){this._initializeForceCalculation(),this._discreteStepNodes();var t=this.constants.minVelocity;this.moving=this._isMoving(t),this._redraw()}},I.prototype.toggleFreeze=function(){0==this.freezeSimulation?this.freezeSimulation=!0:(this.freezeSimulation=!1,this.start())},I.prototype._loadClusterSystem=function(){this.clusterSession=0,this.hubThreshold=5;for(var t in W)W.hasOwnProperty(t)&&(I.prototype[t]=W[t])},I.prototype._loadSectorSystem=function(){this.sectors={},this.activeSector=["default"],this.sectors.active={},this.sectors.active["default"]={nodes:{},edges:{},nodeIndices:[],formationScale:1,drawingNode:void 0},this.sectors.frozen={},this.sectors.navigation={nodes:{},edges:{},nodeIndices:[],formationScale:1,drawingNode:void 0},this.nodeIndices=this.sectors.active["default"].nodeIndices;for(var t in R)R.hasOwnProperty(t)&&(I.prototype[t]=R[t])},I.prototype._loadSelectionSystem=function(){this.selectionObj={};for(var t in j)j.hasOwnProperty(t)&&(I.prototype[t]=j[t])},I.prototype._loadManipulationSystem=function(){this.manipulationDiv=document.createElement("div"),this.manipulationDiv.className="graph-manipulationDiv",this.containerElement.insertBefore(this.manipulationDiv,this.frame);for(var t in H)H.hasOwnProperty(t)&&(I.prototype[t]=H[t]);this._createManipulatorBar()},I.prototype._loadNavigationControls=function(){for(var t in U)U.hasOwnProperty(t)&&(I.prototype[t]=U[t]);1==this.constants.navigation.enabled&&this._loadNavigationElements()},I.prototype._relocateNavigation=function(){},I.prototype._unHighlightAll=function(){};var B={util:z,events:F,Controller:l,DataSet:o,DataView:r,Range:h,Stack:a,TimeStep:TimeStep,EventBus:s,components:{items:{Item:_,ItemBox:w,ItemPoint:b,ItemRange:S},Component:u,Panel:p,RootPanel:f,ItemSet:y,TimeAxis:g},graph:{Node:M,Edge:D,Popup:O,Groups:Groups,Images:Images},Timeline:C,Graph:I};"undefined"!=typeof n&&(n=B),"undefined"!=typeof i&&"undefined"!=typeof i.exports&&(i.exports=B),"function"==typeof t&&t(function(){return B}),"undefined"!=typeof window&&(window.vis=B)},{hammerjs:2,moment:3,mousetrap:4}],2:[function(t,e){!function(t,i){"use strict";function n(){if(!s.READY){s.event.determineEventTypes();for(var t in s.gestures)s.gestures.hasOwnProperty(t)&&s.detection.register(s.gestures[t]);s.event.onTouch(s.DOCUMENT,s.EVENT_MOVE,s.detection.detect),s.event.onTouch(s.DOCUMENT,s.EVENT_END,s.detection.detect),s.READY=!0}}var s=function(t,e){return new s.Instance(t,e||{})};s.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},s.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,s.HAS_TOUCHEVENTS="ontouchstart"in t,s.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,s.NO_MOUSEEVENTS=s.HAS_TOUCHEVENTS&&navigator.userAgent.match(s.MOBILE_REGEX),s.EVENT_TYPES={},s.DIRECTION_DOWN="down",s.DIRECTION_LEFT="left",s.DIRECTION_UP="up",s.DIRECTION_RIGHT="right",s.POINTER_MOUSE="mouse",s.POINTER_TOUCH="touch",s.POINTER_PEN="pen",s.EVENT_START="start",s.EVENT_MOVE="move",s.EVENT_END="end",s.DOCUMENT=document,s.plugins={},s.READY=!1,s.Instance=function(t,e){var i=this;return n(),this.element=t,this.enabled=!0,this.options=s.utils.extend(s.utils.extend({},s.defaults),e||{}),this.options.stop_browser_behavior&&s.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),s.event.onTouch(t,s.EVENT_START,function(t){i.enabled&&s.detection.startDetect(i,t)}),this},s.Instance.prototype={on:function(t,e){for(var i=t.split(" "),n=0;n0&&e==s.EVENT_END?e=s.EVENT_MOVE:d||(e=s.EVENT_END),d||null===o?o=h:h=o,i.call(s.detection,n.collectEventData(t,e,h)),s.HAS_POINTEREVENTS&&e==s.EVENT_END&&(d=s.PointerEvent.updatePointer(e,h))),d||(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),void(this.triggered=!1);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?void t.stopDetect():(e.options.prevent_default&&t.preventDefault(),void(t.eventType==s.EVENT_START&&e.trigger(this.name,t)))}},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)},{}],3:[function(e,i){(function(n){function s(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function o(t,e){return function(i){return p(t.call(this,i),e)}}function r(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function a(){}function h(t){x(t),d(this,t)}function c(t){var e=_(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,c=e.millisecond||0;this._milliseconds=+c+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){var e,i={};for(e in t)t.hasOwnProperty(e)&&_e.hasOwnProperty(e)&&(i[e]=t[e]);return i}function u(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e,i){for(var n=""+Math.abs(t),s=t>=0;n.lengthn;n++)(i&&t[n]!==e[n]||!i&&b(t[n])!==b(e[n]))&&r++;return r+o}function y(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=Xe[t]||Ze[e]||e}return t}function _(t){var e,i,n={};for(i in t)t.hasOwnProperty(i)&&(e=y(i),e&&(n[e]=t[i]));return n}function w(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}re[t]=function(s,o){var r,a,h=re.fn._lang[t],c=[];if("number"==typeof s&&(o=s,s=n),a=function(t){var e=re().utc().set(i,t);return h.call(re.fn._lang,e,s||"")},null!=o)return a(o);for(r=0;e>r;r++)c.push(a(r));return c}}function b(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function S(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function E(t){return T(t)?366:365}function T(t){return t%4===0&&t%100!==0||t%400===0}function x(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[ue]<0||t._a[ue]>11?ue:t._a[pe]<1||t._a[pe]>S(t._a[le],t._a[ue])?pe:t._a[fe]<0||t._a[fe]>23?fe:t._a[ge]<0||t._a[ge]>59?ge:t._a[me]<0||t._a[me]>59?me:t._a[ve]<0||t._a[ve]>999?ve:-1,t._pf._overflowDayOfYear&&(le>e||e>pe)&&(e=pe),t._pf.overflow=e)}function C(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 M(t){return t?t.toLowerCase().replace("_","-"):t}function D(t,e){return e._isUTC?re(t).zone(e._offset||0):re(t).local()}function O(t,e){return e.abbr=t,ye[t]||(ye[t]=new a),ye[t].set(e),ye[t]}function I(t){delete ye[t]}function N(t){var i,n,s,o,r=0,a=function(t){if(!ye[t]&&we)try{e("./lang/"+t)}catch(i){}return ye[t]};if(!t)return re.fn._lang;if(!g(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&&v(o,s,!0)>=i-1)break;i--}r++}return re.fn._lang}function L(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function k(t){var e,i,n=t.match(Te);for(e=0,i=n.length;i>e;e++)n[e]=Je[n[e]]?Je[n[e]]:L(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 A(t,e){return t.isValid()?(e=z(e,t.lang()),Ke[e]||(Ke[e]=k(e)),Ke[e](t)):t.lang().invalidDate()}function z(t,e){function i(t){return e.longDateFormat(t)||t}var n=5;for(xe.lastIndex=0;n>=0&&xe.test(t);)t=t.replace(xe,i),xe.lastIndex=0,n-=1;return t}function P(t,e){var i,n=e._strict;switch(t){case"DDDD":return Fe;case"YYYY":case"GGGG":case"gggg":return n?Ye:De;case"Y":case"G":case"g":return Re;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return n?He:Oe;case"S":if(n)return ze;case"SS":if(n)return Pe;case"SSS":if(n)return Fe;case"DDD":return Me;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ne;case"a":case"A":return N(e._l)._meridiemParse;case"X":return Ae;case"Z":case"ZZ":return Le;case"T":return ke;case"SSSS":return Ie;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return n?Pe:Ce;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Ce;default:return i=new RegExp(B(U(t.replace("\\","")),"i"))}}function F(t){t=t||"";var e=t.match(Le)||[],i=e[e.length-1]||[],n=(i+"").match(Ve)||["-",0,0],s=+(60*n[1])+b(n[2]);return"+"===n[0]?-s:s}function Y(t,e,i){var n,s=i._a;switch(t){case"M":case"MM":null!=e&&(s[ue]=b(e)-1);break;case"MMM":case"MMMM":n=N(i._l).monthsParse(e),null!=n?s[ue]=n:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(s[pe]=b(e));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=b(e));break;case"YY":s[le]=b(e)+(b(e)>68?1900:2e3);break;case"YYYY":case"YYYYY":case"YYYYYY":s[le]=b(e);break;case"a":case"A":i._isPm=N(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":s[fe]=b(e);break;case"m":case"mm":s[ge]=b(e);break;case"s":case"ss":s[me]=b(e);break;case"S":case"SS":case"SSS":case"SSSS":s[ve]=b(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=F(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 H(t){var e,i,n,s,o,r,a,h,c,d,l=[];if(!t._d){for(n=W(t),t._w&&null==t._a[pe]&&null==t._a[ue]&&(o=function(e){var i=parseInt(e,10);return e?e.length<3?i>68?1900+i:2e3+i:i:null==t._a[le]?re().weekYear():t._a[le]},r=t._w,null!=r.GG||null!=r.W||null!=r.E?a=te(o(r.GG),r.W||1,r.E,4,1):(h=N(t._l),c=null!=r.d?K(r.d,h):null!=r.e?parseInt(r.e,10)+h._week.dow:0,d=parseInt(r.w,10)||1,null!=r.d&&cE(s)&&(t._pf._overflowDayOfYear=!0),i=Z(s,0,t._dayOfYear),t._a[ue]=i.getUTCMonth(),t._a[pe]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=l[e]=n[e];for(;7>e;e++)t._a[e]=l[e]=null==t._a[e]?2===e?1:0:t._a[e];l[fe]+=b((t._tzm||0)/60),l[ge]+=b((t._tzm||0)%60),t._d=(t._useUTC?Z:X).apply(null,l)}}function R(t){var e;t._d||(e=_(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],H(t))}function W(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function j(t){t._a=[],t._pf.empty=!0;var e,i,n,s,o,r=N(t._l),a=""+t._i,h=a.length,c=0;for(n=z(t._f,r).match(Te)||[],e=0;e0&&t._pf.unusedInput.push(o),a=a.slice(a.indexOf(i)+i.length),c+=i.length),Je[s]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(s),Y(s,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(s);t._pf.charsLeftOver=h-c,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[fe]<12&&(t._a[fe]+=12),t._isPm===!1&&12===t._a[fe]&&(t._a[fe]=0),H(t),x(t)}function U(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,s){return e||i||n||s})}function B(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function V(t){var e,i,n,o,r;if(0===t._f.length)return t._pf.invalidFormat=!0,void(t._d=new Date(0/0));for(o=0;or)&&(n=r,i=e));d(t,i||e)}function G(t){var e,i,n=t._i,s=We.exec(n);if(s){for(t._pf.iso=!0,e=0,i=Ue.length;i>e;e++)if(Ue[e][1].exec(n)){t._f=Ue[e][0]+(s[6]||" ");break}for(e=0,i=Be.length;i>e;e++)if(Be[e][1].exec(n)){t._f+=Be[e][0];break}n.match(Le)&&(t._f+="Z"),j(t)}else t._d=new Date(n)}function q(t){var e=t._i,i=be.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?G(t):g(e)?(t._a=e.slice(0),H(t)):m(e)?t._d=new Date(+e):"object"==typeof e?R(t):t._d=new Date(e)}function X(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 Z(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function K(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 $(t,e,i,n,s){return s.relativeTime(e||1,!!i,t,n)}function Q(t,e,i){var n=de(Math.abs(t)/1e3),s=de(n/60),o=de(s/60),r=de(o/24),a=de(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",de(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,$.apply({},h)}function J(t,e,i){var n,s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),n=re(t).add("d",o),{week:Math.ceil(n.dayOfYear()/7),year:n.year()}}function te(t,e,i,n,s){var o,r,a=Z(t,0,1).getUTCDay();return i=null!=i?i:s,o=s-a+(a>n?7:0)-(s>a?7:0),r=7*(e-1)+(i-s)+o+1,{year:r>0?t:t-1,dayOfYear:r>0?r:E(t-1)+r}}function ee(t){var e=t._i,i=t._f;return null===e?re.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=N().preparse(e)),re.isMoment(e)?(t=l(e),t._d=new Date(+e._d)):i?g(i)?V(t):j(t):q(t),new h(t))}function ie(t,e){re.fn[t]=re.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),re.updateOffset(this),this):this._d["get"+i+e]()}}function ne(t){re.duration.fn[t]=function(){return this._data[t]}}function se(t,e){re.duration.fn["as"+t]=function(){return+this/e}}function oe(t){var e=!1,i=re; -"undefined"==typeof ender&&(t?(ce.moment=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)},d(ce.moment,i)):ce.moment=re)}for(var re,ae,he="2.5.1",ce=this,de=Math.round,le=0,ue=1,pe=2,fe=3,ge=4,me=5,ve=6,ye={},_e={_isAMomentObject:null,_i:null,_f:null,_l:null,_strict:null,_isUTC:null,_offset:null,_pf:null,_lang:null},we="undefined"!=typeof i&&i.exports&&"undefined"!=typeof e,be=/^\/?Date\((\-?\d+)/i,Se=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Ee=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,Te=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,xe=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Ce=/\d\d?/,Me=/\d{1,3}/,De=/\d{1,4}/,Oe=/[+\-]?\d{1,6}/,Ie=/\d+/,Ne=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Le=/Z|[\+\-]\d\d:?\d\d/gi,ke=/T/i,Ae=/[\+\-]?\d+(\.\d{1,3})?/,ze=/\d/,Pe=/\d\d/,Fe=/\d{3}/,Ye=/\d{4}/,He=/[+-]?\d{6}/,Re=/[+-]?\d+/,We=/^\s*(?:[+-]\d{6}|\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)?|\s*Z)?)?$/,je="YYYY-MM-DDTHH:mm:ssZ",Ue=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],Be=[["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/]],Ve=/([\+\-]|\d\d)/gi,Ge="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),qe={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Xe={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"},Ze={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Ke={},$e="DDD w W M D d".split(" "),Qe="M D H h m s w W".split(" "),Je={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)},YYYYYY:function(){var t=this.year(),e=t>=0?"+":"-";return e+p(Math.abs(t),6)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return p(this.weekYear(),4)},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return p(this.isoWeekYear(),4)},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 b(this.milliseconds()/100)},SS:function(){return p(b(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(b(t/60),2)+":"+p(b(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(b(t/60),2)+p(b(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ti=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];$e.length;)ae=$e.pop(),Je[ae+"o"]=r(Je[ae],ae);for(;Qe.length;)ae=Qe.pop(),Je[ae+ae]=o(Je[ae],2);for(Je.DDDD=o(Je.DDD,3),d(a.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=re.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=re([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 J(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),re=function(t,e,i,o){var r;return"boolean"==typeof i&&(o=i,i=n),r={},r._isAMomentObject=!0,r._i=t,r._f=e,r._l=i,r._strict=o,r._isUTC=!1,r._pf=s(),ee(r)},re.utc=function(t,e,i,o){var r;return"boolean"==typeof i&&(o=i,i=n),r={},r._isAMomentObject=!0,r._useUTC=!0,r._isUTC=!0,r._l=i,r._i=t,r._f=e,r._strict=o,r._pf=s(),ee(r).utc()},re.unix=function(t){return re(1e3*t)},re.duration=function(t,e){var i,n,s,o=t,r=null;return re.isDuration(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(r=Se.exec(t))?(i="-"===r[1]?-1:1,o={y:0,d:b(r[pe])*i,h:b(r[fe])*i,m:b(r[ge])*i,s:b(r[me])*i,ms:b(r[ve])*i}):(r=Ee.exec(t))&&(i="-"===r[1]?-1:1,s=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},o={y:s(r[2]),M:s(r[3]),d:s(r[4]),h:s(r[5]),m:s(r[6]),s:s(r[7]),w:s(r[8])}),n=new c(o),re.isDuration(t)&&t.hasOwnProperty("_lang")&&(n._lang=t._lang),n},re.version=he,re.defaultFormat=je,re.updateOffset=function(){},re.lang=function(t,e){var i;return t?(e?O(M(t),e):null===e?(I(t),t="en"):ye[t]||N(t),i=re.duration.fn._lang=re.fn._lang=N(t),i._abbr):re.fn._lang._abbr},re.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),N(t)},re.isMoment=function(t){return t instanceof h||null!=t&&t.hasOwnProperty("_isAMomentObject")},re.isDuration=function(t){return t instanceof c},ae=ti.length-1;ae>=0;--ae)w(ti[ae]);for(re.normalizeUnits=function(t){return y(t)},re.invalid=function(t){var e=re.utc(0/0);return null!=t?d(e._pf,t):e._pf.userInvalidated=!0,e},re.parseZone=function(t){return re(t).parseZone()},d(re.fn=h.prototype,{clone:function(){return re(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(){var t=re(this).utc();return 00:!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=A(this,t||re.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?re.duration(+e,t):re.duration(t,e),f(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?re.duration(+e,t):re.duration(t,e),f(this,i,-1),this},diff:function(t,e,i){var n,s,o=D(t,this),r=6e4*(this.zone()-o.zone());return e=y(e),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),s=12*(this.year()-o.year())+(this.month()-o.month()),s+=(this-re(this).startOf("month")-(o-re(o).startOf("month")))/n,s-=6e4*(this.zone()-re(this).startOf("month").zone()-(o.zone()-re(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:u(s)},from:function(t,e){return re.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(re(),t)},calendar:function(){var t=D(re(),this).startOf("day"),e=this.diff(t,"days",!0),i=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse";return this.format(this.lang().calendar(i,this))},isLeapYear:function(){return T(this.year())},isDST:function(){return this.zone()+re(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+re(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+D(t,this).startOf(e)},min:function(t){return t=re.apply(null,arguments),this>t?this:t},max:function(t){return t=re.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=F(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&f(this,re.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?re(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return S(this.year(),this.month())},dayOfYear:function(t){var e=de((re(this).startOf("day")-re(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(){return Math.ceil((this.month()+1)/3)},weekYear:function(t){var e=J(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=J(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=J(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=y(t),this[t]()},set:function(t,e){return t=y(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===n?this._lang:(this._lang=N(t),this)}}),ae=0;ae-1?!1:"INPUT"==i||"SELECT"==i||"TEXTAREA"==i||e.contentEditable&&"true"==e.contentEditable}function o(t,e){return t.sort().join(",")===e.sort().join(",")}function r(t){t=t||{};var e,i=!1;for(e in M)t[e]?i=!0:M[e]=0;i||(O=!1)}function a(t,e,i,n,s){var r,a,h=[];if(!x[t])return[];for("keyup"==i&&u(t)&&(e=[t]),r=0;r95&&112>t||b.hasOwnProperty(t)&&(_[b[t]]=t)}return _}function g(t,e,i){return i||(i=f()[t]?"keydown":"keypress"),"keypress"==i&&e.length&&(i="keydown"),i}function m(t,e,i,s){M[t]=0,s||(s=g(e[0],[]));var o,a=function(){O=s,++M[t],p()},h=function(t){c(i,t),"keyup"!==s&&(D=n(t)),setTimeout(r,10)};for(o=0;o1)return m(t,c,e,i);for(h="+"===t?["+"]:t.split("+"),o=0;o":".","?":"/","|":"\\"},T={option:"alt",command:"meta","return":"enter",escape:"esc"},x={},C={},M={},D=!1,O=!1,I=1;20>I;++I)b[111+I]="f"+I;for(I=0;9>=I;++I)b[I+96]=I;i(document,"keypress",l),i(document,"keydown",l),i(document,"keyup",l);var N={bind:function(t,e,i){return y(t instanceof Array?t:[t],e,i),C[t+":"+i]=e,this},unbind:function(t,e){return C[t+":"+e]&&(delete C[t+":"+e],this.bind(t,function(){},e)),this},trigger:function(t,e){return C[t+":"+e](),this},reset:function(){return x={},C={},this}};e.exports=N},{}]},{},[1])(1)}); \ No newline at end of file diff --git a/docs/graph.html b/docs/graph.html index 8dfe2594..8df62843 100644 --- a/docs/graph.html +++ b/docs/graph.html @@ -53,7 +53,9 @@
  • Nodes
  • Edges
  • Groups
  • -
  • Clustering
  • +
  • Physics
  • +
  • Data_manipulation
  • +
  • Clustering
  • Navigation controls
  • Keyboard navigation
  • @@ -529,13 +531,6 @@ var edges = [ type. - - length - number - no - The length of the edge in pixels. - - style string @@ -647,6 +642,25 @@ var options = { Description + + physics + Object + none + + Configuration of the physics system governing the simulation of the nodes and edges. + Barnes-Hut nBody simulation is used by default. See section Physics for an overview of the available options. + + + + + dataManipulation + Object + none + + Settings for manipulating the Dataset. See section Data manipulation for an overview of the available options. + + + clustering Object @@ -710,6 +724,13 @@ var options = { + + smoothCurves + Boolean + true + If true, edges are drawn as smooth curves. This is more computationally intensive since the edge now is a quadratic Bezier curve with control points on both nodes and an invisible node in the center of the edge. This support node is also handed by the physics simulation. + + selectable Boolean @@ -964,12 +985,6 @@ var options = { Only applicable when the line style is dash-line. - - length - Number - 100 - The default length of a edge. - style String @@ -1122,6 +1137,236 @@ var nodes = [ +

    Physics

    +

    + The physics system has been overhauled to increase performance. The original simulation method was based on particel physics with a repulsion field (potential) around each node, + and the edges were modelled as springs. The new system employed the Barnes-Hut gravitational simulation model. The edges are still modelled as springs. + To unify the physics system, the damping, repulsion distance and edge length have been combined in an physics option. To retain good behaviour, both the old repulsion model and the Barnes-Hut model have their own parameters. + If no options for the physics system are supplied, the Barnes-Hut method will be used with the default parameters. +

    +
    +// These variables must be defined in an options object named physics.
    +// If a variable is not supplied, the default value is used.
    +var options = {
    +    physics: {
    +        barnesHut: {
    +            enabled: true,
    +            gravitationalConstant: -2000,
    +            centralGravity: 0.1,
    +            springLength: 100,
    +            springConstant: 0.05,
    +            damping: 0.09
    +        },
    +        repulsion: {
    +            centralGravity: 0.1,
    +            springLength: 50,
    +            springConstant: 0.05,
    +            nodeDistance: 100,
    +            damping: 0.09
    +        },
    +    }
    +
    +
    barnesHut:
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDefaultDescription
    enabledBooleantrueThis switches the Barnes-Hut simulation on or off. If it is turned off, the old repulsion model is used. Barnes-Hut is generally faster and yields better results.
    gravitationalConstantNumber-2000This is the gravitational constand used to calculate the gravity forces. More information is available here.
    centralGravityNumber0.1The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.
    springLengthNumber100In the previous versions this was a property of the edges, called length. This is the length of the springs when they are at rest. During the simulation they will be streched by the gravitational fields. + To greatly reduce the edge length, the gravitationalConstant has to be reduced as well.
    springConstantNumber0.05This is the spring constant used to calculate the spring forces based on Hooke′s Law. More information is available here.
    dampingNumber0.09This is the damping constant. It is used to dissipate energy from the system to have it settle in an equilibrium. More information is available here.
    +
    repulsion:
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDefaultDescription
    centralGravityNumber0.1The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.
    springLengthNumber50In the previous versions this was a property of the edges, called length. This is the length of the springs when they are at rest. During the simulation they will be streched by the gravitational fields. + To greatly reduce the edge length, the gravitationalConstant has to be reduced as well.
    nodeDistanceNumber100This parameter is used to define the distance of influence of the repulsion field of the nodes. Below half this distance, the repulsion is maximal and beyond twice this distance the repulsion is zero.
    springConstantNumber0.05This is the spring constant used to calculate the spring forces based on Hooke′s Law. More information is available here.
    dampingNumber0.09This is the damping constant. It is used to dissipate energy from the system to have it settle in an equilibrium. More information is available here.
    + +

    Data manipulation

    +

    + By using the data manipulation feature of the graph you can dynamically create nodes, connect nodes with edges, edit nodes or delete nodes and edges. + The toolbar is fully HTML and CSS so the user can style this to their preference. To control the behaviour of the data manipulation, users can insert custom functions + into the data manipulation process. For example, an injected function can show an detailed pop-up when a user wants to add a node. In example 21, + two functions have been injected into the add and edit functionality. This is described in more detail in the next subsection. +

    +
    +// These variables must be defined in an options object named dataManipulation.
    +// If a variable is not supplied, the default value is used.
    +var options = {
    +    dataManipulation: {
    +      enabled: false,
    +      initiallyVisible: false
    +    }
    +}
    +// OR to just load the module with default values:
    +var options: {
    +    dataManipulation: true
    +}
    +
    + + + + + + + + + + + + + + + + + + + + +
    NameTypeDefaultDescription
    enabledBooleanfalseEnabling or disabling of the data manipulation toolbar. If it is initially hidden, an edit button appears in the top left corner.
    initiallyVisibleBooleanfalseInitially hide or show the data manipulation toolbar.
    + +

    Data manipulation: custom functionality

    +

    + Users can insert custom functions into the add node, edit node, connect nodes, and delete selected operations. This is done by supplying them in the options. + If the callback is NOT called, nothing happens. Example 21 has two working examples + for the add and edit functions. The data the user is supplied with in these functions has been described in the code below. + For the add data, you can add any and all options that are accepted for node creation as described above. The same goes for edit, however only the fields described + in the code below contain information on the selected node. The callback for connect accepts any options that are used for edge creation. Only the callback for delete selected + requires the same data structure that is supplied to the user.

    + If there is no injected function supplied for the edit operation, the button will not be shown in the toolbar. +

    +
    +// If a variable is not supplied, the default value is used.
    +var options: {
    +    dataManipulation: true,
    +    onAdd: function(data,callback) {
    +        // fixed must be false because we define a set x and y position.
    +        // If fixed is not false, the node cannot move.
    +        /** data = {id: random unique id,
    +        *           label: new,
    +        *           x: x position of click (canvas space),
    +        *           y: y position of click (canvas space),
    +        *           fixed: false
    +        *          };
    +        */
    +        var newData = {..}; // alter the data as you want.
    +                            // all fields normally accepted by a node can be used.
    +        callback(newData);  // call the callback to add a node.
    +    },
    +    onEdit: function(data,callback) {
    +        /** data = {id:...,
    +        *           label: ...,
    +        *           group: ...,
    +        *           shape: ...,
    +        *           color: {
    +        *             background:...,
    +        *             border:...,
    +        *             highlight: {
    +        *               background:...,
    +        *               border:...
    +        *             }
    +        *           }
    +        *          };
    +        */
    +        var newData = {..}; // alter the data as you want.
    +                            // all fields normally accepted by a node can be used.
    +        callback(newData);  // call the callback with the new data to edit the node.
    +    }
    +    onConnect: function(data,callback) {
    +        // data = {from: nodeId1, to: nodeId2};
    +        var newData = {..};      // check or alter data as you see fit.
    +        callback(newData);       // call the callback to connect the nodes.
    +    },
    +    onDelete: function(data,callback) {
    +        // data = {nodes: [selectedNodeIds], edges: [selectedEdgeIds]};
    +        var newData = {..}; // alter the data as you want.
    +                            // the same data structure is required.
    +        callback(newData);  // call the callback to delete the objects.
    +    }
    +};
    +
    +

    + Because the interface elements are CSS and HTML, the user will have to correct for size changes of the canvas. To facilitate this, a new event has been added called frameResize. + A function can be bound to this event. This function is supplied with the new widht and height of the canvas. The CSS can then be updated accordingly. + An code snippet from example 21 is shown below. +

    +
    +graph.on("frameResize", function(params) {console.log(params.width,params.height)});
    +
    +

    Clustering

    The graph now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without @@ -1150,16 +1395,19 @@ var options = { reduceToNodes:300, chainThreshold: 0.4, clusterEdgeThreshold: 20, - sectorThreshold: 50, + sectorThreshold: 100, screenSizeThreshold: 0.2, fontSizeMultiplier: 4.0, - forceAmplification: 0.6, - distanceAmplification: 0.2, - edgeGrowth: 11, - nodeScaling: {width: 10, - height: 10, - radius: 10}, - activeAreaBoxSize: 100 + maxFontSize: 1000, + forceAmplification: 0.1, + distanceAmplification: 0.1, + edgeGrowth: 20, + nodeScaling: {width: 1, + height: 1, + radius: 1}, + maxNodeSizeIncrements: 600, + activeAreaBoxSize: 100, + clusterLevelDifference: 2 } } // OR to just load the module with default values: @@ -1233,6 +1481,12 @@ var options: { 4.0 This parameter denotes the increase in fontSize of the cluster when a single node is added to it. + + maxFontSize + Number + 1000 + This parameter denotes the largest allowed font size. If the font becomes too large, some browsers experience problems displaying this. + forceAmplification Number @@ -1251,7 +1505,7 @@ var options: { edgeGrowth Number - 11 + 20 This factor determines the elongation of edges connected to a cluster. @@ -1272,13 +1526,29 @@ var options: { 10 This factor determines how much the radius of a cluster increases in pixels per added node. - - activeAreaBoxSize + + maxNodeSizeIncrements + Number + 600 + This limits the size clusters can grow to. The default value, 600, implies that if a cluster contains more than 600 nodes, it will no longer grow. + + + activeAreaBoxSize + Number + 100 + Imagine a square with an edge length of activeAreaBoxSize pixels around your cursor. + If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector. + This is regardless of the zoom level. + + + clusterLevelDifference Number - 100 - Imagine a square with an edge length of activeAreaBoxSize pixels around your cursor. - If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector. - This is regardless of the zoom level. + 2 + At every clustering session, Graph will check if the difference between cluster levels is + acceptable. When a cluster is formed when zooming out, that is one cluster level. + If you zoom out further and it encompasses more nodes, that is another level. For example: + If the highest level of your graph at any given time is 3, nodes that have not clustered or + have clustered only once will join their neighbour with the lowest cluster level. diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html index f5a05364..ea1202aa 100755 --- a/examples/graph/02_random_nodes.html +++ b/examples/graph/02_random_nodes.html @@ -57,6 +57,7 @@ j++; } + var from = i; var to = j; edges.push({ @@ -87,7 +88,6 @@ */ var options = { edges: { - length: 50 }, stabilize: false }; @@ -102,7 +102,6 @@ -

    diff --git a/examples/graph/19_scale_free_graph_clustering.html b/examples/graph/19_scale_free_graph_clustering.html index cced805b..640e2207 100644 --- a/examples/graph/19_scale_free_graph_clustering.html +++ b/examples/graph/19_scale_free_graph_clustering.html @@ -88,9 +88,6 @@ }; */ var options = { - edges: { - length: 50 - }, clustering: { enabled: clusteringOn, clusterEdgeThreshold: clusterEdgeThreshold diff --git a/examples/graph/20_navigation.html b/examples/graph/20_navigation.html index 5bfaff77..8bc2006a 100644 --- a/examples/graph/20_navigation.html +++ b/examples/graph/20_navigation.html @@ -34,6 +34,7 @@ + - - + #operation { + font-size:28px; + } + #graph-popUp { + display:none; + position:absolute; + top:350px; + left:170px; + z-index:299; + width:250px; + height:120px; + background-color: #f9f9f9; + border-style:solid; + border-width:3px; + border-color: #5394ed; + padding:10px; + text-align: center; + } + + + + + + -

    Navigation controls and keyboad navigation

    +

    Editing the dataset

    - This example is the same as example 2, except for the navigation controls that has been activated. The navigation controls are described below.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Icons:
    Keyboard shortcuts:
    Up arrow
    Down arrow
    Left arrow
    Right arrow
    =
    [
    Page up
    -
    ]
    Page down
    None
    Description:
    Move up
    Move down
    Move left
    Move right
    Zoom in
    Zoom out
    Zoom extends
    -
    - Apart from clicking the icons, you can also navigate using the keyboard. The buttons are in table above. - Zoom Extends changes the zoom and position of the camera to encompass all visible nodes. - - + In this example we have enabled the data manipulation setting. If the dataManipulation option is set to true, the edit button will appear. + If you prefer to have the toolbar visible initially, you can set the initiallyVisible option to true. The exact method is described in the docs. +

    + The data manipulation allows the user to add nodes, connect them, edit them and delete any selected items. In this example we have created trigger functions + for the add and edit operations. By settings these trigger functions the user can direct the way the data is manipulated. In this example we have created a simple + pop-up that allows us to edit some of the properties.

    - - - - -
    -
    - +
    + node
    + + + + + +
    id
    label
    + + +
    +

    + diff --git a/examples/graph/22_les_miserables.html b/examples/graph/22_les_miserables.html new file mode 100644 index 00000000..8200e051 --- /dev/null +++ b/examples/graph/22_les_miserables.html @@ -0,0 +1,373 @@ + + + + Graph | Multiline text + + + + + + + + + +
    + + diff --git a/examples/graph/index.html b/examples/graph/index.html index 53720b5b..4b38888d 100644 --- a/examples/graph/index.html +++ b/examples/graph/index.html @@ -33,6 +33,7 @@

    19_scale_free_graph_clustering.html

    20_navigation.html

    21_data_manipulation.html

    +

    22_les_miserables.html

    graphviz_gallery.html

    diff --git a/src/graph/Edge.js b/src/graph/Edge.js index 145dac2b..eafcdfdb 100644 --- a/src/graph/Edge.js +++ b/src/graph/Edge.js @@ -31,11 +31,13 @@ function Edge (properties, graph, constants) { this.title = undefined; this.width = constants.edges.width; this.value = undefined; - this.length = constants.edges.length; + this.length = constants.physics.springLength; this.selected = false; + this.smooth = constants.smoothCurves; this.from = null; // a node this.to = null; // a node + this.via = null; // a temp node // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster // by storing the original information we can revert to the original connection when the cluser is opened. @@ -49,13 +51,11 @@ function Edge (properties, graph, constants) { // 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); - } /** @@ -74,6 +74,7 @@ Edge.prototype.setProperties = function(properties, constants) { 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; @@ -82,10 +83,11 @@ Edge.prototype.setProperties = function(properties, constants) { 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;} + + 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 @@ -103,7 +105,6 @@ Edge.prototype.setProperties = function(properties, constants) { 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) { @@ -211,8 +212,7 @@ Edge.prototype.isOverlappingWith = function(obj) { var xObj = obj.left; var yObj = obj.top; - - var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); return (dist < distMax); }; @@ -231,7 +231,7 @@ Edge.prototype._drawLine = function(ctx) { ctx.lineWidth = this._getLineWidth(); var point; - if (this.from != this.to) { + if (this.from != this.to+9) { // draw line this._line(ctx); @@ -286,7 +286,12 @@ 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); + if (this.smooth == true) { + ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); + } + else { + ctx.lineTo(this.to.x, this.to.y); + } ctx.stroke(); }; @@ -348,25 +353,70 @@ Edge.prototype._drawDashLine = function(ctx) { 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 - { + // only firefox and chrome support this method, else we use the legacy one. + if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) { + ctx.beginPath(); ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); + + // configure the dash pattern + var pattern = [0]; + if (this.dash.length !== undefined && this.dash.gap !== undefined) { + pattern = [this.dash.length,this.dash.gap]; + } + else { + pattern = [5,5]; + } + + // set dash settings for chrome or firefox + if (typeof ctx.setLineDash !== 'undefined') { //Chrome + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; + + } else { //Firefox + ctx.mozDash = pattern; + ctx.mozDashOffset = 0; + } + + // draw the line + if (this.smooth == true) { + ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); + } + else { + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + + // restore the dash settings. + if (typeof ctx.setLineDash !== 'undefined') { //Chrome + ctx.setLineDash([0]); + ctx.lineDashOffset = 0; + + } else { //Firefox + ctx.mozDash = [0]; + ctx.mozDashOffset = 0; + } + } + else { // unsupporting smooth lines + // 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(); } - ctx.stroke(); // draw label if (this.label) { @@ -423,10 +473,18 @@ Edge.prototype._drawArrowCenter = function(ctx) { // 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); + // draw an arrow halfway the line + if (this.smooth == true) { + var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x)); + var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y)); + point = {x:midpointX, y:midpointY}; + } + else { + point = this._pointOnLine(0.5); + } + ctx.arrow(point.x, point.y, angle, length); ctx.fill(); ctx.stroke(); @@ -440,18 +498,18 @@ Edge.prototype._drawArrowCenter = function(ctx) { else { // draw circle var x, y; - var radius = this.length / 4; + var radius = 0.25 * Math.max(100,this.length); var node = this.from; if (!node.width) { node.resize(ctx); } if (node.width > node.height) { - x = node.x + node.width / 2; + x = node.x + node.width * 0.5; y = node.y - radius; } else { x = node.x + radius; - y = node.y - node.height / 2; + y = node.y - node.height * 0.5; } this._circle(ctx, x, y, radius); @@ -486,32 +544,43 @@ Edge.prototype._drawArrow = function(ctx) { ctx.fillStyle = this.color; ctx.lineWidth = this._getLineWidth(); - // draw line var angle, length; + //draw a line 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 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + + var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); + var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; + var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; + var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; + + if (this.smooth == true) { + angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x)); + dx = (this.to.x - this.via.x); + dy = (this.to.y - this.via.y); + edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + } + var toBorderDist = this.to.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + var xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x; + var yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * 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.moveTo(xFrom,yFrom); + if (this.smooth == true) { + ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo); + } + else { + ctx.lineTo(xTo, yTo); + } ctx.stroke(); // draw arrow at the end of the line - length = 10 + 5 * this.width; // TODO: make customizable? + length = 10 + 5 * this.width; ctx.arrow(xTo, yTo, angle, length); ctx.fill(); ctx.stroke(); @@ -526,12 +595,12 @@ Edge.prototype._drawArrow = function(ctx) { // draw circle var node = this.from; var x, y, arrow; - var radius = this.length / 4; + var radius = 0.25 * Math.max(100,this.length); if (!node.width) { node.resize(ctx); } if (node.width > node.height) { - x = node.x + node.width / 2; + x = node.x + node.width * 0.5; y = node.y - radius; arrow = { x: x, @@ -541,7 +610,7 @@ Edge.prototype._drawArrow = function(ctx) { } else { x = node.x + radius; - y = node.y - node.height / 2; + y = node.y - node.height * 0.5; arrow = { x: node.x, y: y, @@ -549,7 +618,6 @@ Edge.prototype._drawArrow = function(ctx) { }; } 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(); @@ -582,31 +650,46 @@ Edge.prototype._drawArrow = function(ctx) { * @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; +Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point + if (this.smooth == true) { + var minDistance = 1e9; + var i,t,x,y,dx,dy; + for (i = 0; i < 10; i++) { + t = 0.1*i; + x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2; + y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2; + dx = Math.abs(x3-x); + dy = Math.abs(y3-y); + minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy)); + } + return minDistance } + else { + 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; + 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 + //# 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); + return Math.sqrt(dx*dx + dy*dy); + } }; @@ -623,8 +706,15 @@ Edge.prototype.setScale = function(scale) { Edge.prototype.select = function() { this.selected = true; -} +}; Edge.prototype.unselect = function() { this.selected = false; -} \ No newline at end of file +}; + +Edge.prototype.positionBezierNode = function() { + if (this.via !== null) { + this.via.x = 0.5 * (this.from.x + this.to.x); + this.via.y = 0.5 * (this.from.y + this.to.y); + } +}; \ No newline at end of file diff --git a/src/graph/Graph.js b/src/graph/Graph.js index 408ab33c..fd402de6 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -10,17 +10,25 @@ * @param {Object} options Options */ function Graph (container, data, options) { + + this._initializeMixinLoaders(); + // create variables and set default values this.containerElement = container; this.width = '100%'; this.height = '100%'; - // to give everything a nice fluidity, we seperate the rendering and calculating of the forces - this.renderRefreshRate = 60; // hz (fps) + + // render and calculation settings + this.renderRefreshRate = 60; // hz (fps) this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on - this.stabilize = true; // stabilize before displaying the graph + this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame + this.maxRenderSteps = 4; // max amount of physics ticks per render step. + + this.stabilize = true; // stabilize before displaying the graph this.selectable = true; - this.forceFactor = 50000; + // these functions are triggered when the dataset is edited + this.triggerFunctions = {add:null,edit:null,connect:null,delete:null}; // set constant values this.constants = { @@ -28,15 +36,14 @@ function Graph (container, data, options) { 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', + fontFace: 'verdana', +// fontFace: 'arial', color: { border: '#2B7CE9', background: '#97C2FC', @@ -55,18 +62,38 @@ function Graph (container, data, options) { widthMax: 15, width: 1, style: 'line', - color: '#343434', + color: '#848484', fontColor: '#343434', fontSize: 14, // px fontFace: 'arial', - //distance: 100, //px - length: 100, // px dash: { length: 10, gap: 5, altLength: undefined } }, + physics: { + barnesHut: { + enabled: true, + theta: 1 / 0.6, // inverted to save time during calculation + gravitationalConstant: -2000, + centralGravity: 0.1, + springLength: 100, + springConstant: 0.05, + damping: 0.09 + }, + repulsion: { + centralGravity: 0.1, + springLength: 50, + springConstant: 0.05, + nodeDistance: 100, + damping: 0.09 + }, + damping: null, + centralGravity: null, + springLength: null, + springConstant: null + }, clustering: { // Per Node in Cluster = PNiC enabled: false, // (Boolean) | global on/off switch for clustering. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold. @@ -74,17 +101,19 @@ function Graph (container, data, options) { reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains). clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered. - sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. + sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px). - forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). - distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). - edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. - nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster. - height: 10, // (px PNiC) | growth of the height per node in cluster. - radius: 10}, // (px PNiC) | growth of the radius per node in cluster. - activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open. - massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass + maxFontSize: 1000, + forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). + distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). + edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. + nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster. + height: 1, // (px PNiC) | growth of the height per node in cluster. + radius: 1}, // (px PNiC) | growth of the radius per node in cluster. + maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster. + activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open. + clusterLevelDifference: 2 }, navigation: { enabled: false, @@ -94,102 +123,107 @@ function Graph (container, data, options) { enabled: false, speed: {x: 10, y: 10, zoom: 0.02} }, - dataManipulationToolbar: { - enabled: false + dataManipulation: { + enabled: false, + initiallyVisible: false }, - minVelocity: 2, // px/s + smoothCurves: true, + maxVelocity: 10, + minVelocity: 0.1, // px/s maxIterations: 1000 // maximum number of iteration to stabilize }; + this.editMode = this.constants.dataManipulation.initiallyVisible; // Node variables + var graph = this; this.groups = new Groups(); // object with groups this.images = new Images(); // object with images this.images.setOnloadCallback(function () { graph._redraw(); }); - // navigation variables + + // keyboard navigation variables this.xIncrement = 0; this.yIncrement = 0; this.zoomIncrement = 0; + // loading all the mixins: + // load the force calculation functions, grouped under the physics system. + this._loadPhysicsSystem(); // create a frame and canvas this._create(); - // load the sector system. (mandatory, fully integrated with Graph) this._loadSectorSystem(); - // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) this._loadClusterSystem(); - // load the selection system. (mandatory, required by Graph) this._loadSelectionSystem(); - // apply options this.setOptions(options); // other vars - var graph = this; this.freezeSimulation = false;// freeze the simulation + this.cachedFunctions = {}; + // containers for nodes and edges + this.calculationNodes = {}; + this.calculationNodeIndices = []; this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation this.nodes = {}; // object with Node objects this.edges = {}; // object with Edge objects + // position and scale variables and objects this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action this.scale = 1; // defining the global scale variable in the constructor this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out - // 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 + // datasets or dataviews 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(); + graph._addNodes(params.items); + graph.start(); }, 'update': function (event, params) { - me._updateNodes(params.items); - me.start(); + graph._updateNodes(params.items); + graph.start(); }, 'remove': function (event, params) { - me._removeNodes(params.items); - me.start(); + graph._removeNodes(params.items); + graph.start(); } }; this.edgesListeners = { 'add': function (event, params) { - me._addEdges(params.items); - me.start(); + graph._addEdges(params.items); + graph.start(); }, 'update': function (event, params) { - me._updateEdges(params.items); - me.start(); + graph._updateEdges(params.items); + graph.start(); }, 'remove': function (event, params) { - me._removeEdges(params.items); - me.start(); + graph._removeEdges(params.items); + graph.start(); } }; - // properties of the data - this.moving = false; // True if any of the nodes have an undefined position - this.timer = undefined; + // properties for the animation + this.moving = true; + this.timer = undefined; // Scheduling function. Is definded in this.start(); // load data (the disable start variable will be the same as the enabled clustering) this.setData(data,this.constants.clustering.enabled); - // zoom so all data will fit on the screen - this.zoomToFit(true); + // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here. + this.zoomToFit(true,this.constants.clustering.enabled); // if clustering is disabled, the simulation will have started in the setData function if (this.constants.clustering.enabled) { @@ -244,9 +278,8 @@ Graph.prototype._getRange = function() { * @private */ Graph.prototype._findCenter = function(range) { - var center = {x: (0.5 * (range.maxX + range.minX)), - y: (0.5 * (range.maxY + range.minY))}; - return center; + return {x: (0.5 * (range.maxX + range.minX)), + y: (0.5 * (range.maxY + range.minY))}; }; @@ -272,21 +305,33 @@ Graph.prototype._centerGraph = function(range) { * * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; */ -Graph.prototype.zoomToFit = function(initialZoom) { +Graph.prototype.zoomToFit = function(initialZoom, disableStart) { if (initialZoom === undefined) { initialZoom = false; } var numberOfNodes = this.nodeIndices.length; var range = this._getRange(); + var zoomLevel; if (initialZoom == true) { - if (this.constants.clustering.enabled == true && + if (this.constants.smoothCurves == true) { + if (this.constants.clustering.enabled == true && numberOfNodes >= this.constants.clustering.initialMaxNodes) { - var zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } + else { + zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } } else { - var zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + if (this.constants.clustering.enabled == true && + numberOfNodes >= this.constants.clustering.initialMaxNodes) { + zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } + else { + zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } } } else { @@ -306,7 +351,9 @@ Graph.prototype.zoomToFit = function(initialZoom) { this.pinch.mousewheelScale = zoomLevel; this._setScale(zoomLevel); this._centerGraph(range); - this.start(); + if (disableStart == false || disableStart === undefined) { + this.start(); + } }; @@ -368,7 +415,6 @@ Graph.prototype.setData = function(data, disableStart) { if (this.stabilize) { this._doStabilize(); } - this.moving = true; this.start(); } }; @@ -379,15 +425,53 @@ Graph.prototype.setData = function(data, disableStart) { */ Graph.prototype.setOptions = function (options) { if (options) { + var prop; // 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;} + if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;} + + if (options.onAdd) { + this.triggerFunctions.add = options.onAdd; + } + + if (options.onEdit) { + this.triggerFunctions.edit = options.onEdit; + } + + if (options.onConnect) { + this.triggerFunctions.connect = options.onConnect; + } + + if (options.onDelete) { + this.triggerFunctions.delete = options.onDelete; + } + + if (options.physics) { + if (options.physics.barnesHut) { + this.constants.physics.barnesHut.enabled = true; + for (prop in options.physics.barnesHut) { + if (options.physics.barnesHut.hasOwnProperty(prop)) { + this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop]; + } + } + } + + if (options.physics.repulsion) { + this.constants.physics.barnesHut.enabled = false; + for (prop in options.physics.repulsion) { + if (options.physics.repulsion.hasOwnProperty(prop)) { + this.constants.physics.repulsion[prop] = options.physics.repulsion[prop]; + } + } + } + } if (options.clustering) { this.constants.clustering.enabled = true; - for (var prop in options.clustering) { + for (prop in options.clustering) { if (options.clustering.hasOwnProperty(prop)) { this.constants.clustering[prop] = options.clustering[prop]; } @@ -399,7 +483,7 @@ Graph.prototype.setOptions = function (options) { if (options.navigation) { this.constants.navigation.enabled = true; - for (var prop in options.navigation) { + for (prop in options.navigation) { if (options.navigation.hasOwnProperty(prop)) { this.constants.navigation[prop] = options.navigation[prop]; } @@ -411,7 +495,7 @@ Graph.prototype.setOptions = function (options) { if (options.keyboard) { this.constants.keyboard.enabled = true; - for (var prop in options.keyboard) { + for (prop in options.keyboard) { if (options.keyboard.hasOwnProperty(prop)) { this.constants.keyboard[prop] = options.keyboard[prop]; } @@ -421,16 +505,16 @@ Graph.prototype.setOptions = function (options) { this.constants.keyboard.enabled = false; } - if (options.dataManipulationToolbar) { - this.constants.dataManipulationToolbar.enabled = true; - for (var prop in options.dataManipulationToolbar) { - if (options.dataManipulationToolbar.hasOwnProperty(prop)) { - this.constants.dataManipulationToolbar[prop] = options.dataManipulationToolbar[prop]; + if (options.dataManipulation) { + this.constants.dataManipulation.enabled = true; + for (prop in options.dataManipulation) { + if (options.dataManipulation.hasOwnProperty(prop)) { + this.constants.dataManipulation[prop] = options.dataManipulation[prop]; } } } - else if (options.dataManipulationToolbar !== undefined) { - this.constants.dataManipulationToolbar.enabled = false; + else if (options.dataManipulation !== undefined) { + this.constants.dataManipulation.enabled = false; } // TODO: work out these options and document them @@ -441,12 +525,6 @@ Graph.prototype.setOptions = function (options) { } } - 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; } @@ -493,11 +571,17 @@ Graph.prototype.setOptions = function (options) { } } + + // (Re)loading the mixins that can be enabled or disabled in the options. + // load the force calculation functions, grouped under the physics system. + this._loadPhysicsSystem(); // load the navigation system. this._loadNavigationControls(); - // load the data manipulation system this._loadManipulationSystem(); + // configure the smooth curves + this._configureSmoothCurves(); + // bind keys. If disabled, this will not do anything; this._createKeyBinds(); @@ -517,7 +601,7 @@ Graph.prototype.setOptions = function (options) { * event specific properties. */ Graph.prototype.on = function on (event, callback) { - var available = ['select']; + var available = ['select','frameResize']; if (available.indexOf(event) == -1) { throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); @@ -635,11 +719,13 @@ Graph.prototype._createKeyBinds = function() { this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); } +// this.mousetrap.bind("b",this._toggleBarnesHut.bind(me)); - if (this.constants.dataManipulationToolbar.enabled == true) { + if (this.constants.dataManipulation.enabled == true) { this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); + this.mousetrap.bind("del",this._deleteSelected.bind(me)); } -} +}; /** * Get the pointer location from a touch location @@ -672,6 +758,17 @@ Graph.prototype._onTouch = function (event) { * @private */ Graph.prototype._onDragStart = function () { + this._handleDragStart(); +}; + + +/** + * This function is called by _onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ +Graph.prototype._handleDragStart = function() { var drag = this.drag; var node = this._getNodeAt(drag.pointer); // note: drag.pointer is set in _onTouch to get the initial touch location @@ -714,11 +811,23 @@ Graph.prototype._onDragStart = function () { } }; + /** * handle drag event * @private */ Graph.prototype._onDrag = function (event) { + this._handleOnDrag(event) +}; + + +/** + * This function is called by _onDrag. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ +Graph.prototype._handleOnDrag = function(event) { if (this.drag.pinched) { return; } @@ -726,12 +835,12 @@ Graph.prototype._onDrag = function (event) { var pointer = this._getPointer(event.gesture.touches[0]); var me = this, - drag = this.drag, - selection = drag.selection; + 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; + deltaY = pointer.y - drag.pointer.y; // update position of all selected nodes selection.forEach(function (s) { @@ -746,7 +855,7 @@ Graph.prototype._onDrag = function (event) { } }); - // start animation if not yet running + // start _animationStep if not yet running if (!this.moving) { this.moving = true; this.start(); @@ -758,8 +867,8 @@ Graph.prototype._onDrag = function (event) { var diffY = pointer.y - this.drag.pointer.y; this._setTranslation( - this.drag.translation.x + diffX, - this.drag.translation.y + diffY); + this.drag.translation.x + diffX, + this.drag.translation.y + diffY); this._redraw(); this.moved = true; } @@ -817,11 +926,11 @@ Graph.prototype._onHold = function (event) { /** * handle the release of the screen * - * @param event * @private */ Graph.prototype._onRelease = function (event) { - this._handleOnRelease(); + var pointer = this._getPointer(event.gesture.touches[0]); + this._handleOnRelease(pointer); }; /** @@ -867,8 +976,6 @@ Graph.prototype._zoom = function(scale, pointer) { this.areaCenter = {"x" : this._canvasToX(pointer.x), "y" : this._canvasToY(pointer.y)}; - // this.areaCenter = {"x" : pointer.x,"y" : pointer.y }; -// console.log(translation.x,translation.y,pointer.x,pointer.y,scale); this.pinch.mousewheelScale = scale; this._setScale(scale); this._setTranslation(tx, ty); @@ -878,6 +985,7 @@ Graph.prototype._zoom = function(scale, pointer) { return scale; }; + /** * Event handler for mouse wheel event, used to zoom the timeline * See http://adomas.org/javascript-mouse-wheel/ @@ -917,7 +1025,7 @@ Graph.prototype._onMouseWheel = function(event) { var pointer = this._getPointer(gesture.center); // apply the new scale - scale = this._zoom(scale, pointer); + this._zoom(scale, pointer); // store the new, applied scale -- this is now done in _zoom // this.pinch.mousewheelScale = scale; @@ -1027,6 +1135,7 @@ Graph.prototype._checkShowPopup = function (pointer) { } }; + /** * Check if the popup must be hided, which is the case when the mouse is no * longer hovering on the object @@ -1043,85 +1152,6 @@ Graph.prototype._checkHidePopup = function (pointer) { }; -/** - * 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' @@ -1143,9 +1173,7 @@ Graph.prototype.setSize = function(width, height) { this.manipulationDiv.style.width = this.frame.canvas.clientWidth; } - if (this.constants.navigation.enabled == true) { - this._relocateNavigation(); - } + this._trigger('frameResize', {width:this.frame.canvas.width,height:this.frame.canvas.height}); }; /** @@ -1207,13 +1235,11 @@ Graph.prototype._addNodes = function(ids) { 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() && this.createNodeOnClick != true) { - // 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); + if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) { + var radius = 10 * 0.1*ids.length; + var angle = 2 * Math.PI * Math.random(); + if (node.xFixed == false) {node.x = radius * Math.cos(angle);} + if (node.yFixed == false) {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. @@ -1221,6 +1247,7 @@ Graph.prototype._addNodes = function(ids) { } } this._updateNodeIndexList(); + this._updateCalculationNodes(); this._reconnectEdges(); this._updateValueRange(this.nodes); this.updateLabels(); @@ -1345,6 +1372,8 @@ Graph.prototype._addEdges = function (ids) { this.moving = true; this._updateValueRange(edges); + this._createBezierNodes(); + this._updateCalculationNodes(); }; /** @@ -1373,6 +1402,7 @@ Graph.prototype._updateEdges = function (ids) { } } + this._createBezierNodes(); this.moving = true; this._updateValueRange(edges); }; @@ -1388,6 +1418,9 @@ Graph.prototype._removeEdges = function (ids) { var id = ids[i]; var edge = edges[id]; if (edge) { + if (edge.via != null) { + delete this.sectors['support']['nodes'][edge.via.id]; + } edge.disconnect(); delete edges[id]; } @@ -1395,6 +1428,7 @@ Graph.prototype._removeEdges = function (ids) { this.moving = true; this._updateValueRange(edges); + this._updateCalculationNodes(); }; /** @@ -1492,14 +1526,13 @@ Graph.prototype._redraw = function() { this._doInAllSectors("_drawAllSectorNodes",ctx); this._doInAllSectors("_drawEdges",ctx); - this._doInAllSectors("_drawNodes",ctx,true); + this._doInAllSectors("_drawNodes",ctx,false); + +// this._doInSupportSector("_drawNodes",ctx,true); +// this._drawTree(ctx,"#F00F0F"); // restore original scaling and translation ctx.restore(); - - if (this.constants.navigation.enabled == true) { - this._doInNavigationSector("_drawNodes",ctx,true); - } }; /** @@ -1605,7 +1638,6 @@ Graph.prototype._drawNodes = function(ctx,alwaysShow) { if (alwaysShow === undefined) { alwaysShow = false; } - // first draw the unselected nodes var nodes = this.nodes; var selected = []; @@ -1656,246 +1688,17 @@ Graph.prototype._drawEdges = function(ctx) { * @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._initializeForceCalculation(); - this._discreteStepNodes(); - stable = !this._isMoving(vmin); + while (this.moving && count < this.constants.maxIterations) { + this._physicsTick(); count++; } - this.zoomToFit(); - // var end = new Date(); - - // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup + this.zoomToFit(false,true); }; -/** - * Before calculating the forces, we check if we need to cluster to keep up performance and we check - * if there is more than one node. If it is just one node, we dont calculate anything. - * - * @private - */ -Graph.prototype._initializeForceCalculation = function() { - // stop calculation if there is only one node - if (this.nodeIndices.length == 1) { - this.nodes[this.nodeIndices[0]]._setForce(0,0); - } - else { - // if there are too many nodes on screen, we cluster without repositioning - if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { - this.clusterToFit(this.constants.clustering.reduceToNodes, false); - } - - // we now start the force calculation - this._calculateForces(); - } -}; - - -/** - * Calculate the external forces acting on the nodes - * Forces are caused by: edges, repulsing forces between nodes, gravity - * @private - */ -Graph.prototype._calculateForces = function() { -// var screenCenterPos = {"x":(0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x)), -// "y":(0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y))} - // create a local edge to the nodes and edges, that is faster - var dx, dy, angle, distance, fx, fy, - repulsingForce, springForce, length, edgeLength, - node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter; - var clusterSize; - var nodes = this.nodes; - var edges = this.edges; - - // Gravity is required to keep separated groups from floating off - // the forces are reset to zero in this loop by using _setForce instead - // of _addForce - var gravity = 0.08 * this.forceFactor; - for (i = 0; i < this.nodeIndices.length; i++) { - node = nodes[this.nodeIndices[i]]; - // gravity does not apply when we are in a pocket sector - if (this._sector() == "default") { - dx = -node.x;// + screenCenterPos.x; - dy = -node.y;// + screenCenterPos.y; - - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; - } - else { - fx = 0; - fy = 0; - } - node._setForce(fx, fy); - - node.updateDamping(this.nodeIndices.length); - } - - // repulsing forces between nodes - var minimumDistance = this.constants.nodes.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - var a_base = (-2/3); var b = 4/3; - for (i = 0; i < this.nodeIndices.length-1; i++) { - node1 = nodes[this.nodeIndices[i]]; - for (j = i+1; j < this.nodeIndices.length; j++) { - node2 = nodes[this.nodeIndices[j]]; - clusterSize = (node1.clusterSize + node2.clusterSize - 2); - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - - // clusters have a larger region of influence - minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); - var a = a_base / minimumDistance; - if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 - angle = Math.atan2(dy, dx); - - if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 - repulsingForce = 1.0; - } - else { - // 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 - repulsingForce = a * distance + b; // TODO: test the approximation of the function above - } - // amplify the repulsion for clusters. - repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; - repulsingForce *= this.forceFactor; - - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce ; - - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); - } - } - } - -/* - // repulsion of the edges on the nodes and - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - node = nodes[nodeId]; - for(var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - - // get the center of the edge - xCenter = edge.from.x+(edge.to.x - edge.from.x)/2; - yCenter = edge.from.y+(edge.to.y - edge.from.y)/2; - - // calculate normally distributed force - dx = node.x - xCenter; - dy = node.y - yCenter; - distance = Math.sqrt(dx * dx + dy * dy); - if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 - angle = Math.atan2(dy, dx); - - if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 - repulsingForce = 1.0; - } - else { - // 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; - node._addForce(fx, fy); - edge.from._addForce(-fx/2,-fy/2); - edge.to._addForce(-fx/2,-fy/2); - } - } - } - } - } -*/ - - // forces caused by the edges, modelled as springs - for (edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - if (edge.connected) { - // only calculate forces if nodes are in the same sector - if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { - clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); - 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; - // this implies that the edges between big clusters are longer - edgeLength += clusterSize * this.constants.clustering.edgeGrowth; - length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); - - springForce = edge.stiffness * (edgeLength - length) * this.forceFactor; - - 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); - } - } -*/ -}; /** @@ -1905,10 +1708,9 @@ Graph.prototype._calculateForces = function() { * @private */ Graph.prototype._isMoving = function(vmin) { - var vminCorrected = vmin / this.scale; var nodes = this.nodes; for (var id in nodes) { - if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) { + if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { return true; } } @@ -1920,232 +1722,188 @@ Graph.prototype._isMoving = function(vmin) { * /** * Perform one discrete step for all nodes * - * @param interval * @private */ Graph.prototype._discreteStepNodes = function() { - var interval = 0.01; + var interval = 0.75; var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].discreteStep(interval); + var nodeId; + + if (this.constants.maxVelocity > 0) { + for (nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity); + } } } - - var vmin = this.constants.minVelocity; - this.moving = this._isMoving(vmin); + else { + for (nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + nodes[nodeId].discreteStep(interval); + } + } + } + var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05); + if (vminCorrected > 0.5*this.constants.maxVelocity) { + this.moving = true; + } + else { + this.moving = this._isMoving(vminCorrected); + } }; - -/** - * Start animating nodes and edges - * - * @poram {Boolean} runCalculationStep - */ -Graph.prototype.start = function() { +Graph.prototype._physicsTick = function() { if (!this.freezeSimulation) { - if (this.moving) { this._doInAllActiveSectors("_initializeForceCalculation"); + if (this.constants.smoothCurves) { + this._doInSupportSector("_discreteStepNodes"); + } this._doInAllActiveSectors("_discreteStepNodes"); this._findCenter(this._getRange()) } - - if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { - // start animation. only start calculationTimer if it is not already running - if (!this.timer) { - var graph = this; - this.timer = window.setTimeout(function () { - graph.timer = undefined; - - // keyboad movement - if (graph.xIncrement != 0 || graph.yIncrement != 0) { - var translation = graph._getTranslation(); - graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement); - } - if (graph.zoomIncrement != 0) { - var center = { - x: graph.frame.canvas.clientWidth / 2, - y: graph.frame.canvas.clientHeight / 2 - }; - graph._zoom(graph.scale*(1 + graph.zoomIncrement), center); - } - - graph.start(); - graph._redraw(); - - //this.end = window.performance.now(); - //this.time = this.end - this.startTime; - //console.log('refresh time: ' + this.time); - //this.startTime = window.performance.now(); - - }, this.renderTimestep); - } - } - else { - this._redraw(); - } } }; + /** - * Debug function, does one step of the graph + * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick. + * It reschedules itself at the beginning of the function + * + * @private */ -Graph.prototype.singleStep = function() { - if (this.moving) { - this._initializeForceCalculation(); - this._discreteStepNodes(); +Graph.prototype._animationStep = function() { + // reset the timer so a new scheduled animation step can be set + this.timer = undefined; - var vmin = this.constants.minVelocity; - this.moving = this._isMoving(vmin); - this._redraw(); - } -}; + // handle the keyboad movement + this._handleNavigation(); + // this schedules a new animation step + this.start(); + // start the physics simulation + var calculationTime = Date.now(); + var maxSteps = 1; + this._physicsTick(); + var timeRequired = Date.now() - calculationTime; + while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxRenderSteps) { + this._physicsTick(); + timeRequired = Date.now() - calculationTime; + maxSteps++; -/** - * Freeze the animation - */ -Graph.prototype.toggleFreeze = function() { - if (this.freezeSimulation == false) { - this.freezeSimulation = true; - } - else { - this.freezeSimulation = false; - this.start(); } + + // start the rendering process + var renderTime = Date.now(); + this._redraw(); + this.renderTime = Date.now() - renderTime; }; + /** - * Mixin the cluster system and initialize the parameters required. + * Schedule a animation step with the refreshrate interval. * - * @private + * @poram {Boolean} runCalculationStep */ -Graph.prototype._loadClusterSystem = function() { - this.clusterSession = 0; - this.hubThreshold = 5; - - for (var mixinFunction in ClusterMixin) { - if (ClusterMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction]; +Graph.prototype.start = function() { + if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { + if (!this.timer) { + this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function } } -} + else { + this._redraw(); + } +}; + /** - * Mixin the sector system and initialize the parameters required + * Move the graph according to the keyboard presses. * * @private */ -Graph.prototype._loadSectorSystem = function() { - this.sectors = {}; - this.activeSector = ["default"]; - this.sectors["active"] = {}; - this.sectors["active"]["default"] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": 1.0, - "drawingNode": undefined}; - this.sectors["frozen"] = {}; - this.sectors["navigation"] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": 1.0, - "drawingNode": undefined}; - - this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields - for (var mixinFunction in SectorMixin) { - if (SectorMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = SectorMixin[mixinFunction]; - } +Graph.prototype._handleNavigation = function() { + if (this.xIncrement != 0 || this.yIncrement != 0) { + var translation = this._getTranslation(); + this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement); + } + if (this.zoomIncrement != 0) { + var center = { + x: this.frame.canvas.clientWidth / 2, + y: this.frame.canvas.clientHeight / 2 + }; + this._zoom(this.scale*(1 + this.zoomIncrement), center); } }; /** - * Mixin the selection system and initialize the parameters required - * - * @private + * Freeze the _animationStep */ -Graph.prototype._loadSelectionSystem = function() { - this.selectionObj = {}; - - for (var mixinFunction in SelectionMixin) { - if (SelectionMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = SelectionMixin[mixinFunction]; - } +Graph.prototype.toggleFreeze = function() { + if (this.freezeSimulation == false) { + this.freezeSimulation = true; } -} + else { + this.freezeSimulation = false; + this.start(); + } +}; -/** - * Mixin the navigationUI (User Interface) system and initialize the parameters required - * - * @private - */ -Graph.prototype._loadManipulationSystem = function() { - // reset global variables -- these are used by the selection of nodes and edges. - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false - - - if (this.constants.dataManipulationToolbar.enabled == true) { - // load the manipulator HTML elements. All styling done in css. - if (this.manipulationDiv === undefined) { - this.manipulationDiv = document.createElement('div'); - this.manipulationDiv.className = 'graph-manipulationDiv'; - this.containerElement.insertBefore(this.manipulationDiv, this.frame); - } - // load the manipulation functions - for (var mixinFunction in manipulationMixin) { - if (manipulationMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = manipulationMixin[mixinFunction]; +Graph.prototype._configureSmoothCurves = function() { + if (this.constants.smoothCurves == true) { + this._createBezierNodes(); + } + else { + // delete the support nodes + this.sectors['support']['nodes'] = {}; + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + this.edges[edgeId].smooth = false; + this.edges[edgeId].via = null; } } - - // create the manipulator toolbar - this._createManipulatorBar(); } -} + this._updateCalculationNodes(); + this.moving = true; + this.start(); +}; -/** - * Mixin the navigation (User Interface) system and initialize the parameters required - * - * @private - */ -Graph.prototype._loadNavigationControls = function() { - for (var mixinFunction in NavigationMixin) { - if (NavigationMixin.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction]; +Graph.prototype._createBezierNodes = function() { + if (this.constants.smoothCurves == true) { + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + var edge = this.edges[edgeId]; + if (edge.via == null) { + edge.smooth = true; + var nodeId = "edgeId:".concat(edge.id); + this.sectors['support']['nodes'][nodeId] = new Node( + {id:nodeId, + mass:1, + shape:'circle', + internalMultiplier:1 + },{},{},this.constants); + edge.via = this.sectors['support']['nodes'][nodeId]; + edge.via.parentEdgeId = edge.id; + edge.positionBezierNode(); + } + } } } - - if (this.constants.navigation.enabled == true) { - this._loadNavigationElements(); - } -} - -/** - * this function exists to avoid errors when not loading the navigation system - */ -Graph.prototype._relocateNavigation = function() { - // empty, is overloaded by navigation system -} - -/** - * * this function exists to avoid errors when not loading the navigation system - */ -Graph.prototype._unHighlightAll = function() { - // empty, is overloaded by the navigation system -} - - - +}; +Graph.prototype._initializeMixinLoaders = function () { + for (var mixinFunction in graphMixinLoaders) { + if (graphMixinLoaders.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction]; + } + } +}; diff --git a/src/graph/NavigationMixin.js b/src/graph/NavigationMixin.js deleted file mode 100644 index 20540622..00000000 --- a/src/graph/NavigationMixin.js +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Created by Alex on 1/22/14. - */ - -var NavigationMixin = { - - /** - * This function moves the navigation controls if the canvas size has been changed. If the arugments - * verticaAlignTop and horizontalAlignLeft are false, the correction will be made - * - * @private - */ - _relocateNavigation : function() { - if (this.sectors !== undefined) { - var xOffset = this.navigationClientWidth - this.frame.canvas.clientWidth; - var yOffset = this.navigationClientHeight - this.frame.canvas.clientHeight; - this.navigationClientWidth = this.frame.canvas.clientWidth; - this.navigationClientHeight = this.frame.canvas.clientHeight; - var node = null; - - for (var nodeId in this.sectors["navigation"]["nodes"]) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(nodeId)) { - node = this.sectors["navigation"]["nodes"][nodeId]; - if (!node.horizontalAlignLeft) { - node.x -= xOffset; - } - if (!node.verticalAlignTop) { - node.y -= yOffset; - } - } - } - } - }, - - - /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. - * - * @private - */ - _loadNavigationElements : function() { - var DIR = this.constants.navigation.iconPath; - this.navigationClientWidth = this.frame.canvas.clientWidth; - this.navigationClientHeight = this.frame.canvas.clientHeight; - if (this.navigationClientWidth === undefined) { - this.navigationClientWidth = 0; - this.navigationClientHeight = 0; - } - var offset = 15; - var intermediateOffset = 7; - var navigationNodes = [ - {id: 'navigation_up', shape: 'image', image: DIR + '/uparrow.png', triggerFunction: "_moveUp", - verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 45 - offset - intermediateOffset}, - {id: 'navigation_down', shape: 'image', image: DIR + '/downarrow.png', triggerFunction: "_moveDown", - verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_left', shape: 'image', image: DIR + '/leftarrow.png', triggerFunction: "_moveLeft", - verticalAlignTop: false, x: 15 + offset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_right', shape: 'image', image: DIR + '/rightarrow.png',triggerFunction: "_moveRight", - verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - - {id: 'navigation_plus', shape: 'image', image: DIR + '/plus.png', triggerFunction: "_zoomIn", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 45 - offset - intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_min', shape: 'image', image: DIR + '/minus.png', triggerFunction: "_zoomOut", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_zoomExtends', shape: 'image', image: DIR + '/zoomExtends.png', triggerFunction: "zoomToFit", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 45 - offset - intermediateOffset} - ]; - - var nodeObj = null; - for (var i = 0; i < navigationNodes.length; i++) { - nodeObj = this.sectors["navigation"]['nodes']; - nodeObj[navigationNodes[i]['id']] = new Node(navigationNodes[i], this.images, this.groups, this.constants); - } - }, - - - /** - * By setting the clustersize to be larger than 1, we use the clustering drawing method - * to illustrate the buttons are presed. We call this highlighting. - * - * @param {String} elementId - * @private - */ - _highlightNavigationElement : function(elementId) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { - this.sectors["navigation"]["nodes"][elementId].clusterSize = 2; - } - }, - - - /** - * Reverting back to a normal button - * - * @param {String} elementId - * @private - */ - _unHighlightNavigationElement : function(elementId) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { - this.sectors["navigation"]["nodes"][elementId].clusterSize = 1; - } - }, - - /** - * un-highlight (for lack of a better term) all navigation controls elements - * @private - */ - _unHighlightAll : function() { - for (var nodeId in this.sectors['navigation']['nodes']) { - if (this.sectors['navigation']['nodes'].hasOwnProperty(nodeId)) { - this._unHighlightNavigationElement(nodeId); - } - } - }, - - - _preventDefault : function(event) { - if (event !== undefined) { - if (event.preventDefault) { - event.preventDefault(); - } else { - event.returnValue = false; - } - } - }, - - - /** - * move the screen up - * By using the increments, instead of adding a fixed number to the translation, we keep fluent and - * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently - * To avoid this behaviour, we do the translation in the start loop. - * - * @private - */ - _moveUp : function(event) { - this._highlightNavigationElement("navigation_up"); - this.yIncrement = this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * move the screen down - * @private - */ - _moveDown : function(event) { - this._highlightNavigationElement("navigation_down"); - this.yIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * move the screen left - * @private - */ - _moveLeft : function(event) { - this._highlightNavigationElement("navigation_left"); - this.xIncrement = this.constants.keyboard.speed.x; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * move the screen right - * @private - */ - _moveRight : function(event) { - this._highlightNavigationElement("navigation_right"); - this.xIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * Zoom in, using the same method as the movement. - * @private - */ - _zoomIn : function(event) { - this._highlightNavigationElement("navigation_plus"); - this.zoomIncrement = this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * Zoom out - * @private - */ - _zoomOut : function() { - this._highlightNavigationElement("navigation_min"); - this.zoomIncrement = -this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - }, - - - /** - * Stop zooming and unhighlight the zoom controls - * @private - */ - _stopZoom : function() { - this._unHighlightNavigationElement("navigation_plus"); - this._unHighlightNavigationElement("navigation_min"); - - this.zoomIncrement = 0; - }, - - - /** - * Stop moving in the Y direction and unHighlight the up and down - * @private - */ - _yStopMoving : function() { - this._unHighlightNavigationElement("navigation_up"); - this._unHighlightNavigationElement("navigation_down"); - - this.yIncrement = 0; - }, - - - /** - * Stop moving in the X direction and unHighlight left and right. - * @private - */ - _xStopMoving : function() { - this._unHighlightNavigationElement("navigation_left"); - this._unHighlightNavigationElement("navigation_right"); - - this.xIncrement = 0; - } - - -}; diff --git a/src/graph/Node.js b/src/graph/Node.js index 43f34f1e..83c372fb 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -21,6 +21,7 @@ * 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; @@ -33,6 +34,7 @@ function Node(properties, imagelist, grouplist, constants) { this.fontSize = constants.nodes.fontSize; this.fontFace = constants.nodes.fontFace; this.fontColor = constants.nodes.fontColor; + this.fontDrawThreshold = 3; this.color = constants.nodes.color; @@ -53,9 +55,17 @@ function Node(properties, imagelist, grouplist, constants) { this.radiusMax = constants.nodes.radiusMax; this.imagelist = imagelist; - this.grouplist = grouplist; + // physics properties + 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 = constants.physics.damping; + this.mass = 1; // kg + this.setProperties(properties, constants); // creating the variables for clustering @@ -65,20 +75,15 @@ function Node(properties, imagelist, grouplist, constants) { this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width; this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height; this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius; + this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements; + this.growthIndicator = 0; - // mass, force, velocity - this.mass = 1; // 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; - this.dampingFactor = 75; - + // variables to tell the node about the graph. this.graphScaleInv = 1; + this.graphScale = 1; this.canvasTopLeft = {"x": -300, "y": -300}; this.canvasBottomRight = {"x": 300, "y": 300}; + this.parentEdgeId = null; } /** @@ -105,7 +110,6 @@ Node.prototype.attachEdge = function(edge) { this.dynamicEdges.push(edge); } this.dynamicEdgesLength = this.dynamicEdges.length; - this._updateMass(); }; /** @@ -119,17 +123,8 @@ Node.prototype.detachEdge = function(edge) { this.dynamicEdges.splice(index, 1); } this.dynamicEdgesLength = this.dynamicEdges.length; - 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 = 1 + 0.6 * this.edges.length; // kg -}; /** * Set or overwrite properties for the node @@ -150,6 +145,12 @@ Node.prototype.setProperties = function(properties, constants) { if (properties.y !== undefined) {this.y = properties.y;} if (properties.value !== undefined) {this.value = properties.value;} + + // physics + if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.internalMultiplier;} + if (properties.damping !== undefined) {this.dampingBase = properties.damping;} + if (properties.mass !== undefined) {this.mass = properties.mass;} + // navigation controls properties if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} @@ -226,15 +227,32 @@ Node.prototype.setProperties = function(properties, constants) { Node.parseColor = function(color) { var c; if (util.isString(color)) { - c = { - border: color, - background: color, - highlight: { + if (util.isValidHex(color)) { + var hsv = util.hexToHSV(color); + var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)}; + var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6}; + var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v); + var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v); + + c = { border: color, - background: color - } - }; - // TODO: automatically generate a nice highlight color + border:darkerColorHex, + highlight: { + background:lighterColorHex, + border:darkerColorHex + } + }; + } + else { + c = { + border:color, + border:color, + highlight: { + background:color, + border:color + } + }; + } } else { c = {}; @@ -312,7 +330,6 @@ Node.prototype.distanceToBorder = function (ctx, angle) { this.resize(ctx); } - //noinspection FallthroughInSwitchStatementJS switch (this.shape) { case 'circle': case 'dot': @@ -344,7 +361,6 @@ Node.prototype.distanceToBorder = function (ctx, angle) { } } - // TODO: implement calculation of distance to border for all shapes }; @@ -375,21 +391,44 @@ Node.prototype._addForce = function(fx, fy) { */ Node.prototype.discreteStep = function(interval) { if (!this.xFixed) { - var dx = -this.damping * this.vx; // damping force - var ax = (this.fx + dx) / this.mass; // acceleration + 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 + 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 } }; + +/** + * Perform one discrete step for the node + * @param {number} interval Time interval in seconds + */ +Node.prototype.discreteStepLimited = function(interval, maxVelocity) { + 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.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx; + 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.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy; + 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 @@ -405,16 +444,7 @@ Node.prototype.isFixed = function() { */ // TODO: replace this method with calculating the kinetic energy Node.prototype.isMoving = function(vmin) { - - if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) { -// console.log(vmin,this.vx,this.vy); - return true; - } - else { - this.vx = 0; this.vy = 0; - return false; - } - //return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin); + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin); }; /** @@ -519,10 +549,12 @@ Node.prototype._resizeImage = function (ctx) { this.width = width; this.height = height; + this.growthIndicator = 0; if (this.width > 0 && this.height > 0) { - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; + this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; + this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; + this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; + this.growthIndicator = this.width - width; } } @@ -567,9 +599,11 @@ Node.prototype._resizeBox = function (ctx) { this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; - this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; -// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; + this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; + this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; + this.growthIndicator = this.width - (textSize.width + 2 * margin); +// this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; + } }; @@ -616,9 +650,10 @@ Node.prototype._resizeDatabase = function (ctx) { this.height = size; // scaling used for clustering - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; + this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; + this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; + this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; + this.growthIndicator = this.width - size; } }; @@ -665,9 +700,10 @@ Node.prototype._resizeCircle = function (ctx) { this.height = diameter; // scaling used for clustering -// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; -// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; +// this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; +// this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; + this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; + this.growthIndicator = this.radius - 0.5*diameter; } }; @@ -711,11 +747,13 @@ Node.prototype._resizeEllipse = function (ctx) { if (this.width < this.height) { this.width = this.height; } + var defaultSize = this.width; - // scaling used for clustering - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; + // scaling used for clustering + this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; + this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; + this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; + this.growthIndicator = this.width - defaultSize; } }; @@ -778,9 +816,10 @@ Node.prototype._resizeShape = function (ctx) { this.height = size; // scaling used for clustering - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; + this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; + this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; + this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; + this.growthIndicator = this.width - size; } }; @@ -837,9 +876,10 @@ Node.prototype._resizeText = function (ctx) { this.height = textSize.height + 2 * margin; // scaling used for clustering - this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; - this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; - this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; + this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; + this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; + this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; + this.growthIndicator = this.width - (textSize.width + 2 * margin); } }; @@ -853,7 +893,7 @@ Node.prototype._drawText = function (ctx) { Node.prototype._label = function (ctx, text, x, y, align, baseline) { - if (text) { + if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) { ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; ctx.fillStyle = this.fontColor || "black"; ctx.textAlign = align || "center"; @@ -907,7 +947,7 @@ Node.prototype.inArea = function() { else { return true; } -} +}; /** * checks if the core of the node is in the display area, this is used for opening clusters around zoom @@ -918,7 +958,7 @@ Node.prototype.inView = function() { this.x < this.canvasBottomRight.x && this.y >= this.canvasTopLeft.y && this.y < this.canvasBottomRight.y); -} +}; /** * This allows the zoom level of the graph to influence the rendering @@ -930,6 +970,7 @@ Node.prototype.inView = function() { */ Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { this.graphScaleInv = 1.0/scale; + this.graphScale = scale; this.canvasTopLeft = canvasTopLeft; this.canvasBottomRight = canvasBottomRight; }; @@ -942,17 +983,9 @@ Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) */ Node.prototype.setScale = function(scale) { this.graphScaleInv = 1.0/scale; + this.graphScale = scale; }; -/** - * This function updates the damping parameter for clusters, based ont he - * - * @param {Number} numberOfNodes - */ -Node.prototype.updateDamping = function(numberOfNodes) { - this.damping = (0.8 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2))); - this.damping *= this.dampingFactor; -}; /** @@ -971,8 +1004,10 @@ Node.prototype.clearVelocity = function() { */ Node.prototype.updateVelocity = function(massBeforeClustering) { var energyBefore = this.vx * this.vx * massBeforeClustering; + //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass); this.vx = Math.sqrt(energyBefore/this.mass); energyBefore = this.vy * this.vy * massBeforeClustering; + //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass); this.vy = Math.sqrt(energyBefore/this.mass); }; diff --git a/src/graph/css/graph-manipulation.css b/src/graph/css/graph-manipulation.css new file mode 100644 index 00000000..6d0b41ce --- /dev/null +++ b/src/graph/css/graph-manipulation.css @@ -0,0 +1,128 @@ +div.graph-manipulationDiv { + border-width:0px; + border-bottom: 1px; + border-style:solid; + border-color: #d6d9d8; + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */ + background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */ + + width: 600px; + height:30px; + z-index:10; + position:absolute; +} + +div.graph-manipulation-editMode { + height:30px; + z-index:10; + position:absolute; + margin-top:20px; +} + +div.graph-manipulation-closeDiv { + height:30px; + width:30px; + z-index:11; + position:absolute; + margin-top:3px; + margin-left:590px; + background-position: 0px 0px; + background-repeat:no-repeat; + background-image: url("../../dist/img/cross.png"); + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +span.graph-manipulationUI { + font-family: verdana; + font-size: 12px; + -moz-border-radius: 15px; + border-radius: 15px; + display:inline-block; + background-position: 0px 0px; + background-repeat:no-repeat; + height:24px; + margin: -14px 0px 0px 10px; + vertical-align:middle; + cursor: pointer; + padding: 0px 8px 0px 8px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +span.graph-manipulationUI:hover { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20); +} + +span.graph-manipulationUI:active { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50); +} + +span.graph-manipulationUI.back { + background-image: url("../../dist/img/backIcon.png"); +} + +span.graph-manipulationUI.none:hover { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); + cursor: default; +} +span.graph-manipulationUI.none:active { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); +} +span.graph-manipulationUI.none { + padding: 0px 0px 0px 0px; +} +span.graph-manipulationUI.notification{ + margin: 2px; + font-weight: bold; +} + +span.graph-manipulationUI.add { + background-image: url("../../dist/img/addNodeIcon.png"); +} + +span.graph-manipulationUI.edit { + background-image: url("../../dist/img/editIcon.png"); +} + +span.graph-manipulationUI.edit.editmode { + background-color: #fcfcfc; + border-style:solid; + border-width:1px; + border-color: #cccccc; +} + +span.graph-manipulationUI.connect { + background-image: url("../../dist/img/connectIcon.png"); +} + +span.graph-manipulationUI.delete { + background-image: url("../../dist/img/deleteIcon.png"); +} +/* top right bottom left */ +span.graph-manipulationLabel { + margin: 0px 0px 0px 23px; + line-height: 25px; +} +div.graph-seperatorLine { + display:inline-block; + width:1px; + height:20px; + background-color: #bdbdbd; + margin: 5px 7px 0px 15px; +} \ No newline at end of file diff --git a/src/graph/css/graph-navigation.css b/src/graph/css/graph-navigation.css new file mode 100644 index 00000000..f72c496a --- /dev/null +++ b/src/graph/css/graph-navigation.css @@ -0,0 +1,62 @@ +div.graph-navigation { + width:34px; + height:34px; + z-index:10; + -moz-border-radius: 17px; + border-radius: 17px; + position:absolute; + display:inline-block; + background-position: 2px 2px; + background-repeat:no-repeat; + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.graph-navigation:hover { + box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30); +} + +div.graph-navigation:active { + box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95); +} + +div.graph-navigation.up { + background-image: url("../../dist/img/upArrow.png"); + margin-top:520px; + margin-left:55px; +} +div.graph-navigation.down { + background-image: url("../../dist/img/downArrow.png"); + margin-top:560px; + margin-left:55px; +} +div.graph-navigation.left { + background-image: url("../../dist/img/leftArrow.png"); + margin-top:560px; + margin-left:15px; +} +div.graph-navigation.right { + background-image: url("../../dist/img/rightArrow.png"); + margin-top:560px; + margin-left:95px; +} +div.graph-navigation.zoomIn { + background-image: url("../../dist/img/plus.png"); + margin-top:560px; + margin-left:555px; +} +div.graph-navigation.zoomOut { + background-image: url("../../dist/img/minus.png"); + margin-top:560px; + margin-left:515px; +} +div.graph-navigation.zoomExtends { + background-image: url("../../dist/img/zoomExtends.png"); + margin-top:520px; + margin-left:555px; +} \ No newline at end of file diff --git a/src/graph/ClusterMixin.js b/src/graph/graphMixins/ClusterMixin.js similarity index 84% rename from src/graph/ClusterMixin.js rename to src/graph/graphMixins/ClusterMixin.js index 4cf7a6e6..e0de527a 100644 --- a/src/graph/ClusterMixin.js +++ b/src/graph/graphMixins/ClusterMixin.js @@ -8,23 +8,24 @@ */ var ClusterMixin = { -/** - * This is only called in the constructor of the graph object - * */ + /** + * This is only called in the constructor of the graph object + * + */ startWithClustering : function() { - // cluster if the data set is big - this.clusterToFit(this.constants.clustering.initialMaxNodes, true); + // cluster if the data set is big + this.clusterToFit(this.constants.clustering.initialMaxNodes, true); - // updates the lables after clustering - this.updateLabels(); + // updates the lables after clustering + this.updateLabels(); - // this is called here because if clusterin is disabled, the start and stabilize are called in - // the setData function. - if (this.stabilize) { - this._doStabilize(); - } - this.start(); - }, + // this is called here because if clusterin is disabled, the start and stabilize are called in + // the setData function. + if (this.stabilize) { + this._doStabilize(); + } + this.start(); + }, /** * This function clusters until the initialMaxNodes has been reached @@ -41,20 +42,23 @@ var ClusterMixin = { // we first cluster the hubs, then we pull in the outliers, repeat while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { if (level % 3 == 0) { - this.forceAggregateHubs(); + this.forceAggregateHubs(true); + this.normalizeClusterLevels(); } else { - this.increaseClusterLevel(); + this.increaseClusterLevel(); // this also includes a cluster normalization } + numberOfNodes = this.nodeIndices.length; level += 1; } // after the clustering we reposition the nodes to reduce the initial chaos - if (level > 1 && reposition == true) { + if (level > 0 && reposition == true) { this.repositionNodes(); } - }, + this._updateCalculationNodes(); + }, /** * This function can be called to open up a specific cluster. It is only called by @@ -66,12 +70,16 @@ var ClusterMixin = { var isMovingBeforeClustering = this.moving; if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && !(this._sector() == "default" && this.nodeIndices.length == 1)) { + // this loads a new sector, loads the nodes and edges and nodeIndices of it. this._addSector(node); var level = 0; + + // we decluster until we reach a decent number of nodes while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { this.decreaseClusterLevel(); level += 1; } + } else { this._expandClusterNode(node,false,true); @@ -79,6 +87,7 @@ var ClusterMixin = { // update the index list, dynamic edges and labels this._updateNodeIndexList(); this._updateDynamicEdges(); + this._updateCalculationNodes(); this.updateLabels(); } @@ -86,7 +95,8 @@ var ClusterMixin = { if (this.moving != isMovingBeforeClustering) { this.start(); } - }, + }, + /** * This calls the updateClustes with default arguments @@ -97,6 +107,7 @@ var ClusterMixin = { } }, + /** * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will * be clustered with their connected node. This can be repeated as many times as needed. @@ -104,8 +115,7 @@ var ClusterMixin = { */ increaseClusterLevel : function() { this.updateClusters(-1,false,true); - }, - + }, /** @@ -115,7 +125,7 @@ var ClusterMixin = { */ decreaseClusterLevel : function() { this.updateClusters(1,false,true); - }, + }, /** @@ -129,7 +139,7 @@ var ClusterMixin = { * @param {Boolean} force | enabled or disable forcing * */ - updateClusters : function(zoomDirection,recursive,force) { + updateClusters : function(zoomDirection,recursive,force,doNotStart) { var isMovingBeforeClustering = this.moving; var amountOfNodes = this.nodeIndices.length; @@ -178,13 +188,19 @@ var ClusterMixin = { // if a cluster was formed, we increase the clusterSession if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place this.clusterSession += 1; + // if clusters have been made, we normalize the cluster level + this.normalizeClusterLevels(); } - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); + if (doNotStart == false || doNotStart === undefined) { + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } } - }, + + this._updateCalculationNodes(); + }, /** * This function handles the chains. It is called on every updateClusters(). @@ -196,7 +212,7 @@ var ClusterMixin = { this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage) } - }, + }, /** * this functions starts clustering by hubs @@ -207,14 +223,14 @@ var ClusterMixin = { _aggregateHubs : function(force) { this._getHubSize(); this._formClustersByHub(force,false); - }, + }, /** * This function is fired by keypress. It forces hubs to form. * */ - forceAggregateHubs : function() { + forceAggregateHubs : function(doNotStart) { var isMovingBeforeClustering = this.moving; var amountOfNodes = this.nodeIndices.length; @@ -230,11 +246,13 @@ var ClusterMixin = { this.clusterSession += 1; } - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); + if (doNotStart == false || doNotStart === undefined) { + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } } - }, + }, /** * If a cluster takes up more than a set percentage of the screen, open the cluster @@ -253,7 +271,7 @@ var ClusterMixin = { } } } - }, + }, /** @@ -266,8 +284,9 @@ var ClusterMixin = { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; this._expandClusterNode(node,recursive,force); + this._updateCalculationNodes(); } - }, + }, /** * This function checks if a node has to be opened. This is done by checking the zoom level. @@ -313,7 +332,7 @@ var ClusterMixin = { } } } - }, + }, /** * ONLY CALLED FROM _expandClusterNode @@ -330,14 +349,13 @@ var ClusterMixin = { * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released * @private */ - _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) { + _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) { var childNode = parentNode.containedNodes[containedNodeId]; // if child node has been added on smaller scale than current, kick out if (childNode.formationScale < this.scale || force == true) { - // remove the selection, first remove the selection from the connected edges - this._unselectConnectedEdges(parentNode); - parentNode.unselect(); + // unselect all selected items + this._unselectAll(); // put the child node back in the global nodes object this.nodes[containedNodeId] = childNode; @@ -352,14 +370,14 @@ var ClusterMixin = { this._validateEdges(parentNode); // undo the changes from the clustering operation on the parent node - parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass; - parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; + parentNode.mass -= childNode.mass; parentNode.clusterSize -= childNode.clusterSize; + parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; // place the child node near the parent, not at the exact same location to avoid chaos in the system - childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; - childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; + childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()); + childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()); // remove node from the list delete parentNode.containedNodes[containedNodeId]; @@ -379,24 +397,37 @@ var ClusterMixin = { parentNode.clusterSessions.pop(); } + this._repositionBezierNodes(childNode); +// this._repositionBezierNodes(parentNode); + // remove the clusterSession from the child node childNode.clusterSession = 0; - // restart the simulation to reorganise all nodes - this.moving = true; - // recalculate the size of the node on the next time the node is rendered parentNode.clearSizeCache(); - // this unselects the rest of the edges - this._unselectConnectedEdges(parentNode); + // restart the simulation to reorganise all nodes + this.moving = true; } // check if a further expansion step is possible if recursivity is enabled if (recursive == true) { this._expandClusterNode(childNode,recursive,force,openAll); } - }, + }, + + + /** + * position the bezier nodes at the center of the edges + * + * @param node + * @private + */ + _repositionBezierNodes : function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + node.dynamicEdges[i].positionBezierNode(); + } + }, /** @@ -415,7 +446,8 @@ var ClusterMixin = { else { this._forceClustersByZoom(); } - }, + }, + /** * This function handles the clustering by zooming out, this is based on a minimum edge distance @@ -458,7 +490,7 @@ var ClusterMixin = { } } } - }, + }, /** * This function forces the graph to cluster all nodes with only one connecting edge to their @@ -489,8 +521,41 @@ var ClusterMixin = { } } } - }, + }, + + + /** + * To keep the nodes of roughly equal size we normalize the cluster levels. + * This function clusters a node to its smallest connected neighbour. + * + * @param node + * @private + */ + _clusterToSmallestNeighbour : function(node) { + var smallestNeighbour = -1; + var smallestNeighbourNode = null; + for (var i = 0; i < node.dynamicEdges.length; i++) { + if (node.dynamicEdges[i] !== undefined) { + var neighbour = null; + if (node.dynamicEdges[i].fromId != node.id) { + neighbour = node.dynamicEdges[i].from; + } + else if (node.dynamicEdges[i].toId != node.id) { + neighbour = node.dynamicEdges[i].to; + } + + + if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) { + smallestNeighbour = neighbour.clusterSessions.length; + smallestNeighbourNode = neighbour; + } + } + } + if (neighbour != null && this.nodes[neighbour.id] !== undefined) { + this._addToCluster(neighbour, node, true); + } + }, /** @@ -508,7 +573,7 @@ var ClusterMixin = { this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); } } - }, + }, /** * This function forms a cluster from a specific preselected hub node @@ -578,7 +643,7 @@ var ClusterMixin = { } } } - }, + }, @@ -617,9 +682,9 @@ var ClusterMixin = { // update the properties of the child and parent var massBefore = parentNode.mass; childNode.clusterSession = this.clusterSession; - parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass; + parentNode.mass += childNode.mass; parentNode.clusterSize += childNode.clusterSize; - parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; + parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); // keep track of the clustersessions so we can open the cluster up as it has been formed. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { @@ -649,12 +714,12 @@ var ClusterMixin = { // restart the simulation to reorganise all nodes this.moving = true; - }, + }, /** * This function will apply the changes made to the remainingEdges during the formation of the clusters. - * This is a seperate function to allow for level-wise collapsing of the node tree. + * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree. * It has to be called if a level is collapsed. It is called by _formClusters(). * @private */ @@ -679,7 +744,7 @@ var ClusterMixin = { } node.dynamicEdgesLength -= correction; } - }, + }, /** @@ -708,7 +773,7 @@ var ClusterMixin = { break; } } - }, + }, /** * This function connects an edge that was connected to a child node to the parent node. @@ -739,9 +804,17 @@ var ClusterMixin = { this._addToReroutedEdges(parentNode,childNode,edge); } - }, + }, + /** + * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain + * these edges inside of the cluster. + * + * @param parentNode + * @param childNode + * @private + */ _containCircularEdgesFromNode : function(parentNode, childNode) { // manage all the edges connected to the child and parent nodes for (var i = 0; i < parentNode.dynamicEdges.length; i++) { @@ -812,7 +885,7 @@ var ClusterMixin = { // remove the entry from the rerouted edges delete parentNode.reroutedEdges[childNode.id]; } - }, + }, /** @@ -830,7 +903,7 @@ var ClusterMixin = { parentNode.dynamicEdges.splice(i,1); } } - }, + }, /** @@ -855,7 +928,7 @@ var ClusterMixin = { // remove the entry from the contained edges delete parentNode.containedEdges[childNode.id]; - }, + }, @@ -894,14 +967,56 @@ var ClusterMixin = { } /* Debug Override */ - // for (nodeId in this.nodes) { - // if (this.nodes.hasOwnProperty(nodeId)) { - // node = this.nodes[nodeId]; - // node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale)); - // } - // } +// for (nodeId in this.nodes) { +// if (this.nodes.hasOwnProperty(nodeId)) { +// node = this.nodes[nodeId]; +// node.label = String(node.fx).concat(",",node.fy); +// } +// } + + }, + + + /** + * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes + * if the rest of the nodes are already a few cluster levels in. + * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not + * clustered enough to the clusterToSmallestNeighbours function. + */ + normalizeClusterLevels : function() { + var maxLevel = 0; + var minLevel = 1e9; + var clusterLevel = 0; + + // we loop over all nodes in the list + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + clusterLevel = this.nodes[nodeId].clusterSessions.length; + if (maxLevel < clusterLevel) {maxLevel = clusterLevel;} + if (minLevel > clusterLevel) {minLevel = clusterLevel;} + } + } + + if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) { + var amountOfNodes = this.nodeIndices.length; + var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference; + // we loop over all nodes in the list + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].clusterSessions.length < targetLevel) { + this._clusterToSmallestNeighbour(this.nodes[nodeId]); + } + } + } + this._updateNodeIndexList(); + this._updateDynamicEdges(); + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length != amountOfNodes) { + this.clusterSession += 1; + } + } + }, - }, /** @@ -918,7 +1033,7 @@ var ClusterMixin = { && Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale ) - }, + }, /** @@ -929,17 +1044,15 @@ var ClusterMixin = { repositionNodes : function() { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; - if (!node.isFixed()) { - var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize); + if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) { + var radius = this.constants.physics.springLength * Math.min(100,node.mass); var angle = 2 * Math.PI * Math.random(); - node.x = radius * Math.cos(angle); - node.y = radius * Math.sin(angle); + if (node.xFixed == false) {node.x = radius * Math.cos(angle);} + if (node.yFixed == false) {node.y = radius * Math.sin(angle);} + this._repositionBezierNodes(node); } } - }, - - - + }, /** @@ -955,6 +1068,7 @@ var ClusterMixin = { var largestHub = 0; for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; if (node.dynamicEdgesLength > largestHub) { largestHub = node.dynamicEdgesLength; @@ -979,7 +1093,7 @@ var ClusterMixin = { // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); // console.log("hubThreshold:",this.hubThreshold); - }, + }, /** @@ -1002,7 +1116,7 @@ var ClusterMixin = { } } } - }, + }, /** * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods @@ -1022,5 +1136,6 @@ var ClusterMixin = { } } return chains/total; - } + } + }; diff --git a/src/graph/graphMixins/ManipulationMixin.js b/src/graph/graphMixins/ManipulationMixin.js new file mode 100644 index 00000000..d1dfc0c1 --- /dev/null +++ b/src/graph/graphMixins/ManipulationMixin.js @@ -0,0 +1,433 @@ +/** + * Created by Alex on 2/4/14. + */ + +var manipulationMixin = { + + /** + * clears the toolbar div element of children + * + * @private + */ + _clearManipulatorBar : function() { + while (this.manipulationDiv.hasChildNodes()) { + this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); + } + }, + + /** + * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore + * these functions to their original functionality, we saved them in this.cachedFunctions. + * This function restores these functions to their original function. + * + * @private + */ + _restoreOverloadedFunctions : function() { + for (var functionName in this.cachedFunctions) { + if (this.cachedFunctions.hasOwnProperty(functionName)) { + this[functionName] = this.cachedFunctions[functionName]; + } + } + }, + + /** + * Enable or disable edit-mode. + * + * @private + */ + _toggleEditMode : function() { + this.editMode = !this.editMode; + var toolbar = document.getElementById("graph-manipulationDiv"); + var closeDiv = document.getElementById("graph-manipulation-closeDiv"); + var editModeDiv = document.getElementById("graph-manipulation-editMode"); + if (this.editMode == true) { + toolbar.style.display="block"; + closeDiv.style.display="block"; + editModeDiv.style.display="none"; + closeDiv.onclick = this._toggleEditMode.bind(this); + } + else { + toolbar.style.display="none"; + closeDiv.style.display="none"; + editModeDiv.style.display="block"; + closeDiv.onclick = null; + } + this._createManipulatorBar() + }, + + /** + * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * + * @private + */ + _createManipulatorBar : function() { + // remove bound functions + this.off('select', this.boundFunction); + + // restore overloaded functions + this._restoreOverloadedFunctions(); + + // resume calculation + this.freezeSimulation = false; + + // reset global variables + this.blockConnectingEdgeSelection = false; + this.forceAppendSelection = false + + if (this.editMode == true) { + while (this.manipulationDiv.hasChildNodes()) { + this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); + } + // add the icons to the manipulator div + this.manipulationDiv.innerHTML = "" + + "" + + "Add Node" + + "
    " + + "" + + "Add Link"; + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + this.manipulationDiv.innerHTML += "" + + "
    " + + "" + + "Edit Node"; + } + if (this._selectionIsEmpty() == false) { + this.manipulationDiv.innerHTML += "" + + "
    " + + "" + + "Delete selected"; + } + + + // bind the icons + var addNodeButton = document.getElementById("graph-manipulate-addNode"); + addNodeButton.onclick = this._createAddNodeToolbar.bind(this); + var addEdgeButton = document.getElementById("graph-manipulate-connectNode"); + addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + var editButton = document.getElementById("graph-manipulate-editNode"); + editButton.onclick = this._editNode.bind(this); + } + if (this._selectionIsEmpty() == false) { + var deleteButton = document.getElementById("graph-manipulate-delete"); + deleteButton.onclick = this._deleteSelected.bind(this); + } + var closeDiv = document.getElementById("graph-manipulation-closeDiv"); + closeDiv.onclick = this._toggleEditMode.bind(this); + + this.boundFunction = this._createManipulatorBar.bind(this); + this.on('select', this.boundFunction); + } + else { + this.editModeDiv.innerHTML = "" + + "" + + "Edit" + var editModeButton = document.getElementById("graph-manipulate-editModeButton"); + editModeButton.onclick = this._toggleEditMode.bind(this); + } + }, + + + + /** + * Create the toolbar for adding Nodes + * + * @private + */ + _createAddNodeToolbar : function() { + // clear the toolbar + this._clearManipulatorBar(); + this.off('select', this.boundFunction); + + // create the toolbar contents + this.manipulationDiv.innerHTML = "" + + "" + + "Back" + + "
    " + + "" + + "Click in an empty space to place a new node"; + + // bind the icon + var backButton = document.getElementById("graph-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); + + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + this.boundFunction = this._addNode.bind(this); + this.on('select', this.boundFunction); + }, + + + /** + * create the toolbar to connect nodes + * + * @private + */ + _createAddEdgeToolbar : function() { + // clear the toolbar + this._clearManipulatorBar(); + this._unselectAll(true); + this.freezeSimulation = true; + + this.off('select', this.boundFunction); + + this._unselectAll(); + this.forceAppendSelection = false; + this.blockConnectingEdgeSelection = true; + + this.manipulationDiv.innerHTML = "" + + "" + + "Back" + + "
    " + + "" + + "Click on a node and drag the edge to another node to connect them."; + + // bind the icon + var backButton = document.getElementById("graph-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); + + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + this.boundFunction = this._handleConnect.bind(this); + this.on('select', this.boundFunction); + + // temporarily overload functions + this.cachedFunctions["_handleTouch"] = this._handleTouch; + this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; + this._handleTouch = this._handleConnect; + this._handleOnRelease = this._finishConnect; + + // redraw to show the unselect + this._redraw(); + + }, + + + /** + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * + * @private + */ + _handleConnect : function(pointer) { + if (this._getSelectedNodeCount() == 0) { + var node = this._getNodeAt(pointer); + if (node != null) { + if (node.clusterSize > 1) { + alert("Cannot create edges to a cluster.") + } + else { + this._selectObject(node,false); + // create a node the temporary line can look at + this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); + this.sectors['support']['nodes']['targetNode'].x = node.x; + this.sectors['support']['nodes']['targetNode'].y = node.y; + this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); + this.sectors['support']['nodes']['targetViaNode'].x = node.x; + this.sectors['support']['nodes']['targetViaNode'].y = node.y; + this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; + + // create a temporary edge + this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); + this.edges['connectionEdge'].from = node; + this.edges['connectionEdge'].connected = true; + this.edges['connectionEdge'].smooth = true; + this.edges['connectionEdge'].selected = true; + this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; + this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; + + this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; + this._handleOnDrag = function(event) { + var pointer = this._getPointer(event.gesture.touches[0]); + this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x); + this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y); + this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x); + this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y); + }; + + this.moving = true; + this.start(); + } + } + } + }, + + _finishConnect : function(pointer) { + if (this._getSelectedNodeCount() == 1) { + + // restore the drag function + this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; + delete this.cachedFunctions["_handleOnDrag"]; + + // remember the edge id + var connectFromId = this.edges['connectionEdge'].fromId; + + // remove the temporary nodes and edge + delete this.edges['connectionEdge'] + delete this.sectors['support']['nodes']['targetNode']; + delete this.sectors['support']['nodes']['targetViaNode']; + + var node = this._getNodeAt(pointer); + if (node != null) { + if (node.clusterSize > 1) { + alert("Cannot create edges to a cluster.") + } + else { + this._createEdge(connectFromId,node.id); + this._createManipulatorBar(); + } + } + this._unselectAll(); + } + }, + + + /** + * Adds a node on the specified location + * + * @param {Object} pointer + */ + _addNode : function() { + if (this._selectionIsEmpty() && this.editMode == true) { + var positionObject = this._pointerToPositionObject(this.pointerPosition); + var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",fixed:false}; + if (this.triggerFunctions.add) { + if (this.triggerFunctions.add.length == 2) { + var me = this; + this.triggerFunctions.add(defaultData, function(finalizedData) { + me.createNodeOnClick = true; + me.nodesData.add(finalizedData); + me.createNodeOnClick = false; + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } + else { + alert("The function for add does not support two arguments (data,callback)."); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } + } + else { + this.createNodeOnClick = true; + this.nodesData.add(defaultData); + this.createNodeOnClick = false; + this._createManipulatorBar(); + this.moving = true; + this.start(); + } + } + }, + + + /** + * connect two nodes with a new edge. + * + * @private + */ + _createEdge : function(sourceNodeId,targetNodeId) { + if (this.editMode == true) { + var defaultData = {from:sourceNodeId, to:targetNodeId}; + if (this.triggerFunctions.connect) { + if (this.triggerFunctions.connect.length == 2) { + var me = this; + this.triggerFunctions.connect(defaultData, function(finalizedData) { + me.edgesData.add(finalizedData) + me.moving = true; + me.start(); + }); + } + else { + alert("The function for connect does not support two arguments (data,callback)."); + this.moving = true; + this.start(); + } + } + else { + this.edgesData.add(defaultData) + this.moving = true; + this.start(); + } + } + }, + + + /** + * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. + * + * @private + */ + _editNode : function() { + if (this.triggerFunctions.edit && this.editMode == true) { + var node = this._getSelectedNode(); + var data = {id:node.id, + label: node.label, + group: node.group, + shape: node.shape, + color: { + background:node.color.background, + border:node.color.border, + highlight: { + background:node.color.highlight.background, + border:node.color.highlight.border + } + }}; + if (this.triggerFunctions.edit.length == 2) { + var me = this; + this.triggerFunctions.edit(data, function (finalizedData) { + me.nodesData.update(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } + else { + alert("The function for edit does not support two arguments (data, callback).") + } + } + else { + alert("No edit function has been bound to this button.") + } + }, + + + /** + * delete everything in the selection + * + * @private + */ + _deleteSelected : function() { + if (!this._selectionIsEmpty() && this.editMode == true) { + if (!this._clusterInSelection()) { + var selectedNodes = this.getSelectedNodes(); + var selectedEdges = this.getSelectedEdges(); + if (this.triggerFunctions.delete) { + var me = this; + var data = {nodes: selectedNodes, edges: selectedEdges}; + if (this.triggerFunctions.delete.length = 2) { + this.triggerFunctions.delete(data, function (finalizedData) { + me.edgesData.remove(finalizedData.edges); + me.nodesData.remove(finalizedData.nodes); + this._unselectAll(); + me.moving = true; + me.start(); + }); + } + else { + alert("The function for edit does not support two arguments (data, callback).") + } + } + else { + this.edgesData.remove(selectedEdges); + this.nodesData.remove(selectedNodes); + this._unselectAll(); + this.moving = true; + this.start(); + } + } + else { + alert("Clusters cannot be deleted."); + } + } + } +}; \ No newline at end of file diff --git a/src/graph/graphMixins/MixinLoader.js b/src/graph/graphMixins/MixinLoader.js new file mode 100644 index 00000000..f025b08a --- /dev/null +++ b/src/graph/graphMixins/MixinLoader.js @@ -0,0 +1,219 @@ +/** + * Created by Alex on 2/10/14. + */ + + +var graphMixinLoaders = { + + /** + * Load a mixin into the graph object + * + * @param {Object} sourceVariable | this object has to contain functions. + * @private + */ + _loadMixin : function(sourceVariable) { + for (var mixinFunction in sourceVariable) { + if (sourceVariable.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = sourceVariable[mixinFunction]; + } + } + }, + + + /** + * removes a mixin from the graph object. + * + * @param {Object} sourceVariable | this object has to contain functions. + * @private + */ + _clearMixin : function(sourceVariable) { + for (var mixinFunction in sourceVariable) { + if (sourceVariable.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = undefined; + } + } + }, + + + /** + * Mixin the physics system and initialize the parameters required. + * + * @private + */ + _loadPhysicsSystem : function() { + this._loadMixin(physicsMixin); + this._loadSelectedForceSolver(); + }, + + + /** + * This loads the node force solver based on the barnes hut or repulsion algorithm + * + * @private + */ + _loadSelectedForceSolver : function() { + // this overloads the this._calculateNodeForces + if (this.constants.physics.barnesHut.enabled == true) { + this._clearMixin(repulsionMixin); + + this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; + this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; + this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; + this.constants.physics.damping = this.constants.physics.barnesHut.damping; + this.constants.physics.springGrowthPerMass = this.constants.physics.barnesHut.springGrowthPerMass; + + this._loadMixin(barnesHutMixin); + } + else { + this._clearMixin(barnesHutMixin); + this.barnesHutTree = undefined; + + this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; + this.constants.physics.springLength = this.constants.physics.repulsion.springLength; + this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; + this.constants.physics.damping = this.constants.physics.repulsion.damping; + this.constants.physics.springGrowthPerMass = this.constants.physics.repulsion.springGrowthPerMass; + + this._loadMixin(repulsionMixin); + } + }, + + + /** + * Mixin the cluster system and initialize the parameters required. + * + * @private + */ + _loadClusterSystem : function() { + this.clusterSession = 0; + this.hubThreshold = 5; + this._loadMixin(ClusterMixin); + }, + + + /** + * Mixin the sector system and initialize the parameters required + * + * @private + */ + _loadSectorSystem : function() { + this.sectors = { }, + this.activeSector = ["default"]; + this.sectors["active"] = { }, + this.sectors["active"]["default"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined }; + this.sectors["frozen"] = {}, + this.sectors["support"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined }; + + this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields + + this._loadMixin(SectorMixin); + }, + + + /** + * Mixin the selection system and initialize the parameters required + * + * @private + */ + _loadSelectionSystem : function() { + this.selectionObj = { }; + + this._loadMixin(SelectionMixin); + }, + + + /** + * Mixin the navigationUI (User Interface) system and initialize the parameters required + * + * @private + */ + _loadManipulationSystem : function() { + // reset global variables -- these are used by the selection of nodes and edges. + this.blockConnectingEdgeSelection = false; + this.forceAppendSelection = false + + if (this.constants.dataManipulation.enabled == true) { + // load the manipulator HTML elements. All styling done in css. + if (this.manipulationDiv === undefined) { + this.manipulationDiv = document.createElement('div'); + this.manipulationDiv.className = 'graph-manipulationDiv'; + this.manipulationDiv.id = 'graph-manipulationDiv'; + if (this.editMode == true) { + this.manipulationDiv.style.display = "block"; + } + else { + this.manipulationDiv.style.display = "none"; + } + this.containerElement.insertBefore(this.manipulationDiv, this.frame); + } + + if (this.editModeDiv === undefined) { + this.editModeDiv = document.createElement('div'); + this.editModeDiv.className = 'graph-manipulation-editMode'; + this.editModeDiv.id = 'graph-manipulation-editMode'; + if (this.editMode == true) { + this.editModeDiv.style.display = "none"; + } + else { + this.editModeDiv.style.display = "block"; + } + this.containerElement.insertBefore(this.editModeDiv, this.frame); + } + + if (this.closeDiv === undefined) { + this.closeDiv = document.createElement('div'); + this.closeDiv.className = 'graph-manipulation-closeDiv'; + this.closeDiv.id = 'graph-manipulation-closeDiv'; + this.closeDiv.style.display = this.manipulationDiv.style.display; + this.containerElement.insertBefore(this.closeDiv, this.frame); + } + + // load the manipulation functions + this._loadMixin(manipulationMixin); + + // create the manipulator toolbar + this._createManipulatorBar(); + } + else { + if (this.manipulationDiv !== undefined) { + // removes all the bindings and overloads + this._createManipulatorBar(); + // remove the manipulation divs + this.containerElement.removeChild(this.manipulationDiv); + this.containerElement.removeChild(this.editModeDiv); + this.containerElement.removeChild(this.closeDiv); + + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; + // remove the mixin functions + this._clearMixin(manipulationMixin); + } + } + }, + + + /** + * Mixin the navigation (User Interface) system and initialize the parameters required + * + * @private + */ + _loadNavigationControls : function() { + this._loadMixin(NavigationMixin); + + // the clean function removes the button divs, this is done to remove the bindings. + this._cleanNavigation(); + if (this.constants.navigation.enabled == true) { + this._loadNavigationElements(); + } + } + +} diff --git a/src/graph/graphMixins/NavigationMixin.js b/src/graph/graphMixins/NavigationMixin.js new file mode 100644 index 00000000..60897987 --- /dev/null +++ b/src/graph/graphMixins/NavigationMixin.js @@ -0,0 +1,173 @@ +/** + * Created by Alex on 1/22/14. + */ + +var NavigationMixin = { + + _cleanNavigation : function() { + // clean up previosu navigation items + var wrapper = document.getElementById('graph-navigation_wrapper'); + if (wrapper != null) { + this.containerElement.removeChild(wrapper); + } + document.onmouseup = null; + }, + + /** + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * + * @private + */ + _loadNavigationElements : function() { + this._cleanNavigation(); + + this.navigationDivs = {}; + var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; + var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomToFit']; + + this.navigationDivs['wrapper'] = document.createElement('div'); + this.navigationDivs['wrapper'].id = "graph-navigation_wrapper"; + this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); + + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDivs[navigationDivs[i]] = document.createElement('div'); + this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i]; + this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i]; + this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); + this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); + } + + document.onmouseup = this._stopMovement.bind(this); + }, + + /** + * this stops all movement induced by the navigation buttons + * + * @private + */ + _stopMovement : function() { + this._xStopMoving(); + this._yStopMoving(); + this._stopZoom(); + }, + + + /** + * stops the actions performed by page up and down etc. + * + * @param event + * @private + */ + _preventDefault : function(event) { + if (event !== undefined) { + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + } + }, + + + /** + * move the screen up + * By using the increments, instead of adding a fixed number to the translation, we keep fluent and + * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently + * To avoid this behaviour, we do the translation in the start loop. + * + * @private + */ + _moveUp : function(event) { + console.log("here") + this.yIncrement = this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * move the screen down + * @private + */ + _moveDown : function(event) { + this.yIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * move the screen left + * @private + */ + _moveLeft : function(event) { + this.xIncrement = this.constants.keyboard.speed.x; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * move the screen right + * @private + */ + _moveRight : function(event) { + this.xIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * Zoom in, using the same method as the movement. + * @private + */ + _zoomIn : function(event) { + this.zoomIncrement = this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * Zoom out + * @private + */ + _zoomOut : function() { + this.zoomIncrement = -this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * Stop zooming and unhighlight the zoom controls + * @private + */ + _stopZoom : function() { + this.zoomIncrement = 0; + }, + + + /** + * Stop moving in the Y direction and unHighlight the up and down + * @private + */ + _yStopMoving : function() { + this.yIncrement = 0; + }, + + + /** + * Stop moving in the X direction and unHighlight left and right. + * @private + */ + _xStopMoving : function() { + this.xIncrement = 0; + } + + +}; diff --git a/src/graph/SectorsMixin.js b/src/graph/graphMixins/SectorsMixin.js similarity index 94% rename from src/graph/SectorsMixin.js rename to src/graph/graphMixins/SectorsMixin.js index f242dcfb..331586ef 100644 --- a/src/graph/SectorsMixin.js +++ b/src/graph/graphMixins/SectorsMixin.js @@ -58,28 +58,29 @@ var SectorMixin = { /** * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied frozen sector. + * those of the supplied active sector. * * @param sectorId * @private */ - _switchToFrozenSector : function(sectorId) { - this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["frozen"][sectorId]["nodes"]; - this.edges = this.sectors["frozen"][sectorId]["edges"]; + _switchToSupportSector : function() { + this.nodeIndices = this.sectors["support"]["nodeIndices"]; + this.nodes = this.sectors["support"]["nodes"]; + this.edges = this.sectors["support"]["edges"]; }, /** - * This function sets the global references to nodes, edges and nodeIndices to - * those of the navigation controls sector. + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied frozen sector. * + * @param sectorId * @private */ - _switchToNavigationSector : function() { - this.nodeIndices = this.sectors["navigation"]["nodeIndices"]; - this.nodes = this.sectors["navigation"]["nodes"]; - this.edges = this.sectors["navigation"]["edges"]; + _switchToFrozenSector : function(sectorId) { + this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["frozen"][sectorId]["nodes"]; + this.edges = this.sectors["frozen"][sectorId]["edges"]; }, @@ -349,6 +350,9 @@ var SectorMixin = { // finally, we update the node index list. this._updateNodeIndexList(); + + // we refresh the list with calulation nodes and calculation node indices. + this._updateCalculationNodes(); } } }, @@ -393,6 +397,35 @@ var SectorMixin = { }, + /** + * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we dont pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInSupportSector : function(runFunction,argument) { + if (argument === undefined) { + this._switchToSupportSector(); + this[runFunction](); + } + else { + this._switchToSupportSector(); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + // we revert the global references back to our active sector + this._loadLatestSector(); + }, + + /** * This runs a function in all frozen sectors. This is used in the _redraw(). * @@ -431,33 +464,6 @@ var SectorMixin = { }, - /** - * This runs a function in the navigation controls sector. - * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Graph object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - _doInNavigationSector : function(runFunction,argument) { - this._switchToNavigationSector(); - if (argument === undefined) { - this[runFunction](); - } - else { - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - this._loadLatestSector(); - }, - - /** * This runs a function in all sectors. This is used in the _redraw(). * @@ -483,7 +489,6 @@ var SectorMixin = { this._doInAllFrozenSectors(runFunction,argument); } } - }, diff --git a/src/graph/SelectionMixin.js b/src/graph/graphMixins/SelectionMixin.js similarity index 87% rename from src/graph/SelectionMixin.js rename to src/graph/graphMixins/SelectionMixin.js index 7252ac15..01b59511 100644 --- a/src/graph/SelectionMixin.js +++ b/src/graph/graphMixins/SelectionMixin.js @@ -32,18 +32,6 @@ var SelectionMixin = { }, - /** - * retrieve all nodes in the navigation controls overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - _getAllNavigationNodesOverlappingWith : function (object) { - var overlappingNodes = []; - this._doInNavigationSector("_getNodesOverlappingWith",object,overlappingNodes); - return overlappingNodes; - }, - /** * Return a position object in canvasspace from a single point in screenspace * @@ -61,42 +49,6 @@ var SelectionMixin = { bottom: y}; }, - /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private - */ - _pointerToScreenPositionObject : function(pointer) { - var x = pointer.x; - var y = pointer.y; - - return {left: x, - top: y, - right: x, - bottom: y}; - }, - - - /** - * Get the top navigation controls node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node - * @private - */ - _getNavigationNodeAt : function (pointer) { - var screenPositionObject = this._pointerToScreenPositionObject(pointer); - var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject); - if (overlappingNodes.length > 0) { - return this.sectors["navigation"]["nodes"][overlappingNodes[overlappingNodes.length - 1]]; - } - else { - return null; - } - }, - /** * Get the top node at the a specific point (like a click) @@ -213,9 +165,7 @@ var SelectionMixin = { this.selectionObj = {}; if (doNotTrigger == false) { - this._trigger('select', { - nodes: this.getSelection() - }); + this._trigger('select', this.getSelection()); } }, @@ -242,9 +192,7 @@ var SelectionMixin = { } if (doNotTrigger == false) { - this._trigger('select', { - nodes: this.getSelection() - }); + this._trigger('select', this.getSelection()); } }, @@ -267,6 +215,23 @@ var SelectionMixin = { return count; }, + /** + * return the number of selected nodes + * + * @returns {number} + * @private + */ + _getSelectedNode : function() { + for (var objectId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Node) { + return this.selectionObj[objectId]; + } + } + } + return null; + }, + /** * return the number of selected edges @@ -399,9 +364,7 @@ var SelectionMixin = { this._removeFromSelection(object); } if (doNotTrigger == false) { - this._trigger('select', { - nodes: this.getSelection() - }); + this._trigger('select', this.getSelection()); } }, @@ -415,15 +378,7 @@ var SelectionMixin = { * @private */ _handleTouch : function(pointer) { - if (this.constants.navigation.enabled == true) { - this.pointerPosition = pointer; - var node = this._getNavigationNodeAt(pointer); - if (node != null) { - if (this[node.triggerFunction] !== undefined) { - this[node.triggerFunction](); - } - } - } + }, @@ -494,11 +449,8 @@ var SelectionMixin = { * * @private */ - _handleOnRelease : function() { - this.xIncrement = 0; - this.yIncrement = 0; - this.zoomIncrement = 0; - this._unHighlightAll(); + _handleOnRelease : function(pointer) { + }, diff --git a/src/graph/graphMixins/physics/PhysicsMixin.js b/src/graph/graphMixins/physics/PhysicsMixin.js new file mode 100644 index 00000000..c5513761 --- /dev/null +++ b/src/graph/graphMixins/physics/PhysicsMixin.js @@ -0,0 +1,245 @@ +/** + * Created by Alex on 2/6/14. + */ + + +var physicsMixin = { + + /** + * Toggling barnes Hut calculation on and off. + * + * @private + */ + _toggleBarnesHut : function() { + this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; + this._loadSelectedForceSolver(); + this.moving = true; + this.start(); + }, + + + /** + * Before calculating the forces, we check if we need to cluster to keep up performance and we check + * if there is more than one node. If it is just one node, we dont calculate anything. + * + * @private + */ + _initializeForceCalculation : function() { + // stop calculation if there is only one node + if (this.nodeIndices.length == 1) { + this.nodes[this.nodeIndices[0]]._setForce(0,0); + } + else { + // if there are too many nodes on screen, we cluster without repositioning + if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { + this.clusterToFit(this.constants.clustering.reduceToNodes, false); + } + + // we now start the force calculation + this._calculateForces(); + } + }, + + + /** + * Calculate the external forces acting on the nodes + * Forces are caused by: edges, repulsing forces between nodes, gravity + * @private + */ + _calculateForces : function() { + // Gravity is required to keep separated groups from floating off + // the forces are reset to zero in this loop by using _setForce instead + // of _addForce + + this._calculateGravitationalForces(); + this._calculateNodeForces(); + + + if (this.constants.smoothCurves == true) { + this._calculateSpringForcesWithSupport(); + } + else { + this._calculateSpringForces(); + } + }, + + + /** + * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also + * handled in the calculateForces function. We then use a quadratic curve with the center node as control. + * This function joins the datanodes and invisible (called support) nodes into one object. + * We do this so we do not contaminate this.nodes with the support nodes. + * + * @private + */ + _updateCalculationNodes : function() { + if (this.constants.smoothCurves == true) { + this.calculationNodes = {}; + this.calculationNodeIndices = []; + + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.calculationNodes[nodeId] = this.nodes[nodeId]; + } + } + var supportNodes = this.sectors['support']['nodes']; + for (var supportNodeId in supportNodes) { + if (supportNodes.hasOwnProperty(supportNodeId)) { + if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { + this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; + } + else { + supportNodes[supportNodeId]._setForce(0,0); + } + } + } + + for (var idx in this.calculationNodes) { + if (this.calculationNodes.hasOwnProperty(idx)) { + this.calculationNodeIndices.push(idx); + } + } + } + else { + this.calculationNodes = this.nodes; + this.calculationNodeIndices = this.nodeIndices; + } + }, + + + /** + * this function applies the central gravity effect to keep groups from floating off + * + * @private + */ + _calculateGravitationalForces : function() { + var dx, dy, distance, node, i; + var nodes = this.calculationNodes; + var gravity = this.constants.physics.centralGravity; + var gravityForce = 0; + + for (i = 0; i < this.calculationNodeIndices.length; i++) { + node = nodes[this.calculationNodeIndices[i]]; + node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. + // gravity does not apply when we are in a pocket sector + if (this._sector() == "default") { + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx*dx + dy*dy); + gravityForce = gravity / distance; + + node.fx = dx * gravityForce; + node.fy = dy * gravityForce; + } + else { + node.fx = 0; + node.fy = 0; + } + } + }, + + + /** + * this function calculates the effects of the springs in the case of unsmooth curves. + * + * @private + */ + _calculateSpringForces : function() { + var edgeLength, edge, edgeId; + var dx, dy, fx, fy, springForce, length; + var edges = this.edges; + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected) { + // only calculate forces if nodes are in the same sector + if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { + edgeLength = edge.length; + // this implies that the edges between big clusters are longer + edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; + + dx = (edge.from.x - edge.to.x); + dy = (edge.from.y - edge.to.y); + length = Math.sqrt(dx * dx + dy * dy); + + springForce = this.constants.physics.springConstant * (edgeLength - length) / length; + + fx = dx * springForce; + fy = dy * springForce; + + edge.from.fx += fx; + edge.from.fy += fy; + edge.to.fx -= fx; + edge.to.fy -= fy; + } + } + } + } + }, + + + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + _calculateSpringForcesWithSupport : function() { + var edgeLength, edge, edgeId, combinedClusterSize; + var edges = this.edges; + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected) { + // only calculate forces if nodes are in the same sector + if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { + if (edge.via != null) { + var node1 = edge.to; + var node2 = edge.via; + var node3 = edge.from; + + edgeLength = edge.length; + + combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; + + // this implies that the edges between big clusters are longer + edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; + this._calculateSpringForce(node1,node2,0.5*edgeLength); + this._calculateSpringForce(node2,node3,0.5*edgeLength); + } + } + } + } + } + }, + + + /** + * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. + * + * @param node1 + * @param node2 + * @param edgeLength + * @private + */ + _calculateSpringForce : function(node1,node2,edgeLength) { + var dx, dy, fx, fy, springForce, length; + + dx = (node1.x - node2.x); + dy = (node1.y - node2.y); + length = Math.sqrt(dx * dx + dy * dy); + + springForce = this.constants.physics.springConstant * (edgeLength - length) / length; + + fx = dx * springForce; + fy = dy * springForce; + + node1.fx += fx; + node1.fy += fy; + node2.fx -= fx; + node2.fy -= fy; + } +} \ No newline at end of file diff --git a/src/graph/graphMixins/physics/barnesHut.js b/src/graph/graphMixins/physics/barnesHut.js new file mode 100644 index 00000000..9e658c8c --- /dev/null +++ b/src/graph/graphMixins/physics/barnesHut.js @@ -0,0 +1,373 @@ +/** + * Created by Alex on 2/10/14. + */ + +var barnesHutMixin = { + + /** + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. + * + * @private + */ + _calculateNodeForces : function() { + var node; + var nodes = this.calculationNodes; + var nodeIndices = this.calculationNodeIndices; + var nodeCount = nodeIndices.length; + + this._formBarnesHutTree(nodes,nodeIndices); + + var barnesHutTree = this.barnesHutTree; + + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + // starting with root is irrelevant, it never passes the BarnesHut condition + this._getForceContribution(barnesHutTree.root.children.NW,node); + this._getForceContribution(barnesHutTree.root.children.NE,node); + this._getForceContribution(barnesHutTree.root.children.SW,node); + this._getForceContribution(barnesHutTree.root.children.SE,node); + } + }, + + + /** + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node + * @private + */ + _getForceContribution : function(parentBranch,node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx,dy,distance; + + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // BarnesHut condition + // original condition : s/d < theta = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) { + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.5*Math.random(); + dx = distance; + } + var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + node.fx += fx; + node.fy += fy; + } + else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount == 4) { + this._getForceContribution(parentBranch.children.NW,node); + this._getForceContribution(parentBranch.children.NE,node); + this._getForceContribution(parentBranch.children.SW,node); + this._getForceContribution(parentBranch.children.SE,node); + } + else { // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { // if it is not self + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.5*Math.random(); + dx = distance; + } + var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + node.fx += fx; + node.fy += fy; + } + } + } + } + }, + + /** + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices + * @private + */ + _formBarnesHutTree : function(nodes,nodeIndices) { + var node; + var nodeCount = nodeIndices.length; + + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX =-Number.MAX_VALUE, + maxY =-Number.MAX_VALUE; + + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (x < minX) { minX = x; } + if (x > maxX) { maxX = x; } + if (y < minY) { minY = y; } + if (y > maxY) { maxY = y; } + } + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize + else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize + + + var minimumTreeSize = 1e-5; + var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); + + // construct the barnesHutTree + var barnesHutTree = {root:{ + centerOfMass:{x:0,y:0}, // Center of Mass + mass:0, + range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize, + minY:centerY-halfRootSize,maxY:centerY+halfRootSize}, + size: rootSize, + calcSize: 1 / rootSize, + children: {data:null}, + maxWidth: 0, + level: 0, + childrenCount: 4 + }}; + this._splitBranch(barnesHutTree.root); + + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + this._placeInTree(barnesHutTree.root,node); + } + + // make global + this.barnesHutTree = barnesHutTree + }, + + + _updateBranchMass : function(parentBranch, node) { + var totalMass = parentBranch.mass + node.mass; + var totalMassInv = 1/totalMass; + + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass; + parentBranch.centerOfMass.x *= totalMassInv; + + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass; + parentBranch.centerOfMass.y *= totalMassInv; + + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); + parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; + + }, + + + _placeInTree : function(parentBranch,node,skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch,node); + } + + if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { // in NW + this._placeInRegion(parentBranch,node,"NW"); + } + else { // in SW + this._placeInRegion(parentBranch,node,"SW"); + } + } + else { // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { // in NE + this._placeInRegion(parentBranch,node,"NE"); + } + else { // in SE + this._placeInRegion(parentBranch,node,"SE"); + } + } + }, + + + _placeInRegion : function(parentBranch,node,region) { + switch (parentBranch.children[region].childrenCount) { + case 0: // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region],node); + break; + case 1: // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x == node.x && + parentBranch.children[region].children.data.y == node.y) { + node.x += Math.random(); + node.y += Math.random(); + this._placeInTree(parentBranch,node, true); + } + else { + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region],node); + } + break; + case 4: // place in branch + this._placeInTree(parentBranch.children[region],node); + break; + } + }, + + + /** + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. + * + * @param parentBranch + * @private + */ + _splitBranch : function(parentBranch) { + // if the branch is filled with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount == 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; + } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch,"NW"); + this._insertRegion(parentBranch,"NE"); + this._insertRegion(parentBranch,"SW"); + this._insertRegion(parentBranch,"SE"); + + if (containedNode != null) { + this._placeInTree(parentBranch,containedNode); + } + }, + + + /** + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange + * @private + */ + _insertRegion : function(parentBranch, region) { + var minX,maxX,minY,maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + } + + + parentBranch.children[region] = { + centerOfMass:{x:0,y:0}, + mass:0, + range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: {data:null}, + maxWidth: 0, + level: parentBranch.level+1, + childrenCount: 0 + }; + }, + + + /** + * This function is for debugging purposed, it draws the tree. + * + * @param ctx + * @param color + * @private + */ + _drawTree : function(ctx,color) { + if (this.barnesHutTree !== undefined) { + + ctx.lineWidth = 1; + + this._drawBranch(this.barnesHutTree.root,ctx,color); + } + }, + + + /** + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color + * @private + */ + _drawBranch : function(branch,ctx,color) { + if (color === undefined) { + color = "#FF0000"; + } + + if (branch.childrenCount == 4) { + this._drawBranch(branch.children.NW,ctx); + this._drawBranch(branch.children.NE,ctx); + this._drawBranch(branch.children.SE,ctx); + this._drawBranch(branch.children.SW,ctx); + } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX,branch.range.minY); + ctx.lineTo(branch.range.maxX,branch.range.minY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX,branch.range.minY); + ctx.lineTo(branch.range.maxX,branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX,branch.range.maxY); + ctx.lineTo(branch.range.minX,branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.minX,branch.range.maxY); + ctx.lineTo(branch.range.minX,branch.range.minY); + ctx.stroke(); + + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ + } + +}; \ No newline at end of file diff --git a/src/graph/graphMixins/physics/repulsion.js b/src/graph/graphMixins/physics/repulsion.js new file mode 100644 index 00000000..dccfb9c4 --- /dev/null +++ b/src/graph/graphMixins/physics/repulsion.js @@ -0,0 +1,66 @@ +/** + * Created by Alex on 2/10/14. + */ + +var repulsionMixin = { + + + /** + * Calculate the forces the nodes apply on eachother based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + _calculateNodeForces : function() { + var dx, dy, angle, distance, fx, fy, combinedClusterSize, + repulsingForce, node1, node2, i, j; + + var nodes = this.calculationNodes; + var nodeIndices = this.calculationNodeIndices; + + // approximation constants + var a_base = -2/3; + var b = 4/3; + + // repulsing forces between nodes + var nodeDistance = this.constants.physics.repulsion.nodeDistance; + var minimumDistance = nodeDistance; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (i = 0; i < nodeIndices.length-1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i+1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; + + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); + var a = a_base / minimumDistance; + if (distance < 2*minimumDistance) { + if (distance < 0.5*minimumDistance) { + repulsingForce = 1.0; + } + else { + repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) + } + + // amplify the repulsion for clusters. + repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; + repulsingForce = repulsingForce/distance; + + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + node1.fx -= fx; + node1.fy -= fy; + node2.fx += fx; + node2.fy += fy; + } + } + } + } +} \ No newline at end of file diff --git a/src/graph/img/cross.png b/src/graph/img/cross.png new file mode 100644 index 00000000..9cbd189a Binary files /dev/null and b/src/graph/img/cross.png differ diff --git a/src/graph/img/cross2.png b/src/graph/img/cross2.png new file mode 100644 index 00000000..9fc4b95c Binary files /dev/null and b/src/graph/img/cross2.png differ diff --git a/src/graph/manipulationMixin.js b/src/graph/manipulationMixin.js deleted file mode 100644 index 9f1d6ad0..00000000 --- a/src/graph/manipulationMixin.js +++ /dev/null @@ -1,463 +0,0 @@ -/** - * Created by Alex on 2/4/14. - */ - -var manipulationMixin = { - - /** - * clears the toolbar div element of children - * - * @private - */ - _clearManipulatorBar : function() { - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } - }, - - - /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private - */ - _createManipulatorBar : function() { - // remove bound functions - this.off('select', this.boundFunction); - - // reset global variables - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false - - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } - // add the icons to the manipulator div - this.manipulationDiv.innerHTML = "" + - "Add Node" + - "
    " + - "Edit Selected" + - "
    " + - "Connect Node" + - "
    " + - "Delete selected"; - - // bind the icons - var addButton = document.getElementById("manipulate-addNode"); - addButton.onclick = this._createAddToolbar.bind(this); - var editButton = document.getElementById("manipulate-editNode"); - editButton.onclick = this._createEditToolbar.bind(this); - var connectButton = document.getElementById("manipulate-connectNode"); - connectButton.onclick = this._createConnectToolbar.bind(this); - var deleteButton = document.getElementById("manipulate-delete"); - deleteButton.onclick = this._createDeletionToolbar.bind(this); - }, - - - /** - * Create the toolbar for adding Nodes - * - * @private - */ - _createAddToolbar : function() { - // clear the toolbar - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Back" + - "
    " + - "Click in an empty space to place a new node"; - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._addNode.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * Create the toolbar to edit nodes or edges. - * TODO: edges not implemented yet, unsure what to edit. - * - * @private - */ - _createEditToolbar : function() { - // clear the toolbar - this.blockConnectingEdgeSelection = false; - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - - var message = ""; - if (this._selectionIsEmpty()) { - message = "Select a node or edge to edit."; - } - else { - if (this._getSelectedObjectCount() > 1) { - message = "Select a single node or edge to edit." - this._unselectAll(true); - } - else { - if (this._clusterInSelection()) { - message = "You cannot edit a cluster." - this._unselectAll(true); - } - else { - if (this._getSelectedNodeCount() > 0) { // the selected item is a node - this._createEditNodeToolbar(); - } - else { // the selected item is an edge - this._createEditEdgeToolbar(); - } - } - } - } - - if (message != "") { - this.blockConnectingEdgeSelection = true; - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Back" + - "
    " + - ""+message+""; - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createEditToolbar.bind(this); - this.on('select', this.boundFunction); - } - }, - - - /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. - * TODO: change shape or group? - * - * @private - */ - _createEditNodeToolbar : function() { - // clear the toolbar - this.blockConnectingEdgeSelection = false; - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - var editObject = this._getEditObject(); - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Cancel" + - "
    " + - "label: " + - "
    " + - "color: " + - "
    " + - "" - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - var saveButton = document.getElementById("manipulator-obj-save"); - saveButton.onclick = this._saveNodeData.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * save the changes in the node data - * - * @private - */ - _saveNodeData : function() { - var editObjectId = this._getEditObject().id; - var label = document.getElementById('manipulator-obj-label').value; - - var definedColor = document.getElementById('manipulator-obj-color').value; - var hsv = util.hexToHSV(definedColor); - - var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)}; - var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6}; - var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v); - var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v); - - var updatedSettings = {id:editObjectId, - label: label, - color: { - background:definedColor, - border:darkerColorHex, - highlight: { - background:lighterColorHex, - border:darkerColorHex - } - }}; - this.nodesData.update(updatedSettings); - this._createManipulatorBar(); - }, - - - /** - * creating the toolbar to edit edges. - * - * @private - */ - _createEditEdgeToolbar : function() { - // clear the toolbar - this.blockConnectingEdgeSelection = false; - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Back" + - "
    " + - "Currently only nodes can be edited."; - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * create the toolbar to connect nodes - * - * @private - */ - _createConnectToolbar : function() { - // clear the toolbar - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - this._unselectAll(); - this.forceAppendSelection = false; - this.blockConnectingEdgeSelection = true; - - this.manipulationDiv.innerHTML = "" + - "Back" + - "
    " + - "Select the node you want to connect to other nodes."; - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._handleConnect.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * create the toolbar for deleting selected objects. User has to be sure. - * - * @private - */ - _createDeletionToolbar : function() { - // clear the toolbar - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - if (this._selectionIsEmpty()) { - this.manipulationDiv.innerHTML = "" + - "Cannot delete an empty selection."; - var graph = this; - window.setTimeout (function() {graph._createManipulatorBar()},1500); - } - else { - this.manipulationDiv.innerHTML = "" + - "Back" + - "
    " + - "Are you sure? This cannot be undone." + - "
    " + - "Yes."; - - // bind the buttons - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - var acceptDeleteButton = document.getElementById("manipulate-acceptDelete"); - acceptDeleteButton.onclick = this._deleteSelected.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - } - }, - - - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - _handleConnect : function() { - this.forceAppendSelection = false; - if (this._clusterInSelection()) { - this._unselectClusters(true); - if (!this._selectionIsEmpty()) { - this._setManipulationMessage("You cannot connect a node to a cluster."); - this.forceAppendSelection = true; - } - else { - this._setManipulationMessage("You cannot connect anything to a cluster."); - } - } - else if (!this._selectionIsEmpty()) { - if (this._getSelectedNodeCount() == 2) { - this._connectNodes(); - this._restoreSourceNode(); - this._setManipulationMessage("Click on another node you want to connect this node to or go back."); - } - else { - this._setManipulationMessage("Click on the node you want to connect this node."); - this._setSourceNode(); - this.forceAppendSelection = true; - } - } - else { - this._setManipulationMessage("Select the node you want to connect to other nodes."); - } - }, - - - /** - * returns the object that is selected - * - * @returns {*} - * @private - */ - _getEditObject : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - return this.selectionObj[objectId]; - } - } - return null; - }, - - - /** - * stores the first selected node for the connecting process as the source node. This allows us to remember the direction - * - * @private - */ - _setSourceNode : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - this.manipulationSourceNode = this.selectionObj[objectId]; - } - } - } - }, - - - /** - * gets the node the source connects to. - * - * @returns {*} - * @private - */ - _getTargetNode : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (this.manipulationSourceNode.id != this.selectionObj[objectId].id) { - return this.selectionObj[objectId]; - } - } - } - } - return null; - }, - - - /** - * restore the selection back to only the sourcenode - * - * @private - */ - _restoreSourceNode : function() { - this._unselectAll(true); - this._selectObject(this.manipulationSourceNode); - }, - - - /** - * change the description message on the toolbar - * - * @param message - * @private - */ - _setManipulationMessage : function(message) { - var messageSpan = document.getElementById('manipulatorLabel'); - messageSpan.innerHTML = message; - }, - - - /** - * Adds a node on the specified location - * - * @param {Object} pointer - */ - _addNode : function() { - if (this._selectionIsEmpty()) { - var positionObject = this._pointerToPositionObject(this.pointerPosition); - this.createNodeOnClick = true; - this.nodesData.add({id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",fixed:false}); - this.createNodeOnClick = false; - this.moving = true; - this.start(); - } - }, - - - /** - * connect two nodes with a new edge. - * - * @private - */ - _connectNodes : function() { - var targetNode = this._getTargetNode(); - var sourceNode = this.manipulationSourceNode; - this.edgesData.add({from:sourceNode.id, to:targetNode.id}) - this.moving = true; - this.start(); - }, - - - /** - * delete everything in the selection - * - * @private - */ - _deleteSelected : function() { - if (!this._clusterInSelection()) { - var selectedNodes = this.getSelectedNodes(); - var selectedEdges = this.getSelectedEdges(); - this._removeEdges(selectedEdges); - this._removeNodes(selectedNodes); - this.moving = true; - this.start(); - } - else { - alert("Clusters cannot be deleted.") - } - } - - -} \ No newline at end of file diff --git a/src/util.js b/src/util.js index 8495d68c..c65682a3 100644 --- a/src/util.js +++ b/src/util.js @@ -824,4 +824,9 @@ util.HSVToHex = function HSVToHex(h,s,v) { util.hexToHSV = function hexToHSV(hex) { var rgb = util.hexToRGB(hex); return util.RGBToHSV(rgb.r,rgb.g,rgb.b); -} \ No newline at end of file +} + +util.isValidHex = function isValidHex(hex) { + var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); + return isOk; +}