/**************************************************************************** ** ** This file is part of yFiles FLEX Client Layout Extension 1.2.0.1. ** ** yWorks proprietary/confidential. Use is subject to license terms. ** ** Unauthorized redistribution of this file or of a byte-code version ** of this file is strictly forbidden. ** ** Copyright (c) 2012 by yWorks GmbH, Vor dem Kreuzberg 28, ** 72070 Tuebingen, Germany. All rights reserved. ** ***************************************************************************/ package demo.businessProcessDiagram.bpd { import com.yworks.canvas.CanvasComponent; import com.yworks.canvas.drawing.IHitTestable; import com.yworks.canvas.geom.IRectangle; import com.yworks.canvas.geom.ISize; import com.yworks.canvas.geom.ImmutableRectangle; import com.yworks.canvas.geom.YRectangle; import com.yworks.canvas.input.EventRecognizers; import com.yworks.canvas.input.IInputModeContext; import com.yworks.canvas.input.IPositionHandler; import com.yworks.canvas.input.IReshapeHandleProvider; import com.yworks.canvas.input.IReshapeHandler; import com.yworks.canvas.input.ISizeConstraintProvider; import com.yworks.canvas.input.MoveViewportInputMode; import com.yworks.canvas.input.ReshapeHandleProvider; import com.yworks.graph.drawing.ILabelStyleRenderer; import com.yworks.graph.drawing.INodeInsetsProvider; import com.yworks.graph.drawing.SimpleLabelStyle; import com.yworks.graph.drawing.TemplateNodeStyle; import com.yworks.graph.input.GraphEditorInputMode; import com.yworks.graph.input.NodeDropInputMode; import com.yworks.graph.input.snapLines.GraphSnapContext; import com.yworks.graph.input.snapLines.OrthogonalEdgeEditingContext; import com.yworks.graph.model.DefaultGraph; import com.yworks.graph.model.EmptyPortsCandidateProvider; import com.yworks.graph.model.ExteriorLabelModel; import com.yworks.graph.model.GraphDecorator; import com.yworks.graph.model.GraphEvent; import com.yworks.graph.model.GraphEventKind; import com.yworks.graph.model.IBend; import com.yworks.graph.model.IBendList; import com.yworks.graph.model.IEdge; import com.yworks.graph.model.IEdgePortCandidateProvider; import com.yworks.graph.model.IGraph; import com.yworks.graph.model.IGroupedGraph; import com.yworks.graph.model.ILabel; import com.yworks.graph.model.ILabelModelParameter; import com.yworks.graph.model.INode; import com.yworks.graph.model.INodeHierarchy; import com.yworks.graph.model.IPortCandidateProvider; import com.yworks.graph.model.InteriorLabelModel; import com.yworks.graph.model.InteriorStretchLabelModel; import com.yworks.graph.model.LayoutExecutor; import com.yworks.graph.model.RotatedSliderEdgeLabelModel; import com.yworks.graph.model.SelectionEvent; import com.yworks.graph.model.TagOwnerMapper; import com.yworks.io.SymbolicPackageNameRegistry; import com.yworks.io.graphml.GraphMLConstants; import com.yworks.io.graphml.reader.deserializer.FillDeserializer; import com.yworks.io.graphml.writer.serializer.FillSerializer; import com.yworks.remote.RoundtripEvent; import com.yworks.remote.RoundtripHandler; import com.yworks.support.ArrayList; import com.yworks.support.DictionaryMapper; import com.yworks.support.ITagOwner; import com.yworks.support.Iterator; import com.yworks.ui.GraphCanvasComponent; import demo.businessProcessDiagram.model.BPActivity; import demo.businessProcessDiagram.model.BPArtifact; import demo.businessProcessDiagram.model.BPDeserializer; import demo.businessProcessDiagram.model.BPEvent; import demo.businessProcessDiagram.model.BPGateway; import demo.businessProcessDiagram.model.BPMNElements; import demo.businessProcessDiagram.model.BPPool; import demo.businessProcessDiagram.model.BPRelation; import demo.businessProcessDiagram.model.BPSerializer; import demo.businessProcessDiagram.model.BPSwimlane; import demo.businessProcessDiagram.model.IBPElement; import demo.input.MinimumNodeSizeConstraintProvider; import demo.input.MyMoveViewportInputMode; import demo.input.createPortRelocationHandleProvider; import flash.events.Event; import flash.events.IOErrorEvent; import mx.collections.ArrayCollection; import mx.controls.Alert; import mx.managers.CursorManager; import mx.rpc.Fault; import mx.rpc.events.FaultEvent; /** *

This class can be used to display and manipulate a business process diagram. The diagram consists of one or * several pools containing one or several swim lanes in which business process elements can be placed.

*/ public class BusinessProcessDiagramComponent extends GraphCanvasComponent { /* [Embed(systemFont='Dialog', fontName='dialog', mimeType='application/x-font', embedAsCFF='false', unicodeRange="U+0020-U+007E,U+00A0-U+00FF")] [Embed(systemFont='MS 明朝', fontName='MS 明朝', mimeType='application/x-font', embedAsCFF='false', unicodeRange="U+0020-U+007E,U+00A0-U+00FF")] */ private var Dialog:Class; public static const POOL_POSITION_KEY:String = "pool_position"; public static const SWIM_LANE_POSITION_KEY:String = "swimlane_position"; public static const BPMN_DATA_KEY:String = "demo.businessProcessDiagram.bpd.BusinessProcessDiagramComponent.BPMN_DATA_KEY"; // ArrayList of the pool nodes. private var _pools:ArrayList = new ArrayList(); // Indicates if the order of the pools is in sync with their position in the pool position mapper. private var _poolsDirty:Boolean; // BPPool -> INode private var _poolMapper:DictionaryMapper = new DictionaryMapper(); // BPSwimlane -> INode private var _swimLaneMapper:DictionaryMapper = new DictionaryMapper(); public var _poolToPoolDistance:uint = 100; public var _minimalSwimLaneHeight:uint = 200; public var _minimalSwimLaneWidth:uint = 800; private var _poolBoundsUpdater:PoolBoundsUpdater; private var _loadableGraphList:ArrayCollection; /** * Initializes a new BusinessProcessDiagramComponent. */ public function BusinessProcessDiagramComponent() { initGrouping(); initInputModes(); initDefaultStyles(); initGraph(); } /** * Initializes grouping related issues. */ protected function initGrouping():void { /* Allow grouping */ DefaultGraph(graph).groupingSupported = true; var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; // the bounds of group nodes shall not be adjusted automatically. The adjustment is done by the PoolBoundsUpdater. gg.autoAdjustGroupNodeBounds = false; this._poolBoundsUpdater = new PoolBoundsUpdater(this); graph.mapperRegistry.addMapper(BusinessProcessDiagramComponent.POOL_POSITION_KEY, new DictionaryMapper(true)); graph.mapperRegistry.addMapper(BusinessProcessDiagramComponent.SWIM_LANE_POSITION_KEY, new DictionaryMapper(true)); } /** * Initializes the InputModes. */ protected function initInputModes():void { // Swim lane and pool nodes need to receive mouse click events for their buttons enableMouseChildren = CanvasComponent.MOUSE_CHILDREN_ALWAYS; var snapContext:GraphSnapContext = new GraphSnapContext(); snapContext.collectNodePairSnapLines = false; var mainInputMode:GraphEditorInputMode = new BPGraphEditorInputMode(graph, selection); var moveViewportInputMode:MoveViewportInputMode = new MyMoveViewportInputMode(); mainInputMode.addConcurrent(moveViewportInputMode, 0); inputMode = mainInputMode; mainInputMode.groupSelectionAllowed = false; mainInputMode.ungroupSelectionAllowed = false; mainInputMode.clipboardOperationsAllowed = false; mainInputMode.autoAdjustContentRect = true; mainInputMode.autoRemoveEmptyLabels = false; mainInputMode.nodeCreator = null; // new nodes shall be created via drag'n'drop mainInputMode.createEdgeInputMode.connectToCandidatesOnly = true; mainInputMode.createEdgeInputMode.orthogonalEdgeCreation = true; mainInputMode.orthogonalEdgeEditingContext = new OrthogonalEdgeEditingContext(); // A DropInputMode is used to start edge creation by dragging a relation from the palette var dropEdgeInputMode:DropEdgeInputMode = new DropEdgeInputMode(); dropEdgeInputMode.orthogonalEdgeCreation = true; mainInputMode.addConcurrent(dropEdgeInputMode, mainInputMode.createEdgeModePriority - 10); mainInputMode.moveInputMode.positionHandler = new PoolAdjustingPositionHandlerWrapper(mainInputMode.moveInputMode.positionHandler); mainInputMode.snapContext = snapContext; var nodeDropInputMode:NodeDropInputMode = new BPMNNodeDropInputMode("items"); nodeDropInputMode.validDropHitTestable = new ValidNodeDropHitTestable(this); nodeDropInputMode.nodeDropCreator = nodeDropCreator; mainInputMode.addConcurrent(nodeDropInputMode, -5); mouseWheelZoomFactor = 1.07; mouseWheelZoomCenteredRecognizer = EventRecognizers.MODIFIER_CTRL; mouseWheelZoomMouseLocationRecognizer = EventRecognizers.NO_MODIFIER_PRESSED; var graphDecorator:GraphDecorator = graph.lookup(GraphDecorator) as GraphDecorator; // Allow port/edge relocation graphDecorator.edgeDecorator.handleProviderDecorator.setFactory(createPortRelocationHandleProvider); // Allow to connect edges to ports of other nodes graphDecorator.edgeDecorator.edgePortCandidateProviderDecorator.setFactory( function (edge:IEdge):IEdgePortCandidateProvider { return new BPMNEdgePortCandidateProvider(edge); }); // Return a set of ports for a node depending on the edge that would connect to it. graphDecorator.nodeDecorator.portCandidateProviderDecorator.setFactory( function(node:INode):IPortCandidateProvider { return new BPMNPortCandidateProvider(node); }, function (node:INode):Boolean { var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; return tagOwner != null && tagOwner.tag is IBPElement; }); // No ports shall be returned for swim lane and pool nodes graphDecorator.nodeDecorator.portCandidateProviderDecorator.setFactory( function(node:INode):IPortCandidateProvider { return new EmptyPortsCandidateProvider(); }, function (node:INode):Boolean { var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; return tagOwner != null && (tagOwner.tag is BPPool || tagOwner.tag is BPSwimlane); }); /* Let the swim lane label contribute to its insets */ graphDecorator.nodeDecorator.insetsProviderDecorator.setFactory( function(node:INode):INodeInsetsProvider { return new SimpleInsetsProvider(new ImmutableRectangle(65, 5, 5, 5)); },function(node:INode):Boolean { var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; return tagOwner != null && tagOwner.tag is BPSwimlane; }); /* Let the pool node label contribute to its insets */ graphDecorator.nodeDecorator.insetsProviderDecorator.setFactory( function(node:INode):INodeInsetsProvider { return new SimpleInsetsProvider(new ImmutableRectangle(60, 1, 1, 1)); },function(node:INode):Boolean { var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; return tagOwner != null && tagOwner.tag is BPPool; }); // connects a PositionHandler to the nodes which adjust the nodes' locations after a reparenting gesture. graphDecorator.nodeDecorator.positionHandlerDecorator.setImplementationWrapper( function(node:INode, wrapped:IPositionHandler):IPositionHandler { return new ReparentingPositionHandlerWrapper(node, wrapped); }, function(node:INode):Boolean { var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; return tagOwner != null && tagOwner.tag is IBPElement; }); // connects a ReshapeHandleProvider to the nodes which adjusts the pool bounds after resizing graphDecorator.nodeDecorator.reshapeHandleProviderDecorator.setImplementationWrapper( function(node:INode, wrapped:IReshapeHandleProvider):IReshapeHandleProvider { if (wrapped is ReshapeHandleProvider) { var rhp:ReshapeHandleProvider = ReshapeHandleProvider(wrapped); var sc:ISizeConstraintProvider = new MinimumNodeSizeConstraintProvider(node.lookup(ISizeConstraintProvider) as ISizeConstraintProvider); if (sc != null) { rhp.maximumSize = sc.getMaximumSize(node); rhp.minimumSize = sc.getMinimumSize(node); rhp.minimumEnclosedArea = sc.getMinimumEnclosedArea(node); } } return new PoolAdjustingReshapeHandleProvider(wrapped); }); // no reshape handler shall be used for the group nodes var nh:INodeHierarchy = graph.lookup(INodeHierarchy) as INodeHierarchy; graphDecorator.nodeDecorator.reshapeHandlerDecorator.setImplementationWrapper(function(node:INode, wrapped:IReshapeHandler):IReshapeHandler { if (nh.isLeaf(node)) { return wrapped; } return null; }); // triggers opening the connected URLS if a node is double clicked inputModes.addItem(new BPMNDblClickInputMode()); selection.addEventListener(SelectionEvent.SELECT, selectionFilter); selection.addEventListener(SelectionEvent.SELECT, onNodeSelected); graph.addEventListener(GRAPH_CHANGED, onLabelChanged); if (graph is DefaultGraph) { DefaultGraph(graph).autoAdjustPreferredLabelSize = false; } // add event listener for SwimLaneEvent and PoolEvent which are dispatched by the SwimLaneTemplate and // PoolTemplate if any of their buttons are clicked addEventListener(SwimLaneEvent.ADD_SWIM_LANE, newSwimLaneButtonClicked); addEventListener(SwimLaneEvent.REMOVE_SWIM_LANE, removeSwimLaneButtonClicked); addEventListener(PoolEvent.ADD_POOL, newPoolButtonClicked); addEventListener(PoolEvent.REMOVE_POOL, removePoolButtonClicked); } /** * Called by the BPMNNodeDropInputMode when data is dropped onto the component. */ protected function nodeDropCreator(context:IInputModeContext, graph:IGraph, draggedNode:INode, newNodeLayout:IRectangle):INode { var swimLane:INode = getSwimLaneNodeAtCursor(newNodeLayout.x + newNodeLayout.width/2, newNodeLayout.y + newNodeLayout.height/2); if (swimLane != null) { var newX:Number = newNodeLayout.x; var newY:Number = newNodeLayout.y; // calculate swim lane content area var inip:INodeInsetsProvider = swimLane.lookup(INodeInsetsProvider) as INodeInsetsProvider; var insets:IRectangle; if (inip != null) { insets = inip.getInsets(swimLane); } else { insets = new YRectangle(0, 0, 0, 0); } var swimLaneContentArea:IRectangle = new YRectangle( swimLane.layout.x + insets.x, swimLane.layout.y + insets.y, swimLane.layout.x + swimLane.layout.width - insets.width, swimLane.layout.y + swimLane.layout.height - insets.height); // make sure the new node will be placed inside its containing swim lane if (newNodeLayout.x < swimLaneContentArea.x) { newX = swimLaneContentArea.x; } else if (newNodeLayout.x + newNodeLayout.width > swimLaneContentArea.width) { newX -= newNodeLayout.x + newNodeLayout.width - swimLaneContentArea.width; } if (newNodeLayout.y < swimLaneContentArea.y) { newY = swimLaneContentArea.y; } else if (newNodeLayout.y + newNodeLayout.height > swimLaneContentArea.height) { newY -= newNodeLayout.y + newNodeLayout.height - swimLaneContentArea.height; } // create node var bpmnStyle:BPMNNodeStyle = draggedNode.style as BPMNNodeStyle; if (bpmnStyle != null) { return createNode(BPMNElements.createBPElement(bpmnStyle.shapeNodeShape), YRectangle.create(newX, newY, newNodeLayout.width, newNodeLayout.height), bpmnStyle); } } return null; } /** * Initializes the default styles and layout related configurations. */ protected function initDefaultStyles():void { graph.defaultNodeStyle = new BPMNNodeStyle(BPMNElements.TASK); graph.defaultEdgeStyle = new BPMNEdgeStyle(BPRelation.SEQUENCE_FLOW); var labelStyle:SimpleLabelStyle = new SimpleLabelStyle(); labelStyle.textFormat.font = "dialog"; graph.defaultNodeLabelStyle = labelStyle; graph.defaultEdgeLabelStyle = labelStyle; graph.defaultNodeLabelModelParameter = InteriorStretchLabelModel.center; var edgeLabelModel:RotatedSliderEdgeLabelModel = new RotatedSliderEdgeLabelModel(-10,0,true,false); graph.defaultEdgeLabelModelParameter = edgeLabelModel.createDefaultParameter(); /* Set the default style for group nodes */ var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; if (gg != null) { gg.defaultGroupNodeStyle = new TemplateNodeStyle(SwimLaneTemplate); } } /** * Initializes the graph by adding a pool node containing a swim lane and fits the Canvas to the new graph bounds. */ protected function initGraph():void { addPool("Pool 0", 0); fitGraphBounds(); } /** * Sets graph.defaultNodeLabelModelParameter according to the type of the selected node's tag. * This is done because if a new Label is created for the node via F2 this label will be * positioned correctly concerning the nodes IBPElement type. * @param evt */ protected function onNodeSelected(evt:SelectionEvent):void { var node:INode = evt.item as INode; if (node != null) { var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; if (tagOwner != null) { var bpElement:IBPElement = tagOwner.tag as IBPElement; if (bpElement != null) { if (bpElement.type == BPMNElements.ARTIFACT_TEXT_ANNOTATION) { graph.defaultNodeLabelModelParameter = InteriorLabelModel.center; } else if (bpElement is BPGateway || bpElement is BPEvent) { graph.defaultNodeLabelModelParameter = ExteriorLabelModel.south; } else if (bpElement is BPActivity || bpElement.type == BPMNElements.ARTIFACT_DATA_OBJECT) { var islm:InteriorStretchLabelModel = new InteriorStretchLabelModel(); islm.insets = new YRectangle(5, 5, 0, 0); graph.defaultNodeLabelModelParameter = islm.createParameter(InteriorStretchLabelModel.POSITION_CENTER); } } } } } /** * Deselects a newly selected node if its tag is of type BPSwimlane or BPPool. * @param evt */ protected function selectionFilter(evt:SelectionEvent):void { var node:INode = evt.item as INode; if (node != null) { var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; if (tagOwner != null && (tagOwner.tag is BPSwimlane || tagOwner.tag is BPPool)) { selection.setNodeSelected(node, false); } } } /* * Update the label's preferred size to compensate different font sizes when zooming. * Update the node's size if its tag is a BPArtifact with type == BPMNElements.ARTIFACT_TEXT_ANNOTATION * to fit the label. */ protected function onLabelChanged(evt:GraphEvent):void { if (evt.kind == GraphEventKind.LABEL_CHANGED || evt.kind == GraphEventKind.LABEL_ADDED) { var label:ILabel = evt.item as ILabel; var prefSize:ISize = label.preferredSize as ISize; var prefRendererSize:ISize = ILabelStyleRenderer(label.style.styleRenderer).getPreferredSize(label, label.style); var newWidth:uint = prefRendererSize.width * 1.06 + 5; var newHeight:uint = prefRendererSize.height * 1.06 + 5; if (prefSize.width != newWidth || prefSize.height != newHeight) { graph.setPreferredSize(label, newWidth, newHeight); } var node:INode = label.owner as INode; if (node != null) { var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; if (tagOwner != null) { var bpElement:IBPElement = tagOwner.tag as IBPElement; if (bpElement is BPArtifact && bpElement.type == BPMNElements.ARTIFACT_TEXT_ANNOTATION) { newWidth = (newWidth < 100) ? 100 : newWidth; newHeight = (newHeight < 25) ? 25 : newHeight; if (node.layout.width != newWidth || node.layout.height != newHeight) { graph.setBounds(node, node.layout.x, node.layout.y, newWidth, newHeight); } } } } } } /* * Configure the roundtrips and updates the list of stored graphs. */ public function configure():void { // use a custom fault event handler for service requests. RoundtripHandler.globalFaultEventHandler = faultHandler; SymbolicPackageNameRegistry.add("demo.businessProcessDiagram.bpd", "http://yworks.com/yfiles-flex/bpddemo/bpd"); } protected function createSaveHandler():RoundtripHandler { var saveHandler:RoundtripHandler = new RoundtripHandler(this); saveHandler.sendOptions = GraphCanvasComponent.OPTION_COPY_ALL; saveHandler.updateOptions = GraphCanvasComponent.OPTION_COPY_NONE; configureBaseHandler(saveHandler); return saveHandler; } protected function createLoadHandler():RoundtripHandler { var loadHandler:RoundtripHandler = new RoundtripHandler(this); loadHandler.sendOptions = GraphCanvasComponent.OPTION_COPY_NONE; loadHandler.updateOptions = GraphCanvasComponent.OPTION_COPY_ALL; configureBaseHandler(loadHandler); loadHandler.addEventListener(RoundtripEvent.ROUNDTRIP_COMPLETE, onLoadBusinessGraphComplete); loadHandler.animate = false; return loadHandler; } protected function configureBaseHandler(handler:RoundtripHandler):void { handler.autoReadMapperData = false; handler.autoWriteMapperData = false; handler.addMapperAttribute(BusinessProcessDiagramComponent.POOL_POSITION_KEY, BusinessProcessDiagramComponent.POOL_POSITION_KEY, GraphMLConstants.SCOPE_NODE, GraphMLConstants.TYPE_INT); handler.addMapperAttribute(BusinessProcessDiagramComponent.SWIM_LANE_POSITION_KEY, BusinessProcessDiagramComponent.SWIM_LANE_POSITION_KEY, GraphMLConstants.SCOPE_NODE, GraphMLConstants.TYPE_INT); handler.addSerializer(new BPSerializer()); handler.addDeserializer(new BPDeserializer()); handler.addSerializer(new FillSerializer()); handler.addDeserializer(new FillDeserializer()); handler.inputIOHandler.supportUserTags = true; handler.outputIOHandler.supportUserTags = true; handler.outputIOHandler.writeLabels = true; handler.inputIOHandler.readLabels = true; // The following lines can be inserted to trace the graphML returned from the server for debugging reasons // handler.addEventListener(Event.COMPLETE, function(ev:XMLResultEvent):void { // trace("GraphML sent by the server:\n" + ev.xmlResult); // }); } /** * Custom fault handler for service requests */ protected function faultHandler(evt:FaultEvent):void { var buf:String = ""; var titleString:String = "Service request failed"; var fault:Fault = evt.fault; var ioErrorEvent:IOErrorEvent = fault.rootCause as IOErrorEvent; if (null != ioErrorEvent) { buf += "Please make sure that the server is up and running and that the URL " + "specified in the error details below is correct.\n" + "The base URL that is used for resolving service requests can be changed in the " + "config.xml file.\n\n"; } buf += "Error details: \n\n"; buf += fault.faultDetail != null ? fault.faultDetail : fault.faultString; Alert.show(buf, titleString); CursorManager.removeBusyCursor(); } /** * Run a layout roundtrip */ public function doLayout():void { var layouter:BPMNLayouter = new BPMNLayouter(poolToPoolDistance, minimalSwimLaneHeight, minimalSwimLaneWidth); graph.mapperRegistry.addMapper(BPMN_DATA_KEY, new TagOwnerMapper()); var executor:LayoutExecutor = new LayoutExecutor(this, layouter, null, 500, function(evt:Event):void {setPoolsDirty()}, true); try { executor.start(); } finally { graph.mapperRegistry.removeMapper(BPMN_DATA_KEY); } } /** * Saves the graph on the server under the given name. * @param graphName */ public function saveBusinessGraph(graphName:String):void { var graphML:XML = saveBusinessGraphToXML(); addGraphToLoadableGraphs(graphName, graphML); } private function addGraphToLoadableGraphs(graphName:String, graphML:XML):void { var testName:String = graphName; // if one of the initial graphs has the same graph name, postfix the new graph name with ' (2)' for (var i:int = 0; i < loadableGraphs.length; i++) { var listItem:Object = loadableGraphs.getItemAt(i); if (listItem.path != null && listItem.label == testName) { // it's one of the initial graphs that shall not be overridden testName = testName + " (2)"; } } // if there is already a graph stored by this name, replace it for (var j:int = 0; j < loadableGraphs.length; j++) { var listItem2:Object = loadableGraphs.getItemAt(j); if (listItem2.label == testName) { // replace old graph with new one listItem2.graphML = graphML; return; } } // otherwise add a new graph entry loadableGraphs.addItem({label:testName, graphML:graphML}); } /** * Saves the graph to a XML that is returned. */ public function saveBusinessGraphToXML():XML { return createSaveHandler().outputIOHandler.write(graph); } /** * Loads the graph with the given name either from memory or from an URL. * The entry in the loadableGraphs having the passed name is looked up. * If its path property isn't null this path is used as URL to load the graph. * Otherwise if the graphML property isn't null this GraphML file is parsed as graph. * @param name The name of the graph in the loadableGraphs collection. */ public function loadBusinessGraph(name:String):void { DictionaryMapper(graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.POOL_POSITION_KEY)).clear(); DictionaryMapper(graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.SWIM_LANE_POSITION_KEY)).clear(); for (var i:int = 0; i < loadableGraphs.length; i++) { var listItem:Object = loadableGraphs.getItemAt(i); if (listItem.label == name) { if (listItem.path != null) { loadGraphFromURL(listItem.path, createLoadHandler().inputIOHandler, graph, onLoadBusinessGraphFromFileComplete); } else if (listItem.graphML != null) { loadBusinessGraphFromXML(listItem.graphML); } return; } } } /** * Loads the graph from the given XML. The XML is assumed to represent a GraphML graph which has all * data elements attached that would also be written by the saveBusinessGraphToXML method for this graph. * @param xml The graphML graph to load. */ public function loadBusinessGraphFromXML(xml:XML):void { createLoadHandler().handleResult(xml); } protected function onLoadBusinessGraphComplete(evt:RoundtripEvent):void { updateLoadedGraph() } protected function onLoadBusinessGraphFromFileComplete(evt:GraphEvent):void { updateLoadedGraph(); } /** * Updates the pool and swim lane maps. */ protected function updateLoadedGraph():void { setPoolsDirty(); this._poolMapper.clear(); this._swimLaneMapper.clear(); this.selection.clear(); var nodeIt:Iterator = graph.nodes.iterator(); while (nodeIt.hasNext()) { var node:INode = nodeIt.next() as INode; var tagOwner:ITagOwner = node.lookup(ITagOwner) as ITagOwner; if (tagOwner != null && tagOwner.tag != null) { if (tagOwner.tag is BPSwimlane) { this._swimLaneMapper.mapValue(tagOwner.tag, node); } else if (tagOwner.tag is BPPool) { this._poolMapper.mapValue(tagOwner.tag, node); } } } fixNewLinesInLabels(graph); fitGraphBounds(ImmutableRectangle.create(30,30,30,30)); } /* * Due to a flex bug, '\r\n' are interpreted as two new lines in a label. Call this function * to change '\r\n' into '\n' * @return */ private function fixNewLinesInLabels(graph:IGraph):void { var brokenPattern:RegExp = /\r\n/g; var repairedString:String = "\n"; var it:Iterator = graph.nodeLabels.iterator(); while (it.hasNext()) { var label:ILabel = it.next() as ILabel; var oldPreferredSize:ISize = label.preferredSize; var labelText:String = label.text; labelText = labelText.replace(brokenPattern, repairedString); graph.setLabelText(label, labelText); graph.setPreferredSize(label, oldPreferredSize.width, oldPreferredSize.height); } } /** * Creates a new node and connects it to the swim lane below this position. If there is no swim lane below the * given position, no node is created. * @param bpElement The IBPElement this node shall represent. * @param bounds The bounds of the new node. * @param style A BPMNNodeStyle can be passed to customize the look of the new node. The values * of style.shapeNodeShape and bpElement.type has to match if a style is passed. The width and height of the * style are ignored as those of the given bounds are used. * @return The newly created node. */ public function createNode(bpElement:IBPElement, bounds:IRectangle, style:BPMNNodeStyle = null):INode { if (bpElement == null || (bpElement != null && style != null && bpElement.type != style.shapeNodeShape)) { return null; } if (bpElement.url == null) { bpElement.url = "http://www.yworks.com"; } var swimLaneNode:INode = getSwimLaneNodeAtCursor(bounds.x, bounds.y); var newNode:INode = graph.createNode(bounds, style); var tagOwner:ITagOwner = newNode.lookup(ITagOwner) as ITagOwner; if (tagOwner != null) { tagOwner.tag = bpElement; } var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; if (swimLaneNode != null && gg != null) { gg.setParent(newNode, swimLaneNode); } // create label for Text Annotation if (bpElement.type == BPMNElements.ARTIFACT_TEXT_ANNOTATION) { var lmp:ILabelModelParameter = InteriorLabelModel.center; //InteriorStretchLabelModel.center; graph.addLabel(newNode, "Annotation", lmp, graph.defaultNodeLabelStyle); } return newNode; } /** * Returns the INode of the swim lane or pool at the given view coordinates * if there is any or null otherwise. * @param x horizontal coordinate * @param y vertical coordinate * @return Returns the INode of the swim lane or pool at the given view coordinates * if there is any or null otherwise. */ public function getSwimLaneNodeAtCursor(x:Number, y:Number):INode { var swimLaneNode:INode = null; var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; if (gg == null) return null; var poolIt:Iterator = pools.iterator(); endLoops: while (poolIt.hasNext()) { var pool:INode = poolIt.next() as INode; var swimLaneIt:Iterator = gg.hierarchy.getChildren(pool).iterator(); while (swimLaneIt.hasNext()) { var swimLane:INode = swimLaneIt.next() as INode; var ht:IHitTestable = swimLane.lookup(IHitTestable) as IHitTestable; var tagOwner:ITagOwner = swimLane.lookup(ITagOwner) as ITagOwner; if (null != ht && tagOwner != null) { if (ht.isHit(x, y, this.context) && tagOwner.tag is BPSwimlane) { swimLaneNode = swimLane; break endLoops; } } } } return swimLaneNode; } /** * Adds a new pool node to the graph and returns its corresponding BPPool object. A swim lane node * is added to the new pool as no empty pool may exist. * @param name The name of the pool. * @param position A value between 0 and the number of existing pools. If the given position is outside this * range, no node is created and null is returned. Otherwise all pools with a position greater than or * equal to the given one are moved downwards and the new pool is inserted at this position. * @return The BPPool object corresponding to the new pool or null if no pool has been added. */ public function addPool(name:String, position:uint):BPPool { if (position > this.pools.length()) return null; var pool:INode = createPoolNode(position); var bpPool:BPPool = new BPPool(); bpPool.name = name; this._poolMapper.mapValue(bpPool, pool); updatePoolPositionMapper(position, 1); graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.POOL_POSITION_KEY).mapValue(pool, position); setPoolsDirty(); // as the position mapper has been modified, the property pools is no longer in sync with it. poolBoundsUpdater.updatePoolsVertical(); var tagOwner:ITagOwner = pool.lookup(ITagOwner) as ITagOwner; if (tagOwner != null) { tagOwner.tag = bpPool; } addSwimLane(bpPool, "Swim Lane 0", 0); // there may be no empty pool so add a new swim lane return bpPool; } /* * Updates the mapper holding the positions of the pools. All pools with a position greater than or equal to * fromPosition are increased by diff. */ private function updatePoolPositionMapper(fromPosition:uint, diff:int):void { var poolPositions:DictionaryMapper = graph.mapperRegistry.getMapper(POOL_POSITION_KEY) as DictionaryMapper; var poolsIt:Iterator = poolPositions.keys(); while (poolsIt.hasNext()) { var pool:Object = poolsIt.next(); var poolPositionObject:Object = poolPositions.lookupValue(pool); if (poolPositionObject != null) { var poolPos:int = int(poolPositionObject); if (poolPos >= fromPosition) { poolPositions.mapValue(pool, poolPos + diff); } } } } /* * Called by addPool to create a node to visualize a new pool. */ private function createPoolNode(position:uint):INode { var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; var node:INode; if (gg != null) { var x:Number; var y:Number; var width:Number; var height:Number = minimalSwimLaneHeight; if (this.pools.length() == 0) { // this should only occur on initialization as after that there always has to be at least one pool x = 0; y = 0; width = minimalSwimLaneWidth; } else { var pool0:INode = this.pools.getItemAt(0) as INode; x = pool0.layout.x; width = pool0.layout.width; if (this.pools.length() > position) { y = (this.pools.getItemAt(position) as INode).layout.y; } else { var poolOnTop:INode = this.pools.getItemAt(position - 1) as INode; y = poolOnTop.layout.y + poolOnTop.layout.height + poolToPoolDistance; } } var poolNodeStyle:TemplateNodeStyle = new TemplateNodeStyle(PoolTemplate); node = gg.createGroupNode(null, new ImmutableRectangle(x, y, width, height), poolNodeStyle); } return node; } /** * Adds a new swim lane node to the graph and returns it's corresponding BPSwimlane object. The node * corresponding to the given BPPool is set as parent node of the new swim lane node. If no * INode can be found that has the BPPool as its tag, no swim lane is created and null is * returned. * @param pool The BPPool which this swim lane should be assigned to. * @param name The name of new swim lane. * @param position A value between 0 and the number of existing swim lanes in the given pool. If the given position * is outside this range, no node is created and null is returned. Otherwise all swim lanes with a * position greater than or equal to the given one are moved downwards and the new swim lane is inserted at this position. * @return The BPSwimlane object corresponding to the new swim lane or null if no swim lane has been added. */ public function addSwimLane(pool:BPPool, name:String, position:uint):BPSwimlane { var poolNode:INode = getPoolNode(pool); var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; if (poolNode == null || gg == null) return null; var inip:INodeInsetsProvider = poolNode.lookup(INodeInsetsProvider) as INodeInsetsProvider; var poolInsets:IRectangle; if (inip == null) { poolInsets = new YRectangle(0, 0, 0, 0); } else { poolInsets = inip.getInsets(poolNode) as IRectangle; } if (gg.hierarchy.getChildCount(poolNode) < position) { return null; } var y:Number; var swimLanes:Array = getSwimLanesOfPool(pool); if (swimLanes == null) { return null; } else { if (position >= swimLanes.length) { // take bottom of last swim lane var relSwimLane:INode = _swimLaneMapper.lookupValue(swimLanes[swimLanes.length - 1]) as INode; if (relSwimLane != null) { y = relSwimLane.layout.y + relSwimLane.layout.height; } else { y = poolNode.layout.y + poolInsets.y; } } else { relSwimLane = _swimLaneMapper.lookupValue(swimLanes[position]) as INode; if (relSwimLane != null) { y = relSwimLane.layout.y; } } } var width:Number = poolNode.layout.width - poolInsets.x - poolInsets.width; var bounds:ImmutableRectangle = new ImmutableRectangle(poolNode.layout.x + poolInsets.x, y, width, minimalSwimLaneHeight); var swimLane:INode = createSwimLane(bounds, poolNode); // update swim lane position properties var absoluteSwimLanePosition:uint = getAbsoluteSwimLanePosition(poolNode, position); updateSwimLanePositionMapper(absoluteSwimLanePosition, 1); graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.SWIM_LANE_POSITION_KEY).mapValue(swimLane, absoluteSwimLanePosition); poolBoundsUpdater.updatePoolsVertical(); var bpSwimlane:BPSwimlane = new BPSwimlane(); bpSwimlane.name = name; var swimLaneTagOwner:ITagOwner = swimLane.lookup(ITagOwner) as ITagOwner; if (swimLaneTagOwner != null) { swimLaneTagOwner.tag = bpSwimlane; } this._swimLaneMapper.mapValue(bpSwimlane, swimLane); return bpSwimlane; } /* * Converts the relative position of a swim lane in a pool to the absolute position of this swim lane by adding * the number of swim lanes in pools on top of the given one. */ private function getAbsoluteSwimLanePosition(poolNode:INode, position:uint):uint { var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; var poolPositionMapper:DictionaryMapper = graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.POOL_POSITION_KEY) as DictionaryMapper; var absPosition:uint = 0; var poolPosition:int = int(poolPositionMapper.lookupValue(poolNode)); var poolIt:Iterator = pools.iterator(); while (poolIt.hasNext()) { var pool:INode = poolIt.next() as INode; if (int(poolPositionMapper.lookupValue(pool)) < poolPosition) { absPosition += gg.hierarchy.getChildCount(pool); } } return absPosition += position; } public function getSwimLanesOfPool(pool:BPPool):Array { var poolNode:INode = this._poolMapper.lookupValue(pool) as INode; var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; var spm:DictionaryMapper = graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.SWIM_LANE_POSITION_KEY) as DictionaryMapper; if (poolNode == null || gg == null) return null; var swimLaneNodes:Array = new Array(); var it:Iterator = gg.hierarchy.getChildren(poolNode).iterator(); while (it.hasNext()) { swimLaneNodes.push(it.next()); } swimLaneNodes.sort(function sortOnPosition(a:INode, b:INode):Number { var posA:Number = int(spm.lookupValue(a)); var posB:Number = int(spm.lookupValue(b)); return (posA - posB); }); var result:Array = new Array(); for (var i:int = 0; i < swimLaneNodes.length; i++) { var swimLaneNode:INode = swimLaneNodes[i] as INode; var tagOwner:ITagOwner = swimLaneNode.lookup(ITagOwner) as ITagOwner; if (tagOwner != null && tagOwner.tag is BPSwimlane) { result.push(tagOwner.tag); } } if (swimLaneNodes.length == result.length) { return result; } else { return null; } } /* * Updates the mapper holding the positions of the swim lanes. All swim lanes with a position greater than or equal to * fromPosition are increased by diff. */ private function updateSwimLanePositionMapper(fromPosition:uint, diff:int):void { var swimLanePositions:DictionaryMapper = graph.mapperRegistry.getMapper(SWIM_LANE_POSITION_KEY) as DictionaryMapper; var swimLaneIt:Iterator = swimLanePositions.keys(); while (swimLaneIt.hasNext()) { var swimLane:Object = swimLaneIt.next(); var swimLanePosObject:Object = swimLanePositions.lookupValue(swimLane); if (swimLanePosObject != null) { var swimLanePos:int = int(swimLanePosObject); if (swimLanePos >= fromPosition) { swimLanePositions.mapValue(swimLane, swimLanePos + diff); } } } } /* * Creates a new swim lane. Its parent pool and its new position are determined by the given SwimLaneEvent. * */ private function newSwimLaneButtonClicked(evt:SwimLaneEvent):void { var swimLane:INode = evt.swimLaneNode; if (swimLane != null) { var spMapper:DictionaryMapper = graph.mapperRegistry.getMapper(SWIM_LANE_POSITION_KEY) as DictionaryMapper; var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; if (gg != null && spMapper != null) { var posObject:Object = spMapper.lookupValue(swimLane); // get pool node of the swim lane of the given event. var pool:INode = gg.getParent(swimLane); if (pool != null) { var poolTagOwner:ITagOwner = pool.lookup(ITagOwner) as ITagOwner; if (poolTagOwner == null || !(poolTagOwner.tag is BPPool)) { pool = null; } } if (pool != null && posObject != null) { var absSwimLanePosition:uint = uint(posObject); var relPosition:int = absSwimLanePosition - getAbsoluteSwimLanePosition(pool, 0); if (relPosition < 0) return; // absolute swim lane position was not in this pool if (!evt.above) relPosition++; var numSwimLanes:uint = gg.hierarchy.getChildCount(pool); var poolOwner:ITagOwner = pool.lookup(ITagOwner) as ITagOwner; if (poolOwner != null) { var bpPool:BPPool = poolOwner.tag as BPPool; if (bpPool != null) { addSwimLane(bpPool, "Swim Lane " + numSwimLanes, relPosition); } } } } } } /* * Adds a new pool. The position of the new pool is determined by the given PoolEvent. */ private function newPoolButtonClicked(evt:PoolEvent):void { var pool:INode = evt.poolNode; if (pool != null) { var position:int = this.pools.indexOf(pool); if (!evt.above) position++; addPool("Pool " + this.pools.length(), position); } } /* * Calls removeSwimLane for the swim lane in the SwimLaneEvent. */ private function removeSwimLaneButtonClicked(evt:SwimLaneEvent):void { var swimLane:INode = evt.swimLaneNode; removeSwimLane(swimLane); } /** * Removes the given swim lane if it is empty and if it's not the only swim lane left in the diagram. If it is the * last swim lane in its pool but there are other pools left, its pool is removed as well. * @param swimLane The swim lane to remove. * @return Whether the given swim lane has been removed or not. */ public function removeSwimLane(swimLane:INode):Boolean { if (graph.nodes.length() <= 2) return false; // only the swim lane and its containing pool are left if (swimLane != null) { var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; if (gg != null) { if (gg.hierarchy.getChildCount(swimLane) == 0) { // swim lane is empty var pool:INode = gg.getParent(swimLane); // update position of bends that are located in this swim lane to the upper bound of this swim lane var edgesIt:Iterator = graph.edges.iterator(); while (edgesIt.hasNext()) { var edge:IEdge = edgesIt.next() as IEdge; var bends:IBendList = edge.bends; var bendIt:Iterator = bends.iterator(); while (bendIt.hasNext()) { var bend:IBend = bendIt.next() as IBend; if (bend.y >= swimLane.layout.y && bend.y <= swimLane.layout.y + swimLane.layout.height) { graph.setBendLocation(bend, bend.x, swimLane.layout.y); } } } deleteSwimLaneNode(swimLane); if (gg.hierarchy.getChildCount(pool) == 0) { // swim lane was the only one in this pool, so delete the pool as well deletePoolNode(pool); } poolBoundsUpdater.updatePoolsVertical(); return true; } } } return false; } /* * Calls removePool for the pool in the given PoolEvent. */ private function removePoolButtonClicked(evt:PoolEvent):void { var pool:INode = evt.poolNode; removePool(pool); } /** * Removes the given pool and its swim lanes if all swim lanes are empty and if it isn't the last pool left. * @param pool The pool node to remove. * @return Whether the pool has been removed or not. */ public function removePool(pool:INode):Boolean { if (this._pools.length() <= 1) return false; // it's the only pool left. var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; if (gg != null) { // check if any swim lane in this pool is not empty. var canBeDeleted:Boolean = true; var it:Iterator = gg.hierarchy.getChildren(pool).iterator(); while (it.hasNext()) { var swimLane:INode = it.next() as INode; if (gg.hierarchy.getChildCount(swimLane) > 0) { canBeDeleted = false; break; } } if (canBeDeleted) { // remove swim lanes while (gg.hierarchy.getChildCount(pool) > 0) { swimLane = gg.hierarchy.getChildren(pool).iterator().next() as INode; deleteSwimLaneNode(swimLane); } // update position of bends that are located in this pool to the upper bound of this pool minus the // minimal pool distance var edgesIt:Iterator = graph.edges.iterator(); while (edgesIt.hasNext()) { var edge:IEdge = edgesIt.next() as IEdge; var bends:IBendList = edge.bends; var bendIt:Iterator = bends.iterator(); while (bendIt.hasNext()) { var bend:IBend = bendIt.next() as IBend; if (bend.y >= pool.layout.y && bend.y <= pool.layout.y + pool.layout.height) { graph.setBendLocation(bend, bend.x, pool.layout.y - poolToPoolDistance); } } } // remove pool deletePoolNode(pool); poolBoundsUpdater.updatePoolsVertical(); return true; } } return false; } /* * Deletes the given swim lane node from the graph and updates the positions of the other swim lanes. */ private function deleteSwimLaneNode(swimLane:INode):void { var tagOwner:ITagOwner = swimLane.lookup(ITagOwner) as ITagOwner; if (tagOwner.tag != null && tagOwner.tag is BPSwimlane) { this._swimLaneMapper.unMapValue(tagOwner.tag); } var swimLanePositionMapper:DictionaryMapper = graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.SWIM_LANE_POSITION_KEY) as DictionaryMapper; var position:uint = uint(swimLanePositionMapper.lookupValue(swimLane)); swimLanePositionMapper.unMapValue(swimLane); updateSwimLanePositionMapper(position, -1); graph.removeNode(swimLane); } /* * Deletes the given pool node from the graph and updates the positions of the other swim lanes. */ private function deletePoolNode(pool:INode):void { var tagOwner:ITagOwner = pool.lookup(ITagOwner) as ITagOwner; if (tagOwner != null && tagOwner.tag is BPPool) { this._poolMapper.unMapValue(tagOwner.tag); } var poolPositionMapper:DictionaryMapper = graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.POOL_POSITION_KEY) as DictionaryMapper; var position:uint = uint(poolPositionMapper.lookupValue(pool)); poolPositionMapper.unMapValue(pool); updatePoolPositionMapper(position, -1); graph.removeNode(pool); setPoolsDirty(); } /* * Creates a swim lane node by the grouped graph with the given bounds and sets the given parent node. */ private function createSwimLane(bounds:IRectangle, parent:INode):INode { var gg:IGroupedGraph = graph.lookup(IGroupedGraph) as IGroupedGraph; var node:INode; if (gg != null) { node = gg.createGroupNode(parent, bounds, new TemplateNodeStyle(SwimLaneTemplate)); } return node; } /** * Returns the INode corresponding to the given BPPool. * @param pool The corresponding BPPool * @return The INode for this BPPool */ public function getPoolNode(pool:BPPool):INode { return this._poolMapper.lookupValue(pool) as INode; } /** * Returns the BPPool corresponding to the pool node at the given position in the * list of the property pools. * @param position * @return The BPPool at the specified position. */ public function getPoolAtPosition(position:uint):BPPool { if (position >= this.pools.length()) return null; var poolNode:INode = this.pools.getItemAt(position) as INode; if (poolNode != null) { var tagOwner:ITagOwner = poolNode.lookup(ITagOwner) as ITagOwner; if (tagOwner != null) { return tagOwner.tag as BPPool; } } return null; } /* **************************** GETTER AND SETTER *****************************/ /** * The vertical distance between two adjacent pools. */ public function get poolToPoolDistance():uint { return _poolToPoolDistance; } public function set poolToPoolDistance(val:uint):void { _poolToPoolDistance = val; } /** * The minimal height a pool (or swim lane) node may have. */ public function get minimalSwimLaneHeight():uint { return _minimalSwimLaneHeight } public function set minimalSwimLaneHeight(val:uint):void { _minimalSwimLaneHeight = val; } /** * The minimal width a pool node may have. */ public function get minimalSwimLaneWidth():uint { return _minimalSwimLaneWidth; } public function set minimalSwimLaneWidth(val:uint):void { _minimalSwimLaneWidth = val; } /** * The PoolBoundsUpdater used for this BusinessProcessDiagramComponent. */ public function get poolBoundsUpdater():PoolBoundsUpdater { return _poolBoundsUpdater; } public function set poolBoundsUpdater(val:PoolBoundsUpdater):void { _poolBoundsUpdater = val; } [Bindable] /** * An ArrayCollection containing the file names of graphs stored on * the server. */ public function get loadableGraphs():ArrayCollection { return this._loadableGraphList; } public function set loadableGraphs(ac:ArrayCollection):void { this._loadableGraphList = ac; } /** * An ArrayList of the pool nodes sorted by their position from top to bottom. */ public function get pools():ArrayList { if (this._poolsDirty) { var poolPosMapper:DictionaryMapper = graph.mapperRegistry.getMapper(BusinessProcessDiagramComponent.POOL_POSITION_KEY) as DictionaryMapper; if (poolPosMapper != null) { this._pools.clear(); var it:Iterator = poolPosMapper.keys(); while (it.hasNext()) { var pool:Object = it.next(); if (poolPosMapper.lookupValue(pool) != null) { this._pools.addItem(pool); } } this._pools.sort(function(pool1:Object, pool2:Object):int { return int(poolPosMapper.lookupValue(pool1)) - int(poolPosMapper.lookupValue(pool2)); }); } this._poolsDirty = false; } return this._pools; } /** * Triggers the pools property to be updated according to the pool position mapper on its next access. */ public function setPoolsDirty():void { this._poolsDirty = true; } } } import com.yworks.canvas.ICanvasContext; import com.yworks.canvas.drawing.IHitTestable; import mx.core.mx_internal import demo.businessProcessDiagram.bpd.BusinessProcessDiagramComponent; use namespace mx_internal; /** * This hit testable is used when dragging elements from the palette onto the canvas to decide if the current mouse * location is a valid drop location. * This is the case if the mouse hovers over a swim lane. */ class ValidNodeDropHitTestable implements IHitTestable { private var canvas:BusinessProcessDiagramComponent; public function ValidNodeDropHitTestable(canvas:BusinessProcessDiagramComponent) { this.canvas = canvas; } public function isHit(x:Number, y:Number, ctx:ICanvasContext):Boolean { return canvas.getSwimLaneNodeAtCursor(x, y) != null || !(mx.core.Singleton.getInstance("mx.managers::IDragManager") is mx.managers.DragManagerImpl); } }