diff --git a/dist/vis.js b/dist/vis.js index 474eb338..a3169e8b 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -258,6 +258,22 @@ return /******/ (function(modules) { // webpackBootstrap return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4(); }; + + /** + * assign all keys of an object that are not nested objects to a certain value (used for color objects). + * @param obj + * @param value + */ + exports.assignAllKeys = function (obj, value) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + if (typeof obj[prop] !== "object") { + obj[prop] = value; + } + } + } + }; + /** * Extend object a with the properties of object b or a series of objects * Only properties with defined values are copied @@ -266,7 +282,7 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Object} a */ exports.extend = function (a, b) { - for (var i = 1, len = arguments.length; i < len; i++) { + for (var i = 1; i < arguments.length; i++) { var other = arguments[i]; for (var prop in other) { if (other.hasOwnProperty(prop)) { @@ -23284,21 +23300,21 @@ return /******/ (function(modules) { // webpackBootstrap var EdgesHandler = _interopRequire(__webpack_require__(80)); - var PhysicsEngine = _interopRequire(__webpack_require__(87)); + var PhysicsEngine = _interopRequire(__webpack_require__(81)); - var ClusterEngine = _interopRequire(__webpack_require__(94)); + var ClusterEngine = _interopRequire(__webpack_require__(88)); - var CanvasRenderer = _interopRequire(__webpack_require__(96)); + var CanvasRenderer = _interopRequire(__webpack_require__(90)); - var Canvas = _interopRequire(__webpack_require__(97)); + var Canvas = _interopRequire(__webpack_require__(91)); - var View = _interopRequire(__webpack_require__(98)); + var View = _interopRequire(__webpack_require__(92)); - var InteractionHandler = _interopRequire(__webpack_require__(99)); + var InteractionHandler = _interopRequire(__webpack_require__(93)); - var SelectionHandler = _interopRequire(__webpack_require__(101)); + var SelectionHandler = _interopRequire(__webpack_require__(95)); - var LayoutEngine = _interopRequire(__webpack_require__(102)); + var LayoutEngine = _interopRequire(__webpack_require__(96)); /** * @constructor Network @@ -23401,7 +23417,7 @@ return /******/ (function(modules) { // webpackBootstrap var t0 = new Date().valueOf(); // update shortcut lists _this._updateVisibleIndices(); - _this.physics._updatePhysicsIndices(); + _this.physics.updatePhysicsIndices(); // update values _this._updateValueRange(_this.body.nodes); _this._updateValueRange(_this.body.edges); @@ -23559,41 +23575,9 @@ return /******/ (function(modules) { // webpackBootstrap //// TODO: work out these options and document them - //if (options.edges) { - // if (options.edges.color !== undefined) { - // if (util.isString(options.edges.color)) { - // this.constants.edges.color = {}; - // this.constants.edges.color.color = options.edges.color; - // this.constants.edges.color.highlight = options.edges.color; - // this.constants.edges.color.hover = options.edges.color; - // } - // else { - // if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;} - // if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;} - // if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;} - // } - // this.constants.edges.inheritColor = false; - // } // - // if (!options.edges.fontColor) { - // if (options.edges.color !== undefined) { - // if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;} - // else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;} - // } - // } - //} // - //if (options.nodes) { - // if (options.nodes.color) { - // var newColorObj = util.parseColor(options.nodes.color); - // this.constants.nodes.color.background = newColorObj.background; - // this.constants.nodes.color.border = newColorObj.border; - // this.constants.nodes.color.highlight.background = newColorObj.highlight.background; - // this.constants.nodes.color.highlight.border = newColorObj.highlight.border; - // this.constants.nodes.color.hover.background = newColorObj.hover.background; - // this.constants.nodes.color.hover.border = newColorObj.hover.border; - // } - //} + // //if (options.groups) { // for (var groupname in options.groups) { // if (options.groups.hasOwnProperty(groupname)) { @@ -25678,13 +25662,22 @@ return /******/ (function(modules) { // webpackBootstrap size: 10, value: 1 }; - util.extend(this.options, this.defaultOptions); + + this.setOptions(options); } _prototypeProperties(NodesHandler, null, { setOptions: { - value: function setOptions(options) {}, + value: function setOptions(options) { + if (options) { + util.selectiveNotDeepExtend(["color"], this.options, options); + + if (options.color) { + this.options.color = util.parseColor(options.color); + } + } + }, writable: true, configurable: true }, @@ -25902,33 +25895,25 @@ return /******/ (function(modules) { // webpackBootstrap _classCallCheck(this, Node); this.options = util.bridgeObject(globalOptions); - this.body = body; - this.selected = false; - this.hover = false; this.edges = []; // all edges connected to this node // set defaults for the options this.id = undefined; - this.allowedToMoveX = false; - this.allowedToMoveY = false; - this.xFixed = false; - this.yFixed = false; - this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; this.imagelist = imagelist; this.grouplist = grouplist; - // physics options + // state options this.x = null; this.y = null; this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate + this.selected = false; + this.hover = false; - this.fixedData = { x: null, y: null }; this.labelModule = new Label(this.body, this.options); - this.setOptions(options); } @@ -25989,16 +25974,12 @@ return /******/ (function(modules) { // webpackBootstrap return; } - var fields = ["id", "borderWidth", "borderWidthSelected", "shape", "image", "brokenImage", "size", "label", "customScalingFunction", "icon", "value", "hidden", "physics"]; + var fields = ["borderWidth", "borderWidthSelected", "brokenImage", "customScalingFunction", "font", "hidden", "icon", "id", "image", "label", "physics", "shape", "size", "value"]; util.selectiveDeepExtend(fields, this.options, options); - // basic options if (options.id !== undefined) { this.id = options.id; } - if (options.title !== undefined) { - this.title = options.title; - } if (options.x !== undefined) { this.x = options.x; this.predefinedPosition = true; @@ -26015,10 +25996,6 @@ return /******/ (function(modules) { // webpackBootstrap this.preassignedLevel = true; } - if (options.triggerFunction !== undefined) { - this.triggerFunction = options.triggerFunction; - } - if (this.id === undefined) { throw "Node must have an id"; } @@ -26043,19 +26020,18 @@ return /******/ (function(modules) { // webpackBootstrap } } - if (options.allowedToMoveX !== undefined) { - this.xFixed = !options.allowedToMoveX; - this.allowedToMoveX = options.allowedToMoveX; - } else if (options.x !== undefined && this.allowedToMoveX == false) { - this.xFixed = true; - } - - - if (options.allowedToMoveY !== undefined) { - this.yFixed = !options.allowedToMoveY; - this.allowedToMoveY = options.allowedToMoveY; - } else if (options.y !== undefined && this.allowedToMoveY == false) { - this.yFixed = true; + if (options.fixed !== undefined) { + if (typeof options.fixed == "boolean") { + this.options.fixed.x = true; + this.options.fixed.y = true; + } else { + if (options.fixed.x !== undefined && typeof options.fixed.x == "boolean") { + this.options.fixed.x = options.fixed.x; + } + if (options.fixed.y !== undefined && typeof options.fixed.y == "boolean") { + this.options.fixed.y = options.fixed.y; + } + } } // choose draw method depending on the shape @@ -26105,7 +26081,7 @@ return /******/ (function(modules) { // webpackBootstrap break; } - this.labelModule.setOptions(this.options); + this.labelModule.setOptions(this.options, options); // reset the size of the node, this can be changed this._reset(); @@ -26317,8 +26293,20 @@ return /******/ (function(modules) { // webpackBootstrap _classCallCheck(this, Label); this.body = body; - this.setOptions(options); + this.fontOptions = {}; + this.defaultOptions = { + color: "#343434", + size: 14, // px + face: "arial", + background: "none", + stroke: 0, // px + strokeColor: "white", + align: "horizontal" + }; + util.extend(this.fontOptions, this.defaultOptions); + + this.setOptions(options); this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached } @@ -26329,6 +26317,17 @@ return /******/ (function(modules) { // webpackBootstrap if (options.label !== undefined) { this.labelDirty = true; } + if (options.font) { + if (typeof options.font === "string") { + var optionsArray = options.font.split(" "); + this.fontOptions.size = optionsArray[0].replace("px", ""); + this.fontOptions.face = optionsArray[1]; + this.fontOptions.color = optionsArray[2]; + } else if (typeof options.font == "object") { + util.extend(this.fontOptions, options.font); + } + this.fontOptions.size = Number(this.fontOptions.size); + } }, writable: true, configurable: true @@ -26350,7 +26349,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options.label === undefined) { return; } // check if we have to render the label - var viewFontSize = Number(this.options.font.size) * this.body.view.scale; + var viewFontSize = this.fontOptions.size * this.body.view.scale; if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) { return; } // update the size cache if required @@ -26372,12 +26371,12 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ value: function _drawBackground(ctx) { - if (this.options.font.background !== undefined && this.options.font.background !== "none") { - ctx.fillStyle = this.options.font.background; + if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") { + ctx.fillStyle = this.fontOptions.background; var lineMargin = 2; - switch (this.options.font.align) { + switch (this.fontOptions.align) { case "middle": ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); break; @@ -26408,7 +26407,7 @@ return /******/ (function(modules) { // webpackBootstrap */ value: function _drawText(ctx, selected, x, y) { var baseline = arguments[4] === undefined ? "middle" : arguments[4]; - var fontSize = Number(this.options.font.size); + var fontSize = this.fontOptions.size; var viewFontSize = fontSize * this.body.view.scale; // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) if (viewFontSize >= this.options.scaling.label.maxVisible) { @@ -26431,20 +26430,20 @@ return /******/ (function(modules) { // webpackBootstrap // configure context for drawing the text - ctx.font = (selected ? "bold " : "") + fontSize + "px " + this.options.font.face; + ctx.font = (selected ? "bold " : "") + fontSize + "px " + this.fontOptions.face; ctx.fillStyle = fontColor; ctx.textAlign = "center"; // set the strokeWidth - if (this.options.font.stroke > 0) { - ctx.lineWidth = this.options.font.stroke; + if (this.fontOptions.stroke > 0) { + ctx.lineWidth = this.fontOptions.stroke; ctx.strokeStyle = strokeColor; ctx.lineJoin = "round"; } // draw the text for (var i = 0; i < this.lineCount; i++) { - if (this.options.font.stroke > 0) { + if (this.fontOptions.stroke > 0) { ctx.strokeText(this.lines[i], x, yLine); } ctx.fillText(this.lines[i], x, yLine); @@ -26458,15 +26457,15 @@ return /******/ (function(modules) { // webpackBootstrap value: function _setAlignment(ctx, x, yLine, baseline) { // check for label alignment (for edges) // TODO: make alignment for nodes - if (this.options.font.align !== "horizontal") { + if (this.fontOptions.align !== "horizontal") { x = 0; yLine = 0; var lineMargin = 2; - if (this.options.font.align === "top") { + if (this.fontOptions.align === "top") { ctx.textBaseline = "alphabetic"; yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } else if (this.options.font.align === "bottom") { + } else if (this.fontOptions.align === "bottom") { ctx.textBaseline = "hanging"; yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers } else { @@ -26492,8 +26491,8 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ value: function _getColor(viewFontSize) { - var fontColor = this.options.font.color || "#000000"; - var strokeColor = this.options.font.strokeColor || "#ffffff"; + var fontColor = this.fontOptions.color || "#000000"; + var strokeColor = this.fontOptions.strokeColor || "#ffffff"; if (viewFontSize <= this.options.scaling.label.drawThreshold) { var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); fontColor = util.overrideOpacity(fontColor, opacity); @@ -26517,7 +26516,7 @@ return /******/ (function(modules) { // webpackBootstrap var selected = arguments[1] === undefined ? false : arguments[1]; var size = { width: this._processLabel(ctx, selected), - height: this.options.font.size * this.lineCount + height: this.fontOptions.size * this.lineCount }; return size; }, @@ -26542,12 +26541,12 @@ return /******/ (function(modules) { // webpackBootstrap if (this.labelDirty === true) { this.size.width = this._processLabel(ctx, selected); } - this.size.height = this.options.font.size * this.lineCount; + this.size.height = this.fontOptions.size * this.lineCount; this.size.left = x - this.size.width * 0.5; this.size.top = y - this.size.height * 0.5; - this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.options.font.size; + this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size; if (baseline == "hanging") { - this.size.top += 0.5 * this.options.font.size; + this.size.top += 0.5 * this.fontOptions.size; this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers this.size.yLine += 4; // distance from node } @@ -26574,7 +26573,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options.label !== undefined) { lines = String(this.options.label).split("\n"); lineCount = lines.length; - ctx.font = (selected ? "bold " : "") + this.options.font.size + "px " + this.options.font.face; + ctx.font = (selected ? "bold " : "") + this.fontOptions.size + "px " + this.fontOptions.face; width = ctx.measureText(lines[0]).width; for (var i = 1; i < lineCount; i++) { var lineWidth = ctx.measureText(lines[i]).width; @@ -27875,7 +27874,7 @@ return /******/ (function(modules) { // webpackBootstrap var util = __webpack_require__(1); var DataSet = __webpack_require__(7); var DataView = __webpack_require__(9); - var Edge = __webpack_require__(81); + var Edge = __webpack_require__(97); var EdgesHandler = (function () { function EdgesHandler(body, images, groups) { @@ -27977,7 +27976,18 @@ return /******/ (function(modules) { // webpackBootstrap _prototypeProperties(EdgesHandler, null, { setOptions: { - value: function setOptions(options) {}, + value: function setOptions(options) { + if (options) { + if (options.color !== undefined) { + if (util.isString(options.color)) { + util.assignAllKeys(this.options.color, options.color); + } else { + util.extend(this.options.color, options.color); + } + this.options.color.inherit.enabled = false; + } + } + }, writable: true, configurable: true }, @@ -28082,7 +28092,7 @@ return /******/ (function(modules) { // webpackBootstrap if (edge === null) { // update edge edge.disconnect(); - edge.setOptions(data); + dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. edge.connect(); } else { // create edge @@ -28148,512 +28158,396 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - 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"); } }; - var util = __webpack_require__(1); - - - var Label = _interopRequire(__webpack_require__(63)); - - var BezierEdgeDynamic = _interopRequire(__webpack_require__(82)); - - var BezierEdgeStatic = _interopRequire(__webpack_require__(85)); - - var StraightEdge = _interopRequire(__webpack_require__(86)); - /** - * @class Edge - * - * A edge connects two nodes - * @param {Object} properties Object with options. Must contain - * At least options from and to. - * Available options: 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 + * Created by Alex on 2/23/2015. */ - var Edge = (function () { - function Edge(options, body, globalOptions) { - _classCallCheck(this, Edge); - if (body === undefined) { - throw "No body provided"; - } + var BarnesHutSolver = __webpack_require__(82).BarnesHutSolver; + var Repulsion = __webpack_require__(83).Repulsion; + var HierarchicalRepulsion = __webpack_require__(84).HierarchicalRepulsion; + var SpringSolver = __webpack_require__(85).SpringSolver; + var HierarchicalSpringSolver = __webpack_require__(86).HierarchicalSpringSolver; + var CentralGravitySolver = __webpack_require__(87).CentralGravitySolver; - this.options = util.bridgeObject(globalOptions); - this.body = body; - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.title = undefined; - this.value = undefined; - this.selected = false; - this.hover = false; - this.labelDirty = true; - this.colorDirty = true; + var util = __webpack_require__(1); - this.from = undefined; // a node - this.to = undefined; // a node - this.edgeType = undefined; + var PhysicsEngine = (function () { + function PhysicsEngine(body) { + var _this = this; + _classCallCheck(this, PhysicsEngine); - this.connected = false; + this.body = body; + this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; - this.labelModule = new Label(this.body, this.options); + this.simulationInterval = 1000 / 60; + this.requiresTimeout = true; + this.previousStates = {}; + this.freezeCache = {}; + this.renderTimer == undefined; - this.setOptions(options, true); + this.stabilized = false; + this.stabilizationIterations = 0; + this.ready = false; // will be set to true if the stabilize - this.controlNodesEnabled = false; - this.controlNodes = { from: undefined, to: undefined, positions: {} }; - this.connectedNode = undefined; + // default options + this.options = {}; + this.defaultOptions = { + barnesHut: { + thetaInverted: 1 / 0.5, // inverted to save time during calculation + gravitationalConstant: -2000, + centralGravity: 0.3, + springLength: 95, + springConstant: 0.04, + damping: 0.09 + }, + repulsion: { + centralGravity: 0, + springLength: 200, + springConstant: 0.05, + nodeDistance: 100, + damping: 0.09 + }, + hierarchicalRepulsion: { + centralGravity: 0, + springLength: 100, + springConstant: 0.01, + nodeDistance: 150, + damping: 0.09 + }, + solver: "BarnesHut", + timestep: 0.5, + maxVelocity: 50, + minVelocity: 0.1, // px/s + stabilization: { + enabled: true, + iterations: 1000, // maximum number of iteration to stabilize + updateInterval: 100, + onlyDynamicEdges: false, + zoomExtent: true + } + }; + util.extend(this.options, this.defaultOptions); + + this.body.emitter.on("initPhysics", function () { + _this.initPhysics(); + }); + this.body.emitter.on("resetPhysics", function () { + _this.stopSimulation();_this.ready = false; + }); + this.body.emitter.on("startSimulation", function () { + if (_this.ready === true) { + _this.stabilized = false; + _this.runSimulation(); + } + }); + this.body.emitter.on("stopSimulation", function () { + _this.stopSimulation(); + }); } - _prototypeProperties(Edge, null, { + _prototypeProperties(PhysicsEngine, null, { setOptions: { - - - /** - * Set or overwrite options for the edge - * @param {Object} options an object with options - * @param doNotEmit - */ value: function setOptions(options) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - if (!options) { - return; - } - this.colorDirty = true; - - var fields = ["id", "font", "from", "hidden", "hoverWidth", "label", "length", "line", "opacity", "physics", "scaling", "selfReferenceSize", "to", "value", "width", "widthMin", "widthMax", "widthSelectionMultiplier"]; - util.selectiveDeepExtend(fields, this.options, options); - - util.mergeOptions(this.options, options, "smooth"); - util.mergeOptions(this.options, options, "dashes"); - - if (options.arrows !== undefined) { - util.mergeOptions(this.options.arrows, options.arrows, "to"); - util.mergeOptions(this.options.arrows, options.arrows, "middle"); - util.mergeOptions(this.options.arrows, options.arrows, "from"); - } - - if (options.id !== undefined) { - this.id = options.id; - } - if (options.from !== undefined) { - this.fromId = options.from; - } - if (options.to !== undefined) { - this.toId = options.to; - } - if (options.title !== undefined) { - this.title = options.title; + if (options !== undefined) { + util.selectiveNotDeepExtend(["stabilization"], this.options, options); + util.mergeOptions(this.options, options, "stabilization"); } - if (options.value !== undefined) { - this.value = options.value; + this.init(); + }, + writable: true, + configurable: true + }, + init: { + value: function init() { + var options; + if (this.options.solver == "repulsion") { + options = this.options.repulsion; + this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } else if (this.options.solver == "hierarchicalRepulsion") { + options = this.options.hierarchicalRepulsion; + this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); + this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); + } else { + // barnesHut + options = this.options.barnesHut; + this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); } - if (options.color !== undefined) { - if (util.isString(options.color)) { - this.options.color.color = options.color; - this.options.color.highlight = options.color; - } else { - if (options.color.color !== undefined) { - this.options.color.color = options.color.color; - } - if (options.color.highlight !== undefined) { - this.options.color.highlight = options.color.highlight; - } - if (options.color.hover !== undefined) { - this.options.color.hover = options.color.hover; - } - } - - // inherit colors - if (options.color.inherit === undefined) { - this.options.color.inherit.enabled = false; - } else { - util.mergeOptions(this.options.color, options.color, "inherit"); - } + this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); + this.modelOptions = options; + }, + writable: true, + configurable: true + }, + initPhysics: { + value: function initPhysics() { + this.stabilized = false; + if (this.options.stabilization.enabled === true) { + this.stabilize(); + } else { + this.ready = true; + this.body.emitter.emit("zoomExtent", { duration: 0 }, true); + this.runSimulation(); } - - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); - - this.labelModule.setOptions(this.options); - - this.updateEdgeType(); - - this.edgeType.setOptions(this.options); }, writable: true, configurable: true }, - updateEdgeType: { - value: function updateEdgeType() { - if (this.edgeType !== undefined) { - this.edgeType.cleanup(); + stopSimulation: { + value: function stopSimulation() { + this.stabilized = true; + if (this.viewFunction !== undefined) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.viewFunction = undefined; + this.body.emitter.emit("_stopRendering"); } - - if (this.options.smooth.enabled === true) { - if (this.options.smooth.dynamic === true) { - this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); - } else { - this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); - } - } else { - this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); + }, + writable: true, + configurable: true + }, + runSimulation: { + value: function runSimulation() { + if (this.viewFunction === undefined) { + this.viewFunction = this.simulationStep.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); } }, writable: true, configurable: true }, - togglePhysics: { + simulationStep: { + value: function simulationStep() { + // check if the physics have settled + var startTime = Date.now(); + this.physicsTick(); + var physicsTime = Date.now() - startTime; + // run double speed if it is a little graph + if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { + this.physicsTick(); - /** - * Enable or disable the physics. - * @param status - */ - value: function togglePhysics(status) { - if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { - if (this.via === undefined) { - this.via.pptions.physics = status; + // this makes sure there is no jitter. The decision is taken once to run it at double speed. + this.runDoubleSpeed = true; + } + + if (this.stabilized === true) { + if (this.stabilizationIterations > 1) { + // trigger the "stabilized" event. + // The event is triggered on the next tick, to prevent the case that + // it is fired while initializing the Network, in which case you would not + // be able to catch it + var me = this; + var params = { + iterations: this.stabilizationIterations + }; + this.stabilizationIterations = 0; + this.startedStabilization = false; + setTimeout(function () { + me.body.emitter.emit("stabilized", params); + }, 0); + } else { + this.stabilizationIterations = 0; } + this.stopSimulation(); } - this.options.physics = status; }, writable: true, configurable: true }, - connect: { + physicsTick: { /** - * Connect an edge to its nodes + * A single simulation step (or "tick") in the physics simulation + * + * @private */ - value: function connect() { - this.disconnect(); - - this.from = this.body.nodes[this.fromId] || undefined; - this.to = this.body.nodes[this.toId] || undefined; - this.connected = this.from !== undefined && this.to !== undefined; + value: function physicsTick() { + if (this.stabilized === false) { + this.calculateForces(); + this.stabilized = this.moveNodes(); - 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); + // determine if the network has stabilzied + if (this.stabilized === true) { + this.revert(); + } else { + // this is here to ensure that there is no start event when the network is already stable. + if (this.startedStabilization == false) { + this.body.emitter.emit("startStabilizing"); + this.startedStabilization = true; + } } + + this.stabilizationIterations++; } }, writable: true, configurable: true }, - disconnect: { - + updatePhysicsIndices: { /** - * Disconnect an edge from its nodes + * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also + * handled in the calculateForces function. We then use a quadratic curve with the center node as control. + * This function joins the datanodes and invisible (called support) nodes into one object. + * We do this so we do not contaminate this.body.nodes with the support nodes. + * + * @private */ - value: function disconnect() { - if (this.from) { - this.from.detachEdge(this); - this.from = undefined; + value: function updatePhysicsIndices() { + this.physicsBody.forces = {}; + this.physicsBody.physicsNodeIndices = []; + this.physicsBody.physicsEdgeIndices = []; + var nodes = this.body.nodes; + var edges = this.body.edges; + + // get node indices for physics + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].options.physics === true) { + this.physicsBody.physicsNodeIndices.push(nodeId); + } + } } - if (this.to) { - this.to.detachEdge(this); - this.to = undefined; + + // get edge indices for physics + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].options.physics === true) { + this.physicsBody.physicsEdgeIndices.push(edgeId); + } + } } - this.connected = false; - }, - writable: true, - configurable: true - }, - getTitle: { + // get the velocity and the forces vector + for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { + var nodeId = this.physicsBody.physicsNodeIndices[i]; + this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; + // forces can be reset because they are recalculated. Velocities have to persist. + if (this.physicsBody.velocities[nodeId] === undefined) { + this.physicsBody.velocities[nodeId] = { x: 0, y: 0 }; + } + } - /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. - */ - value: function getTitle() { - return typeof this.title === "function" ? this.title() : this.title; + // clean deleted nodes from the velocity vector + for (var nodeId in this.physicsBody.velocities) { + if (nodes[nodeId] === undefined) { + delete this.physicsBody.velocities[nodeId]; + } + } }, writable: true, configurable: true }, - isSelected: { - + revert: { + value: function revert() { + var nodeIds = Object.keys(this.previousStates); + var nodes = this.body.nodes; + var velocities = this.physicsBody.velocities; - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; + for (var i = 0; i < nodeIds.length; i++) { + var nodeId = nodeIds[i]; + if (nodes[nodeId] !== undefined) { + velocities[nodeId].x = this.previousStates[nodeId].vx; + velocities[nodeId].y = this.previousStates[nodeId].vy; + nodes[nodeId].x = this.previousStates[nodeId].x; + nodes[nodeId].y = this.previousStates[nodeId].y; + } else { + delete this.previousStates[nodeId]; + } + } }, writable: true, configurable: true }, - getValue: { + moveNodes: { + value: function moveNodes() { + var nodesPresent = false; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var maxVelocity = this.options.maxVelocity === 0 ? 1000000000 : this.options.maxVelocity; + var stabilized = true; + var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + var nodeVelocity = this._performStep(nodeId, maxVelocity); + // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized + stabilized = nodeVelocity < vminCorrected && stabilized === true; + nodesPresent = true; + } - /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.value; + if (nodesPresent == true) { + if (vminCorrected > 0.5 * this.options.maxVelocity) { + return false; + } else { + return stabilized; + } + } + return true; }, writable: true, configurable: true }, - setValueRange: { - - - /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - * @param total - */ - value: function setValueRange(min, max, total) { - if (this.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); - var widthDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled == true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.width = this.options.scaling.min + scale * widthDiff; - } - }, - writable: true, - configurable: true - }, - draw: { - - - /** - * 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 - */ - value: function draw(ctx) { - var via = this.edgeType.drawLine(ctx, this.selected, this.hover); - this.drawArrows(ctx, via); - this.drawLabel(ctx, via); - }, - writable: true, - configurable: true - }, - drawArrows: { - value: function drawArrows(ctx, viaNode) { - if (this.options.arrows.from.enabled === true) { - this.edgeType.drawArrowHead(ctx, "from", viaNode); - } - if (this.options.arrows.middle.enabled === true) { - this.edgeType.drawArrowHead(ctx, "middle", viaNode); - } - if (this.options.arrows.to.enabled === true) { - this.edgeType.drawArrowHead(ctx, "to", viaNode); - } - }, - writable: true, - configurable: true - }, - drawLabel: { - value: function drawLabel(ctx, viaNode) { - if (this.options.label !== undefined) { - // set style - var node1 = this.from; - var node2 = this.to; - var selected = this.from.selected || this.to.selected || this.selected; - if (node1.id != node2.id) { - var point = this.edgeType.getPoint(0.5, viaNode); - ctx.save(); - - // if the label has to be rotated: - if (this.options.font.align !== "horizontal") { - this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); - ctx.translate(point.x, this.labelModule.size.yLine); - this._rotateForLabelAlignment(ctx); - } - - // draw the label - this.labelModule.draw(ctx, point.x, point.y, selected); - ctx.restore(); - } else { - var x, y; - var radius = this.options.selfReferenceSize; - if (node1.width > node1.height) { - x = node1.x + node1.width * 0.5; - y = node1.y - radius; - } else { - x = node1.x + radius; - y = node1.y - node1.height * 0.5; - } - point = this._pointOnCircle(x, y, radius, 0.125); - - this.labelModule.draw(ctx, point.x, point.y, selected); - } - } - }, - writable: true, - configurable: true - }, - isOverlappingWith: { - - - /** - * 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 - */ - value: function isOverlappingWith(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; + _performStep: { + value: function _performStep(nodeId, maxVelocity) { + var node = this.body.nodes[nodeId]; + var timestep = this.options.timestep; + var forces = this.physicsBody.forces; + var velocities = this.physicsBody.velocities; - var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + // store the state so we can revert + this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; - return dist < distMax; + if (node.options.fixed.x === false) { + var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force + var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration + velocities[nodeId].x += ax * timestep; // velocity + velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; + node.x += velocities[nodeId].x * timestep; // position } else { - return false; + forces[nodeId].x = 0; + velocities[nodeId].x = 0; } - }, - writable: true, - configurable: true - }, - _rotateForLabelAlignment: { - - - /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _rotateForLabelAlignment(ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; + if (node.options.fixed.y === false) { + var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force + var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration + velocities[nodeId].y += ay * timestep; // velocity + velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; + node.y += velocities[nodeId].y * timestep; // position + } else { + forces[nodeId].y = 0; + velocities[nodeId].y = 0; } - ctx.rotate(angleInDegrees); - }, - writable: true, - configurable: true - }, - _pointOnCircle: { - - - /** - * 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 - */ - value: function _pointOnCircle(x, y, radius, percentage) { - var angle = percentage * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; - }, - writable: true, - configurable: true - }, - select: { - value: function select() { - this.selected = true; + var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); + return totalVelocity; }, writable: true, configurable: true }, - unselect: { - value: function unselect() { - this.selected = false; + calculateForces: { + value: function calculateForces() { + this.gravitySolver.solve(); + this.nodesSolver.solve(); + this.edgesSolver.solve(); }, writable: true, configurable: true }, - _drawControlNodes: { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + _freezeNodes: { - //*************************************************************************************************// - //*************************************************************************************************// - //*************************************************************************************************// - //*************************************************************************************************// - //*********************** MOVE THESE FUNCTIONS TO THE MANIPULATION SYSTEM ************************// - //*************************************************************************************************// - //*************************************************************************************************// - //*************************************************************************************************// - //*************************************************************************************************// @@ -28662,215 +28556,107 @@ return /******/ (function(modules) { // webpackBootstrap /** - * This function draws the control nodes for the manipulator. - * In order to enable this, only set the this.controlNodesEnabled to true. - * @param ctx + * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization + * because only the supportnodes for the smoothCurves have to settle. + * + * @private */ - value: function _drawControlNodes(ctx) { - if (this.controlNodesEnabled == true) { - if (this.controlNodes.from === undefined && this.controlNodes.to === undefined) { - var nodeIdFrom = "edgeIdFrom:".concat(this.id); - var nodeIdTo = "edgeIdTo:".concat(this.id); - var nodeFromOptions = { - id: nodeIdFrom, - shape: "dot", - color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } }, - radius: 7, - borderWidth: 2, - borderWidthSelected: 2, - hidden: false, - physics: false - }; - var nodeToOptions = util.deepExtend({}, nodeFromOptions); - nodeToOptions.id = nodeIdTo; - - - this.controlNodes.from = this.body.functions.createNode(nodeFromOptions); - this.controlNodes.to = this.body.functions.createNode(nodeToOptions); - } - - 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; + value: function _freezeNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].x && nodes[id].y) { + this.freezeCache[id] = { x: nodes[id].options.fixed.x, y: nodes[id].options.fixed.y }; + nodes[id].options.fixed.x = true; + nodes[id].options.fixed.y = true; + } } - - this.controlNodes.from.draw(ctx); - this.controlNodes.to.draw(ctx); - } else { - this.controlNodes = { from: undefined, to: undefined, positions: {} }; } }, writable: true, configurable: true }, - _enableControlNodes: { - - /** - * Enable control nodes. - * @private - */ - value: function _enableControlNodes() { - this.fromBackup = this.from; - this.toBackup = this.to; - this.controlNodesEnabled = true; - }, - writable: true, - configurable: true - }, - _disableControlNodes: { - + _restoreFrozenNodes: { /** - * disable control nodes and remove from dynamicEdges from old node + * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. + * * @private */ - value: function _disableControlNodes() { - 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); + value: function _restoreFrozenNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (this.freezeCache[id] !== undefined) { + nodes[id].options.fixed.x = this.freezeCache[id].x; + nodes[id].options.fixed.y = this.freezeCache[id].y; + } + } } - - this.fromBackup = undefined; - this.toBackup = undefined; - this.controlNodesEnabled = false; + this.freezeCache = {}; }, writable: true, configurable: true }, - _getSelectedControlNode: { - + stabilize: { /** - * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns undefined. - * @param x - * @param y - * @returns {undefined} + * Find a stable position for all nodes * @private */ - value: function _getSelectedControlNode(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 undefined; + value: function stabilize() { + if (this.options.stabilization.onlyDynamicEdges == true) { + this._freezeNodes(); } - }, - writable: true, - configurable: true - }, - _restoreControlNodes: { - - + this.stabilizationSteps = 0; - /** - * this resets the control nodes to their original position. - * @private - */ - value: function _restoreControlNodes() { - if (this.controlNodes.from.selected == true) { - this.from = this.connectedNode; - this.connectedNode = undefined; - this.controlNodes.from.unselect(); - } else if (this.controlNodes.to.selected == true) { - this.to = this.connectedNode; - this.connectedNode = undefined; - this.controlNodes.to.unselect(); - } + setTimeout(this._stabilizationBatch.bind(this), 0); }, writable: true, configurable: true }, - getControlNodeFromPosition: { - + _stabilizationBatch: { + value: function _stabilizationBatch() { + var count = 0; + while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) { + this.physicsTick(); + this.stabilizationSteps++; + count++; + } - /** - * this calculates the position of the control nodes on the edges of the parent nodes. - * - * @param ctx - * @returns {x: *, y: *} - */ - value: function getControlNodeFromPosition(ctx) { - // draw arrow head - var controlnodeFromPos; - if (this.options.smooth.enabled == true) { - controlnodeFromPos = this._findBorderPositionBezier(true, ctx); + if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) { + this.body.emitter.emit("stabilizationProgress", { steps: this.stabilizationSteps, total: this.options.stabilization.iterations }); + setTimeout(this._stabilizationBatch.bind(this), 0); } 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; + this._finalizeStabilization(); } - - return controlnodeFromPos; }, writable: true, configurable: true }, - getControlNodeToPosition: { - - - /** - * 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: *}}} - */ - value: function getControlNodeToPosition(ctx) { - // draw arrow head - var controlnodeToPos; - if (this.options.smooth.enabled == true) { - controlnodeToPos = this._findBorderPositionBezier(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; + _finalizeStabilization: { + value: function _finalizeStabilization() { + if (this.options.stabilization.zoomExtent == true) { + this.body.emitter.emit("zoomExtent", { duration: 0 }); + } - controlnodeToPos = {}; - controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; + if (this.options.stabilization.onlyDynamicEdges == true) { + this._restoreFrozenNodes(); } - return controlnodeToPos; + this.body.emitter.emit("stabilizationIterationsDone"); + this.body.emitter.emit("_requestRedraw"); + this.ready = true; }, writable: true, configurable: true } }); - return Edge; + return PhysicsEngine; })(); - module.exports = Edge; + module.exports = PhysicsEngine; /***/ }, /* 82 */ @@ -28878,790 +28664,939 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Created by Alex on 3/20/2015. + * Created by Alex on 2/23/2015. */ - var BezierBaseEdge = _interopRequire(__webpack_require__(83)); - - var BezierEdgeDynamic = (function (BezierBaseEdge) { - function BezierEdgeDynamic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeDynamic); + var BarnesHutSolver = (function () { + function BarnesHutSolver(body, physicsBody, options) { + _classCallCheck(this, BarnesHutSolver); - this.initializing = true; - this.via = undefined; - _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), "constructor", this).call(this, options, body, labelModule); - this.initializing = false; + this.body = body; + this.physicsBody = physicsBody; + this.barnesHutTree; + this.setOptions(options); } - _inherits(BezierEdgeDynamic, BezierBaseEdge); - - _prototypeProperties(BezierEdgeDynamic, null, { + _prototypeProperties(BarnesHutSolver, null, { setOptions: { value: function setOptions(options) { this.options = options; - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - this.id = this.options.id; - this.setupSupportNode(this.initializing); - }, - writable: true, - configurable: true - }, - cleanup: { - value: function cleanup() { - if (this.via !== undefined) { - delete this.body.nodes[this.via.id]; - this.via = undefined; - this.body.emitter.emit("_dataChanged"); - } }, writable: true, configurable: true }, - setupSupportNode: { + solve: { + /** - * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but - * are used for the force calculation. + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. * * @private */ - value: function setupSupportNode() { - var doNotEmit = arguments[0] === undefined ? false : arguments[0]; - var changedData = false; - if (this.via === undefined) { - changedData = true; - var nodeId = "edgeId:" + this.id; - var node = this.body.functions.createNode({ - id: nodeId, - mass: 1, - shape: "circle", - image: "", - physics: true, - hidden: true - }); - this.body.nodes[nodeId] = node; - this.via = node; - this.via.parentEdgeId = this.id; - this.positionBezierNode(); - } + value: function solve() { + if (this.options.gravitationalConstant != 0) { + var node; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var nodeCount = nodeIndices.length; - // node has been added or deleted - if (changedData === true && doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - positionBezierNode: { - value: function positionBezierNode() { - if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { - 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 !== undefined) { - this.via.x = 0; - this.via.y = 0; + // create the tree + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + + // for debugging + this.barnesHutTree = barnesHutTree; + + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHutSolver condition + this._getForceContribution(barnesHutTree.root.children.NW, node); + this._getForceContribution(barnesHutTree.root.children.NE, node); + this._getForceContribution(barnesHutTree.root.children.SW, node); + this._getForceContribution(barnesHutTree.root.children.SE, node); + } + } } }, writable: true, configurable: true }, - _line: { + _getForceContribution: { + /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node * @private */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); - ctx.stroke(); - return this.via; - }, - writable: true, - configurable: true - }, - getPoint: { + value: function _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx, dy, distance; + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); - /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} - * @private - */ - value: function getPoint(percentage) { - var t = percentage; - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; + // BarnesHutSolver condition + // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.options.thetaInverted) { + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; - return { x: x, y: y }; - }, - writable: true, - configurable: true - }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - console.log(this); - return this._findBorderPositionBezier(nearNode, ctx, this.via); + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount == 4) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); + } else { + // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { + // if it is not self + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.5 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } + } + } + } }, writable: true, configurable: true }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); - }, - writable: true, - configurable: true - } - }); + _formBarnesHutTree: { - return BezierEdgeDynamic; - })(BezierBaseEdge); - module.exports = BezierEdgeDynamic; + /** + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices + * @private + */ + value: function _formBarnesHutTree(nodes, nodeIndices) { + var node; + var nodeCount = nodeIndices.length; -/***/ }, -/* 83 */ -/***/ function(module, exports, __webpack_require__) { + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX = -Number.MAX_VALUE, + maxY = -Number.MAX_VALUE; - "use strict"; + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } + } + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) { + minY -= 0.5 * sizeDiff; + maxY += 0.5 * sizeDiff; + } // xSize > ySize + else { + minX += 0.5 * sizeDiff; + maxX -= 0.5 * sizeDiff; + } // xSize < ySize - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var minimumTreeSize = 0.00001; + var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), + centerY = 0.5 * (minY + maxY); - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + // construct the barnesHutTree + var barnesHutTree = { + root: { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { + minX: centerX - halfRootSize, maxX: centerX + halfRootSize, + minY: centerY - halfRootSize, maxY: centerY + halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: { data: null }, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root, node); + } + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // make global + return barnesHutTree; + }, + writable: true, + configurable: true + }, + _updateBranchMass: { - /** - * Created by Alex on 3/20/2015. - */ - var BaseEdge = _interopRequire(__webpack_require__(84)); + /** + * this updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node + * @private + */ + value: function _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; - var BezierBaseEdge = (function (BaseEdge) { - function BezierBaseEdge(options, body, labelModule) { - _classCallCheck(this, BezierBaseEdge); + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; - _get(Object.getPrototypeOf(BezierBaseEdge.prototype), "constructor", this).call(this, options, body, labelModule); - } + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; - _inherits(BezierBaseEdge, BaseEdge); + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + }, + writable: true, + configurable: true + }, + _placeInTree: { - _prototypeProperties(BezierBaseEdge, null, { - _findBorderPositionBezier: { /** - * This function uses binary search to look for the point where the bezier curve crosses the border of the node. + * determine in which branch the node will be placed. * - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode + * @param parentBranch + * @param node + * @param skipMassUpdate + * @private */ - value: function _findBorderPositionBezier(nearNode, ctx) { - var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; - console.log(nearNode, ctx, viaNode); + value: function _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); + } - var maxIterations = 10; - var iteration = 0; - var low = 0; - var high = 1; - var pos, angle, distanceToBorder, distanceToPoint, difference; - var threshold = 0.2; - var node = this.to; - var from = false; - if (nearNode.id === this.from.id) { - node = this.from; - from = true; + if (parentBranch.children.NW.range.maxX > node.x) { + // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { + // in NW + this._placeInRegion(parentBranch, node, "NW"); + } else { + // in SW + this._placeInRegion(parentBranch, node, "SW"); + } + } else { + // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { + // in NE + this._placeInRegion(parentBranch, node, "NE"); + } else { + // in SE + this._placeInRegion(parentBranch, node, "SE"); + } } + }, + writable: true, + configurable: true + }, + _placeInRegion: { - while (low <= high && iteration < maxIterations) { - var middle = (low + high) * 0.5; - pos = this.getPoint(middle, viaNode); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToPoint; - 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; + /** + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region + * @private + */ + value: function _placeInRegion(parentBranch, node, region) { + switch (parentBranch.children[region].childrenCount) { + case 0: + // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region], node); + break; + case 1: + // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x == node.x && parentBranch.children[region].children.data.y == node.y) { + node.x += Math.random(); + node.y += Math.random(); } else { - low = middle; + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); } - } - - iteration++; + break; + case 4: + // place in branch + this._placeInTree(parentBranch.children[region], node); + break; } - pos.t = middle; - - return pos; }, writable: true, configurable: true }, - _getDistanceToBezierEdge: { - + _splitBranch: { /** - * 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 + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. + * + * @param parentBranch * @private */ - value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { - // x3,y3 is the point - var xVia = undefined, - yVia = undefined; - xVia = via.x; - yVia = via.y; - var minDistance = 1000000000; - var distance = undefined; - var i = undefined, - t = undefined, - x = undefined, - y = undefined; - var lastX = x1; - var lastY = y1; - for (i = 1; 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; + value: function _splitBranch(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount == 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; + parentBranch.centerOfMass.x = 0; + parentBranch.centerOfMass.y = 0; } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch, "NW"); + this._insertRegion(parentBranch, "NE"); + this._insertRegion(parentBranch, "SW"); + this._insertRegion(parentBranch, "SE"); - return minDistance; + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); + } }, writable: true, configurable: true - } - }); + }, + _insertRegion: { - return BezierBaseEdge; - })(BaseEdge); - module.exports = BezierBaseEdge; + /** + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange + * @private + */ + value: function _insertRegion(parentBranch, region) { + var minX, maxX, minY, maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + } -/***/ }, -/* 84 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + parentBranch.children[region] = { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: { data: null }, + maxWidth: 0, + level: parentBranch.level + 1, + childrenCount: 0 + }; + }, + writable: true, + configurable: true + }, + _debug: { - 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 3/20/2015. - */ - var util = __webpack_require__(1); - var BaseEdge = (function () { - function BaseEdge(options, body, labelModule) { - _classCallCheck(this, BaseEdge); + //--------------------------- DEBUGGING BELOW ---------------------------// - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.colorDirty = true; - } - _prototypeProperties(BaseEdge, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - this.id = this.options.id; + /** + * This function is for debugging purposed, it draws the tree. + * + * @param ctx + * @param color + * @private + */ + value: function _debug(ctx, color) { + if (this.barnesHutTree !== undefined) { + ctx.lineWidth = 1; + + this._drawBranch(this.barnesHutTree.root, ctx, color); + } }, writable: true, configurable: true }, - drawLine: { + _drawBranch: { + /** - * 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 + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color * @private */ - value: function drawLine(ctx, selected, hover) { - // set style - ctx.strokeStyle = this.getColor(ctx); - ctx.lineWidth = this.getLineWidth(); - var via = undefined; - if (this.from != this.to) { - // draw line - if (this.options.dashes.enabled == true) { - via = this._drawDashedLine(ctx); - } else { - via = this._line(ctx); - } - } else { - var x = undefined, - y = undefined; - var radius = this.options.selfReferenceSize; - var node = this.from; - node.resize(ctx); - if (node.shape.width > node.shape.height) { - x = node.x + node.shape.width * 0.5; - y = node.y - radius; - } else { - x = node.x + radius; - y = node.y - node.shape.height * 0.5; - } - this._circle(ctx, x, y, radius); + value: function _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; } - return via; + if (branch.childrenCount == 4) { + this._drawBranch(branch.children.NW, ctx); + this._drawBranch(branch.children.NE, ctx); + this._drawBranch(branch.children.SE, ctx); + this._drawBranch(branch.children.SW, ctx); + } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.minY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); + + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ }, writable: true, configurable: true - }, - _drawDashedLine: { - value: function _drawDashedLine(ctx) { - var via = undefined; - // 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.dashes.length !== undefined && this.options.dashes.gap !== undefined) { - pattern = [this.options.dashes.length, this.options.dashes.gap]; - } else { - pattern = [5, 5]; - } + } + }); - // set dash settings for chrome or firefox - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; + return BarnesHutSolver; + })(); - // draw the line - via = this._line(ctx); + exports.BarnesHutSolver = BarnesHutSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); - // restore the dash settings. - ctx.setLineDash([0]); - ctx.lineDashOffset = 0; - ctx.restore(); - } else { - // unsupporting smooth lines - // draw dashes line - ctx.beginPath(); - ctx.lineCap = "round"; - if (this.options.dashes.altLength !== undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap, this.options.dashes.altLength, this.options.dashes.gap]); - } else if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap]); - } else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); - } - return via; +/***/ }, +/* 83 */ +/***/ 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/23/2015. + */ + + var RepulsionSolver = (function () { + function RepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, RepulsionSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(RepulsionSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - findBorderPosition: { - value: function findBorderPosition(nearNode, ctx, options) { - if (this.from != this.to) { - return this._findBorderPosition(nearNode, ctx, options); - } else { - return this._findBorderPositionCircle(nearNode, ctx, options); + solve: { + /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2; + + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; + + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; + + // approximation constants + var a = -2 / 3 / nodeDistance; + var b = 4 / 3; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (var i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (var j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + + if (distance < 2 * nodeDistance) { + if (distance < 0.5 * nodeDistance) { + repulsingForce = 1; + } else { + repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) + } + repulsingForce = repulsingForce / distance; + + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } } }, writable: true, configurable: true - }, - _findBorderPositionCircle: { + } + }); + return RepulsionSolver; + })(); + exports.RepulsionSolver = RepulsionSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); +/***/ }, +/* 84 */ +/***/ function(module, exports, __webpack_require__) { - /** - * This function uses binary search to look for the point where the circle crosses the border of the node. - * @param x - * @param y - * @param radius - * @param node - * @param low - * @param high - * @param direction - * @param ctx - * @returns {*} - * @private - */ - value: function _findBorderPositionCircle(node, ctx, options) { - var x = options.x; - var y = options.y; - var low = options.low; - var high = options.high; - var direction = options.direction; + "use strict"; - var maxIterations = 10; - var iteration = 0; - var radius = this.options.selfReferenceSize; - var pos = undefined, - angle = undefined, - distanceToBorder = undefined, - distanceToPoint = undefined, - difference = undefined; - var threshold = 0.05; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - while (low <= high && iteration < maxIterations) { - var _middle = (low + high) * 0.5; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - pos = this._pointOnCircle(x, y, radius, _middle); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToPoint; - 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 (direction > 0) { - low = _middle; - } else { - high = _middle; - } - } else { - if (direction > 0) { - high = _middle; - } else { - low = _middle; - } - } - iteration++; - } - pos.t = middle; + /** + * Created by Alex on 2/23/2015. + */ - return pos; + var HierarchicalRepulsionSolver = (function () { + function HierarchicalRepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalRepulsionSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(HierarchicalRepulsionSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - getLineWidth: { + solve: { /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * * @private */ - value: function getLineWidth(selected, hover) { - if (selected == true) { - return Math.max(Math.min(this.options.widthSelectionMultiplier * this.options.width, this.options.scaling.max), 0.3 / this.body.view.scale); - } else { - if (hover == true) { - return Math.max(Math.min(this.options.hoverWidth, this.options.scaling.max), 0.3 / this.body.view.scale); - } else { - return Math.max(this.options.width, 0.3 / this.body.view.scale); + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; + + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; + + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + // nodes only affect nodes on their level + if (node1.level == node2.level) { + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); + } else { + repulsingForce = 0; + } + // normalize force with + if (distance == 0) { + distance = 0.01; + } else { + repulsingForce = repulsingForce / distance; + } + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } } } }, writable: true, configurable: true - }, - getColor: { - value: function getColor(ctx) { - var colorObj = this.options.color; + } + }); - if (colorObj.inherit.enabled === true) { - if (colorObj.inherit.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; + return HierarchicalRepulsionSolver; + })(); - if (this.from.selected == false && this.to.selected == false) { - fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); - toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.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); + exports.HierarchicalRepulsionSolver = HierarchicalRepulsionSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); - // -------------------- this returns -------------------- // - return grd; - } +/***/ }, +/* 85 */ +/***/ function(module, exports, __webpack_require__) { - if (this.colorDirty === true) { - if (colorObj.inherit.source == "to") { - colorObj.highlight = this.to.options.color.highlight.border; - colorObj.hover = this.to.options.color.hover.border; - colorObj.color = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); - } else { - // (this.options.color.inherit.source == "from") { - colorObj.highlight = this.from.options.color.highlight.border; - colorObj.hover = this.from.options.color.hover.border; - colorObj.color = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); - } - } - } + "use strict"; - // if color inherit is on and gradients are used, the function has already returned by now. - this.colorDirty = false; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - if (this.selected == true) { - return colorObj.highlight; - } else if (this.hover == true) { - return colorObj.hover; - } else { - return colorObj.color; - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 2/23/2015. + */ + + var SpringSolver = (function () { + function SpringSolver(body, physicsBody, options) { + _classCallCheck(this, SpringSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(SpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - _circle: { + solve: { /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius + * This function calculates the springforces on the nodes, accounting for the support nodes. + * * @private */ - value: function _circle(ctx, x, y, radius) { - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); + value: function solve() { + var edgeLength, edge; + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var edges = this.body.edges; + + // forces caused by the edges, modelled as springs + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + // only calculate forces if nodes are in the same sector + if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { + if (edge.edgeType.via !== undefined) { + edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; + var node1 = edge.to; + var node2 = edge.edgeType.via; + var node3 = edge.from; + + + this._calculateSpringForce(node1, node2, 0.5 * edgeLength); + this._calculateSpringForce(node2, node3, 0.5 * edgeLength); + } else { + // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use + // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. + edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; + this._calculateSpringForce(edge.from, edge.to, edgeLength); + } + } + } + } }, writable: true, configurable: true }, - getDistanceToEdge: { + _calculateSpringForce: { /** - * 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 + * This is the code actually performing the calculation for the function above. + * + * @param node1 + * @param node2 + * @param edgeLength * @private */ - value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { - // x3,y3 is the point - var returnValue = 0; - if (this.from != this.to) { - returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); - } else { - var x, y, dx, dy; - var radius = this.options.selfReferenceSize; - 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); - } + value: function _calculateSpringForce(node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; - if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { - return 0; - } else { - return returnValue; - } + dx = node1.x - node2.x; + dy = node1.y - node2.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + this.physicsBody.forces[node1.id].x += fx; + this.physicsBody.forces[node1.id].y += fy; + this.physicsBody.forces[node2.id].x -= fx; + this.physicsBody.forces[node2.id].y -= fy; }, writable: true, configurable: true - }, - _getDistanceToLine: { - value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { - var px = x2 - x1; - var py = y2 - y1; - var something = px * px + py * py; - var u = ((x3 - x1) * px + (y3 - y1) * py) / something; + } + }); - if (u > 1) { - u = 1; - } else if (u < 0) { - u = 0; - } + return SpringSolver; + })(); - var x = x1 + u * px; - var y = y1 + u * py; - var dx = x - x3; - var dy = y - y3; + exports.SpringSolver = SpringSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); - //# 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 +/***/ }, +/* 86 */ +/***/ function(module, exports, __webpack_require__) { - return Math.sqrt(dx * dx + dy * dy); + "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/25/2015. + */ + + var HierarchicalSpringSolver = (function () { + function HierarchicalSpringSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalSpringSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(HierarchicalSpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - drawArrowHead: { + solve: { /** + * This function calculates the springforces on the nodes, accounting for the support nodes. * - * @param ctx - * @param position - * @param viaNode + * @private */ - value: function drawArrowHead(ctx, position, viaNode) { - // set style - ctx.strokeStyle = this.getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this.getLineWidth(); + value: function solve() { + var edgeLength, edge; + var dx, dy, fx, fy, springForce, distance; + var edges = this.body.edges; + var factor = 0.5; - // set lets - var angle = undefined; - var length = undefined; - var arrowPos = undefined; - var node1 = undefined; - var node2 = undefined; - var guideOffset = undefined; - var scaleFactor = undefined; + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - if (position == "from") { - node1 = this.from; - node2 = this.to; - guideOffset = 0.1; - scaleFactor = this.options.arrows.from.scaleFactor; - } else if (position == "to") { - node1 = this.to; - node2 = this.from; - guideOffset = -0.1; - scaleFactor = this.options.arrows.to.scaleFactor; - } else { - node1 = this.to; - node2 = this.from; - scaleFactor = this.options.arrows.middle.scaleFactor; + // initialize the spring force counters + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].springFx = 0; + forces[nodeId].springFy = 0; } - // if not connected to itself - if (node1 != node2) { - if (position !== "middle") { - // draw arrow head - if (this.options.smooth.enabled == true) { - arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); - var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); - angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + + // forces caused by the edges, modelled as springs + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; + + dx = edge.from.x - edge.to.x; + dy = edge.from.y - edge.to.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + if (edge.to.level != edge.from.level) { + forces[edge.toId].springFx -= fx; + forces[edge.toId].springFy -= fy; + forces[edge.fromId].springFx += fx; + forces[edge.fromId].springFy += fy; } else { - angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - arrowPos = this.findBorderPosition(node1, ctx); + forces[edge.toId].x -= factor * fx; + forces[edge.toId].y -= factor * fy; + forces[edge.fromId].x += factor * fx; + forces[edge.fromId].y += factor * fy; } - } else { - angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. - } - // draw arrow at the end of the line - length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(arrowPos.x, arrowPos.y, angle, length); - ctx.fill(); - ctx.stroke(); - } else { - // draw circle - var _angle = undefined, - point = undefined; - var x = undefined, - y = undefined; - var radius = this.options.selfReferenceSize; - if (!node1.width) { - node1.resize(ctx); } + } - // get circle coordinates - if (node1.width > node1.height) { - x = node1.x + node1.width * 0.5; - y = node1.y - radius; - } else { - x = node1.x + radius; - y = node1.y - node1.height * 0.5; - } + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); + springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); + forces[nodeId].x += springFx; + forces[nodeId].y += springFy; + } - if (position == "from") { - point = this.findBorderPosition(x, y, radius, node1, 0.25, 0.6, -1, ctx); - _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; - } else if (position == "to") { - point = this.findBorderPosition(x, y, radius, node1, 0.6, 0.8, 1, ctx); - _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; - } else { - point = this.findBorderPosition(x, y, radius, 0.175); - _angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; - } + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + totalFx += forces[nodeId].x; + totalFy += forces[nodeId].y; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; - // draw the arrowhead - var _length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(point.x, point.y, _angle, _length); - ctx.fill(); - ctx.stroke(); + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].x -= correctionFx; + forces[nodeId].y -= correctionFy; } }, writable: true, @@ -29669,393 +29604,811 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return BaseEdge; + return HierarchicalSpringSolver; })(); - module.exports = BaseEdge; + exports.HierarchicalSpringSolver = HierarchicalSpringSolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); /***/ }, -/* 85 */ +/* 87 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + /** + * Created by Alex on 2/23/2015. + */ + + var CentralGravitySolver = (function () { + function CentralGravitySolver(body, physicsBody, options) { + _classCallCheck(this, CentralGravitySolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(CentralGravitySolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { + value: function solve() { + var dx, dy, distance, node, i; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; + + + var gravity = this.options.centralGravity; + var gravityForce = 0; + + for (i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + node = nodes[nodeId]; + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + gravityForce = distance == 0 ? 0 : gravity / distance; + forces[nodeId].x = dx * gravityForce; + forces[nodeId].y = dy * gravityForce; + } + }, + writable: true, + configurable: true + } + }); + + return CentralGravitySolver; + })(); + + exports.CentralGravitySolver = CentralGravitySolver; + Object.defineProperty(exports, "__esModule", { + value: true + }); + +/***/ }, +/* 88 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + 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 3/20/2015. + * Created by Alex on 24-Feb-15. */ - var BezierBaseEdge = _interopRequire(__webpack_require__(83)); + var util = __webpack_require__(1); + var Cluster = _interopRequire(__webpack_require__(89)); - var BezierEdgeStatic = (function (BezierBaseEdge) { - function BezierEdgeStatic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeStatic); + var ClusterEngine = (function () { + function ClusterEngine(body) { + _classCallCheck(this, ClusterEngine); - _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), "constructor", this).call(this, options, body, labelModule); + this.body = body; + this.clusteredNodes = {}; } - _inherits(BezierEdgeStatic, BezierBaseEdge); - - _prototypeProperties(BezierEdgeStatic, null, { - cleanup: { - value: function cleanup() {}, + _prototypeProperties(ClusterEngine, null, { + setOptions: { + value: function setOptions(options) {}, writable: true, configurable: true }, - _line: { + clusterByConnectionCount: { + /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - var via = this._getViaCoordinates(); + * + * @param hubsize + * @param options + */ + value: function clusterByConnectionCount(hubsize, options) { + if (hubsize === undefined) { + hubsize = this._getHubSize(); + } else if (tyepof(hubsize) == "object") { + options = this._checkOptions(hubsize); + hubsize = this._getHubSize(); + } - // fallback to normal straight edges - if (via.x === undefined) { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return undefined; - } else { - ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); - ctx.stroke(); - return via; + var nodesToCluster = []; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length >= hubsize) { + nodesToCluster.push(node.id); + } + } + + for (var i = 0; i < nodesToCluster.length; i++) { + var node = this.body.nodes[nodesToCluster[i]]; + this.clusterByConnection(node, options, {}, {}, false); } + this.body.emitter.emit("_dataChanged"); }, writable: true, configurable: true }, - _getViaCoordinates: { - value: function _getViaCoordinates() { - var xVia = undefined; - var yVia = undefined; - var factor = this.options.smooth.roundness; - var type = this.options.smooth.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") { - dx = this.to.x - this.from.x; - dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; - - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); + clusterByNodeData: { - 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") { - dx = this.to.x - this.from.x; - dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); + /** + * loop over all nodes, check if they adhere to the condition and cluster if needed. + * @param options + * @param refreshData + */ + value: function clusterByNodeData() { + var options = arguments[0] === undefined ? {} : arguments[0]; + var refreshData = arguments[1] === undefined ? true : arguments[1]; + if (options.joinCondition === undefined) { + throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); + } - 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; + // check if the options object is fine, append if needed + options = this._checkOptions(options); + + var childNodesObj = {}; + var childEdgesObj = {}; + + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + } + + this._cluster(childNodesObj, childEdgesObj, options, refreshData); + }, + writable: true, + configurable: true + }, + clusterOutliers: { + + + /** + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param refreshData + */ + value: function clusterOutliers(options) { + var refreshData = arguments[1] === undefined ? true : arguments[1]; + options = this._checkOptions(options); + var clusters = []; + + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var childNodesObj = {}; + var childEdgesObj = {}; + var nodeId = this.body.nodeIndices[i]; + if (this.body.nodes[nodeId].edges.length == 1) { + var edge = this.body.nodes[nodeId].edges[0]; + var childNodeId = this._getConnectedId(edge, nodeId); + if (childNodeId != nodeId) { + if (options.joinCondition === undefined) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + clonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } } + clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); } - } 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; + } + } + + for (var i = 0; i < clusters.length; i++) { + this._cluster(clusters[i].nodes, clusters[i].edges, options, false); + } + + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + clusterByConnection: { + + /** + * + * @param nodeId + * @param options + * @param refreshData + */ + value: function clusterByConnection(nodeId, options) { + var refreshData = arguments[2] === undefined ? true : arguments[2]; + // kill conditions + if (nodeId === undefined) { + throw new Error("No nodeId supplied to clusterByConnection!"); + } + if (this.body.nodes[nodeId] === undefined) { + throw new Error("The nodeId given to clusterByConnection does not exist!"); + } + + var node = this.body.nodes[nodeId]; + options = this._checkOptions(options, node); + if (options.clusterNodeProperties.x === undefined) { + options.clusterNodeProperties.x = node.x; + } + if (options.clusterNodeProperties.y === undefined) { + options.clusterNodeProperties.y = node.y; + } + if (options.clusterNodeProperties.fixed === undefined) { + options.clusterNodeProperties.fixed = {}; + options.clusterNodeProperties.fixed.x = node.options.fixed.x; + options.clusterNodeProperties.fixed.y = node.options.fixed.y; + } + + + var childNodesObj = {}; + var childEdgesObj = {}; + var parentNodeId = node.id; + var parentClonedOptions = this._cloneOptions(parentNodeId); + childNodesObj[parentNodeId] = node; + + // collect the nodes that will be in the cluster + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + var childNodeId = this._getConnectedId(edge, parentNodeId); + + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + // clone the options and insert some additional parameters that could be interesting. + var childClonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } - } 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; + } + } else { + childEdgesObj[edge.id] = edge; + } + } + + this._cluster(childNodesObj, childEdgesObj, options, refreshData); + }, + writable: true, + configurable: true + }, + _cloneOptions: { + + + /** + * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. + * @param objId + * @param type + * @returns {{}} + * @private + */ + value: function _cloneOptions(objId, type) { + var clonedOptions = {}; + if (type === undefined || type == "node") { + util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); + util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); + clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; + } else { + util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); + } + return clonedOptions; + }, + writable: true, + configurable: true + }, + _createClusterEdges: { + + + /** + * This function creates the edges that will be attached to the cluster. + * + * @param childNodesObj + * @param childEdgesObj + * @param newEdges + * @param options + * @private + */ + value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { + var edge, childNodeId, childNode; + + var childKeys = Object.keys(childNodesObj); + for (var i = 0; i < childKeys.length; i++) { + childNodeId = childKeys[i]; + childNode = childNodesObj[childNodeId]; + + // mark all edges for removal from global and construct new edges from the cluster to others + for (var j = 0; j < childNode.edges.length; j++) { + edge = childNode.edges[j]; + childEdgesObj[edge.id] = edge; + + var otherNodeId = edge.toId; + var otherOnTo = true; + if (edge.toId != childNodeId) { + otherNodeId = edge.toId; + otherOnTo = true; + } else if (edge.fromId != childNodeId) { + otherNodeId = edge.fromId; + otherOnTo = false; + } + + if (childNodesObj[otherNodeId] === undefined) { + var clonedOptions = this._cloneOptions(edge.id, "edge"); + util.deepExtend(clonedOptions, options.clusterEdgeProperties); + if (otherOnTo === true) { + clonedOptions.from = options.clusterNodeProperties.id; + clonedOptions.to = otherNodeId; + } else { + clonedOptions.from = otherNodeId; + clonedOptions.to = options.clusterNodeProperties.id; } + clonedOptions.id = "clusterEdge:" + util.randomUUID(); + newEdges.push(this.body.functions.createEdge(clonedOptions)); } } } - return { x: xVia, y: yVia }; }, writable: true, configurable: true }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx, options) { - return this._findBorderPositionBezier(nearNode, ctx, options.via); + _checkOptions: { + + + /** + * This function checks the options that can be supplied to the different cluster functions + * for certain fields and inserts defaults if needed + * @param options + * @returns {*} + * @private + */ + value: function _checkOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + if (options.clusterEdgeProperties === undefined) { + options.clusterEdgeProperties = {}; + } + if (options.clusterNodeProperties === undefined) { + options.clusterNodeProperties = {}; + } + + return options; }, writable: true, configurable: true }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); + _cluster: { + + /** + * + * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node + * @param {Object} childEdgesObj | object with edge objects, id as keys + * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} + * @param {Boolean} refreshData | when true, do not wrap up + * @private + */ + value: function _cluster(childNodesObj, childEdgesObj, options) { + var refreshData = arguments[3] === undefined ? true : arguments[3]; + // kill condition: no children so cant cluster + if (Object.keys(childNodesObj).length == 0) { + return; + } + + // check if we have an unique id; + if (options.clusterNodeProperties.id === undefined) { + options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); + } + var clusterId = options.clusterNodeProperties.id; + + // create the new edges that will connect to the cluster + var newEdges = []; + this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); + + // construct the clusterNodeProperties + var clusterNodeProperties = options.clusterNodeProperties; + if (options.processProperties !== undefined) { + // get the childNode options + var childNodesOptions = []; + for (var nodeId in childNodesObj) { + var clonedOptions = this._cloneOptions(nodeId); + childNodesOptions.push(clonedOptions); + } + + // get clusterproperties based on childNodes + var childEdgesOptions = []; + for (var edgeId in childEdgesObj) { + var clonedOptions = this._cloneOptions(edgeId, "edge"); + childEdgesOptions.push(clonedOptions); + } + + clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); + if (!clusterNodeProperties) { + throw new Error("The processClusterProperties function does not return properties!"); + } + } + if (clusterNodeProperties.label === undefined) { + clusterNodeProperties.label = "cluster"; + } + + + // give the clusterNode a postion if it does not have one. + var pos = undefined; + if (clusterNodeProperties.x === undefined) { + pos = this._getClusterPosition(childNodesObj); + clusterNodeProperties.x = pos.x; + clusterNodeProperties.allowedToMoveX = true; + } + if (clusterNodeProperties.x === undefined) { + if (pos === undefined) { + pos = this._getClusterPosition(childNodesObj); + } + clusterNodeProperties.y = pos.y; + clusterNodeProperties.allowedToMoveY = true; + } + + + // force the ID to remain the same + clusterNodeProperties.id = clusterId; + + + // create the clusterNode + var clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); + clusterNode.isCluster = true; + clusterNode.containedNodes = childNodesObj; + clusterNode.containedEdges = childEdgesObj; + + + // disable the childEdges + for (var edgeId in childEdgesObj) { + if (childEdgesObj.hasOwnProperty(edgeId)) { + if (this.body.edges[edgeId] !== undefined) { + var edge = this.body.edges[edgeId]; + edge.togglePhysics(false); + edge.options.hidden = true; + } + } + } + + + // disable the childNodes + for (var nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; + this.body.nodes[nodeId].togglePhysics(false); + this.body.nodes[nodeId].options.hidden = true; + } + } + + + // finally put the cluster node into global + this.body.nodes[clusterNodeProperties.id] = clusterNode; + + + // push new edges to global + for (var i = 0; i < newEdges.length; i++) { + this.body.edges[newEdges[i].id] = newEdges[i]; + this.body.edges[newEdges[i].id].connect(); + } + + // set ID to undefined so no duplicates arise + clusterNodeProperties.id = undefined; + + + // wrap up + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + isCluster: { + + + /** + * Check if a node is a cluster. + * @param nodeId + * @returns {*} + */ + value: function isCluster(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].isCluster === true; + } else { + console.log("Node does not exist."); + return false; + } + }, + writable: true, + configurable: true + }, + _getClusterPosition: { + + /** + * get the position of the cluster node based on what's inside + * @param {object} childNodesObj | object with node objects, id as keys + * @returns {{x: number, y: number}} + * @private + */ + value: function _getClusterPosition(childNodesObj) { + var childKeys = Object.keys(childNodesObj); + var minX = childNodesObj[childKeys[0]].x; + var maxX = childNodesObj[childKeys[0]].x; + var minY = childNodesObj[childKeys[0]].y; + var maxY = childNodesObj[childKeys[0]].y; + var node; + for (var i = 0; i < childKeys.lenght; i++) { + node = childNodesObj[childKeys[0]]; + minX = node.x < minX ? node.x : minX; + maxX = node.x > maxX ? node.x : maxX; + minY = node.y < minY ? node.y : minY; + maxY = node.y > maxY ? node.y : maxY; + } + return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; + }, + writable: true, + configurable: true + }, + openCluster: { + + + /** + * Open a cluster by calling this function. + * @param {String} clusterNodeId | the ID of the cluster node + * @param {Boolean} refreshData | wrap up afterwards if not true + */ + value: function openCluster(clusterNodeId) { + var refreshData = arguments[1] === undefined ? true : arguments[1]; + // kill conditions + if (clusterNodeId === undefined) { + throw new Error("No clusterNodeId supplied to openCluster."); + } + if (this.body.nodes[clusterNodeId] === undefined) { + throw new Error("The clusterNodeId supplied to openCluster does not exist."); + } + if (this.body.nodes[clusterNodeId].containedNodes === undefined) { + console.log("The node:" + clusterNodeId + " is not a cluster.");return; + }; + + var clusterNode = this.body.nodes[clusterNodeId]; + var containedNodes = clusterNode.containedNodes; + var containedEdges = clusterNode.containedEdges; + + // release nodes + for (var nodeId in containedNodes) { + if (containedNodes.hasOwnProperty(nodeId)) { + var containedNode = this.body.nodes[nodeId]; + containedNode = containedNodes[nodeId]; + // inherit position + containedNode.x = clusterNode.x; + containedNode.y = clusterNode.y; + + // inherit speed + containedNode.vx = clusterNode.vx; + containedNode.vy = clusterNode.vy; + + containedNode.options.hidden = false; + containedNode.togglePhysics(true); + + delete this.clusteredNodes[nodeId]; + } + } + + // release edges + for (var edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + var edge = this.body.edges[edgeId]; + edge.options.hidden = false; + edge.togglePhysics(true); + } + } + + // remove all temporary edges + for (var i = 0; i < clusterNode.edges.length; i++) { + var edgeId = clusterNode.edges[i].id; + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = undefined; + delete this.body.nodes[viaId]; + } + // this removes the edge from node.edges, which is why edgeIds is formed + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; + } + + // remove clusterNode + delete this.body.nodes[clusterNodeId]; + + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + _connectEdge: { + + + + /** + * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to + * is currently residing in cluster B + * @param edge + * @param nodeId + * @param from + * @private + */ + value: function _connectEdge(edge, nodeId, from) { + var clusterStack = this._getClusterStack(nodeId); + if (from == true) { + edge.from = clusterStack[clusterStack.length - 1]; + edge.fromId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.fromArray = clusterStack; + } else { + edge.to = clusterStack[clusterStack.length - 1]; + edge.toId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.toArray = clusterStack; + } + edge.connect(); + }, + writable: true, + configurable: true + }, + _getClusterStack: { + + /** + * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node + * @param nodeId + * @returns {Array} + * @private + */ + value: function _getClusterStack(nodeId) { + var stack = []; + var max = 100; + var counter = 0; + + while (this.clusteredNodes[nodeId] !== undefined && counter < max) { + stack.push(this.clusteredNodes[nodeId].node); + nodeId = this.clusteredNodes[nodeId].clusterId; + counter++; + } + stack.push(this.body.nodes[nodeId]); + return stack; + }, + writable: true, + configurable: true + }, + _getConnectedId: { + + + /** + * Get the Id the node is connected to + * @param edge + * @param nodeId + * @returns {*} + * @private + */ + value: function _getConnectedId(edge, nodeId) { + if (edge.toId != nodeId) { + return edge.toId; + } else if (edge.fromId != nodeId) { + return edge.fromId; + } else { + return edge.fromId; + } }, writable: true, configurable: true }, - getPoint: { + _getHubSize: { /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} - * @private - */ - value: function getPoint(percentage) { - var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; - var t = percentage; - 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; + * We determine how many connections denote an important hub. + * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) + * + * @private + */ + value: function _getHubSize() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; - return { x: x, y: y }; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length > largestHub) { + largestHub = node.edges.length; + } + average += node.edges.length; + averageSquared += Math.pow(node.edges.length, 2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; + + var variance = averageSquared - Math.pow(average, 2); + var standardDeviation = Math.sqrt(variance); + + var hubThreshold = Math.floor(average + 2 * standardDeviation); + + // always have at least one to cluster + if (hubThreshold > largestHub) { + hubThreshold = largestHub; + } + + return hubThreshold; }, writable: true, configurable: true } }); - return BezierEdgeStatic; - })(BezierBaseEdge); + return ClusterEngine; + })(); - module.exports = BezierEdgeStatic; + module.exports = ClusterEngine; /***/ }, -/* 86 */ +/* 89 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var Node = _interopRequire(__webpack_require__(62)); + /** - * Created by Alex on 3/20/2015. + * */ + var Cluster = (function (Node) { + function Cluster(options, body, imagelist, grouplist, globalOptions) { + _classCallCheck(this, Cluster); - var BaseEdge = _interopRequire(__webpack_require__(84)); - - var StraightEdge = (function (BaseEdge) { - function StraightEdge(options, body, labelModule) { - _classCallCheck(this, StraightEdge); + _get(Object.getPrototypeOf(Cluster.prototype), "constructor", this).call(this, options, body, imagelist, grouplist, globalOptions); - _get(Object.getPrototypeOf(StraightEdge.prototype), "constructor", this).call(this, options, body, labelModule); + this.isCluster = true; + this.containedNodes = {}; + this.containedEdges = {}; } - _inherits(StraightEdge, BaseEdge); - - _prototypeProperties(StraightEdge, null, { - cleanup: { - value: function cleanup() {}, - writable: true, - configurable: true - }, - _line: { - /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return undefined; - }, - writable: true, - configurable: true - }, - getPoint: { - - - /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} - * @private - */ - value: function getPoint(percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y - }; - }, - writable: true, - configurable: true - }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - var node1 = this.to; - var node2 = this.from; - if (nearNode.id === this.from.id) { - node1 = this.from; - node2 = this.to; - } - - - - var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - var dx = node1.x - node2.x; - var dy = node1.y - node2.y; - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = nearNode.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - - var borderPos = {}; - borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; - borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; - - return borderPos; - }, - writable: true, - configurable: true - }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); - }, - writable: true, - configurable: true - } - }); + _inherits(Cluster, Node); - return StraightEdge; - })(BaseEdge); + return Cluster; + })(Node); - module.exports = StraightEdge; + module.exports = Cluster; /***/ }, -/* 87 */ +/* 90 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -30065,502 +30418,324 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Created by Alex on 2/23/2015. + * Created by Alex on 26-Feb-15. */ - var BarnesHutSolver = __webpack_require__(88).BarnesHutSolver; - var Repulsion = __webpack_require__(89).Repulsion; - var HierarchicalRepulsion = __webpack_require__(90).HierarchicalRepulsion; - var SpringSolver = __webpack_require__(91).SpringSolver; - var HierarchicalSpringSolver = __webpack_require__(92).HierarchicalSpringSolver; - var CentralGravitySolver = __webpack_require__(93).CentralGravitySolver; - + if (typeof window !== "undefined") { + window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; + } var util = __webpack_require__(1); - var PhysicsEngine = (function () { - function PhysicsEngine(body) { + var CanvasRenderer = (function () { + function CanvasRenderer(body, canvas) { var _this = this; - _classCallCheck(this, PhysicsEngine); + _classCallCheck(this, CanvasRenderer); this.body = body; - this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; + this.canvas = canvas; - this.simulationInterval = 1000 / 60; + this.redrawRequested = false; + this.renderTimer = false; this.requiresTimeout = true; - this.previousStates = {}; - this.renderTimer == undefined; + this.renderingActive = false; + this.renderRequests = 0; + this.pixelRatio = undefined; - this.stabilized = false; - this.stabilizationIterations = 0; - this.ready = false; // will be set to true if the stabilize + this.canvasTopLeft = { x: 0, y: 0 }; + this.canvasBottomRight = { x: 0, y: 0 }; - // default options - this.options = {}; - this.defaultOptions = { - barnesHut: { - thetaInverted: 1 / 0.5, // inverted to save time during calculation - gravitationalConstant: -2000, - centralGravity: 0.3, - springLength: 95, - springConstant: 0.04, - damping: 0.09 - }, - repulsion: { - centralGravity: 0, - springLength: 200, - springConstant: 0.05, - nodeDistance: 100, - damping: 0.09 - }, - hierarchicalRepulsion: { - centralGravity: 0, - springLength: 100, - springConstant: 0.01, - nodeDistance: 150, - damping: 0.09 - }, - solver: "BarnesHut", - timestep: 0.5, - maxVelocity: 50, - minVelocity: 0.1, // px/s - stabilization: { - enabled: true, - iterations: 1000, // maximum number of iteration to stabilize - updateInterval: 100, - onlyDynamicEdges: false, - zoomExtent: true - } - }; - util.extend(this.options, this.defaultOptions); + this.dragging = false; - this.body.emitter.on("initPhysics", function () { - _this.initPhysics(); + this.body.emitter.on("dragStart", function () { + _this.dragging = true; }); - this.body.emitter.on("resetPhysics", function () { - _this.stopSimulation();_this.ready = false; + this.body.emitter.on("dragEnd", function () { + return _this.dragging = false; }); - this.body.emitter.on("startSimulation", function () { - if (_this.ready === true) { - _this.stabilized = false; - _this.runSimulation(); + this.body.emitter.on("_redraw", function () { + if (_this.renderingActive === false) { + _this._redraw(); } }); - this.body.emitter.on("stopSimulation", function () { - _this.stopSimulation(); + this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); + this.body.emitter.on("_startRendering", function () { + _this.renderRequests += 1;_this.renderingActive = true;_this.startRendering(); + }); + this.body.emitter.on("_stopRendering", function () { + _this.renderRequests -= 1;_this.renderingActive = _this.renderRequests > 0; }); + + this.options = {}; + this.defaultOptions = { + hideEdgesOnDrag: false, + hideNodesOnDrag: false + }; + util.extend(this.options, this.defaultOptions); + + this._determineBrowserMethod(); } - _prototypeProperties(PhysicsEngine, null, { + _prototypeProperties(CanvasRenderer, null, { setOptions: { value: function setOptions(options) { if (options !== undefined) { - util.selectiveNotDeepExtend(["stabilization"], this.options, options); - util.mergeOptions(this.options, options, "stabilization"); - } - this.init(); - }, - writable: true, - configurable: true - }, - init: { - value: function init() { - var options; - if (this.options.solver == "repulsion") { - options = this.options.repulsion; - this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); - } else if (this.options.solver == "hierarchicalRepulsion") { - options = this.options.hierarchicalRepulsion; - this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); - this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); - } else { - // barnesHut - options = this.options.barnesHut; - this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); - } - - this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); - this.modelOptions = options; - }, - writable: true, - configurable: true - }, - initPhysics: { - value: function initPhysics() { - this.stabilized = false; - if (this.options.stabilization.enabled === true) { - this.stabilize(); - } else { - this.ready = true; - this.body.emitter.emit("zoomExtent", { duration: 0 }, true); - this.runSimulation(); - } - }, - writable: true, - configurable: true - }, - stopSimulation: { - value: function stopSimulation() { - this.stabilized = true; - if (this.viewFunction !== undefined) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.viewFunction = undefined; - this.body.emitter.emit("_stopRendering"); + util.deepExtend(this.options, options); } }, writable: true, configurable: true }, - runSimulation: { - value: function runSimulation() { - if (this.viewFunction === undefined) { - this.viewFunction = this.simulationStep.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); - } + startRendering: { + value: function startRendering() { + 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 + } else { + this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function + } + } + } else {} }, writable: true, configurable: true }, - simulationStep: { - value: function simulationStep() { - // check if the physics have settled - var startTime = Date.now(); - this.physicsTick(); - var physicsTime = Date.now() - startTime; - - // run double speed if it is a little graph - if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { - this.physicsTick(); + renderStep: { + value: function renderStep() { + // reset the renderTimer so a new scheduled animation step can be set + this.renderTimer = undefined; - // this makes sure there is no jitter. The decision is taken once to run it at double speed. - this.runDoubleSpeed = true; + if (this.requiresTimeout == true) { + // this schedules a new simulation step + this.startRendering(); } - if (this.stabilized === true) { - if (this.stabilizationIterations > 1) { - // trigger the "stabilized" event. - // The event is triggered on the next tick, to prevent the case that - // it is fired while initializing the Network, in which case you would not - // be able to catch it - var me = this; - var params = { - iterations: this.stabilizationIterations - }; - this.stabilizationIterations = 0; - this.startedStabilization = false; - setTimeout(function () { - me.body.emitter.emit("stabilized", params); - }, 0); - } else { - this.stabilizationIterations = 0; - } - this.stopSimulation(); + this._redraw(); + + if (this.requiresTimeout == false) { + // this schedules a new simulation step + this.startRendering(); } }, writable: true, configurable: true }, - physicsTick: { + redraw: { /** - * A single simulation step (or "tick") in the physics simulation - * - * @private + * Redraw the network with the current data + * chart will be resized too. */ - value: function physicsTick() { - if (this.stabilized === false) { - this.calculateForces(); - this.stabilized = this.moveNodes(); - - // determine if the network has stabilzied - if (this.stabilized === true) { - this.revert(); - } else { - // this is here to ensure that there is no start event when the network is already stable. - if (this.startedStabilization == false) { - this.body.emitter.emit("startStabilizing"); - this.startedStabilization = true; - } - } - - this.stabilizationIterations++; - } + value: function redraw() { + this.setSize(this.constants.width, this.constants.height); + this._redraw(); }, writable: true, configurable: true }, - _updatePhysicsIndices: { + _requestRedraw: { /** - * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also - * handled in the calculateForces function. We then use a quadratic curve with the center node as control. - * This function joins the datanodes and invisible (called support) nodes into one object. - * We do this so we do not contaminate this.body.nodes with the support nodes. - * + * Redraw the network with the current data + * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. * @private */ - value: function _updatePhysicsIndices() { - this.physicsBody.forces = {}; - this.physicsBody.physicsNodeIndices = []; - this.physicsBody.physicsEdgeIndices = []; - var nodes = this.body.nodes; - var edges = this.body.edges; - - // get node indices for physics - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].options.physics === true) { - this.physicsBody.physicsNodeIndices.push(nodeId); - } - } - } - - // get edge indices for physics - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].options.physics === true) { - this.physicsBody.physicsEdgeIndices.push(edgeId); - } - } - } - - // get the velocity and the forces vector - for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { - var nodeId = this.physicsBody.physicsNodeIndices[i]; - this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; - - // forces can be reset because they are recalculated. Velocities have to persist. - if (this.physicsBody.velocities[nodeId] === undefined) { - this.physicsBody.velocities[nodeId] = { x: 0, y: 0 }; - } - } - - // clean deleted nodes from the velocity vector - for (var nodeId in this.physicsBody.velocities) { - if (nodes[nodeId] === undefined) { - delete this.physicsBody.velocities[nodeId]; - } - } - }, - writable: true, - configurable: true - }, - revert: { - value: function revert() { - var nodeIds = Object.keys(this.previousStates); - var nodes = this.body.nodes; - var velocities = this.physicsBody.velocities; - - for (var i = 0; i < nodeIds.length; i++) { - var nodeId = nodeIds[i]; - if (nodes[nodeId] !== undefined) { - velocities[nodeId].x = this.previousStates[nodeId].vx; - velocities[nodeId].y = this.previousStates[nodeId].vy; - nodes[nodeId].x = this.previousStates[nodeId].x; - nodes[nodeId].y = this.previousStates[nodeId].y; - } else { - delete this.previousStates[nodeId]; - } - } - }, - writable: true, - configurable: true - }, - moveNodes: { - value: function moveNodes() { - var nodesPresent = false; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var maxVelocity = this.options.maxVelocity === 0 ? 1000000000 : this.options.maxVelocity; - var stabilized = true; - var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); - - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - var nodeVelocity = this._performStep(nodeId, maxVelocity); - // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized - stabilized = nodeVelocity < vminCorrected && stabilized === true; - nodesPresent = true; - } - - - if (nodesPresent == true) { - if (vminCorrected > 0.5 * this.options.maxVelocity) { - return false; + value: function _requestRedraw() { + if (this.redrawRequested !== true && this.renderingActive === false) { + this.redrawRequested = true; + if (this.requiresTimeout === true) { + window.setTimeout(this._redraw.bind(this, false), 0); } else { - return stabilized; + window.requestAnimationFrame(this._redraw.bind(this, false)); } } - return true; }, writable: true, configurable: true }, - _performStep: { - value: function _performStep(nodeId, maxVelocity) { - var node = this.body.nodes[nodeId]; - var timestep = this.options.timestep; - var forces = this.physicsBody.forces; - var velocities = this.physicsBody.velocities; + _redraw: { + value: function _redraw() { + var hidden = arguments[0] === undefined ? false : arguments[0]; + this.body.emitter.emit("initRedraw"); - // store the state so we can revert - this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; + this.redrawRequested = false; + var ctx = this.canvas.frame.canvas.getContext("2d"); - if (!node.xFixed) { - var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force - var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration - velocities[nodeId].x += ax * timestep; // velocity - velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; - node.x += velocities[nodeId].x * timestep; // position - } else { - forces[nodeId].x = 0; - velocities[nodeId].x = 0; + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); } - if (!node.yFixed) { - var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force - var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration - velocities[nodeId].y += ay * timestep; // velocity - velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; - node.y += velocities[nodeId].y * timestep; // position - } else { - forces[nodeId].y = 0; - velocities[nodeId].y = 0; - } + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); - return totalVelocity; - }, - writable: true, - configurable: true - }, - calculateForces: { - value: function calculateForces() { - this.gravitySolver.solve(); - this.nodesSolver.solve(); - this.edgesSolver.solve(); - }, - writable: true, - configurable: true - }, - _freezeNodes: { + // clear the canvas + var w = this.canvas.frame.canvas.clientWidth; + var h = this.canvas.frame.canvas.clientHeight; + ctx.clearRect(0, 0, w, h); + this.body.emitter.emit("beforeDrawing", ctx); + // set scaling and translation + ctx.save(); + ctx.translate(this.body.view.translation.x, this.body.view.translation.y); + ctx.scale(this.body.view.scale, this.body.view.scale); + 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 }); + if (hidden === false) { + if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { + this._drawEdges(ctx); + } + } + if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { + this._drawNodes(ctx, hidden); + } + if (this.controlNodesActive === true) { + this._drawControlNodes(ctx); + } + //this.physics.nodesSolver._debug(ctx,"#F00F0F"); + // restore original scaling and translation + ctx.restore(); - /** - * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization - * because only the supportnodes for the smoothCurves have to settle. - * - * @private - */ - value: function _freezeNodes() { - var nodes = this.body.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].x != null && nodes[id].y != null) { - nodes[id].fixedData.x = nodes[id].xFixed; - nodes[id].fixedData.y = nodes[id].yFixed; - nodes[id].xFixed = true; - nodes[id].yFixed = true; - } - } + if (hidden === true) { + ctx.clearRect(0, 0, w, h); } + + this.body.emitter.emit("afterDrawing", ctx); }, writable: true, configurable: true }, - _restoreFrozenNodes: { + _drawNodes: { + /** - * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. - * + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] * @private */ - value: function _restoreFrozenNodes() { + value: function _drawNodes(ctx) { + var alwaysShow = arguments[1] === undefined ? false : arguments[1]; var nodes = this.body.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].fixedData.x != null) { - nodes[id].xFixed = nodes[id].fixedData.x; - nodes[id].yFixed = nodes[id].fixedData.y; + var nodeIndices = this.body.nodeIndices; + var node; + var selected = []; + + // draw unselected nodes; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + // set selected nodes aside + if (node.isSelected()) { + selected.push(nodeIndices[i]); + } else { + if (alwaysShow === true) { + node.draw(ctx); } + // todo: replace check + //else if (node.inArea() === true) { + node.draw(ctx); + //} } } + + // draw the selected nodes on top + for (var i = 0; i < selected.length; i++) { + node = nodes[selected[i]]; + node.draw(ctx); + } }, writable: true, configurable: true }, - stabilize: { + _drawEdges: { + /** - * Find a stable position for all nodes + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function stabilize() { - if (this.options.stabilization.onlyDynamicEdges == true) { - this._freezeNodes(); - } - this.stabilizationSteps = 0; + value: function _drawEdges(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge; - setTimeout(this._stabilizationBatch.bind(this), 0); + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edge.draw(ctx); + } + } }, writable: true, configurable: true }, - _stabilizationBatch: { - value: function _stabilizationBatch() { - var count = 0; - while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) { - this.physicsTick(); - this.stabilizationSteps++; - count++; - } + _drawControlNodes: { - if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) { - this.body.emitter.emit("stabilizationProgress", { steps: this.stabilizationSteps, total: this.options.stabilization.iterations }); - setTimeout(this._stabilizationBatch.bind(this), 0); - } else { - this._finalizeStabilization(); + /** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawControlNodes(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge; + + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + edge._drawControlNodes(ctx); } }, writable: true, configurable: true }, - _finalizeStabilization: { - value: function _finalizeStabilization() { - if (this.options.stabilization.zoomExtent == true) { - this.body.emitter.emit("zoomExtent", { duration: 0 }); - } + _determineBrowserMethod: { - if (this.options.stabilization.onlyDynamicEdges == true) { - this._restoreFrozenNodes(); + /** + * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because + * some implementations (safari and IE9) did not support requestAnimationFrame + * @private + */ + value: function _determineBrowserMethod() { + if (typeof window !== "undefined") { + var browserType = navigator.userAgent.toLowerCase(); + this.requiresTimeout = false; + if (browserType.indexOf("msie 9.0") != -1) { + // IE 9 + this.requiresTimeout = true; + } else if (browserType.indexOf("safari") != -1) { + // safari + if (browserType.indexOf("chrome") <= -1) { + this.requiresTimeout = true; + } + } + } else { + this.requiresTimeout = true; } - - this.body.emitter.emit("stabilizationIterationsDone"); - this.body.emitter.emit("_requestRedraw"); - this.ready = true; }, writable: true, configurable: true } }); - return PhysicsEngine; + return CanvasRenderer; })(); - module.exports = PhysicsEngine; + module.exports = CanvasRenderer; /***/ }, -/* 88 */ +/* 91 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -30569,611 +30744,285 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + + var util = __webpack_require__(1); + /** - * Created by Alex on 2/23/2015. + * Create the main frame for the Network. + * This function is executed once when a Network object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + * @private */ - - var BarnesHutSolver = (function () { - function BarnesHutSolver(body, physicsBody, options) { - _classCallCheck(this, BarnesHutSolver); + var Canvas = (function () { + function Canvas(body) { + var _this = this; + _classCallCheck(this, Canvas); this.body = body; - this.physicsBody = physicsBody; - this.barnesHutTree; - this.setOptions(options); + + this.options = {}; + this.defaultOptions = { + width: "100%", + height: "100%" + }; + util.extend(this.options, this.defaultOptions); + + this.body.emitter.once("resize", function (obj) { + _this.body.view.translation.x = obj.width * 0.5;_this.body.view.translation.y = obj.height * 0.5; + }); + this.body.emitter.on("destroy", function () { + return _this.hammer.destroy(); + }); + + this.pixelRatio = 1; } - _prototypeProperties(BarnesHutSolver, null, { + _prototypeProperties(Canvas, null, { setOptions: { value: function setOptions(options) { - this.options = options; + if (options !== undefined) { + util.deepExtend(this.options, options); + } }, writable: true, configurable: true }, - solve: { + create: { + value: function create() { + // remove all elements from the container element. + while (this.body.container.hasChildNodes()) { + this.body.container.removeChild(this.body.container.firstChild); + } + this.frame = document.createElement("div"); + this.frame.className = "vis network-frame"; + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; + this.frame.tabIndex = 900; - /** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. - * - * @private - */ - value: function solve() { - if (this.options.gravitationalConstant != 0) { - var node; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var nodeCount = nodeIndices.length; + ////////////////////////////////////////////////////////////////// - // create the tree - var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + this.frame.canvas = document.createElement("canvas"); + this.frame.canvas.style.position = "relative"; + this.frame.appendChild(this.frame.canvas); - // for debugging - this.barnesHutTree = barnesHutTree; + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement("DIV"); + noCanvas.style.color = "red"; + noCanvas.style.fontWeight = "bold"; + noCanvas.style.padding = "10px"; + noCanvas.innerHTML = "Error: your browser does not support HTML canvas"; + this.frame.canvas.appendChild(noCanvas); + } else { + var ctx = this.frame.canvas.getContext("2d"); + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHutSolver condition - this._getForceContribution(barnesHutTree.root.children.NW, node); - this._getForceContribution(barnesHutTree.root.children.NE, node); - this._getForceContribution(barnesHutTree.root.children.SW, node); - this._getForceContribution(barnesHutTree.root.children.SE, node); - } - } + this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } - }, - writable: true, - configurable: true - }, - _getForceContribution: { - - - /** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. - * - * @param parentBranch - * @param node - * @private - */ - value: function _getForceContribution(parentBranch, node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx, dy, distance; - - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); - // BarnesHutSolver condition - // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.options.thetaInverted) { - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.1 * Math.random(); - dx = distance; - } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; + // add the frame to the container element + this.body.container.appendChild(this.frame); - this.physicsBody.forces[node.id].x += fx; - this.physicsBody.forces[node.id].y += fy; - } else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount == 4) { - this._getForceContribution(parentBranch.children.NW, node); - this._getForceContribution(parentBranch.children.NE, node); - this._getForceContribution(parentBranch.children.SW, node); - this._getForceContribution(parentBranch.children.SE, node); - } else { - // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { - // if it is not self - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.5 * Math.random(); - dx = distance; - } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; + this.body.view.scale = 1; + this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; - this.physicsBody.forces[node.id].x += fx; - this.physicsBody.forces[node.id].y += fy; - } - } - } - } + this._bindHammer(); }, writable: true, configurable: true }, - _formBarnesHutTree: { + _bindHammer: { /** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. - * - * @param nodes - * @param nodeIndices + * This function binds hammer, it can be repeated over and over due to the uniqueness check. * @private */ - value: function _formBarnesHutTree(nodes, nodeIndices) { - var node; - var nodeCount = nodeIndices.length; - - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX = -Number.MAX_VALUE, - maxY = -Number.MAX_VALUE; - - // get the range of the nodes - for (var i = 0; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { - minX = x; - } - if (x > maxX) { - maxX = x; - } - if (y < minY) { - minY = y; - } - if (y > maxY) { - maxY = y; - } - } - } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) { - minY -= 0.5 * sizeDiff; - maxY += 0.5 * sizeDiff; - } // xSize > ySize - else { - minX += 0.5 * sizeDiff; - maxX -= 0.5 * sizeDiff; - } // xSize < ySize - - - var minimumTreeSize = 0.00001; - var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), - centerY = 0.5 * (minY + maxY); - - // construct the barnesHutTree - var barnesHutTree = { - root: { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { - minX: centerX - halfRootSize, maxX: centerX + halfRootSize, - minY: centerY - halfRootSize, maxY: centerY + halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data: null }, - maxWidth: 0, - level: 0, - childrenCount: 4 - } - }; - this._splitBranch(barnesHutTree.root); - - // place the nodes one by one recursively - for (i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root, node); - } + value: function _bindHammer() { + if (this.hammer !== undefined) { + this.hammer.destroy(); } + this.drag = {}; + this.pinch = {}; - // make global - return barnesHutTree; - }, - writable: true, - configurable: true - }, - _updateBranchMass: { - - - /** - * this updates the mass of a branch. this is increased by adding a node. - * - * @param parentBranch - * @param node - * @private - */ - value: function _updateBranchMass(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1 / totalMass; + // init hammer + this.hammer = new Hammer(this.frame.canvas); + this.hammer.get("pinch").set({ enable: true }); - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; + 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", this.body.eventListeners.onPinch); - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; + // 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", this.body.eventListeners.onMouseWheel); + this.frame.canvas.addEventListener("DOMMouseScroll", this.body.eventListeners.onMouseWheel); - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); - parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + this.frame.canvas.addEventListener("mousemove", this.body.eventListeners.onMouseMove); + + this.hammerFrame = new Hammer(this.frame); + hammerUtil.onRelease(this.hammerFrame, this.body.eventListeners.onRelease); }, writable: true, configurable: true }, - _placeInTree: { + setSize: { /** - * determine in which branch the node will be placed. - * - * @param parentBranch - * @param node - * @param skipMassUpdate - * @private + * Set a new size for the network + * @param {string} width Width in pixels or percentage (for example '800px' + * or '50%') + * @param {string} height Height in pixels or percentage (for example '400px' + * or '30%') */ - value: function _placeInTree(parentBranch, node, skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch, node); - } + value: function setSize() { + var width = arguments[0] === undefined ? this.options.width : arguments[0]; + var height = arguments[1] === undefined ? this.options.height : arguments[1]; + var emitEvent = false; + var oldWidth = this.frame.canvas.width; + var oldHeight = this.frame.canvas.height; + if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { + this.frame.style.width = width; + this.frame.style.height = height; - if (parentBranch.children.NW.range.maxX > node.x) { - // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { - // in NW - this._placeInRegion(parentBranch, node, "NW"); - } else { - // in SW - this._placeInRegion(parentBranch, node, "SW"); - } + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; + + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + + this.options.width = width; + this.options.height = height; + + emitEvent = true; } else { - // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { - // in NE - this._placeInRegion(parentBranch, node, "NE"); - } else { - // in SE - this._placeInRegion(parentBranch, node, "SE"); + // this would adapt the width of the canvas to the width from 100% if and only if + // there is a change. + + if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + emitEvent = true; + } + if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + emitEvent = true; } } + + 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 }); + } }, writable: true, configurable: true }, - _placeInRegion: { + _XconvertDOMtoCanvas: { /** - * actually place the node in a region (or branch) - * - * @param parentBranch - * @param node - * @param region + * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to + * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) + * @param {number} x + * @returns {number} * @private */ - value: function _placeInRegion(parentBranch, node, region) { - switch (parentBranch.children[region].childrenCount) { - case 0: - // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region], node); - break; - case 1: - // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x == node.x && parentBranch.children[region].children.data.y == node.y) { - node.x += Math.random(); - node.y += Math.random(); - } else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region], node); - } - break; - case 4: - // place in branch - this._placeInTree(parentBranch.children[region], node); - break; - } + value: function _XconvertDOMtoCanvas(x) { + return (x - this.body.view.translation.x) / this.body.view.scale; }, writable: true, configurable: true }, - _splitBranch: { - + _XconvertCanvasToDOM: { /** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. - * - * @param parentBranch + * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to + * the X coordinate in DOM-space (coordinate point in browser relative to the container div) + * @param {number} x + * @returns {number} * @private */ - value: function _splitBranch(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount == 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; - parentBranch.centerOfMass.x = 0; - parentBranch.centerOfMass.y = 0; - } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch, "NW"); - this._insertRegion(parentBranch, "NE"); - this._insertRegion(parentBranch, "SW"); - this._insertRegion(parentBranch, "SE"); - - if (containedNode != null) { - this._placeInTree(parentBranch, containedNode); - } + value: function _XconvertCanvasToDOM(x) { + return x * this.body.view.scale + this.body.view.translation.x; }, writable: true, configurable: true }, - _insertRegion: { - + _YconvertDOMtoCanvas: { /** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch - * - * @param parentBranch - * @param region - * @param parentRange + * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to + * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) + * @param {number} y + * @returns {number} * @private */ - value: function _insertRegion(parentBranch, region) { - var minX, maxX, minY, maxY; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - } - - - parentBranch.children[region] = { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: { data: null }, - maxWidth: 0, - level: parentBranch.level + 1, - childrenCount: 0 - }; + value: function _YconvertDOMtoCanvas(y) { + return (y - this.body.view.translation.y) / this.body.view.scale; }, writable: true, configurable: true }, - _debug: { - - - - - //--------------------------- DEBUGGING BELOW ---------------------------// - + _YconvertCanvasToDOM: { /** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color + * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to + * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) + * @param {number} y + * @returns {number} * @private */ - value: function _debug(ctx, color) { - if (this.barnesHutTree !== undefined) { - ctx.lineWidth = 1; - - this._drawBranch(this.barnesHutTree.root, ctx, color); - } + value: function _YconvertCanvasToDOM(y) { + return y * this.body.view.scale + this.body.view.translation.y; }, writable: true, configurable: true }, - _drawBranch: { + canvasToDOM: { /** - * This function is for debugging purposes. It draws the branches recursively. * - * @param branch - * @param ctx - * @param color - * @private + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor */ - value: function _drawBranch(branch, ctx, color) { - if (color === undefined) { - color = "#FF0000"; - } - - if (branch.childrenCount == 4) { - this._drawBranch(branch.children.NW, ctx); - this._drawBranch(branch.children.NE, ctx); - this._drawBranch(branch.children.SE, ctx); - this._drawBranch(branch.children.SW, ctx); - } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.minY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.minY); - ctx.stroke(); - - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } - */ - }, - writable: true, - configurable: true - } - }); - - return BarnesHutSolver; - })(); - - exports.BarnesHutSolver = BarnesHutSolver; - Object.defineProperty(exports, "__esModule", { - value: true - }); - -/***/ }, -/* 89 */ -/***/ 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/23/2015. - */ - - var RepulsionSolver = (function () { - function RepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, RepulsionSolver); - - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } - - _prototypeProperties(RepulsionSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + value: function canvasToDOM(pos) { + return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; }, writable: true, configurable: true }, - solve: { + DOMtoCanvas: { + /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. * - * @private + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor */ - value: function solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2; - - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; - - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; - - // approximation constants - var a = -2 / 3 / nodeDistance; - var b = 4 / 3; - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (var i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (var j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. - if (distance == 0) { - distance = 0.1 * Math.random(); - dx = distance; - } - - if (distance < 2 * nodeDistance) { - if (distance < 0.5 * nodeDistance) { - repulsingForce = 1; - } else { - repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) - } - repulsingForce = repulsingForce / distance; - - fx = dx * repulsingForce; - fy = dy * repulsingForce; - - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; - } - } - } + value: function DOMtoCanvas(pos) { + return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; }, writable: true, configurable: true } }); - return RepulsionSolver; + return Canvas; })(); - exports.RepulsionSolver = RepulsionSolver; - Object.defineProperty(exports, "__esModule", { - value: true - }); + module.exports = Canvas; /***/ }, -/* 90 */ +/* 92 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -31183,321 +31032,393 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Created by Alex on 2/23/2015. + * Created by Alex on 26-Feb-15. */ - var HierarchicalRepulsionSolver = (function () { - function HierarchicalRepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalRepulsionSolver); + var util = __webpack_require__(1); + + var View = (function () { + function View(body, canvas) { + var _this = this; + _classCallCheck(this, View); this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); + this.canvas = canvas; + + this.animationSpeed = 1 / this.renderRefreshRate; + this.animationEasingFunction = "easeInOutQuint"; + this.easingTime = 0; + this.sourceScale = 0; + this.targetScale = 0; + this.sourceTranslation = 0; + this.targetTranslation = 0; + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + this.touchTime = 0; + + this.viewFunction = undefined; + + this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); + this.body.emitter.on("animationFinished", function () { + _this.body.emitter.emit("_stopRendering"); + }); + this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); } - _prototypeProperties(HierarchicalRepulsionSolver, null, { + _prototypeProperties(View, null, { setOptions: { - value: function setOptions(options) { + value: function setOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; this.options = options; }, writable: true, configurable: true }, - solve: { + _getRange: { + + // zoomExtent /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. - * + * Find the center position of the network * @private */ - value: function solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; - - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; - - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - - // nodes only affect nodes on their level - if (node1.level == node2.level) { - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - var steepness = 0.05; - if (distance < nodeDistance) { - repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); - } else { - repulsingForce = 0; + value: function _getRange() { + var specificNodes = arguments[0] === undefined ? [] : arguments[0]; + var minY = 1000000000, + maxY = -1000000000, + minX = 1000000000, + maxX = -1000000000, + node; + if (specificNodes.length > 0) { + for (var i = 0; i < specificNodes.length; i++) { + node = this.body.nodes[specificNodes[i]]; + if (minX > node.boundingBox.left) { + minX = node.boundingBox.left; + } + if (maxX < node.boundingBox.right) { + maxX = node.boundingBox.right; + } + if (minY > node.boundingBox.bottom) { + minY = node.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.boundingBox.top) { + maxY = node.boundingBox.bottom; + } // top is negative, bottom is positive + } + } else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (minX > node.boundingBox.left) { + minX = node.boundingBox.left; } - // normalize force with - if (distance == 0) { - distance = 0.01; - } else { - repulsingForce = repulsingForce / distance; + if (maxX < node.boundingBox.right) { + maxX = node.boundingBox.right; } - fx = dx * repulsingForce; - fy = dy * repulsingForce; - - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; + if (minY > node.boundingBox.bottom) { + minY = node.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.boundingBox.top) { + maxY = node.boundingBox.bottom; + } // top is negative, bottom is positive } } } + + if (minX == 1000000000 && maxX == -1000000000 && minY == 1000000000 && maxY == -1000000000) { + minY = 0, maxY = 0, minX = 0, maxX = 0; + } + return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; + }, + writable: true, + configurable: true + }, + _findCenter: { + + + /** + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} + * @private + */ + value: function _findCenter(range) { + return { x: 0.5 * (range.maxX + range.minX), + y: 0.5 * (range.maxY + range.minY) }; }, writable: true, configurable: true - } - }); + }, + zoomExtent: { - return HierarchicalRepulsionSolver; - })(); - exports.HierarchicalRepulsionSolver = HierarchicalRepulsionSolver; - Object.defineProperty(exports, "__esModule", { - value: true - }); + /** + * This function zooms out to fit all data on screen based on amount of nodes + * @param {Object} + * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; + * @param {Boolean} [disableStart] | If true, start is not called. + */ + value: function zoomExtent() { + var options = arguments[0] === undefined ? { nodes: [] } : arguments[0]; + var initialZoom = arguments[1] === undefined ? false : arguments[1]; + var range; + var zoomLevel; -/***/ }, -/* 91 */ -/***/ function(module, exports, __webpack_require__) { + if (initialZoom == true) { + // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. + var positionDefined = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (node.predefinedPosition == true) { + positionDefined += 1; + } + } + } + if (positionDefined > 0.5 * this.body.nodeIndices.length) { + this.zoomExtent(options, false); + return; + } - "use strict"; + range = this._getRange(options.nodes); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var numberOfNodes = this.body.nodeIndices.length; + zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // correct for larger canvasses. + var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); + zoomLevel *= factor; + } else { + this.body.emitter.emit("_redraw", true); + range = this._getRange(options.nodes); + var xDistance = Math.abs(range.maxX - range.minX) * 1.1; + var yDistance = Math.abs(range.maxY - range.minY) * 1.1; - /** - * Created by Alex on 2/23/2015. - */ + var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; + zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + } - var SpringSolver = (function () { - function SpringSolver(body, physicsBody, options) { - _classCallCheck(this, SpringSolver); + if (zoomLevel > 1) { + zoomLevel = 1; + } - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } - _prototypeProperties(SpringSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + var center = this._findCenter(range); + var animationOptions = { position: center, scale: zoomLevel, animation: options }; + this.moveTo(animationOptions); }, writable: true, configurable: true }, - solve: { + focusOnNode: { + + // animation /** - * This function calculates the springforces on the nodes, accounting for the support nodes. + * Center a node in view. * - * @private + * @param {Number} nodeId + * @param {Number} [options] */ - value: function solve() { - var edgeLength, edge; - var edgeIndices = this.physicsBody.physicsEdgeIndices; - var edges = this.body.edges; - - // forces caused by the edges, modelled as springs - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - // only calculate forces if nodes are in the same sector - if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { - if (edge.edgeType.via !== undefined) { - edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - var node1 = edge.to; - var node2 = edge.edgeType.via; - var node3 = edge.from; - + value: function focusOnNode(nodeId) { + var options = arguments[1] === undefined ? {} : arguments[1]; + if (this.body.nodes[nodeId] !== undefined) { + var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; + options.position = nodePosition; + options.lockedOnNode = nodeId; - this._calculateSpringForce(node1, node2, 0.5 * edgeLength); - this._calculateSpringForce(node2, node3, 0.5 * edgeLength); - } else { - // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use - // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. - edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; - this._calculateSpringForce(edge.from, edge.to, edgeLength); - } - } - } + this.moveTo(options); + } else { + console.log("Node: " + nodeId + " cannot be found."); } }, writable: true, configurable: true }, - _calculateSpringForce: { - + moveTo: { /** - * This is the code actually performing the calculation for the function above. * - * @param node1 - * @param node2 - * @param edgeLength - * @private + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.scale = Number // scale to move to + * | options.position = {x:Number, y:Number} // position to move to + * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to */ - value: function _calculateSpringForce(node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; - - dx = node1.x - node2.x; - dy = node1.y - node2.y; - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance == 0 ? 0.01 : distance; - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; + value: function moveTo(options) { + if (options === undefined) { + options = {}; + return; + } + if (options.offset === undefined) { + options.offset = { x: 0, y: 0 }; + } + if (options.offset.x === undefined) { + options.offset.x = 0; + } + if (options.offset.y === undefined) { + options.offset.y = 0; + } + if (options.scale === undefined) { + options.scale = this.body.view.scale; + } + if (options.position === undefined) { + options.position = this.body.view.translation; + } + if (options.animation === undefined) { + options.animation = { duration: 0 }; + } + if (options.animation === false) { + options.animation = { duration: 0 }; + } + if (options.animation === true) { + options.animation = {}; + } + if (options.animation.duration === undefined) { + options.animation.duration = 1000; + } // default duration + if (options.animation.easingFunction === undefined) { + options.animation.easingFunction = "easeInOutQuad"; + } // default easing function - this.physicsBody.forces[node1.id].x += fx; - this.physicsBody.forces[node1.id].y += fy; - this.physicsBody.forces[node2.id].x -= fx; - this.physicsBody.forces[node2.id].y -= fy; + this.animateView(options); }, writable: true, configurable: true - } - }); - - return SpringSolver; - })(); - - exports.SpringSolver = SpringSolver; - Object.defineProperty(exports, "__esModule", { - value: true - }); - -/***/ }, -/* 92 */ -/***/ function(module, exports, __webpack_require__) { + }, + animateView: { - "use strict"; + /** + * + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.time = Number // animation time in milliseconds + * | options.scale = Number // scale to animate to + * | options.position = {x:Number, y:Number} // position to animate to + * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, + * // easeInCubic, easeOutCubic, easeInOutCubic, + * // easeInQuart, easeOutQuart, easeInOutQuart, + * // easeInQuint, easeOutQuint, easeInOutQuint + */ + value: function animateView(options) { + if (options === undefined) { + return; + } + this.animationEasingFunction = options.animation.easingFunction; + // release if something focussed on the node + this.releaseNode(); + if (options.locked == true) { + this.lockedOnNodeId = options.lockedOnNode; + this.lockedOnNodeOffset = options.offset; + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + // forcefully complete the old animation if it was still running + if (this.easingTime != 0) { + this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + this.sourceScale = this.body.view.scale; + this.sourceTranslation = this.body.view.translation; + this.targetScale = options.scale; - /** - * Created by Alex on 2/25/2015. - */ + // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw + // but at least then we'll have the target transition + this.body.view.scale = this.targetScale; + var viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - options.position.x, + y: viewCenter.y - options.position.y + }; + this.targetTranslation = { + x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, + y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y + }; - var HierarchicalSpringSolver = (function () { - function HierarchicalSpringSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalSpringSolver); + // if the time is set to 0, don't do an animation + if (options.animation.duration == 0) { + if (this.lockedOnNodeId != undefined) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + } else { + this.body.view.scale = this.targetScale; + this.body.view.translation = this.targetTranslation; + this.body.emitter.emit("_requestRedraw"); + } + } else { + this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's + this.animationEasingFunction = options.animation.easingFunction; - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } - _prototypeProperties(HierarchicalSpringSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + this.viewFunction = this._transitionRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } }, writable: true, configurable: true }, - solve: { + _lockedRedraw: { /** - * This function calculates the springforces on the nodes, accounting for the support nodes. - * + * used to animate smoothly by hijacking the redraw function. * @private */ - value: function solve() { - var edgeLength, edge; - var dx, dy, fx, fy, springForce, distance; - var edges = this.body.edges; - var factor = 0.5; - - var edgeIndices = this.physicsBody.physicsEdgeIndices; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + value: function _lockedRedraw() { + var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; + var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - nodePosition.x, + y: viewCenter.y - nodePosition.y + }; + var sourceTranslation = this.body.view.translation; + var targetTranslation = { + x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, + y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y + }; - // initialize the spring force counters - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - forces[nodeId].springFx = 0; - forces[nodeId].springFy = 0; + this.body.view.translation = targetTranslation; + }, + writable: true, + configurable: true + }, + releaseNode: { + value: function releaseNode() { + if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; } + }, + writable: true, + configurable: true + }, + _transitionRedraw: { + /** + * + * @param easingTime + * @private + */ + value: function _transitionRedraw() { + var finished = arguments[0] === undefined ? false : arguments[0]; + this.easingTime += this.animationSpeed; + this.easingTime = finished === true ? 1 : this.easingTime; - // forces caused by the edges, modelled as springs - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - - dx = edge.from.x - edge.to.x; - dy = edge.from.y - edge.to.y; - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance == 0 ? 0.01 : distance; - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; + var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - fx = dx * springForce; - fy = dy * springForce; + this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress; + this.body.view.translation = { + x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, + y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress + }; - if (edge.to.level != edge.from.level) { - forces[edge.toId].springFx -= fx; - forces[edge.toId].springFy -= fy; - forces[edge.fromId].springFx += fx; - forces[edge.fromId].springFy += fy; - } else { - forces[edge.toId].x -= factor * fx; - forces[edge.toId].y -= factor * fy; - forces[edge.fromId].x += factor * fx; - forces[edge.fromId].y += factor * fy; - } + // cleanup + if (this.easingTime >= 1) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.easingTime = 0; + if (this.lockedOnNodeId != undefined) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); } - } - - // normalize spring forces - var springForce = 1; - var springFx, springFy; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); - springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); - - forces[nodeId].x += springFx; - forces[nodeId].y += springFy; - } - - // retain energy balance - var totalFx = 0; - var totalFy = 0; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - totalFx += forces[nodeId].x; - totalFy += forces[nodeId].y; - } - var correctionFx = totalFx / nodeIndices.length; - var correctionFy = totalFy / nodeIndices.length; - - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - forces[nodeId].x -= correctionFx; - forces[nodeId].y -= correctionFy; + this.body.emitter.emit("animationFinished"); } }, writable: true, @@ -31505,13 +31426,10 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return HierarchicalSpringSolver; + return View; })(); - exports.HierarchicalSpringSolver = HierarchicalSpringSolver; - Object.defineProperty(exports, "__esModule", { - value: true - }); + module.exports = View; /***/ }, /* 93 */ @@ -31524,746 +31442,851 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Created by Alex on 2/23/2015. + * Created by Alex on 2/27/2015. + * */ - var CentralGravitySolver = (function () { - function CentralGravitySolver(body, physicsBody, options) { - _classCallCheck(this, CentralGravitySolver); + + var util = __webpack_require__(1); + + var NavigationHandler = __webpack_require__(94).NavigationHandler; + var InteractionHandler = (function () { + function InteractionHandler(body, canvas, selectionHandler) { + _classCallCheck(this, InteractionHandler); this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); + this.canvas = canvas; + this.selectionHandler = selectionHandler; + this.navigationHandler = new NavigationHandler(body, canvas); + + // 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, + hoverEnabled: false, + showNavigationIcons: false, + tooltip: { + delay: 300, + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" + } + }, + keyboard: { + enabled: false, + speed: { x: 10, y: 10, zoom: 0.02 }, + bindToWindow: true + } + }; + util.extend(this.options, this.defaultOptions); } - _prototypeProperties(CentralGravitySolver, null, { + _prototypeProperties(InteractionHandler, null, { setOptions: { value: function setOptions(options) { - this.options = options; + if (options !== undefined) { + // extend all but the values in fields + var fields = ["keyboard"]; + util.selectiveNotDeepExtend(fields, this.options, options); + + // merge the keyboard options in. + util.mergeOptions(this.options, options, "keyboard"); + } + + this.navigationHandler.setOptions(this.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 }, - solve: { - value: function solve() { - var dx, dy, distance, node, i; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; - + onTouch: { - var gravity = this.options.centralGravity; - var gravityForce = 0; - for (i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - node = nodes[nodeId]; - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); + /** + * 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; - gravityForce = distance == 0 ? 0 : gravity / distance; - forces[nodeId].x = dx * gravityForce; - forces[nodeId].y = dy * gravityForce; + // 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: { - return CentralGravitySolver; - })(); + /** + * handle tap/click event: select/unselect a node + * @private + */ + value: function onTap(event) { + var pointer = this.getPointer(event.center); - exports.CentralGravitySolver = CentralGravitySolver; - Object.defineProperty(exports, "__esModule", { - value: true - }); + var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; + var selected = this.selectionHandler.selectOnPoint(pointer); -/***/ }, -/* 94 */ -/***/ function(module, exports, __webpack_require__) { + if (selected === true || previouslySelected == true && selected === false) { + // select or unselect + this.body.emitter.emit("select", this.selectionHandler.getSelection()); + } - "use strict"; + this.selectionHandler._generateClickEvent("click", pointer); + }, + writable: true, + configurable: true + }, + onDoubleTap: { - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + /** + * handle doubletap event + * @private + */ + value: function onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent("doubleClick", pointer); + }, + writable: true, + configurable: true + }, + onHold: { - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - /** - * Created by Alex on 24-Feb-15. - */ - var util = __webpack_require__(1); - var Cluster = _interopRequire(__webpack_require__(95)); + /** + * handle long tap event: multi select nodes + * @private + */ + value: function onHold(event) { + var pointer = this.getPointer(event.center); - var ClusterEngine = (function () { - function ClusterEngine(body) { - _classCallCheck(this, ClusterEngine); + var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); - this.body = body; - this.clusteredNodes = {}; - } + if (selectionChanged === true) { + // select or longpress + this.body.emitter.emit("select", this.selectionHandler.getSelection()); + } - _prototypeProperties(ClusterEngine, null, { - setOptions: { - value: function setOptions(options) {}, + this.selectionHandler._generateClickEvent("click", pointer); + }, writable: true, configurable: true }, - clusterByConnectionCount: { - - /** - * - * @param hubsize - * @param options - */ - value: function clusterByConnectionCount(hubsize, options) { - if (hubsize === undefined) { - hubsize = this._getHubSize(); - } else if (tyepof(hubsize) == "object") { - options = this._checkOptions(hubsize); - hubsize = this._getHubSize(); - } + onRelease: { - var nodesToCluster = []; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var node = this.body.nodes[this.body.nodeIndices[i]]; - if (node.edges.length >= hubsize) { - nodesToCluster.push(node.id); - } - } - for (var i = 0; i < nodesToCluster.length; i++) { - var node = this.body.nodes[nodesToCluster[i]]; - this.clusterByConnection(node, options, {}, {}, false); - } - this.body.emitter.emit("_dataChanged"); + /** + * handle the release of the screen + * + * @private + */ + value: function onRelease(event) { + this.body.emitter.emit("release", event); }, writable: true, configurable: true }, - clusterByNodeData: { + onDragStart: { /** - * loop over all nodes, check if they adhere to the condition and cluster if needed. - * @param options - * @param refreshData - */ - value: function clusterByNodeData() { - var options = arguments[0] === undefined ? {} : arguments[0]; - var refreshData = arguments[1] === undefined ? true : arguments[1]; - if (options.joinCondition === undefined) { - throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); + * 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); } - // check if the options object is fine, append if needed - options = this._checkOptions(options); + // note: drag.pointer is set in onTouch to get the initial touch location + var node = this.selectionHandler.getNodeAt(this.drag.pointer); - var childNodesObj = {}; - var childEdgesObj = {}; + this.drag.dragging = true; + this.drag.selection = []; + this.drag.translation = util.extend({}, this.body.view.translation); // copy the object + this.drag.nodeId = null; - // collect the nodes that will be in the cluster - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - var clonedOptions = this._cloneOptions(nodeId); - if (options.joinCondition(clonedOptions) == true) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; + 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); } - } - this._cluster(childNodesObj, childEdgesObj, options, refreshData); - }, - writable: true, - configurable: true - }, - clusterOutliers: { + 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.options.fixed.x, + yFixed: object.options.fixed.y + }; - /** - * Cluster all nodes in the network that have only 1 edge - * @param options - * @param refreshData - */ - value: function clusterOutliers(options) { - var refreshData = arguments[1] === undefined ? true : arguments[1]; - options = this._checkOptions(options); - var clusters = []; + object.options.fixed.x = true; + object.options.fixed.y = true; - // collect the nodes that will be in the cluster - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var childNodesObj = {}; - var childEdgesObj = {}; - var nodeId = this.body.nodeIndices[i]; - if (this.body.nodes[nodeId].edges.length == 1) { - var edge = this.body.nodes[nodeId].edges[0]; - var childNodeId = this._getConnectedId(edge, nodeId); - if (childNodeId != nodeId) { - if (options.joinCondition === undefined) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } else { - var clonedOptions = this._cloneOptions(nodeId); - if (options.joinCondition(clonedOptions) == true) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; - } - clonedOptions = this._cloneOptions(childNodeId); - if (options.joinCondition(clonedOptions) == true) { - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } - } - clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); + this.drag.selection.push(s); } } } - - for (var i = 0; i < clusters.length; i++) { - this._cluster(clusters[i].nodes, clusters[i].edges, options, false); - } - - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } }, writable: true, configurable: true }, - clusterByConnection: { + onDrag: { + /** - * - * @param nodeId - * @param options - * @param refreshData - */ - value: function clusterByConnection(nodeId, options) { - var refreshData = arguments[2] === undefined ? true : arguments[2]; - // kill conditions - if (nodeId === undefined) { - throw new Error("No nodeId supplied to clusterByConnection!"); - } - if (this.body.nodes[nodeId] === undefined) { - throw new Error("The nodeId given to clusterByConnection does not exist!"); + * handle drag event + * @private + */ + value: function onDrag(event) { + var _this = this; + if (this.drag.pinched === true) { + return; } - var node = this.body.nodes[nodeId]; - options = this._checkOptions(options, node); - if (options.clusterNodeProperties.x === undefined) { - options.clusterNodeProperties.x = node.x;options.clusterNodeProperties.allowedToMoveX = !node.xFixed; - } - if (options.clusterNodeProperties.y === undefined) { - options.clusterNodeProperties.y = node.y;options.clusterNodeProperties.allowedToMoveY = !node.yFixed; - } + // remove the focus on node if it is focussed on by the focusOnNode + this.body.emitter.emit("unlockNode"); - var childNodesObj = {}; - var childEdgesObj = {}; - var parentNodeId = node.id; - var parentClonedOptions = this._cloneOptions(parentNodeId); - childNodesObj[parentNodeId] = node; + 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; - // collect the nodes that will be in the cluster - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - var childNodeId = this._getConnectedId(edge, parentNodeId); + // update position of all selected nodes + selection.forEach(function (selection) { + var node = selection.node; + // only move the node if it was not fixed initially + if (selection.xFixed === false) { + node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); + } + // only move the node if it was not fixed initially + if (selection.yFixed === false) { + node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + } + }); - if (childNodeId !== parentNodeId) { - if (options.joinCondition === undefined) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } else { - // clone the options and insert some additional parameters that could be interesting. - var childClonedOptions = this._cloneOptions(childNodeId); - if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } + + // 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; } - } else { - childEdgesObj[edge.id] = edge; + 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"); } } - - this._cluster(childNodesObj, childEdgesObj, options, refreshData); }, writable: true, configurable: true }, - _cloneOptions: { + onDragEnd: { /** - * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. - * @param objId - * @param type - * @returns {{}} - * @private - */ - value: function _cloneOptions(objId, type) { - var clonedOptions = {}; - if (type === undefined || type == "node") { - util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); - util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); - clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; + * 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.options.fixed.x = s.xFixed; + s.node.options.fixed.y = s.yFixed; + }); + this.body.emitter.emit("startSimulation"); } else { - util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); + this.body.emitter.emit("_requestRedraw"); } - return clonedOptions; + + this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); }, writable: true, configurable: true }, - _createClusterEdges: { - - - /** - * This function creates the edges that will be attached to the cluster. - * - * @param childNodesObj - * @param childEdgesObj - * @param newEdges - * @param options - * @private - */ - value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { - var edge, childNodeId, childNode; + onPinch: { - var childKeys = Object.keys(childNodesObj); - for (var i = 0; i < childKeys.length; i++) { - childNodeId = childKeys[i]; - childNode = childNodesObj[childNodeId]; - // mark all edges for removal from global and construct new edges from the cluster to others - for (var j = 0; j < childNode.edges.length; j++) { - edge = childNode.edges[j]; - childEdgesObj[edge.id] = edge; - var otherNodeId = edge.toId; - var otherOnTo = true; - if (edge.toId != childNodeId) { - otherNodeId = edge.toId; - otherOnTo = true; - } else if (edge.fromId != childNodeId) { - otherNodeId = edge.fromId; - otherOnTo = false; - } + /** + * Handle pinch event + * @param event + * @private + */ + value: function onPinch(event) { + var pointer = this.getPointer(event.center); - if (childNodesObj[otherNodeId] === undefined) { - var clonedOptions = this._cloneOptions(edge.id, "edge"); - util.deepExtend(clonedOptions, options.clusterEdgeProperties); - if (otherOnTo === true) { - clonedOptions.from = options.clusterNodeProperties.id; - clonedOptions.to = otherNodeId; - } else { - clonedOptions.from = otherNodeId; - clonedOptions.to = options.clusterNodeProperties.id; - } - clonedOptions.id = "clusterEdge:" + util.randomUUID(); - newEdges.push(this.body.functions.createEdge(clonedOptions)); - } - } + this.drag.pinched = true; + if (this.pinch.scale === undefined) { + this.pinch.scale = 1; } + + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.scale; + this.zoom(scale, pointer); }, writable: true, configurable: true }, - _checkOptions: { + zoom: { /** - * This function checks the options that can be supplied to the different cluster functions - * for certain fields and inserts defaults if needed - * @param options - * @returns {*} - * @private - */ - value: function _checkOptions() { - var options = arguments[0] === undefined ? {} : arguments[0]; - if (options.clusterEdgeProperties === undefined) { - options.clusterEdgeProperties = {}; - } - if (options.clusterNodeProperties === undefined) { - options.clusterNodeProperties = {}; - } + * 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; + } - return options; + 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 }, - _cluster: { + onMouseWheel: { - /** - * - * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node - * @param {Object} childEdgesObj | object with edge objects, id as keys - * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} - * @param {Boolean} refreshData | when true, do not wrap up - * @private - */ - value: function _cluster(childNodesObj, childEdgesObj, options) { - var refreshData = arguments[3] === undefined ? true : arguments[3]; - // kill condition: no children so cant cluster - if (Object.keys(childNodesObj).length == 0) { - return; - } - // check if we have an unique id; - if (options.clusterNodeProperties.id === undefined) { - options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); + /** + * 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; } - var clusterId = options.clusterNodeProperties.id; - // create the new edges that will connect to the cluster - var newEdges = []; - this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); - - // construct the clusterNodeProperties - var clusterNodeProperties = options.clusterNodeProperties; - if (options.processProperties !== undefined) { - // get the childNode options - var childNodesOptions = []; - for (var nodeId in childNodesObj) { - var clonedOptions = this._cloneOptions(nodeId); - childNodesOptions.push(clonedOptions); + // 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; - // get clusterproperties based on childNodes - var childEdgesOptions = []; - for (var edgeId in childEdgesObj) { - var clonedOptions = this._cloneOptions(edgeId, "edge"); - childEdgesOptions.push(clonedOptions); - } + // calculate the pointer location + var pointer = { x: event.pageX, y: event.pageY }; - clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); - if (!clusterNodeProperties) { - throw new Error("The processClusterProperties function does not return properties!"); - } - } - if (clusterNodeProperties.label === undefined) { - clusterNodeProperties.label = "cluster"; + // apply the new scale + this.zoom(scale, pointer); } + // Prevent default actions caused by mouse wheel. + event.preventDefault(); + }, + writable: true, + configurable: true + }, + onMouseMove: { - // give the clusterNode a postion if it does not have one. - var pos = undefined; - if (clusterNodeProperties.x === undefined) { - pos = this._getClusterPosition(childNodesObj); - clusterNodeProperties.x = pos.x; - clusterNodeProperties.allowedToMoveX = true; - } - if (clusterNodeProperties.x === undefined) { - if (pos === undefined) { - pos = this._getClusterPosition(childNodesObj); - } - clusterNodeProperties.y = pos.y; - clusterNodeProperties.allowedToMoveY = true; - } + /** + * 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 + } + }); - // force the ID to remain the same - clusterNodeProperties.id = clusterId; + return InteractionHandler; + })(); + module.exports = InteractionHandler; + // 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"); + // } - // create the clusterNode - var clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); - clusterNode.isCluster = true; - clusterNode.containedNodes = childNodesObj; - clusterNode.containedEdges = childEdgesObj; +/***/ }, +/* 94 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; - // disable the childEdges - for (var edgeId in childEdgesObj) { - if (childEdgesObj.hasOwnProperty(edgeId)) { - if (this.body.edges[edgeId] !== undefined) { - var edge = this.body.edges[edgeId]; - edge.togglePhysics(false); - edge.options.hidden = true; - } - } - } + 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"); } }; - // disable the childNodes - for (var nodeId in childNodesObj) { - if (childNodesObj.hasOwnProperty(nodeId)) { - this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; - this.body.nodes[nodeId].togglePhysics(false); - this.body.nodes[nodeId].options.hidden = true; - } - } + var util = __webpack_require__(1); + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + var keycharm = __webpack_require__(39); + var NavigationHandler = (function () { + function NavigationHandler(body, canvas) { + var _this = this; + _classCallCheck(this, NavigationHandler); - // finally put the cluster node into global - this.body.nodes[clusterNodeProperties.id] = clusterNode; + this.body = body; + this.canvas = canvas; + this.iconsCreated = false; + this.navigationHammers = []; + this.boundFunctions = {}; + this.touchTime = 0; + this.activated = false; - // push new edges to global - for (var i = 0; i < newEdges.length; i++) { - this.body.edges[newEdges[i].id] = newEdges[i]; - this.body.edges[newEdges[i].id].connect(); - } - // set ID to undefined so no duplicates arise - clusterNodeProperties.id = undefined; + this.body.emitter.on("release", this._stopMovement.bind(this)); + this.body.emitter.on("activate", function () { + _this.activated = true;_this.configureKeyboardBindings(); + }); + this.body.emitter.on("deactivate", function () { + _this.activated = false;_this.configureKeyboardBindings(); + }); + this.body.emitter.on("destroy", function () { + if (_this.keycharm !== undefined) { + _this.keycharm.destroy(); + } + }); + this.options = {}; + } - // wrap up - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); + _prototypeProperties(NavigationHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + this.options = options; + this.create(); } }, writable: true, configurable: true }, - isCluster: { - - - /** - * Check if a node is a cluster. - * @param nodeId - * @returns {*} - */ - value: function isCluster(nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].isCluster === true; - } else { - console.log("Node does not exist."); - return false; + create: { + value: function create() { + if (this.options.showNavigationIcons === true) { + if (this.iconsCreated === false) { + this.loadNavigationElements(); + } + } else if (this.iconsCreated === true) { + this.cleanNavigation(); } + + this.configureKeyboardBindings(); }, writable: true, configurable: true }, - _getClusterPosition: { + cleanNavigation: { + value: function cleanNavigation() { + // clean hammer bindings + if (this.navigationHammers.length != 0) { + for (var i = 0; i < this.navigationHammers.length; i++) { + this.navigationHammers[i].destroy(); + } + this.navigationHammers = []; + } - /** - * get the position of the cluster node based on what's inside - * @param {object} childNodesObj | object with node objects, id as keys - * @returns {{x: number, y: number}} - * @private - */ - value: function _getClusterPosition(childNodesObj) { - var childKeys = Object.keys(childNodesObj); - var minX = childNodesObj[childKeys[0]].x; - var maxX = childNodesObj[childKeys[0]].x; - var minY = childNodesObj[childKeys[0]].y; - var maxY = childNodesObj[childKeys[0]].y; - var node; - for (var i = 0; i < childKeys.lenght; i++) { - node = childNodesObj[childKeys[0]]; - minX = node.x < minX ? node.x : minX; - maxX = node.x > maxX ? node.x : maxX; - minY = node.y < minY ? node.y : minY; - maxY = node.y > maxY ? node.y : maxY; + this._navigationReleaseOverload = function () {}; + + // clean up previous navigation items + if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { + this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); } - return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; + + this.iconsCreated = false; }, writable: true, configurable: true }, - openCluster: { - + loadNavigationElements: { /** - * Open a cluster by calling this function. - * @param {String} clusterNodeId | the ID of the cluster node - * @param {Boolean} refreshData | wrap up afterwards if not true - */ - value: function openCluster(clusterNodeId) { - var refreshData = arguments[1] === undefined ? true : arguments[1]; - // kill conditions - if (clusterNodeId === undefined) { - throw new Error("No clusterNodeId supplied to openCluster."); - } - if (this.body.nodes[clusterNodeId] === undefined) { - throw new Error("The clusterNodeId supplied to openCluster does not exist."); - } - if (this.body.nodes[clusterNodeId].containedNodes === undefined) { - console.log("The node:" + clusterNodeId + " is not a cluster.");return; - }; - - var clusterNode = this.body.nodes[clusterNodeId]; - var containedNodes = clusterNode.containedNodes; - var containedEdges = clusterNode.containedEdges; - - // release nodes - for (var nodeId in containedNodes) { - if (containedNodes.hasOwnProperty(nodeId)) { - var containedNode = this.body.nodes[nodeId]; - containedNode = containedNodes[nodeId]; - // inherit position - containedNode.x = clusterNode.x; - containedNode.y = clusterNode.y; + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * + * @private + */ + value: function loadNavigationElements() { + this.cleanNavigation(); - // inherit speed - containedNode.vx = clusterNode.vx; - containedNode.vy = clusterNode.vy; + this.navigationDOM = {}; + var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; + var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; - containedNode.options.hidden = false; - containedNode.togglePhysics(true); + this.navigationDOM.wrapper = document.createElement("div"); + this.canvas.frame.appendChild(this.navigationDOM.wrapper); - delete this.clusteredNodes[nodeId]; - } - } + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDOM[navigationDivs[i]] = document.createElement("div"); + this.navigationDOM[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; + this.navigationDOM.wrapper.appendChild(this.navigationDOM[navigationDivs[i]]); - // release edges - for (var edgeId in containedEdges) { - if (containedEdges.hasOwnProperty(edgeId)) { - var edge = this.body.edges[edgeId]; - edge.options.hidden = false; - edge.togglePhysics(true); + var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); + if (navigationDivActions[i] == "_zoomExtent") { + hammerUtil.onTouch(hammer, this._zoomExtent.bind(this)); + } else { + hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); } - } - // remove all temporary edges - for (var i = 0; i < clusterNode.edges.length; i++) { - var edgeId = clusterNode.edges[i].id; - var viaId = this.body.edges[edgeId].via.id; - if (viaId) { - this.body.edges[edgeId].via = undefined; - delete this.body.nodes[viaId]; - } - // this removes the edge from node.edges, which is why edgeIds is formed - this.body.edges[edgeId].disconnect(); - delete this.body.edges[edgeId]; + this.navigationHammers.push(hammer); } - // remove clusterNode - delete this.body.nodes[clusterNodeId]; - - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); + this.iconsCreated = true; + }, + writable: true, + configurable: true + }, + bindToRedraw: { + value: function bindToRedraw(action) { + if (this.boundFunctions[action] === undefined) { + this.boundFunctions[action] = this[action].bind(this); + this.body.emitter.on("initRedraw", this.boundFunctions[action]); + this.body.emitter.emit("_startRendering"); } }, writable: true, configurable: true }, - _connectEdge: { - - - - /** - * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to - * is currently residing in cluster B - * @param edge - * @param nodeId - * @param from - * @private - */ - value: function _connectEdge(edge, nodeId, from) { - var clusterStack = this._getClusterStack(nodeId); - if (from == true) { - edge.from = clusterStack[clusterStack.length - 1]; - edge.fromId = clusterStack[clusterStack.length - 1].id; - clusterStack.pop(); - edge.fromArray = clusterStack; - } else { - edge.to = clusterStack[clusterStack.length - 1]; - edge.toId = clusterStack[clusterStack.length - 1].id; - clusterStack.pop(); - edge.toArray = clusterStack; + unbindFromRedraw: { + value: function unbindFromRedraw(action) { + if (this.boundFunctions[action] !== undefined) { + this.body.emitter.off("initRedraw", this.boundFunctions[action]); + this.body.emitter.emit("_stopRendering"); + delete this.boundFunctions[action]; } - edge.connect(); }, writable: true, configurable: true }, - _getClusterStack: { + _zoomExtent: { /** - * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node - * @param nodeId - * @returns {Array} - * @private - */ - value: function _getClusterStack(nodeId) { - var stack = []; - var max = 100; - var counter = 0; - - while (this.clusteredNodes[nodeId] !== undefined && counter < max) { - stack.push(this.clusteredNodes[nodeId].node); - nodeId = this.clusteredNodes[nodeId].clusterId; - counter++; + * this stops all movement induced by the navigation buttons + * + * @private + */ + value: function _zoomExtent() { + if (new Date().valueOf() - this.touchTime > 700) { + // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) + this.body.emitter.emit("zoomExtent", { duration: 700 }); + this.touchTime = new Date().valueOf(); } - stack.push(this.body.nodes[nodeId]); - return stack; }, writable: true, configurable: true }, - _getConnectedId: { - + _stopMovement: { /** - * Get the Id the node is connected to - * @param edge - * @param nodeId - * @returns {*} - * @private - */ - value: function _getConnectedId(edge, nodeId) { - if (edge.toId != nodeId) { - return edge.toId; - } else if (edge.fromId != nodeId) { - return edge.fromId; - } else { - return edge.fromId; + * this stops all movement induced by the navigation buttons + * + * @private + */ + value: function _stopMovement() { + for (var boundAction in this.boundFunctions) { + if (this.boundFunctions.hasOwnProperty(boundAction)) { + this.body.emitter.off("initRedraw", this.boundFunctions[boundAction]); + this.body.emitter.emit("_stopRendering"); + } } + this.boundFunctions = {}; + }, + writable: true, + configurable: true + }, + _moveUp: { + value: function _moveUp() { + this.body.view.translation.y += this.options.keyboard.speed.y; + }, + writable: true, + configurable: true + }, + _moveDown: { + value: function _moveDown() { + this.body.view.translation.y -= this.options.keyboard.speed.y; + }, + writable: true, + configurable: true + }, + _moveLeft: { + value: function _moveLeft() { + this.body.view.translation.x += this.options.keyboard.speed.x; + }, + writable: true, + configurable: true + }, + _moveRight: { + value: function _moveRight() { + this.body.view.translation.x -= this.options.keyboard.speed.x; + }, + writable: true, + configurable: true + }, + _zoomIn: { + value: function _zoomIn() { + this.body.view.scale += this.options.keyboard.speed.zoom; + }, + writable: true, + configurable: true + }, + _zoomOut: { + value: function _zoomOut() { + this.body.view.scale -= this.options.keyboard.speed.zoom; }, writable: true, configurable: true }, - _getHubSize: { + configureKeyboardBindings: { + /** - * We determine how many connections denote an important hub. - * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) - * - * @private - */ - value: function _getHubSize() { - var average = 0; - var averageSquared = 0; - var hubCounter = 0; - var largestHub = 0; + * bind all keys using keycharm. + */ + value: function configureKeyboardBindings() { + if (this.keycharm !== undefined) { + this.keycharm.destroy(); + } - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var node = this.body.nodes[this.body.nodeIndices[i]]; - if (node.edges.length > largestHub) { - largestHub = node.edges.length; + if (this.options.keyboard.enabled === true) { + if (this.options.keyboard.bindToWindow === true) { + this.keycharm = keycharm({ container: window, preventDefault: false }); + } else { + this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: false }); } - average += node.edges.length; - averageSquared += Math.pow(node.edges.length, 2); - hubCounter += 1; - } - average = average / hubCounter; - averageSquared = averageSquared / hubCounter; - var variance = averageSquared - Math.pow(average, 2); - var standardDeviation = Math.sqrt(variance); + this.keycharm.reset(); - var hubThreshold = Math.floor(average + 2 * standardDeviation); + if (this.activated === true) { + this.keycharm.bind("up", this.bindToRedraw.bind(this, "_moveUp"), "keydown"); + this.keycharm.bind("down", this.bindToRedraw.bind(this, "_moveDown"), "keydown"); + this.keycharm.bind("left", this.bindToRedraw.bind(this, "_moveLeft"), "keydown"); + this.keycharm.bind("right", this.bindToRedraw.bind(this, "_moveRight"), "keydown"); + this.keycharm.bind("=", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("num+", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("num-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("[", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("]", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("pageup", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("pagedown", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - // always have at least one to cluster - if (hubThreshold > largestHub) { - hubThreshold = largestHub; + this.keycharm.bind("up", this.unbindFromRedraw.bind(this, "_moveUp"), "keyup"); + this.keycharm.bind("down", this.unbindFromRedraw.bind(this, "_moveDown"), "keyup"); + this.keycharm.bind("left", this.unbindFromRedraw.bind(this, "_moveLeft"), "keyup"); + this.keycharm.bind("right", this.unbindFromRedraw.bind(this, "_moveRight"), "keyup"); + this.keycharm.bind("=", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("num+", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("num-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("[", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("]", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("pageup", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("pagedown", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + } } - - return hubThreshold; }, writable: true, configurable: true } }); - return ClusterEngine; + return NavigationHandler; })(); - module.exports = ClusterEngine; + exports.NavigationHandler = NavigationHandler; + Object.defineProperty(exports, "__esModule", { + value: true + }); /***/ }, /* 95 */ @@ -32271,108 +32294,39 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - var Node = _interopRequire(__webpack_require__(62)); - - /** - * - */ - var Cluster = (function (Node) { - function Cluster(options, body, imagelist, grouplist, globalOptions) { - _classCallCheck(this, Cluster); - - _get(Object.getPrototypeOf(Cluster.prototype), "constructor", this).call(this, options, body, imagelist, grouplist, globalOptions); - - this.isCluster = true; - this.containedNodes = {}; - this.containedEdges = {}; - } - - _inherits(Cluster, Node); - - return Cluster; - })(Node); - - module.exports = Cluster; - -/***/ }, -/* 96 */ -/***/ 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 26-Feb-15. + * Created by Alex on 2/27/2015. */ - if (typeof window !== "undefined") { - window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; - } - + var Node = __webpack_require__(62); var util = __webpack_require__(1); - - var CanvasRenderer = (function () { - function CanvasRenderer(body, canvas) { + var SelectionHandler = (function () { + function SelectionHandler(body, canvas) { var _this = this; - _classCallCheck(this, CanvasRenderer); + _classCallCheck(this, SelectionHandler); this.body = body; this.canvas = canvas; - - this.redrawRequested = false; - this.renderTimer = false; - this.requiresTimeout = true; - 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", function () { - _this.dragging = true; - }); - this.body.emitter.on("dragEnd", function () { - return _this.dragging = false; - }); - 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.renderingActive = true;_this.startRendering(); - }); - this.body.emitter.on("_stopRendering", function () { - _this.renderRequests -= 1;_this.renderingActive = _this.renderRequests > 0; - }); + this.selectionObj = { nodes: [], edges: [] }; this.options = {}; this.defaultOptions = { - hideEdgesOnDrag: false, - hideNodesOnDrag: false + select: true, + selectConnectedEdges: true }; util.extend(this.options, this.defaultOptions); - this._determineBrowserMethod(); + this.body.emitter.on("_dataChanged", function () { + _this.updateSelection(); + }); } - _prototypeProperties(CanvasRenderer, null, { + _prototypeProperties(SelectionHandler, null, { setOptions: { value: function setOptions(options) { if (options !== undefined) { @@ -32382,938 +32336,703 @@ return /******/ (function(modules) { // webpackBootstrap writable: true, configurable: true }, - startRendering: { - value: function startRendering() { - 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 - } else { - this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function - } + selectOnPoint: { + + + + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + value: function selectOnPoint(pointer) { + var selected = false; + if (this.options.select === true) { + this.unselectAll(); + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; + if (obj !== undefined) { + selected = this.selectObject(obj); } - } else {} + this.body.emitter.emit("_requestRedraw"); + } + return selected; }, writable: true, configurable: true }, - renderStep: { - value: function renderStep() { - // reset the renderTimer so a new scheduled animation step can be set - this.renderTimer = undefined; - - if (this.requiresTimeout == true) { - // this schedules a new simulation step - this.startRendering(); - } + selectAdditionalOnPoint: { + value: function selectAdditionalOnPoint(pointer) { + var selectionChanged = false; + if (this.options.select === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - this._redraw(); + if (obj !== undefined) { + selectionChanged = true; + if (obj.isSelected() === true) { + this.deselectObject(obj); + } else { + this.selectObject(obj); + } - if (this.requiresTimeout == false) { - // this schedules a new simulation step - this.startRendering(); + this.body.emitter.emit("_requestRedraw"); + } } + return selectionChanged; }, writable: true, configurable: true }, - redraw: { - - /** - * Redraw the network with the current data - * chart will be resized too. - */ - value: function redraw() { - this.setSize(this.constants.width, this.constants.height); - this._redraw(); + _generateClickEvent: { + value: function _generateClickEvent(eventType, pointer) { + var properties = this.getSelection(); + properties.pointer = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: this.canvas.DOMtoCanvas(pointer) + }; + this.body.emitter.emit(eventType, properties); }, writable: true, configurable: true }, - _requestRedraw: { - - /** - * Redraw the network with the current data - * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. - * @private - */ - value: function _requestRedraw() { - if (this.redrawRequested !== true && this.renderingActive === false) { - this.redrawRequested = true; - if (this.requiresTimeout === true) { - window.setTimeout(this._redraw.bind(this, false), 0); - } else { - window.requestAnimationFrame(this._redraw.bind(this, false)); + selectObject: { + value: function selectObject(obj) { + if (obj !== undefined) { + if (obj instanceof Node) { + if (this.options.selectConnectedEdges === true) { + this._selectConnectedEdges(obj); + } } + obj.select(); + this._addToSelection(obj); + return true; } + return false; }, writable: true, configurable: true }, - _redraw: { - value: function _redraw() { - var hidden = arguments[0] === undefined ? false : arguments[0]; - this.body.emitter.emit("initRedraw"); - - 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 - var w = this.canvas.frame.canvas.clientWidth; - var h = this.canvas.frame.canvas.clientHeight; - ctx.clearRect(0, 0, w, h); - - this.body.emitter.emit("beforeDrawing", ctx); - - // set scaling and translation - ctx.save(); - ctx.translate(this.body.view.translation.x, this.body.view.translation.y); - ctx.scale(this.body.view.scale, this.body.view.scale); - - 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 }); - - if (hidden === false) { - if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { - this._drawEdges(ctx); - } - } - - if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { - this._drawNodes(ctx, hidden); - } - - if (this.controlNodesActive === true) { - this._drawControlNodes(ctx); - } - - //this.physics.nodesSolver._debug(ctx,"#F00F0F"); - - // restore original scaling and translation - ctx.restore(); - - if (hidden === true) { - ctx.clearRect(0, 0, w, h); + deselectObject: { + value: function deselectObject(obj) { + if (obj.isSelected() === true) { + obj.selected = false; + this._removeFromSelection(obj); } - - this.body.emitter.emit("afterDrawing", ctx); }, writable: true, configurable: true }, - _drawNodes: { + _getAllNodesOverlappingWith: { + /** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @param {Boolean} [alwaysShow] + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - value: function _drawNodes(ctx) { - var alwaysShow = arguments[1] === undefined ? false : arguments[1]; + value: function _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; var nodes = this.body.nodes; - var nodeIndices = this.body.nodeIndices; - var node; - var selected = []; - - // draw unselected nodes; - for (var i = 0; i < nodeIndices.length; i++) { - node = nodes[nodeIndices[i]]; - // set selected nodes aside - if (node.isSelected()) { - selected.push(nodeIndices[i]); - } else { - if (alwaysShow === true) { - node.draw(ctx); - } - // todo: replace check - //else if (node.inArea() === true) { - node.draw(ctx); - //} + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); } } - - // draw the selected nodes on top - for (var i = 0; i < selected.length; i++) { - node = nodes[selected[i]]; - node.draw(ctx); - } + return overlappingNodes; }, writable: true, configurable: true }, - _drawEdges: { + _pointerToPositionObject: { /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} * @private */ - value: function _drawEdges(ctx) { - var edges = this.body.edges; - var edgeIndices = this.body.edgeIndices; - var edge; - - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - edge.draw(ctx); - } - } + value: function _pointerToPositionObject(pointer) { + var canvasPos = this.canvas.DOMtoCanvas(pointer); + return { + left: canvasPos.x, + top: canvasPos.y, + right: canvasPos.x, + bottom: canvasPos.y + }; }, writable: true, configurable: true }, - _drawControlNodes: { + getNodeAt: { + /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | undefined} node * @private */ - value: function _drawControlNodes(ctx) { - var edges = this.body.edges; - var edgeIndices = this.body.edgeIndices; - var edge; + value: function getNodeAt(pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - edge._drawControlNodes(ctx); + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } else { + return undefined; } }, writable: true, configurable: true }, - _determineBrowserMethod: { + _getEdgesOverlappingWith: { + /** - * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because - * some implementations (safari and IE9) did not support requestAnimationFrame + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - value: function _determineBrowserMethod() { - if (typeof window !== "undefined") { - var browserType = navigator.userAgent.toLowerCase(); - this.requiresTimeout = false; - if (browserType.indexOf("msie 9.0") != -1) { - // IE 9 - this.requiresTimeout = true; - } else if (browserType.indexOf("safari") != -1) { - // safari - if (browserType.indexOf("chrome") <= -1) { - this.requiresTimeout = true; - } - } - } else { - this.requiresTimeout = true; - } - }, - writable: true, - configurable: true - } - }); - - return CanvasRenderer; - })(); - - module.exports = CanvasRenderer; - -/***/ }, -/* 97 */ -/***/ 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"); } }; - - var Hammer = __webpack_require__(19); - var hammerUtil = __webpack_require__(24); - - var util = __webpack_require__(1); - - /** - * Create the main frame for the Network. - * This function is executed once when a Network object is created. The frame - * contains a canvas, and this canvas contains all objects like the axis and - * nodes. - * @private - */ - var Canvas = (function () { - function Canvas(body) { - var _this = this; - _classCallCheck(this, Canvas); - - this.body = body; - - this.options = {}; - this.defaultOptions = { - width: "100%", - height: "100%" - }; - util.extend(this.options, this.defaultOptions); - - this.body.emitter.once("resize", function (obj) { - _this.body.view.translation.x = obj.width * 0.5;_this.body.view.translation.y = obj.height * 0.5; - }); - this.body.emitter.on("destroy", function () { - return _this.hammer.destroy(); - }); - - this.pixelRatio = 1; - } - - _prototypeProperties(Canvas, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); + value: function _getEdgesOverlappingWith(object, overlappingEdges) { + var edges = this.body.edges; + for (var i = 0; i < this.body.edgeIndices.length; i++) { + var edgeId = this.body.edgeIndices[i]; + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } } }, writable: true, configurable: true }, - create: { - value: function create() { - // remove all elements from the container element. - while (this.body.container.hasChildNodes()) { - this.body.container.removeChild(this.body.container.firstChild); - } + _getAllEdgesOverlappingWith: { - this.frame = document.createElement("div"); - this.frame.className = "vis network-frame"; - this.frame.style.position = "relative"; - this.frame.style.overflow = "hidden"; - this.frame.tabIndex = 900; - ////////////////////////////////////////////////////////////////// + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllEdgesOverlappingWith(object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; + }, + writable: true, + configurable: true + }, + getEdgeAt: { - this.frame.canvas = document.createElement("canvas"); - this.frame.canvas.style.position = "relative"; - this.frame.appendChild(this.frame.canvas); - if (!this.frame.canvas.getContext) { - var noCanvas = document.createElement("DIV"); - noCanvas.style.color = "red"; - noCanvas.style.fontWeight = "bold"; - noCanvas.style.padding = "10px"; - noCanvas.innerHTML = "Error: your browser does not support HTML canvas"; - this.frame.canvas.appendChild(noCanvas); - } else { - var ctx = this.frame.canvas.getContext("2d"); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + /** + * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call + * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {undefined} + * @private + */ + value: function getEdgeAt(pointer) { + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + if (overlappingEdges.length > 0) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } else { + return undefined; } - - // add the frame to the container element - this.body.container.appendChild(this.frame); - - this.body.view.scale = 1; - this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; - - this._bindHammer(); }, writable: true, configurable: true }, - _bindHammer: { + _addToSelection: { /** - * This function binds hammer, it can be repeated over and over due to the uniqueness check. + * Add object to the selection array. + * + * @param obj * @private */ - value: function _bindHammer() { - if (this.hammer !== undefined) { - this.hammer.destroy(); + value: function _addToSelection(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; } - this.drag = {}; - this.pinch = {}; - - // init hammer - this.hammer = new Hammer(this.frame.canvas); - this.hammer.get("pinch").set({ enable: true }); - - 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", 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", this.body.eventListeners.onMouseWheel); - this.frame.canvas.addEventListener("DOMMouseScroll", this.body.eventListeners.onMouseWheel); - - this.frame.canvas.addEventListener("mousemove", this.body.eventListeners.onMouseMove); - - this.hammerFrame = new Hammer(this.frame); - hammerUtil.onRelease(this.hammerFrame, this.body.eventListeners.onRelease); }, writable: true, configurable: true }, - setSize: { - + _addToHover: { /** - * Set a new size for the network - * @param {string} width Width in pixels or percentage (for example '800px' - * or '50%') - * @param {string} height Height in pixels or percentage (for example '400px' - * or '30%') + * Add object to the selection array. + * + * @param obj + * @private */ - value: function setSize() { - var width = arguments[0] === undefined ? this.options.width : arguments[0]; - var height = arguments[1] === undefined ? this.options.height : arguments[1]; - var emitEvent = false; - var oldWidth = this.frame.canvas.width; - var oldHeight = this.frame.canvas.height; - if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { - this.frame.style.width = width; - this.frame.style.height = height; - - this.frame.canvas.style.width = "100%"; - this.frame.canvas.style.height = "100%"; - - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + value: function _addToHover(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } else { + this.hoverObj.edges[obj.id] = obj; + } + }, + writable: true, + configurable: true + }, + _removeFromSelection: { - this.options.width = width; - this.options.height = height; - emitEvent = true; + /** + * Remove a single option from selection. + * + * @param {Object} obj + * @private + */ + value: function _removeFromSelection(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; } else { - // this would adapt the width of the canvas to the width from 100% if and only if - // there is a change. + delete this.selectionObj.edges[obj.id]; + } + }, + writable: true, + configurable: true + }, + unselectAll: { - if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - emitEvent = true; + /** + * Unselect all. The selectionObj is useful for this. + * + * @private + */ + value: function unselectAll() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); } - if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - emitEvent = true; + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); } } - 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.selectionObj = { nodes: {}, edges: {} }; }, writable: true, configurable: true }, - _XconvertDOMtoCanvas: { + _getSelectedNodeCount: { /** - * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} x + * return the number of selected nodes + * * @returns {number} * @private */ - value: function _XconvertDOMtoCanvas(x) { - return (x - this.body.view.translation.x) / this.body.view.scale; + value: function _getSelectedNodeCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + return count; }, writable: true, configurable: true }, - _XconvertCanvasToDOM: { + _getSelectedNode: { /** - * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the X coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} x + * return the selected node + * * @returns {number} * @private */ - value: function _XconvertCanvasToDOM(x) { - return x * this.body.view.scale + this.body.view.translation.x; + value: function _getSelectedNode() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } + } + return undefined; }, writable: true, configurable: true }, - _YconvertDOMtoCanvas: { + _getSelectedEdge: { /** - * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} y + * return the selected edge + * * @returns {number} * @private */ - value: function _YconvertDOMtoCanvas(y) { - return (y - this.body.view.translation.y) / this.body.view.scale; + value: function _getSelectedEdge() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } + } + return undefined; }, writable: true, configurable: true }, - _YconvertCanvasToDOM: { + _getSelectedEdgeCount: { + /** - * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} y + * return the number of selected edges + * * @returns {number} * @private */ - value: function _YconvertCanvasToDOM(y) { - return y * this.body.view.scale + this.body.view.translation.y; + value: function _getSelectedEdgeCount() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; }, writable: true, configurable: true }, - canvasToDOM: { + _getSelectedObjectCount: { /** + * return the number of selected objects. * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor + * @returns {number} + * @private */ - value: function canvasToDOM(pos) { - return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; + value: function _getSelectedObjectCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; }, writable: true, configurable: true }, - DOMtoCanvas: { + _selectionIsEmpty: { /** + * Check if anything is selected * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor + * @returns {boolean} + * @private */ - value: function DOMtoCanvas(pos) { - return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; - }, - writable: true, - configurable: true - } - }); - - return Canvas; - })(); - - module.exports = Canvas; - -/***/ }, -/* 98 */ -/***/ 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 26-Feb-15. - */ - - var util = __webpack_require__(1); - - var View = (function () { - function View(body, canvas) { - var _this = this; - _classCallCheck(this, View); - - this.body = body; - this.canvas = canvas; - - this.animationSpeed = 1 / this.renderRefreshRate; - this.animationEasingFunction = "easeInOutQuint"; - this.easingTime = 0; - this.sourceScale = 0; - this.targetScale = 0; - this.sourceTranslation = 0; - this.targetTranslation = 0; - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; - this.touchTime = 0; - - this.viewFunction = undefined; - - this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); - this.body.emitter.on("animationFinished", function () { - _this.body.emitter.emit("_stopRendering"); - }); - this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); - } - - _prototypeProperties(View, null, { - setOptions: { - value: function setOptions() { - var options = arguments[0] === undefined ? {} : arguments[0]; - this.options = options; + value: function _selectionIsEmpty() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; + } + } + return true; }, writable: true, configurable: true }, - _getRange: { + _clusterInSelection: { - // zoomExtent /** - * Find the center position of the network + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} * @private */ - value: function _getRange() { - var specificNodes = arguments[0] === undefined ? [] : arguments[0]; - var minY = 1000000000, - maxY = -1000000000, - minX = 1000000000, - maxX = -1000000000, - node; - if (specificNodes.length > 0) { - for (var i = 0; i < specificNodes.length; i++) { - node = this.body.nodes[specificNodes[i]]; - if (minX > node.boundingBox.left) { - minX = node.boundingBox.left; - } - if (maxX < node.boundingBox.right) { - maxX = node.boundingBox.right; - } - if (minY > node.boundingBox.bottom) { - minY = node.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.boundingBox.top) { - maxY = node.boundingBox.bottom; - } // top is negative, bottom is positive - } - } else { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (minX > node.boundingBox.left) { - minX = node.boundingBox.left; - } - if (maxX < node.boundingBox.right) { - maxX = node.boundingBox.right; - } - if (minY > node.boundingBox.bottom) { - minY = node.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.boundingBox.top) { - maxY = node.boundingBox.bottom; - } // top is negative, bottom is positive + value: function _clusterInSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; } } } + return false; + }, + writable: true, + configurable: true + }, + _selectConnectedEdges: { - if (minX == 1000000000 && maxX == -1000000000 && minY == 1000000000 && maxY == -1000000000) { - minY = 0, maxY = 0, minX = 0, maxX = 0; + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); } - return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; }, writable: true, configurable: true }, - _findCenter: { - + _hoverConnectedEdges: { /** - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - * @returns {{x: number, y: number}} + * select the edges connected to the node that is being selected + * + * @param {Node} node * @private */ - value: function _findCenter(range) { - return { x: 0.5 * (range.maxX + range.minX), - y: 0.5 * (range.maxY + range.minY) }; + value: function _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); + } }, writable: true, configurable: true }, - zoomExtent: { + _unselectConnectedEdges: { /** - * This function zooms out to fit all data on screen based on amount of nodes - * @param {Object} - * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; - * @param {Boolean} [disableStart] | If true, start is not called. + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private */ - value: function zoomExtent() { - var options = arguments[0] === undefined ? { nodes: [] } : arguments[0]; - var initialZoom = arguments[1] === undefined ? false : arguments[1]; - var range; - var zoomLevel; + value: function _unselectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); + } + }, + writable: true, + configurable: true + }, + _blurObject: { - if (initialZoom == true) { - // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. - var positionDefined = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (node.predefinedPosition == true) { - positionDefined += 1; - } - } - } - if (positionDefined > 0.5 * this.body.nodeIndices.length) { - this.zoomExtent(options, false); - return; - } - range = this._getRange(options.nodes); - var numberOfNodes = this.body.nodeIndices.length; - zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - // correct for larger canvasses. - var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); - zoomLevel *= factor; - } else { - this.body.emitter.emit("_redraw", true); - range = this._getRange(options.nodes); - var xDistance = Math.abs(range.maxX - range.minX) * 1.1; - var yDistance = Math.abs(range.maxY - range.minY) * 1.1; - var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; - var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; - zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function _blurObject(object) { + if (object.hover == true) { + object.hover = false; + this.body.emitter.emit("blurNode", { node: object.id }); } + }, + writable: true, + configurable: true + }, + _hoverObject: { - if (zoomLevel > 1) { - zoomLevel = 1; + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function _hoverObject(object) { + if (object.hover == false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.body.emitter.emit("hoverNode", { node: object.id }); + } + } + if (object instanceof Node) { + this._hoverConnectedEdges(object); } - - - var center = this._findCenter(range); - var animationOptions = { position: center, scale: zoomLevel, animation: options }; - this.moveTo(animationOptions); }, writable: true, configurable: true }, - focusOnNode: { + getSelection: { + + - // animation /** - * Center a node in view. * - * @param {Number} nodeId - * @param {Number} [options] + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection */ - value: function focusOnNode(nodeId) { - var options = arguments[1] === undefined ? {} : arguments[1]; - if (this.body.nodes[nodeId] !== undefined) { - var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; - options.position = nodePosition; - options.lockedOnNode = nodeId; - - this.moveTo(options); - } else { - console.log("Node: " + nodeId + " cannot be found."); - } + value: function getSelection() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return { nodes: nodeIds, edges: edgeIds }; }, writable: true, configurable: true }, - moveTo: { + getSelectedNodes: { /** * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.scale = Number // scale to move to - * | options.position = {x:Number, y:Number} // position to move to - * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. */ - value: function moveTo(options) { - if (options === undefined) { - options = {}; - return; - } - if (options.offset === undefined) { - options.offset = { x: 0, y: 0 }; - } - if (options.offset.x === undefined) { - options.offset.x = 0; - } - if (options.offset.y === undefined) { - options.offset.y = 0; - } - if (options.scale === undefined) { - options.scale = this.body.view.scale; - } - if (options.position === undefined) { - options.position = this.body.view.translation; - } - if (options.animation === undefined) { - options.animation = { duration: 0 }; - } - if (options.animation === false) { - options.animation = { duration: 0 }; - } - if (options.animation === true) { - options.animation = {}; + value: function getSelectedNodes() { + var idArray = []; + if (this.options.select == true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } + } } - if (options.animation.duration === undefined) { - options.animation.duration = 1000; - } // default duration - if (options.animation.easingFunction === undefined) { - options.animation.easingFunction = "easeInOutQuad"; - } // default easing function - - this.animateView(options); + return idArray; }, writable: true, configurable: true }, - animateView: { + getSelectedEdges: { /** * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.time = Number // animation time in milliseconds - * | options.scale = Number // scale to animate to - * | options.position = {x:Number, y:Number} // position to animate to - * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, - * // easeInCubic, easeOutCubic, easeInOutCubic, - * // easeInQuart, easeOutQuart, easeInOutQuart, - * // easeInQuint, easeOutQuint, easeInOutQuint + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. */ - value: function animateView(options) { - if (options === undefined) { - return; - } - this.animationEasingFunction = options.animation.easingFunction; - // release if something focussed on the node - this.releaseNode(); - if (options.locked == true) { - this.lockedOnNodeId = options.lockedOnNode; - this.lockedOnNodeOffset = options.offset; + value: function getSelectedEdges() { + var idArray = []; + if (this.options.select == true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } + } } + return idArray; + }, + writable: true, + configurable: true + }, + selectNodes: { - // forcefully complete the old animation if it was still running - if (this.easingTime != 0) { - this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. - } - this.sourceScale = this.body.view.scale; - this.sourceTranslation = this.body.view.translation; - this.targetScale = options.scale; + /** + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] + */ + value: function selectNodes(selection, highlightEdges) { + var i, iMax, id; - // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw - // but at least then we'll have the target transition - this.body.view.scale = this.targetScale; - var viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - options.position.x, - y: viewCenter.y - options.position.y - }; - this.targetTranslation = { - x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, - y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y - }; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - // if the time is set to 0, don't do an animation - if (options.animation.duration == 0) { - if (this.lockedOnNodeId != undefined) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - } else { - this.body.view.scale = this.targetScale; - this.body.view.translation = this.targetTranslation; - this.body.emitter.emit("_requestRedraw"); - } - } else { - this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's - this.animationEasingFunction = options.animation.easingFunction; + // first unselect any selected node + this.unselectAll(true); + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - this.viewFunction = this._transitionRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); + var node = this.body.nodes[id]; + if (!node) { + throw new RangeError("Node with id \"" + id + "\" not found"); + } + this._selectObject(node, true, true, highlightEdges, true); } + this.redraw(); }, writable: true, configurable: true }, - _lockedRedraw: { + selectEdges: { + /** - * used to animate smoothly by hijacking the redraw function. - * @private + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. */ - value: function _lockedRedraw() { - var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; - var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - nodePosition.x, - y: viewCenter.y - nodePosition.y - }; - var sourceTranslation = this.body.view.translation; - var targetTranslation = { - x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, - y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y - }; + value: function selectEdges(selection) { + var i, iMax, id; - this.body.view.translation = targetTranslation; - }, - writable: true, - configurable: true - }, - releaseNode: { - value: function releaseNode() { - if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + + // first unselect any selected node + this.unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var edge = this.body.edges[id]; + if (!edge) { + throw new RangeError("Edge with id \"" + id + "\" not found"); + } + this._selectObject(edge, true, true, false, true); } + this.redraw(); }, writable: true, configurable: true }, - _transitionRedraw: { + updateSelection: { /** - * - * @param easingTime + * Validate the selection: remove ids of nodes which no longer exist * @private */ - value: function _transitionRedraw() { - var finished = arguments[0] === undefined ? false : arguments[0]; - this.easingTime += this.animationSpeed; - this.easingTime = finished === true ? 1 : this.easingTime; - - var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - - this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress; - this.body.view.translation = { - x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, - y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress - }; - - // cleanup - if (this.easingTime >= 1) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.easingTime = 0; - if (this.lockedOnNodeId != undefined) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); + value: function updateSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } } - this.body.emitter.emit("animationFinished"); } }, writable: true, @@ -33321,13 +33040,13 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return View; + return SelectionHandler; })(); - module.exports = View; + module.exports = SelectionHandler; /***/ }, -/* 99 */ +/* 96 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -33337,1051 +33056,1125 @@ return /******/ (function(modules) { // webpackBootstrap 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. - * + * Created by Alex on 3/3/2015. */ + var LayoutEngine = (function () { + function LayoutEngine(body) { + _classCallCheck(this, LayoutEngine); - var util = __webpack_require__(1); + this.body = body; + } - var NavigationHandler = __webpack_require__(100).NavigationHandler; - var InteractionHandler = (function () { - function InteractionHandler(body, canvas, selectionHandler) { - _classCallCheck(this, InteractionHandler); + _prototypeProperties(LayoutEngine, null, { + setOptions: { + value: function setOptions(options) {}, + writable: true, + configurable: true + }, + positionInitially: { + value: function positionInitially(nodesArray) { + for (var i = 0; i < nodesArray.length; i++) { + var node = nodesArray[i]; + if (!node.isFixed() && (node.x === null || node.y === null)) { + var radius = 10 * 0.1 * nodesArray.length + 10; + var angle = 2 * Math.PI * Math.random(); + if (node.options.fixed.x == false) { + node.x = radius * Math.cos(angle); + } + if (node.options.fixed.x == false) { + node.y = radius * Math.sin(angle); + } + } + } + }, + writable: true, + configurable: true + }, + _resetLevels: { + value: function _resetLevels() { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (node.preassignedLevel == false) { + node.level = -1; + node.hierarchyEnumerated = false; + } + } + } + }, + writable: true, + configurable: true + }, + _setupHierarchicalLayout: { - this.body = body; - this.canvas = canvas; - this.selectionHandler = selectionHandler; - this.navigationHandler = new NavigationHandler(body, canvas); + /** + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly + * + * @private + */ + value: function _setupHierarchicalLayout() { + if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { + // get the size of the largest hubs and check if the user has defined a level for a node. + var hubsize = 0; + var node, nodeId; + var definedLevel = false; + var undefinedLevel = false; - // 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); + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.level != -1) { + definedLevel = true; + } else { + undefinedLevel = true; + } + if (hubsize < node.edges.length) { + hubsize = node.edges.length; + } + } + } - this.touchTime = 0; - this.drag = {}; - this.pinch = {}; - this.pointerPosition = { x: 0, y: 0 }; - this.hoverObj = { nodes: {}, edges: {} }; + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel == true && definedLevel == true) { + throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); + this.zoomExtent({ duration: 0 }, true, this.constants.clustering.enabled); + if (!this.constants.clustering.enabled) { + this.start(); + } + } else { + // setup the system to use hierarchical method. + this._changeConstants(); + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + if (this.constants.hierarchicalLayout.layout == "hubsize") { + this._determineLevels(hubsize); + } else { + this._determineLevelsDirected(false); + } + } + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); - this.options = {}; - this.defaultOptions = { - dragNodes: true, - dragView: true, - zoomView: true, - hoverEnabled: false, - showNavigationIcons: false, - tooltip: { - delay: 300, - fontColor: "black", - fontSize: 14, // px - fontFace: "verdana", - color: { - border: "#666", - background: "#FFFFC6" + // place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize. + this._placeNodesByHierarchy(distribution); + } + } + }, + writable: true, + configurable: true + }, + _placeNodesByHierarchy: { + + + /** + * This function places the nodes on the canvas based on the hierarchial distribution. + * + * @param {Object} distribution | obtained by the function this._getDistribution() + * @private + */ + value: function _placeNodesByHierarchy(distribution) { + var nodeId, node; + + // start placing all the level 0 nodes first. Then recursively position their branches. + for (var level in distribution) { + if (distribution.hasOwnProperty(level)) { + for (nodeId in distribution[level].nodes) { + if (distribution[level].nodes.hasOwnProperty(nodeId)) { + node = distribution[level].nodes[nodeId]; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (node.xFixed) { + node.x = distribution[level].minPos; + node.xFixed = false; + + distribution[level].minPos += distribution[level].nodeSpacing; + } + } else { + if (node.yFixed) { + node.y = distribution[level].minPos; + node.yFixed = false; + + distribution[level].minPos += distribution[level].nodeSpacing; + } + } + this._placeBranchNodes(node.edges, node.id, distribution, node.level); + } + } + } + } + + // stabilize the system after positioning. This function calls zoomExtent. + this._stabilize(); + }, + writable: true, + configurable: true + }, + _getDistribution: { + + + /** + * This function get the distribution of levels based on hubsize + * + * @returns {Object} + * @private + */ + value: function _getDistribution() { + var distribution = {}; + var nodeId, node, level; + + // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // the fix of X is removed after the x value has been set. + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + node.xFixed = true; + node.yFixed = true; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + node.y = this.constants.hierarchicalLayout.levelSeparation * node.level; + } else { + node.x = this.constants.hierarchicalLayout.levelSeparation * node.level; + } + if (distribution[node.level] === undefined) { + distribution[node.level] = { amount: 0, nodes: {}, minPos: 0, nodeSpacing: 0 }; + } + distribution[node.level].amount += 1; + distribution[node.level].nodes[nodeId] = node; + } } - }, - keyboard: { - enabled: false, - speed: { x: 10, y: 10, zoom: 0.02 }, - bindToWindow: true - } - }; - util.extend(this.options, this.defaultOptions); - } - _prototypeProperties(InteractionHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - // extend all but the values in fields - var fields = ["keyboard"]; - util.selectiveNotDeepExtend(fields, this.options, options); + // determine the largest amount of nodes of all levels + var maxCount = 0; + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + if (maxCount < distribution[level].amount) { + maxCount = distribution[level].amount; + } + } + } - // merge the keyboard options in. - util.mergeOptions(this.options, options, "keyboard"); + // set the initial position and spacing of each nodes accordingly + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; + distribution[level].nodeSpacing /= distribution[level].amount + 1; + distribution[level].minPos = distribution[level].nodeSpacing - 0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing; + } } - this.navigationHandler.setOptions(this.options); + return distribution; }, writable: true, configurable: true }, - getPointer: { + _determineLevels: { /** - * Get the pointer location from a touch location - * @param {{x: Number, y: Number}} touch - * @return {{x: Number, y: Number}} pointer + * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * + * @param hubsize * @private */ - value: function getPointer(touch) { - return { - x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), - y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) - }; + value: function _determineLevels(hubsize) { + var nodeId, node; + + // determine hubs + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.edges.length == hubsize) { + node.level = 0; + } + } + } + + // branch from hubs + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.level == 0) { + this._setLevel(1, node.edges, node.id); + } + } + } }, writable: true, configurable: true }, - onTouch: { + _determineLevelsDirected: { + /** - * On start of a touch gesture, store the pointer - * @param event + * this function allocates nodes in levels based on the direction of the edges + * + * @param hubsize * @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; + value: function _determineLevelsDirected() { + var nodeId, node, firstNode; + var minLevel = 10000; - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); + // set first node to source + firstNode = this.body.nodes[this.nodeIndices[0]]; + firstNode.level = minLevel; + this._setLevelDirected(minLevel, firstNode.edges, firstNode.id); + + // get the minimum level + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + minLevel = node.level < minLevel ? node.level : minLevel; + } + } + + // subtract the minimum from the set so we have a range starting from 0 + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + node.level -= minLevel; + } } }, writable: true, configurable: true }, - onTap: { + _changeConstants: { + /** - * handle tap/click event: select/unselect a node + * Since hierarchical layout does not support: + * - smooth curves (based on the physics), + * - clustering (based on dynamic node counts) + * + * We disable both features so there will be no problems. + * * @private */ - value: function onTap(event) { - var pointer = this.getPointer(event.center); - - var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; - var selected = this.selectionHandler.selectOnPoint(pointer); + value: function _changeConstants() { + this.constants.clustering.enabled = false; + this.constants.physics.barnesHut.enabled = false; + this.constants.physics.hierarchicalRepulsion.enabled = true; + this._loadSelectedForceSolver(); + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.dynamic = false; + } + this._configureSmoothCurves(); - if (selected === true || previouslySelected == true && selected === false) { - // select or unselect - this.body.emitter.emit("select", this.selectionHandler.getSelection()); + var config = this.constants.hierarchicalLayout; + config.levelSeparation = Math.abs(config.levelSeparation); + if (config.direction == "RL" || config.direction == "DU") { + config.levelSeparation *= -1; } - this.selectionHandler._generateClickEvent("click", pointer); + if (config.direction == "RL" || config.direction == "LR") { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "vertical"; + } + } else { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "horizontal"; + } + } }, writable: true, configurable: true }, - onDoubleTap: { + _placeBranchNodes: { /** - * handle doubletap event + * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes + * on a X position that ensures there will be no overlap. + * + * @param edges + * @param parentId + * @param distribution + * @param parentLevel * @private */ - value: function onDoubleTap(event) { - var pointer = this.getPointer(event.center); - this.selectionHandler._generateClickEvent("doubleClick", pointer); + value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; + } + + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + var nodeMoved = false; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (childNode.xFixed && childNode.level > parentLevel) { + childNode.xFixed = false; + childNode.x = distribution[childNode.level].minPos; + nodeMoved = true; + } + } else { + if (childNode.yFixed && childNode.level > parentLevel) { + childNode.yFixed = false; + childNode.y = distribution[childNode.level].minPos; + nodeMoved = true; + } + } + + if (nodeMoved == true) { + distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNode.level); + } + } + } }, writable: true, configurable: true }, - onHold: { - + _setLevel: { /** - * handle long tap event: multi select nodes + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. + * + * @param level + * @param edges + * @param parentId * @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("select", this.selectionHandler.getSelection()); + value: function _setLevel(level, edges, parentId) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; + } + if (childNode.level == -1 || childNode.level > level) { + childNode.level = level; + if (childNode.edges.length > 1) { + this._setLevel(level + 1, childNode.edges, childNode.id); + } + } } - - this.selectionHandler._generateClickEvent("click", pointer); }, writable: true, configurable: true }, - onRelease: { - + _setLevelDirected: { /** - * handle the release of the screen + * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction * + * @param level + * @param edges + * @param parentId * @private */ - value: function onRelease(event) { - this.body.emitter.emit("release", event); + value: function _setLevelDirected(level, edges, parentId) { + this.body.nodes[parentId].hierarchyEnumerated = true; + var childNode, direction; + for (var i = 0; i < edges.length; i++) { + direction = 1; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + direction = -1; + } else { + childNode = edges[i].to; + } + if (childNode.level == -1) { + childNode.level = level + direction; + } + } + + for (var i = 0; i < edges.length; i++) { + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; + } + + if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) { + this._setLevelDirected(childNode.level, childNode.edges, childNode.id); + } + } }, writable: true, configurable: true }, - onDragStart: { + _restoreNodes: { /** - * This function is called by onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. + * Unfix nodes * * @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); + value: function _restoreNodes() { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].xFixed = false; + this.body.nodes[nodeId].yFixed = false; + } } + }, + writable: true, + configurable: true + } + }); - var node = this.selectionHandler.getNodeAt(this.drag.pointer); - // note: drag.pointer is set in onTouch to get the initial touch location + return LayoutEngine; + })(); - this.drag.dragging = true; - this.drag.selection = []; - this.drag.translation = util.extend({}, this.body.view.translation); // copy the object - this.drag.nodeId = null; + module.exports = LayoutEngine; - this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); +/***/ }, +/* 97 */ +/***/ function(module, exports, __webpack_require__) { - 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); - } + "use strict"; - 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, + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.xFixed, - yFixed: object.yFixed - }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - object.xFixed = true; - object.yFixed = true; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - this.drag.selection.push(s); - } - } - } - }, - writable: true, - configurable: true - }, - onDrag: { + var util = __webpack_require__(1); + + + var Label = _interopRequire(__webpack_require__(63)); + + var BezierEdgeDynamic = _interopRequire(__webpack_require__(98)); + + var BezierEdgeStatic = _interopRequire(__webpack_require__(101)); + + var StraightEdge = _interopRequire(__webpack_require__(102)); + + /** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with options. Must contain + * At least options from and to. + * Available options: 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 + */ + var Edge = (function () { + function Edge(options, body, globalOptions) { + _classCallCheck(this, Edge); + + if (body === undefined) { + throw "No body provided"; + } + this.options = util.bridgeObject(globalOptions); + this.body = body; + + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.title = undefined; + this.value = undefined; + this.selected = false; + this.hover = false; + this.labelDirty = true; + this.colorDirty = true; + + this.from = undefined; // a node + this.to = undefined; // a node + + this.edgeType = undefined; + + this.connected = false; + + this.labelModule = new Label(this.body, this.options); + + this.setOptions(options); + + this.controlNodesEnabled = false; + this.controlNodes = { from: undefined, to: undefined, positions: {} }; + this.connectedNode = undefined; + } + + _prototypeProperties(Edge, null, { + setOptions: { /** - * handle drag event - * @private + * Set or overwrite options for the edge + * @param {Object} options an object with options + * @param doNotEmit */ - value: function onDrag(event) { - var _this = this; - if (this.drag.pinched === true) { + value: function setOptions(options) { + if (!options) { return; } + this.colorDirty = true; - // remove the focus on node if it is focussed on by the focusOnNode - this.body.emitter.emit("unlockNode"); + var fields = ["id", "font", "from", "hidden", "hoverWidth", "label", "length", "line", "opacity", "physics", "scaling", "selfReferenceSize", "to", "value", "width", "widthMin", "widthMax", "widthSelectionMultiplier"]; + util.selectiveDeepExtend(fields, this.options, options); - 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; + util.mergeOptions(this.options, options, "smooth"); + util.mergeOptions(this.options, options, "dashes"); - // update position of all selected nodes - selection.forEach(function (selection) { - var node = selection.node; + if (options.arrows !== undefined) { + util.mergeOptions(this.options.arrows, options.arrows, "to"); + util.mergeOptions(this.options.arrows, options.arrows, "middle"); + util.mergeOptions(this.options.arrows, options.arrows, "from"); + } - if (!selection.xFixed) { - node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); - } + if (options.id !== undefined) { + this.id = options.id; + } + if (options.from !== undefined) { + this.fromId = options.from; + } + if (options.to !== undefined) { + this.toId = options.to; + } + if (options.title !== undefined) { + this.title = options.title; + } + if (options.value !== undefined) { + this.value = options.value; + } - if (!selection.yFixed) { - node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + if (options.color !== undefined) { + if (util.isString(options.color)) { + this.options.color.color = options.color; + this.options.color.highlight = options.color; + } else { + if (options.color.color !== undefined) { + this.options.color.color = options.color.color; } - }); - - - // 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; + if (options.color.highlight !== undefined) { + this.options.color.highlight = options.color.highlight; } - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; + if (options.color.hover !== undefined) { + this.options.color.hover = options.color.hover; + } + } - this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; - this.body.emitter.emit("_redraw"); + // inherit colors + if (options.color.inherit === undefined) { + this.options.color.inherit.enabled = false; + } else { + util.mergeOptions(this.options.color, options.color, "inherit"); } } + + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); + + this.labelModule.setOptions(this.options); + + this.updateEdgeType(); + + return this.edgeType.setOptions(this.options); }, writable: true, configurable: true }, - onDragEnd: { + updateEdgeType: { + value: function updateEdgeType() { + var dataChanged = false; + var changeInType = true; + if (this.edgeType !== undefined) { + if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { + changeInType = false; + } + if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false) { + changeInType = false; + } + if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) { + changeInType = false; + } + if (changeInType == true) { + dataChanged = this.edgeType.cleanup(); + } + } - /** - * 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"); + if (changeInType === true) { + if (this.options.smooth.enabled === true) { + if (this.options.smooth.dynamic === true) { + dataChanged = true; + this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); + } else { + this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); + } + } else { + this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); + } } - this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); + return dataChanged; }, writable: true, configurable: true }, - onPinch: { - + togglePhysics: { /** - * Handle pinch event - * @param event - * @private + * Enable or disable the physics. + * @param status */ - value: function onPinch(event) { - var pointer = this.getPointer(event.center); - - this.drag.pinched = true; - if (this.pinch.scale === undefined) { - this.pinch.scale = 1; + value: function togglePhysics(status) { + if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { + if (this.via === undefined) { + this.via.pptions.physics = status; + } } - - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.scale; - this.zoom(scale, pointer); + this.options.physics = status; }, writable: true, configurable: true }, - zoom: { - + connect: { /** - * 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 + * Connect an edge to its nodes */ - 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; + value: function connect() { + this.disconnect(); - this.body.view.scale = scale; - this.body.view.translation = { x: tx, y: ty }; + this.from = this.body.nodes[this.fromId] || undefined; + this.to = this.body.nodes[this.toId] || undefined; + this.connected = this.from !== undefined && this.to !== undefined; - if (preScaleDragPointer != null) { - var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); - this.drag.pointer.x = postScaleDragPointer.x; - this.drag.pointer.y = postScaleDragPointer.y; + if (this.connected === true) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } else { + if (this.from) { + this.from.detachEdge(this); } - - this.body.emitter.emit("_requestRedraw"); - - if (scaleOld < scale) { - this.body.emitter.emit("zoom", { direction: "+" }); - } else { - this.body.emitter.emit("zoom", { direction: "-" }); + if (this.to) { + this.to.detachEdge(this); } } }, writable: true, configurable: true }, - onMouseWheel: { + disconnect: { /** - * 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 + * Disconnect an edge from its nodes */ - 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; + value: function disconnect() { + if (this.from) { + this.from.detachEdge(this); + this.from = undefined; } - - // 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); + if (this.to) { + this.to.detachEdge(this); + this.to = undefined; } - // Prevent default actions caused by mouse wheel. - event.preventDefault(); + this.connected = false; }, writable: true, configurable: true }, - onMouseMove: { + getTitle: { /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. */ - value: function onMouseMove(event) {}, + value: function getTitle() { + return typeof this.title === "function" ? this.title() : this.title; + }, writable: true, configurable: true - } - }); - - return InteractionHandler; - })(); - - module.exports = InteractionHandler; - // 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"); - // } - -/***/ }, -/* 100 */ -/***/ 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"); } }; - - var util = __webpack_require__(1); - var Hammer = __webpack_require__(19); - var hammerUtil = __webpack_require__(24); - var keycharm = __webpack_require__(39); - - var NavigationHandler = (function () { - function NavigationHandler(body, canvas) { - var _this = this; - _classCallCheck(this, NavigationHandler); - - this.body = body; - this.canvas = canvas; - - this.iconsCreated = false; - this.navigationHammers = []; - this.boundFunctions = {}; - this.touchTime = 0; - this.activated = false; - - - this.body.emitter.on("release", this._stopMovement.bind(this)); - this.body.emitter.on("activate", function () { - _this.activated = true;_this.configureKeyboardBindings(); - }); - this.body.emitter.on("deactivate", function () { - _this.activated = false;_this.configureKeyboardBindings(); - }); - this.body.emitter.on("destroy", function () { - if (_this.keycharm !== undefined) { - _this.keycharm.destroy(); - } - }); + }, + isSelected: { - this.options = {}; - } - _prototypeProperties(NavigationHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - this.options = options; - this.create(); - } + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; }, writable: true, configurable: true }, - create: { - value: function create() { - if (this.options.showNavigationIcons === true) { - if (this.iconsCreated === false) { - this.loadNavigationElements(); - } - } else if (this.iconsCreated === true) { - this.cleanNavigation(); - } + getValue: { - this.configureKeyboardBindings(); + + + /** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.value; }, writable: true, configurable: true }, - cleanNavigation: { - value: function cleanNavigation() { - // clean hammer bindings - if (this.navigationHammers.length != 0) { - for (var i = 0; i < this.navigationHammers.length; i++) { - this.navigationHammers[i].destroy(); - } - this.navigationHammers = []; - } + setValueRange: { - this._navigationReleaseOverload = function () {}; - // clean up previous navigation items - if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { - this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); + /** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + * @param total + */ + value: function setValueRange(min, max, total) { + if (this.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); + var widthDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled == true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; + } + this.options.width = this.options.scaling.min + scale * widthDiff; } - - this.iconsCreated = false; }, writable: true, configurable: true }, - loadNavigationElements: { + draw: { + /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. - * - * @private + * 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 */ - value: function loadNavigationElements() { - this.cleanNavigation(); - - this.navigationDOM = {}; - var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; - var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; - - this.navigationDOM.wrapper = document.createElement("div"); - this.canvas.frame.appendChild(this.navigationDOM.wrapper); - - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDOM[navigationDivs[i]] = document.createElement("div"); - this.navigationDOM[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; - this.navigationDOM.wrapper.appendChild(this.navigationDOM[navigationDivs[i]]); - - var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); - if (navigationDivActions[i] == "_zoomExtent") { - hammerUtil.onTouch(hammer, this._zoomExtent.bind(this)); - } else { - hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); - } - - this.navigationHammers.push(hammer); - } - - this.iconsCreated = true; + value: function draw(ctx) { + var via = this.edgeType.drawLine(ctx, this.selected, this.hover); + this.drawArrows(ctx, via); + this.drawLabel(ctx, via); }, writable: true, configurable: true }, - bindToRedraw: { - value: function bindToRedraw(action) { - if (this.boundFunctions[action] === undefined) { - this.boundFunctions[action] = this[action].bind(this); - this.body.emitter.on("initRedraw", this.boundFunctions[action]); - this.body.emitter.emit("_startRendering"); + drawArrows: { + value: function drawArrows(ctx, viaNode) { + if (this.options.arrows.from.enabled === true) { + this.edgeType.drawArrowHead(ctx, "from", viaNode); + } + if (this.options.arrows.middle.enabled === true) { + this.edgeType.drawArrowHead(ctx, "middle", viaNode); + } + if (this.options.arrows.to.enabled === true) { + this.edgeType.drawArrowHead(ctx, "to", viaNode); } }, writable: true, configurable: true }, - unbindFromRedraw: { - value: function unbindFromRedraw(action) { - if (this.boundFunctions[action] !== undefined) { - this.body.emitter.off("initRedraw", this.boundFunctions[action]); - this.body.emitter.emit("_stopRendering"); - delete this.boundFunctions[action]; + drawLabel: { + value: function drawLabel(ctx, viaNode) { + if (this.options.label !== undefined) { + // set style + var node1 = this.from; + var node2 = this.to; + var selected = this.from.selected || this.to.selected || this.selected; + if (node1.id != node2.id) { + var point = this.edgeType.getPoint(0.5, viaNode); + ctx.save(); + + // if the label has to be rotated: + if (this.options.font.align !== "horizontal") { + this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); + ctx.translate(point.x, this.labelModule.size.yLine); + this._rotateForLabelAlignment(ctx); + } + + // draw the label + this.labelModule.draw(ctx, point.x, point.y, selected); + ctx.restore(); + } else { + var x, y; + var radius = this.options.selfReferenceSize; + if (node1.width > node1.height) { + x = node1.x + node1.width * 0.5; + y = node1.y - radius; + } else { + x = node1.x + radius; + y = node1.y - node1.height * 0.5; + } + point = this._pointOnCircle(x, y, radius, 0.125); + + this.labelModule.draw(ctx, point.x, point.y, selected); + } } }, writable: true, configurable: true }, - _zoomExtent: { + isOverlappingWith: { + /** - * this stops all movement induced by the navigation buttons - * - * @private + * 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 */ - value: function _zoomExtent() { - if (new Date().valueOf() - this.touchTime > 700) { - // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) - this.body.emitter.emit("zoomExtent", { duration: 700 }); - this.touchTime = new Date().valueOf(); + value: function isOverlappingWith(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.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return dist < distMax; + } else { + return false; } }, writable: true, configurable: true }, - _stopMovement: { + _rotateForLabelAlignment: { + /** - * this stops all movement induced by the navigation buttons - * + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _stopMovement() { - for (var boundAction in this.boundFunctions) { - if (this.boundFunctions.hasOwnProperty(boundAction)) { - this.body.emitter.off("initRedraw", this.boundFunctions[boundAction]); - this.body.emitter.emit("_stopRendering"); - } + value: function _rotateForLabelAlignment(ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); + + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; } - this.boundFunctions = {}; - }, - writable: true, - configurable: true - }, - _moveUp: { - value: function _moveUp() { - this.body.view.translation.y += this.options.keyboard.speed.y; - }, - writable: true, - configurable: true - }, - _moveDown: { - value: function _moveDown() { - this.body.view.translation.y -= this.options.keyboard.speed.y; - }, - writable: true, - configurable: true - }, - _moveLeft: { - value: function _moveLeft() { - this.body.view.translation.x += this.options.keyboard.speed.x; + + ctx.rotate(angleInDegrees); }, writable: true, configurable: true }, - _moveRight: { - value: function _moveRight() { - this.body.view.translation.x -= this.options.keyboard.speed.x; + _pointOnCircle: { + + + /** + * 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 + */ + value: function _pointOnCircle(x, y, radius, percentage) { + var angle = percentage * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + }; }, writable: true, configurable: true }, - _zoomIn: { - value: function _zoomIn() { - this.body.view.scale += this.options.keyboard.speed.zoom; + select: { + value: function select() { + this.selected = true; }, writable: true, configurable: true }, - _zoomOut: { - value: function _zoomOut() { - this.body.view.scale -= this.options.keyboard.speed.zoom; + unselect: { + value: function unselect() { + this.selected = false; }, writable: true, configurable: true }, - configureKeyboardBindings: { + _drawControlNodes: { + + + + + + + + + + + + - /** - * bind all keys using keycharm. - */ - value: function configureKeyboardBindings() { - if (this.keycharm !== undefined) { - this.keycharm.destroy(); - } - if (this.options.keyboard.enabled === true) { - if (this.options.keyboard.bindToWindow === true) { - this.keycharm = keycharm({ container: window, preventDefault: false }); - } else { - this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: false }); - } - this.keycharm.reset(); - if (this.activated === true) { - this.keycharm.bind("up", this.bindToRedraw.bind(this, "_moveUp"), "keydown"); - this.keycharm.bind("down", this.bindToRedraw.bind(this, "_moveDown"), "keydown"); - this.keycharm.bind("left", this.bindToRedraw.bind(this, "_moveLeft"), "keydown"); - this.keycharm.bind("right", this.bindToRedraw.bind(this, "_moveRight"), "keydown"); - this.keycharm.bind("=", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("num+", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("num-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("[", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("]", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("pageup", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("pagedown", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("up", this.unbindFromRedraw.bind(this, "_moveUp"), "keyup"); - this.keycharm.bind("down", this.unbindFromRedraw.bind(this, "_moveDown"), "keyup"); - this.keycharm.bind("left", this.unbindFromRedraw.bind(this, "_moveLeft"), "keyup"); - this.keycharm.bind("right", this.unbindFromRedraw.bind(this, "_moveRight"), "keyup"); - this.keycharm.bind("=", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("num+", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("num-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("[", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("]", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("pageup", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("pagedown", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - } - } - }, - writable: true, - configurable: true - } - }); - return NavigationHandler; - })(); - exports.NavigationHandler = NavigationHandler; - Object.defineProperty(exports, "__esModule", { - value: true - }); -/***/ }, -/* 101 */ -/***/ 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 Node = __webpack_require__(62); - var util = __webpack_require__(1); - var SelectionHandler = (function () { - function SelectionHandler(body, canvas) { - var _this = this; - _classCallCheck(this, SelectionHandler); - this.body = body; - this.canvas = canvas; - this.selectionObj = { nodes: [], edges: [] }; - this.options = {}; - this.defaultOptions = { - select: true, - selectConnectedEdges: true - }; - util.extend(this.options, this.defaultOptions); - this.body.emitter.on("_dataChanged", function () { - _this.updateSelection(); - }); - } - _prototypeProperties(SelectionHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); - } - }, - writable: true, - configurable: true - }, - selectOnPoint: { + + + + + + + //*************************************************************************************************// + //*************************************************************************************************// + //*************************************************************************************************// + //*************************************************************************************************// + //*********************** MOVE THESE FUNCTIONS TO THE MANIPULATION SYSTEM ************************// + //*************************************************************************************************// + //*************************************************************************************************// + //*************************************************************************************************// + //*************************************************************************************************// + + + + /** - * handles the selection part of the tap; - * - * @param {Object} pointer - * @private + * This function draws the control nodes for the manipulator. + * In order to enable this, only set the this.controlNodesEnabled to true. + * @param ctx */ - value: function selectOnPoint(pointer) { - var selected = false; - if (this.options.select === true) { - this.unselectAll(); - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - if (obj !== undefined) { - selected = this.selectObject(obj); - } - this.body.emitter.emit("_requestRedraw"); - } - return selected; - }, - writable: true, - configurable: true - }, - selectAdditionalOnPoint: { - value: function selectAdditionalOnPoint(pointer) { - var selectionChanged = false; - if (this.options.select === true) { - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; + value: function _drawControlNodes(ctx) { + if (this.controlNodesEnabled == true) { + if (this.controlNodes.from === undefined && this.controlNodes.to === undefined) { + var nodeIdFrom = "edgeIdFrom:".concat(this.id); + var nodeIdTo = "edgeIdTo:".concat(this.id); + var nodeFromOptions = { + id: nodeIdFrom, + shape: "dot", + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } }, + radius: 7, + borderWidth: 2, + borderWidthSelected: 2, + hidden: false, + physics: false + }; + var nodeToOptions = util.deepExtend({}, nodeFromOptions); + nodeToOptions.id = nodeIdTo; - if (obj !== undefined) { - selectionChanged = true; - if (obj.isSelected() === true) { - this.deselectObject(obj); - } else { - this.selectObject(obj); - } - this.body.emitter.emit("_requestRedraw"); + this.controlNodes.from = this.body.functions.createNode(nodeFromOptions); + this.controlNodes.to = this.body.functions.createNode(nodeToOptions); } - } - return selectionChanged; - }, - writable: true, - configurable: true - }, - _generateClickEvent: { - value: function _generateClickEvent(eventType, pointer) { - var properties = this.getSelection(); - properties.pointer = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: this.canvas.DOMtoCanvas(pointer) - }; - this.body.emitter.emit(eventType, properties); - }, - writable: true, - configurable: true - }, - selectObject: { - value: function selectObject(obj) { - if (obj !== undefined) { - if (obj instanceof Node) { - if (this.options.selectConnectedEdges === true) { - this._selectConnectedEdges(obj); - } + + 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; } - obj.select(); - this._addToSelection(obj); - return true; - } - return false; - }, - writable: true, - configurable: true - }, - deselectObject: { - value: function deselectObject(obj) { - if (obj.isSelected() === true) { - obj.selected = false; - this._removeFromSelection(obj); + 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; + } + + this.controlNodes.from.draw(ctx); + this.controlNodes.to.draw(ctx); + } else { + this.controlNodes = { from: undefined, to: undefined, positions: {} }; } }, writable: true, configurable: true }, - _getAllNodesOverlappingWith: { - - + _enableControlNodes: { /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes + * Enable control nodes. * @private */ - value: function _getAllNodesOverlappingWith(object) { - var overlappingNodes = []; - var nodes = this.body.nodes; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); - } - } - return overlappingNodes; + value: function _enableControlNodes() { + this.fromBackup = this.from; + this.toBackup = this.to; + this.controlNodesEnabled = true; }, writable: true, configurable: true }, - _pointerToPositionObject: { + _disableControlNodes: { /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} + * disable control nodes and remove from dynamicEdges from old node * @private */ - value: function _pointerToPositionObject(pointer) { - var canvasPos = this.canvas.DOMtoCanvas(pointer); - return { - left: canvasPos.x, - top: canvasPos.y, - right: canvasPos.x, - bottom: canvasPos.y - }; + value: function _disableControlNodes() { + 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 = undefined; + this.toBackup = undefined; + this.controlNodesEnabled = false; }, writable: true, configurable: true }, - getNodeAt: { + _getSelectedControlNode: { /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | undefined} node + * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns undefined. + * @param x + * @param y + * @returns {undefined} * @private */ - value: function getNodeAt(pointer) { - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + value: function _getSelectedControlNode(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 there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + 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 undefined; } @@ -34389,545 +34182,881 @@ return /******/ (function(modules) { // webpackBootstrap writable: true, configurable: true }, - _getEdgesOverlappingWith: { + _restoreControlNodes: { + /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes + * this resets the control nodes to their original position. * @private */ - value: function _getEdgesOverlappingWith(object, overlappingEdges) { - var edges = this.body.edges; - for (var i = 0; i < this.body.edgeIndices.length; i++) { - var edgeId = this.body.edgeIndices[i]; - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); - } + value: function _restoreControlNodes() { + if (this.controlNodes.from.selected == true) { + this.from = this.connectedNode; + this.connectedNode = undefined; + this.controlNodes.from.unselect(); + } else if (this.controlNodes.to.selected == true) { + this.to = this.connectedNode; + this.connectedNode = undefined; + this.controlNodes.to.unselect(); } }, writable: true, configurable: true }, - _getAllEdgesOverlappingWith: { + getControlNodeFromPosition: { /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private + * this calculates the position of the control nodes on the edges of the parent nodes. + * + * @param ctx + * @returns {x: *, y: *} */ - value: function _getAllEdgesOverlappingWith(object) { - var overlappingEdges = []; - this._getEdgesOverlappingWith(object, overlappingEdges); - return overlappingEdges; + value: function getControlNodeFromPosition(ctx) { + // draw arrow head + var controlnodeFromPos; + if (this.options.smooth.enabled == true) { + controlnodeFromPos = this._findBorderPositionBezier(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; }, writable: true, configurable: true }, - getEdgeAt: { + getControlNodeToPosition: { /** - * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call - * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * this calculates the position of the control nodes on the edges of the parent nodes. * - * @param pointer - * @returns {undefined} - * @private + * @param ctx + * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} */ - value: function getEdgeAt(pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - - if (overlappingEdges.length > 0) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + value: function getControlNodeToPosition(ctx) { + // draw arrow head + var controlnodeToPos; + if (this.options.smooth.enabled == true) { + controlnodeToPos = this._findBorderPositionBezier(false, ctx); } else { - return undefined; + 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; + + controlnodeToPos = {}; + controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; + controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; } + + return controlnodeToPos; }, writable: true, configurable: true - }, - _addToSelection: { + } + }); + + return Edge; + })(); + + module.exports = Edge; + +/***/ }, +/* 98 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 3/20/2015. + */ + + var BezierBaseEdge = _interopRequire(__webpack_require__(99)); + + var BezierEdgeDynamic = (function (BezierBaseEdge) { + function BezierEdgeDynamic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeDynamic); + + this.via = undefined; + _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), "constructor", this).call(this, options, body, labelModule); // --> this calls the setOptions below + } + + _inherits(BezierEdgeDynamic, BezierBaseEdge); + + _prototypeProperties(BezierEdgeDynamic, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + this.id = this.options.id; + this.setupSupportNode(); + }, + writable: true, + configurable: true + }, + cleanup: { + value: function cleanup() { + if (this.via !== undefined) { + delete this.body.nodes[this.via.id]; + this.via = undefined; + return true; + } + return false; + }, + writable: true, + configurable: true + }, + setupSupportNode: { /** - * Add object to the selection array. + * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but + * are used for the force calculation. * - * @param obj + * The changed data is not called, if needed, it is returned by the main edge constructor. * @private */ - value: function _addToSelection(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } else { - this.selectionObj.edges[obj.id] = obj; + value: function setupSupportNode() { + if (this.via === undefined) { + var nodeId = "edgeId:" + this.id; + var node = this.body.functions.createNode({ + id: nodeId, + mass: 1, + shape: "circle", + image: "", + physics: true, + hidden: true + }); + this.body.nodes[nodeId] = node; + this.via = node; + this.via.parentEdgeId = this.id; + this.positionBezierNode(); } }, writable: true, configurable: true }, - _addToHover: { - - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - value: function _addToHover(obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; - } else { - this.hoverObj.edges[obj.id] = obj; + positionBezierNode: { + value: function positionBezierNode() { + if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { + 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 !== undefined) { + this.via.x = 0; + this.via.y = 0; } }, writable: true, configurable: true }, - _removeFromSelection: { - + _line: { /** - * Remove a single option from selection. - * - * @param {Object} obj + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _removeFromSelection(obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; - } else { - delete this.selectionObj.edges[obj.id]; - } + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); + ctx.stroke(); + return this.via; }, writable: true, configurable: true }, - unselectAll: { + getPoint: { + /** - * Unselect all. The selectionObj is useful for this. - * + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} * @private */ - value: function unselectAll() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); - } - } + value: function getPoint(percentage) { + var t = percentage; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; - this.selectionObj = { nodes: {}, edges: {} }; + return { x: x, y: y }; }, writable: true, configurable: true }, - _getSelectedNodeCount: { - - - /** - * return the number of selected nodes - * - * @returns {number} - * @private - */ - value: function _getSelectedNodeCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - return count; + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + console.log(this); + return this._findBorderPositionBezier(nearNode, ctx, this.via); }, writable: true, configurable: true }, - _getSelectedNode: { - - /** - * return the selected node - * - * @returns {number} - * @private - */ - value: function _getSelectedNode() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } - } - return undefined; + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); }, writable: true, configurable: true - }, - _getSelectedEdge: { + } + }); + + return BezierEdgeDynamic; + })(BezierBaseEdge); + + module.exports = BezierEdgeDynamic; + +/***/ }, +/* 99 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 3/20/2015. + */ + + var BaseEdge = _interopRequire(__webpack_require__(100)); + + var BezierBaseEdge = (function (BaseEdge) { + function BezierBaseEdge(options, body, labelModule) { + _classCallCheck(this, BezierBaseEdge); + + _get(Object.getPrototypeOf(BezierBaseEdge.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(BezierBaseEdge, BaseEdge); + + _prototypeProperties(BezierBaseEdge, null, { + _findBorderPositionBezier: { /** - * return the selected edge + * This function uses binary search to look for the point where the bezier curve crosses the border of the node. * - * @returns {number} - * @private + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode */ - value: function _getSelectedEdge() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; + value: function _findBorderPositionBezier(nearNode, ctx) { + var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; + console.log(nearNode, ctx, viaNode); + + var maxIterations = 10; + var iteration = 0; + var low = 0; + var high = 1; + var pos, angle, distanceToBorder, distanceToPoint, difference; + var threshold = 0.2; + var node = this.to; + var from = false; + if (nearNode.id === this.from.id) { + node = this.from; + from = true; + } + + while (low <= high && iteration < maxIterations) { + var middle = (low + high) * 0.5; + + pos = this.getPoint(middle, viaNode); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToPoint; + 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; + } } + + iteration++; } - return undefined; + pos.t = middle; + + return pos; }, writable: true, configurable: true }, - _getSelectedEdgeCount: { + _getDistanceToBezierEdge: { + /** - * return the number of selected edges - * - * @returns {number} + * 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 */ - value: function _getSelectedEdgeCount() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; + value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { + // x3,y3 is the point + var xVia = undefined, + yVia = undefined; + xVia = via.x; + yVia = via.y; + var minDistance = 1000000000; + var distance = undefined; + var i = undefined, + t = undefined, + x = undefined, + y = undefined; + var lastX = x1; + var lastY = y1; + for (i = 1; 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; } - return count; + + return minDistance; }, writable: true, configurable: true - }, - _getSelectedObjectCount: { + } + }); + return BezierBaseEdge; + })(BaseEdge); - /** - * return the number of selected objects. - * - * @returns {number} - * @private - */ - value: function _getSelectedObjectCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } - } - return count; + module.exports = BezierBaseEdge; + +/***/ }, +/* 100 */ +/***/ 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 3/20/2015. + */ + var util = __webpack_require__(1); + + var BaseEdge = (function () { + function BaseEdge(options, body, labelModule) { + _classCallCheck(this, BaseEdge); + + this.body = body; + this.labelModule = labelModule; + this.setOptions(options); + this.colorDirty = true; + } + + _prototypeProperties(BaseEdge, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + this.id = this.options.id; }, writable: true, configurable: true }, - _selectionIsEmpty: { + drawLine: { /** - * Check if anything is selected - * - * @returns {boolean} + * 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 */ - value: function _selectionIsEmpty() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; + value: function drawLine(ctx, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx); + ctx.lineWidth = this.getLineWidth(); + var via = undefined; + if (this.from != this.to) { + // draw line + if (this.options.dashes.enabled == true) { + via = this._drawDashedLine(ctx); + } else { + via = this._line(ctx); } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; + } else { + var x = undefined, + y = undefined; + var radius = this.options.selfReferenceSize; + var node = this.from; + node.resize(ctx); + if (node.shape.width > node.shape.height) { + x = node.x + node.shape.width * 0.5; + y = node.y - radius; + } else { + x = node.x + radius; + y = node.y - node.shape.height * 0.5; } + this._circle(ctx, x, y, radius); } - return true; + + return via; }, writable: true, configurable: true }, - _clusterInSelection: { + _drawDashedLine: { + value: function _drawDashedLine(ctx) { + var via = undefined; + // 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.dashes.length !== undefined && this.options.dashes.gap !== undefined) { + pattern = [this.options.dashes.length, this.options.dashes.gap]; + } else { + pattern = [5, 5]; + } + // set dash settings for chrome or firefox + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; - /** - * check if one of the selected nodes is a cluster. - * - * @returns {boolean} - * @private - */ - value: function _clusterInSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; + // draw the line + via = this._line(ctx); + + // restore the dash settings. + ctx.setLineDash([0]); + ctx.lineDashOffset = 0; + ctx.restore(); + } else { + // unsupporting smooth lines + // draw dashes line + ctx.beginPath(); + ctx.lineCap = "round"; + if (this.options.dashes.altLength !== undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap, this.options.dashes.altLength, this.options.dashes.gap]); + } else if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap]); + } else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); } - } + ctx.stroke(); } - return false; + return via; }, writable: true, configurable: true }, - _selectConnectedEdges: { - - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - value: function _selectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.select(); - this._addToSelection(edge); + findBorderPosition: { + value: function findBorderPosition(nearNode, ctx, options) { + if (this.from != this.to) { + return this._findBorderPosition(nearNode, ctx, options); + } else { + return this._findBorderPositionCircle(nearNode, ctx, options); } }, writable: true, configurable: true }, - _hoverConnectedEdges: { + _findBorderPositionCircle: { + - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - value: function _hoverConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.hover = true; - this._addToHover(edge); - } - }, - writable: true, - configurable: true - }, - _unselectConnectedEdges: { /** - * unselect the edges connected to the node that is being selected - * - * @param {Node} node + * This function uses binary search to look for the point where the circle crosses the border of the node. + * @param x + * @param y + * @param radius + * @param node + * @param low + * @param high + * @param direction + * @param ctx + * @returns {*} * @private */ - value: function _unselectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.unselect(); - this._removeFromSelection(edge); + value: function _findBorderPositionCircle(node, ctx, options) { + var x = options.x; + var y = options.y; + var low = options.low; + var high = options.high; + var direction = options.direction; + + var maxIterations = 10; + var iteration = 0; + var radius = this.options.selfReferenceSize; + var pos = undefined, + angle = undefined, + distanceToBorder = undefined, + distanceToPoint = undefined, + difference = undefined; + var threshold = 0.05; + + while (low <= high && iteration < maxIterations) { + var _middle = (low + high) * 0.5; + + pos = this._pointOnCircle(x, y, radius, _middle); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToPoint; + 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 (direction > 0) { + low = _middle; + } else { + high = _middle; + } + } else { + if (direction > 0) { + high = _middle; + } else { + low = _middle; + } + } + iteration++; } - }, - writable: true, - configurable: true - }, - _blurObject: { - - - - - + pos.t = middle; - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - value: function _blurObject(object) { - if (object.hover == true) { - object.hover = false; - this.body.emitter.emit("blurNode", { node: object.id }); - } + return pos; }, writable: true, configurable: true }, - _hoverObject: { + getLineWidth: { /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width * @private */ - value: function _hoverObject(object) { - if (object.hover == false) { - object.hover = true; - this._addToHover(object); - if (object instanceof Node) { - this.body.emitter.emit("hoverNode", { node: object.id }); + value: function getLineWidth(selected, hover) { + if (selected == true) { + return Math.max(Math.min(this.options.widthSelectionMultiplier * this.options.width, this.options.scaling.max), 0.3 / this.body.view.scale); + } else { + if (hover == true) { + return Math.max(Math.min(this.options.hoverWidth, this.options.scaling.max), 0.3 / this.body.view.scale); + } else { + return Math.max(this.options.width, 0.3 / this.body.view.scale); } } - if (object instanceof Node) { - this._hoverConnectedEdges(object); - } }, writable: true, configurable: true }, - getSelection: { - + getColor: { + value: function getColor(ctx) { + var colorObj = this.options.color; + if (colorObj.inherit.enabled === true) { + if (colorObj.inherit.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; + if (this.from.selected == false && this.to.selected == false) { + fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); + toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.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); - /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection - */ - value: function getSelection() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return { nodes: nodeIds, edges: edgeIds }; - }, - writable: true, - configurable: true - }, - getSelectedNodes: { + // -------------------- this returns -------------------- // + return grd; + } - /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. - */ - value: function getSelectedNodes() { - var idArray = []; - if (this.options.select == true) { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); + if (this.colorDirty === true) { + if (colorObj.inherit.source == "to") { + colorObj.highlight = this.to.options.color.highlight.border; + colorObj.hover = this.to.options.color.hover.border; + colorObj.color = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); + } else { + // (this.options.color.inherit.source == "from") { + colorObj.highlight = this.from.options.color.highlight.border; + colorObj.hover = this.from.options.color.hover.border; + colorObj.color = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); } } } - return idArray; + + // if color inherit is on and gradients are used, the function has already returned by now. + this.colorDirty = false; + + if (this.selected == true) { + return colorObj.highlight; + } else if (this.hover == true) { + return colorObj.hover; + } else { + return colorObj.color; + } }, writable: true, configurable: true }, - getSelectedEdges: { + _circle: { /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private */ - value: function getSelectedEdges() { - var idArray = []; - if (this.options.select == true) { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); - } - } - } - return idArray; + value: function _circle(ctx, x, y, radius) { + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); }, writable: true, configurable: true }, - selectNodes: { + getDistanceToEdge: { /** - * select zero or more nodes with the option to highlight edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - * @param {boolean} [highlightEdges] + * 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 */ - value: function selectNodes(selection, highlightEdges) { - var i, iMax, id; - - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - - // first unselect any selected node - this.unselectAll(true); - - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - - var node = this.body.nodes[id]; - if (!node) { - throw new RangeError("Node with id \"" + id + "\" not found"); + value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { + // x3,y3 is the point + var returnValue = 0; + if (this.from != this.to) { + returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); + } else { + var x, y, dx, dy; + var radius = this.options.selfReferenceSize; + 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; } - this._selectObject(node, true, true, highlightEdges, true); + dx = x - x3; + dy = y - y3; + returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); + } + + if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { + return 0; + } else { + return returnValue; } - this.redraw(); }, writable: true, configurable: true }, - selectEdges: { - - - /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - value: function selectEdges(selection) { - var i, iMax, id; + _getDistanceToLine: { + value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { + var px = x2 - x1; + var py = y2 - y1; + var something = px * px + py * py; + var u = ((x3 - x1) * px + (y3 - y1) * py) / something; - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + if (u > 1) { + u = 1; + } else if (u < 0) { + u = 0; + } - // first unselect any selected node - this.unselectAll(true); + var x = x1 + u * px; + var y = y1 + u * py; + var dx = x - x3; + var dy = y - y3; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + //# 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 edge = this.body.edges[id]; - if (!edge) { - throw new RangeError("Edge with id \"" + id + "\" not found"); - } - this._selectObject(edge, true, true, false, true); - } - this.redraw(); + return Math.sqrt(dx * dx + dy * dy); }, writable: true, configurable: true }, - updateSelection: { + drawArrowHead: { /** - * Validate the selection: remove ids of nodes which no longer exist - * @private + * + * @param ctx + * @param position + * @param viaNode */ - value: function updateSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.body.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; - } - } + value: function drawArrowHead(ctx, position, viaNode) { + // set style + ctx.strokeStyle = this.getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this.getLineWidth(); + + // set lets + var angle = undefined; + var length = undefined; + var arrowPos = undefined; + var node1 = undefined; + var node2 = undefined; + var guideOffset = undefined; + var scaleFactor = undefined; + + if (position == "from") { + node1 = this.from; + node2 = this.to; + guideOffset = 0.1; + scaleFactor = this.options.arrows.from.scaleFactor; + } else if (position == "to") { + node1 = this.to; + node2 = this.from; + guideOffset = -0.1; + scaleFactor = this.options.arrows.to.scaleFactor; + } else { + node1 = this.to; + node2 = this.from; + scaleFactor = this.options.arrows.middle.scaleFactor; } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.body.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; + + // if not connected to itself + if (node1 != node2) { + if (position !== "middle") { + // draw arrow head + if (this.options.smooth.enabled == true) { + arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); + var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); + angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + } else { + angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + arrowPos = this.findBorderPosition(node1, ctx); } + } else { + angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. + } + // draw arrow at the end of the line + length = (10 + 5 * this.options.width) * scaleFactor; + ctx.arrow(arrowPos.x, arrowPos.y, angle, length); + ctx.fill(); + ctx.stroke(); + } else { + // draw circle + var _angle = undefined, + point = undefined; + var x = undefined, + y = undefined; + var radius = this.options.selfReferenceSize; + if (!node1.width) { + node1.resize(ctx); + } + + // get circle coordinates + if (node1.width > node1.height) { + x = node1.x + node1.width * 0.5; + y = node1.y - radius; + } else { + x = node1.x + radius; + y = node1.y - node1.height * 0.5; + } + + + if (position == "from") { + point = this.findBorderPosition(x, y, radius, node1, 0.25, 0.6, -1, ctx); + _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; + } else if (position == "to") { + point = this.findBorderPosition(x, y, radius, node1, 0.6, 0.8, 1, ctx); + _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; + } else { + point = this.findBorderPosition(x, y, radius, 0.175); + _angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; } + + // draw the arrowhead + var _length = (10 + 5 * this.options.width) * scaleFactor; + ctx.arrow(point.x, point.y, _angle, _length); + ctx.fill(); + ctx.stroke(); } }, writable: true, @@ -34935,493 +35064,392 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return SelectionHandler; + return BaseEdge; })(); - module.exports = SelectionHandler; + module.exports = BaseEdge; /***/ }, -/* 102 */ +/* 101 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Created by Alex on 3/3/2015. + * Created by Alex on 3/20/2015. */ - var LayoutEngine = (function () { - function LayoutEngine(body) { - _classCallCheck(this, LayoutEngine); + var BezierBaseEdge = _interopRequire(__webpack_require__(99)); - this.body = body; + var BezierEdgeStatic = (function (BezierBaseEdge) { + function BezierEdgeStatic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeStatic); + + _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), "constructor", this).call(this, options, body, labelModule); } - _prototypeProperties(LayoutEngine, null, { - setOptions: { - value: function setOptions(options) {}, + _inherits(BezierEdgeStatic, BezierBaseEdge); + + _prototypeProperties(BezierEdgeStatic, null, { + cleanup: { + value: function cleanup() { + return false; + }, writable: true, configurable: true }, - positionInitially: { - value: function positionInitially(nodesArray) { - for (var i = 0; i < nodesArray.length; i++) { - var node = nodesArray[i]; - if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { - var radius = 10 * 0.1 * nodesArray.length + 10; - var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) { - node.x = radius * Math.cos(angle); - } - if (node.yFixed == false) { - node.y = radius * Math.sin(angle); - } - } + _line: { + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + var via = this._getViaCoordinates(); + + // fallback to normal straight edges + if (via.x === undefined) { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return undefined; + } else { + ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); + ctx.stroke(); + return via; } }, writable: true, configurable: true }, - _resetLevels: { - value: function _resetLevels() { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (node.preassignedLevel == false) { - node.level = -1; - node.hierarchyEnumerated = false; + _getViaCoordinates: { + value: function _getViaCoordinates() { + var xVia = undefined; + var yVia = undefined; + var factor = this.options.smooth.roundness; + var type = this.options.smooth.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; } - } - }, - writable: true, - configurable: true - }, - _setupHierarchicalLayout: { + } else if (type == "curvedCW") { + dx = this.to.x - this.from.x; + dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly - * - * @private - */ - value: function _setupHierarchicalLayout() { - if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { - // get the size of the largest hubs and check if the user has defined a level for a node. - var hubsize = 0; - var node, nodeId; - var definedLevel = false; - var undefinedLevel = false; + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.level != -1) { - definedLevel = true; - } else { - undefinedLevel = true; + 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") { + dx = this.to.x - this.from.x; + dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; + + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); + + 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; } - if (hubsize < node.edges.length) { - hubsize = node.edges.length; + } 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; } } - } - - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel == true && definedLevel == true) { - throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); - this.zoomExtent({ duration: 0 }, true, this.constants.clustering.enabled); - if (!this.constants.clustering.enabled) { - this.start(); - } - } else { - // setup the system to use hierarchical method. - this._changeConstants(); - - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - if (this.constants.hierarchicalLayout.layout == "hubsize") { - this._determineLevels(hubsize); - } else { - this._determineLevelsDirected(false); + } 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; } } - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); - - // place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize. - this._placeNodesByHierarchy(distribution); } } + return { x: xVia, y: yVia }; }, writable: true, configurable: true }, - _placeNodesByHierarchy: { - - - /** - * This function places the nodes on the canvas based on the hierarchial distribution. - * - * @param {Object} distribution | obtained by the function this._getDistribution() - * @private - */ - value: function _placeNodesByHierarchy(distribution) { - var nodeId, node; - - // start placing all the level 0 nodes first. Then recursively position their branches. - for (var level in distribution) { - if (distribution.hasOwnProperty(level)) { - for (nodeId in distribution[level].nodes) { - if (distribution[level].nodes.hasOwnProperty(nodeId)) { - node = distribution[level].nodes[nodeId]; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (node.xFixed) { - node.x = distribution[level].minPos; - node.xFixed = false; - - distribution[level].minPos += distribution[level].nodeSpacing; - } - } else { - if (node.yFixed) { - node.y = distribution[level].minPos; - node.yFixed = false; - - distribution[level].minPos += distribution[level].nodeSpacing; - } - } - this._placeBranchNodes(node.edges, node.id, distribution, node.level); - } - } - } - } - - // stabilize the system after positioning. This function calls zoomExtent. - this._stabilize(); + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx, options) { + return this._findBorderPositionBezier(nearNode, ctx, options.via); }, writable: true, configurable: true }, - _getDistribution: { - - - /** - * This function get the distribution of levels based on hubsize - * - * @returns {Object} - * @private - */ - value: function _getDistribution() { - var distribution = {}; - var nodeId, node, level; - - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - node.xFixed = true; - node.yFixed = true; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - node.y = this.constants.hierarchicalLayout.levelSeparation * node.level; - } else { - node.x = this.constants.hierarchicalLayout.levelSeparation * node.level; - } - if (distribution[node.level] === undefined) { - distribution[node.level] = { amount: 0, nodes: {}, minPos: 0, nodeSpacing: 0 }; - } - distribution[node.level].amount += 1; - distribution[node.level].nodes[nodeId] = node; - } - } - - // determine the largest amount of nodes of all levels - var maxCount = 0; - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - if (maxCount < distribution[level].amount) { - maxCount = distribution[level].amount; - } - } - } - - // set the initial position and spacing of each nodes accordingly - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; - distribution[level].nodeSpacing /= distribution[level].amount + 1; - distribution[level].minPos = distribution[level].nodeSpacing - 0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing; - } - } - - return distribution; + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; + // x3,y3 is the point + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); }, writable: true, configurable: true }, - _determineLevels: { - + getPoint: { /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. - * - * @param hubsize + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} * @private */ - value: function _determineLevels(hubsize) { - var nodeId, node; - - // determine hubs - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.edges.length == hubsize) { - node.level = 0; - } - } - } + value: function getPoint(percentage) { + var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; + var t = percentage; + 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; - // branch from hubs - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.level == 0) { - this._setLevel(1, node.edges, node.id); - } - } - } + return { x: x, y: y }; }, writable: true, configurable: true - }, - _determineLevelsDirected: { + } + }); + return BezierEdgeStatic; + })(BezierBaseEdge); + module.exports = BezierEdgeStatic; - /** - * this function allocates nodes in levels based on the direction of the edges - * - * @param hubsize - * @private - */ - value: function _determineLevelsDirected() { - var nodeId, node, firstNode; - var minLevel = 10000; +/***/ }, +/* 102 */ +/***/ function(module, exports, __webpack_require__) { - // set first node to source - firstNode = this.body.nodes[this.nodeIndices[0]]; - firstNode.level = minLevel; - this._setLevelDirected(minLevel, firstNode.edges, firstNode.id); + "use strict"; - // get the minimum level - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - minLevel = node.level < minLevel ? node.level : minLevel; - } - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // subtract the minimum from the set so we have a range starting from 0 - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - node.level -= minLevel; - } - } - }, - writable: true, - configurable: true - }, - _changeConstants: { + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - /** - * Since hierarchical layout does not support: - * - smooth curves (based on the physics), - * - clustering (based on dynamic node counts) - * - * We disable both features so there will be no problems. - * - * @private - */ - value: function _changeConstants() { - this.constants.clustering.enabled = false; - this.constants.physics.barnesHut.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this._loadSelectedForceSolver(); - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.dynamic = false; - } - this._configureSmoothCurves(); + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - var config = this.constants.hierarchicalLayout; - config.levelSeparation = Math.abs(config.levelSeparation); - if (config.direction == "RL" || config.direction == "DU") { - config.levelSeparation *= -1; - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - if (config.direction == "RL" || config.direction == "LR") { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "vertical"; - } - } else { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "horizontal"; - } - } + /** + * Created by Alex on 3/20/2015. + */ + + var BaseEdge = _interopRequire(__webpack_require__(100)); + + var StraightEdge = (function (BaseEdge) { + function StraightEdge(options, body, labelModule) { + _classCallCheck(this, StraightEdge); + + _get(Object.getPrototypeOf(StraightEdge.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(StraightEdge, BaseEdge); + + _prototypeProperties(StraightEdge, null, { + cleanup: { + value: function cleanup() { + return false; }, writable: true, configurable: true }, - _placeBranchNodes: { - - + _line: { /** - * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes - * on a X position that ensures there will be no overlap. - * - * @param edges - * @param parentId - * @param distribution - * @param parentLevel + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } else { - childNode = edges[i].to; - } - - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - var nodeMoved = false; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (childNode.xFixed && childNode.level > parentLevel) { - childNode.xFixed = false; - childNode.x = distribution[childNode.level].minPos; - nodeMoved = true; - } - } else { - if (childNode.yFixed && childNode.level > parentLevel) { - childNode.yFixed = false; - childNode.y = distribution[childNode.level].minPos; - nodeMoved = true; - } - } - - if (nodeMoved == true) { - distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNode.level); - } - } - } + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return undefined; }, writable: true, configurable: true }, - _setLevel: { + getPoint: { /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. - * - * @param level - * @param edges - * @param parentId + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} * @private */ - value: function _setLevel(level, edges, parentId) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } else { - childNode = edges[i].to; - } - if (childNode.level == -1 || childNode.level > level) { - childNode.level = level; - if (childNode.edges.length > 1) { - this._setLevel(level + 1, childNode.edges, childNode.id); - } - } - } + value: function getPoint(percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + }; }, writable: true, configurable: true }, - _setLevelDirected: { - - /** - * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction - * - * @param level - * @param edges - * @param parentId - * @private - */ - value: function _setLevelDirected(level, edges, parentId) { - this.body.nodes[parentId].hierarchyEnumerated = true; - var childNode, direction; - for (var i = 0; i < edges.length; i++) { - direction = 1; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - direction = -1; - } else { - childNode = edges[i].to; - } - if (childNode.level == -1) { - childNode.level = level + direction; - } + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + var node1 = this.to; + var node2 = this.from; + if (nearNode.id === this.from.id) { + node1 = this.from; + node2 = this.to; } - for (var i = 0; i < edges.length; i++) { - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } else { - childNode = edges[i].to; - } + var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + var dx = node1.x - node2.x; + var dy = node1.y - node2.y; + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = nearNode.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) { - this._setLevelDirected(childNode.level, childNode.edges, childNode.id); - } - } + var borderPos = {}; + borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; + borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; + + return borderPos; }, writable: true, configurable: true }, - _restoreNodes: { - - - /** - * Unfix nodes - * - * @private - */ - value: function _restoreNodes() { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId].xFixed = false; - this.body.nodes[nodeId].yFixed = false; - } - } + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); }, writable: true, configurable: true } }); - return LayoutEngine; - })(); + return StraightEdge; + })(BaseEdge); - module.exports = LayoutEngine; + module.exports = StraightEdge; /***/ } /******/ ]) diff --git a/examples/network/01_basic_usage.html b/examples/network/01_basic_usage.html index 79400ac4..7cee88d5 100644 --- a/examples/network/01_basic_usage.html +++ b/examples/network/01_basic_usage.html @@ -32,8 +32,7 @@ // create an array with edges var edges = [ {from: 1, to: 3}, - {from: 1, to: 1}, - {from: 1, to: 2,smooth:false, arrows:{from:true, middle:true, to: true}}, + {from: 1, to: 2}, {from: 2, to: 4}, {from: 2, to: 5} ]; diff --git a/lib/network/Network.js b/lib/network/Network.js index 4ce18bd6..55e320a5 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -125,7 +125,7 @@ function Network (container, data, options) { var t0 = new Date().valueOf(); // update shortcut lists this._updateVisibleIndices(); - this.physics._updatePhysicsIndices(); + this.physics.updatePhysicsIndices(); // update values this._updateValueRange(this.body.nodes); this._updateValueRange(this.body.edges); @@ -287,41 +287,9 @@ Network.prototype.setOptions = function (options) { //// TODO: work out these options and document them - //if (options.edges) { - // if (options.edges.color !== undefined) { - // if (util.isString(options.edges.color)) { - // this.constants.edges.color = {}; - // this.constants.edges.color.color = options.edges.color; - // this.constants.edges.color.highlight = options.edges.color; - // this.constants.edges.color.hover = options.edges.color; - // } - // else { - // if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;} - // if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;} - // if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;} - // } - // this.constants.edges.inheritColor = false; - // } // - // if (!options.edges.fontColor) { - // if (options.edges.color !== undefined) { - // if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;} - // else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;} - // } - // } - //} // - //if (options.nodes) { - // if (options.nodes.color) { - // var newColorObj = util.parseColor(options.nodes.color); - // this.constants.nodes.color.background = newColorObj.background; - // this.constants.nodes.color.border = newColorObj.border; - // this.constants.nodes.color.highlight.background = newColorObj.highlight.background; - // this.constants.nodes.color.highlight.border = newColorObj.highlight.border; - // this.constants.nodes.color.hover.background = newColorObj.hover.background; - // this.constants.nodes.color.hover.border = newColorObj.hover.border; - // } - //} + // //if (options.groups) { // for (var groupname in options.groups) { // if (options.groups.hasOwnProperty(groupname)) { diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js index 0e1f9540..9c8451ee 100644 --- a/lib/network/modules/Clustering.js +++ b/lib/network/modules/Clustering.js @@ -131,8 +131,14 @@ class ClusterEngine { var node = this.body.nodes[nodeId]; options = this._checkOptions(options, node); - if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;} - if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;} + if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x;} + if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y;} + if (options.clusterNodeProperties.fixed === undefined) { + options.clusterNodeProperties.fixed = {}; + options.clusterNodeProperties.fixed.x = node.options.fixed.x; + options.clusterNodeProperties.fixed.y = node.options.fixed.y; + } + var childNodesObj = {}; var childEdgesObj = {} diff --git a/lib/network/modules/EdgesHandler.js b/lib/network/modules/EdgesHandler.js index 2686c01b..56bd505c 100644 --- a/lib/network/modules/EdgesHandler.js +++ b/lib/network/modules/EdgesHandler.js @@ -99,7 +99,17 @@ class EdgesHandler { } setOptions(options) { - + if (options) { + if (options.color !== undefined) { + if (util.isString(options.color)) { + util.assignAllKeys(this.options.color, options.color); + } + else { + util.extend(this.options.color, options.color); + } + this.options.color.inherit.enabled = false; + } + } } @@ -191,7 +201,7 @@ class EdgesHandler { if (edge === null) { // update edge edge.disconnect(); - edge.setOptions(data); + dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. edge.connect(); } else { diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index 6608ac81..e0fb8139 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -173,8 +173,8 @@ class InteractionHandler { this.onTouch(event); } - var node = this.selectionHandler.getNodeAt(this.drag.pointer); // note: drag.pointer is set in onTouch to get the initial touch location + var node = this.selectionHandler.getNodeAt(this.drag.pointer); this.drag.dragging = true; this.drag.selection = []; @@ -203,12 +203,12 @@ class InteractionHandler { // store original x, y, xFixed and yFixed, make the node temporarily Fixed x: object.x, y: object.y, - xFixed: object.xFixed, - yFixed: object.yFixed + xFixed: object.options.fixed.x, + yFixed: object.options.fixed.y }; - object.xFixed = true; - object.yFixed = true; + object.options.fixed.x = true; + object.options.fixed.y = true; this.drag.selection.push(s); } @@ -239,12 +239,12 @@ class InteractionHandler { // update position of all selected nodes selection.forEach((selection) => { var node = selection.node; - - if (!selection.xFixed) { + // only move the node if it was not fixed initially + if (selection.xFixed === false) { node.x = this.canvas._XconvertDOMtoCanvas(this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); } - - if (!selection.yFixed) { + // only move the node if it was not fixed initially + if (selection.yFixed === false) { node.y = this.canvas._YconvertDOMtoCanvas(this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); } }); @@ -281,8 +281,8 @@ class InteractionHandler { if (selection && selection.length) { selection.forEach(function (s) { // restore original xFixed and yFixed - s.node.xFixed = s.xFixed; - s.node.yFixed = s.yFixed; + s.node.options.fixed.x = s.xFixed; + s.node.options.fixed.y = s.yFixed; }); this.body.emitter.emit("startSimulation"); } diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index e1dc5f73..5b877880 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -14,11 +14,11 @@ class LayoutEngine { positionInitially(nodesArray) { for (var i = 0; i < nodesArray.length; i++) { let node = nodesArray[i]; - if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { + if ((!node.isFixed()) && (node.x === null || node.y === null)) { var radius = 10 * 0.1*nodesArray.length + 10; var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) {node.x = radius * Math.cos(angle);} - if (node.yFixed == false) {node.y = radius * Math.sin(angle);} + if (node.options.fixed.x == false) {node.x = radius * Math.cos(angle);} + if (node.options.fixed.x == false) {node.y = radius * Math.sin(angle);} } } } diff --git a/lib/network/modules/NodesHandler.js b/lib/network/modules/NodesHandler.js index 2044d560..9f8f22b3 100644 --- a/lib/network/modules/NodesHandler.js +++ b/lib/network/modules/NodesHandler.js @@ -90,12 +90,19 @@ class NodesHandler { size: 10, value: 1 }; - util.extend(this.options, this.defaultOptions); + + this.setOptions(options); } setOptions(options) { + if (options) { + util.selectiveNotDeepExtend(['color'], this.options, options); + if (options.color) { + this.options.color = util.parseColor(options.color); + } + } } /** diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index c66e8ebc..97892ff7 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -20,6 +20,7 @@ class PhysicsEngine { this.simulationInterval = 1000 / 60; this.requiresTimeout = true; this.previousStates = {}; + this.freezeCache = {}; this.renderTimer == undefined; this.stabilized = false; @@ -207,7 +208,7 @@ class PhysicsEngine { * * @private */ - _updatePhysicsIndices() { + updatePhysicsIndices() { this.physicsBody.forces = {}; this.physicsBody.physicsNodeIndices = []; this.physicsBody.physicsEdgeIndices = []; @@ -307,7 +308,7 @@ class PhysicsEngine { // store the state so we can revert this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y}; - if (!node.xFixed) { + if (node.options.fixed.x === false) { let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration velocities[nodeId].x += ax * timestep; // velocity @@ -319,7 +320,7 @@ class PhysicsEngine { velocities[nodeId].x = 0; } - if (!node.yFixed) { + if (node.options.fixed.y === false) { let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration velocities[nodeId].y += ay * timestep; // velocity @@ -359,11 +360,10 @@ class PhysicsEngine { var nodes = this.body.nodes; for (var id in nodes) { if (nodes.hasOwnProperty(id)) { - if (nodes[id].x != null && nodes[id].y != null) { - nodes[id].fixedData.x = nodes[id].xFixed; - nodes[id].fixedData.y = nodes[id].yFixed; - nodes[id].xFixed = true; - nodes[id].yFixed = true; + if (nodes[id].x && nodes[id].y) { + this.freezeCache[id] = {x:nodes[id].options.fixed.x,y:nodes[id].options.fixed.y}; + nodes[id].options.fixed.x = true; + nodes[id].options.fixed.y = true; } } } @@ -378,12 +378,13 @@ class PhysicsEngine { var nodes = this.body.nodes; for (var id in nodes) { if (nodes.hasOwnProperty(id)) { - if (nodes[id].fixedData.x != null) { - nodes[id].xFixed = nodes[id].fixedData.x; - nodes[id].yFixed = nodes[id].fixedData.y; + if (this.freezeCache[id] !== undefined) { + nodes[id].options.fixed.x = this.freezeCache[id].x; + nodes[id].options.fixed.y = this.freezeCache[id].y; } } } + this.freezeCache = {}; } /** diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index 4f69db8b..9449d945 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -25,7 +25,6 @@ class Edge { if (body === undefined) { throw "No body provided"; } - this.options = util.bridgeObject(globalOptions); this.body = body; @@ -49,7 +48,7 @@ class Edge { this.labelModule = new Label(this.body, this.options); - this.setOptions(options, true); + this.setOptions(options); this.controlNodesEnabled = false; this.controlNodes = {from: undefined, to: undefined, positions: {}}; @@ -62,7 +61,7 @@ class Edge { * @param {Object} options an object with options * @param doNotEmit */ - setOptions(options, doNotEmit = false) { + setOptions(options) { if (!options) { return; } @@ -138,26 +137,38 @@ class Edge { this.updateEdgeType(); - this.edgeType.setOptions(this.options); - + return this.edgeType.setOptions(this.options); } updateEdgeType() { + let dataChanged = false; + let changeInType = true; if (this.edgeType !== undefined) { - this.edgeType.cleanup(); + if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {changeInType = false;} + if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false){changeInType = false;} + if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) {changeInType = false;} + + if (changeInType == true) { + dataChanged = this.edgeType.cleanup(); + } } - if (this.options.smooth.enabled === true) { - if (this.options.smooth.dynamic === true) { - this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); + if (changeInType === true) { + if (this.options.smooth.enabled === true) { + if (this.options.smooth.dynamic === true) { + dataChanged = true; + this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); + } + else { + this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); + } } else { - this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); + this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); } } - else { - this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); - } + + return dataChanged; } diff --git a/lib/network/modules/components/Node.js b/lib/network/modules/components/Node.js index b1fb132a..04b6ff85 100644 --- a/lib/network/modules/components/Node.js +++ b/lib/network/modules/components/Node.js @@ -44,33 +44,25 @@ import TriangleDown from './nodes/shapes/triangleDown' class Node { constructor(options, body, imagelist, grouplist, globalOptions) { this.options = util.bridgeObject(globalOptions); - this.body = body; - this.selected = false; - this.hover = false; this.edges = []; // all edges connected to this node // set defaults for the options this.id = undefined; - this.allowedToMoveX = false; - this.allowedToMoveY = false; - this.xFixed = false; - this.yFixed = false; - this.boundingBox = {top: 0, left: 0, right: 0, bottom: 0}; this.imagelist = imagelist; this.grouplist = grouplist; - // physics options + // state options this.x = null; this.y = null; this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate + this.selected = false; + this.hover = false; - this.fixedData = {x: null, y: null}; this.labelModule = new Label(this.body, this.options); - this.setOptions(options); } @@ -117,29 +109,26 @@ class Node { } var fields = [ - 'id', 'borderWidth', 'borderWidthSelected', - 'shape', - 'image', 'brokenImage', - 'size', - 'label', 'customScalingFunction', - 'icon', - 'value', + 'font', 'hidden', - 'physics' + 'icon', + 'id', + 'image', + 'label', + 'physics', + 'shape', + 'size', + 'value' ]; util.selectiveDeepExtend(fields, this.options, options); - // basic options if (options.id !== undefined) { this.id = options.id; } - if (options.title !== undefined) { - this.title = options.title; - } if (options.x !== undefined) { this.x = options.x; this.predefinedPosition = true; @@ -156,10 +145,6 @@ class Node { this.preassignedLevel = true; } - if (options.triggerFunction !== undefined) { - this.triggerFunction = options.triggerFunction; - } - if (this.id === undefined) { throw "Node must have an id"; } @@ -185,21 +170,19 @@ class Node { } } - if (options.allowedToMoveX !== undefined) { - this.xFixed = !options.allowedToMoveX; - this.allowedToMoveX = options.allowedToMoveX; - } - else if (options.x !== undefined && this.allowedToMoveX == false) { - this.xFixed = true; - } - - - if (options.allowedToMoveY !== undefined) { - this.yFixed = !options.allowedToMoveY; - this.allowedToMoveY = options.allowedToMoveY; - } - else if (options.y !== undefined && this.allowedToMoveY == false) { - this.yFixed = true; + if (options.fixed !== undefined) { + if (typeof options.fixed == 'boolean') { + this.options.fixed.x = true; + this.options.fixed.y = true; + } + else { + if (options.fixed.x !== undefined && typeof options.fixed.x == 'boolean') { + this.options.fixed.x = options.fixed.x; + } + if (options.fixed.y !== undefined && typeof options.fixed.y == 'boolean') { + this.options.fixed.y = options.fixed.y; + } + } } // choose draw method depending on the shape @@ -249,7 +232,7 @@ class Node { break; } - this.labelModule.setOptions(this.options); + this.labelModule.setOptions(this.options, options); // reset the size of the node, this can be changed this._reset(); diff --git a/lib/network/modules/components/edges/bezierEdgeDynamic.js b/lib/network/modules/components/edges/bezierEdgeDynamic.js index 989fbb2c..f873bffa 100644 --- a/lib/network/modules/components/edges/bezierEdgeDynamic.js +++ b/lib/network/modules/components/edges/bezierEdgeDynamic.js @@ -6,10 +6,8 @@ import BezierBaseEdge from './util/bezierBaseEdge' class BezierEdgeDynamic extends BezierBaseEdge { constructor(options, body, labelModule) { - this.initializing = true; this.via = undefined; - super(options, body, labelModule); - this.initializing = false; + super(options, body, labelModule); // --> this calls the setOptions below } setOptions(options) { @@ -17,27 +15,27 @@ class BezierEdgeDynamic extends BezierBaseEdge { this.from = this.body.nodes[this.options.from]; this.to = this.body.nodes[this.options.to]; this.id = this.options.id; - this.setupSupportNode(this.initializing); + this.setupSupportNode(); } cleanup() { if (this.via !== undefined) { delete this.body.nodes[this.via.id]; this.via = undefined; - this.body.emitter.emit("_dataChanged"); + return true; } + return false; } /** * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but * are used for the force calculation. * + * The changed data is not called, if needed, it is returned by the main edge constructor. * @private */ - setupSupportNode(doNotEmit = false) { - var changedData = false; + setupSupportNode() { if (this.via === undefined) { - changedData = true; var nodeId = "edgeId:" + this.id; var node = this.body.functions.createNode({ id: nodeId, @@ -52,11 +50,6 @@ class BezierEdgeDynamic extends BezierBaseEdge { this.via.parentEdgeId = this.id; this.positionBezierNode(); } - - // node has been added or deleted - if (changedData === true && doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); - } } positionBezierNode() { diff --git a/lib/network/modules/components/edges/bezierEdgeStatic.js b/lib/network/modules/components/edges/bezierEdgeStatic.js index 0fabe414..73d48dd8 100644 --- a/lib/network/modules/components/edges/bezierEdgeStatic.js +++ b/lib/network/modules/components/edges/bezierEdgeStatic.js @@ -9,7 +9,9 @@ class BezierEdgeStatic extends BezierBaseEdge { super(options, body, labelModule); } - cleanup() {} + cleanup() { + return false; + } /** * Draw a line between two nodes * @param {CanvasRenderingContext2D} ctx diff --git a/lib/network/modules/components/edges/straightEdge.js b/lib/network/modules/components/edges/straightEdge.js index 1d7624e7..9807a6b5 100644 --- a/lib/network/modules/components/edges/straightEdge.js +++ b/lib/network/modules/components/edges/straightEdge.js @@ -9,7 +9,9 @@ class StraightEdge extends BaseEdge { super(options, body, labelModule); } - cleanup() {} + cleanup() { + return false; + } /** * Draw a line between two nodes * @param {CanvasRenderingContext2D} ctx @@ -47,8 +49,6 @@ class StraightEdge extends BaseEdge { node2 = this.to; } - - let angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x)); let dx = (node1.x - node2.x); let dy = (node1.y - node2.y); diff --git a/lib/network/modules/components/unified/label.js b/lib/network/modules/components/unified/label.js index e041d6c4..42d3d4b2 100644 --- a/lib/network/modules/components/unified/label.js +++ b/lib/network/modules/components/unified/label.js @@ -7,8 +7,20 @@ let util = require('../../../../util'); class Label { constructor(body,options) { this.body = body; - this.setOptions(options); + this.fontOptions = {}; + this.defaultOptions = { + color: '#343434', + size: 14, // px + face: 'arial', + background: 'none', + stroke: 0, // px + strokeColor: 'white', + align:'horizontal' + } + util.extend(this.fontOptions, this.defaultOptions); + + this.setOptions(options); this.size = {top: 0, left: 0, width: 0, height: 0, yLine: 0}; // could be cached } @@ -17,6 +29,18 @@ class Label { if (options.label !== undefined) { this.labelDirty = true; } + if (options.font) { + if (typeof options.font === 'string') { + let optionsArray = options.font.split(" "); + this.fontOptions.size = optionsArray[0].replace("px",''); + this.fontOptions.face = optionsArray[1]; + this.fontOptions.color = optionsArray[2]; + } + else if (typeof options.font == 'object') { + util.extend(this.fontOptions, options.font); + } + this.fontOptions.size = Number(this.fontOptions.size); + } } @@ -34,7 +58,7 @@ class Label { return; // check if we have to render the label - let viewFontSize = Number(this.options.font.size) * this.body.view.scale; + let viewFontSize = this.fontOptions.size * this.body.view.scale; if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) return; @@ -53,12 +77,12 @@ class Label { * @private */ _drawBackground(ctx) { - if (this.options.font.background !== undefined && this.options.font.background !== "none") { - ctx.fillStyle = this.options.font.background; + if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") { + ctx.fillStyle = this.fontOptions.background; let lineMargin = 2; - switch (this.options.font.align) { + switch (this.fontOptions.align) { case 'middle': ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); break; @@ -84,7 +108,7 @@ class Label { * @private */ _drawText(ctx, selected, x, y, baseline = 'middle') { - let fontSize = Number(this.options.font.size); + let fontSize = this.fontOptions.size; let viewFontSize = fontSize * this.body.view.scale; // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) if (viewFontSize >= this.options.scaling.label.maxVisible) { @@ -96,20 +120,20 @@ class Label { [x, yLine] = this._setAlignment(ctx, x, yLine, baseline); // configure context for drawing the text - ctx.font = (selected ? 'bold ' : '') + fontSize + "px " + this.options.font.face; + ctx.font = (selected ? 'bold ' : '') + fontSize + "px " + this.fontOptions.face; ctx.fillStyle = fontColor; ctx.textAlign = 'center'; // set the strokeWidth - if (this.options.font.stroke > 0) { - ctx.lineWidth = this.options.font.stroke; + if (this.fontOptions.stroke > 0) { + ctx.lineWidth = this.fontOptions.stroke; ctx.strokeStyle = strokeColor; ctx.lineJoin = 'round'; } // draw the text for (let i = 0; i < this.lineCount; i++) { - if (this.options.font.stroke > 0) { + if (this.fontOptions.stroke > 0) { ctx.strokeText(this.lines[i], x, yLine); } ctx.fillText(this.lines[i], x, yLine); @@ -120,16 +144,16 @@ class Label { _setAlignment(ctx, x, yLine, baseline) { // check for label alignment (for edges) // TODO: make alignment for nodes - if (this.options.font.align !== 'horizontal') { + if (this.fontOptions.align !== 'horizontal') { x = 0; yLine = 0; let lineMargin = 2; - if (this.options.font.align === 'top') { + if (this.fontOptions.align === 'top') { ctx.textBaseline = 'alphabetic'; yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers } - else if (this.options.font.align === 'bottom') { + else if (this.fontOptions.align === 'bottom') { ctx.textBaseline = 'hanging'; yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers } @@ -153,8 +177,8 @@ class Label { * @private */ _getColor(viewFontSize) { - let fontColor = this.options.font.color || '#000000'; - let strokeColor = this.options.font.strokeColor || '#ffffff'; + let fontColor = this.fontOptions.color || '#000000'; + let strokeColor = this.fontOptions.strokeColor || '#ffffff'; if (viewFontSize <= this.options.scaling.label.drawThreshold) { let opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); fontColor = util.overrideOpacity(fontColor, opacity); @@ -173,7 +197,7 @@ class Label { getTextSize(ctx, selected = false) { let size = { width: this._processLabel(ctx,selected), - height: this.options.font.size * this.lineCount + height: this.fontOptions.size * this.lineCount }; return size; } @@ -191,12 +215,12 @@ class Label { if (this.labelDirty === true) { this.size.width = this._processLabel(ctx,selected); } - this.size.height = this.options.font.size * this.lineCount; + this.size.height = this.fontOptions.size * this.lineCount; this.size.left = x - this.size.width * 0.5; this.size.top = y - this.size.height * 0.5; - this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.options.font.size; + this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size; if (baseline == "hanging") { - this.size.top += 0.5 * this.options.font.size; + this.size.top += 0.5 * this.fontOptions.size; this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers this.size.yLine += 4; // distance from node } @@ -219,7 +243,7 @@ class Label { if (this.options.label !== undefined) { lines = String(this.options.label).split('\n'); lineCount = lines.length; - ctx.font = (selected ? 'bold ' : '') + this.options.font.size + "px " + this.options.font.face; + ctx.font = (selected ? 'bold ' : '') + this.fontOptions.size + "px " + this.fontOptions.face; width = ctx.measureText(lines[0]).width; for (let i = 1; i < lineCount; i++) { let lineWidth = ctx.measureText(lines[i]).width; diff --git a/lib/util.js b/lib/util.js index e8540039..f9de0d4d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -105,6 +105,22 @@ exports.randomUUID = function() { ); }; + +/** + * assign all keys of an object that are not nested objects to a certain value (used for color objects). + * @param obj + * @param value + */ +exports.assignAllKeys = function (obj, value) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + if (typeof obj[prop] !== 'object') { + obj[prop] = value; + } + } + } +} + /** * Extend object a with the properties of object b or a series of objects * Only properties with defined values are copied @@ -113,7 +129,7 @@ exports.randomUUID = function() { * @return {Object} a */ exports.extend = function (a, b) { - for (var i = 1, len = arguments.length; i < len; i++) { + for (var i = 1; i < arguments.length; i++) { var other = arguments[i]; for (var prop in other) { if (other.hasOwnProperty(prop)) {