/* 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.engine.impl.bpmn.diagram; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Paint; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.font.LineBreakMeasurer; import java.awt.font.TextAttribute; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.RoundRectangle2D; import java.awt.geom.Path2D; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import javax.imageio.ImageIO; import org.activiti.engine.ActivitiException; import org.activiti.engine.impl.util.IoUtil; import org.activiti.engine.impl.util.ReflectUtil; /** * Represents a canvas on which BPMN 2.0 constructs can be drawn. * * Some of the icons used are licenced under a Creative Commons Attribution 2.5 * License, see http://www.famfamfam.com/lab/icons/silk/ * * @see ProcessDiagramGenerator * @author Joram Barrez */ public class ProcessDiagramCanvas { protected static final Logger LOGGER = Logger.getLogger(ProcessDiagramCanvas.class.getName()); // Predefined sized protected static final int ARROW_WIDTH = 5; protected static final int CONDITIONAL_INDICATOR_WIDTH = 16; protected static final int DEFAULT_INDICATOR_WIDTH = 10; protected static final int MARKER_WIDTH = 12; protected static final int FONT_SIZE = 11; protected static final int FONT_SPACING = 2; protected static final int TEXT_PADDING = 3; protected static final int LINE_HEIGHT = FONT_SIZE + FONT_SPACING; // Colors protected static Color TASK_BOX_COLOR = new Color(255, 255, 204); protected static Color BOUNDARY_EVENT_COLOR = new Color(255, 255, 255); protected static Color CONDITIONAL_INDICATOR_COLOR = new Color(255, 255, 255); protected static Color HIGHLIGHT_COLOR = Color.RED; protected static Color LABEL_COLOR = new Color(112, 146, 190); // Fonts protected static Font LABEL_FONT = new Font("Arial", Font.ITALIC, 10); // Strokes protected static Stroke THICK_TASK_BORDER_STROKE = new BasicStroke(3.0f); protected static Stroke GATEWAY_TYPE_STROKE = new BasicStroke(3.0f); protected static Stroke END_EVENT_STROKE = new BasicStroke(3.0f); protected static Stroke MULTI_INSTANCE_STROKE = new BasicStroke(1.3f); protected static Stroke EVENT_SUBPROCESS_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.0f, new float[] { 1.0f }, 0.0f); protected static Stroke INTERRUPTING_EVENT_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.0f, new float[] { 4.0f, 3.0f }, 0.0f); protected static Stroke HIGHLIGHT_FLOW_STROKE = new BasicStroke(1.3f); // icons protected static int ICON_SIZE = 16; protected static int ICON_PADDING = 3; protected static Image USERTASK_IMAGE; protected static Image SCRIPTTASK_IMAGE; protected static Image SERVICETASK_IMAGE; protected static Image RECEIVETASK_IMAGE; protected static Image SENDTASK_IMAGE; protected static Image MANUALTASK_IMAGE; protected static Image BUSINESS_RULE_TASK_IMAGE; protected static Image TIMER_IMAGE; protected static Image ERROR_THROW_IMAGE; protected static Image ERROR_CATCH_IMAGE; protected static Image SIGNAL_CATCH_IMAGE; protected static Image SIGNAL_THROW_IMAGE; // icons are statically loaded for performace static { try { USERTASK_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/user.png")); SCRIPTTASK_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/script.png")); SERVICETASK_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/service.png")); RECEIVETASK_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/receive.png")); SENDTASK_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/send.png")); MANUALTASK_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/manual.png")); BUSINESS_RULE_TASK_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/business_rule.png")); TIMER_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/timer.png")); ERROR_THROW_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/error_throw.png")); ERROR_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/error_catch.png")); SIGNAL_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/signal_catch.png")); SIGNAL_THROW_IMAGE = ImageIO.read(ReflectUtil.getResourceAsStream("org/activiti/engine/impl/bpmn/deployer/signal_throw.png")); } catch (IOException e) { LOGGER.warning("Could not load image for process diagram creation: " + e.getMessage()); } } protected int canvasWidth = -1; protected int canvasHeight = -1; protected int minX = -1; protected int minY = -1; protected BufferedImage processDiagram; protected Graphics2D g; protected FontMetrics fontMetrics; protected boolean closed; /** * Creates an empty canvas with given width and height. */ public ProcessDiagramCanvas(int width, int height) { this.canvasWidth = width; this.canvasHeight = height; this.processDiagram = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); this.g = processDiagram.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setPaint(Color.black); Font font = new Font("Arial", Font.BOLD, FONT_SIZE); g.setFont(font); this.fontMetrics = g.getFontMetrics(); } /** * Creates an empty canvas with given width and height. * * Allows to specify minimal boundaries on the left and upper side of the * canvas. This is useful for diagrams that have white space there (eg * Signavio). Everything beneath these minimum values will be cropped. * * @param minX * Hint that will be used when generating the image. Parts that fall * below minX on the horizontal scale will be cropped. * @param minY * Hint that will be used when generating the image. Parts that fall * below minX on the horizontal scale will be cropped. */ public ProcessDiagramCanvas(int width, int height, int minX, int minY) { this(width, height); this.minX = minX; this.minY = minY; } /** * Generates an image of what currently is drawn on the canvas. * * Throws an {@link ActivitiException} when {@link #close()} is already * called. */ public InputStream generateImage(String imageType) { if (closed) { throw new ActivitiException("ProcessDiagramGenerator already closed"); } ByteArrayOutputStream out = new ByteArrayOutputStream(); try { // Try to remove white space minX = (minX <= 5) ? 5 : minX; minY = (minY <= 5) ? 5 : minY; BufferedImage imageToSerialize = processDiagram; if (minX >= 0 && minY >= 0) { imageToSerialize = processDiagram.getSubimage(minX - 5, minY - 5, canvasWidth - minX + 5, canvasHeight - minY + 5); } ImageIO.write(imageToSerialize, imageType, out); } catch (IOException e) { throw new ActivitiException("Error while generating process image", e); } finally { IoUtil.closeSilently(out); } return new ByteArrayInputStream(out.toByteArray()); } /** * Closes the canvas which dissallows further drawing and releases graphical * resources. */ public void close() { g.dispose(); closed = true; } public void drawNoneStartEvent(int x, int y, int width, int height) { drawStartEvent(x, y, width, height, null); } public void drawTimerStartEvent(int x, int y, int width, int height) { drawStartEvent(x, y, width, height, TIMER_IMAGE); } public void drawStartEvent(int x, int y, int width, int height, Image image) { g.draw(new Ellipse2D.Double(x, y, width, height)); if (image != null) { g.drawImage(image, x, y, width, height, null); } } public void drawNoneEndEvent(int x, int y, int width, int height) { Stroke originalStroke = g.getStroke(); g.setStroke(END_EVENT_STROKE); g.draw(new Ellipse2D.Double(x, y, width, height)); g.setStroke(originalStroke); } public void drawErrorEndEvent(String name, int x, int y, int width, int height) { drawErrorEndEvent(x, y, width, height); drawLabel(name, x, y, width, height); } public void drawErrorEndEvent(int x, int y, int width, int height) { drawNoneEndEvent(x, y, width, height); g.drawImage(ERROR_THROW_IMAGE, x + 3, y + 3, width - 6, height - 6, null); } public void drawErrorStartEvent(int x, int y, int width, int height) { drawNoneStartEvent(x, y, width, height); g.drawImage(ERROR_CATCH_IMAGE, x + 3, y + 3, width - 6, height - 6, null); } public void drawCatchingEvent(int x, int y, int width, int height, boolean isInterrupting, Image image) { // event circles Ellipse2D outerCircle = new Ellipse2D.Double(x, y, width, height); int innerCircleX = x + 3; int innerCircleY = y + 3; int innerCircleWidth = width - 6; int innerCircleHeight = height - 6; Ellipse2D innerCircle = new Ellipse2D.Double(innerCircleX, innerCircleY, innerCircleWidth, innerCircleHeight); Paint originalPaint = g.getPaint(); Stroke originalStroke = g.getStroke(); g.setPaint(BOUNDARY_EVENT_COLOR); g.fill(outerCircle); g.setPaint(originalPaint); if (isInterrupting) g.setStroke(INTERRUPTING_EVENT_STROKE); g.draw(outerCircle); g.setStroke(originalStroke); g.draw(innerCircle); g.drawImage(image, innerCircleX, innerCircleY, innerCircleWidth, innerCircleHeight, null); } public void drawCatchingTimerEvent(String name, int x, int y, int width, int height, boolean isInterrupting) { drawCatchingTimerEvent(x, y, width, height, isInterrupting); drawLabel(name, x, y, width, height); } public void drawCatchingTimerEvent(int x, int y, int width, int height, boolean isInterrupting) { drawCatchingEvent(x, y, width, height, isInterrupting, TIMER_IMAGE); } public void drawCatchingErrorEvent(String name, int x, int y, int width, int height, boolean isInterrupting) { drawCatchingErrorEvent(x, y, width, height, isInterrupting); drawLabel(name, x, y, width, height); } public void drawCatchingErrorEvent(int x, int y, int width, int height, boolean isInterrupting) { drawCatchingEvent(x, y, width, height, isInterrupting, ERROR_CATCH_IMAGE); } public void drawCatchingSignalEvent(String name, int x, int y, int width, int height, boolean isInterrupting) { drawCatchingSignalEvent(x, y, width, height, isInterrupting); drawLabel(name, x, y, width, height); } public void drawCatchingSignalEvent(int x, int y, int width, int height, boolean isInterrupting) { drawCatchingEvent(x, y, width, height, isInterrupting, SIGNAL_CATCH_IMAGE); } public void drawThrowingSignalEvent(int x, int y, int width, int height) { drawCatchingEvent(x, y, width, height, false, SIGNAL_THROW_IMAGE); } public void drawThrowingNoneEvent(int x, int y, int width, int height) { drawCatchingEvent(x, y, width, height, false, null); } public void drawSequenceflow(int srcX, int srcY, int targetX, int targetY, boolean conditional) { drawSequenceflow(srcX, srcY, targetX, targetY, conditional, false); } public void drawSequenceflow(int srcX, int srcY, int targetX, int targetY, boolean conditional, boolean highLighted) { Paint originalPaint = g.getPaint(); if (highLighted) g.setPaint(HIGHLIGHT_COLOR); Line2D.Double line = new Line2D.Double(srcX, srcY, targetX, targetY); g.draw(line); drawArrowHead(line); if (conditional) { drawConditionalSequenceFlowIndicator(line); } if (highLighted) g.setPaint(originalPaint); } public void drawSequenceflow(int[] xPoints, int[] yPoints, boolean conditional, boolean isDefault, boolean highLighted) { Paint originalPaint = g.getPaint(); Stroke originalStroke = g.getStroke(); if (highLighted) { g.setPaint(HIGHLIGHT_COLOR); g.setStroke(HIGHLIGHT_FLOW_STROKE); } int radius = 15; Path2D path = new Path2D.Double(); boolean isDefaultConditionAvailable = false; //Integer nextSrcX=null, nextSrcY=null; for(int i=0; i<xPoints.length; i++) { Integer anchorX = xPoints[i]; Integer anchorY = yPoints[i]; double targetX = anchorX, targetY = anchorY; double ax=0, ay=0, bx=0, by=0, zx=0, zy=0; if (i>0 && i < xPoints.length-1) { Integer cx = anchorX, cy = anchorY; // pivot point of prev line double lineLengthY = yPoints[i] - yPoints[i-1], lineLengthX = xPoints[i] - xPoints[i-1]; double lineLength = Math.sqrt(Math.pow(lineLengthY, 2) + Math.pow(lineLengthX, 2)), dx = lineLengthX * radius / lineLength, dy = lineLengthY * radius / lineLength; targetX = targetX - dx; targetY = targetY - dy; isDefaultConditionAvailable = isDefault && i == 1 && lineLength > 10; if (lineLength < 2*radius && i>1) { targetX = xPoints[i] - lineLengthX/2; targetY = yPoints[i] - lineLengthY/2; } // pivot point of next line lineLengthY = yPoints[i+1] - yPoints[i]; lineLengthX = xPoints[i+1] - xPoints[i]; lineLength = Math.sqrt(Math.pow(lineLengthY, 2) + Math.pow(lineLengthX, 2)); if (lineLength < radius) { lineLength = radius; } dx = lineLengthX * radius / lineLength; dy = lineLengthY * radius / lineLength; double nextSrcX = xPoints[i] + dx, nextSrcY = yPoints[i] + dy; if (lineLength < 2*radius && i<xPoints.length-2) { nextSrcX = xPoints[i] + lineLengthX/2; nextSrcY = yPoints[i] + lineLengthY/2; } double dx0 = (cx - targetX) / 3, dy0 = (cy - targetY) / 3; ax = cx - dx0; ay = cy - dy0; double dx1 = (cx - nextSrcX) / 3, dy1 = (cy - nextSrcY) / 3; bx = cx - dx1; by = cy - dy1; zx=nextSrcX; zy=nextSrcY; } if (i==0) { path.moveTo(targetX, targetY); } else { path.lineTo(targetX, targetY); } if (i>0 && i < xPoints.length-1) { // add curve path.curveTo(ax, ay, bx, by, zx, zy); } if (i == xPoints.length-1) { Line2D.Double lineDouble = new Line2D.Double(xPoints[i-1], yPoints[i-1], xPoints[i], yPoints[i]); drawArrowHead(lineDouble); } } g.draw(path); if (isDefaultConditionAvailable){ Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]); drawDefaultSequenceFlowIndicator(line); } if (conditional) { Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]); drawConditionalSequenceFlowIndicator(line); } g.setPaint(originalPaint); g.setStroke(originalStroke); } public void drawSequenceflowWithoutArrow(int srcX, int srcY, int targetX, int targetY, boolean conditional) { drawSequenceflowWithoutArrow(srcX, srcY, targetX, targetY, conditional, false); } public void drawSequenceflowWithoutArrow(int srcX, int srcY, int targetX, int targetY, boolean conditional, boolean highLighted) { Paint originalPaint = g.getPaint(); if (highLighted) g.setPaint(HIGHLIGHT_COLOR); Line2D.Double line = new Line2D.Double(srcX, srcY, targetX, targetY); g.draw(line); if (conditional) { drawConditionalSequenceFlowIndicator(line); } if (highLighted) g.setPaint(originalPaint); } public void drawArrowHead(Line2D.Double line) { int doubleArrowWidth = 2 * ARROW_WIDTH; Polygon arrowHead = new Polygon(); arrowHead.addPoint(0, 0); arrowHead.addPoint(-ARROW_WIDTH, -doubleArrowWidth); arrowHead.addPoint(ARROW_WIDTH, -doubleArrowWidth); AffineTransform transformation = new AffineTransform(); transformation.setToIdentity(); double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1); transformation.translate(line.x2, line.y2); transformation.rotate((angle - Math.PI / 2d)); AffineTransform originalTransformation = g.getTransform(); g.setTransform(transformation); g.fill(arrowHead); g.setTransform(originalTransformation); } public void drawDefaultSequenceFlowIndicator(Line2D.Double line) { double length = DEFAULT_INDICATOR_WIDTH, halfOfLength = length/2, f = 8; Line2D.Double defaultIndicator = new Line2D.Double(-halfOfLength, 0, halfOfLength, 0); double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1); double dx = f * Math.cos(angle), dy = f * Math.sin(angle), x1 = line.x1 + dx, y1 = line.y1 + dy; AffineTransform transformation = new AffineTransform(); transformation.setToIdentity(); transformation.translate(x1, y1); transformation.rotate((angle - 3 * Math.PI / 4)); AffineTransform originalTransformation = g.getTransform(); g.setTransform(transformation); g.draw(defaultIndicator); g.setTransform(originalTransformation); } public void drawConditionalSequenceFlowIndicator(Line2D.Double line) { int horizontal = (int) (CONDITIONAL_INDICATOR_WIDTH * 0.7); int halfOfHorizontal = horizontal / 2; int halfOfVertical = CONDITIONAL_INDICATOR_WIDTH / 2; Polygon conditionalIndicator = new Polygon(); conditionalIndicator.addPoint(0, 0); conditionalIndicator.addPoint(-halfOfHorizontal, halfOfVertical); conditionalIndicator.addPoint(0, CONDITIONAL_INDICATOR_WIDTH); conditionalIndicator.addPoint(halfOfHorizontal, halfOfVertical); AffineTransform transformation = new AffineTransform(); transformation.setToIdentity(); double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1); transformation.translate(line.x1, line.y1); transformation.rotate((angle - Math.PI / 2d)); AffineTransform originalTransformation = g.getTransform(); g.setTransform(transformation); g.draw(conditionalIndicator); Paint originalPaint = g.getPaint(); g.setPaint(CONDITIONAL_INDICATOR_COLOR); g.fill(conditionalIndicator); g.setPaint(originalPaint); g.setTransform(originalTransformation); } public void drawTask(String name, int x, int y, int width, int height) { drawTask(name, x, y, width, height, false); } public void drawPoolOrLane(String name, int x, int y, int width, int height) { g.drawRect(x, y, width, height); // Add the name as text, vertical if(name != null && name.length() > 0) { // Include some padding int availableTextSpace = height - 6; // Create rotation for derived font AffineTransform transformation = new AffineTransform(); transformation.setToIdentity(); transformation.rotate(270 * Math.PI/180); Font currentFont = g.getFont(); Font theDerivedFont = currentFont.deriveFont(transformation); g.setFont(theDerivedFont); String truncated = fitTextToWidth(name, availableTextSpace); int realWidth = fontMetrics.stringWidth(truncated); g.drawString(truncated, x + 2 + fontMetrics.getHeight(), 3 + y + availableTextSpace - (availableTextSpace - realWidth) / 2); g.setFont(currentFont); } } protected void drawTask(String name, int x, int y, int width, int height, boolean thickBorder) { Paint originalPaint = g.getPaint(); // Create a new gradient paint for every task box, gradient depends on x and y and is not relative g.setPaint(new GradientPaint(x + 50, y, Color.white, x + 50, y + 50, TASK_BOX_COLOR)); // shape RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20); g.fill(rect); g.setPaint(originalPaint); if (thickBorder) { Stroke originalStroke = g.getStroke(); g.setStroke(THICK_TASK_BORDER_STROKE); g.draw(rect); g.setStroke(originalStroke); } else { g.draw(rect); } // text if (name != null) { drawMultilineText(name, x, y, width, height); } } protected void drawMultilineText(String text, int x, int y, int boxWidth, int boxHeight) { int availableHeight = boxHeight - ICON_SIZE - ICON_PADDING; // Create an attributed string based in input text AttributedString attributedString = new AttributedString(text); attributedString.addAttribute(TextAttribute.FONT, g.getFont()); attributedString.addAttribute(TextAttribute.FOREGROUND, Color.black); AttributedCharacterIterator characterIterator = attributedString.getIterator(); int width = boxWidth - (2 * TEXT_PADDING); int currentHeight = 0; // Prepare a list of lines of text we'll be drawing List<TextLayout> layouts = new ArrayList<TextLayout>(); String lastLine = null; LineBreakMeasurer measurer = new LineBreakMeasurer(characterIterator, g.getFontRenderContext()); TextLayout layout = null; while (measurer.getPosition() < characterIterator.getEndIndex() && currentHeight <= availableHeight) { int previousPosition = measurer.getPosition(); // Request next layout layout = measurer.nextLayout(width); int height = ((Float)(layout.getDescent() + layout.getAscent() + layout.getLeading())).intValue(); if(currentHeight + height > availableHeight) { // The line we're about to add should NOT be added anymore, append three dots to previous one instead // to indicate more text is truncated layouts.remove(layouts.size() - 1); if(lastLine.length() >= 4) { lastLine = lastLine.substring(0, lastLine.length() - 4) + "..."; } layouts.add(new TextLayout(lastLine, g.getFont(), g.getFontRenderContext())); } else { layouts.add(layout); lastLine = text.substring(previousPosition, measurer.getPosition()); currentHeight += height; } } int currentY = y + ICON_SIZE + ICON_PADDING + ((availableHeight - currentHeight) /2); int currentX = 0; // Actually draw the lines for(TextLayout textLayout : layouts) { currentY += textLayout.getAscent(); currentX = TEXT_PADDING + x + ((width - ((Double)textLayout.getBounds().getWidth()).intValue()) /2); textLayout.draw(g, currentX, currentY); currentY += textLayout.getDescent() + textLayout.getLeading(); } } protected String fitTextToWidth(String original, int width) { String text = original; // remove length for "..." int maxWidth = width - 10; while (fontMetrics.stringWidth(text + "...") > maxWidth && text.length() > 0) { text = text.substring(0, text.length() - 1); } if (!text.equals(original)) { text = text + "..."; } return text; } public void drawUserTask(String name, int x, int y, int width, int height) { drawTask(name, x, y, width, height); g.drawImage(USERTASK_IMAGE, x + ICON_PADDING, y + ICON_PADDING, ICON_SIZE, ICON_SIZE, null); } public void drawScriptTask(String name, int x, int y, int width, int height) { drawTask(name, x, y, width, height); g.drawImage(SCRIPTTASK_IMAGE, x + ICON_PADDING, y + ICON_PADDING, ICON_SIZE, ICON_SIZE, null); } public void drawServiceTask(String name, int x, int y, int width, int height) { drawTask(name, x, y, width, height); g.drawImage(SERVICETASK_IMAGE, x + ICON_PADDING, y + ICON_PADDING, ICON_SIZE, ICON_SIZE, null); } public void drawReceiveTask(String name, int x, int y, int width, int height) { drawTask(name, x, y, width, height); g.drawImage(RECEIVETASK_IMAGE, x + ICON_PADDING, y + ICON_PADDING, ICON_SIZE, ICON_SIZE, null); } public void drawSendTask(String name, int x, int y, int width, int height) { drawTask(name, x, y, width, height); g.drawImage(SENDTASK_IMAGE, x + ICON_PADDING, y + ICON_PADDING, ICON_SIZE, ICON_SIZE, null); } public void drawManualTask(String name, int x, int y, int width, int height) { drawTask(name, x, y, width, height); g.drawImage(MANUALTASK_IMAGE, x + ICON_PADDING, y + ICON_PADDING, ICON_SIZE, ICON_SIZE, null); } public void drawBusinessRuleTask(String name, int x, int y, int width, int height) { drawTask(name, x, y, width, height); g.drawImage(BUSINESS_RULE_TASK_IMAGE, x + ICON_PADDING, y + ICON_PADDING, ICON_SIZE, ICON_SIZE, null); } public void drawExpandedSubProcess(String name, int x, int y, int width, int height, Boolean isTriggeredByEvent) { RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20); // Use different stroke (dashed) if(isTriggeredByEvent) { Stroke originalStroke = g.getStroke(); g.setStroke(EVENT_SUBPROCESS_STROKE); g.draw(rect); g.setStroke(originalStroke); } else { g.draw(rect); } String text = fitTextToWidth(name, width); g.drawString(text, x + 10, y + 15); } public void drawCollapsedSubProcess(String name, int x, int y, int width, int height, Boolean isTriggeredByEvent) { drawCollapsedTask(name, x, y, width, height, false); } public void drawCollapsedCallActivity(String name, int x, int y, int width, int height) { drawCollapsedTask(name, x, y, width, height, true); } protected void drawCollapsedTask(String name, int x, int y, int width, int height, boolean thickBorder) { // The collapsed marker is now visualized separately drawTask(name, x, y, width, height, thickBorder); } public void drawCollapsedMarker(int x, int y, int width, int height) { // rectangle int rectangleWidth = MARKER_WIDTH; int rectangleHeight = MARKER_WIDTH; Rectangle rect = new Rectangle(x + (width - rectangleWidth) / 2, y + height - rectangleHeight - 3, rectangleWidth, rectangleHeight); g.draw(rect); // plus inside rectangle Line2D.Double line = new Line2D.Double(rect.getCenterX(), rect.getY() + 2, rect.getCenterX(), rect.getMaxY() - 2); g.draw(line); line = new Line2D.Double(rect.getMinX() + 2, rect.getCenterY(), rect.getMaxX() - 2, rect.getCenterY()); g.draw(line); } public void drawActivityMarkers(int x, int y, int width, int height, boolean multiInstanceSequential, boolean multiInstanceParallel, boolean collapsed) { if (collapsed) { if (!multiInstanceSequential && !multiInstanceParallel) { drawCollapsedMarker(x, y, width, height); } else { drawCollapsedMarker(x - MARKER_WIDTH / 2 - 2, y, width, height); if (multiInstanceSequential) { drawMultiInstanceMarker(true, x + MARKER_WIDTH / 2 + 2, y, width, height); } else if (multiInstanceParallel) { drawMultiInstanceMarker(false, x + MARKER_WIDTH / 2 + 2, y, width, height); } } } else { if (multiInstanceSequential) { drawMultiInstanceMarker(true, x, y, width, height); } else if (multiInstanceParallel) { drawMultiInstanceMarker(false, x, y, width, height); } } } public void drawGateway(int x, int y, int width, int height) { Polygon rhombus = new Polygon(); rhombus.addPoint(x, y + (height / 2)); rhombus.addPoint(x + (width / 2), y + height); rhombus.addPoint(x + width, y + (height / 2)); rhombus.addPoint(x + (width / 2), y); g.draw(rhombus); } public void drawParallelGateway(int x, int y, int width, int height) { // rhombus drawGateway(x, y, width, height); // plus inside rhombus Stroke orginalStroke = g.getStroke(); g.setStroke(GATEWAY_TYPE_STROKE); Line2D.Double line = new Line2D.Double(x + 10, y + height / 2, x + width - 10, y + height / 2); // horizontal g.draw(line); line = new Line2D.Double(x + width / 2, y + height - 10, x + width / 2, y + 10); // vertical g.draw(line); g.setStroke(orginalStroke); } public void drawExclusiveGateway(int x, int y, int width, int height) { // rhombus drawGateway(x, y, width, height); int quarterWidth = width / 4; int quarterHeight = height / 4; // X inside rhombus Stroke orginalStroke = g.getStroke(); g.setStroke(GATEWAY_TYPE_STROKE); Line2D.Double line = new Line2D.Double(x + quarterWidth + 3, y + quarterHeight + 3, x + 3 * quarterWidth - 3, y + 3 * quarterHeight - 3); g.draw(line); line = new Line2D.Double(x + quarterWidth + 3, y + 3 * quarterHeight - 3, x + 3 * quarterWidth - 3, y + quarterHeight + 3); g.draw(line); g.setStroke(orginalStroke); } public void drawInclusiveGateway(int x, int y, int width, int height) { // rhombus drawGateway(x, y, width, height); int diameter = width / 2; // circle inside rhombus Stroke orginalStroke = g.getStroke(); g.setStroke(GATEWAY_TYPE_STROKE); Ellipse2D.Double circle = new Ellipse2D.Double(((width - diameter) / 2) + x, ((height - diameter) / 2) + y, diameter, diameter); g.draw(circle); g.setStroke(orginalStroke); } public void drawEventBasedGateway(int x, int y, int width, int height) { // rhombus drawGateway(x, y, width, height); double scale = .6; drawCatchingEvent((int)(x + width*(1-scale)/2), (int)(y + height*(1-scale)/2), (int)(width*scale), (int)(height*scale), false, null); double r = width / 6.; // create pentagon (coords with respect to center) int topX = (int)(.95 * r); // top right corner int topY = (int)(-.31 * r); int bottomX = (int)(.59 * r); // bottom right corner int bottomY = (int)(.81 * r); int[] xPoints = new int[]{ 0, topX, bottomX, -bottomX, -topX }; int[] yPoints = new int[]{ -(int)r, topY, bottomY, bottomY, topY }; Polygon pentagon = new Polygon(xPoints, yPoints, 5); pentagon.translate(x+width/2, y+width/2); // draw g.drawPolygon(pentagon); } public void drawMultiInstanceMarker(boolean sequential, int x, int y, int width, int height) { int rectangleWidth = MARKER_WIDTH; int rectangleHeight = MARKER_WIDTH; int lineX = x + (width - rectangleWidth) / 2; int lineY = y + height - rectangleHeight - 3; Stroke orginalStroke = g.getStroke(); g.setStroke(MULTI_INSTANCE_STROKE); if (sequential) { g.draw(new Line2D.Double(lineX, lineY, lineX + rectangleWidth, lineY)); g.draw(new Line2D.Double(lineX, lineY + rectangleHeight / 2, lineX + rectangleWidth, lineY + rectangleHeight / 2)); g.draw(new Line2D.Double(lineX, lineY + rectangleHeight, lineX + rectangleWidth, lineY + rectangleHeight)); } else { g.draw(new Line2D.Double(lineX, lineY, lineX, lineY + rectangleHeight)); g.draw(new Line2D.Double(lineX + rectangleWidth / 2, lineY, lineX + rectangleWidth / 2, lineY + rectangleHeight)); g.draw(new Line2D.Double(lineX + rectangleWidth, lineY, lineX + rectangleWidth, lineY + rectangleHeight)); } g.setStroke(orginalStroke); } public void drawHighLight(int x, int y, int width, int height) { Paint originalPaint = g.getPaint(); Stroke originalStroke = g.getStroke(); g.setPaint(HIGHLIGHT_COLOR); g.setStroke(THICK_TASK_BORDER_STROKE); RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20); g.draw(rect); g.setPaint(originalPaint); g.setStroke(originalStroke); } public void drawLabel(String name, int x, int y, int width, int height){ // text if (name != null) { Paint originalPaint = g.getPaint(); Font originalFont = g.getFont(); g.setPaint(LABEL_COLOR); g.setFont(LABEL_FONT); int textX = x + width/2 - fontMetrics.stringWidth(name)/2; int textY = y + height + fontMetrics.getHeight(); g.drawString(name, textX, textY); // restore originals g.setFont(originalFont); g.setPaint(originalPaint); } } }