ProcessFlow = (function() { var _processFlow = null; /** * The list of flow items in the process flow diagram */ var _flowItemList = new HashMap(); /** * The list of connections in the process flow diagram */ var _connectionList = new HashMap(); /** * The current connection routing style */ var _currConnRouteStyle = ""; var _isIE = false; // ----------------------------------------------------------- // Start : NodeSelectionHandler // ----------------------------------------------------------- /** * This class handles all node selection operations. * This is a SINGLETON class */ var NodeSelectionHandler = (function() { var _selectionStartPoint = null; var _selectionSvgBlock = null; var _selectionInitiated = false; var _selectedNodes = []; var _jQueryparentOffSet = null; function _clearCurrentSelection(){ for(var i = 0; i < _selectedNodes.length; i++){ _selectedNodes[i].setSelected(false); } _selectedNodes = []; } return { /** * Indicate whether block selection is currently happening */ isSelecting : function(){ return _selectionInitiated; }, /** * Return the list of selected nodes */ getSelectedNodes : function(){ return _selectedNodes; }, /** * Sets the list of selected nodes */ setSelectedNodes : function(nodes){ _clearCurrentSelection(); for(var i = 0; i < nodes.length; i++){ NodeSelectionHandler.selectNode(nodes[i]); } }, /** * Enable the block selection mode * @param {Boolean} enabled */ setBlockSelectionEnabled : function(enabled){ if(enabled){ _clearCurrentSelection(); jQuery(opts.canvas).css('cursor', 'crosshair') .bind('mousedown', NodeSelectionHandler.startBlockSelection) .bind('mouseup', NodeSelectionHandler.completeSelection) .bind('mousemove', NodeSelectionHandler.scaleSelection); jQuery(document) // Allow the user to cancel the block selection process by pressing // the escape key .one('keyup', function(e){ if(e.keyCode == 27){ NodeSelectionHandler.setBlockSelectionEnabled(false); _clearCurrentSelection(); } }) // Allow the user to cancel the block selection by right-clicking // anywhere else on the canvas .one('contextmenu', function(e){ if(e.which == 3){ NodeSelectionHandler.setBlockSelectionEnabled(false); _clearCurrentSelection(); } return false; }); }else{ jQuery(opts.canvas).css('cursor', 'default') .unbind('mousedown', NodeSelectionHandler.startBlockSelection) .unbind('mouseup', NodeSelectionHandler.completeSelection) .unbind('mousemove', NodeSelectionHandler.scaleSelection); } }, /** * Start the block selection action. Click one the user * presses and hold the left mouse button * @param {Event} e */ startBlockSelection : function(e){ _jQueryparentOffSet = jQuery(opts.canvas).offset(); _selectionStartPoint = new Point(e.pageX - _jQueryparentOffSet.left, e.pageY - _jQueryparentOffSet.top); _selectionInitiated = true; }, /** * Scale the selection block size has the user moves their mouse * @param {Event} e */ scaleSelection : function(e){ if(_selectionInitiated){ var selectionX, selectionY, selectionWidth, selectionHeight, mouseX, mouseY; mouseX = e.pageX - _jQueryparentOffSet.left; mouseY = e.pageY - _jQueryparentOffSet.top; // Calculate the X position if(mouseX < _selectionStartPoint.getX()){ selectionX = mouseX; }else{ selectionX = _selectionStartPoint.getX(); } // Calculate the Y position if(mouseY < _selectionStartPoint.getY()){ selectionY = mouseY; }else{ selectionY = _selectionStartPoint.getY(); } selectionWidth = Math.abs(mouseX - _selectionStartPoint.getX()); selectionHeight = Math.abs(mouseY - _selectionStartPoint.getY()); if(_selectionSvgBlock == null){ _selectionSvgBlock = _processFlow.getCanvas().rect(_selectionStartPoint.getX(),_selectionStartPoint.getY(), selectionWidth, selectionHeight, 1) .initZoom() .setAttr({'stroke-width' : 2, 'stroke' : '#558ed5', fill : '#558ed5', 'fill-opacity' : '0.2'}); }else{ _selectionSvgBlock.animate({x : selectionX, y : selectionY, width : selectionWidth, height : selectionHeight},1); } } }, /** * Complete the selection process. * Here we will check to see what items have been included in the * selection block. Called when the user releases their left mouse * button * @param {Event} e */ completeSelection : function(e){ if(_selectionInitiated){ _selectionInitiated = false; if(_selectionSvgBlock != null){ _selectedNodes = []; // Determine which items have been selected var selectionBBox = _selectionSvgBlock.getBBox(); var selectionDim = new Dimension(selectionBBox.x, selectionBBox.y, selectionBBox.width, selectionBBox.height); var nodes = _flowItemList.valSet(); for(var i = 0; i < nodes.length; i++){ if(selectionDim.isContainsPoint(nodes[i].getDimensions().getTopLeft()) && selectionDim.isContainsPoint(nodes[i].getDimensions().getBottomRight())){ NodeSelectionHandler.selectNode(nodes[i]); }else{ nodes[i].setSelected(false); } } // Clean up the selection mechanism _selectionSvgBlock.remove(); _selectionSvgBlock = null; } } }, selectOnlyNode: function(_node) { _clearCurrentSelection(); this.selectNode(_node); }, selectNode: function(_node) { _node.setSelected(true); _selectedNodes.push(_node); } }; })(); // ----------------------------------------------------------- // End : NodeSelectionHandler // ----------------------------------------------------------- /* * ---------------------------------------------------- * Class - Point * ---------------------------------------------------- */ /** * Represents a point on the canvas. Used by the * connection routing mechanism to calculate vertices * coordinates * @constructor * @param {Integer} x * @param {Integer} y */ var Point = function(_x,_y){ // PRIVILEGED Methods /** * Get the X position * @return {Integer} x */ this.getX = function(){ return _x; }; /** * Set the X position * @param {Ineteger} x */ this.setX = function(x){ _x = x; }; /** * Get the Y position * @return {Integer} y */ this.getY = function(){ return _y; }; /** * Set the Y position * @param {Ineteger} x */ this.setY = function(y){ _y = y; }; }; // PUBLIC Methods Point.prototype = { /** * Calculate the distance between this point and the point specified * as a parameter * * @param {Point} other * @returns {Integer} distance from one point to another */ getDistance : function(other){ return Math.sqrt((this.getX()-other.getX())*(this.getX()-other.getX())+(this.getY()-other.getY())*(this.getY()-other.getY())); }, /** * Calculates the relative position of the specified Point to this Point. * @param {Point} p The reference Point * @return NORTH, SOUTH, EAST, or WEST, as defined in {@link PositionConstants} */ getPosition : function(p){ var dx = p._getX() - this._getX(); var dy = p._getY() - this._getY(); if (Math.abs(dx) > Math.abs(dy)){ if (dx < 0) { return PositionConstants.WEST; } return PositionConstants.EAST; } if (dy < 0) { return PositionConstants.NORTH; } return PositionConstants.SOUTH; } }; // // ----------------------------------------------------------- // End - Point // ----------------------------------------------------------- // // ----------------------------------------------------------- // Start : NodeSelectionHandler // ----------------------------------------------------------- /** * This class handles all node selection operations. * This is a SINGLETON class */ var NodeAlignmentHandler = (function(){ return { /** * Align the nodes according to the specified alignments options * @param alignMent * @returns */ alignNodes : function(alignment){ var positions = []; var selectedNodes = NodeSelectionHandler.getSelectedNodes(); // Get the bottom positions of all selected nodes for(var i = 0; i < selectedNodes.length; i++){ var shapeDim = selectedNodes[i].getDimensions(); switch(alignment){ case(NodeAlignmentHandler.ALIGN_LEFT) : positions.push(shapeDim.getX()); break; case(NodeAlignmentHandler.ALIGN_TOP) : positions.push(shapeDim.getY()); break; case(NodeAlignmentHandler.ALIGN_RIGHT) : positions.push(shapeDim.getBottomRight().getX()); break; case(NodeAlignmentHandler.ALIGN_BOTTOM) : positions.push(shapeDim.getBottomRight().getY()); break; case(NodeAlignmentHandler.ALIGN_MIDDLE) : positions.push(shapeDim.getCenter().getY()); break; case(NodeAlignmentHandler.ALIGN_CENTER) : positions.push(shapeDim.getCenter().getX()); break; } } var maxPoint = Array.max(positions); var minPoint = Array.min(positions); for(var i = 0; i < selectedNodes.length; i++){ var shapeDim2 = selectedNodes[i].getDimensions(); switch(alignment){ case(NodeAlignmentHandler.ALIGN_LEFT) : selectedNodes[i].setX(minPoint); break; case(NodeAlignmentHandler.ALIGN_TOP) : selectedNodes[i].setY(minPoint); break; case(NodeAlignmentHandler.ALIGN_RIGHT) : selectedNodes[i].setX(shapeDim2.getX() + (maxPoint - (shapeDim2.getWidth() + shapeDim2.getX()))); break; case(NodeAlignmentHandler.ALIGN_BOTTOM) : selectedNodes[i].setY(shapeDim2.getY() + (maxPoint - (shapeDim2.getHeight() + shapeDim2.getY()))); break; case(NodeAlignmentHandler.ALIGN_MIDDLE) : selectedNodes[i].setY(shapeDim2.getY() + (maxPoint - shapeDim2.getCenter().getY())); break; case(NodeAlignmentHandler.ALIGN_CENTER) : selectedNodes[i].setX(shapeDim2.getX() + (maxPoint - shapeDim2.getCenter().getX())); break; } } } }; })(); NodeAlignmentHandler.ALIGN_TOP = 'top'; NodeAlignmentHandler.ALIGN_BOTTOM = 'bottom'; NodeAlignmentHandler.ALIGN_LEFT = 'left'; NodeAlignmentHandler.ALIGN_RIGHT = 'right'; NodeAlignmentHandler.ALIGN_CENTER = 'center'; NodeAlignmentHandler.ALIGN_MIDDLE = 'middle'; // ----------------------------------------------------------- // End : NodeSelectionHandler // ----------------------------------------------------------- // ----------------------------------------------------------- // Start : NodeConnectHandler // ----------------------------------------------------------- /** * This class handles all connection actions. * This is a SINGLETON class */ var NodeConnectHandler = (function() { var _port1 = null; var _port2 = null; var _indicatorLineSvgObj = null; var _lineColor = '#7f7f7f'; var _candidateNodes = []; var _fromNodeType = null; var _connectionType = null; var _jQueryparentOffset = null; /** * Complete the connection between the 2 selected points */ function _completeConnection(){ var connection = new Connection(opts.connectionIdFactory.call(this), -1, _connectionType.type, _port1, _port2, null , opts.dflt_conn_line_width, _connectionType.line_label, null, null, null, false, false); _connectionList.put(connection.getId(), connection); _stopIndicatorLineDrawing(); } /** * Drawing a connection indicator line between the first * point and the cursor */ function _drawIndicatorLine(e){ if(_indicatorLineSvgObj != null){ _indicatorLineSvgObj.remove(); } var mouseX, mouseY; mouseX = e.pageX - _jQueryparentOffset.left; mouseY = e.pageY - _jQueryparentOffset.top; var startPoint = _port1.getMiddlePoint(); var linePath = 'M ' + startPoint.getX() + ' ' + startPoint.getY(); linePath += 'L ' + (mouseX - 5) + ' ' + (mouseY); _indicatorLineSvgObj = _processFlow.getCanvas().path(linePath) .initZoom() .setAttr({'stroke' : _lineColor, 'stroke-dasharray' : ['-'], 'stroke-width' : 2}); } /** * Stop the indicator line drawing process */ function _stopIndicatorLineDrawing(){ _deactivatePorts(); jQuery(opts.canvas).unbind('mousemove', _drawIndicatorLine) .css('cursor', 'default'); if(_indicatorLineSvgObj != null){ _indicatorLineSvgObj.remove(); _indicatorLineSvgObj = null; } _port1 = null; _port2 = null; } function _deactivatePorts(){ if(_candidateNodes.length > 0){ for(var i = 0; i < _candidateNodes.length; i++){ _candidateNodes[i].setConnectionPortsVisible(false); } } _candidateNodes = []; } /** * Initialize the indicator line drawing */ function _startIndicatorLineDrawing(){ _jQueryparentOffset = jQuery(opts.canvas).offset(); jQuery(opts.canvas).bind('mousemove', _drawIndicatorLine); } return { /** * Start indicator line drawing process for * the specific line type * @param {String} connectionType */ startLineDrawing : function(node, connectionType){ _fromNodeType = node.getType(); _connectionType = connectionType; var supportedConnectionTargets = opts.connection_rules[_connectionType.type]; _deactivatePorts(); if(supportedConnectionTargets != null && supportedConnectionTargets.length > 0){ jQuery(opts.canvas).css('cursor', 'crosshair'); var flowItems = _flowItemList.valSet(); node.setConnectionPortsVisible(true); node.setConnectionPointType(Connection.SOURCE); _candidateNodes.push(node); // Loop through all nodes and check if it one of the supported targets // for this connection type for(var i = 0; i < flowItems.length; i++){ // If the node is the from type or the node is one // of the supported connection target types if (jQuery.inArray(flowItems[i].getType(), supportedConnectionTargets) >= 0){ flowItems[i].setConnectionPortsVisible(true); flowItems[i].setConnectionPointType(Connection.TARGET); _candidateNodes.push(flowItems[i]); } } // Stop the line drawing when the user lets go of the mouse // button while over the canvas jQuery(opts.canvas).one('mouseup', function(e){ _stopIndicatorLineDrawing(); }); // Allow the user to cancel the line drawing process by clicking escape jQuery(document).one('keyup', function(e){ if(e.keyCode == 27){ _stopIndicatorLineDrawing(); } }); } }, setLineValid : function(isOverValidPoint){ if(_indicatorLineSvgObj != null){ if(isOverValidPoint){ _lineColor = Connection.LINE_COLOR_HOVER; }else{ _lineColor = Connection.LINE_COLOR_DEFAULT; } } }, /** * Add a connection point target. * When 2 different target ports are selected the * connection is drawn and a connectionAdded event is fired * @param port */ addConnectionPoint : function (port) { var connection = port.getParentNode().getConnectionBeingChanged(); if(connection != null){ if(connection.getChangingPort() == Connection.SOURCE){ connection.setSourcePort(port); }else{ connection.setTargetPort(port); } }else{ if(_port1 == null){ if(port.getParentNode().getType() == _fromNodeType){ _port1 = port; _startIndicatorLineDrawing(); } }else { // Prevent self joins if(_port1 != port){ _port2 = port; } } if(_port1 != null && _port2 != null){ _completeConnection(); } } }, connectNodes : function(port1, port2, type, line_width, line_label) { var id = opts.connectionIdFactory.call(this); var connection = new Connection(id, -1, type, port1, port2, null , line_width, line_label, null, null, null, false, false); _connectionList.put(connection.getId(), connection); return id; } }; })(); // ----------------------------------------------------------- // End : NodeConnectHandler // ----------------------------------------------------------- // ----------------------------------------------------------- // Start : FlowItemContextMenuFactory // ----------------------------------------------------------- /** * Utility class used to add a context menu to the * flow items */ var FlowItemContextMenuFactory = (function(){ /** * Create the menu item option in the * format expected by the context menu * plugin */ function _createMenuItem(_label, _icon, _onClick, _className){ var menuItem = {}; menuItem[_label] = { onclick : _onClick}; if (_icon) { menuItem[_label].icon = _icon; } if (_className) { menuItem[_label].className = _className; menuItem[_label].hoverClassName = _className + "-hover"; } return menuItem; } /** * Link the menu to the flow items SVG element * so that it will be opened when the user right clicks * on the element. */ function _addContextMenuToItem(node, menuJson){ jQuery(node).contextMenu(menuJson, {theme : 'vista' , showTransition : 'fadeIn', hideTransition : 'fadeOut', showSpeed : 250, hideSpeed : 250}); } /** * Create the context menu for a node item * NOTE: The Connection and Node classes should ideally implement * the same interface but javascript doesn't have native support * for interfaces * @param {Node} node */ function _createNodeContextMenu(node){ var menu = []; // Add the supported connection types as menu items var node_connection_types = opts.node_connection_types[node.getType()]; if(node_connection_types != null && node_connection_types.length > 0){ for(var i = 0; i < node_connection_types.length; i++){ var connectionType = node_connection_types[i]; menu.push(_createMenuItem(connectionType.menu_label, null, function(myConnectionType){ return function() { NodeConnectHandler.startLineDrawing(node, myConnectionType); }; }(connectionType), 'menu-item-connect')); } } menu.push(jQuery.contextMenu.separator); menu.push(_createMenuItem('Edit', null, function(){ if(opts.onNodeEdit != null){ var nodeItemValues = null; if(opts.node_item_values != null){ nodeItemValues = opts.node_item_values['' + node.getId()]; } opts.onNodeEdit.call(opts.onNodeEdit, node, nodeItemValues); } }, 'menu-item-edit' )); menu.push(_createMenuItem('Delete', null, function(){ if(confirm('Delete the "' + node.getLabel() + '" node?')){ node.destroy(); } }, 'menu-item-delete' )); _addContextMenuToItem(node.getShape().getSvgParentShape().node, menu); } /** * Create the context menu for the connection line * NOTE: The Connection and Node classes should ideally implement * the same interface but javascript doesn't have native support * for interfaces. * @param {Connection} connection */ function _createConnectionContexMenu(connection){ var menu = []; menu.push(_createMenuItem('Edit', null, function(){ if(opts.onConnectionEdit != null){ var connItemVals = null; if(opts.connection_item_values != null){ connItemVals = opts.connection_item_values['' + connection.getId()]; } opts.onConnectionEdit.call(this, connection, connItemVals); } } )); menu.push(_createMenuItem('Delete', null, function(){ if(confirm('Delete the "' + connection.getLabel() + '" connection?')){ connection.destroy(); } } )); _addContextMenuToItem(connection.getSvgShape().node, menu); } return { /** * Adds a context menu to the specified flow item. * NOTE: This function accepts an instance of Node or * Connection. Ideally these classes should implement the * same Interface i.e IFlowItem but Javascript doesn't * natively support the concept of an Interface. * @param {IFlowItem} flowItem */ addContextMenu : function(flowItem){ if (flowItem.hasContextMenu()) { return; } if(opts.editable == true){ // console.log("Adding context menu") if(flowItem instanceof Node){ _createNodeContextMenu(flowItem); }else if (flowItem instanceof Connection){ _createConnectionContexMenu(flowItem); } flowItem.setHasContextMenu(true); } } }; })(); // ----------------------------------------------------------- // End : FlowItemContextMenuFactory // ----------------------------------------------------------- // ----------------------------------------------------------- // Start : Shape // ----------------------------------------------------------- /** * The shape * * @constructor * @param {Integer} * x * @param {Integer} * y */ var Shape = function(_entity_id, _x, _y, _borderColor, _borderWidth, _bgColor, _labelColor, _labelFont, _labelFontSize, _labelFontBold, _labelFontItalic) { var _thisObj = this; // Private Variables var _iconUrl = null; var _parentSvgShape = null; var _selectionSvgShape = null; var _svgShapeSet = null; var _svgLabel = null; var _onDragStartCallback = null; var _onDragCallback = null; var _onDragStopCallback = null; var _isSelected = false; var _currentScaleX = 1; var _currentScaleY = 1; var _label = null; // Set default font values if(_labelColor == null || _labelColor.length == 0){ _labelColor = '#000000'; }; if(_labelFont == null || _labelFont.length == 0){ _labelFont = 'Tahoma'; }; if(_labelFontSize == null || _labelFontSize.length == 0){ _labelFontSize = 10; }; if(_labelFontBold == null || _labelFontBold.length == 0){ _labelFontBold = false; }; if(_labelFontItalic == null || _labelFontItalic.length == 0){ _labelFontItalic = false; }; _x = parseInt(_x); _y = parseInt(_y); _borderWidth = parseInt(_borderWidth); // DragAndDrop helper variables // Used for performance optimization var _isSelecting; var _shapeSets; // Private functions /** * Get the position that the shape will be moved * from */ function _getShapeMoveFromPoint(shape){ var currentX = 0; var currentY = 0; if(shape.type == 'circle'){ currentX = shape.attr('cx'); currentY = shape.attr('cy'); } else if (shape.type == 'path') { currentX = 0; currentY = 0; } else{ currentX = shape.attr('x'); currentY = shape.attr('y'); } return new Point(currentX, currentY); } /** * If this shape is selected create a merged * version of all the shape sets from the selected * nodes so that the shapes can be dragged as a unit */ function _getSeletedShapeSets(){ var shapeSets = []; if(_isSelected){ var selectedNodes = NodeSelectionHandler.getSelectedNodes(); for(var i = 0; i < selectedNodes.length; i++){ shapeSets.push(selectedNodes[i].getShape().getSvgShapeSet()); } }else{ shapeSets.push(_svgShapeSet); } return shapeSets; } /** * Move the shape from the specified position * by the specified number of x,y pixels */ function _moveShape(shape, currentX, distX, currentY, distY){ switch (shape.type) { case "rect": case "text": case "image": shape.attr({ x: currentX + distX, y : currentY + distY}); break; case "path": shape.translate(distX, distY); break; case "circle": shape.attr({ cx: currentX + distX, cy: currentY + distY}); } }; // Privileged Methods /** * Set the SVG parent shape */ this.setSvgParentShape = function(parentShape){ _parentSvgShape = parentShape; }; /** * Return the raphael VG shape reference * @return {Raphael} the Raphael SVG shape reference */ this.getSvgParentShape = function(){ return _parentSvgShape; }; /** * Set the SvgShapeSet * @param {Raphael} shape set */ this.setSvgShapeSet = function(shapeSet){ _svgShapeSet = shapeSet; }; /** * Return the Raphael shape set reference. * The shape set contains the parent shape, the icon, and the label * and all other elements that form part of the main shape. */ this.getSvgShapeSet = function(){ return _svgShapeSet; }; /** * Store the reference to the label SVG object * @param svgLabel */ this.setSvgLabel = function(svgLabel){ _svgLabel = svgLabel; }; /** * Set the x position of the shape */ this.setX = function(x){ this.setPosition(x, null); }; /** * Set the y position of the shape */ this.setY = function(y){ this.setPosition(null, y); }; /** * Return the shapes entity ID * Note: This is not the same as the node ID */ this.getEntityId = function(){ return _entity_id; }; /** * Set the entity id * @param {Integer} entity_id */ this.setEntityId = function(entity_id){ _entity_id = entity_id; }; /** * Set the new position */ this.setPosition = function(x, y){ _x = x; _y = y; var mainShapeBBox = _parentSvgShape.getBBox(); var distToMovX = (x == null ? 0 : x - mainShapeBBox.x); var distToMoveY = (y == null ? 0 : y - mainShapeBBox.y); var moveFromPoint = null; for(var i = 0; i < _svgShapeSet.length; i++){ var shape = _svgShapeSet[i]; moveFromPoint = _getShapeMoveFromPoint(shape); _moveShape(shape, moveFromPoint.getX(), distToMovX, moveFromPoint.getY(), distToMoveY); } if(_selectionSvgShape != null){ moveFromPoint = _getShapeMoveFromPoint(_selectionSvgShape); _moveShape(_selectionSvgShape, moveFromPoint.getX(), distToMovX, moveFromPoint.getY(), distToMoveY); } }; /** * Return the shapes border colour */ this.getBorderColor = function(){ return _borderColor; }; /** * Return the shapes border width */ this.getBorderWidth = function(){ return _borderWidth; }; /** * Returns the shapes background colour */ this.getBgColor = function(){ return _bgColor; }; /** * Set the label on the shape */ this.setLabel = function(label){ _label = label; // Redraw the label if the shape has alread been created if(_parentSvgShape != null ){ this.redrawLabel(); } }; /** * Set the shape style. This function expects an objects with the following * properties: * font_color, * border_width, * border_color, * fill */ this.setStyle = function(newStyle){ var styleDflt = { border_color : _borderColor, fill : _bgColor, border_width : _borderWidth, border_color : _borderColor, animate : true, font_style : { font : _labelFont, font_size : _labelFontSize, font_bold : _labelFontBold, font_italic : _labelFontItalic, font_color : _labelColor } }; var style = jQuery.extend(true, {}, styleDflt, newStyle); // Set the parent Shape Style _bgColor = style.fill; _borderWidth = style.border_width; _borderColor = style.border_color; var shapeAttr = { fill : _bgColor, 'stroke-width' : _borderWidth, 'stroke' : _borderColor }; _parentSvgShape.setAttr(shapeAttr); // Set the Label Style _labelColor = style.font_style.font_color; _labelFont = style.font_style.font; _labelFontSize = style.font_style.font_size; _labelFontBold = style.font_style.font_bold; _labelFontItalic = style.font_style.font_italic; if(_label != null && _label.length > 0){ _svgLabel.setAttr({ fill : _labelColor, 'font-family' : _labelFont, 'font-weight' : (_labelFontBold == true ? 'bold' : 'normal'), 'font-size' : _labelFontSize, 'font-style' : (_labelFontItalic == true ? 'italic' : 'normal') }); } if(style.animate == true){ var currScale = _parentSvgShape.attr('scale'); _parentSvgShape.animate( {scale : [currScale.x * 1.1, currScale.y * 1.1]}, 200, function(){ _parentSvgShape.animate({scale : [currScale.x, currScale.y]}, 200); } ); } }; /** * Return the label text displayed on the shape */ this.getLabel = function(){ return _label; }; /** * Set the URL of the icon to be displayed in the shape * * @param iconUrl * @returns */ this.setIconUrl = function(iconUrl){ _iconUrl = iconUrl; }; /** * Return the Icon URL * @return {String} the Icon URL */ this.getIconUrl = function(){ return _iconUrl; }; /** * Return the label color */ this.getLabelColor = function(){ return _labelColor; }; /** * Return the label font family */ this.getLabelFont = function(){ return _labelFont; }; /** * Return the label font size */ this.getLabelFontSize = function(){ return _labelFontSize; }; /** * Return the bold indicator */ this.isLabelBold = function(){ return _labelFontBold; }; /** * Return the italics indicator */ this.isLabelItalic = function(){ return _labelFontItalic; }; /** * Set the onDragStart callback */ this.setOnDragStartCallback = function(callbackFunction){ _onDragStartCallback = callbackFunction; }; /** * Return the onDragStart callback */ this.getOnDragStartCallback = function(){ return _onDragStartCallback; }; /** * Set the onDrag callback */ this.setOnDragCallback = function(callBackFunction){ _onDragCallback = callBackFunction; }; /** * Get the onDrag callback */ this.getOnDragCallback = function(){ return _onDragCallback; }; /** * Set the onDragStop callback */ this.setOnDragCompleteCallback = function(callBackFunction){ _onDragStopCallback = callBackFunction; }; /** * Get the onDragStop callback */ this.getOnDragCompleteCallback = function(){ return _onDragStopCallback; }; /** * Set whether this shape is selected or not */ this.setSelected = function(selected){ _isSelected = selected; }; /** * Drag and drop helper function for the raphael * SVG objects * Set the object properties when the drag starts */ this.dragStart = function() { _isSelecting = NodeSelectionHandler.isSelecting(); _shapeSets = _getSeletedShapeSets(); if(!_isSelecting){ for(var j = 0; j < _shapeSets.length; j++){ var shapeSet = _shapeSets[j]; if(shapeSet.shapeObjRef.getOnDragStartCallback() != null){ shapeSet.shapeObjRef.getOnDragStartCallback().apply(this); } // store the starting point for each item in the set for (var i = 0; i < shapeSet.items.length; i++) { var obj = shapeSet.items[i]; obj.dx = 0; obj.dy = 0; if(obj.type == 'path'){ obj.ox = 0; obj.oy = 0; }else{ obj.ox = (obj.type == 'circle' ? obj.attr("cx") : obj.attr("x")); obj.oy = (obj.type == 'circle' ? obj.attr("cy") : obj.attr("y")); } if(obj.type == 'image' || obj.type == 'text'){ obj.hide(); } shapeSet.items[i].setAttr({ 'opacity' : '0.5'}); } } } }; /** * Drag and drop helper function for the raphael * SVG objects * Set object properties when the drag stops. */ this.dragStop = function() { if(!_isSelecting){ for(var j = 0; j < _shapeSets.length; j++){ var shapeSet = _shapeSets[j]; // remove the starting point for each of the objects for (var i = 0; i < shapeSet.items.length; i++) { var obj = shapeSet.items[i]; if(obj.type == 'image' || obj.type == 'text'){ obj.setAttr({ x: obj.ox + obj.dx, y: obj.oy + obj.dy}); obj.show(); } delete (obj.ox); delete (obj.oy); delete (obj.dx); delete (obj.dy); shapeSet.items[i].setAttr('opacity' , '1'); } if(shapeSet.shapeObjRef.getOnDragCompleteCallback() != null){ shapeSet.shapeObjRef.getOnDragCompleteCallback().apply(this); } } } }; /** * Drag and drop helper function for the raphael * SVG objects. * Manage the object will it is dragged */ this.dragMove = function(dx, dy) { //console.time("DragTime"); if(!_isSelecting){ for(var j = _shapeSets.length - 1; j >= 0; j--){ var shapeSet = _shapeSets[j]; // reposition the objects relative to their start position for (var x = shapeSet.items.length - 1; x >= 0; x--) { var obj = shapeSet.items[x]; switch (obj.type) { case "rect": obj.setAttr({ x: obj.ox + dx, y: obj.oy + dy }); break; case "path": obj.setTranslation(dx - obj.ox, dy - obj.oy); obj.ox = dx; obj.oy = dy; break; case "circle": obj.setAttr({ cx: obj.ox + dx, cy: obj.oy + dy }); } // Store the drag distance // Will be used to reposition the image and label // when dragging is complete obj.dx = dx; obj.dy = dy; } // Execute this conditionally based on browser type // Dont execute in IE if(_isIE == false){ var onDragCallback = shapeSet.shapeObjRef.getOnDragCallback(); if(onDragCallback != null){ onDragCallback.apply(this); } } } } //console.timeEnd("DragTime"); }; /** * Scale all the elements in this shape group * by the specified x and y percentages */ this.scale = function(scaleX, scaleY){ for(var i = 0; i < _svgShapeSet.length; i++){ _svgShapeSet[i].scale(scaleX / _currentScaleX, scaleY / _currentScaleY); _svgShapeSet[i].applyScale(); _currentScaleX = scaleX; _currentScaleY = scaleY; } }; /** * Rotate all the elements in this shape group * by the specified number of degrees */ this.rotate = function(degrees){ for(var i = 0; i < _svgShapeSet.length; i++){ _svgShapeSet[i].rotate(degrees, true); } }; /** * Destroy the shape */ this.destroy = function(){ for(var i = 0; i < _svgShapeSet.length; i++){ _svgShapeSet[i].remove(); } }; }; Shape.prototype = { /** * Abstract method to be implemented by the classes * that extend Shape * @return {Set} */ draw : function(){}, /** * Abstract mentod to be implemented by the classes * that extend Shape * @return {Object} */ toJSON : function(){}, /** * Redraw the label on the shape * To be implemented by the child classes */ redrawLabel : function(){}, /** * Remove the label from the shape */ replaceLabel : function(newLabel){ var svgShapeSet = this.getSvgShapeSet(); // Remove the original label for(var i = 0; i < svgShapeSet.length; i++){ var shape = svgShapeSet[i]; if(shape.type == 'text'){ shape.remove(); } } if(newLabel != null){ svgShapeSet.push(newLabel); } this.setSvgLabel(newLabel); }, /** * Create a new SVG label * @param left * @param top * @return Label */ createSVGLabel : function(left, top, label){ return _processFlow.getCanvas().text(left, top, label).initZoom() .setAttr({'font-family' : this.getLabelFont(), 'font-size' : this.getLabelFontSize(), fill : this.getLabelColor(), 'font-style' : (this.isLabelItalic() == true ? "italic" : "normal"), 'font-weight' : (this.isLabelBold() == true ? "bold" : "normal") }); }, /** * Create a new SVG image * @param iconUrl * @param left * @param top * @return Image */ createSVGImage : function(iconUrl, left, top){ // TODO: Assuming height and width of 16px. Need to make this more dynamic return _processFlow.getCanvas().image(iconUrl, left, top, 16, 16).initZoom(); }, /** * Return the shape dimesions * @return {Dimension} the shape dimensions */ getDimensions : function(){ var svgParentShape = this.getSvgParentShape(); var bbBox = svgParentShape.getBBox(); var borderWidth = svgParentShape.attr("stroke-width"); return new Dimension(bbBox.x - borderWidth / 2, bbBox.y - borderWidth / 2, bbBox.width + borderWidth, bbBox.height + borderWidth); }, /** * Setup the shape drag and drop functionality */ initDragNDrop : function(shapeSet, enclosingShape){ if(opts.editable == true){ enclosingShape.drag(this.dragMove, this.dragStart, this.dragStop); // Store the Shape object ref on the shape set // to make it easier to work with the shape sets // when dragging. shapeSet.shapeObjRef = this; } }, /** * Initialise all shape events */ initEvents : function(){ var thisObject = this; var parentShape = this.getSvgParentShape(); jQuery(parentShape.node).hover( function(){ parentShape.setAttr('stroke-dasharray', ['-']); }, function(){ parentShape.setAttr('stroke-dasharray', ['']); }); } }; // Constants Shape.CIRCLE = 'circle'; Shape.RECTANGLE = 'rect'; Shape.PATH = 'path'; // ----------------------------------------------------------- // End : Shape // ----------------------------------------------------------- // ----------------------------------------------------------- // Start : Circle // ----------------------------------------------------------- /** * A circle * * @constructor * @param {Integer} * x * @param {Integer} * y * @extends Shape */ var Circle = function(_entity_id, _x, _y, _radius, _borderColor, _borderWidth, _bgColor, _labelColor, _labelFont, _labelFontSize, _labelFontBold, _labelFontItalic) { Shape.call(this, _entity_id, _x, _y, _borderColor, _borderWidth, _bgColor, _labelColor, _labelFont, _labelFontSize, _labelFontBold, _labelFontItalic); _radius = parseInt(_radius); // Privileged Methods /** * Return the x position */ this.getX = function(){ var x = parseInt(_x); var parentSvgShape = this.getSvgParentShape(); if(parentSvgShape != null){ x = parentSvgShape.attr("cx"); } return x; }; /** * Return the y position */ this.getY = function(){ var y = parseInt(_y); var parentSvgShape = this.getSvgParentShape(); if(parentSvgShape != null){ y = parentSvgShape.attr("cy"); } return y; }; /** * Return the circle radius */ this.getRadius = function(){ var parentShape = this.getSvgParentShape(); var radius = _radius; if(parentShape != null){ radius = parentShape.attr('r'); } return radius; }; }; Circle.prototype = new Shape(); Circle.prototype.constructor = Circle; Circle.prototype.draw = function(){ // Create the SVG elements var paper = _processFlow.getCanvas(); var circle = paper.circle(this.getX(), this.getY(), this.getRadius()).initZoom() .setAttr({cursor : 'pointer', 'stroke' : this.getBorderColor(), 'stroke-width' : this.getBorderWidth(), fill : this.getBgColor()}); // Store the shape reference. this.setSvgParentShape(circle); var flowItemSet = paper.set(); flowItemSet.push(circle); if(this.getLabel() != null && this.getLabel().length > 0){ var label = Shape.prototype.createSVGLabel.call(this, this.getX(), this.getY(), this.getLabel()); flowItemSet.push(label); this.setSvgLabel(label); } if(this.getIconUrl() != null && this.getIconUrl().length > 0){ var img = Shape.prototype.createSVGImage.call(this, this.getIconUrl(), this.getX() - 42, this.getY() - 8); flowItemSet.push(img); } this.setSvgShapeSet(flowItemSet); // Setup drag and drop Shape.prototype.initDragNDrop.call(this, flowItemSet, circle); Shape.prototype.initEvents.call(this); }; Circle.prototype.redrawLabel = function(){ if(this.getLabel() != null && this.getLabel().length > 0){ this.replaceLabel(Shape.prototype.createSVGLabel.call(this, this.getX(), this.getY(), this.getLabel())); }else{ this.replaceLabel(null); } }; /** * Return the shape definition as a JSON structure */ Circle.prototype.toJSON = function(){ return { type : Shape.CIRCLE, entity_id : this.getEntityId(), x : this.getX(), y : this.getY(), radius : this.getRadius(), border_color : this.getBorderColor(), border_width : this.getBorderWidth(), fill : this.getBgColor() }; }; // ----------------------------------------------------------- // End : Circle // ----------------------------------------------------------- // ----------------------------------------------------------- // Start : Rectangle // ----------------------------------------------------------- /** * A rectangle * * @extends Shape */ var Rectangle = function(_entity_id, _x, _y, _width, _height, _borderRadius, _borderColor, _borderWidth, _bgColor, _labelColor, _labelFont, _labelFontSize, _labelFontBold, _labelFontItalic) { Shape.call(this, _entity_id, _x, _y, _borderColor, _borderWidth, _bgColor, _labelColor, _labelFont, _labelFontSize, _labelFontBold, _labelFontItalic); _width = parseInt(_width); _height = parseInt(_height); _borderRadius = parseInt(_borderRadius); // Private variables // Privileged methods /** * Return the x (left) coordinate of the shape */ this.getX = function(){ var x = parseInt(_x); var parentShape = this.getSvgParentShape(); if(parentShape!=null){ x = parentShape.attr('x'); } return x; }; /** * Return the y (top) coordinate of the shape */ this.getY = function(){ var y = parseInt(_y); var parentShape = this.getSvgParentShape(); if(parentShape!=null){ y = parentShape.attr('y'); } return y; }; /** * Return the width of the rectangle. */ this.getWidth = function(){ var svgParentShape = this.getSvgParentShape(); var width = _width; if(svgParentShape != null){ width = this.getSvgParentShape().attr('width'); } return width; }; /** * Return the height of the rectangle * @return {Integer} the height of the rectangle */ this.getHeight = function(){ var svgParentShape = this.getSvgParentShape(); var height = _height; if(svgParentShape != null){ height = this.getSvgParentShape().attr('height'); } return height; }; /** * Return the * @return {Integer} the border radius */ this.getBorderRadius = function(){ return _borderRadius; }; }; Rectangle.prototype = new Shape(); Rectangle.prototype.constructor = Rectangle; Rectangle.prototype.draw = function(){ // Create the SVG elements var paper = _processFlow.getCanvas(); var rect = paper.rect(this.getX(), this.getY(), this.getWidth(), this.getHeight(), this.getBorderRadius()).initZoom() .setAttr({cursor : 'pointer', 'stroke' : this.getBorderColor(), 'stroke-width' : this.getBorderWidth(), fill : this.getBgColor()}); // Store the shape reference. this.setSvgParentShape(rect); var flowItemSet = paper.set(); flowItemSet.push(rect); if(this.getLabel() != null && this.getLabel().length > 0){ var label = Shape.prototype.createSVGLabel.call(this, this.getX() + (this.getWidth() / 2), this.getY() + (this.getHeight() / 2), this.getLabel()); flowItemSet.push(label); this.setSvgLabel(label); } if(this.getIconUrl() != null && this.getIconUrl().length > 0){ var img = Shape.prototype.createSVGImage.call(this, this.getIconUrl(), this.getX() + 5, this.getY() + (this.getHeight() / 2) - 8); flowItemSet.push(img); } this.setSvgShapeSet(flowItemSet); // Setup drag and drop Shape.prototype.initDragNDrop.call(this, flowItemSet, rect); Shape.prototype.initEvents.call(this); }; Rectangle.prototype.redrawLabel = function(){ if(this.getLabel() != null && this.getLabel().length > 0){ this.replaceLabel(Shape.prototype.createSVGLabel.call(this, this.getX() + (this.getWidth() / 2), this.getY() + (this.getHeight() / 2), this.getLabel())); }else{ this.replaceLabel(null); } }; /** * Return the shape definition as a JSON structure */ Rectangle.prototype.toJSON = function(){ return { type : Shape.RECTANGLE, entity_id : this.getEntityId(), x : this.getX(), y : this.getY(), width : this.getWidth(), height : this.getHeight(), border_radius : this.getBorderRadius(), border_color : this.getBorderColor(), border_width : this.getBorderWidth(), fill : this.getBgColor() }; }; // ----------------------------------------------------------- // End : Rectangle // ----------------------------------------------------------- // ----------------------------------------------------------- // Start : Path // ----------------------------------------------------------- /** * A Path i.e any shape you want drawn according to the specified path * * @extends Shape */ var Path = function(_entity_id, _x, _y, _path, _borderColor, _borderWidth, _bgColor, _labelColor, _labelFont, _labelFontSize, _labelFontBold, _labelFontItalic) { Shape.call(this,_entity_id, _x, _y, _borderColor, _borderWidth, _bgColor, _labelColor, _labelFont, _labelFontSize, _labelFontBold, _labelFontItalic); // Private function function _pathArrayToString(pathArray){ // We ignore the first element...this represents the // starting X,Y of the path. We only want the subsequence // relative path coordinates var pathStr = ""; for(var i = 1; i < pathArray.length; i++){ var pathElemArr = pathArray[i]; for(var j = 0; j < pathElemArr.length; j++){ if(j % 2 == 1){ pathStr += ","; }else{ pathStr += " "; } pathStr += pathElemArr[j]; } } return pathStr; } // Privileged methods this.getX = function(){ var parentShape = this.getSvgParentShape(); var x = parseInt(_x); if(parentShape != null){ var pathArr = parentShape.attr('path'); if(pathArr instanceof Array){ x = pathArr[0][1]; } } return x; }; this.getY = function(){ var parentShape = this.getSvgParentShape(); var y = parseInt(_y); if(parentShape != null){ var pathArr = parentShape.attr('path'); if(pathArr instanceof Array){ y = pathArr[0][2]; } } return y; }; /** * Return the width of the rectangle. */ this.getPath = function(){ var parentShape = this.getSvgParentShape(); var path = _path; if(parentShape != null){ var pathArr = parentShape.attr('path'); if(pathArr instanceof Array){ path = _pathArrayToString(pathArr); } } return path; }; }; Path.prototype = new Shape(); Path.prototype.constructor = Path;// Privileged methods Path.prototype.draw = function(){ // Create the SVG elements var pathString = 'M ' + this.getX() + ' ' + this.getY() + ' ' + this.getPath(); var paper = _processFlow.getCanvas(); var path = paper.path(pathString).initZoom() .setAttr({cursor : 'pointer', 'stroke' : this.getBorderColor(), 'stroke-width' : this.getBorderWidth(), fill : this.getBgColor()}); // Store the shape reference. this.setSvgParentShape(path); var flowItemSet = paper.set(); flowItemSet.push(path); var shapeDm = path.getBBox(); if(this.getLabel() != null && this.getLabel().length > 0){ var label = Shape.prototype.createSVGLabel.call(this, shapeDm.x + (shapeDm.width / 2), shapeDm.y + (shapeDm.height / 2), this.getLabel()); flowItemSet.push(label); this.setSvgLabel(label); } if(this.getIconUrl() != null && this.getIconUrl().length > 0){ var img = Shape.prototype.createSVGImage.call(this, this.getIconUrl(), shapeDm.x + 5, shapeDm.y + (shapeDm.height / 2) - 8); flowItemSet.push(img); } this.setSvgShapeSet(flowItemSet); // Setup drag and drop Shape.prototype.initDragNDrop.call(this, flowItemSet, path); Shape.prototype.initEvents.call(this); }; Path.prototype.redrawLabel = function(){ if(this.getLabel() != null && this.getLabel().length > 0){ var shapeDm = this.getSvgParentShape().getBBox(); this.replaceLabel(Shape.prototype.createSVGLabel.call(this, shapeDm.x + (shapeDm.width / 2), shapeDm.y + (shapeDm.height / 2), this.getLabel())); }else{ this.replaceLabel(null); } }; /** * Return the shape definition as a JSON structure */ Path.prototype.toJSON = function(){ return { type : Shape.PATH, entity_id : this.getEntityId(), x : this.getX(), y : this.getY(), path : this.getPath(), fill : this.getBgColor(), border_color : this.getBorderColor(), border_width : this.getBorderWidth() }; }; // ---------------------------------------------------- // End : Path // ---------------------------------------------------- // ---------------------------------------------------- // Start : NodeSelectionIndicator // ---------------------------------------------------- /** * The box drawn around a node when it is selected * This exposes handles which allow the user to resize * and rotate the node contained by the selection indicator * @constructor * @param {Node} the node contained by this selection box */ var NodeSelectionIndicator = function(_node){ // ------------------------------------------------------------- // Private variables // ------------------------------------------------------------- var _selectionSvgShape = null; /** * The resize handle SVG shape * references and helper variables */ var _nwResizeHandle = null; var _neResizeHandle = null; var _nResizeHandle = null; var _seResizeHandle = null; var _swResizeHandle = null; var _sResizeHandle = null; var _wResizeHandle = null; var _eResizeHandle = null; var _isResizing = false; var _resizeStartX = null; var _resizeStartY = null; var _selectionBoxStartX = null; var _selectionBoxStartY = null; var _selectionBoxStartHeight = null; var _selectionBoxStartWidth = null; var _jQueryparentOffSet = null; var _resizeDir = null; /** * Helper variable used to make * it easier to reposition the drag handles * during the resize */ var dimensionTmp = new Dimension(0,0,0,0); /** * The rotate handle refrence and * helper variables */ var _rotateStickSvgRef = null; var _rotateHandleSvgRef = null; var _degRotated = 0; // ------------------------------------------------------------- // Private functions // ------------------------------------------------------------- /** * Draw the selectiom box around the node */ function _drawSelectionBox(){ var shapeDim = _node.getDimensions(); _selectionSvgShape = _processFlow.getCanvas().rect(shapeDim.getX() - Port.PORT_HEIGHT, shapeDim.getY() - Port.PORT_HEIGHT, shapeDim.getWidth() + (Port.PORT_HEIGHT * 2), shapeDim.getHeight() + (Port.PORT_HEIGHT * 2)).initZoom() .setAttr({'stroke' : NodeSelectionIndicator.SELECTION_BOX_STROKE_COLOR, 'stroke-dasharray' : [NodeSelectionIndicator.SELECTION_DASH_STYLE], 'stroke-width': NodeSelectionIndicator.SELECTION_BOX_WIDTH}); var selectionShapeBBox = _selectionSvgShape.getBBox(); if(opts.editable== true){ _drawResizeHandles(selectionShapeBBox); } //_drawRotateHandle(selectionShapeBBox); } // ----------------------------------------------------------------- // - Start of the shape rotate code // - This sort of works. It can be used as the basis of a proper // shape rotation functionality // ----------------------------------------------------------------- // function _drawRotateHandle(selectionShapeBBox){ // // var _startPos = new Point(selectionShapeBBox.x + (selectionShapeBBox.width / 2), selectionShapeBBox.y); // var _endPos = new Point(_startPos.getX(), _startPos.getY() - NodeSelectionIndicator.ROTATE_STICK_HEIGHT); // // var path = "M" + _startPos.getX() + " " + _startPos.getY() + " L" + _endPos.getX() + " " + _endPos.getY(); // _rotateStickSvgRef = _processFlow.getCanvas().path(path) // .attr({'stroke' : NodeSelectionIndicator.SELECTION_BOX_STROKE_COLOR, // 'stroke-dasharray' : [NodeSelectionIndicator.SELECTION_DASH_STYLE], // 'stroke-width': NodeSelectionIndicator.SELECTION_BOX_WIDTH}); // // _rotateHandleSvgRef = _processFlow.getCanvas().circle(_endPos.getX(), _endPos.getY(), NodeSelectionIndicator.ROTATE_HANDLE_WIDTH / 2) // .attr({'fill' : NodeSelectionIndicator.ROTATE_HANDLE_FILL, // 'stroke' : 'black', // 'stroke-width': 1}); // jQuery(_rotateHandleSvgRef.node).hover(function(){ // jQuery(this).css('cursor', 'e-resize'); // }, // function(){ // jQuery(this).css('cursor', 'default'); // }) // .mousedown(function(event){ // _startRotate(event); // }); // } // // function _startRotate(event){ // _jQueryparentOffSet = jQuery(opts.canvas).offset(); // _startRotateX = event.pageX - _jQueryparentOffSet.left; // _degRotated = 0; // jQuery(opts.canvas) // .bind('mousemove', _doRotate) // .bind('mouseup', _stopRotate); // } // // function _doRotate(event){ // var currentX = event.pageX - _jQueryparentOffSet.left; // var distToRotate = _startRotateX - currentX; // // if(distToRotate > 0 ){ // _degRotated = 360 - distToRotate; // }else{ // _degRotated = 0 - distToRotate; // } // _selectionSvgShape.rotate(_degRotated, false); // // var selBBox = _selectionSvgShape.getBBox(); // _setResizeHandlePositions(selBBox.x,selBBox.y, selBBox.width, selBBox.height); // //_rotateResizeHandles(_degRotated); // } // // function _stopRotate(event){ // jQuery(opts.canvas) // .unbind('mousemove', _doRotate) // .unbind('mouseup', _stopRotate); // _node.rotate(_degRotated); // // } // ----------------------------------------------------------------- // - End of the shape rotate code // ----------------------------------------------------------------- /** * Draw all the resize handles around * the selection box */ function _drawResizeHandles(selectionShapeBBox){ var selectionBoxDim = new Dimension(selectionShapeBBox.x, selectionShapeBBox.y, selectionShapeBBox.width, selectionShapeBBox.height); _nwResizeHandle = _drawResizeHandle(selectionBoxDim, NodeSelectionIndicator.NW_RESIZE_DIR); _neResizeHandle = _drawResizeHandle(selectionBoxDim, NodeSelectionIndicator.NE_RESIZE_DIR); _nResizeHandle = _drawResizeHandle(selectionBoxDim, NodeSelectionIndicator.N_RESIZE_DIR); _swResizeHandle = _drawResizeHandle(selectionBoxDim, NodeSelectionIndicator.SW_RESIZE_DIR); _seResizeHandle = _drawResizeHandle(selectionBoxDim, NodeSelectionIndicator.SE_RESIZE_DIR); _sResizeHandle = _drawResizeHandle(selectionBoxDim, NodeSelectionIndicator.S_RESIZE_DIR); _wResizeHandle = _drawResizeHandle(selectionBoxDim, NodeSelectionIndicator.W_RESIZE_DIR); _eResizeHandle = _drawResizeHandle(selectionBoxDim, NodeSelectionIndicator.E_RESIZE_DIR); } /** * Draw the individual resize handle at the specified position * @param {Position} position * @param {String} resizeDir */ function _drawResizeHandle(selectionBoxDim, resizeDir){ var connectionHandle = null; var resizePos = _calcResizerPos(selectionBoxDim, resizeDir); if(resizeDir == NodeSelectionIndicator.N_RESIZE_DIR || resizeDir == NodeSelectionIndicator.S_RESIZE_DIR || resizeDir == NodeSelectionIndicator.W_RESIZE_DIR || resizeDir == NodeSelectionIndicator.E_RESIZE_DIR){ connectionHandle = _processFlow.getCanvas().rect(resizePos.getX(),resizePos.getY(), NodeSelectionIndicator.RESIZE_HANDLE_WIDTH, NodeSelectionIndicator.RESIZE_HANDLE_WIDTH).initZoom() .setAttr({'fill' : NodeSelectionIndicator.RESIZE_HANDLE_FILL, 'stroke' : 'black', 'stroke-width': 1}); }else{ connectionHandle = _processFlow.getCanvas().circle(resizePos.getX(), resizePos.getY(), NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2).initZoom() .setAttr({'fill' : NodeSelectionIndicator.RESIZE_HANDLE_FILL, 'stroke' : 'black', 'stroke-width': 1}); } var resizing = false; jQuery(connectionHandle.node) .hover(function(){ jQuery(opts.canvas).css('cursor',resizeDir); }, function(){ jQuery(opts.canvas).css('cursor','default'); }) .mousedown(function(event){ resizing = true; _startResize(event, resizeDir); }); jQuery(opts.canvas).mouseup(function(event){ if(!resizing) return; _stopResize(event); resizing = false; }) .mousemove(function(event){ if(!resizing) return; _doResize(event); }); return connectionHandle; } /** * Called when the user starts resizing * @param event * @param resizeDir */ function _startResize(event, resizeDir){ //console.log("Start Resize"); _resizeDir = resizeDir; _jQueryparentOffSet = jQuery(opts.canvas).offset(); _resizeStartX = event.pageX - _jQueryparentOffSet.left; _resizeStartY = event.pageY - _jQueryparentOffSet.top; var selectionShapeBBox = _selectionSvgShape.getBBox(); _selectionBoxStartX = selectionShapeBBox.x; _selectionBoxStartY = selectionShapeBBox.y; _selectionBoxStartWidth = selectionShapeBBox.width; _selectionBoxStartHeight = selectionShapeBBox.height; _isResizing = true; } /** * Called as the user is resizing * @param event */ function _doResize(event){ var distanceY = _resizeStartY - (event.pageY - _jQueryparentOffSet.top); var distanceX = _resizeStartX - (event.pageX - _jQueryparentOffSet.left); var newX = null; var newY = null; var newWidth = null; var newHeight = null; switch(_resizeDir){ case(NodeSelectionIndicator.NW_RESIZE_DIR): newX = _selectionBoxStartX - distanceY; newY = _selectionBoxStartY - distanceY; newWidth = _selectionBoxStartWidth + distanceY; newHeight = _selectionBoxStartHeight + distanceY; break; case (NodeSelectionIndicator.NE_RESIZE_DIR) : newX = _selectionBoxStartX; newY = _selectionBoxStartY - distanceY; newWidth = _selectionBoxStartWidth + distanceY; newHeight = _selectionBoxStartHeight + distanceY; break; case(NodeSelectionIndicator.N_RESIZE_DIR) : newX = _selectionBoxStartX; newY = _selectionBoxStartY - distanceY; newWidth = _selectionBoxStartWidth; newHeight = _selectionBoxStartHeight + distanceY; break; case(NodeSelectionIndicator.SW_RESIZE_DIR) : newX = _selectionBoxStartX + distanceY; newY = _selectionBoxStartY; newWidth = _selectionBoxStartWidth - distanceY; newHeight = _selectionBoxStartHeight - distanceY; break; case(NodeSelectionIndicator.SE_RESIZE_DIR) : newX = _selectionBoxStartX; newY = _selectionBoxStartY; newWidth = _selectionBoxStartWidth - distanceY; newHeight = _selectionBoxStartHeight - distanceY; break; case(NodeSelectionIndicator.S_RESIZE_DIR) : newX = _selectionBoxStartX; newY = _selectionBoxStartY; newWidth = _selectionBoxStartWidth; newHeight = _selectionBoxStartHeight - distanceY; break; case(NodeSelectionIndicator.E_RESIZE_DIR) : newX = _selectionBoxStartX; newY = _selectionBoxStartY; newWidth = _selectionBoxStartWidth - distanceX; newHeight = _selectionBoxStartHeight; break; case(NodeSelectionIndicator.W_RESIZE_DIR) : newX = _selectionBoxStartX - distanceX; newY = _selectionBoxStartY; newWidth = _selectionBoxStartWidth + distanceX; newHeight = _selectionBoxStartHeight; break; }; if(newWidth < 0) { newWidth = -newWidth; newX -= newWidth; } else if(newWidth == 0) newWidth = 1; if(newHeight < 0) { newHeight = -newHeight; newY -= newHeight; } else if(newHeight == 0) newHeight = 1; _selectionSvgShape.animate({x : newX, y : newY, width : newWidth, height : newHeight},1); _setResizeHandlePositions(newX, newY, newWidth, newHeight); } function _calcResizerPos(selectionDim, resizerDir){ var resizerPos = null; switch(resizerDir){ case NodeSelectionIndicator.NW_RESIZE_DIR : resizerPos = selectionDim.getTopLeft(); break; case NodeSelectionIndicator.NE_RESIZE_DIR : resizerPos = selectionDim.getTopRight(); break; case NodeSelectionIndicator.N_RESIZE_DIR : var nX = selectionDim.getX() + (selectionDim.getWidth() / 2) - (NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2); var nY = selectionDim.getY() - (NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2); resizerPos = new Point(nX, nY); break; case NodeSelectionIndicator.SW_RESIZE_DIR : resizerPos = selectionDim.getBottomLeft(); break; case NodeSelectionIndicator.SE_RESIZE_DIR : resizerPos = selectionDim.getBottomRight(); break; case NodeSelectionIndicator.S_RESIZE_DIR : var sX = selectionDim.getX() + (selectionDim.getWidth() / 2) - (NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2); var sY = selectionDim.getY() + selectionDim.getHeight() - (NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2); resizerPos = new Point(sX, sY); break; case NodeSelectionIndicator.E_RESIZE_DIR : var eX = selectionDim.getX() + selectionDim.getWidth() - (NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2); var eY = selectionDim.getY() + (selectionDim.getHeight() / 2) - (NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2); resizerPos = new Point(eX, eY); break; case NodeSelectionIndicator.W_RESIZE_DIR : var wX = selectionDim.getX() - (NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2); var wY = selectionDim.getY() + (selectionDim.getHeight() / 2) - (NodeSelectionIndicator.RESIZE_HANDLE_WIDTH / 2); resizerPos = new Point(wX, wY); break; } return resizerPos; } /** * Set the resize handle positions. Called during the resize operation * @param {Integer} newX * @param {Integer} newY * @param {Integer} newWidth * @param {Integer} newHeight */ function _setResizeHandlePositions(newX, newY, newWidth, newHeight){ dimensionTmp.setX(newX); dimensionTmp.setY(newY); dimensionTmp.setWidth(newWidth); dimensionTmp.setHeight(newHeight); _setNewHandlePosition(_nwResizeHandle, _calcResizerPos(dimensionTmp, NodeSelectionIndicator.NW_RESIZE_DIR)); _setNewHandlePosition(_neResizeHandle, _calcResizerPos(dimensionTmp, NodeSelectionIndicator.NE_RESIZE_DIR)); _setNewHandlePosition(_nResizeHandle, _calcResizerPos(dimensionTmp, NodeSelectionIndicator.N_RESIZE_DIR)); _setNewHandlePosition(_swResizeHandle, _calcResizerPos(dimensionTmp, NodeSelectionIndicator.SW_RESIZE_DIR)); _setNewHandlePosition(_seResizeHandle, _calcResizerPos(dimensionTmp, NodeSelectionIndicator.SE_RESIZE_DIR)); _setNewHandlePosition(_sResizeHandle, _calcResizerPos(dimensionTmp, NodeSelectionIndicator.S_RESIZE_DIR)); _setNewHandlePosition(_eResizeHandle, _calcResizerPos(dimensionTmp, NodeSelectionIndicator.E_RESIZE_DIR)); _setNewHandlePosition(_wResizeHandle, _calcResizerPos(dimensionTmp, NodeSelectionIndicator.W_RESIZE_DIR)); } function _setNewHandlePosition(resizeHandleSVGShape, position){ if(resizeHandleSVGShape.type == 'circle'){ resizeHandleSVGShape.animate({cx : position.getX(), cy : position.getY()}, 1); }else{ resizeHandleSVGShape.animate({x : position.getX(), y : position.getY()}, 1); } } /** * Called when the user stops resizing * @param event */ function _stopResize(event){ //Calculate the scale percentage var nodeDimension = _node.getDimensions(); var currentSelectBBox = _selectionSvgShape.getBBox(); var curSelectionDim = new Dimension(currentSelectBBox.x, currentSelectBBox.y, currentSelectBBox.width, currentSelectBBox.height); var xScalePct = _selectionBoxStartWidth == 0 ? 1 : currentSelectBBox.width / _selectionBoxStartWidth; var yScalePct = _selectionBoxStartHeight == 0 ? 1 : currentSelectBBox.height / _selectionBoxStartHeight; _node.scale(xScalePct, yScalePct, curSelectionDim.getCenter()); _node.redrawConnections(); } function _removeAllSelectionComponents(){ if(_selectionSvgShape != null){ _selectionSvgShape.remove(); } if(_nwResizeHandle != null){ _nwResizeHandle.remove(); } if(_neResizeHandle != null){ _neResizeHandle.remove(); } if(_nResizeHandle != null){ _nResizeHandle.remove(); } if(_seResizeHandle != null){ _seResizeHandle.remove(); } if(_swResizeHandle != null){ _swResizeHandle.remove(); } if(_sResizeHandle != null){ _sResizeHandle.remove(); } if(_wResizeHandle != null){ _wResizeHandle.remove(); } if(_eResizeHandle != null){ _eResizeHandle.remove(); } } // ------------------------------------------------------------- // Protected functions // ------------------------------------------------------------- /** * Set the visibility of the selection indicator * @param {Boolean} visible */ this.setVisible = function(visible){ if(visible){ _drawSelectionBox(); }else{ _removeAllSelectionComponents(); } }; /** * Destroy the selection indicator * Typically called when the parent node is destroyed */ this.destroy = function(){ _removeAllSelectionComponents(); }; /** * Redraw connection indicator */ this.redraw = function(){ _removeAllSelectionComponents(); _drawSelectionBox(); }; // Call the init function }; NodeSelectionIndicator.SELECTION_BOX_WIDTH = 1; NodeSelectionIndicator.SELECTION_BOX_STROKE_COLOR = '#7f7f7f'; NodeSelectionIndicator.SELECTION_DASH_STYLE = '.'; NodeSelectionIndicator.RESIZE_HANDLE_FILL = '90-#e6effd-#b0cffb'; NodeSelectionIndicator.RESIZE_HANDLE_WIDTH = 8; NodeSelectionIndicator.NE_RESIZE_DIR = 'ne-resize'; NodeSelectionIndicator.NW_RESIZE_DIR = 'nw-resize'; NodeSelectionIndicator.SE_RESIZE_DIR = 'se-resize'; NodeSelectionIndicator.SW_RESIZE_DIR = 'sw-resize'; NodeSelectionIndicator.N_RESIZE_DIR = 'n-resize'; NodeSelectionIndicator.S_RESIZE_DIR = 's-resize'; NodeSelectionIndicator.E_RESIZE_DIR = 'e-resize'; NodeSelectionIndicator.W_RESIZE_DIR = 'w-resize'; NodeSelectionIndicator.ROTATE_HANDLE_FILL = '90-#dbffda-#85ff81'; NodeSelectionIndicator.ROTATE_STICK_HEIGHT = 30; NodeSelectionIndicator.ROTATE_HANDLE_WIDTH = NodeSelectionIndicator.RESIZE_HANDLE_WIDTH; // ---------------------------------------------------- // End : NodeSelectionIndicator // ---------------------------------------------------- // ---------------------------------------------------- // Start : Node // ---------------------------------------------------- /** * The flow item node * @constructor * @param {Integer} * nodeId */ var Node = function(_id, _entityId, _type, _label, _iconUrl, _shape) { // ------------------------------------------------------------- // Private variables // ------------------------------------------------------------- var _thisObject = this; var _isDrawing = false; var _selected = false; var _selectionIndicator = null; var _currentScaleX = 1; var _currentScaleY = 1; var _hasContextMenu = false; /** * The array of connections from/to this * node from/to other nodes. */ var _connections = new HashMap(); var _connectionBeingChanged = null; /** * The array of connection points * on this flow item * @private {Port} */ var _ports = []; // PRIVATE methods /** * Initialize the node */ function _init(){ if (_label != null) { _shape.setLabel(_label); } if (_iconUrl != null) { _shape.setIconUrl(_iconUrl); } _shape.setOnDragCallback(function(){ _redrawConnections(false); }); _shape.setOnDragCompleteCallback(function(){ _redrawConnections(true); }); _shape.draw(); _addConnectionPorts(_shape); jQuery(_shape.getSvgParentShape().node).mouseover(function() { if(opts.editable){ FlowItemContextMenuFactory.addContextMenu(_thisObject); } }) .click(function(e) { if(e.shiftKey || e.ctrlKey) NodeSelectionHandler.selectNode(_thisObject); else NodeSelectionHandler.selectOnlyNode(_thisObject); if(opts.onNodeClick != null){ opts.onNodeClick.call(opts.onNodeClick, _thisObject); } }); _selectionIndicator = new NodeSelectionIndicator(_thisObject); _initNodeTooltips(); } function _initNodeTooltips(){ if(opts.tooltips_enabled == true){ jQuery(_shape.getSvgParentShape().node).qtip({ content: 'This is an active list element', position : { corner : { target : 'bottomMiddle', tooltip: 'topLeft' } }, show: { effect: { type: 'slide' } }, hide: { fixed : true, delay : 350 }, style: { border: { width: 1, radius: 3, color: '#a5a5a5' } }, api : { beforeShow : function(){ if(opts.beforeNodeTooltipShow != null){ opts.beforeNodeTooltipShow.call(opts.beforeNodeTooltipShow, _id, this); } } } } ); } }; /** * Loop through all connections and get them to redraw themselves */ function _redrawConnections(finalize){ if(_isDrawing){ return; }else{ _isDrawing = true; _thisObject.movePortsToCurrentPos(); var connections = _connections.valSet(); for(var i = 0; i < connections.length; i++){ connections[i].redraw(finalize); } _isDrawing = false; } } /** * Draw the connection ports on the shape */ function _addConnectionPorts(){ _ports[Port.POSITION_TOP] = new Port(_thisObject, Port.POSITION_TOP); _ports[Port.POSITION_BOTTOM] = new Port(_thisObject, Port.POSITION_BOTTOM); _ports[Port.POSITION_LEFT] = new Port(_thisObject, Port.POSITION_LEFT); _ports[Port.POSITION_RIGHT] = new Port(_thisObject, Port.POSITION_RIGHT); _ports[Port.POSITION_MIDDLE] = new Port(_thisObject, Port.POSITION_MIDDLE); } // ------------------------------------------------------------- // PRIVILEGED methods // ------------------------------------------------------------- /** * Return the id * @return {Integer} id */ this.getId = function(){ return _id; }; /** * Return the entity id * @return {Integer} entity id */ this.getEntityId = function(){ return _entityId; }; /** * Set the entity id * @param {Integer} entityId */ this.setEntityId = function(entityId){ _entityId = entityId; }; /** * Return the connection type * @return {Object} type */ this.getType = function(){ return _type; }; /** * Return the node label */ this.getLabel = function(){ return _label; }; /** * Change the node label * @param {String} nodeLabel */ this.setLabel = function(nodeLabel){ _label = nodeLabel; _shape.setLabel(_label); }; /** * Set the nodes X position * @param {Integer} x */ this.setX = function(x){ _shape.setX(x); _redrawConnections(true); if(_selected){ _selectionIndicator.redraw(); } }; /** * Set the nodes Y position * @param {Integer} y */ this.setY = function(y){ _shape.setY(y); _redrawConnections(true); if(_selected){ _selectionIndicator.redraw(); } }; /** * Set position * @param {Point} position */ this.setPosition = function(position){ _shape.setPosition(position.getX(), position.getY()); this.movePortsToCurrentPos(); _redrawConnections(true); if(_selected){ _selectionIndicator.redraw(); } }; /** * Set the node style. This function expects an objects with the following * properties: * font_color, * border_width, * border_color, * fill */ this.setStyle = function(style){ _shape.setStyle(style); }; /** * Add a connection reference * @param {Connection} */ this.addConnection = function(conn){ _connections.put(conn.getId(), conn); }; this.setHasContextMenu = function(hasContextMenu){ _hasContextMenu = hasContextMenu; }; this.hasContextMenu = function(){ return _hasContextMenu; }; /** * Gets the nodes that this node is connected and where this node is the source of the connection */ this.getOutgoingNodes = function() { var outgoing = []; var connections = _connections.valSet(); for(var i = 0; i < connections.length; i++){ var con = connections[i]; if (con.getSourcePort().getParentNode() == _thisObject) { outgoing.push(con.getTargetPort().getParentNode()); } } return outgoing; }; /** * Gets the nodes that this node is connected and where this node is the target of the connection */ this.getIncomingNodes = function() { var incoming = []; var connections = _connections.valSet(); for(var i = 0; i < connections.length; i++){ var con = connections[i]; if (con.getTargetPort().getParentNode() == _thisObject) { incoming.push(con.getSourcePort().getParentNode()); } } return incoming; }; /** * Remove a connection reference * @param {Integer} connectionId */ this.removeConnection = function(connectionId){ _connections.remove(connectionId); }; /** * Return the port at the specific position * @param {Integer} port position */ this.getPort = function(portPosition){ return _ports[portPosition]; }; /** * The reference to the underlying Shape of the flow item * @return {Shape} the node shape */ this.getShape = function(){ return _shape; }; /** * The dimensions of this node. This is the same as the dimensions * of the underlying SVG shape. * @return {Dimension} the shape dimensions **/ this.getDimensions = function(){ return _shape.getDimensions(); }; /** * Show or hide the connection ports based on the * boolean parameter passed in */ this.setConnectionPortsVisible = function(isVisible){ for(var i = 0; i < _ports.length; i++){ _ports[i].setVisible(isVisible); } }; this.movePortsToCurrentPos = function(){ for(var i = 0; i < _ports.length; i++){ _ports[i].moveToCurrentPos(); } }; this.getConnectionBeingChanged = function(){ return _connectionBeingChanged; }; this.setConnectionBeingChanged = function(connection){ _connectionBeingChanged = connection; }; /** * Indicate whether this node is the connection source * or target. The connection port colors will be changed accordingly. * @param connectPointType * @returns */ this.setConnectionPointType = function(connectPointType){ for(var i = 0; i < _ports.length; i++){ var portColor = Port.PORT_COLOR_DEFAULT; if(connectPointType == Connection.SOURCE){ portColor = Port.PORT_COLOR_SOURCE; } _ports[i].setFillColor(portColor); } }; /** * Set the node selection status */ this.setSelected = function(selected){ if (selected != _selected) { _shape.setSelected(selected); _selected = selected; _selectionIndicator.setVisible(selected); this.redrawConnections(); } }; this.isSelected = function() { return _selected; }; /** * Scale the node by the specied x and y percentages * The shape will be moved so that its center point is * the same as the specified center point * @param {Integer} scaleX * @param {Integer} scaleY * @param {Integer} newCenterPoint */ this.scale = function(scaleX, scaleY, newCenterPoint){ var nodeDim = this.getDimensions(); var newX = newCenterPoint.getX() - (nodeDim.getWidth() / 2); var newY = newCenterPoint.getY() - (nodeDim.getHeight() / 2); _shape.setPosition(newX, newY); // the Raphael scaling isn't cumulative so we have to // do this ouselves _currentScaleX = _currentScaleX * scaleX; _currentScaleY = _currentScaleY * scaleY; _shape.scale(_currentScaleX, _currentScaleY); }; /** * Rotate the node by the specified number of degrees */ this.rotate = function(degrees){ _shape.rotate(degrees); }; /** * Remove connection */ this.destroy = function(){ // Delete the connections var connections = _connections.valSet(); for(var i = 0; i < connections.length; i++){ connections[i].destroy(); } // Remove all the ports for(var i = 0; i < _ports.length; i++){ _ports[i].destroy(); } // Remove this SVG object jQuery(_shape.getSvgParentShape().node).removeContextMenu(); _shape.destroy(); // Remove this items for the list of flowitems _flowItemList.remove(_id); // Fire the node delete callback if configured if(opts.onNodeDelete != null){ opts.onNodeDelete(opts.onNodeDelete, _id); } // Remove the selection indicator _selectionIndicator.destroy(); }; this.redrawConnections = function() { _redrawConnections(true); }; /** * Return the item definition in a JSON structure */ this.toJSON = function(){ return { id : _id, entity_id : (_entityId == null ? "" : _entityId), type : _type, label : (_label == null ? "" : _label), icon_url : (_iconUrl == null ? "" : _iconUrl), shape : _shape.toJSON() }; }; // Call the initialisation function _init(); }; // ---------------------------------------------------- // End : Node // ---------------------------------------------------- // ---------------------------------------------------- // Start : Port // ---------------------------------------------------- var Port = function(_node, _portPosition){ var _thisObject = this; var _raphaelPortShapeRef = null; var _canvas = _processFlow.getCanvas(); var _currentPortColor = Port.PORT_COLOR_DEFAULT; // PRIVATE functions /** * Calculate the new port dimesions */ function _calcNewPortXY(){ var portX, portY, shapeDim; shapeDim = _node.getDimensions(); shapeX = shapeDim.getX(); shapeY = shapeDim.getY(); shapeWidth = shapeDim.getWidth(); shapeHeight = shapeDim.getHeight(); switch(_portPosition){ case Port.POSITION_TOP : portX = shapeX + (shapeWidth / 2) - (Port.PORT_HEIGHT / 2); portY = shapeY - Port.PORT_HEIGHT; break; case Port.POSITION_RIGHT: portX = shapeX + shapeWidth; portY = shapeY + (shapeHeight / 2) - (Port.PORT_HEIGHT / 2); break; case Port.POSITION_BOTTOM: portX = shapeX + (shapeWidth / 2) - (Port.PORT_HEIGHT / 2); portY = shapeY + shapeHeight; break; case Port.POSITION_LEFT: portX = shapeX - Port.PORT_HEIGHT; portY = shapeY + (shapeHeight / 2) - (Port.PORT_HEIGHT / 2); break; case Port.POSITION_MIDDLE : var shapeCenter = shapeDim.getCenter(); portX = shapeCenter.getX() - (Port.PORT_HEIGHT / 2); portY = shapeCenter.getY() - (Port.PORT_HEIGHT / 2); break; } return new Point(portX, portY); } /** * Draw the connection point * @returns */ function _drawPort(){ var portPos = _calcNewPortXY(); _raphaelPortShapeRef = _canvas.rect(portPos.getX(), portPos.getY(), Port.PORT_HEIGHT, Port.PORT_HEIGHT).initZoom() .setAttr({fill : _currentPortColor, 'stroke-width' : 1}).hide(); // Setup port DOM element event listeners jQuery(_raphaelPortShapeRef.node) .mousedown( function(e){ NodeConnectHandler.addConnectionPoint(_thisObject); }) .mouseup( function(e){ NodeConnectHandler.addConnectionPoint(_thisObject); }) .hover( function(e){ _raphaelPortShapeRef.scale(1.3, 1.3); NodeConnectHandler.setLineValid(true); _raphaelPortShapeRef.toFront(); }, function(e){ _raphaelPortShapeRef.scale(1, 1); NodeConnectHandler.setLineValid(false); }); }; /** * Called by the constructor to initialise the object */ function _init(){ _drawPort(); }; // PRIVILEGED functions /** * The parent node that this port belongs to * @return {Node} the parent node. */ this.getParentNode = function(){ return _node; }; /** * Return the port position */ this.getPortPosition = function(){ return _portPosition; }; /** * Return the Raphael SVG shape reference */ this.getRaphaelPortShapeRef = function(){ return _raphaelPortShapeRef; }; /** * Set the Port * @param {Boolean} actice */ this.setActive = function(active){ if(active){ _raphaelPortShapeRef.setAttr('fill', Port.PORT_COLOR_ACTIVE); _raphaelPortShapeRef.toFront(); }else{ _raphaelPortShapeRef.setAttr('fill', _currentPortColor); } }; /** * Change the port color * @param {String} color */ this.setFillColor = function(color){ _currentPortColor = color; this.getRaphaelPortShapeRef().setAttr('fill', color); }; /** * Set the visibility of the port */ this.setVisible = function(visible){ if(visible){ _raphaelPortShapeRef.show(); }else{ _raphaelPortShapeRef.hide(); } }; /** * Move the port to the current position of the shape * Ports are moved to the current position when visibility * is set to true. */ this.moveToCurrentPos = function(){ var currentDim = _raphaelPortShapeRef.getBBox(); var newPos = _calcNewPortXY(); var distX = newPos.getX() - currentDim.x; var distY = newPos.getY() - currentDim.y; _raphaelPortShapeRef.translate(distX, distY); }; /** * Destroy the port */ this.destroy = function(){ _raphaelPortShapeRef.remove(); }; // Call the initialisation function _init(); }; Port.prototype = { /** * Return the middle point of the connect */ getMiddlePoint : function(){ return new Point(this.getRaphaelPortShapeRef().attr('x') + Port.PORT_HEIGHT / 2, this.getRaphaelPortShapeRef().attr('y') + Port.PORT_HEIGHT / 2); }, /** * Get the shape bounds (x, y, width, height) * @return {Dimension} the shape bounds */ getBounds : function(){ // Calculate the dimensions of this object var shapeRef = this.getRaphaelPortShapeRef(); return new Dimension(shapeRef.attr('x'), shapeRef.attr('y'), shapeRef.attr('width'), shapeRef.attr('height')); }, /** * Return the point at the middle of the port edge adjacent to its * parent shape. * @returns {Point} the middle Point */ getEdgeMiddlePoint : function(){ var portX, portY, portWidth, portHeight, middleX, middleY, middlePoint, portPosition; portPosition = this.getPortPosition(); portShapeRef = this.getRaphaelPortShapeRef(); // Get the actual port position portX = portShapeRef.attr('x'); portY = portShapeRef.attr('y'); portWidth = portShapeRef.attr('width'); portHeight = portShapeRef.attr('height');; switch(portPosition){ case Port.POSITION_TOP : middleX = portX + (portWidth / 2); middleY = portY + portHeight; break; case Port.POSITION_RIGHT: middleX = portX; middleY = portY + (portHeight / 2); break; case Port.POSITION_BOTTOM: middleX = portX + (portWidth / 2); middleY = portY; break; case Port.POSITION_LEFT: middleX = portX + portWidth; middleY = portY + (portHeight / 2); break; case Port.POSITION_MIDDLE : var portMiddle = this.getMiddlePoint(); middleX = portMiddle.getX(); middleY = portMiddle.getY(); break; } return new Point(Math.abs(middleX), Math.abs(middleY)); } }; Port.POSITION_TOP = 0; Port.POSITION_RIGHT = 1; Port.POSITION_BOTTOM = 2; Port.POSITION_LEFT = 3; Port.POSITION_MIDDLE = 4; Port.PORT_HEIGHT = 8; Port.PORT_COLOR_HOVER = '#ccff66'; Port.PORT_COLOR_ACTIVE = 'red'; Port.PORT_COLOR_SOURCE = '#2aff0d'; Port.PORT_COLOR_DEFAULT = '#ffff00'; // ---------------------------------------------------- // End : Port // ---------------------------------------------------- /* * ---------------------------------------------------- * Class - PositionConstants * ---------------------------------------------------- */ PositionConstants = function(){ }; PositionConstants.NORTH = 1; PositionConstants.SOUTH = 4; PositionConstants.WEST = 8; PositionConstants.EAST = 16; /* * ---------------------------------------------------- * End - PositionConstants * ---------------------------------------------------- */ /* * ---------------------------------------------------- * Class - Dimension * ---------------------------------------------------- */ /** * @extends {Point} * @constructor * @param {Integer} x * @param {Integer} y * @param {Integer} width * @param {Integer} height */ var Dimension=function(_x, _y, _width, _height){ Point.call(this,_x, _y); // PRIVILEGED Methods /** * Set the width * @param {Integer} width */ this.setWidth = function(width){ _width = width; }; /** * Get the width * @return {Integer} width */ this.getWidth = function(){ return _width; }; /** * Get the height * @return {Integer} height */ this.getHeight = function(){ return _height; }; /** * Set the height * @param {Integer} height */ this.setHeight = function(height){ _height = height; }; }; Dimension.prototype = new Point; Dimension.constructor = Dimension; /** * Sets the parameters of this Rectangle from the Rectangle passed in and * returns this for convenience. * * @param {Dimension} Rectangle providing the bounding values * @return {Dimension} the Dimension passed in */ Dimension.prototype.setBounds=function(/*:Dimension*/ rect){ this.setX(rect.getX()); this.setY(rect.getY()); this.setWidth(rect.getWidth()); this.setHeight(rect.getHeight()); return this; }; /** * Return right (x + width) coordinate of the dimension * @return {Integer} the right position **/ Dimension.prototype.getRight = function(){ return this.getX() + this.getWidth(); }; /** * The bottom (y + height) coordinate of the dimension * @return {Integer} the bottom position **/ Dimension.prototype.getBottom = function(){ return this.getY() + this.getHeight(); }; /** * The top left (x, y) position of this * dimension * @return {Point} the top left position **/ Dimension.prototype.getTopLeft = function(){ return new Point(this.getX(), this.getY()); }; /** * The top right (x, y) position of this dimension * @return {Point} the top right position */ Dimension.prototype.getTopRight = function(){ return new Point(this.getX() + this.getWidth(), this.getY()); }; /** * The centre position (x, y) of the dimension * @return {Point} the centre position **/ Dimension.prototype.getCenter = function(){ return new Point(this.getX() + this.getWidth() / 2, this.getY() + this.getHeight() / 2); }; /** * The bottom left (x, y) position of this dimension * @return {Point} the bottom left position */ Dimension.prototype.getBottomLeft = function(){ return new Point(this.getX(), this.getY() + this.getHeight()); }; /** * The bottom right (x, y) position of the dimension * @return {Point} the bottom right position **/ Dimension.prototype.getBottomRight = function(){ return new Point(this.getX() + this.getWidth(), this.getY() + this.getHeight()); }; /** * Check if this dimension encloses a specific point * @param {Point} point */ Dimension.prototype.isContainsPoint = function(point){ var containsPoint = false; var bottomRight = this.getBottomRight(); if((point.getX() >= this.getX() && point.getY() >= this.getY()) && (point.getX() <= bottomRight.getX() && point.getY() <= bottomRight.getY())){ containsPoint = true; } return containsPoint; }; Dimension.TOP_LEFT_POINT = 0; Dimension.TOP_RIGHT_POINT = 1; Dimension.TOP_MIDDLE_POINT = 2; Dimension.BOTTOM_LEFT_POINT = 3; Dimension.BOTTOM_RIGHT_POINT = 4; Dimension.BOTTOM_MIDDLE_POINT = 5; /* * ---------------------------------------------------- * End - Dimension * ---------------------------------------------------- */ /* * ---------------------------------------------------- * Class - Line * ---------------------------------------------------- */ /** * The connecting line between 2 Nodes * @constructor * @param {Point} startPoint * @param {Point} endPoint */ var Line = function(_startPoint, _endPoint){ // PRIVILEGED Methods /** * The start position of the line * @return {Point} the line start point */ this._getStartPoint = function(){ return _startPoint; }; /** * The end position of the line * @return {Point} the line end position */ this._getEndPoint = function(){ return _endPoint; }; }; Line.prototype = { getStartPoint : function(){ return this._getStartPoint(); }, getEndPoint : function(){ return this._getEndPoint(); } }; Line.LINE_COLOR_DEFAULT = '#7f7f7f'; Line.LINE_COLOR_SELECTED = '#92d050'; Line.LINE_WIDTH_DEFAULT = 2; /* * ---------------------------------------------------- * End - Line * ---------------------------------------------------- */ /* * ---------------------------------------------------- * Class - ConnectionDragHandle * ---------------------------------------------------- */ var ConnectionDragHandle = function(_connection, _point, _onDragStart, _onDragComplete){ // PRIVATE methods and variables var _handleSvgRef = null; var _currentPosition = new Point(_point.getX(), _point.getY()); var _isBeingDragged = false; var _ox = null; var _oy = null; /** * Intialization function */ function _init(){ _drawHandle(); } /** * Called when the user starts dragging the handle */ function _dragStart(){ if(_onDragStart != null){ _onDragStart.apply(this); } _ox = _handleSvgRef.attr("cx"); _oy = _handleSvgRef.attr("cy"); _isBeingDragged = true; }; /** * Called when the user stops dragging the handle */ function _dragStop(){ _isBeingDragged = false; if(_onDragComplete != null){ _onDragComplete.apply(this); } }; /** * Called while the user stops dragging the handle */ function _dragMove(dx, dy) { _handleSvgRef.setAttr({ cx : _ox + dx, cy : _oy + dy }); _connection.redraw(true); }; /** * Draw and initialize the handle */ function _drawHandle(){ _handleSvgRef = _processFlow.getCanvas().circle(_point.getX(), _point.getY(), 7) .initZoom() .setAttr({'fill' : 'red', 'stroke' : 'black', 'stroke-width': 1}) .hide(); // Enable Drag support _handleSvgRef.drag(_dragMove, _dragStart, _dragStop); }; // PRIVILEGED methods /** * Get the dimension of the drag handle */ this.getDimensions = function(){ var bbBox = _handleSvgRef.getBBox(); return new Dimension(bbBox.x, bbBox.y, bbBox.width, bbBox.height); }; /** * Indicate whether this handle is currently being dragged or not */ this.isBeingDragged = function(){ return _isBeingDragged; }; /** * Set the handle position */ this.setPosition = function(position){ _handleSvgRef.attr({ 'cx' : position.getX(), 'cy' : position.getY() }); }; /** * Get the current position of the handle */ this.getPosition = function(){ _currentPosition.setX(_handleSvgRef.attr('cx')); _currentPosition.setY(_handleSvgRef.attr('cy')); return _currentPosition; }; /** * Set the handle visibility. */ this.setVisible = function(visible){ if(visible){ _handleSvgRef.show().toFront(); if(_isBeingDragged){ _handleSvgRef.toBack(); } }else{ _handleSvgRef.hide(); } }; _init(); }; /* * ---------------------------------------------------- * End - ConnectionDragHandle * ---------------------------------------------------- */ /* * ---------------------------------------------------- * Class - Connection * ---------------------------------------------------- */ /** * The connection between 2 ports on 2 different shapes. * @constructor */ var Connection = function(_id, _entityId, _type, _sourcePort, _targetPort, _lineColor, _lineWidth, _label, _labelFont, _labelColor, _labelFontSize, _labelFontBold, _labelFontItalic){ Line.call(this, _sourcePort.getEdgeMiddlePoint(), _targetPort.getEdgeMiddlePoint()); // PRIVATE methods and variables var _thisObject = this; /** * The previous lines end point * Used during the connection routing process. */ var _prevPoint = null; /** * All the line segments making up the connection line * betweeen the 2 shapes */ var _lineSegments = []; /** * The connection router */ var _router = null; /** * The SVG path */ var _svgLineObj = null; var _svgTextObj = null; var _svgEndDecorator = null; /** * The handle references */ var _changingPort = null; var _startHandle = null; var _endHandle = null; var _hasContextMenu = false; var _prevLineColor = null; // PRIVATE functions /** * Initialize the class. Called by the constructor */ function init(){ // Set some default values if(_labelColor == null || _labelColor.length == 0){ _labelColor = '#000000'; }; if(_labelFont == null || _labelFont.length == 0){ _labelFont = 'Tahoma'; }; if(_labelFontSize == null || _labelFontSize.length == 0){ _labelFontSize = 10; }; if(_labelFontBold == null || _labelFontBold.length == 0){ _labelFontBold = false; }; if(_labelFontItalic == null || _labelFontItalic.length == 0){ _labelFontItalic = false; }; if(_lineColor == null || _lineColor.length == 0){ _lineColor = Connection.LINE_COLOR_DEFAULT; }; _prevLineColor = _lineColor; if(_lineWidth == null || _lineWidth.length == 0){ _lineWidth = opts.dflt_conn_line_width; }else{ _lineWidth = parseInt(_lineWidth); }; // Add connection references to parent nodes to facilate redrawing // of connection lines when the parent is dragged. _sourcePort.getParentNode().addConnection(_thisObject); _targetPort.getParentNode().addConnection(_thisObject); // Manhattan Connection Router by Default. _router = (opts.connectionRoute == ConnectionRouter.MANHATTAN_TYPE) ? new ManhattanConnectionRouter() : new StraightLineConnectionRouter(); _drawLine(true); } function _onLineMouseOut() { _svgLineObj.setAttr({'stroke' : _determineStrokeColor(), 'stroke-dasharray' : [""], 'stroke-width' : _lineWidth}); if (_svgEndDecorator != null) { _svgEndDecorator.setAttr({'stroke' : _determineStrokeColor(), 'stroke-dasharray' : [""], 'stroke-width' : _lineWidth}); } _sourcePort.setActive(false); _targetPort.setActive(false); if(opts.editable == true){ _setEndPointHandlesVisible(false); } } function _onLineMouseOver () { _svgLineObj.setAttr({'stroke' : Connection.LINE_COLOR_HOVER, 'stroke-dasharray' : ['-'], 'stroke-width' : _lineWidth}); if (_svgEndDecorator != null) { _svgEndDecorator.setAttr({'stroke' : Connection.LINE_COLOR_HOVER, 'stroke-dasharray' : ['-'], 'stroke-width' : _lineWidth}); } _sourcePort.setActive(true); _targetPort.setActive(true); if(opts.editable == true){ _setEndPointHandlesVisible(true); } } /** * Set the visibility of the start and end point connection * drag handles */ function _setEndPointHandlesVisible(visible){ _startHandle.setPosition(_lineSegments[0].getStartPoint()); _startHandle.setVisible(visible); _endHandle.setPosition(_lineSegments[_lineSegments.length - 1].getEndPoint()); _endHandle.setVisible(visible); } /** * Add the connection drag handles */ function _addConnectionDragHandles(){ if(_startHandle == null){ var startPoint = _lineSegments[0].getStartPoint(); var endPoint = _lineSegments[_lineSegments.length - 1].getEndPoint(); _startHandle = new ConnectionDragHandle(_thisObject, startPoint, function(){ _startManualPortChange(Connection.SOURCE, _sourcePort); }, function(){ _stopManualPortChange(_sourcePort); }); _endHandle = new ConnectionDragHandle(_thisObject, endPoint, function(){ _startManualPortChange(Connection.TARGET, _targetPort); }, function(){ _stopManualPortChange(_targetPort); }); } } function _startManualPortChange(portSide, port){ _changingPort = portSide; _svgLineObj.toBack(); port.getParentNode().setConnectionPortsVisible(true); port.getParentNode().setConnectionBeingChanged(_thisObject); } function _stopManualPortChange(port){ port.getParentNode().setConnectionPortsVisible(false); port.getParentNode().setConnectionBeingChanged(null); _setEndPointHandlesVisible(false); _thisObject.redraw(true); } /** * Draw the connection line * @param {Boolean} finalize */ function _drawLine(finalize){ _router.route(_thisObject); var pathString = null; for(var i = 0; i < _lineSegments.length; i++ ){ var lineSegment = _lineSegments[i]; if(pathString == null){ pathString = 'M ' + lineSegment.getStartPoint().getX() + ' ' + lineSegment.getStartPoint().getY(); } pathString += ' L ' + lineSegment.getEndPoint().getX() + ' ' + lineSegment.getEndPoint().getY() ; } if(opts.draw_conn_decorators == true){ _drawEndDecoratorPath(); } // Draw the line _svgLineObj = _processFlow.getCanvas().path(pathString) .initZoom() .setAttr({cursor : 'pointer', 'stroke-width' : _lineWidth, 'stroke' : _determineStrokeColor() }) .toBack(); // Add the label and context menu if this is the last time the line is going to be drawn if(finalize){ _drawLabel(); _setLabelFontAttr({}); _addConnectionDragHandles(); _addTooltip(); _hasContextMenu = false; jQuery(_svgLineObj.node).mouseout(_onLineMouseOut) .mouseover(function() { _onLineMouseOver(); FlowItemContextMenuFactory.addContextMenu(_thisObject); }) .click(function(){ if(opts.onConnectionClick != null){ opts.onConnectionClick.call(opts.onConnectionClick, _thisObject); } }); } } function _drawLabel(){ if(_label != null && _label.length > 0){ if(_svgTextObj != null){ _svgTextObj.remove(); } var textpoint = _svgLineObj.getPointAtLength(Math.abs(_svgLineObj.getTotalLength() / 2)); _svgTextObj = _processFlow.getCanvas().text(textpoint.x, textpoint.y - 10, _label).initZoom(); } } function _setLabelFontAttr(fontProperties){ var fontDflt = { font : _labelFont, font_size : _labelFontSize, font_bold : _labelFontBold, font_italic : _labelFontItalic, font_color : _labelColor }; var fontStyle = jQuery.extend({}, fontDflt, fontProperties); _labelFont = fontStyle.font; _labelFontSize = fontStyle.font_size; _labelFontBold = fontStyle.font_bold; _labelFontItalic = fontStyle.font_italic; _labelColor = fontStyle.font_color; if(_label != null && _label.length > 0){ _svgTextObj.setAttr({ fill : _labelColor, 'font-family' : _labelFont, 'font-weight' : (_labelFontBold == true ? 'bold' : 'normal'), 'font-size' : _labelFontSize, 'font-style' : (_labelFontItalic == true ? 'italic' : 'normal') }); } } function _addTooltip(){ if(opts.tooltips_enabled == true){ jQuery(_svgLineObj.node).qtip({ content: 'This is an active list element', position : { target : 'mouse' }, show: { effect: { type: 'slide' } }, hide: { fixed : true, delay : 350 }, style: { border: { width: 1, radius: 3, color: '#a5a5a5' } }, api : { beforeShow : function(){ if(opts.beforeConnTooltipShow != null){ opts.beforeConnTooltipShow.call(opts.beforeConnTooltipShow, _id, this); } } } } ); } } function _setLineStyleAttr(lineProperties){ var lineStyleDft = { line_color : _lineColor, line_width : _lineWidth }; var newLineStyle = jQuery.extend({}, lineStyleDft, lineProperties); _lineColor = newLineStyle.line_color; _prevLineColor = _lineColor; _lineWidth = newLineStyle.line_width; _svgLineObj.setAttr({ 'stroke-width' : newLineStyle.line_width, stroke : newLineStyle.line_color}); } function _determineStrokeColor() { var strokeColor = _prevLineColor; if(opts.in_out_line_color_change == true){ if (_thisObject.getSourcePort().getParentNode().isSelected()) { _prevLineColor = _lineColor; strokeColor = Connection.LINE_COLOR_SELECTEDNODE_OUTGOING; } else if (_thisObject.getTargetPort().getParentNode().isSelected()) { _prevLineColor = _lineColor; strokeColor = Connection.LINE_COLOR_SELECTEDNODE_INCOMING; } } return strokeColor; } /** * Draw the end point decortator. * Arrowhead is the currently the only decorator type supported */ function _drawEndDecoratorPath(){ var lastLineSegment = _lineSegments[_lineSegments.length - 1]; var startPoint = lastLineSegment.getStartPoint(); var endPoint = lastLineSegment.getEndPoint(); var arrowHeight = 10; var arrowAngle = 20; var arrowSegWidth = arrowHeight * (Math.tan(arrowAngle * Math.PI/180)); var tip1 = new Point(endPoint.getX() + arrowHeight, endPoint.getY() - arrowSegWidth); var tip2 = new Point(endPoint.getX() + arrowHeight, endPoint.getY() + arrowSegWidth); var arrowPath = 'M ' + tip1.getX() + ' ' + tip1.getY(); arrowPath += 'L ' + endPoint.getX() + ' ' + endPoint.getY(); arrowPath += 'L ' + tip2.getX() + ' ' + tip2.getY(); _svgEndDecorator = _processFlow.getCanvas().path(arrowPath) .initZoom() .setAttr({cursor : 'pointer', 'stroke-width' : _lineWidth, 'stroke' : _determineStrokeColor(), fill : _determineStrokeColor() }) .toFront(); var angleDeg = Raphael.angle(startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY()); _svgEndDecorator.rotate(angleDeg, endPoint.getX(), endPoint.getY()); }; // PRIVILEGED functions /** * Return the connection id * @return {Integer} id */ this.getId = function(){ return _id; }; /** * Return the collection entity id * @return {Integer} entity id */ this.getEntityId = function(){ return _entityId; }; /** * Set the entity_id * @param {Integer} entityId */ this.setEntityId = function(entityId){ _entityId = entityId; }; /** * Return the connection type */ this.getType = function(){ return _type; }; /** * Return the connection label */ this.getLabel = function(){ return _label; }; /** * Set the new connection label * @param {String} label */ this.setLabel = function(label){ _label = label; if(_label == null || _label.length == 0) { if (_svgTextObj != null) { _svgTextObj.remove(); _svgTextObj = null; } }else{ _drawLabel(); } }; /** * Set the connection line style (color, width etc) properties */ this.setStyle = function(newStyle){ var lineStyleDft = { animate : true, line_color : _lineColor, line_width : _lineWidth, font_style : {font : _labelFont, font_size : _labelFontSize, font_bold : _labelFontBold, font_italic : _labelFontItalic, font_color : _labelColor} }; var newStyle = jQuery.extend(true, {}, lineStyleDft, newStyle); _setLineStyleAttr(newStyle); _setLabelFontAttr(newStyle.font_style); if(newStyle.animate == true){ _svgLineObj.animate( {'stroke-width' : _lineWidth + 5}, 200, function(){ _svgLineObj.animate({'stroke-width' : _lineWidth}, 200); } ); } }; /** * Return the end point drag handle reference */ this.getEndPointDragHandle = function(){ return _endHandle; }; /** * Return the start point drag handle reference */ this.getStartPointDragHandle = function(){ return _startHandle; }; /** * Change the connection routing mechanism * @param {ConnectionRouter} */ this.setConnectionRouter = function(router){ _router = router; this.redraw(true); }; /** * Set the source port * @param {Port} the new source port */ this.setSourcePort = function(port){ _sourcePort = port; }; /** * Set the target port * @param {Port} the new target port */ this.setTargetPort = function(port){ _targetPort = port; }; /** * Return contant indicating which side of the connection SOURCE or TARGET * is being change. Will return null if no manual port change is occuring. */ this.getChangingPort = function(){ return _changingPort; }; /** * Redraw the connecting line * @param {Boolean} finalize indicates whether this is the last time this line will be drawn. */ this.redraw = function(finalize){ if(_svgLineObj != null){ _prevPoint = null; _lineSegments = []; _svgLineObj.remove(); } if (_svgTextObj != null) { _svgTextObj.remove(); } if (_svgEndDecorator != null) { _svgEndDecorator.remove(); } _drawLine(finalize); }; /** * The the SVG shape that represents the line */ this.getSvgShape = function(){ return _svgLineObj; }; /** * Return the Source Port * @return {Port} source port */ this.getSourcePort = function(){ return _sourcePort; }; /** * Return the target port * @return {Port} target port */ this.getTargetPort = function(){ return _targetPort; }; /** * Return the previous point * @return {Point} the previous point */ this.getPrevPoint = function(){ return _prevPoint; }; /** * Set the previous point * @param {Point} the previous point */ this.setPrevPoint = function(point){ _prevPoint = point; }; /** * Add a new line segment * @param {Line} the line segment */ this.addLineSegment = function(lineSegment){ _lineSegments.push(lineSegment); }; this.setHasContextMenu = function(hasContextMenu){ _hasContextMenu = hasContextMenu; }; this.hasContextMenu = function(){ return _hasContextMenu; }; /** * Destroy the connection */ this.destroy = function(){ // Remove the context menu jQuery(_svgLineObj.node).removeContextMenu(); _svgLineObj.node; _svgLineObj.remove(); if(_svgTextObj != null){ _svgTextObj.remove(); } if (_svgEndDecorator != null) { _svgEndDecorator.remove(); } // Remove the connection references from the source // and target ports _sourcePort.getParentNode().removeConnection(_id); _targetPort.getParentNode().removeConnection(_id); // Remove the master connection reference _connectionList.remove(_id); }; /** * Return the item state as a JSON structure */ this.toJSON = function(){ return { id: _id, entity_id : (_entityId == null ? "" : _entityId), type : _type, label : _label, from_node_id : _sourcePort.getParentNode().getId(), from_port : _sourcePort.getPortPosition(), to_node_id : _targetPort.getParentNode().getId(), to_port : _targetPort.getPortPosition(), line_label : _label }; }; this.reconsiderPorts = function() { var sourceBB = _sourcePort.getParentNode().getDimensions(); var targetBB = _targetPort.getParentNode().getDimensions(); var sourceX = sourceBB.getX() + (sourceBB.getWidth() / 2); var sourceY = sourceBB.getY() + (sourceBB.getHeight() / 2); var targetX = targetBB.getX() + (targetBB.getWidth() / 2); var targetY = targetBB.getY() + (targetBB.getHeight() /2); var xDiff = sourceX - targetX; var yDiff = sourceY - targetY; if (Math.abs(xDiff) > Math.abs(yDiff)) { _sourcePort = _sourcePort.getParentNode().getPort(xDiff > 0 ? Port.POSITION_LEFT : Port.POSITION_RIGHT); _targetPort = _targetPort.getParentNode().getPort(xDiff > 0 ? Port.POSITION_RIGHT : Port.POSITION_LEFT); } else { _sourcePort = _sourcePort.getParentNode().getPort(yDiff > 0 ? Port.POSITION_TOP : Port.POSITION_BOTTOM); _targetPort = _targetPort.getParentNode().getPort(yDiff > 0 ? Port.POSITION_BOTTOM : Port.POSITION_TOP); } }; // Call the initialization function init(); }; Connection.prototype = new Line; Connection.constructor = Connection; /** * The starting point of the connection. * This is the same as the middle point of the * starting port's edge adjacent to its parent node * * @return {Point} the connection start point */ Connection.prototype.getStartPoint = function (){ var startPoint = null; if(this.getStartPointDragHandle() != null && this.getStartPointDragHandle().isBeingDragged()){ startPoint = this.getStartPointDragHandle().getPosition(); }else{ startPoint = this.getSourcePort().getEdgeMiddlePoint(); } return startPoint; }; /** * Return the dimension of the start point box. * Typically used to calculated the starting point direction */ Connection.prototype.getStartPointDimension = function(){ var startDimension = null; if(this.getStartPointDragHandle() != null && this.getStartPointDragHandle().isBeingDragged()){ startDimension = this.getStartPointDragHandle().getDimensions(); }else{ startDimension = this.getSourcePort().getParentNode().getDimensions(); } return startDimension; }; /** * The end point of the connection. * This is the same as the middle point of the * end port's edge adjacent to its parent node * * @return {Point} the connection end point */ Connection.prototype.getEndPoint = function () { var endPoint = null; if(this.getEndPointDragHandle() != null && this.getEndPointDragHandle().isBeingDragged()){ endPoint = this.getEndPointDragHandle().getPosition(); }else{ endPoint = this.getTargetPort().getEdgeMiddlePoint(); } return endPoint; }; /** * Return the dimension of the end point box. * Typically used to calculated the ending point direction */ Connection.prototype.getEndPointDimension = function(){ var endDimension = null; if(this.getEndPointDragHandle() != null && this.getEndPointDragHandle().isBeingDragged()){ endDimension = this.getEndPointDragHandle().getDimensions(); }else{ endDimension = this.getTargetPort().getParentNode().getDimensions(); } return endDimension; }; /** * Add a new point to the connection. * New line segments will be added as new points are * added. * @param pointToAdd */ Connection.prototype.addPoint = function(pointToAdd) { var currentPoint = new Point(pointToAdd.getX(), pointToAdd.getY()); var prevPoint = this.getPrevPoint(); if(prevPoint != null) { var line = new Line(prevPoint, currentPoint); this.addLineSegment(line); } this.setPrevPoint(currentPoint); }; Connection.LINE_COLOR_DEFAULT = '#7f7f7f'; Connection.LINE_COLOR_SELECTEDNODE_INCOMING = '#ff0000'; Connection.LINE_COLOR_SELECTEDNODE_OUTGOING = '#00ff00'; Connection.LINE_COLOR_HOVER = '#92d050'; Connection.SOURCE = 0; Connection.TARGET = 1; /* * ---------------------------------------------------- * End - Connection * ---------------------------------------------------- */ /* * ---------------------------------------------------- * Class - ConnectionRouter * ---------------------------------------------------- */ /** * Parent class of all connection router types. * @constructor */ var ConnectionRouter = function(){ }; ConnectionRouter.prototype = { /** * Returns the direction the point p is in relation to the given rectangle. * Possible values are LEFT (-1,0), RIGHT (1,0), UP (0,-1) and DOWN (0,1). * * @param {Dimension} r the rectangle * @param {Point} p the point * @return {Integer} the direction from r to p */ getDirection : function(r, p){ // up -> 0 // right -> 1 // down -> 2 // left -> 3 var distance = Math.abs(r.getX() - p.getX()); var direction = ConnectionRouter.DIRECTION_LEFT; var i= Math.abs(r.getY() - p.getY()); if (i <= distance){ distance = i; direction = ConnectionRouter.DIRECTION_UP; } i = Math.abs(r.getBottom() - p.getY()); if (i <= distance){ distance = i; direction = ConnectionRouter.DIRECTION_DOWN; } i = Math.abs(r.getRight() - p.getX()); if (i < distance){ distance = i; direction = ConnectionRouter.DIRECTION_RIGHT; } return direction; }, /** * Return the end direction * @param {Connection} * @return {Integer} end direction */ getEndDirection :function(/*:Connection*/ conn){ var p = conn.getEndPoint(); var rect = conn.getEndPointDimension(); return this.getDirection(rect, p); }, getStartDirection : function(/*:Connection*/ conn){ var p = conn.getStartPoint(); var rect = conn.getStartPointDimension(); return this.getDirection(rect, p); }, route : function(/*:Connection*/ connection){} }; ConnectionRouter.DIRECTION_UP = 0; ConnectionRouter.DIRECTION_RIGHT = 1; ConnectionRouter.DIRECTION_DOWN = 2; ConnectionRouter.DIRECTION_LEFT = 3; ConnectionRouter.STRAIGHTLINE_TYPE = 'straight'; ConnectionRouter.MANHATTAN_TYPE = 'manhattan'; /* * ---------------------------------------------------- * End - PositionConstants * ---------------------------------------------------- */ /* * ---------------------------------------------------- * Class - ManhattanConnectionRouter * ---------------------------------------------------- */ /** * The Manhattan style connection router * @constructor * @extends ConnectionRouter */ var ManhattanConnectionRouter = function(){ ConnectionRouter.call(this); }; ManhattanConnectionRouter.prototype = new ConnectionRouter; ManhattanConnectionRouter.constructor = ManhattanConnectionRouter; /** * Route the connection from the starting position to the * end position * * @param {Connection} conn */ ManhattanConnectionRouter.prototype.route = function(/*:Connection*/ conn){ var fromPt = conn.getStartPoint(); var fromDir = this.getStartDirection(conn); var toPt = conn.getEndPoint(); var toDir = this.getEndDirection(conn); // draw a line between the two points. this._route(conn, toPt, toDir, fromPt, fromDir); }; /** * Recursive function to route from a startpoint to an end point. * * @param {Connection} conn * @param {Point} fromPt * @param {Integer} fromDir * @param {Point} toPt * @param {Integer} toDir */ ManhattanConnectionRouter.prototype._route = function(/*:Connection*/ conn,/*:Point*/ fromPt, /*:int*/fromDir, /*:Point*/toPt, /*:int*/toDir){ var TOL = 0.1; var TOLxTOL = 0.01; var xDiff = fromPt.getX() - toPt.getX(); var yDiff = fromPt.getY() - toPt.getY(); var point; var dir; if (((xDiff * xDiff) < (TOLxTOL)) && ((yDiff * yDiff) < (TOLxTOL))){ conn.addPoint(toPt); return; } if (fromDir == ConnectionRouter.DIRECTION_LEFT){ if ((xDiff > 0) && ((yDiff * yDiff) < TOL) && (toDir == ConnectionRouter.DIRECTION_RIGHT)){ point = toPt; dir = toDir; } else { if (xDiff < 0) { point = new Point(fromPt.getX() - ManhattanConnectionRouter.MIN_DIST, fromPt.getY()); } else if (((yDiff > 0) && (toDir == ConnectionRouter.DIRECTION_DOWN)) || ((yDiff < 0) && (toDir == ConnectionRouter.DIRECTION_UP))) { point = new Point(toPt.getX(), fromPt.getY()); } else if (fromDir == toDir) { var pos = Math.min(fromPt.getX(), toPt.getX()) - ManhattanConnectionRouter.MIN_DIST; point = new Point(pos, fromPt.getY()); } else { point = new Point(fromPt.getX() - (xDiff / 2), fromPt.getY()); } if (yDiff > 0){ dir = ConnectionRouter.DIRECTION_UP; } else { dir = ConnectionRouter.DIRECTION_DOWN; } } } else if (fromDir == ConnectionRouter.DIRECTION_RIGHT) { if ((xDiff < 0) && ((yDiff * yDiff) < TOL)&& (toDir == ConnectionRouter.DIRECTION_LEFT)) { point = toPt; dir = toDir; } else { if (xDiff > 0) { point = new Point(fromPt.getX() + ManhattanConnectionRouter.MIN_DIST, fromPt.getY()); } else if (((yDiff > 0) && (toDir == ConnectionRouter.DIRECTION_DOWN)) || ((yDiff < 0) && (toDir == ConnectionRouter.DIRECTION_UP))) { point = new Point(toPt.getX(), fromPt.getY()); } else if (fromDir == toDir) { var pos = Math.max(fromPt.getX(), toPt.getX()) + ManhattanConnectionRouter.MIN_DIST; point = new Point(pos, fromPt.getY()); } else { point = new Point(fromPt.getX() - (xDiff / 2), fromPt.getY()); } if (yDiff > 0) { dir = ConnectionRouter.DIRECTION_UP; } else { dir = ConnectionRouter.DIRECTION_DOWN; } } } else if (fromDir == ConnectionRouter.DIRECTION_DOWN) { if (((xDiff * xDiff) < TOL) && (yDiff < 0)&& (toDir == ConnectionRouter.DIRECTION_UP)) { point = toPt; dir = toDir; } else { if (yDiff > 0) { point = new Point(fromPt.getX(), fromPt.getY() + ManhattanConnectionRouter.MIN_DIST); } else if (((xDiff > 0) && (toDir == ConnectionRouter.DIRECTION_RIGHT)) || ((xDiff < 0) && (toDir == ConnectionRouter.DIRECTION_LEFT))) { point = new Point(fromPt.getX(), toPt.getY()); } else if (fromDir == toDir) { var pos = Math.max(fromPt.getY(), toPt.getY()) + ManhattanConnectionRouter.MIN_DIST; point = new Point(fromPt.getX(), pos); } else { point = new Point(fromPt.getX(), fromPt.getY() - (yDiff / 2)); } if (xDiff > 0) { dir = ConnectionRouter.DIRECTION_LEFT; } else { dir = ConnectionRouter.DIRECTION_RIGHT; } } } else if (fromDir == ConnectionRouter.DIRECTION_UP) { if (((xDiff * xDiff) < TOL) && (yDiff > 0) && (toDir == ConnectionRouter.DIRECTION_DOWN)) { point = toPt; dir = toDir; } else { if (yDiff < 0) { point = new Point(fromPt.getX(), fromPt.getY() - ManhattanConnectionRouter.MIN_DIST); } else if (((xDiff > 0) && (toDir == ConnectionRouter.DIRECTION_RIGHT)) || ((xDiff < 0) && (toDir == ConnectionRouter.DIRECTION_LEFT))){ point = new Point(fromPt.getX(), toPt.getY()); } else if (fromDir == toDir) { var pos = Math.min(fromPt.getY(), toPt.getY()) - ManhattanConnectionRouter.MIN_DIST; point = new Point(fromPt.getX(), pos); } else { point = new Point(fromPt.getX(), fromPt.getY() - (yDiff / 2)); } if (xDiff > 0) { dir = ConnectionRouter.DIRECTION_LEFT; } else { dir = ConnectionRouter.DIRECTION_RIGHT; } } } this._route(conn, point, dir, toPt, toDir); conn.addPoint(fromPt); }; ManhattanConnectionRouter.MIN_DIST = 20; /* * ---------------------------------------------------- * End - ManhattanConnectionRouter * ---------------------------------------------------- */ /* * ---------------------------------------------------- * Start - StraightLineConnectionRouter * ---------------------------------------------------- */ /** * The simplest of the connection routing algorithms * Draws a straight line from one point to the next */ var StraightLineConnectionRouter = function(){ ConnectionRouter.call(this); }; StraightLineConnectionRouter.prototype = new ConnectionRouter; StraightLineConnectionRouter.constructor = StraightLineConnectionRouter; /** * Route the connection * @param {Connection} conn */ StraightLineConnectionRouter.prototype.route = function(conn){ conn.addPoint(conn.getStartPoint()); conn.addPoint(conn.getEndPoint()); }; // ---------------------------------------------------- // Start : ProcessFlow PRIVATE variables and methods // ---------------------------------------------------- /** * Process flow diagram configuration option defaults */ var _defaults = { canvas : 'body', // The DOM element in which the flow elements will be drawn editable : true, // Indicates whether the diagram will be editable or not. node_items : [] , // The definition of all flow items currrently in the diagram node_item_values : {}, // The definition of any addition node properties. These will be displayed in a default popup edit dialog if you choose to use the default edit dialog functionality node_field_config : [], // The defination of all the available node properties to be displayed in the default editor dialog. node_connections : [], // The definition of all existing connections between flow items connection_item_values: [], // The definition of any additional connection properties. node_connection_types : [], // The list of supported connection types per node connection_rules : [], // The list of supported connection targets per connection type dflt_conn_line_width : 1, // The default width of all connection lines onNodeEdit : null, // The function to be called when the user click the "Edit" link on the node context menu onNodeDelete : null, // The function to be called for each node that is deleted. onNodeClick : null, // The function to be called when the user clicks on the node onConnectionClick : null, // The function to be called when the user clicks on a connection onConnectionEdit : null, // The function to be called when the user clicks on the "Edit" link on the connection context menu connectionIdFactory : null, // The function used to generate IDs for new connections nodeIdFactory : null, // The function used to generate IDs for new nodes, cavasScaleFactor : 0.1, // The factor to scale the canvase size by draw_conn_decorators : true, // Indicate whether the connection decorator should be drawn on the connection line or not. autosize_canvas : true, // Indicate whether the drawing canvas should be automatically sized to fit its contents when the diagram is drawn for the first time. If false then the canvas dimensions must be specified. canvas_size : null, // The width and height of the canvas. Required if autosize_canvas == false. Provide an object with "width" and "height" properties. tooltips_enabled : false, // Indicate whether tooltips will be displayed with the user hovers over a node or connection. the jQuery Qtip plugin is used for this. If this is true the user must implement the beforeNodeTooltipShow and beforeConnectionTooltipShow callbacks beforeNodeTooltipShow : null, // The function to be called just before the tooltip is shown beforeConnTooltipShow : null, // The function to be called just before the connection tooltip is shown in_out_line_color_change : true, // Indicate whether the inbound and outbound connection lines on the selected node will change color, according to connection direction, when a node is selected connectionRoute : ConnectionRouter.MANHATTAN_TYPE // The factor to scale the canvase size by }; var opts = null; /** * The Raphael canvas */ var _canvas = null; /** * Read the list of existing flow items * and render them into the flow diagram */ function _loadNodeItems(){ if(opts.node_items != null){ for(var i = 0; i < opts.node_items.length; i++){ var flowItemCfg = opts.node_items[i]; var nodeShape = null; if(flowItemCfg.shape.type == Shape.CIRCLE){ nodeShape = new Circle(flowItemCfg.shape.entity_id, flowItemCfg.shape.x, flowItemCfg.shape.y, flowItemCfg.shape.radius, flowItemCfg.shape.border_color, flowItemCfg.shape.border_width, flowItemCfg.shape.fill, flowItemCfg.label_color, flowItemCfg.label_font, flowItemCfg.label_font_size, flowItemCfg.label_font_bold, flowItemCfg.label_font_italic); }else if (flowItemCfg.shape.type == Shape.RECTANGLE){ nodeShape = new Rectangle(flowItemCfg.shape.entity_id, flowItemCfg.shape.x, flowItemCfg.shape.y, flowItemCfg.shape.width, flowItemCfg.shape.height, flowItemCfg.shape.border_radius, flowItemCfg.shape.border_color, flowItemCfg.shape.border_width, flowItemCfg.shape.fill, flowItemCfg.label_color, flowItemCfg.label_font, flowItemCfg.label_font_size, flowItemCfg.label_font_bold, flowItemCfg.label_font_italic); } else if (flowItemCfg.shape.type == Shape.PATH){ nodeShape = new Path(flowItemCfg.shape.entity_id, flowItemCfg.shape.x, flowItemCfg.shape.y, flowItemCfg.shape.path, flowItemCfg.shape.border_color, flowItemCfg.shape.border_width, flowItemCfg.shape.fill, flowItemCfg.label_color, flowItemCfg.label_font, flowItemCfg.label_font_size, flowItemCfg.label_font_bold, flowItemCfg.label_font_italic); } var node = new Node(flowItemCfg.id, flowItemCfg.entity_id, flowItemCfg.type, flowItemCfg.label, flowItemCfg.icon_url, nodeShape); _flowItemList.put(flowItemCfg.id, node); } } }; /** * Read the list of existing connections between flow nodes * and render them into the flow diagram */ function _loadConnections(){ if(opts.node_connections != null){ for(var i = 0; i < opts.node_connections.length; i++){ var nodeConnectionCfg = opts.node_connections[i]; var connection = new Connection(nodeConnectionCfg.id, nodeConnectionCfg.entity_id, nodeConnectionCfg.type, _flowItemList.get(nodeConnectionCfg.from_node_id).getPort(nodeConnectionCfg.from_port), _flowItemList.get(nodeConnectionCfg.to_node_id).getPort(nodeConnectionCfg.to_port), nodeConnectionCfg.line_color, nodeConnectionCfg.line_width, nodeConnectionCfg.line_label, nodeConnectionCfg.label_font, nodeConnectionCfg.label_font_color, nodeConnectionCfg.label_font_size, nodeConnectionCfg.label_font_bold, nodeConnectionCfg.label_font_italic); _connectionList.put(connection.getId(), connection); } } }; /** * Set the canvas size */ function _setCanvasSize(width, height){ jQuery(opts.canvas).css({ height: height + 'px', width : width + 'px' }); // Set the Raphael canvas size _canvas.setSize(width, height); }; function _setZoomLevel(zoomLevel){ // We also need to increase the canvas size. var newZoom = zoomLevel / _canvas.zoom; var cHeight = jQuery(opts.canvas).height() * newZoom; var cWidth = jQuery(opts.canvas).width() * newZoom; _setCanvasSize(cWidth, cHeight); _canvas.setZoom(zoomLevel); } // ---------------------------------------------------- // End : ProcessFlow PRIVATE variables and methods // ---------------------------------------------------- // ---------------------------------------------------- // Start : ProcessFlow PUBLIC variables and methods // ---------------------------------------------------- return { /** * Create a new flow node */ createNode : function(nodeId, entityId, type, label, iconUrl, shape){ return new Node(nodeId, entityId, type, label, iconUrl, shape); }, /** * Create a new Circle shape * * @param x * @param y * @param radius * @param border_color * @param border_width * @param bg_color * @returns {Circle} */ createCircle : function(_entity_id, x, y, radius, borderColor, borderWidth, bgColor) { return new Circle(_entity_id, x, y, radius, borderColor, borderWidth, bgColor); }, /** * Create a new Rectangle Shape * * @param x * @param y * @param width * @param height * @param border_radius * @param border_color * @param border_width * @param bg_color * @returns {Rectangle} */ createRect : function(_entity_id, x, y, width, height, borderRadius, borderColor, borderWidth, bgColor) { return new Rectangle(_entity_id, x, y, width, height, borderRadius, borderColor, borderWidth, bgColor); }, /** * Create a new Path * @param x * @param y * @param path * @param borderColor * @param borderWidth * @param bgColor * @returns {Path} */ createPath : function(_entity_id, x, y, path, borderColor, borderWidth, bgColor) { return new Path(_entity_id, x, y, path, borderColor, borderWidth, bgColor); }, getCanvas : function(){ return _canvas; }, /** * Returns the node object with the given node id */ getNodeById : function(nodeId) { return _flowItemList.get(nodeId); }, /** * Returns the connection object with the given node id */ getConnectionById : function(id) { return _connectionList.get(id); }, /** * Selects the given node on the canvas */ selectOnlyNode : function(_node) { NodeSelectionHandler.selectOnlyNode(_node); }, /** * Selects the node with the given id on the canvas */ selectOnlyNodeById : function(_node_id) { NodeSelectionHandler.selectOnlyNode(this.getNodeById(_node_id)); }, /** * Selects the nodes on the canvas with the ids in the array * eg. selectNodesByIds([1,2,3]) */ selectNodesByIds : function(ids) { var nodes = []; for(var i = 0; i < ids.length; i++) nodes.push(this.getNodeById(ids[i])); return NodeSelectionHandler.setSelectedNodes(nodes); }, /** * Returns all the nodes that are currently selected */ getSelectedNodes : function() { return NodeSelectionHandler.getSelectedNodes(); }, /** * Delete selected nodes */ deleteSelectedNodes : function(){ var selectedNodes = NodeSelectionHandler.getSelectedNodes(); if(selectedNodes.length > 0){ if(confirm('Delete ' + selectedNodes.length + ' node(s) and their associated connections?')){ for(var i = 0; i < selectedNodes.length; i++){ selectedNodes[i].destroy(); } selectedNodes = []; } } }, /** * Put the process flow design back into default operation */ stopItemBlockSelection : function(){ NodeSelectionHandler.setBlockSelectionEnabled(false); }, /** * Turn on the block selection mode allowing the * user to select multiple elements by drawing a block * around them */ startItemBlockSelection : function(){ NodeSelectionHandler.setBlockSelectionEnabled(true); }, /** * Add an array of nodes to the the process flow diagram * @param {Array} nodeList */ addNodes : function(nodeList){ _flowItemList = nodeList; }, /** * Add a single node to the process flow diagram * @param {Node} nodeItem */ addNode : function(nodeItem){ _flowItemList.put(nodeItem.getId(), nodeItem); }, /** * Align the selected nodes according to the specified * alignment option */ alignNodes : function(alignment){ NodeAlignmentHandler.alignNodes(alignment); }, /** * Allow the calling program to set specific node properties * like the node label and the node icon. Properties * are passed in a JSON form e.g * {label : 'New Label, * icon_url : 'url/icon.png'} */ setNodeProperties : function(nodeId, nodeProperties){ var node = _flowItemList.get(nodeId); if(nodeProperties.label != null){ node.setLabel(nodeProperties.label); } if(nodeProperties.style != null){ node.setStyle(nodeProperties.style); } }, /** * Allow the calling program to set specific connecition properties * like the connection label and the label font style */ setConnectionProperties : function(connId, connProperties){ var conn = _connectionList.get(connId); if(connProperties.label != null){ conn.setLabel(connProperties.label); } if(connProperties.style != null){ conn.setStyle(connProperties.style); } }, /** * Return the currect state of the flow diagram */ getCurrentStateJSON : function(){ if(!opts) return null; var jQuerycanvas = jQuery(opts.canvas); var currentStateJSON = { "diagram" : { connectionRoute : _currConnRouteStyle, width : jQuerycanvas.width(), height : jQuerycanvas.height() }, "nodes" : [], "connections" : [], "node_item_values" : opts.node_item_values, "connection_item_values" : opts.connection_item_values }; // Get the current state of all nodes var nodes = _flowItemList.valSet(); for(var i = 0; i < nodes.length; i++){ currentStateJSON.nodes.push(nodes[i].toJSON()); } // Get the current state of all connections var connections = _connectionList.valSet(); for(var i = 0; i < connections.length; i++){ currentStateJSON.connections.push(connections[i].toJSON()); } return currentStateJSON; }, /** * Updates the JSON of the flow diagram */ updateCurrentStateJSON : function(jsonString){ var json = JSON.parse(jsonString); for(var i=0; i maxX) { maxX = nodeX; } var nodeY = bBox.y + bBox.height; if (nodeY > maxY) { maxY = nodeY; } } var canvasWidth = maxX + 500; var canvasHeight = maxY + 500; _setCanvasSize(canvasWidth, canvasHeight); }, /** * Set the zoom level of the the diagram. * This affects the size of the drawing area and the * shapes and lines of the diagram. */ setZoomLevel : function(zoomLevel){ _setZoomLevel(zoomLevel); }, /** * Redraws all the connections */ redrawAllConnections : function(){ var connections = _connectionList.valSet(); for(var i = 0; i < connections.length; i++){ connections[i].reconsiderPorts(); } var nodes = _flowItemList.valSet(); for(var i = 0; i < nodes.length; i++){ var node = nodes[i]; node.movePortsToCurrentPos(); } for(var i = 0; i < connections.length; i++){ connections[i].redraw(true); } }, /** * Gets all the nodes without any incoming connections */ getRootNodes: function() { var roots = []; var nodes = _flowItemList.valSet(); for(var i = 0; i < nodes.length; i++){ var node = nodes[i]; if (node.getIncomingNodes().length == 0) { roots.push(node); } } return roots; }, /** * Performs a directed layout */ layoutDirected: function() { var roots = this.getRootNodes(); var levels = []; var allNodes = []; var maxElementsInALevel = 0; var positionedNodes = []; var maxWidth = 0; var maxHeight = 0; doLayout(); /** * Recursively assign a level to each node, and break all cycles * @param currentNode The current node * @param depth The current depth * @param ancestorNode The ancestor of the current node */ function addNodes(currentNode, depth, ancestorNode) { if (jQuery.inArray(currentNode, allNodes) != -1) { return 0; } currentNode.spaceRequired = 1; if (ancestorNode != null) { if (currentNode.ancestorNode == null) { currentNode.ancestorNode = ancestorNode; } if (currentNode.ancestorNode.directChildren == null) { currentNode.ancestorNode.directChildren = []; } currentNode.ancestorNode.directChildren.push(currentNode); } var nodesAtDepth = levels[depth]; if (nodesAtDepth == null) { nodesAtDepth = []; levels[depth] = nodesAtDepth; } nodesAtDepth.push(currentNode); allNodes.push(currentNode); if (nodesAtDepth.length > maxElementsInALevel) { maxElementsInALevel = nodesAtDepth.length; } var outgoingNodes = currentNode.getOutgoingNodes(); for (var node in outgoingNodes) { var childSpaceRequired = addNodes(outgoingNodes[node], depth + 1, currentNode); if (childSpaceRequired > 0) { currentNode.spaceRequired += childSpaceRequired; } } if (currentNode.spaceRequired > 1) { currentNode.spaceRequired--; } return currentNode.spaceRequired; } /** * Redraws the graph after layout * @param levels A two-dimensional array of nodes by depth, which may contain gaps to add space. */ function redrawAfterLayout(levels) { var width = maxWidth + 50; var height = maxHeight + 30; for (var depth = 0; depth < levels.length; depth++) { var nodesAtDepth = levels[depth]; for (var x = 0; x < nodesAtDepth.length; x++) { var node = nodesAtDepth[x]; if (node != null) { node.getShape().setPosition(node.position * width, (depth + 1) * height); } } } ProcessFlow.redrawAllConnections(); ProcessFlow.scaleCanvasDynamic(); } /** * Places a node in the center of its allocated space, and recursively allocate space to its children * @param node The node * @param startOfAllocation The first position of the node space allocation * @param endOfAllocation The last position of the node space allocation */ function allocateNode(node, startOfAllocation, endOfAllocation) { node.position = startOfAllocation + Math.round((endOfAllocation - startOfAllocation) / 2); var startChildAllocation = startOfAllocation; if (node.directChildren) { for (var c = 0; c < node.directChildren.length; c++) { var directChild = node.directChildren[c]; var endChildAllocation = startChildAllocation + directChild.spaceRequired; allocateNode(directChild, startChildAllocation, endChildAllocation); startChildAllocation = endChildAllocation; } } //console.log("Node position: " + node.position + ", Allocation:" + startOfAllocation + "-" + endOfAllocation + " : " + node.getLabel()); } /** * The main layout process */ function doLayout() { var nodes = _flowItemList.valSet(); for(var i = 0; i < nodes.length; i++){ var node = nodes[i]; node.ancestorNode = null; node.position = null; node.directChildren = null; if (node.isSelected()) { node.setSelected(false); } var bbox = node.getShape().getSvgParentShape().getBBox(); if (bbox.width > maxWidth) { maxWidth = bbox.width; } if (bbox.height > maxHeight) { maxHeight= bbox.height; } } for (var i = 0; i < roots.length; i++) { addNodes(roots[i], 0, null); } for (var i = 0; i < roots.length; i++) { allocateNode(roots[i], 1, roots[i].spaceRequired); } redrawAfterLayout(levels); } }, /** * Performs a graph layout */ layoutSpring : function(spacingFactor){ spacingFactor = spacingFactor || 1; var g = new Graph(); var nodes = _flowItemList.valSet(); for(var i = 0; i < nodes.length; i++){ var node = nodes[i]; if (node.isSelected()) { node.setSelected(false); } g.addNode(node.getId()); } var edges = _connectionList.valSet(); for(var i = 0; i < edges.length; i++){ var edge = edges[i]; g.addEdge(edge.getSourcePort().getParentNode().getId(), edge.getTargetPort().getParentNode().getId()); } var layouter = new Graph.Layout.Spring(g); layouter.layout(); for(var i = 0; i < nodes.length; i++){ var workflowNode = nodes[i]; var layoutNode = g.nodes[workflowNode.getId()]; var newX = Math.round(((layoutNode.layoutPosX - g.layoutMinX) * 80 + 200) * spacingFactor); var newY = Math.round(((layoutNode.layoutPosY - g.layoutMinY) * 80 + 200) * spacingFactor); workflowNode.getShape().setPosition(newX, newY); } this.redrawAllConnections(); this.scaleCanvasDynamic(); }, /** * Change the connection routing mechanism */ setConnectionRoutingType : function(type){ _currConnRouteStyle = type; var connections = _connectionList.valSet(); for(var i = 0; i < connections.length; i++){ var router = null; if(type == ConnectionRouter.STRAIGHTLINE_TYPE){ router = new StraightLineConnectionRouter(); }else if (type == ConnectionRouter.MANHATTAN_TYPE){ router = new ManhattanConnectionRouter(); } connections[i].setConnectionRouter(router); } }, /** * Converts a div into the default look-and-feel catalogue. The drop event must still be provided and handled by the * client application. * * @param selector jQuery selector to get the catalogue div * @param catalogueConfig Object containing the catalogue config. The layout of this object must follow: * {"section_name_1" : {"item_name1" : {"attrib1" : "value1", "attrib2" : "value2", "attrib3" : ...}, "item_name_2" : ...}, * "section_name_2" : ... * } * In the above example, "section_name" indicates a panel in the accordian pane, "item_name" represents an item in a * panel, and "attrib1" indicates the item meta attributes, typically things like "type", "shape", "label" etc * @param itemsPerRow The number of items to display on each row of the catalogue */ initCatalogueItems: function (selector, catalogueConfig, itemsPerRow) { var result = '
'; for (var categoryName in catalogueConfig) { var categoryMap = catalogueConfig[categoryName]; result += '

' + categoryName + '

'; var itemCount = 0; var itemNames = []; for (var itemName in categoryMap) { itemNames.push(itemName); var itemMap = categoryMap[itemName]; result += ''; if (++itemCount == itemsPerRow) { result += ''; for (c in itemNames) { result += ''; } result += ''; itemCount = 0; itemNames = []; } } result += '
' + itemNames[c] + '
'; } result += '
'; jQuery(selector).append(result); jQuery('.catalogueItem').each(function(){ var nodeItemMeta = jQuery(this).metadata(); var paper = Raphael(jQuery(this)[0], jQuery(this).width(), jQuery(this).height()); if(nodeItemMeta.shape.type == 'circle'){ var circleXYR = (jQuery(this).height() / 2); paper.circle(circleXYR, circleXYR, circleXYR - 2).attr({fill : nodeItemMeta.shape.fill}); } else if(nodeItemMeta.shape.type == 'rect'){ paper.rect(1, 1, nodeItemMeta.shape.width - 2,nodeItemMeta.shape.height - 2, 5).attr({fill : nodeItemMeta.shape.fill}); } else if(nodeItemMeta.shape.type == 'path'){ paper.path(nodeItemMeta.shape.path).attr({fill : nodeItemMeta.shape.fill}); } }); jQuery('.catalogueItem').draggable({helper : 'clone', 'appendTo': '.canvas', containment : '.canvas', opacity : 0.7}); jQuery(".accordion").accordion({fillSpace: true}); jQuery(selector).dialog({ resize: function(event, ui) { // This is a bit buggy in Chrome, which cuts off the bottom of the accordian jQuery(".accordion").accordion("resize"); } }); }, /** * Adds a connection between two ports using the parameters specified. * Ports are obtained through the Node.getPort() */ connectNodes : function(port1, port2, type, line_width, line_label) { return NodeConnectHandler.connectNodes(port1, port2, type, line_width, line_label); }, /** * Shows the default data editing dialog for Nodes. Currently supports text input (default), select, and textarea. The type * of input component is determined by an optional 'type' attribute on the node's fieldConfig. * * @param nodeEditDialog 'Node' or 'Connection' * @param nodeId The ID of the item being edited * @param nodeType The type of the item being edited * * This method currently requires a div name 'nodeEditDialog' to exist, which is used as the dialog. */ showNodeEditDialog : function (nodeEditDialog, nodeId, nodeType){ // Clear the dialog before building the new one jQuery('#nodeEditDialog').empty(); if(nodeEditDialog == 'Node'){ var formStructure = jQuery('
'); var cfg = opts.node_field_config[nodeType]; var nodeValues = opts.node_item_values[nodeId]; if ( ! nodeValues) { nodeValues = {}; opts.node_item_values[nodeId] = nodeValues; } var inputFields = []; if(cfg != null){ for(var i = 0; i < cfg.length; i++){ var config = cfg[i]; var fieldRow = jQuery(''); fieldRow.append('' + config.label + ''); var displayValue = nodeValues[config.field_name]; if ( ! displayValue) { nodeValues[config.field_name] = null; displayValue = ''; } var inputFieldStr; if ( ! config.type || config.type == 'text') { inputFieldStr = ''; } else if (config.type == 'select') { inputFieldStr = ''; } else if (config.type == 'textarea') { var rows = config.rows ? config.rows : 4; var cols = config.cols ? config.cols : 40; inputFieldStr = '"; } var inputField = jQuery(inputFieldStr); inputFields.push(inputField); var fieldCell = jQuery(''); fieldCell.append(inputField); // Add field validation if (config.validators) { var fieldValidators = config.validators; var validation = function(){ for(var j = 0; j < fieldValidators.length; j++){ if(fieldValidators[j].validateMethod.call(this, jQuery(this).val()) == false){ jQuery('#nodeEditDialog').append(fieldValidators[j].message + '
'); } } }; inputField.change(validation); inputField.change(); } fieldRow.append(fieldCell); formStructure.append(fieldRow); } jQuery('#nodeEditDialog').append(formStructure); jQuery('#nodeEditDialog').dialog({ autoOpen : false, modal : true, buttons: { "Apply" : function() { for (i in inputFields) { var name = inputFields[i].attr('name'); var val = inputFields[i].val(); nodeValues[name] = val; // Do some implicit binding here // so that the node label is updated // if the user is allowed to edit a "label" property if(name == "label"){ var node = _flowItemList.get(nodeId); node.setLabel(val); } } jQuery( this ).dialog( "destroy" ); }, "Close" : function() { jQuery( this ).dialog( "destroy" ); } }}); jQuery('#nodeEditDialog').dialog('open'); } } }, /** * Initialise the process flow diagram * * @returns */ init : function(config) { /* You need to disable text selection on the canvas, otherwise resizing * and dragging on the canvas leads to strange behaviour. * http://stackoverflow.com/questions/2700000/how-to-disable-text-selection-using-jquery */ (function($){ $.fn.disableSelection = function() { return this .attr('unselectable', 'on') .css('user-select', 'none') .bind('selectstart', false); }; })(jQuery); _processFlow = this; opts = jQuery.extend({}, _defaults, config); _isIE = jQuery.browser ? jQuery.browser.msie : (!jQuery.support.leadingWhitespace); _currConnRouteStyle = opts.connectionRoute; var jQueryelem = jQuery(opts.canvas); jQueryelem.disableSelection(); _canvas = Raphael(jQueryelem[0], jQueryelem.outerWidth(), jQueryelem.outerHeight()).initZoom(); _loadNodeItems(); _loadConnections(); if(opts.autosize_canvas == true){ this.scaleCanvasDynamic(); }else{ if(opts.autosize_canvas != null){ _setCanvasSize(opts.canvas_size.width, opts.canvas_size.height); } } } // ---------------------------------------------------- // End : ProcessFlow PUBLIC variables and methods // ---------------------------------------------------- }; })();