diff --git a/Kieker.WebGUI/src/main/webapp/css/FlowEditor.css b/Kieker.WebGUI/src/main/webapp/css/FlowEditor.css index eb0b8e77bcc6002603699b20d9087faad23bb02a..e442b7aab89218a9db809aadfcc8a86ba734611f 100644 --- a/Kieker.WebGUI/src/main/webapp/css/FlowEditor.css +++ b/Kieker.WebGUI/src/main/webapp/css/FlowEditor.css @@ -1,5 +1,3 @@ -@charset "UTF-8"; - #inner-details { font-size:12px; } diff --git a/Kieker.WebGUI/src/main/webapp/js/flowEditor.js b/Kieker.WebGUI/src/main/webapp/js/flowEditor.js index 36ef8a34ca2e10edd58d4f7a65c62e2da4ce987d..96fef984ae4f3f92dc9cb79bf5fe1c0d4a9516ca 100644 --- a/Kieker.WebGUI/src/main/webapp/js/flowEditor.js +++ b/Kieker.WebGUI/src/main/webapp/js/flowEditor.js @@ -1,3 +1,21 @@ +/************************************************************************** + +Copyright [2012] [Software Engineering Group - Kiel University] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +***************************************************************************/ + var labelType, useGradients, nativeTextSupport, animate; /** @@ -38,7 +56,7 @@ function GraphFlow(){ var visContainer = 'infovis'; - /** Color Palettes */ + /** Colors */ var nodeFillColor = []; nodeFillColor['nodeFamily'] = "#DEDEDE"; @@ -62,48 +80,50 @@ function GraphFlow(){ /** FlowGraph Object */ var fd; - /** Nodes and edges where the mouse moves over */ + /** Nodes and edges that are highlighted when the mouse moves over them */ var hover = null; + var markedNode = null; /** This array stores all Nodes and is initialized with a dummy, - used for drawing edges between a node and the mousepointer. + used for drawing edges between a node and the mousepointer. Empty + elements are dynamically reserved for additional nodes. */ var json; - /** Used for managing space for node creation/deletion - since the array is divided into to parts, adding and removing nodes - would cause many costly splices. We a similar approach to ArrayLists. + /** Used for dynamically adding and removing space for node creation/deletion + resulting in better performance when manipulating the json array. */ var jsonCapacity = {'occupied' : 0, 'max' : 10}; - /** selected node: {id : <the id of the selected node, - from : <the data.$type of the selected node>} + /** selected node: {id : the id of the selected node, + from : the data.$type of the selected node, + label: the label of a selected edge} When clicking a node or edge, this variable will contain node - information, that is required for creating edges. + information, that is required for attaching/detaching edges. */ var selectedNode = null; - /** This points to the DOM-node (not the json-array node!) that is + /** This points to a DOM-node (not the json-array node!) which is an invisible dummy for moving an edge with the mouse */ var mouseNode = null; - /** true if the mouse is over a port-node/edge */ + /** true if the mouse is over a port/edge */ var mouseOverNode = false; var mouseOverEdge = false; - /** A hashtable of mouse and node events to arrays of functions that - should happen on these events + /** A hashtable, mapping mouse and node events to arrays of functions that + are called when these events happen */ var listener = []; - /** Can be changed to allow or prohibit node movement or creation */ + /** Can be changed to allow or prohibit node movement or deletion/creation */ var readOnly = {'deleteCreate' : true, 'move' : true}; - /** Options concerning the grid. */ + /** Grid dummy-node which is drawn over the graph if it is visible */ var grid = { 'data': {'$dim' : vertexLabelSize*4, '$type' : 'grid', @@ -116,7 +136,9 @@ function GraphFlow(){ 'snap' : false }; - /** Stores information about the canvas, such as its dimensions and its position + /** Stores information about the canvas, such as its dimensions and its position. + This information is needed to correctly draw the Grid and may come in handy in + future functions. */ var navi = { 'width' : document.getElementById(visContainer).clientWidth, 'height': document.getElementById(visContainer).clientHeight, @@ -131,18 +153,243 @@ function GraphFlow(){ ///////////////////////////////////////////// + /** + Returns a XML-String, representing the graph as KGraph + This function is work in progress and will be tuned + for efficiency in the future! + */ + this.convertToKGraph = function(){ + // define helper strings + var ie = '<children incomingEdges="'; + var pt = '<ports edges="'; + var oe = [ '<outgoingEdges target="//@children.', + '" sourcePort="//@children.', + '/@ports.', + '" targetPort="//@children.', + '">\n<data xsi:type="klayoutdata:KEdgeLayout"/>\n</outgoingEdges>\n' + ]; + + // this array will contain XMI-Strings + var children = [], + indexMap = []; // KGraphs use indices instead of IDs + + // this loop prepares the children array + var child; + for(var n = 0, l = jsonCapacity.occupied; n < l; n++){ + // reserve space for edges + child = { + incomingEdges : ie, + ports : [], + outgoingEdges : "" + }; + // reserve space for port strings + for(var p = 0, pl = json[n].data.$portCount-1; p < pl; p++){ // TODO: adjust for ReadOnly + child.ports.push({"text":pt, "isEast":false}); + } + children.push(child); + + // add node id to index map + indexMap[json[n].id] = n; + } + + // determine port indices + var node, + indexCounter = 0; + for(var n = jsonCapacity.max, l = json.length; n < l; n++){ + node = json[n]; + if(node.data.$type == "crossBox"){ + indexCounter = 0; + } + else{ + indexMap[json[n].id] = indexCounter; + indexCounter++; + } + } + + // now prepare all strings needed + indexCounter = -1; + var edgeCounter = 0, portCounter = 0; + var adja, edge, edgeString, targetChild, targetPort, relativeY; + for(var n = jsonCapacity.max+1, l = json.length; n < l; n++){ + node = json[n]; + if(node.data.$type == "crossBox"){ // TODO: do not rely on crossBox, make more generic + indexCounter++; + edgeCounter = 0; + portCounter = 0; + } + else{ + // is the port the first of its type within the node (e.g. the first inputPort) + relativeY = node.data.$relativeY; + if(relativeY){ + children[indexCounter].ports[portCounter].yPos = relativeY; + } + + // is the port positioned on the east side of the node? + children[indexCounter].ports[portCounter].isEast = node.data.$type == "inputPort"; + + adja = node.adjacencies; + for(var e = 0, el = adja.length; e < el; e++){ + edge = adja[e]; + edgeString = "//@children." + + indexCounter + +"/@outgoingEdges." + + edgeCounter + " "; + targetChild = indexMap[edge.data.$targetFamily]; + targetPort = indexMap[edge.nodeTo]; + + // add edge info to outgoing family + children[indexCounter].outgoingEdges += + oe[0] + targetChild + oe[1] + indexCounter + oe[2] + portCounter + + oe[3] + targetChild + oe[2] + targetPort + oe[4]; + children[indexCounter].ports[portCounter].text += edgeString; + + // add edge info to target family + children[targetChild].ports[targetPort].text += edgeString; + children[targetChild].incomingEdges += edgeString; + + edgeCounter++; + } + + portCounter++; + } + } + // redefine helper strings + ie = '">\n<data xsi:type="klayoutdata:KShapeLayout" xpos="0.0" ypos="0.0" width="'; + pt = '">\n<data xsi:type="klayoutdata:KShapeLayout" xpos="'; + var ie2 = '">\n<persistentEntries key="de.cau.cs.kieler.sizeConstraint" value="FIXED"/>\n' + + '<persistentEntries key="de.cau.cs.kieler.portConstraints" value="FIXED_POS"/>\n<insets/>\n</data>\n', + pt2 = '" width="'+(vertexLabelSize*2)+'" height="'+vertexLabelSize+'">\n</data>\n</ports>\n'; + + // assemble XMI-String + var xmi = '<?xml version="1.0" encoding="UTF-8"?>\n' + + '<kgraph:KNode xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" ' + + 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:kgraph="http://kieler.cs.cau.de/KGraph" ' + + 'xmlns:klayoutdata="http://kieler.cs.cau.de/KLayoutData">\n<data xsi:type="klayoutdata:KShapeLayout" ' + + 'width="348.0" height="112.0">\n<persistentEntries key="de.cau.cs.kieler.sizeConstraint" value="FIXED"/>\n' + + '<insets/>\n</data>\n\n'; + var ports, xPos, yPos, width, height; + for(var c = 0, l = children.length; c < l; c++){ + child = children[c]; + width = json[c].data.$width; + height = json[c].data.$height; + xmi += child.incomingEdges + ie + width + '" height="'+ height + ie2; + + ports = child.ports; + for(var p = 0, pl = ports.length; p < pl; p++){ + + // determine the y-Position of the port + relativeY = ports[p].yPos; + if(relativeY){ + yPos = relativeY + height/2; + } + else{ + yPos += 2*vertexLabelSize; + } + + // determine the x-Position of the port + if(ports[p].isEast){ + xPos = -vertexLabelSize; + } + else { + xPos = width - vertexLabelSize; + } + xmi += ports[p].text + pt + xPos + '" ypos="'+ yPos +pt2; + } + + xmi += child.outgoingEdges + '</children>\n'; + + delete children[c]; + } + xmi += '</kgraph:KNode>'; + + return xmi; + } + + /** + Checks an input color-string for validation. + Sends an onError-event if the string is not valid. + @param callFunction - the function where the argument needs to be checked + @param color - the color argument which is checked + @returns - false if the string is not a well formed color string + */ + function validArgColor(callFunction, color){ + var valid = false; + + // is it a string at all ? + if(typeof color == "string"){ + // does the string match "#RRGGBB" ? + var regEx = /^#[a-f\d]{6}$/i; + valid = color.match(regEx); + } + + if(!valid){ + callListener("onError", ["Error in function "+ callFunction + + ": Invalid color '"+ color + + "'. Must be structured like '#RRGGBB'."]); + } + return valid; + } + + + /** + Checks an input object for validation. + Sends an onError-event if the Object does not have an id + @param callFunction - the function where the argument needs to be checked + @param node - the object argument which is checked + @returns - false if the object does not have an id + */ + function validArgNode(callFunction, node){ + var valid = false; + + // is it an object at all ? + if(node && typeof node == "object"){ + // does the object have an id + valid = (typeof node.id != "undefined"); + } + + if(!valid){ + callListener("onError", ["Error in function "+ callFunction + + ": Invalid Node '"+ node + + "'. Must have a field 'id'."]); + } + return valid; + } + + + /** + Checks an argument for type correctness. + Sends an onError-event if the argument is not valid. + @param callFunction - the function where the argument needs to be checked + @param arg - the argument which is checked + @param type - the type of which the argument must be + @returns - false if the string is not a well formed color string + */ + function validArg(callFunction, arg, type){ + var valid = (typeof arg == type); + if(!valid){ + callListener("onError", ["Error in function "+ callFunction + + ": Invalid argument type for argument '" + + arg + "'. Must be of type " + type + "."]); + } + return valid; + } + /** Changes the color of the grid. Requires a refresh(). */ this.setGridColor = function(newColor){ - grid.data.$color = newColor; + if(validArgColor('setGridColor()', newColor)){ + grid.data.$color = newColor; + } } /** Changes the size of the grid. Requires a refresh(). */ this.setGridSize = function(newSize){ - grid.data.$dim = newSize; + if(validArg('setGridSize()', newSize, 'number')){ + grid.data.$dim = newSize; + } } /** @@ -159,7 +406,6 @@ function GraphFlow(){ grid.snap = snap; } - /** Scales and centers the graph to fit the space provided by the container in which it is drawn. @@ -238,206 +484,82 @@ function GraphFlow(){ icon will be deleted */ this.setNodeIcon = function(nodeType, path){ + if(!validArg('setNodeIcon()', nodeType, 'string')){ + return; + } + if(path == null){ delete nodeIcons[nodeType]; //TODO: adjust shrink width } else{ - // if nodes were already created, we need to - // adjust their width and iconPath - if(nodeIcons[nodeType] == undefined){ - var icon = Image(); - addSize = 6* vertexLabelSize, - addPortSize = addSize / 2, - max = jsonCapacity.max; + if(!validArg('setNodeIcon()', path, 'string')){ + return; + } + var icon = Image(); + icon.src = path; - icon.src = path; + // if the image was loaded successfully, its width must be greater 0 + if(icon.width > 0){ + // if nodes were already created, we need to + // adjust their width and iconPath + if(nodeIcons[nodeType] == undefined){ + var addSize = 6* vertexLabelSize, + addPortSize = addSize / 2, + max = jsonCapacity.max; - var data, portData, portCount, addPortSizeRel; - for(var n = 0, l = jsonCapacity.occupied; n < l; n++){ - // adjust node width - data = json[n].data; - if(data.$nodeType == nodeType){ - data.$icon = icon; - data.$width += addSize; - portCount = data.$portCount; - - // adjust port positions - for(var p = data.$portIndex+max, s = p+portCount; p < s; p++){ - portData = json[p].data; - if(portData.$type == "inputPort"){ - addPortSizeRel = -addPortSize; - }else{ - addPortSizeRel = addPortSize; - } - if(portData.$relativeX != undefined){ - portData.$relativeX += addPortSizeRel; + var data, portData, portCount, addPortSizeRel; + for(var n = 0, l = jsonCapacity.occupied; n < l; n++){ + // adjust node width + data = json[n].data; + if(data.$nodeType == nodeType){ + data.$icon = icon; + data.$width += addSize; + portCount = data.$portCount; + + // adjust port positions + for(var p = data.$portIndex+max, s = p+portCount; p < s; p++){ + portData = json[p].data; + if(portData.$type == "inputPort"){ + addPortSizeRel = -addPortSize; + }else{ + addPortSizeRel = addPortSize; + } + if(portData.$relativeX != undefined){ + portData.$relativeX += addPortSizeRel; + } + portData.$xPos += addPortSizeRel; } - portData.$xPos += addPortSizeRel; } } } + nodeIcons[nodeType] = icon; + } + else{ + callListener("onError", ["Error in function setNodeIcon(): Could not load '"+ path +"'."]); } - nodeIcons[nodeType] = icon; } } /** - Sets the mouseCursor to a new one. If the new cursor is null, - it will be reset to the default mouse cursor. + Changes the appearance of the mouse cursor. + @param newCursor - a String-name of the new cursor. + If null, restore the cursor. */ this.setMouseCursor = function(newCursor){ if(newCursor == null){ fd.canvas.getElement().style.cursor = ''; - }else{ + + }else if(validArg('setMouseCursor()', newCursor, 'string')){ fd.canvas.getElement().style.cursor = newCursor; } } - /** - Converts the Graph to dot language, returning it as a string. - */ - this.graphToDot = function(filename){ - var node; - var dotGraph = 'digraph structs {\nnode [shape=plaintext];\n\n' - for(var n=0; n < json.length; n++){ - node = json[n]; - if(node.data.$type == "nodeFamily"){ - dotGraph += nodeToDot(node); - dotGraph += '\n'; - } - } - dotGraph += '}'; - console.log(dotGraph); - } - - /** - Converts a single node to a node in dot language, and returns it as a string. - @param nodeFamily - the node which is to be converted - */ - this.nodeToDot = function(nodeFamily){ - var pWidth = (nodeFamily.data.$width+vertexLabelSize)/2; - var tableDef = '<TABLE BORDER="0" CELLSPACING="0" CELLBORDER="1" CELLPADDING="0">\n', - portDefA = '<TR><TD WIDTH="'+pWidth+'" FIXEDSIZE="true" HEIGHT="'+(2*vertexLabelSize)+'" PORT="', - portDefB = '">p</TD></TR>\n', - padDefA = '<TR><TD FIXEDSIZE="true" WIDTH="'+pWidth+'" HEIGHT="', - padDefB = '"></TD></TR>\n', - emptyDef = '<TD FIXEDSIZE="true" HEIGHT="1" WIDTH="'+pWidth+'"></TD>\n'; - - // this variable will contain a string, representing a node in dot-code - var dot = nodeFamily.id, - dotEdges = ''; - - dot+= ' [label=< '+tableDef; - dot+= '<TR><TD COLSPAN="2" HEIGHT="'+(2.5*vertexLabelSize)+'"></TD></TR><TR>\n'; - - var portIndex = nodeFamily.data.$jsonIndex+2, - lastPortIndex = nodeFamily.data.$jsonIndex+ nodeFamily.data.$portCount; - var port = json[portIndex]; - if(port.data.$type == "inputPort"){ - - dot += '<TD>'+tableDef; - var dotInput = '', - countInput = 0; - - // write the dot representation of ports into a temporary string - while(portIndex <= lastPortIndex && port.data.$type == "inputPort"){ - dotInput += portDefA+ port.id + portDefB; - countInput++; - portIndex++; - port = json[portIndex]; - } - - // apply padding if there is space above and below the ports - var padding = nodeFamily.data.$height/2 - (1.5+countInput)*vertexLabelSize; - if(padding > 0){ - dotInput = padDefA+padding+padDefB + dotInput + padDefA+padding+padDefB; - } - - // append temp string to result - dot += dotInput + '</TABLE></TD>'; - } - else{ - dot += emptyDef; - } - - if (portIndex <= lastPortIndex){ - // we get here if there were no input ports OR if we handled them all - var dotRight = '', - countRight = 0, - adja, // outgoing edges of port - adjaIter; // used to iterate through the edges - - // add repository ports - if(port.data.$type == "repositoryPort"){ - - dot += '<TD>'+tableDef; - - // write the dot representation of ports into a temporary string - while(portIndex <= lastPortIndex && port.data.$type == "repositoryPort"){ - dotRight += portDefA+ port.id + portDefB; - - // add edges - adja = port.adjacencies; - for(a = 0; a < adja.length; a++){ - adjaIter = adja[a]; - dotEdges += nodeFamily.id+":"+port.id + ":e->" + adjaIter.data.$targetFamily + ":" + adjaIter.nodeTo+":w;\n"; - } - - countRight++; - portIndex++; - port = json[portIndex]; - } - } - // add output ports - if(portIndex <= lastPortIndex && port.data.$type == "outputPort"){ - - // if we have preceding repo ports, insert a little gap to separate - // the output ports - if(json[portIndex-1].data.$type == "repositoryPort"){ - dotRight += padDefA + vertexLabelSize + padDefB; - countRight += 0.5; - } - else{ - dot += '<TD>'+tableDef; - } - - // write the dot representation of ports into a temporary string - while(portIndex <= lastPortIndex && port.data.$type == "outputPort"){ - dotRight += portDefA+ port.id + portDefB; - - // add edges - adja = port.adjacencies; - for(var a = 0; a < adja.length; a++){ - adjaIter = adja[a]; - dotEdges += nodeFamily.id+":"+port.id + ":e->" + adjaIter.data.$targetFamily + ":" + adjaIter.nodeTo+":w;\n"; - } - - countRight++; - portIndex++; - port = json[portIndex]; - } - } - // apply padding to right sideif there is space above and below the ports - var padding = nodeFamily.data.$height/2 - (1.5+countRight)*vertexLabelSize; - if(padding > 0){ - dotRight = padDefA+padding+padDefB + dotRight + padDefA+padding+padDefB; - } - dot += dotRight + '</TABLE></TD>'; - }else{ - dot += emptyDef; - } - // end of ports - - dot+= '</TR><TR><TD COLSPAN="2" HEIGHT="'+vertexLabelSize/2+'"></TD></TR></TABLE>>];\n'; - dot+= dotEdges; - - return dot; - } /** - reloads the graph, trying to restore all positions - this function needs to be called whenever nodes or egdges are + Reloads the graph, restoring all positions and displaying all + changes to the graph that happened since the last refresh. + This function needs to be called whenever nodes or egdges are added or deleted! */ this.refresh = function(){ @@ -467,24 +589,29 @@ function GraphFlow(){ onDragCancel, onDragEnd Or one of these events: + onError(errorMessage) onCreateEdge(sourceNode, targetNode, sourcePort, targetPort) : boolean, onCreateNode(node) : boolean, onRemoveEdge(sourceNode, targetNode, sourcePort, targetPort) : boolean, onRemoveNode(node) : boolean - When altering the graph, do not forget to refresh() it at the - end! - @param event - the name of the mouse event ( see list above) + @param eventName - the name of the mouse event ( see list above) @param listenerFunction - a function that may alter the graph */ - this.addListener = function(event, listenerFunction){ - if(listener[event] == undefined){ - listener[event] = []; + this.addListener = function(eventName, listenerFunction){ + // check arguments + if(!validArg('addListener()', eventName, 'string') || + !validArg('addListener()', listenerFunction, 'function')){ + return; + } + + if(listener[eventName] == undefined){ + listener[eventName] = []; } - listener[event].push(listenerFunction); + listener[eventName].push(listenerFunction); } /** - Calls all listener that are registered under the eventName. + Calls all listeners that are registered under the eventName. @param eventName - see 'addListener()' @param arguments - an array of arguments. For mouseEvents it is [node, eventInfo, e], for nodeEvents it is [node], @@ -497,7 +624,6 @@ function GraphFlow(){ if(!lArr){ return true; } - var permitted = true; var testval; @@ -578,10 +704,15 @@ function GraphFlow(){ } /** - Iterates through all nodes and ports, calling a given function for each. + Iterates through all nodes and ports, calling a given function for each one. @param nodeFunction - a single parameter function, called on a node. */ this.iterateAllNodes = function(nodeFunction){ + // check arguments + if(validArg('iterateAllNodes()', nodeFunction, 'function')){ + return; + } + var node; for(var n=0, l=jsonCapacity.occupied; n < l; n++){ node = json[n]; @@ -595,20 +726,45 @@ function GraphFlow(){ /** Changes the colors of the entire graph. + Requires a refresh(). + @param fillColor - the fill color of nodes. If null, restore Node Color from global Array. + @param strokeColor - the stroke color of nodes. If null, restore Stroke Color from global Array. + @param portColor - the color of all ports. If null, restore Port Color from global Array. + @param edgeColor - the color of edges. If null, restore Edge Color from global Array. + @param highlightColor - the of highlighted objects. If null, restore Edge Color from global Array. */ - this.setNodeStyle = function(fillColor, strokeColor, portColor){ - nodeFillColor['nodeFamily'] = fillColor; - nodeStrokeColor['nodeFamily'] = strokeColor; - nodeStrokeColor['inputPort'] = portColor; - nodeStrokeColor['outputPort'] = portColor; - nodeStrokeColor['repositoryPort'] = portColor; + this.setNodeStyle = function(fillColor, strokeColor, portColor, edColor, fcColor){ - var data; + if(fillColor && validArgColor('setNodeStyle()', fillColor)){ + nodeFillColor['nodeFamily'] = fillColor; + } + if(strokeColor && validArgColor('setNodeStyle()', strokeColor)){ + nodeStrokeColor['nodeFamily'] = strokeColor; + } + + if(portColor && validArgColor('setNodeStyle()', portColor)){ + nodeStrokeColor['inputPort'] = portColor; + nodeStrokeColor['outputPort'] = portColor; + nodeStrokeColor['repositoryPort'] = portColor; + } + if(edColor && validArgColor('setNodeStyle()', edColor)){ + edgeColor = edColor; + } + if(fcColor && validArgColor('setNodeStyle()', fcColor)){ + edgeColorFocus = fcColor; + nodeColorFocus = fcColor; + } + + + var data, a, al, adja; // change family colors for(var n = 0, l = jsonCapacity.occupied; n < l; n++){ data = json[n].data; - data.$fillColor = nodeFillColor["nodeFamily"]; - data.$color = nodeStrokeColor["nodeFamily"]; + // change only if node is not marked + if(json[n] != markedNode){ + data.$fillColor = nodeFillColor["nodeFamily"]; + data.$color = nodeStrokeColor["nodeFamily"]; + } } // change port colors for(var n = jsonCapacity.max+1, l = json.length; n < l; n++){ @@ -617,18 +773,24 @@ function GraphFlow(){ if(data.$type == "crossBox"){ data.$fillColor = nodeFillColor["nodeFamily"]; } + // change edge colors + if(edColor){ + adja = data.adjacencies; + for(a = 0, al = adja.length; a < al; a++){ + adja[a].data.$color = edgeColor; + } + } } - refresh(); } /** - Permits or prohibits node and edge deletion/creation and hides the cross + Permits or prohibits node and edge deletion/creation and hides the deletion-cross accordingly. @param deleteCreate - if true, one can change the graph in any way @param move - if true, one can change the graph in any way */ this.setReadOnly = function(deleteCreate, move){ - + if(deleteCreate != readOnly.deleteCreate){ // set crossBoxes (in)visible var data; @@ -636,6 +798,7 @@ function GraphFlow(){ data = json[n].data; if(data.$type == "crossBox"){ data.$visible = deleteCreate; + } } readOnly.deleteCreate = deleteCreate; @@ -648,12 +811,60 @@ function GraphFlow(){ /** Adds a Node to the graph. + @param xPosition - the horizontal position of the added node. 0 is the center of the canvas' starting position + @param xPosition - the vertical position of the added node. 0 is the center of the canvas' starting position @param nodeFamily - describes some properties of the node: (id, name, nodeClass, tooltip) - @param inputPortIDs - an array of properties for the input ports (id, name, tooltip) - @param outputPortIDs - an array of properties for the output ports (id, name, tooltip) + @param repositoryPorts - an array of properties for the input ports (id, name, tooltip) + @param inputPorts - an array of properties for the input ports (id, name, tooltip) + @param outputPorts - an array of properties for the output ports (id, name, tooltip) + @param nodeType - a string-identifier for a certain group of nodefamilies eg. Filter, Repository + @param forced - if true, no event listener will be called and the node will be created no matter what */ this.addNode = function(xPosition, yPosition, nodeFamily, repositoryPorts, inputPorts, outputPorts, nodeType, forced){ + // determine array size + var countRepo = 0, + countInput = 0, + countOutput = 0, + iconSpace = 0; + + if(repositoryPorts){ + countRepo = repositoryPorts.length; + // check if it is an array + if(!validArg(caller, repositoryPorts, 'object') || !countRepo){ + countRepo = 0; + } + } + if(inputPorts){ + countInput = inputPorts.length; + // check if it is an array + if(!validArg(caller, inputPorts, 'object') || !countInput){ + countInput = 0; + } + } + if(outputPorts){ + countOutput = outputPorts.length; + // check if it is an array + if(!validArg(caller, outputPorts, 'object') || !countOutput){ + countOutput = 0; + } + } + + // check for other illegal arguments + var caller = 'addNode()'; + if(!validArg(caller, xPosition, 'number')){ + xPosition = 0; + } + if(!validArg(caller, yPosition, 'number')){ + yPosition = 0; + } + if(!validArg(caller, nodeType, 'string')){ + nodeType = 'Filter'; + } + if(!validArgNode(caller, nodeFamily)){ + return; + } + // check if we have enough space reserved var max = jsonCapacity.max; var occ = jsonCapacity.occupied; @@ -669,24 +880,9 @@ function GraphFlow(){ json = families.concat(ports); } - var countRepo = 0, - countInput = 0, - countOutput = 0, - iconSpace = 0; - - if(repositoryPorts != null){ - countRepo= repositoryPorts.length; - } - if(inputPorts != null){ - countInput= inputPorts.length; - } - if(outputPorts != null){ - countOutput= outputPorts.length; - } - var maxPorts = Math.max(countInput, countRepo+countOutput+0.5), size = vertexLabelSize*2; - + // insert some space between repo ports and output ports, only if both are present if(maxPorts > countInput && (countRepo == 0 || countOutput == 0)){ maxPorts-= 0.5; @@ -746,7 +942,7 @@ function GraphFlow(){ "$relativeY": -height/2, "$color": strokeColor, "$fillColor": nodeColor, - "$visible":true + "$visible": readOnly.deleteCreate }, "id": nodeFamily.id+".close", "name": "x" @@ -802,25 +998,30 @@ function GraphFlow(){ for(var p=0; p < loopPorts.length; p++){ var port = loopPorts[p]; - var newPort ={ - "adjacencies": [], - "data": { - "$dim": size, - "$type": loopType, - "$xPos": x, - "$yPos": y, - "$color": nodeStrokeColor[loopType], - "$fillColor": nodeColor, - "$tooltip": port.tooltip}, - "id": nodeFamily.id+"."+port.id, - "name": port.name}; - - if(p == 0){ - newPort.data.$relativeX= relativeX; - newPort.data.$relativeY= relativeY; + + // only attempt to add a node if it has an id + if(validArgNode(caller, port)){ + + var newPort ={ + "adjacencies": [], + "data": { + "$dim": size, + "$type": loopType, + "$xPos": x, + "$yPos": y, + "$color": nodeStrokeColor[loopType], + "$fillColor": nodeColor, + "$tooltip": port.tooltip}, + "id": nodeFamily.id+"."+port.id, + "name": port.name}; + + if(p == 0){ + newPort.data.$relativeX= relativeX; + newPort.data.$relativeY= relativeY; + } + json.push(newPort); + y += size; } - json.push(newPort); - y += size; } } } @@ -833,16 +1034,27 @@ function GraphFlow(){ } /** - Removes a Node and all its edges from the graph. + Removes a Node and all its attached edges from the graph. @param nodeFamily - the node which is to be removed + @param forced - if true, the node will be removed unconditionally and no + event listeners will be called */ this.removeNode = function(nodeFamily, forced){ - //fd.graph.removeNode(node.id); + + // check for valid arguments + if(!validArgNode("removeNode()", nodeFamily)){ + return; + } // call listener if(!forced && !callListener("onRemoveNode", [nodeFamily])){ return; } + // unmark node + if (markedNode && markedNode.id == nodeFamily.id){ + markedNode = null; + } + var deleteFrom = nodeFamily.data.$portIndex + jsonCapacity.max, deleteUntil = deleteFrom+ nodeFamily.data.$portCount, familyIndex = nodeFamily.data.$jsonIndex, @@ -872,16 +1084,6 @@ function GraphFlow(){ json.splice(deleteFrom, deletionSum); selectedNode = null; - /* - // update nodeFamily indices - for(var n = deleteUntil-deletionSum+1; n < json.length; n++){ - var familyIndex = json[n].data.$jsonIndex; - if(familyIndex != undefined){ - json[n].data.$jsonIndex = n; - } - } - */ - // update nodeFamily indices and remove the deletion gap // in the array space by shifting all following families up var data; @@ -894,7 +1096,6 @@ function GraphFlow(){ } json[occ] = null; - // check if we should remove some space var max = jsonCapacity.max; if( (occ*4) < max && max > 10){ @@ -909,10 +1110,12 @@ function GraphFlow(){ } /** - Prepares a drag opertion for a node, by memorizing all corresponding graph nodes. + Prepares a drag operation for a node, caching all graph nodes that need to be moved. This speeds up the moveNode() function considerably. + @param node - the node which will be moved */ function prepareNodeMove(node){ + var nodes = []; var familyIndex = node.data.$jsonIndex, moveFrom = node.data.$portIndex+ jsonCapacity.max, @@ -934,6 +1137,8 @@ function GraphFlow(){ /** Moves a nodeFamily and its subnodes to the specified position. + @param xPos - the horizontal position of the moved node. 0 is the center of the canvas' starting position + @param yPos - the vertical position of the moved node. 0 is the center of the canvas' starting position */ function moveNode(xPos, yPos){ var nodes = navi.draggedNodes, @@ -1020,7 +1225,8 @@ function GraphFlow(){ /** Saves the position of a node, so that it will be placed there again, - when the graph is altered. + when the graph is refreshed. This function will only be called once + when a node drag operation has finished. */ function saveNodeMove(){ var node = navi.draggedNodes[0], @@ -1047,13 +1253,27 @@ function GraphFlow(){ /** - * adds an edge to the graph - * @param sourceID the id of the node from which the edge starts - * @param targetID the id of the node where the edge ends - * @return true if the edge was successfully added + * Adds a directed edge between two nodes to the graph. + * @param sourceID - the id of the node from which the edge starts + * @param targetID - the id of the node where the edge ends + * @param edgeLabel - the label of the edge. Can be null if no label is required + * @param forced - if true, creates the edge unconditionally and does not trigger + a listener event + * @return true - if the edge was successfully added */ this.addEdge = function(sourceID, targetID, edgeLabel, forced){ + // check for valid arguments + if(!validArg("addEdge()", sourceID, "string")){ + return false; + } + if(!validArg("addEdge()", targetID, "string")){ + return false; + } + if(edgeLabel && !validArg("addEdge()", edgeLabel, "string")){ + edgeLabel = null; + } + // look up the source node and the nodeFamilys of both nodes var source, sourceFamily, targetFamily, target; @@ -1107,13 +1327,14 @@ function GraphFlow(){ "nodeTo": targetID, "nodeFrom": sourceID, "data": {"$direction" : [ sourceID, targetID ], - "$targetFamily" : targetFamilyID}, + "$targetFamily" : targetFamilyID, + "$color" : edgeColor}, }; - if(edgeLabel != null && edgeLabel != undefined){ + if(edgeLabel){ edge.data.$label = edgeLabel; } - + adja.push(edge); if(sourceID == mouseNode.id || targetID == mouseNode.id){ @@ -1124,11 +1345,21 @@ function GraphFlow(){ } /** - Removes the edge between two vertices. - @param source the source Node - @param targetID the id of the target Node + Removes the edge between two nodes. + @param sourceID - the if of the source Node + @param targetID - the id of the target Node + @param forced - if true, removes the edge unconditionally and + does not trigger a listener event */ this.removeEdge = function(sourceID, targetID, forced){ + // check for valid arguments + if(!validArg("removeEdge()", sourceID, "string")){ + return; + } + if(!validArg("removeEdge()", targetID, "string")){ + return; + } + // look up the source node and the nodeFamilys of both nodes var source, sourceFamily, targetFamily, target; @@ -1188,44 +1419,30 @@ function GraphFlow(){ } /** - makes it so that a text label is not selectable. - (unfortunately, this does not prevent CTRL+A selection) - */ - function setLabelUnselectable(label){ - // internet explorer: - if (typeof label.onselectstart!="undefined"){ - label.onselectstart = - function(){return false}; - } - // mozilla firefox: - else if (typeof label.style.MozUserSelect!="undefined"){ - label.style.MozUserSelect = "none"; - } - // others: - else{ - label.onmousedown = - function(){return false}; - } - } - - /** - returns a jsonNode by id - @param nodeID the id of the desired node + Returns a node from the json array + @param nodeID - the id of the desired node + @return - the node object or null if the node does not exist */ this.getNode = function(nodeID){ - for(var n=0; n< json.length; n++){ + // check for valid arguments + if(!validArg("getNode()", nodeID, "string")){ + return; + } + for(var n=0, l = json.length; n < l; n++){ var node = json[n]; - if(node != undefined && node.id == nodeID){ + if(node && node.id == nodeID){ return json[n]; } } return null; } + + /** - clears all highlights, caused by mouse-enter events - @param clearNodes - if true, highlighted nodes will be cleared - @param clearEdges - if true, highlighted edges will be cleared + Highlights exactly one node or edge, and un-highlights the previously highlighted + node or edge. + @param node - the node or edge object which will be highlighted */ function setHighlight(node){ // animation parameters @@ -1240,7 +1457,13 @@ function GraphFlow(){ } else{ var type = hover.data.$type; - hover.setData('color', nodeStrokeColor[type], 'end'); + if(markedNode != null && hover.id == markedNode.id){ + hover.setData('color', markedNode.data.$color, 'end'); + } + else{ + hover.setData('color', nodeStrokeColor[type], 'end'); + } + } hover = null; } @@ -1270,9 +1493,78 @@ function GraphFlow(){ duration: dur }); } + + /** + Highlights exactly one node, and un-highlights the previously highlighted + node. + @param node - the node or edge object which will be highlighted. + Can be null if no new node shall be highlighted + @param strokeColor - the color of the node stroke or a port's fillcolor. + Can be null for no change. + @param fillColor - the fill color of a nodefamily + Can be null for no change. + */ + this.markNode = function(node, strokeColor, fillColor){ + + // check for valid arguments + if(node && !validArgNode("markNode()", node)){ + return; + } + if(strokeColor && !validArgColor("markNode()", strokeColor)){ + return; + } + if(fillColor && !validArgColor("markNode()", fillColor)){ + return; + } + + // clean up the old marking + if(markedNode != null){ + + if(markedNode.nodeFrom != undefined){ + // ignore edges + } + else{ + var type = markedNode.data.$type; + markedNode.data.$color = nodeStrokeColor[type]; + + // change FillColor only if it exists + if(nodeFillColor[type]){ + markedNode.data.$fillColor = nodeFillColor[type]; + } + } + markedNode = null; + } + + // do we highlight something new? + if(node && node != null){ + // convert dom node to json node + + if(node.nodeFrom != undefined){ + // ignore edges + }else{ + var jsonNode = getNode(node.id); + if(!jsonNode){ + callListener("onError", ["Error in function markNode(): Marked Node was deleted."]); + return; + } + + if(strokeColor != null){ + jsonNode.data.$color = strokeColor; + } + + // change FillColor only if it exists + if(fillColor != null && nodeFillColor[node.data.$type]){ + jsonNode.data.$fillColor = fillColor; + } + } + markedNode = jsonNode; + } + } /** - Initializes the Graph + Initializes the Graph, enabling canvas navigation and tooltips. + Also the json array is initialized with a dummy mouse node and + what happens on various mouse events is determined. */ this.initGraph = function(){ json = [ null, null, null, null, null, null, null, null, null, null, @@ -1527,23 +1819,24 @@ function GraphFlow(){ else{ addEdge(node.id, mouseNode.id); } + var pos = node.pos.getc(true); refresh(); + + // set mouse position + mouseNode.pos.setc(pos.x, pos.y); + fd.plot(); return; } // click outside the edge drag box else{ // add Edge if the selectedNode differs from the clickedNode - var newEdgeAdded; if(selectedNode.from == "inputPort"){ var label = selectedNode.label; - newEdgeAdded = addEdge(node.id, selectedNode.id, label); + addEdge(node.id, selectedNode.id, label); } else{ var label = selectedNode.label; - newEdgeAdded = addEdge(selectedNode.id, node.id, label); - } - if(newEdgeAdded){ - refresh(); + addEdge(selectedNode.id, node.id, label); } } break; @@ -1559,7 +1852,11 @@ function GraphFlow(){ removeEdge(selectedNode.id, node.data.$direction[1]); // add edge to mouseNode addEdge(selectedNode.id, mouseNode.id, label); + var pos = eventInfo.getPos(); refresh(); + // set mouse position + mouseNode.pos.setc(pos.x, pos.y-2); + fd.plot(); return; } } @@ -1586,59 +1883,7 @@ function GraphFlow(){ iterations: 0, //Edge length levelDistance: 0, - // This method is only triggered - // on label creation and only for DOM labels (not native canvas ones). - /*onCreateLabel: function(domElement, node){ - // do not add labels if it is a dummy element - // instead save pointer to domElement - if(node.id == "#DUMMY_MOUSE_NODE"){ - mouseNode = node; - return; - } - if(node.data.$type != "nodeFamily"){ - return; - } - - domElement.innerHTML = node.name; - - // set style for name - var style = domElement.style; - style.fontSize = 1.65* fd.canvas.scaleOffsetX* vertexLabelSize+"px"; - style.left = 0.2*vertexLabelSize-domElement.offsetWidth/2+"px"; - setLabelUnselectable(domElement); - }, - - // Change node styles when DOM labels are placed - // or moved. - onPlaceLabel: function(domElement, node){ - if(node.data.$type != "nodeFamily"){ - return; - } - - var style = domElement.style; - var left = parseInt(style.left); - var top = parseInt(style.top); - var scale = fd.canvas.scaleOffsetX; - var w = scale*vertexLabelSize* node.name.length; - - var classWidth = scale*(vertexLabelSize-2)* (node.data.$nodeClass.length+2); - if(classWidth > w){ - w = classWidth; - } - - style.fontSize = scale* 1.65 * vertexLabelSize+"px"; - style.fontFamily = "Lucida Console"; - style.left = (left - w/ 2) + 'px'; - style.top = (top - h ) + 'px'; - style.color = node.data.$color; - style.display = ''; - // add class name - if(node.data.$nodeClass != undefined){ - domElement.innerHTML = node.name; - var fontSize= scale*1.65*(vertexLabelSize-2); - domElement.innerHTML += "<div style=\"font-size:"+fontSize+"px\"> <"+node.data.$nodeClass+">"; - } - }*/ + }); diff --git a/Kieker.WebGUI/src/main/webapp/js/jit.js b/Kieker.WebGUI/src/main/webapp/js/jit.js index 51c63e28681c73fead7041cee5e4840dcb58dea6..15130dccc5618b77dd912ece0d4ddc93971f8358 100644 --- a/Kieker.WebGUI/src/main/webapp/js/jit.js +++ b/Kieker.WebGUI/src/main/webapp/js/jit.js @@ -1228,8 +1228,6 @@ Options.Node = { align: "center", angularWidth:1, span:1, - // FLOWGRAPH: - infoText:"info", //Raw canvas styles to be //applied to the context instance //before plotting a node @@ -4008,6 +4006,12 @@ $jit.Graph = new Class({ var node = this.addNode(obj); var x = node.data.$xPos, y = node.data.$yPos; + if(isNaN(0-x)){ + x = 0; + } + if(isNaN(0-y)){ + y = 0; + } node.pos.setc(x,y); return node; }, @@ -17148,10 +17152,9 @@ $jit.Hypertree.$extend = true; /////////////////////////////////////////////////////////////////// -// FLOW GRAPH MODEL (a slightly different ForceDirected version) // +// GRAPH FLOW MODEL (an extended ForceDirected version) // /////////////////////////////////////////////////////////////////// - /* * File: Layouts.FlowGraph.js * @@ -17874,6 +17877,7 @@ $jit.FlowGraph.$extend = true; } // paint class + var nodeClass = node.data.$nodeClass; if(nodeClass != undefined){ nodeClass = "<"+nodeClass+">"; @@ -17891,6 +17895,7 @@ $jit.FlowGraph.$extend = true; } // draw icon + if(img){ var ix = posX - textWidth - dim*1.3, @@ -17970,40 +17975,6 @@ $jit.FlowGraph.$extend = true; ctx.font = bSize +"px Arial Black"; ctx.fillText("R", bx, by); - /*var grid = size/8, - i = by + 2*grid, - b = by + grid, - h = grid/2, - j = h + grid, - k = by + 3*grid + h, - xh = bx + h, - xmh = bx - h; - - // draw R - ctx.fillStyle = node.getData('fillColor'); - ctx.beginPath(); - ctx.moveTo(bx - j , by + h); - ctx.lineTo(xh , by + h); - ctx.lineTo(bx + j , b); - ctx.lineTo(bx + grid, i); - ctx.lineTo(xh , i); - ctx.lineTo(bx + j , k); - ctx.lineTo(xh , k); - ctx.lineTo(xmh , i); - ctx.lineTo(xmh , k); - ctx.lineTo(bx - j , k); - ctx.closePath(); - ctx.fill(); - - ctx.fillStyle = node.getData('color'); - ctx.beginPath(); - ctx.moveTo(xmh , b); - ctx.lineTo(xh + h/2, b); - ctx.lineTo(xh , by + j); - ctx.lineTo(xmh , by + j); - ctx.closePath(); - ctx.fill();*/ - }, 'contains': function(node, pos){ @@ -18101,19 +18072,19 @@ $jit.FlowGraph.$extend = true; 'grid': { 'render': function(grid, canvas){ - var scale = canvas.scaleOffsetX, - centerX = - canvas.translateOffsetX/scale, - centerY = - canvas.translateOffsetY/scale, + var scale = 1/canvas.scaleOffsetX, + centerX = - canvas.translateOffsetX*scale, + centerY = - canvas.translateOffsetY*scale, size = grid.data.$dim, - width = grid.data.$width/(2*scale), - height = grid.data.$height/(2*scale), + width = grid.data.$width*scale*2, + height = grid.data.$height*scale*2, left = centerX - width, up = centerY - height, right = centerX + width, bottom = centerY + height, ctx = canvas.getCtx(); - ctx.lineWidth = 1; + ctx.lineWidth = scale; ctx.beginPath(); // draw vertical lines for(var x = left - (left % size), l = centerX + width; x < l; x += size){ @@ -18169,24 +18140,35 @@ $jit.FlowGraph.$extend = true; 'render': function(adj, canvas) { var dim = adj.getData('dim'), - direction = adj.data.$direction, - inv = (direction && direction.length>1 && direction[0] != adj.nodeFrom.id), + inverse = adj.data.$direction[0] != adj.nodeFrom.id, posFrom = adj.nodeFrom.pos.getc(true), posTo = adj.nodeTo.pos.getc(true); var from = {"x": posFrom.x, "y" : posFrom.y+dim/2}; var to = {"x": posTo.x, "y" : posTo.y+dim/2}; - if(direction[0] == adj.nodeFrom.id){ - to.x -= dim; - from.x += dim; + // swap points if the edge direction is "wrong" + if(inverse){ + var temp = to; + to = from; + from = temp; } - else{ - to.x += dim; - from.x -= dim; + + to.x -= dim; + from.x += dim; + + // draw potential bend points + var bend = adj.data.$bendPoints, + toBend; + if(bend){ + for(var b = 0, l = bend.length; b < l; b++){ + toBend = bend[b]; + this.edgeHelper.line.render(from, toBend, canvas); + from = bend[b]; + } } - this.edgeHelper.flowarrow.render(from, to, dim, inv, canvas); + this.edgeHelper.flowarrow.render(from, to, dim, false, canvas); // add the label var label = adj.data.$label; @@ -18203,24 +18185,35 @@ $jit.FlowGraph.$extend = true; }, 'contains': function(adj, pos) { var dim = adj.getData('dim'), - direction = adj.data.$direction, - inv = (direction && direction.length > 1 && direction[0] != adj.nodeFrom.id), + inverse = adj.data.$direction[0] != adj.nodeFrom.id, posFrom = adj.nodeFrom.pos.getc(true), posTo = adj.nodeTo.pos.getc(true); var from = {"x": posFrom.x, "y" : posFrom.y+dim/2}; var to = {"x": posTo.x, "y" : posTo.y+dim/2}; - if(direction[0] == adj.nodeFrom.id){ - to.x -= dim; - from.x += dim; - } - else{ - to.x += dim; - from.x -= dim; + // swap if the arrow points in inverse order + if(inverse){ + var temp = to; + to = from; + from = temp; } + to.x -= dim; + from.x += dim; + + // check if mouse is between bendpoints + var bend = adj.data.$bendPoints, + toBend, + contains = false; + if(bend){ + for(var b = 0, l = bend.length; b < l; b++){ + toBend = bend[b]; + contains |= this.edgeHelper.flowarrow.contains(from, toBend, pos, this.edge.epsilon); + from = bend[b]; + } - return this.edgeHelper.flowarrow.contains(from, to, pos, this.edge.epsilon); + } + return contains || this.edgeHelper.flowarrow.contains(from, to, pos, this.edge.epsilon); } }, @@ -18245,16 +18238,22 @@ $jit.FlowGraph.$extend = true; this.edgeHelper.flowarrow.render(from, to, dim, inv, canvas); // add the label - // apparently this throws an error on FireFox... - /*var label = adj.data.$label; + var label = adj.data.$label; if(label != undefined){ var ctx = canvas.getCtx(); ctx.font = (1.23 * dim)+"px Arial"; var midX = (from.x + to.x - ctx.measureText(label).width) / 2, midY = (from.y + to.y -dim) / 2; - ctx.fillText(label, midX, midY); - }*/ + try{ + ctx.fillText(label, midX, midY); + } + catch(e){ + // do nothing. This "fixes" the + // "An invalid or illegal string was specified" + // bug in FireFox + } + } }, 'contains': function(adj, pos) { return false; diff --git a/Kieker.WebGUI/src/main/webapp/pages/AnalysisEditorPage.xhtml b/Kieker.WebGUI/src/main/webapp/pages/AnalysisEditorPage.xhtml index 1f7694466ba46a3607014971bbfa1953514ddbc5..e15d5389aba1a72a2dfeac28b84e362e31205ab0 100644 --- a/Kieker.WebGUI/src/main/webapp/pages/AnalysisEditorPage.xhtml +++ b/Kieker.WebGUI/src/main/webapp/pages/AnalysisEditorPage.xhtml @@ -41,6 +41,9 @@ <script> nodeClickListener = function(node, info, e) { nodeClickCommand([{name : 'ID', value : node.id}]); + markNode(node, '#FF0000'); + // TODO Selektieren + graph.refresh(); } nodeRemoveListener = function(node) {