From d3506a82b3876223ff4f3ff2769e6b41cd2a8367 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 13 Feb 2014 17:58:04 +0100 Subject: [PATCH] refactoring, altered data manipulation, added callbacks and triggers, inserted temporary function overloading --- examples/graph/02_random_nodes.html | 1 - examples/graph/21_data_manipulation.html | 31 +- examples/graph/index.html | 1 + src/graph/Edge.js | 60 ++- src/graph/Graph.js | 121 ++--- src/graph/Node.js | 33 +- src/graph/graphMixins/ManipulationMixin.js | 565 +++++++++------------ src/graph/graphMixins/MixinLoader.js | 15 + src/graph/graphMixins/SelectionMixin.js | 19 +- src/graph/graphMixins/physics/repulsion.js | 6 +- src/util.js | 4 + 11 files changed, 439 insertions(+), 417 deletions(-) diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html index e7ed640d..ee648bbc 100755 --- a/examples/graph/02_random_nodes.html +++ b/examples/graph/02_random_nodes.html @@ -88,7 +88,6 @@ */ var options = { edges: { - }, stabilize: false }; diff --git a/examples/graph/21_data_manipulation.html b/examples/graph/21_data_manipulation.html index 4b7236bf..1ea7694e 100644 --- a/examples/graph/21_data_manipulation.html +++ b/examples/graph/21_data_manipulation.html @@ -38,13 +38,14 @@ border-style:solid; border-color: #d6d9d8; background: #ffffff; /* Old browsers */ - background: -moz-linear-gradient(top, #ffffff 0%, #f3f3f3 50%, #ededed 51%, #ffffff 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(50%,#f3f3f3), color-stop(51%,#ededed), color-stop(100%,#ffffff)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%); /* Opera 11.10+ */ - background: -ms-linear-gradient(top, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%); /* IE10+ */ - background: linear-gradient(to bottom, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%); /* W3C */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#ffffff',GradientType=0 ); /* IE6-9 */ + background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */ + background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */ + width: 600px; height:30px; z-index:10; @@ -223,7 +224,21 @@ clustering:false, navigation: true, keyboard: true, - dataManipulationToolbar: true + dataManipulationToolbar: true, + triggerFunctions: {add: function(data,callback) { + data.label = "hello"; + callback(data); + }, + edit: function(data,callback) { + data.label='edited' + callback(data); + }, + edit: function(data,callback) { + data.label='edited' + callback(data); + } + } + }; graph = new vis.Graph(container, data, options); diff --git a/examples/graph/index.html b/examples/graph/index.html index 53720b5b..4b38888d 100644 --- a/examples/graph/index.html +++ b/examples/graph/index.html @@ -33,6 +33,7 @@

19_scale_free_graph_clustering.html

20_navigation.html

21_data_manipulation.html

+

22_les_miserables.html

graphviz_gallery.html

