Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: dee9c1d2b439d73ecd05ec4adb8bee4b63f8d9cd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
/*****************************************************************************
 * Copyright (c) 2018 CEA LIST, EclipseSource and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   EclipseSource - Initial API and implementation
 *
 *****************************************************************************/

package org.eclipse.papyrus.uml.diagram.sequence.figures;

import java.util.List;

import org.eclipse.draw2d.ConnectionRouter;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.PolylineConnection;
import org.eclipse.draw2d.PolylineDecoration;
import org.eclipse.draw2d.geometry.Geometry;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gmf.runtime.draw2d.ui.figures.WrappingLabel;
import org.eclipse.papyrus.infra.gmfdiag.common.figure.node.PapyrusWrappingLabel;
import org.eclipse.papyrus.uml.diagram.common.figure.edge.UMLEdgeFigure;
import org.eclipse.swt.SWT;
import org.eclipse.uml2.uml.DurationConstraint;
import org.eclipse.uml2.uml.DurationObservation;

/**
 * <p>
 * A Figure for Durations ({@link DurationObservation Observation} or {@link DurationConstraint Constraint})
 * represented as an arrow between two events. The figure consists of two horizontal dashed lines,
 * with a vertical arrow between them:
 * </p>
 * <img src="./doc-files/DurationLinkFigure.png" />
 * <p>
 * The horizontal lines are <code>anchored</code> to the source/target of the link. By default, the vertical line will
 * be placed in the middle of the bounds formed by these two anchors, although the figure supports a horizontal delta,
 * to position the arrow closer to one or the other anchor.
 * </p>
 *
 * <p>
 * The figure can also be rotated 90° (i.e. vertical dashed lines and horizontal arrow),
 * when the source and target points are on the same Y coordinate (Typically for horizontal messages)
 * </p>
 */
public class DurationLinkFigure extends UMLEdgeFigure {

	/**
	 * The orientation of the figure changes from the default value {@link Orientation#VERTICAL} to {@link Orientation#HORIZONTAL}
	 * if the difference in pixels between end and start points are no more than this amount of pixels.
	 */
	private static final int ORIENTATION_SWITCH_DIFFERENCE = 30;

	/**
	 * When the arrow is in an {@link Orientation#HORIZONTAL} position, the start line is drawn as a 90° bent line, with
	 * an horizontal segment connected to the start point and a vertical segment. This offset determines the length of
	 * the horizontal segment, in pixels.
	 */
	private static final int HORIZOTAL_ARROW_START_LINE_OFFSET = 15;

	/**
	 * When the arrow is in an {@link Orientation#HORIZONTAL} position, the end line is drawn as a 90° bent line, with
	 * an horizontal segment connected to the end point and a vertical segment. This offset determines the length of
	 * the horizontal segment, in pixels.
	 */
	private static final int HORIZOTAL_ARROW_END_LINE_OFFSET = 15;

	/**
	 * The connecting end and start dashed lines will be drawn slightly further
	 * than the arrow, by this amount of pixels.
	 */
	private static final int ARROW_PADDING = 15;
	private Orientation arrowOrientation = Orientation.VERTICAL;
	private int arrowPositionDelta = 0;
	private PapyrusWrappingLabel durationLabel;

	/**
	 * Thin lines may be difficult to select, so we add a tolerance area around it
	 * to make selection easier.
	 *
	 * @see #containsPoint(int, int)
	 */
	private static final int SELECTION_TOLERANCE = 3;

	@Override
	protected void outlineShape(Graphics graphics) {
		// Skip super; we're not drawing a polyline connection
		arrowOrientation = computeOptimalOrientation();
		paintStartLine(graphics);
		paintEndLine(graphics);
		paintArrow(graphics);
	}

	/**
	 * Paint the line from this figure to the start point/event (Typically a horizontal line)
	 *
	 * @param graphics
	 */
	protected void paintStartLine(Graphics graphics) {
		graphics.pushState();
		graphics.setLineStyle(SWT.LINE_DASH);
		try {
			PointList startLinePoints = getStartLinePoints();
			graphics.drawPolyline(startLinePoints);
		} finally {
			graphics.popState();
		}
	}

	/** Returns the points for the start line - the line connecting the start point to the arrow. */
	protected PointList getStartLinePoints() {
		if (arrowOrientation == Orientation.HORIZONTAL) {
			return getStartLinePointsHorizontal();
		}
		// Orientation.VERTICAL and default case
		return getStartLinePointsVertical();
	}

