Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathilde Arnaud2017-02-20 13:51:34 +0000
committerGerrit Code Review @ Eclipse.org2017-02-21 09:20:21 +0000
commiteaf1839039c18be45c26b15881da2c6831c6c791 (patch)
tree7ab597d7d55186965467aa14fede9c671708e8c6
parent75b675ffa572cd757490287fdd0b82c40828834d (diff)
downloadorg.eclipse.papyrus-eaf1839039c18be45c26b15881da2c6831c6c791.tar.gz
org.eclipse.papyrus-eaf1839039c18be45c26b15881da2c6831c6c791.tar.xz
org.eclipse.papyrus-eaf1839039c18be45c26b15881da2c6831c6c791.zip
Bug 512425 : [Sequence diagram] Impossible to move found or lost
messages Fixed algorithmic mistake. Change-Id: I6143707929149d7a98446ba25724b119a5b32e89 Signed-off-by: Mathilde Arnaud <mathilde.arnaud@cea.fr>
-rw-r--r--plugins/uml/diagram/org.eclipse.papyrus.uml.diagram.sequence/custom-src/org/eclipse/papyrus/uml/diagram/sequence/draw2d/routers/MessageRouter.java1682
1 files changed, 844 insertions, 838 deletions
diff --git a/plugins/uml/diagram/org.eclipse.papyrus.uml.diagram.sequence/custom-src/org/eclipse/papyrus/uml/diagram/sequence/draw2d/routers/MessageRouter.java b/plugins/uml/diagram/org.eclipse.papyrus.uml.diagram.sequence/custom-src/org/eclipse/papyrus/uml/diagram/sequence/draw2d/routers/MessageRouter.java
index 94ca41c6fcc..3fa5e01be99 100644
--- a/plugins/uml/diagram/org.eclipse.papyrus.uml.diagram.sequence/custom-src/org/eclipse/papyrus/uml/diagram/sequence/draw2d/routers/MessageRouter.java
+++ b/plugins/uml/diagram/org.eclipse.papyrus.uml.diagram.sequence/custom-src/org/eclipse/papyrus/uml/diagram/sequence/draw2d/routers/MessageRouter.java
@@ -1,838 +1,844 @@
-/*****************************************************************************
- * Copyright (c) 2010 CEA
- *
- *
- * 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:
- * Atos Origin - Initial API and implementation
- *
- *****************************************************************************/
-package org.eclipse.papyrus.uml.diagram.sequence.draw2d.routers;
-
-import org.eclipse.draw2d.Connection;
-import org.eclipse.draw2d.IFigure;
-import org.eclipse.draw2d.PositionConstants;
-import org.eclipse.draw2d.geometry.Dimension;
-import org.eclipse.draw2d.geometry.Point;
-import org.eclipse.draw2d.geometry.PointList;
-import org.eclipse.draw2d.geometry.PrecisionPoint;
-import org.eclipse.draw2d.geometry.PrecisionRectangle;
-import org.eclipse.draw2d.geometry.Ray;
-import org.eclipse.draw2d.geometry.Rectangle;
-import org.eclipse.gmf.runtime.common.core.util.StringStatics;
-import org.eclipse.gmf.runtime.draw2d.ui.figures.BaseSlidableAnchor;
-import org.eclipse.gmf.runtime.draw2d.ui.figures.FigureUtilities;
-import org.eclipse.gmf.runtime.draw2d.ui.figures.OrthogonalConnectionAnchor;
-import org.eclipse.gmf.runtime.draw2d.ui.geometry.PointListUtilities;
-import org.eclipse.gmf.runtime.draw2d.ui.internal.routers.ObliqueRouter;
-import org.eclipse.gmf.runtime.draw2d.ui.internal.routers.OrthogonalRouterUtilities;
-import org.eclipse.gmf.runtime.draw2d.ui.mapmode.MapModeUtil;
-import org.eclipse.papyrus.uml.diagram.sequence.edit.helpers.AnchorHelper;
-import org.eclipse.papyrus.uml.diagram.sequence.figures.LifelineFigure;
-import org.eclipse.papyrus.uml.diagram.sequence.figures.MessageAsync;
-import org.eclipse.papyrus.uml.diagram.sequence.figures.MessageCreate;
-import org.eclipse.swt.SWT;
-import org.eclipse.swt.widgets.Event;
-import org.eclipse.swt.widgets.Listener;
-import org.eclipse.ui.PlatformUI;
-
-/**
- * A multi behavior router which enable to draw message.
- * It can behave as an oblique router (with no bendpoint), or as an horizontal router (with no bendpoint),
- * or as a rectilinear router with 2 bendpoints.
- *
- * @author mvelten and vhemery
- */
-@SuppressWarnings({ "restriction", "deprecation" })
-public class MessageRouter extends ObliqueRouter {
-
- private static final int MAX_DELTA = 65;
- private static final int PRECISION_DELTA = 10;
-
- private static boolean precisionMode;
-
- public static enum RouterKind {
- HORIZONTAL, OBLIQUE, SELF;
-
- public static RouterKind getKind(Connection conn, PointList newLine) {
-
-
- PlatformUI.getWorkbench().getDisplay().addFilter(SWT.KeyDown, new Listener() {
-
- @Override
- public void handleEvent(Event event) {
- // in case the SHIFT key is down, the creation enter in the precision Mode
- precisionMode = (event.keyCode == SWT.SHIFT);
- }
- });
-
-
- PlatformUI.getWorkbench().getDisplay().addFilter(SWT.KeyUp, new Listener() {
-
- @Override
- public void handleEvent(Event event) {
- // in case the SHIFT key is released, the creation mode goes back to normal
- if (event.keyCode == SWT.SHIFT) {
- precisionMode = false;
- }
- }
- });
-
-
-
-
- if (isSelfConnection(conn)) {
-
- return SELF;
- }
- if (isHorizontalConnection(conn, newLine)) {
-
- return HORIZONTAL;
- }
-
- return OBLIQUE;
- }
-
-
-
- private static boolean isHorizontalConnection(Connection conn, PointList newLine) {
- boolean horizontal = true;
-
- if (!(conn instanceof MessageAsync)) {
- horizontal = false;
- }
- Point sourcePoint = newLine.getFirstPoint();
- Point targetPoint = newLine.getLastPoint();
-
- int realDelta = Math.abs(sourcePoint.y - targetPoint.y);
-
- if (!precisionMode) {
- horizontal = (realDelta <= MAX_DELTA);
-
- } else {
- horizontal = (realDelta <= PRECISION_DELTA);
- }
-
- return horizontal;
- }
-
- /**
- * It is self if the parent lifeline is the same.
- */
- private static boolean isSelfConnection(Connection conn) {
- if (conn == null || conn.getSourceAnchor() == null || conn.getTargetAnchor() == null) {
- return false;
- }
- IFigure sourceLifeline = conn.getSourceAnchor().getOwner();
- while (sourceLifeline != null && !(sourceLifeline instanceof LifelineFigure)) {
- sourceLifeline = sourceLifeline.getParent();
- }
- IFigure targetLifeline = conn.getTargetAnchor().getOwner();
- while (targetLifeline != null && !(targetLifeline instanceof LifelineFigure)) {
- targetLifeline = targetLifeline.getParent();
- }
- return sourceLifeline != null && sourceLifeline.equals(targetLifeline);
- }
- }
-
- @Override
- public void routeLine(Connection conn, int nestedRoutingDepth, PointList newLine) {
- Point sourcePoint, targetPoint;
- switch (RouterKind.getKind(conn, newLine)) {
- case HORIZONTAL:
- originalRectilinearRouteLine(conn, nestedRoutingDepth, newLine);
- // force 2 bendpoints on the same Y coordinate
- sourcePoint = newLine.getFirstPoint();
- targetPoint = newLine.getLastPoint();
- targetPoint.y = sourcePoint.y;
- newLine.removeAllPoints();
- newLine.addPoint(sourcePoint);
- newLine.addPoint(targetPoint);
- break;
- case OBLIQUE:
- super.routeLine(conn, nestedRoutingDepth, newLine);
- adjustCreateEndpoint(conn, newLine);
- // force 2 bendpoints only
- if (newLine.size() > 2) {
- sourcePoint = newLine.getFirstPoint();
- targetPoint = newLine.getLastPoint();
- newLine.removeAllPoints();
- newLine.addPoint(sourcePoint);
- newLine.addPoint(targetPoint);
- }
- break;
- case SELF:
- // Handle special routing: self connections and intersecting shapes connections
- if (checkSelfRelConnection(conn, newLine)) {
- super.resetEndPointsToEdge(conn, newLine);
- OrthogonalRouterUtilities.transformToOrthogonalPointList(newLine, getOffShapeDirection(getAnchorOffRectangleDirection(newLine.getFirstPoint(), sourceBoundsRelativeToConnection(conn))),
- getOffShapeDirection(getAnchorOffRectangleDirection(newLine.getLastPoint(), targetBoundsRelativeToConnection(conn))));
- removeRedundantPoints(newLine);
- return;
- }
- break;
- }
- }
-
-
-
- @Override
- protected boolean checkShapesIntersect(Connection conn, PointList newLine) {
- // Fixed bug about MessageLost and MessageFound.
- if (conn.getSourceAnchor() instanceof AnchorHelper.InnerPointAnchor || conn.getTargetAnchor() instanceof AnchorHelper.InnerPointAnchor) {
- return false;
- }
- if (conn.getTargetAnchor().getOwner() instanceof AnchorHelper.CombinedFragmentNodeFigure) {
- return false;
- }
- return super.checkShapesIntersect(conn, newLine);
- }
-
- protected void adjustCreateEndpoint(Connection conn, PointList newLine) {
- if (conn instanceof MessageCreate) {
- if (newLine.size() >= 2) {
- Point start = newLine.getFirstPoint();
- Point end = newLine.getLastPoint();
- if (start.y != end.y) {
- start.y = end.y;
- newLine.setPoint(start, 0);
- }
- }
- }
- }
-
- @Override
- protected void getSelfRelVertices(Connection conn, PointList newLine) {
- // Copy the points calculated from Bendpoints.
- PointList oldLine = newLine.getCopy();
- rectilinearResetEndPointsToEdge(conn, newLine);
- IFigure owner = conn.getSourceAnchor().getOwner();
- Point middle = owner.getBounds().getCenter();
- // ensure that the end points are anchored properly to the shape.
- Point ptS2 = newLine.getPoint(0);
- Point ptS1 = conn.getSourceAnchor().getReferencePoint();
- conn.translateToRelative(ptS1);
- Point ptAbsS2 = new Point(ptS2);
- conn.translateToAbsolute(ptAbsS2);
- Point ptEdge = conn.getSourceAnchor().getLocation(ptAbsS2);
- conn.translateToRelative(ptEdge);
- ptS1 = getStraightEdgePoint(ptEdge, ptS1, ptS2);
- Point ptE2 = newLine.getPoint(newLine.size() - 1);
- Point ptE1 = conn.getTargetAnchor().getReferencePoint();
- conn.translateToRelative(ptE1);
- Point ptAbsE2 = new Point(ptE2);
- conn.translateToAbsolute(ptAbsE2);
- ptEdge = conn.getTargetAnchor().getLocation(ptAbsE2);
- conn.translateToRelative(ptEdge);
- ptE1 = getStraightEdgePoint(ptEdge, ptE1, ptE2);
- newLine.removeAllPoints();
- newLine.addPoint(ptS1);
- // Fixed bug about custom self message. If there are 4 points, insert middle two ones for supporting bendpoints.
- if (oldLine.size() == 4) {
- Point p2 = oldLine.getPoint(1);
- Point p3 = oldLine.getPoint(2);
- int x = Math.min(p2.x, p3.x);
- newLine.addPoint(x, ptS1.y);
- newLine.addPoint(x, ptE1.y);
- } else {
- // insert two points
- Point extraPoint1 = ptS2.getTranslated(ptE2).scale(0.5);
- if (isOnRightHand(conn, owner, middle)) {
- extraPoint1.translate(SELFRELSIZEINIT, 0);
- } else {
- extraPoint1.translate(-SELFRELSIZEINIT, 0);
- }
- Point extraPoint2 = extraPoint1.getCopy();
- if (isFeedback(conn)) {
- extraPoint1.y = ptS2.y;
- extraPoint2.y = ptE2.y;
- } else {
- extraPoint1.y = ptS1.y;
- extraPoint2.y = ptE1.y;
- }
- newLine.addPoint(extraPoint1);
- newLine.addPoint(extraPoint2);
- }
- newLine.addPoint(ptE1);
- }
-
- protected boolean isOnRightHand(Connection conn, IFigure owner, Point middle) {
- boolean right = true;
- if (conn.getTargetAnchor() instanceof AnchorHelper.SideAnchor) {
- AnchorHelper.SideAnchor anchor = (AnchorHelper.SideAnchor) conn.getTargetAnchor();
- right = anchor.isRight();
- } else {
- PointList list = conn.getPoints();
- if (list.getPoint(0).x > list.getPoint(1).x) {
- right = false;
- }
- }
- return right;
- }
-
- @Override
- protected boolean checkSelfRelConnection(Connection conn, PointList newLine) {
- if (RouterKind.getKind(conn, newLine).equals(RouterKind.SELF)) {
- getSelfRelVertices(conn, newLine);
- return true;
- }
- return false;
- }
-
- /**
- * All the code after this comment is copied from RectilinearRouter and RouterHelper
- *
- * Copyright (c) 2002, 2010 IBM Corporation and others.
- */
- private void originalRectilinearRouteLine(Connection conn, int nestedRoutingDepth, PointList newLine) {
- boolean skipNormalization = (routerFlags & ROUTER_FLAG_SKIPNORMALIZATION) != 0;
- // if we are reorienting, then just default to the super class implementation and
- // don't try to do rectilinear routing.
- if (isReorienting(conn)) {
- super.routeLine(conn, nestedRoutingDepth, newLine);
- return;
- }
- /*
- * Remove and store former anchor points. Anchor points will be re-calculated anyway.
- * However, the old anchor points may be useful if connection didn't have any bend points
- * except the anchor points.
- */
- Point lastStartAnchor = newLine.removePoint(0);
- Point lastEndAnchor = newLine.removePoint(newLine.size() - 1);
- /*
- * Check if connection is rectilinear and if not make it rectilinear
- */
- if (!OrthogonalRouterUtilities.isRectilinear(newLine)) {
- OrthogonalRouterUtilities.transformToOrthogonalPointList(newLine, PositionConstants.NONE, PositionConstants.NONE);
- }
- removeRedundantPoints(newLine);
- /*
- * Remove unnecessary points that are contained within source and/or target shapes
- * as well as insert extra points if all points are within source and/or target shapes
- */
- removePointsInViews(conn, newLine, lastStartAnchor, lastEndAnchor);
- Dimension tolerance = new Dimension(3, 0);
- if (!isFeedback(conn)) {
- tolerance = (Dimension) MapModeUtil.getMapMode(conn).DPtoLP(tolerance);
- }
- /*
- * Normalize polyline to eliminate extra segments. (This makes 3 segments collapsing into
- * one, while line segments are moved)
- */
- if (!skipNormalization) {
- if (PointListUtilities.normalizeSegments(newLine, tolerance.width)) {
- /*
- * Normalization can make our polyline not rectilinear. Hence, we need to normalize
- * segments of polyline to straight line tolerance.
- */
- normalizeToStraightLineTolerance(newLine, tolerance.width);
- }
- }
- /*
- * Normalization is not touching the end points, hence we'd like to handle this here.
- * If distance between start and end (which are the only points in a polyline) points
- * is too short we'll remove one of the points
- */
- if (newLine.size() == 2) {
- Ray middleSeg = new Ray(newLine.getFirstPoint(), newLine.getLastPoint());
- if (middleSeg.length() <= tolerance.width) {
- newLine.removePoint(0);
- }
- }
- /*
- * Calculate connection anchor points and possibly some extra routing work to keep
- * the connection rectilinear if anchor points make it not rectilinear.
- */
- rectilinearResetEndPointsToEdge(conn, newLine);
- if (nestedRoutingDepth < 1 && !isValidRectilinearLine(conn, newLine)) {
- routeLine(conn, ++nestedRoutingDepth, newLine);
- }
- }
-
- /**
- * Rectilinear polyline is invalid if:
- * 1. First bend point is within the source
- * 2. Last bend point is within the target
- * 3. First bend point and source anchor are on different sides of the source shape
- * 4. Last bend point and target anchor are on different sides of the target shape
- *
- * @param conn
- * connection
- * @param line
- * rectilinear polyline
- * @return <code>true</code> if the line is valid
- */
- private boolean isValidRectilinearLine(Connection conn, PointList line) {
- if (!(conn.getSourceAnchor().getOwner() instanceof Connection)) {
- Rectangle source = new PrecisionRectangle(FigureUtilities.getAnchorableFigureBounds(conn.getSourceAnchor().getOwner()));
- conn.getSourceAnchor().getOwner().translateToAbsolute(source);
- conn.translateToRelative(source);
- if (source.contains(line.getPoint(1))) {
- return false;
- }
- int firstSegmentOrientation = line.getFirstPoint().x == line.getPoint(1).x ? PositionConstants.VERTICAL : PositionConstants.HORIZONTAL;
- if (getOutisePointOffRectanglePosition(line.getPoint(1), source) != getAnchorLocationBasedOnSegmentOrientation(line.getFirstPoint(), source, firstSegmentOrientation)) {
- return false;
- }
- }
- if (!(conn.getTargetAnchor().getOwner() instanceof Connection)) {
- Rectangle target = new PrecisionRectangle(FigureUtilities.getAnchorableFigureBounds(conn.getTargetAnchor().getOwner()));
- conn.getTargetAnchor().getOwner().translateToAbsolute(target);
- conn.translateToRelative(target);
- if (target.contains(line.getPoint(line.size() - 2))) {
- return false;
- }
- int lastSegmentOrientation = line.getLastPoint().x == line.getPoint(line.size() - 2).x ? PositionConstants.VERTICAL : PositionConstants.HORIZONTAL;
- if (getOutisePointOffRectanglePosition(line.getPoint(line.size() - 2), target) != getAnchorLocationBasedOnSegmentOrientation(line.getLastPoint(), target, lastSegmentOrientation)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Calculates geographic position of a point located outside the given rectangle relative
- * to the rectangle
- *
- * @param p
- * point outside of rectangle
- * @param r
- * the rectangle
- * @return geographic position of the point relative to the recatangle
- */
- private int getOutisePointOffRectanglePosition(Point p, Rectangle r) {
- int position = PositionConstants.NONE;
- if (r.x > p.x) {
- position |= PositionConstants.WEST;
- } else if (r.x + r.width < p.x) {
- position |= PositionConstants.EAST;
- }
- if (r.y > p.y) {
- position |= PositionConstants.NORTH;
- } else if (r.y + r.height < p.y) {
- position |= PositionConstants.SOUTH;
- }
- return position;
- }
-
- /**
- * Given the coordinates of the connection anchor point the shape's rectangle and the
- * orientation of the first rectilinear connection segment that comes out from the anchor
- * point the method detemines on which geographic side of the rectangle the anchor point
- * is located on.
- *
- * @param anchorPoint
- * coordinates of the anchor point
- * @param rectangle
- * the shape's bounding rectangle
- * @param segmentOrientation
- * orinetation of the segment coming out from the anchor point
- * @return geographic position of the anchor point relative to the rectangle
- */
- private int getAnchorLocationBasedOnSegmentOrientation(Point anchorPoint, Rectangle rectangle, int segmentOrientation) {
- if (segmentOrientation == PositionConstants.VERTICAL) {
- if (Math.abs(anchorPoint.y - rectangle.y) < Math.abs(anchorPoint.y - rectangle.y - rectangle.height)) {
- return PositionConstants.NORTH;
- } else {
- return PositionConstants.SOUTH;
- }
- } else if (segmentOrientation == PositionConstants.HORIZONTAL) {
- if (Math.abs(anchorPoint.x - rectangle.x) < Math.abs(anchorPoint.x - rectangle.x - rectangle.width)) {
- return PositionConstants.WEST;
- } else {
- return PositionConstants.EAST;
- }
- }
- return PositionConstants.NONE;
- }
-
- /**
- * Goes through line segments of a polyline and makes strict straight segments
- * from nearly straight segments.
- *
- * @param line
- * polyline
- * @param tolerance
- * tolerance value specifying nearly straight lines.
- */
- private void normalizeToStraightLineTolerance(PointList line, int tolerance) {
- for (int i = 0; i < line.size() - 1; i++) {
- Point pt1 = line.getPoint(i);
- Point pt2 = line.getPoint(i + 1);
- if (Math.abs(pt1.x - pt2.x) < tolerance) {
- line.setPoint(new Point(pt1.x, pt2.y), i + 1);
- } else if (Math.abs(pt1.y - pt2.y) < tolerance) {
- line.setPoint(new Point(pt2.x, pt1.y), i + 1);
- }
- }
- }
-
- /**
- * Removes consecutive points contained within the source shape and removes consecutive
- * points contained within the target shape. If all points have been removed an extra point
- * outside source and target shapes will be added.
- *
- * @param conn
- * connection
- * @param newLine
- * polyline of the connection (routed connection)
- * @param start
- * old start anchor point
- * @param end
- * old end anchor point
- */
- private void removePointsInViews(Connection conn, PointList newLine, Point start, Point end) {
- /*
- * Get the bounds of anchorable figure of the source and target and translate it to
- * connection relative coordinates.
- */
- PrecisionRectangle source = conn.getSourceAnchor().getOwner() != null ? new PrecisionRectangle(FigureUtilities.getAnchorableFigureBounds(conn.getSourceAnchor().getOwner())) : null;
- PrecisionRectangle target = conn.getTargetAnchor().getOwner() != null ? new PrecisionRectangle(FigureUtilities.getAnchorableFigureBounds(conn.getTargetAnchor().getOwner())) : null;
- if (source != null) {
- conn.getSourceAnchor().getOwner().translateToAbsolute(source);
- conn.translateToRelative(source);
- }
- if (target != null) {
- conn.getTargetAnchor().getOwner().translateToAbsolute(target);
- conn.translateToRelative(target);
- }
- Point lastRemovedFromSource = null;
- Point lastRemovedFromTarget = null;
- /*
- * Starting from the first point of polyline remove points that are contained
- * within the source shape until the first point outside is found.
- * Remember the point that was removed from the source shape last for a possible
- * case of all points removed from polyline.
- */
- if (!(conn.getSourceAnchor().getOwner() instanceof Connection) && newLine.size() != 0 && source.contains(new PrecisionPoint(newLine.getFirstPoint()))) {
- lastRemovedFromSource = newLine.removePoint(0);
- for (int i = 0; i < newLine.size() && source.contains(new PrecisionPoint(newLine.getPoint(i))); i++) {
- lastRemovedFromSource = newLine.removePoint(i--);
- }
- }
- /*
- * Starting from the end point of polyline remove points that are contained
- * within the target shape until the first point outside is found.
- * Remember the point that was removed from the target shape last for a possible
- * case of all points removed from polyline.
- */
- if (!(conn.getTargetAnchor().getOwner() instanceof Connection) && newLine.size() != 0 && target.contains(new PrecisionPoint(newLine.getLastPoint()))) {
- lastRemovedFromTarget = newLine.removePoint(newLine.size() - 1);
- for (int i = newLine.size(); i > 0 && target.contains(new PrecisionPoint(newLine.getPoint(i - 1))); i--) {
- lastRemovedFromTarget = newLine.removePoint(i - 1);
- }
- }
- /*
- * Handle the special case of all points removed from polyline.
- */
- if (newLine.size() == 0) {
- Dimension tolerance = new Dimension(1, 0);
- if (!isFeedback(conn)) {
- tolerance = (Dimension) MapModeUtil.getMapMode(conn).DPtoLP(tolerance);
- }
- int toleranceValue = tolerance.width;
- if (lastRemovedFromSource == null) {
- lastRemovedFromSource = start;
- }
- if (lastRemovedFromTarget == null) {
- lastRemovedFromTarget = end;
- }
- /*
- * If last point removed from source and the points removed from target form
- * a vertical or horizontal line we'll find a point located on this line and is
- * outside of source and target shape and insert it in the polyline.
- * The check for vertical and horizontal segment is using tolerance value, because
- * bend point location extracted from RelativeBendpoint can have precision errors due
- * to non-integer weight factors.
- */
- if (Math.abs(lastRemovedFromSource.x - lastRemovedFromTarget.x) < toleranceValue) {
- // Vertical
- if (source.preciseY < target.preciseY) {
- newLine.addPoint(lastRemovedFromSource.x, (source.getBottom().y + target.getTop().y) / 2);
- } else {
- newLine.addPoint(lastRemovedFromSource.x, (source.getTop().y + target.getBottom().y) / 2);
- }
- } else if (Math.abs(lastRemovedFromSource.y - lastRemovedFromTarget.y) < toleranceValue) {
- // Horizontal
- if (source.preciseX < target.preciseX) {
- newLine.addPoint((source.getRight().x + target.getLeft().x) / 2, lastRemovedFromSource.y);
- } else {
- newLine.addPoint((source.getLeft().x + target.getRight().x) / 2, lastRemovedFromSource.y);
- }
- } else if ((conn.getSourceAnchor() instanceof BaseSlidableAnchor && StringStatics.BLANK.equals(((BaseSlidableAnchor) conn.getSourceAnchor()).getTerminal()) && (conn.getTargetAnchor() instanceof BaseSlidableAnchor && StringStatics.BLANK
- .equals(((BaseSlidableAnchor) conn.getTargetAnchor()).getTerminal())))) {
- /*
- * This a special case for old diagrams with rectilinear connections routed by
- * the old router to look good with the new router
- */
- if (lastRemovedFromSource != null && lastRemovedFromTarget != null) {
- newLine.addPoint((lastRemovedFromSource.x + lastRemovedFromTarget.x) / 2, (lastRemovedFromSource.y + lastRemovedFromTarget.y) / 2);
- } else {
- double startX = Math.max(source.preciseX, target.preciseX);
- double endX = Math.min(source.preciseX + source.preciseWidth, target.preciseX + target.preciseWidth);
- double startY = Math.max(source.preciseY, target.preciseY);
- double endY = Math.min(source.preciseY + source.preciseHeight, target.preciseY + target.preciseHeight);
- if (startX < endX) {
- if (source.preciseY < target.preciseY) {
- newLine.addPoint((int) Math.round((startX + endX) / 2.0), (source.getBottom().y + target.getTop().y) / 2);
- } else {
- newLine.addPoint((int) Math.round((startX + endX) / 2.0), (source.getTop().y + target.getBottom().y) / 2);
- }
- } else if (startY < endY) {
- if (source.preciseX < target.preciseX) {
- newLine.addPoint((source.getRight().x + target.getLeft().x) / 2, (int) Math.round((startY + endY) / 2.0));
- } else {
- newLine.addPoint((source.getLeft().x + target.getRight().x) / 2, (int) Math.round((startY + endY) / 2.0));
- }
- }
- }
- }
- }
- }
-
- protected void rectilinearResetEndPointsToEdge(Connection conn, PointList line) {
- if (isReorienting(conn)) {
- /*
- * If the connection doesn't have a shape as a source or target we'll
- * let the oblique router to do the work. The connection doesn't need to
- * be rectilinear at this point. There is no support for making a rectilinear
- * connection for which one of the ends is not connected to anything.
- */
- super.resetEndPointsToEdge(conn, line);
- return;
- }
- PrecisionRectangle source = sourceBoundsRelativeToConnection(conn);
- PrecisionRectangle target = targetBoundsRelativeToConnection(conn);
- int offSourceDirection = PositionConstants.NONE;
- int offTargetDirection = PositionConstants.NONE;
- int sourceAnchorRelativeLocation = PositionConstants.NONE;
- int targetAnchorRelativeLocation = PositionConstants.NONE;
- if (line.size() == 0) {
- /*
- * If there are no valid bend points, we'll use the oblique connection anchor points
- * and just convert the polyline from oblique to rectilinear.
- */
- // Need to add 2 dumb points to ensure that RouterHelper#resetEndPointsToEdge works
- line.addPoint(new Point());
- line.addPoint(new Point());
- super.resetEndPointsToEdge(conn, line);
- sourceAnchorRelativeLocation = getAnchorOffRectangleDirection(line.getFirstPoint(), source);
- targetAnchorRelativeLocation = getAnchorOffRectangleDirection(line.getLastPoint(), target);
- /*
- * We need to find two points offset from the source and target anchors outside the shapes
- * such that when the polyline is converted to rectilinear from oblique we won't have
- * rectilinear line segments alligned with source or target shapes edges.
- */
- Point offStart = line.getFirstPoint();
- Point offEnd = line.getLastPoint();
- Dimension offsetDim = offStart.getDifference(offEnd).scale(0.5);
- offStart.translate(getTranslationValue(sourceAnchorRelativeLocation, Math.abs(offsetDim.width), Math.abs(offsetDim.height)));
- offEnd.translate(getTranslationValue(targetAnchorRelativeLocation, Math.abs(offsetDim.width), Math.abs(offsetDim.height)));
- line.insertPoint(offStart, 1);
- line.insertPoint(offEnd, 2);
- offSourceDirection = getOffShapeDirection(sourceAnchorRelativeLocation);
- offTargetDirection = getOffShapeDirection(targetAnchorRelativeLocation);
- } else {
- Point start = line.getFirstPoint();
- Point end = line.getLastPoint();
- if (conn.getSourceAnchor() instanceof OrthogonalConnectionAnchor) {
- line.insertPoint(OrthogonalRouterUtilities.getOrthogonalLineSegToAnchorLoc(conn, conn.getSourceAnchor(), start).getOrigin(), 0);
- } else {
- /*
- * If anchor is not supporting orthogonal connections we'll use the oblique connection
- * anchors and then convert it to rectilinear.
- */
- PrecisionPoint reference = new PrecisionPoint(start);
- conn.getSourceAnchor().getOwner().translateToAbsolute(reference);
- PrecisionPoint anchorLocation = new PrecisionPoint(conn.getSourceAnchor().getLocation(reference));
- conn.translateToRelative(anchorLocation);
- line.insertPoint(anchorLocation, 0);
- }
- if (conn.getTargetAnchor() instanceof OrthogonalConnectionAnchor) {
- line.addPoint(OrthogonalRouterUtilities.getOrthogonalLineSegToAnchorLoc(conn, conn.getTargetAnchor(), end).getOrigin());
- } else {
- /*
- * If anchor is not supporting orthogonal connections we'll use the oblique connection
- * anchors and then convert it to rectilinear.
- */
- PrecisionPoint reference = new PrecisionPoint(end);
- conn.getSourceAnchor().getOwner().translateToAbsolute(reference);
- PrecisionPoint anchorLocation = new PrecisionPoint(conn.getTargetAnchor().getLocation(reference));
- conn.translateToRelative(anchorLocation);
- line.addPoint(anchorLocation);
- }
- sourceAnchorRelativeLocation = getAnchorOffRectangleDirection(line.getFirstPoint(), source);
- offSourceDirection = getOffShapeDirection(sourceAnchorRelativeLocation);
- targetAnchorRelativeLocation = getAnchorOffRectangleDirection(line.getLastPoint(), target);
- offTargetDirection = getOffShapeDirection(targetAnchorRelativeLocation);
- }
- /*
- * Convert the polyline to rectilinear. If the connection is rectilinear already then the
- * connection will remain as it is.
- */
- OrthogonalRouterUtilities.transformToOrthogonalPointList(line, offSourceDirection, offTargetDirection);
- removeRedundantPoints(line);
- }
-
- /**
- * Returns a translation dimension for the anchor point. Translation dimension
- * translates the anchor point off the shape. The off shape direction
- * is specified by the relative to the shape geographic position of the anchor
- *
- * @param position
- * relative to the shape geographic position of the anchor
- * @param xFactorValue
- * translation value along x-axis
- * @param yFactorValue
- * translation value along y-axis
- * @return
- */
- private Dimension getTranslationValue(int position, int xFactorValue, int yFactorValue) {
- Dimension translationDimension = new Dimension();
- if (position == PositionConstants.EAST) {
- translationDimension.width = xFactorValue;
- } else if (position == PositionConstants.SOUTH) {
- translationDimension.height = yFactorValue;
- } else if (position == PositionConstants.WEST) {
- translationDimension.width = -xFactorValue;
- } else if (position == PositionConstants.NORTH) {
- translationDimension.height = -yFactorValue;
- }
- return translationDimension;
- }
-
- /**
- * Target bounding rectangle relative to connection figure coordinates
- *
- * @param conn
- * connection
- * @return <code>PrecisionRectangle</code> target bounds relative to connection's coordinate
- * system
- */
- private PrecisionRectangle targetBoundsRelativeToConnection(Connection conn) {
- PrecisionRectangle target = new PrecisionRectangle(conn.getTargetAnchor().getOwner().getBounds());
- conn.getTargetAnchor().getOwner().translateToAbsolute(target);
- conn.translateToRelative(target);
- return target;
- }
-
- /**
- * Iterates through points of a polyline and does the following:
- * if 3 points lie on the same line the middle point is removed
- *
- * @param line
- * polyline's points
- */
- private boolean removeRedundantPoints(PointList line) {
- int initialNumberOfPoints = line.size();
- if (line.size() > 2) {
- PointList newLine = new PointList(line.size());
- newLine.addPoint(line.removePoint(0));
- while (line.size() >= 2) {
- Point p0 = newLine.getLastPoint();
- Point p1 = line.getPoint(0);
- Point p2 = line.getPoint(1);
- if (p0.x == p1.x && p0.x == p2.x) {
- // Have two vertical segments in a row
- // get rid of the point between
- line.removePoint(0);
- } else if (p0.y == p1.y && p0.y == p2.y) {
- // Have two horizontal segments in a row
- // get rid of the point between
- line.removePoint(0);
- } else {
- newLine.addPoint(line.removePoint(0));
- }
- }
- while (line.size() > 0) {
- newLine.addPoint(line.removePoint(0));
- }
- line.removeAllPoints();
- line.addAll(newLine);
- }
- return line.size() != initialNumberOfPoints;
- }
-
- /**
- * Determines whether the rectilinear line segment coming out of the shape should be
- * horizontal or vertical based on the anchor geographic position relative to the shape
- *
- * @param anchorRelativeLocation
- * @return
- */
- private int getOffShapeDirection(int anchorRelativeLocation) {
- if (anchorRelativeLocation == PositionConstants.EAST || anchorRelativeLocation == PositionConstants.WEST) {
- return PositionConstants.HORIZONTAL;
- } else if (anchorRelativeLocation == PositionConstants.NORTH || anchorRelativeLocation == PositionConstants.SOUTH) {
- return PositionConstants.VERTICAL;
- }
- return PositionConstants.NONE;
- }
-
- /**
- * Source bounding rectangle relative to connection figure coordinates
- *
- * @param conn
- * connection
- * @return <code>PrecisionRectangle</code> source bounds relative to connection's coordinate
- * system
- */
- private PrecisionRectangle sourceBoundsRelativeToConnection(Connection conn) {
- PrecisionRectangle source = new PrecisionRectangle(conn.getSourceAnchor().getOwner().getBounds());
- conn.getSourceAnchor().getOwner().translateToAbsolute(source);
- conn.translateToRelative(source);
- return source;
- }
-
- /**
- * Determines the relative to rectangle geographic location of a point.
- * Example: If shape is closer to the the top edge of the rectangle location
- * would be north.
- * Method used to determine which side of shape's bounding rectangle is closer
- * to connection's anchor point.
- * All geometric quantities must be in the same coordinate system.
- *
- * @param anchorPoint
- * location of the anchor point
- * @param rect
- * bounding rectangle of the shape
- * @return
- */
- private int getAnchorOffRectangleDirection(Point anchorPoint, Rectangle rect) {
- int position = PositionConstants.NORTH;
- int criteriaValue = Math.abs(anchorPoint.y - rect.y);
- int tempCriteria = Math.abs(anchorPoint.y - rect.y - rect.height);
- if (tempCriteria < criteriaValue) {
- criteriaValue = tempCriteria;
- position = PositionConstants.SOUTH;
- }
- tempCriteria = Math.abs(anchorPoint.x - rect.x);
- if (tempCriteria < criteriaValue) {
- criteriaValue = tempCriteria;
- position = PositionConstants.WEST;
- }
- tempCriteria = Math.abs(anchorPoint.x - rect.x - rect.width);
- if (tempCriteria < criteriaValue) {
- criteriaValue = tempCriteria;
- position = PositionConstants.EAST;
- }
- return position;
- }
-
- /**
- * @param conn
- * the <code>Connection</code> that is to be check if it is a feedback
- * connection or not.
- * @return <code>true</code> is it is a feedback connection, <code>false</code> otherwise.
- */
- private static boolean isFeedback(Connection conn) {
- Dimension dim = new Dimension(100, 100);
- Dimension dimCheck = dim.getCopy();
- conn.translateToRelative(dimCheck);
- return dim.equals(dimCheck);
- }
-}
+/*****************************************************************************
+ * Copyright (c) 2010 CEA
+ *
+ *
+ * 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:
+ * Atos Origin - Initial API and implementation
+ *
+ *****************************************************************************/
+package org.eclipse.papyrus.uml.diagram.sequence.draw2d.routers;
+
+import org.eclipse.draw2d.Connection;
+import org.eclipse.draw2d.IFigure;
+import org.eclipse.draw2d.PositionConstants;
+import org.eclipse.draw2d.geometry.Dimension;
+import org.eclipse.draw2d.geometry.Point;
+import org.eclipse.draw2d.geometry.PointList;
+import org.eclipse.draw2d.geometry.PrecisionPoint;
+import org.eclipse.draw2d.geometry.PrecisionRectangle;
+import org.eclipse.draw2d.geometry.Ray;
+import org.eclipse.draw2d.geometry.Rectangle;
+import org.eclipse.gmf.runtime.common.core.util.StringStatics;
+import org.eclipse.gmf.runtime.draw2d.ui.figures.BaseSlidableAnchor;
+import org.eclipse.gmf.runtime.draw2d.ui.figures.FigureUtilities;
+import org.eclipse.gmf.runtime.draw2d.ui.figures.OrthogonalConnectionAnchor;
+import org.eclipse.gmf.runtime.draw2d.ui.geometry.PointListUtilities;
+import org.eclipse.gmf.runtime.draw2d.ui.internal.routers.ObliqueRouter;
+import org.eclipse.gmf.runtime.draw2d.ui.internal.routers.OrthogonalRouterUtilities;
+import org.eclipse.gmf.runtime.draw2d.ui.mapmode.MapModeUtil;
+import org.eclipse.papyrus.uml.diagram.sequence.edit.helpers.AnchorHelper;
+import org.eclipse.papyrus.uml.diagram.sequence.figures.LifelineFigure;
+import org.eclipse.papyrus.uml.diagram.sequence.figures.MessageAsync;
+import org.eclipse.papyrus.uml.diagram.sequence.figures.MessageCreate;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * A multi behavior router which enable to draw message.
+ * It can behave as an oblique router (with no bendpoint), or as an horizontal router (with no bendpoint),
+ * or as a rectilinear router with 2 bendpoints.
+ *
+ * @author mvelten and vhemery
+ */
+@SuppressWarnings({ "restriction", "deprecation" })
+public class MessageRouter extends ObliqueRouter {
+
+ private static final int MAX_DELTA = 65;
+ private static final int PRECISION_DELTA = 10;
+
+ private static boolean precisionMode;
+
+ public static enum RouterKind {
+ HORIZONTAL, OBLIQUE, SELF;
+
+ public static RouterKind getKind(Connection conn, PointList newLine) {
+
+
+ PlatformUI.getWorkbench().getDisplay().addFilter(SWT.KeyDown, new Listener() {
+
+ @Override
+ public void handleEvent(Event event) {
+ // in case the SHIFT key is down, the creation enter in the precision Mode
+ precisionMode = (event.keyCode == SWT.SHIFT);
+ }
+ });
+
+
+ PlatformUI.getWorkbench().getDisplay().addFilter(SWT.KeyUp, new Listener() {
+
+ @Override
+ public void handleEvent(Event event) {
+ // in case the SHIFT key is released, the creation mode goes back to normal
+ if (event.keyCode == SWT.SHIFT) {
+ precisionMode = false;
+ }
+ }
+ });
+
+
+
+
+ if (isSelfConnection(conn)) {
+
+ return SELF;
+ }
+ if (isHorizontalConnection(conn, newLine)) {
+
+ return HORIZONTAL;
+ }
+
+ return OBLIQUE;
+ }
+
+
+ /**
+ * isHorizontalConnection tests whether an asynchronous message is horizontal
+ * @param conn controller representing the link
+ * @param newLine points corresponding to message ends
+ * @return false if message is not asynchronous
+ * true if the message is asynchronous and horizontal
+ */
+ private static boolean isHorizontalConnection(Connection conn, PointList newLine) {
+ boolean horizontal = true;
+
+ if (!(conn instanceof MessageAsync)) {
+ return false;
+ }
+ Point sourcePoint = newLine.getFirstPoint();
+ Point targetPoint = newLine.getLastPoint();
+
+ int realDelta = Math.abs(sourcePoint.y - targetPoint.y);
+
+ if (!precisionMode) {
+ horizontal = (realDelta <= MAX_DELTA);
+
+ } else {
+ horizontal = (realDelta <= PRECISION_DELTA);
+ }
+
+ return horizontal;
+ }
+
+ /**
+ * It is self if the parent lifeline is the same.
+ */
+ private static boolean isSelfConnection(Connection conn) {
+ if (conn == null || conn.getSourceAnchor() == null || conn.getTargetAnchor() == null) {
+ return false;
+ }
+ IFigure sourceLifeline = conn.getSourceAnchor().getOwner();
+ while (sourceLifeline != null && !(sourceLifeline instanceof LifelineFigure)) {
+ sourceLifeline = sourceLifeline.getParent();
+ }
+ IFigure targetLifeline = conn.getTargetAnchor().getOwner();
+ while (targetLifeline != null && !(targetLifeline instanceof LifelineFigure)) {
+ targetLifeline = targetLifeline.getParent();
+ }
+ return sourceLifeline != null && sourceLifeline.equals(targetLifeline);
+ }
+ }
+
+ @Override
+ public void routeLine(Connection conn, int nestedRoutingDepth, PointList newLine) {
+ Point sourcePoint, targetPoint;
+ switch (RouterKind.getKind(conn, newLine)) {
+ case HORIZONTAL:
+ originalRectilinearRouteLine(conn, nestedRoutingDepth, newLine);
+ // force 2 bendpoints on the same Y coordinate
+ sourcePoint = newLine.getFirstPoint();
+ targetPoint = newLine.getLastPoint();
+ targetPoint.y = sourcePoint.y;
+ newLine.removeAllPoints();
+ newLine.addPoint(sourcePoint);
+ newLine.addPoint(targetPoint);
+ break;
+ case OBLIQUE:
+ super.routeLine(conn, nestedRoutingDepth, newLine);
+ adjustCreateEndpoint(conn, newLine);
+ // force 2 bendpoints only
+ if (newLine.size() > 2) {
+ sourcePoint = newLine.getFirstPoint();
+ targetPoint = newLine.getLastPoint();
+ newLine.removeAllPoints();
+ newLine.addPoint(sourcePoint);
+ newLine.addPoint(targetPoint);
+ }
+ break;
+ case SELF:
+ // Handle special routing: self connections and intersecting shapes connections
+ if (checkSelfRelConnection(conn, newLine)) {
+ super.resetEndPointsToEdge(conn, newLine);
+ OrthogonalRouterUtilities.transformToOrthogonalPointList(newLine, getOffShapeDirection(getAnchorOffRectangleDirection(newLine.getFirstPoint(), sourceBoundsRelativeToConnection(conn))),
+ getOffShapeDirection(getAnchorOffRectangleDirection(newLine.getLastPoint(), targetBoundsRelativeToConnection(conn))));
+ removeRedundantPoints(newLine);
+ return;
+ }
+ break;
+ }
+ }
+
+
+
+ @Override
+ protected boolean checkShapesIntersect(Connection conn, PointList newLine) {
+ // Fixed bug about MessageLost and MessageFound.
+ if (conn.getSourceAnchor() instanceof AnchorHelper.InnerPointAnchor || conn.getTargetAnchor() instanceof AnchorHelper.InnerPointAnchor) {
+ return false;
+ }
+ if (conn.getTargetAnchor().getOwner() instanceof AnchorHelper.CombinedFragmentNodeFigure) {
+ return false;
+ }
+ return super.checkShapesIntersect(conn, newLine);
+ }
+
+ protected void adjustCreateEndpoint(Connection conn, PointList newLine) {
+ if (conn instanceof MessageCreate) {
+ if (newLine.size() >= 2) {
+ Point start = newLine.getFirstPoint();
+ Point end = newLine.getLastPoint();
+ if (start.y != end.y) {
+ start.y = end.y;
+ newLine.setPoint(start, 0);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void getSelfRelVertices(Connection conn, PointList newLine) {
+ // Copy the points calculated from Bendpoints.
+ PointList oldLine = newLine.getCopy();
+ rectilinearResetEndPointsToEdge(conn, newLine);
+ IFigure owner = conn.getSourceAnchor().getOwner();
+ Point middle = owner.getBounds().getCenter();
+ // ensure that the end points are anchored properly to the shape.
+ Point ptS2 = newLine.getPoint(0);
+ Point ptS1 = conn.getSourceAnchor().getReferencePoint();
+ conn.translateToRelative(ptS1);
+ Point ptAbsS2 = new Point(ptS2);
+ conn.translateToAbsolute(ptAbsS2);
+ Point ptEdge = conn.getSourceAnchor().getLocation(ptAbsS2);
+ conn.translateToRelative(ptEdge);
+ ptS1 = getStraightEdgePoint(ptEdge, ptS1, ptS2);
+ Point ptE2 = newLine.getPoint(newLine.size() - 1);
+ Point ptE1 = conn.getTargetAnchor().getReferencePoint();
+ conn.translateToRelative(ptE1);
+ Point ptAbsE2 = new Point(ptE2);
+ conn.translateToAbsolute(ptAbsE2);
+ ptEdge = conn.getTargetAnchor().getLocation(ptAbsE2);
+ conn.translateToRelative(ptEdge);
+ ptE1 = getStraightEdgePoint(ptEdge, ptE1, ptE2);
+ newLine.removeAllPoints();
+ newLine.addPoint(ptS1);
+ // Fixed bug about custom self message. If there are 4 points, insert middle two ones for supporting bendpoints.
+ if (oldLine.size() == 4) {
+ Point p2 = oldLine.getPoint(1);
+ Point p3 = oldLine.getPoint(2);
+ int x = Math.min(p2.x, p3.x);
+ newLine.addPoint(x, ptS1.y);
+ newLine.addPoint(x, ptE1.y);
+ } else {
+ // insert two points
+ Point extraPoint1 = ptS2.getTranslated(ptE2).scale(0.5);
+ if (isOnRightHand(conn, owner, middle)) {
+ extraPoint1.translate(SELFRELSIZEINIT, 0);
+ } else {
+ extraPoint1.translate(-SELFRELSIZEINIT, 0);
+ }
+ Point extraPoint2 = extraPoint1.getCopy();
+ if (isFeedback(conn)) {
+ extraPoint1.y = ptS2.y;
+ extraPoint2.y = ptE2.y;
+ } else {
+ extraPoint1.y = ptS1.y;
+ extraPoint2.y = ptE1.y;
+ }
+ newLine.addPoint(extraPoint1);
+ newLine.addPoint(extraPoint2);
+ }
+ newLine.addPoint(ptE1);
+ }
+
+ protected boolean isOnRightHand(Connection conn, IFigure owner, Point middle) {
+ boolean right = true;
+ if (conn.getTargetAnchor() instanceof AnchorHelper.SideAnchor) {
+ AnchorHelper.SideAnchor anchor = (AnchorHelper.SideAnchor) conn.getTargetAnchor();
+ right = anchor.isRight();
+ } else {
+ PointList list = conn.getPoints();
+ if (list.getPoint(0).x > list.getPoint(1).x) {
+ right = false;
+ }
+ }
+ return right;
+ }
+
+ @Override
+ protected boolean checkSelfRelConnection(Connection conn, PointList newLine) {
+ if (RouterKind.getKind(conn, newLine).equals(RouterKind.SELF)) {
+ getSelfRelVertices(conn, newLine);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * All the code after this comment is copied from RectilinearRouter and RouterHelper
+ *
+ * Copyright (c) 2002, 2010 IBM Corporation and others.
+ */
+ private void originalRectilinearRouteLine(Connection conn, int nestedRoutingDepth, PointList newLine) {
+ boolean skipNormalization = (routerFlags & ROUTER_FLAG_SKIPNORMALIZATION) != 0;
+ // if we are reorienting, then just default to the super class implementation and
+ // don't try to do rectilinear routing.
+ if (isReorienting(conn)) {
+ super.routeLine(conn, nestedRoutingDepth, newLine);
+ return;
+ }
+ /*
+ * Remove and store former anchor points. Anchor points will be re-calculated anyway.
+ * However, the old anchor points may be useful if connection didn't have any bend points
+ * except the anchor points.
+ */
+ Point lastStartAnchor = newLine.removePoint(0);
+ Point lastEndAnchor = newLine.removePoint(newLine.size() - 1);
+ /*
+ * Check if connection is rectilinear and if not make it rectilinear
+ */
+ if (!OrthogonalRouterUtilities.isRectilinear(newLine)) {
+ OrthogonalRouterUtilities.transformToOrthogonalPointList(newLine, PositionConstants.NONE, PositionConstants.NONE);
+ }
+ removeRedundantPoints(newLine);
+ /*
+ * Remove unnecessary points that are contained within source and/or target shapes
+ * as well as insert extra points if all points are within source and/or target shapes
+ */
+ removePointsInViews(conn, newLine, lastStartAnchor, lastEndAnchor);
+ Dimension tolerance = new Dimension(3, 0);
+ if (!isFeedback(conn)) {
+ tolerance = (Dimension) MapModeUtil.getMapMode(conn).DPtoLP(tolerance);
+ }
+ /*
+ * Normalize polyline to eliminate extra segments. (This makes 3 segments collapsing into
+ * one, while line segments are moved)
+ */
+ if (!skipNormalization) {
+ if (PointListUtilities.normalizeSegments(newLine, tolerance.width)) {
+ /*
+ * Normalization can make our polyline not rectilinear. Hence, we need to normalize
+ * segments of polyline to straight line tolerance.
+ */
+ normalizeToStraightLineTolerance(newLine, tolerance.width);
+ }
+ }
+ /*
+ * Normalization is not touching the end points, hence we'd like to handle this here.
+ * If distance between start and end (which are the only points in a polyline) points
+ * is too short we'll remove one of the points
+ */
+ if (newLine.size() == 2) {
+ Ray middleSeg = new Ray(newLine.getFirstPoint(), newLine.getLastPoint());
+ if (middleSeg.length() <= tolerance.width) {
+ newLine.removePoint(0);
+ }
+ }
+ /*
+ * Calculate connection anchor points and possibly some extra routing work to keep
+ * the connection rectilinear if anchor points make it not rectilinear.
+ */
+ rectilinearResetEndPointsToEdge(conn, newLine);
+ if (nestedRoutingDepth < 1 && !isValidRectilinearLine(conn, newLine)) {
+ routeLine(conn, ++nestedRoutingDepth, newLine);
+ }
+ }
+
+ /**
+ * Rectilinear polyline is invalid if:
+ * 1. First bend point is within the source
+ * 2. Last bend point is within the target
+ * 3. First bend point and source anchor are on different sides of the source shape
+ * 4. Last bend point and target anchor are on different sides of the target shape
+ *
+ * @param conn
+ * connection
+ * @param line
+ * rectilinear polyline
+ * @return <code>true</code> if the line is valid
+ */
+ private boolean isValidRectilinearLine(Connection conn, PointList line) {
+ if (!(conn.getSourceAnchor().getOwner() instanceof Connection)) {
+ Rectangle source = new PrecisionRectangle(FigureUtilities.getAnchorableFigureBounds(conn.getSourceAnchor().getOwner()));
+ conn.getSourceAnchor().getOwner().translateToAbsolute(source);
+ conn.translateToRelative(source);
+ if (source.contains(line.getPoint(1))) {
+ return false;
+ }
+ int firstSegmentOrientation = line.getFirstPoint().x == line.getPoint(1).x ? PositionConstants.VERTICAL : PositionConstants.HORIZONTAL;
+ if (getOutisePointOffRectanglePosition(line.getPoint(1), source) != getAnchorLocationBasedOnSegmentOrientation(line.getFirstPoint(), source, firstSegmentOrientation)) {
+ return false;
+ }
+ }
+ if (!(conn.getTargetAnchor().getOwner() instanceof Connection)) {
+ Rectangle target = new PrecisionRectangle(FigureUtilities.getAnchorableFigureBounds(conn.getTargetAnchor().getOwner()));
+ conn.getTargetAnchor().getOwner().translateToAbsolute(target);
+ conn.translateToRelative(target);
+ if (target.contains(line.getPoint(line.size() - 2))) {
+ return false;
+ }
+ int lastSegmentOrientation = line.getLastPoint().x == line.getPoint(line.size() - 2).x ? PositionConstants.VERTICAL : PositionConstants.HORIZONTAL;
+ if (getOutisePointOffRectanglePosition(line.getPoint(line.size() - 2), target) != getAnchorLocationBasedOnSegmentOrientation(line.getLastPoint(), target, lastSegmentOrientation)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Calculates geographic position of a point located outside the given rectangle relative
+ * to the rectangle
+ *
+ * @param p
+ * point outside of rectangle
+ * @param r
+ * the rectangle
+ * @return geographic position of the point relative to the recatangle
+ */
+ private int getOutisePointOffRectanglePosition(Point p, Rectangle r) {
+ int position = PositionConstants.NONE;
+ if (r.x > p.x) {
+ position |= PositionConstants.WEST;
+ } else if (r.x + r.width < p.x) {
+ position |= PositionConstants.EAST;
+ }
+ if (r.y > p.y) {
+ position |= PositionConstants.NORTH;
+ } else if (r.y + r.height < p.y) {
+ position |= PositionConstants.SOUTH;
+ }
+ return position;
+ }
+
+ /**
+ * Given the coordinates of the connection anchor point the shape's rectangle and the
+ * orientation of the first rectilinear connection segment that comes out from the anchor
+ * point the method detemines on which geographic side of the rectangle the anchor point
+ * is located on.
+ *
+ * @param anchorPoint
+ * coordinates of the anchor point
+ * @param rectangle
+ * the shape's bounding rectangle
+ * @param segmentOrientation
+ * orinetation of the segment coming out from the anchor point
+ * @return geographic position of the anchor point relative to the rectangle
+ */
+ private int getAnchorLocationBasedOnSegmentOrientation(Point anchorPoint, Rectangle rectangle, int segmentOrientation) {
+ if (segmentOrientation == PositionConstants.VERTICAL) {
+ if (Math.abs(anchorPoint.y - rectangle.y) < Math.abs(anchorPoint.y - rectangle.y - rectangle.height)) {
+ return PositionConstants.NORTH;
+ } else {
+ return PositionConstants.SOUTH;
+ }
+ } else if (segmentOrientation == PositionConstants.HORIZONTAL) {
+ if (Math.abs(anchorPoint.x - rectangle.x) < Math.abs(anchorPoint.x - rectangle.x - rectangle.width)) {
+ return PositionConstants.WEST;
+ } else {
+ return PositionConstants.EAST;
+ }
+ }
+ return PositionConstants.NONE;
+ }
+
+ /**
+ * Goes through line segments of a polyline and makes strict straight segments
+ * from nearly straight segments.
+ *
+ * @param line
+ * polyline
+ * @param tolerance
+ * tolerance value specifying nearly straight lines.
+ */
+ private void normalizeToStraightLineTolerance(PointList line, int tolerance) {
+ for (int i = 0; i < line.size() - 1; i++) {
+ Point pt1 = line.getPoint(i);
+ Point pt2 = line.getPoint(i + 1);
+ if (Math.abs(pt1.x - pt2.x) < tolerance) {
+ line.setPoint(new Point(pt1.x, pt2.y), i + 1);
+ } else if (Math.abs(pt1.y - pt2.y) < tolerance) {
+ line.setPoint(new Point(pt2.x, pt1.y), i + 1);
+ }
+ }
+ }
+
+ /**
+ * Removes consecutive points contained within the source shape and removes consecutive
+ * points contained within the target shape. If all points have been removed an extra point
+ * outside source and target shapes will be added.
+ *
+ * @param conn
+ * connection
+ * @param newLine
+ * polyline of the connection (routed connection)
+ * @param start
+ * old start anchor point
+ * @param end
+ * old end anchor point
+ */
+ private void removePointsInViews(Connection conn, PointList newLine, Point start, Point end) {
+ /*
+ * Get the bounds of anchorable figure of the source and target and translate it to
+ * connection relative coordinates.
+ */
+ PrecisionRectangle source = conn.getSourceAnchor().getOwner() != null ? new PrecisionRectangle(FigureUtilities.getAnchorableFigureBounds(conn.getSourceAnchor().getOwner())) : null;
+ PrecisionRectangle target = conn.getTargetAnchor().getOwner() != null ? new PrecisionRectangle(FigureUtilities.getAnchorableFigureBounds(conn.getTargetAnchor().getOwner())) : null;
+ if (source != null) {
+ conn.getSourceAnchor().getOwner().translateToAbsolute(source);
+ conn.translateToRelative(source);
+ }
+ if (target != null) {
+ conn.getTargetAnchor().getOwner().translateToAbsolute(target);
+ conn.translateToRelative(target);
+ }
+ Point lastRemovedFromSource = null;
+ Point lastRemovedFromTarget = null;
+ /*
+ * Starting from the first point of polyline remove points that are contained
+ * within the source shape until the first point outside is found.
+ * Remember the point that was removed from the source shape last for a possible
+ * case of all points removed from polyline.
+ */
+ if (!(conn.getSourceAnchor().getOwner() instanceof Connection) && newLine.size() != 0 && source.contains(new PrecisionPoint(newLine.getFirstPoint()))) {
+ lastRemovedFromSource = newLine.removePoint(0);
+ for (int i = 0; i < newLine.size() && source.contains(new PrecisionPoint(newLine.getPoint(i))); i++) {
+ lastRemovedFromSource = newLine.removePoint(i--);
+ }
+ }
+ /*
+ * Starting from the end point of polyline remove points that are contained
+ * within the target shape until the first point outside is found.
+ * Remember the point that was removed from the target shape last for a possible
+ * case of all points removed from polyline.
+ */
+ if (!(conn.getTargetAnchor().getOwner() instanceof Connection) && newLine.size() != 0 && target.contains(new PrecisionPoint(newLine.getLastPoint()))) {
+ lastRemovedFromTarget = newLine.removePoint(newLine.size() - 1);
+ for (int i = newLine.size(); i > 0 && target.contains(new PrecisionPoint(newLine.getPoint(i - 1))); i--) {
+ lastRemovedFromTarget = newLine.removePoint(i - 1);
+ }
+ }
+ /*
+ * Handle the special case of all points removed from polyline.
+ */
+ if (newLine.size() == 0) {
+ Dimension tolerance = new Dimension(1, 0);
+ if (!isFeedback(conn)) {
+ tolerance = (Dimension) MapModeUtil.getMapMode(conn).DPtoLP(tolerance);
+ }
+ int toleranceValue = tolerance.width;
+ if (lastRemovedFromSource == null) {
+ lastRemovedFromSource = start;
+ }
+ if (lastRemovedFromTarget == null) {
+ lastRemovedFromTarget = end;
+ }
+ /*
+ * If last point removed from source and the points removed from target form
+ * a vertical or horizontal line we'll find a point located on this line and is
+ * outside of source and target shape and insert it in the polyline.
+ * The check for vertical and horizontal segment is using tolerance value, because
+ * bend point location extracted from RelativeBendpoint can have precision errors due
+ * to non-integer weight factors.
+ */
+ if (Math.abs(lastRemovedFromSource.x - lastRemovedFromTarget.x) < toleranceValue) {
+ // Vertical
+ if (source.preciseY < target.preciseY) {
+ newLine.addPoint(lastRemovedFromSource.x, (source.getBottom().y + target.getTop().y) / 2);
+ } else {
+ newLine.addPoint(lastRemovedFromSource.x, (source.getTop().y + target.getBottom().y) / 2);
+ }
+ } else if (Math.abs(lastRemovedFromSource.y - lastRemovedFromTarget.y) < toleranceValue) {
+ // Horizontal
+ if (source.preciseX < target.preciseX) {
+ newLine.addPoint((source.getRight().x + target.getLeft().x) / 2, lastRemovedFromSource.y);
+ } else {
+ newLine.addPoint((source.getLeft().x + target.getRight().x) / 2, lastRemovedFromSource.y);
+ }
+ } else if ((conn.getSourceAnchor() instanceof BaseSlidableAnchor && StringStatics.BLANK.equals(((BaseSlidableAnchor) conn.getSourceAnchor()).getTerminal()) && (conn.getTargetAnchor() instanceof BaseSlidableAnchor && StringStatics.BLANK
+ .equals(((BaseSlidableAnchor) conn.getTargetAnchor()).getTerminal())))) {
+ /*
+ * This a special case for old diagrams with rectilinear connections routed by
+ * the old router to look good with the new router
+ */
+ if (lastRemovedFromSource != null && lastRemovedFromTarget != null) {
+ newLine.addPoint((lastRemovedFromSource.x + lastRemovedFromTarget.x) / 2, (lastRemovedFromSource.y + lastRemovedFromTarget.y) / 2);
+ } else {
+ double startX = Math.max(source.preciseX, target.preciseX);
+ double endX = Math.min(source.preciseX + source.preciseWidth, target.preciseX + target.preciseWidth);
+ double startY = Math.max(source.preciseY, target.preciseY);
+ double endY = Math.min(source.preciseY + source.preciseHeight, target.preciseY + target.preciseHeight);
+ if (startX < endX) {
+ if (source.preciseY < target.preciseY) {
+ newLine.addPoint((int) Math.round((startX + endX) / 2.0), (source.getBottom().y + target.getTop().y) / 2);
+ } else {
+ newLine.addPoint((int) Math.round((startX + endX) / 2.0), (source.getTop().y + target.getBottom().y) / 2);
+ }
+ } else if (startY < endY) {
+ if (source.preciseX < target.preciseX) {
+ newLine.addPoint((source.getRight().x + target.getLeft().x) / 2, (int) Math.round((startY + endY) / 2.0));
+ } else {
+ newLine.addPoint((source.getLeft().x + target.getRight().x) / 2, (int) Math.round((startY + endY) / 2.0));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected void rectilinearResetEndPointsToEdge(Connection conn, PointList line) {
+ if (isReorienting(conn)) {
+ /*
+ * If the connection doesn't have a shape as a source or target we'll
+ * let the oblique router to do the work. The connection doesn't need to
+ * be rectilinear at this point. There is no support for making a rectilinear
+ * connection for which one of the ends is not connected to anything.
+ */
+ super.resetEndPointsToEdge(conn, line);
+ return;
+ }
+ PrecisionRectangle source = sourceBoundsRelativeToConnection(conn);
+ PrecisionRectangle target = targetBoundsRelativeToConnection(conn);
+ int offSourceDirection = PositionConstants.NONE;
+ int offTargetDirection = PositionConstants.NONE;
+ int sourceAnchorRelativeLocation = PositionConstants.NONE;
+ int targetAnchorRelativeLocation = PositionConstants.NONE;
+ if (line.size() == 0) {
+ /*
+ * If there are no valid bend points, we'll use the oblique connection anchor points
+ * and just convert the polyline from oblique to rectilinear.
+ */
+ // Need to add 2 dumb points to ensure that RouterHelper#resetEndPointsToEdge works
+ line.addPoint(new Point());
+ line.addPoint(new Point());
+ super.resetEndPointsToEdge(conn, line);
+ sourceAnchorRelativeLocation = getAnchorOffRectangleDirection(line.getFirstPoint(), source);
+ targetAnchorRelativeLocation = getAnchorOffRectangleDirection(line.getLastPoint(), target);
+ /*
+ * We need to find two points offset from the source and target anchors outside the shapes
+ * such that when the polyline is converted to rectilinear from oblique we won't have
+ * rectilinear line segments alligned with source or target shapes edges.
+ */
+ Point offStart = line.getFirstPoint();
+ Point offEnd = line.getLastPoint();
+ Dimension offsetDim = offStart.getDifference(offEnd).scale(0.5);
+ offStart.translate(getTranslationValue(sourceAnchorRelativeLocation, Math.abs(offsetDim.width), Math.abs(offsetDim.height)));
+ offEnd.translate(getTranslationValue(targetAnchorRelativeLocation, Math.abs(offsetDim.width), Math.abs(offsetDim.height)));
+ line.insertPoint(offStart, 1);
+ line.insertPoint(offEnd, 2);
+ offSourceDirection = getOffShapeDirection(sourceAnchorRelativeLocation);
+ offTargetDirection = getOffShapeDirection(targetAnchorRelativeLocation);
+ } else {
+ Point start = line.getFirstPoint();
+ Point end = line.getLastPoint();
+ if (conn.getSourceAnchor() instanceof OrthogonalConnectionAnchor) {
+ line.insertPoint(OrthogonalRouterUtilities.getOrthogonalLineSegToAnchorLoc(conn, conn.getSourceAnchor(), start).getOrigin(), 0);
+ } else {
+ /*
+ * If anchor is not supporting orthogonal connections we'll use the oblique connection
+ * anchors and then convert it to rectilinear.
+ */
+ PrecisionPoint reference = new PrecisionPoint(start);
+ conn.getSourceAnchor().getOwner().translateToAbsolute(reference);
+ PrecisionPoint anchorLocation = new PrecisionPoint(conn.getSourceAnchor().getLocation(reference));
+ conn.translateToRelative(anchorLocation);
+ line.insertPoint(anchorLocation, 0);
+ }
+ if (conn.getTargetAnchor() instanceof OrthogonalConnectionAnchor) {
+ line.addPoint(OrthogonalRouterUtilities.getOrthogonalLineSegToAnchorLoc(conn, conn.getTargetAnchor(), end).getOrigin());
+ } else {
+ /*
+ * If anchor is not supporting orthogonal connections we'll use the oblique connection
+ * anchors and then convert it to rectilinear.
+ */
+ PrecisionPoint reference = new PrecisionPoint(end);
+ conn.getSourceAnchor().getOwner().translateToAbsolute(reference);
+ PrecisionPoint anchorLocation = new PrecisionPoint(conn.getTargetAnchor().getLocation(reference));
+ conn.translateToRelative(anchorLocation);
+ line.addPoint(anchorLocation);
+ }
+ sourceAnchorRelativeLocation = getAnchorOffRectangleDirection(line.getFirstPoint(), source);
+ offSourceDirection = getOffShapeDirection(sourceAnchorRelativeLocation);
+ targetAnchorRelativeLocation = getAnchorOffRectangleDirection(line.getLastPoint(), target);
+ offTargetDirection = getOffShapeDirection(targetAnchorRelativeLocation);
+ }
+ /*
+ * Convert the polyline to rectilinear. If the connection is rectilinear already then the
+ * connection will remain as it is.
+ */
+ OrthogonalRouterUtilities.transformToOrthogonalPointList(line, offSourceDirection, offTargetDirection);
+ removeRedundantPoints(line);
+ }
+
+ /**
+ * Returns a translation dimension for the anchor point. Translation dimension
+ * translates the anchor point off the shape. The off shape direction
+ * is specified by the relative to the shape geographic position of the anchor
+ *
+ * @param position
+ * relative to the shape geographic position of the anchor
+ * @param xFactorValue
+ * translation value along x-axis
+ * @param yFactorValue
+ * translation value along y-axis
+ * @return
+ */
+ private Dimension getTranslationValue(int position, int xFactorValue, int yFactorValue) {
+ Dimension translationDimension = new Dimension();
+ if (position == PositionConstants.EAST) {
+ translationDimension.width = xFactorValue;
+ } else if (position == PositionConstants.SOUTH) {
+ translationDimension.height = yFactorValue;
+ } else if (position == PositionConstants.WEST) {
+ translationDimension.width = -xFactorValue;
+ } else if (position == PositionConstants.NORTH) {
+ translationDimension.height = -yFactorValue;
+ }
+ return translationDimension;
+ }
+
+ /**
+ * Target bounding rectangle relative to connection figure coordinates
+ *
+ * @param conn
+ * connection
+ * @return <code>PrecisionRectangle</code> target bounds relative to connection's coordinate
+ * system
+ */
+ private PrecisionRectangle targetBoundsRelativeToConnection(Connection conn) {
+ PrecisionRectangle target = new PrecisionRectangle(conn.getTargetAnchor().getOwner().getBounds());
+ conn.getTargetAnchor().getOwner().translateToAbsolute(target);
+ conn.translateToRelative(target);
+ return target;
+ }
+
+ /**
+ * Iterates through points of a polyline and does the following:
+ * if 3 points lie on the same line the middle point is removed
+ *
+ * @param line
+ * polyline's points
+ */
+ private boolean removeRedundantPoints(PointList line) {
+ int initialNumberOfPoints = line.size();
+ if (line.size() > 2) {
+ PointList newLine = new PointList(line.size());
+ newLine.addPoint(line.removePoint(0));
+ while (line.size() >= 2) {
+ Point p0 = newLine.getLastPoint();
+ Point p1 = line.getPoint(0);
+ Point p2 = line.getPoint(1);
+ if (p0.x == p1.x && p0.x == p2.x) {
+ // Have two vertical segments in a row
+ // get rid of the point between
+ line.removePoint(0);
+ } else if (p0.y == p1.y && p0.y == p2.y) {
+ // Have two horizontal segments in a row
+ // get rid of the point between
+ line.removePoint(0);
+ } else {
+ newLine.addPoint(line.removePoint(0));
+ }
+ }
+ while (line.size() > 0) {
+ newLine.addPoint(line.removePoint(0));
+ }
+ line.removeAllPoints();
+ line.addAll(newLine);
+ }
+ return line.size() != initialNumberOfPoints;
+ }
+
+ /**
+ * Determines whether the rectilinear line segment coming out of the shape should be
+ * horizontal or vertical based on the anchor geographic position relative to the shape
+ *
+ * @param anchorRelativeLocation
+ * @return
+ */
+ private int getOffShapeDirection(int anchorRelativeLocation) {
+ if (anchorRelativeLocation == PositionConstants.EAST || anchorRelativeLocation == PositionConstants.WEST) {
+ return PositionConstants.HORIZONTAL;
+ } else if (anchorRelativeLocation == PositionConstants.NORTH || anchorRelativeLocation == PositionConstants.SOUTH) {
+ return PositionConstants.VERTICAL;
+ }
+ return PositionConstants.NONE;
+ }
+
+ /**
+ * Source bounding rectangle relative to connection figure coordinates
+ *
+ * @param conn
+ * connection
+ * @return <code>PrecisionRectangle</code> source bounds relative to connection's coordinate
+ * system
+ */
+ private PrecisionRectangle sourceBoundsRelativeToConnection(Connection conn) {
+ PrecisionRectangle source = new PrecisionRectangle(conn.getSourceAnchor().getOwner().getBounds());
+ conn.getSourceAnchor().getOwner().translateToAbsolute(source);
+ conn.translateToRelative(source);
+ return source;
+ }
+
+ /**
+ * Determines the relative to rectangle geographic location of a point.
+ * Example: If shape is closer to the the top edge of the rectangle location
+ * would be north.
+ * Method used to determine which side of shape's bounding rectangle is closer
+ * to connection's anchor point.
+ * All geometric quantities must be in the same coordinate system.
+ *
+ * @param anchorPoint
+ * location of the anchor point
+ * @param rect
+ * bounding rectangle of the shape
+ * @return
+ */
+ private int getAnchorOffRectangleDirection(Point anchorPoint, Rectangle rect) {
+ int position = PositionConstants.NORTH;
+ int criteriaValue = Math.abs(anchorPoint.y - rect.y);
+ int tempCriteria = Math.abs(anchorPoint.y - rect.y - rect.height);
+ if (tempCriteria < criteriaValue) {
+ criteriaValue = tempCriteria;
+ position = PositionConstants.SOUTH;
+ }
+ tempCriteria = Math.abs(anchorPoint.x - rect.x);
+ if (tempCriteria < criteriaValue) {
+ criteriaValue = tempCriteria;
+ position = PositionConstants.WEST;
+ }
+ tempCriteria = Math.abs(anchorPoint.x - rect.x - rect.width);
+ if (tempCriteria < criteriaValue) {
+ criteriaValue = tempCriteria;
+ position = PositionConstants.EAST;
+ }
+ return position;
+ }
+
+ /**
+ * @param conn
+ * the <code>Connection</code> that is to be check if it is a feedback
+ * connection or not.
+ * @return <code>true</code> is it is a feedback connection, <code>false</code> otherwise.
+ */
+ private static boolean isFeedback(Connection conn) {
+ Dimension dim = new Dimension(100, 100);
+ Dimension dimCheck = dim.getCopy();
+ conn.translateToRelative(dimCheck);
+ return dim.equals(dimCheck);
+ }
+}

Back to the top