diff --git a/src/graph/Edge.js b/src/graph/Edge.js index 06281645..ba5aa6da 100644 --- a/src/graph/Edge.js +++ b/src/graph/Edge.js @@ -212,8 +212,7 @@ Edge.prototype.isOverlappingWith = function(obj) { var xObj = obj.left; var yObj = obj.top; - - var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); return (dist < distMax); }; @@ -651,31 +650,46 @@ Edge.prototype._drawArrow = function(ctx) { * @param {number} y3 * @private */ -Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point - var px = x2-x1, - py = y2-y1, - something = px*px + py*py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; - - if (u > 1) { - u = 1; - } - else if (u < 0) { - u = 0; +Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point + if (this.smooth == true) { + var minDistance = 1e9; + var i,t,x,y,dx,dy; + for (i = 0; i < 10; i++) { + t = 0.1*i; + x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2; + y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + 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)); + } + return minDistance } + else { + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; - var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; + if (u > 1) { + u = 1; + } + else if (u < 0) { + u = 0; + } - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; - return Math.sqrt(dx*dx + dy*dy); + //# 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); + } }; diff --git a/src/graph/Graph.js b/src/graph/Graph.js index a9b43fb6..bbfff971 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -22,6 +22,8 @@ function Graph (container, data, options) { this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on this.stabilize = true; // stabilize before displaying the graph this.selectable = true; + // these functions can be triggered when the dataset is edited + this.triggerFunctions = {add:null,edit:null,connect:null,delete:null}; // set constant values this.constants = { @@ -29,7 +31,6 @@ function Graph (container, data, options) { radiusMin: 5, radiusMax: 20, radius: 5, - distance: 100, // px shape: 'ellipse', image: undefined, widthMin: 16, // px @@ -117,13 +118,15 @@ function Graph (container, data, options) { speed: {x: 10, y: 10, zoom: 0.02} }, dataManipulationToolbar: { - enabled: false + enabled: false, + initiallyVisible: false }, smoothCurves: true, maxVelocity: 25, minVelocity: 0.1, // px/s maxIterations: 1000 // maximum number of iteration to stabilize }; + this.editMode = this.constants.dataManipulationToolbar.initiallyVisible; // Node variables this.groups = new Groups(); // object with groups @@ -158,7 +161,7 @@ function Graph (container, data, options) { // other vars var graph = this; this.freezeSimulation = false;// freeze the simulation - + this.cachedFunctions = {}; this.calculationNodes = {}; this.calculationNodeIndices = []; @@ -416,6 +419,12 @@ Graph.prototype.setOptions = function (options) { if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} if (options.selectable !== undefined) {this.selectable = options.selectable;} + if (options.triggerFunctions) { + for (prop in options.triggerFunctions) { + this.triggerFunctions[prop] = options.triggerFunctions[prop]; + } + } + if (options.physics) { if (options.physics.barnesHut) { this.constants.physics.barnesHut.enabled = true; @@ -553,6 +562,7 @@ Graph.prototype.setOptions = function (options) { this.setSize(this.width, this.height); this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); this._setScale(1); + this.zoomToFit() this._redraw(); }; @@ -687,6 +697,8 @@ Graph.prototype._createKeyBinds = function() { if (this.constants.dataManipulationToolbar.enabled == true) { this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); + this.mousetrap.bind("del",this._deleteSelected.bind(me)); + this.mousetrap.bind("e",this._toggleEditMode.bind(me)); } } @@ -721,6 +733,11 @@ Graph.prototype._onTouch = function (event) { * @private */ Graph.prototype._onDragStart = function () { + this._handleDragStart(); +}; + + +Graph.prototype._handleDragStart = function() { var drag = this.drag; var node = this._getNodeAt(drag.pointer); // note: drag.pointer is set in _onTouch to get the initial touch location @@ -761,13 +778,18 @@ Graph.prototype._onDragStart = function () { } } } -}; +} + /** * handle drag event * @private */ Graph.prototype._onDrag = function (event) { + this._handleOnDrag(event) +}; + +Graph.prototype._handleOnDrag = function(event) { if (this.drag.pinched) { return; } @@ -775,12 +797,12 @@ Graph.prototype._onDrag = function (event) { var pointer = this._getPointer(event.gesture.touches[0]); var me = this, - drag = this.drag, - selection = drag.selection; + drag = this.drag, + selection = drag.selection; if (selection && selection.length) { // calculate delta's and new location var deltaX = pointer.x - drag.pointer.x, - deltaY = pointer.y - drag.pointer.y; + deltaY = pointer.y - drag.pointer.y; // update position of all selected nodes selection.forEach(function (s) { @@ -807,12 +829,12 @@ Graph.prototype._onDrag = function (event) { var diffY = pointer.y - this.drag.pointer.y; this._setTranslation( - this.drag.translation.x + diffX, - this.drag.translation.y + diffY); + this.drag.translation.x + diffX, + this.drag.translation.y + diffY); this._redraw(); this.moved = true; } -}; +} /** * handle drag start event @@ -869,7 +891,8 @@ Graph.prototype._onHold = function (event) { * @private */ Graph.prototype._onRelease = function (event) { - this._handleOnRelease(); + var pointer = this._getPointer(event.gesture.touches[0]); + this._handleOnRelease(pointer); }; /** @@ -1188,6 +1211,7 @@ Graph.prototype._addNodes = function(ids) { } } this._updateNodeIndexList(); + this._setCalculationNodes() this._reconnectEdges(); this._updateValueRange(this.nodes); this.updateLabels(); @@ -1710,7 +1734,6 @@ Graph.prototype._discreteStepNodes = function() { */ Graph.prototype.start = function() { if (!this.freezeSimulation) { - if (this.moving) { this._doInAllActiveSectors("_initializeForceCalculation"); if (this.constants.smoothCurves) { @@ -1719,55 +1742,39 @@ Graph.prototype.start = function() { this._doInAllActiveSectors("_discreteStepNodes"); this._findCenter(this._getRange()) } + } - if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { - // start animation. only start calculationTimer if it is not already running - if (!this.timer) { - var graph = this; - this.timer = window.setTimeout(function () { - graph.timer = undefined; - - // keyboad movement - if (graph.xIncrement != 0 || graph.yIncrement != 0) { - var translation = graph._getTranslation(); - graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement); - } - if (graph.zoomIncrement != 0) { - var center = { - x: graph.frame.canvas.clientWidth / 2, - y: graph.frame.canvas.clientHeight / 2 - }; - graph._zoom(graph.scale*(1 + graph.zoomIncrement), center); - } + if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { + // start animation. only start calculationTimer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; -// var calctimeStart = Date.now(); - - graph.start(); - graph.start(); -// var calctime = Date.now() - calctimeStart; -// var rendertimeStart = Date.now(); - graph._redraw(); -// var rendertime = Date.now() - rendertimeStart; - -// this.end = window.performance.now(); -// this.time = this.end - this.startTime; -// console.log('refresh time: ' + this.time); -// this.startTime = window.performance.now(); -// var DOMelement = document.getElementById("calctimereporter"); -// if (DOMelement !== undefined) { -// DOMelement.innerHTML = calctime; -// } -// DOMelement = document.getElementById("rendertimereporter"); -// if (DOMelement !== undefined) { -// DOMelement.innerHTML = rendertime; -// } - }, this.renderTimestep); - } - } - else { - this._redraw(); + // keyboad movement + if (graph.xIncrement != 0 || graph.yIncrement != 0) { + var translation = graph._getTranslation(); + graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement); + } + if (graph.zoomIncrement != 0) { + var center = { + x: graph.frame.canvas.clientWidth / 2, + y: graph.frame.canvas.clientHeight / 2 + }; + graph._zoom(graph.scale*(1 + graph.zoomIncrement), center); + } + + + graph.start(); + graph.start(); + graph._redraw(); + + }, this.renderTimestep); } } + else { + this._redraw(); + } }; /** diff --git a/src/graph/Node.js b/src/graph/Node.js index e36d427d..ba86acde 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -228,15 +228,32 @@ Node.prototype.setProperties = function(properties, constants) { Node.parseColor = function(color) { var c; if (util.isString(color)) { - c = { - border: color, - background: color, - highlight: { + if (util.isValidHex(color)) { + var hsv = util.hexToHSV(color); + var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)}; + var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6}; + var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v); + var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v); + + c = { border: color, - background: color - } - }; - // TODO: automatically generate a nice highlight color + border:darkerColorHex, + highlight: { + background:lighterColorHex, + border:darkerColorHex + } + }; + } + else { + c = { + border:color, + border:color, + highlight: { + background:color, + border:color + } + }; + } } else { c = {}; diff --git a/src/graph/graphMixins/ManipulationMixin.js b/src/graph/graphMixins/ManipulationMixin.js index 9bc261bb..7e734f68 100644 --- a/src/graph/graphMixins/ManipulationMixin.js +++ b/src/graph/graphMixins/ManipulationMixin.js @@ -16,203 +16,94 @@ var manipulationMixin = { }, - /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private - */ - _createManipulatorBar : function() { - // remove bound functions - this.off('select', this.boundFunction); + _restoreOverloadedFunctions : function() { + for (var functionName in this.cachedFunctions) { + if (this.cachedFunctions.hasOwnProperty(functionName)) { + this[functionName] = this.cachedFunctions[functionName]; + } + } + }, - // reset global variables - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); + _toggleEditMode : function() { + this.editMode = !this.editMode; + var toolbar = document.getElementById("graph-manipulationDiv") + if (this.editMode == true) { + toolbar.style.display="block"; } - // add the icons to the manipulator div - this.manipulationDiv.innerHTML = "" + - "Add Node" + - "
" + - "Edit Selected" + - "
" + - "Connect Node" + - "
" + - "Delete selected"; - - // bind the icons - var addButton = document.getElementById("manipulate-addNode"); - addButton.onclick = this._createAddToolbar.bind(this); - var editButton = document.getElementById("manipulate-editNode"); - editButton.onclick = this._createEditToolbar.bind(this); - var connectButton = document.getElementById("manipulate-connectNode"); - connectButton.onclick = this._createConnectToolbar.bind(this); - var deleteButton = document.getElementById("manipulate-delete"); - deleteButton.onclick = this._createDeletionToolbar.bind(this); + else { + toolbar.style.display="none"; + } + this._createManipulatorBar() }, - /** - * Create the toolbar for adding Nodes + * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. * * @private */ - _createAddToolbar : function() { - // clear the toolbar - this._clearManipulatorBar(); + _createManipulatorBar : function() { + // remove bound functions this.off('select', this.boundFunction); - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Back" + - "
" + - "Click in an empty space to place a new node"; - - // bind the icon - var backButton = document.getElementById("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); - }, + // restore overloaded functions + this._restoreOverloadedFunctions(); + // resume calculation + this.freezeSimulation = false; - /** - * Create the toolbar to edit nodes or edges. - * TODO: edges not implemented yet, unsure what to edit. - * - * @private - */ - _createEditToolbar : function() { - // clear the toolbar + // reset global variables this.blockConnectingEdgeSelection = false; - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - + this.forceAppendSelection = false - var message = ""; - if (this._selectionIsEmpty()) { - message = "Select a node or edge to edit."; - } - else { - if (this._getSelectedObjectCount() > 1) { - message = "Select a single node or edge to edit." - this._unselectAll(true); + if (this.editMode == true) { + while (this.manipulationDiv.hasChildNodes()) { + this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); } - else { - if (this._clusterInSelection()) { - message = "You cannot edit a cluster." - this._unselectAll(true); - } - else { - if (this._getSelectedNodeCount() > 0) { // the selected item is a node - this._createEditNodeToolbar(); - } - else { // the selected item is an edge - this._createEditEdgeToolbar(); - } - } - } - } - - if (message != "") { - this.blockConnectingEdgeSelection = true; - // create the toolbar contents + // add the icons to the manipulator div this.manipulationDiv.innerHTML = "" + - "Back" + + "Add Node" + "
" + - ""+message+""; + "Add Link"; + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + this.manipulationDiv.innerHTML += "" + + "
" + + "Edit Node"; + } + if (this._selectionIsEmpty() == false) { + this.manipulationDiv.innerHTML += "" + + "
" + + "Delete selected"; + } - // bind the icon - var backButton = document.getElementById("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._createEditToolbar.bind(this); + // bind the icons + var addNodeButton = document.getElementById("manipulate-addNode"); + addNodeButton.onclick = this._createAddNodeToolbar.bind(this); + var addEdgeButton = document.getElementById("manipulate-connectNode"); + addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + var editButton = document.getElementById("manipulate-editNode"); + editButton.onclick = this._editNode.bind(this); + } + if (this._selectionIsEmpty() == false) { + var deleteButton = document.getElementById("manipulate-delete"); + deleteButton.onclick = this._deleteSelected.bind(this); + } + this.boundFunction = this._createManipulatorBar.bind(this); this.on('select', this.boundFunction); } }, - /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. - * TODO: change shape or group? - * - * @private - */ - _createEditNodeToolbar : function() { - // clear the toolbar - this.blockConnectingEdgeSelection = false; - this._clearManipulatorBar(); - this.off('select', this.boundFunction); - - var editObject = this._getEditObject(); - - // create the toolbar contents - this.manipulationDiv.innerHTML = "" + - "Cancel" + - "
" + - "label: " + - "
" + - "color: " + - "
" + - "" - - // bind the icon - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - var saveButton = document.getElementById("manipulator-obj-save"); - saveButton.onclick = this._saveNodeData.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - }, - - - /** - * save the changes in the node data - * - * @private - */ - _saveNodeData : function() { - var editObjectId = this._getEditObject().id; - var label = document.getElementById('manipulator-obj-label').value; - - var definedColor = document.getElementById('manipulator-obj-color').value; - var hsv = util.hexToHSV(definedColor); - - var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)}; - var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6}; - var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v); - var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v); - - var updatedSettings = {id:editObjectId, - label: label, - color: { - background:definedColor, - border:darkerColorHex, - highlight: { - background:lighterColorHex, - border:darkerColorHex - } - }}; - this.nodesData.update(updatedSettings); - this._createManipulatorBar(); - }, - /** - * creating the toolbar to edit edges. + * Create the toolbar for adding Nodes * * @private */ - _createEditEdgeToolbar : function() { + _createAddNodeToolbar : function() { // clear the toolbar - this.blockConnectingEdgeSelection = false; this._clearManipulatorBar(); this.off('select', this.boundFunction); @@ -220,14 +111,14 @@ var manipulationMixin = { this.manipulationDiv.innerHTML = "" + "Back" + "
" + - "Currently only nodes can be edited."; + "Click in an empty space to place a new node"; // bind the icon var backButton = document.getElementById("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._createManipulatorBar.bind(this); + this.boundFunction = this._addNode.bind(this); this.on('select', this.boundFunction); }, @@ -237,9 +128,12 @@ var manipulationMixin = { * * @private */ - _createConnectToolbar : function() { + _createAddEdgeToolbar : function() { // clear the toolbar this._clearManipulatorBar(); + this._unselectAll(true); + this.freezeSimulation = true; + this.off('select', this.boundFunction); this._unselectAll(); @@ -249,7 +143,7 @@ var manipulationMixin = { this.manipulationDiv.innerHTML = "" + "Back" + "
" + - "Select the node you want to connect to other nodes."; + "Click on a node and drag the edge to another node."; // bind the icon var backButton = document.getElementById("manipulate-back"); @@ -258,43 +152,16 @@ var manipulationMixin = { // 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; - /** - * create the toolbar for deleting selected objects. User has to be sure. - * - * @private - */ - _createDeletionToolbar : function() { - // clear the toolbar - this._clearManipulatorBar(); - this.off('select', this.boundFunction); + // redraw to show the unselect + this._redraw(); - if (this._selectionIsEmpty()) { - this.manipulationDiv.innerHTML = "" + - "Cannot delete an empty selection."; - var graph = this; - window.setTimeout (function() {graph._createManipulatorBar()},1500); - } - else { - this.manipulationDiv.innerHTML = "" + - "Back" + - "
" + - "Are you sure? This cannot be undone." + - "
" + - "Yes."; - - // bind the buttons - var backButton = document.getElementById("manipulate-back"); - backButton.onclick = this._createManipulatorBar.bind(this); - var acceptDeleteButton = document.getElementById("manipulate-acceptDelete"); - acceptDeleteButton.onclick = this._deleteSelected.bind(this); - - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - this.boundFunction = this._createManipulatorBar.bind(this); - this.on('select', this.boundFunction); - } }, @@ -304,162 +171,226 @@ var manipulationMixin = { * * @private */ - _handleConnect : function() { - this.forceAppendSelection = false; - if (this._clusterInSelection()) { - this._unselectClusters(true); - if (!this._selectionIsEmpty()) { - this._setManipulationMessage("You cannot connect a node to a cluster."); - this.forceAppendSelection = true; - } - else { - this._setManipulationMessage("You cannot connect anything to a cluster."); - } - } - else if (!this._selectionIsEmpty()) { - if (this._getSelectedNodeCount() == 2) { - this._connectNodes(); - this._restoreSourceNode(); - this._setManipulationMessage("Click on another node you want to connect this node to or go back."); - } - else { - this._setManipulationMessage("Click on the node you want to connect this node."); - this._setSourceNode(); - this.forceAppendSelection = true; + _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.touches[0]); + this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x); + this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y); + this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x); + this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y); + }; + + this.moving = true; + this.start(); + } } } - else { - this._setManipulationMessage("Select the node you want to connect to other nodes."); - } }, + _finishConnect : function(pointer) { + if (this._getSelectedNodeCount() == 1) { - /** - * returns the object that is selected - * - * @returns {*} - * @private - */ - _getEditObject : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - return this.selectionObj[objectId]; - } - } - return null; - }, + // restore the drag function + this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; + delete this.cachedFunctions["_handleOnDrag"]; + // remember the edge id + var connectFromId = this.edges['connectionEdge'].fromId; - /** - * stores the first selected node for the connecting process as the source node. This allows us to remember the direction - * - * @private - */ - _setSourceNode : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - this.manipulationSourceNode = this.selectionObj[objectId]; + // 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(); } }, /** - * gets the node the source connects to. + * Adds a node on the specified location * - * @returns {*} - * @private + * @param {Object} pointer */ - _getTargetNode : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (this.manipulationSourceNode.id != this.selectionObj[objectId].id) { - return this.selectionObj[objectId]; - } + _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",fixed:false}; + if (this.triggerFunctions.add) { + if (this.triggerFunctions.add.length == 2) { + var me = this; + this.triggerFunctions.add(defaultData, function(finalizedData) { + me.createNodeOnClick = true; + me.nodesData.add(finalizedData); + me.createNodeOnClick = false; + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } + else { + alert("The function for add does not support two arguments (data,callback)."); + this._createManipulatorBar(); + this.moving = true; + this.start(); } } + else { + console.log("didnt use funciton") + this.createNodeOnClick = true; + this.nodesData.add(defaultData); + this.createNodeOnClick = false; + this._createManipulatorBar(); + this.moving = true; + this.start(); + } } - return null; - }, - - - /** - * restore the selection back to only the sourcenode - * - * @private - */ - _restoreSourceNode : function() { - this._unselectAll(true); - this._selectObject(this.manipulationSourceNode); }, /** - * change the description message on the toolbar + * connect two nodes with a new edge. * - * @param message * @private */ - _setManipulationMessage : function(message) { - var messageSpan = document.getElementById('manipulatorLabel'); - messageSpan.innerHTML = message; - }, - - - /** - * Adds a node on the specified location - * - * @param {Object} pointer - */ - _addNode : function() { - if (this._selectionIsEmpty()) { - var positionObject = this._pointerToPositionObject(this.pointerPosition); - this.createNodeOnClick = true; - this.nodesData.add({id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",fixed:false}); - this.createNodeOnClick = false; - this.moving = true; - this.start(); + _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("The function for connect does not support two arguments (data,callback)."); + this.moving = true; + this.start(); + } + } + else { + this.edgesData.add(defaultData) + this.moving = true; + this.start(); + } } }, /** - * connect two nodes with a new edge. + * 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 */ - _connectNodes : function() { - var targetNode = this._getTargetNode(); - var sourceNode = this.manipulationSourceNode; - this.edgesData.add({from:sourceNode.id, to:targetNode.id}) - this.moving = true; - this.start(); + _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("The function for edit does not support two arguments (data, callback).") + } + } + else { + alert("No edit function has been bound to this button.") + } }, /** * delete everything in the selection - * TODO : place the alert in the gui. - * * * @private */ _deleteSelected : function() { - if (!this._clusterInSelection()) { - var selectedNodes = this.getSelectedNodes(); - var selectedEdges = this.getSelectedEdges(); - this._removeEdges(selectedEdges); - this._removeNodes(selectedNodes); - this.moving = true; - this.start(); - } - else { - alert("Clusters cannot be deleted.") + if (!this._selectionIsEmpty() && this.editMode == true) { + if (!this._clusterInSelection()) { + var selectedNodes = this.getSelectedNodes(); + var selectedEdges = this.getSelectedEdges(); + if (this.triggerFunctions.delete) { + var me = this; + var data = {nodes: selectedNodes, edges: selectedEdges}; + if (this.triggerFunctions.delete.length = 2) { + this.triggerFunctions.delete(data, function (finalizedData) { + me.edgesData.remove(finalizedData.edges); + me.nodesData.remove(finalizedData.nodes); + me.moving = true; + me.start(); + }); + } + else { + alert("The function for edit does not support two arguments (data, callback).") + } + } + else { + this.edgesData.remove(selectedEdges); + this.nodesData.remove(selectedNodes); + this.moving = true; + this.start(); + } + } + else { + alert("Clusters cannot be deleted."); + } } } - - }; \ No newline at end of file diff --git a/src/graph/graphMixins/MixinLoader.js b/src/graph/graphMixins/MixinLoader.js index 062e65ad..325febf9 100644 --- a/src/graph/graphMixins/MixinLoader.js +++ b/src/graph/graphMixins/MixinLoader.js @@ -149,6 +149,13 @@ var graphMixinLoaders = { if (this.manipulationDiv === undefined) { this.manipulationDiv = document.createElement('div'); this.manipulationDiv.className = 'graph-manipulationDiv'; + this.manipulationDiv.id = 'graph-manipulationDiv'; + if (this.editMode == true) { + this.manipulationDiv.style.display = "block"; + } + else { + this.manipulationDiv.style.display = "none"; + } this.containerElement.insertBefore(this.manipulationDiv, this.frame); } // load the manipulation functions @@ -157,6 +164,14 @@ var graphMixinLoaders = { // create the manipulator toolbar this._createManipulatorBar(); } + else { + if (this.manipulationDiv !== undefined) { + this._createManipulatorBar(); + this.containerElement.removeChild(this.manipulationDiv); + this.manipulationDiv = undefined; + this._clearMixin(manipulationMixin); + } + } }, diff --git a/src/graph/graphMixins/SelectionMixin.js b/src/graph/graphMixins/SelectionMixin.js index 5ed2272a..01078805 100644 --- a/src/graph/graphMixins/SelectionMixin.js +++ b/src/graph/graphMixins/SelectionMixin.js @@ -263,6 +263,23 @@ var SelectionMixin = { return count; }, + /** + * return the number of selected nodes + * + * @returns {number} + * @private + */ + _getSelectedNode : function() { + for (var objectId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Node) { + return this.selectionObj[objectId]; + } + } + } + return null; + }, + /** * return the number of selected edges @@ -488,7 +505,7 @@ var SelectionMixin = { * * @private */ - _handleOnRelease : function() { + _handleOnRelease : function(pointer) { this.xIncrement = 0; this.yIncrement = 0; this.zoomIncrement = 0; diff --git a/src/graph/graphMixins/physics/repulsion.js b/src/graph/graphMixins/physics/repulsion.js index 8df7236f..c1bc805f 100644 --- a/src/graph/graphMixins/physics/repulsion.js +++ b/src/graph/graphMixins/physics/repulsion.js @@ -23,7 +23,9 @@ var repulsionMixin = { var b = 4/3; // repulsing forces between nodes - var minimumDistance = this.constants.nodes.distance; + var nodeDistance = this.constants.physics.repulsion.nodeDistance; + var minimumDistance = nodeDistance; + // we loop from i over all but the last entree in the array // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j for (i = 0; i < nodeIndices.length-1; i++) { @@ -36,7 +38,7 @@ var repulsionMixin = { dy = node2.y - node1.y; distance = Math.sqrt(dx * dx + dy * dy); - minimumDistance = (combinedClusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); + minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); var a = a_base / minimumDistance; if (distance < 2*minimumDistance) { angle = Math.atan2(dy, dx); diff --git a/src/util.js b/src/util.js index 6b49996c..c65682a3 100644 --- a/src/util.js +++ b/src/util.js @@ -826,3 +826,7 @@ util.hexToHSV = function hexToHSV(hex) { return util.RGBToHSV(rgb.r,rgb.g,rgb.b); } +util.isValidHex = function isValidHex(hex) { + var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); + return isOk; +}