/******************************************************************************* * 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.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.gef.fx.anchors.AnchorKey; import org.eclipse.gef.fx.anchors.DynamicAnchor; import org.eclipse.gef.fx.anchors.DynamicAnchor.AnchorageReferenceGeometry; import org.eclipse.gef.fx.anchors.DynamicAnchor.AnchoredReferencePoint; import org.eclipse.gef.fx.anchors.IAnchor; import org.eclipse.gef.fx.anchors.StaticAnchor; import org.eclipse.gef.fx.utils.NodeUtils; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.convert.fx.Geometry2FX; import org.eclipse.gef.geometry.euclidean.Vector; import org.eclipse.gef.geometry.planar.IGeometry; import org.eclipse.gef.geometry.planar.Point; import javafx.beans.binding.ObjectBinding; import javafx.geometry.Point2D; import javafx.scene.Node; /** * Abstract base class for {@link IConnectionRouter}s implementing a routing * strategy that can be specialized by subclasses: * <ol> * <li>Remove anchors previously inserted by the router. * <li>Copy connection points before updating the computation parameters. * <li>Update computation parameters based on the copied connection points (i.e. * not influenced by parameter changes). * <li>Record connection point manipulations using * {@link ControlPointManipulator}. * <li>Apply all recorded changes to the connection. * </ol> * * @author anyssen * @author mwienand */ public abstract class AbstractRouter implements IConnectionRouter { /** * A {@link ControlPointManipulator} can be used to record, perform, and * roll back control point changes during routing. */ protected static class ControlPointManipulator { private Connection connection; private Map<Integer, List<Point>> pointsToInsert = new HashMap<>(); private int index; private Vector direction; private Point point; private List<IAnchor> controlAnchors; /** * Constructs a new {@link ControlPointManipulator} for the given * {@link Connection}. * * @param c * The {@link Connection} that is manipulated. */ public ControlPointManipulator(Connection c) { this.connection = c; this.controlAnchors = new ArrayList<>( connection.getControlAnchors()); } /** * Records the specified change. * * @param index * The index at which to insert a control point. * @param point * The start coordinates for the change. * @param dx * The horizontal component of the out direction. * @param dy * The vertical component of the out direction. * @return A {@link Vector} specifying the out direction. */ public Vector addRoutingPoint(int index, Point point, double dx, double dy) { Point insertion = point.getTranslated(dx, dy); if (!pointsToInsert.containsKey(index)) { pointsToInsert.put(index, new ArrayList<Point>()); } pointsToInsert.get(index).add(insertion); return new Vector(dx, dy); } /** * Records the specified change. * * @param delta * A {@link Vector} specifying the out direction. * @return A {@link Vector} specifying the out direction. */ public Vector addRoutingPoint(Vector delta) { direction = direction.getSubtracted( addRoutingPoint(index, point, delta.x, delta.y)); return direction; } /** * Records the given changes. * * @param index * The start index for the changes. * @param point * The start coordinates for the changes. * @param deltas * The out directions for the new points. */ public void addRoutingPoints(int index, Point point, double... deltas) { if (deltas == null) { throw new IllegalArgumentException( "Even number of routing point deltas required, but got <null>."); } if (deltas.length == 0) { throw new IllegalArgumentException( "Even number of routing point deltas required, but got 0."); } if (deltas.length % 2 != 0) { throw new IllegalArgumentException( "Even number of routing point deltas required, but got " + deltas.length + "."); } // create array list if needed if (!pointsToInsert.containsKey(index)) { pointsToInsert.put(index, new ArrayList<Point>()); } // insert points for (int i = 0; i < deltas.length; i += 2) { Point insertion = point.getTranslated(deltas[i], deltas[i + 1]); pointsToInsert.get(index).add(insertion); } } /** * Performs the recorded changes. */ public void applyChanges() { if (controlAnchors == null) { throw new IllegalStateException("Cannot apply changes twice."); } int pointsInserted = 0; for (int insertionIndex : pointsToInsert.keySet()) { // XXX: We need to keep track of those way points we insert, so // we can remove them in a succeeding routing pass; we use a // special subclass of StaticAnchor for this purpose, so we can // easily identify them through an instance check. for (Point pointToInsert : pointsToInsert.get(insertionIndex)) { controlAnchors.add(insertionIndex + pointsInserted - 1, new VolatileStaticAnchor(connection, pointToInsert)); pointsInserted++; } } // exchange the connection's points all at once connection.setControlAnchors(controlAnchors); // guard against applying changes twice controlAnchors = null; } /** * Returns the {@link Connection} that is manipulated. * * @return The {@link Connection} that is manipulated. */ public Connection getConnection() { return connection; } /** * Returns the current insertion index for manipulations. * * @return The current index. */ public int getIndex() { return index; } /** * Returns the current {@link Point} on the {@link Connection}. * * @return The current {@link Point}. */ public Point getPoint() { return point; } /** * Initializes this {@link ControlPointManipulator} for the recording of * changes. * * @param index * The index of the control point after which points are to * be added. * @param point * The start coordinates for the changes. * @param direction * The current direction. */ public void setRoutingData(int index, Point point, Vector direction) { this.index = index; this.point = point; this.direction = direction; } } /** * The {@link VolatileStaticAnchor} is a {@link StaticAnchor} that may be * inserted by an {@link AbstractRouter} during * {@link AbstractRouter#route(Connection) route(Connection)}, and, hence, * will be removed when routing is performed again. A subtype is used so * that the inserted anchors can easily be identified. */ protected static class VolatileStaticAnchor extends StaticAnchor { /** * Constructs a new {@link VolatileStaticAnchor}. Uses the given * {@link Connection} as the anchorage, and the given {@link Point} as * the {@link #getReferencePosition() reference position}. * * @param connection * The {@link Connection} that serves as the anchorage for * this {@link VolatileStaticAnchor}. * @param referencePositionInAnchorageLocal * The {@link Point} that specifies the * {@link #getReferencePosition() reference position} for * this {@link VolatileStaticAnchor}, interpreted in the * local coordinate system of the {@link Connection}. */ public VolatileStaticAnchor(Connection connection, Point referencePositionInAnchorageLocal) { super(connection, referencePositionInAnchorageLocal); } @Override public String toString() { return "VolatileStaticAnchor[referencePosition=" + getReferencePosition() + "]"; } } private Connection connection; /** * Returns a newly created {@link ControlPointManipulator} that can be used * to insert control points into the given {@link Connection}. * * @param connection * The {@link Connection} for which to create a * {@link ControlPointManipulator}. * @return The {@link ControlPointManipulator} for the given * {@link Connection}. */ protected ControlPointManipulator createControlPointManipulator( Connection connection) { return new ControlPointManipulator(connection); } /** * Retrieves the geometry of the anchorage at the given index within the * coordinate system of the {@link Connection}, in case the respective * anchor is connected. * * @param index * The index of the anchor whose anchorage geometry is to be * retrieved. * @return A geometry resembling the anchorage reference geometry of the * anchor at the given index, or <code>null</code> if the anchor is * not connected. */ protected IGeometry getAnchorageGeometry(int index) { IAnchor anchor = connection.getAnchor(index); if (connection.isConnected(anchor)) { Node anchorage = anchor.getAnchorage(); if (anchor instanceof DynamicAnchor) { IGeometry geometry = ((DynamicAnchor) anchor) .getComputationParameter(connection.getAnchorKey(index), AnchorageReferenceGeometry.class) .get(); return NodeUtils.sceneToLocal(connection, NodeUtils.localToScene(anchorage, geometry)); } // fall back to using the shape outline return NodeUtils.sceneToLocal(connection, NodeUtils.localToScene( anchorage, NodeUtils.getShapeOutline(anchorage))); } return null; } /** * Returns the {@link AnchoredReferencePoint} parameter value (within the * coordinate system of the {@link Connection}) for the anchor specified by * the given index. * * @param points * The list of {@link Point}s from which the {@link Connection} * is currently constituted. * @param index * The index of the {@link IAnchor} for which to compute the * {@link AnchoredReferencePoint} parameter value. * @return The anchored reference {@link Point} for the specified anchor. */ protected abstract Point getAnchoredReferencePoint(List<Point> points, int index); /** * Returns the {@link Connection} of the last {@link #route(Connection)} * call. * * @return The {@link Connection} passed into {@link #route(Connection)}. */ protected Connection getConnection() { return connection; } /** * Inserts router anchors into the {@link Connection}. * * @param connection * The {@link Connection}. */ protected void insertRouterAnchors(Connection connection) { // XXX: Copy points just to be sure they are not modified. List<Point> pts = new ArrayList<>(connection.getPointsUnmodifiable()); for (int i = 0; i < pts.size(); i++) { Point pos = connection.getAnchor(i) .getPosition(connection.getAnchorKey(i)); pts.set(i, FX2Geometry.toPoint(connection.getCurve() .localToParent(Geometry2FX.toFXPoint(pos)))); } ControlPointManipulator cpm = createControlPointManipulator(connection); Vector inDirection = null; Vector outDirection = null; for (int i = 0; i < pts.size() - 1; i++) { Point currentPoint = pts.get(i); // direction between preceding way/control point and current one has // been computed in previous iteration inDirection = outDirection; // compute the direction between the current way/control point and // the succeeding one outDirection = new Vector(currentPoint, pts.get(i + 1)); // prepare CPM for manipulations cpm.setRoutingData(i, currentPoint, outDirection); // insert router anchors if necessary outDirection = route(cpm, inDirection, outDirection); } cpm.applyChanges(); } /** * Removes volatile anchors (i.e. {@link #wasInserted(IAnchor) inserted by * the router}). * * @param connection * The {@link Connection} from which to remove volatile anchors. */ protected void removeVolatileAnchors(Connection connection) { List<IAnchor> realAnchors = new ArrayList<>(); for (IAnchor a : connection.getControlAnchors()) { if (!wasInserted(a)) { realAnchors.add(a); } } connection.setControlAnchors(realAnchors); } @Override public void route(Connection connection) { this.connection = connection; // Remove previously inserted route points, so that the Connection is // only constituted by the user-defined anchors. removeVolatileAnchors(connection); // Compute dynamic anchor parameters. updateComputationParameters(connection); // Insert route points where necessary. insertRouterAnchors(connection); } /** * Inserts router anchors where necessary. Returns the {@link Vector} that * points to the next point. * * @param cpm * The {@link ControlPointManipulator} that can be used to insert * points. * @param inDirection * The {@link Vector} from the previous point to the current * point. * @param outDirection * The {@link Vector} from the current point to the next point. * @return The adjusted {@link Vector} from the current point to the next * point. */ protected Vector route(ControlPointManipulator cpm, Vector inDirection, Vector outDirection) { return outDirection; } /** * Updates all computation parameters for the anchors of the given * {@link Connection}. * * @param connection * The {@link Connection}. */ protected void updateComputationParameters(Connection connection) { // XXX: Copy current connection points before updating the computation // parameters for the individual DynamicAnchors so that the computation // of the first parameter does not influence the computation of a // parameter that is computed later on. List<Point> pts = new ArrayList<>(connection.getPointsUnmodifiable()); // update parameters for all DynamicAnchors for (int i = 0; i < pts.size(); i++) { IAnchor anchor = connection.getAnchor(i); if (anchor instanceof DynamicAnchor) { DynamicAnchor da = ((DynamicAnchor) anchor); AnchorKey key = connection.getAnchorKey(i); // XXX: The independent copy of the points is passed to the // computation method. updateComputationParameters(pts, i, da, key); } } } /** * Update's the reference point of the anchor with the given index. * * @param points * The {@link Connection}'s points (snapshot taken before * parameters are updated, i.e. independent from the parameter * changes). * @param index * The index of the connection anchor (and anchor key) for which * the computation parameters are updated. * @param anchor * The {@link DynamicAnchor} for which to update the computation * parameters. * @param key * The {@link AnchorKey}, corresponding to the index, for which * to update the computation parameters. */ protected void updateComputationParameters(List<Point> points, int index, DynamicAnchor anchor, AnchorKey key) { // only update if necessary (when it changes) AnchoredReferencePoint referencePointParameter = anchor .getComputationParameter(key, AnchoredReferencePoint.class); Point oldRef = referencePointParameter.get(); Point oldRefInScene = oldRef == null ? null : FX2Geometry.toPoint(key.getAnchored() .localToScene(Geometry2FX.toFXPoint(oldRef))); // if we have a position hint for the anchor, we need to use this as the // reference point // Point newRef = getAnchoredReferencePoint(points, index); Point2D newRefInConnection = Geometry2FX .toFXPoint(getAnchoredReferencePoint(points, index)); Point newRefInScene = FX2Geometry .toPoint(getConnection().localToScene(newRefInConnection)); if (oldRefInScene == null || !newRefInScene.equals(oldRefInScene)) { ObjectBinding<Point> refBinding = new ObjectBinding<Point>() { { bind(key.getAnchored().localToParentTransformProperty()); } @Override protected Point computeValue() { return FX2Geometry.toPoint(key.getAnchored() .parentToLocal(newRefInConnection)); } }; referencePointParameter.bind(refBinding); } } @Override public boolean wasInserted(IAnchor anchor) { return anchor instanceof VolatileStaticAnchor; } }