/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.activiti.kickstart.diagram;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.activiti.engine.impl.bpmn.diagram.ProcessDiagramCanvas;
import org.activiti.kickstart.bpmn20.model.BaseElement;
import org.activiti.kickstart.bpmn20.model.Definitions;
import org.activiti.kickstart.bpmn20.model.FlowElement;
import org.activiti.kickstart.bpmn20.model.Process;
import org.activiti.kickstart.bpmn20.model.activity.Task;
import org.activiti.kickstart.bpmn20.model.activity.type.ScriptTask;
import org.activiti.kickstart.bpmn20.model.activity.type.ServiceTask;
import org.activiti.kickstart.bpmn20.model.activity.type.UserTask;
import org.activiti.kickstart.bpmn20.model.bpmndi.BPMNEdge;
import org.activiti.kickstart.bpmn20.model.bpmndi.BPMNPlane;
import org.activiti.kickstart.bpmn20.model.bpmndi.BPMNShape;
import org.activiti.kickstart.bpmn20.model.bpmndi.dc.Bounds;
import org.activiti.kickstart.bpmn20.model.bpmndi.dc.Point;
import org.activiti.kickstart.bpmn20.model.connector.SequenceFlow;
import org.activiti.kickstart.bpmn20.model.event.EndEvent;
import org.activiti.kickstart.bpmn20.model.event.StartEvent;
import org.activiti.kickstart.bpmn20.model.gateway.ParallelGateway;
import org.activiti.kickstart.dto.KickstartWorkflow;
import org.activiti.kickstart.dto.KickstartTaskBlock;
import org.activiti.kickstart.service.Bpmn20MarshallingService;
/**
* @author Joram Barrez
*/
public class ProcessDiagramGenerator {
// Constants
protected static final int SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH = 45;
protected static final int ARROW_WIDTH = 5;
protected static final int SEQUENCE_FLOW_WIDTH = SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH + ARROW_WIDTH;
protected static final int LONG_SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH = SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH * 2;
protected static final int LONG_SEQUENCE_FLOW_WIDTH = LONG_SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH + ARROW_WIDTH;
protected static final int TASK_WIDTH = 130;
protected static final int TASK_HEIGHT = 60;
protected static final int TASK_HEIGHT_SPACING = 10;
protected static final int EVENT_WIDTH = 20;
protected static final int GATEWAY_WIDTH = 40;
protected static final int GATEWAY_HEIGHT = 40;
protected int TASK_BLOCK_WIDTH = GATEWAY_WIDTH + TASK_WIDTH + SEQUENCE_FLOW_WIDTH + LONG_SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH;
// Instance members
protected Bpmn20MarshallingService marshallingService;
protected KickstartWorkflow kickstartWorkflow;
// Will be set during image generation
protected int startX;
protected int startY;
protected int currentWidth;
protected ProcessDiagramCanvas processDiagramCanvas;
protected BPMNPlane plane;
protected Map<String, List<SequenceFlow>> outgoingSequenceFlowMapping;
protected Map<String, List<SequenceFlow>> incomingSequenceFlowMapping;
protected Set<String> handledElements;
public ProcessDiagramGenerator(KickstartWorkflow kickstartWorkflow, Bpmn20MarshallingService marshallingService) {
this.kickstartWorkflow = kickstartWorkflow;
this.marshallingService = marshallingService;
}
public Bpmn20MarshallingService getMarshallingService() {
return marshallingService;
}
public void setMarshallingService(Bpmn20MarshallingService marshallingService) {
this.marshallingService = marshallingService;
}
public InputStream execute() {
this.startX = 0;
this.startY = calculateMaximumHeight() / 2 + 10;
this.currentWidth = 0;
int width = calculateMaximumWidth() + 50;
int height = calculateMaximumHeight() + 50;
processDiagramCanvas = new ProcessDiagramCanvas(width, height);
Definitions definitions = marshallingService.convertToBpmn(kickstartWorkflow);
Process process = getProcess(definitions);
this.plane = getPlane(definitions);
List<FlowElement> flowElements = process.getFlowElement();
generateSequenceflowMappings(flowElements);
this.handledElements = new HashSet<String>();
for (FlowElement flowElement : flowElements) {
if (!handledElements.contains(flowElement.getId())) {
if (flowElement instanceof StartEvent) {
drawStartEvent(flowElement, startX, startY, EVENT_WIDTH, EVENT_WIDTH);
} else if (flowElement instanceof EndEvent) {
drawSequenceFlow(incomingSequenceFlowMapping.get(flowElement.getId()).get(0),
currentWidth, startY + EVENT_WIDTH / 2, currentWidth
+ SEQUENCE_FLOW_WIDTH, startY + EVENT_WIDTH / 2);
drawEndEvent(flowElement, currentWidth, startY, EVENT_WIDTH, EVENT_WIDTH);
} else if (flowElement instanceof ParallelGateway
&& outgoingSequenceFlowMapping.get(flowElement.getId()).size() > 1) { // fork
ParallelGateway parallelGateway = (ParallelGateway) flowElement;
drawSequenceFlow(incomingSequenceFlowMapping.get(flowElement.getId()).get(0),
currentWidth, startY + EVENT_WIDTH / 2, currentWidth
+ SEQUENCE_FLOW_WIDTH, startY + EVENT_WIDTH / 2);
drawParallelBlock(currentWidth, startY - EVENT_WIDTH / 2, parallelGateway);
} else if (flowElement instanceof Task) {
drawSequenceFlow(incomingSequenceFlowMapping.get(flowElement.getId()).get(0),
currentWidth, startY + EVENT_WIDTH / 2, currentWidth
+ SEQUENCE_FLOW_WIDTH, startY + EVENT_WIDTH / 2);
drawTask(flowElement, currentWidth, startY - ((TASK_HEIGHT - EVENT_WIDTH) / 2),
TASK_WIDTH, TASK_HEIGHT);
}
}
}
return processDiagramCanvas.generateImage("png");
}
protected Process getProcess(Definitions definitions) {
for (BaseElement rootElement : definitions.getRootElement()) {
if (rootElement instanceof Process) {
return (Process) rootElement;
}
}
return null;
}
protected BPMNPlane getPlane(Definitions definitions) {
return definitions.getDiagram().get(0).getBPMNPlane();
}
protected void generateSequenceflowMappings(List<FlowElement> flowElements) {
this.outgoingSequenceFlowMapping = new HashMap<String, List<SequenceFlow>>();
this.incomingSequenceFlowMapping = new HashMap<String, List<SequenceFlow>>();
for (FlowElement flowElement : flowElements) {
if (flowElement instanceof SequenceFlow) {
SequenceFlow sequenceFlow = (SequenceFlow) flowElement;
String srcId = sequenceFlow.getSourceRef().getId();
String targetId = sequenceFlow.getTargetRef().getId();
if (outgoingSequenceFlowMapping.get(srcId) == null) {
outgoingSequenceFlowMapping.put(srcId, new ArrayList<SequenceFlow>());
}
outgoingSequenceFlowMapping.get(srcId).add(sequenceFlow);
if (incomingSequenceFlowMapping.get(targetId) == null) {
incomingSequenceFlowMapping.put(targetId, new ArrayList<SequenceFlow>());
}
incomingSequenceFlowMapping.get(targetId).add(sequenceFlow);
}
}
}
protected int calculateMaximumWidth() {
int width = 0;
for (KickstartTaskBlock taskBlock : kickstartWorkflow.getTaskBlocks()) {
if (taskBlock.getNrOfTasks() == 1) {
width += TASK_WIDTH + SEQUENCE_FLOW_WIDTH;
} else {
width += TASK_BLOCK_WIDTH + SEQUENCE_FLOW_WIDTH;
}
}
width += SEQUENCE_FLOW_WIDTH + 2 * EVENT_WIDTH;
return width;
}
protected int calculateMaximumHeight() {
int maxNrOfTasksInOneBlock = 0;
for (KickstartTaskBlock taskBlock : kickstartWorkflow.getTaskBlocks()) {
if (taskBlock.getNrOfTasks() > maxNrOfTasksInOneBlock) {
maxNrOfTasksInOneBlock = taskBlock.getNrOfTasks();
}
}
int extra = 0;
if (maxNrOfTasksInOneBlock % 2 == 0) { // If there is an even nr of tasks -> evenly spread, but no task in the middle
extra = 2 * TASK_HEIGHT;
}
return (maxNrOfTasksInOneBlock * (TASK_HEIGHT + TASK_HEIGHT_SPACING)) + extra;
}
protected void drawParallelBlock(int x, int y, ParallelGateway parallelGateway) {
int originalCurrentWidth = currentWidth;
List<SequenceFlow> sequenceFlows = outgoingSequenceFlowMapping.get(parallelGateway.getId());
int nrOfTasks = sequenceFlows.size();
// First parallel gateway
drawParallelGateway(parallelGateway, x, y, GATEWAY_WIDTH, GATEWAY_HEIGHT);
handledElements.add(parallelGateway.getId());
// Sequence flow up and down
int centerOfRhombus = x + GATEWAY_WIDTH / 2;
int maxHeight = (nrOfTasks / 2) * (TASK_HEIGHT + TASK_HEIGHT_SPACING);
int currentHeight = y - maxHeight;
// first half
for (int i = 0; i < nrOfTasks / 2; i++) {
SequenceFlow sequenceFlow1 = sequenceFlows.get(i);
drawSequenceFlow(sequenceFlow1, centerOfRhombus, y, centerOfRhombus, currentHeight,
centerOfRhombus + SEQUENCE_FLOW_WIDTH, currentHeight);
FlowElement userTask = sequenceFlow1.getTargetRef();
drawTask(userTask, centerOfRhombus + SEQUENCE_FLOW_WIDTH,
currentHeight - ((TASK_HEIGHT + TASK_HEIGHT_SPACING) / 2), TASK_WIDTH, TASK_HEIGHT);
handledElements.add(sequenceFlow1.getTargetRef().getId());
int seqFlowX = centerOfRhombus + SEQUENCE_FLOW_WIDTH + TASK_WIDTH;
SequenceFlow sequenceFlow2 = outgoingSequenceFlowMapping.get(userTask.getId()).get(0);
drawSequenceFlow(sequenceFlow2, seqFlowX, currentHeight,
seqFlowX + LONG_SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH, currentHeight,
seqFlowX + LONG_SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH, y);
currentHeight += TASK_HEIGHT + TASK_HEIGHT_SPACING;
}
// middle task
if (nrOfTasks % 2 != 0) {
SequenceFlow sequenceFlow1 = sequenceFlows.get(nrOfTasks / 2);
drawSequenceFlow(sequenceFlow1, centerOfRhombus + GATEWAY_WIDTH / 2,
startY + EVENT_WIDTH / 2, centerOfRhombus + SEQUENCE_FLOW_WIDTH,
startY + EVENT_WIDTH / 2);
FlowElement userTask = sequenceFlow1.getTargetRef();
drawTask(sequenceFlow1.getTargetRef(), centerOfRhombus + SEQUENCE_FLOW_WIDTH,
startY - ((TASK_HEIGHT - GATEWAY_HEIGHT)), TASK_WIDTH, TASK_HEIGHT);
handledElements.add(sequenceFlow1.getTargetRef().getId());
int seqflowX = centerOfRhombus + GATEWAY_WIDTH / 2 + (SEQUENCE_FLOW_WIDTH - GATEWAY_WIDTH / 2) + TASK_WIDTH;
SequenceFlow sequenceFlow2 = outgoingSequenceFlowMapping.get(userTask.getId()).get(0);
drawSequenceFlow(sequenceFlow2, seqflowX, startY + EVENT_WIDTH / 2,
seqflowX + LONG_SEQUENCE_FLOW_WIDTH - GATEWAY_WIDTH / 2 - ARROW_WIDTH, startY
+ EVENT_WIDTH / 2);
}
currentHeight = y + GATEWAY_HEIGHT + TASK_HEIGHT + TASK_HEIGHT_SPACING;
// second half
int startIndex = nrOfTasks % 2 == 0 ? nrOfTasks / 2 : (nrOfTasks / 2) + 1;
for (int i = startIndex; i < nrOfTasks; i++) {
SequenceFlow sequenceFlow1 = sequenceFlows.get(i);
drawSequenceFlow(sequenceFlow1, centerOfRhombus, y + GATEWAY_HEIGHT, centerOfRhombus,
currentHeight, centerOfRhombus + SEQUENCE_FLOW_WIDTH, currentHeight);
FlowElement userTask = sequenceFlow1.getTargetRef();
drawTask(sequenceFlow1.getTargetRef(), centerOfRhombus + SEQUENCE_FLOW_WIDTH,
currentHeight - ((TASK_HEIGHT + TASK_HEIGHT_SPACING) / 2), TASK_WIDTH,
TASK_HEIGHT);
int seqFlowX = centerOfRhombus + SEQUENCE_FLOW_WIDTH + TASK_WIDTH;
SequenceFlow sequenceFlow2 = outgoingSequenceFlowMapping.get(userTask.getId()).get(0);
drawSequenceFlow(sequenceFlow2, seqFlowX, currentHeight,
seqFlowX + LONG_SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH, currentHeight, seqFlowX
+ LONG_SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH, y + GATEWAY_HEIGHT);
handledElements.add(sequenceFlow1.getTargetRef().getId());
currentHeight += TASK_HEIGHT + TASK_HEIGHT_SPACING;
}
// Second parallel gateway
String someTaskId = sequenceFlows.get(0).getTargetRef().getId();
FlowElement join = outgoingSequenceFlowMapping.get(someTaskId).get(0).getTargetRef();
centerOfRhombus = centerOfRhombus + SEQUENCE_FLOW_WIDTH + TASK_WIDTH + LONG_SEQUENCE_FLOW_WITHOUT_ARROW_WIDTH;
drawParallelGateway(join, centerOfRhombus - GATEWAY_WIDTH / 2, y, GATEWAY_WIDTH, GATEWAY_HEIGHT);
handledElements.add(join.getId());
currentWidth = originalCurrentWidth + TASK_BLOCK_WIDTH;
}
protected void drawStartEvent(FlowElement flowElement, int x, int y, int width, int height) {
processDiagramCanvas.drawNoneStartEvent(x, y, width, height);
currentWidth += EVENT_WIDTH;
createDiagramInterchangeInformation(flowElement, x, y, width, height);
}
protected void drawEndEvent(FlowElement flowElement, int x, int y, int width, int height) {
processDiagramCanvas.drawNoneEndEvent(x, y, width, height);
currentWidth += EVENT_WIDTH;
createDiagramInterchangeInformation(flowElement, x, y, width, height);
}
protected void drawParallelGateway(FlowElement flowElement, int x, int y, int width, int height) {
processDiagramCanvas.drawParallelGateway(x, y, width, height);
currentWidth += GATEWAY_WIDTH;
createDiagramInterchangeInformation(flowElement, x, y, width, height);
}
protected void drawTask(FlowElement flowElement, int x, int y, int width,
int height) {
if (flowElement instanceof UserTask) {
processDiagramCanvas.drawUserTask(flowElement.getName(), x, y, width, height);
} else if (flowElement instanceof ServiceTask) {
processDiagramCanvas.drawServiceTask(flowElement.getName(), x, y, width, height);
} else if (flowElement instanceof ScriptTask) {
processDiagramCanvas.drawScriptTask(flowElement.getName(), x, y, width, height);
}
currentWidth += TASK_WIDTH;
createDiagramInterchangeInformation(flowElement, x, y, width, height);
}
protected void drawSequenceFlow(SequenceFlow sequenceFlow, int... waypoints) {
// Drawing
int minX = Integer.MAX_VALUE;
int maxX = 0;
for (int i = 2; i < waypoints.length; i += 2) { // waypoints.size()
// minimally 4: x1, y1, x2,
// y2
if (i < waypoints.length - 2) {
processDiagramCanvas.drawSequenceflowWithoutArrow(waypoints[i - 2],
waypoints[i - 1], waypoints[i], waypoints[i + 1], false);
} else {
processDiagramCanvas.drawSequenceflow(waypoints[i - 2],
waypoints[i - 1], waypoints[i], waypoints[i + 1], false);
}
if (waypoints[i - 2] < minX || waypoints[i] < minX) {
minX = Math.min(waypoints[i - 2], waypoints[i]);
}
if (waypoints[i - 2] > maxX || waypoints[i] > maxX) {
maxX = Math.max(waypoints[i - 2], waypoints[i]);
}
}
currentWidth += maxX - minX;
// DI information
BPMNEdge edge = new BPMNEdge();
edge.setId(sequenceFlow.getId() + "_edge");
edge.setBpmnElement(sequenceFlow);
for (int i = 0; i < waypoints.length; i += 2) {
edge.getWaypoint().add(new Point(waypoints[i], waypoints[i + 1]));
}
plane.getDiagramElement().add(edge);
}
protected void createDiagramInterchangeInformation(FlowElement flowElement,
int x, int y, int width, int height) {
BPMNShape shape = new BPMNShape();
shape.setId(flowElement.getId() + "_shape");
shape.setBpmnElement(flowElement);
Bounds bounds = new Bounds();
bounds.setX(x);
bounds.setY(y);
bounds.setWidth(width);
bounds.setHeight(height);
shape.setBounds(bounds);
plane.getDiagramElement().add(shape);
}
}