From b9c97e18d8202a3ac8b1f30a0a3080827228bd8e Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Tue, 24 Mar 2015 13:53:49 +0100 Subject: [PATCH] moved groups to v4.0, fixed some bugs in edges --- dist/vis.js | 10385 ++++++++-------- index.js | 1 - lib/network/Groups.js | 107 - lib/network/Network.js | 48 +- lib/network/modules/EdgesHandler.js | 4 - lib/network/modules/Groups.js | 116 + lib/network/modules/InteractionHandler.js | 2 +- lib/network/modules/components/Edge.js | 6 +- .../modules/components/edges/util/EdgeBase.js | 16 +- 9 files changed, 5321 insertions(+), 5364 deletions(-) delete mode 100644 lib/network/Groups.js create mode 100644 lib/network/modules/Groups.js diff --git a/dist/vis.js b/dist/vis.js index f4f77e44..1d82bc58 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -139,8 +139,7 @@ return /******/ (function(modules) { // webpackBootstrap // Network exports.Network = __webpack_require__(53); exports.network = { - Groups: __webpack_require__(57), - Images: __webpack_require__(58), + Images: __webpack_require__(57), dotparser: __webpack_require__(55), gephiParser: __webpack_require__(56) }; @@ -23303,32 +23302,33 @@ return /******/ (function(modules) { // webpackBootstrap var DataView = __webpack_require__(9); var dotparser = __webpack_require__(55); var gephiParser = __webpack_require__(56); - var Groups = __webpack_require__(57); - var Images = __webpack_require__(58); + var Images = __webpack_require__(57); var Activator = __webpack_require__(38); - var locales = __webpack_require__(59); + var locales = __webpack_require__(58); + var Groups = _interopRequire(__webpack_require__(59)); + var NodesHandler = _interopRequire(__webpack_require__(60)); var EdgesHandler = _interopRequire(__webpack_require__(80)); - var PhysicsEngine = _interopRequire(__webpack_require__(87)); + var PhysicsEngine = _interopRequire(__webpack_require__(82)); - var ClusterEngine = _interopRequire(__webpack_require__(94)); + var ClusterEngine = _interopRequire(__webpack_require__(89)); - var CanvasRenderer = _interopRequire(__webpack_require__(96)); + var CanvasRenderer = _interopRequire(__webpack_require__(91)); - var Canvas = _interopRequire(__webpack_require__(97)); + var Canvas = _interopRequire(__webpack_require__(92)); - var View = _interopRequire(__webpack_require__(98)); + var View = _interopRequire(__webpack_require__(93)); - var InteractionHandler = _interopRequire(__webpack_require__(99)); + var InteractionHandler = _interopRequire(__webpack_require__(94)); - var SelectionHandler = _interopRequire(__webpack_require__(100)); + var SelectionHandler = _interopRequire(__webpack_require__(97)); - var LayoutEngine = _interopRequire(__webpack_require__(101)); + var LayoutEngine = _interopRequire(__webpack_require__(98)); /** * @constructor Network @@ -23354,9 +23354,7 @@ return /******/ (function(modules) { // webpackBootstrap initiallyVisible: false }, locale: "en", - locales: locales, - useDefaultGroups: true - }; + locales: locales }; // containers for nodes and edges this.body = { @@ -23402,11 +23400,11 @@ return /******/ (function(modules) { // webpackBootstrap this.bindEventListeners(); // setting up all modules - var groups = new Groups(); // object with groups var images = new Images(function () { return _this.body.emitter.emit("_requestRedraw"); }); // object with images + this.groups = new Groups(); // object with groups this.canvas = new Canvas(this.body); // DOM handler this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key @@ -23416,8 +23414,8 @@ return /******/ (function(modules) { // webpackBootstrap this.layoutEngine = new LayoutEngine(this.body); this.clustering = new ClusterEngine(this.body); // clustering api - this.nodesHandler = new NodesHandler(this.body, images, groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options - this.edgesHandler = new EdgesHandler(this.body, images, groups); // Handle adding, deleting and updating of edges as well as global options + this.nodesHandler = new NodesHandler(this.body, images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options + this.edgesHandler = new EdgesHandler(this.body, images, this.groups); // Handle adding, deleting and updating of edges as well as global options // create the DOM elements this.canvas.create(); @@ -23472,8 +23470,6 @@ return /******/ (function(modules) { // webpackBootstrap // call the dataUpdated event because the only difference between the two is the updating of the indices _this.body.emitter.emit("_dataUpdated"); - // start simulation (can be called safely, even if already running) - _this.body.emitter.emit("startSimulation"); console.log("_dataChanged took:", new Date().valueOf() - t0); }); @@ -23483,11 +23479,9 @@ return /******/ (function(modules) { // webpackBootstrap // update values _this._updateValueRange(_this.body.nodes); _this._updateValueRange(_this.body.edges); - // update edges - _this._reconnectEdges(); - _this._markAllEdgesAsDirty(); // start simulation (can be called safely, even if already running) _this.body.emitter.emit("startSimulation"); + console.log("_dataUpdated took:", new Date().valueOf() - t0); }); }; @@ -23550,14 +23544,6 @@ return /******/ (function(modules) { // webpackBootstrap */ Network.prototype.setOptions = function (options) { if (options !== undefined) { - //var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation', - // 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse' - //]; - // extend all but the values in fields - //util.selectiveNotDeepExtend(fields,this.constants, options); - //util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes); - //util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges); - //this.groups.useDefaultGroups = this.constants.useDefaultGroups; // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. @@ -23573,32 +23559,8 @@ return /******/ (function(modules) { // webpackBootstrap this.selectionHandler.setOptions(options.selection); this.clustering.setOptions(options.clustering); - //util.mergeOptions(this.constants, options,'smoothCurves'); - //util.mergeOptions(this.constants, options,'hierarchicalLayout'); - //util.mergeOptions(this.constants, options,'clustering'); - //util.mergeOptions(this.constants, options,'navigation'); - //util.mergeOptions(this.constants, options,'keyboard'); - //util.mergeOptions(this.constants, options,'dataManipulation'); - - - //if (options.dataManipulation) { - // this.editMode = this.constants.dataManipulation.initiallyVisible; - //} - - - //// TODO: work out these options and document them // // - // - //if (options.groups) { - // for (var groupname in options.groups) { - // if (options.groups.hasOwnProperty(groupname)) { - // var group = options.groups[groupname]; - // this.groups.add(groupname, group); - // } - // } - //} - // //if (options.tooltip) { // for (prop in options.tooltip) { // if (options.tooltip.hasOwnProperty(prop)) { @@ -25024,117 +24986,6 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - var util = __webpack_require__(1); - - /** - * @class Groups - * This class can store groups and options specific for groups. - */ - function Groups() { - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; - this.useDefaultGroups = true; - } - - - /** - * default constants for group colors - */ - Groups.DEFAULT = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue - { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow - { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red - { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green - { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta - { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple - { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange - { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue - { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink - { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint - - { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red - - { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange - { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue - { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green - { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta - { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple - { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue - { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink - { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint - - { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; - - - /** - * Clear all groups - */ - Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function () { - var i = 0; - for (var p in this) { - if (this.hasOwnProperty(p)) { - i++; - } - } - return i; - }; - }; - - - /** - * get group options of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group options - */ - Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - if (this.useDefaultGroups === false && this.groupsArray.length > 0) { - // create new group - var index = this.groupIndex % this.groupsArray.length; - this.groupIndex++; - group = {}; - group.color = this.groups[this.groupsArray[index]]; - this.groups[groupname] = group; - } else { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; - } - } - - return group; - }; - - /** - * Add a custom group style - * @param {String} groupName - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object - */ - Groups.prototype.add = function (groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; - }; - - module.exports = Groups; - // 20:bright red - -/***/ }, -/* 58 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - /** * @class Images * This class loads images and keeps them stored. @@ -25203,7 +25054,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Images; /***/ }, -/* 59 */ +/* 58 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -25244,6 +25095,152 @@ return /******/ (function(modules) { // webpackBootstrap exports.nl_NL = exports.nl; exports.nl_BE = exports.nl; +/***/ }, +/* 59 */ +/***/ 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); + + /** + * @class Groups + * This class can store groups and options specific for groups. + */ + var Groups = (function () { + function Groups() { + _classCallCheck(this, Groups); + + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; + + this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue + { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow + { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red + { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green + { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta + { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple + { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange + { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue + { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink + { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint + + { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red + + { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange + { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue + { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green + { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta + { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple + { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue + { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink + { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint + + { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; + + this.options = {}; + this.defaultOptions = { + useDefaultGroups: true + }; + util.extend(this.options, this.defaultOptions); + } + + _prototypeProperties(Groups, null, { + setOptions: { + value: function setOptions(options) { + var optionFields = ["useDefaultGroups"]; + + if (options !== undefined) { + for (var groupname in options) { + if (options.hasOwnProperty(groupname)) { + if (optionFields.indexOf(groupName) == -1) { + var group = options[groupname]; + this.add(groupname, group); + } + } + } + } + }, + writable: true, + configurable: true + }, + clear: { + + + /** + * Clear all groups + */ + value: function clear() { + this.groups = {}; + this.groupsArray = []; + }, + writable: true, + configurable: true + }, + get: { + + /** + * get group options of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group options + */ + value: function get(groupname) { + var group = this.groups[groupname]; + if (group == undefined) { + if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; + } else { + // create new group + var index = this.defaultIndex % this.defaultGroups.length; + this.defaultIndex++; + group = {}; + group.color = this.defaultGroups[index]; + this.groups[groupname] = group; + } + } + + return group; + }, + writable: true, + configurable: true + }, + add: { + + /** + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ + value: function add(groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; + }, + writable: true, + configurable: true + } + }); + + return Groups; + })(); + + module.exports = Groups; + // 20:bright red + /***/ }, /* 60 */ /***/ function(module, exports, __webpack_require__) { @@ -27759,12 +27756,8 @@ return /******/ (function(modules) { // webpackBootstrap // this is called when options of EXISTING nodes or edges have changed. this.body.emitter.on("_dataUpdated", function () { - var t0 = new Date().valueOf(); - // update values _this.reconnectEdges(); _this.markAllEdgesAsDirty(); - // start simulation (can be called safely, even if already running) - console.log("_dataUpdated took:", new Date().valueOf() - t0); }); } @@ -28041,11 +28034,11 @@ return /******/ (function(modules) { // webpackBootstrap var Label = _interopRequire(__webpack_require__(62)); - var BezierEdgeDynamic = _interopRequire(__webpack_require__(82)); + var BezierEdgeDynamic = _interopRequire(__webpack_require__(99)); - var BezierEdgeStatic = _interopRequire(__webpack_require__(85)); + var BezierEdgeStatic = _interopRequire(__webpack_require__(102)); - var StraightEdge = _interopRequire(__webpack_require__(86)); + var StraightEdge = _interopRequire(__webpack_require__(103)); /** * @class Edge @@ -28375,13 +28368,13 @@ return /******/ (function(modules) { // webpackBootstrap drawArrows: { value: function drawArrows(ctx, viaNode) { if (this.options.arrows.from.enabled === true) { - this.edgeType.drawArrowHead(ctx, "from", viaNode); + this.edgeType.drawArrowHead(ctx, "from", viaNode, this.selected, this.hover); } if (this.options.arrows.middle.enabled === true) { - this.edgeType.drawArrowHead(ctx, "middle", viaNode); + this.edgeType.drawArrowHead(ctx, "middle", viaNode, this.selected, this.hover); } if (this.options.arrows.to.enabled === true) { - this.edgeType.drawArrowHead(ctx, "to", viaNode); + this.edgeType.drawArrowHead(ctx, "to", viaNode, this.selected, this.hover); } }, writable: true, @@ -28789,306 +28782,525 @@ return /******/ (function(modules) { // webpackBootstrap 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 BezierEdgeBase = _interopRequire(__webpack_require__(83)); + var BarnesHutSolver = _interopRequire(__webpack_require__(83)); - var BezierEdgeDynamic = (function (BezierEdgeBase) { - function BezierEdgeDynamic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeDynamic); + var Repulsion = _interopRequire(__webpack_require__(84)); - this.via = undefined; - _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), "constructor", this).call(this, options, body, labelModule); // --> this calls the setOptions below - } + var HierarchicalRepulsion = _interopRequire(__webpack_require__(85)); - _inherits(BezierEdgeDynamic, BezierEdgeBase); + var SpringSolver = _interopRequire(__webpack_require__(86)); - _prototypeProperties(BezierEdgeDynamic, null, { + var HierarchicalSpringSolver = _interopRequire(__webpack_require__(87)); + + var CentralGravitySolver = _interopRequire(__webpack_require__(88)); + + var util = __webpack_require__(1); + + + var PhysicsEngine = (function () { + function PhysicsEngine(body) { + var _this = this; + _classCallCheck(this, PhysicsEngine); + + this.body = body; + this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; + + this.physicsEnabled = true; + this.simulationInterval = 1000 / 60; + this.requiresTimeout = true; + this.previousStates = {}; + this.freezeCache = {}; + this.renderTimer == undefined; + + this.stabilized = false; + this.stabilizationIterations = 0; + this.ready = false; // will be set to true if the stabilize + + // 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: 120, + 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 () { + console.log(4);_this.stopSimulation(); + }); + } + + _prototypeProperties(PhysicsEngine, 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(); + if (options === false) { + this.physicsEnabled = false; + this.stopSimulation(); + } else { + if (options !== undefined) { + util.selectiveNotDeepExtend(["stabilization"], this.options, options); + util.mergeOptions(this.options, options, "stabilization"); + } + this.init(); + } }, writable: true, configurable: true }, - cleanup: { - value: function cleanup() { - if (this.via !== undefined) { - delete this.body.nodes[this.via.id]; - this.via = undefined; - return 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); } - return false; + + this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); + this.modelOptions = options; }, writable: true, configurable: true }, - setupSupportNode: { - - /** - * 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 - */ - 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(); + initPhysics: { + value: function initPhysics() { + if (this.physicsEnabled === true) { + 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(); + } + } else { + this.ready = true; + this.body.emitter.emit("_redraw"); } }, 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; + 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"); } }, 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.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); - ctx.stroke(); - return this.via; + runSimulation: { + value: function runSimulation() { + if (this.physicsEnabled === true) { + if (this.viewFunction === undefined) { + this.viewFunction = this.simulationStep.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + } else { + this.body.emitter.emit("_redraw"); + } }, writable: true, configurable: true }, - getPoint: { + 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(); - /** - * 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; + // this makes sure there is no jitter. The decision is taken once to run it at double speed. + this.runDoubleSpeed = true; + } - return { x: x, y: y }; + 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(); + } }, writable: true, configurable: true }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - return this._findBorderPositionBezier(nearNode, ctx, this.via); + physicsTick: { + + /** + * A single simulation step (or "tick") in the physics simulation + * + * @private + */ + 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++; + } }, 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 - } - }); - - return BezierEdgeDynamic; - })(BezierEdgeBase); - - module.exports = BezierEdgeDynamic; - -/***/ }, -/* 83 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + updatePhysicsIndices: { - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + /** + * 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 updatePhysicsIndices() { + this.physicsBody.forces = {}; + this.physicsBody.physicsNodeIndices = []; + this.physicsBody.physicsEdgeIndices = []; + var nodes = this.body.nodes; + var edges = this.body.edges; - 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); } }; + // 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); + } + } + } - 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; }; + // 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); + } + } + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // 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 }; - /** - * Created by Alex on 3/20/2015. - */ + // 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 }; + } + } - var EdgeBase = _interopRequire(__webpack_require__(84)); + // 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; - var BezierEdgeBase = (function (EdgeBase) { - function BezierEdgeBase(options, body, labelModule) { - _classCallCheck(this, BezierEdgeBase); - - _get(Object.getPrototypeOf(BezierEdgeBase.prototype), "constructor", this).call(this, options, body, labelModule); - } - - _inherits(BezierEdgeBase, EdgeBase); - - _prototypeProperties(BezierEdgeBase, null, { - _findBorderPositionBezier: { + 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); - /** - * This function uses binary search to look for the point where the bezier curve crosses the border of the node. - * - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode - */ - value: function _findBorderPositionBezier(nearNode, ctx) { - var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; - 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; + 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; } - 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; - } + if (nodesPresent == true) { + if (vminCorrected > 0.5 * this.options.maxVelocity) { + return false; } else { - if (from == false) { - high = middle; - } else { - low = middle; - } + return stabilized; } + } + 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; - iteration++; + // 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.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 { + forces[nodeId].x = 0; + velocities[nodeId].x = 0; } - pos.t = middle; - return pos; + 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; + } + + var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); + return totalVelocity; }, writable: true, configurable: true }, - _getDistanceToBezierEdge: { + calculateForces: { + value: function calculateForces() { + this.gravitySolver.solve(); + this.nodesSolver.solve(); + this.edgesSolver.solve(); + }, + writable: true, + configurable: true + }, + _freezeNodes: { + + + + + + /** - * 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 + * 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 _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; + 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; + } + } + } + }, + writable: true, + configurable: true + }, + _restoreFrozenNodes: { + + /** + * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. + * + * @private + */ + 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; + } } - lastX = x; - lastY = y; } + this.freezeCache = {}; + }, + writable: true, + configurable: true + }, + stabilize: { - return minDistance; + /** + * Find a stable position for all nodes + * @private + */ + value: function stabilize() { + if (this.options.stabilization.onlyDynamicEdges == true) { + this._freezeNodes(); + } + this.stabilizationSteps = 0; + + setTimeout(this._stabilizationBatch.bind(this), 0); + }, + 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++; + } + + 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(); + } + }, + writable: true, + configurable: true + }, + _finalizeStabilization: { + value: function _finalizeStabilization() { + if (this.options.stabilization.zoomExtent == true) { + this.body.emitter.emit("zoomExtent", { duration: 0 }); + } + + if (this.options.stabilization.onlyDynamicEdges == true) { + this._restoreFrozenNodes(); + } + + this.body.emitter.emit("stabilizationIterationsDone"); + this.body.emitter.emit("_requestRedraw"); + this.ready = true; }, writable: true, configurable: true } }); - return BezierEdgeBase; - })(EdgeBase); + return PhysicsEngine; + })(); - module.exports = BezierEdgeBase; + module.exports = PhysicsEngine; /***/ }, -/* 84 */ +/* 83 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -29098,744 +29310,696 @@ 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 3/20/2015. + * Created by Alex on 2/23/2015. */ - var util = __webpack_require__(1); - var EdgeBase = (function () { - function EdgeBase(options, body, labelModule) { - _classCallCheck(this, EdgeBase); + var BarnesHutSolver = (function () { + function BarnesHutSolver(body, physicsBody, options) { + _classCallCheck(this, BarnesHutSolver); this.body = body; - this.labelModule = labelModule; + this.physicsBody = physicsBody; + this.barnesHutTree; this.setOptions(options); - this.colorDirty = true; } - _prototypeProperties(EdgeBase, 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; }, writable: true, configurable: true }, - drawLine: { + solve: { + /** - * 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 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 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); - } - - return via; - }, - 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]; - } + value: function solve() { + if (this.options.gravitationalConstant != 0) { + var node; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var nodeCount = nodeIndices.length; - // set dash settings for chrome or firefox - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; + // create the tree + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); - // draw the line - via = this._line(ctx); + // for debugging + this.barnesHutTree = barnesHutTree; - // 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); + // 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); } - ctx.stroke(); - } - return via; - }, - 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); + } } }, writable: true, configurable: true }, - _findBorderPositionCircle: { - - + _getForceContribution: { /** - * 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 + * 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 - * @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; - - 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; + value: function _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx, dy, distance; - while (low <= high && iteration < maxIterations) { - var _middle = (low + high) * 0.5; + // 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); - 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; + // 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; + + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; } else { - if (direction > 0) { - high = _middle; + // 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 { - low = _middle; + // 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; + } } } - iteration++; } - pos.t = middle; - - return pos; }, writable: true, configurable: true }, - getLineWidth: { + _formBarnesHutTree: { + /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width + * 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 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 _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; + } } } - }, - writable: true, - configurable: true - }, - getColor: { - value: function getColor(ctx) { - var colorObj = this.options.color; + // 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 - 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); + 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); - // -------------------- this returns -------------------- // - return grd; + // 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); - 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); - } + // 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); } } - // 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; - } + // make global + return barnesHutTree; }, writable: true, configurable: true }, - _circle: { + _updateBranchMass: { + /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius + * this updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node * @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 _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; + + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; + + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; + + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; }, writable: true, configurable: true }, - getDistanceToEdge: { + _placeInTree: { /** - * 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 + * determine in which branch the node will be placed. + * + * @param parentBranch + * @param node + * @param skipMassUpdate * @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); + value: function _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); + } + + if (parentBranch.children.NW.range.maxX > node.x) { + // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { + // in NW + this._placeInRegion(parentBranch, node, "NW"); + } else { + // in SW + this._placeInRegion(parentBranch, node, "SW"); + } } else { - 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; + // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { + // in NE + this._placeInRegion(parentBranch, node, "NE"); } else { - x = node.x + radius; - y = node.y - 0.5 * node.height; + // in SE + this._placeInRegion(parentBranch, node, "SE"); } - dx = x - x3; - dy = y - y3; - returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); } + }, + writable: true, + configurable: true + }, + _placeInRegion: { - 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; + + /** + * 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 { + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); + } + break; + case 4: + // place in branch + this._placeInTree(parentBranch.children[region], node); + break; } }, 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; + _splitBranch: { - if (u > 1) { - u = 1; - } else if (u < 0) { - u = 0; + + /** + * 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 _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"); - var x = x1 + u * px; - var y = y1 + u * py; - var dx = x - x3; - var dy = y - y3; + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); + } + }, + writable: true, + configurable: true + }, + _insertRegion: { - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance - return Math.sqrt(dx * dx + dy * dy); + /** + * This 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; + } + + + 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 }, - drawArrowHead: { + _debug: { + + + + + //--------------------------- DEBUGGING BELOW ---------------------------// + /** + * This function is for debugging purposed, it draws the tree. * * @param ctx - * @param position - * @param viaNode + * @param color + * @private */ - value: function drawArrowHead(ctx, position, viaNode) { - // set style - ctx.strokeStyle = this.getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this.getLineWidth(); + value: function _debug(ctx, color) { + if (this.barnesHutTree !== undefined) { + ctx.lineWidth = 1; - // set lets - var angle = undefined; - var length = undefined; - var arrowPos = undefined; - var node1 = undefined; - var node2 = undefined; - var guideOffset = undefined; - var scaleFactor = undefined; + this._drawBranch(this.barnesHutTree.root, ctx, color); + } + }, + writable: true, + configurable: true + }, + _drawBranch: { - 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; + + /** + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color + * @private + */ + value: function _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; } - // 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); - } + 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(); - // 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; - } + 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(); - 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; - } + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); - // draw the arrowhead - var _length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(point.x, point.y, _angle, _length); - ctx.fill(); - ctx.stroke(); - } + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ }, writable: true, configurable: true } }); - return EdgeBase; + return BarnesHutSolver; })(); - module.exports = EdgeBase; + module.exports = BarnesHutSolver; /***/ }, -/* 85 */ +/* 84 */ /***/ 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. + * Created by Alex on 2/23/2015. */ - var BezierEdgeBase = _interopRequire(__webpack_require__(83)); - - var BezierEdgeStatic = (function (BezierEdgeBase) { - function BezierEdgeStatic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeStatic); + var RepulsionSolver = (function () { + function RepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, RepulsionSolver); - _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), "constructor", this).call(this, options, body, labelModule); - } - - _inherits(BezierEdgeStatic, BezierEdgeBase); + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - _prototypeProperties(BezierEdgeStatic, null, { - cleanup: { - value: function cleanup() { - return false; + _prototypeProperties(RepulsionSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - _line: { + solve: { /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * * @private */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - var via = this._getViaCoordinates(); + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2; - // 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 - }, - _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 nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; - 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; + // approximation constants + var a = -2 / 3 / nodeDistance; + var b = 4 / 3; - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); + // 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]]; - 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; - } + 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; } - } 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; + + 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; } } } - return { x: xVia, y: yVia }; - }, - writable: true, - configurable: true - }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx, options) { - return this._findBorderPositionBezier(nearNode, ctx, options.via); }, 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); + } + }); + + return RepulsionSolver; + })(); + + module.exports = RepulsionSolver; + +/***/ }, +/* 85 */ +/***/ 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 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 }, - getPoint: { + solve: { /** - * 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}} + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * * @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; + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; - return { x: x, y: y }; + 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 } }); - return BezierEdgeStatic; - })(BezierEdgeBase); + return HierarchicalRepulsionSolver; + })(); - module.exports = BezierEdgeStatic; + module.exports = HierarchicalRepulsionSolver; /***/ }, /* 86 */ @@ -29843,113 +30007,110 @@ 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 EdgeBase = _interopRequire(__webpack_require__(84)); - - var StraightEdge = (function (EdgeBase) { - function StraightEdge(options, body, labelModule) { - _classCallCheck(this, StraightEdge); + var SpringSolver = (function () { + function SpringSolver(body, physicsBody, options) { + _classCallCheck(this, SpringSolver); - _get(Object.getPrototypeOf(StraightEdge.prototype), "constructor", this).call(this, options, body, labelModule); + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); } - _inherits(StraightEdge, EdgeBase); - - _prototypeProperties(StraightEdge, null, { - cleanup: { - value: function cleanup() { - return false; + _prototypeProperties(SpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - _line: { + solve: { + /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx + * This function calculates the springforces on the nodes, accounting for the support nodes. + * * @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; + 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 }, - getPoint: { + _calculateSpringForce: { + /** - * 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}} + * This is the code actually performing the calculation for the function above. + * + * @param node1 + * @param node2 + * @param edgeLength * @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; - } + value: function _calculateSpringForce(node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; - 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; + dx = node1.x - node2.x; + dy = node1.y - node2.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; - var borderPos = {}; - borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; - borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; - 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); + 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 } }); - return StraightEdge; - })(EdgeBase); + return SpringSolver; + })(); - module.exports = StraightEdge; + module.exports = SpringSolver; /***/ }, /* 87 */ @@ -29957,529 +30118,920 @@ 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"); } }; /** - * Created by Alex on 2/23/2015. + * Created by Alex on 2/25/2015. */ - var BarnesHutSolver = _interopRequire(__webpack_require__(88)); - - var Repulsion = _interopRequire(__webpack_require__(89)); + var HierarchicalSpringSolver = (function () { + function HierarchicalSpringSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalSpringSolver); - var HierarchicalRepulsion = _interopRequire(__webpack_require__(90)); + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - var SpringSolver = _interopRequire(__webpack_require__(91)); + _prototypeProperties(HierarchicalSpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { - var HierarchicalSpringSolver = _interopRequire(__webpack_require__(92)); + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + value: function solve() { + var edgeLength, edge; + var dx, dy, fx, fy, springForce, distance; + var edges = this.body.edges; + var factor = 0.5; - var CentralGravitySolver = _interopRequire(__webpack_require__(93)); + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - var util = __webpack_require__(1); + // 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; + } - var PhysicsEngine = (function () { - function PhysicsEngine(body) { - var _this = this; - _classCallCheck(this, PhysicsEngine); + // 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; - this.body = body; - this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; + 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; - this.physicsEnabled = true; - this.simulationInterval = 1000 / 60; - this.requiresTimeout = true; - this.previousStates = {}; - this.freezeCache = {}; - this.renderTimer == undefined; + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; - this.stabilized = false; - this.stabilizationIterations = 0; - this.ready = false; // will be set to true if the stabilize + fx = dx * springForce; + fy = dy * springForce; - // 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: 120, - damping: 0.09 + 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; + } + } + } + + // 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; + } }, - 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); + writable: true, + configurable: true + } + }); - 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 () { - console.log(4);_this.stopSimulation(); - }); + return HierarchicalSpringSolver; + })(); + + module.exports = HierarchicalSpringSolver; + +/***/ }, +/* 88 */ +/***/ 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 CentralGravitySolver = (function () { + function CentralGravitySolver(body, physicsBody, options) { + _classCallCheck(this, CentralGravitySolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); } - _prototypeProperties(PhysicsEngine, null, { + _prototypeProperties(CentralGravitySolver, null, { setOptions: { value: function setOptions(options) { - if (options === false) { - this.physicsEnabled = false; - this.stopSimulation(); - } else { - if (options !== undefined) { - util.selectiveNotDeepExtend(["stabilization"], this.options, options); - util.mergeOptions(this.options, options, "stabilization"); - } - this.init(); - } + this.options = options; }, 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); - } + 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; - this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); - this.modelOptions = options; + + 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; + })(); + + module.exports = CentralGravitySolver; + +/***/ }, +/* 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 _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__(90)); + + var ClusterEngine = (function () { + function ClusterEngine(body) { + _classCallCheck(this, ClusterEngine); + + this.body = body; + this.clusteredNodes = {}; + } + + _prototypeProperties(ClusterEngine, null, { + setOptions: { + value: function setOptions(options) {}, + writable: true, + configurable: true }, - initPhysics: { - value: function initPhysics() { - if (this.physicsEnabled === true) { - 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(); + 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(); + } + + 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); } - } else { - this.ready = true; - this.body.emitter.emit("_redraw"); } + + 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 }, - 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"); + clusterByNodeData: { + + + /** + * 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."); + } + + // 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 }, - runSimulation: { - value: function runSimulation() { - if (this.physicsEnabled === true) { - if (this.viewFunction === undefined) { - this.viewFunction = this.simulationStep.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); + 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 { - this.body.emitter.emit("_redraw"); + } + + 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 }, - simulationStep: { - value: function simulationStep() { - // check if the physics have settled - var startTime = Date.now(); - this.physicsTick(); - var physicsTime = Date.now() - startTime; + clusterByConnection: { - // run double speed if it is a little graph - if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { - this.physicsTick(); + /** + * + * @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!"); + } - // this makes sure there is no jitter. The decision is taken once to run it at double speed. - this.runDoubleSpeed = true; + 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; } - 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); + + 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 { - this.stabilizationIterations = 0; + childEdgesObj[edge.id] = edge; } - this.stopSimulation(); } + + this._cluster(childNodesObj, childEdgesObj, options, refreshData); }, writable: true, configurable: true }, - physicsTick: { + _cloneOptions: { + /** - * A single simulation step (or "tick") in the physics simulation - * - * @private - */ - value: function physicsTick() { - if (this.stabilized === false) { - this.calculateForces(); - this.stabilized = this.moveNodes(); + * 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: { - // 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 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)); } } + } + }, + writable: true, + configurable: true + }, + _checkOptions: { - this.stabilizationIterations++; + + /** + * 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 }, - updatePhysicsIndices: { + _cluster: { /** - * 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 updatePhysicsIndices() { - this.physicsBody.forces = {}; - this.physicsBody.physicsNodeIndices = []; - this.physicsBody.physicsEdgeIndices = []; - var nodes = this.body.nodes; - var edges = this.body.edges; + * + * @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; + } - // 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); - } - } + // check if we have an unique id; + if (options.clusterNodeProperties.id === undefined) { + options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); } + var clusterId = options.clusterNodeProperties.id; - // 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); - } + // 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"; } - // 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 }; + // 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; } - // clean deleted nodes from the velocity vector - for (var nodeId in this.physicsBody.velocities) { - if (nodes[nodeId] === undefined) { - delete this.physicsBody.velocities[nodeId]; + + // 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; + } } } - }, - 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]; + + // 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; } } - }, - 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; + + // 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; - if (nodesPresent == true) { - if (vminCorrected > 0.5 * this.options.maxVelocity) { - return false; - } else { - return stabilized; - } + + // wrap up + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); } - 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; + isCluster: { - // 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.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 { - forces[nodeId].x = 0; - velocities[nodeId].x = 0; - } - - 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 + /** + * 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 { - forces[nodeId].y = 0; - velocities[nodeId].y = 0; + console.log("Node does not exist."); + return false; } - - 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(); + _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 }, - _freezeNodes: { + 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); + } + } - /** - * 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 && 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; - } + // 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 }, - _restoreFrozenNodes: { + _connectEdge: { + + /** - * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. - * - * @private - */ - 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; - } - } + * 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; } - this.freezeCache = {}; + edge.connect(); }, writable: true, configurable: true }, - stabilize: { + _getClusterStack: { /** - * Find a stable position for all nodes - * @private - */ - value: function stabilize() { - if (this.options.stabilization.onlyDynamicEdges == true) { - this._freezeNodes(); - } - this.stabilizationSteps = 0; + * 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; - setTimeout(this._stabilizationBatch.bind(this), 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 }, - _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++; - } + _getConnectedId: { - 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); + + /** + * 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 { - this._finalizeStabilization(); + return edge.fromId; } }, writable: true, configurable: true }, - _finalizeStabilization: { - value: function _finalizeStabilization() { - if (this.options.stabilization.zoomExtent == true) { - this.body.emitter.emit("zoomExtent", { duration: 0 }); + _getHubSize: { + + /** + * 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; + + 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; - if (this.options.stabilization.onlyDynamicEdges == true) { - this._restoreFrozenNodes(); + 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; } - this.body.emitter.emit("stabilizationIterationsDone"); - this.body.emitter.emit("_requestRedraw"); - this.ready = true; + return hubThreshold; }, writable: true, configurable: true } }); - return PhysicsEngine; + return ClusterEngine; })(); - module.exports = PhysicsEngine; + module.exports = ClusterEngine; /***/ }, -/* 88 */ +/* 90 */ +/***/ function(module, exports, __webpack_require__) { + + "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__(61)); + + /** + * + */ + 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; + +/***/ }, +/* 91 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -30489,506 +31041,325 @@ 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 = (function () { - function BarnesHutSolver(body, physicsBody, options) { - _classCallCheck(this, BarnesHutSolver); + if (typeof window !== "undefined") { + window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; + } + + var util = __webpack_require__(1); + + + var CanvasRenderer = (function () { + function CanvasRenderer(body, canvas) { + var _this = this; + _classCallCheck(this, CanvasRenderer); this.body = body; - this.physicsBody = physicsBody; - this.barnesHutTree; - this.setOptions(options); - } + this.canvas = canvas; - _prototypeProperties(BarnesHutSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true - }, - solve: { + this.redrawRequested = false; + this.renderTimer = false; + this.requiresTimeout = true; + this.renderingActive = false; + this.renderRequests = 0; + this.pixelRatio = undefined; + // redefined in this._redraw + this.canvasTopLeft = { x: 0, y: 0 }; + this.canvasBottomRight = { x: 0, y: 0 }; - /** - * 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; + this.dragging = false; - // create the tree - var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + 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; + }); - // for debugging - this.barnesHutTree = barnesHutTree; + this.options = {}; + this.defaultOptions = { + hideEdgesOnDrag: false, + hideNodesOnDrag: false + }; + util.extend(this.options, this.defaultOptions); - // 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._determineBrowserMethod(); + } + + _prototypeProperties(CanvasRenderer, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); } }, 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; - - 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); + 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 { - // 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; - } + this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function } } - } + } else {} }, writable: true, configurable: true }, - _formBarnesHutTree: { - - - /** - * 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; - - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX = -Number.MAX_VALUE, - maxY = -Number.MAX_VALUE; + renderStep: { + value: function renderStep() { + // reset the renderTimer so a new scheduled animation step can be set + this.renderTimer = undefined; - // 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; - } - } + if (this.requiresTimeout == true) { + // this schedules a new simulation step + this.startRendering(); } - // 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); + this._redraw(); - // 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); - } + if (this.requiresTimeout == false) { + // this schedules a new simulation step + this.startRendering(); } - - // make global - return barnesHutTree; }, writable: true, configurable: true }, - _updateBranchMass: { - + redraw: { /** - * this updates the mass of a branch. this is increased by adding a node. - * - * @param parentBranch - * @param node - * @private + * Redraw the network with the current data + * chart will be resized too. */ - value: function _updateBranchMass(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1 / totalMass; - - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; - - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; - - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); - parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + value: function redraw() { + this.setSize(this.constants.width, this.constants.height); + this._redraw(); }, writable: true, configurable: true }, - _placeInTree: { - + _requestRedraw: { /** - * determine in which branch the node will be placed. - * - * @param parentBranch - * @param node - * @param skipMassUpdate + * 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 _placeInTree(parentBranch, node, skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch, node); - } - - if (parentBranch.children.NW.range.maxX > node.x) { - // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { - // in NW - this._placeInRegion(parentBranch, node, "NW"); - } else { - // in SW - this._placeInRegion(parentBranch, node, "SW"); - } - } else { - // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { - // in NE - this._placeInRegion(parentBranch, node, "NE"); + 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 { - // in SE - this._placeInRegion(parentBranch, node, "SE"); + window.requestAnimationFrame(this._redraw.bind(this, false)); } } }, writable: true, configurable: true }, - _placeInRegion: { + _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"); - /** - * 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 { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region], node); - } - break; - case 4: - // place in branch - this._placeInTree(parentBranch.children[region], node); - break; + // restore original scaling and translation + ctx.restore(); + + if (hidden === true) { + ctx.clearRect(0, 0, w, h); } + + this.body.emitter.emit("afterDrawing", ctx); }, writable: true, configurable: true }, - _splitBranch: { + _drawNodes: { /** - * 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 + * 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 _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; + value: function _drawNodes(ctx) { + var alwaysShow = arguments[1] === undefined ? false : arguments[1]; + 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); + //} + } } - 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); + // 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 }, - _insertRegion: { + _drawEdges: { /** - * 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 + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx * @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; - } - + value: function _drawEdges(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge; - 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 - }; + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edge.draw(ctx); + } + } }, writable: true, configurable: true }, - _debug: { - - - - - //--------------------------- DEBUGGING BELOW ---------------------------// - + _drawControlNodes: { /** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _debug(ctx, color) { - if (this.barnesHutTree !== undefined) { - ctx.lineWidth = 1; + value: function _drawControlNodes(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge; - this._drawBranch(this.barnesHutTree.root, ctx, color); + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + edge._drawControlNodes(ctx); } }, writable: true, configurable: true }, - _drawBranch: { - + _determineBrowserMethod: { /** - * This function is for debugging purposes. It draws the branches recursively. - * - * @param branch - * @param ctx - * @param color + * 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 _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); + 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; } - 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; + return CanvasRenderer; })(); - module.exports = BarnesHutSolver; + module.exports = CanvasRenderer; /***/ }, -/* 89 */ +/* 92 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -30997,191 +31368,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 RepulsionSolver = (function () { - function RepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, RepulsionSolver); + var Canvas = (function () { + function Canvas(body) { + var _this = this; + _classCallCheck(this, Canvas); this.body = body; - this.physicsBody = physicsBody; - 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(RepulsionSolver, 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.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); + + this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + } + + // 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: { + + /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. - * + * This function binds hammer, it can be repeated over and over due to the uniqueness check. * @private */ - value: function solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2; + value: function _bindHammer() { + if (this.hammer !== undefined) { + this.hammer.destroy(); + } + this.drag = {}; + this.pinch = {}; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + // init hammer + this.hammer = new Hammer(this.frame.canvas); + this.hammer.get("pinch").set({ enable: true }); - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; + 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); - // approximation constants - var a = -2 / 3 / nodeDistance; - var b = 4 / 3; + // 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); - // 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]]; + this.frame.canvas.addEventListener("mousemove", this.body.eventListeners.onMouseMove); - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + this.hammerFrame = new Hammer(this.frame); + hammerUtil.onRelease(this.hammerFrame, this.body.eventListeners.onRelease); + }, + writable: true, + configurable: true + }, + setSize: { - // 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; + /** + * 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 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; - fx = dx * repulsingForce; - fy = dy * repulsingForce; + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; - } + 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 { + // 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 + }, + _XconvertDOMtoCanvas: { + + + /** + * 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 _XconvertDOMtoCanvas(x) { + return (x - this.body.view.translation.x) / this.body.view.scale; + }, + writable: true, + configurable: true + }, + _XconvertCanvasToDOM: { + + /** + * 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 _XconvertCanvasToDOM(x) { + return x * this.body.view.scale + this.body.view.translation.x; + }, + writable: true, + configurable: true + }, + _YconvertDOMtoCanvas: { + + /** + * 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 _YconvertDOMtoCanvas(y) { + return (y - this.body.view.translation.y) / this.body.view.scale; + }, + writable: true, + configurable: true + }, + _YconvertCanvasToDOM: { + + /** + * 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 _YconvertCanvasToDOM(y) { + return y * this.body.view.scale + this.body.view.translation.y; }, writable: true, configurable: true - } - }); - - return RepulsionSolver; - })(); - - module.exports = RepulsionSolver; - -/***/ }, -/* 90 */ -/***/ 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 HierarchicalRepulsionSolver = (function () { - function HierarchicalRepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalRepulsionSolver); + }, + canvasToDOM: { - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } - _prototypeProperties(HierarchicalRepulsionSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + /** + * + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor + */ + 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, 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; - } - } - } + value: function DOMtoCanvas(pos) { + return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; }, writable: true, configurable: true } }); - return HierarchicalRepulsionSolver; + return Canvas; })(); - module.exports = HierarchicalRepulsionSolver; + module.exports = Canvas; /***/ }, -/* 91 */ +/* 93 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -31191,284 +31656,392 @@ 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 SpringSolver = (function () { - function SpringSolver(body, physicsBody, options) { - _classCallCheck(this, SpringSolver); + 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(SpringSolver, 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 /** - * This function calculates the springforces on the nodes, accounting for the support nodes. - * + * Find the center position of the network * @private */ - 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); + 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.shape.boundingBox.left) { + minX = node.shape.boundingBox.left; + } + if (maxX < node.shape.boundingBox.right) { + maxX = node.shape.boundingBox.right; + } + if (minY > node.shape.boundingBox.bottom) { + minY = node.shape.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.shape.boundingBox.top) { + maxY = node.shape.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.shape.boundingBox.left) { + minX = node.shape.boundingBox.left; + } + if (maxX < node.shape.boundingBox.right) { + maxX = node.shape.boundingBox.right; } + if (minY > node.shape.boundingBox.bottom) { + minY = node.shape.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.shape.boundingBox.top) { + maxY = node.shape.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 }, - _calculateSpringForce: { + _findCenter: { /** - * This is the code actually performing the calculation for the function above. - * - * @param node1 - * @param node2 - * @param edgeLength + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} * @private */ - 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; - - 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; + value: function _findCenter(range) { + return { x: 0.5 * (range.maxX + range.minX), + y: 0.5 * (range.maxY + range.minY) }; }, writable: true, configurable: true - } - }); - - return SpringSolver; - })(); + }, + zoomExtent: { - module.exports = SpringSolver; -/***/ }, -/* 92 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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; - "use strict"; + 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; + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + range = this._getRange(options.nodes); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + 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. - /** - * Created by Alex on 2/25/2015. - */ + // 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 HierarchicalSpringSolver = (function () { - function HierarchicalSpringSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalSpringSolver); + var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; + zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + } - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + if (zoomLevel > 1) { + zoomLevel = 1; + } - _prototypeProperties(HierarchicalSpringSolver, 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 dx, dy, fx, fy, springForce, distance; - var edges = this.body.edges; - var factor = 0.5; + 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; - var edgeIndices = this.physicsBody.physicsEdgeIndices; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + this.moveTo(options); + } else { + console.log("Node: " + nodeId + " cannot be found."); + } + }, + writable: true, + configurable: true + }, + moveTo: { - // 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; + /** + * + * @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 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.animateView(options); + }, + writable: true, + configurable: true + }, + animateView: { - // 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; + /** + * + * @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; + } - 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; + // 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. + } - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; + this.sourceScale = this.body.view.scale; + this.sourceTranslation = this.body.view.translation; + this.targetScale = options.scale; - fx = dx * springForce; - fy = dy * springForce; + // 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 (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; - } + // 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"); } - } - - // 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; - } + } 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; - // 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.viewFunction = this._transitionRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); } }, writable: true, configurable: true - } - }); - - return HierarchicalSpringSolver; - })(); - - module.exports = HierarchicalSpringSolver; - -/***/ }, -/* 93 */ -/***/ 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 CentralGravitySolver = (function () { - function CentralGravitySolver(body, physicsBody, options) { - _classCallCheck(this, CentralGravitySolver); + }, + _lockedRedraw: { - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + /** + * used to animate smoothly by hijacking the redraw function. + * @private + */ + 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 + }; - _prototypeProperties(CentralGravitySolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + this.body.view.translation = targetTranslation; }, 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; + 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; - var gravity = this.options.centralGravity; - var gravityForce = 0; + var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - 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); + 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 + }; - gravityForce = distance == 0 ? 0 : gravity / distance; - forces[nodeId].x = dx * gravityForce; - forces[nodeId].y = dy * gravityForce; + // 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); + } + this.body.emitter.emit("animationFinished"); } }, writable: true, @@ -31476,10 +32049,10 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return CentralGravitySolver; + return View; })(); - module.exports = CentralGravitySolver; + module.exports = View; /***/ }, /* 94 */ @@ -31494,683 +32067,709 @@ 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 24-Feb-15. + * Created by Alex on 2/27/2015. + * */ var util = __webpack_require__(1); - var Cluster = _interopRequire(__webpack_require__(95)); - var ClusterEngine = (function () { - function ClusterEngine(body) { - _classCallCheck(this, ClusterEngine); + var NavigationHandler = _interopRequire(__webpack_require__(95)); + + var Popup = _interopRequire(__webpack_require__(96)); + + var InteractionHandler = (function () { + function InteractionHandler(body, canvas, selectionHandler) { + _classCallCheck(this, InteractionHandler); this.body = body; - this.clusteredNodes = {}; + 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.popup = undefined; + this.popupObj = undefined; + this.popupTimer = undefined; + + + 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); + + this.body.emitter.on("_dataChanged", function () {}); } - _prototypeProperties(ClusterEngine, null, { + _prototypeProperties(InteractionHandler, null, { setOptions: { - value: function setOptions(options) {}, + value: function setOptions(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 }, - 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(); - } + getPointer: { - 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"); + /** + * 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 }, - clusterByNodeData: { + onTouch: { /** - * 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."); + * On start of a touch gesture, store the pointer + * @param event + * @private + */ + value: function onTouch(event) { + if (new Date().valueOf() - this.touchTime > 100) { + this.drag.pointer = this.getPointer(event.center); + this.drag.pinched = false; + this.pinch.scale = this.body.view.scale; + + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); } + }, + writable: true, + configurable: true + }, + onTap: { - // check if the options object is fine, append if needed - options = this._checkOptions(options); + /** + * handle tap/click event: select/unselect a node + * @private + */ + value: function onTap(event) { + var pointer = this.getPointer(event.center); - var childNodesObj = {}; - var childEdgesObj = {}; + var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; + var selected = this.selectionHandler.selectOnPoint(pointer); - // 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]; - } + if (selected === true || previouslySelected == true && selected === false) { + // select or unselect + this.body.emitter.emit("select", this.selectionHandler.getSelection()); } - this._cluster(childNodesObj, childEdgesObj, options, refreshData); + this.selectionHandler._generateClickEvent("click", pointer); }, writable: true, configurable: true }, - clusterOutliers: { + onDoubleTap: { /** - * 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 = []; + * handle doubletap event + * @private + */ + value: function onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent("doubleClick", pointer); + }, + writable: true, + configurable: true + }, + onHold: { - // 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 }); - } - } - } - 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"); + /** + * handle long tap event: multi select nodes + * @private + */ + value: function onHold(event) { + var pointer = this.getPointer(event.center); + + var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); + + if (selectionChanged === true) { + // select or longpress + this.body.emitter.emit("select", this.selectionHandler.getSelection()); } + + this.selectionHandler._generateClickEvent("click", pointer); }, writable: true, configurable: true }, - clusterByConnection: { + onRelease: { + /** - * - * @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 the release of the screen + * + * @private + */ + value: function onRelease(event) { + this.body.emitter.emit("release", event); + }, + writable: true, + configurable: true + }, + onDragStart: { - 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; + + /** + * 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); } + // 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 = {}; - var parentNodeId = node.id; - var parentClonedOptions = this._cloneOptions(parentNodeId); - childNodesObj[parentNodeId] = node; + 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 < node.edges.length; i++) { - var edge = node.edges[i]; - var childNodeId = this._getConnectedId(edge, parentNodeId); + this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); - 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]; - } + if (node != null && this.options.dragNodes === true) { + this.drag.nodeId = node.id; + // select the clicked node if not yet selected + if (node.isSelected() === false) { + this.selectionHandler.unselectAll(); + this.selectionHandler.selectObject(node); + } + + var selection = this.selectionHandler.selectionObj.nodes; + // create an array with the selected nodes and their original location and status + for (var nodeId in selection) { + if (selection.hasOwnProperty(nodeId)) { + var object = selection[nodeId]; + var s = { + id: object.id, + node: object, + + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: object.x, + y: object.y, + xFixed: object.options.fixed.x, + yFixed: object.options.fixed.y + }; + + object.options.fixed.x = true; + object.options.fixed.y = true; + + this.drag.selection.push(s); } - } else { - childEdgesObj[edge.id] = edge; } } - - this._cluster(childNodesObj, childEdgesObj, options, refreshData); }, writable: true, configurable: true }, - _cloneOptions: { + onDrag: { /** - * 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); + * handle drag event + * @private + */ + value: function onDrag(event) { + var _this = this; + if (this.drag.pinched === true) { + return; } - return clonedOptions; - }, - writable: true, - configurable: true - }, - _createClusterEdges: { + // remove the focus on node if it is focussed on by the focusOnNode + this.body.emitter.emit("unlockNode"); - /** - * 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 pointer = this.getPointer(event.center); + var selection = this.drag.selection; + if (selection && selection.length && this.options.dragNodes === true) { + (function () { + // calculate delta's and new location + var deltaX = pointer.x - _this.drag.pointer.x; + var deltaY = pointer.y - _this.drag.pointer.y; - var childKeys = Object.keys(childNodesObj); - for (var i = 0; i < childKeys.length; i++) { - childNodeId = childKeys[i]; - childNode = childNodesObj[childNodeId]; + // 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); + } + }); - // 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; + // start the simulation of the physics + _this.body.emitter.emit("startSimulation"); + })(); + } else { + // move the network + if (this.options.dragView === true) { + // if the drag was not started properly because the click started outside the network div, start it now. + if (this.drag.pointer === undefined) { + this._handleDragStart(event); + return; } + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; - 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.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; + this.body.emitter.emit("_redraw"); } } }, writable: true, configurable: true }, - _checkOptions: { + onDragEnd: { /** - * 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 = {}; + * 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 { + this.body.emitter.emit("_requestRedraw"); } - return options; + this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); }, writable: true, configurable: true }, - _cluster: { + onPinch: { + + /** - * - * @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; - } + * Handle pinch event + * @param event + * @private + */ + value: function onPinch(event) { + var pointer = this.getPointer(event.center); - // check if we have an unique id; - if (options.clusterNodeProperties.id === undefined) { - options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); + this.drag.pinched = true; + if (this.pinch.scale === undefined) { + this.pinch.scale = 1; } - var clusterId = options.clusterNodeProperties.id; - // create the new edges that will connect to the cluster - var newEdges = []; - this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.scale; + this.zoom(scale, pointer); + }, + writable: true, + configurable: true + }, + zoom: { - // 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); + /** + * 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; } - - clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); - if (!clusterNodeProperties) { - throw new Error("The processClusterProperties function does not return properties!"); + if (scale > 10) { + scale = 10; } - } - 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); + var preScaleDragPointer = null; + if (this.drag !== undefined) { + if (this.drag.dragging === true) { + preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); + } } - clusterNodeProperties.y = pos.y; - clusterNodeProperties.allowedToMoveY = true; - } - - - // force the ID to remain the same - clusterNodeProperties.id = clusterId; - + // + this.canvas.frame.canvas.clientHeight / 2 + var translation = this.body.view.translation; - // create the clusterNode - var clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); - clusterNode.isCluster = true; - clusterNode.containedNodes = childNodesObj; - clusterNode.containedEdges = childEdgesObj; + 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 }; - // 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; - } + 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"); - // 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; + if (scaleOld < scale) { + this.body.emitter.emit("zoom", { direction: "+" }); + } else { + this.body.emitter.emit("zoom", { direction: "-" }); } } - - - // 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: { + onMouseWheel: { /** - * 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; + * 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; } - }, - 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; + // 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 = this.getPointer({ x: event.pageX, y: event.pageY }); + + // apply the new scale + this.zoom(scale, pointer); } - return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; + + // Prevent default actions caused by mouse wheel. + event.preventDefault(); }, writable: true, configurable: true }, - openCluster: { + onMouseMove: { /** - * 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; + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + value: function onMouseMove(event) { + var _this = this; + var pointer = { x: event.pageX, y: event.pageY }; + var popupVisible = false; - containedNode.options.hidden = false; - containedNode.togglePhysics(true); + // check if the previously selected node is still selected + if (this.popup !== undefined) { + if (this.popup.hidden === false) { + this._checkHidePopup(pointer); + } - delete this.clusteredNodes[nodeId]; + // 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(); } } - // release edges - for (var edgeId in containedEdges) { - if (containedEdges.hasOwnProperty(edgeId)) { - var edge = this.body.edges[edgeId]; - edge.options.hidden = false; - edge.togglePhysics(true); - } + // 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(); } - // 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]; + // start a timeout that will check if the mouse is positioned above an element + if (popupVisible === false) { + if (this.popupTimer !== undefined) { + clearInterval(this.popupTimer); // stop any running calculationTimer + this.popupTimer = undefined; + } + if (!this.drag.dragging) { + this.popupTimer = setTimeout(function () { + return _this._checkShowPopup(pointer); + }, this.options.tooltip.delay); } - // 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]; + /** + * 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]; + } + } - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); + // adding hover highlights + var obj = this.selectionHandler.getNodeAt(pointer); + if (obj == null) { + obj = this.selectionHandler.getEdgeAt(pointer); + } + if (obj != null) { + this.selectionHandler.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.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); + delete this.hoverObj.nodes[nodeId]; + } + } + } + this.body.emitter.emit("_requestRedraw"); } }, writable: true, configurable: true }, - _connectEdge: { + _checkShowPopup: { /** - * 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: { + * Check if there is an element on the given position in the network + * (a node or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {{x:Number, y:Number}} pointer + * @private + */ + value: function _checkShowPopup(pointer) { + var x = this.canvas._XconvertDOMtoCanvas(pointer.x); + var y = this.canvas._YconvertDOMtoCanvas(pointer.y); + var pointerObj = { + left: x, + top: y, + right: x, + bottom: y + }; - /** - * 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; + var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; + var nodeUnderCursor = false; + var popupType = "node"; + + // check if a node is under the cursor. + if (this.popupObj === undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodeIndices = this.body.nodeIndices; + var nodes = this.body.nodes; + var node = undefined; + var overlappingNodes = []; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + if (node.isOverlappingWith(pointerObj) === true) { + if (node.getTitle() !== undefined) { + overlappingNodes.push(nodeIndices[i]); + } + } + } + + if (overlappingNodes.length > 0) { + // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others + this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; + // if you hover over a node, the title of the edge is not supposed to be shown. + nodeUnderCursor = true; + } + } - while (this.clusteredNodes[nodeId] !== undefined && counter < max) { - stack.push(this.clusteredNodes[nodeId].node); - nodeId = this.clusteredNodes[nodeId].clusterId; - counter++; + if (this.popupObj === undefined && nodeUnderCursor == false) { + // search the edges for overlap + var edgeIndices = this.body.edgeIndices; + var edges = this.body.edges; + var edge = undefined; + var overlappingEdges = []; + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.isOverlappingWith(pointerObj) === true) { + if (edge.connected === true && edge.getTitle() !== undefined) { + overlappingEdges.push(edgeIndices[i]); + } + } + } + + if (overlappingEdges.length > 0) { + this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; + popupType = "edge"; + } } - stack.push(this.body.nodes[nodeId]); - return stack; - }, - writable: true, - configurable: true - }, - _getConnectedId: { + if (this.popupObj !== undefined) { + // show popup message window + if (this.popupObj.id != previousPopupObjId) { + if (this.popup === undefined) { + this.popup = new Popup(this.frame, this.options.tooltip); + } + + this.popup.popupTargetType = popupType; + this.popup.popupTargetId = this.popupObj.id; - /** - * 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; + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.setText(this.popupObj.getTitle()); + this.popup.show(); + } } else { - return edge.fromId; + if (this.popup) { + this.popup.hide(); + } } }, writable: true, configurable: true }, - _getHubSize: { + _checkHidePopup: { + /** - * 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; + * Check if the popup must be hidden, which is the case when the mouse is no + * longer hovering on the object + * @param {{x:Number, y:Number}} pointer + * @private + */ + value: function _checkHidePopup(pointer) { + var x = this.canvas._XconvertDOMtoCanvas(pointer.x); + var y = this.canvas._YconvertDOMtoCanvas(pointer.y); + var pointerObj = { + left: x, + top: y, + right: x, + bottom: 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; + var stillOnObj = false; + if (this.popup.popupTargetType == "node") { + if (this.body.nodes[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); + + // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. + // we initially only check stillOnObj because this is much faster. + if (stillOnObj === true) { + var overNode = this.selectionHandler.getNodeAt(pointer); + stillOnObj = overNode.id == this.popup.popupTargetId; + } + } + } else { + if (this.selectionHandler.getNodeAt(pointer) === null) { + if (this.body.edges[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + } } - 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; + if (stillOnObj === false) { + this.popupObj = undefined; + this.popup.hide(); } - - return hubThreshold; }, writable: true, configurable: true } }); - return ClusterEngine; + return InteractionHandler; })(); - module.exports = ClusterEngine; + module.exports = InteractionHandler; /***/ }, /* 95 */ @@ -32178,353 +32777,281 @@ 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__(61)); - - /** - * - */ - 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. - */ - - if (typeof window !== "undefined") { - window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; - } - var util = __webpack_require__(1); + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + var keycharm = __webpack_require__(39); - - var CanvasRenderer = (function () { - function CanvasRenderer(body, canvas) { + var NavigationHandler = (function () { + function NavigationHandler(body, canvas) { var _this = this; - _classCallCheck(this, CanvasRenderer); + _classCallCheck(this, NavigationHandler); this.body = body; this.canvas = canvas; - this.redrawRequested = false; - this.renderTimer = false; - this.requiresTimeout = true; - this.renderingActive = false; - this.renderRequests = 0; - this.pixelRatio = undefined; - - // redefined in this._redraw - this.canvasTopLeft = { x: 0, y: 0 }; - this.canvasBottomRight = { x: 0, y: 0 }; + this.iconsCreated = false; + this.navigationHammers = []; + this.boundFunctions = {}; + this.touchTime = 0; + this.activated = false; - this.dragging = false; - this.body.emitter.on("dragStart", function () { - _this.dragging = true; + this.body.emitter.on("release", this._stopMovement.bind(this)); + this.body.emitter.on("activate", function () { + _this.activated = true;_this.configureKeyboardBindings(); }); - this.body.emitter.on("dragEnd", function () { - return _this.dragging = false; + this.body.emitter.on("deactivate", function () { + _this.activated = false;_this.configureKeyboardBindings(); }); - this.body.emitter.on("_redraw", function () { - if (_this.renderingActive === false) { - _this._redraw(); + this.body.emitter.on("destroy", function () { + if (_this.keycharm !== undefined) { + _this.keycharm.destroy(); } }); - 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(CanvasRenderer, null, { + _prototypeProperties(NavigationHandler, null, { setOptions: { value: function setOptions(options) { if (options !== undefined) { - util.deepExtend(this.options, options); + this.options = options; + this.create(); } }, 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 - } + create: { + value: function create() { + if (this.options.showNavigationIcons === true) { + if (this.iconsCreated === false) { + this.loadNavigationElements(); } - } else {} + } else if (this.iconsCreated === true) { + this.cleanNavigation(); + } + + this.configureKeyboardBindings(); }, 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(); + 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 = []; } - this._redraw(); + this._navigationReleaseOverload = function () {}; - if (this.requiresTimeout == false) { - // this schedules a new simulation step - this.startRendering(); + // clean up previous navigation items + if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { + this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); } - }, - 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(); + this.iconsCreated = false; }, writable: true, configurable: true }, - _requestRedraw: { + loadNavigationElements: { /** - * 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. + * 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 _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)); - } - } - }, - 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); + value: function loadNavigationElements() { + this.cleanNavigation(); - this.body.emitter.emit("beforeDrawing", ctx); + this.navigationDOM = {}; + var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; + var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; - // 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.navigationDOM.wrapper = document.createElement("div"); + this.canvas.frame.appendChild(this.navigationDOM.wrapper); - 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 }); + 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]]); - if (hidden === false) { - if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { - this._drawEdges(ctx); + 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])); } - } - if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { - this._drawNodes(ctx, hidden); + this.navigationHammers.push(hammer); } - if (this.controlNodesActive === true) { - this._drawControlNodes(ctx); + 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"); } - - //this.physics.nodesSolver._debug(ctx,"#F00F0F"); - - // restore original scaling and translation - ctx.restore(); - - if (hidden === true) { - ctx.clearRect(0, 0, w, h); + }, + 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]; } - - this.body.emitter.emit("afterDrawing", ctx); }, writable: true, configurable: true }, - _drawNodes: { - + _zoomExtent: { /** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @param {Boolean} [alwaysShow] + * this stops all movement induced by the navigation buttons + * * @private */ - value: function _drawNodes(ctx) { - var alwaysShow = arguments[1] === undefined ? false : arguments[1]; - 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); - //} - } - } - - // draw the selected nodes on top - for (var i = 0; i < selected.length; i++) { - node = nodes[selected[i]]; - node.draw(ctx); + 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(); } }, writable: true, configurable: true }, - _drawEdges: { - + _stopMovement: { /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx + * this stops all movement induced by the navigation buttons + * * @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 _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 }, - _drawControlNodes: { - - /** - * 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); - } + _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 }, - _determineBrowserMethod: { + configureKeyboardBindings: { + /** - * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because - * some implementations (safari and IE9) did not support requestAnimationFrame - * @private + * bind all keys using keycharm. */ - 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; - } + 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"); } - } else { - this.requiresTimeout = true; } }, writable: true, @@ -32532,13 +33059,13 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return CanvasRenderer; + return NavigationHandler; })(); - module.exports = CanvasRenderer; + module.exports = NavigationHandler; /***/ }, -/* 97 */ +/* 96 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32547,285 +33074,166 @@ 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); - /** - * 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 + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} [x] + * @param {Number} [y] + * @param {String} [text] + * @param {Object} [style] An object containing borderColor, + * backgroundColor, etc. */ - 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); - } - }, - 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); - } - - 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.frame.canvas = document.createElement("canvas"); - this.frame.canvas.style.position = "relative"; - this.frame.appendChild(this.frame.canvas); + var Popup = (function () { + function Popup(container, x, y, text, style) { + _classCallCheck(this, Popup); - 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); + if (container) { + this.container = container; + } else { + this.container = document.body; + } - this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - } + // x, y and text are optional, see if a style object was passed in their place + if (style === undefined) { + if (typeof x === "object") { + style = x; + x = undefined; + } else if (typeof text === "object") { + style = text; + text = undefined; + } else { + // for backwards compatibility, in case clients other than Network are creating Popup directly + style = { + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" + } + }; + } + } - // add the frame to the container element - this.body.container.appendChild(this.frame); + this.x = 0; + this.y = 0; + this.padding = 5; + this.hidden = false; - this.body.view.scale = 1; - this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; + if (x !== undefined && y !== undefined) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } - this._bindHammer(); - }, - writable: true, - configurable: true - }, - _bindHammer: { + // create the frame + this.frame = document.createElement("div"); + this.frame.className = "network-tooltip"; + this.frame.style.color = style.fontColor; + this.frame.style.backgroundColor = style.color.background; + this.frame.style.borderColor = style.color.border; + this.frame.style.fontSize = style.fontSize + "px"; + this.frame.style.fontFamily = style.fontFace; + this.container.appendChild(this.frame); + } + _prototypeProperties(Popup, null, { + setPosition: { /** - * This function binds hammer, it can be repeated over and over due to the uniqueness check. - * @private + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window */ - value: function _bindHammer() { - if (this.hammer !== undefined) { - this.hammer.destroy(); - } - 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); + value: function setPosition(x, y) { + this.x = parseInt(x); + this.y = parseInt(y); }, writable: true, configurable: true }, - setSize: { - + setText: { /** - * 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%') + * Set the content for the popup window. This can be HTML code or text. + * @param {string | Element} content */ - 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; - - this.options.width = width; - this.options.height = height; - - emitEvent = true; + value: function setText(content) { + if (content instanceof Element) { + this.frame.innerHTML = ""; + this.frame.appendChild(content); } else { - // 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 }); + this.frame.innerHTML = content; // string containing text or HTML } }, writable: true, configurable: true }, - _XconvertDOMtoCanvas: { - - - /** - * 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 _XconvertDOMtoCanvas(x) { - return (x - this.body.view.translation.x) / this.body.view.scale; - }, - writable: true, - configurable: true - }, - _XconvertCanvasToDOM: { + show: { /** - * 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 + * Show the popup window + * @param {boolean} show Optional. Show or hide the window */ - value: function _XconvertCanvasToDOM(x) { - return x * this.body.view.scale + this.body.view.translation.x; - }, - writable: true, - configurable: true - }, - _YconvertDOMtoCanvas: { + value: function show(show) { + if (show === undefined) { + show = true; + } - /** - * 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 _YconvertDOMtoCanvas(y) { - return (y - this.body.view.translation.y) / this.body.view.scale; - }, - writable: true, - configurable: true - }, - _YconvertCanvasToDOM: { + if (show === true) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; - /** - * 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 _YconvertCanvasToDOM(y) { - return y * this.body.view.scale + this.body.view.translation.y; - }, - writable: true, - configurable: true - }, - canvasToDOM: { + var top = this.y - height; + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; + } + if (left < this.padding) { + left = this.padding; + } - /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor - */ - value: function canvasToDOM(pos) { - return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + this.hidden = false; + } else { + this.hide(); + } }, writable: true, configurable: true }, - DOMtoCanvas: { + hide: { /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor + * Hide the popup window */ - value: function DOMtoCanvas(pos) { - return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; + value: function hide() { + this.hidden = true; + this.frame.style.visibility = "hidden"; }, writable: true, configurable: true } }); - return Canvas; + return Popup; })(); - module.exports = Canvas; + module.exports = Popup; /***/ }, -/* 98 */ +/* 97 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32835,1123 +33243,754 @@ 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 26-Feb-15. + * Created by Alex on 2/27/2015. */ + var Node = __webpack_require__(61); var util = __webpack_require__(1); - var View = (function () { - function View(body, canvas) { + var SelectionHandler = (function () { + function SelectionHandler(body, canvas) { var _this = this; - _classCallCheck(this, View); + _classCallCheck(this, SelectionHandler); this.body = body; this.canvas = canvas; + this.selectionObj = { nodes: [], edges: [] }; - 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.options = {}; + this.defaultOptions = { + select: true, + selectConnectedEdges: true + }; + util.extend(this.options, this.defaultOptions); - this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); - this.body.emitter.on("animationFinished", function () { - _this.body.emitter.emit("_stopRendering"); + this.body.emitter.on("_dataChanged", function () { + _this.updateSelection(); }); - 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; - }, - writable: true, - configurable: true - }, - _getRange: { - - - // zoomExtent - /** - * Find the center position of the network - * @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.shape.boundingBox.left) { - minX = node.shape.boundingBox.left; - } - if (maxX < node.shape.boundingBox.right) { - maxX = node.shape.boundingBox.right; - } - if (minY > node.shape.boundingBox.bottom) { - minY = node.shape.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.shape.boundingBox.top) { - maxY = node.shape.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.shape.boundingBox.left) { - minX = node.shape.boundingBox.left; - } - if (maxX < node.shape.boundingBox.right) { - maxX = node.shape.boundingBox.right; - } - if (minY > node.shape.boundingBox.bottom) { - minY = node.shape.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.shape.boundingBox.top) { - maxY = node.shape.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; + + _prototypeProperties(SelectionHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); } - return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; }, writable: true, configurable: true }, - _findCenter: { + selectOnPoint: { + /** - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - * @returns {{x: number, y: number}} + * handles the selection part of the tap; + * + * @param {Object} pointer * @private */ - value: function _findCenter(range) { - return { x: 0.5 * (range.maxX + range.minX), - y: 0.5 * (range.maxY + range.minY) }; + 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 }, - zoomExtent: { - - - /** - * 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; + selectAdditionalOnPoint: { + value: function selectAdditionalOnPoint(pointer) { + var selectionChanged = false; + if (this.options.select === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - 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 (obj !== undefined) { + selectionChanged = true; + if (obj.isSelected() === true) { + this.deselectObject(obj); + } else { + this.selectObject(obj); } + + this.body.emitter.emit("_requestRedraw"); } - if (positionDefined > 0.5 * this.body.nodeIndices.length) { - this.zoomExtent(options, false); - return; + } + 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); + } } - - 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; + obj.select(); + this._addToSelection(obj); + return true; } - - if (zoomLevel > 1) { - zoomLevel = 1; + return false; + }, + writable: true, + configurable: true + }, + deselectObject: { + value: function deselectObject(obj) { + if (obj.isSelected() === true) { + obj.selected = false; + this._removeFromSelection(obj); } - - var center = this._findCenter(range); - var animationOptions = { position: center, scale: zoomLevel, animation: options }; - this.moveTo(animationOptions); }, writable: true, configurable: true }, - focusOnNode: { + _getAllNodesOverlappingWith: { + - // animation /** - * Center a node in view. - * - * @param {Number} nodeId - * @param {Number} [options] + * 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 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 _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; }, writable: true, configurable: true }, - moveTo: { + _pointerToPositionObject: { + /** + * Return a position object in canvasspace from a single point in screenspace * - * @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 + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private */ - 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.animateView(options); + 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 }, - animateView: { + getNodeAt: { + /** + * Get the top node at the a specific point (like a click) * - * @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 + * @param {{x: Number, y: Number}} pointer + * @return {Node | undefined} node + * @private */ - 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 getNodeAt(pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - // 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. + // 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 + }, + _getEdgesOverlappingWith: { - this.sourceScale = this.body.view.scale; - this.sourceTranslation = this.body.view.translation; - this.targetScale = options.scale; - - // 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 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"); + /** + * 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 _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); } - } 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.viewFunction = this._transitionRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); } }, writable: true, configurable: true }, - _lockedRedraw: { + _getAllEdgesOverlappingWith: { + /** - * used to animate smoothly by hijacking the redraw function. + * 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 _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 - }; - - this.body.view.translation = targetTranslation; + value: function _getAllEdgesOverlappingWith(object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; }, 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; + getEdgeAt: { + + + /** + * 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); + + if (overlappingEdges.length > 0) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } else { + return undefined; } }, writable: true, configurable: true }, - _transitionRedraw: { + _addToSelection: { + /** + * Add object to the selection array. * - * @param easingTime + * @param obj * @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); - } - this.body.emitter.emit("animationFinished"); + value: function _addToSelection(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; } }, writable: true, configurable: true - } - }); - - return View; - })(); - - module.exports = View; - -/***/ }, -/* 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 _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - /** - * Created by Alex on 2/27/2015. - * - */ - - var util = __webpack_require__(1); - - var NavigationHandler = _interopRequire(__webpack_require__(102)); - - var Popup = _interopRequire(__webpack_require__(103)); - - var InteractionHandler = (function () { - function InteractionHandler(body, canvas, selectionHandler) { - _classCallCheck(this, InteractionHandler); - - this.body = body; - 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.popup = undefined; - this.popupObj = undefined; - this.popupTimer = undefined; - + }, + _addToHover: { - 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" + /** + * 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; } }, - keyboard: { - enabled: false, - speed: { x: 10, y: 10, zoom: 0.02 }, - bindToWindow: true - } - }; - util.extend(this.options, this.defaultOptions); - - this.body.emitter.on("_dataChanged", function () {}); - } + writable: true, + configurable: true + }, + _removeFromSelection: { - _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); - // merge the keyboard options in. - util.mergeOptions(this.options, options, "keyboard"); + /** + * 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 { + delete this.selectionObj.edges[obj.id]; } - - this.navigationHandler.setOptions(this.options); }, writable: true, configurable: true }, - getPointer: { - + unselectAll: { /** - * Get the pointer location from a touch location - * @param {{x: Number, y: Number}} touch - * @return {{x: Number, y: Number}} pointer + * Unselect all. The selectionObj is useful for this. + * * @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 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(); + } + } + + this.selectionObj = { nodes: {}, edges: {} }; }, writable: true, configurable: true }, - onTouch: { + _getSelectedNodeCount: { /** - * On start of a touch gesture, store the pointer - * @param event + * return the number of selected nodes + * + * @returns {number} * @private */ - value: function onTouch(event) { - if (new Date().valueOf() - this.touchTime > 100) { - this.drag.pointer = this.getPointer(event.center); - this.drag.pinched = false; - this.pinch.scale = this.body.view.scale; - - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); + 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 }, - onTap: { + _getSelectedNode: { /** - * handle tap/click event: select/unselect a node + * return the selected node + * + * @returns {number} * @private */ - value: function onTap(event) { - var pointer = this.getPointer(event.center); - - var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; - var selected = this.selectionHandler.selectOnPoint(pointer); - - if (selected === true || previouslySelected == true && selected === false) { - // select or unselect - this.body.emitter.emit("select", this.selectionHandler.getSelection()); + value: function _getSelectedNode() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } } - - this.selectionHandler._generateClickEvent("click", pointer); + return undefined; }, writable: true, configurable: true }, - onDoubleTap: { - + _getSelectedEdge: { /** - * handle doubletap event + * return the selected edge + * + * @returns {number} * @private */ - value: function onDoubleTap(event) { - var pointer = this.getPointer(event.center); - this.selectionHandler._generateClickEvent("doubleClick", pointer); + 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 }, - onHold: { - + _getSelectedEdgeCount: { /** - * handle long tap event: multi select nodes + * return the number of selected edges + * + * @returns {number} * @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 _getSelectedEdgeCount() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } } - - this.selectionHandler._generateClickEvent("click", pointer); + return count; }, writable: true, configurable: true }, - onRelease: { + _getSelectedObjectCount: { /** - * handle the release of the screen + * return the number of selected objects. * + * @returns {number} * @private */ - value: function onRelease(event) { - this.body.emitter.emit("release", event); + 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 }, - onDragStart: { - + _selectionIsEmpty: { /** - * This function is called by onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. + * Check if anything is selected * + * @returns {boolean} * @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); - } - - // 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 = []; - this.drag.translation = util.extend({}, this.body.view.translation); // copy the object - this.drag.nodeId = null; - - this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); - - if (node != null && this.options.dragNodes === true) { - this.drag.nodeId = node.id; - // select the clicked node if not yet selected - if (node.isSelected() === false) { - this.selectionHandler.unselectAll(); - this.selectionHandler.selectObject(node); + value: function _selectionIsEmpty() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; } - - 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 - }; - - object.options.fixed.x = true; - object.options.fixed.y = true; - - this.drag.selection.push(s); - } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; } } + return true; }, writable: true, configurable: true }, - onDrag: { + _clusterInSelection: { /** - * handle drag event + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} * @private */ - value: function onDrag(event) { - var _this = this; - if (this.drag.pinched === true) { - return; - } - - // remove the focus on node if it is focussed on by the focusOnNode - this.body.emitter.emit("unlockNode"); - - var pointer = this.getPointer(event.center); - var selection = this.drag.selection; - if (selection && selection.length && this.options.dragNodes === true) { - (function () { - // calculate delta's and new location - var deltaX = pointer.x - _this.drag.pointer.x; - var deltaY = pointer.y - _this.drag.pointer.y; - - // update position of all selected nodes - selection.forEach(function (selection) { - var node = selection.node; - // 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); - } - }); - - - // 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; + 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; } - 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"); } } + return false; }, writable: true, configurable: true }, - onDragEnd: { - + _selectConnectedEdges: { /** - * handle drag start event + * select the edges connected to the node that is being selected + * + * @param {Node} node * @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 { - this.body.emitter.emit("_requestRedraw"); + value: function _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); } - - this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); }, writable: true, configurable: true }, - onPinch: { - - + _hoverConnectedEdges: { /** - * Handle pinch event - * @param event + * select the edges connected to the node that is being selected + * + * @param {Node} node * @private */ - 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 _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); } - - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.scale; - this.zoom(scale, pointer); }, writable: true, configurable: true }, - zoom: { + _unselectConnectedEdges: { /** - * 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 + * unselect the edges connected to the node that is being selected + * + * @param {Node} node * @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; - } + 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: { - 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: "-" }); - } + /** + * 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 }, - onMouseWheel: { - + hoverObject: { /** - * 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 + * 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 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 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); } + }, + writable: true, + configurable: true + }, + getSelection: { - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - // calculate the new scale - var scale = this.body.view.scale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= 1 + zoom; - // calculate the pointer location - var pointer = { x: event.pageX, y: event.pageY }; - // apply the new scale - this.zoom(scale, pointer); - } - // Prevent default actions caused by mouse wheel. - event.preventDefault(); + /** + * + * 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 }, - onMouseMove: { - + getSelectedNodes: { /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. */ - value: function onMouseMove(event) { - var _this = this; - 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) { - if (this.popupTimer !== undefined) { - clearInterval(this.popupTimer); // stop any running calculationTimer - this.popupTimer = undefined; - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(function () { - return _this._checkShowPopup(pointer); - }, 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]; + 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); } } - - // adding hover highlights - var obj = this.selectionHandler.getNodeAt(pointer); - if (obj == null) { - obj = this.selectionHandler.getEdgeAt(pointer); - } - if (obj != null) { - this.selectionHandler.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.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } + } + return idArray; + }, + writable: true, + configurable: true + }, + getSelectedEdges: { + + /** + * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + 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); } } - this.body.emitter.emit("_requestRedraw"); } + return idArray; }, writable: true, configurable: true }, - _checkShowPopup: { - + selectNodes: { /** - * Check if there is an element on the given position in the network - * (a node or edge). If so, and if this element has a title, - * show a popup window with its title. - * - * @param {{x:Number, y:Number}} pointer - * @private + * 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 _checkShowPopup(pointer) { - var x = this.canvas._XconvertDOMtoCanvas(pointer.x); - var y = this.canvas._YconvertDOMtoCanvas(pointer.y); - var pointerObj = { - left: x, - top: y, - right: x, - bottom: y - }; + value: function selectNodes(selection, highlightEdges) { + var i, iMax, id; - var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; - var nodeUnderCursor = false; - var popupType = "node"; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - // check if a node is under the cursor. - if (this.popupObj === undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodeIndices = this.body.nodeIndices; - var nodes = this.body.nodes; - var node = undefined; - var overlappingNodes = []; - for (var i = 0; i < nodeIndices.length; i++) { - node = nodes[nodeIndices[i]]; - if (node.isOverlappingWith(pointerObj) === true) { - if (node.getTitle() !== undefined) { - overlappingNodes.push(nodeIndices[i]); - } - } - } + // first unselect any selected node + this.unselectAll(true); - if (overlappingNodes.length > 0) { - // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others - this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; - // if you hover over a node, the title of the edge is not supposed to be shown. - nodeUnderCursor = 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"); } + this._selectObject(node, true, true, highlightEdges, true); } + this.redraw(); + }, + writable: true, + configurable: true + }, + selectEdges: { - if (this.popupObj === undefined && nodeUnderCursor == false) { - // search the edges for overlap - var edgeIndices = this.body.edgeIndices; - var edges = this.body.edges; - var edge = undefined; - var overlappingEdges = []; - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.isOverlappingWith(pointerObj) === true) { - if (edge.connected === true && edge.getTitle() !== undefined) { - overlappingEdges.push(edgeIndices[i]); - } - } - } - if (overlappingEdges.length > 0) { - this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; - popupType = "edge"; - } - } + /** + * 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; - if (this.popupObj !== undefined) { - // show popup message window - if (this.popupObj.id != previousPopupObjId) { - if (this.popup === undefined) { - this.popup = new Popup(this.frame, this.options.tooltip); - } + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - this.popup.popupTargetType = popupType; - this.popup.popupTargetId = this.popupObj.id; + // first unselect any selected node + this.unselectAll(true); - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.setText(this.popupObj.getTitle()); - this.popup.show(); - } - } else { - if (this.popup) { - this.popup.hide(); + 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 }, - _checkHidePopup: { - + updateSelection: { /** - * Check if the popup must be hidden, which is the case when the mouse is no - * longer hovering on the object - * @param {{x:Number, y:Number}} pointer + * Validate the selection: remove ids of nodes which no longer exist * @private */ - value: function _checkHidePopup(pointer) { - var x = this.canvas._XconvertDOMtoCanvas(pointer.x); - var y = this.canvas._YconvertDOMtoCanvas(pointer.y); - var pointerObj = { - left: x, - top: y, - right: x, - bottom: y - }; - - var stillOnObj = false; - if (this.popup.popupTargetType == "node") { - if (this.body.nodes[this.popup.popupTargetId] !== undefined) { - stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); - - // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. - // we initially only check stillOnObj because this is much faster. - if (stillOnObj === true) { - var overNode = this.selectionHandler.getNodeAt(pointer); - stillOnObj = overNode.id == this.popup.popupTargetId; + 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]; } } - } else { - if (this.selectionHandler.getNodeAt(pointer) === null) { - if (this.body.edges[this.popup.popupTargetId] !== undefined) { - stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + } + 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 (stillOnObj === false) { - this.popupObj = undefined; - this.popup.hide(); - } }, writable: true, configurable: true } }); - return InteractionHandler; + return SelectionHandler; })(); - module.exports = InteractionHandler; + module.exports = SelectionHandler; /***/ }, -/* 100 */ +/* 98 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -33961,751 +34000,775 @@ 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 Node = __webpack_require__(61); var util = __webpack_require__(1); - var SelectionHandler = (function () { - function SelectionHandler(body, canvas) { + var LayoutEngine = (function () { + function LayoutEngine(body) { var _this = this; - _classCallCheck(this, SelectionHandler); + _classCallCheck(this, LayoutEngine); this.body = body; - this.canvas = canvas; - this.selectionObj = { nodes: [], edges: [] }; this.options = {}; this.defaultOptions = { - select: true, - selectConnectedEdges: true + hierarchical: { + enabled: false, + levelSeparation: 150, + direction: "UD", // UD, DU, LR, RL + sortMethod: "hubsize" // hubsize, directed + } }; util.extend(this.options, this.defaultOptions); + this.hierarchicalLevels = {}; + this.body.emitter.on("_dataChanged", function () { - _this.updateSelection(); + _this.setupHierarchicalLayout(); }); } - _prototypeProperties(SelectionHandler, null, { + _prototypeProperties(LayoutEngine, null, { setOptions: { - value: function setOptions(options) { + value: function setOptions(options, allOptions) { if (options !== undefined) { - util.deepExtend(this.options, options); - } - }, - writable: true, - configurable: true - }, - 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); - } - 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);; - - if (obj !== undefined) { - selectionChanged = true; - if (obj.isSelected() === true) { - this.deselectObject(obj); + util.mergeOptions(this.options, options, "hierarchical"); + if (this.options.hierarchical.enabled === true) { + // make sure the level seperation is the right way up + if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "DU") { + if (this.options.hierarchical.levelSeparation > 0) { + this.options.hierarchical.levelSeparation *= -1; + } } else { - this.selectObject(obj); + if (this.options.hierarchical.levelSeparation < 0) { + this.options.hierarchical.levelSeparation *= -1; + } } - this.body.emitter.emit("_requestRedraw"); + // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. + return this.adaptAllOptions(allOptions); } } - 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); + return allOptions; }, writable: true, configurable: true }, - selectObject: { - value: function selectObject(obj) { - if (obj !== undefined) { - if (obj instanceof Node) { - if (this.options.selectConnectedEdges === true) { - this._selectConnectedEdges(obj); - } + adaptAllOptions: { + value: function adaptAllOptions(allOptions) { + if (this.options.hierarchical.enabled === true) { + // set the physics + if (allOptions.physics === undefined || allOptions.physics === true) { + allOptions.physics = { solver: "hierarchicalRepulsion" }; + } else if (options.physics !== false) { + allOptions.physics.solver = "hierarchicalRepulsion"; } - 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); - } - }, - writable: true, - configurable: true - }, - _getAllNodesOverlappingWith: { - - - /** - * 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 _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); + // get the type of static smooth curve in case it is required + var type = "horizontal"; + if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "LR") { + type = "vertical"; } - } - return overlappingNodes; - }, - writable: true, - configurable: true - }, - _pointerToPositionObject: { - - - /** - * 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 _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 - }, - getNodeAt: { + // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. + if (allOptions.edges === undefined) { + allOptions.edges = { smooth: false }; + } else if (allOptions.edges.smooth === undefined) { + allOptions.edges.smooth = false; + } else { + allOptions.edges.smooth = { enabled: true, dynamic: false, type: type }; + } - /** - * 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 getNodeAt(pointer) { - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } else { - return undefined; + // force all edges into static smooth curves. + this.body.emitter.emit("_forceDisableDynamicCurves", type); } + return allOptions; }, writable: true, configurable: true }, - _getEdgesOverlappingWith: { - - - /** - * 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 _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); + positionInitially: { + value: function positionInitially(nodesArray) { + if (this.options.hierarchical.enabled !== true) { + for (var i = 0; i < nodesArray.length; i++) { + var node = nodesArray[i]; + if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { + 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 }, - _getAllEdgesOverlappingWith: { - - - /** - * 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: { - + setupHierarchicalLayout: { /** - * 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 is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly * - * @param pointer - * @returns {undefined} * @private */ - value: function getEdgeAt(pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + value: function setupHierarchicalLayout() { + if (this.options.hierarchical.enabled == true && this.body.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 = undefined, + nodeId = undefined; + var definedLevel = false; + var undefinedLevel = false; + this.hierarchicalLevels = {}; + this.nodeSpacing = 100; - if (overlappingEdges.length > 0) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; - } else { - return undefined; - } - }, - writable: true, - configurable: true - }, - _addToSelection: { + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.options.level !== undefined) { + definedLevel = true; + this.hierarchicalLevels[nodeId] = node.options.level; + } else { + undefinedLevel = true; + } + } + } + // 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."); + return; + } else { + // setup the system to use hierarchical method. + //this._changeConstants(); - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - value: function _addToSelection(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } else { - this.selectionObj.edges[obj.id] = obj; + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + if (this.options.hierarchical.sortMethod == "hubsize") { + this._determineLevelsByHubsize(); + } else if (this.options.hierarchical.sortMethod == "directed" || "direction") { + this._determineLevelsDirected(); + } + } + console.log(this.hierarchicalLevels); + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); + + // place the nodes on the canvas. + this._placeNodesByHierarchy(distribution); + } } }, writable: true, configurable: true }, - _addToHover: { + _placeNodesByHierarchy: { /** - * Add object to the selection array. + * This function places the nodes on the canvas based on the hierarchial distribution. * - * @param obj + * @param {Object} distribution | obtained by the function this._getDistribution() * @private */ - 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: { + value: function _placeNodesByHierarchy(distribution) { + var nodeId = undefined, + node = undefined; + this.positionedNodes = {}; + // 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.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + if (node.x === undefined) { + node.x = distribution[level].distance; + } + distribution[level].distance = node.x + this.nodeSpacing; + } else { + if (node.y === undefined) { + node.y = distribution[level].distance; + } + distribution[level].distance = node.y + this.nodeSpacing; + } - /** - * 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 { - delete this.selectionObj.edges[obj.id]; + this.positionedNodes[nodeId] = true; + this._placeBranchNodes(node.edges, node.id, distribution, level); + } + } + } } }, writable: true, configurable: true }, - unselectAll: { + _getDistribution: { + /** - * Unselect all. The selectionObj is useful for this. + * This function get the distribution of levels based on hubsize * + * @returns {Object} * @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 _getDistribution() { + var distribution = {}; + var nodeId = undefined, + node = undefined; + + // 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]; + if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + node.y = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; + node.options.fixed.y = true; + } else { + node.x = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; + node.options.fixed.x = true; + } + if (distribution[this.hierarchicalLevels[nodeId]] === undefined) { + distribution[this.hierarchicalLevels[nodeId]] = { amount: 0, nodes: {}, distance: 0 }; + } + distribution[this.hierarchicalLevels[nodeId]].amount += 1; + distribution[this.hierarchicalLevels[nodeId]].nodes[nodeId] = node; } } - - this.selectionObj = { nodes: {}, edges: {} }; + return distribution; }, writable: true, configurable: true }, - _getSelectedNodeCount: { + _getHubSize: { /** - * return the number of selected nodes + * Get the hubsize from all remaining unlevelled 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; + value: function _getHubSize() { + var hubSize = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (this.hierarchicalLevels[nodeId] === undefined) { + hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; + } } } - return count; + return hubSize; }, writable: true, configurable: true }, - _getSelectedNode: { + _determineLevelsByHubsize: { + /** - * return the selected node + * this function allocates nodes in levels based on the recursive branching from the largest hubs. * - * @returns {number} + * @param hubsize * @private */ - value: function _getSelectedNode() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; + value: function _determineLevelsByHubsize() { + var nodeId = undefined, + node = undefined; + var hubSize = 1; + + while (hubSize > 0) { + // determine hubs + hubSize = this._getHubSize(); + if (hubSize == 0) break; + + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.edges.length == hubSize) { + this._setLevel(0, node); + } + } } } - return undefined; }, writable: true, configurable: true }, - _getSelectedEdge: { + _setLevel: { + /** - * return the selected edge + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. * - * @returns {number} + * @param level + * @param edges + * @param parentId * @private */ - value: function _getSelectedEdge() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; + value: function _setLevel(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) { + return; + }var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId == node.id) { + childNode = node.edges[i].from; + } else { + childNode = node.edges[i].to; } + this._setLevel(level + 1, childNode); } - return undefined; }, writable: true, configurable: true }, - _getSelectedEdgeCount: { + _determineLevelsDirected: { + /** - * return the number of selected edges + * this function allocates nodes in levels based on the direction of the edges * - * @returns {number} + * @param hubsize * @private */ - value: function _getSelectedEdgeCount() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; + value: function _determineLevelsDirected() { + var nodeId = undefined, + node = undefined; + var minLevel = 10000; + + // set first node to source + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + this._setLevelDirected(minLevel, node); } } - return count; - }, - writable: true, - configurable: true - }, - _getSelectedObjectCount: { - - /** - * 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; + // get the minimum level + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; } } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; + + // 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)) { + this.hierarchicalLevels[nodeId] -= minLevel; } } - return count; }, writable: true, configurable: true }, - _selectionIsEmpty: { + _setLevelDirected: { + /** - * Check if anything is selected + * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction * - * @returns {boolean} + * @param level + * @param edges + * @param parentId * @private */ - 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; + value: function _setLevelDirected(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) { + return; + }var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId == node.id) { + childNode = node.edges[i].from; + this._setLevelDirected(level - 1, childNode); + } else { + childNode = node.edges[i].to; + this._setLevelDirected(level + 1, childNode); } } - return true; }, writable: true, configurable: true }, - _clusterInSelection: { + _placeBranchNodes: { + /** - * check if one of the selected nodes is a cluster. + * 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. * - * @returns {boolean} + * @param edges + * @param parentId + * @param distribution + * @param parentLevel * @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; + value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = undefined; + var parentNode = undefined; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + parentNode = edges[i].to; + } else { + childNode = edges[i].to; + parentNode = edges[i].from; + } + var childNodeLevel = this.hierarchicalLevels[childNode.id]; + if (this.positionedNodes[childNode.id] === undefined) { + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + if (childNodeLevel > parentLevel) { + if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + if (childNode.x === undefined) { + childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); + } + distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; + this.positionedNodes[childNode.id] = true; + } else { + if (childNode.y === undefined) { + childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); + } + distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; + } + this.positionedNodes[childNode.id] = true; + + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); + } } } } - return false; }, 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); - } + return LayoutEngine; + })(); + + module.exports = LayoutEngine; + +/***/ }, +/* 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 BezierEdgeBase = _interopRequire(__webpack_require__(100)); + + var BezierEdgeDynamic = (function (BezierEdgeBase) { + 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, BezierEdgeBase); + + _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 }, - _hoverConnectedEdges: { - - /** - * 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); + 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 }, - _unselectConnectedEdges: { - + setupSupportNode: { /** - * unselect the edges connected to the node that is being selected + * 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 {Node} node + * The changed data is not called, if needed, it is returned by the main edge constructor. * @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 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 }, - blurObject: { - - - - - - - /** - * 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 }); + 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 }, - hoverObject: { + _line: { /** - * 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 + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx * @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); - } + 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 }, - getSelection: { - - + getPoint: { /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection + * 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 getSelection() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return { nodes: nodeIds, edges: edgeIds }; + 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; + + return { x: x, y: y }; }, writable: true, configurable: true }, - getSelectedNodes: { - - /** - * - * 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); - } - } - } - return idArray; + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + return this._findBorderPositionBezier(nearNode, ctx, this.via); }, writable: true, configurable: true }, - getSelectedEdges: { - - /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. - */ - 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; + _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 - }, - selectNodes: { + } + }); + return BezierEdgeDynamic; + })(BezierEdgeBase); - /** - * 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; + module.exports = BezierEdgeDynamic; - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; +/***/ }, +/* 100 */ +/***/ function(module, exports, __webpack_require__) { - // first unselect any selected node - this.unselectAll(true); + "use strict"; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - 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 - }, - selectEdges: { + 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); } }; - /** - * 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; + 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; }; - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // first unselect any selected node - this.unselectAll(true); + /** + * Created by Alex on 3/20/2015. + */ - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + var EdgeBase = _interopRequire(__webpack_require__(101)); - var edge = this.body.edges[id]; - if (!edge) { - throw new RangeError("Edge with id \"" + id + "\" not found"); + var BezierEdgeBase = (function (EdgeBase) { + function BezierEdgeBase(options, body, labelModule) { + _classCallCheck(this, BezierEdgeBase); + + _get(Object.getPrototypeOf(BezierEdgeBase.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(BezierEdgeBase, EdgeBase); + + _prototypeProperties(BezierEdgeBase, null, { + _findBorderPositionBezier: { + + /** + * This function uses binary search to look for the point where the bezier curve crosses the border of the node. + * + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode + */ + value: function _findBorderPositionBezier(nearNode, ctx) { + var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; + 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; + } } - this._selectObject(edge, true, true, false, true); + + iteration++; } - this.redraw(); + pos.t = middle; + + return pos; }, writable: true, configurable: true }, - updateSelection: { + _getDistanceToBezierEdge: { + + /** - * Validate the selection: remove ids of nodes which no longer exist + * 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 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]; - } + 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 minDistance; }, writable: true, configurable: true } }); - return SelectionHandler; - })(); + return BezierEdgeBase; + })(EdgeBase); - module.exports = SelectionHandler; + module.exports = BezierEdgeBase; /***/ }, /* 101 */ @@ -34718,456 +34781,464 @@ 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 3/3/2015. + * Created by Alex on 3/20/2015. */ - var util = __webpack_require__(1); - var LayoutEngine = (function () { - function LayoutEngine(body) { - var _this = this; - _classCallCheck(this, LayoutEngine); + var EdgeBase = (function () { + function EdgeBase(options, body, labelModule) { + _classCallCheck(this, EdgeBase); this.body = body; - - this.options = {}; - this.defaultOptions = { - hierarchical: { - enabled: false, - levelSeparation: 150, - direction: "UD", // UD, DU, LR, RL - sortMethod: "hubsize" // hubsize, directed - } - }; - util.extend(this.options, this.defaultOptions); - - this.hierarchicalLevels = {}; - - this.body.emitter.on("_dataChanged", function () { - _this.setupHierarchicalLayout(); - }); + this.labelModule = labelModule; + this.setOptions(options); + this.colorDirty = true; } - _prototypeProperties(LayoutEngine, null, { + _prototypeProperties(EdgeBase, null, { setOptions: { - value: function setOptions(options, allOptions) { - if (options !== undefined) { - util.mergeOptions(this.options, options, "hierarchical"); - if (this.options.hierarchical.enabled === true) { - // make sure the level seperation is the right way up - if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "DU") { - if (this.options.hierarchical.levelSeparation > 0) { - this.options.hierarchical.levelSeparation *= -1; - } - } else { - if (this.options.hierarchical.levelSeparation < 0) { - this.options.hierarchical.levelSeparation *= -1; - } - } - - // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. - return this.adaptAllOptions(allOptions); - } - } - return allOptions; + 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 }, - adaptAllOptions: { - value: function adaptAllOptions(allOptions) { - if (this.options.hierarchical.enabled === true) { - // set the physics - if (allOptions.physics === undefined || allOptions.physics === true) { - allOptions.physics = { solver: "hierarchicalRepulsion" }; - } else if (options.physics !== false) { - allOptions.physics.solver = "hierarchicalRepulsion"; - } + drawLine: { - // get the type of static smooth curve in case it is required - var type = "horizontal"; - if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "LR") { - type = "vertical"; + /** + * 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 drawLine(ctx, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx); + ctx.lineWidth = this.getLineWidth(selected, hover); + 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); } - - // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. - if (allOptions.edges === undefined) { - allOptions.edges = { smooth: false }; - } else if (allOptions.edges.smooth === undefined) { - allOptions.edges.smooth = 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 { - allOptions.edges.smooth = { enabled: true, dynamic: false, type: type }; + x = node.x + radius; + y = node.y - node.shape.height * 0.5; } - - // force all edges into static smooth curves. - this.body.emitter.emit("_forceDisableDynamicCurves", type); + this._circle(ctx, x, y, radius); } - return allOptions; + + return via; }, writable: true, configurable: true }, - positionInitially: { - value: function positionInitially(nodesArray) { - if (this.options.hierarchical.enabled !== true) { - for (var i = 0; i < nodesArray.length; i++) { - var node = nodesArray[i]; - if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { - 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); - } - } + _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]; } - } - }, - writable: true, - configurable: true - }, - setupHierarchicalLayout: { - - /** - * 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.options.hierarchical.enabled == true && this.body.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 = undefined, - nodeId = undefined; - var definedLevel = false; - var undefinedLevel = false; - this.hierarchicalLevels = {}; - this.nodeSpacing = 100; - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.options.level !== undefined) { - definedLevel = true; - this.hierarchicalLevels[nodeId] = node.options.level; - } else { - undefinedLevel = true; - } - } - } + // set dash settings for chrome or firefox + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; - // 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."); - return; - } else { - // setup the system to use hierarchical method. - //this._changeConstants(); + // draw the line + via = this._line(ctx); - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - if (this.options.hierarchical.sortMethod == "hubsize") { - this._determineLevelsByHubsize(); - } else if (this.options.hierarchical.sortMethod == "directed" || "direction") { - this._determineLevelsDirected(); - } + // 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); } - console.log(this.hierarchicalLevels); - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); - - // place the nodes on the canvas. - this._placeNodesByHierarchy(distribution); - } + ctx.stroke(); } + return via; }, 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 = undefined, - node = undefined; - this.positionedNodes = {}; - - // 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.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - if (node.x === undefined) { - node.x = distribution[level].distance; - } - distribution[level].distance = node.x + this.nodeSpacing; - } else { - if (node.y === undefined) { - node.y = distribution[level].distance; - } - distribution[level].distance = node.y + this.nodeSpacing; - } - - this.positionedNodes[nodeId] = true; - this._placeBranchNodes(node.edges, node.id, distribution, level); - } - } - } + 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 }, - _getDistribution: { + _findBorderPositionCircle: { /** - * This function get the distribution of levels based on hubsize - * - * @returns {Object} + * 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 _getDistribution() { - var distribution = {}; - var nodeId = undefined, - node = undefined; + 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; - // 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]; - if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - node.y = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; - node.options.fixed.y = true; + 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 { - node.x = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; - node.options.fixed.x = true; + high = _middle; } - if (distribution[this.hierarchicalLevels[nodeId]] === undefined) { - distribution[this.hierarchicalLevels[nodeId]] = { amount: 0, nodes: {}, distance: 0 }; + } else { + if (direction > 0) { + high = _middle; + } else { + low = _middle; } - distribution[this.hierarchicalLevels[nodeId]].amount += 1; - distribution[this.hierarchicalLevels[nodeId]].nodes[nodeId] = node; } + iteration++; } - return distribution; + pos.t = middle; + + return pos; }, writable: true, configurable: true }, - _getHubSize: { - + getLineWidth: { /** - * Get the hubsize from all remaining unlevelled nodes. - * - * @returns {number} + * 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 _getHubSize() { - var hubSize = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (this.hierarchicalLevels[nodeId] === undefined) { - hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; - } + 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); } } - return hubSize; }, writable: true, configurable: true }, - _determineLevelsByHubsize: { + 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; - /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. - * - * @param hubsize - * @private - */ - value: function _determineLevelsByHubsize() { - var nodeId = undefined, - node = undefined; - var hubSize = 1; + 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); - while (hubSize > 0) { - // determine hubs - hubSize = this._getHubSize(); - if (hubSize == 0) break; + // -------------------- this returns -------------------- // + return grd; + } - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.edges.length == hubSize) { - this._setLevel(0, node); - } + 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); } } } + + // 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 }, - _setLevel: { - + _circle: { /** - * 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 + * 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 _setLevel(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) { - return; - }var childNode = undefined; - this.hierarchicalLevels[node.id] = level; - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId == node.id) { - childNode = node.edges[i].from; - } else { - childNode = node.edges[i].to; - } - this._setLevel(level + 1, childNode); - } + 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 }, - _determineLevelsDirected: { - + getDistanceToEdge: { /** - * this function allocates nodes in levels based on the direction of the edges - * - * @param hubsize + * 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 _determineLevelsDirected() { - var nodeId = undefined, - node = undefined; - var minLevel = 10000; - - // set first node to source - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - this._setLevelDirected(minLevel, node); - } - } - - // get the minimum level - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; + 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); } - // 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)) { - this.hierarchicalLevels[nodeId] -= minLevel; - } + 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; } }, writable: true, configurable: true }, - _setLevelDirected: { + _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; + } - /** - * 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, node) { - if (this.hierarchicalLevels[node.id] !== undefined) { - return; - }var childNode = undefined; - this.hierarchicalLevels[node.id] = level; + var x = x1 + u * px; + var y = y1 + u * py; + var dx = x - x3; + var dy = y - y3; - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId == node.id) { - childNode = node.edges[i].from; - this._setLevelDirected(level - 1, childNode); - } else { - childNode = node.edges[i].to; - this._setLevelDirected(level + 1, childNode); - } - } + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance + + return Math.sqrt(dx * dx + dy * dy); }, writable: true, configurable: true }, - _placeBranchNodes: { - - + drawArrowHead: { /** - * 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 + * @param ctx + * @param position + * @param viaNode */ - value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = undefined; - var parentNode = undefined; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - parentNode = edges[i].to; + value: function drawArrowHead(ctx, position, viaNode, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this.getLineWidth(selected, hover); + + // 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; + } + + // 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 { - childNode = edges[i].to; - parentNode = edges[i].from; + 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); } - var childNodeLevel = this.hierarchicalLevels[childNode.id]; - if (this.positionedNodes[childNode.id] === undefined) { - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - if (childNodeLevel > parentLevel) { - if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - if (childNode.x === undefined) { - childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); - } - distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; - this.positionedNodes[childNode.id] = true; - } else { - if (childNode.y === undefined) { - childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); - } - distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; - } - this.positionedNodes[childNode.id] = true; - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); - } - } + // 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, @@ -35175,10 +35246,10 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return LayoutEngine; + return EdgeBase; })(); - module.exports = LayoutEngine; + module.exports = EdgeBase; /***/ }, /* 102 */ @@ -35186,292 +35257,266 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var util = __webpack_require__(1); - var Hammer = __webpack_require__(19); - var hammerUtil = __webpack_require__(24); - var keycharm = __webpack_require__(39); + 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 NavigationHandler = (function () { - function NavigationHandler(body, canvas) { - var _this = this; - _classCallCheck(this, NavigationHandler); + 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; }; - this.body = body; - this.canvas = canvas; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - this.iconsCreated = false; - this.navigationHammers = []; - this.boundFunctions = {}; - this.touchTime = 0; - this.activated = false; + /** + * Created by Alex on 3/20/2015. + */ + var BezierEdgeBase = _interopRequire(__webpack_require__(100)); - 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(); - } - }); + var BezierEdgeStatic = (function (BezierEdgeBase) { + function BezierEdgeStatic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeStatic); - this.options = {}; + _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), "constructor", this).call(this, options, body, labelModule); } - _prototypeProperties(NavigationHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - this.options = options; - this.create(); - } - }, - 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(); - } - - this.configureKeyboardBindings(); - }, - 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 = []; - } - - 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); - } - - this.iconsCreated = false; - }, - writable: true, - configurable: true - }, - loadNavigationElements: { - - /** - * 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(); - - 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); - } + _inherits(BezierEdgeStatic, BezierEdgeBase); - 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 - }, - 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]; - } + _prototypeProperties(BezierEdgeStatic, null, { + cleanup: { + value: function cleanup() { + return false; }, writable: true, configurable: true }, - _zoomExtent: { - + _line: { /** - * this stops all movement induced by the navigation buttons - * + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx * @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(); + 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 }, - _stopMovement: { + _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; - /** - * 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"); + 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 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; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } + } } } - 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; + return { x: xVia, y: yVia }; }, writable: true, configurable: true }, - _zoomIn: { - value: function _zoomIn() { - this.body.view.scale += this.options.keyboard.speed.zoom; + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx, options) { + return this._findBorderPositionBezier(nearNode, ctx, options.via); }, writable: true, configurable: true }, - _zoomOut: { - value: function _zoomOut() { - this.body.view.scale -= this.options.keyboard.speed.zoom; + _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 }, - configureKeyboardBindings: { - + getPoint: { /** - * bind all keys using keycharm. + * 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 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"); + 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; - 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 { x: x, y: y }; }, writable: true, configurable: true } }); - return NavigationHandler; - })(); + return BezierEdgeStatic; + })(BezierEdgeBase); - module.exports = NavigationHandler; + module.exports = BezierEdgeStatic; /***/ }, /* 103 */ @@ -35479,167 +35524,113 @@ 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"); } }; /** - * Popup is a class to create a popup window with some text - * @param {Element} container The container object. - * @param {Number} [x] - * @param {Number} [y] - * @param {String} [text] - * @param {Object} [style] An object containing borderColor, - * backgroundColor, etc. + * Created by Alex on 3/20/2015. */ - var Popup = (function () { - function Popup(container, x, y, text, style) { - _classCallCheck(this, Popup); - - if (container) { - this.container = container; - } else { - this.container = document.body; - } - - // x, y and text are optional, see if a style object was passed in their place - if (style === undefined) { - if (typeof x === "object") { - style = x; - x = undefined; - } else if (typeof text === "object") { - style = text; - text = undefined; - } else { - // for backwards compatibility, in case clients other than Network are creating Popup directly - style = { - fontColor: "black", - fontSize: 14, // px - fontFace: "verdana", - color: { - border: "#666", - background: "#FFFFC6" - } - }; - } - } - this.x = 0; - this.y = 0; - this.padding = 5; - this.hidden = false; + var EdgeBase = _interopRequire(__webpack_require__(101)); - if (x !== undefined && y !== undefined) { - this.setPosition(x, y); - } - if (text !== undefined) { - this.setText(text); - } + var StraightEdge = (function (EdgeBase) { + function StraightEdge(options, body, labelModule) { + _classCallCheck(this, StraightEdge); - // create the frame - this.frame = document.createElement("div"); - this.frame.className = "network-tooltip"; - this.frame.style.color = style.fontColor; - this.frame.style.backgroundColor = style.color.background; - this.frame.style.borderColor = style.color.border; - this.frame.style.fontSize = style.fontSize + "px"; - this.frame.style.fontFamily = style.fontFace; - this.container.appendChild(this.frame); + _get(Object.getPrototypeOf(StraightEdge.prototype), "constructor", this).call(this, options, body, labelModule); } - _prototypeProperties(Popup, null, { - setPosition: { + _inherits(StraightEdge, EdgeBase); - /** - * @param {number} x Horizontal position of the popup window - * @param {number} y Vertical position of the popup window - */ - value: function setPosition(x, y) { - this.x = parseInt(x); - this.y = parseInt(y); + _prototypeProperties(StraightEdge, null, { + cleanup: { + value: function cleanup() { + return false; }, writable: true, configurable: true }, - setText: { - + _line: { /** - * Set the content for the popup window. This can be HTML code or text. - * @param {string | Element} content + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private */ - value: function setText(content) { - if (content instanceof Element) { - this.frame.innerHTML = ""; - this.frame.appendChild(content); - } else { - this.frame.innerHTML = content; // string containing text or HTML - } + 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 }, - show: { + getPoint: { /** - * Show the popup window - * @param {boolean} show Optional. Show or hide the window + * 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 show(show) { - if (show === undefined) { - show = true; + 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; } - if (show === true) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; - - var top = this.y - height; - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; - } - if (top < this.padding) { - top = this.padding; - } + var 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 left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; - } - if (left < this.padding) { - left = this.padding; - } + var borderPos = {}; + borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; + borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; - this.frame.style.left = left + "px"; - this.frame.style.top = top + "px"; - this.frame.style.visibility = "visible"; - this.hidden = false; - } else { - this.hide(); - } + return borderPos; }, writable: true, configurable: true }, - hide: { - - /** - * Hide the popup window - */ - value: function hide() { - this.hidden = true; - this.frame.style.visibility = "hidden"; + _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 Popup; - })(); + return StraightEdge; + })(EdgeBase); - module.exports = Popup; + module.exports = StraightEdge; /***/ } /******/ ]) diff --git a/index.js b/index.js index 38775ebe..f7c5944e 100644 --- a/index.js +++ b/index.js @@ -54,7 +54,6 @@ exports.timeline = { // Network exports.Network = require('./lib/network/Network'); exports.network = { - Groups: require('./lib/network/Groups'), Images: require('./lib/network/Images'), dotparser: require('./lib/network/dotparser'), gephiParser: require('./lib/network/gephiParser') diff --git a/lib/network/Groups.js b/lib/network/Groups.js deleted file mode 100644 index 1cd27737..00000000 --- a/lib/network/Groups.js +++ /dev/null @@ -1,107 +0,0 @@ -var util = require('../util'); - -/** - * @class Groups - * This class can store groups and options specific for groups. - */ -function Groups() { - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; - this.useDefaultGroups = true; -} - - -/** - * default constants for group colors - */ -Groups.DEFAULT = [ - {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // 0: blue - {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // 1: yellow - {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // 2: red - {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // 3: green - {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // 4: magenta - {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // 5: purple - {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // 6: orange - {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // 7: darkblue - {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // 8: pink - {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}}, // 9: mint - - {border: "#990000", background: "#EE0000", highlight: {border: "#BB0000", background: "#FF3333"}, hover: {border: "#BB0000", background: "#FF3333"}}, // 10:bright red - - {border: "#FF6000", background: "#FF6000", highlight: {border: "#FF6000", background: "#FF6000"}, hover: {border: "#FF6000", background: "#FF6000"}}, // 12: real orange - {border: "#97C2FC", background: "#2B7CE9", highlight: {border: "#D2E5FF", background: "#2B7CE9"}, hover: {border: "#D2E5FF", background: "#2B7CE9"}}, // 13: blue - {border: "#399605", background: "#255C03", highlight: {border: "#399605", background: "#255C03"}, hover: {border: "#399605", background: "#255C03"}}, // 14: green - {border: "#B70054", background: "#FF007E", highlight: {border: "#B70054", background: "#FF007E"}, hover: {border: "#B70054", background: "#FF007E"}}, // 15: magenta - {border: "#AD85E4", background: "#7C29F0", highlight: {border: "#D3BDF0", background: "#7C29F0"}, hover: {border: "#D3BDF0", background: "#7C29F0"}}, // 16: purple - {border: "#4557FA", background: "#000EA1", highlight: {border: "#6E6EFD", background: "#000EA1"}, hover: {border: "#6E6EFD", background: "#000EA1"}}, // 17: darkblue - {border: "#FFC0CB", background: "#FD5A77", highlight: {border: "#FFD1D9", background: "#FD5A77"}, hover: {border: "#FFD1D9", background: "#FD5A77"}}, // 18: pink - {border: "#C2FABC", background: "#74D66A", highlight: {border: "#E6FFE3", background: "#74D66A"}, hover: {border: "#E6FFE3", background: "#74D66A"}}, // 19: mint - - {border: "#EE0000", background: "#990000", highlight: {border: "#FF3333", background: "#BB0000"}, hover: {border: "#FF3333", background: "#BB0000"}}, // 20:bright red -]; - - -/** - * Clear all groups - */ -Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function() - { - var i = 0; - for ( var p in this ) { - if (this.hasOwnProperty(p)) { - i++; - } - } - return i; - } -}; - - -/** - * get group options of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group options - */ -Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - if (this.useDefaultGroups === false && this.groupsArray.length > 0) { - // create new group - var index = this.groupIndex % this.groupsArray.length; - this.groupIndex++; - group = {}; - group.color = this.groups[this.groupsArray[index]]; - this.groups[groupname] = group; - } - else { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; - } - } - - return group; -}; - -/** - * Add a custom group style - * @param {String} groupName - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object - */ -Groups.prototype.add = function (groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; -}; - -module.exports = Groups; diff --git a/lib/network/Network.js b/lib/network/Network.js index aad897e5..20d33704 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -8,13 +8,13 @@ var DataSet = require('../DataSet'); var DataView = require('../DataView'); var dotparser = require('./dotparser'); var gephiParser = require('./gephiParser'); -var Groups = require('./Groups'); var Images = require('./Images'); var Activator = require('../shared/Activator'); var locales = require('./locales'); +import Groups from './modules/Groups'; import NodesHandler from './modules/NodesHandler'; import EdgesHandler from './modules/EdgesHandler'; import PhysicsEngine from './modules/PhysicsEngine'; @@ -50,7 +50,6 @@ function Network (container, data, options) { }, locale: 'en', locales: locales, - useDefaultGroups: true }; // containers for nodes and edges @@ -97,9 +96,9 @@ function Network (container, data, options) { this.bindEventListeners(); // setting up all modules - var groups = new Groups(); // object with groups - var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images + var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images + this.groups = new Groups(); // object with groups this.canvas = new Canvas(this.body); // DOM handler this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key @@ -109,8 +108,8 @@ function Network (container, data, options) { this.layoutEngine = new LayoutEngine(this.body); this.clustering = new ClusterEngine(this.body); // clustering api - this.nodesHandler = new NodesHandler(this.body, images, groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options - this.edgesHandler = new EdgesHandler(this.body, images, groups); // Handle adding, deleting and updating of edges as well as global options + this.nodesHandler = new NodesHandler(this.body, images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options + this.edgesHandler = new EdgesHandler(this.body, images, this.groups); // Handle adding, deleting and updating of edges as well as global options // create the DOM elements this.canvas.create(); @@ -165,8 +164,6 @@ Network.prototype.bindEventListeners = function() { // call the dataUpdated event because the only difference between the two is the updating of the indices this.body.emitter.emit("_dataUpdated"); - // start simulation (can be called safely, even if already running) - this.body.emitter.emit("startSimulation"); console.log("_dataChanged took:", new Date().valueOf() - t0); }); @@ -176,11 +173,9 @@ Network.prototype.bindEventListeners = function() { // update values this._updateValueRange(this.body.nodes); this._updateValueRange(this.body.edges); - // update edges - this._reconnectEdges(); - this._markAllEdgesAsDirty(); // start simulation (can be called safely, even if already running) this.body.emitter.emit("startSimulation"); + console.log("_dataUpdated took:", new Date().valueOf() - t0); }); } @@ -246,13 +241,6 @@ Network.prototype.setData = function(data) { */ Network.prototype.setOptions = function (options) { if (options !== undefined) { - //var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation', - // 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse' - //]; - // extend all but the values in fields - //util.selectiveNotDeepExtend(fields,this.constants, options); - //util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes); - //util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges); //this.groups.useDefaultGroups = this.constants.useDefaultGroups; @@ -269,31 +257,7 @@ Network.prototype.setOptions = function (options) { this.selectionHandler.setOptions(options.selection); this.clustering.setOptions(options.clustering); - //util.mergeOptions(this.constants, options,'smoothCurves'); - //util.mergeOptions(this.constants, options,'hierarchicalLayout'); - //util.mergeOptions(this.constants, options,'clustering'); - //util.mergeOptions(this.constants, options,'navigation'); - //util.mergeOptions(this.constants, options,'keyboard'); - //util.mergeOptions(this.constants, options,'dataManipulation'); - - - //if (options.dataManipulation) { - // this.editMode = this.constants.dataManipulation.initiallyVisible; - //} - - - //// TODO: work out these options and document them - // - // // - //if (options.groups) { - // for (var groupname in options.groups) { - // if (options.groups.hasOwnProperty(groupname)) { - // var group = options.groups[groupname]; - // this.groups.add(groupname, group); - // } - // } - //} // //if (options.tooltip) { // for (prop in options.tooltip) { diff --git a/lib/network/modules/EdgesHandler.js b/lib/network/modules/EdgesHandler.js index 533415a7..a5cea6c4 100644 --- a/lib/network/modules/EdgesHandler.js +++ b/lib/network/modules/EdgesHandler.js @@ -123,12 +123,8 @@ class EdgesHandler { // this is called when options of EXISTING nodes or edges have changed. this.body.emitter.on("_dataUpdated", () => { - var t0 = new Date().valueOf(); - // update values this.reconnectEdges(); this.markAllEdgesAsDirty(); - // start simulation (can be called safely, even if already running) - console.log("_dataUpdated took:", new Date().valueOf() - t0); }); } diff --git a/lib/network/modules/Groups.js b/lib/network/modules/Groups.js new file mode 100644 index 00000000..9b0d396f --- /dev/null +++ b/lib/network/modules/Groups.js @@ -0,0 +1,116 @@ +let util = require('../../util'); + +/** + * @class Groups + * This class can store groups and options specific for groups. + */ +class Groups { + constructor() { + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; + + this.defaultGroups = [ + {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // 0: blue + {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // 1: yellow + {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // 2: red + {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // 3: green + {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // 4: magenta + {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // 5: purple + {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // 6: orange + {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // 7: darkblue + {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // 8: pink + {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}}, // 9: mint + + {border: "#990000", background: "#EE0000", highlight: {border: "#BB0000", background: "#FF3333"}, hover: {border: "#BB0000", background: "#FF3333"}}, // 10:bright red + + {border: "#FF6000", background: "#FF6000", highlight: {border: "#FF6000", background: "#FF6000"}, hover: {border: "#FF6000", background: "#FF6000"}}, // 12: real orange + {border: "#97C2FC", background: "#2B7CE9", highlight: {border: "#D2E5FF", background: "#2B7CE9"}, hover: {border: "#D2E5FF", background: "#2B7CE9"}}, // 13: blue + {border: "#399605", background: "#255C03", highlight: {border: "#399605", background: "#255C03"}, hover: {border: "#399605", background: "#255C03"}}, // 14: green + {border: "#B70054", background: "#FF007E", highlight: {border: "#B70054", background: "#FF007E"}, hover: {border: "#B70054", background: "#FF007E"}}, // 15: magenta + {border: "#AD85E4", background: "#7C29F0", highlight: {border: "#D3BDF0", background: "#7C29F0"}, hover: {border: "#D3BDF0", background: "#7C29F0"}}, // 16: purple + {border: "#4557FA", background: "#000EA1", highlight: {border: "#6E6EFD", background: "#000EA1"}, hover: {border: "#6E6EFD", background: "#000EA1"}}, // 17: darkblue + {border: "#FFC0CB", background: "#FD5A77", highlight: {border: "#FFD1D9", background: "#FD5A77"}, hover: {border: "#FFD1D9", background: "#FD5A77"}}, // 18: pink + {border: "#C2FABC", background: "#74D66A", highlight: {border: "#E6FFE3", background: "#74D66A"}, hover: {border: "#E6FFE3", background: "#74D66A"}}, // 19: mint + + {border: "#EE0000", background: "#990000", highlight: {border: "#FF3333", background: "#BB0000"}, hover: {border: "#FF3333", background: "#BB0000"}}, // 20:bright red + ]; + + this.options = {}; + this.defaultOptions = { + useDefaultGroups: true + } + util.extend(this.options, this.defaultOptions); + } + + + setOptions(options) { + let optionFields = ['useDefaultGroups']; + + if (options !== undefined) { + for (let groupname in options) { + if (options.hasOwnProperty(groupname)) { + if (optionFields.indexOf(groupName) == -1) { + let group = options[groupname]; + this.add(groupname, group); + } + } + } + } + } + + + /** + * Clear all groups + */ + clear() { + this.groups = {}; + this.groupsArray = []; + } + + /** + * get group options of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group options + */ + get(groupname) { + let group = this.groups[groupname]; + if (group == undefined) { + if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + let index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; + } + else { + // create new group + let index = this.defaultIndex % this.defaultGroups.length; + this.defaultIndex++; + group = {}; + group.color = this.defaultGroups[index]; + this.groups[groupname] = group; + } + } + + return group; + } + + /** + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ + add(groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; + } +} + +export default Groups; diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index 418ccdfb..f1e00441 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -404,7 +404,7 @@ class InteractionHandler { scale *= (1 + zoom); // calculate the pointer location - let pointer = {x:event.pageX, y:event.pageY}; + let pointer = this.getPointer({x:event.pageX, y:event.pageY}); // apply the new scale this.zoom(scale, pointer); diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index ee9f87f3..d3ca2275 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -294,9 +294,9 @@ class Edge { } 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);} + if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx,'from', viaNode, this.selected, this.hover);} + if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx,'middle', viaNode, this.selected, this.hover);} + if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx,'to', viaNode, this.selected, this.hover);} } drawLabel(ctx, viaNode) { diff --git a/lib/network/modules/components/edges/util/EdgeBase.js b/lib/network/modules/components/edges/util/EdgeBase.js index 254aa472..585b280f 100644 --- a/lib/network/modules/components/edges/util/EdgeBase.js +++ b/lib/network/modules/components/edges/util/EdgeBase.js @@ -28,7 +28,7 @@ class EdgeBase { drawLine(ctx, selected, hover) { // set style ctx.strokeStyle = this.getColor(ctx); - ctx.lineWidth = this.getLineWidth(); + ctx.lineWidth = this.getLineWidth(selected, hover); let via = undefined; if (this.from != this.to) { // draw line @@ -120,8 +120,6 @@ class EdgeBase { } - - /** * This function uses binary search to look for the point where the circle crosses the border of the node. * @param x @@ -151,7 +149,7 @@ class EdgeBase { while (low <= high && iteration < maxIterations) { let middle = (low + high) * 0.5; - pos = this._pointOnCircle(x,y,radius,middle); + 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)); @@ -352,11 +350,11 @@ class EdgeBase { * @param position * @param viaNode */ - drawArrowHead(ctx,position,viaNode) { + drawArrowHead(ctx, position, viaNode, selected, hover) { // set style ctx.strokeStyle = this.getColor(ctx); ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this.getLineWidth(); + ctx.lineWidth = this.getLineWidth(selected, hover); // set lets let angle; @@ -390,8 +388,8 @@ class EdgeBase { if (position !== 'middle') { // draw arrow head if (this.options.smooth.enabled == true) { - arrowPos = this.findBorderPosition(node1, ctx, {via:viaNode}); - let guidePos = this.getPoint(Math.max(0.0,Math.min(1.0,arrowPos.t + guideOffset)), viaNode); + arrowPos = this.findBorderPosition(node1, ctx, {via: viaNode}); + let guidePos = this.getPoint(Math.max(0.0, Math.min(1.0, arrowPos.t + guideOffset)), viaNode); angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x)); } else { @@ -438,7 +436,7 @@ class EdgeBase { angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; } else { - point = this.findBorderPosition(x,y,radius,0.175); + point = this.findBorderPosition(x, y, radius, 0.175); angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; }