	private PointList getStartLinePointsHorizontal() {
		PointList points = new PointList(3);

		points.addPoint(getStart());

		Point startOffsetEnd = getStart().getCopy();
		startOffsetEnd.setX(startOffsetEnd.x() + HORIZOTAL_ARROW_START_LINE_OFFSET);
		points.addPoint(startOffsetEnd);

		int arrowYCoordinate = (getStart().y() + getEnd().y()) / 2;

		// the vertical segment
		Point startLineEnd = startOffsetEnd.getCopy();
		if (arrowYCoordinate > startOffsetEnd.y) {
			startLineEnd.setY(arrowYCoordinate + ARROW_PADDING);
		} else {
			startLineEnd.setY(arrowYCoordinate - ARROW_PADDING);
		}
		points.addPoint(startLineEnd);
		return points;
	}

	private PointList getStartLinePointsVertical() {
		PointList points = new PointList(2);

		// start
		points.addPoint(getStart());

		// end
		int arrowLinePosition = getArrowLineVerticalX();
		Point startLineEnd = getStart().getCopy();
		if (arrowLinePosition > getStart().x()) {
			startLineEnd.setX(arrowLinePosition + ARROW_PADDING);
		} else {
			startLineEnd.setX(arrowLinePosition - ARROW_PADDING);
		}
		points.addPoint(startLineEnd);
		return points;
	}

	private int getArrowLineVerticalX() {
		if (getPoints().size() < 2) {
			// The connection is not configured yet
			return 0;
		}
		return (getStart().x() + getEnd().x()) / 2 + arrowPositionDelta;
	}

	private int getArrowLineVerticalY() {
		return getEnd().y();
	}

	/**
	 * Paint the line from this figure to the end point/event (Typically a horizontal line)
	 *
	 * @param graphics
	 */
	protected void paintEndLine(Graphics graphics) {
		graphics.pushState();
		graphics.setLineStyle(SWT.LINE_DASH);
		try {
			PointList endLinePoints = getEndLinePoints();
			graphics.drawPolyline(endLinePoints);
		} finally {
			graphics.popState();
		}
	}

	/** Returns the points for the end line - the line connecting the end point to the arrow. */
	protected PointList getEndLinePoints() {
		if (arrowOrientation == Orientation.HORIZONTAL) {
			return getEndLinePointsHorizontal();
		}
		// Orientation.VERTICAL and default case
		return getEndLinePointsVertical();
	}

	private PointList getEndLinePointsHorizontal() {
		PointList points = new PointList(2);

		points.addPoint(getEnd());
		Point endOffsetEnd = getEnd().getCopy();
		endOffsetEnd.setX(endOffsetEnd.x() - HORIZOTAL_ARROW_END_LINE_OFFSET);
		points.addPoint(endOffsetEnd);
		int arrowYCoordinate = (getStart().y() + getEnd().y()) / 2;
		// paint the end line
		Point endLineEnd = endOffsetEnd.getCopy();
		if (arrowYCoordinate < getEnd().y) {
			endLineEnd.setY(arrowYCoordinate - HORIZOTAL_ARROW_END_LINE_OFFSET);
		} else {
			endLineEnd.setY(arrowYCoordinate + HORIZOTAL_ARROW_END_LINE_OFFSET);
		}
		points.addPoint(endLineEnd);

		return points;
	}

	private PointList getEndLinePointsVertical() {
		PointList points = new PointList(2);

		// start
		points.addPoint(getEnd());

		// end
		int arrowLinePosition = getArrowLineVerticalX();
		Point endLineEnd = getEnd().getCopy();
		if (arrowLinePosition < getEnd().x()) {
			endLineEnd.setX(arrowLinePosition - ARROW_PADDING);
		} else {
			endLineEnd.setX(arrowLinePosition + ARROW_PADDING);
		}
		points.addPoint(endLineEnd);
		return points;
	}

	/**
	 * Paint the arrow between the start line and end line (Typically a vertical arrow)
	 *
	 * @param graphics
	 */
	protected void paintArrow(Graphics graphics) {
		PolylineConnection arrowLine = new PolylineConnection();
		PointList arrowPoints = getArrowLinePoints();
		Point arrowStart = arrowPoints.getFirstPoint();
		Point arrowEnd = arrowPoints.getLastPoint();

		arrowLine.setStart(arrowStart);
		arrowLine.setEnd(arrowEnd);

		decorateArrowLine(arrowLine, arrowStart, arrowEnd);
		arrowLine.paint(graphics);
	}

