/*******************************************************************************
* Copyright (c) 2016 itemis AG 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:
* Alexander Nyßen (itemis AG) - initial API and implementation
* Matthias Wienand (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.fx.nodes;
import java.util.List;
import org.eclipse.gef.fx.anchors.AnchorKey;
import org.eclipse.gef.fx.anchors.DynamicAnchor;
import org.eclipse.gef.fx.anchors.DynamicAnchor.AnchoredReferencePoint;
import org.eclipse.gef.fx.anchors.DynamicAnchor.PreferredOrientation;
import org.eclipse.gef.fx.utils.NodeUtils;
import org.eclipse.gef.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.euclidean.Vector;
import org.eclipse.gef.geometry.planar.IGeometry;
import org.eclipse.gef.geometry.planar.Line;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.geometry.planar.Polygon;
import org.eclipse.gef.geometry.planar.Rectangle;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
/**
* An {@link IConnectionRouter} that interprets the {@link Connection} control
* points as way points and adjusts the way points (if necessary) so that the
* {@link Connection} is routed orthogonally.
*
* @author anyssen
* @author mwienand
*
*/
public class OrthogonalRouter extends AbstractRouter {
private static final double OFFSET = 15;
/**
* Iterates the connection's points starting at the first candidate index (
* <i>anchorIndex</i> + <i>step</i>) and stepping by the given step. Returns
* the index of the first point that is not contained within the given
* anchorage geometry. If all points are contained within the given
* anchorage geometry, the first reference candidate (i.e.
* <i>anchorIndex</i> + <i>step</i>) is returned.
*
* @param connection
* The connection.
* @param anchorIndex
* The start index within the connection's points.
* @param anchorageGeometry
* The anchorage geometry.
* @param step
* The step that is used to iterate the connection's points.
* @return The index of the first point that is not contained within the
* anchorage geometry.
*/
private int findReferenceIndex(List<Point> points, int anchorIndex,
IGeometry anchorageGeometry, int step) {
int startIndex = anchorIndex + step;
for (int i = startIndex; step < 0 ? i >= 0
: i < points.size(); i += step) {
Point point = points.get(i);
if (!anchorageGeometry.contains(point)) {
return i;
}
}
return startIndex;
}
/**
* Returns the reference point for the anchor at the given index.
*
* @param points
* The {@link Connection} that is currently routed.
* @param index
* The index specifying the anchor for which to provide a
* reference point.
* @return The reference point for the anchor at the given index in the
* local coordinate system of the anchored, which is the
* connection's curve.
*/
@Override
protected Point getAnchoredReferencePoint(List<Point> points, int index) {
if (index < 0 || index >= points.size()) {
throw new IndexOutOfBoundsException();
}
Connection connection = getConnection();
IGeometry geometry = getAnchorageGeometry(index);
int referenceIndex = findReferenceIndex(points, index, geometry,
index < points.size() - 1 ? 1 : -1);
IGeometry referenceGeometry = getAnchorageGeometry(referenceIndex);
if (referenceGeometry != null) {
if (geometry != null) {
// find opposite reference index
int oppositeReferenceIndex = findReferenceIndex(points,
index == 0 ? points.size() - 1 : 0, geometry,
index < points.size() - 1 ? -1 : 1);
if (getAnchorageGeometry(oppositeReferenceIndex) == null) {
return points.get(oppositeReferenceIndex);
}
// XXX: if a position hint is supplied for the current index,
// return that hint as the reference point.
if (index == 0) {
Point startPointHint = connection.getStartPointHint();
if (startPointHint != null) {
return startPointHint;
}
} else if (index == points.size() - 1) {
Point endPointHint = connection.getEndPointHint();
if (endPointHint != null) {
return endPointHint;
}
}
// XXX: if index and reference index both point to anchors that
// use a reference geometry, we have to compute a horizontal or
// vertical projection between both geometries (if existent)
// before falling back to the super strategy.
Rectangle bounds = geometry.getBounds();
Rectangle refBounds = referenceGeometry.getBounds();
double x1 = Math.max(bounds.getX(), refBounds.getX());
double x2 = Math.min(bounds.getX() + bounds.getWidth(),
refBounds.getX() + refBounds.getWidth());
if (x1 <= x2) {
// horizontal overlap => return vertically stable position
return new Point(x1 + (x2 - x1) / 2,
refBounds.getY() > bounds.getY()
+ bounds.getHeight() ? refBounds.getY()
: refBounds.getY()
+ refBounds.getHeight());
}
double y1 = Math.max(bounds.getY(), refBounds.getY());
double y2 = Math.min(bounds.getY() + bounds.getHeight(),
refBounds.getY() + refBounds.getHeight());
if (y1 <= y2) {
// vertical overlap => return horizontally stable position
return new Point(
refBounds.getX() > bounds.getX() + bounds.getWidth()
? refBounds.getX()
: refBounds.getX() + refBounds.getWidth(),
y1 + (y2 - y1) / 2);
}
// fallback to nearest bounds projection
// TODO: revise handling of this case -> we could optimize this
// by providing a desired direction
return getNearestBoundsProjection(referenceGeometry,
geometry.getBounds().getCenter());
}
}
return points.get(referenceIndex);
}
private Point getNearestBoundsProjection(IGeometry g, Point p) {
Line[] outlineSegments = g.getBounds().getOutlineSegments();
Point nearestProjection = null;
double nearestDistance = 0;
for (Line l : outlineSegments) {
Point projection = l.getProjection(p);
double distance = p.getDistance(projection);
if (nearestProjection == null || distance < nearestDistance) {
nearestDistance = distance;
nearestProjection = projection;
}
}
return nearestProjection;
}
private Polygon[] getTriangles(Connection connection, int i) {
Node anchorage = connection.getAnchor(i).getAnchorage();
Bounds boundsInScene = anchorage
.localToScene(anchorage.getLayoutBounds());
Rectangle rectangle = FX2Geometry.toRectangle(boundsInScene);
Polygon top = new Polygon(rectangle.getTopLeft(),
rectangle.getTopRight(), rectangle.getCenter());
Polygon bottom = new Polygon(rectangle.getBottomLeft(),
rectangle.getBottomRight(), rectangle.getCenter());
Polygon left = new Polygon(rectangle.getTopLeft(),
rectangle.getBottomLeft(), rectangle.getCenter());
Polygon right = new Polygon(rectangle.getTopRight(),
rectangle.getBottomRight(), rectangle.getCenter());
return new Polygon[] { top, right, bottom, left };
}
private boolean isBottom(Connection connection, int i, Point currentPoint) {
Point2D pointInScene = connection.localToScene(currentPoint.x,
currentPoint.y);
Point point = FX2Geometry.toPoint(pointInScene);
Polygon[] triangles = getTriangles(connection, i);
return triangles[2].contains(point);
}
private boolean isLeft(Connection connection, int i, Point currentPoint) {
Point2D pointInScene = connection.localToScene(currentPoint.x,
currentPoint.y);
Point point = FX2Geometry.toPoint(pointInScene);
Polygon[] triangles = getTriangles(connection, i);
return triangles[3].contains(point);
}
private boolean isRight(Connection connection, int i, Point currentPoint) {
Point2D pointInScene = connection.localToScene(currentPoint.x,
currentPoint.y);
Point point = FX2Geometry.toPoint(pointInScene);
Polygon[] triangles = getTriangles(connection, i);
return triangles[1].contains(point);
}
private boolean isSufficientlyHorizontal(Vector currentDirection) {
return Math.abs(currentDirection.y) < 0.5
&& Math.abs(currentDirection.x) > Math.abs(currentDirection.y);
}
private boolean isSufficientlyVertical(Vector currentDirection) {
return Math.abs(currentDirection.y) > Math.abs(currentDirection.x)
&& Math.abs(currentDirection.x) < 0.5;
}
private boolean isTop(Connection connection, int i, Point currentPoint) {
Point2D pointInScene = connection.localToScene(currentPoint.x,
currentPoint.y);
Point point = FX2Geometry.toPoint(pointInScene);
Polygon[] triangles = getTriangles(connection, i);
return triangles[0].contains(point);
}
private boolean isTopOrBottom(Connection connection, int i,
Point currentPoint) {
Point2D pointInScene = connection.localToScene(currentPoint.x,
currentPoint.y);
Point point = FX2Geometry.toPoint(pointInScene);
Polygon[] triangles = getTriangles(connection, i);
return triangles[0].contains(point) || triangles[2].contains(point);
}
@Override
protected Vector route(ControlPointManipulator cpm, Vector inDirection,
Vector outDirection) {
if (Math.abs(outDirection.x) <= 0.05
&& Math.abs(outDirection.y) <= 0.05) {
// effectively 0 => do not insert point
// => use previous direction as current direction
return inDirection;
}
// given the direction, determine if points have to be added
if (isSufficientlyHorizontal(outDirection)
|| isSufficientlyVertical(outDirection)) {
// XXX: We may have to adjust an already orthogonal segment in
// case it overlaps with an anchorage outline.
// currentDirection = routeOrthogonalSegment(connection,
// controlPointManipulator, currentDirection, i,
// currentPoint);
return super.route(cpm, inDirection, outDirection);
} else {
return routeNonOrthogonalSegment(cpm.getConnection(), cpm,
inDirection, outDirection, cpm.getIndex(), cpm.getPoint());
}
}
/**
* This method is called for a non-orthogonal direction from the last point
* on the connection to the current point on the connection.
*
* @param connection
* The {@link Connection} that is manipulated.
* @param controlPointManipulator
* The helper that is used for inserting route points.
* @param inDirection
* The previous direction, or <code>null</code> (for the end
* point).
* @param outDirection
* The current direction, or <code>null</code> (for the start
* point).
* @param i
* The index of the current point.
* @param currentPoint
* The current {@link Point}.
* @return The manipulated current direction.
*/
protected Vector routeNonOrthogonalSegment(Connection connection,
ControlPointManipulator controlPointManipulator, Vector inDirection,
Vector outDirection, int i, Point currentPoint) {
controlPointManipulator.setRoutingData(i + 1, currentPoint,
outDirection);
Vector moveVertically = new Vector(0, outDirection.y);
Vector moveHorizontally = new Vector(outDirection.x, 0);
if (i == 0 && connection.isStartConnected()
|| i == connection.getPointsUnmodifiable().size() - 2
&& connection.isEndConnected()) {
if (i == 0 && i != connection.getPointsUnmodifiable().size() - 2) {
// move left/right if current point is on top or
// bottom anchorage outline
if (isTopOrBottom(connection, i, currentPoint)) {
// System.out.println("1");
// point on top or bottom, move vertically
outDirection = controlPointManipulator
.addRoutingPoint(moveVertically);
} else {
// System.out.println("2");
// point on left/right, move horizontally
outDirection = controlPointManipulator
.addRoutingPoint(moveHorizontally);
}
} else if (i != 0
&& i == connection.getPointsUnmodifiable().size() - 2) {
// move left/right if next point is on top or
// bottom anchorage outline
if (isTopOrBottom(connection, i + 1, currentPoint
.getTranslated(outDirection.x, outDirection.y))) {
// System.out.println("3");
// point on top or bottom, move horizontally
outDirection = controlPointManipulator
.addRoutingPoint(moveHorizontally);
} else {
// System.out.println("4");
// point on left/right, move vertically
outDirection = controlPointManipulator
.addRoutingPoint(moveVertically);
}
} else {
// split direction in the middle and generate new
// control points
boolean currentIsTopOrBottom = isTopOrBottom(connection, i,
currentPoint);
boolean nextIsTopOrBottom = isTopOrBottom(connection, i + 1,
currentPoint.getTranslated(outDirection.x,
outDirection.y));
if (currentIsTopOrBottom && nextIsTopOrBottom) {
// System.out.println("5");
// both top/bottom
controlPointManipulator.addRoutingPoints(i + 1,
currentPoint, 0, outDirection.y / 2, outDirection.x,
outDirection.y / 2);
} else if (!currentIsTopOrBottom && !nextIsTopOrBottom) {
// System.out.println("6");
// both left/right
controlPointManipulator.addRoutingPoints(i + 1,
currentPoint, outDirection.x / 2, 0,
outDirection.x / 2, outDirection.y);
} else {
// on different sides
if (currentIsTopOrBottom) {
// System.out.println("7");
// use x coordinate of current point
outDirection = controlPointManipulator
.addRoutingPoint(moveVertically);
} else {
// System.out.println("8");
// use y coordinate of current point
outDirection = controlPointManipulator
.addRoutingPoint(moveHorizontally);
}
}
}
} else {
if (inDirection == null) {
// System.out.println("9");
// move horizontally first
outDirection = controlPointManipulator
.addRoutingPoint(moveHorizontally);
} else {
// adjust by inserting a control point; try to follow
// previous direction as long as possible
if (inDirection.isHorizontal()) {
if (inDirection.x < 0 && outDirection.x < 0
|| inDirection.x > 0 && outDirection.x > 0) {
// System.out.println("10");
// prolong current direction horizontally
outDirection = controlPointManipulator
.addRoutingPoint(moveHorizontally);
} else {
// System.out.println("11");
// move vertically first
outDirection = controlPointManipulator
.addRoutingPoint(moveVertically);
}
} else {
if (inDirection.y < 0 && outDirection.y < 0
|| inDirection.y > 0 && outDirection.y > 0) {
// System.out.println("12");
// prolong current direction vertically
outDirection = controlPointManipulator
.addRoutingPoint(moveVertically);
} else {
// System.out.println("13");
// move horizontally first
outDirection = controlPointManipulator
.addRoutingPoint(moveHorizontally);
}
}
}
}
return outDirection;
}
/**
* This method is called for an orthogonal direction from the last point on
* the connection to the current point on the connection.
*
* @param connection
* The {@link Connection} that is manipulated.
* @param controlPointManipulator
* The helper that is used to insert route points.
* @param currentDirection
* The current direction.
* @param i
* The index of the current point.
* @param currentPoint
* The current {@link Point}.
* @return The manipulated current direction.
*/
protected Vector routeOrthogonalSegment(Connection connection,
ControlPointManipulator controlPointManipulator,
Vector currentDirection, int i, Point currentPoint) {
// completely horizontal/vertical is not allowed for connected
// anchors
if (i == 0 && connection.isStartConnected()
&& i != connection.getPointsUnmodifiable().size() - 2) {
// start point, connected
if (currentDirection.isVertical()) {
boolean isLeft = isLeft(connection, i, currentPoint);
boolean isRight = isRight(connection, i, currentPoint);
boolean isBottom = isBottom(connection, i, currentPoint);
boolean isTop = isTop(connection, i, currentPoint);
if ((isLeft || isRight) && !(isBottom || isTop)) {
// insert two control points
double offset = isLeft ? -OFFSET : OFFSET;
controlPointManipulator.addRoutingPoints(i + 1,
currentPoint, offset, 0, offset,
currentDirection.y);
currentDirection = new Vector(-offset, 0);
}
} else if (currentDirection.isHorizontal()) {
boolean isLeft = isLeft(connection, i, currentPoint);
boolean isRight = isRight(connection, i, currentPoint);
boolean isBottom = isBottom(connection, i, currentPoint);
boolean isTop = isTop(connection, i, currentPoint);
if ((isTop || isBottom) && !(isLeft || isRight)) {
// insert two control points above
double offset = isTop ? -OFFSET : OFFSET;
controlPointManipulator.addRoutingPoints(i + 1,
currentPoint, 0, offset, currentDirection.x,
offset);
currentDirection = new Vector(0, -offset);
}
}
} else if (i != 0 && i == connection.getPointsUnmodifiable().size() - 2
&& connection.isEndConnected()) {
// end point, connected
if (currentDirection.isHorizontal()) {
boolean isLeft = isLeft(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
boolean isRight = isRight(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
boolean isTop = isTop(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
boolean isBottom = isBottom(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
if ((isTop || isBottom) && !(isLeft || isRight)) {
// insert 2 points above
double offset = isTop ? -OFFSET : OFFSET;
controlPointManipulator.addRoutingPoints(i + 1,
currentPoint, 0, offset, currentDirection.x,
offset);
currentDirection = new Vector(0, -offset);
}
} else if (currentDirection.isVertical()) {
boolean isLeft = isLeft(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
boolean isRight = isRight(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
boolean isTop = isTop(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
boolean isBottom = isBottom(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
if ((isLeft || isRight) && !(isTop || isBottom)) {
// insert 2 points on the left
double offset = isLeft ? -OFFSET : OFFSET;
controlPointManipulator.addRoutingPoints(i + 1,
currentPoint, offset, 0, offset,
currentDirection.y);
currentDirection = new Vector(-offset, 0);
}
}
} else if (i == 0 && i == connection.getPointsUnmodifiable().size() - 2
&& connection.isStartConnected()
&& connection.isEndConnected()) {
// start and end point, connected
if (currentDirection.isHorizontal()) {
boolean isCurrentTop = isTop(connection, i, currentPoint);
boolean isNextBottom = isBottom(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
boolean isCurrentBottom = isBottom(connection, i, currentPoint);
boolean isNextTop = isTop(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
if (isCurrentTop && isNextBottom
|| isCurrentBottom && isNextTop) {
double offset = isCurrentTop ? -OFFSET : OFFSET;
// from top to bottom => insert 4 control points
controlPointManipulator.addRoutingPoints(i + 1,
currentPoint, 0, offset, currentDirection.x / 2,
offset, currentDirection.x / 2,
currentDirection.y - offset, currentDirection.x,
currentDirection.y - offset);
currentDirection = new Vector(0, offset);
}
} else if (currentDirection.isVertical()) {
boolean isCurrentLeft = isLeft(connection, i, currentPoint);
boolean isNextRight = isRight(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
boolean isCurrentRight = isRight(connection, i, currentPoint);
boolean isNextLeft = isLeft(connection, i + 1, currentPoint
.getTranslated(currentDirection.x, currentDirection.y));
if (isCurrentLeft && isNextRight
|| isCurrentRight && isNextLeft) {
double offset = isCurrentLeft ? -OFFSET : OFFSET;
// from left to right => insert 4 control points
controlPointManipulator.addRoutingPoints(i + 1,
currentPoint, offset, 0, offset,
currentDirection.y / 2, currentDirection.x - offset,
currentDirection.y / 2, currentDirection.x - offset,
currentDirection.y);
}
}
}
return currentDirection;
}
@Override
protected void updateComputationParameters(List<Point> points, int index,
DynamicAnchor anchor, AnchorKey key) {
// set anchored reference point
super.updateComputationParameters(points, index, anchor, key);
// set orientation hint for first and last anchor
if (index == 0 || index == points.size() - 1) {
// update orientation hint
Point neighborPoint = points
.get(index == 0 ? index + 1 : index - 1);
Point refPoint = NodeUtils
.sceneToLocal(getConnection(),
NodeUtils.localToScene(key.getAnchored(),
anchor.getComputationParameter(key,
AnchoredReferencePoint.class)
.get()));
Point delta = neighborPoint.getDifference(refPoint);
Orientation hint = null;
if (Math.abs(delta.x) < 5
&& Math.abs(delta.x) < Math.abs(delta.y)) {
// very small x difference => go in vertically
hint = Orientation.VERTICAL;
} else if (Math.abs(delta.y) < 5
&& Math.abs(delta.y) < Math.abs(delta.x)) {
// very small y difference => go in horizontally
hint = Orientation.HORIZONTAL;
}
// provide a hint to the anchor's computation strategy
anchor.getComputationParameter(key, PreferredOrientation.class)
.set(hint);
}
}
}