From 084c19400e9d3bba18f6c84eb464fa633f1bf079 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Tue, 3 Mar 2015 12:30:09 +0100 Subject: [PATCH] interaction tweaks --- dist/vis.js | 5472 +++++++++++---------- examples/network/01_basic_usage.html | 2 +- lib/network/Edge.js | 8 + lib/network/Network.js | 15 +- lib/network/modules/Canvas.js | 28 +- lib/network/modules/CanvasRenderer.js | 34 +- lib/network/modules/InteractionHandler.js | 143 +- lib/network/modules/SelectionHandler.js | 10 +- lib/network/modules/View.js | 10 +- 9 files changed, 2861 insertions(+), 2861 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index 9766b6c4..e6f25418 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 4.0.0-SNAPSHOT - * @date 2015-03-02 + * @date 2015-03-03 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -139,13 +139,13 @@ return /******/ (function(modules) { // webpackBootstrap // Network exports.Network = __webpack_require__(53); exports.network = { - Edge: __webpack_require__(54), + Edge: __webpack_require__(59), Groups: __webpack_require__(56), Images: __webpack_require__(57), - Node: __webpack_require__(55), - Popup: __webpack_require__(58), - dotparser: __webpack_require__(59), - gephiParser: __webpack_require__(60) + Node: __webpack_require__(58), + Popup: __webpack_require__(60), + dotparser: __webpack_require__(54), + gephiParser: __webpack_require__(55) }; // Deprecated since v3.0.0 @@ -23264,13 +23264,13 @@ return /******/ (function(modules) { // webpackBootstrap var hammerUtil = __webpack_require__(24); var DataSet = __webpack_require__(7); var DataView = __webpack_require__(9); - var dotparser = __webpack_require__(59); - var gephiParser = __webpack_require__(60); + var dotparser = __webpack_require__(54); + var gephiParser = __webpack_require__(55); var Groups = __webpack_require__(56); var Images = __webpack_require__(57); - var Node = __webpack_require__(55); - var Edge = __webpack_require__(54); - var Popup = __webpack_require__(58); + var Node = __webpack_require__(58); + var Edge = __webpack_require__(59); + var Popup = __webpack_require__(60); var MixinLoader = __webpack_require__(61); var Activator = __webpack_require__(38); var locales = __webpack_require__(66); @@ -23283,8 +23283,8 @@ return /******/ (function(modules) { // webpackBootstrap var CanvasRenderer = __webpack_require__(76).CanvasRenderer; var Canvas = __webpack_require__(77).Canvas; var View = __webpack_require__(78).View; - var TouchEventHandler = __webpack_require__(80).TouchEventHandler; - var SelectionHandler = __webpack_require__(79).SelectionHandler; + var InteractionHandler = __webpack_require__(79).InteractionHandler; + var SelectionHandler = __webpack_require__(80).SelectionHandler; /** @@ -23487,7 +23487,7 @@ return /******/ (function(modules) { // webpackBootstrap // modules this.canvas = new Canvas(this.body); this.selectionHandler = new SelectionHandler(this.body, this.canvas); - this.touchHandler = new TouchEventHandler(this.body, this.canvas, this.selectionHandler); + this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); this.view = new View(this.body, this.canvas); this.renderer = new CanvasRenderer(this.body, this.canvas); this.clustering = new ClusterEngine(this.body); @@ -23527,13 +23527,6 @@ return /******/ (function(modules) { // webpackBootstrap // apply options this.setOptions(options); - // other vars - this.cachedFunctions = {}; - this.startedStabilization = false; - this.stabilized = false; - this.stabilizationIterations = null; - this.draggingNodes = false; - // position and scale variables and objects this.pointerPosition = { x: 0, y: 0 }; // coordinates of the bottom right of the canvas. they will be set during _redraw @@ -23716,7 +23709,7 @@ return /******/ (function(modules) { // webpackBootstrap this.physics.setOptions(options.physics); this.canvas.setOptions(options.canvas); this.renderer.setOptions(options.rendering); - this.touchHandler.setOptions(options.interaction); + this.interactionHandler.setOptions(options.interaction); this.selectionHandler.setOptions(options.selection); @@ -23841,7 +23834,7 @@ return /******/ (function(modules) { // webpackBootstrap //this._configureSmoothCurves(); // bind hammer - this.canvas._bindHammer(); + //this.canvas._bindHammer(); // bind keys. If disabled, this will not do anything; //this._createKeyBinds(); @@ -24761,1357 +24754,1087 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - var util = __webpack_require__(1); - var Node = __webpack_require__(55); - /** - * @class Edge + * 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. * - * A edge connects two nodes - * @param {Object} properties Object with properties. Must contain - * At least properties from and to. - * Available properties: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color + * DOT language reference: http://www.graphviz.org/doc/info/lang.html + * + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graph An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges */ - function Edge(properties, body, networkConstants) { - if (body === undefined) { - throw "No body provided"; - } - var fields = ["edges"]; - var constants = util.selectiveBridgeObject(fields, networkConstants); - this.options = constants.edges; - - this.options.smoothCurves = networkConstants.smoothCurves; - this.body = body; + function parseDOT(data) { + dot = data; + return parseGraph(); + } - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.title = undefined; - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - this.value = undefined; - this.selected = false; - this.hover = false; - this.labelDimensions = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached - this.dirtyLabel = true; - this.colorDirty = true; + // token types enumeration + var TOKENTYPE = { + NULL: 0, + DELIMITER: 1, + IDENTIFIER: 2, + UNKNOWN: 3 + }; - this.from = null; // a node - this.to = null; // a node - this.via = null; // a temp node + // map with all delimiters + var DELIMITERS = { + "{": true, + "}": true, + "[": true, + "]": true, + ";": true, + "=": true, + ",": true, - this.fromBackup = null; // used to clean up after reconnect (used for manipulation) - this.toBackup = null; // used to clean up after reconnect (used for manipulation) + "->": true, + "--": true + }; - // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster - // by storing the original information we can revert to the original connection when the cluser is opened. - this.fromArray = []; - this.toArray = []; + var dot = ""; // current dot file + var index = 0; // current index in dot file + var c = ""; // current token character in expr + var token = ""; // current token + var tokenType = TOKENTYPE.NULL; // type of the token - this.connected = false; + /** + * Get the first character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function first() { + index = 0; + c = dot.charAt(0); + } - this.widthFixed = false; - this.lengthFixed = false; + /** + * Get the next character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function next() { + index++; + c = dot.charAt(index); + } - this.setProperties(properties); + /** + * Preview the next character from the dot file. + * @return {String} cNext + */ + function nextPreview() { + return dot.charAt(index + 1); + } - this.controlNodesEnabled = false; - this.controlNodes = { from: null, to: null, positions: {} }; - this.connectedNode = null; + /** + * Test whether given character is alphabetic or numeric + * @param {String} c + * @return {Boolean} isAlphaNumeric + */ + var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; + function isAlphaNumeric(c) { + return regexAlphaNumeric.test(c); } /** - * Set or overwrite properties for the edge - * @param {Object} properties an object with properties - * @param {Object} constants and object with default, global properties + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a */ - Edge.prototype.setProperties = function (properties) { - this.colorDirty = true; - if (!properties) { - return; + function merge(a, b) { + if (!a) { + a = {}; } - this.properties = properties; - - var fields = ["style", "fontSize", "fontFace", "fontColor", "fontFill", "fontStrokeWidth", "fontStrokeColor", "width", "widthSelectionMultiplier", "hoverWidth", "arrowScaleFactor", "dash", "inheritColor", "labelAlignment", "opacity", "customScalingFunction", "useGradients", "value"]; - util.selectiveDeepExtend(fields, this.options, properties); - if (properties.from !== undefined) { - this.fromId = properties.from; - } - if (properties.to !== undefined) { - this.toId = properties.to; + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } } + return a; + } - if (properties.id !== undefined) { - this.id = properties.id; - } - if (properties.label !== undefined) { - this.label = properties.label;this.dirtyLabel = true; + /** + * Set a value in an object, where the provided parameter name can be a + * path with nested parameters. For example: + * + * var obj = {a: 2}; + * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} + * + * @param {Object} obj + * @param {String} path A parameter name or dot-separated parameter path, + * like "color.highlight.border". + * @param {*} value + */ + function setValue(obj, path, value) { + var keys = path.split("."); + var o = obj; + while (keys.length) { + var key = keys.shift(); + if (keys.length) { + // this isn't the end point + if (!o[key]) { + o[key] = {}; + } + o = o[key]; + } else { + // this is the end point + o[key] = value; + } } + } - if (properties.title !== undefined) { - this.title = properties.title; - } - if (properties.value !== undefined) { - this.value = properties.value; - } - if (properties.length !== undefined) { - this.physics.springLength = properties.length; + /** + * Add a node to a graph object. If there is already a node with + * the same id, their attributes will be merged. + * @param {Object} graph + * @param {Object} node + */ + function addNode(graph, node) { + var i, len; + var current = null; + + // find root graph (in case of subgraph) + var graphs = [graph]; // list with all graphs from current graph to root graph + var root = graph; + while (root.parent) { + graphs.push(root.parent); + root = root.parent; } - if (properties.color !== undefined) { - this.options.inheritColor = false; - if (util.isString(properties.color)) { - this.options.color.color = properties.color; - this.options.color.highlight = properties.color; - } else { - if (properties.color.color !== undefined) { - this.options.color.color = properties.color.color; - } - if (properties.color.highlight !== undefined) { - this.options.color.highlight = properties.color.highlight; - } - if (properties.color.hover !== undefined) { - this.options.color.hover = properties.color.hover; + // find existing node (at root level) by its id + if (root.nodes) { + for (i = 0, len = root.nodes.length; i < len; i++) { + if (node.id === root.nodes[i].id) { + current = root.nodes[i]; + break; } } } - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); - - this.widthFixed = this.widthFixed || properties.width !== undefined; - this.lengthFixed = this.lengthFixed || properties.length !== undefined; - - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - - // set draw method based on style - switch (this.options.style) { - case "line": - this.draw = this._drawLine;break; - case "arrow": - this.draw = this._drawArrow;break; - case "arrow-center": - this.draw = this._drawArrowCenter;break; - case "dash-line": - this.draw = this._drawDashLine;break; - default: - this.draw = this._drawLine;break; + if (!current) { + // this is a new node + current = { + id: node.id + }; + if (graph.node) { + // clone default attributes + current.attr = merge(current.attr, graph.node); + } } - }; - - - /** - * Connect an edge to its nodes - */ - Edge.prototype.connect = function () { - this.disconnect(); - this.from = this.body.nodes[this.fromId] || null; - this.to = this.body.nodes[this.toId] || null; - this.connected = this.from !== null && this.to !== null; + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; - if (this.connected === true) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } else { - if (this.from) { - this.from.detachEdge(this); + if (!g.nodes) { + g.nodes = []; } - if (this.to) { - this.to.detachEdge(this); + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); } } - }; - /** - * Disconnect an edge from its nodes - */ - Edge.prototype.disconnect = function () { - if (this.from) { - this.from.detachEdge(this); - this.from = null; - } - if (this.to) { - this.to.detachEdge(this); - this.to = null; + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); } - - this.connected = false; - }; + } /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. + * Add an edge to a graph object + * @param {Object} graph + * @param {Object} edge */ - Edge.prototype.getTitle = function () { - return typeof this.title === "function" ? this.title() : this.title; - }; - + function addEdge(graph, edge) { + if (!graph.edges) { + graph.edges = []; + } + graph.edges.push(edge); + if (graph.edge) { + var attr = merge({}, graph.edge); // clone default attributes + edge.attr = merge(attr, edge.attr); // merge attributes + } + } /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value + * Create an edge to a graph object + * @param {Object} graph + * @param {String | Number | Object} from + * @param {String | Number | Object} to + * @param {String} type + * @param {Object | null} attr + * @return {Object} edge */ - Edge.prototype.getValue = function () { - return this.value; - }; + function createEdge(graph, from, to, type, attr) { + var edge = { + from: from, + to: to, + type: type + }; - /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - */ - Edge.prototype.setValueRange = function (min, max, total) { - if (!this.widthFixed && this.value !== undefined) { - var scale = this.options.customScalingFunction(min, max, total, this.value); - var widthDiff = this.options.widthMax - this.options.widthMin; - this.options.width = this.options.widthMin + scale * widthDiff; - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes } - }; + edge.attr = merge(edge.attr || {}, attr); // merge attributes - /** - * Redraw a edge - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - Edge.prototype.draw = function (ctx) { - throw "Method draw not initialized in edge"; - }; + return 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 + * Get next token in the current dot file. + * The token and token type are available as token and tokenType */ - Edge.prototype.isOverlappingWith = function (obj) { - if (this.connected) { - 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 = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + function getToken() { + tokenType = TOKENTYPE.NULL; + token = ""; - return dist < distMax; - } else { - return false; + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); } - }; - - Edge.prototype._getColor = function (ctx) { - var colorObj = this.options.color; - if (this.options.useGradients == true) { - var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); - var fromColor, toColor; - fromColor = this.from.options.color.highlight.border; - toColor = this.to.options.color.highlight.border; + do { + var isComment = false; - if (this.from.selected == false && this.to.selected == false) { - fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity); - toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity); - } else if (this.from.selected == true && this.to.selected == false) { - toColor = this.to.options.color.border; - } else if (this.from.selected == false && this.to.selected == true) { - fromColor = this.from.options.color.border; + // skip comment + if (c == "#") { + // find the previous non-space character + var i = index - 1; + while (dot.charAt(i) == " " || dot.charAt(i) == "\t") { + i--; + } + if (dot.charAt(i) == "\n" || dot.charAt(i) == "") { + // the # is at the start of a line, this is indeed a line comment + while (c != "" && c != "\n") { + next(); + } + isComment = true; + } + } + if (c == "/" && nextPreview() == "/") { + // skip line comment + while (c != "" && c != "\n") { + next(); + } + isComment = true; + } + if (c == "/" && nextPreview() == "*") { + // skip block comment + while (c != "") { + if (c == "*" && nextPreview() == "/") { + // end of block comment found. skip these last two characters + next(); + next(); + break; + } else { + next(); + } + } + isComment = true; } - grd.addColorStop(0, fromColor); - grd.addColorStop(1, toColor); - return grd; - } - if (this.colorDirty === true) { - if (this.options.inheritColor == "to") { - colorObj = { - highlight: this.to.options.color.highlight.border, - hover: this.to.options.color.hover.border, - color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) - }; - } else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { - colorObj = { - highlight: this.from.options.color.highlight.border, - hover: this.from.options.color.hover.border, - color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) - }; + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); } - this.options.color = colorObj; - this.colorDirty = false; - } + } while (isComment); + // check for end of dot file + if (c == "") { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; + } + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; + } - if (this.selected == true) { - return colorObj.highlight; - } else if (this.hover == true) { - return colorObj.hover; - } else { - return colorObj.color; + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; } - }; + // check for an identifier (number or string) + // TODO: more precise parsing of numbers/strings (and the port separator ':') + if (isAlphaNumeric(c) || c == "-") { + token += c; + next(); - /** - * Redraw a edge as a line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._drawLine = function (ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.lineWidth = this._getLineWidth(); - - if (this.from != this.to) { - // draw line - var via = this._line(ctx); + while (isAlphaNumeric(c)) { + token += c; + next(); + } + if (token == "false") { + token = false; // convert to boolean + } else if (token == "true") { + token = true; // convert to boolean + } else if (!isNaN(Number(token))) { + token = Number(token); // convert to number + } + tokenType = TOKENTYPE.IDENTIFIER; + return; + } - // draw label - var point; - if (this.label) { - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); - var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); - point = { x: midpointX, y: midpointY }; - } else { - point = this._pointOnLine(0.5); + // check for a string enclosed by double quotes + if (c == "\"") { + next(); + while (c != "" && (c != "\"" || c == "\"" && nextPreview() == "\"")) { + token += c; + if (c == "\"") { + // skip the escape character + next(); } - this._label(ctx, this.label, point.x, point.y); - } - } else { - var x, y; - var radius = this.physics.springLength / 4; - var node = this.from; - if (!node.width) { - node.resize(ctx); + next(); } - 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; + if (c != "\"") { + throw newSyntaxError("End of string \" expected"); } - this._circle(ctx, x, y, radius); - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + next(); + tokenType = TOKENTYPE.IDENTIFIER; + return; } - }; + + // something unknown is found, wrong characters, a syntax error + tokenType = TOKENTYPE.UNKNOWN; + while (c != "") { + token += c; + next(); + } + throw new SyntaxError("Syntax error in part \"" + chop(token, 30) + "\""); + } /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width - * @private + * Parse a graph. + * @returns {Object} graph */ - Edge.prototype._getLineWidth = function () { - if (this.selected == true) { - return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3 * this.networkScaleInv); - } else { - if (this.hover == true) { - return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3 * this.networkScaleInv); - } else { - return Math.max(this.options.width, 0.3 * this.networkScaleInv); - } - } - }; + function parseGraph() { + var graph = {}; - Edge.prototype._getViaCoordinates = function () { - if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) { - return this.via; - } else if (this.options.smoothCurves.enabled == false) { - return { x: 0, y: 0 }; - } else { - var xVia = null; - var yVia = null; - var factor = this.options.smoothCurves.roundness; - var type = this.options.smoothCurves.type; - var dx = Math.abs(this.from.x - this.to.x); - var dy = Math.abs(this.from.y - this.to.y); - if (type == "discrete" || type == "diagonalCross") { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - } - } - if (type == "discrete") { - xVia = dx < factor * dy ? this.from.x : xVia; - } - } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - } - } - if (type == "discrete") { - yVia = dy < factor * dx ? this.from.y : yVia; - } - } - } else if (type == "straightCross") { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - // up - down - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1 - factor) * dy; - } else { - yVia = this.to.y + (1 - factor) * dy; - } - } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - // left - right - if (this.from.x < this.to.x) { - xVia = this.to.x - (1 - factor) * dx; - } else { - xVia = this.to.x + (1 - factor) * dx; - } - yVia = this.from.y; - } - } else if (type == "horizontal") { - if (this.from.x < this.to.x) { - xVia = this.to.x - (1 - factor) * dx; - } else { - xVia = this.to.x + (1 - factor) * dx; - } - yVia = this.from.y; - } else if (type == "vertical") { - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1 - factor) * dy; - } else { - yVia = this.to.y + (1 - factor) * dy; - } - } else if (type == "curvedCW") { - var dx = this.to.x - this.from.x; - var dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; + first(); + getToken(); - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); + // optional strict keyword + if (token == "strict") { + graph.strict = true; + getToken(); + } - xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); - yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); - } else if (type == "curvedCCW") { - var dx = this.to.x - this.from.x; - var dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; + // graph or digraph keyword + if (token == "graph" || token == "digraph") { + graph.type = token; + getToken(); + } - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); + } - xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); - yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); - } else { - // continuous - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - xVia = this.to.x < xVia ? this.to.x : xVia; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - xVia = this.to.x > xVia ? this.to.x : xVia; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x < xVia ? this.to.x : xVia; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x > xVia ? this.to.x : xVia; - } - } - } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - yVia = this.to.y > yVia ? this.to.y : yVia; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - yVia = this.to.y > yVia ? this.to.y : yVia; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } - } - } - } + // open angle bracket + if (token != "{") { + throw newSyntaxError("Angle bracket { expected"); + } + getToken(); + // statements + parseStatements(graph); - return { x: xVia, y: yVia }; + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); } - }; + getToken(); + + // end of file + if (token !== "") { + throw newSyntaxError("End of file expected"); + } + getToken(); + + // remove temporary default properties + delete graph.node; + delete graph.edge; + delete graph.graph; + + return graph; + } /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private + * Parse a list with statements. + * @param {Object} graph */ - Edge.prototype._line = function (ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - if (this.options.smoothCurves.enabled == true) { - if (this.options.smoothCurves.dynamic == false) { - var via = this._getViaCoordinates(); - if (via.x == null) { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return null; - } else { - // this.via.x = via.x; - // this.via.y = via.y; - ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); - ctx.stroke(); - //ctx.circle(via.x,via.y,2) - //ctx.stroke(); - return via; - } - } else { - ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); - ctx.stroke(); - return this.via; + function parseStatements(graph) { + while (token !== "" && token != "}") { + parseStatement(graph); + if (token == ";") { + getToken(); } - } else { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return null; } - }; + } /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @private + * Parse a single statement. Can be a an attribute statement, node + * statement, a series of node statements and edge statements, or a + * parameter. + * @param {Object} graph */ - 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(); - }; + function parseStatement(graph) { + // parse subgraph + var subgraph = parseSubgraph(graph); + if (subgraph) { + // edge statements + parseEdge(graph, subgraph); - /** - * Draw label with white background and with the middle at (x, y) - * @param {CanvasRenderingContext2D} ctx - * @param {String} text - * @param {Number} x - * @param {Number} y - * @private - */ - Edge.prototype._label = function (ctx, text, x, y) { - if (text) { - ctx.font = (this.from.selected || this.to.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace; - var yLine; + return; + } - if (this.dirtyLabel == true) { - var lines = String(text).split("\n"); - var lineCount = lines.length; - var fontSize = Number(this.options.fontSize); - yLine = y + (1 - lineCount) / 2 * fontSize; + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; + } - var width = ctx.measureText(lines[0]).width; - for (var i = 1; i < lineCount; i++) { - var lineWidth = ctx.measureText(lines[i]).width; - width = lineWidth > width ? lineWidth : width; - } - var height = this.options.fontSize * lineCount; - var left = x - width / 2; - var top = y - height / 2; + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier expected"); + } + var id = token; // id can be a string or a number + getToken(); - // cache - this.labelDimensions = { top: top, left: left, width: width, height: height, yLine: yLine }; + if (token == "=") { + // id statement + getToken(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier expected"); } + graph[id] = token; + getToken(); + // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " + } else { + parseNodeStatement(graph, id); + } + } - var yLine = this.labelDimensions.yLine; + /** + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph + */ + function parseSubgraph(graph) { + var subgraph = null; - ctx.save(); + // optional subgraph keyword + if (token == "subgraph") { + subgraph = {}; + subgraph.type = "subgraph"; + getToken(); - if (this.options.labelAlignment != "horizontal") { - ctx.translate(x, yLine); - this._rotateForLabelAlignment(ctx); - x = 0; - yLine = 0; + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); } + } + // open angle bracket + if (token == "{") { + getToken(); - this._drawLabelRect(ctx); - this._drawLabelText(ctx, x, yLine, lines, lineCount, fontSize); + if (!subgraph) { + subgraph = {}; + } + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; - ctx.restore(); + // statements + parseStatements(subgraph); + + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); + } + getToken(); + + // remove temporary default properties + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; + + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; + } + graph.subgraphs.push(subgraph); } - }; + + return subgraph; + } /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private + * parse an attribute statement like "node [shape=circle fontSize=16]". + * Available keywords are 'node', 'edge', 'graph'. + * The previous list with default attributes will be replaced + * @param {Object} graph + * @returns {String | null} keyword Returns the name of the parsed attribute + * (node, edge, graph), or null if nothing + * is parsed. */ - Edge.prototype._rotateForLabelAlignment = function (ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); + function parseAttributeStatement(graph) { + // attribute statements + if (token == "node") { + getToken(); - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; + // node attributes + graph.node = parseAttributeList(); + return "node"; + } else if (token == "edge") { + getToken(); + + // edge attributes + graph.edge = parseAttributeList(); + return "edge"; + } else if (token == "graph") { + getToken(); + + // graph attributes + graph.graph = parseAttributeList(); + return "graph"; } - ctx.rotate(angleInDegrees); - }; + return null; + } /** - * Draws the label rectangle - * @param {CanvasRenderingContext2D} ctx - * @param {String} labelAlignment - * @private + * parse a node statement + * @param {Object} graph + * @param {String | Number} id */ - Edge.prototype._drawLabelRect = function (ctx) { - if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { - ctx.fillStyle = this.options.fontFill; - - var lineMargin = 2; - - if (this.options.labelAlignment == "line-center") { - ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height); - } else if (this.options.labelAlignment == "line-above") { - ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height); - } else if (this.options.labelAlignment == "line-below") { - ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height); - } else { - ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height); - } + function parseNodeStatement(graph, id) { + // node statement + var node = { + id: id + }; + var attr = parseAttributeList(); + if (attr) { + node.attr = attr; } - }; + addNode(graph, node); + + // edge statements + parseEdge(graph, id); + } /** - * Draws the label text - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} yLine - * @param {Array} lines - * @param {Number} lineCount - * @param {Number} fontSize - * @private + * Parse an edge or a series of edges + * @param {Object} graph + * @param {String | Number} from Id of the from node */ - Edge.prototype._drawLabelText = function (ctx, x, yLine, lines, lineCount, fontSize) { - // draw text - ctx.fillStyle = this.options.fontColor || "black"; - ctx.textAlign = "center"; + function parseEdge(graph, from) { + while (token == "->" || token == "--") { + var to; + var type = token; + getToken(); - // check for label alignment - if (this.options.labelAlignment != "horizontal") { - var lineMargin = 2; - if (this.options.labelAlignment == "line-above") { - ctx.textBaseline = "alphabetic"; - yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } else if (this.options.labelAlignment == "line-below") { - ctx.textBaseline = "hanging"; - yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers + var subgraph = parseSubgraph(graph); + if (subgraph) { + to = subgraph; } else { - ctx.textBaseline = "middle"; + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier or subgraph expected"); + } + to = token; + addNode(graph, { + id: to + }); + getToken(); } - } else { - ctx.textBaseline = "middle"; - } - // check for strokeWidth - if (this.options.fontStrokeWidth > 0) { - ctx.lineWidth = this.options.fontStrokeWidth; - ctx.strokeStyle = this.options.fontStrokeColor; - ctx.lineJoin = "round"; - } - for (var i = 0; i < lineCount; i++) { - if (this.options.fontStrokeWidth > 0) { - ctx.strokeText(lines[i], x, yLine); - } - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; + // parse edge attributes + var attr = parseAttributeList(); + + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); + + from = to; } - }; + } /** - * Redraw a edge as a dashed line - * Draw this edge in the given canvas - * @author David Jordan - * @date 2012-08-08 - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr */ - Edge.prototype._drawDashLine = function (ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.lineWidth = this._getLineWidth(); + function parseAttributeList() { + var attr = null; - var via = null; - // only firefox and chrome support this method, else we use the legacy one. - if (ctx.setLineDash !== undefined) { - ctx.save(); - // configure the dash pattern - var pattern = [0]; - if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) { - pattern = [this.options.dash.length, this.options.dash.gap]; - } else { - pattern = [5, 5]; - } + while (token == "[") { + getToken(); + attr = {}; + while (token !== "" && token != "]") { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute name expected"); + } + var name = token; - // set dash settings for chrome or firefox - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; + getToken(); + if (token != "=") { + throw newSyntaxError("Equal sign = expected"); + } + getToken(); - // draw the line - via = this._line(ctx); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute value expected"); + } + var value = token; + setValue(attr, name, value); // name can be a path - // restore the dash settings. - ctx.setLineDash([0]); - ctx.lineDashOffset = 0; - ctx.restore(); - } else { - // unsupporting smooth lines - // draw dashed line - ctx.beginPath(); - ctx.lineCap = "round"; - if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap, this.options.dash.altLength, this.options.dash.gap]); - } else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap]); - } else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); + getToken(); + if (token == ",") { + getToken(); } - ctx.stroke(); - } + } - // draw label - if (this.label) { - var point; - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); - var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); - point = { x: midpointX, y: midpointY }; - } else { - point = this._pointOnLine(0.5); + if (token != "]") { + throw newSyntaxError("Bracket ] expected"); } - this._label(ctx, this.label, point.x, point.y); + getToken(); } - }; + + return attr; + } /** - * Get a point on a line - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private + * Create a syntax error with extra information on current token and index. + * @param {String} message + * @returns {SyntaxError} err */ - 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 - }; - }; + function newSyntaxError(message) { + return new SyntaxError(message + ", got \"" + chop(token, 30) + "\" (char " + index + ")"); + } /** - * 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 + * Chop off text after a maximum length + * @param {String} text + * @param {Number} maxLength + * @returns {String} */ - 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) - }; - }; + function chop(text, maxLength) { + return text.length <= maxLength ? text : text.substr(0, 27) + "..."; + } /** - * Redraw a edge as a line with an arrow halfway the line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private + * Execute a function fn for each pair of elements in two arrays + * @param {Array | *} array1 + * @param {Array | *} array2 + * @param {function} fn */ - Edge.prototype._drawArrowCenter = function (ctx) { - var point; - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this._getLineWidth(); - - if (this.from != this.to) { - // draw line - var via = this._line(ctx); - - var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - // draw an arrow halfway the line - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); - var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); - point = { x: midpointX, y: midpointY }; - } else { - point = this._pointOnLine(0.5); - } - - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - this._label(ctx, this.label, point.x, point.y); - } + function forEach2(array1, array2, fn) { + if (Array.isArray(array1)) { + array1.forEach(function (elem1) { + if (Array.isArray(array2)) { + array2.forEach(function (elem2) { + fn(elem1, elem2); + }); + } else { + fn(elem1, array2); + } + }); } else { - // draw circle - var x, y; - var radius = 0.25 * Math.max(100, this.physics.springLength); - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width * 0.5; - y = node.y - radius; + if (Array.isArray(array2)) { + array2.forEach(function (elem2) { + fn(array1, elem2); + }); } else { - x = node.x + radius; - y = node.y - node.height * 0.5; - } - this._circle(ctx, x, y, radius); - - // draw all arrows - var angle = 0.2 * Math.PI; - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - point = this._pointOnCircle(x, y, radius, 0.5); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + fn(array1, array2); } } - }; - - Edge.prototype._pointOnBezier = function (t) { - var via = this._getViaCoordinates(); - - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; - - return { x: x, y: y }; - }; + } /** - * This function uses binary search to look for the point where the bezier curve crosses the border of the node. - * - * @param from - * @param ctx - * @returns {*} - * @private + * 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 */ - Edge.prototype._findBorderPosition = function (from, ctx) { - var maxIterations = 10; - var iteration = 0; - var low = 0; - var high = 1; - var pos, angle, distanceToBorder, distanceToNodes, difference; - var threshold = 0.2; - var node = this.to; - if (from == true) { - node = this.from; + function DOTToGraph(data) { + // parse the DOT file + var dotData = parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: {} + }; + + // copy the nodes + if (dotData.nodes) { + dotData.nodes.forEach(function (dotNode) { + var graphNode = { + id: dotNode.id, + label: String(dotNode.label || dotNode.id) + }; + merge(graphNode, dotNode.attr); + if (graphNode.image) { + graphNode.shape = "image"; + } + graphData.nodes.push(graphNode); + }); } - while (low <= high && iteration < maxIterations) { - var middle = (low + high) * 0.5; + // copy the edges + if (dotData.edges) { + /** + * Convert an edge in DOT format to an edge with VisGraph format + * @param {Object} dotEdge + * @returns {Object} graphEdge + */ + var convertEdge = function (dotEdge) { + var graphEdge = { + from: dotEdge.from, + to: dotEdge.to + }; + merge(graphEdge, dotEdge.attr); + graphEdge.style = dotEdge.type == "->" ? "arrow" : "line"; + return graphEdge; + }; - pos = this._pointOnBezier(middle); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToNodes = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToNodes; - if (Math.abs(difference) < threshold) { - break; // found - } else if (difference < 0) { - // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. - if (from == false) { - low = middle; + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; } else { - high = middle; + from = { + id: dotEdge.from + }; } - } else { - if (from == false) { - high = middle; + + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; } else { - low = middle; + to = { + id: dotEdge.to + }; } - } - iteration++; - } - pos.t = middle; + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } - return pos; - }; + forEach2(from, to, function (from, to) { + var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); - /** - * Redraw a edge as a line with an arrow - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._drawArrow = function (ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this._getLineWidth(); + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + }); + } - // set vars - var angle, length, arrowPos; + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; + } - // if not connected to itself - if (this.from != this.to) { - // draw line - this._line(ctx); + return graphData; + } - // draw arrow head - if (this.options.smoothCurves.enabled == true) { - var via = this._getViaCoordinates(); - arrowPos = this._findBorderPosition(false, ctx); - var guidePos = this._pointOnBezier(Math.max(0, arrowPos.t - 0.1)); - angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); - } else { - 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 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = this.to.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; - arrowPos = {}; - arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; - } +/***/ }, +/* 55 */ +/***/ function(module, exports, __webpack_require__) { - // draw arrow at the end of the line - length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(arrowPos.x, arrowPos.y, angle, length); - ctx.fill(); - ctx.stroke(); + "use strict"; - // draw label - if (this.label) { - var point; - if (this.options.smoothCurves.enabled == true && via != null) { - point = this._pointOnBezier(0.5); - } else { - point = this._pointOnLine(0.5); - } - this._label(ctx, this.label, point.x, point.y); - } - } else { - // draw circle - var node = this.from; - var x, y, arrow; - var radius = 0.25 * Math.max(100, this.physics.springLength); - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width * 0.5; - 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 * 0.5; - arrow = { - x: node.x, - y: y, - angle: 0.6 * Math.PI - }; + function parseGephi(gephiJSON, options) { + var edges = []; + var nodes = []; + this.options = { + edges: { + inheritColor: true + }, + nodes: { + allowedToMove: false, + parseColor: false } - ctx.beginPath(); - // 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 - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(arrow.x, arrow.y, arrow.angle, length); - ctx.fill(); - ctx.stroke(); + if (options !== undefined) { + this.options.nodes.allowedToMove = options.allowedToMove | false; + this.options.nodes.parseColor = options.parseColor | false; + this.options.edges.inheritColor = options.inheritColor | true; + } - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); - } + var gEdges = gephiJSON.edges; + var gNodes = gephiJSON.nodes; + for (var i = 0; i < gEdges.length; i++) { + var edge = {}; + var gEdge = gEdges[i]; + edge.id = gEdge.id; + edge.from = gEdge.source; + edge.to = gEdge.target; + edge.attributes = gEdge.attributes; + // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; + // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; + edge.color = gEdge.color; + edge.inheritColor = edge.color !== undefined ? false : this.options.inheritColor; + edges.push(edge); } - }; - /** - * Calculate the distance between a point (x3,y3) and a line segment from - * (x1,y1) to (x2,y2). - * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment - * @param {number} x1 - * @param {number} y1 - * @param {number} x2 - * @param {number} y2 - * @param {number} x3 - * @param {number} y3 - * @private - */ - Edge.prototype._getDistanceToEdge = function (x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - var returnValue = 0; - if (this.from != this.to) { - if (this.options.smoothCurves.enabled == true) { - var xVia, yVia; - if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { - xVia = this.via.x; - yVia = this.via.y; - } else { - var via = this._getViaCoordinates(); - xVia = via.x; - yVia = via.y; - } - var minDistance = 1000000000; - var distance; - var i, t, x, y, lastX, lastY; - for (i = 0; i < 10; i++) { - t = 0.1 * i; - x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; - y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; - if (i > 0) { - distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); - minDistance = distance < minDistance ? distance : minDistance; - } - lastX = x;lastY = y; - } - returnValue = minDistance; - } else { - returnValue = this._getDistanceToLine(x1, y1, x2, y2, x3, y3); - } - } else { - var x, y, dx, dy; - var radius = 0.25 * this.physics.springLength; - var node = this.from; - if (node.width > node.height) { - x = node.x + 0.5 * node.width; - y = node.y - radius; + for (var i = 0; i < gNodes.length; i++) { + var node = {}; + var gNode = gNodes[i]; + node.id = gNode.id; + node.attributes = gNode.attributes; + node.x = gNode.x; + node.y = gNode.y; + node.label = gNode.label; + if (this.options.nodes.parseColor == true) { + node.color = gNode.color; } else { - x = node.x + radius; - y = node.y - 0.5 * node.height; + node.color = gNode.color !== undefined ? { background: gNode.color, border: gNode.color } : undefined; } - dx = x - x3; - dy = y - y3; - returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); + node.radius = gNode.size; + node.allowedToMoveX = this.options.nodes.allowedToMove; + node.allowedToMoveY = this.options.nodes.allowedToMove; + nodes.push(node); } - if (this.labelDimensions.left < x3 && this.labelDimensions.left + this.labelDimensions.width > x3 && this.labelDimensions.top < y3 && this.labelDimensions.top + this.labelDimensions.height > y3) { - return 0; - } else { - return returnValue; - } - }; + return { nodes: nodes, edges: edges }; + } - Edge.prototype._getDistanceToLine = function (x1, y1, x2, y2, x3, y3) { - var px = x2 - x1, - py = y2 - y1, - something = px * px + py * py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; + exports.parseGephi = parseGephi; - if (u > 1) { - u = 1; - } else if (u < 0) { - u = 0; - } +/***/ }, +/* 56 */ +/***/ function(module, exports, __webpack_require__) { - var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; + "use strict"; - //# 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 + var util = __webpack_require__(1); + + /** + * @class Groups + * This class can store groups and properties specific for groups. + */ + function Groups() { + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; + this.useDefaultGroups = true; + } - return Math.sqrt(dx * dx + dy * dy); - }; /** - * This allows the zoom level of the network to influence the rendering - * - * @param scale + * default constants for group colors */ - Edge.prototype.setScale = function (scale) { - this.networkScaleInv = 1 / scale; - }; + Groups.DEFAULT = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue + { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow + { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red + { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green + { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta + { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple + { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange + { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue + { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink + { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint + { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red - Edge.prototype.select = function () { - this.selected = true; - }; + { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange + { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue + { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green + { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta + { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple + { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue + { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink + { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint - Edge.prototype.unselect = function () { - this.selected = false; - }; + { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; - Edge.prototype.positionBezierNode = function () { - if (this.via !== null && this.from !== null && this.to !== null) { - this.via.x = 0.5 * (this.from.x + this.to.x); - this.via.y = 0.5 * (this.from.y + this.to.y); - } else if (this.via !== null) { - this.via.x = 0; - this.via.y = 0; - } - }; /** - * This function draws the control nodes for the manipulator. - * In order to enable this, only set the this.controlNodesEnabled to true. - * @param ctx + * Clear all groups */ - Edge.prototype._drawControlNodes = function (ctx) { - if (this.controlNodesEnabled == true) { - if (this.controlNodes.from === null && this.controlNodes.to === null) { - var nodeIdFrom = "edgeIdFrom:".concat(this.id); - var nodeIdTo = "edgeIdTo:".concat(this.id); - var constants = { - nodes: { group: "", radius: 7, borderWidth: 2, borderWidthSelected: 2 }, - physics: { damping: 0 }, - clustering: { maxNodeSizeIncrements: 0, nodeScaling: { width: 0, height: 0, radius: 0 } } - }; - this.controlNodes.from = new Node({ id: nodeIdFrom, - shape: "dot", - color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } - }, {}, {}, constants); - this.controlNodes.to = new Node({ id: nodeIdTo, - shape: "dot", - color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } - }, {}, {}, constants); - } - - this.controlNodes.positions = {}; - if (this.controlNodes.from.selected == false) { - this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx); - this.controlNodes.from.x = this.controlNodes.positions.from.x; - this.controlNodes.from.y = this.controlNodes.positions.from.y; - } - if (this.controlNodes.to.selected == false) { - this.controlNodes.positions.to = this.getControlNodeToPosition(ctx); - this.controlNodes.to.x = this.controlNodes.positions.to.x; - this.controlNodes.to.y = this.controlNodes.positions.to.y; + Groups.prototype.clear = function () { + this.groups = {}; + this.groups.length = function () { + var i = 0; + for (var p in this) { + if (this.hasOwnProperty(p)) { + i++; + } } - - this.controlNodes.from.draw(ctx); - this.controlNodes.to.draw(ctx); - } else { - this.controlNodes = { from: null, to: null, positions: {} }; - } + return i; + }; }; - /** - * Enable control nodes. - * @private - */ - Edge.prototype._enableControlNodes = function () { - this.fromBackup = this.from; - this.toBackup = this.to; - this.controlNodesEnabled = true; - }; /** - * disable control nodes and remove from dynamicEdges from old node - * @private + * 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 */ - Edge.prototype._disableControlNodes = function () { - this.fromId = this.from.id; - this.toId = this.to.id; - if (this.fromId != this.fromBackup.id) { - // from was changed, remove edge from old 'from' node dynamic edges - this.fromBackup.detachEdge(this); - } else if (this.toId != this.toBackup.id) { - // to was changed, remove edge from old 'to' node dynamic edges - this.toBackup.detachEdge(this); + Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + if (group == undefined) { + if (this.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; + } else { + // create new group + var index = this.defaultIndex % Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.color = Groups.DEFAULT[index]; + this.groups[groupname] = group; + } } - this.fromBackup = null; - this.toBackup = null; - this.controlNodesEnabled = false; + return group; }; - /** - * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null. - * @param x - * @param y - * @returns {null} - * @private + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object */ - Edge.prototype._getSelectedControlNode = function (x, y) { - var positions = this.controlNodes.positions; - var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2)); - var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2)); - - if (fromDistance < 15) { - this.connectedNode = this.from; - this.from = this.controlNodes.from; - return this.controlNodes.from; - } else if (toDistance < 15) { - this.connectedNode = this.to; - this.to = this.controlNodes.to; - return this.controlNodes.to; - } else { - return null; - } + Groups.prototype.add = function (groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; }; + module.exports = Groups; + // 20:bright red + +/***/ }, +/* 57 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; /** - * this resets the control nodes to their original position. - * @private + * @class Images + * This class loads images and keeps them stored. */ - Edge.prototype._restoreControlNodes = function () { - if (this.controlNodes.from.selected == true) { - this.from = this.connectedNode; - this.connectedNode = null; - this.controlNodes.from.unselect(); - } else if (this.controlNodes.to.selected == true) { - this.to = this.connectedNode; - this.connectedNode = null; - this.controlNodes.to.unselect(); - } - }; + function Images() { + this.images = {}; + this.imageBroken = {}; + this.callback = undefined; + } /** - * this calculates the position of the control nodes on the edges of the parent nodes. - * - * @param ctx - * @returns {x: *, y: *} + * Set an onload callback function. This will be called each time an image + * is loaded + * @param {function} callback */ - Edge.prototype.getControlNodeFromPosition = function (ctx) { - // draw arrow head - var controlnodeFromPos; - if (this.options.smoothCurves.enabled == true) { - controlnodeFromPos = this._findBorderPosition(true, ctx); - } else { - var 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 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - - var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); - var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; - controlnodeFromPos = {}; - controlnodeFromPos.x = fromBorderPoint * this.from.x + (1 - fromBorderPoint) * this.to.x; - controlnodeFromPos.y = fromBorderPoint * this.from.y + (1 - fromBorderPoint) * this.to.y; - } - - return controlnodeFromPos; + Images.prototype.setOnloadCallback = function (callback) { + this.callback = callback; }; /** - * this calculates the position of the control nodes on the edges of the parent nodes. * - * @param ctx - * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} + * @param {string} url Url of the image + * @param {string} url Url of an image to use if the url image is not found + * @return {Image} img The image object */ - Edge.prototype.getControlNodeToPosition = function (ctx) { - // draw arrow head - var controlnodeFromPos, controlnodeToPos; - if (this.options.smoothCurves.enabled == true) { - controlnodeToPos = this._findBorderPosition(false, ctx); - } else { - var 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 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = this.to.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + Images.prototype.load = function (url, brokenUrl) { + var img = this.images[url]; // make a pointer + if (img === undefined) { + // create the image + var me = this; + img = new Image(); + img.onload = function () { + // IE11 fix -- thanks dponch! + if (this.width == 0) { + document.body.appendChild(this); + this.width = this.offsetWidth; + this.height = this.offsetHeight; + document.body.removeChild(this); + } - controlnodeToPos = {}; - controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; + if (me.callback) { + me.images[url] = img; + me.callback(this); + } + }; + + img.onerror = function () { + if (brokenUrl === undefined) { + console.error("Could not load image:", url); + delete this.src; + if (me.callback) { + me.callback(this); + } + } else { + if (me.imageBroken[url] === true) { + if (this.src == brokenUrl) { + console.error("Could not load brokenImage:", brokenUrl); + delete this.src; + if (me.callback) { + me.callback(this); + } + } else { + console.error("Could not load image:", url); + this.src = brokenUrl; + } + } else { + console.error("Could not load image:", url); + this.src = brokenUrl; + me.imageBroken[url] = true; + } + } + }; + + img.src = url; } - return controlnodeToPos; + return img; }; - module.exports = Edge; + module.exports = Images; /***/ }, -/* 55 */ +/* 58 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -27104,1163 +26827,1367 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Node; /***/ }, -/* 56 */ +/* 59 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(1); + var Node = __webpack_require__(58); /** - * @class Groups - * This class can store groups and properties specific for groups. + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color */ - function Groups() { - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; - this.useDefaultGroups = true; - } + function Edge(properties, body, networkConstants) { + if (body === undefined) { + throw "No body provided"; + } + var fields = ["edges"]; + var constants = util.selectiveBridgeObject(fields, networkConstants); + this.options = constants.edges; + this.options.smoothCurves = networkConstants.smoothCurves; + this.body = body; - /** - * default constants for group colors - */ - Groups.DEFAULT = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue - { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow - { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red - { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green - { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta - { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple - { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange - { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue - { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink - { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.title = undefined; + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + this.value = undefined; + this.selected = false; + this.hover = false; + this.labelDimensions = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached + this.dirtyLabel = true; + this.colorDirty = true; - { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red + this.from = null; // a node + this.to = null; // a node + this.via = null; // a temp node - { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange - { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue - { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green - { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta - { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple - { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue - { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink - { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint + this.fromBackup = null; // used to clean up after reconnect (used for manipulation) + this.toBackup = null; // used to clean up after reconnect (used for manipulation) - { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; + // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster + // by storing the original information we can revert to the original connection when the cluser is opened. + this.fromArray = []; + this.toArray = []; + + this.connected = false; + + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties); + this.controlNodesEnabled = false; + this.controlNodes = { from: null, to: null, positions: {} }; + this.connectedNode = null; + } /** - * Clear all groups + * Set or overwrite properties for the edge + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties */ - Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function () { - var i = 0; - for (var p in this) { - if (this.hasOwnProperty(p)) { - i++; + Edge.prototype.setProperties = function (properties) { + this.colorDirty = true; + if (!properties) { + return; + } + this.properties = properties; + + var fields = ["style", "fontSize", "fontFace", "fontColor", "fontFill", "fontStrokeWidth", "fontStrokeColor", "width", "widthSelectionMultiplier", "hoverWidth", "arrowScaleFactor", "dash", "inheritColor", "labelAlignment", "opacity", "customScalingFunction", "useGradients", "value"]; + util.selectiveDeepExtend(fields, this.options, properties); + + if (properties.from !== undefined) { + this.fromId = properties.from; + } + if (properties.to !== undefined) { + this.toId = properties.to; + } + + if (properties.id !== undefined) { + this.id = properties.id; + } + if (properties.label !== undefined) { + this.label = properties.label;this.dirtyLabel = true; + } + + if (properties.title !== undefined) { + this.title = properties.title; + } + if (properties.value !== undefined) { + this.value = properties.value; + } + if (properties.length !== undefined) { + this.physics.springLength = properties.length; + } + + if (properties.color !== undefined) { + this.options.inheritColor = false; + if (util.isString(properties.color)) { + this.options.color.color = properties.color; + this.options.color.highlight = properties.color; + } else { + if (properties.color.color !== undefined) { + this.options.color.color = properties.color.color; + } + if (properties.color.highlight !== undefined) { + this.options.color.highlight = properties.color.highlight; + } + if (properties.color.hover !== undefined) { + this.options.color.hover = properties.color.hover; } } - return i; - }; + } + + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); + + this.widthFixed = this.widthFixed || properties.width !== undefined; + this.lengthFixed = this.lengthFixed || properties.length !== undefined; + + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + + // set draw method based on style + switch (this.options.style) { + case "line": + this.draw = this._drawLine;break; + case "arrow": + this.draw = this._drawArrow;break; + case "arrow-center": + this.draw = this._drawArrowCenter;break; + case "dash-line": + this.draw = this._drawDashLine;break; + default: + this.draw = this._drawLine;break; + } }; /** - * 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 + * Connect an edge to its nodes */ - Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - if (this.useDefaultGroups === false && this.groupsArray.length > 0) { - // create new group - var index = this.groupIndex % this.groupsArray.length; - this.groupIndex++; - group = {}; - group.color = this.groups[this.groupsArray[index]]; - this.groups[groupname] = group; - } else { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; + Edge.prototype.connect = function () { + this.disconnect(); + + this.from = this.body.nodes[this.fromId] || null; + this.to = this.body.nodes[this.toId] || null; + this.connected = this.from !== null && this.to !== null; + + if (this.connected === true) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } else { + if (this.from) { + this.from.detachEdge(this); + } + if (this.to) { + this.to.detachEdge(this); } } - - 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 + * Disconnect an edge from its nodes */ - Groups.prototype.add = function (groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; + Edge.prototype.disconnect = function () { + if (this.from) { + this.from.detachEdge(this); + this.from = null; + } + if (this.to) { + this.to.detachEdge(this); + this.to = null; + } + + this.connected = false; }; - module.exports = Groups; - // 20:bright red + /** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ + Edge.prototype.getTitle = function () { + return typeof this.title === "function" ? this.title() : this.title; + }; -/***/ }, -/* 57 */ -/***/ function(module, exports, __webpack_require__) { + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + Edge.prototype.isSelected = function () { + return this.selected; + }; - "use strict"; /** - * @class Images - * This class loads images and keeps them stored. + * Retrieve the value of the edge. Can be undefined + * @return {Number} value */ - function Images() { - this.images = {}; - this.imageBroken = {}; - this.callback = undefined; - } + Edge.prototype.getValue = function () { + return this.value; + }; /** - * Set an onload callback function. This will be called each time an image - * is loaded - * @param {function} callback + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max */ - Images.prototype.setOnloadCallback = function (callback) { - this.callback = callback; + Edge.prototype.setValueRange = function (min, max, total) { + if (!this.widthFixed && this.value !== undefined) { + var scale = this.options.customScalingFunction(min, max, total, this.value); + var widthDiff = this.options.widthMax - this.options.widthMin; + this.options.width = this.options.widthMin + scale * widthDiff; + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + } }; /** - * - * @param {string} url Url of the image - * @param {string} url Url of an image to use if the url image is not found - * @return {Image} img The image object + * 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 */ - Images.prototype.load = function (url, brokenUrl) { - var img = this.images[url]; // make a pointer - if (img === undefined) { - // create the image - var me = this; - img = new Image(); - img.onload = function () { - // IE11 fix -- thanks dponch! - if (this.width == 0) { - document.body.appendChild(this); - this.width = this.offsetWidth; - this.height = this.offsetHeight; - document.body.removeChild(this); - } + Edge.prototype.draw = function (ctx) { + throw "Method draw not initialized in edge"; + }; - if (me.callback) { - me.images[url] = img; - me.callback(this); - } - }; + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge + */ + Edge.prototype.isOverlappingWith = function (obj) { + if (this.connected) { + 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; - img.onerror = function () { - if (brokenUrl === undefined) { - console.error("Could not load image:", url); - delete this.src; - if (me.callback) { - me.callback(this); - } - } else { - if (me.imageBroken[url] === true) { - if (this.src == brokenUrl) { - console.error("Could not load brokenImage:", brokenUrl); - delete this.src; - if (me.callback) { - me.callback(this); - } - } else { - console.error("Could not load image:", url); - this.src = brokenUrl; - } - } else { - console.error("Could not load image:", url); - this.src = brokenUrl; - me.imageBroken[url] = true; - } - } - }; + var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - img.src = url; + return dist < distMax; + } else { + return false; } - - return img; }; - module.exports = Images; - -/***/ }, -/* 58 */ -/***/ function(module, exports, __webpack_require__) { + Edge.prototype._getColor = function (ctx) { + var colorObj = this.options.color; + if (this.options.useGradients == true) { + var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); + var fromColor, toColor; + fromColor = this.from.options.color.highlight.border; + toColor = this.to.options.color.highlight.border; - "use strict"; - /** - * 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] - * @param {Object} [style] An object containing borderColor, - * backgroundColor, etc. - */ - function Popup(container, x, y, text, style) { - if (container) { - this.container = container; - } else { - this.container = document.body; + if (this.from.selected == false && this.to.selected == false) { + fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity); + toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity); + } else if (this.from.selected == true && this.to.selected == false) { + toColor = this.to.options.color.border; + } else if (this.from.selected == false && this.to.selected == true) { + fromColor = this.from.options.color.border; + } + grd.addColorStop(0, fromColor); + grd.addColorStop(1, toColor); + return grd; } - // x, y and text are optional, see if a style object was passed in their place - if (style === undefined) { - if (typeof x === "object") { - style = x; - x = undefined; - } else if (typeof text === "object") { - style = text; - text = undefined; - } else { - // for backwards compatibility, in case clients other than Network are creating Popup directly - style = { - fontColor: "black", - fontSize: 14, // px - fontFace: "verdana", - color: { - border: "#666", - background: "#FFFFC6" - } + if (this.colorDirty === true) { + if (this.options.inheritColor == "to") { + colorObj = { + highlight: this.to.options.color.highlight.border, + hover: this.to.options.color.hover.border, + color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) + }; + } else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { + colorObj = { + highlight: this.from.options.color.highlight.border, + hover: this.from.options.color.hover.border, + color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) }; } + this.options.color = colorObj; + this.colorDirty = false; } - this.x = 0; - this.y = 0; - this.padding = 5; - this.hidden = false; - - if (x !== undefined && y !== undefined) { - this.setPosition(x, y); - } - if (text !== undefined) { - this.setText(text); - } - - // create the frame - this.frame = document.createElement("div"); - this.frame.className = "network-tooltip"; - this.frame.style.color = style.fontColor; - this.frame.style.backgroundColor = style.color.background; - this.frame.style.borderColor = style.color.border; - this.frame.style.fontSize = style.fontSize + "px"; - this.frame.style.fontFamily = style.fontFace; - this.container.appendChild(this.frame); - } - /** - * @param {number} x Horizontal position of the popup window - * @param {number} y Vertical position of the popup window - */ - Popup.prototype.setPosition = function (x, y) { - this.x = parseInt(x); - this.y = parseInt(y); - }; - /** - * Set the content for the popup window. This can be HTML code or text. - * @param {string | Element} content - */ - Popup.prototype.setText = function (content) { - if (content instanceof Element) { - this.frame.innerHTML = ""; - this.frame.appendChild(content); + if (this.selected == true) { + return colorObj.highlight; + } else if (this.hover == true) { + return colorObj.hover; } else { - this.frame.innerHTML = content; // string containing text or HTML + return colorObj.color; } }; + /** - * Show the popup window - * @param {boolean} show Optional. Show or hide the window + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private */ - Popup.prototype.show = function (show) { - if (show === undefined) { - show = true; - } + Edge.prototype._drawLine = function (ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.lineWidth = this._getLineWidth(); - if (show) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; + if (this.from != this.to) { + // draw line + var via = this._line(ctx); - var top = this.y - height; - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; - } - if (top < this.padding) { - top = this.padding; + // draw label + var point; + if (this.label) { + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { + point = this._pointOnLine(0.5); + } + this._label(ctx, this.label, point.x, point.y); } - - var left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; + } else { + var x, y; + var radius = this.physics.springLength / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); } - if (left < this.padding) { - left = this.padding; + 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.frame.style.left = left + "px"; - this.frame.style.top = top + "px"; - this.frame.style.visibility = "visible"; - this.hidden = false; - } else { - this.hide(); + this._circle(ctx, x, y, radius); + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); } }; /** - * Hide the popup window - */ - Popup.prototype.hide = function () { - this.hidden = true; - this.frame.style.visibility = "hidden"; - }; - - module.exports = Popup; - -/***/ }, -/* 59 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - /** - * Parse a text source containing data in DOT language into a JSON object. - * The object contains two lists: one with nodes and one with edges. - * - * DOT language reference: http://www.graphviz.org/doc/info/lang.html - * - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graph An object containing two parameters: - * {Object[]} nodes - * {Object[]} edges + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width + * @private */ - function parseDOT(data) { - dot = data; - return parseGraph(); - } - - // token types enumeration - var TOKENTYPE = { - NULL: 0, - DELIMITER: 1, - IDENTIFIER: 2, - UNKNOWN: 3 - }; - - // map with all delimiters - var DELIMITERS = { - "{": true, - "}": true, - "[": true, - "]": true, - ";": true, - "=": true, - ",": true, - - "->": true, - "--": true + Edge.prototype._getLineWidth = function () { + if (this.selected == true) { + return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3 * this.networkScaleInv); + } else { + if (this.hover == true) { + return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3 * this.networkScaleInv); + } else { + return Math.max(this.options.width, 0.3 * this.networkScaleInv); + } + } }; - var dot = ""; // current dot file - var index = 0; // current index in dot file - var c = ""; // current token character in expr - var token = ""; // current token - var tokenType = TOKENTYPE.NULL; // type of the token - - /** - * Get the first character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function first() { - index = 0; - c = dot.charAt(0); - } - - /** - * Get the next character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function next() { - index++; - c = dot.charAt(index); - } + Edge.prototype._getViaCoordinates = function () { + if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) { + return this.via; + } else if (this.options.smoothCurves.enabled == false) { + return { x: 0, y: 0 }; + } else { + var xVia = null; + var yVia = null; + var factor = this.options.smoothCurves.roundness; + var type = this.options.smoothCurves.type; + var dx = Math.abs(this.from.x - this.to.x); + var dy = Math.abs(this.from.y - this.to.y); + if (type == "discrete" || type == "diagonalCross") { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + } + } + if (type == "discrete") { + xVia = dx < factor * dy ? this.from.x : xVia; + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + } + } + if (type == "discrete") { + yVia = dy < factor * dx ? this.from.y : yVia; + } + } + } else if (type == "straightCross") { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + // up - down + xVia = this.from.x; + if (this.from.y < this.to.y) { + yVia = this.to.y - (1 - factor) * dy; + } else { + yVia = this.to.y + (1 - factor) * dy; + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + // left - right + if (this.from.x < this.to.x) { + xVia = this.to.x - (1 - factor) * dx; + } else { + xVia = this.to.x + (1 - factor) * dx; + } + yVia = this.from.y; + } + } else if (type == "horizontal") { + if (this.from.x < this.to.x) { + xVia = this.to.x - (1 - factor) * dx; + } else { + xVia = this.to.x + (1 - factor) * dx; + } + yVia = this.from.y; + } else if (type == "vertical") { + xVia = this.from.x; + if (this.from.y < this.to.y) { + yVia = this.to.y - (1 - factor) * dy; + } else { + yVia = this.to.y + (1 - factor) * dy; + } + } else if (type == "curvedCW") { + var dx = this.to.x - this.from.x; + var dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - /** - * Preview the next character from the dot file. - * @return {String} cNext - */ - function nextPreview() { - return dot.charAt(index + 1); - } + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); - /** - * Test whether given character is alphabetic or numeric - * @param {String} c - * @return {Boolean} isAlphaNumeric - */ - var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; - function isAlphaNumeric(c) { - return regexAlphaNumeric.test(c); - } + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } else if (type == "curvedCCW") { + var dx = this.to.x - this.from.x; + var dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - /** - * Merge all properties of object b into object b - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - function merge(a, b) { - if (!a) { - a = {}; - } + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); - if (b) { - for (var name in b) { - if (b.hasOwnProperty(name)) { - a[name] = b[name]; + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } else { + // continuous + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } + } } } + + + return { x: xVia, y: yVia }; } - return a; - } + }; /** - * Set a value in an object, where the provided parameter name can be a - * path with nested parameters. For example: - * - * var obj = {a: 2}; - * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} - * - * @param {Object} obj - * @param {String} path A parameter name or dot-separated parameter path, - * like "color.highlight.border". - * @param {*} value + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private */ - function setValue(obj, path, value) { - var keys = path.split("."); - var o = obj; - while (keys.length) { - var key = keys.shift(); - if (keys.length) { - // this isn't the end point - if (!o[key]) { - o[key] = {}; + Edge.prototype._line = function (ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + if (this.options.smoothCurves.enabled == true) { + if (this.options.smoothCurves.dynamic == false) { + var via = this._getViaCoordinates(); + if (via.x == null) { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; + } else { + // this.via.x = via.x; + // this.via.y = via.y; + ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); + ctx.stroke(); + //ctx.circle(via.x,via.y,2) + //ctx.stroke(); + return via; } - o = o[key]; } else { - // this is the end point - o[key] = value; + ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); + ctx.stroke(); + return this.via; } + } else { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; } - } + }; /** - * Add a node to a graph object. If there is already a node with - * the same id, their attributes will be merged. - * @param {Object} graph - * @param {Object} node + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private */ - function addNode(graph, node) { - var i, len; - var current = null; + 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(); + }; - // find root graph (in case of subgraph) - var graphs = [graph]; // list with all graphs from current graph to root graph - var root = graph; - while (root.parent) { - graphs.push(root.parent); - root = root.parent; - } + /** + * Draw label with white background and with the middle at (x, y) + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} x + * @param {Number} y + * @private + */ + Edge.prototype._label = function (ctx, text, x, y) { + if (text) { + ctx.font = (this.from.selected || this.to.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace; + var yLine; - // find existing node (at root level) by its id - if (root.nodes) { - for (i = 0, len = root.nodes.length; i < len; i++) { - if (node.id === root.nodes[i].id) { - current = root.nodes[i]; - break; + if (this.dirtyLabel == true) { + var lines = String(text).split("\n"); + var lineCount = lines.length; + var fontSize = Number(this.options.fontSize); + yLine = y + (1 - lineCount) / 2 * fontSize; + + var width = ctx.measureText(lines[0]).width; + for (var i = 1; i < lineCount; i++) { + var lineWidth = ctx.measureText(lines[i]).width; + width = lineWidth > width ? lineWidth : width; } - } - } + var height = this.options.fontSize * lineCount; + var left = x - width / 2; + var top = y - height / 2; - if (!current) { - // this is a new node - current = { - id: node.id - }; - if (graph.node) { - // clone default attributes - current.attr = merge(current.attr, graph.node); + // cache + this.labelDimensions = { top: top, left: left, width: width, height: height, yLine: yLine }; } - } - // add node to this (sub)graph and all its parent graphs - for (i = graphs.length - 1; i >= 0; i--) { - var g = graphs[i]; + var yLine = this.labelDimensions.yLine; - if (!g.nodes) { - g.nodes = []; - } - if (g.nodes.indexOf(current) == -1) { - g.nodes.push(current); + ctx.save(); + + if (this.options.labelAlignment != "horizontal") { + ctx.translate(x, yLine); + this._rotateForLabelAlignment(ctx); + x = 0; + yLine = 0; } - } - // merge attributes - if (node.attr) { - current.attr = merge(current.attr, node.attr); - } - } - /** - * Add an edge to a graph object - * @param {Object} graph - * @param {Object} edge - */ - function addEdge(graph, edge) { - if (!graph.edges) { - graph.edges = []; - } - graph.edges.push(edge); - if (graph.edge) { - var attr = merge({}, graph.edge); // clone default attributes - edge.attr = merge(attr, edge.attr); // merge attributes + this._drawLabelRect(ctx); + this._drawLabelText(ctx, x, yLine, lines, lineCount, fontSize); + + ctx.restore(); } - } + }; /** - * Create an edge to a graph object - * @param {Object} graph - * @param {String | Number | Object} from - * @param {String | Number | Object} to - * @param {String} type - * @param {Object | null} attr - * @return {Object} edge + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx + * @private */ - function createEdge(graph, from, to, type, attr) { - var edge = { - from: from, - to: to, - type: type - }; + Edge.prototype._rotateForLabelAlignment = function (ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); - if (graph.edge) { - edge.attr = merge({}, graph.edge); // clone default attributes + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; } - edge.attr = merge(edge.attr || {}, attr); // merge attributes - return edge; - } + ctx.rotate(angleInDegrees); + }; /** - * Get next token in the current dot file. - * The token and token type are available as token and tokenType + * Draws the label rectangle + * @param {CanvasRenderingContext2D} ctx + * @param {String} labelAlignment + * @private */ - function getToken() { - tokenType = TOKENTYPE.NULL; - token = ""; - - // skip over whitespaces - while (c == " " || c == "\t" || c == "\n" || c == "\r") { - // space, tab, enter - next(); - } + Edge.prototype._drawLabelRect = function (ctx) { + if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { + ctx.fillStyle = this.options.fontFill; - do { - var isComment = false; + var lineMargin = 2; - // skip comment - if (c == "#") { - // find the previous non-space character - var i = index - 1; - while (dot.charAt(i) == " " || dot.charAt(i) == "\t") { - i--; - } - if (dot.charAt(i) == "\n" || dot.charAt(i) == "") { - // the # is at the start of a line, this is indeed a line comment - while (c != "" && c != "\n") { - next(); - } - isComment = true; - } - } - if (c == "/" && nextPreview() == "/") { - // skip line comment - while (c != "" && c != "\n") { - next(); - } - isComment = true; - } - if (c == "/" && nextPreview() == "*") { - // skip block comment - while (c != "") { - if (c == "*" && nextPreview() == "/") { - // end of block comment found. skip these last two characters - next(); - next(); - break; - } else { - next(); - } - } - isComment = true; + if (this.options.labelAlignment == "line-center") { + ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height); + } else if (this.options.labelAlignment == "line-above") { + ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height); + } else if (this.options.labelAlignment == "line-below") { + ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height); + } else { + ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height); } + } + }; - // skip over whitespaces - while (c == " " || c == "\t" || c == "\n" || c == "\r") { - // space, tab, enter - next(); - } - } while (isComment); + /** + * Draws the label text + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} yLine + * @param {Array} lines + * @param {Number} lineCount + * @param {Number} fontSize + * @private + */ + Edge.prototype._drawLabelText = function (ctx, x, yLine, lines, lineCount, fontSize) { + // draw text + ctx.fillStyle = this.options.fontColor || "black"; + ctx.textAlign = "center"; - // check for end of dot file - if (c == "") { - // token is still empty - tokenType = TOKENTYPE.DELIMITER; - return; + // check for label alignment + if (this.options.labelAlignment != "horizontal") { + var lineMargin = 2; + if (this.options.labelAlignment == "line-above") { + ctx.textBaseline = "alphabetic"; + yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers + } else if (this.options.labelAlignment == "line-below") { + ctx.textBaseline = "hanging"; + yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers + } else { + ctx.textBaseline = "middle"; + } + } else { + ctx.textBaseline = "middle"; } - // check for delimiters consisting of 2 characters - var c2 = c + nextPreview(); - if (DELIMITERS[c2]) { - tokenType = TOKENTYPE.DELIMITER; - token = c2; - next(); - next(); - return; + // check for strokeWidth + if (this.options.fontStrokeWidth > 0) { + ctx.lineWidth = this.options.fontStrokeWidth; + ctx.strokeStyle = this.options.fontStrokeColor; + ctx.lineJoin = "round"; } - - // check for delimiters consisting of 1 character - if (DELIMITERS[c]) { - tokenType = TOKENTYPE.DELIMITER; - token = c; - next(); - return; + for (var i = 0; i < lineCount; i++) { + if (this.options.fontStrokeWidth > 0) { + ctx.strokeText(lines[i], x, yLine); + } + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; } + }; - // check for an identifier (number or string) - // TODO: more precise parsing of numbers/strings (and the port separator ':') - if (isAlphaNumeric(c) || c == "-") { - token += c; - next(); + /** + * Redraw a edge as a dashed line + * Draw this edge in the given canvas + * @author David Jordan + * @date 2012-08-08 + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._drawDashLine = function (ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.lineWidth = this._getLineWidth(); - while (isAlphaNumeric(c)) { - token += c; - next(); - } - if (token == "false") { - token = false; // convert to boolean - } else if (token == "true") { - token = true; // convert to boolean - } else if (!isNaN(Number(token))) { - token = Number(token); // convert to number + var via = null; + // only firefox and chrome support this method, else we use the legacy one. + if (ctx.setLineDash !== undefined) { + ctx.save(); + // configure the dash pattern + var pattern = [0]; + if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) { + pattern = [this.options.dash.length, this.options.dash.gap]; + } else { + pattern = [5, 5]; } - tokenType = TOKENTYPE.IDENTIFIER; - return; - } - // check for a string enclosed by double quotes - if (c == "\"") { - next(); - while (c != "" && (c != "\"" || c == "\"" && nextPreview() == "\"")) { - token += c; - if (c == "\"") { - // skip the escape character - next(); + // set dash settings for chrome or firefox + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; + + // draw the line + via = this._line(ctx); + + // restore the dash settings. + ctx.setLineDash([0]); + ctx.lineDashOffset = 0; + ctx.restore(); + } else { + // unsupporting smooth lines + // draw dashed line + ctx.beginPath(); + ctx.lineCap = "round"; + if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap, this.options.dash.altLength, this.options.dash.gap]); + } else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap]); + } else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); } - next(); - } - if (c != "\"") { - throw newSyntaxError("End of string \" expected"); + ctx.stroke(); + } + + // draw label + if (this.label) { + var point; + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { + point = this._pointOnLine(0.5); } - next(); - tokenType = TOKENTYPE.IDENTIFIER; - return; + this._label(ctx, this.label, point.x, point.y); } + }; - // something unknown is found, wrong characters, a syntax error - tokenType = TOKENTYPE.UNKNOWN; - while (c != "") { - token += c; - next(); - } - throw new SyntaxError("Syntax error in part \"" + chop(token, 30) + "\""); - } + /** + * Get a point on a line + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ + Edge.prototype._pointOnLine = function (percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + }; + }; /** - * Parse a graph. - * @returns {Object} graph + * 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 */ - function parseGraph() { - var 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) + }; + }; - first(); - getToken(); + /** + * Redraw a edge as a line with an arrow halfway the line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._drawArrowCenter = function (ctx) { + var point; + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this._getLineWidth(); - // optional strict keyword - if (token == "strict") { - graph.strict = true; - getToken(); - } + if (this.from != this.to) { + // draw line + var via = this._line(ctx); - // graph or digraph keyword - if (token == "graph" || token == "digraph") { - graph.type = token; - getToken(); - } + var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + // draw an arrow halfway the line + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { + point = this._pointOnLine(0.5); + } - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - graph.id = token; - getToken(); - } + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); - // open angle bracket - if (token != "{") { - throw newSyntaxError("Angle bracket { expected"); - } - getToken(); + // draw label + if (this.label) { + this._label(ctx, this.label, point.x, point.y); + } + } else { + // draw circle + var x, y; + var radius = 0.25 * Math.max(100, this.physics.springLength); + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width * 0.5; + y = node.y - radius; + } else { + x = node.x + radius; + y = node.y - node.height * 0.5; + } + this._circle(ctx, x, y, radius); - // statements - parseStatements(graph); + // draw all arrows + var angle = 0.2 * Math.PI; + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + point = this._pointOnCircle(x, y, radius, 0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); - // close angle bracket - if (token != "}") { - throw newSyntaxError("Angle bracket } expected"); + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } } - getToken(); + }; - // end of file - if (token !== "") { - throw newSyntaxError("End of file expected"); - } - getToken(); + Edge.prototype._pointOnBezier = function (t) { + var via = this._getViaCoordinates(); - // remove temporary default properties - delete graph.node; - delete graph.edge; - delete graph.graph; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; - return graph; - } + return { x: x, y: y }; + }; /** - * Parse a list with statements. - * @param {Object} graph + * This function uses binary search to look for the point where the bezier curve crosses the border of the node. + * + * @param from + * @param ctx + * @returns {*} + * @private */ - function parseStatements(graph) { - while (token !== "" && token != "}") { - parseStatement(graph); - if (token == ";") { - getToken(); - } + Edge.prototype._findBorderPosition = function (from, ctx) { + var maxIterations = 10; + var iteration = 0; + var low = 0; + var high = 1; + var pos, angle, distanceToBorder, distanceToNodes, difference; + var threshold = 0.2; + var node = this.to; + if (from == true) { + node = this.from; } - } - - /** - * Parse a single statement. Can be a an attribute statement, node - * statement, a series of node statements and edge statements, or a - * parameter. - * @param {Object} graph - */ - function parseStatement(graph) { - // parse subgraph - var subgraph = parseSubgraph(graph); - if (subgraph) { - // edge statements - parseEdge(graph, subgraph); - return; - } + while (low <= high && iteration < maxIterations) { + var middle = (low + high) * 0.5; - // parse an attribute statement - var attr = parseAttributeStatement(graph); - if (attr) { - return; - } + pos = this._pointOnBezier(middle); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToNodes = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToNodes; + if (Math.abs(difference) < threshold) { + break; // found + } else if (difference < 0) { + // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + if (from == false) { + low = middle; + } else { + high = middle; + } + } else { + if (from == false) { + high = middle; + } else { + low = middle; + } + } - // parse node - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier expected"); + iteration++; } - var id = token; // id can be a string or a number - getToken(); + pos.t = middle; - if (token == "=") { - // id statement - getToken(); - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier expected"); - } - graph[id] = token; - getToken(); - // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " - } else { - parseNodeStatement(graph, id); - } - } + return pos; + }; /** - * Parse a subgraph - * @param {Object} graph parent graph object - * @return {Object | null} subgraph + * Redraw a edge as a line with an arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private */ - function parseSubgraph(graph) { - var subgraph = null; + Edge.prototype._drawArrow = function (ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this._getLineWidth(); - // optional subgraph keyword - if (token == "subgraph") { - subgraph = {}; - subgraph.type = "subgraph"; - getToken(); + // set vars + var angle, length, arrowPos; - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - subgraph.id = token; - getToken(); - } - } + // if not connected to itself + if (this.from != this.to) { + // draw line + this._line(ctx); - // open angle bracket - if (token == "{") { - getToken(); + // draw arrow head + if (this.options.smoothCurves.enabled == true) { + var via = this._getViaCoordinates(); + arrowPos = this._findBorderPosition(false, ctx); + var guidePos = this._pointOnBezier(Math.max(0, arrowPos.t - 0.1)); + angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + } else { + 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 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = this.to.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - if (!subgraph) { - subgraph = {}; + arrowPos = {}; + arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; + arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; } - subgraph.parent = graph; - subgraph.node = graph.node; - subgraph.edge = graph.edge; - subgraph.graph = graph.graph; - // statements - parseStatements(subgraph); + // draw arrow at the end of the line + length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + ctx.arrow(arrowPos.x, arrowPos.y, angle, length); + ctx.fill(); + ctx.stroke(); - // close angle bracket - if (token != "}") { - throw newSyntaxError("Angle bracket } expected"); + // draw label + if (this.label) { + var point; + if (this.options.smoothCurves.enabled == true && via != null) { + point = this._pointOnBezier(0.5); + } else { + point = this._pointOnLine(0.5); + } + this._label(ctx, this.label, point.x, point.y); } - getToken(); + } else { + // draw circle + var node = this.from; + var x, y, arrow; + var radius = 0.25 * Math.max(100, this.physics.springLength); + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width * 0.5; + 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 * 0.5; + arrow = { + x: node.x, + y: y, + angle: 0.6 * Math.PI + }; + } + ctx.beginPath(); + // 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(); - // remove temporary default properties - delete subgraph.node; - delete subgraph.edge; - delete subgraph.graph; - delete subgraph.parent; + // draw all arrows + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + ctx.arrow(arrow.x, arrow.y, arrow.angle, length); + ctx.fill(); + ctx.stroke(); - // register at the parent graph - if (!graph.subgraphs) { - graph.subgraphs = []; + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); } - graph.subgraphs.push(subgraph); } - - return subgraph; - } + }; /** - * parse an attribute statement like "node [shape=circle fontSize=16]". - * Available keywords are 'node', 'edge', 'graph'. - * The previous list with default attributes will be replaced - * @param {Object} graph - * @returns {String | null} keyword Returns the name of the parsed attribute - * (node, edge, graph), or null if nothing - * is parsed. + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 + * @private */ - function parseAttributeStatement(graph) { - // attribute statements - if (token == "node") { - getToken(); - - // node attributes - graph.node = parseAttributeList(); - return "node"; - } else if (token == "edge") { - getToken(); - - // edge attributes - graph.edge = parseAttributeList(); - return "edge"; - } else if (token == "graph") { - getToken(); - - // graph attributes - graph.graph = parseAttributeList(); - return "graph"; + Edge.prototype._getDistanceToEdge = function (x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + var returnValue = 0; + if (this.from != this.to) { + if (this.options.smoothCurves.enabled == true) { + var xVia, yVia; + if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { + xVia = this.via.x; + yVia = this.via.y; + } else { + var via = this._getViaCoordinates(); + xVia = via.x; + yVia = via.y; + } + var minDistance = 1000000000; + var distance; + var i, t, x, y, lastX, lastY; + for (i = 0; i < 10; i++) { + t = 0.1 * i; + x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; + y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; + if (i > 0) { + distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); + minDistance = distance < minDistance ? distance : minDistance; + } + lastX = x;lastY = y; + } + returnValue = minDistance; + } else { + returnValue = this._getDistanceToLine(x1, y1, x2, y2, x3, y3); + } + } else { + var x, y, dx, dy; + var radius = 0.25 * this.physics.springLength; + var node = this.from; + if (node.width > node.height) { + x = node.x + 0.5 * node.width; + y = node.y - radius; + } else { + x = node.x + radius; + y = node.y - 0.5 * node.height; + } + dx = x - x3; + dy = y - y3; + returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); } - return null; - } - - /** - * parse a node statement - * @param {Object} graph - * @param {String | Number} id - */ - function parseNodeStatement(graph, id) { - // node statement - var node = { - id: id - }; - var attr = parseAttributeList(); - if (attr) { - node.attr = attr; + if (this.labelDimensions.left < x3 && this.labelDimensions.left + this.labelDimensions.width > x3 && this.labelDimensions.top < y3 && this.labelDimensions.top + this.labelDimensions.height > y3) { + return 0; + } else { + return returnValue; } - addNode(graph, node); - - // edge statements - parseEdge(graph, id); - } + }; - /** - * Parse an edge or a series of edges - * @param {Object} graph - * @param {String | Number} from Id of the from node - */ - function parseEdge(graph, from) { - while (token == "->" || token == "--") { - var to; - var type = token; - getToken(); + Edge.prototype._getDistanceToLine = function (x1, y1, x2, y2, x3, y3) { + var px = x2 - x1, + py = y2 - y1, + something = px * px + py * py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; - var subgraph = parseSubgraph(graph); - if (subgraph) { - to = subgraph; - } else { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier or subgraph expected"); - } - to = token; - addNode(graph, { - id: to - }); - getToken(); - } + if (u > 1) { + u = 1; + } else if (u < 0) { + u = 0; + } - // parse edge attributes - var attr = parseAttributeList(); + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; - // create edge - var edge = createEdge(graph, from, to, type, attr); - addEdge(graph, edge); + //# 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 - from = to; - } - } + return Math.sqrt(dx * dx + dy * dy); + }; /** - * Parse a set with attributes, - * for example [label="1.000", shape=solid] - * @return {Object | null} attr + * This allows the zoom level of the network to influence the rendering + * + * @param scale */ - function parseAttributeList() { - var attr = null; + Edge.prototype.setScale = function (scale) { + this.networkScaleInv = 1 / scale; + }; - while (token == "[") { - getToken(); - attr = {}; - while (token !== "" && token != "]") { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Attribute name expected"); - } - var name = token; - getToken(); - if (token != "=") { - throw newSyntaxError("Equal sign = expected"); - } - getToken(); + Edge.prototype.select = function () { + this.selected = true; + }; - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Attribute value expected"); - } - var value = token; - setValue(attr, name, value); // name can be a path + Edge.prototype.unselect = function () { + this.selected = false; + }; - getToken(); - if (token == ",") { - getToken(); - } + Edge.prototype.positionBezierNode = function () { + if (this.via !== null && this.from !== null && this.to !== null) { + this.via.x = 0.5 * (this.from.x + this.to.x); + this.via.y = 0.5 * (this.from.y + this.to.y); + } else if (this.via !== null) { + this.via.x = 0; + this.via.y = 0; + } + }; + + /** + * This function draws the control nodes for the manipulator. + * In order to enable this, only set the this.controlNodesEnabled to true. + * @param ctx + */ + Edge.prototype._drawControlNodes = function (ctx) { + if (this.controlNodesEnabled == true) { + if (this.controlNodes.from === null && this.controlNodes.to === null) { + var nodeIdFrom = "edgeIdFrom:".concat(this.id); + var nodeIdTo = "edgeIdTo:".concat(this.id); + var constants = { + nodes: { group: "", radius: 7, borderWidth: 2, borderWidthSelected: 2 }, + physics: { damping: 0 }, + clustering: { maxNodeSizeIncrements: 0, nodeScaling: { width: 0, height: 0, radius: 0 } } + }; + this.controlNodes.from = new Node({ id: nodeIdFrom, + shape: "dot", + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } + }, {}, {}, constants); + this.controlNodes.to = new Node({ id: nodeIdTo, + shape: "dot", + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } + }, {}, {}, constants); } - if (token != "]") { - throw newSyntaxError("Bracket ] expected"); + this.controlNodes.positions = {}; + if (this.controlNodes.from.selected == false) { + this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx); + this.controlNodes.from.x = this.controlNodes.positions.from.x; + this.controlNodes.from.y = this.controlNodes.positions.from.y; + } + if (this.controlNodes.to.selected == false) { + this.controlNodes.positions.to = this.getControlNodeToPosition(ctx); + this.controlNodes.to.x = this.controlNodes.positions.to.x; + this.controlNodes.to.y = this.controlNodes.positions.to.y; } - getToken(); - } - return attr; - } + this.controlNodes.from.draw(ctx); + this.controlNodes.to.draw(ctx); + } else { + this.controlNodes = { from: null, to: null, positions: {} }; + } + }; /** - * Create a syntax error with extra information on current token and index. - * @param {String} message - * @returns {SyntaxError} err + * Enable control nodes. + * @private */ - function newSyntaxError(message) { - return new SyntaxError(message + ", got \"" + chop(token, 30) + "\" (char " + index + ")"); - } + Edge.prototype._enableControlNodes = function () { + this.fromBackup = this.from; + this.toBackup = this.to; + this.controlNodesEnabled = true; + }; /** - * Chop off text after a maximum length - * @param {String} text - * @param {Number} maxLength - * @returns {String} + * disable control nodes and remove from dynamicEdges from old node + * @private */ - function chop(text, maxLength) { - return text.length <= maxLength ? text : text.substr(0, 27) + "..."; - } + Edge.prototype._disableControlNodes = function () { + this.fromId = this.from.id; + this.toId = this.to.id; + if (this.fromId != this.fromBackup.id) { + // from was changed, remove edge from old 'from' node dynamic edges + this.fromBackup.detachEdge(this); + } else if (this.toId != this.toBackup.id) { + // to was changed, remove edge from old 'to' node dynamic edges + this.toBackup.detachEdge(this); + } + + this.fromBackup = null; + this.toBackup = null; + this.controlNodesEnabled = false; + }; + /** - * Execute a function fn for each pair of elements in two arrays - * @param {Array | *} array1 - * @param {Array | *} array2 - * @param {function} fn + * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null. + * @param x + * @param y + * @returns {null} + * @private */ - function forEach2(array1, array2, fn) { - if (Array.isArray(array1)) { - array1.forEach(function (elem1) { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(elem1, elem2); - }); - } else { - fn(elem1, array2); - } - }); + Edge.prototype._getSelectedControlNode = function (x, y) { + var positions = this.controlNodes.positions; + var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2)); + var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2)); + + if (fromDistance < 15) { + this.connectedNode = this.from; + this.from = this.controlNodes.from; + return this.controlNodes.from; + } else if (toDistance < 15) { + this.connectedNode = this.to; + this.to = this.controlNodes.to; + return this.controlNodes.to; } else { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(array1, elem2); - }); - } else { - fn(array1, array2); - } + return null; } - } + }; + /** - * 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 + * this resets the control nodes to their original position. + * @private */ - function DOTToGraph(data) { - // parse the DOT file - var dotData = parseDOT(data); - var graphData = { - nodes: [], - edges: [], - options: {} - }; - - // copy the nodes - if (dotData.nodes) { - dotData.nodes.forEach(function (dotNode) { - var graphNode = { - id: dotNode.id, - label: String(dotNode.label || dotNode.id) - }; - merge(graphNode, dotNode.attr); - if (graphNode.image) { - graphNode.shape = "image"; - } - graphData.nodes.push(graphNode); - }); + Edge.prototype._restoreControlNodes = function () { + if (this.controlNodes.from.selected == true) { + this.from = this.connectedNode; + this.connectedNode = null; + this.controlNodes.from.unselect(); + } else if (this.controlNodes.to.selected == true) { + this.to = this.connectedNode; + this.connectedNode = null; + this.controlNodes.to.unselect(); } + }; - // copy the edges - if (dotData.edges) { - /** - * Convert an edge in DOT format to an edge with VisGraph format - * @param {Object} dotEdge - * @returns {Object} graphEdge - */ - var convertEdge = function (dotEdge) { - var graphEdge = { - from: dotEdge.from, - to: dotEdge.to - }; - merge(graphEdge, dotEdge.attr); - graphEdge.style = dotEdge.type == "->" ? "arrow" : "line"; - return graphEdge; - }; - - dotData.edges.forEach(function (dotEdge) { - var from, to; - if (dotEdge.from instanceof Object) { - from = dotEdge.from.nodes; - } else { - from = { - id: dotEdge.from - }; - } - - if (dotEdge.to instanceof Object) { - to = dotEdge.to.nodes; - } else { - to = { - id: dotEdge.to - }; - } + /** + * this calculates the position of the control nodes on the edges of the parent nodes. + * + * @param ctx + * @returns {x: *, y: *} + */ + Edge.prototype.getControlNodeFromPosition = function (ctx) { + // draw arrow head + var controlnodeFromPos; + if (this.options.smoothCurves.enabled == true) { + controlnodeFromPos = this._findBorderPosition(true, ctx); + } else { + var 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 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - if (dotEdge.from instanceof Object && dotEdge.from.edges) { - dotEdge.from.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } + var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); + var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; + controlnodeFromPos = {}; + controlnodeFromPos.x = fromBorderPoint * this.from.x + (1 - fromBorderPoint) * this.to.x; + controlnodeFromPos.y = fromBorderPoint * this.from.y + (1 - fromBorderPoint) * this.to.y; + } - forEach2(from, to, function (from, to) { - var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); + return controlnodeFromPos; + }; - if (dotEdge.to instanceof Object && dotEdge.to.edges) { - dotEdge.to.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - }); - } + /** + * this calculates the position of the control nodes on the edges of the parent nodes. + * + * @param ctx + * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} + */ + Edge.prototype.getControlNodeToPosition = function (ctx) { + // draw arrow head + var controlnodeFromPos, controlnodeToPos; + if (this.options.smoothCurves.enabled == true) { + controlnodeToPos = this._findBorderPosition(false, ctx); + } else { + var 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 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = this.to.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - // copy the options - if (dotData.attr) { - graphData.options = dotData.attr; + controlnodeToPos = {}; + controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; + controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; } - return graphData; - } + return controlnodeToPos; + }; - // exports - exports.parseDOT = parseDOT; - exports.DOTToGraph = DOTToGraph; + module.exports = Edge; /***/ }, /* 60 */ @@ -28268,64 +28195,138 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - function parseGephi(gephiJSON, options) { - var edges = []; - var nodes = []; - this.options = { - edges: { - inheritColor: true - }, - nodes: { - allowedToMove: false, - parseColor: false + /** + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} [x] + * @param {Number} [y] + * @param {String} [text] + * @param {Object} [style] An object containing borderColor, + * backgroundColor, etc. + */ + function Popup(container, x, y, text, style) { + if (container) { + this.container = container; + } else { + this.container = document.body; + } + + // x, y and text are optional, see if a style object was passed in their place + if (style === undefined) { + if (typeof x === "object") { + style = x; + x = undefined; + } else if (typeof text === "object") { + style = text; + text = undefined; + } else { + // for backwards compatibility, in case clients other than Network are creating Popup directly + style = { + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" + } + }; } - }; + } - if (options !== undefined) { - this.options.nodes.allowedToMove = options.allowedToMove | false; - this.options.nodes.parseColor = options.parseColor | false; - this.options.edges.inheritColor = options.inheritColor | true; + this.x = 0; + this.y = 0; + this.padding = 5; + this.hidden = false; + + if (x !== undefined && y !== undefined) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); } - var gEdges = gephiJSON.edges; - var gNodes = gephiJSON.nodes; - for (var i = 0; i < gEdges.length; i++) { - var edge = {}; - var gEdge = gEdges[i]; - edge.id = gEdge.id; - edge.from = gEdge.source; - edge.to = gEdge.target; - edge.attributes = gEdge.attributes; - // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; - // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; - edge.color = gEdge.color; - edge.inheritColor = edge.color !== undefined ? false : this.options.inheritColor; - edges.push(edge); + // create the frame + this.frame = document.createElement("div"); + this.frame.className = "network-tooltip"; + this.frame.style.color = style.fontColor; + this.frame.style.backgroundColor = style.color.background; + this.frame.style.borderColor = style.color.border; + this.frame.style.fontSize = style.fontSize + "px"; + this.frame.style.fontFamily = style.fontFace; + this.container.appendChild(this.frame); + } + + /** + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window + */ + Popup.prototype.setPosition = function (x, y) { + this.x = parseInt(x); + this.y = parseInt(y); + }; + + /** + * Set the content for the popup window. This can be HTML code or text. + * @param {string | Element} content + */ + Popup.prototype.setText = function (content) { + if (content instanceof Element) { + this.frame.innerHTML = ""; + this.frame.appendChild(content); + } else { + this.frame.innerHTML = content; // string containing text or HTML + } + }; + + /** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ + Popup.prototype.show = function (show) { + if (show === undefined) { + show = true; } - for (var i = 0; i < gNodes.length; i++) { - var node = {}; - var gNode = gNodes[i]; - node.id = gNode.id; - node.attributes = gNode.attributes; - node.x = gNode.x; - node.y = gNode.y; - node.label = gNode.label; - if (this.options.nodes.parseColor == true) { - node.color = gNode.color; - } else { - node.color = gNode.color !== undefined ? { background: gNode.color, border: gNode.color } : undefined; + 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; } - node.radius = gNode.size; - node.allowedToMoveX = this.options.nodes.allowedToMove; - node.allowedToMoveY = this.options.nodes.allowedToMove; - nodes.push(node); + 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"; + this.hidden = false; + } else { + this.hide(); } + }; - return { nodes: nodes, edges: edges }; - } + /** + * Hide the popup window + */ + Popup.prototype.hide = function () { + this.hidden = true; + this.frame.style.visibility = "hidden"; + }; - exports.parseGephi = parseGephi; + module.exports = Popup; /***/ }, /* 61 */ @@ -28494,7 +28495,7 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - var Node = __webpack_require__(55); + var Node = __webpack_require__(58); /** * @@ -29197,8 +29198,8 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; var util = __webpack_require__(1); - var Node = __webpack_require__(55); - var Edge = __webpack_require__(54); + var Node = __webpack_require__(58); + var Edge = __webpack_require__(59); var Hammer = __webpack_require__(19); var hammerUtil = __webpack_require__(24); @@ -33018,8 +33019,9 @@ return /******/ (function(modules) { // webpackBootstrap this.redrawRequested = false; this.renderTimer = false; this.requiresTimeout = true; - this.continueRendering = false; + this.renderingActive = false; this.renderRequests = 0; + this.pixelRatio = undefined; this.canvasTopLeft = { x: 0, y: 0 }; this.canvasBottomRight = { x: 0, y: 0 }; @@ -33027,18 +33029,22 @@ return /******/ (function(modules) { // webpackBootstrap this.dragging = false; this.body.emitter.on("dragStart", function () { - _this.dragging = true;console.log("here"); + _this.dragging = true; }); this.body.emitter.on("dragEnd", function () { return _this.dragging = false; }); - this.body.emitter.on("_redraw", this._redraw.bind(this)); + this.body.emitter.on("_redraw", function () { + if (_this.renderingActive === false) { + _this._redraw(); + } + }); this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); this.body.emitter.on("_startRendering", function () { - _this.renderRequests += 1;_this.continueRendering = true;_this.startRendering(); + _this.renderRequests += 1;_this.renderingActive = true;_this.startRendering(); }); this.body.emitter.on("_stopRendering", function () { - _this.renderRequests -= 1;_this.continueRendering = _this.renderRequests > 0; + _this.renderRequests -= 1;_this.renderingActive = _this.renderRequests > 0; }); this.options = {}; @@ -33048,7 +33054,6 @@ return /******/ (function(modules) { // webpackBootstrap }; util.extend(this.options, this.defaultOptions); - this._determineBrowserMethod(); } @@ -33064,7 +33069,7 @@ return /******/ (function(modules) { // webpackBootstrap }, startRendering: { value: function startRendering() { - if (this.continueRendering === true) { + if (this.renderingActive === true) { if (!this.renderTimer) { if (this.requiresTimeout == true) { this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function @@ -33118,7 +33123,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ value: function _requestRedraw() { - if (this.redrawRequested !== true) { + if (this.redrawRequested !== true && this.renderingActive === false) { this.redrawRequested = true; if (this.requiresTimeout === true) { window.setTimeout(this._redraw.bind(this, false), 0); @@ -33138,6 +33143,10 @@ return /******/ (function(modules) { // webpackBootstrap this.redrawRequested = false; var ctx = this.canvas.frame.canvas.getContext("2d"); + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + } + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); // clear the canvas @@ -33155,8 +33164,6 @@ return /******/ (function(modules) { // webpackBootstrap this.canvasTopLeft = this.canvas.DOMtoCanvas({ x: 0, y: 0 }); this.canvasBottomRight = this.canvas.DOMtoCanvas({ x: this.canvas.frame.canvas.clientWidth, y: this.canvas.frame.canvas.clientHeight }); - console.log(this.dragging); - if (hidden === false) { if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { this._drawEdges(ctx); @@ -33167,14 +33174,12 @@ return /******/ (function(modules) { // webpackBootstrap this._drawNodes(ctx, this.body.nodes, hidden); } - if (hidden === false) { - if (this.controlNodesActive == true) { - this._drawControlNodes(ctx); - } + if (this.controlNodesActive === true) { + this._drawControlNodes(ctx); } //this._drawNodes(ctx,this.body.supportNodes,true); - // this.physics.nodesSolver._debug(ctx,"#F00F0F"); + //this.physics.nodesSolver._debug(ctx,"#F00F0F"); // restore original scaling and translation ctx.restore(); @@ -33391,7 +33396,6 @@ return /******/ (function(modules) { // webpackBootstrap var ctx = this.frame.canvas.getContext("2d"); this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } @@ -33414,30 +33418,31 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ value: function _bindHammer() { - var me = this; if (this.hammer !== undefined) { this.hammer.destroy(); } this.drag = {}; this.pinch = {}; this.hammer = new Hammer(this.frame.canvas); - this.hammer.on("tap", me.body.eventListeners.onTap); - this.hammer.on("doubletap", me.body.eventListeners.onDoubleTap); - this.hammer.on("press", me.body.eventListeners.onHold); - hammerUtil.onTouch(this.hammer, me.body.eventListeners.onTouch); - this.hammer.on("panstart", me.body.eventListeners.onDragStart); - this.hammer.on("panmove", me.body.eventListeners.onDrag); - this.hammer.on("panend", me.body.eventListeners.onDragEnd); - this.hammer.on("pinch", me.body.eventListeners.onPinch.bind(me)); + this.hammer.on("tap", this.body.eventListeners.onTap); + this.hammer.on("doubletap", this.body.eventListeners.onDoubleTap); + this.hammer.on("press", this.body.eventListeners.onHold); + hammerUtil.onTouch(this.hammer, this.body.eventListeners.onTouch); + this.hammer.on("panstart", this.body.eventListeners.onDragStart); + this.hammer.on("panmove", this.body.eventListeners.onDrag); + this.hammer.on("panend", this.body.eventListeners.onDragEnd); + this.hammer.on("pinch", function () { + console.log("pinching!"); + }); //this.body.eventListeners.onPinch ); // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? - this.frame.canvas.addEventListener("mousewheel", me.body.eventListeners.onMouseWheel.bind(me)); - this.frame.canvas.addEventListener("DOMMouseScroll", me.body.eventListeners.onMouseWheel.bind(me)); + this.frame.canvas.addEventListener("mousewheel", this.body.eventListeners.onMouseWheel); + this.frame.canvas.addEventListener("DOMMouseScroll", this.body.eventListeners.onMouseWheel); - this.frame.canvas.addEventListener("mousemove", me.body.eventListeners.onMouseMove.bind(me)); + this.frame.canvas.addEventListener("mousemove", this.body.eventListeners.onMouseMove); this.hammerFrame = new Hammer(this.frame); - hammerUtil.onRelease(this.hammerFrame, me.body.eventListeners.onRelease.bind(me)); + hammerUtil.onRelease(this.hammerFrame, this.body.eventListeners.onRelease); }, writable: true, configurable: true @@ -33487,7 +33492,7 @@ return /******/ (function(modules) { // webpackBootstrap } if (emitEvent === true) { - this.body.emitter.emit("resize", { width: this.frame.canvas.width * this.pixelRatio, height: this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio }); + this.body.emitter.emit("resize", { width: this.frame.canvas.width / this.pixelRatio, height: this.frame.canvas.height / this.pixelRatio, oldWidth: oldWidth / this.pixelRatio, oldHeight: oldHeight / this.pixelRatio }); } }, writable: true, @@ -33624,8 +33629,8 @@ return /******/ (function(modules) { // webpackBootstrap this.targetScale = 0; this.sourceTranslation = 0; this.targetTranslation = 0; - this.lockedOnNodeId = null; - this.lockedOnNodeOffset = null; + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; this.touchTime = 0; this.viewFunction = undefined; @@ -33910,7 +33915,7 @@ return /******/ (function(modules) { // webpackBootstrap // if the time is set to 0, don't do an animation if (options.animation.duration == 0) { - if (this.lockedOnNodeId != null) { + if (this.lockedOnNodeId != undefined) { this.viewFunction = this._lockedRedraw.bind(this); this.body.emitter.on("_beforeRender", this.viewFunction); } else { @@ -33957,7 +33962,7 @@ return /******/ (function(modules) { // webpackBootstrap }, releaseNode: { value: function releaseNode() { - if (this.lockedOnNodeId !== undefined) { + if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { this.body.emitter.off("_beforeRender", this.viewFunction); this.lockedOnNodeId = undefined; this.lockedOnNodeOffset = undefined; @@ -33990,7 +33995,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this.easingTime >= 1) { this.body.emitter.off("_beforeRender", this.viewFunction); this.easingTime = 0; - if (this.lockedOnNodeId != null) { + if (this.lockedOnNodeId != undefined) { this.viewFunction = this._lockedRedraw.bind(this); this.body.emitter.on("_beforeRender", this.viewFunction); } @@ -34011,7 +34016,547 @@ return /******/ (function(modules) { // webpackBootstrap }); /***/ }, -/* 79 */ +/* 79 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 2/27/2015. + * + */ + + + var util = __webpack_require__(1); + + var InteractionHandler = (function () { + function InteractionHandler(body, canvas, selectionHandler) { + _classCallCheck(this, InteractionHandler); + + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; + + // bind the events from hammer to functions in this object + this.body.eventListeners.onTap = this.onTap.bind(this); + this.body.eventListeners.onTouch = this.onTouch.bind(this); + this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); + this.body.eventListeners.onHold = this.onHold.bind(this); + this.body.eventListeners.onDragStart = this.onDragStart.bind(this); + this.body.eventListeners.onDrag = this.onDrag.bind(this); + this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); + this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); + this.body.eventListeners.onPinch = this.onPinch.bind(this); + this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); + this.body.eventListeners.onRelease = this.onRelease.bind(this); + + this.touchTime = 0; + this.drag = {}; + this.pinch = {}; + this.pointerPosition = { x: 0, y: 0 }; + this.hoverObj = { nodes: {}, edges: {} }; + + + this.options = {}; + this.defaultOptions = { + dragNodes: true, + dragView: true, + zoomView: true, + selectEnabled: true, + hoverEnabled: false + }; + util.extend(this.options, this.defaultOptions); + } + + _prototypeProperties(InteractionHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + }, + writable: true, + configurable: true + }, + getPointer: { + + + /** + * Get the pointer location from a touch location + * @param {{x: Number, y: Number}} touch + * @return {{x: Number, y: Number}} pointer + * @private + */ + value: function getPointer(touch) { + return { + x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), + y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) + }; + }, + writable: true, + configurable: true + }, + onTouch: { + + + /** + * On start of a touch gesture, store the pointer + * @param event + * @private + */ + value: function onTouch(event) { + if (new Date().valueOf() - this.touchTime > 100) { + this.drag.pointer = this.getPointer(event.center); + this.drag.pinched = false; + this.pinch.scale = this.body.view.scale; + + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); + } + }, + writable: true, + configurable: true + }, + onTap: { + + /** + * handle tap/click event: select/unselect a node + * @private + */ + value: function onTap(event) { + var pointer = this.getPointer(event.center); + + var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; + var selected = this.selectionHandler.selectOnPoint(pointer); + + if (selected === true || previouslySelected == true && selected === false) { + // select or unselect + this.body.emitter.emit("selected", this.selectionHandler.getSelection()); + } + + this.selectionHandler._generateClickEvent("click", pointer); + }, + writable: true, + configurable: true + }, + onDoubleTap: { + + + /** + * handle doubletap event + * @private + */ + value: function onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent("doubleClick", pointer); + }, + writable: true, + configurable: true + }, + onHold: { + + + + /** + * handle long tap event: multi select nodes + * @private + */ + value: function onHold(event) { + var pointer = this.getPointer(event.center); + + var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); + + if (selectionChanged === true) { + // select or longpress + this.body.emitter.emit("selected", this.selectionHandler.getSelection()); + } + + this.selectionHandler._generateClickEvent("click", pointer); + }, + writable: true, + configurable: true + }, + onRelease: { + + + /** + * handle the release of the screen + * + * @private + */ + value: function onRelease() {}, + writable: true, + configurable: true + }, + onDragStart: { + + + /** + * This function is called by onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ + value: function onDragStart(event) { + //in case the touch event was triggered on an external div, do the initial touch now. + if (this.drag.pointer === undefined) { + this.onTouch(event); + } + + var node = this.selectionHandler.getNodeAt(this.drag.pointer); + // note: drag.pointer is set in onTouch to get the initial touch location + + this.drag.dragging = true; + this.drag.selection = []; + this.drag.translation = util.extend({}, this.body.view.translation); // copy the object + this.drag.nodeId = null; + + this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); + + if (node != null && this.options.dragNodes === true) { + this.drag.nodeId = node.id; + // select the clicked node if not yet selected + if (node.isSelected() === false) { + this.selectionHandler.unselectAll(); + this.selectionHandler.selectObject(node); + } + + var selection = this.selectionHandler.selectionObj.nodes; + // create an array with the selected nodes and their original location and status + for (var nodeId in selection) { + if (selection.hasOwnProperty(nodeId)) { + var object = selection[nodeId]; + var s = { + id: object.id, + node: object, + + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: object.x, + y: object.y, + xFixed: object.xFixed, + yFixed: object.yFixed + }; + + object.xFixed = true; + object.yFixed = true; + + this.drag.selection.push(s); + } + } + } + }, + writable: true, + configurable: true + }, + onDrag: { + + + /** + * handle drag event + * @private + */ + value: function onDrag(event) { + var _this = this; + if (this.drag.pinched === true) { + return; + } + + // remove the focus on node if it is focussed on by the focusOnNode + this.body.emitter.emit("unlockNode"); + + var pointer = this.getPointer(event.center); + var selection = this.drag.selection; + if (selection && selection.length && this.options.dragNodes === true) { + // calculate delta's and new location + var deltaX = pointer.x - this.drag.pointer.x; + var deltaY = pointer.y - this.drag.pointer.y; + + // update position of all selected nodes + selection.forEach(function (selection) { + var node = selection.node; + + if (!selection.xFixed) { + node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); + } + + if (!selection.yFixed) { + node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + } + }); + + + // start the simulation of the physics + this.body.emitter.emit("startSimulation"); + } else { + // move the network + if (this.options.dragView === true) { + // if the drag was not started properly because the click started outside the network div, start it now. + if (this.drag.pointer === undefined) { + this._handleDragStart(event); + return; + } + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; + + this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; + this.body.emitter.emit("_redraw"); + } + } + }, + writable: true, + configurable: true + }, + onDragEnd: { + + + /** + * handle drag start event + * @private + */ + value: function onDragEnd(event) { + this.drag.dragging = false; + var selection = this.drag.selection; + if (selection && selection.length) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.xFixed = s.xFixed; + s.node.yFixed = s.yFixed; + }); + this.body.emitter.emit("startSimulation"); + } else { + this.body.emitter.emit("_requestRedraw"); + } + + this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); + }, + writable: true, + configurable: true + }, + onPinch: { + + + + /** + * Handle pinch event + * @param event + * @private + */ + value: function onPinch(event) { + console.log("on pinch"); + var pointer = this.getPointer(event.center); + + this.drag.pinched = true; + if (this.pinch[scale] === undefined) { + this.pinch.scale = 1; + } + + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.gesture.scale; + this.zoom(scale, pointer); + }, + writable: true, + configurable: true + }, + zoom: { + + + /** + * Zoom the network in or out + * @param {Number} scale a number around 1, and between 0.01 and 10 + * @param {{x: Number, y: Number}} pointer Position on screen + * @return {Number} appliedScale scale is limited within the boundaries + * @private + */ + value: function zoom(scale, pointer) { + if (this.options.zoomView === true) { + var scaleOld = this.body.view.scale; + if (scale < 0.00001) { + scale = 0.00001; + } + if (scale > 10) { + scale = 10; + } + + var preScaleDragPointer = null; + if (this.drag !== undefined) { + if (this.drag.dragging === true) { + preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); + } + } + // + this.canvas.frame.canvas.clientHeight / 2 + var translation = this.body.view.translation; + + var scaleFrac = scale / scaleOld; + var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + + this.body.view.scale = scale; + this.body.view.translation = { x: tx, y: ty }; + + if (preScaleDragPointer != null) { + var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + this.drag.pointer.x = postScaleDragPointer.x; + this.drag.pointer.y = postScaleDragPointer.y; + } + + this.body.emitter.emit("_requestRedraw"); + + if (scaleOld < scale) { + this.body.emitter.emit("zoom", { direction: "+" }); + } else { + this.body.emitter.emit("zoom", { direction: "-" }); + } + } + }, + writable: true, + configurable: true + }, + onMouseWheel: { + + + /** + * Event handler for mouse wheel event, used to zoom the timeline + * See http://adomas.org/javascript-mouse-wheel/ + * https://github.com/EightMedia/hammer.js/issues/256 + * @param {MouseEvent} event + * @private + */ + value: function onMouseWheel(event) { + // retrieve delta + var delta = 0; + if (event.wheelDelta) { + /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { + /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; + } + + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + // calculate the new scale + var scale = this.body.view.scale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= 1 + zoom; + + // calculate the pointer location + var pointer = { x: event.pageX, y: event.pageY }; + + // apply the new scale + this.zoom(scale, pointer); + } + + // Prevent default actions caused by mouse wheel. + event.preventDefault(); + }, + writable: true, + configurable: true + }, + onMouseMove: { + + + /** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + value: function onMouseMove(event) {}, + writable: true, + configurable: true + } + }); + + return InteractionHandler; + })(); + + exports.InteractionHandler = InteractionHandler; + Object.defineProperty(exports, "__esModule", { + value: true + }); + + // var pointer = {x:event.pageX, y:event.pageY}; + // var popupVisible = false; + // + // // check if the previously selected node is still selected + // if (this.popup !== undefined) { + // if (this.popup.hidden === false) { + // this._checkHidePopup(pointer); + // } + // + // // if the popup was not hidden above + // if (this.popup.hidden === false) { + // popupVisible = true; + // this.popup.setPosition(pointer.x + 3, pointer.y - 5) + // this.popup.show(); + // } + // } + // + // // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over + // if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) { + // this.canvas.frame.focus(); + // } + // + // // start a timeout that will check if the mouse is positioned above an element + // if (popupVisible === false) { + // var me = this; + // var checkShow = function() { + // me._checkShowPopup(pointer); + // }; + // + // if (this.popupTimer) { + // clearInterval(this.popupTimer); // stop any running calculationTimer + // } + // if (!this.drag.dragging) { + // this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay); + // } + // } + // + // /** + // * Adding hover highlights + // */ + // if (this.options.hoverEnabled === true) { + // // removing all hover highlights + // for (var edgeId in this.hoverObj.edges) { + // if (this.hoverObj.edges.hasOwnProperty(edgeId)) { + // this.hoverObj.edges[edgeId].hover = false; + // delete this.hoverObj.edges[edgeId]; + // } + // } + // + // // adding hover highlights + // var obj = this.selectionHandler.getNodeAt(pointer); + // if (obj == null) { + // obj = this.selectionHandler.getEdgeAt(pointer); + // } + // if (obj != null) { + // this._hoverObject(obj); + // } + // + // // removing all node hover highlights except for the selected one. + // for (var nodeId in this.hoverObj.nodes) { + // if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + // if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { + // this._blurObject(this.hoverObj.nodes[nodeId]); + // delete this.hoverObj.nodes[nodeId]; + // } + // } + // } + // this.body.emitter.emit("_requestRedraw"); + // } + +/***/ }, +/* 80 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34024,7 +34569,7 @@ return /******/ (function(modules) { // webpackBootstrap * Created by Alex on 2/27/2015. */ - var Node = __webpack_require__(55); + var Node = __webpack_require__(58); var util = __webpack_require__(1); var SelectionHandler = (function () { @@ -34066,15 +34611,11 @@ return /******/ (function(modules) { // webpackBootstrap value: function selectOnPoint(pointer) { var selected = false; if (this.options.select === true) { + this.unselectAll(); var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - - // deselect - if (obj === undefined) { - this.unselectAll(); - } else { + if (obj !== undefined) { selected = this.selectObject(obj); } - this.body.emitter.emit("_requestRedraw"); } return selected; @@ -34771,545 +35312,6 @@ return /******/ (function(modules) { // webpackBootstrap value: true }); -/***/ }, -/* 80 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - /** - * Created by Alex on 2/27/2015. - * - */ - - - var util = __webpack_require__(1); - - var InteractionHandler = (function () { - function InteractionHandler(body, canvas, selectionHandler) { - _classCallCheck(this, InteractionHandler); - - this.body = body; - this.canvas = canvas; - this.selectionHandler = selectionHandler; - - // bind the events from hammer to functions in this object - this.body.eventListeners.onTap = this.onTap.bind(this); - this.body.eventListeners.onTouch = this.onTouch.bind(this); - this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); - this.body.eventListeners.onHold = this.onHold.bind(this); - this.body.eventListeners.onDragStart = this.onDragStart.bind(this); - this.body.eventListeners.onDrag = this.onDrag.bind(this); - this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); - this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); - this.body.eventListeners.onPinch = this.onPinch.bind(this); - this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); - this.body.eventListeners.onRelease = this.onRelease.bind(this); - - this.touchTime = 0; - this.drag = {}; - this.pinch = {}; - this.pointerPosition = { x: 0, y: 0 }; - this.hoverObj = { nodes: {}, edges: {} }; - - - this.options = {}; - this.defaultOptions = { - dragNodes: true, - dragView: true, - zoomView: true, - selectEnabled: true, - hoverEnabled: false - }; - util.extend(this.options, this.defaultOptions); - } - - _prototypeProperties(InteractionHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); - } - }, - writable: true, - configurable: true - }, - getPointer: { - - - /** - * Get the pointer location from a touch location - * @param {{x: Number, y: Number}} touch - * @return {{x: Number, y: Number}} pointer - * @private - */ - value: function getPointer(touch) { - return { - x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), - y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) - }; - }, - writable: true, - configurable: true - }, - onTouch: { - - - /** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ - value: function onTouch(event) { - if (new Date().valueOf() - this.touchTime > 100) { - this.drag.pointer = this.getPointer(event.center); - this.drag.pinched = false; - this.pinch.scale = this.body.view.scale; - - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); - } - }, - writable: true, - configurable: true - }, - onTap: { - - /** - * handle tap/click event: select/unselect a node - * @private - */ - value: function onTap(event) { - var pointer = this.getPointer(event.center); - - var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; - var selected = this.selectionHandler.selectOnPoint(pointer); - - if (selected === true || previouslySelected == true && selected === false) { - // select or unselect - this.body.emitter.emit("selected", this.selectionHandler.getSelection()); - } - - this.selectionHandler._generateClickEvent("click", pointer); - }, - writable: true, - configurable: true - }, - onDoubleTap: { - - - /** - * handle doubletap event - * @private - */ - value: function onDoubleTap(event) { - var pointer = this.getPointer(event.center); - this.selectionHandler._generateClickEvent("doubleClick", pointer); - }, - writable: true, - configurable: true - }, - onHold: { - - - - /** - * handle long tap event: multi select nodes - * @private - */ - value: function onHold(event) { - var pointer = this.getPointer(event.center); - - var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); - - if (selectionChanged === true) { - // select or longpress - this.body.emitter.emit("selected", this.selectionHandler.getSelection()); - } - - this.selectionHandler._generateClickEvent("click", pointer); - }, - writable: true, - configurable: true - }, - onRelease: { - - - /** - * handle the release of the screen - * - * @private - */ - value: function onRelease() {}, - writable: true, - configurable: true - }, - onDragStart: { - - - /** - * This function is called by onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. - * - * @private - */ - value: function onDragStart(event) { - //in case the touch event was triggered on an external div, do the initial touch now. - if (this.drag.pointer === undefined) { - this.onTouch(event); - } - - var node = this.selectionHandler.getNodeAt(this.drag.pointer); - // note: drag.pointer is set in onTouch to get the initial touch location - - this.drag.dragging = true; - this.drag.selection = []; - this.drag.translation = util.extend({}, this.body.view.translation); // copy the object - this.drag.nodeId = null; - this.draggingNodes = false; - - this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); - - if (node != null && this.options.dragNodes === true) { - this.drag.nodeId = node.id; - // select the clicked node if not yet selected - if (node.isSelected() === false) { - this.selectionHandler.selectObject(node); - } - - var selection = this.selectionHandler.selectionObj.nodes; - // create an array with the selected nodes and their original location and status - for (var nodeId in selection) { - if (selection.hasOwnProperty(nodeId)) { - var object = selection[nodeId]; - var s = { - id: object.id, - node: object, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.xFixed, - yFixed: object.yFixed - }; - - object.xFixed = true; - object.yFixed = true; - - this.drag.selection.push(s); - } - } - } - }, - writable: true, - configurable: true - }, - onDrag: { - - - /** - * handle drag event - * @private - */ - value: function onDrag(event) { - var _this = this; - if (this.drag.pinched === true) { - return; - } - - // remove the focus on node if it is focussed on by the focusOnNode - this.body.emitter.emit("unlockNode"); - - var pointer = this.getPointer(event.center); - var selection = this.drag.selection; - if (selection && selection.length && this.options.dragNodes === true) { - // calculate delta's and new location - var deltaX = pointer.x - this.drag.pointer.x; - var deltaY = pointer.y - this.drag.pointer.y; - - // update position of all selected nodes - selection.forEach(function (selection) { - var node = selection.node; - - if (!selection.xFixed) { - node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); - } - - if (!selection.yFixed) { - node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); - } - }); - - - // start the simulation of the physics - this.body.emitter.emit("startSimulation"); - } else { - // move the network - if (this.options.dragView === true) { - // if the drag was not started properly because the click started outside the network div, start it now. - if (this.drag.pointer === undefined) { - this._handleDragStart(event); - return; - } - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; - - this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; - this.body.emitter.emit("_redraw"); - } - } - }, - writable: true, - configurable: true - }, - onDragEnd: { - - - /** - * handle drag start event - * @private - */ - value: function onDragEnd(event) { - this.drag.dragging = false; - var selection = this.drag.selection; - if (selection && selection.length) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.xFixed = s.xFixed; - s.node.yFixed = s.yFixed; - }); - this.body.emitter.emit("startSimulation"); - } else { - this.body.emitter.emit("_requestRedraw"); - } - - this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); - }, - writable: true, - configurable: true - }, - onPinch: { - - - - /** - * Handle pinch event - * @param event - * @private - */ - value: function onPinch(event) { - var pointer = this.getPointer(event.center); - - this.drag.pinched = true; - if (this.pinch[scale] === undefined) { - this.pinch.scale = 1; - } - - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.gesture.scale; - this.zoom(scale, pointer); - }, - writable: true, - configurable: true - }, - zoom: { - - - /** - * Zoom the network in or out - * @param {Number} scale a number around 1, and between 0.01 and 10 - * @param {{x: Number, y: Number}} pointer Position on screen - * @return {Number} appliedScale scale is limited within the boundaries - * @private - */ - value: function zoom(scale, pointer) { - if (this.options.zoomView === true) { - var scaleOld = this.body.view.scale; - if (scale < 0.00001) { - scale = 0.00001; - } - if (scale > 10) { - scale = 10; - } - - var preScaleDragPointer = null; - if (this.drag !== undefined) { - if (this.drag.dragging === true) { - preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); - } - } - // + this.canvas.frame.canvas.clientHeight / 2 - var translation = this.body.view.translation; - - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - - this.body.view.scale = scale; - this.body.view.translation = { x: tx, y: ty }; - - if (preScaleDragPointer != null) { - var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); - this.drag.pointer.x = postScaleDragPointer.x; - this.drag.pointer.y = postScaleDragPointer.y; - } - - this.body.emitter.emit("_requestRedraw"); - - if (scaleOld < scale) { - this.body.emitter.emit("zoom", { direction: "+" }); - } else { - this.body.emitter.emit("zoom", { direction: "-" }); - } - } - }, - writable: true, - configurable: true - }, - onMouseWheel: { - - - /** - * Event handler for mouse wheel event, used to zoom the timeline - * See http://adomas.org/javascript-mouse-wheel/ - * https://github.com/EightMedia/hammer.js/issues/256 - * @param {MouseEvent} event - * @private - */ - value: function onMouseWheel(event) { - // retrieve delta - var delta = 0; - if (event.wheelDelta) { - /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { - /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; - } - - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - // calculate the new scale - var scale = this.body.view.scale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= 1 + zoom; - - // calculate the pointer location - var pointer = { x: event.pageX, y: event.pageY }; - - // apply the new scale - this.zoom(scale, pointer); - } - - // Prevent default actions caused by mouse wheel. - event.preventDefault(); - }, - writable: true, - configurable: true - }, - onMouseMove: { - - - /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private - */ - value: function onMouseMove(event) { - var pointer = { x: event.pageX, y: event.pageY }; - var popupVisible = false; - - // check if the previously selected node is still selected - if (this.popup !== undefined) { - if (this.popup.hidden === false) { - this._checkHidePopup(pointer); - } - - // if the popup was not hidden above - if (this.popup.hidden === false) { - popupVisible = true; - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.show(); - } - } - - // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over - if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) { - this.canvas.frame.focus(); - } - - // start a timeout that will check if the mouse is positioned above an element - if (popupVisible === false) { - var me = this; - var checkShow = function () { - me._checkShowPopup(pointer); - }; - - if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running calculationTimer - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay); - } - } - - /** - * Adding hover highlights - */ - if (this.options.hoverEnabled === true) { - // removing all hover highlights - for (var edgeId in this.hoverObj.edges) { - if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - this.hoverObj.edges[edgeId].hover = false; - delete this.hoverObj.edges[edgeId]; - } - } - - // adding hover highlights - var obj = this.selectionHandler.getNodeAt(pointer); - if (obj == null) { - obj = this.selectionHandler.getEdgeAt(pointer); - } - if (obj != null) { - this._hoverObject(obj); - } - - // removing all node hover highlights except for the selected one. - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { - this._blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } - } - } - this.body.emitter.emit("_requestRedraw"); - } - }, - writable: true, - configurable: true - } - }); - - return InteractionHandler; - })(); - - exports.TouchEventHandler = TouchEventHandler; - Object.defineProperty(exports, "__esModule", { - value: true - }); - /***/ } /******/ ]) }); diff --git a/examples/network/01_basic_usage.html b/examples/network/01_basic_usage.html index e03e75fe..b704463b 100644 --- a/examples/network/01_basic_usage.html +++ b/examples/network/01_basic_usage.html @@ -43,7 +43,7 @@ nodes: nodes, edges: edges }; - var options = {}; + var options = {}//{physics:{stabilization:false}}; var network = new vis.Network(container, data, options); diff --git a/lib/network/Edge.js b/lib/network/Edge.js index b91697d0..fa1e8191 100644 --- a/lib/network/Edge.js +++ b/lib/network/Edge.js @@ -173,6 +173,14 @@ Edge.prototype.getTitle = function() { return typeof this.title === "function" ? this.title() : this.title; }; +/** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ +Edge.prototype.isSelected = function() { + return this.selected; +}; + /** * Retrieve the value of the edge. Can be undefined diff --git a/lib/network/Network.js b/lib/network/Network.js index 800e0fac..a92bf432 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -24,7 +24,7 @@ import { ClusterEngine } from './modules/Clustering' import { CanvasRenderer } from './modules/CanvasRenderer' import { Canvas } from './modules/Canvas' import { View } from './modules/View' -import { TouchEventHandler } from './modules/InteractionHandler' +import { InteractionHandler } from './modules/InteractionHandler' import { SelectionHandler } from "./modules/SelectionHandler" /** @@ -228,7 +228,7 @@ function Network (container, data, options) { // modules this.canvas = new Canvas(this.body); this.selectionHandler = new SelectionHandler(this.body, this.canvas); - this.touchHandler = new TouchEventHandler(this.body, this.canvas, this.selectionHandler); + this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); this.view = new View(this.body, this.canvas); this.renderer = new CanvasRenderer(this.body, this.canvas); this.clustering = new ClusterEngine(this.body); @@ -268,13 +268,6 @@ function Network (container, data, options) { // apply options this.setOptions(options); - // other vars - this.cachedFunctions = {}; - this.startedStabilization = false; - this.stabilized = false; - this.stabilizationIterations = null; - this.draggingNodes = false; - // position and scale variables and objects this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw @@ -465,7 +458,7 @@ Network.prototype.setOptions = function (options) { this.physics.setOptions(options.physics); this.canvas.setOptions(options.canvas); this.renderer.setOptions(options.rendering); - this.touchHandler.setOptions(options.interaction); + this.interactionHandler.setOptions(options.interaction); this.selectionHandler.setOptions(options.selection); @@ -573,7 +566,7 @@ Network.prototype.setOptions = function (options) { //this._configureSmoothCurves(); // bind hammer - this.canvas._bindHammer(); + //this.canvas._bindHammer(); // bind keys. If disabled, this will not do anything; //this._createKeyBinds(); diff --git a/lib/network/modules/Canvas.js b/lib/network/modules/Canvas.js index aa64b02c..f14e5b64 100644 --- a/lib/network/modules/Canvas.js +++ b/lib/network/modules/Canvas.js @@ -66,7 +66,6 @@ class Canvas { ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } @@ -85,30 +84,29 @@ class Canvas { * @private */ _bindHammer() { - var me = this; if (this.hammer !== undefined) { this.hammer.destroy(); } this.drag = {}; this.pinch = {}; this.hammer = new Hammer(this.frame.canvas); - this.hammer.on('tap', me.body.eventListeners.onTap ); - this.hammer.on('doubletap', me.body.eventListeners.onDoubleTap ); - this.hammer.on('press', me.body.eventListeners.onHold ); - hammerUtil.onTouch(this.hammer, me.body.eventListeners.onTouch ); - this.hammer.on('panstart', me.body.eventListeners.onDragStart ); - this.hammer.on('panmove', me.body.eventListeners.onDrag ); - this.hammer.on('panend', me.body.eventListeners.onDragEnd ); - this.hammer.on('pinch', me.body.eventListeners.onPinch.bind(me) ); + this.hammer.on('tap', this.body.eventListeners.onTap ); + this.hammer.on('doubletap', this.body.eventListeners.onDoubleTap ); + this.hammer.on('press', this.body.eventListeners.onHold ); + hammerUtil.onTouch(this.hammer, this.body.eventListeners.onTouch ); + this.hammer.on('panstart', this.body.eventListeners.onDragStart ); + this.hammer.on('panmove', this.body.eventListeners.onDrag ); + this.hammer.on('panend', this.body.eventListeners.onDragEnd ); + this.hammer.on('pinch', function() {console.log("pinching!");});//this.body.eventListeners.onPinch ); // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? - this.frame.canvas.addEventListener('mousewheel', me.body.eventListeners.onMouseWheel.bind(me)); - this.frame.canvas.addEventListener('DOMMouseScroll', me.body.eventListeners.onMouseWheel.bind(me)); + this.frame.canvas.addEventListener('mousewheel', this.body.eventListeners.onMouseWheel); + this.frame.canvas.addEventListener('DOMMouseScroll', this.body.eventListeners.onMouseWheel); - this.frame.canvas.addEventListener('mousemove', me.body.eventListeners.onMouseMove.bind(me)); + this.frame.canvas.addEventListener('mousemove', this.body.eventListeners.onMouseMove); this.hammerFrame = new Hammer(this.frame); - hammerUtil.onRelease(this.hammerFrame, me.body.eventListeners.onRelease.bind(me) ); + hammerUtil.onRelease(this.hammerFrame, this.body.eventListeners.onRelease); } @@ -153,7 +151,7 @@ class Canvas { } if (emitEvent === true) { - this.body.emitter.emit('resize', {width:this.frame.canvas.width * this.pixelRatio,height:this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio}); + this.body.emitter.emit('resize', {width:this.frame.canvas.width / this.pixelRatio, height:this.frame.canvas.height / this.pixelRatio, oldWidth: oldWidth / this.pixelRatio, oldHeight: oldHeight / this.pixelRatio}); } }; diff --git a/lib/network/modules/CanvasRenderer.js b/lib/network/modules/CanvasRenderer.js index d66ef446..a288a541 100644 --- a/lib/network/modules/CanvasRenderer.js +++ b/lib/network/modules/CanvasRenderer.js @@ -18,20 +18,21 @@ class CanvasRenderer { this.redrawRequested = false; this.renderTimer = false; this.requiresTimeout = true; - this.continueRendering = false; + this.renderingActive = false; this.renderRequests = 0; + this.pixelRatio = undefined; this.canvasTopLeft = {x: 0, y: 0}; this.canvasBottomRight = {x: 0, y: 0}; this.dragging = false; - this.body.emitter.on("dragStart", () => {this.dragging = true; console.log("here")}); + this.body.emitter.on("dragStart", () => {this.dragging = true;}); this.body.emitter.on("dragEnd", () => this.dragging = false); - this.body.emitter.on("_redraw", this._redraw.bind(this)); + this.body.emitter.on("_redraw", () => {if (this.renderingActive === false) {this._redraw();}}); this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); - this.body.emitter.on("_startRendering", () => {this.renderRequests += 1; this.continueRendering = true; this.startRendering();}); - this.body.emitter.on("_stopRendering", () => {this.renderRequests -= 1; this.continueRendering = this.renderRequests > 0;}); + this.body.emitter.on("_startRendering", () => {this.renderRequests += 1; this.renderingActive = true; this.startRendering();}); + this.body.emitter.on("_stopRendering", () => {this.renderRequests -= 1; this.renderingActive = this.renderRequests > 0;}); this.options = {}; this.defaultOptions = { @@ -40,7 +41,6 @@ class CanvasRenderer { } util.extend(this.options,this.defaultOptions); - this._determineBrowserMethod(); } @@ -52,7 +52,7 @@ class CanvasRenderer { startRendering() { - if (this.continueRendering === true) { + if (this.renderingActive === true) { if (!this.renderTimer) { if (this.requiresTimeout == true) { this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function @@ -99,7 +99,7 @@ class CanvasRenderer { * @private */ _requestRedraw() { - if (this.redrawRequested !== true) { + if (this.redrawRequested !== true && this.renderingActive === false) { this.redrawRequested = true; if (this.requiresTimeout === true) { window.setTimeout(this._redraw.bind(this, false),0); @@ -116,6 +116,14 @@ class CanvasRenderer { this.redrawRequested = false; var ctx = this.canvas.frame.canvas.getContext('2d'); + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio || 1); + } + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); // clear the canvas @@ -133,8 +141,6 @@ class CanvasRenderer { this.canvasTopLeft = this.canvas.DOMtoCanvas({x:0,y:0}); this.canvasBottomRight = this.canvas.DOMtoCanvas({x:this.canvas.frame.canvas.clientWidth,y:this.canvas.frame.canvas.clientHeight}); - console.log(this.dragging) - if (hidden === false) { if (this.dragging === false || (this.dragging === true && this.options.hideEdgesOnDrag === false)) { this._drawEdges(ctx); @@ -145,14 +151,12 @@ class CanvasRenderer { this._drawNodes(ctx, this.body.nodes, hidden); } - if (hidden === false) { - if (this.controlNodesActive == true) { - this._drawControlNodes(ctx); - } + if (this.controlNodesActive === true) { + this._drawControlNodes(ctx); } //this._drawNodes(ctx,this.body.supportNodes,true); - // this.physics.nodesSolver._debug(ctx,"#F00F0F"); + //this.physics.nodesSolver._debug(ctx,"#F00F0F"); // restore original scaling and translation ctx.restore(); diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index dcf20df8..b798b462 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -153,7 +153,6 @@ class InteractionHandler { this.drag.selection = []; this.drag.translation = util.extend({},this.body.view.translation); // copy the object this.drag.nodeId = null; - this.draggingNodes = false; this.body.emitter.emit("dragStart", {nodeIds: this.selectionHandler.getSelection().nodes}); @@ -161,6 +160,7 @@ class InteractionHandler { this.drag.nodeId = node.id; // select the clicked node if not yet selected if (node.isSelected() === false) { + this.selectionHandler.unselectAll(); this.selectionHandler.selectObject(node); } @@ -274,6 +274,7 @@ class InteractionHandler { * @private */ onPinch(event) { + console.log("on pinch") var pointer = this.getPointer(event.center); this.drag.pinched = true; @@ -387,76 +388,76 @@ class InteractionHandler { * @private */ onMouseMove(event) { - var pointer = {x:event.pageX, y:event.pageY}; - var popupVisible = false; - - // check if the previously selected node is still selected - if (this.popup !== undefined) { - if (this.popup.hidden === false) { - this._checkHidePopup(pointer); - } - - // if the popup was not hidden above - if (this.popup.hidden === false) { - popupVisible = true; - this.popup.setPosition(pointer.x + 3, pointer.y - 5) - this.popup.show(); - } - } - - // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over - if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) { - this.canvas.frame.focus(); - } - - // start a timeout that will check if the mouse is positioned above an element - if (popupVisible === false) { - var me = this; - var checkShow = function() { - me._checkShowPopup(pointer); - }; - - if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running calculationTimer - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay); - } - } - - /** - * Adding hover highlights - */ - if (this.options.hoverEnabled === true) { - // removing all hover highlights - for (var edgeId in this.hoverObj.edges) { - if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - this.hoverObj.edges[edgeId].hover = false; - delete this.hoverObj.edges[edgeId]; - } - } - - // adding hover highlights - var obj = this.selectionHandler.getNodeAt(pointer); - if (obj == null) { - obj = this.selectionHandler.getEdgeAt(pointer); - } - if (obj != null) { - this._hoverObject(obj); - } - - // removing all node hover highlights except for the selected one. - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { - this._blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } - } - } - this.body.emitter.emit("_requestRedraw"); - } + // var pointer = {x:event.pageX, y:event.pageY}; + // var popupVisible = false; + // + // // check if the previously selected node is still selected + // if (this.popup !== undefined) { + // if (this.popup.hidden === false) { + // this._checkHidePopup(pointer); + // } + // + // // if the popup was not hidden above + // if (this.popup.hidden === false) { + // popupVisible = true; + // this.popup.setPosition(pointer.x + 3, pointer.y - 5) + // this.popup.show(); + // } + // } + // + // // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over + // if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) { + // this.canvas.frame.focus(); + // } + // + // // start a timeout that will check if the mouse is positioned above an element + // if (popupVisible === false) { + // var me = this; + // var checkShow = function() { + // me._checkShowPopup(pointer); + // }; + // + // if (this.popupTimer) { + // clearInterval(this.popupTimer); // stop any running calculationTimer + // } + // if (!this.drag.dragging) { + // this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay); + // } + // } + // + // /** + // * Adding hover highlights + // */ + // if (this.options.hoverEnabled === true) { + // // removing all hover highlights + // for (var edgeId in this.hoverObj.edges) { + // if (this.hoverObj.edges.hasOwnProperty(edgeId)) { + // this.hoverObj.edges[edgeId].hover = false; + // delete this.hoverObj.edges[edgeId]; + // } + // } + // + // // adding hover highlights + // var obj = this.selectionHandler.getNodeAt(pointer); + // if (obj == null) { + // obj = this.selectionHandler.getEdgeAt(pointer); + // } + // if (obj != null) { + // this._hoverObject(obj); + // } + // + // // removing all node hover highlights except for the selected one. + // for (var nodeId in this.hoverObj.nodes) { + // if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + // if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { + // this._blurObject(this.hoverObj.nodes[nodeId]); + // delete this.hoverObj.nodes[nodeId]; + // } + // } + // } + // this.body.emitter.emit("_requestRedraw"); + // } } } -export {TouchEventHandler}; +export {InteractionHandler}; diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index 2375496d..2e760798 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -36,17 +36,11 @@ class SelectionHandler { selectOnPoint(pointer) { var selected = false; if (this.options.select === true) { - + this.unselectAll(); var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - - // deselect - if (obj === undefined) { - this.unselectAll(); - } - else { + if (obj !== undefined) { selected = this.selectObject(obj); } - this.body.emitter.emit("_requestRedraw"); } return selected; diff --git a/lib/network/modules/View.js b/lib/network/modules/View.js index 81f9e5ba..b7bf2eda 100644 --- a/lib/network/modules/View.js +++ b/lib/network/modules/View.js @@ -16,8 +16,8 @@ class View { this.targetScale = 0; this.sourceTranslation = 0; this.targetTranslation = 0; - this.lockedOnNodeId = null; - this.lockedOnNodeOffset = null; + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; this.touchTime = 0; this.viewFunction = undefined; @@ -250,7 +250,7 @@ class View { // if the time is set to 0, don't do an animation if (options.animation.duration == 0) { - if (this.lockedOnNodeId != null) { + if (this.lockedOnNodeId != undefined) { this.viewFunction = this._lockedRedraw.bind(this); this.body.emitter.on("_beforeRender", this.viewFunction); } @@ -292,7 +292,7 @@ class View { } releaseNode() { - if (this.lockedOnNodeId !== undefined) { + if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { this.body.emitter.off("_beforeRender", this.viewFunction); this.lockedOnNodeId = undefined; this.lockedOnNodeOffset = undefined; @@ -320,7 +320,7 @@ class View { if (this.easingTime >= 1.0) { this.body.emitter.off("_beforeRender", this.viewFunction); this.easingTime = 0; - if (this.lockedOnNodeId != null) { + if (this.lockedOnNodeId != undefined) { this.viewFunction = this._lockedRedraw.bind(this); this.body.emitter.on("_beforeRender", this.viewFunction); }