	/** Returns the points for the arrow line drawn between the and and start lines. */
	protected PointList getArrowLinePoints() {
		PointList points = new PointList(2);
		Point arrowStart = null, arrowEnd = null;
		if (arrowOrientation == Orientation.HORIZONTAL) {
			int arrowYCoordinate = (getStart().y() + getEnd().y()) / 2;
			arrowStart = getStart().getCopy().setX(getStart().x() + ARROW_PADDING).setY(arrowYCoordinate);
			arrowEnd = getEnd().getCopy().setX(getEnd().x() - ARROW_PADDING).setY(arrowYCoordinate);
		} else {
			arrowStart = getStart().getCopy().setX(getArrowLineVerticalX());
			arrowEnd = arrowStart.getCopy().setY(getArrowLineVerticalY());
		}
		points.addPoint(arrowStart);
		points.addPoint(arrowEnd);
		return points;
	}

	/** Adds decorations(e.g. arrow triangles) to the arrow line. */
	protected void decorateArrowLine(PolylineConnection arrowLine, Point arrowStart, Point arrowEnd) {
		// source
		PolylineDecoration source = new PolylineDecoration();
		source.setLocation(arrowStart);
		source.setLineWidth(1);
		source.setReferencePoint(arrowEnd);
		arrowLine.setSourceDecoration(source);

		// target
		PolylineDecoration target = new PolylineDecoration();
		target.setLocation(arrowEnd);
		target.setLineWidth(1);
		target.setReferencePoint(arrowStart);
		arrowLine.setTargetDecoration(target);
	}


	private Orientation computeOptimalOrientation() {
		if (Math.abs(getStart().y - getEnd().y) < ORIENTATION_SWITCH_DIFFERENCE) {
			return Orientation.HORIZONTAL;
		}
		return Orientation.VERTICAL;
	}


	/**
	 * {@inheritDoc}
	 * <p>
	 * Override containsPoint to handle clicks on any of the 3 lines (start, end and arrow line)
	 * </p>
	 */
	@Override
	public boolean containsPoint(int x, int y) {
		// START LINE
		PointList startLinePoints = getStartLinePoints();
		if (Geometry.polylineContainsPoint(startLinePoints, x, y, SELECTION_TOLERANCE)) {
			return true;
		}

		// END LINE
		PointList endLinePoints = getEndLinePoints();
		if (Geometry.polylineContainsPoint(endLinePoints, x, y, SELECTION_TOLERANCE)) {
			return true;
		}

		// ARROW
		PointList arrowPoints = getArrowLinePoints();
		if (Geometry.polylineContainsPoint(arrowPoints, x, y, SELECTION_TOLERANCE)) {
			return true;
		}

		// Child labels
		@SuppressWarnings("unchecked")
		List<IFigure> children = getChildren();
		return children.stream().anyMatch(child -> child.containsPoint(x, y));
	}


	/**
	 * <p>
	 * By default, the arrow is centered between its start and end point (delta = 0). The position
	 * delta can be used to move it to the right (delta > 0) or to the left (delta < 0).
	 * </p>
	 *
	 * @param delta
	 */
	public void setArrowPositionDelta(int delta) {
		if (delta != this.arrowPositionDelta) {
			this.arrowPositionDelta = delta;
			revalidate();
		}
	}

	@Override
	public Rectangle getBounds() {
		Rectangle bounds = super.getBounds();

		// The arrow may be moved outside of the bounds defined by (start, end).
		// In that case, we need to update the bounds, to make sure we can draw
		// everything
		if (getPoints().size() >= 2) {
			PointList allPoints = new PointList();
			allPoints.addAll(getStartLinePoints());
			allPoints.addAll(getEndLinePoints());
			allPoints.addAll(getArrowLinePoints());
			bounds.union(allPoints.getBounds());
		}
		return bounds;
	}

	@Override
	public void setConnectionRouter(ConnectionRouter cr) {
		// Skip; this figure doesn't support routers/bendpoints
	}

	/* package */ static enum Orientation {
		VERTICAL, HORIZONTAL;
	}

	public WrappingLabel getDurationLabelFigure() {
		return this.durationLabel;
	}


	@Override
	protected void createContents() {
		super.createContents();
		this.durationLabel = new PapyrusWrappingLabel();
		this.durationLabel.setText(""); //$NON-NLS-1$
		add(this.durationLabel);
	}

}

Back to the top