diff --git a/HISTORY.md b/HISTORY.md index 564ff235..4235ed80 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -20,6 +20,12 @@ http://visjs.org - Added multiple types of smoothCurve drawing for greatly improved performance. - Option for inherited edge colors from connected nodes. - Option to disable the drawing of nodes or edges on drag. +- Fixed support nodes not being cleaned up if edges are removed. +- Improved edge selection detection for long smooth curves. +- Fixed dot radius bug. +- Updated max velocity of nodes to three times it's original value. +- Made "stabilized" event fire every time the network stabilizes. + ## 2014-07-07, version 3.0.0 diff --git a/dist/vis.js b/dist/vis.js index 5d762273..c0588f24 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5695,7 +5695,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(41); - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(43); var util = __webpack_require__(1); var DataSet = __webpack_require__(3); var DataView = __webpack_require__(4); @@ -6601,7 +6601,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(41); - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(43); var util = __webpack_require__(1); var DataSet = __webpack_require__(3); var DataView = __webpack_require__(4); @@ -9038,7 +9038,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 14 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(43); var util = __webpack_require__(1); var Component = __webpack_require__(12); @@ -10270,7 +10270,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 18 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(43); var util = __webpack_require__(1); var DataSet = __webpack_require__(3); var DataView = __webpack_require__(4); @@ -13295,7 +13295,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 22 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(43); /** * @constructor Item @@ -13896,7 +13896,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 25 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(43); var Item = __webpack_require__(22); /** @@ -14194,7 +14194,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(41); - var Hammer = __webpack_require__(49); + var Hammer = __webpack_require__(43); var mousetrap = __webpack_require__(42); var util = __webpack_require__(1); var DataSet = __webpack_require__(3); @@ -14302,7 +14302,7 @@ return /******/ (function(modules) { // webpackBootstrap gap: 5, altLength: undefined }, - inheritColor: false // to, from, false, true (== from) + inheritColor: "from" // to, from, false, true (== from) }, configurePhysics:false, physics: { @@ -14381,7 +14381,7 @@ return /******/ (function(modules) { // webpackBootstrap roundness: 0.5 }, dynamicSmoothCurves: true, - maxVelocity: 10, + maxVelocity: 30, minVelocity: 0.1, // px/s stabilizationIterations: 1000, // maximum number of iteration to stabilize labels:{ @@ -14727,7 +14727,6 @@ return /******/ (function(modules) { // webpackBootstrap } this._putDataInSector(); - if (!disableStart) { // find a stable position or start animating to a stable position if (this.stabilize) { @@ -15974,7 +15973,7 @@ return /******/ (function(modules) { // webpackBootstrap this._doInAllSectors("_drawControlNodes",ctx); } - // this._doInSupportSector("_drawNodes",ctx,true); + this._doInSupportSector("_drawNodes",ctx,true); // this._drawTree(ctx,"#F00F0F"); // restore original scaling and translation @@ -16284,7 +16283,12 @@ return /******/ (function(modules) { // webpackBootstrap this.moving = true; } else { - this.moving = this._isMoving(vminCorrected) || this.constants.configurePhysics; + this.moving = this._isMoving(vminCorrected); + if (this.moving == false) { + this.emit("stabilized",{iterations:null}); + } + this.moving = this.moving || this.configurePhysics; + } } }; @@ -16424,6 +16428,14 @@ return /******/ (function(modules) { // webpackBootstrap } if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { this._createBezierNodes(); + // cleanup unused support nodes + for (var nodeId in this.sectors['support']['nodes']) { + if (this.sectors['support']['nodes'].hasOwnProperty(nodeId)) { + if (this.edges[this.sectors['support']['nodes'][nodeId]] === undefined) { + delete this.sectors['support']['nodes'][nodeId]; + } + } + } } else { // delete the support nodes @@ -16435,6 +16447,8 @@ return /******/ (function(modules) { // webpackBootstrap } } } + + this._updateCalculationNodes(); if (!disableStart) { this.moving = true; @@ -17484,42 +17498,22 @@ return /******/ (function(modules) { // webpackBootstrap yVia = via.y; } var minDistance = 1e9; - var i,t,x,y,dx,dy; + var distance; + var i,t,x,y, lastX, lastY; for (i = 0; i < 10; i++) { t = 0.1*i; x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2; y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2; - dx = Math.abs(x3-x); - dy = Math.abs(y3-y); - minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy)); + if (i > 0) { + distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3); + minDistance = distance < minDistance ? distance : minDistance; + } + lastX = x; lastY = y; } return minDistance } else { - var px = x2-x1, - py = y2-y1, - something = px*px + py*py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; - - if (u > 1) { - u = 1; - } - else if (u < 0) { - u = 0; - } - - var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; - - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance - - return Math.sqrt(dx*dx + dy*dy); + return this._getDistanceToLine(x1,y1,x2,y2,x3,y3); } } else { @@ -17543,7 +17537,32 @@ return /******/ (function(modules) { // webpackBootstrap } }; + Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) { + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; + + if (u > 1) { + u = 1; + } + else if (u < 0) { + u = 0; + } + + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance + + return Math.sqrt(dx*dx + dy*dy); + } /** * This allows the zoom level of the network to influence the rendering @@ -18045,7 +18064,7 @@ return /******/ (function(modules) { // webpackBootstrap // individual shape properties if (properties.shape !== undefined) {this.shape = properties.shape;} if (properties.image !== undefined) {this.image = properties.image;} - if (properties.radius !== undefined) {this.radius = properties.radius;} + if (properties.radius !== undefined) {this.radius = properties.radius; this.baseRadiusValue = this.radius;} if (properties.color !== undefined) {this.color = util.parseColor(properties.color);} if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;} @@ -20526,7 +20545,7 @@ return /******/ (function(modules) { // webpackBootstrap // Only load hammer.js when in a browser environment // (loading hammer.js in a node.js environment gives errors) if (typeof window !== 'undefined') { - module.exports = window['Hammer'] || __webpack_require__(49); + module.exports = window['Hammer'] || __webpack_require__(43); // TODO: throw an error when hammerjs is not available? } else { @@ -20550,12 +20569,12 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var PhysicsMixin = __webpack_require__(50); - var ClusterMixin = __webpack_require__(43); - var SectorsMixin = __webpack_require__(44); - var SelectionMixin = __webpack_require__(45); - var ManipulationMixin = __webpack_require__(46); - var NavigationMixin = __webpack_require__(47); - var HierarchicalLayoutMixin = __webpack_require__(48); + var ClusterMixin = __webpack_require__(44); + var SectorsMixin = __webpack_require__(45); + var SelectionMixin = __webpack_require__(46); + var ManipulationMixin = __webpack_require__(47); + var NavigationMixin = __webpack_require__(48); + var HierarchicalLayoutMixin = __webpack_require__(49); /** * Load a mixin into the network object @@ -21728,1791 +21747,1699 @@ return /******/ (function(modules) { // webpackBootstrap /* 43 */ /***/ function(module, exports, __webpack_require__) { - /** - * Creation of the ClusterMixin var. + /*! Hammer.JS - v1.0.5 - 2013-04-07 + * http://eightmedia.github.com/hammer.js * - * This contains all the functions the Network object can use to employ clustering - */ + * Copyright (c) 2013 Jorik Tangelder ; + * Licensed under the MIT license */ + + (function(window, undefined) { + 'use strict'; /** - * This is only called in the constructor of the network object - * - */ - exports.startWithClustering = function() { - // cluster if the data set is big - this.clusterToFit(this.constants.clustering.initialMaxNodes, true); + * Hammer + * use this to create instances + * @param {HTMLElement} element + * @param {Object} options + * @returns {Hammer.Instance} + * @constructor + */ + var Hammer = function(element, options) { + return new Hammer.Instance(element, options || {}); + }; - // updates the lables after clustering - this.updateLabels(); + // default settings + Hammer.defaults = { + // add styles and attributes to the element to prevent the browser from doing + // its native behavior. this doesnt prevent the scrolling, but cancels + // the contextmenu, tap highlighting etc + // set to false to disable this + stop_browser_behavior: { + // this also triggers onselectstart=false for IE + userSelect: 'none', + // this makes the element blocking in IE10 >, you could experiment with the value + // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 + touchAction: 'none', + touchCallout: 'none', + contentZooming: 'none', + userDrag: 'none', + tapHighlightColor: 'rgba(0,0,0,0)' + } - // this is called here because if clusterin is disabled, the start and stabilize are called in - // the setData function. - if (this.stabilize) { - this._stabilize(); - } - this.start(); + // more settings are defined per gesture at gestures.js }; - /** - * This function clusters until the initialMaxNodes has been reached - * - * @param {Number} maxNumberOfNodes - * @param {Boolean} reposition - */ - exports.clusterToFit = function(maxNumberOfNodes, reposition) { - var numberOfNodes = this.nodeIndices.length; + // detect touchevents + Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; + Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); - var maxLevels = 50; - var level = 0; + // dont use mouseevents on mobile devices + Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX); - // we first cluster the hubs, then we pull in the outliers, repeat - while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { - if (level % 3 == 0) { - this.forceAggregateHubs(true); - this.normalizeClusterLevels(); - } - else { - this.increaseClusterLevel(); // this also includes a cluster normalization - } + // eventtypes per touchevent (start, move, end) + // are filled by Hammer.event.determineEventTypes on setup + Hammer.EVENT_TYPES = {}; - numberOfNodes = this.nodeIndices.length; - level += 1; - } + // direction defines + Hammer.DIRECTION_DOWN = 'down'; + Hammer.DIRECTION_LEFT = 'left'; + Hammer.DIRECTION_UP = 'up'; + Hammer.DIRECTION_RIGHT = 'right'; - // after the clustering we reposition the nodes to reduce the initial chaos - if (level > 0 && reposition == true) { - this.repositionNodes(); - } - this._updateCalculationNodes(); - }; + // pointer type + Hammer.POINTER_MOUSE = 'mouse'; + Hammer.POINTER_TOUCH = 'touch'; + Hammer.POINTER_PEN = 'pen'; + + // touch event defines + Hammer.EVENT_START = 'start'; + Hammer.EVENT_MOVE = 'move'; + Hammer.EVENT_END = 'end'; + + // hammer document where the base events are added at + Hammer.DOCUMENT = document; + + // plugins namespace + Hammer.plugins = {}; + + // if the window events are set... + Hammer.READY = false; /** - * This function can be called to open up a specific cluster. It is only called by - * It will unpack the cluster back one level. - * - * @param node | Node object: cluster to open. + * setup events to detect gestures on the document */ - exports.openCluster = function(node) { - var isMovingBeforeClustering = this.moving; - if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && - !(this._sector() == "default" && this.nodeIndices.length == 1)) { - // this loads a new sector, loads the nodes and edges and nodeIndices of it. - this._addSector(node); - var level = 0; - - // we decluster until we reach a decent number of nodes - while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { - this.decreaseClusterLevel(); - level += 1; + function setup() { + if(Hammer.READY) { + return; } - } - else { - this._expandClusterNode(node,false,true); + // find what eventtypes we add listeners to + Hammer.event.determineEventTypes(); - // update the index list, dynamic edges and labels - this._updateNodeIndexList(); - this._updateDynamicEdges(); - this._updateCalculationNodes(); - this.updateLabels(); - } + // Register all gestures inside Hammer.gestures + for(var name in Hammer.gestures) { + if(Hammer.gestures.hasOwnProperty(name)) { + Hammer.detection.register(Hammer.gestures[name]); + } + } - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); - } - }; + // Add touch events on the document + Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect); + Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect); + // Hammer is ready...! + Hammer.READY = true; + } /** - * This calls the updateClustes with default arguments + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * @param {HTMLElement} element + * @param {Object} [options={}] + * @returns {Hammer.Instance} + * @constructor */ - exports.updateClustersDefault = function() { - if (this.constants.clustering.enabled == true) { - this.updateClusters(0,false,false); - } - }; + Hammer.Instance = function(element, options) { + var self = this; + // setup HammerJS window events and register all gestures + // this also sets up the default options + setup(); - /** - * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will - * be clustered with their connected node. This can be repeated as many times as needed. - * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. - */ - exports.increaseClusterLevel = function() { - this.updateClusters(-1,false,true); - }; + this.element = element; + // start/stop detection option + this.enabled = true; - /** - * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will - * be unpacked if they are a cluster. This can be repeated as many times as needed. - * This can be called externally (by a key-bind for instance) to look into clusters without zooming. - */ - exports.decreaseClusterLevel = function() { - this.updateClusters(1,false,true); - }; + // merge options + this.options = Hammer.utils.extend( + Hammer.utils.extend({}, Hammer.defaults), + options || {}); + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.stop_browser_behavior) { + Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); + } - /** - * This is the main clustering function. It clusters and declusters on zoom or forced - * This function clusters on zoom, it can be called with a predefined zoom direction - * If out, check if we can form clusters, if in, check if we can open clusters. - * This function is only called from _zoom() - * - * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn - * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters - * @param {Boolean} force | enabled or disable forcing - * @param {Boolean} doNotStart | if true do not call start - * - */ - exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; + // start detection on touchstart + Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) { + if(self.enabled) { + Hammer.detection.startDetect(self, ev); + } + }); - // on zoom out collapse the sector if the scale is at the level the sector was made - if (this.previousScale > this.scale && zoomDirection == 0) { - this._collapseSector(); - } + // return instance + return this; + }; - // check if we zoom in or out - if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out - // forming clusters when forced pulls outliers in. When not forced, the edge length of the - // outer nodes determines if it is being clustered - this._formClusters(force); - } - else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in - if (force == true) { - // _openClusters checks for each node if the formationScale of the cluster is smaller than - // the current scale and if so, declusters. When forced, all clusters are reduced by one step - this._openClusters(recursive,force); - } - else { - // if a cluster takes up a set percentage of the active window - this._openClustersBySize(); - } - } - this._updateNodeIndexList(); - // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs - if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { - this._aggregateHubs(force); - this._updateNodeIndexList(); - } + Hammer.Instance.prototype = { + /** + * bind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {Hammer.Instance} + */ + on: function onEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t=0; t this.scale || zoomDirection == -1) { // zoom out - this.handleChains(); - this._updateNodeIndexList(); - } - this.previousScale = this.scale; + /** + * unbind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {Hammer.Instance} + */ + off: function offEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t=0; t this.constants.clustering.chainThreshold) { - this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage) - } + /** + * enable of disable hammer.js detection + * @param {Boolean} state + * @returns {Hammer.Instance} + */ + enable: function enable(state) { + this.enabled = state; + return this; + } }; /** - * this functions starts clustering by hubs - * The minimum hub threshold is set globally - * - * @private + * this holds the last move event, + * used to fix empty touchend issue + * see the onTouch event for an explanation + * @type {Object} */ - exports._aggregateHubs = function(force) { - this._getHubSize(); - this._formClustersByHub(force,false); - }; + var last_move_event = null; /** - * This function is fired by keypress. It forces hubs to form. - * + * when the mouse is hold down, this is true + * @type {Boolean} */ - exports.forceAggregateHubs = function(doNotStart) { - var isMovingBeforeClustering = this.moving; - var amountOfNodes = this.nodeIndices.length; - - this._aggregateHubs(true); - - // update the index list, dynamic edges and labels - this._updateNodeIndexList(); - this._updateDynamicEdges(); - this.updateLabels(); - - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; - } + var enable_detect = false; - if (doNotStart == false || doNotStart === undefined) { - // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded - if (this.moving != isMovingBeforeClustering) { - this.start(); - } - } - }; /** - * If a cluster takes up more than a set percentage of the screen, open the cluster - * - * @private + * when touch events have been fired, this is true + * @type {Boolean} */ - exports._openClustersBySize = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.inView() == true) { - if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || - (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { - this.openCluster(node); + var touch_triggered = false; + + + Hammer.event = { + /** + * simple addEventListener + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + */ + bindDom: function(element, type, handler) { + var types = type.split(' '); + for(var t=0; t 1) { - // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 - if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) { - openAll = true; - } - recursive = openAll ? true : recursive; + this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) { + var sourceEventType = ev.type.toLowerCase(); - // if the last child has been added on a smaller scale than current scale decluster - if (parentNode.formationScale < this.scale || force == true) { - // we will check if any of the contained child nodes should be removed from the cluster - for (var containedNodeId in parentNode.containedNodes) { - if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) { - var childNode = parentNode.containedNodes[containedNodeId]; + // onmouseup, but when touchend has been fired we do nothing. + // this is for touchdevices which also fire a mouseup on touchend + if(sourceEventType.match(/mouse/) && touch_triggered) { + return; + } - // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that - // the largest cluster is the one that comes from outside - if (force == true) { - if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] - || openAll) { - this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + // mousebutton must be down or a touch event + else if( sourceEventType.match(/touch/) || // touch events are always on screen + sourceEventType.match(/pointerdown/) || // pointerevents touch + (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed + ){ + enable_detect = true; } - } - else { - if (this._nodeInActiveArea(parentNode)) { - this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + + // we are in a touch event, set the touch triggered bool to true, + // this for the conflicts that may occur on ios and android + if(sourceEventType.match(/touch|pointer/)) { + touch_triggered = true; } - } - } - } - } - } - }; - /** - * ONLY CALLED FROM _expandClusterNode - * - * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove - * the child node from the parent contained_node object and put it back into the global nodes object. - * The same holds for the edge that was connected to the child node. It is moved back into the global edges object. - * - * @param {Node} parentNode | the parent node - * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node - * @param {Boolean} recursive | This will also check if the child needs to be expanded. - * With force and recursive both true, the entire cluster is unpacked - * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent - * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released - * @private - */ - exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) { - var childNode = parentNode.containedNodes[containedNodeId]; + // count the total touches on the screen + var count_touches = 0; - // if child node has been added on smaller scale than current, kick out - if (childNode.formationScale < this.scale || force == true) { - // unselect all selected items - this._unselectAll(); + // when touch has been triggered in this detection session + // and we are now handling a mouse event, we stop that to prevent conflicts + if(enable_detect) { + // update pointerevent + if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) { + count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); + } + // touch + else if(sourceEventType.match(/touch/)) { + count_touches = ev.touches.length; + } + // mouse + else if(!touch_triggered) { + count_touches = sourceEventType.match(/up/) ? 0 : 1; + } - // put the child node back in the global nodes object - this.nodes[containedNodeId] = childNode; + // if we are in a end event, but when we remove one touch and + // we still have enough, set eventType to move + if(count_touches > 0 && eventType == Hammer.EVENT_END) { + eventType = Hammer.EVENT_MOVE; + } + // no touches, force the end event + else if(!count_touches) { + eventType = Hammer.EVENT_END; + } - // release the contained edges from this childNode back into the global edges - this._releaseContainedEdges(parentNode,childNode); + // because touchend has no touches, and we often want to use these in our gestures, + // we send the last move event as our eventData in touchend + if(!count_touches && last_move_event !== null) { + ev = last_move_event; + } + // store the last move event + else { + last_move_event = ev; + } - // reconnect rerouted edges to the childNode - this._connectEdgeBackToChild(parentNode,childNode); + // trigger the handler + handler.call(Hammer.detection, self.collectEventData(element, eventType, ev)); - // validate all edges in dynamicEdges - this._validateEdges(parentNode); + // remove pointerevent from list + if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) { + count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); + } + } - // undo the changes from the clustering operation on the parent node - parentNode.mass -= childNode.mass; - parentNode.clusterSize -= childNode.clusterSize; - parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); - parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; + //debug(sourceEventType +" "+ eventType); - // place the child node near the parent, not at the exact same location to avoid chaos in the system - childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()); - childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()); + // on the end we reset everything + if(!count_touches) { + last_move_event = null; + enable_detect = false; + touch_triggered = false; + Hammer.PointerEvent.reset(); + } + }); + }, - // remove node from the list - delete parentNode.containedNodes[containedNodeId]; - // check if there are other childs with this clusterSession in the parent. - var othersPresent = false; - for (var childNodeId in parentNode.containedNodes) { - if (parentNode.containedNodes.hasOwnProperty(childNodeId)) { - if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) { - othersPresent = true; - break; - } - } - } - // if there are no others, remove the cluster session from the list - if (othersPresent == false) { - parentNode.clusterSessions.pop(); - } + /** + * we have different events for each device/browser + * determine what we need and set them in the Hammer.EVENT_TYPES constant + */ + determineEventTypes: function determineEventTypes() { + // determine the eventtype we want to set + var types; - this._repositionBezierNodes(childNode); - // this._repositionBezierNodes(parentNode); + // pointerEvents magic + if(Hammer.HAS_POINTEREVENTS) { + types = Hammer.PointerEvent.getEvents(); + } + // on Android, iOS, blackberry, windows mobile we dont want any mouseevents + else if(Hammer.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel']; + } + // for non pointer events browsers and mixed browsers, + // like chrome on windows8 touch laptop + else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup']; + } - // remove the clusterSession from the child node - childNode.clusterSession = 0; + Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0]; + Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1]; + Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2]; + }, - // recalculate the size of the node on the next time the node is rendered - parentNode.clearSizeCache(); - // restart the simulation to reorganise all nodes - this.moving = true; - } + /** + * create touchlist depending on the event + * @param {Object} ev + * @param {String} eventType used by the fakemultitouch plugin + */ + getTouchList: function getTouchList(ev/*, eventType*/) { + // get the fake pointerEvent touchlist + if(Hammer.HAS_POINTEREVENTS) { + return Hammer.PointerEvent.getTouchList(); + } + // get the touchlist + else if(ev.touches) { + return ev.touches; + } + // make fake touchlist from mouse position + else { + return [{ + identifier: 1, + pageX: ev.pageX, + pageY: ev.pageY, + target: ev.target + }]; + } + }, - // check if a further expansion step is possible if recursivity is enabled - if (recursive == true) { - this._expandClusterNode(childNode,recursive,force,openAll); - } - }; + /** + * collect event data for Hammer js + * @param {HTMLElement} element + * @param {String} eventType like Hammer.EVENT_MOVE + * @param {Object} eventData + */ + collectEventData: function collectEventData(element, eventType, ev) { + var touches = this.getTouchList(ev, eventType); - /** - * position the bezier nodes at the center of the edges - * - * @param node - * @private - */ - exports._repositionBezierNodes = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - node.dynamicEdges[i].positionBezierNode(); - } - }; + // find out pointerType + var pointerType = Hammer.POINTER_TOUCH; + if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) { + pointerType = Hammer.POINTER_MOUSE; + } + return { + center : Hammer.utils.getCenter(touches), + timeStamp : new Date().getTime(), + target : ev.target, + touches : touches, + eventType : eventType, + pointerType : pointerType, + srcEvent : ev, - /** - * This function checks if any nodes at the end of their trees have edges below a threshold length - * This function is called only from updateClusters() - * forceLevelCollapse ignores the length of the edge and collapses one level - * This means that a node with only one edge will be clustered with its connected node - * - * @private - * @param {Boolean} force - */ - exports._formClusters = function(force) { - if (force == false) { - this._formClustersByZoom(); - } - else { - this._forceClustersByZoom(); - } - }; + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + if(this.srcEvent.preventManipulation) { + this.srcEvent.preventManipulation(); + } + if(this.srcEvent.preventDefault) { + this.srcEvent.preventDefault(); + } + }, - /** - * This function handles the clustering by zooming out, this is based on a minimum edge distance - * - * @private - */ - exports._formClustersByZoom = function() { - var dx,dy,length, - minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, - // check if any edges are shorter than minLength and start the clustering - // the clustering favours the node with the larger mass - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - var edge = this.edges[edgeId]; - if (edge.connected) { - if (edge.toId != edge.fromId) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - length = Math.sqrt(dx * dx + dy * dy); - - - if (length < minLength) { - // first check which node is larger - var parentNode = edge.from; - var childNode = edge.to; - if (edge.to.mass > edge.from.mass) { - parentNode = edge.to; - childNode = edge.from; - } - - if (childNode.dynamicEdgesLength == 1) { - this._addToCluster(parentNode,childNode,false); - } - else if (parentNode.dynamicEdgesLength == 1) { - this._addToCluster(childNode,parentNode,false); + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return Hammer.detection.stopDetect(); } - } - } - } + }; } - } }; - /** - * This function forces the network to cluster all nodes with only one connecting edge to their - * connected node. - * - * @private - */ - exports._forceClustersByZoom = function() { - for (var nodeId in this.nodes) { - // another node could have absorbed this child. - if (this.nodes.hasOwnProperty(nodeId)) { - var childNode = this.nodes[nodeId]; + Hammer.PointerEvent = { + /** + * holds all pointers + * @type {Object} + */ + pointers: {}, - // the edges can be swallowed by another decrease - if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) { - var edge = childNode.dynamicEdges[0]; - var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; + /** + * get a list of pointers + * @returns {Array} touchlist + */ + getTouchList: function() { + var self = this; + var touchlist = []; - // group to the largest node - if (childNode.id != parentNode.id) { - if (parentNode.mass > childNode.mass) { - this._addToCluster(parentNode,childNode,true); - } - else { - this._addToCluster(childNode,parentNode,true); - } - } - } - } - } - }; + // we can use forEach since pointerEvents only is in IE10 + Object.keys(self.pointers).sort().forEach(function(id) { + touchlist.push(self.pointers[id]); + }); + return touchlist; + }, + /** + * update the position of a pointer + * @param {String} type Hammer.EVENT_END + * @param {Object} pointerEvent + */ + updatePointer: function(type, pointerEvent) { + if(type == Hammer.EVENT_END) { + this.pointers = {}; + } + else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } - /** - * To keep the nodes of roughly equal size we normalize the cluster levels. - * This function clusters a node to its smallest connected neighbour. - * - * @param node - * @private - */ - exports._clusterToSmallestNeighbour = function(node) { - var smallestNeighbour = -1; - var smallestNeighbourNode = null; - for (var i = 0; i < node.dynamicEdges.length; i++) { - if (node.dynamicEdges[i] !== undefined) { - var neighbour = null; - if (node.dynamicEdges[i].fromId != node.id) { - neighbour = node.dynamicEdges[i].from; - } - else if (node.dynamicEdges[i].toId != node.id) { - neighbour = node.dynamicEdges[i].to; - } + return Object.keys(this.pointers).length; + }, + /** + * check if ev matches pointertype + * @param {String} pointerType Hammer.POINTER_MOUSE + * @param {PointerEvent} ev + */ + matchType: function(pointerType, ev) { + if(!ev.pointerType) { + return false; + } - if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) { - smallestNeighbour = neighbour.clusterSessions.length; - smallestNeighbourNode = neighbour; - } - } - } + var types = {}; + types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE); + types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH); + types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN); + return types[pointerType]; + }, - if (neighbour != null && this.nodes[neighbour.id] !== undefined) { - this._addToCluster(neighbour, node, true); - } - }; + /** + * get events + */ + getEvents: function() { + return [ + 'pointerdown MSPointerDown', + 'pointermove MSPointerMove', + 'pointerup pointercancel MSPointerUp MSPointerCancel' + ]; + }, - /** - * This function forms clusters from hubs, it loops over all nodes - * - * @param {Boolean} force | Disregard zoom level - * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges - * @private - */ - exports._formClustersByHub = function(force, onlyEqual) { - // we loop over all nodes in the list - for (var nodeId in this.nodes) { - // we check if it is still available since it can be used by the clustering in this loop - if (this.nodes.hasOwnProperty(nodeId)) { - this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); + /** + * reset the list + */ + reset: function() { + this.pointers = {}; } - } }; - /** - * This function forms a cluster from a specific preselected hub node - * - * @param {Node} hubNode | the node we will cluster as a hub - * @param {Boolean} force | Disregard zoom level - * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges - * @param {Number} [absorptionSizeOffset] | - * @private - */ - exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) { - if (absorptionSizeOffset === undefined) { - absorptionSizeOffset = 0; - } - // we decide if the node is a hub - if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || - (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { - // initialize variables - var dx,dy,length; - var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; - var allowCluster = false; - // we create a list of edges because the dynamicEdges change over the course of this loop - var edgesIdarray = []; - var amountOfInitialEdges = hubNode.dynamicEdges.length; - for (var j = 0; j < amountOfInitialEdges; j++) { - edgesIdarray.push(hubNode.dynamicEdges[j].id); - } + Hammer.utils = { + /** + * extend method, + * also used for cloning when dest is an empty object + * @param {Object} dest + * @param {Object} src + * @parm {Boolean} merge do a merge + * @returns {Object} dest + */ + extend: function extend(dest, src, merge) { + for (var key in src) { + if(dest[key] !== undefined && merge) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, - // if the hub clustering is not forces, we check if one of the edges connected - // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold - if (force == false) { - allowCluster = false; - for (j = 0; j < amountOfInitialEdges; j++) { - var edge = this.edges[edgesIdarray[j]]; - if (edge !== undefined) { - if (edge.connected) { - if (edge.toId != edge.fromId) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - length = Math.sqrt(dx * dx + dy * dy); - if (length < minLength) { - allowCluster = true; - break; - } + /** + * find if a node is in the given parent + * used for event delegation tricks + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @returns {boolean} has_parent + */ + hasParent: function(node, parent) { + while(node){ + if(node == parent) { + return true; } - } + node = node.parentNode; } - } - } + return false; + }, - // start the clustering if allowed - if ((!force && allowCluster) || force) { - // we loop over all edges INITIALLY connected to this hub - for (j = 0; j < amountOfInitialEdges; j++) { - edge = this.edges[edgesIdarray[j]]; - // the edge can be clustered by this function in a previous loop - if (edge !== undefined) { - var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; - // we do not want hubs to merge with other hubs nor do we want to cluster itself. - if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) && - (childNode.id != hubNode.id)) { - this._addToCluster(hubNode,childNode,force); - } + + /** + * get the center of all the touches + * @param {Array} touches + * @returns {Object} center + */ + getCenter: function getCenter(touches) { + var valuesX = [], valuesY = []; + + for(var t= 0,len=touches.length; t= y) { + return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; + } + else { + return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; + } + }, - // keep track of the clustersessions so we can open the cluster up as it has been formed. - if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { - parentNode.clusterSessions.push(this.clusterSession); - } - // forced clusters only open from screen size and double tap - if (force == true) { - // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); - parentNode.formationScale = 0; - } - else { - parentNode.formationScale = this.scale; // The latest child has been added on this scale - } + /** + * calculate the distance between two touches + * @param {Touch} touch1 + * @param {Touch} touch2 + * @returns {Number} distance + */ + getDistance: function getDistance(touch1, touch2) { + var x = touch2.pageX - touch1.pageX, + y = touch2.pageY - touch1.pageY; + return Math.sqrt((x*x) + (y*y)); + }, - // recalculate the size of the node on the next time the node is rendered - parentNode.clearSizeCache(); - // set the pop-out scale for the childnode - parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale; + /** + * calculate the scale factor between two touchLists (fingers) + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start + * @param {Array} end + * @returns {Number} scale + */ + getScale: function getScale(start, end) { + // need two fingers... + if(start.length >= 2 && end.length >= 2) { + return this.getDistance(end[0], end[1]) / + this.getDistance(start[0], start[1]); + } + return 1; + }, - // nullify the movement velocity of the child, this is to avoid hectic behaviour - childNode.clearVelocity(); - // the mass has altered, preservation of energy dictates the velocity to be updated - parentNode.updateVelocity(massBefore); + /** + * calculate the rotation degrees between two touchLists (fingers) + * @param {Array} start + * @param {Array} end + * @returns {Number} rotation + */ + getRotation: function getRotation(start, end) { + // need two fingers + if(start.length >= 2 && end.length >= 2) { + return this.getAngle(end[1], end[0]) - + this.getAngle(start[1], start[0]); + } + return 0; + }, - // restart the simulation to reorganise all nodes - this.moving = true; - }; + /** + * boolean if the direction is vertical + * @param {String} direction + * @returns {Boolean} is_vertical + */ + isVertical: function isVertical(direction) { + return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN); + }, - /** - * This function will apply the changes made to the remainingEdges during the formation of the clusters. - * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree. - * It has to be called if a level is collapsed. It is called by _formClusters(). - * @private - */ - exports._updateDynamicEdges = function() { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - node.dynamicEdgesLength = node.dynamicEdges.length; - // this corrects for multiple edges pointing at the same other node - var correction = 0; - if (node.dynamicEdgesLength > 1) { - for (var j = 0; j < node.dynamicEdgesLength - 1; j++) { - var edgeToId = node.dynamicEdges[j].toId; - var edgeFromId = node.dynamicEdges[j].fromId; - for (var k = j+1; k < node.dynamicEdgesLength; k++) { - if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) || - (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) { - correction += 1; - } + /** + * stop browser default behavior with css props + * @param {HtmlElement} element + * @param {Object} css_props + */ + stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) { + var prop, + vendors = ['webkit','khtml','moz','ms','o','']; + + if(!css_props || !element.style) { + return; } - } - } - node.dynamicEdgesLength -= correction; - } - }; + // with css properties for modern browsers + for(var i = 0; i < vendors.length; i++) { + for(var p in css_props) { + if(css_props.hasOwnProperty(p)) { + prop = p; - /** - * This adds an edge from the childNode to the contained edges of the parent node - * - * @param parentNode | Node object - * @param childNode | Node object - * @param edge | Edge object - * @private - */ - exports._addToContainedEdges = function(parentNode, childNode, edge) { - // create an array object if it does not yet exist for this childNode - if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) { - parentNode.containedEdges[childNode.id] = [] - } - // add this edge to the list - parentNode.containedEdges[childNode.id].push(edge); + // vender prefix at the property + if(vendors[i]) { + prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); + } - // remove the edge from the global edges object - delete this.edges[edge.id]; + // set the style + element.style[prop] = css_props[p]; + } + } + } - // remove the edge from the parent object - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - if (parentNode.dynamicEdges[i].id == edge.id) { - parentNode.dynamicEdges.splice(i,1); - break; + // also the disable onselectstart + if(css_props.userSelect == 'none') { + element.onselectstart = function() { + return false; + }; + } } - } }; - /** - * This function connects an edge that was connected to a child node to the parent node. - * It keeps track of which nodes it has been connected to with the originalId array. - * - * @param {Node} parentNode | Node object - * @param {Node} childNode | Node object - * @param {Edge} edge | Edge object - * @private - */ - exports._connectEdgeToCluster = function(parentNode, childNode, edge) { - // handle circular edges - if (edge.toId == edge.fromId) { - this._addToContainedEdges(parentNode, childNode, edge); - } - else { - if (edge.toId == childNode.id) { // edge connected to other node on the "to" side - edge.originalToId.push(childNode.id); - edge.to = parentNode; - edge.toId = parentNode.id; - } - else { // edge connected to other node with the "from" side - - edge.originalFromId.push(childNode.id); - edge.from = parentNode; - edge.fromId = parentNode.id; - } - - this._addToReroutedEdges(parentNode,childNode,edge); - } - }; + Hammer.detection = { + // contains all registred Hammer.gestures in the correct order + gestures: [], + // data of the current Hammer.gesture detection session + current: null, - /** - * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain - * these edges inside of the cluster. - * - * @param parentNode - * @param childNode - * @private - */ - exports._containCircularEdgesFromNode = function(parentNode, childNode) { - // manage all the edges connected to the child and parent nodes - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - var edge = parentNode.dynamicEdges[i]; - // handle circular edges - if (edge.toId == edge.fromId) { - this._addToContainedEdges(parentNode, childNode, edge); - } - } - }; + // the previous Hammer.gesture session data + // is a full clone of the previous gesture.current object + previous: null, + // when this becomes true, no gestures are fired + stopped: false, - /** - * This adds an edge from the childNode to the rerouted edges of the parent node - * - * @param parentNode | Node object - * @param childNode | Node object - * @param edge | Edge object - * @private - */ - exports._addToReroutedEdges = function(parentNode, childNode, edge) { - // create an array object if it does not yet exist for this childNode - // we store the edge in the rerouted edges so we can restore it when the cluster pops open - if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) { - parentNode.reroutedEdges[childNode.id] = []; - } - parentNode.reroutedEdges[childNode.id].push(edge); - // this edge becomes part of the dynamicEdges of the cluster node - parentNode.dynamicEdges.push(edge); - }; + /** + * start Hammer.gesture detection + * @param {Hammer.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a Hammer.gesture detection on an element + if(this.current) { + return; + } + this.stopped = false; + this.current = { + inst : inst, // reference to HammerInstance we're working for + startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent : false, // last eventData + name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; - /** - * This function connects an edge that was connected to a cluster node back to the child node. - * - * @param parentNode | Node object - * @param childNode | Node object - * @private - */ - exports._connectEdgeBackToChild = function(parentNode, childNode) { - if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) { - for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) { - var edge = parentNode.reroutedEdges[childNode.id][i]; - if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) { - edge.originalFromId.pop(); - edge.fromId = childNode.id; - edge.from = childNode; - } - else { - edge.originalToId.pop(); - edge.toId = childNode.id; - edge.to = childNode; - } + this.detect(eventData); + }, - // append this edge to the list of edges connecting to the childnode - childNode.dynamicEdges.push(edge); - // remove the edge from the parent object - for (var j = 0; j < parentNode.dynamicEdges.length; j++) { - if (parentNode.dynamicEdges[j].id == edge.id) { - parentNode.dynamicEdges.splice(j,1); - break; + /** + * Hammer.gesture detection + * @param {Object} eventData + * @param {Object} eventData + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return; } - } - } - // remove the entry from the rerouted edges - delete parentNode.reroutedEdges[childNode.id]; - } - }; + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); - /** - * When loops are clustered, an edge can be both in the rerouted array and the contained array. - * This function is called last to verify that all edges in dynamicEdges are in fact connected to the - * parentNode - * - * @param parentNode | Node object - * @private - */ - exports._validateEdges = function(parentNode) { - for (var i = 0; i < parentNode.dynamicEdges.length; i++) { - var edge = parentNode.dynamicEdges[i]; - if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { - parentNode.dynamicEdges.splice(i,1); - } - } - }; + // instance options + var inst_options = this.current.inst.options; + // call Hammer.gesture handlers + for(var g=0,len=this.gestures.length; g 1) { - node.label = "[".concat(String(node.clusterSize),"]"); - } - } - } + /** + * extend eventData for Hammer.gestures + * @param {Object} ev + * @returns {Object} ev + */ + extendEventData: function extendEventData(ev) { + var startEv = this.current.startEvent; - // update node labels - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.clusterSize == 1) { - if (node.originalLabel !== undefined) { - node.label = node.originalLabel; - } - else { - node.label = String(node.id); + // if the touches change, set the new touches over the startEvent touches + // this because touchevents don't have all the touches on touchstart, or the + // user must place his fingers at the EXACT same time on the screen, which is not realistic + // but, sometimes it happens that both fingers are touching at the EXACT same time + if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) { + // extend 1 level deep to get the touchlist with the touch objects + startEv.touches = []; + for(var i=0,len=ev.touches.length; i clusterLevel) {minLevel = clusterLevel;} - } - } + distance : Hammer.utils.getDistance(startEv.center, ev.center), + angle : Hammer.utils.getAngle(startEv.center, ev.center), + direction : Hammer.utils.getDirection(startEv.center, ev.center), - if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) { - var amountOfNodes = this.nodeIndices.length; - var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference; - // we loop over all nodes in the list - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].clusterSessions.length < targetLevel) { - this._clusterToSmallestNeighbour(this.nodes[nodeId]); - } - } - } - this._updateNodeIndexList(); - this._updateDynamicEdges(); - // if a cluster was formed, we increase the clusterSession - if (this.nodeIndices.length != amountOfNodes) { - this.clusterSession += 1; - } - } - }; + scale : Hammer.utils.getScale(startEv.touches, ev.touches), + rotation : Hammer.utils.getRotation(startEv.touches, ev.touches), + startEvent : startEv + }); + return ev; + }, - /** - * This function determines if the cluster we want to decluster is in the active area - * this means around the zoom center - * - * @param {Node} node - * @returns {boolean} - * @private - */ - exports._nodeInActiveArea = function(node) { - return ( - Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale - && - Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale - ) - }; + /** + * register new gesture + * @param {Object} gesture object, see gestures.js for documentation + * @returns {Array} gestures + */ + register: function register(gesture) { + // add an enable gesture options if there is no given + var options = gesture.defaults || {}; + if(options[gesture.name] === undefined) { + options[gesture.name] = true; + } - /** - * This is an adaptation of the original repositioning function. This is called if the system is clustered initially - * It puts large clusters away from the center and randomizes the order. - * - */ - exports.repositionNodes = function() { - for (var i = 0; i < this.nodeIndices.length; i++) { - var node = this.nodes[this.nodeIndices[i]]; - if ((node.xFixed == false || node.yFixed == false)) { - var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass); - var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) {node.x = radius * Math.cos(angle);} - if (node.yFixed == false) {node.y = radius * Math.sin(angle);} - this._repositionBezierNodes(node); - } - } - }; + // extend Hammer default options with the Hammer.gesture options + Hammer.utils.extend(Hammer.defaults, options, true); + // set its index + gesture.index = gesture.index || 1000; - /** - * 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 - */ - exports._getHubSize = function() { - var average = 0; - var averageSquared = 0; - var hubCounter = 0; - var largestHub = 0; + // add Hammer.gesture to the list + this.gestures.push(gesture); - for (var i = 0; i < this.nodeIndices.length; i++) { + // sort the list by index + this.gestures.sort(function(a, b) { + if (a.index < b.index) { + return -1; + } + if (a.index > b.index) { + return 1; + } + return 0; + }); - var node = this.nodes[this.nodeIndices[i]]; - if (node.dynamicEdgesLength > largestHub) { - largestHub = node.dynamicEdgesLength; + return this.gestures; } - average += node.dynamicEdgesLength; - averageSquared += Math.pow(node.dynamicEdgesLength,2); - hubCounter += 1; - } - average = average / hubCounter; - averageSquared = averageSquared / hubCounter; - - var variance = averageSquared - Math.pow(average,2); - - var standardDeviation = Math.sqrt(variance); - - this.hubThreshold = Math.floor(average + 2*standardDeviation); - - // always have at least one to cluster - if (this.hubThreshold > largestHub) { - this.hubThreshold = largestHub; - } - - // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); - // console.log("hubThreshold:",this.hubThreshold); }; - /** - * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods - * with this amount we can cluster specifically on these chains. - * - * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce - * @private - */ - exports._reduceAmountOfChains = function(fraction) { - this.hubThreshold = 2; - var reduceAmount = Math.floor(this.nodeIndices.length * fraction); - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { - if (reduceAmount > 0) { - this._formClusterFromHub(this.nodes[nodeId],true,true,1); - reduceAmount -= 1; - } - } - } - } - }; + Hammer.gestures = Hammer.gestures || {}; /** - * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods - * with this amount we can cluster specifically on these chains. + * Custom gestures + * ============================== * - * @private - */ - exports._getChainFraction = function() { - var chains = 0; - var total = 0; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { - chains += 1; - } - total += 1; - } - } - return chains/total; - }; - - -/***/ }, -/* 44 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - - /** - * Creation of the SectorMixin var. + * Gesture object + * -------------------- + * The object structure of a gesture: * - * This contains all the functions the Network object can use to employ the sector system. - * The sector system is always used by Network, though the benefits only apply to the use of clustering. - * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. - */ + * { name: 'mygesture', + * index: 1337, + * defaults: { + * mygesture_option: true + * } + * handler: function(type, ev, inst) { + * // trigger gesture event + * inst.trigger(this.name, ev); + * } + * } - /** - * This function is only called by the setData function of the Network object. - * This loads the global references into the active sector. This initializes the sector. + * @param {String} name + * this should be the name of the gesture, lowercase + * it is also being used to disable/enable the gesture per instance config. * - * @private - */ - exports._putDataInSector = function() { - this.sectors["active"][this._sector()].nodes = this.nodes; - this.sectors["active"][this._sector()].edges = this.edges; - this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; - }; - - - /** - * /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied (active) sector. If a type is defined, do the specific type + * @param {Number} [index=1000] + * the index of the gesture, where it is going to be in the stack of gestures detection + * like when you build an gesture that depends on the drag gesture, it is a good + * idea to place it after the index of the drag gesture. * - * @param {String} sectorId - * @param {String} [sectorType] | "active" or "frozen" - * @private - */ - exports._switchToSector = function(sectorId, sectorType) { - if (sectorType === undefined || sectorType == "active") { - this._switchToActiveSector(sectorId); - } - else { - this._switchToFrozenSector(sectorId); - } - }; - - - /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied active sector. + * @param {Object} [defaults={}] + * the default settings of the gesture. these are added to the instance settings, + * and can be overruled per instance. you can also add the name of the gesture, + * but this is also added by default (and set to true). + * + * @param {Function} handler + * this handles the gesture detection of your custom gesture and receives the + * following arguments: + * + * @param {Object} eventData + * event data containing the following properties: + * timeStamp {Number} time the event occurred + * target {HTMLElement} target element + * touches {Array} touches (fingers, pointers, mouse) on the screen + * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH + * center {Object} center position of the touches. contains pageX and pageY + * deltaTime {Number} the total time of the touches in the screen + * deltaX {Number} the delta on x axis we haved moved + * deltaY {Number} the delta on y axis we haved moved + * velocityX {Number} the velocity on the x + * velocityY {Number} the velocity on y + * angle {Number} the angle we are moving + * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT + * distance {Number} the distance we haved moved + * scale {Number} scaling of the touches, needs 2 touches + * rotation {Number} rotation of the touches, needs 2 touches * + * eventType {String} matches Hammer.EVENT_START|MOVE|END + * srcEvent {Object} the source event, like TouchStart or MouseDown * + * startEvent {Object} contains the same properties as above, + * but from the first touch. this is used to calculate + * distances, deltaTime, scaling etc + * + * @param {Hammer.Instance} inst + * the instance we are doing the detection for. you can get the options from + * the inst.options object and trigger the gesture event by calling inst.trigger + * + * + * Handle gestures + * -------------------- + * inside the handler you can get/set Hammer.detection.current. This is the current + * detection session. It has the following properties + * @param {String} name + * contains the name of the gesture we have detected. it has not a real function, + * only to check in other gestures if something is detected. + * like in the drag gesture we set it to 'drag' and in the swipe gesture we can + * check if the current gesture is 'drag' by accessing Hammer.detection.current.name + * + * @readonly + * @param {Hammer.Instance} inst + * the instance we do the detection for + * + * @readonly + * @param {Object} startEvent + * contains the properties of the first gesture detection in this session. + * Used for calculations about timing, distance, etc. + * + * @readonly + * @param {Object} lastEvent + * contains all the properties of the last gesture detect in this session. + * + * after the gesture detection session has been completed (user has released the screen) + * the Hammer.detection.current object is copied into Hammer.detection.previous, + * this is usefull for gestures like doubletap, where you need to know if the + * previous gesture was a tap + * + * options that have been set by the instance can be received by calling inst.options + * + * You can trigger a gesture event by calling inst.trigger("mygesture", event). + * The first param is the name of your gesture, the second the event argument + * + * + * Register gestures + * -------------------- + * When an gesture is added to the Hammer.gestures object, it is auto registered + * at the setup of the first Hammer instance. You can also call Hammer.detection.register + * manually and pass your gesture object as a param * - * @param sectorId - * @private */ - exports._switchToActiveSector = function(sectorId) { - this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["active"][sectorId]["nodes"]; - this.edges = this.sectors["active"][sectorId]["edges"]; - }; - /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied active sector. - * - * @private + * Hold + * Touch stays at the same place for x time + * @events hold */ - exports._switchToSupportSector = function() { - this.nodeIndices = this.sectors["support"]["nodeIndices"]; - this.nodes = this.sectors["support"]["nodes"]; - this.edges = this.sectors["support"]["edges"]; - }; + Hammer.gestures.Hold = { + name: 'hold', + index: 10, + defaults: { + hold_timeout : 500, + hold_threshold : 1 + }, + timer: null, + handler: function holdGesture(ev, inst) { + switch(ev.eventType) { + case Hammer.EVENT_START: + // clear any running timers + clearTimeout(this.timer); + // set the gesture so we can check in the timeout if it still is + Hammer.detection.current.name = this.name; - /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the supplied frozen sector. - * - * @param sectorId - * @private - */ - exports._switchToFrozenSector = function(sectorId) { - this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; - this.nodes = this.sectors["frozen"][sectorId]["nodes"]; - this.edges = this.sectors["frozen"][sectorId]["edges"]; - }; + // set timer and if after the timeout it still is hold, + // we trigger the hold event + this.timer = setTimeout(function() { + if(Hammer.detection.current.name == 'hold') { + inst.trigger('hold', ev); + } + }, inst.options.hold_timeout); + break; + // when you move or end we clear the timer + case Hammer.EVENT_MOVE: + if(ev.distance > inst.options.hold_threshold) { + clearTimeout(this.timer); + } + break; - /** - * This function sets the global references to nodes, edges and nodeIndices back to - * those of the currently active sector. - * - * @private - */ - exports._loadLatestSector = function() { - this._switchToSector(this._sector()); + case Hammer.EVENT_END: + clearTimeout(this.timer); + break; + } + } }; /** - * This function returns the currently active sector Id - * - * @returns {String} - * @private + * Tap/DoubleTap + * Quick touch at a place or double at the same place + * @events tap, doubletap */ - exports._sector = function() { - return this.activeSector[this.activeSector.length-1]; - }; + Hammer.gestures.Tap = { + name: 'tap', + index: 100, + defaults: { + tap_max_touchtime : 250, + tap_max_distance : 10, + tap_always : true, + doubletap_distance : 20, + doubletap_interval : 300 + }, + handler: function tapGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + // previous gesture, for the double tap since these are two different gesture detections + var prev = Hammer.detection.previous, + did_doubletap = false; + // when the touchtime is higher then the max touch time + // or when the moving distance is too much + if(ev.deltaTime > inst.options.tap_max_touchtime || + ev.distance > inst.options.tap_max_distance) { + return; + } - /** - * This function returns the previously active sector Id - * - * @returns {String} - * @private - */ - exports._previousSector = function() { - if (this.activeSector.length > 1) { - return this.activeSector[this.activeSector.length-2]; - } - else { - throw new TypeError('there are not enough sectors in the this.activeSector array.'); - } + // check if double tap + if(prev && prev.name == 'tap' && + (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval && + ev.distance < inst.options.doubletap_distance) { + inst.trigger('doubletap', ev); + did_doubletap = true; + } + + // do a single tap + if(!did_doubletap || inst.options.tap_always) { + Hammer.detection.current.name = 'tap'; + inst.trigger(Hammer.detection.current.name, ev); + } + } + } }; /** - * We add the active sector at the end of the this.activeSector array - * This ensures it is the currently active sector returned by _sector() and it reaches the top - * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. - * - * @param newId - * @private + * Swipe + * triggers swipe events when the end velocity is above the threshold + * @events swipe, swipeleft, swiperight, swipeup, swipedown */ - exports._setActiveSector = function(newId) { - this.activeSector.push(newId); + Hammer.gestures.Swipe = { + name: 'swipe', + index: 40, + defaults: { + // set 0 for unlimited, but this can conflict with transform + swipe_max_touches : 1, + swipe_velocity : 0.7 + }, + handler: function swipeGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + // max touches + if(inst.options.swipe_max_touches > 0 && + ev.touches.length > inst.options.swipe_max_touches) { + return; + } + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.velocityX > inst.options.swipe_velocity || + ev.velocityY > inst.options.swipe_velocity) { + // trigger swipe events + inst.trigger(this.name, ev); + inst.trigger(this.name + ev.direction, ev); + } + } + } }; /** - * We remove the currently active sector id from the active sector stack. This happens when - * we reactivate the previously active sector - * - * @private - */ - exports._forgetLastSector = function() { - this.activeSector.pop(); - }; - - - /** - * This function creates a new active sector with the supplied newId. This newId - * is the expanding node id. - * - * @param {String} newId | Id of the new active sector - * @private + * Drag + * Move with x fingers (default 1) around on the page. Blocking the scrolling when + * moving left and right is a good practice. When all the drag events are blocking + * you disable scrolling on that area. + * @events drag, drapleft, dragright, dragup, dragdown */ - exports._createNewSector = function(newId) { - // create the new sector - this.sectors["active"][newId] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": this.scale, - "drawingNode": undefined}; + Hammer.gestures.Drag = { + name: 'drag', + index: 50, + defaults: { + drag_min_distance : 10, + // set 0 for unlimited, but this can conflict with transform + drag_max_touches : 1, + // prevent default browser behavior when dragging occurs + // be careful with it, it makes the element a blocking element + // when you are using the drag gesture, it is a good practice to set this true + drag_block_horizontal : false, + drag_block_vertical : false, + // drag_lock_to_axis keeps the drag gesture on the axis that it started on, + // It disallows vertical directions if the initial direction was horizontal, and vice versa. + drag_lock_to_axis : false, + // drag lock only kicks in when distance > drag_lock_min_distance + // This way, locking occurs only when the distance has become large enough to reliably determine the direction + drag_lock_min_distance : 25 + }, + triggered: false, + handler: function dragGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(Hammer.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } - // create the new sector render node. This gives visual feedback that you are in a new sector. - this.sectors["active"][newId]['drawingNode'] = new Node( - {id:newId, - color: { - background: "#eaefef", - border: "495c5e" + // max touches + if(inst.options.drag_max_touches > 0 && + ev.touches.length > inst.options.drag_max_touches) { + return; } - },{},{},this.constants); - this.sectors["active"][newId]['drawingNode'].clusterSize = 2; - }; + switch(ev.eventType) { + case Hammer.EVENT_START: + this.triggered = false; + break; - /** - * This function removes the currently active sector. This is called when we create a new - * active sector. - * - * @param {String} sectorId | Id of the active sector that will be removed - * @private - */ - exports._deleteActiveSector = function(sectorId) { - delete this.sectors["active"][sectorId]; - }; + case Hammer.EVENT_MOVE: + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.distance < inst.options.drag_min_distance && + Hammer.detection.current.name != this.name) { + return; + } + // we are dragging! + Hammer.detection.current.name = this.name; - /** - * This function removes the currently active sector. This is called when we reactivate - * the previously active sector. - * - * @param {String} sectorId | Id of the active sector that will be removed - * @private - */ - exports._deleteFrozenSector = function(sectorId) { - delete this.sectors["frozen"][sectorId]; - }; + // lock drag to axis? + if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) { + ev.drag_locked_to_axis = true; + } + var last_direction = Hammer.detection.current.lastEvent.direction; + if(ev.drag_locked_to_axis && last_direction !== ev.direction) { + // keep direction on the axis that the drag gesture started on + if(Hammer.utils.isVertical(last_direction)) { + ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; + } + else { + ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; + } + } + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } - /** - * Freezing an active sector means moving it from the "active" object to the "frozen" object. - * We copy the references, then delete the active entree. - * - * @param sectorId - * @private - */ - exports._freezeSector = function(sectorId) { - // we move the set references from the active to the frozen stack. - this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; + // trigger normal event + inst.trigger(this.name, ev); - // we have moved the sector data into the frozen set, we now remove it from the active set - this._deleteActiveSector(sectorId); - }; + // direction event, like dragdown + inst.trigger(this.name + ev.direction, ev); + // block the browser events + if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) || + (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) { + ev.preventDefault(); + } + break; - /** - * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" - * object to the "active" object. - * - * @param sectorId - * @private - */ - exports._activateSector = function(sectorId) { - // we move the set references from the frozen to the active stack. - this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; + case Hammer.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } - // we have moved the sector data into the active set, we now remove it from the frozen stack - this._deleteFrozenSector(sectorId); + this.triggered = false; + break; + } + } }; /** - * This function merges the data from the currently active sector with a frozen sector. This is used - * in the process of reverting back to the previously active sector. - * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it - * upon the creation of a new active sector. - * - * @param sectorId - * @private + * Transform + * User want to scale or rotate with 2 fingers + * @events transform, pinch, pinchin, pinchout, rotate */ - exports._mergeThisWithFrozen = function(sectorId) { - // copy all nodes - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; - } - } + Hammer.gestures.Transform = { + name: 'transform', + index: 45, + defaults: { + // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 + transform_min_scale : 0.01, + // rotation in degrees + transform_min_rotation : 1, + // prevent default browser behavior when two touches are on the screen + // but it makes the element a blocking element + // when you are using the transform gesture, it is a good practice to set this true + transform_always_block : false + }, + triggered: false, + handler: function transformGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(Hammer.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } - // copy all edges (if not fully clustered, else there are no edges) - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; - } - } + // atleast multitouch + if(ev.touches.length < 2) { + return; + } - // merge the nodeIndices - for (var i = 0; i < this.nodeIndices.length; i++) { - this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); - } - }; + // prevent default when two fingers are on the screen + if(inst.options.transform_always_block) { + ev.preventDefault(); + } + switch(ev.eventType) { + case Hammer.EVENT_START: + this.triggered = false; + break; - /** - * This clusters the sector to one cluster. It was a single cluster before this process started so - * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. - * - * @private - */ - exports._collapseThisToSingleCluster = function() { - this.clusterToFit(1,false); - }; + case Hammer.EVENT_MOVE: + var scale_threshold = Math.abs(1-ev.scale); + var rotation_threshold = Math.abs(ev.rotation); + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(scale_threshold < inst.options.transform_min_scale && + rotation_threshold < inst.options.transform_min_rotation) { + return; + } - /** - * We create a new active sector from the node that we want to open. - * - * @param node - * @private - */ - exports._addSector = function(node) { - // this is the currently active sector - var sector = this._sector(); + // we are transforming! + Hammer.detection.current.name = this.name; - // // this should allow me to select nodes from a frozen set. - // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { - // console.log("the node is part of the active sector"); - // } - // else { - // console.log("I dont know what the fuck happened!!"); - // } - - // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. - delete this.nodes[node.id]; - - var unqiueIdentifier = util.randomUUID(); + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } - // we fully freeze the currently active sector - this._freezeSector(sector); + inst.trigger(this.name, ev); // basic transform event - // we create a new active sector. This sector has the Id of the node to ensure uniqueness - this._createNewSector(unqiueIdentifier); + // trigger rotate event + if(rotation_threshold > inst.options.transform_min_rotation) { + inst.trigger('rotate', ev); + } - // we add the active sector to the sectors array to be able to revert these steps later on - this._setActiveSector(unqiueIdentifier); + // trigger pinch event + if(scale_threshold > inst.options.transform_min_scale) { + inst.trigger('pinch', ev); + inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev); + } + break; - // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier - this._switchToSector(this._sector()); + case Hammer.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } - // finally we add the node we removed from our previous active sector to the new active sector - this.nodes[node.id] = node; + this.triggered = false; + break; + } + } }; /** - * We close the sector that is currently open and revert back to the one before. - * If the active sector is the "default" sector, nothing happens. - * - * @private + * Touch + * Called as first, tells the user has touched the screen + * @events touch */ - exports._collapseSector = function() { - // the currently active sector - var sector = this._sector(); - - // we cannot collapse the default sector - if (sector != "default") { - if ((this.nodeIndices.length == 1) || - (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || - (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { - var previousSector = this._previousSector(); - - // we collapse the sector back to a single cluster - this._collapseThisToSingleCluster(); + Hammer.gestures.Touch = { + name: 'touch', + index: -Infinity, + defaults: { + // call preventDefault at touchstart, and makes the element blocking by + // disabling the scrolling of the page, but it improves gestures like + // transforming and dragging. + // be careful with using this, it can be very annoying for users to be stuck + // on the page + prevent_default: false, - // we move the remaining nodes, edges and nodeIndices to the previous sector. - // This previous sector is the one we will reactivate - this._mergeThisWithFrozen(previousSector); + // disable mouse events, so only touch (or pen!) input triggers events + prevent_mouseevents: false + }, + handler: function touchGesture(ev, inst) { + if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) { + ev.stopDetect(); + return; + } - // the previously active (frozen) sector now has all the data from the currently active sector. - // we can now delete the active sector. - this._deleteActiveSector(sector); + if(inst.options.prevent_default) { + ev.preventDefault(); + } - // we activate the previously active (and currently frozen) sector. - this._activateSector(previousSector); + if(ev.eventType == Hammer.EVENT_START) { + inst.trigger(this.name, ev); + } + } + }; - // we load the references from the newly active sector into the global references - this._switchToSector(previousSector); - // we forget the previously active sector because we reverted to the one before - this._forgetLastSector(); + /** + * Release + * Called as last, tells the user has released the screen + * @events release + */ + Hammer.gestures.Release = { + name: 'release', + index: Infinity, + handler: function releaseGesture(ev, inst) { + if(ev.eventType == Hammer.EVENT_END) { + inst.trigger(this.name, ev); + } + } + }; - // finally, we update the node index list. - this._updateNodeIndexList(); + // node export + if(typeof module === 'object' && typeof module.exports === 'object'){ + module.exports = Hammer; + } + // just window export + else { + window.Hammer = Hammer; - // we refresh the list with calulation nodes and calculation node indices. - this._updateCalculationNodes(); + // requireJS module definition + if(typeof window.define === 'function' && window.define.amd) { + window.define('hammer', [], function() { + return Hammer; + }); } - } - }; + } + })(this); +/***/ }, +/* 44 */ +/***/ function(module, exports, __webpack_require__) { /** - * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * Creation of the ClusterMixin var. * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we dont pass the function itself because then the "this" is the window object - * | instead of the Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private + * This contains all the functions the Network object can use to employ clustering */ - exports._doInAllActiveSectors = function(runFunction,argument) { - if (argument === undefined) { - for (var sector in this.sectors["active"]) { - if (this.sectors["active"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToActiveSector(sector); - this[runFunction](); - } - } - } - else { - for (var sector in this.sectors["active"]) { - if (this.sectors["active"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToActiveSector(sector); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - } - } - // we revert the global references back to our active sector - this._loadLatestSector(); - }; + /** + * This is only called in the constructor of the network object + * + */ + exports.startWithClustering = function() { + // cluster if the data set is big + this.clusterToFit(this.constants.clustering.initialMaxNodes, true); + + // updates the lables after clustering + this.updateLabels(); + + // this is called here because if clusterin is disabled, the start and stabilize are called in + // the setData function. + if (this.stabilize) { + this._stabilize(); + } + this.start(); + }; /** - * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * This function clusters until the initialMaxNodes has been reached * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we dont pass the function itself because then the "this" is the window object - * | instead of the Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private + * @param {Number} maxNumberOfNodes + * @param {Boolean} reposition */ - exports._doInSupportSector = function(runFunction,argument) { - if (argument === undefined) { - this._switchToSupportSector(); - this[runFunction](); - } - else { - this._switchToSupportSector(); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); + exports.clusterToFit = function(maxNumberOfNodes, reposition) { + var numberOfNodes = this.nodeIndices.length; + + var maxLevels = 50; + var level = 0; + + // we first cluster the hubs, then we pull in the outliers, repeat + while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { + if (level % 3 == 0) { + this.forceAggregateHubs(true); + this.normalizeClusterLevels(); } else { - this[runFunction](argument); + this.increaseClusterLevel(); // this also includes a cluster normalization } + + numberOfNodes = this.nodeIndices.length; + level += 1; } - // we revert the global references back to our active sector - this._loadLatestSector(); - }; + // after the clustering we reposition the nodes to reduce the initial chaos + if (level > 0 && reposition == true) { + this.repositionNodes(); + } + this._updateCalculationNodes(); + }; /** - * This runs a function in all frozen sectors. This is used in the _redraw(). + * This function can be called to open up a specific cluster. It is only called by + * It will unpack the cluster back one level. * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private + * @param node | Node object: cluster to open. */ - exports._doInAllFrozenSectors = function(runFunction,argument) { - if (argument === undefined) { - for (var sector in this.sectors["frozen"]) { - if (this.sectors["frozen"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToFrozenSector(sector); - this[runFunction](); - } - } + exports.openCluster = function(node) { + var isMovingBeforeClustering = this.moving; + if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && + !(this._sector() == "default" && this.nodeIndices.length == 1)) { + // this loads a new sector, loads the nodes and edges and nodeIndices of it. + this._addSector(node); + var level = 0; + + // we decluster until we reach a decent number of nodes + while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { + this.decreaseClusterLevel(); + level += 1; + } + } else { - for (var sector in this.sectors["frozen"]) { - if (this.sectors["frozen"].hasOwnProperty(sector)) { - // switch the global references to those of this sector - this._switchToFrozenSector(sector); - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - } + this._expandClusterNode(node,false,true); + + // update the index list, dynamic edges and labels + this._updateNodeIndexList(); + this._updateDynamicEdges(); + this._updateCalculationNodes(); + this.updateLabels(); + } + + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); } - this._loadLatestSector(); }; /** - * This runs a function in all sectors. This is used in the _redraw(). - * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Network object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private + * This calls the updateClustes with default arguments */ - exports._doInAllSectors = function(runFunction,argument) { - var args = Array.prototype.splice.call(arguments, 1); - if (argument === undefined) { - this._doInAllActiveSectors(runFunction); - this._doInAllFrozenSectors(runFunction); - } - else { - if (args.length > 1) { - this._doInAllActiveSectors(runFunction,args[0],args[1]); - this._doInAllFrozenSectors(runFunction,args[0],args[1]); - } - else { - this._doInAllActiveSectors(runFunction,argument); - this._doInAllFrozenSectors(runFunction,argument); - } + exports.updateClustersDefault = function() { + if (this.constants.clustering.enabled == true) { + this.updateClusters(0,false,false); } }; /** - * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the - * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. - * - * @private + * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will + * be clustered with their connected node. This can be repeated as many times as needed. + * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. */ - exports._clearNodeIndexList = function() { - var sector = this._sector(); - this.sectors["active"][sector]["nodeIndices"] = []; - this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; + exports.increaseClusterLevel = function() { + this.updateClusters(-1,false,true); }; /** - * Draw the encompassing sector node + * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will + * be unpacked if they are a cluster. This can be repeated as many times as needed. + * This can be called externally (by a key-bind for instance) to look into clusters without zooming. + */ + exports.decreaseClusterLevel = function() { + this.updateClusters(1,false,true); + }; + + + /** + * This is the main clustering function. It clusters and declusters on zoom or forced + * This function clusters on zoom, it can be called with a predefined zoom direction + * If out, check if we can form clusters, if in, check if we can open clusters. + * This function is only called from _zoom() + * + * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn + * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters + * @param {Boolean} force | enabled or disable forcing + * @param {Boolean} doNotStart | if true do not call start * - * @param ctx - * @param sectorType - * @private */ - exports._drawSectorNodes = function(ctx,sectorType) { - var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; - for (var sector in this.sectors[sectorType]) { - if (this.sectors[sectorType].hasOwnProperty(sector)) { - if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { + exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) { + var isMovingBeforeClustering = this.moving; + var amountOfNodes = this.nodeIndices.length; - this._switchToSector(sector,sectorType); + // on zoom out collapse the sector if the scale is at the level the sector was made + if (this.previousScale > this.scale && zoomDirection == 0) { + this._collapseSector(); + } - minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.resize(ctx); - if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} - if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} - if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} - if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} - } - } - node = this.sectors[sectorType][sector]["drawingNode"]; - node.x = 0.5 * (maxX + minX); - node.y = 0.5 * (maxY + minY); - node.width = 2 * (node.x - minX); - node.height = 2 * (node.y - minY); - node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); - node.setScale(this.scale); - node._drawCircle(ctx); - } + // check if we zoom in or out + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + // forming clusters when forced pulls outliers in. When not forced, the edge length of the + // outer nodes determines if it is being clustered + this._formClusters(force); + } + else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in + if (force == true) { + // _openClusters checks for each node if the formationScale of the cluster is smaller than + // the current scale and if so, declusters. When forced, all clusters are reduced by one step + this._openClusters(recursive,force); + } + else { + // if a cluster takes up a set percentage of the active window + this._openClustersBySize(); } } - }; + this._updateNodeIndexList(); - exports._drawAllSectorNodes = function(ctx) { - this._drawSectorNodes(ctx,"frozen"); - this._drawSectorNodes(ctx,"active"); - this._loadLatestSector(); - }; + // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs + if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { + this._aggregateHubs(force); + this._updateNodeIndexList(); + } + // we now reduce chains. + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + this.handleChains(); + this._updateNodeIndexList(); + } -/***/ }, -/* 45 */ -/***/ function(module, exports, __webpack_require__) { + this.previousScale = this.scale; - var Node = __webpack_require__(30); + // rest of the update the index list, dynamic edges and labels + this._updateDynamicEdges(); + this.updateLabels(); - /** - * This function can be called from the _doInAllSectors function - * - * @param object - * @param overlappingNodes - * @private - */ - exports._getNodesOverlappingWith = function(object, overlappingNodes) { - var nodes = this.nodes; - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); - } + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place + this.clusterSession += 1; + // if clusters have been made, we normalize the cluster level + this.normalizeClusterLevels(); + } + + if (doNotStart == false || doNotStart === undefined) { + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); } } + + this._updateCalculationNodes(); }; /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private + * This function handles the chains. It is called on every updateClusters(). */ - exports._getAllNodesOverlappingWith = function (object) { - var overlappingNodes = []; - this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); - return overlappingNodes; - }; + exports.handleChains = function() { + // after clustering we check how many chains there are + var chainPercentage = this._getChainFraction(); + if (chainPercentage > this.constants.clustering.chainThreshold) { + this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage) + } + }; /** - * Return a position object in canvasspace from a single point in screenspace + * this functions starts clustering by hubs + * The minimum hub threshold is set globally * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} * @private */ - exports._pointerToPositionObject = function(pointer) { - var x = this._XconvertDOMtoCanvas(pointer.x); - var y = this._YconvertDOMtoCanvas(pointer.y); - - return { - left: x, - top: y, - right: x, - bottom: y - }; + exports._aggregateHubs = function(force) { + this._getHubSize(); + this._formClustersByHub(force,false); }; /** - * Get the top node at the a specific point (like a click) + * This function is fired by keypress. It forces hubs to form. * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node - * @private */ - exports._getNodeAt = function (pointer) { - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + exports.forceAggregateHubs = function(doNotStart) { + var isMovingBeforeClustering = this.moving; + var amountOfNodes = this.nodeIndices.length; - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } - else { - return null; + this._aggregateHubs(true); + + // update the index list, dynamic edges and labels + this._updateNodeIndexList(); + this._updateDynamicEdges(); + this.updateLabels(); + + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length != amountOfNodes) { + this.clusterSession += 1; } - }; + if (doNotStart == false || doNotStart === undefined) { + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + } + }; /** - * 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 + * If a cluster takes up more than a set percentage of the screen, open the cluster + * * @private */ - exports._getEdgesOverlappingWith = function (object, overlappingEdges) { - var edges = this.edges; - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); + exports._openClustersBySize = function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.inView() == true) { + if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { + this.openCluster(node); + } } } } @@ -23520,3150 +23447,3242 @@ return /******/ (function(modules) { // webpackBootstrap /** - * 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 + * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it + * has to be opened based on the current zoom level. + * * @private */ - exports._getAllEdgesOverlappingWith = function (object) { - var overlappingEdges = []; - this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); - return overlappingEdges; + exports._openClusters = function(recursive,force) { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + this._expandClusterNode(node,recursive,force); + this._updateCalculationNodes(); + } }; /** - * 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 function checks if a node has to be opened. This is done by checking the zoom level. + * If the node contains child nodes, this function is recursively called on the child nodes as well. + * This recursive behaviour is optional and can be set by the recursive argument. * - * @param pointer - * @returns {null} + * @param {Node} parentNode | to check for cluster and expand + * @param {Boolean} recursive | enabled or disable recursive calling + * @param {Boolean} force | enabled or disable forcing + * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released * @private */ - exports._getEdgeAt = function(pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + exports._expandClusterNode = function(parentNode, recursive, force, openAll) { + // first check if node is a cluster + if (parentNode.clusterSize > 1) { + // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 + if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) { + openAll = true; + } + recursive = openAll ? true : recursive; - if (overlappingEdges.length > 0) { - return this.edges[overlappingEdges[overlappingEdges.length - 1]]; - } - else { - return null; + // if the last child has been added on a smaller scale than current scale decluster + if (parentNode.formationScale < this.scale || force == true) { + // we will check if any of the contained child nodes should be removed from the cluster + for (var containedNodeId in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) { + var childNode = parentNode.containedNodes[containedNodeId]; + + // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that + // the largest cluster is the one that comes from outside + if (force == true) { + if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] + || openAll) { + this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + } + } + else { + if (this._nodeInActiveArea(parentNode)) { + this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + } + } + } + } + } } }; - /** - * Add object to the selection array. + * ONLY CALLED FROM _expandClusterNode * - * @param obj + * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove + * the child node from the parent contained_node object and put it back into the global nodes object. + * The same holds for the edge that was connected to the child node. It is moved back into the global edges object. + * + * @param {Node} parentNode | the parent node + * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node + * @param {Boolean} recursive | This will also check if the child needs to be expanded. + * With force and recursive both true, the entire cluster is unpacked + * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent + * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released * @private */ - exports._addToSelection = function(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; + exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) { + var childNode = parentNode.containedNodes[containedNodeId]; + + // if child node has been added on smaller scale than current, kick out + if (childNode.formationScale < this.scale || force == true) { + // unselect all selected items + this._unselectAll(); + + // put the child node back in the global nodes object + this.nodes[containedNodeId] = childNode; + + // release the contained edges from this childNode back into the global edges + this._releaseContainedEdges(parentNode,childNode); + + // reconnect rerouted edges to the childNode + this._connectEdgeBackToChild(parentNode,childNode); + + // validate all edges in dynamicEdges + this._validateEdges(parentNode); + + // undo the changes from the clustering operation on the parent node + parentNode.mass -= childNode.mass; + parentNode.clusterSize -= childNode.clusterSize; + parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); + parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; + + // place the child node near the parent, not at the exact same location to avoid chaos in the system + childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()); + childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()); + + // remove node from the list + delete parentNode.containedNodes[containedNodeId]; + + // check if there are other childs with this clusterSession in the parent. + var othersPresent = false; + for (var childNodeId in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(childNodeId)) { + if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) { + othersPresent = true; + break; + } + } + } + // if there are no others, remove the cluster session from the list + if (othersPresent == false) { + parentNode.clusterSessions.pop(); + } + + this._repositionBezierNodes(childNode); + // this._repositionBezierNodes(parentNode); + + // remove the clusterSession from the child node + childNode.clusterSession = 0; + + // recalculate the size of the node on the next time the node is rendered + parentNode.clearSizeCache(); + + // restart the simulation to reorganise all nodes + this.moving = true; } - else { - this.selectionObj.edges[obj.id] = obj; + + // check if a further expansion step is possible if recursivity is enabled + if (recursive == true) { + this._expandClusterNode(childNode,recursive,force,openAll); } }; + /** - * Add object to the selection array. + * position the bezier nodes at the center of the edges * - * @param obj + * @param node * @private */ - exports._addToHover = function(obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; - } - else { - this.hoverObj.edges[obj.id] = obj; + exports._repositionBezierNodes = function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + node.dynamicEdges[i].positionBezierNode(); } }; /** - * Remove a single option from selection. + * This function checks if any nodes at the end of their trees have edges below a threshold length + * This function is called only from updateClusters() + * forceLevelCollapse ignores the length of the edge and collapses one level + * This means that a node with only one edge will be clustered with its connected node * - * @param {Object} obj * @private + * @param {Boolean} force */ - exports._removeFromSelection = function(obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; + exports._formClusters = function(force) { + if (force == false) { + this._formClustersByZoom(); } else { - delete this.selectionObj.edges[obj.id]; + this._forceClustersByZoom(); } }; + /** - * Unselect all. The selectionObj is useful for this. + * This function handles the clustering by zooming out, this is based on a minimum edge distance * - * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - exports._unselectAll = function(doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - 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(); - } - } + exports._formClustersByZoom = function() { + var dx,dy,length, + minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; - this.selectionObj = {nodes:{},edges:{}}; + // check if any edges are shorter than minLength and start the clustering + // the clustering favours the node with the larger mass + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + var edge = this.edges[edgeId]; + if (edge.connected) { + if (edge.toId != edge.fromId) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); - } - }; - /** - * Unselect all clusters. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._unselectClusters = function(doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } + if (length < minLength) { + // first check which node is larger + var parentNode = edge.from; + var childNode = edge.to; + if (edge.to.mass > edge.from.mass) { + parentNode = edge.to; + childNode = edge.from; + } - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - this.selectionObj.nodes[nodeId].unselect(); - this._removeFromSelection(this.selectionObj.nodes[nodeId]); + if (childNode.dynamicEdgesLength == 1) { + this._addToCluster(parentNode,childNode,false); + } + else if (parentNode.dynamicEdgesLength == 1) { + this._addToCluster(childNode,parentNode,false); + } + } + } } } } - - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); - } }; - /** - * return the number of selected nodes + * This function forces the network to cluster all nodes with only one connecting edge to their + * connected node. * - * @returns {number} * @private */ - exports._getSelectedNodeCount = function() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - return count; - }; + exports._forceClustersByZoom = function() { + for (var nodeId in this.nodes) { + // another node could have absorbed this child. + if (this.nodes.hasOwnProperty(nodeId)) { + var childNode = this.nodes[nodeId]; - /** - * return the selected node - * - * @returns {number} - * @private - */ - exports._getSelectedNode = function() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } - } - return null; - }; + // the edges can be swallowed by another decrease + if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) { + var edge = childNode.dynamicEdges[0]; + var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; - /** - * return the selected edge - * - * @returns {number} - * @private - */ - exports._getSelectedEdge = function() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; + // group to the largest node + if (childNode.id != parentNode.id) { + if (parentNode.mass > childNode.mass) { + this._addToCluster(parentNode,childNode,true); + } + else { + this._addToCluster(childNode,parentNode,true); + } + } + } } } - return null; }; /** - * return the number of selected edges + * To keep the nodes of roughly equal size we normalize the cluster levels. + * This function clusters a node to its smallest connected neighbour. * - * @returns {number} + * @param node * @private */ - exports._getSelectedEdgeCount = function() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; + exports._clusterToSmallestNeighbour = function(node) { + var smallestNeighbour = -1; + var smallestNeighbourNode = null; + for (var i = 0; i < node.dynamicEdges.length; i++) { + if (node.dynamicEdges[i] !== undefined) { + var neighbour = null; + if (node.dynamicEdges[i].fromId != node.id) { + neighbour = node.dynamicEdges[i].from; + } + else if (node.dynamicEdges[i].toId != node.id) { + neighbour = node.dynamicEdges[i].to; + } + + + if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) { + smallestNeighbour = neighbour.clusterSessions.length; + smallestNeighbourNode = neighbour; + } } } - return count; + + if (neighbour != null && this.nodes[neighbour.id] !== undefined) { + this._addToCluster(neighbour, node, true); + } }; /** - * return the number of selected objects. + * This function forms clusters from hubs, it loops over all nodes * - * @returns {number} + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges * @private */ - exports._getSelectedObjectCount = function() { - 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; + exports._formClustersByHub = function(force, onlyEqual) { + // we loop over all nodes in the list + for (var nodeId in this.nodes) { + // we check if it is still available since it can be used by the clustering in this loop + if (this.nodes.hasOwnProperty(nodeId)) { + this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); } } - return count; }; /** - * Check if anything is selected + * This function forms a cluster from a specific preselected hub node * - * @returns {boolean} + * @param {Node} hubNode | the node we will cluster as a hub + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @param {Number} [absorptionSizeOffset] | * @private */ - exports._selectionIsEmpty = function() { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; - } + exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) { + if (absorptionSizeOffset === undefined) { + absorptionSizeOffset = 0; } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; + // we decide if the node is a hub + if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || + (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { + // initialize variables + var dx,dy,length; + var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; + var allowCluster = false; + + // we create a list of edges because the dynamicEdges change over the course of this loop + var edgesIdarray = []; + var amountOfInitialEdges = hubNode.dynamicEdges.length; + for (var j = 0; j < amountOfInitialEdges; j++) { + edgesIdarray.push(hubNode.dynamicEdges[j].id); } - } - return true; - }; + // if the hub clustering is not forces, we check if one of the edges connected + // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold + if (force == false) { + allowCluster = false; + for (j = 0; j < amountOfInitialEdges; j++) { + var edge = this.edges[edgesIdarray[j]]; + if (edge !== undefined) { + if (edge.connected) { + if (edge.toId != edge.fromId) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); - /** - * check if one of the selected nodes is a cluster. - * - * @returns {boolean} - * @private - */ - exports._clusterInSelection = function() { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; + if (length < minLength) { + allowCluster = true; + break; + } + } + } + } } } - } - return false; - }; - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - exports._selectConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.select(); - this._addToSelection(edge); + // start the clustering if allowed + if ((!force && allowCluster) || force) { + // we loop over all edges INITIALLY connected to this hub + for (j = 0; j < amountOfInitialEdges; j++) { + edge = this.edges[edgesIdarray[j]]; + // the edge can be clustered by this function in a previous loop + if (edge !== undefined) { + var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; + // we do not want hubs to merge with other hubs nor do we want to cluster itself. + if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) && + (childNode.id != hubNode.id)) { + this._addToCluster(hubNode,childNode,force); + } + } + } + } } }; - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - exports._hoverConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.hover = true; - this._addToHover(edge); - } - }; /** - * unselect the edges connected to the node that is being selected + * This function adds the child node to the parent node, creating a cluster if it is not already. * - * @param {Node} node + * @param {Node} parentNode | this is the node that will house the child node + * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node + * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse * @private */ - exports._unselectConnectedEdges = function(node) { - for (var i = 0; i < node.dynamicEdges.length; i++) { - var edge = node.dynamicEdges[i]; - edge.unselect(); - this._removeFromSelection(edge); + exports._addToCluster = function(parentNode, childNode, force) { + // join child node in the parent node + parentNode.containedNodes[childNode.id] = childNode; + + // manage all the edges connected to the child and parent nodes + for (var i = 0; i < childNode.dynamicEdges.length; i++) { + var edge = childNode.dynamicEdges[i]; + if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode + this._addToContainedEdges(parentNode,childNode,edge); + } + else { + this._connectEdgeToCluster(parentNode,childNode,edge); + } } - }; + // a contained node has no dynamic edges. + childNode.dynamicEdges = []; + // remove circular edges from clusters + this._containCircularEdgesFromNode(parentNode,childNode); + // remove the childNode from the global nodes object + delete this.nodes[childNode.id]; - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @param {Boolean} append - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._selectObject = function(object, append, doNotTrigger, highlightEdges) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - if (highlightEdges === undefined) { - highlightEdges = true; - } + // update the properties of the child and parent + var massBefore = parentNode.mass; + childNode.clusterSession = this.clusterSession; + parentNode.mass += childNode.mass; + parentNode.clusterSize += childNode.clusterSize; + parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize); - if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { - this._unselectAll(true); + // keep track of the clustersessions so we can open the cluster up as it has been formed. + if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { + parentNode.clusterSessions.push(this.clusterSession); } - if (object.selected == false) { - object.select(); - this._addToSelection(object); - if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { - this._selectConnectedEdges(object); - } + // forced clusters only open from screen size and double tap + if (force == true) { + // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); + parentNode.formationScale = 0; } else { - object.unselect(); - this._removeFromSelection(object); + parentNode.formationScale = this.scale; // The latest child has been added on this scale } - if (doNotTrigger == false) { - this.emit('select', this.getSelection()); - } - }; + // recalculate the size of the node on the next time the node is rendered + parentNode.clearSizeCache(); + // set the pop-out scale for the childnode + parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale; - /** - * 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 - */ - exports._blurObject = function(object) { - if (object.hover == true) { - object.hover = false; - this.emit("blurNode",{node:object.id}); - } + // nullify the movement velocity of the child, this is to avoid hectic behaviour + childNode.clearVelocity(); + + // the mass has altered, preservation of energy dictates the velocity to be updated + parentNode.updateVelocity(massBefore); + + // restart the simulation to reorganise all nodes + this.moving = true; }; + /** - * This 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 + * This function will apply the changes made to the remainingEdges during the formation of the clusters. + * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree. + * It has to be called if a level is collapsed. It is called by _formClusters(). * @private */ - exports._hoverObject = function(object) { - if (object.hover == false) { - object.hover = true; - this._addToHover(object); - if (object instanceof Node) { - this.emit("hoverNode",{node:object.id}); + exports._updateDynamicEdges = function() { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + node.dynamicEdgesLength = node.dynamicEdges.length; + + // this corrects for multiple edges pointing at the same other node + var correction = 0; + if (node.dynamicEdgesLength > 1) { + for (var j = 0; j < node.dynamicEdgesLength - 1; j++) { + var edgeToId = node.dynamicEdges[j].toId; + var edgeFromId = node.dynamicEdges[j].fromId; + for (var k = j+1; k < node.dynamicEdgesLength; k++) { + if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) || + (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) { + correction += 1; + } + } + } } - } - if (object instanceof Node) { - this._hoverConnectedEdges(object); + node.dynamicEdgesLength -= correction; } }; /** - * handles the selection part of the touch, only for navigation controls elements; - * Touch is triggered before tap, also before hold. Hold triggers after a while. - * This is the most responsive solution + * This adds an edge from the childNode to the contained edges of the parent node * - * @param {Object} pointer + * @param parentNode | Node object + * @param childNode | Node object + * @param edge | Edge object * @private */ - exports._handleTouch = function(pointer) { - }; + exports._addToContainedEdges = function(parentNode, childNode, edge) { + // create an array object if it does not yet exist for this childNode + if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) { + parentNode.containedEdges[childNode.id] = [] + } + // add this edge to the list + parentNode.containedEdges[childNode.id].push(edge); + + // remove the edge from the global edges object + delete this.edges[edge.id]; + // remove the edge from the parent object + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + if (parentNode.dynamicEdges[i].id == edge.id) { + parentNode.dynamicEdges.splice(i,1); + break; + } + } + }; /** - * handles the selection part of the tap; + * This function connects an edge that was connected to a child node to the parent node. + * It keeps track of which nodes it has been connected to with the originalId array. * - * @param {Object} pointer + * @param {Node} parentNode | Node object + * @param {Node} childNode | Node object + * @param {Edge} edge | Edge object * @private */ - exports._handleTap = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node,false); + exports._connectEdgeToCluster = function(parentNode, childNode, edge) { + // handle circular edges + if (edge.toId == edge.fromId) { + this._addToContainedEdges(parentNode, childNode, edge); } else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge,false); - } - else { - this._unselectAll(); + if (edge.toId == childNode.id) { // edge connected to other node on the "to" side + edge.originalToId.push(childNode.id); + edge.to = parentNode; + edge.toId = parentNode.id; } - } - this.emit("click", this.getSelection()); - this._redraw(); - }; + else { // edge connected to other node with the "from" side + edge.originalFromId.push(childNode.id); + edge.from = parentNode; + edge.fromId = parentNode.id; + } - /** - * handles the selection part of the double tap and opens a cluster if needed - * - * @param {Object} pointer - * @private - */ - exports._handleDoubleTap = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null && node !== undefined) { - // we reset the areaCenter here so the opening of the node will occur - this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), - "y" : this._YconvertDOMtoCanvas(pointer.y)}; - this.openCluster(node); + this._addToReroutedEdges(parentNode,childNode,edge); } - this.emit("doubleClick", this.getSelection()); }; /** - * Handle the onHold selection part + * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain + * these edges inside of the cluster. * - * @param pointer + * @param parentNode + * @param childNode * @private */ - exports._handleOnHold = function(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node,true); - } - else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge,true); + exports._containCircularEdgesFromNode = function(parentNode, childNode) { + // manage all the edges connected to the child and parent nodes + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + var edge = parentNode.dynamicEdges[i]; + // handle circular edges + if (edge.toId == edge.fromId) { + this._addToContainedEdges(parentNode, childNode, edge); } } - this._redraw(); }; /** - * handle the onRelease event. These functions are here for the navigation controls module. + * This adds an edge from the childNode to the rerouted edges of the parent node * - * @private + * @param parentNode | Node object + * @param childNode | Node object + * @param edge | Edge object + * @private */ - exports._handleOnRelease = function(pointer) { + exports._addToReroutedEdges = function(parentNode, childNode, edge) { + // create an array object if it does not yet exist for this childNode + // we store the edge in the rerouted edges so we can restore it when the cluster pops open + if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) { + parentNode.reroutedEdges[childNode.id] = []; + } + parentNode.reroutedEdges[childNode.id].push(edge); - }; + // this edge becomes part of the dynamicEdges of the cluster node + parentNode.dynamicEdges.push(edge); + }; /** + * This function connects an edge that was connected to a cluster node back to the child node. * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection + * @param parentNode | Node object + * @param childNode | Node object + * @private */ - exports.getSelection = function() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return {nodes:nodeIds, edges:edgeIds}; - }; + exports._connectEdgeBackToChild = function(parentNode, childNode) { + if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) { + for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) { + var edge = parentNode.reroutedEdges[childNode.id][i]; + if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) { + edge.originalFromId.pop(); + edge.fromId = childNode.id; + edge.from = childNode; + } + else { + edge.originalToId.pop(); + edge.toId = childNode.id; + edge.to = childNode; + } - /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. - */ - exports.getSelectedNodes = function() { - var idArray = []; - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); + // append this edge to the list of edges connecting to the childnode + childNode.dynamicEdges.push(edge); + + // remove the edge from the parent object + for (var j = 0; j < parentNode.dynamicEdges.length; j++) { + if (parentNode.dynamicEdges[j].id == edge.id) { + parentNode.dynamicEdges.splice(j,1); + break; + } + } } + // remove the entry from the rerouted edges + delete parentNode.reroutedEdges[childNode.id]; } - return idArray }; + /** + * When loops are clustered, an edge can be both in the rerouted array and the contained array. + * This function is called last to verify that all edges in dynamicEdges are in fact connected to the + * parentNode * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. + * @param parentNode | Node object + * @private */ - exports.getSelectedEdges = function() { - var idArray = []; - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); + exports._validateEdges = function(parentNode) { + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + var edge = parentNode.dynamicEdges[i]; + if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { + parentNode.dynamicEdges.splice(i,1); } } - return idArray; }; /** - * select zero or more nodes - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. + * This function released the contained edges back into the global domain and puts them back into the + * dynamic edges of both parent and child. + * + * @param {Node} parentNode | + * @param {Node} childNode | + * @private */ - exports.setSelection = function(selection) { - var i, iMax, id; - - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; - - // first unselect any selected node - this._unselectAll(true); + exports._releaseContainedEdges = function(parentNode, childNode) { + for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) { + var edge = parentNode.containedEdges[childNode.id][i]; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + // put the edge back in the global edges object + this.edges[edge.id] = edge; - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); - } - this._selectObject(node,true,true); + // put the edge back in the dynamic edges of the child and parent + childNode.dynamicEdges.push(edge); + parentNode.dynamicEdges.push(edge); } + // remove the entry from the contained edges + delete parentNode.containedEdges[childNode.id]; - console.log("setSelection is deprecated. Please use selectNodes instead.") - - this.redraw(); }; - /** - * 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] - */ - exports.selectNodes = function(selection, highlightEdges) { - var i, iMax, id; - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; - // first unselect any selected node - this._unselectAll(true); + // ------------------- UTILITY FUNCTIONS ---------------------------- // - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); + /** + * This updates the node labels for all nodes (for debugging purposes) + */ + exports.updateLabels = function() { + var nodeId; + // update node labels + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.clusterSize > 1) { + node.label = "[".concat(String(node.clusterSize),"]"); + } } - this._selectObject(node,true,true,highlightEdges); } - this.redraw(); - }; + // update node labels + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.clusterSize == 1) { + if (node.originalLabel !== undefined) { + node.label = node.originalLabel; + } + else { + node.label = String(node.id); + } + } + } + } - /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - exports.selectEdges = function(selection) { - var i, iMax, id; + // /* Debug Override */ + // for (nodeId in this.nodes) { + // if (this.nodes.hasOwnProperty(nodeId)) { + // node = this.nodes[nodeId]; + // node.label = String(node.level); + // } + // } - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; + }; - // first unselect any selected node - this._unselectAll(true); - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + /** + * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes + * if the rest of the nodes are already a few cluster levels in. + * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not + * clustered enough to the clusterToSmallestNeighbours function. + */ + exports.normalizeClusterLevels = function() { + var maxLevel = 0; + var minLevel = 1e9; + var clusterLevel = 0; + var nodeId; - var edge = this.edges[id]; - if (!edge) { - throw new RangeError('Edge with id "' + id + '" not found'); + // we loop over all nodes in the list + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + clusterLevel = this.nodes[nodeId].clusterSessions.length; + if (maxLevel < clusterLevel) {maxLevel = clusterLevel;} + if (minLevel > clusterLevel) {minLevel = clusterLevel;} } - this._selectObject(edge,true,true,highlightEdges); } - this.redraw(); - }; - /** - * Validate the selection: remove ids of nodes which no longer exist - * @private - */ - exports._updateSelection = function () { - for(var nodeId in this.selectionObj.nodes) { - if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; + if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) { + var amountOfNodes = this.nodeIndices.length; + var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference; + // we loop over all nodes in the list + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].clusterSessions.length < targetLevel) { + this._clusterToSmallestNeighbour(this.nodes[nodeId]); + } } } - } - for(var edgeId in this.selectionObj.edges) { - if(this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; - } + this._updateNodeIndexList(); + this._updateDynamicEdges(); + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length != amountOfNodes) { + this.clusterSession += 1; } } }; -/***/ }, -/* 46 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - var Node = __webpack_require__(30); - var Edge = __webpack_require__(27); /** - * clears the toolbar div element of children + * This function determines if the cluster we want to decluster is in the active area + * this means around the zoom center * + * @param {Node} node + * @returns {boolean} * @private */ - exports._clearManipulatorBar = function() { - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } + exports._nodeInActiveArea = function(node) { + return ( + Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale + && + Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale + ) }; + /** - * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore - * these functions to their original functionality, we saved them in this.cachedFunctions. - * This function restores these functions to their original function. + * This is an adaptation of the original repositioning function. This is called if the system is clustered initially + * It puts large clusters away from the center and randomizes the order. * - * @private */ - exports._restoreOverloadedFunctions = function() { - for (var functionName in this.cachedFunctions) { - if (this.cachedFunctions.hasOwnProperty(functionName)) { - this[functionName] = this.cachedFunctions[functionName]; - } - } - }; - - /** - * Enable or disable edit-mode. - * - * @private - */ - exports._toggleEditMode = function() { - this.editMode = !this.editMode; - var toolbar = document.getElementById("network-manipulationDiv"); - var closeDiv = document.getElementById("network-manipulation-closeDiv"); - var editModeDiv = document.getElementById("network-manipulation-editMode"); - if (this.editMode == true) { - toolbar.style.display="block"; - closeDiv.style.display="block"; - editModeDiv.style.display="none"; - closeDiv.onclick = this._toggleEditMode.bind(this); - } - else { - toolbar.style.display="none"; - closeDiv.style.display="none"; - editModeDiv.style.display="block"; - closeDiv.onclick = null; + exports.repositionNodes = function() { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + if ((node.xFixed == false || node.yFixed == false)) { + var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass); + var angle = 2 * Math.PI * Math.random(); + if (node.xFixed == false) {node.x = radius * Math.cos(angle);} + if (node.yFixed == false) {node.y = radius * Math.sin(angle);} + this._repositionBezierNodes(node); + } } - this._createManipulatorBar() }; + /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * 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 */ - exports._createManipulatorBar = function() { - // remove bound functions - if (this.boundFunction) { - this.off('select', this.boundFunction); - } - - if (this.edgeBeingEdited !== undefined) { - this.edgeBeingEdited._disableControlNodes(); - this.edgeBeingEdited = undefined; - this.selectedControlNode = null; - this.controlNodesActive = false; - } - - // restore overloaded functions - this._restoreOverloadedFunctions(); - - // resume calculation - this.freezeSimulation = false; + exports._getHubSize = function() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; - // reset global variables - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false; + for (var i = 0; i < this.nodeIndices.length; i++) { - if (this.editMode == true) { - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } - // add the icons to the manipulator div - this.manipulationDiv.innerHTML = "" + - "" + - ""+this.constants.labels['add'] +"" + - "
" + - "" + - ""+this.constants.labels['link'] +""; - if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - this.manipulationDiv.innerHTML += "" + - "
" + - "" + - ""+this.constants.labels['editNode'] +""; - } - else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { - this.manipulationDiv.innerHTML += "" + - "
" + - "" + - ""+this.constants.labels['editEdge'] +""; - } - if (this._selectionIsEmpty() == false) { - this.manipulationDiv.innerHTML += "" + - "
" + - "" + - ""+this.constants.labels['del'] +""; + var node = this.nodes[this.nodeIndices[i]]; + if (node.dynamicEdgesLength > largestHub) { + largestHub = node.dynamicEdgesLength; } + average += node.dynamicEdgesLength; + averageSquared += Math.pow(node.dynamicEdgesLength,2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; + var variance = averageSquared - Math.pow(average,2); - // bind the icons - var addNodeButton = document.getElementById("network-manipulate-addNode"); - addNodeButton.onclick = this._createAddNodeToolbar.bind(this); - var addEdgeButton = document.getElementById("network-manipulate-connectNode"); - addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); - if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - var editButton = document.getElementById("network-manipulate-editNode"); - editButton.onclick = this._editNode.bind(this); - } - else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { - var editButton = document.getElementById("network-manipulate-editEdge"); - editButton.onclick = this._createEditEdgeToolbar.bind(this); - } - if (this._selectionIsEmpty() == false) { - var deleteButton = document.getElementById("network-manipulate-delete"); - deleteButton.onclick = this._deleteSelected.bind(this); - } - var closeDiv = document.getElementById("network-manipulation-closeDiv"); - closeDiv.onclick = this._toggleEditMode.bind(this); + var standardDeviation = Math.sqrt(variance); - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - } - else { - this.editModeDiv.innerHTML = "" + - "" + - "" + this.constants.labels['edit'] + ""; - var editModeButton = document.getElementById("network-manipulate-editModeButton"); - editModeButton.onclick = this._toggleEditMode.bind(this); + this.hubThreshold = Math.floor(average + 2*standardDeviation); + + // always have at least one to cluster + if (this.hubThreshold > largestHub) { + this.hubThreshold = largestHub; } - }; + // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); + // console.log("hubThreshold:",this.hubThreshold); + }; /** - * Create the toolbar for adding Nodes + * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these chains. * + * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce * @private */ - exports._createAddNodeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); - if (this.boundFunction) { - this.off('select', this.boundFunction); + exports._reduceAmountOfChains = function(fraction) { + this.hubThreshold = 2; + var reduceAmount = Math.floor(this.nodeIndices.length * fraction); + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { + if (reduceAmount > 0) { + this._formClusterFromHub(this.nodes[nodeId],true,true,1); + reduceAmount -= 1; + } + } + } } - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "" + - "" + this.constants.labels['back'] + " " + - "
" + - "" + - "" + this.constants.labels['addDescription'] + ""; - - // bind the icon - var backButton = document.getElementById("network-manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._addNode.bind(this); - this.on('select', this.boundFunction); }; - /** - * create the toolbar to connect nodes + * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these chains. * * @private */ - exports._createAddEdgeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); - this._unselectAll(true); - this.freezeSimulation = true; - - if (this.boundFunction) { - this.off('select', this.boundFunction); + exports._getChainFraction = function() { + var chains = 0; + var total = 0; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { + chains += 1; + } + total += 1; + } } + return chains/total; + }; - this._unselectAll(); - this.forceAppendSelection = false; - this.blockConnectingEdgeSelection = true; - - this.manipulationDiv.innerHTML = "" + - "" + - "" + this.constants.labels['back'] + " " + - "
" + - "" + - "" + this.constants.labels['linkDescription'] + ""; - - // bind the icon - var backButton = document.getElementById("network-manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._handleConnect.bind(this); - this.on('select', this.boundFunction); +/***/ }, +/* 45 */ +/***/ function(module, exports, __webpack_require__) { - // temporarily overload functions - this.cachedFunctions["_handleTouch"] = this._handleTouch; - this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; - this._handleTouch = this._handleConnect; - this._handleOnRelease = this._finishConnect; + var util = __webpack_require__(1); - // redraw to show the unselect - this._redraw(); - }; + /** + * Creation of the SectorMixin var. + * + * This contains all the functions the Network object can use to employ the sector system. + * The sector system is always used by Network, though the benefits only apply to the use of clustering. + * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. + */ /** - * create the toolbar to edit edges + * This function is only called by the setData function of the Network object. + * This loads the global references into the active sector. This initializes the sector. * * @private */ - exports._createEditEdgeToolbar = function() { - // clear the toolbar - this._clearManipulatorBar(); - this.controlNodesActive = true; - - if (this.boundFunction) { - this.off('select', this.boundFunction); - } - - this.edgeBeingEdited = this._getSelectedEdge(); - this.edgeBeingEdited._enableControlNodes(); - - this.manipulationDiv.innerHTML = "" + - "" + - "" + this.constants.labels['back'] + " " + - "
" + - "" + - "" + this.constants.labels['editEdgeDescription'] + ""; - - // bind the icon - var backButton = document.getElementById("network-manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - - // temporarily overload functions - this.cachedFunctions["_handleTouch"] = this._handleTouch; - this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; - this.cachedFunctions["_handleTap"] = this._handleTap; - this.cachedFunctions["_handleDragStart"] = this._handleDragStart; - this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; - this._handleTouch = this._selectControlNode; - this._handleTap = function () {}; - this._handleOnDrag = this._controlNodeDrag; - this._handleDragStart = function () {} - this._handleOnRelease = this._releaseControlNode; - - // redraw to show the unselect - this._redraw(); - }; - - - + exports._putDataInSector = function() { + this.sectors["active"][this._sector()].nodes = this.nodes; + this.sectors["active"][this._sector()].edges = this.edges; + this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; + }; /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. + * /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied (active) sector. If a type is defined, do the specific type * + * @param {String} sectorId + * @param {String} [sectorType] | "active" or "frozen" * @private */ - exports._selectControlNode = function(pointer) { - this.edgeBeingEdited.controlNodes.from.unselect(); - this.edgeBeingEdited.controlNodes.to.unselect(); - this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); - if (this.selectedControlNode !== null) { - this.selectedControlNode.select(); - this.freezeSimulation = true; + exports._switchToSector = function(sectorId, sectorType) { + if (sectorType === undefined || sectorType == "active") { + this._switchToActiveSector(sectorId); + } + else { + this._switchToFrozenSector(sectorId); } - this._redraw(); }; + /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied active sector. * + * @param sectorId * @private */ - exports._controlNodeDrag = function(event) { - var pointer = this._getPointer(event.gesture.center); - if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { - this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); - this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); - } - this._redraw(); + exports._switchToActiveSector = function(sectorId) { + this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["active"][sectorId]["nodes"]; + this.edges = this.sectors["active"][sectorId]["edges"]; }; - exports._releaseControlNode = function(pointer) { - var newNode = this._getNodeAt(pointer); - if (newNode != null) { - if (this.edgeBeingEdited.controlNodes.from.selected == true) { - this._editEdge(newNode.id, this.edgeBeingEdited.to.id); - this.edgeBeingEdited.controlNodes.from.unselect(); - } - if (this.edgeBeingEdited.controlNodes.to.selected == true) { - this._editEdge(this.edgeBeingEdited.from.id, newNode.id); - this.edgeBeingEdited.controlNodes.to.unselect(); - } - } - else { - this.edgeBeingEdited._restoreControlNodes(); - } - this.freezeSimulation = false; - this._redraw(); - }; /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied active sector. * * @private */ - exports._handleConnect = function(pointer) { - if (this._getSelectedNodeCount() == 0) { - var node = this._getNodeAt(pointer); - if (node != null) { - if (node.clusterSize > 1) { - alert("Cannot create edges to a cluster.") - } - else { - this._selectObject(node,false); - // create a node the temporary line can look at - this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); - this.sectors['support']['nodes']['targetNode'].x = node.x; - this.sectors['support']['nodes']['targetNode'].y = node.y; - this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); - this.sectors['support']['nodes']['targetViaNode'].x = node.x; - this.sectors['support']['nodes']['targetViaNode'].y = node.y; - this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; - - // create a temporary edge - this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); - this.edges['connectionEdge'].from = node; - this.edges['connectionEdge'].connected = true; - this.edges['connectionEdge'].smooth = true; - this.edges['connectionEdge'].selected = true; - this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; - this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; - - this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; - this._handleOnDrag = function(event) { - var pointer = this._getPointer(event.gesture.center); - this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x); - this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y); - this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x); - this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y); - }; - - this.moving = true; - this.start(); - } - } - } + exports._switchToSupportSector = function() { + this.nodeIndices = this.sectors["support"]["nodeIndices"]; + this.nodes = this.sectors["support"]["nodes"]; + this.edges = this.sectors["support"]["edges"]; }; - exports._finishConnect = function(pointer) { - if (this._getSelectedNodeCount() == 1) { - - // restore the drag function - this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; - delete this.cachedFunctions["_handleOnDrag"]; - // remember the edge id - var connectFromId = this.edges['connectionEdge'].fromId; + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied frozen sector. + * + * @param sectorId + * @private + */ + exports._switchToFrozenSector = function(sectorId) { + this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["frozen"][sectorId]["nodes"]; + this.edges = this.sectors["frozen"][sectorId]["edges"]; + }; - // remove the temporary nodes and edge - delete this.edges['connectionEdge']; - delete this.sectors['support']['nodes']['targetNode']; - delete this.sectors['support']['nodes']['targetViaNode']; - var node = this._getNodeAt(pointer); - if (node != null) { - if (node.clusterSize > 1) { - alert("Cannot create edges to a cluster.") - } - else { - this._createEdge(connectFromId,node.id); - this._createManipulatorBar(); - } - } - this._unselectAll(); - } + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the currently active sector. + * + * @private + */ + exports._loadLatestSector = function() { + this._switchToSector(this._sector()); }; /** - * Adds a node on the specified location + * This function returns the currently active sector Id + * + * @returns {String} + * @private */ - exports._addNode = function() { - if (this._selectionIsEmpty() && this.editMode == true) { - var positionObject = this._pointerToPositionObject(this.pointerPosition); - var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; - if (this.triggerFunctions.add) { - if (this.triggerFunctions.add.length == 2) { - var me = this; - this.triggerFunctions.add(defaultData, function(finalizedData) { - me.nodesData.add(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels['addError']); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } - else { - this.nodesData.add(defaultData); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } + exports._sector = function() { + return this.activeSector[this.activeSector.length-1]; }; /** - * connect two nodes with a new edge. + * This function returns the previously active sector Id * + * @returns {String} * @private */ - exports._createEdge = function(sourceNodeId,targetNodeId) { - if (this.editMode == true) { - var defaultData = {from:sourceNodeId, to:targetNodeId}; - if (this.triggerFunctions.connect) { - if (this.triggerFunctions.connect.length == 2) { - var me = this; - this.triggerFunctions.connect(defaultData, function(finalizedData) { - me.edgesData.add(finalizedData); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels["linkError"]); - this.moving = true; - this.start(); - } - } - else { - this.edgesData.add(defaultData); - this.moving = true; - this.start(); - } + exports._previousSector = function() { + if (this.activeSector.length > 1) { + return this.activeSector[this.activeSector.length-2]; + } + else { + throw new TypeError('there are not enough sectors in the this.activeSector array.'); } }; + /** - * connect two nodes with a new edge. + * We add the active sector at the end of the this.activeSector array + * This ensures it is the currently active sector returned by _sector() and it reaches the top + * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. * + * @param newId * @private */ - exports._editEdge = function(sourceNodeId,targetNodeId) { - if (this.editMode == true) { - var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; - if (this.triggerFunctions.editEdge) { - if (this.triggerFunctions.editEdge.length == 2) { - var me = this; - this.triggerFunctions.editEdge(defaultData, function(finalizedData) { - me.edgesData.update(finalizedData); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels["linkError"]); - this.moving = true; - this.start(); - } - } - else { - this.edgesData.update(defaultData); - this.moving = true; - this.start(); - } - } + exports._setActiveSector = function(newId) { + this.activeSector.push(newId); }; + /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. + * We remove the currently active sector id from the active sector stack. This happens when + * we reactivate the previously active sector * * @private */ - exports._editNode = function() { - if (this.triggerFunctions.edit && this.editMode == true) { - var node = this._getSelectedNode(); - var data = {id:node.id, - label: node.label, - group: node.group, - shape: node.shape, - color: { - background:node.color.background, - border:node.color.border, - highlight: { - background:node.color.highlight.background, - border:node.color.highlight.border - } - }}; - if (this.triggerFunctions.edit.length == 2) { - var me = this; - this.triggerFunctions.edit(data, function (finalizedData) { - me.nodesData.update(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels["editError"]); - } - } - else { - alert(this.constants.labels["editBoundError"]); - } + exports._forgetLastSector = function() { + this.activeSector.pop(); }; - - /** - * delete everything in the selection + * This function creates a new active sector with the supplied newId. This newId + * is the expanding node id. * + * @param {String} newId | Id of the new active sector * @private */ - exports._deleteSelected = function() { - if (!this._selectionIsEmpty() && this.editMode == true) { - if (!this._clusterInSelection()) { - var selectedNodes = this.getSelectedNodes(); - var selectedEdges = this.getSelectedEdges(); - if (this.triggerFunctions.del) { - var me = this; - var data = {nodes: selectedNodes, edges: selectedEdges}; - if (this.triggerFunctions.del.length = 2) { - this.triggerFunctions.del(data, function (finalizedData) { - me.edgesData.remove(finalizedData.edges); - me.nodesData.remove(finalizedData.nodes); - me._unselectAll(); - me.moving = true; - me.start(); - }); - } - else { - alert(this.constants.labels["deleteError"]) + exports._createNewSector = function(newId) { + // create the new sector + this.sectors["active"][newId] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": this.scale, + "drawingNode": undefined}; + + // create the new sector render node. This gives visual feedback that you are in a new sector. + this.sectors["active"][newId]['drawingNode'] = new Node( + {id:newId, + color: { + background: "#eaefef", + border: "495c5e" } - } - else { - this.edgesData.remove(selectedEdges); - this.nodesData.remove(selectedNodes); - this._unselectAll(); - this.moving = true; - this.start(); - } - } - else { - alert(this.constants.labels["deleteClusterError"]); - } - } + },{},{},this.constants); + this.sectors["active"][newId]['drawingNode'].clusterSize = 2; }; -/***/ }, -/* 47 */ -/***/ function(module, exports, __webpack_require__) { - - exports._cleanNavigation = function() { - // clean up previous navigation items - var wrapper = document.getElementById('network-navigation_wrapper'); - if (wrapper != null) { - this.containerElement.removeChild(wrapper); - } - document.onmouseup = null; - }; - /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * This function removes the currently active sector. This is called when we create a new + * active sector. * + * @param {String} sectorId | Id of the active sector that will be removed * @private */ - exports._loadNavigationElements = function() { - this._cleanNavigation(); - - this.navigationDivs = {}; - var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; - var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; - - this.navigationDivs['wrapper'] = document.createElement('div'); - this.navigationDivs['wrapper'].id = "network-navigation_wrapper"; - this.navigationDivs['wrapper'].style.position = "absolute"; - this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; - this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; - this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); - - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDivs[navigationDivs[i]] = document.createElement('div'); - this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i]; - this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; - this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); - this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); - } - - document.onmouseup = this._stopMovement.bind(this); + exports._deleteActiveSector = function(sectorId) { + delete this.sectors["active"][sectorId]; }; + /** - * this stops all movement induced by the navigation buttons + * This function removes the currently active sector. This is called when we reactivate + * the previously active sector. * + * @param {String} sectorId | Id of the active sector that will be removed * @private */ - exports._stopMovement = function() { - this._xStopMoving(); - this._yStopMoving(); - this._stopZoom(); + exports._deleteFrozenSector = function(sectorId) { + delete this.sectors["frozen"][sectorId]; }; /** - * stops the actions performed by page up and down etc. + * Freezing an active sector means moving it from the "active" object to the "frozen" object. + * We copy the references, then delete the active entree. * - * @param event + * @param sectorId * @private */ - exports._preventDefault = function(event) { - if (event !== undefined) { - if (event.preventDefault) { - event.preventDefault(); - } else { - event.returnValue = false; - } - } + exports._freezeSector = function(sectorId) { + // we move the set references from the active to the frozen stack. + this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; + + // we have moved the sector data into the frozen set, we now remove it from the active set + this._deleteActiveSector(sectorId); }; /** - * move the screen up - * By using the increments, instead of adding a fixed number to the translation, we keep fluent and - * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently - * To avoid this behaviour, we do the translation in the start loop. + * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" + * object to the "active" object. * + * @param sectorId * @private */ - exports._moveUp = function(event) { - this.yIncrement = this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['up'].className += " active"; - } + exports._activateSector = function(sectorId) { + // we move the set references from the frozen to the active stack. + this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; + + // we have moved the sector data into the active set, we now remove it from the frozen stack + this._deleteFrozenSector(sectorId); }; /** - * move the screen down + * This function merges the data from the currently active sector with a frozen sector. This is used + * in the process of reverting back to the previously active sector. + * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it + * upon the creation of a new active sector. + * + * @param sectorId * @private */ - exports._moveDown = function(event) { - this.yIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['down'].className += " active"; + exports._mergeThisWithFrozen = function(sectorId) { + // copy all nodes + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; + } + } + + // copy all edges (if not fully clustered, else there are no edges) + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; + } + } + + // merge the nodeIndices + for (var i = 0; i < this.nodeIndices.length; i++) { + this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); } }; /** - * move the screen left + * This clusters the sector to one cluster. It was a single cluster before this process started so + * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. + * * @private */ - exports._moveLeft = function(event) { - this.xIncrement = this.constants.keyboard.speed.x; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['left'].className += " active"; - } + exports._collapseThisToSingleCluster = function() { + this.clusterToFit(1,false); }; /** - * move the screen right + * We create a new active sector from the node that we want to open. + * + * @param node * @private */ - exports._moveRight = function(event) { - this.xIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['right'].className += " active"; - } + exports._addSector = function(node) { + // this is the currently active sector + var sector = this._sector(); + + // // this should allow me to select nodes from a frozen set. + // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { + // console.log("the node is part of the active sector"); + // } + // else { + // console.log("I dont know what the fuck happened!!"); + // } + + // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. + delete this.nodes[node.id]; + + var unqiueIdentifier = util.randomUUID(); + + // we fully freeze the currently active sector + this._freezeSector(sector); + + // we create a new active sector. This sector has the Id of the node to ensure uniqueness + this._createNewSector(unqiueIdentifier); + + // we add the active sector to the sectors array to be able to revert these steps later on + this._setActiveSector(unqiueIdentifier); + + // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier + this._switchToSector(this._sector()); + + // finally we add the node we removed from our previous active sector to the new active sector + this.nodes[node.id] = node; }; /** - * Zoom in, using the same method as the movement. + * We close the sector that is currently open and revert back to the one before. + * If the active sector is the "default" sector, nothing happens. + * * @private */ - exports._zoomIn = function(event) { - this.zoomIncrement = this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['zoomIn'].className += " active"; + exports._collapseSector = function() { + // the currently active sector + var sector = this._sector(); + + // we cannot collapse the default sector + if (sector != "default") { + if ((this.nodeIndices.length == 1) || + (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { + var previousSector = this._previousSector(); + + // we collapse the sector back to a single cluster + this._collapseThisToSingleCluster(); + + // we move the remaining nodes, edges and nodeIndices to the previous sector. + // This previous sector is the one we will reactivate + this._mergeThisWithFrozen(previousSector); + + // the previously active (frozen) sector now has all the data from the currently active sector. + // we can now delete the active sector. + this._deleteActiveSector(sector); + + // we activate the previously active (and currently frozen) sector. + this._activateSector(previousSector); + + // we load the references from the newly active sector into the global references + this._switchToSector(previousSector); + + // we forget the previously active sector because we reverted to the one before + this._forgetLastSector(); + + // finally, we update the node index list. + this._updateNodeIndexList(); + + // we refresh the list with calulation nodes and calculation node indices. + this._updateCalculationNodes(); + } } }; /** - * Zoom out + * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we dont pass the function itself because then the "this" is the window object + * | instead of the Network object + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - exports._zoomOut = function() { - this.zoomIncrement = -this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - this._preventDefault(event); - if (this.navigationDivs) { - this.navigationDivs['zoomOut'].className += " active"; + exports._doInAllActiveSectors = function(runFunction,argument) { + if (argument === undefined) { + for (var sector in this.sectors["active"]) { + if (this.sectors["active"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToActiveSector(sector); + this[runFunction](); + } + } + } + else { + for (var sector in this.sectors["active"]) { + if (this.sectors["active"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToActiveSector(sector); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + } } + // we revert the global references back to our active sector + this._loadLatestSector(); }; /** - * Stop zooming and unhighlight the zoom controls + * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we dont pass the function itself because then the "this" is the window object + * | instead of the Network object + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - exports._stopZoom = function() { - this.zoomIncrement = 0; - if (this.navigationDivs) { - this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); - this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); + exports._doInSupportSector = function(runFunction,argument) { + if (argument === undefined) { + this._switchToSupportSector(); + this[runFunction](); + } + else { + this._switchToSupportSector(); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } } + // we revert the global references back to our active sector + this._loadLatestSector(); }; /** - * Stop moving in the Y direction and unHighlight the up and down + * This runs a function in all frozen sectors. This is used in the _redraw(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Network object + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - exports._yStopMoving = function() { - this.yIncrement = 0; - if (this.navigationDivs) { - this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); - this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); + exports._doInAllFrozenSectors = function(runFunction,argument) { + if (argument === undefined) { + for (var sector in this.sectors["frozen"]) { + if (this.sectors["frozen"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToFrozenSector(sector); + this[runFunction](); + } + } + } + else { + for (var sector in this.sectors["frozen"]) { + if (this.sectors["frozen"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToFrozenSector(sector); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + } } + this._loadLatestSector(); }; /** - * Stop moving in the X direction and unHighlight left and right. + * This runs a function in all sectors. This is used in the _redraw(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Network object + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - exports._xStopMoving = function() { - this.xIncrement = 0; - if (this.navigationDivs) { - this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); - this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); + exports._doInAllSectors = function(runFunction,argument) { + var args = Array.prototype.splice.call(arguments, 1); + if (argument === undefined) { + this._doInAllActiveSectors(runFunction); + this._doInAllFrozenSectors(runFunction); + } + else { + if (args.length > 1) { + this._doInAllActiveSectors(runFunction,args[0],args[1]); + this._doInAllFrozenSectors(runFunction,args[0],args[1]); + } + else { + this._doInAllActiveSectors(runFunction,argument); + this._doInAllFrozenSectors(runFunction,argument); + } } }; -/***/ }, -/* 48 */ -/***/ function(module, exports, __webpack_require__) { - - exports._resetLevels = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.preassignedLevel == false) { - node.level = -1; - } - } - } + /** + * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the + * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. + * + * @private + */ + exports._clearNodeIndexList = function() { + var sector = this._sector(); + this.sectors["active"][sector]["nodeIndices"] = []; + this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; }; + /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly + * Draw the encompassing sector node * + * @param ctx + * @param sectorType * @private */ - exports._setupHierarchicalLayout = function() { - if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { - if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { - this.constants.hierarchicalLayout.levelSeparation *= -1; - } - else { - this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); - } + exports._drawSectorNodes = function(ctx,sectorType) { + var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; + for (var sector in this.sectors[sectorType]) { + if (this.sectors[sectorType].hasOwnProperty(sector)) { + if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { - if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "vertical"; - } - } - else { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "horizontal"; - } - } - // get the size of the largest hubs and check if the user has defined a level for a node. - var hubsize = 0; - var node, nodeId; - var definedLevel = false; - var undefinedLevel = false; + this._switchToSector(sector,sectorType); - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.level != -1) { - definedLevel = true; - } - else { - undefinedLevel = true; - } - if (hubsize < node.edges.length) { - hubsize = node.edges.length; + minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + node.resize(ctx); + if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} + if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} + if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} + if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} + } } + node = this.sectors[sectorType][sector]["drawingNode"]; + node.x = 0.5 * (maxX + minX); + node.y = 0.5 * (maxY + minY); + node.width = 2 * (node.x - minX); + node.height = 2 * (node.y - minY); + node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); + node.setScale(this.scale); + node._drawCircle(ctx); } } + } + }; - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel == true && definedLevel == true) { - alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); - this.zoomExtent(true,this.constants.clustering.enabled); - if (!this.constants.clustering.enabled) { - this.start(); - } - } - else { - // setup the system to use hierarchical method. - this._changeConstants(); - - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - this._determineLevels(hubsize); - } - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); + exports._drawAllSectorNodes = function(ctx) { + this._drawSectorNodes(ctx,"frozen"); + this._drawSectorNodes(ctx,"active"); + this._loadLatestSector(); + }; - // place the nodes on the canvas. This also stablilizes the system. - this._placeNodesByHierarchy(distribution); - // start the simulation. - this.start(); - } - } - }; +/***/ }, +/* 46 */ +/***/ function(module, exports, __webpack_require__) { + var Node = __webpack_require__(30); /** - * This function places the nodes on the canvas based on the hierarchial distribution. + * This function can be called from the _doInAllSectors function * - * @param {Object} distribution | obtained by the function this._getDistribution() + * @param object + * @param overlappingNodes * @private */ - exports._placeNodesByHierarchy = function(distribution) { - var nodeId, node; - - // start placing all the level 0 nodes first. Then recursively position their branches. - for (nodeId in distribution[0].nodes) { - if (distribution[0].nodes.hasOwnProperty(nodeId)) { - node = distribution[0].nodes[nodeId]; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (node.xFixed) { - node.x = distribution[0].minPos; - node.xFixed = false; - - distribution[0].minPos += distribution[0].nodeSpacing; - } - } - else { - if (node.yFixed) { - node.y = distribution[0].minPos; - node.yFixed = false; - - distribution[0].minPos += distribution[0].nodeSpacing; - } + exports._getNodesOverlappingWith = function(object, overlappingNodes) { + var nodes = this.nodes; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); } - this._placeBranchNodes(node.edges,node.id,distribution,node.level); } } + }; - // stabilize the system after positioning. This function calls zoomExtent. - this._stabilize(); + /** + * 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 + */ + exports._getAllNodesOverlappingWith = function (object) { + var overlappingNodes = []; + this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); + return overlappingNodes; }; /** - * This function get the distribution of levels based on hubsize + * Return a position object in canvasspace from a single point in screenspace * - * @returns {Object} + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} * @private */ - exports._getDistribution = function() { - var distribution = {}; - var nodeId, node, level; - - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.xFixed = true; - node.yFixed = true; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; - } - else { - node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; - } - if (!distribution.hasOwnProperty(node.level)) { - distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; - } - distribution[node.level].amount += 1; - distribution[node.level].nodes[node.id] = node; - } - } - - // determine the largest amount of nodes of all levels - var maxCount = 0; - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - if (maxCount < distribution[level].amount) { - maxCount = distribution[level].amount; - } - } - } - - // set the initial position and spacing of each nodes accordingly - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; - distribution[level].nodeSpacing /= (distribution[level].amount + 1); - distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); - } - } + exports._pointerToPositionObject = function(pointer) { + var x = this._XconvertDOMtoCanvas(pointer.x); + var y = this._YconvertDOMtoCanvas(pointer.y); - return distribution; + return { + left: x, + top: y, + right: x, + bottom: y + }; }; /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * Get the top node at the a specific point (like a click) * - * @param hubsize + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node * @private */ - exports._determineLevels = function(hubsize) { - var nodeId, node; + exports._getNodeAt = function (pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - // determine hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.edges.length == hubsize) { - node.level = 0; - } - } + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; } - - // branch from hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.level == 0) { - this._setLevel(1,node.edges,node.id); - } - } + else { + return null; } }; /** - * Since hierarchical layout does not support: - * - smooth curves (based on the physics), - * - clustering (based on dynamic node counts) - * - * We disable both features so there will be no problems. - * + * 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 */ - exports._changeConstants = function() { - this.constants.clustering.enabled = false; - this.constants.physics.barnesHut.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this._loadSelectedForceSolver(); - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.dynamic = false; + exports._getEdgesOverlappingWith = function (object, overlappingEdges) { + var edges = this.edges; + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } } - this._configureSmoothCurves(); }; /** - * 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 + * 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 */ - exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } - else { - childNode = edges[i].to; - } + exports._getAllEdgesOverlappingWith = function (object) { + var overlappingEdges = []; + this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); + return overlappingEdges; + }; - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - var nodeMoved = false; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (childNode.xFixed && childNode.level > parentLevel) { - childNode.xFixed = false; - childNode.x = distribution[childNode.level].minPos; - nodeMoved = true; - } - } - else { - if (childNode.yFixed && childNode.level > parentLevel) { - childNode.yFixed = false; - childNode.y = distribution[childNode.level].minPos; - nodeMoved = true; - } - } + /** + * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call + * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {null} + * @private + */ + exports._getEdgeAt = function(pointer) { + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - if (nodeMoved == true) { - distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); - } - } + if (overlappingEdges.length > 0) { + return this.edges[overlappingEdges[overlappingEdges.length - 1]]; + } + else { + return null; } }; /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. + * Add object to the selection array. * - * @param level - * @param edges - * @param parentId + * @param obj * @private */ - exports._setLevel = function(level, edges, parentId) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } - else { - childNode = edges[i].to; - } - if (childNode.level == -1 || childNode.level > level) { - childNode.level = level; - if (edges.length > 1) { - this._setLevel(level+1, childNode.edges, childNode.id); - } - } + exports._addToSelection = function(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } + else { + this.selectionObj.edges[obj.id] = obj; } }; - /** - * Unfix nodes + * Add object to the selection array. * + * @param obj * @private */ - exports._restoreNodes = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.nodes[nodeId].xFixed = false; - this.nodes[nodeId].yFixed = false; - } + exports._addToHover = function(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } + else { + this.hoverObj.edges[obj.id] = obj; } }; -/***/ }, -/* 49 */ -/***/ function(module, exports, __webpack_require__) { - - /*! Hammer.JS - v1.0.5 - 2013-04-07 - * http://eightmedia.github.com/hammer.js - * - * Copyright (c) 2013 Jorik Tangelder ; - * Licensed under the MIT license */ - - (function(window, undefined) { - 'use strict'; - /** - * Hammer - * use this to create instances - * @param {HTMLElement} element - * @param {Object} options - * @returns {Hammer.Instance} - * @constructor + * Remove a single option from selection. + * + * @param {Object} obj + * @private */ - var Hammer = function(element, options) { - return new Hammer.Instance(element, options || {}); + exports._removeFromSelection = function(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } + else { + delete this.selectionObj.edges[obj.id]; + } }; - // default settings - Hammer.defaults = { - // add styles and attributes to the element to prevent the browser from doing - // its native behavior. this doesnt prevent the scrolling, but cancels - // the contextmenu, tap highlighting etc - // set to false to disable this - stop_browser_behavior: { - // this also triggers onselectstart=false for IE - userSelect: 'none', - // this makes the element blocking in IE10 >, you could experiment with the value - // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 - touchAction: 'none', - touchCallout: 'none', - contentZooming: 'none', - userDrag: 'none', - tapHighlightColor: 'rgba(0,0,0,0)' + /** + * Unselect all. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + exports._unselectAll = function(doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + 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(); + } + } - // more settings are defined per gesture at gestures.js - }; - - // detect touchevents - Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; - Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); - - // dont use mouseevents on mobile devices - Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX); - - // eventtypes per touchevent (start, move, end) - // are filled by Hammer.event.determineEventTypes on setup - Hammer.EVENT_TYPES = {}; - - // direction defines - Hammer.DIRECTION_DOWN = 'down'; - Hammer.DIRECTION_LEFT = 'left'; - Hammer.DIRECTION_UP = 'up'; - Hammer.DIRECTION_RIGHT = 'right'; - - // pointer type - Hammer.POINTER_MOUSE = 'mouse'; - Hammer.POINTER_TOUCH = 'touch'; - Hammer.POINTER_PEN = 'pen'; - - // touch event defines - Hammer.EVENT_START = 'start'; - Hammer.EVENT_MOVE = 'move'; - Hammer.EVENT_END = 'end'; - - // hammer document where the base events are added at - Hammer.DOCUMENT = document; - - // plugins namespace - Hammer.plugins = {}; + this.selectionObj = {nodes:{},edges:{}}; - // if the window events are set... - Hammer.READY = false; + if (doNotTrigger == false) { + this.emit('select', this.getSelection()); + } + }; /** - * setup events to detect gestures on the document + * Unselect all clusters. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private */ - function setup() { - if(Hammer.READY) { - return; - } - - // find what eventtypes we add listeners to - Hammer.event.determineEventTypes(); + exports._unselectClusters = function(doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } - // Register all gestures inside Hammer.gestures - for(var name in Hammer.gestures) { - if(Hammer.gestures.hasOwnProperty(name)) { - Hammer.detection.register(Hammer.gestures[name]); - } + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + this.selectionObj.nodes[nodeId].unselect(); + this._removeFromSelection(this.selectionObj.nodes[nodeId]); + } } + } - // Add touch events on the document - Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect); - Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect); + if (doNotTrigger == false) { + this.emit('select', this.getSelection()); + } + }; - // Hammer is ready...! - Hammer.READY = true; - } /** - * create new hammer instance - * all methods should return the instance itself, so it is chainable. - * @param {HTMLElement} element - * @param {Object} [options={}] - * @returns {Hammer.Instance} - * @constructor + * return the number of selected nodes + * + * @returns {number} + * @private */ - Hammer.Instance = function(element, options) { - var self = this; - - // setup HammerJS window events and register all gestures - // this also sets up the default options - setup(); - - this.element = element; - - // start/stop detection option - this.enabled = true; - - // merge options - this.options = Hammer.utils.extend( - Hammer.utils.extend({}, Hammer.defaults), - options || {}); - - // add some css to the element to prevent the browser from doing its native behavoir - if(this.options.stop_browser_behavior) { - Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); + exports._getSelectedNodeCount = function() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; } - - // start detection on touchstart - Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) { - if(self.enabled) { - Hammer.detection.startDetect(self, ev); - } - }); - - // return instance - return this; + } + return count; }; + /** + * return the selected node + * + * @returns {number} + * @private + */ + exports._getSelectedNode = function() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } + } + return null; + }; - Hammer.Instance.prototype = { - /** - * bind events to the instance - * @param {String} gesture - * @param {Function} handler - * @returns {Hammer.Instance} - */ - on: function onEvent(gesture, handler){ - var gestures = gesture.split(' '); - for(var t=0; t 1) { + return true; + } + } + } + return false; + }; + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + exports._selectConnectedEdges = function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + var edge = node.dynamicEdges[i]; + edge.select(); + this._addToSelection(edge); + } + }; /** - * when touch events have been fired, this is true - * @type {Boolean} + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private */ - var touch_triggered = false; + exports._hoverConnectedEdges = function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + var edge = node.dynamicEdges[i]; + edge.hover = true; + this._addToHover(edge); + } + }; - Hammer.event = { - /** - * simple addEventListener - * @param {HTMLElement} element - * @param {String} type - * @param {Function} handler - */ - bindDom: function(element, type, handler) { - var types = type.split(' '); - for(var t=0; t 0 && eventType == Hammer.EVENT_END) { - eventType = Hammer.EVENT_MOVE; - } - // no touches, force the end event - else if(!count_touches) { - eventType = Hammer.EVENT_END; - } + /** + * 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 + */ + exports._blurObject = function(object) { + if (object.hover == true) { + object.hover = false; + this.emit("blurNode",{node:object.id}); + } + }; - // because touchend has no touches, and we often want to use these in our gestures, - // we send the last move event as our eventData in touchend - if(!count_touches && last_move_event !== null) { - ev = last_move_event; - } - // store the last move event - else { - last_move_event = ev; - } + /** + * 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 + */ + exports._hoverObject = function(object) { + if (object.hover == false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.emit("hoverNode",{node:object.id}); + } + } + if (object instanceof Node) { + this._hoverConnectedEdges(object); + } + }; - // trigger the handler - handler.call(Hammer.detection, self.collectEventData(element, eventType, ev)); - // remove pointerevent from list - if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) { - count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); - } - } + /** + * handles the selection part of the touch, only for navigation controls elements; + * Touch is triggered before tap, also before hold. Hold triggers after a while. + * This is the most responsive solution + * + * @param {Object} pointer + * @private + */ + exports._handleTouch = function(pointer) { + }; - //debug(sourceEventType +" "+ eventType); - // on the end we reset everything - if(!count_touches) { - last_move_event = null; - enable_detect = false; - touch_triggered = false; - Hammer.PointerEvent.reset(); - } - }); - }, + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + exports._handleTap = function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node,false); + } + else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge,false); + } + else { + this._unselectAll(); + } + } + this.emit("click", this.getSelection()); + this._redraw(); + }; - /** - * we have different events for each device/browser - * determine what we need and set them in the Hammer.EVENT_TYPES constant - */ - determineEventTypes: function determineEventTypes() { - // determine the eventtype we want to set - var types; + /** + * handles the selection part of the double tap and opens a cluster if needed + * + * @param {Object} pointer + * @private + */ + exports._handleDoubleTap = function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null && node !== undefined) { + // we reset the areaCenter here so the opening of the node will occur + this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), + "y" : this._YconvertDOMtoCanvas(pointer.y)}; + this.openCluster(node); + } + this.emit("doubleClick", this.getSelection()); + }; - // pointerEvents magic - if(Hammer.HAS_POINTEREVENTS) { - types = Hammer.PointerEvent.getEvents(); - } - // on Android, iOS, blackberry, windows mobile we dont want any mouseevents - else if(Hammer.NO_MOUSEEVENTS) { - types = [ - 'touchstart', - 'touchmove', - 'touchend touchcancel']; - } - // for non pointer events browsers and mixed browsers, - // like chrome on windows8 touch laptop - else { - types = [ - 'touchstart mousedown', - 'touchmove mousemove', - 'touchend touchcancel mouseup']; - } - Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0]; - Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1]; - Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2]; - }, + /** + * Handle the onHold selection part + * + * @param pointer + * @private + */ + exports._handleOnHold = function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node,true); + } + else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge,true); + } + } + this._redraw(); + }; - /** - * create touchlist depending on the event - * @param {Object} ev - * @param {String} eventType used by the fakemultitouch plugin - */ - getTouchList: function getTouchList(ev/*, eventType*/) { - // get the fake pointerEvent touchlist - if(Hammer.HAS_POINTEREVENTS) { - return Hammer.PointerEvent.getTouchList(); - } - // get the touchlist - else if(ev.touches) { - return ev.touches; - } - // make fake touchlist from mouse position - else { - return [{ - identifier: 1, - pageX: ev.pageX, - pageY: ev.pageY, - target: ev.target - }]; - } - }, - - - /** - * collect event data for Hammer js - * @param {HTMLElement} element - * @param {String} eventType like Hammer.EVENT_MOVE - * @param {Object} eventData - */ - collectEventData: function collectEventData(element, eventType, ev) { - var touches = this.getTouchList(ev, eventType); + /** + * handle the onRelease event. These functions are here for the navigation controls module. + * + * @private + */ + exports._handleOnRelease = function(pointer) { - // find out pointerType - var pointerType = Hammer.POINTER_TOUCH; - if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) { - pointerType = Hammer.POINTER_MOUSE; - } + }; - return { - center : Hammer.utils.getCenter(touches), - timeStamp : new Date().getTime(), - target : ev.target, - touches : touches, - eventType : eventType, - pointerType : pointerType, - srcEvent : ev, - /** - * prevent the browser default actions - * mostly used to disable scrolling of the browser - */ - preventDefault: function() { - if(this.srcEvent.preventManipulation) { - this.srcEvent.preventManipulation(); - } - if(this.srcEvent.preventDefault) { - this.srcEvent.preventDefault(); - } - }, + /** + * + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection + */ + exports.getSelection = function() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return {nodes:nodeIds, edges:edgeIds}; + }; - /** - * stop bubbling the event up to its parents - */ - stopPropagation: function() { - this.srcEvent.stopPropagation(); - }, + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + exports.getSelectedNodes = function() { + var idArray = []; + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } + } + return idArray + }; - /** - * immediately stop gesture detection - * might be useful after a swipe was detected - * @return {*} - */ - stopDetect: function() { - return Hammer.detection.stopDetect(); - } - }; + /** + * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + exports.getSelectedEdges = function() { + var idArray = []; + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); } + } + return idArray; }; - Hammer.PointerEvent = { - /** - * holds all pointers - * @type {Object} - */ - pointers: {}, - /** - * get a list of pointers - * @returns {Array} touchlist - */ - getTouchList: function() { - var self = this; - var touchlist = []; + /** + * select zero or more nodes + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + exports.setSelection = function(selection) { + var i, iMax, id; - // we can use forEach since pointerEvents only is in IE10 - Object.keys(self.pointers).sort().forEach(function(id) { - touchlist.push(self.pointers[id]); - }); - return touchlist; - }, + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; - /** - * update the position of a pointer - * @param {String} type Hammer.EVENT_END - * @param {Object} pointerEvent - */ - updatePointer: function(type, pointerEvent) { - if(type == Hammer.EVENT_END) { - this.pointers = {}; - } - else { - pointerEvent.identifier = pointerEvent.pointerId; - this.pointers[pointerEvent.pointerId] = pointerEvent; - } + // first unselect any selected node + this._unselectAll(true); - return Object.keys(this.pointers).length; - }, + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - /** - * check if ev matches pointertype - * @param {String} pointerType Hammer.POINTER_MOUSE - * @param {PointerEvent} ev - */ - matchType: function(pointerType, ev) { - if(!ev.pointerType) { - return false; - } + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); + } + this._selectObject(node,true,true); + } - var types = {}; - types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE); - types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH); - types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN); - return types[pointerType]; - }, + console.log("setSelection is deprecated. Please use selectNodes instead.") + this.redraw(); + }; - /** - * get events - */ - getEvents: function() { - return [ - 'pointerdown MSPointerDown', - 'pointermove MSPointerMove', - 'pointerup pointercancel MSPointerUp MSPointerCancel' - ]; - }, - /** - * reset the list - */ - reset: function() { - this.pointers = {}; + /** + * 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] + */ + exports.selectNodes = function(selection, highlightEdges) { + var i, iMax, id; + + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); } + this._selectObject(node,true,true,highlightEdges); + } + this.redraw(); }; - Hammer.utils = { - /** - * extend method, - * also used for cloning when dest is an empty object - * @param {Object} dest - * @param {Object} src - * @parm {Boolean} merge do a merge - * @returns {Object} dest - */ - extend: function extend(dest, src, merge) { - for (var key in src) { - if(dest[key] !== undefined && merge) { - continue; - } - dest[key] = src[key]; - } - return dest; - }, + /** + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + exports.selectEdges = function(selection) { + var i, iMax, id; + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; - /** - * find if a node is in the given parent - * used for event delegation tricks - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @returns {boolean} has_parent - */ - hasParent: function(node, parent) { - while(node){ - if(node == parent) { - return true; - } - node = node.parentNode; - } - return false; - }, + // first unselect any selected node + this._unselectAll(true); + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - /** - * get the center of all the touches - * @param {Array} touches - * @returns {Object} center - */ - getCenter: function getCenter(touches) { - var valuesX = [], valuesY = []; + var edge = this.edges[id]; + if (!edge) { + throw new RangeError('Edge with id "' + id + '" not found'); + } + this._selectObject(edge,true,true,highlightEdges); + } + this.redraw(); + }; - for(var t= 0,len=touches.length; t= y) { - return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; - } - else { - return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; - } - }, + if (this.edgeBeingEdited !== undefined) { + this.edgeBeingEdited._disableControlNodes(); + this.edgeBeingEdited = undefined; + this.selectedControlNode = null; + this.controlNodesActive = false; + } + // restore overloaded functions + this._restoreOverloadedFunctions(); - /** - * calculate the distance between two touches - * @param {Touch} touch1 - * @param {Touch} touch2 - * @returns {Number} distance - */ - getDistance: function getDistance(touch1, touch2) { - var x = touch2.pageX - touch1.pageX, - y = touch2.pageY - touch1.pageY; - return Math.sqrt((x*x) + (y*y)); - }, + // resume calculation + this.freezeSimulation = false; + // reset global variables + this.blockConnectingEdgeSelection = false; + this.forceAppendSelection = false; - /** - * calculate the scale factor between two touchLists (fingers) - * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out - * @param {Array} start - * @param {Array} end - * @returns {Number} scale - */ - getScale: function getScale(start, end) { - // need two fingers... - if(start.length >= 2 && end.length >= 2) { - return this.getDistance(end[0], end[1]) / - this.getDistance(start[0], start[1]); - } - return 1; - }, + if (this.editMode == true) { + while (this.manipulationDiv.hasChildNodes()) { + this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); + } + // add the icons to the manipulator div + this.manipulationDiv.innerHTML = "" + + "" + + ""+this.constants.labels['add'] +"" + + "
" + + "" + + ""+this.constants.labels['link'] +""; + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + this.manipulationDiv.innerHTML += "" + + "
" + + "" + + ""+this.constants.labels['editNode'] +""; + } + else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + this.manipulationDiv.innerHTML += "" + + "
" + + "" + + ""+this.constants.labels['editEdge'] +""; + } + if (this._selectionIsEmpty() == false) { + this.manipulationDiv.innerHTML += "" + + "
" + + "" + + ""+this.constants.labels['del'] +""; + } - /** - * calculate the rotation degrees between two touchLists (fingers) - * @param {Array} start - * @param {Array} end - * @returns {Number} rotation - */ - getRotation: function getRotation(start, end) { - // need two fingers - if(start.length >= 2 && end.length >= 2) { - return this.getAngle(end[1], end[0]) - - this.getAngle(start[1], start[0]); - } - return 0; - }, + // bind the icons + var addNodeButton = document.getElementById("network-manipulate-addNode"); + addNodeButton.onclick = this._createAddNodeToolbar.bind(this); + var addEdgeButton = document.getElementById("network-manipulate-connectNode"); + addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + var editButton = document.getElementById("network-manipulate-editNode"); + editButton.onclick = this._editNode.bind(this); + } + else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + var editButton = document.getElementById("network-manipulate-editEdge"); + editButton.onclick = this._createEditEdgeToolbar.bind(this); + } + if (this._selectionIsEmpty() == false) { + var deleteButton = document.getElementById("network-manipulate-delete"); + deleteButton.onclick = this._deleteSelected.bind(this); + } + var closeDiv = document.getElementById("network-manipulation-closeDiv"); + closeDiv.onclick = this._toggleEditMode.bind(this); + this.boundFunction = this._createManipulatorBar.bind(this); + this.on('select', this.boundFunction); + } + else { + this.editModeDiv.innerHTML = "" + + "" + + "" + this.constants.labels['edit'] + ""; + var editModeButton = document.getElementById("network-manipulate-editModeButton"); + editModeButton.onclick = this._toggleEditMode.bind(this); + } + }; - /** - * boolean if the direction is vertical - * @param {String} direction - * @returns {Boolean} is_vertical - */ - isVertical: function isVertical(direction) { - return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN); - }, - - - /** - * stop browser default behavior with css props - * @param {HtmlElement} element - * @param {Object} css_props - */ - stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) { - var prop, - vendors = ['webkit','khtml','moz','ms','o','']; - if(!css_props || !element.style) { - return; - } - // with css properties for modern browsers - for(var i = 0; i < vendors.length; i++) { - for(var p in css_props) { - if(css_props.hasOwnProperty(p)) { - prop = p; + /** + * Create the toolbar for adding Nodes + * + * @private + */ + exports._createAddNodeToolbar = function() { + // clear the toolbar + this._clearManipulatorBar(); + if (this.boundFunction) { + this.off('select', this.boundFunction); + } - // vender prefix at the property - if(vendors[i]) { - prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); - } + // create the toolbar contents + this.manipulationDiv.innerHTML = "" + + "" + + "" + this.constants.labels['back'] + " " + + "
" + + "" + + "" + this.constants.labels['addDescription'] + ""; - // set the style - element.style[prop] = css_props[p]; - } - } - } + // bind the icon + var backButton = document.getElementById("network-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); - // also the disable onselectstart - if(css_props.userSelect == 'none') { - element.onselectstart = function() { - return false; - }; - } - } + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + this.boundFunction = this._addNode.bind(this); + this.on('select', this.boundFunction); }; - Hammer.detection = { - // contains all registred Hammer.gestures in the correct order - gestures: [], - - // data of the current Hammer.gesture detection session - current: null, - - // the previous Hammer.gesture session data - // is a full clone of the previous gesture.current object - previous: null, - // when this becomes true, no gestures are fired - stopped: false, + /** + * create the toolbar to connect nodes + * + * @private + */ + exports._createAddEdgeToolbar = function() { + // clear the toolbar + this._clearManipulatorBar(); + this._unselectAll(true); + this.freezeSimulation = true; + if (this.boundFunction) { + this.off('select', this.boundFunction); + } - /** - * start Hammer.gesture detection - * @param {Hammer.Instance} inst - * @param {Object} eventData - */ - startDetect: function startDetect(inst, eventData) { - // already busy with a Hammer.gesture detection on an element - if(this.current) { - return; - } + this._unselectAll(); + this.forceAppendSelection = false; + this.blockConnectingEdgeSelection = true; - this.stopped = false; + this.manipulationDiv.innerHTML = "" + + "" + + "" + this.constants.labels['back'] + " " + + "
" + + "" + + "" + this.constants.labels['linkDescription'] + ""; - this.current = { - inst : inst, // reference to HammerInstance we're working for - startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc - lastEvent : false, // last eventData - name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc - }; + // bind the icon + var backButton = document.getElementById("network-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); - this.detect(eventData); - }, + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + this.boundFunction = this._handleConnect.bind(this); + this.on('select', this.boundFunction); + // temporarily overload functions + this.cachedFunctions["_handleTouch"] = this._handleTouch; + this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; + this._handleTouch = this._handleConnect; + this._handleOnRelease = this._finishConnect; - /** - * Hammer.gesture detection - * @param {Object} eventData - * @param {Object} eventData - */ - detect: function detect(eventData) { - if(!this.current || this.stopped) { - return; - } + // redraw to show the unselect + this._redraw(); + }; - // extend event data with calculations about scale, distance etc - eventData = this.extendEventData(eventData); + /** + * create the toolbar to edit edges + * + * @private + */ + exports._createEditEdgeToolbar = function() { + // clear the toolbar + this._clearManipulatorBar(); + this.controlNodesActive = true; - // instance options - var inst_options = this.current.inst.options; + if (this.boundFunction) { + this.off('select', this.boundFunction); + } - // call Hammer.gesture handlers - for(var g=0,len=this.gestures.length; g" + + "" + this.constants.labels['back'] + " " + + "
" + + "" + + "" + this.constants.labels['editEdgeDescription'] + ""; - // store as previous event event - if(this.current) { - this.current.lastEvent = eventData; - } + // bind the icon + var backButton = document.getElementById("network-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); - // endevent, but not the last touch, so dont stop - if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) { - this.stopDetect(); - } + // temporarily overload functions + this.cachedFunctions["_handleTouch"] = this._handleTouch; + this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; + this.cachedFunctions["_handleTap"] = this._handleTap; + this.cachedFunctions["_handleDragStart"] = this._handleDragStart; + this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; + this._handleTouch = this._selectControlNode; + this._handleTap = function () {}; + this._handleOnDrag = this._controlNodeDrag; + this._handleDragStart = function () {} + this._handleOnRelease = this._releaseControlNode; - return eventData; - }, + // redraw to show the unselect + this._redraw(); + }; - /** - * clear the Hammer.gesture vars - * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected - * to stop other Hammer.gestures from being fired - */ - stopDetect: function stopDetect() { - // clone current data to the store as the previous gesture - // used for the double tap gesture, since this is an other gesture detect session - this.previous = Hammer.utils.extend({}, this.current); - // reset the current - this.current = null; - // stopped! - this.stopped = true; - }, + /** + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * + * @private + */ + exports._selectControlNode = function(pointer) { + this.edgeBeingEdited.controlNodes.from.unselect(); + this.edgeBeingEdited.controlNodes.to.unselect(); + this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); + if (this.selectedControlNode !== null) { + this.selectedControlNode.select(); + this.freezeSimulation = true; + } + this._redraw(); + }; - /** - * extend eventData for Hammer.gestures - * @param {Object} ev - * @returns {Object} ev - */ - extendEventData: function extendEventData(ev) { - var startEv = this.current.startEvent; + /** + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * + * @private + */ + exports._controlNodeDrag = function(event) { + var pointer = this._getPointer(event.gesture.center); + if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { + this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); + this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); + } + this._redraw(); + }; - // if the touches change, set the new touches over the startEvent touches - // this because touchevents don't have all the touches on touchstart, or the - // user must place his fingers at the EXACT same time on the screen, which is not realistic - // but, sometimes it happens that both fingers are touching at the EXACT same time - if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) { - // extend 1 level deep to get the touchlist with the touch objects - startEv.touches = []; - for(var i=0,len=ev.touches.length; i 1) { + alert("Cannot create edges to a cluster.") + } + else { + this._selectObject(node,false); + // create a node the temporary line can look at + this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); + this.sectors['support']['nodes']['targetNode'].x = node.x; + this.sectors['support']['nodes']['targetNode'].y = node.y; + this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); + this.sectors['support']['nodes']['targetViaNode'].x = node.x; + this.sectors['support']['nodes']['targetViaNode'].y = node.y; + this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; - Hammer.utils.extend(ev, { - deltaTime : delta_time, + // create a temporary edge + this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); + this.edges['connectionEdge'].from = node; + this.edges['connectionEdge'].connected = true; + this.edges['connectionEdge'].smooth = true; + this.edges['connectionEdge'].selected = true; + this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; + this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; - deltaX : delta_x, - deltaY : delta_y, + this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; + this._handleOnDrag = function(event) { + var pointer = this._getPointer(event.gesture.center); + this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x); + this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y); + this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x); + this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y); + }; - velocityX : velocity.x, - velocityY : velocity.y, + this.moving = true; + this.start(); + } + } + } + }; - distance : Hammer.utils.getDistance(startEv.center, ev.center), - angle : Hammer.utils.getAngle(startEv.center, ev.center), - direction : Hammer.utils.getDirection(startEv.center, ev.center), + exports._finishConnect = function(pointer) { + if (this._getSelectedNodeCount() == 1) { - scale : Hammer.utils.getScale(startEv.touches, ev.touches), - rotation : Hammer.utils.getRotation(startEv.touches, ev.touches), + // restore the drag function + this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; + delete this.cachedFunctions["_handleOnDrag"]; - startEvent : startEv - }); + // remember the edge id + var connectFromId = this.edges['connectionEdge'].fromId; - return ev; - }, + // remove the temporary nodes and edge + delete this.edges['connectionEdge']; + delete this.sectors['support']['nodes']['targetNode']; + delete this.sectors['support']['nodes']['targetViaNode']; + var node = this._getNodeAt(pointer); + if (node != null) { + if (node.clusterSize > 1) { + alert("Cannot create edges to a cluster.") + } + else { + this._createEdge(connectFromId,node.id); + this._createManipulatorBar(); + } + } + this._unselectAll(); + } + }; - /** - * register new gesture - * @param {Object} gesture object, see gestures.js for documentation - * @returns {Array} gestures - */ - register: function register(gesture) { - // add an enable gesture options if there is no given - var options = gesture.defaults || {}; - if(options[gesture.name] === undefined) { - options[gesture.name] = true; - } - // extend Hammer default options with the Hammer.gesture options - Hammer.utils.extend(Hammer.defaults, options, true); + /** + * Adds a node on the specified location + */ + exports._addNode = function() { + if (this._selectionIsEmpty() && this.editMode == true) { + var positionObject = this._pointerToPositionObject(this.pointerPosition); + var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; + if (this.triggerFunctions.add) { + if (this.triggerFunctions.add.length == 2) { + var me = this; + this.triggerFunctions.add(defaultData, function(finalizedData) { + me.nodesData.add(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } + else { + alert(this.constants.labels['addError']); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } + } + else { + this.nodesData.add(defaultData); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } + } + }; - // set its index - gesture.index = gesture.index || 1000; - // add Hammer.gesture to the list - this.gestures.push(gesture); + /** + * connect two nodes with a new edge. + * + * @private + */ + exports._createEdge = function(sourceNodeId,targetNodeId) { + if (this.editMode == true) { + var defaultData = {from:sourceNodeId, to:targetNodeId}; + if (this.triggerFunctions.connect) { + if (this.triggerFunctions.connect.length == 2) { + var me = this; + this.triggerFunctions.connect(defaultData, function(finalizedData) { + me.edgesData.add(finalizedData); + me.moving = true; + me.start(); + }); + } + else { + alert(this.constants.labels["linkError"]); + this.moving = true; + this.start(); + } + } + else { + this.edgesData.add(defaultData); + this.moving = true; + this.start(); + } + } + }; - // sort the list by index - this.gestures.sort(function(a, b) { - if (a.index < b.index) { - return -1; - } - if (a.index > b.index) { - return 1; - } - return 0; + /** + * connect two nodes with a new edge. + * + * @private + */ + exports._editEdge = function(sourceNodeId,targetNodeId) { + if (this.editMode == true) { + var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; + if (this.triggerFunctions.editEdge) { + if (this.triggerFunctions.editEdge.length == 2) { + var me = this; + this.triggerFunctions.editEdge(defaultData, function(finalizedData) { + me.edgesData.update(finalizedData); + me.moving = true; + me.start(); }); + } + else { + alert(this.constants.labels["linkError"]); + this.moving = true; + this.start(); + } + } + else { + this.edgesData.update(defaultData); + this.moving = true; + this.start(); + } + } + }; - return this.gestures; + /** + * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. + * + * @private + */ + exports._editNode = function() { + if (this.triggerFunctions.edit && this.editMode == true) { + var node = this._getSelectedNode(); + var data = {id:node.id, + label: node.label, + group: node.group, + shape: node.shape, + color: { + background:node.color.background, + border:node.color.border, + highlight: { + background:node.color.highlight.background, + border:node.color.highlight.border + } + }}; + if (this.triggerFunctions.edit.length == 2) { + var me = this; + this.triggerFunctions.edit(data, function (finalizedData) { + me.nodesData.update(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); } + else { + alert(this.constants.labels["editError"]); + } + } + else { + alert(this.constants.labels["editBoundError"]); + } }; - Hammer.gestures = Hammer.gestures || {}; - /** - * Custom gestures - * ============================== - * - * Gesture object - * -------------------- - * The object structure of a gesture: - * - * { name: 'mygesture', - * index: 1337, - * defaults: { - * mygesture_option: true - * } - * handler: function(type, ev, inst) { - * // trigger gesture event - * inst.trigger(this.name, ev); - * } - * } - * @param {String} name - * this should be the name of the gesture, lowercase - * it is also being used to disable/enable the gesture per instance config. - * - * @param {Number} [index=1000] - * the index of the gesture, where it is going to be in the stack of gestures detection - * like when you build an gesture that depends on the drag gesture, it is a good - * idea to place it after the index of the drag gesture. - * - * @param {Object} [defaults={}] - * the default settings of the gesture. these are added to the instance settings, - * and can be overruled per instance. you can also add the name of the gesture, - * but this is also added by default (and set to true). - * - * @param {Function} handler - * this handles the gesture detection of your custom gesture and receives the - * following arguments: - * - * @param {Object} eventData - * event data containing the following properties: - * timeStamp {Number} time the event occurred - * target {HTMLElement} target element - * touches {Array} touches (fingers, pointers, mouse) on the screen - * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH - * center {Object} center position of the touches. contains pageX and pageY - * deltaTime {Number} the total time of the touches in the screen - * deltaX {Number} the delta on x axis we haved moved - * deltaY {Number} the delta on y axis we haved moved - * velocityX {Number} the velocity on the x - * velocityY {Number} the velocity on y - * angle {Number} the angle we are moving - * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT - * distance {Number} the distance we haved moved - * scale {Number} scaling of the touches, needs 2 touches - * rotation {Number} rotation of the touches, needs 2 touches * - * eventType {String} matches Hammer.EVENT_START|MOVE|END - * srcEvent {Object} the source event, like TouchStart or MouseDown * - * startEvent {Object} contains the same properties as above, - * but from the first touch. this is used to calculate - * distances, deltaTime, scaling etc - * - * @param {Hammer.Instance} inst - * the instance we are doing the detection for. you can get the options from - * the inst.options object and trigger the gesture event by calling inst.trigger - * - * - * Handle gestures - * -------------------- - * inside the handler you can get/set Hammer.detection.current. This is the current - * detection session. It has the following properties - * @param {String} name - * contains the name of the gesture we have detected. it has not a real function, - * only to check in other gestures if something is detected. - * like in the drag gesture we set it to 'drag' and in the swipe gesture we can - * check if the current gesture is 'drag' by accessing Hammer.detection.current.name - * - * @readonly - * @param {Hammer.Instance} inst - * the instance we do the detection for - * - * @readonly - * @param {Object} startEvent - * contains the properties of the first gesture detection in this session. - * Used for calculations about timing, distance, etc. - * - * @readonly - * @param {Object} lastEvent - * contains all the properties of the last gesture detect in this session. - * - * after the gesture detection session has been completed (user has released the screen) - * the Hammer.detection.current object is copied into Hammer.detection.previous, - * this is usefull for gestures like doubletap, where you need to know if the - * previous gesture was a tap - * - * options that have been set by the instance can be received by calling inst.options - * - * You can trigger a gesture event by calling inst.trigger("mygesture", event). - * The first param is the name of your gesture, the second the event argument + /** + * delete everything in the selection * + * @private + */ + exports._deleteSelected = function() { + if (!this._selectionIsEmpty() && this.editMode == true) { + if (!this._clusterInSelection()) { + var selectedNodes = this.getSelectedNodes(); + var selectedEdges = this.getSelectedEdges(); + if (this.triggerFunctions.del) { + var me = this; + var data = {nodes: selectedNodes, edges: selectedEdges}; + if (this.triggerFunctions.del.length = 2) { + this.triggerFunctions.del(data, function (finalizedData) { + me.edgesData.remove(finalizedData.edges); + me.nodesData.remove(finalizedData.nodes); + me._unselectAll(); + me.moving = true; + me.start(); + }); + } + else { + alert(this.constants.labels["deleteError"]) + } + } + else { + this.edgesData.remove(selectedEdges); + this.nodesData.remove(selectedNodes); + this._unselectAll(); + this.moving = true; + this.start(); + } + } + else { + alert(this.constants.labels["deleteClusterError"]); + } + } + }; + + +/***/ }, +/* 48 */ +/***/ function(module, exports, __webpack_require__) { + + exports._cleanNavigation = function() { + // clean up previous navigation items + var wrapper = document.getElementById('network-navigation_wrapper'); + if (wrapper != null) { + this.containerElement.removeChild(wrapper); + } + document.onmouseup = null; + }; + + /** + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. * - * Register gestures - * -------------------- - * When an gesture is added to the Hammer.gestures object, it is auto registered - * at the setup of the first Hammer instance. You can also call Hammer.detection.register - * manually and pass your gesture object as a param + * @private + */ + exports._loadNavigationElements = function() { + this._cleanNavigation(); + + this.navigationDivs = {}; + var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; + var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; + + this.navigationDivs['wrapper'] = document.createElement('div'); + this.navigationDivs['wrapper'].id = "network-navigation_wrapper"; + this.navigationDivs['wrapper'].style.position = "absolute"; + this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; + this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; + this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); + + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDivs[navigationDivs[i]] = document.createElement('div'); + this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i]; + this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; + this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); + this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); + } + + document.onmouseup = this._stopMovement.bind(this); + }; + + /** + * this stops all movement induced by the navigation buttons * + * @private */ + exports._stopMovement = function() { + this._xStopMoving(); + this._yStopMoving(); + this._stopZoom(); + }; + /** - * Hold - * Touch stays at the same place for x time - * @events hold + * stops the actions performed by page up and down etc. + * + * @param event + * @private */ - Hammer.gestures.Hold = { - name: 'hold', - index: 10, - defaults: { - hold_timeout : 500, - hold_threshold : 1 - }, - timer: null, - handler: function holdGesture(ev, inst) { - switch(ev.eventType) { - case Hammer.EVENT_START: - // clear any running timers - clearTimeout(this.timer); + exports._preventDefault = function(event) { + if (event !== undefined) { + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + } + }; - // set the gesture so we can check in the timeout if it still is - Hammer.detection.current.name = this.name; - // set timer and if after the timeout it still is hold, - // we trigger the hold event - this.timer = setTimeout(function() { - if(Hammer.detection.current.name == 'hold') { - inst.trigger('hold', ev); - } - }, inst.options.hold_timeout); - break; + /** + * move the screen up + * By using the increments, instead of adding a fixed number to the translation, we keep fluent and + * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently + * To avoid this behaviour, we do the translation in the start loop. + * + * @private + */ + exports._moveUp = function(event) { + this.yIncrement = this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['up'].className += " active"; + } + }; - // when you move or end we clear the timer - case Hammer.EVENT_MOVE: - if(ev.distance > inst.options.hold_threshold) { - clearTimeout(this.timer); - } - break; - case Hammer.EVENT_END: - clearTimeout(this.timer); - break; - } - } + /** + * move the screen down + * @private + */ + exports._moveDown = function(event) { + this.yIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['down'].className += " active"; + } }; /** - * Tap/DoubleTap - * Quick touch at a place or double at the same place - * @events tap, doubletap + * move the screen left + * @private */ - Hammer.gestures.Tap = { - name: 'tap', - index: 100, - defaults: { - tap_max_touchtime : 250, - tap_max_distance : 10, - tap_always : true, - doubletap_distance : 20, - doubletap_interval : 300 - }, - handler: function tapGesture(ev, inst) { - if(ev.eventType == Hammer.EVENT_END) { - // previous gesture, for the double tap since these are two different gesture detections - var prev = Hammer.detection.previous, - did_doubletap = false; + exports._moveLeft = function(event) { + this.xIncrement = this.constants.keyboard.speed.x; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['left'].className += " active"; + } + }; - // when the touchtime is higher then the max touch time - // or when the moving distance is too much - if(ev.deltaTime > inst.options.tap_max_touchtime || - ev.distance > inst.options.tap_max_distance) { - return; - } - // check if double tap - if(prev && prev.name == 'tap' && - (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval && - ev.distance < inst.options.doubletap_distance) { - inst.trigger('doubletap', ev); - did_doubletap = true; - } + /** + * move the screen right + * @private + */ + exports._moveRight = function(event) { + this.xIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['right'].className += " active"; + } + }; - // do a single tap - if(!did_doubletap || inst.options.tap_always) { - Hammer.detection.current.name = 'tap'; - inst.trigger(Hammer.detection.current.name, ev); - } - } - } + + /** + * Zoom in, using the same method as the movement. + * @private + */ + exports._zoomIn = function(event) { + this.zoomIncrement = this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['zoomIn'].className += " active"; + } }; /** - * Swipe - * triggers swipe events when the end velocity is above the threshold - * @events swipe, swipeleft, swiperight, swipeup, swipedown + * Zoom out + * @private */ - Hammer.gestures.Swipe = { - name: 'swipe', - index: 40, - defaults: { - // set 0 for unlimited, but this can conflict with transform - swipe_max_touches : 1, - swipe_velocity : 0.7 - }, - handler: function swipeGesture(ev, inst) { - if(ev.eventType == Hammer.EVENT_END) { - // max touches - if(inst.options.swipe_max_touches > 0 && - ev.touches.length > inst.options.swipe_max_touches) { - return; - } + exports._zoomOut = function() { + this.zoomIncrement = -this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + if (this.navigationDivs) { + this.navigationDivs['zoomOut'].className += " active"; + } + }; - // when the distance we moved is too small we skip this gesture - // or we can be already in dragging - if(ev.velocityX > inst.options.swipe_velocity || - ev.velocityY > inst.options.swipe_velocity) { - // trigger swipe events - inst.trigger(this.name, ev); - inst.trigger(this.name + ev.direction, ev); - } - } - } + + /** + * Stop zooming and unhighlight the zoom controls + * @private + */ + exports._stopZoom = function() { + this.zoomIncrement = 0; + if (this.navigationDivs) { + this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); + this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); + } }; /** - * Drag - * Move with x fingers (default 1) around on the page. Blocking the scrolling when - * moving left and right is a good practice. When all the drag events are blocking - * you disable scrolling on that area. - * @events drag, drapleft, dragright, dragup, dragdown + * Stop moving in the Y direction and unHighlight the up and down + * @private */ - Hammer.gestures.Drag = { - name: 'drag', - index: 50, - defaults: { - drag_min_distance : 10, - // set 0 for unlimited, but this can conflict with transform - drag_max_touches : 1, - // prevent default browser behavior when dragging occurs - // be careful with it, it makes the element a blocking element - // when you are using the drag gesture, it is a good practice to set this true - drag_block_horizontal : false, - drag_block_vertical : false, - // drag_lock_to_axis keeps the drag gesture on the axis that it started on, - // It disallows vertical directions if the initial direction was horizontal, and vice versa. - drag_lock_to_axis : false, - // drag lock only kicks in when distance > drag_lock_min_distance - // This way, locking occurs only when the distance has become large enough to reliably determine the direction - drag_lock_min_distance : 25 - }, - triggered: false, - handler: function dragGesture(ev, inst) { - // current gesture isnt drag, but dragged is true - // this means an other gesture is busy. now call dragend - if(Hammer.detection.current.name != this.name && this.triggered) { - inst.trigger(this.name +'end', ev); - this.triggered = false; - return; - } + exports._yStopMoving = function() { + this.yIncrement = 0; + if (this.navigationDivs) { + this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); + this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); + } + }; - // max touches - if(inst.options.drag_max_touches > 0 && - ev.touches.length > inst.options.drag_max_touches) { - return; - } - switch(ev.eventType) { - case Hammer.EVENT_START: - this.triggered = false; - break; + /** + * Stop moving in the X direction and unHighlight left and right. + * @private + */ + exports._xStopMoving = function() { + this.xIncrement = 0; + if (this.navigationDivs) { + this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); + this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); + } + }; - case Hammer.EVENT_MOVE: - // when the distance we moved is too small we skip this gesture - // or we can be already in dragging - if(ev.distance < inst.options.drag_min_distance && - Hammer.detection.current.name != this.name) { - return; - } - // we are dragging! - Hammer.detection.current.name = this.name; +/***/ }, +/* 49 */ +/***/ function(module, exports, __webpack_require__) { - // lock drag to axis? - if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) { - ev.drag_locked_to_axis = true; - } - var last_direction = Hammer.detection.current.lastEvent.direction; - if(ev.drag_locked_to_axis && last_direction !== ev.direction) { - // keep direction on the axis that the drag gesture started on - if(Hammer.utils.isVertical(last_direction)) { - ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN; - } - else { - ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT; - } - } + exports._resetLevels = function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.preassignedLevel == false) { + node.level = -1; + } + } + } + }; - // first time, trigger dragstart event - if(!this.triggered) { - inst.trigger(this.name +'start', ev); - this.triggered = true; - } + /** + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly + * + * @private + */ + exports._setupHierarchicalLayout = function() { + if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { + if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { + this.constants.hierarchicalLayout.levelSeparation *= -1; + } + else { + this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); + } - // trigger normal event - inst.trigger(this.name, ev); + if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "vertical"; + } + } + else { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "horizontal"; + } + } + // get the size of the largest hubs and check if the user has defined a level for a node. + var hubsize = 0; + var node, nodeId; + var definedLevel = false; + var undefinedLevel = false; - // direction event, like dragdown - inst.trigger(this.name + ev.direction, ev); + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.level != -1) { + definedLevel = true; + } + else { + undefinedLevel = true; + } + if (hubsize < node.edges.length) { + hubsize = node.edges.length; + } + } + } - // block the browser events - if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) || - (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) { - ev.preventDefault(); - } - break; + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel == true && definedLevel == true) { + alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); + this.zoomExtent(true,this.constants.clustering.enabled); + if (!this.constants.clustering.enabled) { + this.start(); + } + } + else { + // setup the system to use hierarchical method. + this._changeConstants(); - case Hammer.EVENT_END: - // trigger dragend - if(this.triggered) { - inst.trigger(this.name +'end', ev); - } + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + this._determineLevels(hubsize); + } + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); - this.triggered = false; - break; - } + // place the nodes on the canvas. This also stablilizes the system. + this._placeNodesByHierarchy(distribution); + + // start the simulation. + this.start(); } + } }; /** - * Transform - * User want to scale or rotate with 2 fingers - * @events transform, pinch, pinchin, pinchout, rotate + * This function places the nodes on the canvas based on the hierarchial distribution. + * + * @param {Object} distribution | obtained by the function this._getDistribution() + * @private */ - Hammer.gestures.Transform = { - name: 'transform', - index: 45, - defaults: { - // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 - transform_min_scale : 0.01, - // rotation in degrees - transform_min_rotation : 1, - // prevent default browser behavior when two touches are on the screen - // but it makes the element a blocking element - // when you are using the transform gesture, it is a good practice to set this true - transform_always_block : false - }, - triggered: false, - handler: function transformGesture(ev, inst) { - // current gesture isnt drag, but dragged is true - // this means an other gesture is busy. now call dragend - if(Hammer.detection.current.name != this.name && this.triggered) { - inst.trigger(this.name +'end', ev); - this.triggered = false; - return; - } + exports._placeNodesByHierarchy = function(distribution) { + var nodeId, node; - // atleast multitouch - if(ev.touches.length < 2) { - return; + // start placing all the level 0 nodes first. Then recursively position their branches. + for (nodeId in distribution[0].nodes) { + if (distribution[0].nodes.hasOwnProperty(nodeId)) { + node = distribution[0].nodes[nodeId]; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (node.xFixed) { + node.x = distribution[0].minPos; + node.xFixed = false; + + distribution[0].minPos += distribution[0].nodeSpacing; } + } + else { + if (node.yFixed) { + node.y = distribution[0].minPos; + node.yFixed = false; - // prevent default when two fingers are on the screen - if(inst.options.transform_always_block) { - ev.preventDefault(); + distribution[0].minPos += distribution[0].nodeSpacing; } + } + this._placeBranchNodes(node.edges,node.id,distribution,node.level); + } + } + + // stabilize the system after positioning. This function calls zoomExtent. + this._stabilize(); + }; - switch(ev.eventType) { - case Hammer.EVENT_START: - this.triggered = false; - break; - case Hammer.EVENT_MOVE: - var scale_threshold = Math.abs(1-ev.scale); - var rotation_threshold = Math.abs(ev.rotation); + /** + * This function get the distribution of levels based on hubsize + * + * @returns {Object} + * @private + */ + exports._getDistribution = function() { + var distribution = {}; + var nodeId, node, level; - // when the distance we moved is too small we skip this gesture - // or we can be already in dragging - if(scale_threshold < inst.options.transform_min_scale && - rotation_threshold < inst.options.transform_min_rotation) { - return; - } + // we 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.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + node.xFixed = true; + node.yFixed = true; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; + } + else { + node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; + } + if (!distribution.hasOwnProperty(node.level)) { + distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; + } + distribution[node.level].amount += 1; + distribution[node.level].nodes[node.id] = node; + } + } - // we are transforming! - Hammer.detection.current.name = this.name; + // determine the largest amount of nodes of all levels + var maxCount = 0; + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + if (maxCount < distribution[level].amount) { + maxCount = distribution[level].amount; + } + } + } - // first time, trigger dragstart event - if(!this.triggered) { - inst.trigger(this.name +'start', ev); - this.triggered = true; - } + // set the initial position and spacing of each nodes accordingly + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; + distribution[level].nodeSpacing /= (distribution[level].amount + 1); + distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); + } + } - inst.trigger(this.name, ev); // basic transform event + return distribution; + }; - // trigger rotate event - if(rotation_threshold > inst.options.transform_min_rotation) { - inst.trigger('rotate', ev); - } - // trigger pinch event - if(scale_threshold > inst.options.transform_min_scale) { - inst.trigger('pinch', ev); - inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev); - } - break; + /** + * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * + * @param hubsize + * @private + */ + exports._determineLevels = function(hubsize) { + var nodeId, node; - case Hammer.EVENT_END: - // trigger dragend - if(this.triggered) { - inst.trigger(this.name +'end', ev); - } + // determine hubs + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.edges.length == hubsize) { + node.level = 0; + } + } + } - this.triggered = false; - break; - } + // branch from hubs + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.level == 0) { + this._setLevel(1,node.edges,node.id); + } } + } }; /** - * Touch - * Called as first, tells the user has touched the screen - * @events touch + * Since hierarchical layout does not support: + * - smooth curves (based on the physics), + * - clustering (based on dynamic node counts) + * + * We disable both features so there will be no problems. + * + * @private */ - Hammer.gestures.Touch = { - name: 'touch', - index: -Infinity, - defaults: { - // call preventDefault at touchstart, and makes the element blocking by - // disabling the scrolling of the page, but it improves gestures like - // transforming and dragging. - // be careful with using this, it can be very annoying for users to be stuck - // on the page - prevent_default: false, + exports._changeConstants = function() { + this.constants.clustering.enabled = false; + this.constants.physics.barnesHut.enabled = false; + this.constants.physics.hierarchicalRepulsion.enabled = true; + this._loadSelectedForceSolver(); + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.dynamic = false; + } + this._configureSmoothCurves(); + }; - // disable mouse events, so only touch (or pen!) input triggers events - prevent_mouseevents: false - }, - handler: function touchGesture(ev, inst) { - if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) { - ev.stopDetect(); - return; - } - if(inst.options.prevent_default) { - ev.preventDefault(); - } + /** + * 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 + */ + exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } + else { + childNode = edges[i].to; + } - if(ev.eventType == Hammer.EVENT_START) { - inst.trigger(this.name, ev); - } + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + var nodeMoved = false; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (childNode.xFixed && childNode.level > parentLevel) { + childNode.xFixed = false; + childNode.x = distribution[childNode.level].minPos; + nodeMoved = true; + } + } + else { + if (childNode.yFixed && childNode.level > parentLevel) { + childNode.yFixed = false; + childNode.y = distribution[childNode.level].minPos; + nodeMoved = true; + } + } + + if (nodeMoved == true) { + distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); + } } + } }; /** - * Release - * Called as last, tells the user has released the screen - * @events release + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. + * + * @param level + * @param edges + * @param parentId + * @private */ - Hammer.gestures.Release = { - name: 'release', - index: Infinity, - handler: function releaseGesture(ev, inst) { - if(ev.eventType == Hammer.EVENT_END) { - inst.trigger(this.name, ev); - } + exports._setLevel = function(level, edges, parentId) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } + else { + childNode = edges[i].to; + } + if (childNode.level == -1 || childNode.level > level) { + childNode.level = level; + if (edges.length > 1) { + this._setLevel(level+1, childNode.edges, childNode.id); + } } + } }; - // node export - if(typeof module === 'object' && typeof module.exports === 'object'){ - module.exports = Hammer; - } - // just window export - else { - window.Hammer = Hammer; - // requireJS module definition - if(typeof window.define === 'function' && window.define.amd) { - window.define('hammer', [], function() { - return Hammer; - }); + /** + * Unfix nodes + * + * @private + */ + exports._restoreNodes = function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.nodes[nodeId].xFixed = false; + this.nodes[nodeId].yFixed = false; } - } - })(this); + } + }; + /***/ }, /* 50 */ @@ -26766,7 +26785,7 @@ return /******/ (function(modules) { // webpackBootstrap this._calculateGravitationalForces(); this._calculateNodeForces(); - if (this.constants.springConstant > 0) { + if (this.constants.physics.springConstant > 0) { if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { this._calculateSpringForcesWithSupport(); } diff --git a/docs/network.html b/docs/network.html index ef3866ff..070d902a 100644 --- a/docs/network.html +++ b/docs/network.html @@ -2417,7 +2417,8 @@ network.off('select', onSelect); stabilized - Fired when the network has been stabilized after initialization. This event can be used to trigger the .storePosition() function after stabilization. + Fired when the network has been stabilized. This event can be used to trigger the .storePosition() function after stabilization. When the network in initialized, the parameter + iterations will be the amount of iterations it took to stabilize. After initialization, this parameter is null.
  • iterations: number of iterations used to stabilize
  • diff --git a/lib/network/Network.js b/lib/network/Network.js index b7875307..2ce9f280 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -2088,7 +2088,12 @@ Network.prototype._discreteStepNodes = function() { this.moving = true; } else { - this.moving = this._isMoving(vminCorrected) || this.constants.configurePhysics; + this.moving = this._isMoving(vminCorrected); + if (this.moving == false) { + this.emit("stabilized",{iterations:null}); + } + this.moving = this.moving || this.configurePhysics; + } } };