package com.baselet.element.sequence_aio.facet; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.baselet.control.basics.Line1D; import com.baselet.control.basics.geom.PointDouble; import com.baselet.control.enums.AlignHorizontal; import com.baselet.control.enums.AlignVertical; import com.baselet.control.enums.LineType; import com.baselet.diagram.draw.DrawHandler; import com.baselet.diagram.draw.TextSplitter; import com.baselet.element.draw.DrawHelper; public class Lifeline { private static final double ACTOR_DIMENSION = 10; private static final PointDouble ACTOR_SIZE = new PointDouble(DrawHelper.armLength(ACTOR_DIMENSION) * 2, DrawHelper.headToLegLength(ACTOR_DIMENSION)); private static final double ACTIVE_CLASS_DOUBLE_BORDER_GAP = 10; private static final double HEAD_VERTICAL_BORDER_PADDING = 5; private static final double HEAD_HORIZONTAL_BORDER_PADDING = 5; private static final double EXECUTIONSPECIFICATION_WIDTH = 20; private static final double EXECUTIONSPECIFICATION_OVERLAPP = 8; private static final double DESTROY_SIZE = 20; // width and height of the destroy symbol X private static final Logger log = LoggerFactory.getLogger(Lifeline.class); private final String[] text; /** position in the array = numbered from left to right starting at 0 */ private final int index; private final LifelineHeadType headType; /** If false it will be created by the first message sent to this object */ private boolean createdOnStart; /** The tick time when the lifeline is created can be null even when createdOnStart is false, then nothing is drawn*/ private Integer created; /** The tick time when the lifeline is destroyed (X symbol) or null if it exists till the end (no X symbol at the end) */ private Integer destroyed; private boolean execSpecFromStart; private final LinkedHashMap<Integer, LifelineOccurrence> lifeline; /** order according to the start tick */ private final List<ExecutionSpecification> activeAreas; /** * * @param text lines need to be separated by \n * @param index * @param headType * @param createdOnStart * @param execSpecFromStart true if an execution specification should be active from the creation */ public Lifeline(String text, int index, LifelineHeadType headType, boolean createdOnStart, boolean execSpecFromStart) { super(); this.text = text.split("\n"); this.index = index; this.headType = headType; this.createdOnStart = createdOnStart; created = null; destroyed = null; this.execSpecFromStart = execSpecFromStart; lifeline = new LinkedHashMap<Integer, LifelineOccurrence>(); activeAreas = new ArrayList<ExecutionSpecification>(); } public int getIndex() { return index; } // public String[] getText() { // return text; // } public void setCreatedOnStart(boolean createdOnStart) { this.createdOnStart = createdOnStart; } public boolean isCreatedOnStart() { return createdOnStart; } public void setCreated(Integer created) { this.created = created; } public Integer getCreated() { return created; } public void setDestroyed(Integer destroyed) { this.destroyed = destroyed; } public Integer getDestroyed() { return destroyed; } public boolean isExecSpecFromStart() { return execSpecFromStart; } public void setExecSpecFromStart(boolean execSpecFromStart) { this.execSpecFromStart = execSpecFromStart; } /** * * @param occurrence * @param tick * @throws SequenceDiagramCheckedException if the lifeline already contains an occurrence at the specified tick or * if the lifeline is not created on start and the specified tick is prior in time to the create tick */ public void addLifelineOccurrenceAtTick(LifelineOccurrence occurrence, Integer tick) throws SequenceDiagramCheckedException { if (!isCreatedOnStart()) { if (created == null || created >= tick) { throw new SequenceDiagramCheckedException("The lifeline can not contain occurrences before it is created."); } } if (lifeline.containsKey(tick)) { throw new SequenceDiagramCheckedException("The lifeline already contains an occurence at the tick " + tick); } lifeline.put(tick, occurrence); } public void addExecutionSpecification(ExecutionSpecification execSpec) { int i = 0; for (; i < activeAreas.size() && activeAreas.get(i).getStartTick() < execSpec.getStartTick(); i++) {} activeAreas.add(i, execSpec); } /** * @param tick the time at which the width should be calculated * @return 0 if it is only a line, otherwise return the width from * the center to the left outermost border of the execution specification * @see Coregion Coregion for an example how this is used for drawing */ public double getLifelineLeftPartWidth(int tick) { return getCurrentlyActiveExecutionSpecifications(tick) > 0 ? EXECUTIONSPECIFICATION_WIDTH / 2.0 : 0; } /** * @param tick the time at which the width should be calculated * @return 0 if it is only a line, otherwise return the width from * the center to the right outermost border of the execution specification * @see Coregion Coregion for an example how this is used for drawing */ public double getLifelineRightPartWidth(int tick) { int currentlyActiveExecSpec = getCurrentlyActiveExecutionSpecifications(tick); if (currentlyActiveExecSpec == 0) { return 0; } else { return (currentlyActiveExecSpec - 1) * (EXECUTIONSPECIFICATION_WIDTH - EXECUTIONSPECIFICATION_OVERLAPP) + EXECUTIONSPECIFICATION_WIDTH / 2.0; } } /** * @param drawHandler * @return the minimum width which is needed by this lifeline */ public double getMinWidth(DrawHandler drawHandler) { double minWidth = DESTROY_SIZE; for (LifelineOccurrence llOccurrence : lifeline.values()) { minWidth = Math.max(minWidth, llOccurrence.getMinWidth(drawHandler)); } if (activeAreas.size() == 1) { minWidth = Math.max(minWidth, EXECUTIONSPECIFICATION_WIDTH); } else if (activeAreas.size() > 1) { int maxSimultaneousExecSpec = 0; // for each start of an area calculate the currently active ExecutionSpecifications and set the maximum for (ExecutionSpecification activeArea : activeAreas) { maxSimultaneousExecSpec = Math.max(maxSimultaneousExecSpec, getCurrentlyActiveExecutionSpecifications(activeArea.getStartTick())); } minWidth = Math.max(minWidth, (maxSimultaneousExecSpec - 1) * (EXECUTIONSPECIFICATION_WIDTH - EXECUTIONSPECIFICATION_OVERLAPP) * 2 + EXECUTIONSPECIFICATION_WIDTH); } minWidth = Math.max(minWidth, getHeadMinWidth(drawHandler)); return minWidth; } private int getCurrentlyActiveExecutionSpecifications(int tick) { int currentlyActiveExecSpec = 0; for (int i = 0; i < activeAreas.size() && tick >= activeAreas.get(i).getStartTick(); i++) { if (activeAreas.get(i).enclosesTick(tick)) { currentlyActiveExecSpec++; } } return currentlyActiveExecSpec; } /** * Calculates the additonal height which are needed by the occurrences on this lifeline * @param drawHandler * @param drawingInfo * @param defaultTickHeight * @return a Map which stores the ticks as keys and the additional height as values * (i.e. the height which exceeds the tickHeight). This values are all >= 0 */ public Map<Integer, Double> getAdditionalYHeights(DrawHandler drawHandler, LifelineHorizontalDrawingInfo drawingInfo, double defaultTickHeight) { PointDouble size; double additionalY; Map<Integer, Double> ret = new HashMap<Integer, Double>(); for (Map.Entry<Integer, LifelineOccurrence> e : lifeline.entrySet()) { size = new PointDouble(drawingInfo.getSymmetricWidth(e.getKey()), defaultTickHeight); additionalY = e.getValue().getAdditionalYHeight(drawHandler, size); if (additionalY > 0) { ret.put(e.getKey(), additionalY); } } // add head size if it the obj is created with an message if (!createdOnStart && created != null) { double headAdditionalHeight = getHeadMinHeight(drawHandler, drawingInfo.getSymmetricWidth(created)) - defaultTickHeight; if (headAdditionalHeight > 0) { if (ret.containsKey(created)) { ret.put(created, Math.max(ret.get(created), headAdditionalHeight)); } else { ret.put(created, headAdditionalHeight); } } } if (destroyed != null && DESTROY_SIZE > defaultTickHeight) { if (ret.containsKey(destroyed)) { ret.put(destroyed, Math.max(ret.get(destroyed), DESTROY_SIZE - defaultTickHeight)); } else { ret.put(destroyed, DESTROY_SIZE - defaultTickHeight); } } return ret; } private double getHeadMinWidth(DrawHandler drawHandler) { double minWidth = ACTOR_SIZE.x; // actor width is small, so use it as minimum width minWidth = Math.max(minWidth, TextSplitter.getTextMinWidth(text, drawHandler)); if (headType == LifelineHeadType.STANDARD) { minWidth = minWidth + HEAD_HORIZONTAL_BORDER_PADDING * 2; } else if (headType == LifelineHeadType.ACTIVE_CLASS) { minWidth = minWidth + HEAD_HORIZONTAL_BORDER_PADDING * 2 + ACTIVE_CLASS_DOUBLE_BORDER_GAP * 2; } return minWidth; } public double getHeadMinHeight(DrawHandler drawHandler, double width) { if (headType == LifelineHeadType.STANDARD || headType == LifelineHeadType.ACTIVE_CLASS) { width -= HEAD_HORIZONTAL_BORDER_PADDING * 2; if (headType == LifelineHeadType.ACTIVE_CLASS) { width -= ACTIVE_CLASS_DOUBLE_BORDER_GAP * 2; } } double minHeight = TextSplitter.getSplitStringHeight(text, width, drawHandler); if (headType == LifelineHeadType.ACTOR) { minHeight += ACTOR_SIZE.y; } else if (headType == LifelineHeadType.ACTIVE_CLASS || headType == LifelineHeadType.STANDARD) { minHeight += HEAD_VERTICAL_BORDER_PADDING * 2; } else { log.error("Encountered unhandled enumeration value '" + headType + "'."); } return minHeight; } /** * @param drawHandler * @param drawingInfo information about the drawing positions * @param lifelineLastTick the last tick of the diagram */ public void draw(DrawHandler drawHandler, LifelineDrawingInfo drawingInfo, int lifelineLastTick) { // draw Head with text if (createdOnStart) { drawHead(drawHandler, drawingInfo.getHorizontalStart(), drawingInfo.getVerticalHeadStart(), drawingInfo.getWidth(), drawingInfo.getHeadHeight()); } // check if the creation tick was set, while writing a diagram it can be possible that the message that // creates this head wasn't yet written else if (created != null) { drawHead(drawHandler, drawingInfo.getSymmetricHorizontalStart(created), drawingInfo.getVerticalStart(created), drawingInfo.getSymmetricWidth(created), drawingInfo.getTickHeight(created)); } // without an starting point we can not draw anything if (createdOnStart || created != null) { // draw lifeline occurrences for (Map.Entry<Integer, LifelineOccurrence> e : lifeline.entrySet()) { int tick = e.getKey(); PointDouble topLeftOccurence = new PointDouble(drawingInfo.getSymmetricHorizontalStart(tick), drawingInfo.getVerticalStart(tick)); PointDouble sizeOccurence = new PointDouble(drawingInfo.getSymmetricWidth(tick), drawingInfo.getTickHeight(tick)); Line1D llInterruption = e.getValue().draw(drawHandler, topLeftOccurence, sizeOccurence); if (llInterruption != null) { drawingInfo.addInterruptedArea(llInterruption); } } // draw actual lifeline (horizontal line) drawLifeline(drawHandler, drawingInfo, lifelineLastTick); } } /** * Draws the actual line (dashed or the rectangles of the execution specification) * @param drawHandler * @param drawingInfo * @param lifelineLastTick */ private void drawLifeline(DrawHandler drawHandler, LifelineDrawingInfo drawingInfo, int lifelineLastTick) { int currentStartTick = 0; int endTick; int currentActiveCount = 0; boolean startInc = false; boolean endInc; // used as stack with newest elements at start LinkedList<ExecutionSpecification> active = new LinkedList<ExecutionSpecification>(); if (!createdOnStart) { currentStartTick = created + 1; } ListIterator<ExecutionSpecification> execSpecIter = activeAreas.listIterator(); ListIterator<Line1D> interruptedAreasIter = drawingInfo.getInterruptedAreas().listIterator(); LineType oldLt = drawHandler.getLineType(); // add execution specification which start from the creation before the loop so that they are drawn directly beneath the head if (execSpecFromStart && execSpecIter.hasNext()) { ExecutionSpecification execSpec = execSpecIter.next(); if (execSpec.getStartTick() == currentStartTick - 1) { active.addFirst(execSpec); startInc = true; } else { execSpecIter.previous(); } } double llTopY = drawingInfo.getVerticalStart(currentStartTick) - drawingInfo.getTickVerticalPadding(); while (active.size() > 0 || execSpecIter.hasNext()) { // find change of drawing style; if a new executionSpecification starts or an old ends currentActiveCount = active.size(); if (active.size() > 0 && execSpecIter.hasNext()) { ExecutionSpecification execSpec = execSpecIter.next(); if (active.getFirst().getEndTick() < execSpec.getStartTick()) { execSpecIter.previous(); endInc = false; endTick = active.removeFirst().getEndTick(); } else { endInc = true; endTick = execSpec.getStartTick(); active.addFirst(execSpec); } } else if (active.size() > 0) { endInc = false; endTick = active.removeFirst().getEndTick(); } else { // execSpecIter.hasNext() is true ExecutionSpecification execSpec = execSpecIter.next(); endInc = true; endTick = execSpec.getStartTick(); active.addFirst(execSpec); } double llBottomY = drawingInfo.getVerticalCenter(endTick); // topY + endTick * tickHeight + tickHeight / 2 + accumulativeAddiontalHeightOffsets[endTick] / 2 + accumulativeAddiontalHeightOffsets[endTick + 1] / 2; drawLifelinePart(drawHandler, drawingInfo.getHorizontalCenter(), llTopY, startInc, llBottomY, endInc, currentActiveCount, interruptedAreasIter); currentStartTick = endTick; llTopY = llBottomY; startInc = endInc; } // draw final line segment if (destroyed == null) { drawLifelinePart(drawHandler, drawingInfo.getHorizontalCenter(), llTopY, false, drawingInfo.getVerticalEnd(lifelineLastTick), false, 0, interruptedAreasIter); } else { drawHandler.setLineType(LineType.SOLID); double halfSize = DESTROY_SIZE / 2.0; double centerX = drawingInfo.getHorizontalCenter(); double centerY = drawingInfo.getVerticalCenter(destroyed); drawHandler.drawLine(centerX - halfSize, centerY - halfSize, centerX + halfSize, centerY + halfSize); drawHandler.drawLine(centerX + halfSize, centerY - halfSize, centerX - halfSize, centerY + halfSize); if (destroyed > currentStartTick) { drawLifelinePart(drawHandler, drawingInfo.getHorizontalCenter(), llTopY, false, drawingInfo.getVerticalCenter(destroyed), false, 0, interruptedAreasIter); } } drawHandler.setLineType(oldLt); } /** * draw a part of the Lifeline which has the same active count * (same number of active ExecutionSpecifications), which changes at startY ending at endY. * Draws head if increment at start, draws end if decrement at end. * * @param drawHandler * @param centerX * @param startY * @param activeCountIncStart true if the active count was lower before the start * @param endY * @param activeCountIncEnd true if the active count is higher after the end * @param activeCount * @param interruptedAreas Iterator which point to the first span where span.End > startY, after the call it points to the first span where span.End > endY */ private void drawLifelinePart(DrawHandler drawHandler, final double centerX, final double startY, boolean activeCountIncStart, final double endY, boolean activeCountIncEnd, int activeCount, ListIterator<Line1D> interruptedAreas) { double nextStartY = startY; boolean drawHead = true; // check if we must draw the head if (interruptedAreas.hasNext()) { Line1D area = interruptedAreas.next(); if (area.contains(nextStartY)) { drawHead = false; nextStartY = area.getHigh(); } else { interruptedAreas.previous(); // no intersection so push it back } } boolean drawingFinished = false; boolean drawEnd = false; double currentEndY; double currentStartY; while (!drawingFinished) { currentStartY = nextStartY; if (interruptedAreas.hasNext()) { Line1D area = interruptedAreas.next(); if (area.getLow() < endY) { currentEndY = area.getLow(); nextStartY = area.getHigh(); if (area.getHigh() > endY) { drawingFinished = true; interruptedAreas.previous(); } } // the interruption start after the end else { interruptedAreas.previous(); drawingFinished = true; drawEnd = true; currentEndY = endY; } } // we can draw till the end else { drawingFinished = true; drawEnd = true; currentEndY = endY; } // drawing part if (activeCount == 0) { drawHandler.setLineType(LineType.DASHED); drawHandler.drawLine(centerX, currentStartY, centerX, currentEndY); } else { drawHandler.setLineType(LineType.SOLID); // the right border of an rectangle is always overlaid by the rectangle to its right -> draw left lines and the right most line double lineX = centerX - EXECUTIONSPECIFICATION_WIDTH / 2.0; drawHandler.drawLine(lineX, currentStartY, lineX, currentEndY); for (int i = 0; i < activeCount - 1; i++) { lineX += EXECUTIONSPECIFICATION_WIDTH - EXECUTIONSPECIFICATION_OVERLAPP; drawHandler.drawLine(lineX, currentStartY, lineX, currentEndY); } lineX += EXECUTIONSPECIFICATION_WIDTH; drawHandler.drawLine(lineX, currentStartY, lineX, currentEndY); if (drawHead && activeCountIncStart) { // draw top border drawHandler.drawLine(lineX - EXECUTIONSPECIFICATION_WIDTH, currentStartY, lineX, currentStartY); } if (drawEnd && !activeCountIncEnd) { // draw bottom border drawHandler.drawLine(lineX - EXECUTIONSPECIFICATION_WIDTH, currentEndY, lineX, currentEndY); } } drawHead = false; } } private void drawHead(DrawHandler drawHandler, double x, double y, double width, double height) { if (headType == LifelineHeadType.STANDARD || headType == LifelineHeadType.ACTIVE_CLASS) { drawHandler.drawRectangle(x, y, width, height); if (headType == LifelineHeadType.ACTIVE_CLASS) { drawHandler.drawLine(x + ACTIVE_CLASS_DOUBLE_BORDER_GAP, y, x + ACTIVE_CLASS_DOUBLE_BORDER_GAP, y + height); drawHandler.drawLine(x + width - ACTIVE_CLASS_DOUBLE_BORDER_GAP, y, x + width - ACTIVE_CLASS_DOUBLE_BORDER_GAP, y + height); x += ACTIVE_CLASS_DOUBLE_BORDER_GAP; width -= ACTIVE_CLASS_DOUBLE_BORDER_GAP * 2; } x += HEAD_HORIZONTAL_BORDER_PADDING; width -= HEAD_HORIZONTAL_BORDER_PADDING * 2; y += HEAD_VERTICAL_BORDER_PADDING; height -= HEAD_VERTICAL_BORDER_PADDING * 2; // draw Text in x,y with width,height TextSplitter.drawText(drawHandler, text, x, y, width, height, AlignHorizontal.CENTER, AlignVertical.CENTER); } else if (headType == LifelineHeadType.ACTOR) { DrawHelper.drawActor(drawHandler, (int) (x + width / 2.0), (int) y, ACTOR_DIMENSION); y += ACTOR_SIZE.y; height -= ACTOR_SIZE.y; // draw Text in x,y with width,height TextSplitter.drawText(drawHandler, text, x, y, width, height, AlignHorizontal.CENTER, AlignVertical.BOTTOM); } else { log.error("Encountered unhandled enumeration value '" + headType + "'."); } } public enum LifelineHeadType { STANDARD, ACTIVE_CLASS, ACTOR } }