diff --git a/Jakefile.js b/Jakefile.js index 5d696bee..d0b3052d 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -66,6 +66,7 @@ task('build', {async: true}, function () { './src/component/groupset.js', './src/timeline.js', + './src/graph/graph.js', './src/module/exports.js' ], diff --git a/examples/graph/01_basic_usage.html b/examples/graph/01_basic_usage.html new file mode 100644 index 00000000..a858738d --- /dev/null +++ b/examples/graph/01_basic_usage.html @@ -0,0 +1,45 @@ + + + + Graph | Basic usage + + + + + + +
+ + + + + diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html new file mode 100755 index 00000000..4471d474 --- /dev/null +++ b/examples/graph/02_random_nodes.html @@ -0,0 +1,105 @@ + + + + + Graph | Random nodes + + + + + + + + + + +
+ + + +
+
+ +
+ +

+ + diff --git a/examples/graph/03_images.html b/examples/graph/03_images.html new file mode 100755 index 00000000..f9765a42 --- /dev/null +++ b/examples/graph/03_images.html @@ -0,0 +1,78 @@ + + + + Graph | Images + + + + + + + + + + +
+ + + diff --git a/examples/graph/04_shapes.html b/examples/graph/04_shapes.html new file mode 100755 index 00000000..c8833b9b --- /dev/null +++ b/examples/graph/04_shapes.html @@ -0,0 +1,73 @@ + + + + Graph | Shapes + + + + + + + + + +
+ +
+ + diff --git a/examples/graph/05_social_network.html b/examples/graph/05_social_network.html new file mode 100644 index 00000000..41c00711 --- /dev/null +++ b/examples/graph/05_social_network.html @@ -0,0 +1,96 @@ + + + + Graph | Social Network + + + + + + + + + + +
+

+ Icons: Scrap Icons by Deleket +

+ +
+ + diff --git a/examples/graph/06_groups.html b/examples/graph/06_groups.html new file mode 100644 index 00000000..8fdefd92 --- /dev/null +++ b/examples/graph/06_groups.html @@ -0,0 +1,140 @@ + + + + Graph | Groups + + + + + + + + + + +
+ Number of groups: + + Number of nodes per group: + + +
+
+ +
+ +
+ + diff --git a/examples/graph/08_selections.html b/examples/graph/08_selections.html new file mode 100644 index 00000000..8e8985f3 --- /dev/null +++ b/examples/graph/08_selections.html @@ -0,0 +1,67 @@ + + + + Graph | Selections + + + + + + +
+
+ + + + + diff --git a/examples/graph/16_scalable_images.html b/examples/graph/16_scalable_images.html new file mode 100644 index 00000000..498aba88 --- /dev/null +++ b/examples/graph/16_scalable_images.html @@ -0,0 +1,82 @@ + + + + Graph | Scalable images + + + + + + + + + +
+ +
+ + diff --git a/examples/graph/19_dot_language.html b/examples/graph/19_dot_language.html new file mode 100644 index 00000000..06db5d67 --- /dev/null +++ b/examples/graph/19_dot_language.html @@ -0,0 +1,37 @@ + + + + Graph | DOT Language + + + + + + +
+ + + + diff --git a/examples/graph/20_dot_language_playground.html b/examples/graph/20_dot_language_playground.html new file mode 100644 index 00000000..85985865 --- /dev/null +++ b/examples/graph/20_dot_language_playground.html @@ -0,0 +1,212 @@ + + + + Graph | DOT language playground + + + + + + + + + + + + + + + + + +
+

Directed graph from data in DOT language

+
+ example 1 + example 2 + example 3 +
+
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + diff --git a/examples/graph/img/refresh-cl/Hardware-Fax-icon.png b/examples/graph/img/refresh-cl/Hardware-Fax-icon.png new file mode 100644 index 00000000..eab07c7e Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-Fax-icon.png differ diff --git a/examples/graph/img/refresh-cl/Hardware-Laptop-1-icon.png b/examples/graph/img/refresh-cl/Hardware-Laptop-1-icon.png new file mode 100644 index 00000000..5e0c2ac5 Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-Laptop-1-icon.png differ diff --git a/examples/graph/img/refresh-cl/Hardware-Mobile-Phone-icon.png b/examples/graph/img/refresh-cl/Hardware-Mobile-Phone-icon.png new file mode 100644 index 00000000..66a6d35f Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-Mobile-Phone-icon.png differ diff --git a/examples/graph/img/refresh-cl/Hardware-My-Computer-3-icon.png b/examples/graph/img/refresh-cl/Hardware-My-Computer-3-icon.png new file mode 100644 index 00000000..26ac6260 Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-My-Computer-3-icon.png differ diff --git a/examples/graph/img/refresh-cl/Hardware-My-PDA-02-icon.png b/examples/graph/img/refresh-cl/Hardware-My-PDA-02-icon.png new file mode 100644 index 00000000..a761307d Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-My-PDA-02-icon.png differ diff --git a/examples/graph/img/refresh-cl/Hardware-My-PDA-04-icon.png b/examples/graph/img/refresh-cl/Hardware-My-PDA-04-icon.png new file mode 100644 index 00000000..a1fae460 Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-My-PDA-04-icon.png differ diff --git a/examples/graph/img/refresh-cl/Hardware-My-PDA-05-icon.png b/examples/graph/img/refresh-cl/Hardware-My-PDA-05-icon.png new file mode 100644 index 00000000..d337f861 Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-My-PDA-05-icon.png differ diff --git a/examples/graph/img/refresh-cl/Hardware-My-Phone-Picture-icon.png b/examples/graph/img/refresh-cl/Hardware-My-Phone-Picture-icon.png new file mode 100644 index 00000000..5e9ea6c5 Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-My-Phone-Picture-icon.png differ diff --git a/examples/graph/img/refresh-cl/Hardware-Printer-Blue-icon.png b/examples/graph/img/refresh-cl/Hardware-Printer-Blue-icon.png new file mode 100644 index 00000000..076a542f Binary files /dev/null and b/examples/graph/img/refresh-cl/Hardware-Printer-Blue-icon.png differ diff --git a/examples/graph/img/refresh-cl/Misc-Scanner-default-icon.png b/examples/graph/img/refresh-cl/Misc-Scanner-default-icon.png new file mode 100644 index 00000000..637c619b Binary files /dev/null and b/examples/graph/img/refresh-cl/Misc-Scanner-default-icon.png differ diff --git a/examples/graph/img/refresh-cl/Network-Drive-icon.png b/examples/graph/img/refresh-cl/Network-Drive-icon.png new file mode 100644 index 00000000..e669165c Binary files /dev/null and b/examples/graph/img/refresh-cl/Network-Drive-icon.png differ diff --git a/examples/graph/img/refresh-cl/Network-Internet-Connection-icon.png b/examples/graph/img/refresh-cl/Network-Internet-Connection-icon.png new file mode 100644 index 00000000..277047cc Binary files /dev/null and b/examples/graph/img/refresh-cl/Network-Internet-Connection-icon.png differ diff --git a/examples/graph/img/refresh-cl/Network-Pipe-icon.png b/examples/graph/img/refresh-cl/Network-Pipe-icon.png new file mode 100644 index 00000000..c5f34b77 Binary files /dev/null and b/examples/graph/img/refresh-cl/Network-Pipe-icon.png differ diff --git a/examples/graph/img/refresh-cl/System-Firewall-2-icon.png b/examples/graph/img/refresh-cl/System-Firewall-2-icon.png new file mode 100644 index 00000000..e04fe6c2 Binary files /dev/null and b/examples/graph/img/refresh-cl/System-Firewall-2-icon.png differ diff --git a/examples/graph/img/refresh-cl/System-Globe-icon.png b/examples/graph/img/refresh-cl/System-Globe-icon.png new file mode 100644 index 00000000..a317665f Binary files /dev/null and b/examples/graph/img/refresh-cl/System-Globe-icon.png differ diff --git a/examples/graph/img/refresh-cl/license.txt b/examples/graph/img/refresh-cl/license.txt new file mode 100644 index 00000000..7b8b9d43 --- /dev/null +++ b/examples/graph/img/refresh-cl/license.txt @@ -0,0 +1,14 @@ +Refresh Cl icon set + +http://www.iconarchive.com/show/refresh-cl-icons-by-tpdkdesign.net.html +http://www.iconarchive.com/artist/tpdkdesign.net.html + +Artist: TpdkDesign.net +License: Free for non-commercial use. + +Name: TpdkDesign.net +URL: http://www.tpdkdesign.net +Available for custom work: No +Default License: Free for non-commercial use. +Commercial usage: Not allowed + diff --git a/examples/graph/img/soft-scraps-icons/Document-icon24.png b/examples/graph/img/soft-scraps-icons/Document-icon24.png new file mode 100644 index 00000000..c420b202 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Document-icon24.png differ diff --git a/examples/graph/img/soft-scraps-icons/Document-icon32.png b/examples/graph/img/soft-scraps-icons/Document-icon32.png new file mode 100644 index 00000000..5938c512 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Document-icon32.png differ diff --git a/examples/graph/img/soft-scraps-icons/Document-icon48.png b/examples/graph/img/soft-scraps-icons/Document-icon48.png new file mode 100644 index 00000000..5938c512 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Document-icon48.png differ diff --git a/examples/graph/img/soft-scraps-icons/Email-icon24.png b/examples/graph/img/soft-scraps-icons/Email-icon24.png new file mode 100644 index 00000000..c01c9040 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Email-icon24.png differ diff --git a/examples/graph/img/soft-scraps-icons/Email-icon32.png b/examples/graph/img/soft-scraps-icons/Email-icon32.png new file mode 100644 index 00000000..86ed5de9 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Email-icon32.png differ diff --git a/examples/graph/img/soft-scraps-icons/Email-icon48.png b/examples/graph/img/soft-scraps-icons/Email-icon48.png new file mode 100644 index 00000000..799ebb70 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Email-icon48.png differ diff --git a/examples/graph/img/soft-scraps-icons/Folder-icon24.png b/examples/graph/img/soft-scraps-icons/Folder-icon24.png new file mode 100644 index 00000000..16d05084 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Folder-icon24.png differ diff --git a/examples/graph/img/soft-scraps-icons/Folder-icon32.png b/examples/graph/img/soft-scraps-icons/Folder-icon32.png new file mode 100644 index 00000000..73111b15 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Folder-icon32.png differ diff --git a/examples/graph/img/soft-scraps-icons/Folder-icon48.png b/examples/graph/img/soft-scraps-icons/Folder-icon48.png new file mode 100644 index 00000000..94d69cad Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Folder-icon48.png differ diff --git a/examples/graph/img/soft-scraps-icons/Folder-icon64.png b/examples/graph/img/soft-scraps-icons/Folder-icon64.png new file mode 100644 index 00000000..6c5d457a Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Folder-icon64.png differ diff --git a/examples/graph/img/soft-scraps-icons/Smiley-Angry-icon.png b/examples/graph/img/soft-scraps-icons/Smiley-Angry-icon.png new file mode 100644 index 00000000..f40bc381 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Smiley-Angry-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/Smiley-Grin-icon.png b/examples/graph/img/soft-scraps-icons/Smiley-Grin-icon.png new file mode 100644 index 00000000..6ff39eb7 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/Smiley-Grin-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/User-Administrator-Blue-icon.png b/examples/graph/img/soft-scraps-icons/User-Administrator-Blue-icon.png new file mode 100644 index 00000000..ba2ffac7 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/User-Administrator-Blue-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/User-Administrator-Green-icon.png b/examples/graph/img/soft-scraps-icons/User-Administrator-Green-icon.png new file mode 100644 index 00000000..a3d31677 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/User-Administrator-Green-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/User-Coat-Blue-icon.png b/examples/graph/img/soft-scraps-icons/User-Coat-Blue-icon.png new file mode 100644 index 00000000..ff367221 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/User-Coat-Blue-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/User-Coat-Green-icon.png b/examples/graph/img/soft-scraps-icons/User-Coat-Green-icon.png new file mode 100644 index 00000000..4a4326b7 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/User-Coat-Green-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/User-Coat-Red-icon.png b/examples/graph/img/soft-scraps-icons/User-Coat-Red-icon.png new file mode 100644 index 00000000..06f0130f Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/User-Coat-Red-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/User-Executive-Green-icon.png b/examples/graph/img/soft-scraps-icons/User-Executive-Green-icon.png new file mode 100644 index 00000000..a639861c Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/User-Executive-Green-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/User-Preppy-Blue-icon.png b/examples/graph/img/soft-scraps-icons/User-Preppy-Blue-icon.png new file mode 100644 index 00000000..b3c77408 Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/User-Preppy-Blue-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/User-Preppy-Red-icon.png b/examples/graph/img/soft-scraps-icons/User-Preppy-Red-icon.png new file mode 100644 index 00000000..f569776e Binary files /dev/null and b/examples/graph/img/soft-scraps-icons/User-Preppy-Red-icon.png differ diff --git a/examples/graph/img/soft-scraps-icons/license.txt b/examples/graph/img/soft-scraps-icons/license.txt new file mode 100644 index 00000000..750841c8 --- /dev/null +++ b/examples/graph/img/soft-scraps-icons/license.txt @@ -0,0 +1,12 @@ +Scrap Icons by Deleket + +http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html + + +Artist: Deleket (Jojo Mendoza) (Available for custom work) +License: CC Attribution-Noncommercial-No Derivate 3.0 + +http://creativecommons.org/licenses/by-nc-nd/3.0/ + +Commercial usage: Allowed (Author Approval required -> Visit artist homepage for details). + diff --git a/src/graph/graph.js b/src/graph/graph.js new file mode 100644 index 00000000..2ea86a29 --- /dev/null +++ b/src/graph/graph.js @@ -0,0 +1,5876 @@ +/** + * @constructor Graph + * Create a graph visualization connecting nodes via edges. + * @param {Element} container The DOM element in which the Graph will + * be created. Normally a div element. + */ +function Graph (container) { + // create variables and set default values + this.containerElement = container; + this.width = "100%"; + this.height = "100%"; + this.refreshRate = 50; // milliseconds + this.stabilize = true; // stabilize before displaying the network + this.selectable = true; + + // set constant values + this.constants = { + "nodes": { + "radiusMin": 5, + "radiusMax": 20, + "radius": 5, + "distance": 100, // px + "style": "rect", + "image": undefined, + "widthMin": 16, // px + "widthMax": 64, // px + "fontColor": "black", + "fontSize": 14, // px + //"fontFace": "verdana", + "fontFace": "arial", + "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 + "dashlength": 10, + "dashgap": 5 + }, + "packages": { + "radius": 5, + "radiusMin": 5, + "radiusMax": 10, + "style": "dot", + "color": "#2B7CE9", + "image": undefined, + "widthMin": 16, // px + "widthMax": 64, // px + "duration": 1.0 // seconds + }, + "minForce": 0.05, + "minVelocity": 0.02, // px/s + "maxIterations": 1000 // maximum number of iteration to stabilize + }; + + this.nodes = []; // array with Node objects + this.edges = []; // array with Edge objects + this.packages = []; // array with all Package packages + this.images = new Graph.Images(); // object with images + this.groups = new Graph.Groups(); // object with groups + + // properties of the data + this.hasMovingEdges = false; // True if one or more of the edges or nodes have an animation + this.hasMovingNodes = false; // True if any of the nodes have an undefined position + this.hasMovingPackages = false; // True if there are one or more packages + + this.selection = []; + this.timer = undefined; + + // create a frame and canvas + this._create(); +}; + +/** + * Main drawing logic. This is the function that needs to be called + * in the html page, to draw the Network. + * Note that Object DataTable is defined in google.visualization.DataTable + * + * A data table with the events must be provided, and an options table. + * @param {google.visualization.DataTable | Array} [nodes] The data containing the nodes. + * @param {google.visualization.DataTable | Array} [edges] The data containing the edges. + * @param {google.visualization.DataTable | Array} [packages] The data containing the packages + * @param {Object} options A name/value map containing settings + */ +Graph.prototype.draw = function(nodes, edges, packages, options) { + var nodesTable, edgesTable, packagesTable; + // correctly read the parameters. edges and packages are optional. + if (options != undefined) { + nodesTable = nodes; + edgesTable = edges; + packagesTable = packages; + } + else if (packages != undefined) { + nodesTable = nodes; + edgesTable = edges; + packagesTable = undefined; + options = packages; + } + else if (edges != undefined) { + nodesTable = nodes; + edgesTable = undefined; + packagesTable = undefined; + options = edges; + } + else if (nodes != undefined) { + nodesTable = undefined; + edgesTable = undefined; + packagesTable = undefined; + options = nodes; + } + + if (options != undefined) { + // retrieve parameter values + if (options.width != undefined) {this.width = options.width;} + if (options.height != undefined) {this.height = options.height;} + if (options.stabilize != undefined) {this.stabilize = options.stabilize;} + if (options.selectable != undefined) {this.selectable = options.selectable;} + + // TODO: work out these options and document them + if (options.edges) { + for (var prop in options.edges) { + if (options.edges.hasOwnProperty(prop)) { + this.constants.edges[prop] = options.edges[prop]; + } + } + + if (options.edges.length != undefined && + options.nodes && options.nodes.distance == undefined) { + this.constants.edges.length = options.edges.length; + this.constants.nodes.distance = options.edges.length * 1.25; + } + + if (!options.edges.fontColor) { + this.constants.edges.fontColor = options.edges.color; + } + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (options.edges.dashlength != undefined) { + this.constants.edges.dashlength = options.edges.dashlength; + } + if (options.edges.dashgap != undefined) { + this.constants.edges.dashgap = options.edges.dashgap; + } + if (options.edges.altdashlength != undefined) { + this.constants.edges.altdashlength = options.edges.altdashlength; + } + } + if (options.nodes) { + for (prop in options.nodes) { + if (options.nodes.hasOwnProperty(prop)) { + this.constants.nodes[prop] = options.nodes[prop]; + } + } + + /* + if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; + if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; + */ + } + if (options.packages) { + for (prop in options.packages) { + if (options.packages.hasOwnProperty(prop)) { + this.constants.packages[prop] = options.packages[prop]; + } + } + + /* + if (options.packages.widthMin) this.constants.packages.radiusMin = options.packages.widthMin; + if (options.packages.widthMax) this.constants.packages.radiusMax = options.packages.widthMax; + */ + } + + if (options.groups) { + for (var groupname in options.groups) { + if (options.groups.hasOwnProperty(groupname)) { + var group = options.groups[groupname]; + this.groups.add(groupname, group); + } + } + } + } + + this._setBackgroundColor(options.backgroundColor); + + this._setSize(this.width, this.height); + this._setTranslation(0, 0); + this._setScale(1.0); + + // set all data + this.hasTimestamps = false; + this.setNodes(nodesTable); + this.setEdges(edgesTable); + this.setPackages(packagesTable); + + this._reposition(); // TODO: bad solution + if (this.stabilize) { + this._doStabilize(); + } + this.start(); + + // create an onload callback method for the images + var network = this; + var callback = function () { + network._redraw(); + }; + this.images.setOnloadCallback(callback); + + // fire the ready event + this.trigger('ready'); +}; + +/** + * fire an event + * @param {String} event The name of an event, for example "select" or "ready" + * @param {Object} params Optional object with event parameters + */ +Graph.prototype.trigger = function (event, params) { + // trigger the edges event bus + events.trigger(this, event, params); + + // trigger the google event bus + if (typeof google !== 'undefined' && google.visualization && google.visualization.events) { + google.visualization.events.trigger(this, event, params); + } +}; + + +/** + * Create the main frame for the Network. + * This function is executed once when a Network object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + */ +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 = "network-frame"; + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; + + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement( "canvas" ); + this.frame.canvas.style.position = "relative"; + this.frame.appendChild(this.frame.canvas); + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement( "DIV" ); + noCanvas.style.color = "red"; + noCanvas.style.fontWeight = "bold" ; + noCanvas.style.padding = "10px"; + noCanvas.innerHTML = "Error: your browser does not support HTML canvas"; + this.frame.canvas.appendChild(noCanvas); + } + + // create event listeners + var me = this; + var onmousedown = function (event) {me._onMouseDown(event);}; + var onmousemove = function (event) {me._onMouseMoveTitle(event);}; + var onmousewheel = function (event) {me._onMouseWheel(event);}; + var ontouchstart = function (event) {me._onTouchStart(event);}; + Graph.addEventListener(this.frame.canvas, "mousedown", onmousedown); + Graph.addEventListener(this.frame.canvas, "mousemove", onmousemove); + Graph.addEventListener(this.frame.canvas, "mousewheel", onmousewheel); + Graph.addEventListener(this.frame.canvas, "touchstart", ontouchstart); + + // add the frame to the container element + this.containerElement.appendChild(this.frame); +}; + +/** + * Set the background and border styling for the graph + * @param {String | Object} backgroundColor + */ +Graph.prototype._setBackgroundColor = function(backgroundColor) { + var fill = "white"; + var stroke = "lightgray"; + var strokeWidth = 1; + + if (typeof(backgroundColor) == "string") { + fill = backgroundColor; + stroke = "none"; + strokeWidth = 0; + } + else if (typeof(backgroundColor) == "object") { + if (backgroundColor.fill != undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke != undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth != undefined) strokeWidth = backgroundColor.strokeWidth; + } + else if (backgroundColor == undefined) { + // use use defaults + } + else { + throw "Unsupported type of backgroundColor"; + } + + this.frame.style.boxSizing = 'border-box'; + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + "px"; + this.frame.style.borderStyle = "solid"; +}; + + +/** + * handle on mouse down event + */ +Graph.prototype._onMouseDown = function (event) { + event = event || window.event; + + if (!this.selectable) { + return; + } + + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); + } + + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); + if (!this.leftButtonDown && !this.touchDown) { + return; + } + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the timeline, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + if (!this.onmousemove) { + this.onmousemove = function (event) {me._onMouseMove(event);}; + Graph.addEventListener(document, "mousemove", me.onmousemove); + } + if (!this.onmouseup) { + this.onmouseup = function (event) {me._onMouseUp(event);}; + Graph.addEventListener(document, "mouseup", me.onmouseup); + } + Graph.preventDefault(event); + + // store the start x and y position of the mouse + this.startMouseX = event.clientX || event.targetTouches[0].clientX; + this.startMouseY = event.clientY || event.targetTouches[0].clientY; + this.startFrameLeft = Graph._getAbsoluteLeft(this.frame.canvas); + this.startFrameTop = Graph._getAbsoluteTop(this.frame.canvas); + this.startTranslation = this._getTranslation(); + + this.ctrlKeyDown = event.ctrlKey; + this.shiftKeyDown = event.shiftKey; + + var obj = { + "left" : this._xToCanvas(this.startMouseX - this.startFrameLeft), + "top" : this._yToCanvas(this.startMouseY - this.startFrameTop), + "right" : this._xToCanvas(this.startMouseX - this.startFrameLeft), + "bottom" : this._yToCanvas(this.startMouseY - this.startFrameTop) + }; + var overlappingNodes = this._getNodesOverlappingWith(obj); + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + this.startClickedObj = (overlappingNodes.length > 0) ? + overlappingNodes[overlappingNodes.length - 1] : undefined; + + if (this.startClickedObj) { + // move clicked node with the mouse + + // make the clicked node temporarily fixed, and store their original state + var node = this.nodes[this.startClickedObj.row]; + this.startClickedObj.xFixed = node.xFixed; + this.startClickedObj.yFixed = node.yFixed; + node.xFixed = true; + node.yFixed = true; + + if (!this.ctrlKeyDown || !node.isSelected()) { + // select this node + this._selectNodes([this.startClickedObj], this.ctrlKeyDown); + } + else { + // unselect this node + this._unselectNodes([this.startClickedObj]); + } + + if (!this.hasMovingNodes) { + this._redraw(); + } + } + else if (this.shiftKeyDown) { + // start selection of multiple nodes + } + else { + // start moving the graph + this.moved = false; + } +}; + +/** + * handle on mouse move event + */ +Graph.prototype._onMouseMove = function (event) { + event = event || window.event; + + if (!this.selectable) { + return; + } + + var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0; + var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0; + this.mouseX = mouseX; + this.mouseY = mouseY; + + if (this.startClickedObj) { + var node = this.nodes[this.startClickedObj.row]; + + if (!this.startClickedObj.xFixed) + node.x = this._xToCanvas(mouseX - this.startFrameLeft); + + if (!this.startClickedObj.yFixed) + node.y = this._yToCanvas(mouseY - this.startFrameTop); + + // start animation if not yet running + if (!this.hasMovingNodes) { + this.hasMovingNodes = true; + this.start(); + } + } + else if (this.shiftKeyDown) { + // draw a rect from start mouse location to current mouse location + if (this.frame.selRect == undefined) { + this.frame.selRect = document.createElement("DIV"); + this.frame.appendChild(this.frame.selRect); + + this.frame.selRect.style.position = "absolute"; + this.frame.selRect.style.border = "1px dashed red"; + } + + var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft; + var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop; + var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft; + var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop; + + this.frame.selRect.style.left = left + "px"; + this.frame.selRect.style.top = top + "px"; + this.frame.selRect.style.width = (right - left) + "px"; + this.frame.selRect.style.height = (bottom - top) + "px"; + } + else { + // move the network + var diffX = mouseX - this.startMouseX; + var diffY = mouseY - this.startMouseY; + + this._setTranslation( + this.startTranslation.x + diffX, + this.startTranslation.y + diffY); + this._redraw(); + + this.moved = true; + } + + Graph.preventDefault(event); +}; + +/** + * handle on mouse up event + */ +Graph.prototype._onMouseUp = function (event) { + event = event || window.event; + + if (!this.selectable) { + return; + } + + // remove event listeners here, important for Safari + if (this.onmousemove) { + Graph.removeEventListener(document, "mousemove", this.onmousemove); + this.onmousemove = undefined; + } + if (this.onmouseup) { + Graph.removeEventListener(document, "mouseup", this.onmouseup); + this.onmouseup = undefined; + } + Graph.preventDefault(event); + + // check selected nodes + var endMouseX = event.clientX || this.mouseX || 0; + var endMouseY = event.clientY || this.mouseY || 0; + + var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey; + + if (this.startClickedObj) { + // restore the original fixed state + var node = this.nodes[this.startClickedObj.row]; + node.xFixed = this.startClickedObj.xFixed; + node.yFixed = this.startClickedObj.yFixed; + } + else if (this.shiftKeyDown) { + // select nodes inside selection area + var obj = { + "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft), + "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop), + "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft), + "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop) + }; + var overlappingNodes = this._getNodesOverlappingWith(obj); + this._selectNodes(overlappingNodes, ctrlKey); + this.redraw(); + + // remove the selection rectangle + if (this.frame.selRect) { + this.frame.removeChild(this.frame.selRect); + this.frame.selRect = undefined; + } + } + else { + if (!this.ctrlKeyDown && !this.moved) { + // remove selection + this._unselectNodes(); + this._redraw(); + } + } + + this.leftButtonDown = false; + this.ctrlKeyDown = false; +}; + + +/** + * Event handler for mouse wheel event, used to zoom the timeline + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event + */ +Graph.prototype._onMouseWheel = function(event) { + event = event || window.event; + var mouseX = event.clientX; + var mouseY = event.clientY; + + // 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) { + // determine zoom factor, and adjust the zoom factor such that zooming in + // and zooming out correspond wich each other + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + + var scaleOld = this._getScale(); + var scaleNew = scaleOld * (1 + zoom); + if (scaleNew < 0.01) { + scaleNew = 0.01; + } + if (scaleNew > 10) { + scaleNew = 10; + } + + var frameLeft = Graph._getAbsoluteLeft(this.frame.canvas); + var frameTop = Graph._getAbsoluteTop(this.frame.canvas); + var x = mouseX - frameLeft; + var y = mouseY - frameTop; + + var translation = this._getTranslation(); + var scaleFrac = scaleNew / scaleOld; + var tx = (1 - scaleFrac) * x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * y + translation.y * scaleFrac; + + this._setScale(scaleNew); + this._setTranslation(tx, ty); + this._redraw(); + } + + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here... + Graph.preventDefault(event); +}; + + +/** + * Mouse move handler for checking whether the title moves over a node or + * package with a title. + */ +Graph.prototype._onMouseMoveTitle = function (event) { + event = event || window.event; + + var startMouseX = event.clientX; + var startMouseY = event.clientY; + this.startFrameLeft = this.startFrameLeft || Graph._getAbsoluteLeft(this.frame.canvas); + this.startFrameTop = this.startFrameTop || Graph._getAbsoluteTop(this.frame.canvas); + + var x = startMouseX - this.startFrameLeft; + var y = startMouseY - this.startFrameTop; + + // check if the previously selected node is still selected + if (this.popupNode) { + this._checkHidePopup(x, y); + } + + // start a timeout that will check if the mouse is positioned above + // an element + var me = this; + var checkShow = function() { + me._checkShowPopup(x, y); + }; + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running timer + } + if (!this.leftButtonDown) { + this.popupTimer = setTimeout(checkShow, 300); + } +}; + +/** + * Check if there is an element on the given position in the network ( + * (a node, package, or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {number} x + * @param {number} y + */ +Graph.prototype._checkShowPopup = function (x, y) { + var obj = { + "left" : this._xToCanvas(x), + "top" : this._yToCanvas(y), + "right" : this._xToCanvas(x), + "bottom" : this._yToCanvas(y) + }; + + var i, len; + var lastPopupNode = this.popupNode; + + if (this.popupNode == undefined) { + // search the packages for overlap + + for (i = 0, len = this.packages.length; i < len; i++) { + var p = this.packages[i]; + if (p.getTitle() != undefined && p.isOverlappingWith(obj)) { + this.popupNode = p; + break; + } + } + } + + if (this.popupNode == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.nodes; + for (i = nodes.length - 1; i >= 0; i--) { + var node = nodes[i]; + if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { + this.popupNode = node; + break; + } + } + } + + if (this.popupNode == undefined) { + // search the edges for overlap + var allEdges = this.edges; + for (i = 0, len = allEdges.length; i < len; i++) { + var edge = allEdges[i]; + if (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 Graph.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(x - 3, 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 {number} x + * @param {number} y + */ +Graph.prototype._checkHidePopup = function (x, y) { + var obj = { + "left" : x, + "top" : y, + "right" : x, + "bottom" : y + }; + + if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) { + this.popupNode = undefined; + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Event handler for touchstart event on mobile devices + */ +Graph.prototype._onTouchStart = function(event) { + Graph.preventDefault(event); + + if (this.touchDown) { + // if already moving, return + return; + } + this.touchDown = true; + + var me = this; + if (!this.ontouchmove) { + this.ontouchmove = function (event) {me._onTouchMove(event);}; + Graph.addEventListener(document, "touchmove", this.ontouchmove); + } + if (!this.ontouchend) { + this.ontouchend = function (event) {me._onTouchEnd(event);}; + Graph.addEventListener(document, "touchend", this.ontouchend); + } + + this._onMouseDown(event); +}; + +/** + * Event handler for touchmove event on mobile devices + */ +Graph.prototype._onTouchMove = function(event) { + Graph.preventDefault(event); + this._onMouseMove(event); +}; + +/** + * Event handler for touchend event on mobile devices + */ +Graph.prototype._onTouchEnd = function(event) { + Graph.preventDefault(event); + + this.touchDown = false; + + if (this.ontouchmove) { + Graph.removeEventListener(document, "touchmove", this.ontouchmove); + this.ontouchmove = undefined; + } + if (this.ontouchend) { + Graph.removeEventListener(document, "touchend", this.ontouchend); + this.ontouchend = undefined; + } + + this._onMouseUp(event); +}; + + +/** + * Unselect selected nodes. If no selection array is provided, all nodes + * are unselected + * @param {Object[]} selection Array with selection objects, each selection + * object has a parameter row. Optional + * @param {Boolean} triggerSelect If true (default), the select event + * is triggered when nodes are unselected + * @return {Boolean} changed True if the selection is changed + */ +Graph.prototype._unselectNodes = function(selection, triggerSelect) { + var changed = false; + var i, iMax, row; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + row = selection[i].row; + this.nodes[row].unselect(); + + var j = 0; + while (j < this.selection.length) { + if (this.selection[j].row == row) { + this.selection.splice(j, 1); + changed = true; + } + else { + j++; + } + } + } + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + row = this.selection[i].row; + this.nodes[row].unselect(); + changed = true; + } + this.selection = []; + } + + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this.trigger('select'); + } + + return changed; +}; + +/** + * select all nodes on given location x, y + * @param {Array} selection an array with selection objects. Each selection + * object has a parameter row + * @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 + */ +Graph.prototype._selectNodes = function(selection, append) { + var changed = false; + var i, iMax; + + // TODO: the selectNodes method is a little messy, rework this + + // check if the current selection equals the desired selection + var selectionAlreadyDone = true; + if (selection.length != this.selection.length) { + selectionAlreadyDone = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i].row != this.selection[i].row) { + selectionAlreadyDone = false; + break; + } + } + } + if (selectionAlreadyDone) { + return changed; + } + + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } + + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var row = selection[i].row; + var isDuplicate = false; + for (var j = 0, jMax = this.selection.length; j < jMax; j++) { + if (this.selection[j].row == row) { + isDuplicate = true; + break; + } + } + + if (!isDuplicate) { + this.nodes[row].select(); + this.selection.push(selection[i]); + changed = true; + } + } + + if (changed) { + // fire the select event + this.trigger('select'); + } + + return changed; +}; + +/** + * retrieve all nodes overlapping with given object + * @param {Object} obj An object with parameters left, top, right, bottom + * @return {Object[]} An array with selection objects containing + * the parameter row. + */ +Graph.prototype._getNodesOverlappingWith = function (obj) { + var overlappingNodes = []; + + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i].isOverlappingWith(obj)) { + var sel = {"row": i}; + overlappingNodes.push(sel); + } + } + + return overlappingNodes; +}; + +/** + * retrieve the currently selected nodes + * @return {Object[]} an array with zero or more objects. Each object + * contains the parameter row + */ +Graph.prototype.getSelection = function() { + var selection = []; + + for (var i = 0; i < this.selection.length; i++) { + var row = this.selection[i].row; + selection.push({"row": row}); + } + + return selection; +}; + +/** + * select zero or more nodes + * @param {object[]} selection an array with zero or more objects. Each object + * contains the parameter row + */ +Graph.prototype.setSelection = function(selection) { + var i, iMax, row; + + if (selection.length == undefined) + throw "Selection must be an array with objects"; + + // first unselect any selected node + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + row = this.selection[i].row; + this.nodes[row].unselect(); + } + + this.selection = []; + + for (i = 0, iMax = selection.length; i < iMax; i++) { + row = selection[i].row; + + if (row == undefined) + throw "Parameter row missing in selection object"; + if (row > this.nodes.length-1) + throw "Parameter row out of range"; + + var sel = {"row": row}; + this.selection.push(sel); + this.nodes[row].select(); + } + + this.redraw(); +}; + + +/** + * 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 + */ +Graph.prototype._getConnectionCount = function(level) { + var conn = this.edges; + 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 + for (var i = 0, iMax = conn.length; i < iMax; i++) { + var other = null; + + // check if connected + if (conn[i].from == node) + other = conn[i].to; + else if (conn[i].to == node) + other = conn[i].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 level0 = []; + var nodes = this.nodes; + var i, iMax; + for (i = 0, iMax = nodes.length; i < iMax; i++) { + var c = [nodes[i]]; + for (var l = 0; l < level; l++) { + c = c.concat(getConnectedNodes(c)); + } + connections.push(c); + } + + var hubs = []; + for (i = 0, len = connections.length; i < len; i++) { + hubs.push(connections[i].length); + } + + return hubs; +}; + + +/** + * Set a new size for the network + * @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.slider) { + this.slider.redraw(); + } +}; + +/** + * Convert a Google DataTable to a Javascript Array + * @param {google.visualization.DataTable} table + * @return {Array} array + */ +Graph.tableToArray = function(table) { + var array = []; + var col; + + // read the column names + var colCount = table.getNumberOfColumns(); + var cols = {}; + for (col = 0; col < colCount; col++) { + var label = table.getColumnLabel(col); + cols[label] = col; + } + + var rowCount = table.getNumberOfRows(); + for (var i = 0; i < rowCount; i++) { + // copy all properties from the table columns to an object + var properties = {}; + for (col in cols) { + if (cols.hasOwnProperty(col)) { + properties[col] = table.getValue(i, cols[col]); + } + } + + array.push(properties); + } + + return array; +}; + +/** + * Append nodes + * Nodes with a duplicate id will be replaced + * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes. + */ +Graph.prototype.addNodes = function(nodesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + nodesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(nodesTable); + } + else if (Graph.isArray(nodesTable)){ + // Javascript Array + table = nodesTable; + } + else { + return; + } + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.value != undefined) { + hasValues = true; + } + if (properties.id == undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + + this._createNode(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.nodes); + } + + this.start(); +}; + +/** + * Load all nodes by reading the data table nodesTable + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes. + */ +Graph.prototype.setNodes = function(nodesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + nodesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(nodesTable); + } + else if (Graph.isArray(nodesTable)){ + // Javascript Array + table = nodesTable; + } + else { + return; + } + + this.hasMovingNodes = false; + this.nodesTable = table; + this.nodes = []; + this.selection = []; + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.value != undefined) { + hasValues = true; + } + if (properties.timestamp) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + if (properties.id == undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + this._createNode(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.nodes); + } +}; + +/** + * Filter the current nodes table for nodes with a timestamp older than given + * timestamp. Can only be used for nodes added via setNodes(), not via + * addNodes(). + * @param {*} [timestamp] If timestamp is undefined, all nodes are shown + */ +Graph.prototype._filterNodes = function(timestamp) { + if (this.nodesTable == undefined) { + return; + } + + // remove existing nodes with a too new timestamp + if (timestamp !== undefined) { + var ns = this.nodes; + var n = 0; + while (n < ns.length) { + var t = ns[n].timestamp; + if (t !== undefined && t > timestamp) { + // remove this node + ns.splice(n, 1); + } + else { + n++; + } + } + } + + // add all nodes with an old enough timestamp + var table = this.nodesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + // copy all properties + var properties = table[i]; + + if (properties.id === undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + + // check what the timestamp is + var ts = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (ts !== undefined && timestamp !== undefined && ts > timestamp) { + visible = false; + } + + if (visible) { + // create or update the node + this._createNode(properties); + } + } + + this.start(); +}; + +/** + * Create a node with the given properties + * If the new node has an id identical to an existing package, the existing + * node will be overwritten. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createNode = function(properties) { + var action = properties.action ? properties.action : "update"; + var id, index, newNode, oldNode; + + if (action === "create") { + // create the node + newNode = new Graph.Node(properties, this.images, this.groups, this.constants); + id = properties.id; + index = (id !== undefined) ? this._findNode(id) : undefined; + + if (index !== undefined) { + // replace node + oldNode = this.nodes[index]; + this.nodes[index] = newNode; + + // remove selection of old node + if (oldNode.selected) { + this._unselectNodes([{'row': index}], false); + } + + /* TODO: implement this? -> will give performance issues, searching all edges and node... + // update edges linking to this node + var edgesTable = this.edges; + for (var i = 0, iMax = edgesTable.length; i < iMax; i++) { + var edge = edgesTable[i]; + if (edge.from == oldNode) { + edge.from = newNode; + } + if (edge.to == oldNode) { + edge.to = newNode; + } + } + + // update packages linking to this node + var packagesTable = this.packages; + for (var i = 0, iMax = packagesTable.length; i < iMax; i++) { + var package = packagesTable[i]; + if (package.from == oldNode) { + package.from = newNode; + } + if (package.to == oldNode) { + package.to = newNode; + } + } + */ + } + else { + // add new node + this.nodes.push(newNode); + } + + if (!newNode.isFixed()) { + // 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.hasMovingNodes = true; + } + } + else if (action === "update") { + // update existing node, or create it when not yet existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a node without id"; + } + + index = this._findNode(id); + if (index !== undefined) { + // update node + this.nodes[index].setProperties(properties, this.constants); + } + else { + // create node + newNode = new Graph.Node(properties, this.images, this.groups, this.constants); + this.nodes.push(newNode); + + if (!newNode.isFixed()) { + // 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.hasMovingNodes = true; + } + } + } + else if (action === "delete") { + // delete existing node + id = properties.id; + if (id === undefined) { + throw "Cannot delete node without its id"; + } + + index = this._findNode(id); + if (index !== undefined) { + oldNode = this.nodes[index]; + // remove selection of old node + if (oldNode.selected) { + this._unselectNodes([{'row': index}], false); + } + this.nodes.splice(index, 1); + } + else { + throw "Node with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Find a node by its id + * @param {Number} id Id of the node + * @return {Number} index Index of the node in the array this.nodes, or + * undefined when not found. * + */ +Graph.prototype._findNode = function (id) { + var nodes = this.nodes; + for (var n = 0, len = nodes.length; n < len; n++) { + if (nodes[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a node by its rowNumber + * @param {Number} row Row number of the node + * @return {Graph.Node} node     The node with the given row number, or + *                            undefined when not found. + */ +Graph.prototype._findNodeByRow = function (row) { + return this.nodes[row]; +}; + +/** + * Load edges by reading the data table + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges. + */ +Graph.prototype.setEdges = function(edgesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + edgesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(edgesTable); + } + else if (Graph.isArray(edgesTable)){ + // Javascript Array + table = edgesTable; + } + else { + return; + } + + this.edgesTable = table; + this.edges = []; + this.hasMovingEdges = false; + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + if (properties.timestamp != undefined) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + if (properties.value != undefined) { + hasValues = true; + } + + this._createEdge(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.edges); + } +}; + + +/** + * Load edges by reading the data table + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges. + */ +Graph.prototype.addEdges = function(edgesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + edgesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(edgesTable); + } + else if (Graph.isArray(edgesTable)){ + // Javascript Array + table = edgesTable; + } + else { + return; + } + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + // copy all properties + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + if (properties.value != undefined) { + hasValues = true; + } + + this._createEdge(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.edges); + } + + this.start(); +}; + + +/** + * Filter the current edges table for edges with a timestamp below given + * timestamp. Can only be used for edges added via setEdges(), not via + * addEdges(). + * @param {*} [timestamp] If timestamp is undefined, all edges are shown + */ +Graph.prototype._filterEdges = function(timestamp) { + if (this.edgesTable == undefined) { + return; + } + + // remove existing packages with a too new timestamp + if (timestamp !== undefined) { + var ls = this.edges; + var l = 0; + while (l < ls.length) { + var t = ls[l].timestamp; + if (t !== undefined && t > timestamp) { + // remove this edge + ls.splice(l, 1); + } + else { + l++; + } + } + } + + // add all edges with an old enough timestamp + var table = this.edgesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + + // check what the timestamp is + var ts = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (ts !== undefined && timestamp !== undefined && ts > timestamp) { + visible = false; + } + + if (visible) { + // create or update the edge + this._createEdge(properties); + } + } + + this.start(); +}; + +/** + * Create a edge with the given properties + * If the new edge has an id identical to an existing edge, the existing + * edge will be overwritten or updated. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createEdge = function(properties) { + var action = properties.action ? properties.action : "create"; + var id, index, edge, oldEdge, newEdge; + + if (action === "create") { + // create the edge, or replace it if already existing + id = properties.id; + index = (id !== undefined) ? this._findEdge(id) : undefined; + edge = new Graph.Edge(properties, this, this.constants); + + if (index !== undefined) { + // replace existing edge + oldEdge = this.edges[index]; + oldEdge.from.detachEdge(oldEdge); + oldEdge.to.detachEdge(oldEdge); + this.edges[index] = edge; + } + else { + // add new edge + this.edges.push(edge); + } + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + + if (edge.isMoving()) { + this.hasMovingEdges = true; + } + } + else if (action === "update") { + // update existing edge, or create the edge if not existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a edge without id"; + } + + index = this._findEdge(id); + if (index !== undefined) { + // update edge + edge = this.edges[index]; + edge.from.detachEdge(edge); + edge.to.detachEdge(edge); + + edge.setProperties(properties, this.constants); + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + } + else { + // add new edge + edge = new Graph.Edge(properties, this, this.constants); + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + this.edges.push(edge); + if (edge.isMoving()) { + this.hasMovingEdges = true; + } + } + } + else if (action === "delete") { + // delete existing edge + id = properties.id; + if (id === undefined) { + throw "Cannot delete edge without its id"; + } + + index = this._findEdge(id); + if (index !== undefined) { + oldEdge = this.edges[id]; + edge.from.detachEdge(oldEdge); + edge.to.detachEdge(oldEdge); + this.edges.splice(index, 1); + } + else { + throw "Edge with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Update the edge to oldNode in all edges and packages. + * @param {Node} oldNode + * @param {Node} newNode + */ +// TODO: start utilizing this method _updateNodeReferences +Graph.prototype._updateNodeReferences = function(oldNode, newNode) { + var arrays = [this.edges, this.packages]; + for (var a = 0, aMax = arrays.length; a < aMax; a++) { + var array = arrays[a]; + for (var i = 0, iMax = array.length; i < iMax; i++) { + if (array.from === oldNode) { + array.from = newNode; + } + if (array.to === oldNode) { + array.to = newNode; + } + } + } +}; + +/** + * Find a edge by its id + * @param {Number} id Id of the edge + * @return {Number} index Index of the edge in the array this.edges, or + * undefined when not found. * + */ +Graph.prototype._findEdge = function (id) { + var edges = this.edges; + for (var n = 0, len = edges.length; n < len; n++) { + if (edges[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a edge by its row + * @param {Number} row Row of the edge + * @return {Graph.Edge} the found edge, or undefined when not found + */ +Graph.prototype._findEdgeByRow = function (row) { + return this.edges[row]; +}; + +/** + * Append packages + * Packages with a duplicate id will be replaced + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages. + */ +Graph.prototype.addPackages = function(packagesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + packagesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(packagesTable); + } + else if (Graph.isArray(packagesTable)){ + // Javascript Array + table = packagesTable; + } + else { + return; + } + + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + + this._createPackage(properties); + } + + // calculate scaling function when value is provided + this._updateValueRange(this.packages); + + this.start(); +}; + +/** + * Set a new packages table + * Packages with a duplicate id will be replaced + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages. + */ +Graph.prototype.setPackages = function(packagesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + packagesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(packagesTable); + } + else if (Graph.isArray(packagesTable)){ + // Javascript Array + table = packagesTable; + } + else { + return; + } + + this.packagesTable = table; + this.packages = []; + + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + if (properties.timestamp) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + + this._createPackage(properties); + } + + // calculate scaling function when value is provided + this._updateValueRange(this.packages); + + /* TODO: adjust examples and documentation for this? + this.start(); + */ +}; + +/** + * Filter the current package table for packages with a timestamp below given + * timestamp. Can only be used for packages added via setPackages(), not via + * addPackages(). + * @param {*} [timestamp] If timestamp is undefined, all packages are shown + */ +Graph.prototype._filterPackages = function(timestamp) { + if (this.packagesTable == undefined) { + return; + } + + // remove all current packages + this.packages = []; + + /* TODO: cleanup + // remove existing packages with a too new timestamp + if (timestamp !== undefined) { + var packages = this.packages; + var p = 0; + while (p < packages.length) { + var package = packages[p]; + var t = package.timestamp; + + if (t !== undefined && t > timestamp ) { + // remove this package + packages.splice(p, 1); + } + else { + p++; + } + } + } + */ + + // add all packages with an old enough timestamp + var table = this.packagesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + // check what the timestamp is + var pTimestamp = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (pTimestamp !== undefined && timestamp !== undefined && pTimestamp > timestamp) { + visible = false; + } + + if (visible === true) { + if (properties.progress == undefined) { + // when no progress is provided, we need to add our own progress + var duration = properties.duration || this.constants.packages.duration; // seconds + + var diff = (timestamp.getTime() - pTimestamp.getTime()) / 1000; // seconds + if (diff < duration) { + // copy the properties, and fill in the current progress based on the + // timestamp and the duration + var original = properties; + properties = {}; + for (var j in original) { + if (original.hasOwnProperty(j)) { + properties[j] = original[j]; + } + } + + properties.progress = diff / duration; // scale 0-1 + } + else { + visible = false; + } + } + } + + if (visible === true) { + // create or update the package + this._createPackage(properties); + } + } + + this.start(); +}; + +/** + * Create a package with the given properties + * If the new package has an id identical to an existing package, the existing + * package will be overwritten. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createPackage = function(properties) { + var action = properties.action ? properties.action : "create"; + var id, index, newPackage; + + if (action === "create") { + // create the package + id = properties.id; + index = (id !== undefined) ? this._findPackage(id) : undefined; + newPackage = new Graph.Package(properties, this, this.images, this.constants); + + if (index !== undefined) { + // replace existing package + this.packages[index] = newPackage; + } + else { + // add new package + this.packages.push(newPackage); + } + + if (newPackage.isMoving()) { + this.hasMovingPackages = true; + } + } + else if (action === "update") { + // update a package, or create it when not existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a edge without id"; + } + + index = this._findPackage(id); + if (index !== undefined) { + // update existing package + this.packages[index].setProperties(properties, this.constants); + } + else { + // add new package + newPackage = new Graph.Package(properties, this, this.images, this.constants); + this.packages.push(newPackage); + if (newPackage.isMoving()) { + this.hasMovingPackages = true; + } + } + } + else if (action === "delete") { + // delete existing package + id = properties.id; + if (id === undefined) { + throw "Cannot delete package without its id"; + } + + index = this._findPackage(id); + if (index !== undefined) { + this.packages.splice(index, 1); + } + else { + throw "Package with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Find a package by its id. + * @param {Number} id + * @return {Number} index Index of the package in the array this.packages, + * or undefined when not found + */ +Graph.prototype._findPackage = function (id) { + var packages = this.packages; + for (var n = 0, len = packages.length; n < len; n++) { + if (packages[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a package by its row + * @param {Number} row Row of the package + * @return {Graph.Package} the found package, or undefined when not found + */ +Graph.prototype._findPackageByRow = function (row) { + return this.packages[row]; +}; + +/** + * Retrieve an object which maps the column ids by their names + * For example a table with columns [id, name, value] will return an + * object {"id": 0, "name": 1, "value": 2} + * @param {google.visualization.DataTable} table A google datatable + * @return {Object} columnIds An object + */ +// TODO: cleanup this unused method +Graph.prototype._getColumnNames = function (table) { + var colCount = table.getNumberOfColumns(); + var cols = {}; + for (var col = 0; col < colCount; col++) { + var label = table.getColumnLabel(col); + cols[label] = col; + } + return cols; +}; + + +/** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Array} array. An array with objects like Edges, Nodes, or Packages + * The objects must have a method getValue() and + * setValueRange(min, max). + */ +Graph.prototype._updateValueRange = function(array) { + var count = array.length; + var i; + + // determine the range of the node values + var valueMin = undefined; + var valueMax = undefined; + for (i = 0; i < count; i++) { + var value = array[i].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 nodes + if (valueMin !== undefined && valueMax !== undefined) { + for (i = 0; i < count; i++) { + array[i].setValueRange(valueMin, valueMax); + } + } +}; + + +/** + * Set the current timestamp. All packages with a timestamp smaller or equal + * than the given timestamp will be drawn. + * @param {Date | Number} timestamp + */ +Graph.prototype.setTimestamp = function(timestamp) { + this._filterNodes(timestamp); + this._filterEdges(timestamp); + this._filterPackages(timestamp); +}; + + +/** + * Get the range of all timestamps defined in the nodes, edges and packages + * @return {Object} A range object, containing parameters start and end. + */ +Graph.prototype._getRange = function() { + // range is stored as number. at the end of the method, it is converted to + // Date when needed. + var range = { + "start": undefined, + "end": undefined + }; + + var tables = [this.nodesTable, this.edgesTable]; + for (var t = 0, tMax = tables.length; t < tMax; t++) { + var table = tables[t]; + + if (table !== undefined) { + for (var i = 0, iMax = table.length; i < iMax; i++) { + var timestamp = table[i].timestamp; + if (timestamp) { + // to long + if (timestamp instanceof Date) { + timestamp = timestamp.getTime(); + } + + // calculate new range + range.start = range.start ? Math.min(timestamp, range.start) : timestamp; + range.end = range.end ? Math.max(timestamp, range.end) : timestamp; + } + } + } + } + + // calculate the range for the packagesTable by hand. In case of packages + // without a progress provided, we need to calculate the end time by hand. + if (this.packagesTable) { + var packagesTable = this.packagesTable; + for (var row = 0, len = packagesTable.length; row < len; row ++) { + var pkg = packagesTable[row], + timestamp = pkg.timestamp, + progress = pkg.progress, + duration = pkg.duration || this.constants.packages.duration; + + // convert to number + if (timestamp instanceof Date) { + timestamp = timestamp.getTime(); + } + + if (timestamp != undefined) { + var start = timestamp, + end = progress ? timestamp : (timestamp + duration * 1000); + + range.start = range.start ? Math.min(start, range.start) : start; + range.end = range.end ? Math.max(end, range.end) : end; + } + } + } + + // convert to the right type: number or date + var rangeFormat = { + "start": new Date(range.start), + "end": new Date(range.end) + }; + + return rangeFormat; +}; + +/** + * Start animation. + * Only applicable when packages with a timestamp are available + */ +Graph.prototype.animationStart = function() { + if (this.slider) { + this.slider.play(); + } +}; + +/** + * Start animation. + * Only applicable when packages with a timestamp are available + */ +Graph.prototype.animationStop = function() { + if (this.slider) { + this.slider.stop(); + } +}; + +/** + * Set framerate for the animation. + * Only applicable when packages with a timestamp are available + * @param {number} framerate The framerate in frames per second + */ +Graph.prototype.setAnimationFramerate = function(framerate) { + if (this.slider) { + this.slider.setFramerate(framerate); + } +} + +/** + * Set the duration of playing the whole package history + * Only applicable when packages with a timestamp are available + * @param {number} duration The duration in seconds + */ +Graph.prototype.setAnimationDuration = function(duration) { + if (this.slider) { + this.slider.setDuration(duration); + } +}; + +/** + * Set the time acceleration for playing the history. + * Only applicable when packages with a timestamp are available + * @param {number} acceleration Acceleration, for example 10 means play + * ten times as fast as real time. A value + * of 1 will play the history in real time. + */ +Graph.prototype.setAnimationAcceleration = function(acceleration) { + if (this.slider) { + this.slider.setAcceleration(acceleration); + } +}; + +/** + * 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 + */ +Graph.prototype._redraw = function() { + var ctx = this.frame.canvas.getContext("2d"); + + // clear the canvas + var w = this.frame.canvas.width; + var h = this.frame.canvas.height; + ctx.clearRect(0, 0, w, h); + + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); + + this._drawEdges(ctx); + this._drawNodes(ctx); + this._drawPackages(ctx); + this._drawSlider(); + + // restore original scaling and translation + ctx.restore(); +}; + +/** + * Set the translation of the graph + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset + */ +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 + */ +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 + */ +Graph.prototype._setScale = function(scale) { + this.scale = scale; +}; +/** + * Get the current scale of the graph + * @return {Number} scale Scaling factor 1.0 is unscaled + */ +Graph.prototype._getScale = function() { + return this.scale; +}; + +Graph.prototype._xToCanvas = function(x) { + return (x - this.translation.x) / this.scale; +}; + +Graph.prototype._canvasToX = function(x) { + return x * this.scale + this.translation.x; +}; + +Graph.prototype._yToCanvas = function(y) { + return (y - this.translation.y) / this.scale; +}; + +Graph.prototype._canvasToY = function(y) { + return y * this.scale + this.translation.y ; +}; + + + +/** + * Get a node by its id + * @param {number} id + * @return {Node} node, or null if not found + */ +Graph.prototype._getNode = function(id) { + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i].id == id) + return this.nodes[i]; + } + + return null; +}; + +/** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawNodes = function(ctx) { + // first draw the unselected nodes + var nodes = this.nodes; + var selected = []; + for (var i = 0, iMax = nodes.length; i < iMax; i++) { + if (nodes[i].isSelected()) { + selected.push(i); + } + else { + nodes[i].draw(ctx); + } + } + + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + nodes[selected[s]].draw(ctx); + } +}; + +/** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawEdges = function(ctx) { + var edges = this.edges; + for (var i = 0, iMax = edges.length; i < iMax; i++) { + edges[i].draw(ctx); + } +}; + +/** + * Redraw all packages + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawPackages = function(ctx) { + var packages = this.packages; + for (var i = 0, iMax = packages.length; i < iMax; i++) { + packages[i].draw(ctx); + } +}; + + +/** + * Redraw the filter + */ +Graph.prototype._drawSlider = function() { + var sliderNode; + if (this.hasTimestamps) { + sliderNode = this.frame.slider; + if (sliderNode === undefined) { + sliderNode = document.createElement( "div" ); + sliderNode.style.position = "absolute"; + sliderNode.style.bottom = "0px"; + sliderNode.style.left = "0px"; + sliderNode.style.right = "0px"; + sliderNode.style.backgroundColor = "rgba(255, 255, 255, 0.7)"; + + this.frame.slider = sliderNode; + this.frame.slider.style.padding = "10px"; + //this.frame.filter.style.backgroundColor = "#EFEFEF"; + this.frame.appendChild(sliderNode); + + + var range = this._getRange(); + this.slider = new Graph.Slider(sliderNode); + this.slider.setLoop(false); + this.slider.setRange(range.start, range.end); + + // create an event handler + var me = this; + var onchange = function () { + var timestamp = me.slider.getValue(); + me.setTimestamp(timestamp); + // TODO: do only a redraw when the graph is not still moving + me.redraw(); + }; + this.slider.setOnChangeCallback(onchange); + onchange(); // perform the first update by hand. + } + } + else { + sliderNode = this.frame.slider; + if (sliderNode !== undefined) { + this.frame.removeChild(sliderNode); + this.frame.slider = undefined; + this.slider = undefined; + } + } +}; + +/** + * Recalculate the best positions for all nodes + */ +Graph.prototype._reposition = function() { + // TODO: implement function reposition + + + /* + var w = this.frame.canvas.clientWidth; + var h = this.frame.canvas.clientHeight; + for (var i = 0; i < this.nodes.length; i++) { + if (!this.nodes[i].xFixed) this.nodes[i].x = w * Math.random(); + if (!this.nodes[i].yFixed) this.nodes[i].y = h * Math.random(); + } + //*/ + + //* + // TODO + var radius = this.constants.edges.length * 2; + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + for (var i = 0; i < this.nodes.length; i++) { + var angle = 2*Math.PI * (i / this.nodes.length); + + if (!this.nodes[i].xFixed) this.nodes[i].x = cx + radius * Math.cos(angle); + if (!this.nodes[i].yFixed) this.nodes[i].y = cy + radius * Math.sin(angle); + + } + //*/ + + /* + // TODO + var radius = this.constants.edges.length * 2; + var w = this.frame.canvas.clientWidth, + h = this.frame.canvas.clientHeight; + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + var s = Math.sqrt(this.nodes.length); + for (var i = 0; i < this.nodes.length; i++) { + //var angle = 2*Math.PI * (i / this.nodes.length); + + if (!this.nodes[i].xFixed) this.nodes[i].x = w/s * (i % s); + if (!this.nodes[i].yFixed) this.nodes[i].y = h/s * (i / s); + } + //*/ + + + /* + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + for (var i = 0; i < this.nodes.length; i++) { + this.nodes[i].x = cx; + this.nodes[i].y = cy; + } + + //*/ + +}; + + +/** + * Find a stable position for all nodes + */ +Graph.prototype._doStabilize = function() { + var start = new Date(); + + // find stable position + var count = 0; + var vmin = this.constants.minVelocity; + var stable = false; + while (!stable && count < this.constants.maxIterations) { + this._calculateForces(); + this._discreteStepNodes(); + stable = !this.isMoving(vmin); + count++; + } + + var end = new Date(); + + //console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup +}; + +/** + * Calculate the external forces acting on the nodes + * Forces are caused by: edges, repulsing forces between nodes, gravity + */ +Graph.prototype._calculateForces = function(nodeId) { + // create a local edge to the nodes and edges, that is faster + var nodes = this.nodes, + edges = this.edges; + + // gravity, add a small constant force to pull the nodes towards the center of + // the graph + // Also, the forces are reset to zero in this loop by using _setForce instead + // of _addForce + var gravity = 0.01, + gx = this.frame.canvas.clientWidth / 2, + gy = this.frame.canvas.clientHeight / 2; + for (var n = 0; n < nodes.length; n++) { + var dx = gx - nodes[n].x, + dy = gy - nodes[n].y, + angle = Math.atan2(dy, dx), + fx = Math.cos(angle) * gravity, + fy = Math.sin(angle) * gravity; + + this.nodes[n]._setForce(fx, fy); + } + + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var n = 0; n < nodes.length; n++) { + for (var n2 = n + 1; n2 < this.nodes.length; n2++) { + //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), + + // calculate normally distributed force + var dx = nodes[n2].x - nodes[n].x, + dy = nodes[n2].y - nodes[n].y, + 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; + + this.nodes[n]._addForce(-fx, -fy); + this.nodes[n2]._addForce(fx, fy); + } + /* TODO: re-implement repulsion of edges + for (var l = 0; l < edges.length; l++) { + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + + // calculate normally distributed force + dx = nodes[n].x - lx, + dy = nodes[n].y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + nodes[n]._addForce(fx, fy); + edges[l].from._addForce(-fx/2,-fy/2); + edges[l].to._addForce(-fx/2,-fy/2); + } + */ + } + + // forces caused by the edges, modelled as springs + for (var l = 0, lMax = edges.length; l < lMax; l++) { + var edge = edges[l], + + dx = (edge.to.x - edge.from.x), + dy = (edge.to.y - edge.from.y), + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length, // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length, // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2, + edgeLength = edge.length, + length = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + springforce = edge.stiffness * (edgeLength - length), + + fx = Math.cos(angle) * springforce, + fy = Math.sin(angle) * springforce; + + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } + + /* TODO: re-implement repulsion of edges + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } + */ +}; + + +/** + * Check if any of the nodes is still moving + * @param {number} vmin the minimum velocity considered as "moving" + * @return {boolean} true if moving, false if non of the nodes is moving + */ +Graph.prototype.isMoving = function(vmin) { + // TODO: ismoving does not work well: should check the kinetic energy, not its velocity + var nodes = this.nodes; + for (var n = 0, nMax = nodes.length; n < nMax; n++) { + if (nodes[n].isMoving(vmin)) { + return true; + } + } + return false; +}; + + +/** + * Perform one discrete step for all nodes + */ +Graph.prototype._discreteStepNodes = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var nodes = this.nodes; + for (var n = 0, nMax = nodes.length; n < nMax; n++) { + nodes[n].discreteStep(interval); + } +}; + + +/** + * Perform one discrete step for all packages + */ +Graph.prototype._discreteStepPackages = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var packages = this.packages; + for (var n = 0, nMax = packages.length; n < nMax; n++) { + packages[n].discreteStep(interval); + } +}; + + +/** + * Cleanup finished packages. + * also checks if there are moving packages + */ +Graph.prototype._deleteFinishedPackages = function() { + var n = 0; + var hasMovingPackages = false; + while (n < this.packages.length) { + if (this.packages[n].isFinished()) { + this.packages.splice(n, 1); + n--; + } + else if (this.packages[n].isMoving()) { + hasMovingPackages = true; + } + n++; + } + + this.hasMovingPackages = hasMovingPackages; +}; + +/** + * Start animating nodes, edges, and packages. + */ +Graph.prototype.start = function() { + if (this.hasMovingNodes) { + this._calculateForces(); + this._discreteStepNodes(); + + var vmin = this.constants.minVelocity; + this.hasMovingNodes = this.isMoving(vmin); + } + + if (this.hasMovingPackages) { + this._discreteStepPackages(); + this._deleteFinishedPackages(); + } + + if (this.hasMovingNodes || this.hasMovingEdges || this.hasMovingPackages) { + // start animation. only start timer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + graph.start(); + graph._redraw(); + }, this.refreshRate); + } + } + else { + this._redraw(); + } +}; + +/** + * Stop animating nodes, edges, and packages. + */ +Graph.prototype.stop = function () { + if (this.timer) { + window.clearInterval(this.timer); + this.timer = undefined; + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * 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 + */ +Graph.addEventListener = function (element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent("on" + action, listener); // IE browsers + } +}; + +/** + * Remove an event listener from an element + * @param {Element} element An html dom element + * @param {string} action The name of the event, for example "mousedown" + * @param {function} listener The listener function + * @param {boolean} useCapture + */ +Graph.removeEventListener = function(element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent("on" + action, listener); + } +}; + + +/** + * Stop event propagation + */ +Graph.stopPropagation = function (event) { + if (!event) + event = window.event; + + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers + } +}; + + +/** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ +Graph.preventDefault = function (event) { + if (!event) + event = window.event; + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } +}; + +/** + * Retrieve the absolute left value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} left The absolute left position of this element + * in the browser page. + */ +Graph._getAbsoluteLeft = function(elem) { + var left = 0; + while( elem != null ) { + left += elem.offsetLeft; + left -= elem.scrollLeft; + elem = elem.offsetParent; + } + if (!document.body.scrollLeft && window.pageXOffset) { + // FF + left -= window.pageXOffset; + } + return left; +}; + +/** + * Retrieve the absolute top value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} top The absolute top position of this element + * in the browser page. + */ +Graph._getAbsoluteTop = function(elem) { + var top = 0; + while( elem != null ) { + top += elem.offsetTop; + top -= elem.scrollTop; + elem = elem.offsetParent; + } + if (!document.body.scrollTop && window.pageYOffset) { + // FF + top -= window.pageYOffset; + } + return top; +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @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} text Title for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} style Drawing style, available: + * "database", "circle", "rect", + * "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 + */ +Graph.Node = function (properties, imagelist, grouplist, constants) { + this.selected = false; + + this.edges = []; // all edges connected to this node + this.group = constants.nodes.group; + + this.fontSize = constants.nodes.fontSize; + this.fontFace = constants.nodes.fontFace; + this.fontColor = constants.nodes.fontColor; + + this.borderColor = constants.nodes.borderColor; + this.backgroundColor = constants.nodes.backgroundColor; + this.highlightColor = constants.nodes.highlightColor; + + // set defaults for the properties + this.id = undefined; + this.style = constants.nodes.style; + this.image = constants.nodes.image; + this.x = 0; + this.y = 0; + this.xFixed = false; + this.yFixed = false; + this.radius = constants.nodes.radius; + this.radiusFixed = false; + this.radiusMin = constants.nodes.radiusMin; + this.radiusMax = constants.nodes.radiusMax; + + this.imagelist = imagelist; + this.grouplist = grouplist; + + this.setProperties(properties, constants); + + // mass, force, velocity + this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.fx = 0.0; // external force x + this.fy = 0.0; // external force y + this.vx = 0.0; // velocity x + this.vy = 0.0; // velocity y + this.minForce = constants.minForce; + this.damping = 0.9; // damping factor +}; + +/** + * Attach a edge to the node + * @param {Graph.Edge} edge + */ +Graph.Node.prototype.attachEdge = function(edge) { + this.edges.push(edge); + this._updateMass(); +}; + +/** + * Detach a edge from the node + * @param {Graph.Edge} edge + */ +Graph.Node.prototype.detachEdge = function(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + this._updateMass(); +}; + +/** + * Update the nodes mass, which is determined by the number of edges connecting + * to it (more edges -> heavier node). + * @private + */ +Graph.Node.prototype._updateMass = function() { + this.mass = 50 + 20 * this.edges.length; // kg +}; + +/** + * Set or overwrite properties for the node + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Node.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // basic properties + if (properties.id != undefined) {this.id = properties.id;} + if (properties.text != undefined) {this.text = properties.text;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.group != undefined) {this.group = properties.group;} + if (properties.x != undefined) {this.x = properties.x;} + if (properties.y != undefined) {this.y = properties.y;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + + 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 style properties + if (properties.style != undefined) {this.style = properties.style;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.borderColor != undefined) {this.borderColor = properties.borderColor;} + if (properties.backgroundColor != undefined){this.backgroundColor = properties.backgroundColor;} + if (properties.highlightColor != undefined) {this.highlightColor = properties.highlightColor;} + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + + + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + this.xFixed = this.xFixed || (properties.x != undefined); + this.yFixed = this.yFixed || (properties.y != undefined); + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + + if (this.style == 'image') { + this.radiusMin = constants.nodes.widthMin; + this.radiusMax = constants.nodes.widthMax; + } + + // choose draw method depending on the style + var style = this.style; + switch (style) { + case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; + case 'rect': this.draw = this._drawRect; this.resize = this._resizeRect; break; + case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; + // TODO: add ellipse shape + // 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._drawRect; this.resize = this._resizeRect; break; + } + + // reset the size of the node, this can be changed + this._reset(); +}; + +/** + * select this node + */ +Graph.Node.prototype.select = function() { + this.selected = true; + this._reset(); +}; + +/** + * unselect this node + */ +Graph.Node.prototype.unselect = function() { + this.selected = false; + this._reset(); +}; + +/** + * Reset the calculated size of the node, forces it to recalculate its size + */ +Graph.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. + */ +Graph.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 + */ +Graph.Node.prototype.distanceToBorder = function (ctx, angle) { + var borderWidth = 1; + + if (!this.width) { + this.resize(ctx); + } + + //noinspection FallthroughInSwitchStatementJS + switch (this.style) { + case 'circle': + case 'dot': + return this.radius + borderWidth; + + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown + + case 'rect': + 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 rect + } + 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 + */ +Graph.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 + */ +Graph.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 + */ +Graph.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 + */ +Graph.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 +Graph.Node.prototype.isMoving = function(vmin) { + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || + (!this.xFixed && Math.abs(this.fx) > this.minForce) || + (!this.yFixed && Math.abs(this.fy) > this.minForce)); +}; + +/** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ +Graph.Node.prototype.isSelected = function() { + return this.selected; +}; + +/** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ +Graph.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 + */ +Graph.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 + */ +Graph.Node.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + var scale = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * scale + this.radiusMin; + } +}; + +/** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.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 + */ +Graph.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 + */ +Graph.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); +}; + +Graph.Node.prototype._resizeImage = function (ctx) { + // TODO: pre calculate the image size + if (!this.width) { // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + this.width = width; + this.height = height; + } +}; + +Graph.Node.prototype._drawImage = function (ctx) { + this._resizeImage(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + var yText; + if (this.imageObj) { + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + yText = this.y + this.height / 2; + } + else { + // image still loading... just draw the text for now + yText = this.y; + } + + this._text(ctx, this.text, this.x, yText, undefined, "top"); +}; + + +Graph.Node.prototype._resizeRect = 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; + } +}; + +Graph.Node.prototype._drawRect = function (ctx) { + this._resizeRect(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.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; + } +}; + +Graph.Node.prototype._drawDatabase = function (ctx) { + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.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; + } +}; + +Graph.Node.prototype._drawCircle = function (ctx) { + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.circle(this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + +Graph.Node.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Graph.Node.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Graph.Node.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Graph.Node.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Graph.Node.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Graph.Node.prototype._resizeShape = function (ctx) { + if (!this.width) { + var size = 2 * this.radius; + this.width = size; + this.height = size; + } +}; + +Graph.Node.prototype._drawShape = function (ctx, shape) { + this._resizeShape(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + + ctx[shape](this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + if (this.text) { + this._text(ctx, this.text, this.x, this.y + this.height / 2, undefined, 'top'); + } +}; + +Graph.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; + } +}; + +Graph.Node.prototype._drawText = function (ctx) { + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.Node.prototype._text = 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; + } + } +}; + + +Graph.Node.prototype.getTextSize = function(ctx) { + if (this.text != undefined) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + + var lines = this.text.split('\n'), + height = (this.fontSize + 4) * lines.length, + width = 0; + + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); + } + + return {"width": width, "height": height}; + } + else { + return {"width": 0, "height": 0}; + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), 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 + */ +Graph.Edge = function (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.style = constants.edges.style; + this.title = undefined; + this.width = constants.edges.width; + this.value = undefined; + this.length = constants.edges.length; + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + this.dashlength = constants.edges.dashlength; + this.dashgap = constants.edges.dashgap; + this.altdashlength = constants.edges.altdashlength; + + this.stiffness = undefined; // depends on the length of the edge + this.color = constants.edges.color; + this.timestamp = undefined; + 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 + */ +Graph.Edge.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);} + if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);} + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.text != undefined) {this.text = properties.text;} + if (this.text) { + 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.dashlength != undefined) {this.dashlength = properties.dashlength;} + if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;} + if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;} + + if (properties.color != undefined) {this.color = properties.color;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + + if (!this.from) { + throw "Node with id " + properties.from + " not found"; + } + if (!this.to) { + throw "Node with id " + properties.to + " not found"; + } + + this.widthFixed = this.widthFixed || (properties.width != undefined); + this.lengthFixed = this.lengthFixed || (properties.length != undefined); + + this.stiffness = 1 / this.length; + + // initialize animation + if (this.style === 'arrow') { + this.arrows = [0.5]; + this.animation = false; + } + else if (this.style === 'arrow-end') { + this.animation = false; + } + else if (this.style === 'moving-arrows') { + this.arrows = []; + var arrowCount = 3; // TODO: make customizable + for (var a = 0; a < arrowCount; a++) { + this.arrows.push(a / arrowCount); + } + this.animation = true; + } + else if (this.style === 'moving-dot') { + this.dot = 0.0; + this.animation = true; + } + else { + this.animation = false; + } + + // 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-end': this.draw = this._drawArrowEnd; break; + case 'moving-arrows': this.draw = this._drawMovingArrows; break; + case 'moving-dot': this.draw = this._drawMovingDot; break; + case 'dash-line': this.draw = this._drawDashLine; break; + default: this.draw = this._drawLine; break; + } +}; + + + +/** + * Check if a node has an animating contents. If so, the graph needs to be + * redrawn regularly + * @return {boolean} true if this edge needs animation, else false + */ +Graph.Edge.prototype.isMoving = function() { + // TODO: be able to set the interval somehow + + return this.animation; +}; + +/** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ +Graph.Edge.prototype.getTitle = function() { + return this.title; +}; + + +/** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ +Graph.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 + */ +Graph.Edge.prototype.setValueRange = function(min, max) { + if (!this.widthFixed && this.value !== undefined) { + var factor = (this.widthMax - this.widthMin) / (max - min); + this.width = (this.value - min) * factor + this.widthMin; + } +}; + + +/** + * Check if the length is fixed. + * @return {boolean} lengthFixed True if the length is fixed, else false + */ +Graph.Edge.prototype.isLengthFixed = function() { + return this.lengthFixed; +}; + +/** + * Retrieve the length of the edge. Can be undefined + * @return {Number} length + */ +Graph.Edge.prototype.getLength = function() { + return this.length; +}; + +/** + * Adjust the length of the edge. This can only be done when the length + * is not fixed (which is the case when the edge is created with a length property) + * @param {Number} length + */ +Graph.Edge.prototype.setLength = function(length) { + if (!this.lengthFixed) { + this.length = length; + } +}; + +/** + * Retrieve the length of the edges dashes. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} dashlength + */ +Graph.Edge.prototype.getDashLength = function() { + return this.dashlength; +}; + +/** + * Adjust the length of the edges dashes. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} dashlength + */ +Graph.Edge.prototype.setDashLength = function(dashlength) { + this.dashlength = dashlength; +}; + +/** + * Retrieve the length of the edges dashes gaps. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} dashgap + */ +Graph.Edge.prototype.getDashGap = function() { + return this.dashgap; +}; + +/** + * Adjust the length of the edges dashes gaps. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} dashgap + */ +Graph.Edge.prototype.setDashGap = function(dashgap) { + this.dashgap = dashgap; +}; + +/** + * Retrieve the length of the edges alternate dashes. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} altdashlength + */ +Graph.Edge.prototype.getAltDashLength = function() { + return this.altdashlength; +}; + +/** + * Adjust the length of the edges alternate dashes. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} altdashlength + */ +Graph.Edge.prototype.setAltDashLength = function(altdashlength) { + this.altdashlength = altdashlength; +}; + + + +/** + * 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 + */ +Graph.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 + */ +Graph.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 = Graph._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return (dist < distMax); +}; + +/** + * 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 + */ +Graph._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); +}; + +/** + * 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 + */ +Graph.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 text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + var radius = this.length / 2 / Math.PI; + var x, y; + 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._text(ctx, this.text, 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 + */ +Graph.Edge.prototype._getLineWidth = function() { + if (this.from.selected || this.to.selected) { + return Math.min(this.width * 2, this.widthMax); + } + else { + return this.width; + } +}; + +/** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Graph.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 + */ +Graph.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 text with white background and with the middle at (x, y) + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} x + * @param {Number} y + */ +Graph.Edge.prototype._text = 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(this.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(this.text, left, top); + } +}; + +/** + * 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 + */ +var CP = (typeof window !== 'undefined') && + window.CanvasRenderingContext2D && + CanvasRenderingContext2D.prototype; +if (CP && CP.lineTo){ + CP.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; + } + } +} + +/** + * 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 + */ +Graph.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.altdashlength != 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.dashlength,this.dashgap,this.altdashlength,this.dashgap]); + } + else if (this.dashlength != undefined && this.dashgap != 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.dashlength,this.dashgap]); + } + 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 text + if (this.text) { + var point = this._pointOnLine(0.5); + this._text(ctx, this.text, 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 + */ +Graph.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 + */ +Graph.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 a moving 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 + */ +Graph.Edge.prototype._drawMovingArrows = function(ctx) { + this._drawArrow(ctx); + + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + this.arrows[a] += 0.02; // TODO determine speed from interval + if (this.arrows[a] > 1.0) this.arrows[a] = 0.0; + } + } +}; + +/** + * Redraw a edge as a line with a moving dot + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawMovingDot = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var point; + if (this.from != this.to) { + this._line(ctx); + + // draw dot + var radius = 4 + this.width * 2; + point = this._pointOnLine(this.dot); + ctx.circle(point.x, point.y, radius); + ctx.fill(); + + // move the dot to the next position + this.dot += 0.05; // TODO determine speed from interval + if (this.dot > 1.0) this.dot = 0.0; + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // TODO: moving dot for a circular edge + } +}; + + +/** + * 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 + */ +Graph.Edge.prototype._drawArrow = 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 all arrows + 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? + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + point = this._pointOnLine(this.arrows[a]); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + } + } + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // draw circle + var radius = this.length / 2 / Math.PI; + var x, y; + 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? + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + point = this._pointOnCircle(x, y, radius, this.arrows[a]); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + } + } + + // draw text + if (this.text) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, 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 + */ +Graph.Edge.prototype._drawArrowEnd = 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.to.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 text + if (this.text) { + var point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // draw circle + var radius = this.length / 2 / Math.PI; + var x, y, arrow; + 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; + 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 text + if (this.text) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + +}; + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Images + * This class loades images and keeps them stored. + */ +Graph.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 + */ +Graph.Images.prototype.setOnloadCallback = function(callback) { + this.callback = callback; +}; + + +/** + * + * @param {string} url Url of the image + * @return {Image} img The image object + */ +Graph.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; +}; + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Package + * This class contains one package + * + * @param {number} properties Properties for the package. Optional. Available + * properties are: id {number}, title {string}, + * style {string} with available values "dot" and + * "image", radius {number}, image {string}, + * color {string}, progress {number} with a value + * between 0-1, duration {number}, timestamp {number + * or Date}. + * @param {Graph} graph The graph object, used to find + * and edge to nodes. + * @param {Graph.Images} imagelist An Images object. Only needed + * when the package has style 'image' + * @param {Object} constants An object with default values for + * example for the color + */ +Graph.Package = function (properties, graph, imagelist, constants) { + if (graph == undefined) { + throw "No graph provided"; + } + + // constants + this.radiusMin = constants.packages.radiusMin; + this.radiusMax = constants.packages.radiusMax; + this.imagelist = imagelist; + this.graph = graph; + + // initialize variables + this.id = undefined; + this.from = undefined; + this.to = undefined; + this.title = undefined; + this.style = constants.packages.style; + this.radius = constants.packages.radius; + this.color = constants.packages.color; + this.image = constants.packages.image; + this.value = undefined; + this.progress = 0.0; + this.timestamp = undefined; + this.duration = constants.packages.duration; + this.autoProgress = true; + this.radiusFixed = false; + + // set properties + this.setProperties(properties, constants); +}; + +Graph.Package.DEFAULT_DURATION = 1.0; // seconds + +/** + * Set or overwrite properties for the package + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Package.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // note that the provided properties can also be null, when they come from the Google DataTable + if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);} + if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);} + + if (!this.from) { + throw "Node with id " + properties.from + " not found"; + } + if (!this.to) { + throw "Node with id " + properties.to + " not found"; + } + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.color != undefined) {this.color = properties.color;} + if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;} + if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;} + if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;} + if (properties.progress != undefined) {this.progress = properties.progress;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + if (properties.duration != undefined) {this.duration = properties.duration;} + + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + this.autoProgress = (this.autoProgress == true) ? (properties.progress == undefined) : false; + + if (this.style == 'image') { + this.radiusMin = constants.packages.widthMin; + this.radiusMax = constants.packages.widthMax; + } + + // handle progress + if (this.progress < 0.0) {this.progress = 0.0;} + if (this.progress > 1.0) {this.progress = 1.0;} + + // handle image + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + // choose draw method depending on the style + switch (this.style) { + // TODO: add more styles + case 'dot': this.draw = this._drawDot; break; + case 'square': this.draw = this._drawSquare; break; + case 'triangle': this.draw = this._drawTriangle; break; + case 'triangleDown':this.draw = this._drawTriangleDown; break; + case 'star': this.draw = this._drawStar; break; + case 'image': this.draw = this._drawImage; break; + default: this.draw = this._drawDot; break; + } +}; + +/** + * Set a new value for the progress of the package + * @param {number} progress A value between 0 and 1 + */ +Graph.Package.prototype.setProgress = function (progress) { + this.progress = progress; + this.autoProgress = false; +}; + +/** + * Check if a package is finished, if it has reached its destination. + * If so, the package can be removed. + * Only packages with automatically animated progress can be finished + * @return {boolean} true if finished, else false. + */ +Graph.Package.prototype.isFinished = function () { + return (this.autoProgress == true && this.progress >= 1.0); +}; + +/** + * Check if this package is moving. + * A packages moves when it has automatic progress and not yet reached its + * destination. + * @return {boolean} true if moving, else false. + */ +Graph.Package.prototype.isMoving = function () { + return (this.autoProgress || this.isFinished()); +}; + + +/** + * Perform one discrete step for the package. Only applicable when the + * package has no manually set, fixed progress. + * @param {number} interval Time interval in seconds + */ +Graph.Package.prototype.discreteStep = function(interval) { + if (this.autoProgress == true) { + this.progress += (parseFloat(interval) / this.duration); + + if (this.progress > 1.0) + this.progress = 1.0; + } +}; + + +/** + * Draw this package in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Package.prototype.draw = function(ctx) { + throw "Draw method not initialized for package"; +}; + + +/** + * 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 + */ +Graph.Package.prototype.isOverlappingWith = function(obj) { + // radius minimum 10px else it is too hard to get your mouse at the exact right position + var radius = Math.max(this.radius, 10); + var pos = this._getPosition(); + + return (pos.x - radius < obj.right && + pos.x + radius > obj.left && + pos.y - radius < obj.bottom && + pos.y + radius > obj.top); +}; + +/** + * Calculate the current position of the package + * @return {Object} position The object has parameters x and y. + */ +Graph.Package.prototype._getPosition = function() { + return { + "x" : (1 - this.progress) * this.from.x + this.progress * this.to.x, + "y" : (1 - this.progress) * this.from.y + this.progress * this.to.y + }; +}; + + +/** + * get the title of this package. + * @return {string} title The title of the package, or undefined when no + * title has been set. + */ +Graph.Package.prototype.getTitle = function() { + return this.title; +}; + +/** + * Retrieve the value of the package. Can be undefined + * @return {Number} value + */ +Graph.Package.prototype.getValue = function() { + return this.value; +}; + +/** + * Calculate the distance from the packages location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ +Graph.Package.prototype.getDistance = function(x, y) { + var pos = this._getPosition(), + dx = pos.x - x, + dy = pos.y - y; + return Math.sqrt(dx * dx + dy * dy); +}; + +/** + * Adjust the value range of the package. The package will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Graph.Package.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + var factor = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * factor + this.radiusMin; + } +}; + + + +/** + * Redraw a package as a dot + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +/* TODO: cleanup + Graph.Package.prototype._drawDot = function(ctx) { + // set style + ctx.fillStyle = this.color; + // draw dot + var pos = this._getPosition(); + ctx.circle(pos.x, pos.y, this.radius); + ctx.fill(); + } + */ + +Graph.Package.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Graph.Package.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Graph.Package.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Graph.Package.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Graph.Package.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Graph.Package.prototype._drawShape = function (ctx, shape) { + // set style + ctx.fillStyle = this.color; + + // draw shape + var pos = this._getPosition(); + ctx[shape](pos.x, pos.y, this.radius); + ctx.fill(); +}; + +/** + * Redraw a package as an image + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Package.prototype._drawImage = function (ctx) { + if (this.imageObj) { + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + var pos = this._getPosition(); + + ctx.drawImage(this.imageObj, pos.x - width / 2, pos.y - height / 2, width, height); + } + else { + console.log("image still loading..."); + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Groups + * This class can store groups and properties specific for groups. + */ +Graph.Groups = function () { + this.clear(); + this.defaultIndex = 0; +}; + + +/** + * default constants for group colors + */ +Graph.Groups.DEFAULT = [ + {"borderColor": "#2B7CE9", "backgroundColor": "#97C2FC", "highlightColor": "#D2E5FF"}, // blue + {"borderColor": "#FFA500", "backgroundColor": "#FFFF00", "highlightColor": "#FFFFA3"}, // yellow + {"borderColor": "#FA0A10", "backgroundColor": "#FB7E81", "highlightColor": "#FFAFB1"}, // red + {"borderColor": "#41A906", "backgroundColor": "#7BE141", "highlightColor": "#A1EC76"}, // green + {"borderColor": "#E129F0", "backgroundColor": "#EB7DF4", "highlightColor": "#F0B3F5"}, // magenta + {"borderColor": "#7C29F0", "backgroundColor": "#AD85E4", "highlightColor": "#D3BDF0"}, // purple + {"borderColor": "#C37F00", "backgroundColor": "#FFA807", "highlightColor": "#FFCA66"}, // orange + {"borderColor": "#4220FB", "backgroundColor": "#6E6EFD", "highlightColor": "#9B9BFD"}, // darkblue + {"borderColor": "#FD5A77", "backgroundColor": "#FFC0CB", "highlightColor": "#FFD1D9"}, // pink + {"borderColor": "#4AD63A", "backgroundColor": "#C2FABC", "highlightColor": "#E6FFE3"} // mint +]; + + +/** + * Clear all groups + */ +Graph.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 + */ +Graph.Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + + if (group == undefined) { + // create new group + var index = this.defaultIndex % Graph.Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.borderColor = Graph.Groups.DEFAULT[index].borderColor; + group.backgroundColor = Graph.Groups.DEFAULT[index].backgroundColor; + group.highlightColor = Graph.Groups.DEFAULT[index].highlightColor; + 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 + */ +Graph.Groups.prototype.add = function (groupname, style) { + this.groups[groupname] = style; + return style; +}; + +/** + * Check if given object is a Javascript Array + * @param {*} obj + * @return {Boolean} isArray true if the given object is an array + */ +// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni +Graph.isArray = function (obj) { + if (obj instanceof Array) { + return true; + } + return (Object.prototype.toString.call(obj) === '[object Array]'); +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Slider + * + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + */ +Graph.Slider = function(container) { + if (container === undefined) throw "Error: No container element defined"; + + this.container = container; + + this.frame = document.createElement("DIV"); + //this.frame.style.backgroundColor = "#E5E5E5"; + this.frame.style.width = "100%"; + this.frame.style.position = "relative"; + + this.title = document.createElement("DIV"); + this.title.style.margin = "2px"; + this.title.style.marginBottom = "5px"; + this.title.innerHTML = ""; + this.container.appendChild(this.title); + + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; + this.frame.appendChild(this.frame.prev); + + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; + this.frame.appendChild(this.frame.play); + + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; + this.frame.appendChild(this.frame.next); + + this.frame.bar = document.createElement("INPUT"); + this.frame.bar.type = "BUTTON"; + this.frame.bar.style.position = "absolute"; + this.frame.bar.style.border = "1px solid red"; + this.frame.bar.style.width = "100px"; + this.frame.bar.style.height = "6px"; + this.frame.bar.style.borderRadius = "2px"; + this.frame.bar.style.MozBorderRadius = "2px"; + this.frame.bar.style.border = "1px solid #7F7F7F"; + this.frame.bar.style.backgroundColor = "#E5E5E5"; + this.frame.appendChild(this.frame.bar); + + this.frame.slide = document.createElement("INPUT"); + this.frame.slide.type = "BUTTON"; + this.frame.slide.style.margin = "0px"; + this.frame.slide.value = " "; + this.frame.slide.style.position = "relative"; + this.frame.slide.style.left = "-100px"; + this.frame.appendChild(this.frame.slide); + + // create events + var me = this; + this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; + this.frame.prev.onclick = function (event) {me.prev(event);}; + this.frame.play.onclick = function (event) {me.togglePlay(event);}; + this.frame.next.onclick = function (event) {me.next(event);}; + + this.container.appendChild(this.frame); + + this.onChangeCallback = undefined; + + this.playTimeout = undefined; + this.framerate = 20; // frames per second + this.duration = 10; // seconds + this.doLoop = true; + + this.start = 0; + this.end = 0; + this.value = 0; + this.step = 0; + this.rangeIsDate = false; + + this.redraw(); +}; + +/** + * Retrieve the step size, depending on the range, framerate, and duration + */ +Graph.Slider.prototype._updateStep = function() { + var range = (this.end - this.start); + var frameCount = this.duration * this.framerate; + + this.step = range / frameCount; +}; + +/** + * Select the previous index + */ +Graph.Slider.prototype.prev = function() { + this._setValue(this.value - this.step); +}; + +/** + * Select the next index + */ +Graph.Slider.prototype.next = function() { + this._setValue(this.value + this.step); +}; + +/** + * Select the next index + */ +Graph.Slider.prototype.playNext = function() { + var start = new Date(); + + if (!this.leftButtonDown) { + if (this.value + this.step < this.end) { + this._setValue(this.value + this.step); + } + else { + if (this.doLoop) { + this._setValue(this.start); + } + else { + this._setValue(this.end); + this.stop(); + return; + } + } + } + + var end = new Date(); + var diff = (end - start); + + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(1000 / this.framerate - diff, 0); + + var me = this; + this.playTimeout = setTimeout(function() {me.playNext();}, interval); +}; + +/** + * Toggle start or stop playing + */ +Graph.Slider.prototype.togglePlay = function() { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } +}; + +/** + * Start playing + */ +Graph.Slider.prototype.play = function() { + this.frame.play.value = "Stop"; + + this.playNext(); +}; + +/** + * Stop playing + */ +Graph.Slider.prototype.stop = function() { + this.frame.play.value = "Play"; + + clearInterval(this.playTimeout); + this.playTimeout = undefined; +}; + +/** + * Set a callback function which will be triggered when the value of the + * slider bar has changed. + */ +Graph.Slider.prototype.setOnChangeCallback = function(callback) { + this.onChangeCallback = callback; +}; + +/** + * Set the interval for playing the list + * @param {number} framerate Framerate in frames per second + */ +Graph.Slider.prototype.setFramerate = function(framerate) { + this.framerate = framerate; + this._updateStep(); +}; + +/** + * Retrieve the current framerate + * @return {number} framerate in frames per second + */ +Graph.Slider.prototype.getFramerate = function() { + return this.framerate; +}; + +/** + * Set the duration for playing + * @param {number} duration Duration in seconds + */ +Graph.Slider.prototype.setDuration = function(duration) { + this.duration = duration; + this._updateStep(); +}; + +/** + * Set the time acceleration for playing the history. Only applicable when + * the values are of type Date. + * @param {number} acceleration Acceleration, for example 10 means play + * ten times as fast as real time. A value + * of 1 will play the history in real time. + */ +Graph.Slider.prototype.setAcceleration = function(acceleration) { + var durationRealtime = (this.end - this.start) / 1000; // in seconds + + this.duration = durationRealtime / acceleration; + this._updateStep(); +}; + + +/** + * Set looping on or off + * @param {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Graph.Slider.prototype.setLoop = function(doLoop) { + this.doLoop = doLoop; +}; + +/** + * Retrieve the current value of loop + * @return {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Graph.Slider.prototype.getLoop = function() { + return this.doLoop; +}; + + +/** + * Execute the onchange callback function + */ +Graph.Slider.prototype.onChange = function() { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } +}; + +/** + * redraw the slider on the correct place + */ +Graph.Slider.prototype.redraw = function() { + // resize the bar + var barTop = (this.frame.clientHeight/2 - + this.frame.bar.offsetHeight/2); + var barWidth = (this.frame.clientWidth - + this.frame.prev.clientWidth - + this.frame.play.clientWidth - + this.frame.next.clientWidth - 30); + this.frame.bar.style.top = barTop + "px"; + this.frame.bar.style.width = barWidth + "px"; + + // position the slider button + this.frame.slide.title = this.getValue(); + this.frame.slide.style.left = this._valueToLeft(this.value) + "px"; + + // set the title + this.title.innerHTML = this.getValue(); +}; + + +/** + * Set the range for the slider + * @param {Date | Number} start Start of the range + * @param {Date | Number} end End of the range + */ +Graph.Slider.prototype.setRange = function(start, end) { + if (start === undefined || start === null || start === NaN) { + this.start = 0; + this.rangeIsDate = false; + } + else if (start instanceof Date) { + this.start = start.getTime(); + this.rangeIsDate = true; + } + else { + this.start = start; + this.rangeIsDate = false; + } + + if (end === undefined || end === null || end === NaN) { + if (this.start instanceof Date) { + this.end = new Date(this.start); + } + else { + this.end = this.start; + } + } + else if (end instanceof Date) { + this.end = end.getTime(); + } + else { + this.end = end; + } + + this.value = this.start; + + this._updateStep(); + this.redraw(); +}; + + + +/** + * Set a value for the slider. The value must be between start and end + * When the range are Dates, the value will be translated to a date + * @param {Number} value + */ +Graph.Slider.prototype._setValue = function(value) { + this.value = this._limitValue(value); + this.redraw(); + + this.onChange(); +}; + +/** + * retrieve the current value in the correct type, Number or Date + * @return {Date | Number} value + */ +Graph.Slider.prototype.getValue = function() { + if (this.rangeIsDate) { + return new Date(this.value); + } + else { + return this.value; + } +}; + + +Graph.Slider.prototype.offset = 3; + +Graph.Slider.prototype._leftToValue = function (left) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + var x = left - this.offset; + + var range = this.end - this.start; + var value = this._limitValue(x / width * range + this.start); + + return value; +}; + +Graph.Slider.prototype._valueToLeft = function (value) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + + var x; + if (this.end > this.start) { + x = (value - this.start) / (this.end - this.start) * width; + } + else { + x = 0; + } + var left = x + this.offset; + + return left; +}; + +Graph.Slider.prototype._limitValue = function(value) { + if (value < this.start) { + value = this.start + } + if (value > this.end) { + value = this.end; + } + + return value; +}; + +Graph.Slider.prototype._onMouseDown = function(event) { + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!this.leftButtonDown) return; + + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + Graph.addEventListener(document, "mousemove", this.onmousemove); + Graph.addEventListener(document, "mouseup", this.onmouseup); + Graph.preventDefault(event); +}; + + +Graph.Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var value = this._leftToValue(x); + this._setValue(value); + + Graph.preventDefault(event); +}; + + +Graph.Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + + this.leftButtonDown = false; + + // remove event listeners + Graph.removeEventListener(document, "mousemove", this.onmousemove); + Graph.removeEventListener(document, "mouseup", this.onmouseup); + + Graph.preventDefault(event); +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * 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 + */ +Graph.Popup = function (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 + */ +Graph.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 + */ +Graph.Popup.prototype.setText = function(text) { + this.frame.innerHTML = text; +}; + +/** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ +Graph.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 + */ +Graph.Popup.prototype.hide = function () { + this.frame.style.visibility = "hidden"; +}; + + +/**--------------------------------------------------------------------------**/ + +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(); + }; + + + // TODO: add diamond shape +} + + +/*----------------------------------------------------------------------------*/ + +// utility methods +Graph.util = {}; + +/** + * 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. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} json An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges + */ +Graph.util.parseDOT = function (data) { + /** + * Test whether given character is a whitespace character + * @param {String} c + * @return {Boolean} isWhitespace + */ + function isWhitespace(c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + /** + * Test whether given character is a delimeter + * @param {String} c + * @return {Boolean} isDelimeter + */ + function isDelimeter(c) { + return '[]{}();,=->'.indexOf(c) != -1; + } + + var i = -1; // current index in the data + var c = ''; // current character in the data + + /** + * Read the next character from the data + */ + function next() { + i++; + c = data[i]; + } + + /** + * Preview the next character in the data + * @returns {String} nextChar + */ + function previewNext () { + return data[i + 1]; + } + + /** + * Preview the next character in the data + * @returns {String} nextChar + */ + function previewPrevious () { + return data[i + 1]; + } + + /** + * Get a text description of the the current index in the data + * @return {String} desc + */ + function pos() { + return '(char ' + i + ')'; + } + + /** + * Skip whitespace and comments + */ + function parseWhitespace() { + // skip whitespace + while (c && isWhitespace(c)) { + next(); + } + + // test for comment + var cNext = data[i + 1]; + var cPrev = data[i - 1]; + var c2 = c + cNext; + if (c2 == '/*') { + // block comment. skip until the block is closed + while (c && !(c == '*' && data[i + 1] == '/')) { + next(); + } + next(); + next(); + + parseWhitespace(); + } + else if (c2 == '//' || (c == '#' && cPrev == '\n')) { + // line comment. skip until the next return + while (c && c != '\n') { + next(); + } + next(); + parseWhitespace(); + } + } + + /** + * Parse a string + * The string may be enclosed by double quotes + * @return {String | undefined} value + */ + function parseString() { + parseWhitespace(); + + var name = ''; + if (c == '"') { + next(); + while (c && c != '"') { + name += c; + next(); + } + next(); // skip the closing quote + } + else { + while (c && !isWhitespace(c) && !isDelimeter(c)) { + name += c; + next(); + } + + // cast string to number or boolean + var number = Number(name); + if (!isNaN(number)) { + name = number; + } + else if (name == 'true') { + name = true; + } + else if (name == 'false') { + name = false; + } + else if (name == 'null') { + name = null; + } + } + + return name; + } + + /** + * Parse a value, can be a string, number, or boolean. + * The value may be enclosed by double quotes + * @return {String | Number | Boolean | undefined} value + */ + function parseValue() { + parseWhitespace(); + + if (c == '"') { + return parseString(); + } + else { + var value = parseString(); + if (value != undefined) { + // cast string to number or boolean + var number = Number(value); + if (!isNaN(number)) { + value = number; + } + else if (value == 'true') { + value = true; + } + else if (value == 'false') { + value = false; + } + else if (value == 'null') { + value = null; + } + } + return value; + } + } + + /** + * Parse a set with attributes, + * for example [label="1.000", style=solid] + * @return {Object | undefined} attr + */ + function parseAttributes() { + parseWhitespace(); + + if (c == '[') { + next(); + var attr = {}; + while (c && c != ']') { + parseWhitespace(); + + var name = parseString(); + if (!name) { + throw new SyntaxError('Attribute name expected ' + pos()); + } + + parseWhitespace(); + if (c != '=') { + throw new SyntaxError('Equal sign = expected ' + pos()); + } + next(); + + var value = parseValue(); + if (!value) { + throw new SyntaxError('Attribute value expected ' + pos()); + } + attr[name] = value; + + parseWhitespace(); + + if (c ==',') { + next(); + } + } + next(); + + return attr; + } + else { + return undefined; + } + } + + /** + * Parse a directed or undirected arrow '->' or '--' + * @return {String | undefined} arrow + */ + function parseArrow() { + parseWhitespace(); + + if (c == '-') { + next(); + if (c == '>' || c == '-') { + var arrow = '-' + c; + next(); + return arrow; + } + else { + throw new SyntaxError('Arrow "->" or "--" expected ' + pos()); + } + } + + return undefined; + } + + /** + * Parse a line separator ';' + * @return {String | undefined} separator + */ + function parseSeparator() { + parseWhitespace(); + + if (c == ';') { + next(); + return ';'; + } + + return undefined; + } + + /** + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + */ + function merge (a, b) { + if (a && b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + } + + var nodeMap = {}; + var edgeList = []; + + /** + * Register a node with attributes + * @param {String} id + * @param {Object} [attr] + */ + function addNode(id, attr) { + var node = { + id: String(id), + attr: attr || {} + }; + if (!nodeMap[id]) { + nodeMap[id] = node; + } + else { + merge(nodeMap[id].attr, node.attr); + } + } + + /** + * Register an edge + * @param {String} from + * @param {String} to + * @param {String} type A string "->" or "--" + * @param {Object} [attr] + */ + function addEdge(from, to, type, attr) { + edgeList.push({ + from: String(from), + to: String(to), + type: type, + attr: attr || {} + }); + } + + // find the opening curly bracket + next(); + while (c && c != '{') { + next(); + } + if (c != '{') { + throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos()) + } + next(); + + // parse all data until a closing curly bracket is encountered + while (c && c != '}') { + // parse node id and optional node attributes + var id = parseString(); + if (id == undefined) { + throw new SyntaxError('String with id expected ' + pos()); + } + var attr = parseAttributes(); + addNode(id, attr); + + // TODO: parse global attributes "graph", "node", "edge" + + // parse arrow + var type = parseArrow(); + while (type) { + // parse node id + var prevId = id; + id = parseString(); + if (id == undefined) { + throw new SyntaxError('String with id expected ' + pos()); + } + addNode(id); + + // parse edge attributes and register edge + attr = parseAttributes(); + addEdge(prevId, id, type, attr); + + // parse next arrow (optional) + type = parseArrow(); + } + + // parse separator (optional) + parseSeparator(); + + parseWhitespace(); + } + if (c != '}') { + throw new SyntaxError('Invalid data. Curly bracket } expected'); + } + + // crop data between the curly brackets + var start = data.indexOf('{'); + var end = data.indexOf('}', start); + var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined; + + if (!text) { + throw new Error('Invalid data. no curly brackets containing data found'); + } + + // return the results + var nodeList = []; + for (id in nodeMap) { + if (nodeMap.hasOwnProperty(id)) { + nodeList.push(nodeMap[id]); + } + } + return { + nodes: nodeList, + edges: edgeList + } +}; + +/** + * 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 + */ +Graph.util.DOTToGraph = function (data) { + // parse the DOT file + var dotData = Graph.util.parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: { + nodes: {}, + edges: {} + } + }; + + /** + * Merge the properties of object b into object a, and adjust properties + * not supported by Graph (for example replace "shape" with "style" + * @param {Object} a + * @param {Object} b + * @param {Array} [ignore] Optional array with property names to be ignored + */ + function merge (a, b, ignore) { + for (var prop in b) { + if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) { + a[prop] = b[prop]; + } + } + + // Convert aliases to configuration settings supported by Graph + if (a.label) { + a.text = a.label; + delete a.label; + } + if (a.shape) { + a.style = a.shape; + delete a.shape; + } + } + + dotData.nodes.forEach(function (node) { + if (node.id.toLowerCase() == 'graph') { + merge(graphData.options, node.attr); + } + else if (node.id.toLowerCase() == 'node') { + merge(graphData.options.nodes, node.attr); + } + else if (node.id.toLowerCase() == 'edge') { + merge(graphData.options.edges, node.attr); + } + else { + var graphNode = {}; + graphNode.id = node.id; + graphNode.text = node.id; + merge(graphNode, node.attr); + graphData.nodes.push(graphNode); + } + }); + + dotData.edges.forEach(function (edge) { + var graphEdge = {}; + graphEdge.from = edge.from; + graphEdge.to = edge.to; + graphEdge.text = edge.id; + graphEdge.style = (edge.type == '->') ? 'arrow-end' : 'line'; + merge(graphEdge, edge.attr); + graphData.edges.push(graphEdge); + }); + + return graphData; +}; diff --git a/src/module/exports.js b/src/module/exports.js index 9d84c54f..91c9484a 100644 --- a/src/module/exports.js +++ b/src/module/exports.js @@ -28,7 +28,8 @@ var vis = { TimeAxis: TimeAxis }, - Timeline: Timeline + Timeline: Timeline, + Graph: Graph }; /** diff --git a/vis.js b/vis.js index 11a40b8d..e044cb3a 100644 --- a/vis.js +++ b/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 0.0.8 - * @date 2013-05-31 + * @date 2013-06-03 * * @license * Copyright (C) 2011-2013 Almende B.V, http://almende.com @@ -24,7 +24,7 @@ */ (function(e){if("function"==typeof bootstrap)bootstrap("vis",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=e}else"undefined"!=typeof window?window.vis=e():global.vis=e()})(function(){var define,ses,bootstrap,module,exports; return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s 0) ? + overlappingNodes[overlappingNodes.length - 1] : undefined; + + if (this.startClickedObj) { + // move clicked node with the mouse + + // make the clicked node temporarily fixed, and store their original state + var node = this.nodes[this.startClickedObj.row]; + this.startClickedObj.xFixed = node.xFixed; + this.startClickedObj.yFixed = node.yFixed; + node.xFixed = true; + node.yFixed = true; + + if (!this.ctrlKeyDown || !node.isSelected()) { + // select this node + this._selectNodes([this.startClickedObj], this.ctrlKeyDown); + } + else { + // unselect this node + this._unselectNodes([this.startClickedObj]); + } + + if (!this.hasMovingNodes) { + this._redraw(); + } + } + else if (this.shiftKeyDown) { + // start selection of multiple nodes + } + else { + // start moving the graph + this.moved = false; + } +}; /** - * Window exports + * handle on mouse move event */ -if (typeof window !== 'undefined') { - // attach the module to the window, load as a regular javascript file - window['vis'] = vis; -} +Graph.prototype._onMouseMove = function (event) { + event = event || window.event; -// inject css -util.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.point {\n background: none;\n}\n\n.graph .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n"); + if (!this.selectable) { + return; + } + + var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0; + var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0; + this.mouseX = mouseX; + this.mouseY = mouseY; + + if (this.startClickedObj) { + var node = this.nodes[this.startClickedObj.row]; + + if (!this.startClickedObj.xFixed) + node.x = this._xToCanvas(mouseX - this.startFrameLeft); + + if (!this.startClickedObj.yFixed) + node.y = this._yToCanvas(mouseY - this.startFrameTop); + + // start animation if not yet running + if (!this.hasMovingNodes) { + this.hasMovingNodes = true; + this.start(); + } + } + else if (this.shiftKeyDown) { + // draw a rect from start mouse location to current mouse location + if (this.frame.selRect == undefined) { + this.frame.selRect = document.createElement("DIV"); + this.frame.appendChild(this.frame.selRect); + + this.frame.selRect.style.position = "absolute"; + this.frame.selRect.style.border = "1px dashed red"; + } + + var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft; + var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop; + var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft; + var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop; + + this.frame.selRect.style.left = left + "px"; + this.frame.selRect.style.top = top + "px"; + this.frame.selRect.style.width = (right - left) + "px"; + this.frame.selRect.style.height = (bottom - top) + "px"; + } + else { + // move the network + var diffX = mouseX - this.startMouseX; + var diffY = mouseY - this.startMouseY; + + this._setTranslation( + this.startTranslation.x + diffX, + this.startTranslation.y + diffY); + this._redraw(); + + this.moved = true; + } + + Graph.preventDefault(event); +}; + +/** + * handle on mouse up event + */ +Graph.prototype._onMouseUp = function (event) { + event = event || window.event; + + if (!this.selectable) { + return; + } + + // remove event listeners here, important for Safari + if (this.onmousemove) { + Graph.removeEventListener(document, "mousemove", this.onmousemove); + this.onmousemove = undefined; + } + if (this.onmouseup) { + Graph.removeEventListener(document, "mouseup", this.onmouseup); + this.onmouseup = undefined; + } + Graph.preventDefault(event); + + // check selected nodes + var endMouseX = event.clientX || this.mouseX || 0; + var endMouseY = event.clientY || this.mouseY || 0; + + var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey; + + if (this.startClickedObj) { + // restore the original fixed state + var node = this.nodes[this.startClickedObj.row]; + node.xFixed = this.startClickedObj.xFixed; + node.yFixed = this.startClickedObj.yFixed; + } + else if (this.shiftKeyDown) { + // select nodes inside selection area + var obj = { + "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft), + "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop), + "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft), + "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop) + }; + var overlappingNodes = this._getNodesOverlappingWith(obj); + this._selectNodes(overlappingNodes, ctrlKey); + this.redraw(); + + // remove the selection rectangle + if (this.frame.selRect) { + this.frame.removeChild(this.frame.selRect); + this.frame.selRect = undefined; + } + } + else { + if (!this.ctrlKeyDown && !this.moved) { + // remove selection + this._unselectNodes(); + this._redraw(); + } + } + + this.leftButtonDown = false; + this.ctrlKeyDown = false; +}; + + +/** + * Event handler for mouse wheel event, used to zoom the timeline + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event + */ +Graph.prototype._onMouseWheel = function(event) { + event = event || window.event; + var mouseX = event.clientX; + var mouseY = event.clientY; + + // 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) { + // determine zoom factor, and adjust the zoom factor such that zooming in + // and zooming out correspond wich each other + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + + var scaleOld = this._getScale(); + var scaleNew = scaleOld * (1 + zoom); + if (scaleNew < 0.01) { + scaleNew = 0.01; + } + if (scaleNew > 10) { + scaleNew = 10; + } + + var frameLeft = Graph._getAbsoluteLeft(this.frame.canvas); + var frameTop = Graph._getAbsoluteTop(this.frame.canvas); + var x = mouseX - frameLeft; + var y = mouseY - frameTop; + + var translation = this._getTranslation(); + var scaleFrac = scaleNew / scaleOld; + var tx = (1 - scaleFrac) * x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * y + translation.y * scaleFrac; + + this._setScale(scaleNew); + this._setTranslation(tx, ty); + this._redraw(); + } + + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here... + Graph.preventDefault(event); +}; + + +/** + * Mouse move handler for checking whether the title moves over a node or + * package with a title. + */ +Graph.prototype._onMouseMoveTitle = function (event) { + event = event || window.event; + + var startMouseX = event.clientX; + var startMouseY = event.clientY; + this.startFrameLeft = this.startFrameLeft || Graph._getAbsoluteLeft(this.frame.canvas); + this.startFrameTop = this.startFrameTop || Graph._getAbsoluteTop(this.frame.canvas); + + var x = startMouseX - this.startFrameLeft; + var y = startMouseY - this.startFrameTop; + + // check if the previously selected node is still selected + if (this.popupNode) { + this._checkHidePopup(x, y); + } + + // start a timeout that will check if the mouse is positioned above + // an element + var me = this; + var checkShow = function() { + me._checkShowPopup(x, y); + }; + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running timer + } + if (!this.leftButtonDown) { + this.popupTimer = setTimeout(checkShow, 300); + } +}; + +/** + * Check if there is an element on the given position in the network ( + * (a node, package, or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {number} x + * @param {number} y + */ +Graph.prototype._checkShowPopup = function (x, y) { + var obj = { + "left" : this._xToCanvas(x), + "top" : this._yToCanvas(y), + "right" : this._xToCanvas(x), + "bottom" : this._yToCanvas(y) + }; + + var i, len; + var lastPopupNode = this.popupNode; + + if (this.popupNode == undefined) { + // search the packages for overlap + + for (i = 0, len = this.packages.length; i < len; i++) { + var p = this.packages[i]; + if (p.getTitle() != undefined && p.isOverlappingWith(obj)) { + this.popupNode = p; + break; + } + } + } + + if (this.popupNode == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.nodes; + for (i = nodes.length - 1; i >= 0; i--) { + var node = nodes[i]; + if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { + this.popupNode = node; + break; + } + } + } + + if (this.popupNode == undefined) { + // search the edges for overlap + var allEdges = this.edges; + for (i = 0, len = allEdges.length; i < len; i++) { + var edge = allEdges[i]; + if (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 Graph.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(x - 3, 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 {number} x + * @param {number} y + */ +Graph.prototype._checkHidePopup = function (x, y) { + var obj = { + "left" : x, + "top" : y, + "right" : x, + "bottom" : y + }; + if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) { + this.popupNode = undefined; + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Event handler for touchstart event on mobile devices + */ +Graph.prototype._onTouchStart = function(event) { + Graph.preventDefault(event); + + if (this.touchDown) { + // if already moving, return + return; + } + this.touchDown = true; + + var me = this; + if (!this.ontouchmove) { + this.ontouchmove = function (event) {me._onTouchMove(event);}; + Graph.addEventListener(document, "touchmove", this.ontouchmove); + } + if (!this.ontouchend) { + this.ontouchend = function (event) {me._onTouchEnd(event);}; + Graph.addEventListener(document, "touchend", this.ontouchend); + } + + this._onMouseDown(event); +}; + +/** + * Event handler for touchmove event on mobile devices + */ +Graph.prototype._onTouchMove = function(event) { + Graph.preventDefault(event); + this._onMouseMove(event); +}; + +/** + * Event handler for touchend event on mobile devices + */ +Graph.prototype._onTouchEnd = function(event) { + Graph.preventDefault(event); + + this.touchDown = false; + + if (this.ontouchmove) { + Graph.removeEventListener(document, "touchmove", this.ontouchmove); + this.ontouchmove = undefined; + } + if (this.ontouchend) { + Graph.removeEventListener(document, "touchend", this.ontouchend); + this.ontouchend = undefined; + } + + this._onMouseUp(event); +}; + + +/** + * Unselect selected nodes. If no selection array is provided, all nodes + * are unselected + * @param {Object[]} selection Array with selection objects, each selection + * object has a parameter row. Optional + * @param {Boolean} triggerSelect If true (default), the select event + * is triggered when nodes are unselected + * @return {Boolean} changed True if the selection is changed + */ +Graph.prototype._unselectNodes = function(selection, triggerSelect) { + var changed = false; + var i, iMax, row; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + row = selection[i].row; + this.nodes[row].unselect(); + + var j = 0; + while (j < this.selection.length) { + if (this.selection[j].row == row) { + this.selection.splice(j, 1); + changed = true; + } + else { + j++; + } + } + } + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + row = this.selection[i].row; + this.nodes[row].unselect(); + changed = true; + } + this.selection = []; + } + + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this.trigger('select'); + } + + return changed; +}; + +/** + * select all nodes on given location x, y + * @param {Array} selection an array with selection objects. Each selection + * object has a parameter row + * @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 + */ +Graph.prototype._selectNodes = function(selection, append) { + var changed = false; + var i, iMax; + + // TODO: the selectNodes method is a little messy, rework this + + // check if the current selection equals the desired selection + var selectionAlreadyDone = true; + if (selection.length != this.selection.length) { + selectionAlreadyDone = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i].row != this.selection[i].row) { + selectionAlreadyDone = false; + break; + } + } + } + if (selectionAlreadyDone) { + return changed; + } + + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } + + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var row = selection[i].row; + var isDuplicate = false; + for (var j = 0, jMax = this.selection.length; j < jMax; j++) { + if (this.selection[j].row == row) { + isDuplicate = true; + break; + } + } + + if (!isDuplicate) { + this.nodes[row].select(); + this.selection.push(selection[i]); + changed = true; + } + } + + if (changed) { + // fire the select event + this.trigger('select'); + } + + return changed; +}; + +/** + * retrieve all nodes overlapping with given object + * @param {Object} obj An object with parameters left, top, right, bottom + * @return {Object[]} An array with selection objects containing + * the parameter row. + */ +Graph.prototype._getNodesOverlappingWith = function (obj) { + var overlappingNodes = []; + + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i].isOverlappingWith(obj)) { + var sel = {"row": i}; + overlappingNodes.push(sel); + } + } + + return overlappingNodes; +}; + +/** + * retrieve the currently selected nodes + * @return {Object[]} an array with zero or more objects. Each object + * contains the parameter row + */ +Graph.prototype.getSelection = function() { + var selection = []; + + for (var i = 0; i < this.selection.length; i++) { + var row = this.selection[i].row; + selection.push({"row": row}); + } + + return selection; +}; + +/** + * select zero or more nodes + * @param {object[]} selection an array with zero or more objects. Each object + * contains the parameter row + */ +Graph.prototype.setSelection = function(selection) { + var i, iMax, row; + + if (selection.length == undefined) + throw "Selection must be an array with objects"; + + // first unselect any selected node + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + row = this.selection[i].row; + this.nodes[row].unselect(); + } + + this.selection = []; + + for (i = 0, iMax = selection.length; i < iMax; i++) { + row = selection[i].row; + + if (row == undefined) + throw "Parameter row missing in selection object"; + if (row > this.nodes.length-1) + throw "Parameter row out of range"; + + var sel = {"row": row}; + this.selection.push(sel); + this.nodes[row].select(); + } + + this.redraw(); +}; + + +/** + * 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 + */ +Graph.prototype._getConnectionCount = function(level) { + var conn = this.edges; + 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 + for (var i = 0, iMax = conn.length; i < iMax; i++) { + var other = null; + + // check if connected + if (conn[i].from == node) + other = conn[i].to; + else if (conn[i].to == node) + other = conn[i].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 level0 = []; + var nodes = this.nodes; + var i, iMax; + for (i = 0, iMax = nodes.length; i < iMax; i++) { + var c = [nodes[i]]; + for (var l = 0; l < level; l++) { + c = c.concat(getConnectedNodes(c)); + } + connections.push(c); + } + + var hubs = []; + for (i = 0, len = connections.length; i < len; i++) { + hubs.push(connections[i].length); + } + + return hubs; +}; + + +/** + * Set a new size for the network + * @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.slider) { + this.slider.redraw(); + } +}; + +/** + * Convert a Google DataTable to a Javascript Array + * @param {google.visualization.DataTable} table + * @return {Array} array + */ +Graph.tableToArray = function(table) { + var array = []; + var col; + + // read the column names + var colCount = table.getNumberOfColumns(); + var cols = {}; + for (col = 0; col < colCount; col++) { + var label = table.getColumnLabel(col); + cols[label] = col; + } + + var rowCount = table.getNumberOfRows(); + for (var i = 0; i < rowCount; i++) { + // copy all properties from the table columns to an object + var properties = {}; + for (col in cols) { + if (cols.hasOwnProperty(col)) { + properties[col] = table.getValue(i, cols[col]); + } + } + + array.push(properties); + } + + return array; +}; + +/** + * Append nodes + * Nodes with a duplicate id will be replaced + * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes. + */ +Graph.prototype.addNodes = function(nodesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + nodesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(nodesTable); + } + else if (Graph.isArray(nodesTable)){ + // Javascript Array + table = nodesTable; + } + else { + return; + } + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.value != undefined) { + hasValues = true; + } + if (properties.id == undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + + this._createNode(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.nodes); + } + + this.start(); +}; + +/** + * Load all nodes by reading the data table nodesTable + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes. + */ +Graph.prototype.setNodes = function(nodesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + nodesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(nodesTable); + } + else if (Graph.isArray(nodesTable)){ + // Javascript Array + table = nodesTable; + } + else { + return; + } + + this.hasMovingNodes = false; + this.nodesTable = table; + this.nodes = []; + this.selection = []; + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.value != undefined) { + hasValues = true; + } + if (properties.timestamp) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + if (properties.id == undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + this._createNode(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.nodes); + } +}; + +/** + * Filter the current nodes table for nodes with a timestamp older than given + * timestamp. Can only be used for nodes added via setNodes(), not via + * addNodes(). + * @param {*} [timestamp] If timestamp is undefined, all nodes are shown + */ +Graph.prototype._filterNodes = function(timestamp) { + if (this.nodesTable == undefined) { + return; + } + + // remove existing nodes with a too new timestamp + if (timestamp !== undefined) { + var ns = this.nodes; + var n = 0; + while (n < ns.length) { + var t = ns[n].timestamp; + if (t !== undefined && t > timestamp) { + // remove this node + ns.splice(n, 1); + } + else { + n++; + } + } + } + + // add all nodes with an old enough timestamp + var table = this.nodesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + // copy all properties + var properties = table[i]; + + if (properties.id === undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + + // check what the timestamp is + var ts = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (ts !== undefined && timestamp !== undefined && ts > timestamp) { + visible = false; + } + + if (visible) { + // create or update the node + this._createNode(properties); + } + } + + this.start(); +}; + +/** + * Create a node with the given properties + * If the new node has an id identical to an existing package, the existing + * node will be overwritten. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createNode = function(properties) { + var action = properties.action ? properties.action : "update"; + var id, index, newNode, oldNode; + + if (action === "create") { + // create the node + newNode = new Graph.Node(properties, this.images, this.groups, this.constants); + id = properties.id; + index = (id !== undefined) ? this._findNode(id) : undefined; + + if (index !== undefined) { + // replace node + oldNode = this.nodes[index]; + this.nodes[index] = newNode; + + // remove selection of old node + if (oldNode.selected) { + this._unselectNodes([{'row': index}], false); + } + + /* TODO: implement this? -> will give performance issues, searching all edges and node... + // update edges linking to this node + var edgesTable = this.edges; + for (var i = 0, iMax = edgesTable.length; i < iMax; i++) { + var edge = edgesTable[i]; + if (edge.from == oldNode) { + edge.from = newNode; + } + if (edge.to == oldNode) { + edge.to = newNode; + } + } + + // update packages linking to this node + var packagesTable = this.packages; + for (var i = 0, iMax = packagesTable.length; i < iMax; i++) { + var package = packagesTable[i]; + if (package.from == oldNode) { + package.from = newNode; + } + if (package.to == oldNode) { + package.to = newNode; + } + } + */ + } + else { + // add new node + this.nodes.push(newNode); + } + + if (!newNode.isFixed()) { + // 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.hasMovingNodes = true; + } + } + else if (action === "update") { + // update existing node, or create it when not yet existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a node without id"; + } + + index = this._findNode(id); + if (index !== undefined) { + // update node + this.nodes[index].setProperties(properties, this.constants); + } + else { + // create node + newNode = new Graph.Node(properties, this.images, this.groups, this.constants); + this.nodes.push(newNode); + + if (!newNode.isFixed()) { + // 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.hasMovingNodes = true; + } + } + } + else if (action === "delete") { + // delete existing node + id = properties.id; + if (id === undefined) { + throw "Cannot delete node without its id"; + } + + index = this._findNode(id); + if (index !== undefined) { + oldNode = this.nodes[index]; + // remove selection of old node + if (oldNode.selected) { + this._unselectNodes([{'row': index}], false); + } + this.nodes.splice(index, 1); + } + else { + throw "Node with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Find a node by its id + * @param {Number} id Id of the node + * @return {Number} index Index of the node in the array this.nodes, or + * undefined when not found. * + */ +Graph.prototype._findNode = function (id) { + var nodes = this.nodes; + for (var n = 0, len = nodes.length; n < len; n++) { + if (nodes[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a node by its rowNumber + * @param {Number} row Row number of the node + * @return {Graph.Node} node     The node with the given row number, or + *                            undefined when not found. + */ +Graph.prototype._findNodeByRow = function (row) { + return this.nodes[row]; +}; + +/** + * Load edges by reading the data table + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges. + */ +Graph.prototype.setEdges = function(edgesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + edgesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(edgesTable); + } + else if (Graph.isArray(edgesTable)){ + // Javascript Array + table = edgesTable; + } + else { + return; + } + + this.edgesTable = table; + this.edges = []; + this.hasMovingEdges = false; + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + if (properties.timestamp != undefined) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + if (properties.value != undefined) { + hasValues = true; + } + + this._createEdge(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.edges); + } +}; + + +/** + * Load edges by reading the data table + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges. + */ +Graph.prototype.addEdges = function(edgesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + edgesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(edgesTable); + } + else if (Graph.isArray(edgesTable)){ + // Javascript Array + table = edgesTable; + } + else { + return; + } + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + // copy all properties + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + if (properties.value != undefined) { + hasValues = true; + } + + this._createEdge(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.edges); + } + + this.start(); +}; + + +/** + * Filter the current edges table for edges with a timestamp below given + * timestamp. Can only be used for edges added via setEdges(), not via + * addEdges(). + * @param {*} [timestamp] If timestamp is undefined, all edges are shown + */ +Graph.prototype._filterEdges = function(timestamp) { + if (this.edgesTable == undefined) { + return; + } + + // remove existing packages with a too new timestamp + if (timestamp !== undefined) { + var ls = this.edges; + var l = 0; + while (l < ls.length) { + var t = ls[l].timestamp; + if (t !== undefined && t > timestamp) { + // remove this edge + ls.splice(l, 1); + } + else { + l++; + } + } + } + + // add all edges with an old enough timestamp + var table = this.edgesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + + // check what the timestamp is + var ts = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (ts !== undefined && timestamp !== undefined && ts > timestamp) { + visible = false; + } + + if (visible) { + // create or update the edge + this._createEdge(properties); + } + } + + this.start(); +}; + +/** + * Create a edge with the given properties + * If the new edge has an id identical to an existing edge, the existing + * edge will be overwritten or updated. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createEdge = function(properties) { + var action = properties.action ? properties.action : "create"; + var id, index, edge, oldEdge, newEdge; + + if (action === "create") { + // create the edge, or replace it if already existing + id = properties.id; + index = (id !== undefined) ? this._findEdge(id) : undefined; + edge = new Graph.Edge(properties, this, this.constants); + + if (index !== undefined) { + // replace existing edge + oldEdge = this.edges[index]; + oldEdge.from.detachEdge(oldEdge); + oldEdge.to.detachEdge(oldEdge); + this.edges[index] = edge; + } + else { + // add new edge + this.edges.push(edge); + } + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + + if (edge.isMoving()) { + this.hasMovingEdges = true; + } + } + else if (action === "update") { + // update existing edge, or create the edge if not existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a edge without id"; + } + + index = this._findEdge(id); + if (index !== undefined) { + // update edge + edge = this.edges[index]; + edge.from.detachEdge(edge); + edge.to.detachEdge(edge); + + edge.setProperties(properties, this.constants); + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + } + else { + // add new edge + edge = new Graph.Edge(properties, this, this.constants); + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + this.edges.push(edge); + if (edge.isMoving()) { + this.hasMovingEdges = true; + } + } + } + else if (action === "delete") { + // delete existing edge + id = properties.id; + if (id === undefined) { + throw "Cannot delete edge without its id"; + } + + index = this._findEdge(id); + if (index !== undefined) { + oldEdge = this.edges[id]; + edge.from.detachEdge(oldEdge); + edge.to.detachEdge(oldEdge); + this.edges.splice(index, 1); + } + else { + throw "Edge with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Update the edge to oldNode in all edges and packages. + * @param {Node} oldNode + * @param {Node} newNode + */ +// TODO: start utilizing this method _updateNodeReferences +Graph.prototype._updateNodeReferences = function(oldNode, newNode) { + var arrays = [this.edges, this.packages]; + for (var a = 0, aMax = arrays.length; a < aMax; a++) { + var array = arrays[a]; + for (var i = 0, iMax = array.length; i < iMax; i++) { + if (array.from === oldNode) { + array.from = newNode; + } + if (array.to === oldNode) { + array.to = newNode; + } + } + } +}; + +/** + * Find a edge by its id + * @param {Number} id Id of the edge + * @return {Number} index Index of the edge in the array this.edges, or + * undefined when not found. * + */ +Graph.prototype._findEdge = function (id) { + var edges = this.edges; + for (var n = 0, len = edges.length; n < len; n++) { + if (edges[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a edge by its row + * @param {Number} row Row of the edge + * @return {Graph.Edge} the found edge, or undefined when not found + */ +Graph.prototype._findEdgeByRow = function (row) { + return this.edges[row]; +}; + +/** + * Append packages + * Packages with a duplicate id will be replaced + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages. + */ +Graph.prototype.addPackages = function(packagesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + packagesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(packagesTable); + } + else if (Graph.isArray(packagesTable)){ + // Javascript Array + table = packagesTable; + } + else { + return; + } + + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + + this._createPackage(properties); + } + + // calculate scaling function when value is provided + this._updateValueRange(this.packages); + + this.start(); +}; + +/** + * Set a new packages table + * Packages with a duplicate id will be replaced + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages. + */ +Graph.prototype.setPackages = function(packagesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + packagesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(packagesTable); + } + else if (Graph.isArray(packagesTable)){ + // Javascript Array + table = packagesTable; + } + else { + return; + } + + this.packagesTable = table; + this.packages = []; + + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + if (properties.timestamp) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + + this._createPackage(properties); + } + + // calculate scaling function when value is provided + this._updateValueRange(this.packages); + + /* TODO: adjust examples and documentation for this? + this.start(); + */ +}; + +/** + * Filter the current package table for packages with a timestamp below given + * timestamp. Can only be used for packages added via setPackages(), not via + * addPackages(). + * @param {*} [timestamp] If timestamp is undefined, all packages are shown + */ +Graph.prototype._filterPackages = function(timestamp) { + if (this.packagesTable == undefined) { + return; + } + + // remove all current packages + this.packages = []; + + /* TODO: cleanup + // remove existing packages with a too new timestamp + if (timestamp !== undefined) { + var packages = this.packages; + var p = 0; + while (p < packages.length) { + var package = packages[p]; + var t = package.timestamp; + + if (t !== undefined && t > timestamp ) { + // remove this package + packages.splice(p, 1); + } + else { + p++; + } + } + } + */ + + // add all packages with an old enough timestamp + var table = this.packagesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + // check what the timestamp is + var pTimestamp = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (pTimestamp !== undefined && timestamp !== undefined && pTimestamp > timestamp) { + visible = false; + } + + if (visible === true) { + if (properties.progress == undefined) { + // when no progress is provided, we need to add our own progress + var duration = properties.duration || this.constants.packages.duration; // seconds + + var diff = (timestamp.getTime() - pTimestamp.getTime()) / 1000; // seconds + if (diff < duration) { + // copy the properties, and fill in the current progress based on the + // timestamp and the duration + var original = properties; + properties = {}; + for (var j in original) { + if (original.hasOwnProperty(j)) { + properties[j] = original[j]; + } + } + + properties.progress = diff / duration; // scale 0-1 + } + else { + visible = false; + } + } + } + + if (visible === true) { + // create or update the package + this._createPackage(properties); + } + } + + this.start(); +}; + +/** + * Create a package with the given properties + * If the new package has an id identical to an existing package, the existing + * package will be overwritten. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createPackage = function(properties) { + var action = properties.action ? properties.action : "create"; + var id, index, newPackage; + + if (action === "create") { + // create the package + id = properties.id; + index = (id !== undefined) ? this._findPackage(id) : undefined; + newPackage = new Graph.Package(properties, this, this.images, this.constants); + + if (index !== undefined) { + // replace existing package + this.packages[index] = newPackage; + } + else { + // add new package + this.packages.push(newPackage); + } + + if (newPackage.isMoving()) { + this.hasMovingPackages = true; + } + } + else if (action === "update") { + // update a package, or create it when not existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a edge without id"; + } + + index = this._findPackage(id); + if (index !== undefined) { + // update existing package + this.packages[index].setProperties(properties, this.constants); + } + else { + // add new package + newPackage = new Graph.Package(properties, this, this.images, this.constants); + this.packages.push(newPackage); + if (newPackage.isMoving()) { + this.hasMovingPackages = true; + } + } + } + else if (action === "delete") { + // delete existing package + id = properties.id; + if (id === undefined) { + throw "Cannot delete package without its id"; + } + + index = this._findPackage(id); + if (index !== undefined) { + this.packages.splice(index, 1); + } + else { + throw "Package with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Find a package by its id. + * @param {Number} id + * @return {Number} index Index of the package in the array this.packages, + * or undefined when not found + */ +Graph.prototype._findPackage = function (id) { + var packages = this.packages; + for (var n = 0, len = packages.length; n < len; n++) { + if (packages[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a package by its row + * @param {Number} row Row of the package + * @return {Graph.Package} the found package, or undefined when not found + */ +Graph.prototype._findPackageByRow = function (row) { + return this.packages[row]; +}; + +/** + * Retrieve an object which maps the column ids by their names + * For example a table with columns [id, name, value] will return an + * object {"id": 0, "name": 1, "value": 2} + * @param {google.visualization.DataTable} table A google datatable + * @return {Object} columnIds An object + */ +// TODO: cleanup this unused method +Graph.prototype._getColumnNames = function (table) { + var colCount = table.getNumberOfColumns(); + var cols = {}; + for (var col = 0; col < colCount; col++) { + var label = table.getColumnLabel(col); + cols[label] = col; + } + return cols; +}; + + +/** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Array} array. An array with objects like Edges, Nodes, or Packages + * The objects must have a method getValue() and + * setValueRange(min, max). + */ +Graph.prototype._updateValueRange = function(array) { + var count = array.length; + var i; + + // determine the range of the node values + var valueMin = undefined; + var valueMax = undefined; + for (i = 0; i < count; i++) { + var value = array[i].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 nodes + if (valueMin !== undefined && valueMax !== undefined) { + for (i = 0; i < count; i++) { + array[i].setValueRange(valueMin, valueMax); + } + } +}; + + +/** + * Set the current timestamp. All packages with a timestamp smaller or equal + * than the given timestamp will be drawn. + * @param {Date | Number} timestamp + */ +Graph.prototype.setTimestamp = function(timestamp) { + this._filterNodes(timestamp); + this._filterEdges(timestamp); + this._filterPackages(timestamp); +}; + + +/** + * Get the range of all timestamps defined in the nodes, edges and packages + * @return {Object} A range object, containing parameters start and end. + */ +Graph.prototype._getRange = function() { + // range is stored as number. at the end of the method, it is converted to + // Date when needed. + var range = { + "start": undefined, + "end": undefined + }; + + var tables = [this.nodesTable, this.edgesTable]; + for (var t = 0, tMax = tables.length; t < tMax; t++) { + var table = tables[t]; + + if (table !== undefined) { + for (var i = 0, iMax = table.length; i < iMax; i++) { + var timestamp = table[i].timestamp; + if (timestamp) { + // to long + if (timestamp instanceof Date) { + timestamp = timestamp.getTime(); + } + + // calculate new range + range.start = range.start ? Math.min(timestamp, range.start) : timestamp; + range.end = range.end ? Math.max(timestamp, range.end) : timestamp; + } + } + } + } + + // calculate the range for the packagesTable by hand. In case of packages + // without a progress provided, we need to calculate the end time by hand. + if (this.packagesTable) { + var packagesTable = this.packagesTable; + for (var row = 0, len = packagesTable.length; row < len; row ++) { + var pkg = packagesTable[row], + timestamp = pkg.timestamp, + progress = pkg.progress, + duration = pkg.duration || this.constants.packages.duration; + + // convert to number + if (timestamp instanceof Date) { + timestamp = timestamp.getTime(); + } + + if (timestamp != undefined) { + var start = timestamp, + end = progress ? timestamp : (timestamp + duration * 1000); + + range.start = range.start ? Math.min(start, range.start) : start; + range.end = range.end ? Math.max(end, range.end) : end; + } + } + } + + // convert to the right type: number or date + var rangeFormat = { + "start": new Date(range.start), + "end": new Date(range.end) + }; + + return rangeFormat; +}; + +/** + * Start animation. + * Only applicable when packages with a timestamp are available + */ +Graph.prototype.animationStart = function() { + if (this.slider) { + this.slider.play(); + } +}; + +/** + * Start animation. + * Only applicable when packages with a timestamp are available + */ +Graph.prototype.animationStop = function() { + if (this.slider) { + this.slider.stop(); + } +}; + +/** + * Set framerate for the animation. + * Only applicable when packages with a timestamp are available + * @param {number} framerate The framerate in frames per second + */ +Graph.prototype.setAnimationFramerate = function(framerate) { + if (this.slider) { + this.slider.setFramerate(framerate); + } +} + +/** + * Set the duration of playing the whole package history + * Only applicable when packages with a timestamp are available + * @param {number} duration The duration in seconds + */ +Graph.prototype.setAnimationDuration = function(duration) { + if (this.slider) { + this.slider.setDuration(duration); + } +}; + +/** + * Set the time acceleration for playing the history. + * Only applicable when packages with a timestamp are available + * @param {number} acceleration Acceleration, for example 10 means play + * ten times as fast as real time. A value + * of 1 will play the history in real time. + */ +Graph.prototype.setAnimationAcceleration = function(acceleration) { + if (this.slider) { + this.slider.setAcceleration(acceleration); + } +}; + +/** + * 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 + */ +Graph.prototype._redraw = function() { + var ctx = this.frame.canvas.getContext("2d"); + + // clear the canvas + var w = this.frame.canvas.width; + var h = this.frame.canvas.height; + ctx.clearRect(0, 0, w, h); + + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); + + this._drawEdges(ctx); + this._drawNodes(ctx); + this._drawPackages(ctx); + this._drawSlider(); + + // restore original scaling and translation + ctx.restore(); +}; + +/** + * Set the translation of the graph + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset + */ +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 + */ +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 + */ +Graph.prototype._setScale = function(scale) { + this.scale = scale; +}; +/** + * Get the current scale of the graph + * @return {Number} scale Scaling factor 1.0 is unscaled + */ +Graph.prototype._getScale = function() { + return this.scale; +}; + +Graph.prototype._xToCanvas = function(x) { + return (x - this.translation.x) / this.scale; +}; + +Graph.prototype._canvasToX = function(x) { + return x * this.scale + this.translation.x; +}; + +Graph.prototype._yToCanvas = function(y) { + return (y - this.translation.y) / this.scale; +}; + +Graph.prototype._canvasToY = function(y) { + return y * this.scale + this.translation.y ; +}; + + + +/** + * Get a node by its id + * @param {number} id + * @return {Node} node, or null if not found + */ +Graph.prototype._getNode = function(id) { + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i].id == id) + return this.nodes[i]; + } + + return null; +}; + +/** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawNodes = function(ctx) { + // first draw the unselected nodes + var nodes = this.nodes; + var selected = []; + for (var i = 0, iMax = nodes.length; i < iMax; i++) { + if (nodes[i].isSelected()) { + selected.push(i); + } + else { + nodes[i].draw(ctx); + } + } + + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + nodes[selected[s]].draw(ctx); + } +}; + +/** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawEdges = function(ctx) { + var edges = this.edges; + for (var i = 0, iMax = edges.length; i < iMax; i++) { + edges[i].draw(ctx); + } +}; + +/** + * Redraw all packages + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawPackages = function(ctx) { + var packages = this.packages; + for (var i = 0, iMax = packages.length; i < iMax; i++) { + packages[i].draw(ctx); + } +}; + + +/** + * Redraw the filter + */ +Graph.prototype._drawSlider = function() { + var sliderNode; + if (this.hasTimestamps) { + sliderNode = this.frame.slider; + if (sliderNode === undefined) { + sliderNode = document.createElement( "div" ); + sliderNode.style.position = "absolute"; + sliderNode.style.bottom = "0px"; + sliderNode.style.left = "0px"; + sliderNode.style.right = "0px"; + sliderNode.style.backgroundColor = "rgba(255, 255, 255, 0.7)"; + + this.frame.slider = sliderNode; + this.frame.slider.style.padding = "10px"; + //this.frame.filter.style.backgroundColor = "#EFEFEF"; + this.frame.appendChild(sliderNode); + + + var range = this._getRange(); + this.slider = new Graph.Slider(sliderNode); + this.slider.setLoop(false); + this.slider.setRange(range.start, range.end); + + // create an event handler + var me = this; + var onchange = function () { + var timestamp = me.slider.getValue(); + me.setTimestamp(timestamp); + // TODO: do only a redraw when the graph is not still moving + me.redraw(); + }; + this.slider.setOnChangeCallback(onchange); + onchange(); // perform the first update by hand. + } + } + else { + sliderNode = this.frame.slider; + if (sliderNode !== undefined) { + this.frame.removeChild(sliderNode); + this.frame.slider = undefined; + this.slider = undefined; + } + } +}; + +/** + * Recalculate the best positions for all nodes + */ +Graph.prototype._reposition = function() { + // TODO: implement function reposition + + + /* + var w = this.frame.canvas.clientWidth; + var h = this.frame.canvas.clientHeight; + for (var i = 0; i < this.nodes.length; i++) { + if (!this.nodes[i].xFixed) this.nodes[i].x = w * Math.random(); + if (!this.nodes[i].yFixed) this.nodes[i].y = h * Math.random(); + } + //*/ + + //* + // TODO + var radius = this.constants.edges.length * 2; + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + for (var i = 0; i < this.nodes.length; i++) { + var angle = 2*Math.PI * (i / this.nodes.length); + + if (!this.nodes[i].xFixed) this.nodes[i].x = cx + radius * Math.cos(angle); + if (!this.nodes[i].yFixed) this.nodes[i].y = cy + radius * Math.sin(angle); + + } + //*/ + + /* + // TODO + var radius = this.constants.edges.length * 2; + var w = this.frame.canvas.clientWidth, + h = this.frame.canvas.clientHeight; + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + var s = Math.sqrt(this.nodes.length); + for (var i = 0; i < this.nodes.length; i++) { + //var angle = 2*Math.PI * (i / this.nodes.length); + + if (!this.nodes[i].xFixed) this.nodes[i].x = w/s * (i % s); + if (!this.nodes[i].yFixed) this.nodes[i].y = h/s * (i / s); + } + //*/ + + + /* + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + for (var i = 0; i < this.nodes.length; i++) { + this.nodes[i].x = cx; + this.nodes[i].y = cy; + } + + //*/ + +}; + + +/** + * Find a stable position for all nodes + */ +Graph.prototype._doStabilize = function() { + var start = new Date(); + + // find stable position + var count = 0; + var vmin = this.constants.minVelocity; + var stable = false; + while (!stable && count < this.constants.maxIterations) { + this._calculateForces(); + this._discreteStepNodes(); + stable = !this.isMoving(vmin); + count++; + } + + var end = new Date(); + + //console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup +}; + +/** + * Calculate the external forces acting on the nodes + * Forces are caused by: edges, repulsing forces between nodes, gravity + */ +Graph.prototype._calculateForces = function(nodeId) { + // create a local edge to the nodes and edges, that is faster + var nodes = this.nodes, + edges = this.edges; + + // gravity, add a small constant force to pull the nodes towards the center of + // the graph + // Also, the forces are reset to zero in this loop by using _setForce instead + // of _addForce + var gravity = 0.01, + gx = this.frame.canvas.clientWidth / 2, + gy = this.frame.canvas.clientHeight / 2; + for (var n = 0; n < nodes.length; n++) { + var dx = gx - nodes[n].x, + dy = gy - nodes[n].y, + angle = Math.atan2(dy, dx), + fx = Math.cos(angle) * gravity, + fy = Math.sin(angle) * gravity; + + this.nodes[n]._setForce(fx, fy); + } + + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var n = 0; n < nodes.length; n++) { + for (var n2 = n + 1; n2 < this.nodes.length; n2++) { + //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), + + // calculate normally distributed force + var dx = nodes[n2].x - nodes[n].x, + dy = nodes[n2].y - nodes[n].y, + 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; + + this.nodes[n]._addForce(-fx, -fy); + this.nodes[n2]._addForce(fx, fy); + } + /* TODO: re-implement repulsion of edges + for (var l = 0; l < edges.length; l++) { + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + + // calculate normally distributed force + dx = nodes[n].x - lx, + dy = nodes[n].y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + nodes[n]._addForce(fx, fy); + edges[l].from._addForce(-fx/2,-fy/2); + edges[l].to._addForce(-fx/2,-fy/2); + } + */ + } + + // forces caused by the edges, modelled as springs + for (var l = 0, lMax = edges.length; l < lMax; l++) { + var edge = edges[l], + + dx = (edge.to.x - edge.from.x), + dy = (edge.to.y - edge.from.y), + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length, // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length, // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2, + edgeLength = edge.length, + length = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + springforce = edge.stiffness * (edgeLength - length), + + fx = Math.cos(angle) * springforce, + fy = Math.sin(angle) * springforce; + + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } + + /* TODO: re-implement repulsion of edges + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } + */ +}; + + +/** + * Check if any of the nodes is still moving + * @param {number} vmin the minimum velocity considered as "moving" + * @return {boolean} true if moving, false if non of the nodes is moving + */ +Graph.prototype.isMoving = function(vmin) { + // TODO: ismoving does not work well: should check the kinetic energy, not its velocity + var nodes = this.nodes; + for (var n = 0, nMax = nodes.length; n < nMax; n++) { + if (nodes[n].isMoving(vmin)) { + return true; + } + } + return false; +}; + + +/** + * Perform one discrete step for all nodes + */ +Graph.prototype._discreteStepNodes = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var nodes = this.nodes; + for (var n = 0, nMax = nodes.length; n < nMax; n++) { + nodes[n].discreteStep(interval); + } +}; + + +/** + * Perform one discrete step for all packages + */ +Graph.prototype._discreteStepPackages = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var packages = this.packages; + for (var n = 0, nMax = packages.length; n < nMax; n++) { + packages[n].discreteStep(interval); + } +}; + + +/** + * Cleanup finished packages. + * also checks if there are moving packages + */ +Graph.prototype._deleteFinishedPackages = function() { + var n = 0; + var hasMovingPackages = false; + while (n < this.packages.length) { + if (this.packages[n].isFinished()) { + this.packages.splice(n, 1); + n--; + } + else if (this.packages[n].isMoving()) { + hasMovingPackages = true; + } + n++; + } + + this.hasMovingPackages = hasMovingPackages; +}; + +/** + * Start animating nodes, edges, and packages. + */ +Graph.prototype.start = function() { + if (this.hasMovingNodes) { + this._calculateForces(); + this._discreteStepNodes(); + + var vmin = this.constants.minVelocity; + this.hasMovingNodes = this.isMoving(vmin); + } + + if (this.hasMovingPackages) { + this._discreteStepPackages(); + this._deleteFinishedPackages(); + } + + if (this.hasMovingNodes || this.hasMovingEdges || this.hasMovingPackages) { + // start animation. only start timer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + graph.start(); + graph._redraw(); + }, this.refreshRate); + } + } + else { + this._redraw(); + } +}; + +/** + * Stop animating nodes, edges, and packages. + */ +Graph.prototype.stop = function () { + if (this.timer) { + window.clearInterval(this.timer); + this.timer = undefined; + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * 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 + */ +Graph.addEventListener = function (element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent("on" + action, listener); // IE browsers + } +}; + +/** + * Remove an event listener from an element + * @param {Element} element An html dom element + * @param {string} action The name of the event, for example "mousedown" + * @param {function} listener The listener function + * @param {boolean} useCapture + */ +Graph.removeEventListener = function(element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent("on" + action, listener); + } +}; + + +/** + * Stop event propagation + */ +Graph.stopPropagation = function (event) { + if (!event) + event = window.event; + + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers + } +}; + + +/** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ +Graph.preventDefault = function (event) { + if (!event) + event = window.event; + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } +}; + +/** + * Retrieve the absolute left value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} left The absolute left position of this element + * in the browser page. + */ +Graph._getAbsoluteLeft = function(elem) { + var left = 0; + while( elem != null ) { + left += elem.offsetLeft; + left -= elem.scrollLeft; + elem = elem.offsetParent; + } + if (!document.body.scrollLeft && window.pageXOffset) { + // FF + left -= window.pageXOffset; + } + return left; +}; + +/** + * Retrieve the absolute top value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} top The absolute top position of this element + * in the browser page. + */ +Graph._getAbsoluteTop = function(elem) { + var top = 0; + while( elem != null ) { + top += elem.offsetTop; + top -= elem.scrollTop; + elem = elem.offsetParent; + } + if (!document.body.scrollTop && window.pageYOffset) { + // FF + top -= window.pageYOffset; + } + return top; +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @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} text Title for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} style Drawing style, available: + * "database", "circle", "rect", + * "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 + */ +Graph.Node = function (properties, imagelist, grouplist, constants) { + this.selected = false; + + this.edges = []; // all edges connected to this node + this.group = constants.nodes.group; + + this.fontSize = constants.nodes.fontSize; + this.fontFace = constants.nodes.fontFace; + this.fontColor = constants.nodes.fontColor; + + this.borderColor = constants.nodes.borderColor; + this.backgroundColor = constants.nodes.backgroundColor; + this.highlightColor = constants.nodes.highlightColor; + + // set defaults for the properties + this.id = undefined; + this.style = constants.nodes.style; + this.image = constants.nodes.image; + this.x = 0; + this.y = 0; + this.xFixed = false; + this.yFixed = false; + this.radius = constants.nodes.radius; + this.radiusFixed = false; + this.radiusMin = constants.nodes.radiusMin; + this.radiusMax = constants.nodes.radiusMax; + + this.imagelist = imagelist; + this.grouplist = grouplist; + + this.setProperties(properties, constants); + + // mass, force, velocity + this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.fx = 0.0; // external force x + this.fy = 0.0; // external force y + this.vx = 0.0; // velocity x + this.vy = 0.0; // velocity y + this.minForce = constants.minForce; + this.damping = 0.9; // damping factor +}; + +/** + * Attach a edge to the node + * @param {Graph.Edge} edge + */ +Graph.Node.prototype.attachEdge = function(edge) { + this.edges.push(edge); + this._updateMass(); +}; + +/** + * Detach a edge from the node + * @param {Graph.Edge} edge + */ +Graph.Node.prototype.detachEdge = function(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + this._updateMass(); +}; + +/** + * Update the nodes mass, which is determined by the number of edges connecting + * to it (more edges -> heavier node). + * @private + */ +Graph.Node.prototype._updateMass = function() { + this.mass = 50 + 20 * this.edges.length; // kg +}; + +/** + * Set or overwrite properties for the node + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Node.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // basic properties + if (properties.id != undefined) {this.id = properties.id;} + if (properties.text != undefined) {this.text = properties.text;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.group != undefined) {this.group = properties.group;} + if (properties.x != undefined) {this.x = properties.x;} + if (properties.y != undefined) {this.y = properties.y;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + + 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 style properties + if (properties.style != undefined) {this.style = properties.style;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.borderColor != undefined) {this.borderColor = properties.borderColor;} + if (properties.backgroundColor != undefined){this.backgroundColor = properties.backgroundColor;} + if (properties.highlightColor != undefined) {this.highlightColor = properties.highlightColor;} + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + + + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + this.xFixed = this.xFixed || (properties.x != undefined); + this.yFixed = this.yFixed || (properties.y != undefined); + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + + if (this.style == 'image') { + this.radiusMin = constants.nodes.widthMin; + this.radiusMax = constants.nodes.widthMax; + } + + // choose draw method depending on the style + var style = this.style; + switch (style) { + case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; + case 'rect': this.draw = this._drawRect; this.resize = this._resizeRect; break; + case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; + // TODO: add ellipse shape + // 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._drawRect; this.resize = this._resizeRect; break; + } + + // reset the size of the node, this can be changed + this._reset(); +}; + +/** + * select this node + */ +Graph.Node.prototype.select = function() { + this.selected = true; + this._reset(); +}; + +/** + * unselect this node + */ +Graph.Node.prototype.unselect = function() { + this.selected = false; + this._reset(); +}; + +/** + * Reset the calculated size of the node, forces it to recalculate its size + */ +Graph.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. + */ +Graph.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 + */ +Graph.Node.prototype.distanceToBorder = function (ctx, angle) { + var borderWidth = 1; + + if (!this.width) { + this.resize(ctx); + } + + //noinspection FallthroughInSwitchStatementJS + switch (this.style) { + case 'circle': + case 'dot': + return this.radius + borderWidth; + + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown + + case 'rect': + 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 rect + } + 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 + */ +Graph.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 + */ +Graph.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 + */ +Graph.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 + */ +Graph.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 +Graph.Node.prototype.isMoving = function(vmin) { + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || + (!this.xFixed && Math.abs(this.fx) > this.minForce) || + (!this.yFixed && Math.abs(this.fy) > this.minForce)); +}; + +/** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ +Graph.Node.prototype.isSelected = function() { + return this.selected; +}; + +/** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ +Graph.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 + */ +Graph.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 + */ +Graph.Node.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + var scale = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * scale + this.radiusMin; + } +}; + +/** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.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 + */ +Graph.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 + */ +Graph.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); +}; + +Graph.Node.prototype._resizeImage = function (ctx) { + // TODO: pre calculate the image size + if (!this.width) { // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + this.width = width; + this.height = height; + } +}; + +Graph.Node.prototype._drawImage = function (ctx) { + this._resizeImage(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + var yText; + if (this.imageObj) { + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + yText = this.y + this.height / 2; + } + else { + // image still loading... just draw the text for now + yText = this.y; + } + + this._text(ctx, this.text, this.x, yText, undefined, "top"); +}; + + +Graph.Node.prototype._resizeRect = 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; + } +}; + +Graph.Node.prototype._drawRect = function (ctx) { + this._resizeRect(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.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; + } +}; + +Graph.Node.prototype._drawDatabase = function (ctx) { + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.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; + } +}; + +Graph.Node.prototype._drawCircle = function (ctx) { + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.circle(this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + +Graph.Node.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Graph.Node.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Graph.Node.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Graph.Node.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Graph.Node.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Graph.Node.prototype._resizeShape = function (ctx) { + if (!this.width) { + var size = 2 * this.radius; + this.width = size; + this.height = size; + } +}; + +Graph.Node.prototype._drawShape = function (ctx, shape) { + this._resizeShape(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + + ctx[shape](this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + if (this.text) { + this._text(ctx, this.text, this.x, this.y + this.height / 2, undefined, 'top'); + } +}; + +Graph.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; + } +}; + +Graph.Node.prototype._drawText = function (ctx) { + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.Node.prototype._text = 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; + } + } +}; + + +Graph.Node.prototype.getTextSize = function(ctx) { + if (this.text != undefined) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + + var lines = this.text.split('\n'), + height = (this.fontSize + 4) * lines.length, + width = 0; + + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); + } + + return {"width": width, "height": height}; + } + else { + return {"width": 0, "height": 0}; + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), 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 + */ +Graph.Edge = function (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.style = constants.edges.style; + this.title = undefined; + this.width = constants.edges.width; + this.value = undefined; + this.length = constants.edges.length; + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + this.dashlength = constants.edges.dashlength; + this.dashgap = constants.edges.dashgap; + this.altdashlength = constants.edges.altdashlength; + + this.stiffness = undefined; // depends on the length of the edge + this.color = constants.edges.color; + this.timestamp = undefined; + 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 + */ +Graph.Edge.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);} + if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);} + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.text != undefined) {this.text = properties.text;} + if (this.text) { + 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.dashlength != undefined) {this.dashlength = properties.dashlength;} + if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;} + if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;} + + if (properties.color != undefined) {this.color = properties.color;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + + if (!this.from) { + throw "Node with id " + properties.from + " not found"; + } + if (!this.to) { + throw "Node with id " + properties.to + " not found"; + } + + this.widthFixed = this.widthFixed || (properties.width != undefined); + this.lengthFixed = this.lengthFixed || (properties.length != undefined); + + this.stiffness = 1 / this.length; + + // initialize animation + if (this.style === 'arrow') { + this.arrows = [0.5]; + this.animation = false; + } + else if (this.style === 'arrow-end') { + this.animation = false; + } + else if (this.style === 'moving-arrows') { + this.arrows = []; + var arrowCount = 3; // TODO: make customizable + for (var a = 0; a < arrowCount; a++) { + this.arrows.push(a / arrowCount); + } + this.animation = true; + } + else if (this.style === 'moving-dot') { + this.dot = 0.0; + this.animation = true; + } + else { + this.animation = false; + } + + // 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-end': this.draw = this._drawArrowEnd; break; + case 'moving-arrows': this.draw = this._drawMovingArrows; break; + case 'moving-dot': this.draw = this._drawMovingDot; break; + case 'dash-line': this.draw = this._drawDashLine; break; + default: this.draw = this._drawLine; break; + } +}; + + + +/** + * Check if a node has an animating contents. If so, the graph needs to be + * redrawn regularly + * @return {boolean} true if this edge needs animation, else false + */ +Graph.Edge.prototype.isMoving = function() { + // TODO: be able to set the interval somehow + + return this.animation; +}; + +/** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ +Graph.Edge.prototype.getTitle = function() { + return this.title; +}; + + +/** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ +Graph.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 + */ +Graph.Edge.prototype.setValueRange = function(min, max) { + if (!this.widthFixed && this.value !== undefined) { + var factor = (this.widthMax - this.widthMin) / (max - min); + this.width = (this.value - min) * factor + this.widthMin; + } +}; + + +/** + * Check if the length is fixed. + * @return {boolean} lengthFixed True if the length is fixed, else false + */ +Graph.Edge.prototype.isLengthFixed = function() { + return this.lengthFixed; +}; + +/** + * Retrieve the length of the edge. Can be undefined + * @return {Number} length + */ +Graph.Edge.prototype.getLength = function() { + return this.length; +}; + +/** + * Adjust the length of the edge. This can only be done when the length + * is not fixed (which is the case when the edge is created with a length property) + * @param {Number} length + */ +Graph.Edge.prototype.setLength = function(length) { + if (!this.lengthFixed) { + this.length = length; + } +}; + +/** + * Retrieve the length of the edges dashes. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} dashlength + */ +Graph.Edge.prototype.getDashLength = function() { + return this.dashlength; +}; + +/** + * Adjust the length of the edges dashes. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} dashlength + */ +Graph.Edge.prototype.setDashLength = function(dashlength) { + this.dashlength = dashlength; +}; + +/** + * Retrieve the length of the edges dashes gaps. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} dashgap + */ +Graph.Edge.prototype.getDashGap = function() { + return this.dashgap; +}; + +/** + * Adjust the length of the edges dashes gaps. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} dashgap + */ +Graph.Edge.prototype.setDashGap = function(dashgap) { + this.dashgap = dashgap; +}; + +/** + * Retrieve the length of the edges alternate dashes. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} altdashlength + */ +Graph.Edge.prototype.getAltDashLength = function() { + return this.altdashlength; +}; + +/** + * Adjust the length of the edges alternate dashes. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} altdashlength + */ +Graph.Edge.prototype.setAltDashLength = function(altdashlength) { + this.altdashlength = altdashlength; +}; + + + +/** + * 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 + */ +Graph.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 + */ +Graph.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 = Graph._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return (dist < distMax); +}; + +/** + * 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 + */ +Graph._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); +}; + +/** + * 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 + */ +Graph.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 text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + var radius = this.length / 2 / Math.PI; + var x, y; + 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._text(ctx, this.text, 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 + */ +Graph.Edge.prototype._getLineWidth = function() { + if (this.from.selected || this.to.selected) { + return Math.min(this.width * 2, this.widthMax); + } + else { + return this.width; + } +}; + +/** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Graph.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 + */ +Graph.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 text with white background and with the middle at (x, y) + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} x + * @param {Number} y + */ +Graph.Edge.prototype._text = 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(this.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(this.text, left, top); + } +}; + +/** + * 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 + */ +var CP = (typeof window !== 'undefined') && + window.CanvasRenderingContext2D && + CanvasRenderingContext2D.prototype; +if (CP && CP.lineTo){ + CP.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; + } + } +} + +/** + * 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 + */ +Graph.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.altdashlength != 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.dashlength,this.dashgap,this.altdashlength,this.dashgap]); + } + else if (this.dashlength != undefined && this.dashgap != 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.dashlength,this.dashgap]); + } + 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 text + if (this.text) { + var point = this._pointOnLine(0.5); + this._text(ctx, this.text, 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 + */ +Graph.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 + */ +Graph.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 a moving 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 + */ +Graph.Edge.prototype._drawMovingArrows = function(ctx) { + this._drawArrow(ctx); + + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + this.arrows[a] += 0.02; // TODO determine speed from interval + if (this.arrows[a] > 1.0) this.arrows[a] = 0.0; + } + } +}; + +/** + * Redraw a edge as a line with a moving dot + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawMovingDot = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var point; + if (this.from != this.to) { + this._line(ctx); + + // draw dot + var radius = 4 + this.width * 2; + point = this._pointOnLine(this.dot); + ctx.circle(point.x, point.y, radius); + ctx.fill(); + + // move the dot to the next position + this.dot += 0.05; // TODO determine speed from interval + if (this.dot > 1.0) this.dot = 0.0; + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // TODO: moving dot for a circular edge + } +}; + + +/** + * 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 + */ +Graph.Edge.prototype._drawArrow = 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 all arrows + 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? + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + point = this._pointOnLine(this.arrows[a]); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + } + } + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // draw circle + var radius = this.length / 2 / Math.PI; + var x, y; + 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? + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + point = this._pointOnCircle(x, y, radius, this.arrows[a]); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + } + } + + // draw text + if (this.text) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, 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 + */ +Graph.Edge.prototype._drawArrowEnd = 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.to.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 text + if (this.text) { + var point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // draw circle + var radius = this.length / 2 / Math.PI; + var x, y, arrow; + 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; + 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 text + if (this.text) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + +}; + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Images + * This class loades images and keeps them stored. + */ +Graph.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 + */ +Graph.Images.prototype.setOnloadCallback = function(callback) { + this.callback = callback; +}; + + +/** + * + * @param {string} url Url of the image + * @return {Image} img The image object + */ +Graph.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; +}; + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Package + * This class contains one package + * + * @param {number} properties Properties for the package. Optional. Available + * properties are: id {number}, title {string}, + * style {string} with available values "dot" and + * "image", radius {number}, image {string}, + * color {string}, progress {number} with a value + * between 0-1, duration {number}, timestamp {number + * or Date}. + * @param {Graph} graph The graph object, used to find + * and edge to nodes. + * @param {Graph.Images} imagelist An Images object. Only needed + * when the package has style 'image' + * @param {Object} constants An object with default values for + * example for the color + */ +Graph.Package = function (properties, graph, imagelist, constants) { + if (graph == undefined) { + throw "No graph provided"; + } + + // constants + this.radiusMin = constants.packages.radiusMin; + this.radiusMax = constants.packages.radiusMax; + this.imagelist = imagelist; + this.graph = graph; + + // initialize variables + this.id = undefined; + this.from = undefined; + this.to = undefined; + this.title = undefined; + this.style = constants.packages.style; + this.radius = constants.packages.radius; + this.color = constants.packages.color; + this.image = constants.packages.image; + this.value = undefined; + this.progress = 0.0; + this.timestamp = undefined; + this.duration = constants.packages.duration; + this.autoProgress = true; + this.radiusFixed = false; + + // set properties + this.setProperties(properties, constants); +}; + +Graph.Package.DEFAULT_DURATION = 1.0; // seconds + +/** + * Set or overwrite properties for the package + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Package.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // note that the provided properties can also be null, when they come from the Google DataTable + if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);} + if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);} + + if (!this.from) { + throw "Node with id " + properties.from + " not found"; + } + if (!this.to) { + throw "Node with id " + properties.to + " not found"; + } + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.color != undefined) {this.color = properties.color;} + if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;} + if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;} + if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;} + if (properties.progress != undefined) {this.progress = properties.progress;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + if (properties.duration != undefined) {this.duration = properties.duration;} + + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + this.autoProgress = (this.autoProgress == true) ? (properties.progress == undefined) : false; + + if (this.style == 'image') { + this.radiusMin = constants.packages.widthMin; + this.radiusMax = constants.packages.widthMax; + } + + // handle progress + if (this.progress < 0.0) {this.progress = 0.0;} + if (this.progress > 1.0) {this.progress = 1.0;} + + // handle image + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + // choose draw method depending on the style + switch (this.style) { + // TODO: add more styles + case 'dot': this.draw = this._drawDot; break; + case 'square': this.draw = this._drawSquare; break; + case 'triangle': this.draw = this._drawTriangle; break; + case 'triangleDown':this.draw = this._drawTriangleDown; break; + case 'star': this.draw = this._drawStar; break; + case 'image': this.draw = this._drawImage; break; + default: this.draw = this._drawDot; break; + } +}; + +/** + * Set a new value for the progress of the package + * @param {number} progress A value between 0 and 1 + */ +Graph.Package.prototype.setProgress = function (progress) { + this.progress = progress; + this.autoProgress = false; +}; + +/** + * Check if a package is finished, if it has reached its destination. + * If so, the package can be removed. + * Only packages with automatically animated progress can be finished + * @return {boolean} true if finished, else false. + */ +Graph.Package.prototype.isFinished = function () { + return (this.autoProgress == true && this.progress >= 1.0); +}; + +/** + * Check if this package is moving. + * A packages moves when it has automatic progress and not yet reached its + * destination. + * @return {boolean} true if moving, else false. + */ +Graph.Package.prototype.isMoving = function () { + return (this.autoProgress || this.isFinished()); +}; + + +/** + * Perform one discrete step for the package. Only applicable when the + * package has no manually set, fixed progress. + * @param {number} interval Time interval in seconds + */ +Graph.Package.prototype.discreteStep = function(interval) { + if (this.autoProgress == true) { + this.progress += (parseFloat(interval) / this.duration); + + if (this.progress > 1.0) + this.progress = 1.0; + } +}; + + +/** + * Draw this package in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Package.prototype.draw = function(ctx) { + throw "Draw method not initialized for package"; +}; + + +/** + * 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 + */ +Graph.Package.prototype.isOverlappingWith = function(obj) { + // radius minimum 10px else it is too hard to get your mouse at the exact right position + var radius = Math.max(this.radius, 10); + var pos = this._getPosition(); + + return (pos.x - radius < obj.right && + pos.x + radius > obj.left && + pos.y - radius < obj.bottom && + pos.y + radius > obj.top); +}; + +/** + * Calculate the current position of the package + * @return {Object} position The object has parameters x and y. + */ +Graph.Package.prototype._getPosition = function() { + return { + "x" : (1 - this.progress) * this.from.x + this.progress * this.to.x, + "y" : (1 - this.progress) * this.from.y + this.progress * this.to.y + }; +}; + + +/** + * get the title of this package. + * @return {string} title The title of the package, or undefined when no + * title has been set. + */ +Graph.Package.prototype.getTitle = function() { + return this.title; +}; + +/** + * Retrieve the value of the package. Can be undefined + * @return {Number} value + */ +Graph.Package.prototype.getValue = function() { + return this.value; +}; + +/** + * Calculate the distance from the packages location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ +Graph.Package.prototype.getDistance = function(x, y) { + var pos = this._getPosition(), + dx = pos.x - x, + dy = pos.y - y; + return Math.sqrt(dx * dx + dy * dy); +}; + +/** + * Adjust the value range of the package. The package will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Graph.Package.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + var factor = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * factor + this.radiusMin; + } +}; + + + +/** + * Redraw a package as a dot + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +/* TODO: cleanup + Graph.Package.prototype._drawDot = function(ctx) { + // set style + ctx.fillStyle = this.color; + // draw dot + var pos = this._getPosition(); + ctx.circle(pos.x, pos.y, this.radius); + ctx.fill(); + } + */ + +Graph.Package.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Graph.Package.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Graph.Package.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Graph.Package.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Graph.Package.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Graph.Package.prototype._drawShape = function (ctx, shape) { + // set style + ctx.fillStyle = this.color; + + // draw shape + var pos = this._getPosition(); + ctx[shape](pos.x, pos.y, this.radius); + ctx.fill(); +}; + +/** + * Redraw a package as an image + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Package.prototype._drawImage = function (ctx) { + if (this.imageObj) { + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + var pos = this._getPosition(); + + ctx.drawImage(this.imageObj, pos.x - width / 2, pos.y - height / 2, width, height); + } + else { + console.log("image still loading..."); + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Groups + * This class can store groups and properties specific for groups. + */ +Graph.Groups = function () { + this.clear(); + this.defaultIndex = 0; +}; + + +/** + * default constants for group colors + */ +Graph.Groups.DEFAULT = [ + {"borderColor": "#2B7CE9", "backgroundColor": "#97C2FC", "highlightColor": "#D2E5FF"}, // blue + {"borderColor": "#FFA500", "backgroundColor": "#FFFF00", "highlightColor": "#FFFFA3"}, // yellow + {"borderColor": "#FA0A10", "backgroundColor": "#FB7E81", "highlightColor": "#FFAFB1"}, // red + {"borderColor": "#41A906", "backgroundColor": "#7BE141", "highlightColor": "#A1EC76"}, // green + {"borderColor": "#E129F0", "backgroundColor": "#EB7DF4", "highlightColor": "#F0B3F5"}, // magenta + {"borderColor": "#7C29F0", "backgroundColor": "#AD85E4", "highlightColor": "#D3BDF0"}, // purple + {"borderColor": "#C37F00", "backgroundColor": "#FFA807", "highlightColor": "#FFCA66"}, // orange + {"borderColor": "#4220FB", "backgroundColor": "#6E6EFD", "highlightColor": "#9B9BFD"}, // darkblue + {"borderColor": "#FD5A77", "backgroundColor": "#FFC0CB", "highlightColor": "#FFD1D9"}, // pink + {"borderColor": "#4AD63A", "backgroundColor": "#C2FABC", "highlightColor": "#E6FFE3"} // mint +]; + + +/** + * Clear all groups + */ +Graph.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 + */ +Graph.Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + + if (group == undefined) { + // create new group + var index = this.defaultIndex % Graph.Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.borderColor = Graph.Groups.DEFAULT[index].borderColor; + group.backgroundColor = Graph.Groups.DEFAULT[index].backgroundColor; + group.highlightColor = Graph.Groups.DEFAULT[index].highlightColor; + 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 + */ +Graph.Groups.prototype.add = function (groupname, style) { + this.groups[groupname] = style; + return style; +}; + +/** + * Check if given object is a Javascript Array + * @param {*} obj + * @return {Boolean} isArray true if the given object is an array + */ +// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni +Graph.isArray = function (obj) { + if (obj instanceof Array) { + return true; + } + return (Object.prototype.toString.call(obj) === '[object Array]'); +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Slider + * + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + */ +Graph.Slider = function(container) { + if (container === undefined) throw "Error: No container element defined"; + + this.container = container; + + this.frame = document.createElement("DIV"); + //this.frame.style.backgroundColor = "#E5E5E5"; + this.frame.style.width = "100%"; + this.frame.style.position = "relative"; + + this.title = document.createElement("DIV"); + this.title.style.margin = "2px"; + this.title.style.marginBottom = "5px"; + this.title.innerHTML = ""; + this.container.appendChild(this.title); + + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; + this.frame.appendChild(this.frame.prev); + + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; + this.frame.appendChild(this.frame.play); + + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; + this.frame.appendChild(this.frame.next); + + this.frame.bar = document.createElement("INPUT"); + this.frame.bar.type = "BUTTON"; + this.frame.bar.style.position = "absolute"; + this.frame.bar.style.border = "1px solid red"; + this.frame.bar.style.width = "100px"; + this.frame.bar.style.height = "6px"; + this.frame.bar.style.borderRadius = "2px"; + this.frame.bar.style.MozBorderRadius = "2px"; + this.frame.bar.style.border = "1px solid #7F7F7F"; + this.frame.bar.style.backgroundColor = "#E5E5E5"; + this.frame.appendChild(this.frame.bar); + + this.frame.slide = document.createElement("INPUT"); + this.frame.slide.type = "BUTTON"; + this.frame.slide.style.margin = "0px"; + this.frame.slide.value = " "; + this.frame.slide.style.position = "relative"; + this.frame.slide.style.left = "-100px"; + this.frame.appendChild(this.frame.slide); + + // create events + var me = this; + this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; + this.frame.prev.onclick = function (event) {me.prev(event);}; + this.frame.play.onclick = function (event) {me.togglePlay(event);}; + this.frame.next.onclick = function (event) {me.next(event);}; + + this.container.appendChild(this.frame); + + this.onChangeCallback = undefined; + + this.playTimeout = undefined; + this.framerate = 20; // frames per second + this.duration = 10; // seconds + this.doLoop = true; + + this.start = 0; + this.end = 0; + this.value = 0; + this.step = 0; + this.rangeIsDate = false; + + this.redraw(); +}; + +/** + * Retrieve the step size, depending on the range, framerate, and duration + */ +Graph.Slider.prototype._updateStep = function() { + var range = (this.end - this.start); + var frameCount = this.duration * this.framerate; + + this.step = range / frameCount; +}; + +/** + * Select the previous index + */ +Graph.Slider.prototype.prev = function() { + this._setValue(this.value - this.step); +}; + +/** + * Select the next index + */ +Graph.Slider.prototype.next = function() { + this._setValue(this.value + this.step); +}; + +/** + * Select the next index + */ +Graph.Slider.prototype.playNext = function() { + var start = new Date(); + + if (!this.leftButtonDown) { + if (this.value + this.step < this.end) { + this._setValue(this.value + this.step); + } + else { + if (this.doLoop) { + this._setValue(this.start); + } + else { + this._setValue(this.end); + this.stop(); + return; + } + } + } + + var end = new Date(); + var diff = (end - start); + + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(1000 / this.framerate - diff, 0); + + var me = this; + this.playTimeout = setTimeout(function() {me.playNext();}, interval); +}; + +/** + * Toggle start or stop playing + */ +Graph.Slider.prototype.togglePlay = function() { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } +}; + +/** + * Start playing + */ +Graph.Slider.prototype.play = function() { + this.frame.play.value = "Stop"; + + this.playNext(); +}; + +/** + * Stop playing + */ +Graph.Slider.prototype.stop = function() { + this.frame.play.value = "Play"; + + clearInterval(this.playTimeout); + this.playTimeout = undefined; +}; + +/** + * Set a callback function which will be triggered when the value of the + * slider bar has changed. + */ +Graph.Slider.prototype.setOnChangeCallback = function(callback) { + this.onChangeCallback = callback; +}; + +/** + * Set the interval for playing the list + * @param {number} framerate Framerate in frames per second + */ +Graph.Slider.prototype.setFramerate = function(framerate) { + this.framerate = framerate; + this._updateStep(); +}; + +/** + * Retrieve the current framerate + * @return {number} framerate in frames per second + */ +Graph.Slider.prototype.getFramerate = function() { + return this.framerate; +}; + +/** + * Set the duration for playing + * @param {number} duration Duration in seconds + */ +Graph.Slider.prototype.setDuration = function(duration) { + this.duration = duration; + this._updateStep(); +}; + +/** + * Set the time acceleration for playing the history. Only applicable when + * the values are of type Date. + * @param {number} acceleration Acceleration, for example 10 means play + * ten times as fast as real time. A value + * of 1 will play the history in real time. + */ +Graph.Slider.prototype.setAcceleration = function(acceleration) { + var durationRealtime = (this.end - this.start) / 1000; // in seconds + + this.duration = durationRealtime / acceleration; + this._updateStep(); +}; + + +/** + * Set looping on or off + * @param {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Graph.Slider.prototype.setLoop = function(doLoop) { + this.doLoop = doLoop; +}; + +/** + * Retrieve the current value of loop + * @return {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Graph.Slider.prototype.getLoop = function() { + return this.doLoop; +}; + + +/** + * Execute the onchange callback function + */ +Graph.Slider.prototype.onChange = function() { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } +}; + +/** + * redraw the slider on the correct place + */ +Graph.Slider.prototype.redraw = function() { + // resize the bar + var barTop = (this.frame.clientHeight/2 - + this.frame.bar.offsetHeight/2); + var barWidth = (this.frame.clientWidth - + this.frame.prev.clientWidth - + this.frame.play.clientWidth - + this.frame.next.clientWidth - 30); + this.frame.bar.style.top = barTop + "px"; + this.frame.bar.style.width = barWidth + "px"; + + // position the slider button + this.frame.slide.title = this.getValue(); + this.frame.slide.style.left = this._valueToLeft(this.value) + "px"; + + // set the title + this.title.innerHTML = this.getValue(); +}; + + +/** + * Set the range for the slider + * @param {Date | Number} start Start of the range + * @param {Date | Number} end End of the range + */ +Graph.Slider.prototype.setRange = function(start, end) { + if (start === undefined || start === null || start === NaN) { + this.start = 0; + this.rangeIsDate = false; + } + else if (start instanceof Date) { + this.start = start.getTime(); + this.rangeIsDate = true; + } + else { + this.start = start; + this.rangeIsDate = false; + } + + if (end === undefined || end === null || end === NaN) { + if (this.start instanceof Date) { + this.end = new Date(this.start); + } + else { + this.end = this.start; + } + } + else if (end instanceof Date) { + this.end = end.getTime(); + } + else { + this.end = end; + } + + this.value = this.start; + + this._updateStep(); + this.redraw(); +}; + + + +/** + * Set a value for the slider. The value must be between start and end + * When the range are Dates, the value will be translated to a date + * @param {Number} value + */ +Graph.Slider.prototype._setValue = function(value) { + this.value = this._limitValue(value); + this.redraw(); + + this.onChange(); +}; + +/** + * retrieve the current value in the correct type, Number or Date + * @return {Date | Number} value + */ +Graph.Slider.prototype.getValue = function() { + if (this.rangeIsDate) { + return new Date(this.value); + } + else { + return this.value; + } +}; + + +Graph.Slider.prototype.offset = 3; + +Graph.Slider.prototype._leftToValue = function (left) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + var x = left - this.offset; + + var range = this.end - this.start; + var value = this._limitValue(x / width * range + this.start); + + return value; +}; + +Graph.Slider.prototype._valueToLeft = function (value) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + + var x; + if (this.end > this.start) { + x = (value - this.start) / (this.end - this.start) * width; + } + else { + x = 0; + } + var left = x + this.offset; + + return left; +}; + +Graph.Slider.prototype._limitValue = function(value) { + if (value < this.start) { + value = this.start + } + if (value > this.end) { + value = this.end; + } + + return value; +}; + +Graph.Slider.prototype._onMouseDown = function(event) { + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!this.leftButtonDown) return; + + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + Graph.addEventListener(document, "mousemove", this.onmousemove); + Graph.addEventListener(document, "mouseup", this.onmouseup); + Graph.preventDefault(event); +}; + + +Graph.Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var value = this._leftToValue(x); + this._setValue(value); + + Graph.preventDefault(event); +}; + + +Graph.Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + + this.leftButtonDown = false; + + // remove event listeners + Graph.removeEventListener(document, "mousemove", this.onmousemove); + Graph.removeEventListener(document, "mouseup", this.onmouseup); + + Graph.preventDefault(event); +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * 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 + */ +Graph.Popup = function (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 + */ +Graph.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 + */ +Graph.Popup.prototype.setText = function(text) { + this.frame.innerHTML = text; +}; + +/** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ +Graph.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 + */ +Graph.Popup.prototype.hide = function () { + this.frame.style.visibility = "hidden"; +}; + + +/**--------------------------------------------------------------------------**/ + +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(); + }; + + + // TODO: add diamond shape +} + + +/*----------------------------------------------------------------------------*/ + +// utility methods +Graph.util = {}; + +/** + * 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. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} json An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges + */ +Graph.util.parseDOT = function (data) { + /** + * Test whether given character is a whitespace character + * @param {String} c + * @return {Boolean} isWhitespace + */ + function isWhitespace(c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + /** + * Test whether given character is a delimeter + * @param {String} c + * @return {Boolean} isDelimeter + */ + function isDelimeter(c) { + return '[]{}();,=->'.indexOf(c) != -1; + } + + var i = -1; // current index in the data + var c = ''; // current character in the data + + /** + * Read the next character from the data + */ + function next() { + i++; + c = data[i]; + } + + /** + * Preview the next character in the data + * @returns {String} nextChar + */ + function previewNext () { + return data[i + 1]; + } + + /** + * Preview the next character in the data + * @returns {String} nextChar + */ + function previewPrevious () { + return data[i + 1]; + } + + /** + * Get a text description of the the current index in the data + * @return {String} desc + */ + function pos() { + return '(char ' + i + ')'; + } + + /** + * Skip whitespace and comments + */ + function parseWhitespace() { + // skip whitespace + while (c && isWhitespace(c)) { + next(); + } + + // test for comment + var cNext = data[i + 1]; + var cPrev = data[i - 1]; + var c2 = c + cNext; + if (c2 == '/*') { + // block comment. skip until the block is closed + while (c && !(c == '*' && data[i + 1] == '/')) { + next(); + } + next(); + next(); + + parseWhitespace(); + } + else if (c2 == '//' || (c == '#' && cPrev == '\n')) { + // line comment. skip until the next return + while (c && c != '\n') { + next(); + } + next(); + parseWhitespace(); + } + } + + /** + * Parse a string + * The string may be enclosed by double quotes + * @return {String | undefined} value + */ + function parseString() { + parseWhitespace(); + + var name = ''; + if (c == '"') { + next(); + while (c && c != '"') { + name += c; + next(); + } + next(); // skip the closing quote + } + else { + while (c && !isWhitespace(c) && !isDelimeter(c)) { + name += c; + next(); + } + + // cast string to number or boolean + var number = Number(name); + if (!isNaN(number)) { + name = number; + } + else if (name == 'true') { + name = true; + } + else if (name == 'false') { + name = false; + } + else if (name == 'null') { + name = null; + } + } + + return name; + } + + /** + * Parse a value, can be a string, number, or boolean. + * The value may be enclosed by double quotes + * @return {String | Number | Boolean | undefined} value + */ + function parseValue() { + parseWhitespace(); + + if (c == '"') { + return parseString(); + } + else { + var value = parseString(); + if (value != undefined) { + // cast string to number or boolean + var number = Number(value); + if (!isNaN(number)) { + value = number; + } + else if (value == 'true') { + value = true; + } + else if (value == 'false') { + value = false; + } + else if (value == 'null') { + value = null; + } + } + return value; + } + } + + /** + * Parse a set with attributes, + * for example [label="1.000", style=solid] + * @return {Object | undefined} attr + */ + function parseAttributes() { + parseWhitespace(); + + if (c == '[') { + next(); + var attr = {}; + while (c && c != ']') { + parseWhitespace(); + + var name = parseString(); + if (!name) { + throw new SyntaxError('Attribute name expected ' + pos()); + } + + parseWhitespace(); + if (c != '=') { + throw new SyntaxError('Equal sign = expected ' + pos()); + } + next(); + + var value = parseValue(); + if (!value) { + throw new SyntaxError('Attribute value expected ' + pos()); + } + attr[name] = value; + + parseWhitespace(); + + if (c ==',') { + next(); + } + } + next(); + + return attr; + } + else { + return undefined; + } + } + + /** + * Parse a directed or undirected arrow '->' or '--' + * @return {String | undefined} arrow + */ + function parseArrow() { + parseWhitespace(); + + if (c == '-') { + next(); + if (c == '>' || c == '-') { + var arrow = '-' + c; + next(); + return arrow; + } + else { + throw new SyntaxError('Arrow "->" or "--" expected ' + pos()); + } + } + + return undefined; + } + + /** + * Parse a line separator ';' + * @return {String | undefined} separator + */ + function parseSeparator() { + parseWhitespace(); + + if (c == ';') { + next(); + return ';'; + } + + return undefined; + } + + /** + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + */ + function merge (a, b) { + if (a && b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + } + + var nodeMap = {}; + var edgeList = []; + + /** + * Register a node with attributes + * @param {String} id + * @param {Object} [attr] + */ + function addNode(id, attr) { + var node = { + id: String(id), + attr: attr || {} + }; + if (!nodeMap[id]) { + nodeMap[id] = node; + } + else { + merge(nodeMap[id].attr, node.attr); + } + } + + /** + * Register an edge + * @param {String} from + * @param {String} to + * @param {String} type A string "->" or "--" + * @param {Object} [attr] + */ + function addEdge(from, to, type, attr) { + edgeList.push({ + from: String(from), + to: String(to), + type: type, + attr: attr || {} + }); + } + + // find the opening curly bracket + next(); + while (c && c != '{') { + next(); + } + if (c != '{') { + throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos()) + } + next(); + + // parse all data until a closing curly bracket is encountered + while (c && c != '}') { + // parse node id and optional node attributes + var id = parseString(); + if (id == undefined) { + throw new SyntaxError('String with id expected ' + pos()); + } + var attr = parseAttributes(); + addNode(id, attr); + + // TODO: parse global attributes "graph", "node", "edge" + + // parse arrow + var type = parseArrow(); + while (type) { + // parse node id + var prevId = id; + id = parseString(); + if (id == undefined) { + throw new SyntaxError('String with id expected ' + pos()); + } + addNode(id); + + // parse edge attributes and register edge + attr = parseAttributes(); + addEdge(prevId, id, type, attr); + + // parse next arrow (optional) + type = parseArrow(); + } + + // parse separator (optional) + parseSeparator(); + + parseWhitespace(); + } + if (c != '}') { + throw new SyntaxError('Invalid data. Curly bracket } expected'); + } + + // crop data between the curly brackets + var start = data.indexOf('{'); + var end = data.indexOf('}', start); + var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined; + + if (!text) { + throw new Error('Invalid data. no curly brackets containing data found'); + } + + // return the results + var nodeList = []; + for (id in nodeMap) { + if (nodeMap.hasOwnProperty(id)) { + nodeList.push(nodeMap[id]); + } + } + return { + nodes: nodeList, + edges: edgeList + } +}; + +/** + * 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 + */ +Graph.util.DOTToGraph = function (data) { + // parse the DOT file + var dotData = Graph.util.parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: { + nodes: {}, + edges: {} + } + }; + + /** + * Merge the properties of object b into object a, and adjust properties + * not supported by Graph (for example replace "shape" with "style" + * @param {Object} a + * @param {Object} b + * @param {Array} [ignore] Optional array with property names to be ignored + */ + function merge (a, b, ignore) { + for (var prop in b) { + if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) { + a[prop] = b[prop]; + } + } + + // Convert aliases to configuration settings supported by Graph + if (a.label) { + a.text = a.label; + delete a.label; + } + if (a.shape) { + a.style = a.shape; + delete a.shape; + } + } + + dotData.nodes.forEach(function (node) { + if (node.id.toLowerCase() == 'graph') { + merge(graphData.options, node.attr); + } + else if (node.id.toLowerCase() == 'node') { + merge(graphData.options.nodes, node.attr); + } + else if (node.id.toLowerCase() == 'edge') { + merge(graphData.options.edges, node.attr); + } + else { + var graphNode = {}; + graphNode.id = node.id; + graphNode.text = node.id; + merge(graphNode, node.attr); + graphData.nodes.push(graphNode); + } + }); + + dotData.edges.forEach(function (edge) { + var graphEdge = {}; + graphEdge.from = edge.from; + graphEdge.to = edge.to; + graphEdge.text = edge.id; + graphEdge.style = (edge.type == '->') ? 'arrow-end' : 'line'; + merge(graphEdge, edge.attr); + graphData.edges.push(graphEdge); + }); + + return graphData; +}; + +/** + * 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 + }, + + Timeline: Timeline, + Graph: Graph +}; + +/** + * CommonJS module exports + */ +if (typeof exports !== 'undefined') { + exports = vis; +} +if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = vis; +} + +/** + * AMD module exports + */ +if (typeof(define) === 'function') { + define(function () { + return vis; + }); +} + +/** + * Window exports + */ +if (typeof window !== 'undefined') { + // attach the module to the window, load as a regular javascript file + window['vis'] = vis; +} + +// inject css +util.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.point {\n background: none;\n}\n\n.graph .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n"); + +})() },{"moment":2}],2:[function(require,module,exports){ (function(){// moment.js // version : 2.0.0 diff --git a/vis.min.js b/vis.min.js index 866ee356..0dbdae9b 100644 --- a/vis.min.js +++ b/vis.min.js @@ -22,6 +22,8 @@ * License for the specific language governing permissions and limitations under * the License. */ -(function(t){if("function"==typeof bootstrap)bootstrap("vis",t);else if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=t}else"undefined"!=typeof window?window.vis=t():global.vis=t()})(function(){var t;return function(t,e,i){function n(i,o){if(!e[i]){if(!t[i]){var s="function"==typeof require&&require;if(!o&&s)return s(i,!0);if(r)return r(i,!0);throw Error("Cannot find module '"+i+"'")}var a=e[i]={exports:{}};t[i][0].call(a.exports,function(e){var r=t[i][1][e];return n(r?r:e)},a,a.exports)}return e[i].exports}for(var r="function"==typeof require&&require,o=0;i.length>o;o++)n(i[o]);return n}({1:[function(e,i,n){function r(t){if(this.id=E.randomUUID(),this.options=t||{},this.data={},this.fieldId=this.options.fieldId||"id",this.fieldTypes={},this.options.fieldTypes)for(var e in this.options.fieldTypes)if(this.options.fieldTypes.hasOwnProperty(e)){var i=this.options.fieldTypes[e];this.fieldTypes[e]="Date"==i||"ISODate"==i||"ASPDate"==i?"Date":i}this.subscribers={},this.internalIds={}}function o(t,e){this.id=E.randomUUID(),this.data=null,this.ids={},this.options=e||{},this.fieldId="id",this.subscribers={};var i=this;this.listener=function(){i._onEvent.apply(i,arguments)},this.setData(t)}function s(t,e){this.parent=t,this.options=e||{},this.defaultOptions={order:function(t,e){if(t instanceof y){if(e instanceof y){var i=t.data.end-t.data.start,n=e.data.end-e.data.start;return i-n||t.data.start-e.data.start}return-1}return e instanceof y?1:t.data.start-e.data.start},margin:{item:10}},this.ordered=[]}function a(t){this.id=E.randomUUID(),this.start=0,this.end=0,this.options={min:null,max:null,zoomMin:null,zoomMax:null},this.listeners=[],this.setOptions(t)}function h(){this.subscriptions=[]}function p(){this.id=E.randomUUID(),this.components={},this.repaintTimer=void 0,this.reflowTimer=void 0}function u(){this.id=null,this.parent=null,this.depends=null,this.controller=null,this.options=null,this.frame=null,this.top=0,this.left=0,this.width=0,this.height=0}function c(t,e,i){this.id=E.randomUUID(),this.parent=t,this.depends=e,this.options=i||{}}function d(t,e){this.id=E.randomUUID(),this.container=t,this.options=e||{},this.defaultOptions={autoResize:!0},this.listeners={}}function l(t,e,i){this.id=E.randomUUID(),this.parent=t,this.depends=e,this.dom={majorLines:[],majorTexts:[],minorLines:[],minorTexts:[],redundant:{majorLines:[],majorTexts:[],minorLines:[],minorTexts:[]}},this.props={range:{start:0,end:0,minimumStep:0},lineTop:0},this.options=i||{},this.defaultOptions={orientation:"bottom",showMinorLabels:!0,showMajorLabels:!0},this.conversion=null,this.range=null}function f(t,e,i){this.id=E.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={style:"box",align:"center",orientation:"bottom",margin:{axis:20,item:10},padding:5},this.dom={};var n=this;this.itemsData=null,this.range=null,this.listeners={add:function(t,e,i){i!=n.id&&n._onAdd(e.items)},update:function(t,e,i){i!=n.id&&n._onUpdate(e.items)},remove:function(t,e,i){i!=n.id&&n._onRemove(e.items)}},this.items={},this.queue={},this.stack=new s(this,Object.create(this.options)),this.conversion=null}function m(t,e,i,n){this.parent=t,this.data=e,this.dom=null,this.options=i||{},this.defaultOptions=n||{},this.selected=!1,this.visible=!1,this.top=0,this.left=0,this.width=0,this.height=0}function g(t,e,i,n){this.props={dot:{left:0,top:0,width:0,height:0},line:{top:0,left:0,width:0,height:0}},m.call(this,t,e,i,n)}function v(t,e,i,n){this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0}},m.call(this,t,e,i,n)}function y(t,e,i,n){this.props={content:{left:0,width:0}},m.call(this,t,e,i,n)}function w(t,e,i){this.id=E.randomUUID(),this.parent=t,this.groupId=e,this.itemsData=null,this.itemset=null,this.options=i||{},this.options.top=0,this.top=0,this.left=0,this.width=0,this.height=0}function b(t,e,i){this.id=E.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.range=null,this.itemsData=null,this.groupsData=null,this.groups={},this.queue={};var n=this;this.listeners={add:function(t,e){n._onAdd(e.items)},update:function(t,e){n._onUpdate(e.items)},remove:function(t,e){n._onRemove(e.items)}}}function S(t,e,i){var n=this;if(this.options=E.extend({orientation:"bottom",min:null,max:null,zoomMin:10,zoomMax:31536e10,moveable:!0,zoomable:!0,showMinorLabels:!0,showMajorLabels:!0,autoResize:!1},i),this.controller=new p,!t)throw Error("No container element provided");var r=Object.create(this.options);r.height=function(){return n.options.height?n.options.height:n.timeaxis.height+n.content.height},this.root=new d(t,r),this.controller.add(this.root);var o=T().hours(0).minutes(0).seconds(0).milliseconds(0);this.range=new a({start:o.clone().add("days",-3).valueOf(),end:o.clone().add("days",4).valueOf()}),this.range.subscribe(this.root,"move","horizontal"),this.range.subscribe(this.root,"zoom","horizontal"),this.range.on("rangechange",function(){var t=!0;n.controller.requestReflow(t)}),this.range.on("rangechanged",function(){var t=!0;n.controller.requestReflow(t)});var s=Object.create(r);s.range=this.range,this.timeaxis=new l(this.root,[],s),this.timeaxis.setRange(this.range),this.controller.add(this.timeaxis),this.setGroups(null),this.itemsData=null,this.groupsData=null,e&&this.setItems(e)}var T=e("moment"),E={};E.isNumber=function(t){return t instanceof Number||"number"==typeof t},E.isString=function(t){return t instanceof String||"string"==typeof t},E.isDate=function(t){if(t instanceof Date)return!0;if(E.isString(t)){var e=M.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},E.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},E.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},E.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var r in n)n.hasOwnProperty(r)&&void 0!==n[r]&&(t[r]=n[r])}return t},E.cast=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 Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t);case"string":case"String":return t+"";case"Date":if(E.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(T.isMoment(t))return new Date(t.valueOf());if(E.isString(t))return i=M.exec(t),i?new Date(Number(i[1])):T(t).toDate();throw Error("Cannot cast object of type "+E.getType(t)+" to type Date");case"Moment":if(E.isNumber(t))return T(t);if(t instanceof Date)return T(t.valueOf());if(T.isMoment(t))return T.clone();if(E.isString(t))return i=M.exec(t),i?T(Number(i[1])):T(t);throw Error("Cannot cast object of type "+E.getType(t)+" to type Date");case"ISODate":if(t instanceof Date)return t.toISOString();if(T.isMoment(t))return t.toDate().toISOString();if(E.isNumber(t)||E.isString(t))return T(t).toDate().toISOString();throw Error("Cannot cast object of type "+E.getType(t)+" to type ISODate");case"ASPDate":if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(E.isNumber(t)||E.isString(t))return"/Date("+T(t).valueOf()+")/";throw Error("Cannot cast object of type "+E.getType(t)+" to type ASPDate");default:throw Error("Cannot cast object of type "+E.getType(t)+' to type "'+e+'"')}};var M=/^\/?Date\((\-?\d+)/i;if(E.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},E.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,r=t.offsetParent;null!=r&&r!=i&&r!=e;)n+=r.offsetLeft,n-=r.scrollLeft,r=r.offsetParent;return n},E.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,r=t.offsetParent;null!=r&&r!=i&&r!=e;)n+=r.offsetTop,n-=r.scrollTop,r=r.offsetParent;return n},E.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)},E.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)},E.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},E.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},E.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)},E.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},E.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)},E.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)},E.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},E.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},E.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},E.option={},E.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},E.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},E.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?t+"":e||null},E.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),E.isString(t)?t:E.isNumber(t)?t+"px":e||null},E.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},E.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}},!Array.prototype.indexOf){Array.prototype.indexOf=function(t){for(var e=0;this.length>e;e++)if(this[e]==t)return e;return-1};try{console.log("Warning: Ancient browser detected. Please update your browser")}catch(D){}}Array.prototype.forEach||(Array.prototype.forEach=function(t,e){for(var i=0,n=this.length;n>i;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,r;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),s=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=Array(s),r=0;s>r;){var a,h;r in o&&(a=o[r],h=t.call(i,a,r,o),n[r]=h),r++}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=[],r=arguments[1],o=0;i>o;o++)if(o in e){var s=e[o];t.call(r,s,o,e)&&n.push(s)}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(r){if("object"!=typeof r&&"function"!=typeof r||null===r)throw new TypeError("Object.keys called on non-object");var o=[];for(var s in r)t.call(r,s)&&o.push(s);if(e)for(var a=0;n>a;a++)t.call(r,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(){},r=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,r.prototype=new n,r}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e});var _={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var r=e[i];if(r&&r.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),r=this.listeners[n];r||(r={object:t,events:{}},this.listeners.push(r));var o=r.events[e];o||(o=[],r.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),r=this.listeners[n];if(r){var o=r.events[e];o&&(n=o.indexOf(i),-1!=n&&o.splice(n,1),0==o.length&&delete r.events[e]);var s=0,a=r.events;for(var h in a)a.hasOwnProperty(h)&&s++;0==s&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),r=this.listeners[n];if(r){var o=r.events[e];if(o)for(var s=0,a=o.length;a>s;s++)o[s](i)}}};TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){t instanceof Date&&e instanceof Date&&(this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i))},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step);break;default:}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(6>this.current.getMonth())switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+60*1e3*this.step);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+60*60*1e3*this.step);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step);break;default:}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);break;default:}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,r=36e5,o=6e4,s=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*r>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),r>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*s>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*s>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*s>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),s>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 T(t).format("SSS");case TimeStep.SCALE.SECOND:return T(t).format("s");case TimeStep.SCALE.MINUTE:return T(t).format("HH:mm");case TimeStep.SCALE.HOUR:return T(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return T(t).format("ddd D");case TimeStep.SCALE.DAY:return T(t).format("D");case TimeStep.SCALE.MONTH:return T(t).format("MMM");case TimeStep.SCALE.YEAR:return T(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return T(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return T(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return T(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return T(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return T(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},r.prototype.subscribe=function(t,e,i){var n=this.subscribers[t];n||(n=[],this.subscribers[t]=n),n.push({id:i?i+"":null,callback:e})},r.prototype.unsubscribe=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},r.prototype._trigger=function(t,e,i){if("*"==t)throw Error("Cannot trigger event *");var n=[];t in this.subscribers&&(n=n.concat(this.subscribers[t])),"*"in this.subscribers&&(n=n.concat(this.subscribers["*"]));for(var r=0;n.length>r;r++){var o=n[r];o.callback&&o.callback(t,e,i||null)}},r.prototype.add=function(t,e){var i,n=[],r=this;if(t instanceof Array)for(var o=0,s=t.length;s>o;o++)i=r._addItem(t[o]),n.push(i);else if(E.isDataTable(t))for(var a=this._getColumnNames(t),h=0,p=t.getNumberOfRows();p>h;h++){for(var u={},c=0,d=a.length;d>c;c++){var l=a[c];u[l]=t.getValue(h,c)}i=r._addItem(u),n.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=r._addItem(t),n.push(i)}n.length&&this._trigger("add",{items:n},e)},r.prototype.update=function(t,e){var i=[],n=[],r=this,o=r.fieldId,s=function(t){var e=t[o];r.data[e]?(e=r._updateItem(t),n.push(e)):(e=r._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)s(t[a]);else if(E.isDataTable(t))for(var p=this._getColumnNames(t),u=0,c=t.getNumberOfRows();c>u;u++){for(var d={},l=0,f=p.length;f>l;l++){var m=p[l];d[m]=t.getValue(u,l)}s(d)}else{if(!(t instanceof Object))throw Error("Unknown dataType");s(t)}i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e)},r.prototype.get=function(){var t,e,i,n,r=this,o=E.getType(arguments[0]);"String"==o||"Number"==o?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==o?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var s;if(i&&i.type){if(s="DataTable"==i.type?"DataTable":"Array",n&&s!=E.getType(n))throw Error('Type of parameter "data" ('+E.getType(n)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==s&&!E.isDataTable(n))throw Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else s=n?"DataTable"==E.getType(n)?"DataTable":"Array":"Array";var a,h,p,u,c=i&&i.fieldTypes||this.options.fieldTypes,d=i&&i.filter,l=[];if(void 0!=t)a=r._getItem(t,c),d&&!d(a)&&(a=null);else if(void 0!=e)for(p=0,u=e.length;u>p;p++)a=r._getItem(e[p],c),(!d||d(a))&&l.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=r._getItem(h,c),(!d||d(a))&&l.push(a));if(i&&i.order&&void 0==t&&this._sort(l,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(p=0,u=l.length;u>p;p++)l[p]=this._filterFields(l[p],f)}if("DataTable"==s){var m=this._getColumnNames(n);if(void 0!=t)r._appendRow(n,m,a);else for(p=0,u=l.length;u>p;p++)r._appendRow(n,m,l[p]);return n}if(void 0!=t)return a;if(n){for(p=0,u=l.length;u>p;p++)n.push(l[p]);return n}return l},r.prototype.getIds=function(t){var e,i,n,r,o,s=this.data,a=t&&t.filter,h=t&&t.order,p=t&&t.fieldTypes||this.options.fieldTypes,u=[];if(a)if(h){o=[];for(n in s)s.hasOwnProperty(n)&&(r=this._getItem(n,p),a(r)&&o.push(r));for(this._sort(o,h),e=0,i=o.length;i>e;e++)u[e]=o[e][this.fieldId]}else for(n in s)s.hasOwnProperty(n)&&(r=this._getItem(n,p),a(r)&&u.push(r[this.fieldId]));else if(h){o=[];for(n in s)s.hasOwnProperty(n)&&o.push(s[n]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)u[e]=o[e][this.fieldId]}else for(n in s)s.hasOwnProperty(n)&&(r=s[n],u.push(r[this.fieldId]));return u},r.prototype.forEach=function(t,e){var i,n,r=e&&e.filter,o=e&&e.fieldTypes||this.options.fieldTypes,s=this.data;if(e&&e.order)for(var a=this.get(e),h=0,p=a.length;p>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in s)s.hasOwnProperty(n)&&(i=this._getItem(n,o),(!r||r(i))&&t(i,n))},r.prototype.map=function(t,e){var i,n=e&&e.filter,r=e&&e.fieldTypes||this.options.fieldTypes,o=[],s=this.data;for(var a in s)s.hasOwnProperty(a)&&(i=this._getItem(a,r),(!n||n(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},r.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},r.prototype._sort=function(t,e){if(E.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],r=e[i];return n>r?1:r>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},r.prototype.remove=function(t,e){var i,n,r=[];if(E.isNumber(t)||E.isString(t))delete this.data[t],delete this.internalIds[t],r.push(t);else if(t instanceof Array){for(i=0,n=t.length;n>i;i++)this.remove(t[i]);r=items.concat(t)}else if(t instanceof Object)for(i in this.data)this.data.hasOwnProperty(i)&&this.data[i]==t&&(delete this.data[i],delete this.internalIds[i],r.push(i));r.length&&this._trigger("remove",{items:r},e)},r.prototype.clear=function(t){var e=Object.keys(this.data);this.data={},this.internalIds={},this._trigger("remove",{items:e},t)},r.prototype.max=function(t){var e=this.data,i=null,n=null;for(var r in e)if(e.hasOwnProperty(r)){var o=e[r],s=o[t];null!=s&&(!i||s>n)&&(i=o,n=s)}return i},r.prototype.min=function(t){var e=this.data,i=null,n=null;for(var r in e)if(e.hasOwnProperty(r)){var o=e[r],s=o[t];null!=s&&(!i||n>s)&&(i=o,n=s)}return i},r.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.fieldTypes[t],r=0;for(var o in e)if(e.hasOwnProperty(o)){for(var s=e[o],a=E.cast(s[t],n),h=!1,p=0;r>p;p++)if(i[p]==a){h=!0;break}h||(i[r]=a,r++)}return i},r.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw Error("Cannot add item: item with id "+e+" already exists")}else e=E.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var r=this.fieldTypes[n];i[n]=E.cast(t[n],r)}return this.data[e]=i,e},r.prototype._getItem=function(t,e){var i,n,r=this.data[t];if(!r)return null;var o={},s=this.fieldId,a=this.internalIds;if(e)for(i in r)r.hasOwnProperty(i)&&(n=r[i],i==s&&n in a||(o[i]=E.cast(n,e[i])));else for(i in r)r.hasOwnProperty(i)&&(n=r[i],i==s&&n in a||(o[i]=n));return o},r.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var r=this.fieldTypes[n];i[n]=E.cast(t[n],r)}return e},r.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},r.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),r=0,o=e.length;o>r;r++){var s=e[r];t.setValue(n,r,i[s])}},o.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var r in this.ids)this.ids.hasOwnProperty(r)&&e.push(r);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++)r=e[i],this.ids[r]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},o.prototype.get=function(){var t,e,i,n=this,r=E.getType(arguments[0]);"String"==r||"Number"==r||"Array"==r?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=E.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return n.options.filter(t)&&e.filter(t)});var s=[];return void 0!=t&&s.push(t),s.push(o),s.push(i),this.data&&this.data.get.apply(this.data,s)},o.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},o.prototype._onEvent=function(t,e,i){var n,r,o,s,a=e&&e.items,h=this.data,p=[],u=[],c=[];if(a&&h){switch(t){case"add":for(n=0,r=a.length;r>n;n++)o=a[n],s=this.get(o),s&&(this.ids[o]=!0,p.push(o));break;case"update":for(n=0,r=a.length;r>n;n++)o=a[n],s=this.get(o),s?this.ids[o]?u.push(o):(this.ids[o]=!0,p.push(o)):this.ids[o]&&(delete this.ids[o],c.push(o));break;case"remove":for(n=0,r=a.length;r>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],c.push(o))}p.length&&this._trigger("add",{items:p},i),u.length&&this._trigger("update",{items:u},i),c.length&&this._trigger("remove",{items:c},i)}},o.prototype.subscribe=r.prototype.subscribe,o.prototype.unsubscribe=r.prototype.unsubscribe,o.prototype._trigger=r.prototype._trigger,s.prototype.setOptions=function(t){E.extend(this.options,t)},s.prototype.update=function(){this._order(),this._stack()},s.prototype._order=function(){var t=this.parent.items;if(!t)throw Error("Cannot stack items: parent does not contain items"); -var e=[],i=0;E.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw Error("Option order must be a function");e.sort(n),this.ordered=e},s.prototype._stack=function(){var t,e,i,n=this.ordered,r=this.options,o=r.orientation||this.defaultOptions.orientation,s="top"==o;for(i=r.margin&&void 0!==r.margin.item?r.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=s?h.top+h.height+i:h.top-a.height-i);while(h)}},s.prototype.checkOverlap=function(t,e,i,n,r){for(var o=this.collision,s=t[e],a=n;a>=i;a--){var h=t[a];if(o(s,h,r)&&a!=e)return h}return null},s.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},a.prototype.setOptions=function(t){E.extend(this.options,t),(null!=t.start||null!=t.end)&&this.setRange(t.start,t.end)},a.prototype.subscribe=function(t,e,i){var n,r=this;if("horizontal"!=i&&"vertical"!=i)throw new TypeError('Unknown direction "'+i+'". '+'Choose "horizontal" or "vertical".');if("move"==e)n={component:t,event:e,direction:i,callback:function(t){r._onMouseDown(t,n)},params:{}},t.on("mousedown",n.callback),r.listeners.push(n);else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');n={component:t,event:e,direction:i,callback:function(t){r._onMouseWheel(t,n)},params:{}},t.on("mousewheel",n.callback),r.listeners.push(n)}},a.prototype.on=function(t,e){_.addListener(this,t,e)},a.prototype._trigger=function(t){_.trigger(this,t,{start:this.start,end:this.end})},a.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},a.prototype._applyRange=function(t,e){var i,n=null!=t?E.cast(t,"Number"):this.start,r=null!=e?E.cast(e,"Number"):this.end;if(isNaN(n))throw Error('Invalid start "'+t+'"');if(isNaN(r))throw Error('Invalid end "'+e+'"');if(n>r&&(r=n),null!=this.options.min){var o=this.options.min.valueOf();o>n&&(i=o-n,n+=i,r+=i)}if(null!=this.options.max){var s=this.options.max.valueOf();r>s&&(i=r-s,n-=i,r-=i)}if(null!=this.options.zoomMin){var a=this.options.zoomMin.valueOf();0>a&&(a=0),a>r-n&&(this.end-this.start>a?(i=a-(r-n),n-=i/2,r+=i/2):(n=this.start,r=this.end))}if(null!=this.options.zoomMax){var h=this.options.zoomMax.valueOf();0>h&&(h=0),r-n>h&&(h>this.end-this.start?(i=r-n-h,n+=i/2,r-=i/2):(n=this.start,r=this.end))}var p=this.start!=n||this.end!=r;return this.start=n,this.end=r,p},a.prototype.getRange=function(){return{start:this.start,end:this.end}},a.prototype.conversion=function(t){return this.start,this.end,a.conversion(this.start,this.end,t)},a.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,factor:i/(e-t)}:{offset:0,factor:1}},a.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=E.getPageX(t),i.mouseY=E.getPageY(t),i.previousLeft=0,i.previousOffset=0,i.moved=!1,i.start=this.start,i.end=this.end;var r=e.component.frame;r&&(r.style.cursor="move");var o=this;i.onMouseMove||(i.onMouseMove=function(t){o._onMouseMove(t,e)},E.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){o._onMouseUp(t,e)},E.addEventListener(document,"mouseup",i.onMouseUp)),E.preventDefault(t)}},a.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=E.getPageX(t),r=E.getPageY(t);void 0==i.mouseX&&(i.mouseX=n),void 0==i.mouseY&&(i.mouseY=r);var o=n-i.mouseX,s=r-i.mouseY,a="horizontal"==e.direction?o:s;Math.abs(a)>=1&&(i.moved=!0);var h=i.end-i.start,p="horizontal"==e.direction?e.component.width:e.component.height,u=-a/p*h;this._applyRange(i.start+u,i.end+u),this._trigger("rangechange"),E.preventDefault(t)},a.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(E.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(E.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},a.prototype._onMouseWheel=function(t,e){t=t||window.event;var i=0;if(t.wheelDelta?i=t.wheelDelta/120:t.detail&&(i=-t.detail/3),i){var n=this,r=function(){var r=i/5,o=null,s=e.component.frame;if(s){var a,h;if("horizontal"==e.direction){a=e.component.width,h=n.conversion(a);var p=E.getAbsoluteLeft(s),u=E.getPageX(t);o=(u-p)/h.factor+h.offset}else{a=e.component.height,h=n.conversion(a);var c=E.getAbsoluteTop(s),d=E.getPageY(t);o=(c+a-d-c)/h.factor+h.offset}}n.zoom(r,o)};r()}E.preventDefault(t)},a.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2),t>=1&&(t=.9),-1>=t&&(t=-.9),0>t&&(t/=1+t);var i=this.start-e,n=this.end-e,r=this.start-i*t,o=this.end-n*t;this.setRange(r,o)},a.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.on=function(t,e,i){var n=t instanceof RegExp?t:RegExp(t.replace("*","\\w+")),r={id:E.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(r),r.id},h.prototype.off=function(t){for(var e=0;this.subscriptions.length>e;){var i=this.subscriptions[e],n=!0;if(t instanceof Object)for(var r in t)t.hasOwnProperty(r)&&t[r]!==i[r]&&(n=!1);else n=i.id==t;n?this.subscriptions.splice(e,1):e++}},h.prototype.emit=function(t,e,i){for(var n=0;this.subscriptions.length>n;n++){var r=this.subscriptions[n];r.regexp.test(t)&&r.callback&&r.callback(t,e,i)}},p.prototype.add=function(t){if(void 0==t.id)throw Error("Component has no field id");if(!(t instanceof u||t instanceof p))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},p.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]},p.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)}},p.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)}},p.prototype.repaint=function(){function t(n,r){r in i||(n.depends&&n.depends.forEach(function(e){t(e,e.id)}),n.parent&&t(n.parent,n.parent.id),e=n.repaint()||e,i[r]=!0)}var e=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var i={};E.forEach(this.components,t),e&&this.reflow()},p.prototype.reflow=function(){function t(n,r){r in i||(n.depends&&n.depends.forEach(function(e){t(e,e.id)}),n.parent&&t(n.parent,n.parent.id),e=n.reflow()||e,i[r]=!0)}var e=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};E.forEach(this.components,t),e&&this.repaint()},u.prototype.setOptions=function(t){t&&(E.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 Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},u.prototype.requestReflow=function(){if(!this.controller)throw Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},c.prototype=new u,c.prototype.setOptions=u.prototype.setOptions,c.prototype.getContainer=function(){return this.frame},c.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.frame;if(!r){r=document.createElement("div"),r.className="panel";var o=n.className;o&&("function"==typeof o?E.addClassName(r,o()+""):E.addClassName(r,o+"")),this.frame=r,t+=1}if(!r.parentNode){if(!this.parent)throw Error("Cannot repaint panel: no parent attached");var s=this.parent.getContainer();if(!s)throw Error("Cannot repaint panel: parent has no container element");s.appendChild(r),t+=1}return t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,"100%")),t>0},c.prototype.reflow=function(){var t=0,e=E.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},d.prototype=new c,d.prototype.setOptions=u.prototype.setOptions,d.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.frame;if(!r){r=document.createElement("div"),r.className="graph panel";var o=n.className;o&&E.addClassName(r,E.option.asString(o)),this.frame=r,t+=1}if(!r.parentNode){if(!this.container)throw Error("Cannot repaint root panel: no container attached");this.container.appendChild(r),t+=1}return t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},d.prototype.reflow=function(){var t=0,e=E.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},d.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},d.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};E.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},d.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},d.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},d.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;E.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var r=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=r,E.addEventListener(n,i,r)}}})}},l.prototype=new u,l.prototype.setOptions=u.prototype.setOptions,l.prototype.setRange=function(t){if(!(t instanceof a||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},l.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},l.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},l.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.getOption("orientation"),o=this.props,s=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis "+r,!a.parentNode){if(!this.parent)throw Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var p=a.parentNode;if(p){var u=a.nextSibling;p.removeChild(a);var c="bottom"==r&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,c)),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(),s.first();for(var d=void 0,l=0;s.hasNext()&&1e3>l;){l++;var f=s.getCurrent(),m=this.toScreen(f),g=s.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,s.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==d&&(d=m),this._repaintMajorText(m,s.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),s.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=s.getLabelMajor(v),w=y.length*(o.majorCharWidth||10)+10;(void 0==d||d>w)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),u?p.insertBefore(a,u):p.appendChild(a)}return t>0},l.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=[]},l.prototype._repaintEnd=function(){E.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},l.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"},l.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"},l.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"},l.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"},l.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},l.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}},l.prototype.reflow=function(){var t=0,e=E.updateProperty,i=this.frame,n=this.range;if(!n)throw Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var r=this.props,o=this.getOption("showMinorLabels"),s=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(r.minorCharHeight=a.clientHeight,r.minorCharWidth=a.clientWidth),h&&(r.majorCharHeight=h.clientHeight,r.majorCharWidth=h.clientWidth);var p=i.parentNode?i.parentNode.offsetHeight:0;switch(p!=r.parentHeight&&(r.parentHeight=p,t+=1),this.getOption("orientation")){case"bottom":r.minorLabelHeight=o?r.minorCharHeight:0,r.majorLabelHeight=s?r.majorCharHeight:0,r.minorLabelTop=0,r.majorLabelTop=r.minorLabelTop+r.minorLabelHeight,r.minorLineTop=-this.top,r.minorLineHeight=Math.max(this.top+r.majorLabelHeight,0),r.minorLineWidth=1,r.majorLineTop=-this.top,r.majorLineHeight=Math.max(this.top+r.minorLabelHeight+r.majorLabelHeight,0),r.majorLineWidth=1,r.lineTop=0;break;case"top":r.minorLabelHeight=o?r.minorCharHeight:0,r.majorLabelHeight=s?r.majorCharHeight:0,r.majorLabelTop=0,r.minorLabelTop=r.majorLabelTop+r.majorLabelHeight,r.minorLineTop=r.minorLabelTop,r.minorLineHeight=Math.max(p-r.majorLabelHeight-this.top),r.minorLineWidth=1,r.majorLineTop=0,r.majorLineHeight=Math.max(p-this.top),r.majorLineWidth=1,r.lineTop=r.majorLabelHeight+r.minorLabelHeight;break;default:throw Error('Unkown orientation "'+this.getOption("orientation")+'"')}var u=r.minorLabelHeight+r.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",u),this._updateConversion();var c=E.cast(n.start,"Date"),d=E.cast(n.end,"Date"),l=this.toTime(5*(r.minorCharWidth||10))-this.toTime(0);this.step=new TimeStep(c,d,l),t+=e(r.range,"start",c.valueOf()),t+=e(r.range,"end",d.valueOf()),t+=e(r.range,"minimumStep",l.valueOf())}return t>0},l.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):a.conversion(t.start,t.end,this.width)},f.prototype=new c,f.types={box:g,range:y,point:v},f.prototype.setOptions=u.prototype.setOptions,f.prototype.setRange=function(t){if(!(t instanceof a||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},f.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.getOption("orientation"),o=this.defaultOptions,s=this.frame;if(!s){s=document.createElement("div"),s.className="itemset";var a=n.className;a&&E.addClassName(s,E.option.asString(a));var h=document.createElement("div");h.className="background",s.appendChild(h),this.dom.background=h;var p=document.createElement("div");p.className="foreground",s.appendChild(p),this.dom.foreground=p;var u=document.createElement("div");u.className="itemset-axis",this.dom.axis=u,this.frame=s,t+=1}if(!this.parent)throw Error("Cannot repaint itemset: no parent attached");var c=this.parent.getContainer();if(!c)throw Error("Cannot repaint itemset: parent has no container element");s.parentNode||(c.appendChild(s),t+=1),this.dom.axis.parentNode||(c.appendChild(this.dom.axis),t+=1),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.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"==r?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var d=this,l=this.queue,m=this.itemsData,g=this.items,v={fields:[m&&m.fieldId||"id","start","end","content","type"]};return Object.keys(l).forEach(function(e){var i=l[e],r=g[e];switch(i){case"add":case"update":var s=m&&m.get(e,v);if(s){var a=s.type||s.start&&s.end&&"range"||"box",h=f.types[a];if(r&&(h&&r instanceof h?(r.data=s,t++):(t+=r.hide(),r=null)),!r){if(!h)throw new TypeError('Unknown item type "'+a+'"');r=new h(d,s,n,o),t++}r.repaint(),g[e]=r}delete l[e];break;case"remove":r&&(t+=r.hide()),delete g[e],delete l[e];break;default:console.log('Error: unknown action "'+i+'"')}}),E.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},f.prototype.getForeground=function(){return this.dom.foreground},f.prototype.getBackground=function(){return this.dom.background},f.prototype.getAxis=function(){return this.dom.axis},f.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,r=E.updateProperty,o=E.option.asNumber,s=E.option.asSize,a=this.frame;if(a){this._updateConversion(),E.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,p=o(e.maxHeight),u=null!=s(e.height);if(u)h=a.offsetHeight;else{var c=this.stack.ordered;if(c.length){var d=c[0].top,l=c[0].top+c[0].height;E.forEach(c,function(t){d=Math.min(d,t.top),l=Math.max(l,t.top+t.height)}),h=l-d+i+n}else h=i+n}null!=p&&(h=Math.min(h,p)),t+=r(this,"height",h),t+=r(this,"top",a.offsetTop),t+=r(this,"left",a.offsetLeft),t+=r(this,"width",a.offsetWidth)}else t+=1;return t>0},f.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},f.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(n&&(E.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),t){if(!(t instanceof r||t instanceof o))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(this.itemsData){var s=this.id;E.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},f.prototype.getItems=function(){return this.itemsData},f.prototype._onUpdate=function(t){this._toQueue("update",t)},f.prototype._onAdd=function(t){this._toQueue("add",t)},f.prototype._onRemove=function(t){this._toQueue("remove",t)},f.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},f.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):a.conversion(t.start,t.end,this.width)},f.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},f.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},m.prototype.select=function(){this.selected=!0},m.prototype.unselect=function(){this.selected=!1},m.prototype.show=function(){return!1},m.prototype.hide=function(){return!1},m.prototype.repaint=function(){return!1},m.prototype.reflow=function(){return!1},g.prototype=new m(null,null),g.prototype.select=function(){this.selected=!0},g.prototype.unselect=function(){this.selected=!1},g.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");var n=this.parent.getBackground();if(!n)throw Error("Cannot repaint time axis: parent has no background container element");var r=this.parent.getAxis();if(!n)throw Error("Cannot repaint time axis: parent has no axis container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(n.appendChild(e.line),t=!0),e.dot.parentNode||(r.appendChild(e.dot),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw 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},g.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},g.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},g.prototype.reflow=function(){var t,e,i,n,r,o,s,a,h,p,u,c,d=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(u=this.data,c=this.parent&&this.parent.range,this.visible=u&&c?u.start>c.start&&u.start0},g.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")},g.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,r=t.line,o=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",r.style.left=e.line.left+"px","top"==i?(r.style.top="0px",r.style.height=this.top+"px"):(r.style.top=this.top+this.height+"px",r.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"}},v.prototype=new m(null,null),v.prototype.select=function(){this.selected=!0},v.prototype.unselect=function(){this.selected=!1},v.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw 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 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},v.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},v.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},v.prototype.reflow=function(){var t,e,i,n,r,o,s,a,h,p,u=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(h=this.data,p=this.parent&&this.parent.range,this.visible=h&&p?h.start>p.start&&h.start0},v.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))},v.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")},y.prototype=new m(null,null),y.prototype.select=function(){this.selected=!0},y.prototype.unselect=function(){this.selected=!1},y.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw 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 Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?""+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},y.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},y.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},y.prototype.reflow=function(){var t,e,i,n,r,o,s,a,h,p,u,c,d,l,f,m,g=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw Error('Property "end" missing in item '+this.data.id);return h=this.data,p=this.parent&&this.parent.range,this.visible=h&&p?h.startp.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,s=o.toScreen(this.data.start),a=o.toScreen(this.data.end),u=E.updateProperty,c=t.box,d=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,r=i.padding||this.defaultOptions.padding,g+=u(e.content,"width",t.content.offsetWidth),g+=u(this,"height",c.offsetHeight),-d>s&&(s=-d),a>2*d&&(a=2*d),l=0>s?Math.min(-s,a-s-e.content.width-2*r):0,g+=u(e.content,"left",l),"top"==f?(m=n,g+=u(this,"top",m)):(m=o.height-this.height-n,g+=u(this,"top",m)),g+=u(this,"left",s),g+=u(this,"width",Math.max(a-s,1))):g+=1),g>0},y.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))},y.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")},w.prototype=new u,w.prototype.setOptions=u.prototype.setOptions,w.prototype.getContainer=function(){return this.parent.getContainer()},w.prototype.setItems=function(t){if(this.itemset&&(this.itemset.hide(),this.itemset.setItems(),this.parent.controller.remove(this.itemset),this.itemset=null),t){var e=this.groupId,i=Object.create(this.options);this.itemset=new f(this,null,i),this.itemset.setRange(this.parent.range),this.view=new o(t,{filter:function(t){return t.group==e}}),this.itemset.setItems(this.view),this.parent.controller.add(this.itemset)}},w.prototype.repaint=function(){return!1},w.prototype.reflow=function(){var t=0,e=E.updateProperty;return t+=e(this,"top",this.itemset?this.itemset.top:0),t+=e(this,"height",this.itemset?this.itemset.height:0),t>0},b.prototype=new c,b.prototype.setOptions=u.prototype.setOptions,b.prototype.setRange=function(){},b.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},b.prototype.getItems=function(){return this.itemsData},b.prototype.setRange=function(t){this.range=t},b.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(E.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t) -}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof r?this.groupsData=t:(this.groupsData=new r({fieldTypes:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;E.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},b.prototype.getGroups=function(){return this.groupsData},b.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.frame;if(!r){r=document.createElement("div"),r.className="groupset";var o=n.className;o&&E.addClassName(r,E.option.asString(o)),this.frame=r,t+=1}if(!this.parent)throw Error("Cannot repaint groupset: no parent attached");var s=this.parent.getContainer();if(!s)throw Error("Cannot repaint groupset: parent has no container element");r.parentNode||(s.appendChild(r),t+=1),t+=e(r.style,"height",i(n.height,this.height+"px")),t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"width",i(n.width,"100%"));var a=this,h=this.queue,p=this.groups,u=this.groupsData,c=Object.keys(h);if(c.length){c.forEach(function(t){var e=h[t],i=p[t];switch(e){case"add":case"update":if(!i){var n=Object.create(a.options);i=new w(a,t,n),i.setItems(a.itemsData),p[t]=i,a.controller.add(i)}i.data=u.get(t),delete h[t];break;case"remove":i&&(i.setItems(),delete p[t],a.controller.remove(i)),delete h[t];break;default:console.log('Error: unknown action "'+e+'"')}});for(var d=this.groupsData.getIds({order:this.options.groupsOrder}),l=0;d.length>l;l++)(function(t,e){var i=0;e&&(i=function(){return e.top+e.height}),t.setOptions({top:i})})(p[d[l]],p[d[l-1]]);t++}return t>0},b.prototype.getContainer=function(){return this.frame},b.prototype.reflow=function(){var t=0,e=this.options,i=E.updateProperty,n=E.option.asNumber,r=E.option.asSize,o=this.frame;if(o){var s,a=n(e.maxHeight),h=null!=r(e.height);if(h)s=o.offsetHeight;else{s=0;for(var p in this.groups)if(this.groups.hasOwnProperty(p)){var u=this.groups[p];s+=u.height}}null!=a&&(s=Math.min(s,a)),t+=i(this,"height",s),t+=i(this,"top",o.offsetTop),t+=i(this,"left",o.offsetLeft),t+=i(this,"width",o.offsetWidth)}return t>0},b.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},b.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},b.prototype._onUpdate=function(t){this._toQueue(t,"update")},b.prototype._onAdd=function(t){this._toQueue(t,"add")},b.prototype._onRemove=function(t){this._toQueue(t,"remove")},b.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},S.prototype.setOptions=function(t){t&&E.extend(this.options,t),this.controller.reflow(),this.controller.repaint()},S.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof r&&(e=t):e=null,t instanceof r||(e=new r({fieldTypes:{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(),o=n.min,s=n.max;if(null!=o&&null!=s){var a=s.valueOf()-o.valueOf();o=new Date(o.valueOf()-.05*a),s=new Date(s.valueOf()+.05*a)}void 0!=this.options.start&&(o=new Date(this.options.start.valueOf())),void 0!=this.options.end&&(s=new Date(this.options.end.valueOf())),(null!=o||null!=s)&&this.range.setRange(o,s)}},S.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?b:f;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);E.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.root.height-e.timeaxis.height-e.content.height},height:function(){return e.options.height?e.root.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!E.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null}}),this.content=new i(this.root,[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)}},S.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 r=t.max("start");r&&(i=r.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}};var C={util:E,events:_,Controller:p,DataSet:r,DataView:o,Range:a,Stack:s,TimeStep:TimeStep,EventBus:h,components:{items:{Item:m,ItemBox:g,ItemPoint:v,ItemRange:y},Component:u,Panel:c,RootPanel:d,ItemSet:f,TimeAxis:l},Timeline:S};n!==void 0&&(n=C),i!==void 0&&i.exports!==void 0&&(i.exports=C),"function"==typeof t&&t(function(){return C}),"undefined"!=typeof window&&(window.vis=C),E.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.point {\n background: none;\n}\n\n.graph .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n")},{moment:2}],2:[function(e,i){(function(){(function(n){function r(t,e){return function(i){return c(t.call(this,i),e)}}function o(t){return function(e){return this.lang().ordinal(t.call(this,e))}}function s(){}function a(t){p(this,t)}function h(t){var e=this._data={},i=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,r=t.weeks||t.week||t.w||0,o=t.days||t.day||t.d||0,s=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,h=t.seconds||t.second||t.s||0,p=t.milliseconds||t.millisecond||t.ms||0;this._milliseconds=p+1e3*h+6e4*a+36e5*s,this._days=o+7*r,this._months=n+12*i,e.milliseconds=p%1e3,h+=u(p/1e3),e.seconds=h%60,a+=u(h/60),e.minutes=a%60,s+=u(a/60),e.hours=s%24,o+=u(s/24),o+=7*r,e.days=o%30,n+=u(o/30),e.months=n%12,i+=u(n/12),e.years=i}function p(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function u(t){return 0>t?Math.ceil(t):Math.floor(t)}function c(t,e){for(var i=t+"";e>i.length;)i="0"+i;return i}function d(t,e,i){var n,r=e._milliseconds,o=e._days,s=e._months;r&&t._d.setTime(+t+r*i),o&&t.date(t.date()+o*i),s&&(n=t.date(),t.date(1).month(t.month()+s*i).date(Math.min(n,t.daysInMonth())))}function l(t){return"[object Array]"===Object.prototype.toString.call(t)}function f(t,e){var i,n=Math.min(t.length,e.length),r=Math.abs(t.length-e.length),o=0;for(i=0;n>i;i++)~~t[i]!==~~e[i]&&o++;return o+r}function m(t,e){return e.abbr=t,R[t]||(R[t]=new s),R[t].set(e),R[t]}function g(t){return t?(!R[t]&&U&&e("./lang/"+t),R[t]):I.fn._lang}function v(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,i,n=t.match(F);for(e=0,i=n.length;i>e;e++)n[e]=ae[n[e]]?ae[n[e]]:v(n[e]);return function(r){var o="";for(e=0;i>e;e++)o+="function"==typeof n[e].call?n[e].call(r,t):n[e];return o}}function w(t,e){function i(e){return t.lang().longDateFormat(e)||e}for(var n=5;n--&&z.test(e);)e=e.replace(z,i);return re[e]||(re[e]=y(e)),re[e](t)}function b(t){switch(t){case"DDDD":return V;case"YYYY":return B;case"YYYYY":return Z;case"S":case"SS":case"SSS":case"DDD":return q;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":case"a":case"A":return X;case"X":return J;case"Z":case"ZZ":return K;case"T":return G;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return W;default:return RegExp(t.replace("\\",""))}}function S(t,e,i){var n,r=i._a;switch(t){case"M":case"MM":r[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":n=g(i._l).monthsParse(e),null!=n?r[1]=n:i._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(r[2]=~~e);break;case"YY":r[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":r[0]=~~e;break;case"a":case"A":i._isPm="pm"===(e+"").toLowerCase();break;case"H":case"HH":case"h":case"hh":r[3]=~~e;break;case"m":case"mm":r[4]=~~e;break;case"s":case"ss":r[5]=~~e;break;case"S":case"SS":case"SSS":r[6]=~~(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,n=(e+"").match(ee),n&&n[1]&&(i._tzh=~~n[1]),n&&n[2]&&(i._tzm=~~n[2]),n&&"+"===n[0]&&(i._tzh=-i._tzh,i._tzm=-i._tzm)}null==e&&(i._isValid=!1)}function T(t){var e,i,n=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=n[e]=null==t._a[e]?2===e?1:0:t._a[e];n[3]+=t._tzh||0,n[4]+=t._tzm||0,i=new Date(0),t._useUTC?(i.setUTCFullYear(n[0],n[1],n[2]),i.setUTCHours(n[3],n[4],n[5],n[6])):(i.setFullYear(n[0],n[1],n[2]),i.setHours(n[3],n[4],n[5],n[6])),t._d=i}}function E(t){var e,i,n=t._f.match(F),r=t._i;for(t._a=[],e=0;n.length>e;e++)i=(b(n[e]).exec(r)||[])[0],i&&(r=r.slice(r.indexOf(i)+i.length)),ae[n[e]]&&S(n[e],i,t);t._isPm&&12>t._a[3]&&(t._a[3]+=12),t._isPm===!1&&12===t._a[3]&&(t._a[3]=0),T(t)}function M(t){for(var e,i,n,r,o=99;t._f.length;){if(e=p({},t),e._f=t._f.pop(),E(e),i=new a(e),i.isValid()){n=i;break}r=f(e._a,i.toArray()),o>r&&(o=r,n=i)}p(t,n)}function D(t){var e,i=t._i;if(Q.exec(i)){for(t._f="YYYY-MM-DDT",e=0;4>e;e++)if(te[e][1].exec(i)){t._f+=te[e][0];break}K.exec(i)&&(t._f+=" Z"),E(t)}else t._d=new Date(i)}function _(t){var e=t._i,i=P.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?D(t):l(e)?(t._a=e.slice(0),T(t)):t._d=e instanceof Date?new Date(+e):new Date(e)}function C(t,e,i,n,r){return r.relativeTime(e||1,!!i,t,n)}function x(t,e,i){var n=j(Math.abs(t)/1e3),r=j(n/60),o=j(r/60),s=j(o/24),a=j(s/365),h=45>n&&["s",n]||1===r&&["m"]||45>r&&["mm",r]||1===o&&["h"]||22>o&&["hh",o]||1===s&&["d"]||25>=s&&["dd",s]||45>=s&&["M"]||345>s&&["MM",j(s/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,C.apply({},h)}function L(t,e,i){var n=i-e,r=i-t.day();return r>n&&(r-=7),n-7>r&&(r+=7),Math.ceil(I(t).add("d",r).dayOfYear()/7)}function O(t){var e=t._i,i=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=g().preparse(e)),I.isMoment(e)?(t=p({},e),t._d=new Date(+e._d)):i?l(i)?M(t):E(t):_(t),new a(t))}function A(t,e){I.fn[t]=I.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),this):this._d["get"+i+e]()}}function N(t){I.duration.fn[t]=function(){return this._data[t]}}function Y(t,e){I.duration.fn["as"+t]=function(){return+this/e}}for(var I,k,H="2.0.0",j=Math.round,R={},U=i!==n&&i.exports,P=/^\/?Date\((\-?\d+)/i,F=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,z=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,W=/\d\d?/,q=/\d{1,3}/,V=/\d{3}/,B=/\d{1,4}/,Z=/[+\-]?\d{1,6}/,X=/[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i,K=/Z|[\+\-]\d\d:?\d\d/i,G=/T/i,J=/[\+\-]?\d+(\.\d{1,3})?/,Q=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,$="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(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/]],ee=/([\+\-]|\d\d)/gi,ie="Month|Date|Hours|Minutes|Seconds|Milliseconds".split("|"),ne={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},re={},oe="DDD w W M D d".split(" "),se="M D H h m s w W".split(" "),ae={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 c(this.year()%100,2)},YYYY:function(){return c(this.year(),4)},YYYYY:function(){return c(this.year(),5)},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~~(this.milliseconds()/100)},SS:function(){return c(~~(this.milliseconds()/10),2)},SSS:function(){return c(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+c(~~(t/60),2)+":"+c(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+c(~~(10*t/6),4)},X:function(){return this.unix()}};oe.length;)k=oe.pop(),ae[k+"o"]=o(ae[k]);for(;se.length;)k=se.pop(),ae[k+k]=r(ae[k],2);for(ae.DDDD=r(ae.DDD,3),s.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=I([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=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()]},_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},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 r=this._relativeTime[i];return"function"==typeof r?r(t,e,i,n):r.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 L(t,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},I=function(t,e,i){return O({_i:t,_f:e,_l:i,_isUTC:!1})},I.utc=function(t,e,i){return O({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e})},I.unix=function(t){return I(1e3*t)},I.duration=function(t,e){var i,n=I.isDuration(t),r="number"==typeof t,o=n?t._data:r?{}:t;return r&&(e?o[e]=t:o.milliseconds=t),i=new h(o),n&&t.hasOwnProperty("_lang")&&(i._lang=t._lang),i},I.version=H,I.defaultFormat=$,I.lang=function(t,e){return t?(e?m(t,e):R[t]||g(t),I.duration.fn._lang=I.fn._lang=g(t),n):I.fn._lang._abbr},I.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),g(t)},I.isMoment=function(t){return t instanceof a},I.isDuration=function(t){return t instanceof h},I.fn=a.prototype={clone:function(){return I(this)},valueOf:function(){return+this._d},unix:function(){return Math.floor(+this._d/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._d},toJSON:function(){return I.utc(this).format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!f(this._a,(this._isUTC?I.utc(this._a):I(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this._isUTC=!0,this},local:function(){return this._isUTC=!1,this},format:function(t){var e=w(this,t||I.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?I.duration(+e,t):I.duration(t,e),d(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?I.duration(+e,t):I.duration(t,e),d(this,i,-1),this},diff:function(t,e,i){var n,r,o=this._isUTC?I(t).utc():I(t).local(),s=6e4*(this.zone()-o.zone());return e&&(e=e.replace(/s$/,"")),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),r=12*(this.year()-o.year())+(this.month()-o.month()),r+=(this-I(this).startOf("month")-(o-I(o).startOf("month")))/n,"year"===e&&(r/=12)):(n=this-o-s,r="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?n/864e5:"week"===e?n/6048e5:n),i?r:u(r)},from:function(t,e){return I.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(I(),t)},calendar:function(){var t=this.diff(I().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+I(t).startOf(e)},isBefore:function(t,e){return e=e!==n?e:"millisecond",+this.clone().startOf(e)<+I(t).startOf(e)},isSame:function(t,e){return e=e!==n?e:"millisecond",+this.clone().startOf(e)===+I(t).startOf(e)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return I.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=j((I(this).startOf("day")-I(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},isoWeek:function(t){var e=L(this,1,4);return null==t?e:this.add("d",7*(t-e))},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},lang:function(t){return t===n?this._lang:(this._lang=g(t),this)}},k=0;ie.length>k;k++)A(ie[k].toLowerCase().replace(/s$/,""),ie[k]);A("year","FullYear"),I.fn.days=I.fn.day,I.fn.weeks=I.fn.week,I.fn.isoWeeks=I.fn.isoWeek,I.duration.fn=h.prototype={weeks:function(){return u(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*this._months},humanize:function(t){var e=+this,i=x(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},lang:I.fn.lang};for(k in ne)ne.hasOwnProperty(k)&&(Y(k,ne[k]),N(k.toLowerCase()));Y("Weeks",6048e5),I.lang("en",{ordinal:function(t){var e=t%10,i=1===~~(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),U&&(i.exports=I),"undefined"==typeof ender&&(this.moment=I),"function"==typeof t&&t.amd&&t("moment",[],function(){return I})}).call(this)})()},{}]},{},[1])(1)}); \ No newline at end of file +(function(t){if("function"==typeof bootstrap)bootstrap("vis",t);else if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=t}else"undefined"!=typeof window?window.vis=t():global.vis=t()})(function(){var t;return function(t,e,i){function s(i,n){if(!e[i]){if(!t[i]){var r="function"==typeof require&&require;if(!n&&r)return r(i,!0);if(o)return o(i,!0);throw Error("Cannot find module '"+i+"'")}var a=e[i]={exports:{}};t[i][0].call(a.exports,function(e){var o=t[i][1][e];return s(o?o:e)},a,a.exports)}return e[i].exports}for(var o="function"==typeof require&&require,n=0;i.length>n;n++)s(i[n]);return s}({1:[function(e,i,s){(function(){function o(t){if(this.id=M.randomUUID(),this.options=t||{},this.data={},this.fieldId=this.options.fieldId||"id",this.fieldTypes={},this.options.fieldTypes)for(var e in this.options.fieldTypes)if(this.options.fieldTypes.hasOwnProperty(e)){var i=this.options.fieldTypes[e];this.fieldTypes[e]="Date"==i||"ISODate"==i||"ASPDate"==i?"Date":i}this.subscribers={},this.internalIds={}}function n(t,e){this.id=M.randomUUID(),this.data=null,this.ids={},this.options=e||{},this.fieldId="id",this.subscribers={};var i=this;this.listener=function(){i._onEvent.apply(i,arguments)},this.setData(t)}function r(t,e){this.parent=t,this.options=e||{},this.defaultOptions={order:function(t,e){if(t instanceof y){if(e instanceof y){var i=t.data.end-t.data.start,s=e.data.end-e.data.start;return i-s||t.data.start-e.data.start}return-1}return e instanceof y?1:t.data.start-e.data.start},margin:{item:10}},this.ordered=[]}function a(t){this.id=M.randomUUID(),this.start=0,this.end=0,this.options={min:null,max:null,zoomMin:null,zoomMax:null},this.listeners=[],this.setOptions(t)}function h(){this.subscriptions=[]}function d(){this.id=M.randomUUID(),this.components={},this.repaintTimer=void 0,this.reflowTimer=void 0}function l(){this.id=null,this.parent=null,this.depends=null,this.controller=null,this.options=null,this.frame=null,this.top=0,this.left=0,this.width=0,this.height=0}function p(t,e,i){this.id=M.randomUUID(),this.parent=t,this.depends=e,this.options=i||{}}function u(t,e){this.id=M.randomUUID(),this.container=t,this.options=e||{},this.defaultOptions={autoResize:!0},this.listeners={}}function c(t,e,i){this.id=M.randomUUID(),this.parent=t,this.depends=e,this.dom={majorLines:[],majorTexts:[],minorLines:[],minorTexts:[],redundant:{majorLines:[],majorTexts:[],minorLines:[],minorTexts:[]}},this.props={range:{start:0,end:0,minimumStep:0},lineTop:0},this.options=i||{},this.defaultOptions={orientation:"bottom",showMinorLabels:!0,showMajorLabels:!0},this.conversion=null,this.range=null}function f(t,e,i){this.id=M.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={style:"box",align:"center",orientation:"bottom",margin:{axis:20,item:10},padding:5},this.dom={};var s=this;this.itemsData=null,this.range=null,this.listeners={add:function(t,e,i){i!=s.id&&s._onAdd(e.items)},update:function(t,e,i){i!=s.id&&s._onUpdate(e.items)},remove:function(t,e,i){i!=s.id&&s._onRemove(e.items)}},this.items={},this.queue={},this.stack=new r(this,Object.create(this.options)),this.conversion=null}function m(t,e,i,s){this.parent=t,this.data=e,this.dom=null,this.options=i||{},this.defaultOptions=s||{},this.selected=!1,this.visible=!1,this.top=0,this.left=0,this.width=0,this.height=0}function g(t,e,i,s){this.props={dot:{left:0,top:0,width:0,height:0},line:{top:0,left:0,width:0,height:0}},m.call(this,t,e,i,s)}function v(t,e,i,s){this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0}},m.call(this,t,e,i,s)}function y(t,e,i,s){this.props={content:{left:0,width:0}},m.call(this,t,e,i,s)}function w(t,e,i){this.id=M.randomUUID(),this.parent=t,this.groupId=e,this.itemsData=null,this.itemset=null,this.options=i||{},this.options.top=0,this.top=0,this.left=0,this.width=0,this.height=0}function b(t,e,i){this.id=M.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.range=null,this.itemsData=null,this.groupsData=null,this.groups={},this.queue={};var s=this;this.listeners={add:function(t,e){s._onAdd(e.items)},update:function(t,e){s._onUpdate(e.items)},remove:function(t,e){s._onRemove(e.items)}}}function _(t,e,i){var s=this;if(this.options=M.extend({orientation:"bottom",min:null,max:null,zoomMin:10,zoomMax:31536e10,moveable:!0,zoomable:!0,showMinorLabels:!0,showMajorLabels:!0,autoResize:!1},i),this.controller=new d,!t)throw Error("No container element provided");var o=Object.create(this.options);o.height=function(){return s.options.height?s.options.height:s.timeaxis.height+s.content.height},this.root=new u(t,o),this.controller.add(this.root);var n=T().hours(0).minutes(0).seconds(0).milliseconds(0);this.range=new a({start:n.clone().add("days",-3).valueOf(),end:n.clone().add("days",4).valueOf()}),this.range.subscribe(this.root,"move","horizontal"),this.range.subscribe(this.root,"zoom","horizontal"),this.range.on("rangechange",function(){var t=!0;s.controller.requestReflow(t)}),this.range.on("rangechanged",function(){var t=!0;s.controller.requestReflow(t)});var r=Object.create(o);r.range=this.range,this.timeaxis=new c(this.root,[],r),this.timeaxis.setRange(this.range),this.controller.add(this.timeaxis),this.setGroups(null),this.itemsData=null,this.groupsData=null,e&&this.setItems(e)}function x(t){this.containerElement=t,this.width="100%",this.height="100%",this.refreshRate=50,this.stabilize=!0,this.selectable=!0,this.constants={nodes:{radiusMin:5,radiusMax:20,radius:5,distance:100,style:"rect",image:void 0,widthMin:16,widthMax:64,fontColor:"black",fontSize:14,fontFace:"arial",borderColor:"#2B7CE9",backgroundColor:"#97C2FC",highlightColor:"#D2E5FF",group:void 0},edges:{widthMin:1,widthMax:15,width:1,style:"line",color:"#343434",fontColor:"#343434",fontSize:14,fontFace:"arial",length:100,dashlength:10,dashgap:5},packages:{radius:5,radiusMin:5,radiusMax:10,style:"dot",color:"#2B7CE9",image:void 0,widthMin:16,widthMax:64,duration:1},minForce:.05,minVelocity:.02,maxIterations:1e3},this.nodes=[],this.edges=[],this.packages=[],this.images=new x.Images,this.groups=new x.Groups,this.hasMovingEdges=!1,this.hasMovingNodes=!1,this.hasMovingPackages=!1,this.selection=[],this.timer=void 0,this._create()}var T=e("moment"),M={};M.isNumber=function(t){return t instanceof Number||"number"==typeof t},M.isString=function(t){return t instanceof String||"string"==typeof t},M.isDate=function(t){if(t instanceof Date)return!0;if(M.isString(t)){var e=S.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},M.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},M.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},M.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var s=arguments[e];for(var o in s)s.hasOwnProperty(o)&&void 0!==s[o]&&(t[o]=s[o])}return t},M.cast=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 Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t);case"string":case"String":return t+"";case"Date":if(M.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(T.isMoment(t))return new Date(t.valueOf());if(M.isString(t))return i=S.exec(t),i?new Date(Number(i[1])):T(t).toDate();throw Error("Cannot cast object of type "+M.getType(t)+" to type Date");case"Moment":if(M.isNumber(t))return T(t);if(t instanceof Date)return T(t.valueOf());if(T.isMoment(t))return T.clone();if(M.isString(t))return i=S.exec(t),i?T(Number(i[1])):T(t);throw Error("Cannot cast object of type "+M.getType(t)+" to type Date");case"ISODate":if(t instanceof Date)return t.toISOString();if(T.isMoment(t))return t.toDate().toISOString();if(M.isNumber(t)||M.isString(t))return T(t).toDate().toISOString();throw Error("Cannot cast object of type "+M.getType(t)+" to type ISODate");case"ASPDate":if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(M.isNumber(t)||M.isString(t))return"/Date("+T(t).valueOf()+")/";throw Error("Cannot cast object of type "+M.getType(t)+" to type ASPDate");default:throw Error("Cannot cast object of type "+M.getType(t)+' to type "'+e+'"')}};var S=/^\/?Date\((\-?\d+)/i;if(M.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},M.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetLeft,o=t.offsetParent;null!=o&&o!=i&&o!=e;)s+=o.offsetLeft,s-=o.scrollLeft,o=o.offsetParent;return s},M.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetTop,o=t.offsetParent;null!=o&&o!=i&&o!=e;)s+=o.offsetTop,s-=o.scrollTop,o=o.offsetParent;return s},M.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,s=document.body;return e+(i&&i.scrollTop||s&&s.scrollTop||0)-(i&&i.clientTop||s&&s.clientTop||0)},M.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,s=document.body;return e+(i&&i.scrollLeft||s&&s.scrollLeft||0)-(i&&i.clientLeft||s&&s.clientLeft||0)},M.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},M.removeClassName=function(t,e){var i=t.className.split(" "),s=i.indexOf(e);-1!=s&&(i.splice(s,1),t.className=i.join(" "))},M.forEach=function(t,e){var i,s;if(t instanceof Array)for(i=0,s=t.length;s>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},M.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},M.addEventListener=function(t,e,i,s){t.addEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,s)):t.attachEvent("on"+e,i)},M.removeEventListener=function(t,e,i,s){t.removeEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,s)):t.detachEvent("on"+e,i)},M.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},M.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},M.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},M.option={},M.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},M.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},M.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?t+"":e||null},M.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),M.isString(t)?t:M.isNumber(t)?t+"px":e||null},M.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},M.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}},!Array.prototype.indexOf){Array.prototype.indexOf=function(t){for(var e=0;this.length>e;e++)if(this[e]==t)return e;return-1};try{console.log("Warning: Ancient browser detected. Please update your browser")}catch(C){}}Array.prototype.forEach||(Array.prototype.forEach=function(t,e){for(var i=0,s=this.length;s>i;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,s,o;if(null==this)throw new TypeError(" this is null or not defined");var n=Object(this),r=n.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),s=Array(r),o=0;r>o;){var a,h;o in n&&(a=n[o],h=t.call(i,a,o,n),s[o]=h),o++}return s}),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 s=[],o=arguments[1],n=0;i>n;n++)if(n in e){var r=e[n];t.call(o,r,n,e)&&s.push(r)}return s}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],s=i.length;return function(o){if("object"!=typeof o&&"function"!=typeof o||null===o)throw new TypeError("Object.keys called on non-object");var n=[];for(var r in o)t.call(o,r)&&n.push(r);if(e)for(var a=0;s>a;a++)t.call(o,i[a])&&n.push(i[a]);return n}}()),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,s=function(){},o=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,o.prototype=new s,o}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e});var E={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,s=this.listeners.length;s>i;i++){var o=e[i];if(o&&o.object==t)return i}return-1},addListener:function(t,e,i){var s=this.indexOf(t),o=this.listeners[s];o||(o={object:t,events:{}},this.listeners.push(o));var n=o.events[e];n||(n=[],o.events[e]=n),-1==n.indexOf(i)&&n.push(i)},removeListener:function(t,e,i){var s=this.indexOf(t),o=this.listeners[s];if(o){var n=o.events[e];n&&(s=n.indexOf(i),-1!=s&&n.splice(s,1),0==n.length&&delete o.events[e]);var r=0,a=o.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[s]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var s=this.indexOf(t),o=this.listeners[s];if(o){var n=o.events[e];if(n)for(var r=0,a=n.length;a>r;r++)n[r](i)}}};TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){t instanceof Date&&e instanceof Date&&(this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i))},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step);break;default:}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(6>this.current.getMonth())switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+60*1e3*this.step);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+60*60*1e3*this.step);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step);break;default:}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);break;default:}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,s=864e5,o=36e5,n=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*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),s/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*o>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),o>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),n>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 T(t).format("SSS");case TimeStep.SCALE.SECOND:return T(t).format("s");case TimeStep.SCALE.MINUTE:return T(t).format("HH:mm");case TimeStep.SCALE.HOUR:return T(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return T(t).format("ddd D");case TimeStep.SCALE.DAY:return T(t).format("D");case TimeStep.SCALE.MONTH:return T(t).format("MMM");case TimeStep.SCALE.YEAR:return T(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return T(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return T(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return T(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return T(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return T(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},o.prototype.subscribe=function(t,e,i){var s=this.subscribers[t];s||(s=[],this.subscribers[t]=s),s.push({id:i?i+"":null,callback:e})},o.prototype.unsubscribe=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},o.prototype._trigger=function(t,e,i){if("*"==t)throw Error("Cannot trigger event *");var s=[];t in this.subscribers&&(s=s.concat(this.subscribers[t])),"*"in this.subscribers&&(s=s.concat(this.subscribers["*"]));for(var o=0;s.length>o;o++){var n=s[o];n.callback&&n.callback(t,e,i||null)}},o.prototype.add=function(t,e){var i,s=[],o=this;if(t instanceof Array)for(var n=0,r=t.length;r>n;n++)i=o._addItem(t[n]),s.push(i);else if(M.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},p=0,u=a.length;u>p;p++){var c=a[p];l[c]=t.getValue(h,p)}i=o._addItem(l),s.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=o._addItem(t),s.push(i)}s.length&&this._trigger("add",{items:s},e)},o.prototype.update=function(t,e){var i=[],s=[],o=this,n=o.fieldId,r=function(t){var e=t[n];o.data[e]?(e=o._updateItem(t),s.push(e)):(e=o._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(M.isDataTable(t))for(var d=this._getColumnNames(t),l=0,p=t.getNumberOfRows();p>l;l++){for(var u={},c=0,f=d.length;f>c;c++){var m=d[c];u[m]=t.getValue(l,c)}r(u)}else{if(!(t instanceof Object))throw Error("Unknown dataType");r(t)}i.length&&this._trigger("add",{items:i},e),s.length&&this._trigger("update",{items:s},e)},o.prototype.get=function(){var t,e,i,s,o=this,n=M.getType(arguments[0]);"String"==n||"Number"==n?(t=arguments[0],i=arguments[1],s=arguments[2]):"Array"==n?(e=arguments[0],i=arguments[1],s=arguments[2]):(i=arguments[0],s=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",s&&r!=M.getType(s))throw Error('Type of parameter "data" ('+M.getType(s)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!M.isDataTable(s))throw Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=s?"DataTable"==M.getType(s)?"DataTable":"Array":"Array";var a,h,d,l,p=i&&i.fieldTypes||this.options.fieldTypes,u=i&&i.filter,c=[];if(void 0!=t)a=o._getItem(t,p),u&&!u(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=o._getItem(e[d],p),(!u||u(a))&&c.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=o._getItem(h,p),(!u||u(a))&&c.push(a));if(i&&i.order&&void 0==t&&this._sort(c,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(d=0,l=c.length;l>d;d++)c[d]=this._filterFields(c[d],f)}if("DataTable"==r){var m=this._getColumnNames(s);if(void 0!=t)o._appendRow(s,m,a);else for(d=0,l=c.length;l>d;d++)o._appendRow(s,m,c[d]);return s}if(void 0!=t)return a;if(s){for(d=0,l=c.length;l>d;d++)s.push(c[d]);return s}return c},o.prototype.getIds=function(t){var e,i,s,o,n,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.fieldTypes||this.options.fieldTypes,l=[];if(a)if(h){n=[];for(s in r)r.hasOwnProperty(s)&&(o=this._getItem(s,d),a(o)&&n.push(o));for(this._sort(n,h),e=0,i=n.length;i>e;e++)l[e]=n[e][this.fieldId]}else for(s in r)r.hasOwnProperty(s)&&(o=this._getItem(s,d),a(o)&&l.push(o[this.fieldId]));else if(h){n=[];for(s in r)r.hasOwnProperty(s)&&n.push(r[s]);for(this._sort(n,h),e=0,i=n.length;i>e;e++)l[e]=n[e][this.fieldId]}else for(s in r)r.hasOwnProperty(s)&&(o=r[s],l.push(o[this.fieldId]));return l},o.prototype.forEach=function(t,e){var i,s,o=e&&e.filter,n=e&&e.fieldTypes||this.options.fieldTypes,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],s=i[this.fieldId],t(i,s);else for(s in r)r.hasOwnProperty(s)&&(i=this._getItem(s,n),(!o||o(i))&&t(i,s))},o.prototype.map=function(t,e){var i,s=e&&e.filter,o=e&&e.fieldTypes||this.options.fieldTypes,n=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,o),(!s||s(i))&&n.push(t(i,a)));return e&&e.order&&this._sort(n,e.order),n},o.prototype._filterFields=function(t,e){var i={};for(var s in t)t.hasOwnProperty(s)&&-1!=e.indexOf(s)&&(i[s]=t[s]);return i},o.prototype._sort=function(t,e){if(M.isString(e)){var i=e;t.sort(function(t,e){var s=t[i],o=e[i];return s>o?1:o>s?-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,s,o=[];if(M.isNumber(t)||M.isString(t))delete this.data[t],delete this.internalIds[t],o.push(t);else if(t instanceof Array){for(i=0,s=t.length;s>i;i++)this.remove(t[i]);o=items.concat(t)}else if(t instanceof Object)for(i in this.data)this.data.hasOwnProperty(i)&&this.data[i]==t&&(delete this.data[i],delete this.internalIds[i],o.push(i));o.length&&this._trigger("remove",{items:o},e)},o.prototype.clear=function(t){var e=Object.keys(this.data);this.data={},this.internalIds={},this._trigger("remove",{items:e},t)},o.prototype.max=function(t){var e=this.data,i=null,s=null;for(var o in e)if(e.hasOwnProperty(o)){var n=e[o],r=n[t];null!=r&&(!i||r>s)&&(i=n,s=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,s=null;for(var o in e)if(e.hasOwnProperty(o)){var n=e[o],r=n[t];null!=r&&(!i||s>r)&&(i=n,s=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],s=this.options.fieldTypes[t],o=0;for(var n in e)if(e.hasOwnProperty(n)){for(var r=e[n],a=M.cast(r[t],s),h=!1,d=0;o>d;d++)if(i[d]==a){h=!0;break}h||(i[o]=a,o++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw Error("Cannot add item: item with id "+e+" already exists")}else e=M.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var s in t)if(t.hasOwnProperty(s)){var o=this.fieldTypes[s];i[s]=M.cast(t[s],o)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,s,o=this.data[t];if(!o)return null;var n={},r=this.fieldId,a=this.internalIds;if(e)for(i in o)o.hasOwnProperty(i)&&(s=o[i],i==r&&s in a||(n[i]=M.cast(s,e[i])));else for(i in o)o.hasOwnProperty(i)&&(s=o[i],i==r&&s in a||(n[i]=s));return n},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw Error("Cannot update item: no item with id "+e+" found");for(var s in t)if(t.hasOwnProperty(s)){var o=this.fieldTypes[s];i[s]=M.cast(t[s],o)}return e},o.prototype._getColumnNames=function(t){for(var e=[],i=0,s=t.getNumberOfColumns();s>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var s=t.addRow(),o=0,n=e.length;n>o;o++){var r=e[o];t.setValue(s,o,i[r])}},n.prototype.setData=function(t){var e,i,s;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var o in this.ids)this.ids.hasOwnProperty(o)&&e.push(o);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,s=e.length;s>i;i++)o=e[i],this.ids[o]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},n.prototype.get=function(){var t,e,i,s=this,o=M.getType(arguments[0]);"String"==o||"Number"==o||"Array"==o?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var n=M.extend({},this.options,e);this.options.filter&&e&&e.filter&&(n.filter=function(t){return s.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(n),r.push(i),this.data&&this.data.get.apply(this.data,r)},n.prototype.getIds=function(t){var e;if(this.data){var i,s=this.options.filter;i=t&&t.filter?s?function(e){return s(e)&&t.filter(e)}:t.filter:s,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e +},n.prototype._onEvent=function(t,e,i){var s,o,n,r,a=e&&e.items,h=this.data,d=[],l=[],p=[];if(a&&h){switch(t){case"add":for(s=0,o=a.length;o>s;s++)n=a[s],r=this.get(n),r&&(this.ids[n]=!0,d.push(n));break;case"update":for(s=0,o=a.length;o>s;s++)n=a[s],r=this.get(n),r?this.ids[n]?l.push(n):(this.ids[n]=!0,d.push(n)):this.ids[n]&&(delete this.ids[n],p.push(n));break;case"remove":for(s=0,o=a.length;o>s;s++)n=a[s],this.ids[n]&&(delete this.ids[n],p.push(n))}d.length&&this._trigger("add",{items:d},i),l.length&&this._trigger("update",{items:l},i),p.length&&this._trigger("remove",{items:p},i)}},n.prototype.subscribe=o.prototype.subscribe,n.prototype.unsubscribe=o.prototype.unsubscribe,n.prototype._trigger=o.prototype._trigger,r.prototype.setOptions=function(t){M.extend(this.options,t)},r.prototype.update=function(){this._order(),this._stack()},r.prototype._order=function(){var t=this.parent.items;if(!t)throw Error("Cannot stack items: parent does not contain items");var e=[],i=0;M.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var s=this.options.order||this.defaultOptions.order;if("function"!=typeof s)throw Error("Option order must be a function");e.sort(s),this.ordered=e},r.prototype._stack=function(){var t,e,i,s=this.ordered,o=this.options,n=o.orientation||this.defaultOptions.orientation,r="top"==n;for(i=o.margin&&void 0!==o.margin.item?o.margin.item:this.defaultOptions.margin.item,t=0,e=s.length;e>t;t++){var a=s[t],h=null;do h=this.checkOverlap(s,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},r.prototype.checkOverlap=function(t,e,i,s,o){for(var n=this.collision,r=t[e],a=s;a>=i;a--){var h=t[a];if(n(r,h,o)&&a!=e)return h}return null},r.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},a.prototype.setOptions=function(t){M.extend(this.options,t),(null!=t.start||null!=t.end)&&this.setRange(t.start,t.end)},a.prototype.subscribe=function(t,e,i){var s,o=this;if("horizontal"!=i&&"vertical"!=i)throw new TypeError('Unknown direction "'+i+'". '+'Choose "horizontal" or "vertical".');if("move"==e)s={component:t,event:e,direction:i,callback:function(t){o._onMouseDown(t,s)},params:{}},t.on("mousedown",s.callback),o.listeners.push(s);else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');s={component:t,event:e,direction:i,callback:function(t){o._onMouseWheel(t,s)},params:{}},t.on("mousewheel",s.callback),o.listeners.push(s)}},a.prototype.on=function(t,e){E.addListener(this,t,e)},a.prototype._trigger=function(t){E.trigger(this,t,{start:this.start,end:this.end})},a.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},a.prototype._applyRange=function(t,e){var i,s=null!=t?M.cast(t,"Number"):this.start,o=null!=e?M.cast(e,"Number"):this.end;if(isNaN(s))throw Error('Invalid start "'+t+'"');if(isNaN(o))throw Error('Invalid end "'+e+'"');if(s>o&&(o=s),null!=this.options.min){var n=this.options.min.valueOf();n>s&&(i=n-s,s+=i,o+=i)}if(null!=this.options.max){var r=this.options.max.valueOf();o>r&&(i=o-r,s-=i,o-=i)}if(null!=this.options.zoomMin){var a=this.options.zoomMin.valueOf();0>a&&(a=0),a>o-s&&(this.end-this.start>a?(i=a-(o-s),s-=i/2,o+=i/2):(s=this.start,o=this.end))}if(null!=this.options.zoomMax){var h=this.options.zoomMax.valueOf();0>h&&(h=0),o-s>h&&(h>this.end-this.start?(i=o-s-h,s+=i/2,o-=i/2):(s=this.start,o=this.end))}var d=this.start!=s||this.end!=o;return this.start=s,this.end=o,d},a.prototype.getRange=function(){return{start:this.start,end:this.end}},a.prototype.conversion=function(t){return this.start,this.end,a.conversion(this.start,this.end,t)},a.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,factor:i/(e-t)}:{offset:0,factor:1}},a.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,s=t.which?1==t.which:1==t.button;if(s){i.mouseX=M.getPageX(t),i.mouseY=M.getPageY(t),i.previousLeft=0,i.previousOffset=0,i.moved=!1,i.start=this.start,i.end=this.end;var o=e.component.frame;o&&(o.style.cursor="move");var n=this;i.onMouseMove||(i.onMouseMove=function(t){n._onMouseMove(t,e)},M.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){n._onMouseUp(t,e)},M.addEventListener(document,"mouseup",i.onMouseUp)),M.preventDefault(t)}},a.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,s=M.getPageX(t),o=M.getPageY(t);void 0==i.mouseX&&(i.mouseX=s),void 0==i.mouseY&&(i.mouseY=o);var n=s-i.mouseX,r=o-i.mouseY,a="horizontal"==e.direction?n:r;Math.abs(a)>=1&&(i.moved=!0);var h=i.end-i.start,d="horizontal"==e.direction?e.component.width:e.component.height,l=-a/d*h;this._applyRange(i.start+l,i.end+l),this._trigger("rangechange"),M.preventDefault(t)},a.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(M.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(M.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},a.prototype._onMouseWheel=function(t,e){t=t||window.event;var i=0;if(t.wheelDelta?i=t.wheelDelta/120:t.detail&&(i=-t.detail/3),i){var s=this,o=function(){var o=i/5,n=null,r=e.component.frame;if(r){var a,h;if("horizontal"==e.direction){a=e.component.width,h=s.conversion(a);var d=M.getAbsoluteLeft(r),l=M.getPageX(t);n=(l-d)/h.factor+h.offset}else{a=e.component.height,h=s.conversion(a);var p=M.getAbsoluteTop(r),u=M.getPageY(t);n=(p+a-u-p)/h.factor+h.offset}}s.zoom(o,n)};o()}M.preventDefault(t)},a.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2),t>=1&&(t=.9),-1>=t&&(t=-.9),0>t&&(t/=1+t);var i=this.start-e,s=this.end-e,o=this.start-i*t,n=this.end-s*t;this.setRange(o,n)},a.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,s=this.end+e*t;this.start=i,this.end=s},h.prototype.on=function(t,e,i){var s=t instanceof RegExp?t:RegExp(t.replace("*","\\w+")),o={id:M.randomUUID(),event:t,regexp:s,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(o),o.id},h.prototype.off=function(t){for(var e=0;this.subscriptions.length>e;){var i=this.subscriptions[e],s=!0;if(t instanceof Object)for(var o in t)t.hasOwnProperty(o)&&t[o]!==i[o]&&(s=!1);else s=i.id==t;s?this.subscriptions.splice(e,1):e++}},h.prototype.emit=function(t,e,i){for(var s=0;this.subscriptions.length>s;s++){var o=this.subscriptions[s];o.regexp.test(t)&&o.callback&&o.callback(t,e,i)}},d.prototype.add=function(t){if(void 0==t.id)throw Error("Component has no field id");if(!(t instanceof l||t instanceof d))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},d.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},d.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},d.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},d.prototype.repaint=function(){function t(s,o){o in i||(s.depends&&s.depends.forEach(function(e){t(e,e.id)}),s.parent&&t(s.parent,s.parent.id),e=s.repaint()||e,i[o]=!0)}var e=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var i={};M.forEach(this.components,t),e&&this.reflow()},d.prototype.reflow=function(){function t(s,o){o in i||(s.depends&&s.depends.forEach(function(e){t(e,e.id)}),s.parent&&t(s.parent,s.parent.id),e=s.reflow()||e,i[o]=!0)}var e=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};M.forEach(this.components,t),e&&this.repaint()},l.prototype.setOptions=function(t){t&&(M.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},l.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},l.prototype.getContainer=function(){return null},l.prototype.getFrame=function(){return this.frame},l.prototype.repaint=function(){return!1},l.prototype.reflow=function(){return!1},l.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},l.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},l.prototype.requestRepaint=function(){if(!this.controller)throw Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},l.prototype.requestReflow=function(){if(!this.controller)throw Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new l,p.prototype.setOptions=l.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.frame;if(!o){o=document.createElement("div"),o.className="panel";var n=s.className;n&&("function"==typeof n?M.addClassName(o,n()+""):M.addClassName(o,n+"")),this.frame=o,t+=1}if(!o.parentNode){if(!this.parent)throw Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw Error("Cannot repaint panel: parent has no container element");r.appendChild(o),t+=1}return t+=e(o.style,"top",i(s.top,"0px")),t+=e(o.style,"left",i(s.left,"0px")),t+=e(o.style,"width",i(s.width,"100%")),t+=e(o.style,"height",i(s.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=M.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},u.prototype=new p,u.prototype.setOptions=l.prototype.setOptions,u.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.frame;if(!o){o=document.createElement("div"),o.className="graph panel";var n=s.className;n&&M.addClassName(o,M.option.asString(n)),this.frame=o,t+=1}if(!o.parentNode){if(!this.container)throw Error("Cannot repaint root panel: no container attached");this.container.appendChild(o),t+=1}return t+=e(o.style,"top",i(s.top,"0px")),t+=e(o.style,"left",i(s.left,"0px")),t+=e(o.style,"width",i(s.width,"100%")),t+=e(o.style,"height",i(s.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},u.prototype.reflow=function(){var t=0,e=M.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},u.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},u.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};M.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},u.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},u.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},u.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;M.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var s=t.frame;if(s){var o=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=o,M.addEventListener(s,i,o)}}})}},c.prototype=new l,c.prototype.setOptions=l.prototype.setOptions,c.prototype.setRange=function(t){if(!(t instanceof a||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},c.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},c.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},c.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.getOption("orientation"),n=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis "+o,!a.parentNode){if(!this.parent)throw Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var l=a.nextSibling;d.removeChild(a);var p="bottom"==o&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(s.top,p)),t+=e(a.style,"left",i(s.left,"0px")),t+=e(a.style,"width",i(s.width,"100%")),t+=e(a.style,"height",i(s.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var u=void 0,c=0;r.hasNext()&&1e3>c;){c++;var f=r.getCurrent(),m=this.toScreen(f),g=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,r.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==u&&(u=m),this._repaintMajorText(m,r.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),w=y.length*(n.majorCharWidth||10)+10;(void 0==u||u>w)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),l?d.insertBefore(a,l):d.appendChild(a)}return t>0},c.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=[]},c.prototype._repaintEnd=function(){M.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},c.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var s=document.createTextNode("");i=document.createElement("div"),i.appendChild(s),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"},c.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var s=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(s),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"},c.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"},c.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"},c.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},c.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 s=document.createElement("DIV");s.className="text major measure",s.appendChild(t),this.frame.appendChild(s),e.measureCharMajor=s}},c.prototype.reflow=function(){var t=0,e=M.updateProperty,i=this.frame,s=this.range;if(!s)throw Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var o=this.props,n=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(o.minorCharHeight=a.clientHeight,o.minorCharWidth=a.clientWidth),h&&(o.majorCharHeight=h.clientHeight,o.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=o.parentHeight&&(o.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":o.minorLabelHeight=n?o.minorCharHeight:0,o.majorLabelHeight=r?o.majorCharHeight:0,o.minorLabelTop=0,o.majorLabelTop=o.minorLabelTop+o.minorLabelHeight,o.minorLineTop=-this.top,o.minorLineHeight=Math.max(this.top+o.majorLabelHeight,0),o.minorLineWidth=1,o.majorLineTop=-this.top,o.majorLineHeight=Math.max(this.top+o.minorLabelHeight+o.majorLabelHeight,0),o.majorLineWidth=1,o.lineTop=0;break;case"top":o.minorLabelHeight=n?o.minorCharHeight:0,o.majorLabelHeight=r?o.majorCharHeight:0,o.majorLabelTop=0,o.minorLabelTop=o.majorLabelTop+o.majorLabelHeight,o.minorLineTop=o.minorLabelTop,o.minorLineHeight=Math.max(d-o.majorLabelHeight-this.top),o.minorLineWidth=1,o.majorLineTop=0,o.majorLineHeight=Math.max(d-this.top),o.majorLineWidth=1,o.lineTop=o.majorLabelHeight+o.minorLabelHeight;break;default:throw Error('Unkown orientation "'+this.getOption("orientation")+'"')}var l=o.minorLabelHeight+o.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",l),this._updateConversion();var p=M.cast(s.start,"Date"),u=M.cast(s.end,"Date"),c=this.toTime(5*(o.minorCharWidth||10))-this.toTime(0);this.step=new TimeStep(p,u,c),t+=e(o.range,"start",p.valueOf()),t+=e(o.range,"end",u.valueOf()),t+=e(o.range,"minimumStep",c.valueOf())}return t>0},c.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):a.conversion(t.start,t.end,this.width)},f.prototype=new p,f.types={box:g,range:y,point:v},f.prototype.setOptions=l.prototype.setOptions,f.prototype.setRange=function(t){if(!(t instanceof a||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},f.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.getOption("orientation"),n=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=s.className;a&&M.addClassName(r,M.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var l=document.createElement("div");l.className="itemset-axis",this.dom.axis=l,this.frame=r,t+=1}if(!this.parent)throw Error("Cannot repaint itemset: no parent attached");var p=this.parent.getContainer();if(!p)throw Error("Cannot repaint itemset: parent has no container element");r.parentNode||(p.appendChild(r),t+=1),this.dom.axis.parentNode||(p.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(s.left,"0px")),t+=e(r.style,"top",i(s.top,"0px")),t+=e(r.style,"width",i(s.width,"100%")),t+=e(r.style,"height",i(s.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(s.left,"0px")),t+=e(this.dom.axis.style,"width",i(s.width,"100%")),t+="bottom"==o?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,c=this.queue,m=this.itemsData,g=this.items,v={fields:[m&&m.fieldId||"id","start","end","content","type"]};return Object.keys(c).forEach(function(e){var i=c[e],o=g[e];switch(i){case"add":case"update":var r=m&&m.get(e,v);if(r){var a=r.type||r.start&&r.end&&"range"||"box",h=f.types[a];if(o&&(h&&o instanceof h?(o.data=r,t++):(t+=o.hide(),o=null)),!o){if(!h)throw new TypeError('Unknown item type "'+a+'"');o=new h(u,r,s,n),t++}o.repaint(),g[e]=o}delete c[e];break;case"remove":o&&(t+=o.hide()),delete g[e],delete c[e];break;default:console.log('Error: unknown action "'+i+'"')}}),M.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},f.prototype.getForeground=function(){return this.dom.foreground},f.prototype.getBackground=function(){return this.dom.background},f.prototype.getAxis=function(){return this.dom.axis},f.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,s=e.margin&&e.margin.item||this.defaultOptions.margin.item,o=M.updateProperty,n=M.option.asNumber,r=M.option.asSize,a=this.frame;if(a){this._updateConversion(),M.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=n(e.maxHeight),l=null!=r(e.height);if(l)h=a.offsetHeight;else{var p=this.stack.ordered;if(p.length){var u=p[0].top,c=p[0].top+p[0].height;M.forEach(p,function(t){u=Math.min(u,t.top),c=Math.max(c,t.top+t.height)}),h=c-u+i+s}else h=i+s}null!=d&&(h=Math.min(h,d)),t+=o(this,"height",h),t+=o(this,"top",a.offsetTop),t+=o(this,"left",a.offsetLeft),t+=o(this,"width",a.offsetWidth)}else t+=1;return t>0},f.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},f.prototype.setItems=function(t){var e,i=this,s=this.itemsData;if(s&&(M.forEach(this.listeners,function(t,e){s.unsubscribe(e,t)}),e=s.getIds(),this._onRemove(e)),t){if(!(t instanceof o||t instanceof n))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(this.itemsData){var r=this.id;M.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,r)}),e=this.itemsData.getIds(),this._onAdd(e)}},f.prototype.getItems=function(){return this.itemsData},f.prototype._onUpdate=function(t){this._toQueue("update",t)},f.prototype._onAdd=function(t){this._toQueue("add",t)},f.prototype._onRemove=function(t){this._toQueue("remove",t)},f.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},f.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):a.conversion(t.start,t.end,this.width)},f.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},f.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},m.prototype.select=function(){this.selected=!0},m.prototype.unselect=function(){this.selected=!1},m.prototype.show=function(){return!1},m.prototype.hide=function(){return!1},m.prototype.repaint=function(){return!1},m.prototype.reflow=function(){return!1},g.prototype=new m(null,null),g.prototype.select=function(){this.selected=!0},g.prototype.unselect=function(){this.selected=!1},g.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");var s=this.parent.getBackground();if(!s)throw Error("Cannot repaint time axis: parent has no background container element");var o=this.parent.getAxis();if(!s)throw Error("Cannot repaint time axis: parent has no axis container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(s.appendChild(e.line),t=!0),e.dot.parentNode||(o.appendChild(e.dot),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw 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 box"+n,e.line.className="item line"+n,e.dot.className="item dot"+n,t=!0)}return t},g.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},g.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},g.prototype.reflow=function(){var t,e,i,s,o,n,r,a,h,d,l,p,u=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(l=this.data,p=this.parent&&this.parent.range,this.visible=l&&p?l.start>p.start&&l.start0},g.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")},g.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var s=t.box,o=t.line,n=t.dot;s.style.left=this.left+"px",s.style.top=this.top+"px",o.style.left=e.line.left+"px","top"==i?(o.style.top="0px",o.style.height=this.top+"px"):(o.style.top=this.top+this.height+"px",o.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),n.style.left=e.dot.left+"px",n.style.top=e.dot.top+"px"}},v.prototype=new m(null,null),v.prototype.select=function(){this.selected=!0},v.prototype.unselect=function(){this.selected=!1},v.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw 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 Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var s=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=s&&(this.className=s,e.point.className="item point"+s,t=!0)}return t},v.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},v.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},v.prototype.reflow=function(){var t,e,i,s,o,n,r,a,h,d,l=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.start>d.start&&h.start0},v.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))},v.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")},y.prototype=new m(null,null),y.prototype.select=function(){this.selected=!0},y.prototype.unselect=function(){this.selected=!1},y.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw 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 Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var s=this.data.className?""+this.data.className:"";this.className!=s&&(this.className=s,e.box.className="item range"+s,t=!0)}return t},y.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},y.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},y.prototype.reflow=function(){var t,e,i,s,o,n,r,a,h,d,l,p,u,c,f,m,g=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,n=this.parent,r=n.toScreen(this.data.start),a=n.toScreen(this.data.end),l=M.updateProperty,p=t.box,u=n.width,f=i.orientation||this.defaultOptions.orientation,s=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,o=i.padding||this.defaultOptions.padding,g+=l(e.content,"width",t.content.offsetWidth),g+=l(this,"height",p.offsetHeight),-u>r&&(r=-u),a>2*u&&(a=2*u),c=0>r?Math.min(-r,a-r-e.content.width-2*o):0,g+=l(e.content,"left",c),"top"==f?(m=s,g+=l(this,"top",m)):(m=n.height-this.height-s,g+=l(this,"top",m)),g+=l(this,"left",r),g+=l(this,"width",Math.max(a-r,1))):g+=1),g>0},y.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))},y.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")},w.prototype=new l,w.prototype.setOptions=l.prototype.setOptions,w.prototype.getContainer=function(){return this.parent.getContainer()},w.prototype.setItems=function(t){if(this.itemset&&(this.itemset.hide(),this.itemset.setItems(),this.parent.controller.remove(this.itemset),this.itemset=null),t){var e=this.groupId,i=Object.create(this.options); +this.itemset=new f(this,null,i),this.itemset.setRange(this.parent.range),this.view=new n(t,{filter:function(t){return t.group==e}}),this.itemset.setItems(this.view),this.parent.controller.add(this.itemset)}},w.prototype.repaint=function(){return!1},w.prototype.reflow=function(){var t=0,e=M.updateProperty;return t+=e(this,"top",this.itemset?this.itemset.top:0),t+=e(this,"height",this.itemset?this.itemset.height:0),t>0},b.prototype=new p,b.prototype.setOptions=l.prototype.setOptions,b.prototype.setRange=function(){},b.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},b.prototype.getItems=function(){return this.itemsData},b.prototype.setRange=function(t){this.range=t},b.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(M.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({fieldTypes:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var s=this.id;M.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,s)}),e=this.groupsData.getIds(),this._onAdd(e)}},b.prototype.getGroups=function(){return this.groupsData},b.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.frame;if(!o){o=document.createElement("div"),o.className="groupset";var n=s.className;n&&M.addClassName(o,M.option.asString(n)),this.frame=o,t+=1}if(!this.parent)throw Error("Cannot repaint groupset: no parent attached");var r=this.parent.getContainer();if(!r)throw Error("Cannot repaint groupset: parent has no container element");o.parentNode||(r.appendChild(o),t+=1),t+=e(o.style,"height",i(s.height,this.height+"px")),t+=e(o.style,"top",i(s.top,"0px")),t+=e(o.style,"left",i(s.left,"0px")),t+=e(o.style,"width",i(s.width,"100%"));var a=this,h=this.queue,d=this.groups,l=this.groupsData,p=Object.keys(h);if(p.length){p.forEach(function(t){var e=h[t],i=d[t];switch(e){case"add":case"update":if(!i){var s=Object.create(a.options);i=new w(a,t,s),i.setItems(a.itemsData),d[t]=i,a.controller.add(i)}i.data=l.get(t),delete h[t];break;case"remove":i&&(i.setItems(),delete d[t],a.controller.remove(i)),delete h[t];break;default:console.log('Error: unknown action "'+e+'"')}});for(var u=this.groupsData.getIds({order:this.options.groupsOrder}),c=0;u.length>c;c++)(function(t,e){var i=0;e&&(i=function(){return e.top+e.height}),t.setOptions({top:i})})(d[u[c]],d[u[c-1]]);t++}return t>0},b.prototype.getContainer=function(){return this.frame},b.prototype.reflow=function(){var t=0,e=this.options,i=M.updateProperty,s=M.option.asNumber,o=M.option.asSize,n=this.frame;if(n){var r,a=s(e.maxHeight),h=null!=o(e.height);if(h)r=n.offsetHeight;else{r=0;for(var d in this.groups)if(this.groups.hasOwnProperty(d)){var l=this.groups[d];r+=l.height}}null!=a&&(r=Math.min(r,a)),t+=i(this,"height",r),t+=i(this,"top",n.offsetTop),t+=i(this,"left",n.offsetLeft),t+=i(this,"width",n.offsetWidth)}return t>0},b.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},b.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},b.prototype._onUpdate=function(t){this._toQueue(t,"update")},b.prototype._onAdd=function(t){this._toQueue(t,"add")},b.prototype._onRemove=function(t){this._toQueue(t,"remove")},b.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},_.prototype.setOptions=function(t){t&&M.extend(this.options,t),this.controller.reflow(),this.controller.repaint()},_.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({fieldTypes:{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 s=this.getItemRange(),n=s.min,r=s.max;if(null!=n&&null!=r){var a=r.valueOf()-n.valueOf();n=new Date(n.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(n=new Date(this.options.start.valueOf())),void 0!=this.options.end&&(r=new Date(this.options.end.valueOf())),(null!=n||null!=r)&&this.range.setRange(n,r)}},_.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?b:f;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 s=Object.create(this.options);M.extend(s,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.root.height-e.timeaxis.height-e.content.height},height:function(){return e.options.height?e.root.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!M.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null}}),this.content=new i(this.root,[this.timeaxis],s),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)}},_.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var s=t.min("start");e=s?s.start.valueOf():null;var o=t.max("start");o&&(i=o.start.valueOf());var n=t.max("end");n&&(i=null==i?n.end.valueOf():Math.max(i,n.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},x.prototype.draw=function(t,e,i,s){var o,n,r;if(void 0!=s?(o=t,n=e,r=i):void 0!=i?(o=t,n=e,r=void 0,s=i):void 0!=e?(o=t,n=void 0,r=void 0,s=e):void 0!=t&&(o=void 0,n=void 0,r=void 0,s=t),void 0!=s){if(void 0!=s.width&&(this.width=s.width),void 0!=s.height&&(this.height=s.height),void 0!=s.stabilize&&(this.stabilize=s.stabilize),void 0!=s.selectable&&(this.selectable=s.selectable),s.edges){for(var a in s.edges)s.edges.hasOwnProperty(a)&&(this.constants.edges[a]=s.edges[a]);void 0!=s.edges.length&&s.nodes&&void 0==s.nodes.distance&&(this.constants.edges.length=s.edges.length,this.constants.nodes.distance=1.25*s.edges.length),s.edges.fontColor||(this.constants.edges.fontColor=s.edges.color),void 0!=s.edges.dashlength&&(this.constants.edges.dashlength=s.edges.dashlength),void 0!=s.edges.dashgap&&(this.constants.edges.dashgap=s.edges.dashgap),void 0!=s.edges.altdashlength&&(this.constants.edges.altdashlength=s.edges.altdashlength)}if(s.nodes)for(a in s.nodes)s.nodes.hasOwnProperty(a)&&(this.constants.nodes[a]=s.nodes[a]);if(s.packages)for(a in s.packages)s.packages.hasOwnProperty(a)&&(this.constants.packages[a]=s.packages[a]);if(s.groups)for(var h in s.groups)if(s.groups.hasOwnProperty(h)){var d=s.groups[h];this.groups.add(h,d)}}this._setBackgroundColor(s.backgroundColor),this._setSize(this.width,this.height),this._setTranslation(0,0),this._setScale(1),this.hasTimestamps=!1,this.setNodes(o),this.setEdges(n),this.setPackages(r),this._reposition(),this.stabilize&&this._doStabilize(),this.start();var l=this,p=function(){l._redraw()};this.images.setOnloadCallback(p),this.trigger("ready")},x.prototype.trigger=function(t,e){E.trigger(this,t,e),google&&google.visualization&&google.visualization.events&&google.visualization.events.trigger(this,t,e)},x.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="network-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",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,i=function(t){e._onMouseDown(t)},s=function(t){e._onMouseMoveTitle(t)},o=function(t){e._onMouseWheel(t)},n=function(t){e._onTouchStart(t)};x.addEventListener(this.frame.canvas,"mousedown",i),x.addEventListener(this.frame.canvas,"mousemove",s),x.addEventListener(this.frame.canvas,"mousewheel",o),x.addEventListener(this.frame.canvas,"touchstart",n),this.containerElement.appendChild(this.frame)},x.prototype._setBackgroundColor=function(t){var e="white",i="lightgray",s=1;if("string"==typeof t)e=t,i="none",s=0;else if("object"==typeof t)void 0!=t.fill&&(e=t.fill),void 0!=t.stroke&&(i=t.stroke),void 0!=t.strokeWidth&&(s=t.strokeWidth);else if(void 0!=t)throw"Unsupported type of backgroundColor";this.frame.style.boxSizing="border-box",this.frame.style.backgroundColor=e,this.frame.style.borderColor=i,this.frame.style.borderWidth=s+"px",this.frame.style.borderStyle="solid"},x.prototype._onMouseDown=function(t){if(t=t||window.event,this.selectable&&(this.leftButtonDown&&this._onMouseUp(t),this.leftButtonDown=t.which?1==t.which:1==t.button,this.leftButtonDown||this.touchDown)){var e=this;this.onmousemove||(this.onmousemove=function(t){e._onMouseMove(t)},x.addEventListener(document,"mousemove",e.onmousemove)),this.onmouseup||(this.onmouseup=function(t){e._onMouseUp(t)},x.addEventListener(document,"mouseup",e.onmouseup)),x.preventDefault(t),this.startMouseX=t.clientX||t.targetTouches[0].clientX,this.startMouseY=t.clientY||t.targetTouches[0].clientY,this.startFrameLeft=x._getAbsoluteLeft(this.frame.canvas),this.startFrameTop=x._getAbsoluteTop(this.frame.canvas),this.startTranslation=this._getTranslation(),this.ctrlKeyDown=t.ctrlKey,this.shiftKeyDown=t.shiftKey;var i={left:this._xToCanvas(this.startMouseX-this.startFrameLeft),top:this._yToCanvas(this.startMouseY-this.startFrameTop),right:this._xToCanvas(this.startMouseX-this.startFrameLeft),bottom:this._yToCanvas(this.startMouseY-this.startFrameTop)},s=this._getNodesOverlappingWith(i);if(this.startClickedObj=s.length>0?s[s.length-1]:void 0,this.startClickedObj){var o=this.nodes[this.startClickedObj.row];this.startClickedObj.xFixed=o.xFixed,this.startClickedObj.yFixed=o.yFixed,o.xFixed=!0,o.yFixed=!0,this.ctrlKeyDown&&o.isSelected()?this._unselectNodes([this.startClickedObj]):this._selectNodes([this.startClickedObj],this.ctrlKeyDown),this.hasMovingNodes||this._redraw()}else this.shiftKeyDown||(this.moved=!1)}},x.prototype._onMouseMove=function(t){if(t=t||window.event,this.selectable){var e=t.clientX||t.targetTouches&&t.targetTouches[0].clientX||0,i=t.clientY||t.targetTouches&&t.targetTouches[0].clientY||0;if(this.mouseX=e,this.mouseY=i,this.startClickedObj){var s=this.nodes[this.startClickedObj.row];this.startClickedObj.xFixed||(s.x=this._xToCanvas(e-this.startFrameLeft)),this.startClickedObj.yFixed||(s.y=this._yToCanvas(i-this.startFrameTop)),this.hasMovingNodes||(this.hasMovingNodes=!0,this.start())}else if(this.shiftKeyDown){void 0==this.frame.selRect&&(this.frame.selRect=document.createElement("DIV"),this.frame.appendChild(this.frame.selRect),this.frame.selRect.style.position="absolute",this.frame.selRect.style.border="1px dashed red");var o=Math.min(this.startMouseX,e)-this.startFrameLeft,n=Math.min(this.startMouseY,i)-this.startFrameTop,r=Math.max(this.startMouseX,e)-this.startFrameLeft,a=Math.max(this.startMouseY,i)-this.startFrameTop;this.frame.selRect.style.left=o+"px",this.frame.selRect.style.top=n+"px",this.frame.selRect.style.width=r-o+"px",this.frame.selRect.style.height=a-n+"px"}else{var h=e-this.startMouseX,d=i-this.startMouseY;this._setTranslation(this.startTranslation.x+h,this.startTranslation.y+d),this._redraw(),this.moved=!0}x.preventDefault(t)}},x.prototype._onMouseUp=function(t){if(t=t||window.event,this.selectable){this.onmousemove&&(x.removeEventListener(document,"mousemove",this.onmousemove),this.onmousemove=void 0),this.onmouseup&&(x.removeEventListener(document,"mouseup",this.onmouseup),this.onmouseup=void 0),x.preventDefault(t);var e=t.clientX||this.mouseX||0,i=t.clientY||this.mouseY||0,s=t?t.ctrlKey:window.event.ctrlKey;if(this.startClickedObj){var o=this.nodes[this.startClickedObj.row];o.xFixed=this.startClickedObj.xFixed,o.yFixed=this.startClickedObj.yFixed}else if(this.shiftKeyDown){var n={left:this._xToCanvas(Math.min(this.startMouseX,e)-this.startFrameLeft),top:this._yToCanvas(Math.min(this.startMouseY,i)-this.startFrameTop),right:this._xToCanvas(Math.max(this.startMouseX,e)-this.startFrameLeft),bottom:this._yToCanvas(Math.max(this.startMouseY,i)-this.startFrameTop)},r=this._getNodesOverlappingWith(n);this._selectNodes(r,s),this.redraw(),this.frame.selRect&&(this.frame.removeChild(this.frame.selRect),this.frame.selRect=void 0)}else this.ctrlKeyDown||this.moved||(this._unselectNodes(),this._redraw());this.leftButtonDown=!1,this.ctrlKeyDown=!1}},x.prototype._onMouseWheel=function(t){t=t||window.event;var e=t.clientX,i=t.clientY,s=0;if(t.wheelDelta?s=t.wheelDelta/120:t.detail&&(s=-t.detail/3),s){var o=s/10;0>s&&(o/=1-o);var n=this._getScale(),r=n*(1+o);.01>r&&(r=.01),r>10&&(r=10);var a=x._getAbsoluteLeft(this.frame.canvas),h=x._getAbsoluteTop(this.frame.canvas),d=e-a,l=i-h,p=this._getTranslation(),u=r/n,c=(1-u)*d+p.x*u,f=(1-u)*l+p.y*u;this._setScale(r),this._setTranslation(c,f),this._redraw()}x.preventDefault(t)},x.prototype._onMouseMoveTitle=function(t){t=t||window.event;var e=t.clientX,i=t.clientY;this.startFrameLeft=this.startFrameLeft||x._getAbsoluteLeft(this.frame.canvas),this.startFrameTop=this.startFrameTop||x._getAbsoluteTop(this.frame.canvas);var s=e-this.startFrameLeft,o=i-this.startFrameTop;this.popupNode&&this._checkHidePopup(s,o);var n=this,r=function(){n._checkShowPopup(s,o)};this.popupTimer&&clearInterval(this.popupTimer),this.leftButtonDown||(this.popupTimer=setTimeout(r,300))},x.prototype._checkShowPopup=function(t,e){var i,s,o={left:this._xToCanvas(t),top:this._yToCanvas(e),right:this._xToCanvas(t),bottom:this._yToCanvas(e)},n=this.popupNode;if(void 0==this.popupNode)for(i=0,s=this.packages.length;s>i;i++){var r=this.packages[i];if(void 0!=r.getTitle()&&r.isOverlappingWith(o)){this.popupNode=r;break}}if(void 0==this.popupNode){var a=this.nodes;for(i=a.length-1;i>=0;i--){var h=a[i];if(void 0!=h.getTitle()&&h.isOverlappingWith(o)){this.popupNode=h;break}}}if(void 0==this.popupNode){var d=this.edges;for(i=0,s=d.length;s>i;i++){var l=d[i];if(void 0!=l.getTitle()&&l.isOverlappingWith(o)){this.popupNode=l;break}}}if(this.popupNode){if(this.popupNode!=n){var p=this;p.popup||(p.popup=new x.Popup(p.frame)),p.popup.setPosition(t-3,e-3),p.popup.setText(p.popupNode.getTitle()),p.popup.show()}}else this.popup&&this.popup.hide()},x.prototype._checkHidePopup=function(t,e){var i={left:t,top:e,right:t,bottom:e};this.popupNode&&this.popupNode.isOverlappingWith(i)||(this.popupNode=void 0,this.popup&&this.popup.hide())},x.prototype._onTouchStart=function(t){if(x.preventDefault(t),!this.touchDown){this.touchDown=!0;var e=this;this.ontouchmove||(this.ontouchmove=function(t){e._onTouchMove(t)},x.addEventListener(document,"touchmove",this.ontouchmove)),this.ontouchend||(this.ontouchend=function(t){e._onTouchEnd(t)},x.addEventListener(document,"touchend",this.ontouchend)),this._onMouseDown(t)}},x.prototype._onTouchMove=function(t){x.preventDefault(t),this._onMouseMove(t)},x.prototype._onTouchEnd=function(t){x.preventDefault(t),this.touchDown=!1,this.ontouchmove&&(x.removeEventListener(document,"touchmove",this.ontouchmove),this.ontouchmove=void 0),this.ontouchend&&(x.removeEventListener(document,"touchend",this.ontouchend),this.ontouchend=void 0),this._onMouseUp(t)},x.prototype._unselectNodes=function(t,e){var i,s,o,n=!1;if(t)for(i=0,s=t.length;s>i;i++){o=t[i].row,this.nodes[o].unselect();for(var r=0;this.selection.length>r;)this.selection[r].row==o?(this.selection.splice(r,1),n=!0):r++}else if(this.selection&&this.selection.length){for(i=0,s=this.selection.length;s>i;i++)o=this.selection[i].row,this.nodes[o].unselect(),n=!0;this.selection=[]}return!n||1!=e&&void 0!=e||this.trigger("select"),n},x.prototype._selectNodes=function(t,e){var i,s,o=!1,n=!0;if(t.length!=this.selection.length)n=!1;else for(i=0,s=Math.min(t.length,this.selection.length);s>i;i++)if(t[i].row!=this.selection[i].row){n=!1;break}if(n)return o;if(void 0==e||0==e){var r=!1;o=this._unselectNodes(void 0,r)}for(i=0,s=t.length;s>i;i++){for(var a=t[i].row,h=!1,d=0,l=this.selection.length;l>d;d++)if(this.selection[d].row==a){h=!0;break}h||(this.nodes[a].select(),this.selection.push(t[i]),o=!0)}return o&&this.trigger("select"),o},x.prototype._getNodesOverlappingWith=function(t){for(var e=[],i=0;this.nodes.length>i;i++)if(this.nodes[i].isOverlappingWith(t)){var s={row:i};e.push(s)}return e},x.prototype.getSelection=function(){for(var t=[],e=0;this.selection.length>e;e++){var i=this.selection[e].row;t.push({row:i})}return t},x.prototype.setSelection=function(t){var e,i,s;if(void 0==t.length)throw"Selection must be an array with objects";for(e=0,i=this.selection.length;i>e;e++)s=this.selection[e].row,this.nodes[s].unselect();for(this.selection=[],e=0,i=t.length;i>e;e++){if(s=t[e].row,void 0==s)throw"Parameter row missing in selection object";if(s>this.nodes.length-1)throw"Parameter row out of range";var o={row:s};this.selection.push(o),this.nodes[s].select()}this.redraw()},x.prototype._getConnectionCount=function(t){function e(t){for(var e=[],s=0,o=t.length;o>s;s++)for(var n=t[s],r=0,a=i.length;a>r;r++){var h=null;i[r].from==n?h=i[r].to:i[r].to==n&&(h=i[r].from);var d,l;if(h)for(d=0,l=t.length;l>d;d++)if(t[d]==h){h=null;break}if(h)for(d=0,l=e.length;l>d;d++)if(e[d]==h){h=null;break}h&&e.push(h)}return e}var i=this.edges;void 0==t&&(t=1);var s,o,n=[],r=this.nodes;for(s=0,o=r.length;o>s;s++){for(var a=[r[s]],h=0;t>h;h++)a=a.concat(e(a));n.push(a)}var d=[];for(s=0,len=n.length;len>s;s++)d.push(n[s].length);return d},x.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,this.slider&&this.slider.redraw()},x.tableToArray=function(t){var e,i=[],s=t.getNumberOfColumns(),o={};for(e=0;s>e;e++){var n=t.getColumnLabel(e);o[n]=e}for(var r=t.getNumberOfRows(),a=0;r>a;a++){var h={};for(e in o)o.hasOwnProperty(e)&&(h[e]=t.getValue(a,o[e]));i.push(h)}return i},x.prototype.addNodes=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}for(var i=!1,s=e.length,o=0;s>o;o++){var n=e[o];if(void 0!=n.value&&(i=!0),void 0==n.id)throw"Column 'id' missing in table with nodes (row "+o+")";this._createNode(n)}i&&this._updateValueRange(this.nodes),this.start()},x.prototype.setNodes=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}this.hasMovingNodes=!1,this.nodesTable=e,this.nodes=[],this.selection=[];for(var i=!1,s=e.length,o=0;s>o;o++){var n=e[o];if(void 0!=n.value&&(i=!0),n.timestamp&&(this.hasTimestamps=this.hasTimestamps||n.timestamp),void 0==n.id)throw"Column 'id' missing in table with nodes (row "+o+")";this._createNode(n)}i&&this._updateValueRange(this.nodes)},x.prototype._filterNodes=function(t){if(void 0!=this.nodesTable){if(void 0!==t)for(var e=this.nodes,i=0;e.length>i;){var s=e[i].timestamp;void 0!==s&&s>t?e.splice(i,1):i++}for(var o=this.nodesTable,n=o.length,r=0;n>r;r++){var a=o[r];if(void 0===a.id)throw"Column 'id' missing in table with nodes (row "+r+")";var h=a.timestamp?a.timestamp:void 0,d=!0;void 0!==h&&void 0!==t&&h>t&&(d=!1),d&&this._createNode(a)}this.start()}},x.prototype._createNode=function(t){var e,i,s,o,n=t.action?t.action:"update";if("create"===n)s=new x.Node(t,this.images,this.groups,this.constants),e=t.id,i=void 0!==e?this._findNode(e):void 0,void 0!==i?(o=this.nodes[i],this.nodes[i]=s,o.selected&&this._unselectNodes([{row:i}],!1)):this.nodes.push(s),s.isFixed()||(this.hasMovingNodes=!0);else if("update"===n){if(e=t.id,void 0===e)throw"Cannot update a node without id";i=this._findNode(e),void 0!==i?this.nodes[i].setProperties(t,this.constants):(s=new x.Node(t,this.images,this.groups,this.constants),this.nodes.push(s),s.isFixed()||(this.hasMovingNodes=!0))}else{if("delete"!==n)throw"Unknown action "+n+". Choose 'create', 'update', or 'delete'.";if(e=t.id,void 0===e)throw"Cannot delete node without its id";if(i=this._findNode(e),void 0===i)throw"Node with id "+e+" not found";o=this.nodes[i],o.selected&&this._unselectNodes([{row:i}],!1),this.nodes.splice(i,1)}},x.prototype._findNode=function(t){for(var e=this.nodes,i=0,s=e.length;s>i;i++)if(e[i].id===t)return i;return void 0},x.prototype._findNodeByRow=function(t){return this.nodes[t]},x.prototype.setEdges=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}this.edgesTable=e,this.edges=[],this.hasMovingEdges=!1;for(var i=!1,s=e.length,o=0;s>o;o++){var n=e[o];if(void 0===n.from)throw"Column 'from' missing in table with edges (row "+o+")";if(void 0===n.to)throw"Column 'to' missing in table with edges (row "+o+")";void 0!=n.timestamp&&(this.hasTimestamps=this.hasTimestamps||n.timestamp),void 0!=n.value&&(i=!0),this._createEdge(n)}i&&this._updateValueRange(this.edges)},x.prototype.addEdges=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}for(var i=!1,s=e.length,o=0;s>o;o++){var n=e[o];if(void 0===n.from)throw"Column 'from' missing in table with edges (row "+o+")";if(void 0===n.to)throw"Column 'to' missing in table with edges (row "+o+")";void 0!=n.value&&(i=!0),this._createEdge(n)}i&&this._updateValueRange(this.edges),this.start()},x.prototype._filterEdges=function(t){if(void 0!=this.edgesTable){if(void 0!==t)for(var e=this.edges,i=0;e.length>i;){var s=e[i].timestamp;void 0!==s&&s>t?e.splice(i,1):i++}for(var o=this.edgesTable,n=o.length,r=0;n>r;r++){var a=o[r];if(void 0===a.from)throw"Column 'from' missing in table with edges (row "+r+")";if(void 0===a.to)throw"Column 'to' missing in table with edges (row "+r+")";var h=a.timestamp?a.timestamp:void 0,d=!0;void 0!==h&&void 0!==t&&h>t&&(d=!1),d&&this._createEdge(a)}this.start()}},x.prototype._createEdge=function(t){var e,i,s,o,n=t.action?t.action:"create";if("create"===n)e=t.id,i=void 0!==e?this._findEdge(e):void 0,s=new x.Edge(t,this,this.constants),void 0!==i?(o=this.edges[i],o.from.detachEdge(o),o.to.detachEdge(o),this.edges[i]=s):this.edges.push(s),s.from.attachEdge(s),s.to.attachEdge(s),s.isMoving()&&(this.hasMovingEdges=!0);else if("update"===n){if(e=t.id,void 0===e)throw"Cannot update a edge without id";i=this._findEdge(e),void 0!==i?(s=this.edges[i],s.from.detachEdge(s),s.to.detachEdge(s),s.setProperties(t,this.constants),s.from.attachEdge(s),s.to.attachEdge(s)):(s=new x.Edge(t,this,this.constants),s.from.attachEdge(s),s.to.attachEdge(s),this.edges.push(s),s.isMoving()&&(this.hasMovingEdges=!0))}else{if("delete"!==n)throw"Unknown action "+n+". Choose 'create', 'update', or 'delete'.";if(e=t.id,void 0===e)throw"Cannot delete edge without its id";if(i=this._findEdge(e),void 0===i)throw"Edge with id "+e+" not found";o=this.edges[e],s.from.detachEdge(o),s.to.detachEdge(o),this.edges.splice(i,1)}},x.prototype._updateNodeReferences=function(t,e){for(var i=[this.edges,this.packages],s=0,o=i.length;o>s;s++)for(var n=i[s],r=0,a=n.length;a>r;r++)n.from===t&&(n.from=e),n.to===t&&(n.to=e)},x.prototype._findEdge=function(t){for(var e=this.edges,i=0,s=e.length;s>i;i++)if(e[i].id===t)return i;return void 0},x.prototype._findEdgeByRow=function(t){return this.edges[t]},x.prototype.addPackages=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}for(var i=e.length,s=0;i>s;s++){var o=e[s];if(void 0===o.from)throw"Column 'from' missing in table with packages (row "+s+")";if(void 0===o.to)throw"Column 'to' missing in table with packages (row "+s+")";this._createPackage(o)}this._updateValueRange(this.packages),this.start()},x.prototype.setPackages=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}this.packagesTable=e,this.packages=[];for(var i=e.length,s=0;i>s;s++){var o=e[s];if(void 0===o.from)throw"Column 'from' missing in table with packages (row "+s+")";if(void 0===o.to)throw"Column 'to' missing in table with packages (row "+s+")";o.timestamp&&(this.hasTimestamps=this.hasTimestamps||o.timestamp),this._createPackage(o)}this._updateValueRange(this.packages)},x.prototype._filterPackages=function(t){if(void 0!=this.packagesTable){this.packages=[];for(var e=this.packagesTable,i=e.length,s=0;i>s;s++){var o=e[s];if(void 0===o.from)throw"Column 'from' missing in table with packages (row "+s+")";if(void 0===o.to)throw"Column 'to' missing in table with packages (row "+s+")";var n=o.timestamp?o.timestamp:void 0,r=!0;if(void 0!==n&&void 0!==t&&n>t&&(r=!1),r===!0&&void 0==o.progress){var a=o.duration||this.constants.packages.duration,h=(t.getTime()-n.getTime())/1e3;if(a>h){var d=o;o={};for(var l in d)d.hasOwnProperty(l)&&(o[l]=d[l]);o.progress=h/a}else r=!1}r===!0&&this._createPackage(o)}this.start()}},x.prototype._createPackage=function(t){var e,i,s,o=t.action?t.action:"create";if("create"===o)e=t.id,i=void 0!==e?this._findPackage(e):void 0,s=new x.Package(t,this,this.images,this.constants),void 0!==i?this.packages[i]=s:this.packages.push(s),s.isMoving()&&(this.hasMovingPackages=!0);else if("update"===o){if(e=t.id,void 0===e)throw"Cannot update a edge without id";i=this._findPackage(e),void 0!==i?this.packages[i].setProperties(t,this.constants):(s=new x.Package(t,this,this.images,this.constants),this.packages.push(s),s.isMoving()&&(this.hasMovingPackages=!0))}else{if("delete"!==o)throw"Unknown action "+o+". Choose 'create', 'update', or 'delete'.";if(e=t.id,void 0===e)throw"Cannot delete package without its id";if(i=this._findPackage(e),void 0===i)throw"Package with id "+e+" not found";this.packages.splice(i,1)}},x.prototype._findPackage=function(t){for(var e=this.packages,i=0,s=e.length;s>i;i++)if(e[i].id===t)return i;return void 0},x.prototype._findPackageByRow=function(t){return this.packages[t]},x.prototype._getColumnNames=function(t){for(var e=t.getNumberOfColumns(),i={},s=0;e>s;s++){var o=t.getColumnLabel(s);i[o]=s}return i},x.prototype._updateValueRange=function(t){var e,i=t.length,s=void 0,o=void 0;for(e=0;i>e;e++){var n=t[e].getValue();void 0!==n&&(s=void 0===s?n:Math.min(n,s),o=void 0===o?n:Math.max(n,o))}if(void 0!==s&&void 0!==o)for(e=0;i>e;e++)t[e].setValueRange(s,o)},x.prototype.setTimestamp=function(t){this._filterNodes(t),this._filterEdges(t),this._filterPackages(t)},x.prototype._getRange=function(){for(var t={start:void 0,end:void 0},e=[this.nodesTable,this.edgesTable],i=0,s=e.length;s>i;i++){var o=e[i];if(void 0!==o)for(var n=0,r=o.length;r>n;n++){var a=o[n].timestamp;a&&(a instanceof Date&&(a=a.getTime()),t.start=t.start?Math.min(a,t.start):a,t.end=t.end?Math.max(a,t.end):a)}}if(this.packagesTable)for(var h=this.packagesTable,d=0,l=h.length;l>d;d++){var p=h[d],a=p.timestamp,u=p.progress,c=p.duration||this.constants.packages.duration;if(a instanceof Date&&(a=a.getTime()),void 0!=a){var f=a,m=u?a:a+1e3*c;t.start=t.start?Math.min(f,t.start):f,t.end=t.end?Math.max(m,t.end):m}}var g={start:new Date(t.start),end:new Date(t.end)};return g},x.prototype.animationStart=function(){this.slider&&this.slider.play()},x.prototype.animationStop=function(){this.slider&&this.slider.stop()},x.prototype.setAnimationFramerate=function(t){this.slider&&this.slider.setFramerate(t)},x.prototype.setAnimationDuration=function(t){this.slider&&this.slider.setDuration(t)},x.prototype.setAnimationAcceleration=function(t){this.slider&&this.slider.setAcceleration(t)},x.prototype.redraw=function(){this._setSize(this.width,this.height),this._redraw()},x.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this._drawEdges(t),this._drawNodes(t),this._drawPackages(t),this._drawSlider(),t.restore()},x.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)},x.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},x.prototype._setScale=function(t){this.scale=t},x.prototype._getScale=function(){return this.scale},x.prototype._xToCanvas=function(t){return(t-this.translation.x)/this.scale},x.prototype._canvasToX=function(t){return t*this.scale+this.translation.x},x.prototype._yToCanvas=function(t){return(t-this.translation.y)/this.scale},x.prototype._canvasToY=function(t){return t*this.scale+this.translation.y},x.prototype._getNode=function(t){for(var e=0;this.nodes.length>e;e++)if(this.nodes[e].id==t)return this.nodes[e];return null},x.prototype._drawNodes=function(t){for(var e=this.nodes,i=[],s=0,o=e.length;o>s;s++)e[s].isSelected()?i.push(s):e[s].draw(t);for(var n=0,r=i.length;r>n;n++)e[i[n]].draw(t)},x.prototype._drawEdges=function(t){for(var e=this.edges,i=0,s=e.length;s>i;i++)e[i].draw(t)},x.prototype._drawPackages=function(t){for(var e=this.packages,i=0,s=e.length;s>i;i++)e[i].draw(t)},x.prototype._drawSlider=function(){var t;if(this.hasTimestamps){if(t=this.frame.slider,void 0===t){t=document.createElement("div"),t.style.position="absolute",t.style.bottom="0px",t.style.left="0px",t.style.right="0px",t.style.backgroundColor="rgba(255, 255, 255, 0.7)",this.frame.slider=t,this.frame.slider.style.padding="10px",this.frame.appendChild(t);var e=this._getRange();this.slider=new x.Slider(t),this.slider.setLoop(!1),this.slider.setRange(e.start,e.end);var i=this,s=function(){var t=i.slider.getValue();i.setTimestamp(t),i.redraw()};this.slider.setOnChangeCallback(s),s()}}else t=this.frame.slider,void 0!==t&&(this.frame.removeChild(t),this.frame.slider=void 0,this.slider=void 0)},x.prototype._reposition=function(){for(var t=2*this.constants.edges.length,e=this.frame.canvas.clientWidth/2,i=this.frame.canvas.clientHeight/2,s=0;this.nodes.length>s;s++){var o=2*Math.PI*(s/this.nodes.length);this.nodes[s].xFixed||(this.nodes[s].x=e+t*Math.cos(o)),this.nodes[s].yFixed||(this.nodes[s].y=i+t*Math.sin(o))}},x.prototype._doStabilize=function(){new Date;for(var t=0,e=this.constants.minVelocity,i=!1;!i&&this.constants.maxIterations>t;)this._calculateForces(),this._discreteStepNodes(),i=!this.isMoving(e),t++;new Date},x.prototype._calculateForces=function(){for(var t=this.nodes,e=this.edges,i=.01,s=this.frame.canvas.clientWidth/2,o=this.frame.canvas.clientHeight/2,n=0;t.length>n;n++){var r=s-t[n].x,a=o-t[n].y,h=Math.atan2(a,r),d=Math.cos(h)*i,l=Math.sin(h)*i;this.nodes[n]._setForce(d,l)}for(var p=this.constants.nodes.distance,u=10,n=0;t.length>n;n++)for(var c=n+1;this.nodes.length>c;c++){var r=t[c].x-t[n].x,a=t[c].y-t[n].y,f=Math.sqrt(r*r+a*a),h=Math.atan2(a,r),m=1/(1+Math.exp((f/p-1)*u)),d=Math.cos(h)*m,l=Math.sin(h)*m;this.nodes[n]._addForce(-d,-l),this.nodes[c]._addForce(d,l)}for(var g=0,v=e.length;v>g;g++){var y=e[g],r=y.to.x-y.from.x,a=y.to.y-y.from.y,w=y.length,b=Math.sqrt(r*r+a*a),h=Math.atan2(a,r),_=y.stiffness*(w-b),d=Math.cos(h)*_,l=Math.sin(h)*_;y.from._addForce(-d,-l),y.to._addForce(d,l)}},x.prototype.isMoving=function(t){for(var e=this.nodes,i=0,s=e.length;s>i;i++)if(e[i].isMoving(t))return!0;return!1},x.prototype._discreteStepNodes=function(){for(var t=this.refreshRate/1e3,e=this.nodes,i=0,s=e.length;s>i;i++)e[i].discreteStep(t) +},x.prototype._discreteStepPackages=function(){for(var t=this.refreshRate/1e3,e=this.packages,i=0,s=e.length;s>i;i++)e[i].discreteStep(t)},x.prototype._deleteFinishedPackages=function(){for(var t=0,e=!1;this.packages.length>t;)this.packages[t].isFinished()?(this.packages.splice(t,1),t--):this.packages[t].isMoving()&&(e=!0),t++;this.hasMovingPackages=e},x.prototype.start=function(){if(this.hasMovingNodes){this._calculateForces(),this._discreteStepNodes();var t=this.constants.minVelocity;this.hasMovingNodes=this.isMoving(t)}if(this.hasMovingPackages&&(this._discreteStepPackages(),this._deleteFinishedPackages()),this.hasMovingNodes||this.hasMovingEdges||this.hasMovingPackages){if(!this.timer){var e=this;this.timer=window.setTimeout(function(){e.timer=void 0,e.start(),e._redraw()},this.refreshRate)}}else this._redraw()},x.prototype.stop=function(){this.timer&&(window.clearInterval(this.timer),this.timer=void 0)},x.addEventListener=function(t,e,i,s){t.addEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,s)):t.attachEvent("on"+e,i)},x.removeEventListener=function(t,e,i,s){t.removeEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,s)):t.detachEvent("on"+e,i)},x.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},x.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},x._getAbsoluteLeft=function(t){for(var e=0;null!=t;)e+=t.offsetLeft,e-=t.scrollLeft,t=t.offsetParent;return!document.body.scrollLeft&&window.pageXOffset&&(e-=window.pageXOffset),e},x._getAbsoluteTop=function(t){for(var e=0;null!=t;)e+=t.offsetTop,e-=t.scrollTop,t=t.offsetParent;return!document.body.scrollTop&&window.pageYOffset&&(e-=window.pageYOffset),e},x.Node=function(t,e,i,s){this.selected=!1,this.edges=[],this.group=s.nodes.group,this.fontSize=s.nodes.fontSize,this.fontFace=s.nodes.fontFace,this.fontColor=s.nodes.fontColor,this.borderColor=s.nodes.borderColor,this.backgroundColor=s.nodes.backgroundColor,this.highlightColor=s.nodes.highlightColor,this.id=void 0,this.style=s.nodes.style,this.image=s.nodes.image,this.x=0,this.y=0,this.xFixed=!1,this.yFixed=!1,this.radius=s.nodes.radius,this.radiusFixed=!1,this.radiusMin=s.nodes.radiusMin,this.radiusMax=s.nodes.radiusMax,this.imagelist=e,this.grouplist=i,this.setProperties(t,s),this.mass=50,this.fx=0,this.fy=0,this.vx=0,this.vy=0,this.minForce=s.minForce,this.damping=.9},x.Node.prototype.attachEdge=function(t){this.edges.push(t),this._updateMass()},x.Node.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&this.edges.splice(e,1),this._updateMass()},x.Node.prototype._updateMass=function(){this.mass=50+20*this.edges.length},x.Node.prototype.setProperties=function(t,e){if(t){if(void 0!=t.id&&(this.id=t.id),void 0!=t.text&&(this.text=t.text),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.timestamp&&(this.timestamp=t.timestamp),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var s in i)i.hasOwnProperty(s)&&(this[s]=i[s])}if(void 0!=t.style&&(this.style=t.style),void 0!=t.image&&(this.image=t.image),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.borderColor&&(this.borderColor=t.borderColor),void 0!=t.backgroundColor&&(this.backgroundColor=t.backgroundColor),void 0!=t.highlightColor&&(this.highlightColor=t.highlightColor),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)}this.xFixed=this.xFixed||void 0!=t.x,this.yFixed=this.yFixed||void 0!=t.y,this.radiusFixed=this.radiusFixed||void 0!=t.radius,"image"==this.style&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax);var o=this.style;switch(o){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"rect":this.draw=this._drawRect,this.resize=this._resizeRect;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;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._drawRect,this.resize=this._resizeRect}this._reset()}},x.Node.prototype.select=function(){this.selected=!0,this._reset()},x.Node.prototype.unselect=function(){this.selected=!1,this._reset()},x.Node.prototype._reset=function(){this.width=void 0,this.height=void 0},x.Node.prototype.getTitle=function(){return this.title},x.Node.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.style){case"circle":case"dot":return this.radius+i;case"rect":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}},x.Node.prototype._setForce=function(t,e){this.fx=t,this.fy=e},x.Node.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},x.Node.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 s=-this.damping*this.vy,o=(this.fy+s)/this.mass;this.vy+=o/t,this.y+=this.vy/t}},x.Node.prototype.isFixed=function(){return this.xFixed&&this.yFixed},x.Node.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t||!this.xFixed&&Math.abs(this.fx)>this.minForce||!this.yFixed&&Math.abs(this.fy)>this.minForce},x.Node.prototype.isSelected=function(){return this.selected},x.Node.prototype.getValue=function(){return this.value},x.Node.prototype.getDistance=function(t,e){var i=this.x-t,s=this.y-e;return Math.sqrt(i*i+s*s)},x.Node.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value){var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},x.Node.prototype.draw=function(){throw"Draw method not initialized for node"},x.Node.prototype.resize=function(){throw"Resize method not initialized for node"},x.Node.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},x.Node.prototype._resizeImage=function(){if(!this.width){var t,e;if(this.value){var i=this.imageObj.height/this.imageObj.width;t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e}},x.Node.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;this.imageObj?(t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2):e=this.y,this._text(t,this.text,this.x,e,void 0,"top")},x.Node.prototype._resizeRect=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},x.Node.prototype._drawRect=function(t){this._resizeRect(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.borderColor,t.fillStyle=this.selected?this.highlightColor:this.backgroundColor,t.lineWidth=this.selected?2:1,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._text(t,this.text,this.x,this.y)},x.Node.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=i.width+2*e;this.width=s,this.height=s}},x.Node.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.borderColor,t.fillStyle=this.selected?this.highlightColor:this.backgroundColor,t.lineWidth=this.selected?2:1,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._text(t,this.text,this.x,this.y)},x.Node.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=Math.max(i.width,i.height)+2*e;this.radius=s/2,this.width=s,this.height=s}},x.Node.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.borderColor,t.fillStyle=this.selected?this.highlightColor:this.backgroundColor,t.lineWidth=this.selected?2:1,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._text(t,this.text,this.x,this.y)},x.Node.prototype._drawDot=function(t){this._drawShape(t,"circle")},x.Node.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},x.Node.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},x.Node.prototype._drawSquare=function(t){this._drawShape(t,"square")},x.Node.prototype._drawStar=function(t){this._drawShape(t,"star")},x.Node.prototype._resizeShape=function(){if(!this.width){var t=2*this.radius;this.width=t,this.height=t}},x.Node.prototype._drawShape=function(t,e){this._resizeShape(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.borderColor,t.fillStyle=this.selected?this.highlightColor:this.backgroundColor,t.lineWidth=this.selected?2:1,t[e](this.x,this.y,this.radius),t.fill(),t.stroke(),this.text&&this._text(t,this.text,this.x,this.y+this.height/2,void 0,"top")},x.Node.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}},x.Node.prototype._drawText=function(t){this._resizeText(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,this._text(t,this.text,this.x,this.y)},x.Node.prototype._text=function(t,e,i,s,o,n){if(e){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=o||"center",t.textBaseline=n||"middle";for(var r=e.split("\n"),a=r.length,h=this.fontSize+4,d=s+(1-a)/2*h,l=0;a>l;l++)t.fillText(r[l],i,d),d+=h}},x.Node.prototype.getTextSize=function(t){if(void 0!=this.text){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.text.split("\n"),i=(this.fontSize+4)*e.length,s=0,o=0,n=e.length;n>o;o++)s=Math.max(s,t.measureText(e[o]).width);return{width:s,height:i}}return{width:0,height:0}},x.Edge=function(t,e,i){if(!e)throw"No graph provided";this.graph=e,this.widthMin=i.edges.widthMin,this.widthMax=i.edges.widthMax,this.id=void 0,this.style=i.edges.style,this.title=void 0,this.width=i.edges.width,this.value=void 0,this.length=i.edges.length,this.dashlength=i.edges.dashlength,this.dashgap=i.edges.dashgap,this.altdashlength=i.edges.altdashlength,this.stiffness=void 0,this.color=i.edges.color,this.timestamp=void 0,this.widthFixed=!1,this.lengthFixed=!1,this.setProperties(t,i)},x.Edge.prototype.setProperties=function(t,e){if(t){if(void 0!=t.from&&(this.from=this.graph._getNode(t.from)),void 0!=t.to&&(this.to=this.graph._getNode(t.to)),void 0!=t.id&&(this.id=t.id),void 0!=t.style&&(this.style=t.style),void 0!=t.text&&(this.text=t.text),this.text&&(this.fontSize=e.edges.fontSize,this.fontFace=e.edges.fontFace,this.fontColor=e.edges.fontColor,void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace)),void 0!=t.title&&(this.title=t.title),void 0!=t.width&&(this.width=t.width),void 0!=t.value&&(this.value=t.value),void 0!=t.length&&(this.length=t.length),void 0!=t.dashlength&&(this.dashlength=t.dashlength),void 0!=t.dashgap&&(this.dashgap=t.dashgap),void 0!=t.altdashlength&&(this.altdashlength=t.altdashlength),void 0!=t.color&&(this.color=t.color),void 0!=t.timestamp&&(this.timestamp=t.timestamp),!this.from)throw"Node with id "+t.from+" not found";if(!this.to)throw"Node with id "+t.to+" not found";if(this.widthFixed=this.widthFixed||void 0!=t.width,this.lengthFixed=this.lengthFixed||void 0!=t.length,this.stiffness=1/this.length,"arrow"===this.style)this.arrows=[.5],this.animation=!1;else if("arrow-end"===this.style)this.animation=!1;else if("moving-arrows"===this.style){this.arrows=[];for(var i=3,s=0;i>s;s++)this.arrows.push(s/i);this.animation=!0}else"moving-dot"===this.style?(this.dot=0,this.animation=!0):this.animation=!1;switch(this.style){case"line":this.draw=this._drawLine;break;case"arrow":this.draw=this._drawArrow;break;case"arrow-end":this.draw=this._drawArrowEnd;break;case"moving-arrows":this.draw=this._drawMovingArrows;break;case"moving-dot":this.draw=this._drawMovingDot;break;case"dash-line":this.draw=this._drawDashLine;break;default:this.draw=this._drawLine}}},x.Edge.prototype.isMoving=function(){return this.animation},x.Edge.prototype.getTitle=function(){return this.title},x.Edge.prototype.getValue=function(){return this.value},x.Edge.prototype.setValueRange=function(t,e){if(!this.widthFixed&&void 0!==this.value){var i=(this.widthMax-this.widthMin)/(e-t);this.width=(this.value-t)*i+this.widthMin}},x.Edge.prototype.isLengthFixed=function(){return this.lengthFixed},x.Edge.prototype.getLength=function(){return this.length},x.Edge.prototype.setLength=function(t){this.lengthFixed||(this.length=t)},x.Edge.prototype.getDashLength=function(){return this.dashlength},x.Edge.prototype.setDashLength=function(t){this.dashlength=t},x.Edge.prototype.getDashGap=function(){return this.dashgap},x.Edge.prototype.setDashGap=function(t){this.dashgap=t},x.Edge.prototype.getAltDashLength=function(){return this.altdashlength},x.Edge.prototype.setAltDashLength=function(t){this.altdashlength=t},x.Edge.prototype.draw=function(){throw"Method draw not initialized in edge"},x.Edge.prototype.isOverlappingWith=function(t){var e=10,i=this.from.x,s=this.from.y,o=this.to.x,n=this.to.y,r=t.left,a=t.top,h=x._dist(i,s,o,n,r,a);return e>h},x._dist=function(t,e,i,s,o,n){var r=i-t,a=s-e,h=r*r+a*a,d=((o-t)*r+(n-e)*a)/h;d>1?d=1:0>d&&(d=0);var l=t+d*r,p=e+d*a,u=l-o,c=p-n;return Math.sqrt(u*u+c*c)},x.Edge.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.text&&(e=this._pointOnLine(.5),this._text(t,this.text,e.x,e.y));else{var i,s,o=this.length/2/Math.PI,n=this.from;n.width||n.resize(t),n.width>n.height?(i=n.x+n.width/2,s=n.y-o):(i=n.x+o,s=n.y-n.height/2),this._circle(t,i,s,o),e=this._pointOnCircle(i,s,o,.5),this._text(t,this.text,e.x,e.y)}},x.Edge.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax):this.width},x.Edge.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},x.Edge.prototype._circle=function(t,e,i,s){t.beginPath(),t.arc(e,i,s,0,2*Math.PI,!1),t.stroke()},x.Edge.prototype._text=function(t,e,i,s){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var o=t.measureText(this.text).width,n=this.fontSize,r=i-o/2,a=s-n/2;t.fillRect(r,a,o,n),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(this.text,r,a)}};var D="undefined"!=typeof window&&window.CanvasRenderingContext2D&&CanvasRenderingContext2D.prototype;D&&D.lineTo&&(D.dashedLine=function(t,e,i,s,o){o||(o=[10,5]),0==u&&(u=.001);var n=o.length;this.moveTo(t,e);for(var r=i-t,a=s-e,h=a/r,d=Math.sqrt(r*r+a*a),l=0,p=!0;d>=.1;){var u=o[l++%n];u>d&&(u=d);var c=Math.sqrt(u*u/(1+h*h));0>r&&(c=-c),t+=c,e+=h*c,this[p?"lineTo":"moveTo"](t,e),d-=u,p=!p}}),x.Edge.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!=this.altdashlength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap,this.altdashlength,this.dashgap]):void 0!=this.dashlength&&void 0!=this.dashgap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.text){var e=this._pointOnLine(.5);this._text(t,this.text,e.x,e.y)}},x.Edge.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},x.Edge.prototype._pointOnCircle=function(t,e,i,s){var o=2*(s-3/8)*Math.PI;return{x:t+i*Math.cos(o),y:e-i*Math.sin(o)}},x.Edge.prototype._drawMovingArrows=function(t){this._drawArrow(t);for(var e in this.arrows)this.arrows.hasOwnProperty(e)&&(this.arrows[e]+=.02,this.arrows[e]>1&&(this.arrows[e]=0))},x.Edge.prototype._drawMovingDot=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to){this._line(t);var i=4+2*this.width;e=this._pointOnLine(this.dot),t.circle(e.x,e.y,i),t.fill(),this.dot+=.05,this.dot>1&&(this.dot=0),this.text&&(e=this._pointOnLine(.5),this._text(t,this.text,e.x,e.y))}},x.Edge.prototype._drawArrow=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),s=10+5*this.width;for(var o in this.arrows)this.arrows.hasOwnProperty(o)&&(e=this._pointOnLine(this.arrows[o]),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke());this.text&&(e=this._pointOnLine(.5),this._text(t,this.text,e.x,e.y))}else{var n,r,a=this.length/2/Math.PI,h=this.from;h.width||h.resize(t),h.width>h.height?(n=h.x+h.width/2,r=h.y-a):(n=h.x+a,r=h.y-h.height/2),this._circle(t,n,r,a);var i=.2*Math.PI,s=10+5*this.width;for(var o in this.arrows)this.arrows.hasOwnProperty(o)&&(e=this._pointOnCircle(n,r,a,this.arrows[o]),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke());this.text&&(e=this._pointOnCircle(n,r,a,.5),this._text(t,this.text,e.x,e.y))}},x.Edge.prototype._drawArrowEnd=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 s=this.to.x-this.from.x,o=this.to.y-this.from.y,n=Math.sqrt(s*s+o*o),r=this.to.distanceToBorder(t,e+Math.PI),a=(n-r)/n,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,l=this.to.distanceToBorder(t,e),p=(n-l)/n,u=(1-p)*this.from.x+p*this.to.x,c=(1-p)*this.from.y+p*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(u,c),t.stroke(),i=10+5*this.width,t.arrow(u,c,e,i),t.fill(),t.stroke(),this.text){var f=this._pointOnLine(.5);this._text(t,this.text,f.x,f.y)}}else{var m,g,v,y=this.length/2/Math.PI,w=this.from;w.width||w.resize(t),w.width>w.height?(m=w.x+w.width/2,g=w.y-y,v={x:m,y:w.y,angle:.9*Math.PI}):(m=w.x+y,g=w.y-w.height/2,v={x:w.x,y:g,angle:.6*Math.PI}),t.beginPath(),t.arc(m,g,y,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.text&&(f=this._pointOnCircle(m,g,y,.5),this._text(t,this.text,f.x,f.y))}},x.Images=function(){this.images={},this.callback=void 0},x.Images.prototype.setOnloadCallback=function(t){this.callback=t},x.Images.prototype.load=function(t){var e=this.images[t];if(void 0==e){var i=this;e=new Image,this.images[t]=e,e.onload=function(){i.callback&&i.callback(this)},e.src=t}return e},x.Package=function(t,e,i,s){if(void 0==e)throw"No graph provided";this.radiusMin=s.packages.radiusMin,this.radiusMax=s.packages.radiusMax,this.imagelist=i,this.graph=e,this.id=void 0,this.from=void 0,this.to=void 0,this.title=void 0,this.style=s.packages.style,this.radius=s.packages.radius,this.color=s.packages.color,this.image=s.packages.image,this.value=void 0,this.progress=0,this.timestamp=void 0,this.duration=s.packages.duration,this.autoProgress=!0,this.radiusFixed=!1,this.setProperties(t,s)},x.Package.DEFAULT_DURATION=1,x.Package.prototype.setProperties=function(t,e){if(t){if(void 0!=t.from&&(this.from=this.graph._getNode(t.from)),void 0!=t.to&&(this.to=this.graph._getNode(t.to)),!this.from)throw"Node with id "+t.from+" not found";if(!this.to)throw"Node with id "+t.to+" not found";if(void 0!=t.id&&(this.id=t.id),void 0!=t.title&&(this.title=t.title),void 0!=t.style&&(this.style=t.style),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.value&&(this.value=t.value),void 0!=t.image&&(this.image=t.image),void 0!=t.color&&(this.color=t.color),void 0!=t.dashlength&&(this.dashlength=t.dashlength),void 0!=t.dashgap&&(this.dashgap=t.dashgap),void 0!=t.altdashlength&&(this.altdashlength=t.altdashlength),void 0!=t.progress&&(this.progress=t.progress),void 0!=t.timestamp&&(this.timestamp=t.timestamp),void 0!=t.duration&&(this.duration=t.duration),this.radiusFixed=this.radiusFixed||void 0!=t.radius,this.autoProgress=1==this.autoProgress?void 0==t.progress:!1,"image"==this.style&&(this.radiusMin=e.packages.widthMin,this.radiusMax=e.packages.widthMax),0>this.progress&&(this.progress=0),this.progress>1&&(this.progress=1),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.style){case"dot":this.draw=this._drawDot;break;case"square":this.draw=this._drawSquare;break;case"triangle":this.draw=this._drawTriangle;break;case"triangleDown":this.draw=this._drawTriangleDown;break;case"star":this.draw=this._drawStar;break;case"image":this.draw=this._drawImage;break;default:this.draw=this._drawDot}}},x.Package.prototype.setProgress=function(t){this.progress=t,this.autoProgress=!1},x.Package.prototype.isFinished=function(){return 1==this.autoProgress&&this.progress>=1},x.Package.prototype.isMoving=function(){return this.autoProgress||this.isFinished()},x.Package.prototype.discreteStep=function(t){1==this.autoProgress&&(this.progress+=parseFloat(t)/this.duration,this.progress>1&&(this.progress=1))},x.Package.prototype.draw=function(){throw"Draw method not initialized for package"},x.Package.prototype.isOverlappingWith=function(t){var e=Math.max(this.radius,10),i=this._getPosition();return i.x-et.left&&i.y-et.top},x.Package.prototype._getPosition=function(){return{x:(1-this.progress)*this.from.x+this.progress*this.to.x,y:(1-this.progress)*this.from.y+this.progress*this.to.y}},x.Package.prototype.getTitle=function(){return this.title},x.Package.prototype.getValue=function(){return this.value},x.Package.prototype.getDistance=function(t,e){var i=this._getPosition(),s=i.x-t,o=i.y-e;return Math.sqrt(s*s+o*o)},x.Package.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value){var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},x.Package.prototype._drawDot=function(t){this._drawShape(t,"circle")},x.Package.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},x.Package.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},x.Package.prototype._drawSquare=function(t){this._drawShape(t,"square")},x.Package.prototype._drawStar=function(t){this._drawShape(t,"star")},x.Package.prototype._drawShape=function(t,e){t.fillStyle=this.color;var i=this._getPosition();t[e](i.x,i.y,this.radius),t.fill()},x.Package.prototype._drawImage=function(t){if(this.imageObj){var e,i;if(this.value){var s=this.imageObj.height/this.imageObj.width;e=this.radius||this.imageObj.width,i=this.radius*s||this.imageObj.height}else e=this.imageObj.width,i=this.imageObj.height;var o=this._getPosition();t.drawImage(this.imageObj,o.x-e/2,o.y-i/2,e,i)}else console.log("image still loading...")},x.Groups=function(){this.clear(),this.defaultIndex=0},x.Groups.DEFAULT=[{borderColor:"#2B7CE9",backgroundColor:"#97C2FC",highlightColor:"#D2E5FF"},{borderColor:"#FFA500",backgroundColor:"#FFFF00",highlightColor:"#FFFFA3"},{borderColor:"#FA0A10",backgroundColor:"#FB7E81",highlightColor:"#FFAFB1"},{borderColor:"#41A906",backgroundColor:"#7BE141",highlightColor:"#A1EC76"},{borderColor:"#E129F0",backgroundColor:"#EB7DF4",highlightColor:"#F0B3F5"},{borderColor:"#7C29F0",backgroundColor:"#AD85E4",highlightColor:"#D3BDF0"},{borderColor:"#C37F00",backgroundColor:"#FFA807",highlightColor:"#FFCA66"},{borderColor:"#4220FB",backgroundColor:"#6E6EFD",highlightColor:"#9B9BFD"},{borderColor:"#FD5A77",backgroundColor:"#FFC0CB",highlightColor:"#FFD1D9"},{borderColor:"#4AD63A",backgroundColor:"#C2FABC",highlightColor:"#E6FFE3"}],x.Groups.prototype.clear=function(){this.groups={},this.groups.length=function(){var t=0;for(var e in this)this.hasOwnProperty(e)&&t++;return t}},x.Groups.prototype.get=function(t){var e=this.groups[t];if(void 0==e){var i=this.defaultIndex%x.Groups.DEFAULT.length;this.defaultIndex++,e={},e.borderColor=x.Groups.DEFAULT[i].borderColor,e.backgroundColor=x.Groups.DEFAULT[i].backgroundColor,e.highlightColor=x.Groups.DEFAULT[i].highlightColor,this.groups[t]=e}return e},x.Groups.prototype.add=function(t,e){return this.groups[t]=e,e},x.isArray=function(t){return t instanceof Array?!0:"[object Array]"===Object.prototype.toString.call(t)},x.Slider=function(t){if(void 0===t)throw"Error: No container element defined";this.container=t,this.frame=document.createElement("DIV"),this.frame.style.width="100%",this.frame.style.position="relative",this.title=document.createElement("DIV"),this.title.style.margin="2px",this.title.style.marginBottom="5px",this.title.innerHTML="",this.container.appendChild(this.title),this.frame.prev=document.createElement("INPUT"),this.frame.prev.type="BUTTON",this.frame.prev.value="Prev",this.frame.appendChild(this.frame.prev),this.frame.play=document.createElement("INPUT"),this.frame.play.type="BUTTON",this.frame.play.value="Play",this.frame.appendChild(this.frame.play),this.frame.next=document.createElement("INPUT"),this.frame.next.type="BUTTON",this.frame.next.value="Next",this.frame.appendChild(this.frame.next),this.frame.bar=document.createElement("INPUT"),this.frame.bar.type="BUTTON",this.frame.bar.style.position="absolute",this.frame.bar.style.border="1px solid red",this.frame.bar.style.width="100px",this.frame.bar.style.height="6px",this.frame.bar.style.borderRadius="2px",this.frame.bar.style.MozBorderRadius="2px",this.frame.bar.style.border="1px solid #7F7F7F",this.frame.bar.style.backgroundColor="#E5E5E5",this.frame.appendChild(this.frame.bar),this.frame.slide=document.createElement("INPUT"),this.frame.slide.type="BUTTON",this.frame.slide.style.margin="0px",this.frame.slide.value=" ",this.frame.slide.style.position="relative",this.frame.slide.style.left="-100px",this.frame.appendChild(this.frame.slide);var e=this;this.frame.slide.onmousedown=function(t){e._onMouseDown(t)},this.frame.prev.onclick=function(t){e.prev(t)},this.frame.play.onclick=function(t){e.togglePlay(t)},this.frame.next.onclick=function(t){e.next(t)},this.container.appendChild(this.frame),this.onChangeCallback=void 0,this.playTimeout=void 0,this.framerate=20,this.duration=10,this.doLoop=!0,this.start=0,this.end=0,this.value=0,this.step=0,this.rangeIsDate=!1,this.redraw()},x.Slider.prototype._updateStep=function(){var t=this.end-this.start,e=this.duration*this.framerate;this.step=t/e},x.Slider.prototype.prev=function(){this._setValue(this.value-this.step)},x.Slider.prototype.next=function(){this._setValue(this.value+this.step)},x.Slider.prototype.playNext=function(){var t=new Date;if(!this.leftButtonDown)if(this.value+this.stepthis.start?(t-this.start)/(this.end-this.start)*i:0;var s=e+this.offset;return s},x.Slider.prototype._limitValue=function(t){return this.start>t&&(t=this.start),t>this.end&&(t=this.end),t},x.Slider.prototype._onMouseDown=function(t){if(this.leftButtonDown=t.which?1===t.which:1===t.button,this.leftButtonDown){this.startClientX=t.clientX,this.startSlideX=parseFloat(this.frame.slide.style.left),this.frame.style.cursor="move";var e=this;this.onmousemove=function(t){e._onMouseMove(t)},this.onmouseup=function(t){e._onMouseUp(t)},x.addEventListener(document,"mousemove",this.onmousemove),x.addEventListener(document,"mouseup",this.onmouseup),x.preventDefault(t)}},x.Slider.prototype._onMouseMove=function(t){var e=t.clientX-this.startClientX,i=this.startSlideX+e,s=this._leftToValue(i);this._setValue(s),x.preventDefault(t)},x.Slider.prototype._onMouseUp=function(t){this.frame.style.cursor="auto",this.leftButtonDown=!1,x.removeEventListener(document,"mousemove",this.onmousemove),x.removeEventListener(document,"mouseup",this.onmouseup),x.preventDefault(t)},x.Popup=function(t,e,i,s){this.container=t?t:document.body,this.x=0,this.y=0,this.padding=5,void 0!==e&&void 0!==i&&this.setPosition(e,i),void 0!==s&&this.setText(s),this.frame=document.createElement("div");var o=this.frame.style;o.position="absolute",o.visibility="hidden",o.border="1px solid #666",o.color="black",o.padding=this.padding+"px",o.backgroundColor="#FFFFC6",o.borderRadius="3px",o.MozBorderRadius="3px",o.WebkitBorderRadius="3px",o.boxShadow="3px 3px 10px rgba(128, 128, 128, 0.5)",o.whiteSpace="nowrap",this.container.appendChild(this.frame)},x.Popup.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},x.Popup.prototype.setText=function(t){this.frame.innerHTML=t},x.Popup.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,s=this.frame.parentNode.clientHeight,o=this.frame.parentNode.clientWidth,n=this.y-e; +n+e+this.padding>s&&(n=s-e-this.padding),this.padding>n&&(n=this.padding);var r=this.x;r+i+this.padding>o&&(r=o-i-this.padding),this.padding>r&&(r=this.padding),this.frame.style.left=r+"px",this.frame.style.top=n+"px",this.frame.style.visibility="visible"}else this.hide()},x.Popup.prototype.hide=function(){this.frame.style.visibility="hidden"},"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 s=2*i,o=s/2,n=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-o*o);this.moveTo(t,e-(r-n)),this.lineTo(t+o,e+n),this.lineTo(t-o,e+n),this.lineTo(t,e-(r-n)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var s=2*i,o=s/2,n=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-o*o);this.moveTo(t,e+(r-n)),this.lineTo(t+o,e-n),this.lineTo(t-o,e-n),this.lineTo(t,e+(r-n)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var s=0;10>s;s++){var o=0===s%2?1.3*i:.5*i;this.lineTo(t+o*Math.sin(2*s*Math.PI/10),e-o*Math.cos(2*s*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,s,o){var n=Math.PI/180;0>i-2*o&&(o=i/2),0>s-2*o&&(o=s/2),this.beginPath(),this.moveTo(t+o,e),this.lineTo(t+i-o,e),this.arc(t+i-o,e+o,o,270*n,360*n,!1),this.lineTo(t+i,e+s-o),this.arc(t+i-o,e+s-o,o,0,90*n,!1),this.lineTo(t+o,e+s),this.arc(t+o,e+s-o,o,90*n,180*n,!1),this.lineTo(t,e+o),this.arc(t+o,e+o,o,180*n,270*n,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,s){var o=.5522848,n=i/2*o,r=s/2*o,a=t+i,h=e+s,d=t+i/2,l=e+s/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-n,e,d,e),this.bezierCurveTo(d+n,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+n,h,d,h),this.bezierCurveTo(d-n,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,s){var o=1/3,n=i,r=s*o,a=.5522848,h=n/2*a,d=r/2*a,l=t+n,p=e+r,u=t+n/2,c=e+r/2,f=e+(s-r/2),m=e+s;this.beginPath(),this.moveTo(l,c),this.bezierCurveTo(l,c+d,u+h,p,u,p),this.bezierCurveTo(u-h,p,t,c+d,t,c),this.bezierCurveTo(t,c-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,l,c-d,l,c),this.lineTo(l,f),this.bezierCurveTo(l,f+d,u+h,m,u,m),this.bezierCurveTo(u-h,m,t,f+d,t,f),this.lineTo(t,c)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,s){var o=t-s*Math.cos(i),n=e-s*Math.sin(i),r=t-.9*s*Math.cos(i),a=e-.9*s*Math.sin(i),h=o+s/3*Math.cos(i+.5*Math.PI),d=n+s/3*Math.sin(i+.5*Math.PI),l=o+s/3*Math.cos(i-.5*Math.PI),p=n+s/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(l,p),this.closePath()}),x.util={},x.util.parseDOT=function(t){function e(t){return" "==t||" "==t||"\n"==t||"\r"==t}function i(t){return-1!="[]{}();,=->".indexOf(t)}function s(){f++,m=t[f]}function o(){return"(char "+f+")"}function n(){for(;m&&e(m);)s();var i=t[f+1],o=t[f-1],r=m+i;if("/*"==r){for(;m&&("*"!=m||"/"!=t[f+1]);)s();s(),s(),n()}else if("//"==r||"#"==m&&"\n"==o){for(;m&&"\n"!=m;)s();s(),n()}}function r(){n();var t="";if('"'==m){for(s();m&&'"'!=m;)t+=m,s();s()}else{for(;m&&!e(m)&&!i(m);)t+=m,s();var o=Number(t);isNaN(o)?"true"==t?t=!0:"false"==t?t=!1:"null"==t&&(t=null):t=o}return t}function a(){if(n(),'"'==m)return r();var t=r();if(void 0!=t){var e=Number(t);isNaN(e)?"true"==t?t=!0:"false"==t?t=!1:"null"==t&&(t=null):t=e}return t}function h(){if(n(),"["==m){s();for(var t={};m&&"]"!=m;){n();var e=r();if(!e)throw new SyntaxError("Attribute name expected "+o());if(n(),"="!=m)throw new SyntaxError("Equal sign = expected "+o());s();var i=a();if(!i)throw new SyntaxError("Attribute value expected "+o());t[e]=i,n(),","==m&&s()}return s(),t}return void 0}function d(){if(n(),"-"==m){if(s(),">"==m||"-"==m){var t="-"+m;return s(),t}throw new SyntaxError('Arrow "->" or "--" expected '+o())}return void 0}function l(){return n(),";"==m?(s(),";"):void 0}function p(t,e){if(t&&e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}function u(t,e){var i={id:t+"",attr:e||{}};g[t]?p(g[t].attr,i.attr):g[t]=i}function c(t,e,i,s){v.push({from:t+"",to:e+"",type:i,attr:s||{}})}var f=-1,m="",g={},v=[];for(s();m&&"{"!=m;)s();if("{"!=m)throw new SyntaxError("Invalid data. Curly bracket { expected "+o());for(s();m&&"}"!=m;){var y=r();if(void 0==y)throw new SyntaxError("String with id expected "+o());var w=h();u(y,w);for(var b=d();b;){var _=y;if(y=r(),void 0==y)throw new SyntaxError("String with id expected "+o());u(y),w=h(),c(_,y,b,w),b=d()}l(),n()}if("}"!=m)throw new SyntaxError("Invalid data. Curly bracket } expected");var x=t.indexOf("{"),T=t.indexOf("}",x),M=-1!=x&&-1!=T?t.substring(x+1,T):void 0;if(!M)throw Error("Invalid data. no curly brackets containing data found");var S=[];for(y in g)g.hasOwnProperty(y)&&S.push(g[y]);return{nodes:S,edges:v}},x.util.DOTToGraph=function(t){function e(t,e,i){for(var s in e)!e.hasOwnProperty(s)||i&&-1!=i.indexOf(s)||(t[s]=e[s]);t.label&&(t.text=t.label,delete t.label),t.shape&&(t.style=t.shape,delete t.shape)}var i=x.util.parseDOT(t),s={nodes:[],edges:[],options:{nodes:{},edges:{}}};return i.nodes.forEach(function(t){if("graph"==t.id.toLowerCase())e(s.options,t.attr);else if("node"==t.id.toLowerCase())e(s.options.nodes,t.attr);else if("edge"==t.id.toLowerCase())e(s.options.edges,t.attr);else{var i={};i.id=t.id,i.text=t.id,e(i,t.attr),s.nodes.push(i)}}),i.edges.forEach(function(t){var i={};i.from=t.from,i.to=t.to,i.text=t.id,i.style="->"==t.type?"arrow-end":"line",e(i,t.attr),s.edges.push(i)}),s};var k={util:M,events:E,Controller:d,DataSet:o,DataView:n,Range:a,Stack:r,TimeStep:TimeStep,EventBus:h,components:{items:{Item:m,ItemBox:g,ItemPoint:v,ItemRange:y},Component:l,Panel:p,RootPanel:u,ItemSet:f,TimeAxis:c},Timeline:_,Graph:x};s!==void 0&&(s=k),i!==void 0&&i.exports!==void 0&&(i.exports=k),"function"==typeof t&&t(function(){return k}),"undefined"!=typeof window&&(window.vis=k),M.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.point {\n background: none;\n}\n\n.graph .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n")})()},{moment:2}],2:[function(e,i){(function(){(function(s){function o(t,e){return function(i){return p(t.call(this,i),e)}}function n(t){return function(e){return this.lang().ordinal(t.call(this,e))}}function r(){}function a(t){d(this,t)}function h(t){var e=this._data={},i=t.years||t.year||t.y||0,s=t.months||t.month||t.M||0,o=t.weeks||t.week||t.w||0,n=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,h=t.seconds||t.second||t.s||0,d=t.milliseconds||t.millisecond||t.ms||0;this._milliseconds=d+1e3*h+6e4*a+36e5*r,this._days=n+7*o,this._months=s+12*i,e.milliseconds=d%1e3,h+=l(d/1e3),e.seconds=h%60,a+=l(h/60),e.minutes=a%60,r+=l(a/60),e.hours=r%24,n+=l(r/24),n+=7*o,e.days=n%30,s+=l(n/30),e.months=s%12,i+=l(s/12),e.years=i}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function l(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e){for(var i=t+"";e>i.length;)i="0"+i;return i}function u(t,e,i){var s,o=e._milliseconds,n=e._days,r=e._months;o&&t._d.setTime(+t+o*i),n&&t.date(t.date()+n*i),r&&(s=t.date(),t.date(1).month(t.month()+r*i).date(Math.min(s,t.daysInMonth())))}function c(t){return"[object Array]"===Object.prototype.toString.call(t)}function f(t,e){var i,s=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),n=0;for(i=0;s>i;i++)~~t[i]!==~~e[i]&&n++;return n+o}function m(t,e){return e.abbr=t,z[t]||(z[t]=new r),z[t].set(e),z[t]}function g(t){return t?(!z[t]&&R&&e("./lang/"+t),z[t]):F.fn._lang}function v(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,i,s=t.match(j);for(e=0,i=s.length;i>e;e++)s[e]=ae[s[e]]?ae[s[e]]:v(s[e]);return function(o){var n="";for(e=0;i>e;e++)n+="function"==typeof s[e].call?s[e].call(o,t):s[e];return n}}function w(t,e){function i(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&U.test(e);)e=e.replace(U,i);return oe[e]||(oe[e]=y(e)),oe[e](t)}function b(t){switch(t){case"DDDD":return B;case"YYYY":return q;case"YYYYY":return X;case"S":case"SS":case"SSS":case"DDD":return V;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":case"a":case"A":return K;case"X":return J;case"Z":case"ZZ":return G;case"T":return Z;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return W;default:return RegExp(t.replace("\\",""))}}function _(t,e,i){var s,o=i._a;switch(t){case"M":case"MM":o[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=g(i._l).monthsParse(e),null!=s?o[1]=s:i._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(o[2]=~~e);break;case"YY":o[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":o[0]=~~e;break;case"a":case"A":i._isPm="pm"===(e+"").toLowerCase();break;case"H":case"HH":case"h":case"hh":o[3]=~~e;break;case"m":case"mm":o[4]=~~e;break;case"s":case"ss":o[5]=~~e;break;case"S":case"SS":case"SSS":o[6]=~~(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,s=(e+"").match(ee),s&&s[1]&&(i._tzh=~~s[1]),s&&s[2]&&(i._tzm=~~s[2]),s&&"+"===s[0]&&(i._tzh=-i._tzh,i._tzm=-i._tzm)}null==e&&(i._isValid=!1)}function x(t){var e,i,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=t._tzh||0,s[4]+=t._tzm||0,i=new Date(0),t._useUTC?(i.setUTCFullYear(s[0],s[1],s[2]),i.setUTCHours(s[3],s[4],s[5],s[6])):(i.setFullYear(s[0],s[1],s[2]),i.setHours(s[3],s[4],s[5],s[6])),t._d=i}}function T(t){var e,i,s=t._f.match(j),o=t._i;for(t._a=[],e=0;s.length>e;e++)i=(b(s[e]).exec(o)||[])[0],i&&(o=o.slice(o.indexOf(i)+i.length)),ae[s[e]]&&_(s[e],i,t);t._isPm&&12>t._a[3]&&(t._a[3]+=12),t._isPm===!1&&12===t._a[3]&&(t._a[3]=0),x(t)}function M(t){for(var e,i,s,o,n=99;t._f.length;){if(e=d({},t),e._f=t._f.pop(),T(e),i=new a(e),i.isValid()){s=i;break}o=f(e._a,i.toArray()),n>o&&(n=o,s=i)}d(t,s)}function S(t){var e,i=t._i;if(Q.exec(i)){for(t._f="YYYY-MM-DDT",e=0;4>e;e++)if(te[e][1].exec(i)){t._f+=te[e][0];break}G.exec(i)&&(t._f+=" Z"),T(t)}else t._d=new Date(i)}function C(t){var e=t._i,i=H.exec(e);e===s?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?S(t):c(e)?(t._a=e.slice(0),x(t)):t._d=e instanceof Date?new Date(+e):new Date(e)}function E(t,e,i,s,o){return o.relativeTime(e||1,!!i,t,s)}function D(t,e,i){var s=Y(Math.abs(t)/1e3),o=Y(s/60),n=Y(o/60),r=Y(n/24),a=Y(r/365),h=45>s&&["s",s]||1===o&&["m"]||45>o&&["mm",o]||1===n&&["h"]||22>n&&["hh",n]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",Y(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,E.apply({},h)}function k(t,e,i){var s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),Math.ceil(F(t).add("d",o).dayOfYear()/7)}function L(t){var e=t._i,i=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=g().preparse(e)),F.isMoment(e)?(t=d({},e),t._d=new Date(+e._d)):i?c(i)?M(t):T(t):C(t),new a(t))}function O(t,e){F.fn[t]=F.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),this):this._d["get"+i+e]()}}function N(t){F.duration.fn[t]=function(){return this._data[t]}}function A(t,e){F.duration.fn["as"+t]=function(){return+this/e}}for(var F,P,I="2.0.0",Y=Math.round,z={},R=i!==s&&i.exports,H=/^\/?Date\((\-?\d+)/i,j=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,U=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,W=/\d\d?/,V=/\d{1,3}/,B=/\d{3}/,q=/\d{1,4}/,X=/[+\-]?\d{1,6}/,K=/[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i,G=/Z|[\+\-]\d\d:?\d\d/i,Z=/T/i,J=/[\+\-]?\d+(\.\d{1,3})?/,Q=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,$="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(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/]],ee=/([\+\-]|\d\d)/gi,ie="Month|Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},oe={},ne="DDD w W M D d".split(" "),re="M D H h m s w W".split(" "),ae={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)},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~~(this.milliseconds()/100)},SS:function(){return p(~~(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(t/60),2)+":"+p(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(10*t/6),4)},X:function(){return this.unix()}};ne.length;)P=ne.pop(),ae[P+"o"]=n(ae[P]);for(;re.length;)P=re.pop(),ae[P+P]=o(ae[P],2);for(ae.DDDD=o(ae.DDD,3),r.prototype={set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=F([2e3,e]),s="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=RegExp(s.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()]},_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},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,s){var o=this._relativeTime[i];return"function"==typeof o?o(t,e,i,s):o.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 k(t,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},F=function(t,e,i){return L({_i:t,_f:e,_l:i,_isUTC:!1})},F.utc=function(t,e,i){return L({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e})},F.unix=function(t){return F(1e3*t)},F.duration=function(t,e){var i,s=F.isDuration(t),o="number"==typeof t,n=s?t._data:o?{}:t;return o&&(e?n[e]=t:n.milliseconds=t),i=new h(n),s&&t.hasOwnProperty("_lang")&&(i._lang=t._lang),i},F.version=I,F.defaultFormat=$,F.lang=function(t,e){return t?(e?m(t,e):z[t]||g(t),F.duration.fn._lang=F.fn._lang=g(t),s):F.fn._lang._abbr},F.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),g(t)},F.isMoment=function(t){return t instanceof a},F.isDuration=function(t){return t instanceof h},F.fn=a.prototype={clone:function(){return F(this)},valueOf:function(){return+this._d},unix:function(){return Math.floor(+this._d/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._d},toJSON:function(){return F.utc(this).format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!f(this._a,(this._isUTC?F.utc(this._a):F(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this._isUTC=!0,this},local:function(){return this._isUTC=!1,this},format:function(t){var e=w(this,t||F.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?F.duration(+e,t):F.duration(t,e),u(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?F.duration(+e,t):F.duration(t,e),u(this,i,-1),this},diff:function(t,e,i){var s,o,n=this._isUTC?F(t).utc():F(t).local(),r=6e4*(this.zone()-n.zone());return e&&(e=e.replace(/s$/,"")),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+n.daysInMonth()),o=12*(this.year()-n.year())+(this.month()-n.month()),o+=(this-F(this).startOf("month")-(n-F(n).startOf("month")))/s,"year"===e&&(o/=12)):(s=this-n-r,o="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?s/864e5:"week"===e?s/6048e5:s),i?o:l(o)},from:function(t,e){return F.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(F(),t)},calendar:function(){var t=this.diff(F().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+F(t).startOf(e)},isBefore:function(t,e){return e=e!==s?e:"millisecond",+this.clone().startOf(e)<+F(t).startOf(e)},isSame:function(t,e){return e=e!==s?e:"millisecond",+this.clone().startOf(e)===+F(t).startOf(e)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return F.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=Y((F(this).startOf("day")-F(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},isoWeek:function(t){var e=k(this,1,4);return null==t?e:this.add("d",7*(t-e))},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},lang:function(t){return t===s?this._lang:(this._lang=g(t),this)}},P=0;ie.length>P;P++)O(ie[P].toLowerCase().replace(/s$/,""),ie[P]);O("year","FullYear"),F.fn.days=F.fn.day,F.fn.weeks=F.fn.week,F.fn.isoWeeks=F.fn.isoWeek,F.duration.fn=h.prototype={weeks:function(){return l(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*this._months},humanize:function(t){var e=+this,i=D(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},lang:F.fn.lang};for(P in se)se.hasOwnProperty(P)&&(A(P,se[P]),N(P.toLowerCase()));A("Weeks",6048e5),F.lang("en",{ordinal:function(t){var e=t%10,i=1===~~(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),R&&(i.exports=F),"undefined"==typeof ender&&(this.moment=F),"function"==typeof t&&t.amd&&t("moment",[],function(){return F})}).call(this)})()},{}]},{},[1])(1)}); \ No newline at end of file