package com.baselet.element.sequence_aio.facet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
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;
/**
* Draws a combined fragment.
* The border starts at the top of the start tick and ends at the bottom of the end tick + half of the tick y padding.
*
*/
public class CombinedFragment implements LifelineSpanningTickSpanningOccurrence, Container {
/** how much space is between the header and the first constraint */
private static final double HEADER_CONSTRAINT_PADDING = 5;
private static final double CONSTRAINT_Y_PADDING = 2;
private static final double COMBINED_FRAGMENT_HORIZONTAL_BORDER_PADDING = 7;
private static final double COMBINED_FRAGMENT_VERTICAL_BORDER_PADDING = 7;
private final Lifeline[] coveredLifelines;
/** first tick which is in the combined fragment, contains the operator and the InteractionConstraint of the first operand */
private final int startTick;
private final String[] operatorLines;
private final Deque<Operand> operands;
/**
*
* @param coveredLifelines the lifelines which are covered by this combined fragment
* @param startTick
* @param operator can be multiple lines (but only \n is allowed)
*/
public CombinedFragment(Lifeline[] coveredLifelines, int startTick, String operator) {
super();
this.coveredLifelines = Arrays.copyOf(coveredLifelines, coveredLifelines.length);
this.startTick = startTick;
operatorLines = operator.split("\n");
operands = new LinkedList<CombinedFragment.Operand>();
}
@Override
public Lifeline getFirstLifeline() {
return coveredLifelines[0];
}
@Override
public Lifeline getLastLifeline() {
return coveredLifelines[coveredLifelines.length - 1];
}
@Override
public int getStartTick() {
return startTick;
}
// public void setEndTick(int endTick) {
// this.endTick = endTick;
// }
/**
* the last tick which lies in the combined fragment
*/
@Override
public int getEndTick() {
if (operands.isEmpty()) {
return startTick;
}
return operands.getLast().endTick;
}
/**
* Adds an operand to the combined fragement.
* @param startTick
* @param endTick
*/
public void addOperand(int startTick, int endTick) {
if (operands.size() == 0 && this.startTick != startTick) {
throw new IllegalArgumentException("The start tick of the first operand must be equal to the start tick of the combined fragment");
}
operands.add(new Operand(startTick, endTick));
}
/**
* Adds an operand with an interaction constraint to the combined fragment.
* @param startTick
* @param endTick
* @param text
* @param constraintLifeline
* @throws SequenceDiagramCheckedException if the lifeline already contains an occurrence at the startTick or
* if the lifeline is not created on start and the startTick is prior in time to the create tick
*/
public void addOperand(int startTick, int endTick, String text, Lifeline constraintLifeline) throws SequenceDiagramCheckedException {
if (operands.size() == 0 && this.startTick != startTick) {
throw new IllegalArgumentException("The start tick of the first operand must be equal to the start tick of the combined fragment");
}
operands.add(new Operand(startTick, endTick, text, constraintLifeline));
}
@Override
public void draw(DrawHandler drawHandler, DrawingInfo drawingInfo) {
// draw operand lines, the pentagon and the first operand
drawOperandLines(drawHandler, drawingInfo);
PointDouble[] rectangle = new PointDouble[] {
new PointDouble(drawingInfo.getHorizontalStart(this),
drawingInfo.getVerticalStart(this)),
new PointDouble(drawingInfo.getHorizontalEnd(this),
drawingInfo.getVerticalStart(this)),
new PointDouble(drawingInfo.getHorizontalEnd(this),
drawingInfo.getVerticalEnd(this)),
new PointDouble(drawingInfo.getHorizontalStart(this),
drawingInfo.getVerticalEnd(this)),
null
};
rectangle[4] = rectangle[0];
drawHandler.drawLines(rectangle);
Collection<Line1D> slopeNotPermittedAreas = new ArrayList<Line1D>(coveredLifelines.length);
for (Lifeline ll : coveredLifelines) {
if (ll.getLifelineLeftPartWidth(startTick) > 0) {
double llCenter = drawingInfo.getHDrawingInfo(ll).getHorizontalCenter();
slopeNotPermittedAreas.add(new Line1D(llCenter - ll.getLifelineLeftPartWidth(startTick),
llCenter + ll.getLifelineRightPartWidth(startTick)));
}
}
PointDouble headerSize = PentagonDrawingHelper.draw(drawHandler, operatorLines,
drawingInfo.getWidth(this), rectangle[0], slopeNotPermittedAreas);
// add interruptions for all affected lifelines
double endOfHeadX = drawingInfo.getHorizontalStart(this) + headerSize.x;
for (Lifeline ll : coveredLifelines) {
if (drawingInfo.getHDrawingInfo(ll).getHorizontalCenter() - ll.getLifelineLeftPartWidth(startTick) <= endOfHeadX) {
drawingInfo.getDrawingInfo(ll).addInterruptedArea(new Line1D(rectangle[0].y, rectangle[0].y + headerSize.y));
}
else {
// we traverse the lifelines from left to right, therfore after the first ll which is not covered we can stop
break;
}
}
// draw operand and add the interrupted area
if (operands.size() > 0 && operands.getFirst().constraint != null) {
Operand firstOperand = operands.getFirst();
Operand.InteractionConstraint firstIntConst = firstOperand.constraint;
double constraintTopY = drawingInfo.getVerticalStart(this) + headerSize.y + HEADER_CONSTRAINT_PADDING;
double constraintBottomY = drawingInfo.getVerticalEnd(startTick);
double textHeight = TextSplitter.getSplitStringHeight(firstIntConst.textLines,
drawingInfo.getHDrawingInfo(firstIntConst.affectedLifeline).getSymmetricWidth(startTick), drawHandler);
textHeight += CONSTRAINT_Y_PADDING * 2;
constraintTopY += (constraintBottomY - constraintTopY - textHeight) / 2;
TextSplitter.drawText(drawHandler, firstIntConst.textLines,
drawingInfo.getHDrawingInfo(firstIntConst.affectedLifeline).getSymmetricHorizontalStart(startTick),
constraintTopY,
drawingInfo.getHDrawingInfo(firstIntConst.affectedLifeline).getSymmetricWidth(startTick),
textHeight, AlignHorizontal.CENTER, AlignVertical.CENTER);
drawingInfo.getDrawingInfo(firstIntConst.affectedLifeline).addInterruptedArea(
new Line1D(constraintTopY, constraintTopY + textHeight));
}
}
private void drawOperandLines(DrawHandler drawHandler, DrawingInfo drawingInfo) {
Iterator<Operand> operandIter = operands.iterator();
if (operandIter.hasNext()) {
operandIter.next(); // skip first operand
}
drawHandler.setLineType(LineType.DASHED);
while (operandIter.hasNext()) {
Operand op = operandIter.next();
drawHandler.drawLine(drawingInfo.getHorizontalStart(this),
drawingInfo.getVerticalStart(op.startTick) - drawingInfo.getTickVerticalPadding() / 2.0,
drawingInfo.getHorizontalEnd(this),
drawingInfo.getVerticalStart(op.startTick) - drawingInfo.getTickVerticalPadding() / 2.0);
}
drawHandler.setLineType(LineType.SOLID);
}
@Override
public double getOverallMinWidth(DrawHandler drawHandler, double lifelineHorizontalPadding) {
// we only need to calculate the minimum width of the pentagon and the first operand,
// other operands are handled as LifelineOccurrence. Add the width of the execution specification of the last
// lifeline as buffere because in rare cases the slope could "jump" over it (to avoid that the slope is in the ExecSpec)
// add the border padding so nested combined fragments look nice
double minWidth = PentagonDrawingHelper.getMinimumWidth(drawHandler, operatorLines);
if (operands.size() > 0 && operands.getFirst().constraint != null) {
double constraintMinWidth = operands.getFirst().constraint.getMinWidth(drawHandler) * coveredLifelines.length + (coveredLifelines.length - 1) * lifelineHorizontalPadding;
minWidth = Math.max(minWidth, constraintMinWidth);
}
return minWidth + getLastLifeline().getLifelineLeftPartWidth(startTick) + getLastLifeline().getLifelineRightPartWidth(startTick);
}
@Override
public Map<Integer, Double> getEveryAdditionalYHeight(DrawHandler drawHandler, HorizontalDrawingInfo hInfo, double defaultTickHeight) {
Map<Integer, Double> ret = new HashMap<Integer, Double>();
// we only need to calculate the pentagon + the first operand/InteractionConstrain because
// the separating lines are too thin to make a difference and the other operands are
// handled as LifelineOccurrence
double headerHeight = PentagonDrawingHelper.getHeight(drawHandler, operatorLines, hInfo.getWidth(this));
headerHeight -= COMBINED_FRAGMENT_VERTICAL_BORDER_PADDING; // head draws into the padding
if (operands.size() > 0 && operands.getFirst().constraint != null) {
headerHeight += HEADER_CONSTRAINT_PADDING;
headerHeight += TextSplitter.getSplitStringHeight(operands.getFirst().constraint.textLines,
hInfo.getHDrawingInfo(operands.getFirst().constraint.affectedLifeline).getSymmetricWidth(startTick),
drawHandler);
headerHeight += CONSTRAINT_Y_PADDING * 2;
}
if (headerHeight > defaultTickHeight) {
ret.put(startTick, headerHeight - defaultTickHeight);
}
return ret;
}
@Override
public ContainerPadding getPaddingInformation() {
return new ContainerPadding(this, COMBINED_FRAGMENT_HORIZONTAL_BORDER_PADDING,
COMBINED_FRAGMENT_HORIZONTAL_BORDER_PADDING,
COMBINED_FRAGMENT_VERTICAL_BORDER_PADDING,
COMBINED_FRAGMENT_VERTICAL_BORDER_PADDING);
}
private class Operand {
private final int startTick;
private final int endTick;
private final InteractionConstraint constraint;
public Operand(int startTick, int endTick) {
super();
this.startTick = startTick;
this.endTick = endTick;
constraint = null;
}
/**
*
* @param startTick
* @param endTick
* @param text
* @param constraintLifeline
* @throws SequenceDiagramCheckedException if the lifeline already contains an occurrence at the startTick or
* if the lifeline is not created on start and the startTick is prior in time to the create tick
*/
public Operand(int startTick, int endTick, String text, Lifeline constraintLifeline) throws SequenceDiagramCheckedException {
super();
this.startTick = startTick;
this.endTick = endTick;
constraint = new InteractionConstraint(text, constraintLifeline);
}
public boolean isFirstOperand() {
return operands.getFirst() == this;
}
public boolean isLastOperand() {
return operands.getLast() == this;
}
/**
* Represents an interaction constraint of an operand of a combined fragment.
* The drawing and calculation of the height for the interaction constraint
* of the first operand is handled by the combined fragment because the
* positioning of it is tightly coupled with the operator drawing.
*
*/
private class InteractionConstraint implements LifelineOccurrence {
private final String[] textLines;
private final Lifeline affectedLifeline;
/**
* @param constraintText the constraint without the square brackets
* @param affectedLifeline the lifeline on which the constraint should be placed
* @throws SequenceDiagramCheckedException if the lifeline already contains an occurrence at the {@link Operand#startTick} or
* if the lifeline is not created on start and the {@link Operand#startTick} is prior in time to the create tick
*/
public InteractionConstraint(String constraintText, Lifeline affectedLifeline) throws SequenceDiagramCheckedException {
super();
textLines = ('[' + constraintText + ']').split("\n");
this.affectedLifeline = affectedLifeline;
affectedLifeline.addLifelineOccurrenceAtTick(this, startTick);
}
@Override
public Line1D draw(DrawHandler drawHandler, PointDouble topLeft, PointDouble size) {
if (isFirstOperand()) {
// first operand is handled by the combined fragment
return null;
}
else {
double textHeight = TextSplitter.getSplitStringHeight(textLines, size.x, drawHandler);
Line1D interruptedArea = new Line1D(topLeft.y + (size.y - textHeight) / 2 - CONSTRAINT_Y_PADDING,
topLeft.y + (size.y - textHeight) / 2 + textHeight + CONSTRAINT_Y_PADDING);
TextSplitter.drawText(drawHandler, textLines, topLeft.x, interruptedArea.getLow() + CONSTRAINT_Y_PADDING,
size.x, textHeight, AlignHorizontal.CENTER, AlignVertical.CENTER);
return interruptedArea;
}
}
@Override
public double getMinWidth(DrawHandler drawHandler) {
return TextSplitter.getTextMinWidth(textLines, drawHandler);
}
@Override
public double getAdditionalYHeight(DrawHandler drawHandler, PointDouble size) {
if (isFirstOperand()) {
// first operand is handled by the combined fragment
return -1;
}
else {
return TextSplitter.getSplitStringHeight(textLines, size.x, drawHandler) + CONSTRAINT_Y_PADDING * 2 - size.y;
}
}
}
}
}