/*******************************************************************************
* Copyright (c) 2014, 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:
* Matthias Wienand (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.policies;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.gef.fx.anchors.DynamicAnchor;
import org.eclipse.gef.fx.anchors.IAnchor;
import org.eclipse.gef.fx.anchors.StaticAnchor;
import org.eclipse.gef.fx.nodes.Connection;
import org.eclipse.gef.fx.nodes.IConnectionRouter;
import org.eclipse.gef.fx.nodes.OrthogonalRouter;
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.Point;
import org.eclipse.gef.mvc.fx.models.GridModel;
import org.eclipse.gef.mvc.fx.operations.AbstractCompositeOperation;
import org.eclipse.gef.mvc.fx.operations.BendConnectionOperation;
import org.eclipse.gef.mvc.fx.operations.BendContentOperation;
import org.eclipse.gef.mvc.fx.operations.ForwardUndoCompositeOperation;
import org.eclipse.gef.mvc.fx.operations.ITransactionalOperation;
import org.eclipse.gef.mvc.fx.operations.UpdateAnchorHintsOperation;
import org.eclipse.gef.mvc.fx.parts.IBendableContentPart;
import org.eclipse.gef.mvc.fx.parts.IBendableContentPart.BendPoint;
import org.eclipse.gef.mvc.fx.parts.IContentPart;
import org.eclipse.gef.mvc.fx.parts.IVisualPart;
import org.eclipse.gef.mvc.fx.parts.PartUtils;
import org.eclipse.gef.mvc.fx.providers.IAnchorProvider;
import org.eclipse.gef.mvc.fx.viewer.IViewer;
import com.google.inject.Provider;
import javafx.scene.Node;
/**
* The {@link BendConnectionPolicy} can be used to manipulate the points
* constituting an {@link Connection}, i.e. its start, way, and end points. Each
* point is realized though an {@link IAnchor}, which may either be local to the
* {@link Connection} (i.e. the anchor refers to the {@link Connection} as
* anchorage), or it may be provided by another {@link IVisualPart} (i.e. the
* anchor is provided by a {@link Provider} adapted to the part), to which the
* connection is being connected.
*
* When moving a point the policy takes care of:
* <ul>
* <li>Removing overlaid neighbor points.</li>
* <li>Re-adding temporarily removed neighbor points.</li>
* <li>Reconnecting points to the {@link IVisualPart} under mouse when
* applicable.</li>
* </ul>
*
* @author mwienand
* @author anyssen
*/
public class BendConnectionPolicy extends AbstractPolicy {
/**
* An {@link ImplicitGroup} stores an {@link AnchorHandle} and a number of
* subsequent implicit {@link Point}s.
*
* @author mwienand
*
*/
private static class ImplicitGroup {
int precedingExplicitIndex;
List<Point> points = new ArrayList<>();
public ImplicitGroup(int precedingExplicitIndex) {
this.precedingExplicitIndex = precedingExplicitIndex;
}
}
/**
* The overlay threshold, i.e. the distance between two points so that they
* are regarded as overlying.
*/
protected static final double DEFAULT_OVERLAY_THRESHOLD = 10;
/**
* The overlay threshold, i.e. the distance between two points so that they
* are regarded as overlying.
*/
protected static final double DEFAULT_SEGMENT_OVERLAY_THRESHOLD = 6;
private List<Integer> selectedExplicitAnchorIndices = new ArrayList<>();
private List<Point> selectedInitialPositions = new ArrayList<>();
private List<IAnchor> preMoveExplicitAnchors = new ArrayList<>();
private Point preMoveStartHint = null;
private Point preMoveEndHint = null;
private boolean isSelectionHorizontal = false;
// TODO: remove usePreMoveHints
private boolean usePreMoveHints = false;
private boolean isNormalizationNeeded = false;
private List<BendPoint> initialBendPoints;
/**
* Determines if the anchor at the given explicit index can be replaced with
* an anchor that is obtained from an underlying visual part. Per default,
* only the start and the end index can be connected.
*
* @param explicitAnchorIndex
* The explicit anchor index for which to determine if it can be
* connected.
* @return <code>true</code> if the anchor at the given index can be
* connected, otherwise <code>false</code>.
*/
protected boolean canConnect(int explicitAnchorIndex) {
return explicitAnchorIndex == 0
|| explicitAnchorIndex == getBendOperation().getNewAnchors()
.size() - 1;
}
@Override
public ITransactionalOperation commit() {
if (isNormalizationNeeded) {
// showAnchors("pre-norm:");
normalize();
// showAnchors("commit:");
}
ITransactionalOperation commitOperation = super.commit();
if (commitOperation != null && !commitOperation.isNoOp()
&& getHost() instanceof IBendableContentPart) {
// chain content changes
// unconnected control points
ForwardUndoCompositeOperation composite = new ForwardUndoCompositeOperation(
"Bend Content");
composite.add(commitOperation);
BendContentOperation resizeOperation = new BendContentOperation(
getHost(), getInitialBendPoints(), getCurrentBendPoints());
composite.add(resizeOperation);
commitOperation = composite;
}
// clear state
initialBendPoints = null;
return commitOperation;
}
private Point computeEndHint() {
if (getConnection().getEndAnchor() instanceof DynamicAnchor
&& getConnection().getPointsUnmodifiable().size() > 1) {
Point endPoint = getConnection().getEndPoint();
Point neighbor = getConnection().getPoint(
getConnection().getPointsUnmodifiable().size() - 2);
Point translated = endPoint.getTranslated(
endPoint.getDifference(neighbor).getScaled(0.5));
return translated;
}
return null;
}
private Point computeStartHint() {
if (getConnection().getStartAnchor() instanceof DynamicAnchor
&& getConnection().getPointsUnmodifiable().size() > 1) {
Point startPoint = getConnection().getStartPoint();
Point neighbor = getConnection().getPoint(1);
Point translated = startPoint.getTranslated(
startPoint.getDifference(neighbor).getScaled(0.5));
return translated;
}
return null;
}
/**
* Creates a new anchor after the anchor specified by the given explicit
* anchor index. Returns the new anchor's explicit index.
*
* @param explicitAnchorIndex
* An explicit anchor index that references the explicit anchor
* after which the new anchor is inserted.
* @param mouseInScene
* The position for the new anchor in scene coordinates.
*
* @return The index for the new anchor.
*/
public int createAfter(int explicitAnchorIndex, Point mouseInScene) {
checkInitialized();
// create point => normalization needed after commit
isNormalizationNeeded = true;
// determine insertion index
int insertionIndex = explicitAnchorIndex + 1;
// insert new anchor
insertExplicitAnchor(insertionIndex, mouseInScene);
// return handle to newly created anchor
return insertionIndex;
}
/**
* Creates a new anchor before the anchor specified by the given explicit
* anchor index. Returns the new anchor's explicit index.
*
* @param explicitAnchorIndex
* An explicit anchor index that references the explicit anchor
* before which the new anchor is inserted.
* @param mouseInScene
* The position for the new anchor in scene coordinates.
*
* @return The index for the new anchor.
*/
public int createBefore(int explicitAnchorIndex, Point mouseInScene) {
checkInitialized();
// create point => normalization needed after commit
isNormalizationNeeded = true;
// determine insertion index
int insertionIndex = explicitAnchorIndex;
// insert new anchor
insertExplicitAnchor(insertionIndex, mouseInScene);
// return handle to newly created anchor
return insertionIndex;
}
@Override
protected ITransactionalOperation createOperation() {
ForwardUndoCompositeOperation fwdOp = new ForwardUndoCompositeOperation(
"BendPlusHints");
fwdOp.add(new BendConnectionOperation(getConnection()));
fwdOp.add(new UpdateAnchorHintsOperation(getConnection()));
return fwdOp;
}
/**
* Creates an (unconnected) anchor (i.e. one anchored on the
* {@link Connection}) for the given position (in scene coordinates).
*
* @param selectedPointCurrentPositionInLocal
* The location in local coordinates of the connection
* @return An {@link IAnchor} that yields the given position.
*/
protected IAnchor createUnconnectedAnchor(
Point selectedPointCurrentPositionInLocal) {
return new StaticAnchor(getConnection(),
selectedPointCurrentPositionInLocal);
}
/**
* Determines the {@link IAnchor} that should replace the anchor of the
* currently selected point. If the point can connect, the
* {@link IVisualPart} at the mouse position is queried for an
* {@link IAnchor} via a {@link Provider}<{@link IAnchor}> adapter.
* Otherwise an (unconnected) anchor is create using
* {@link #createUnconnectedAnchor(Point)} .
*
* @param positionInLocal
* A position in local coordinates of the connection.
* @param canConnect
* <code>true</code> if the point can be attached to an
* underlying {@link IVisualPart}, otherwise <code>false</code>.
* @return The {@link IAnchor} that replaces the anchor of the currently
* modified point.
*/
protected IAnchor findOrCreateAnchor(Point positionInLocal,
boolean canConnect) {
IAnchor anchor = null;
// try to find an anchor that is provided from an underlying node
if (canConnect) {
Point selectedPointCurrentPositionInScene = FX2Geometry
.toPoint(getConnection().localToScene(
Geometry2FX.toFXPoint(positionInLocal)));
List<Node> pickedNodes = NodeUtils.getNodesAt(
getHost().getRoot().getVisual(),
selectedPointCurrentPositionInScene.x,
selectedPointCurrentPositionInScene.y);
anchor = getCompatibleAnchor(getParts(pickedNodes));
}
if (anchor == null) {
anchor = createUnconnectedAnchor(positionInLocal);
}
return anchor;
}
/**
* Returns an {@link BendConnectionOperation} that is extracted from the
* operation created by {@link #createOperation()}.
*
* @return an {@link BendConnectionOperation} that is extracted from the
* operation created by {@link #createOperation()}.
*/
protected BendConnectionOperation getBendOperation() {
return (BendConnectionOperation) ((AbstractCompositeOperation) super.getOperation())
.getOperations().get(0);
}
private IAnchor getCompatibleAnchor(
List<IContentPart<? extends Node>> partsUnderMouse) {
for (IContentPart<? extends Node> part : partsUnderMouse) {
if (part == getHost()) {
continue;
}
IAnchorProvider anchorProvider = part
.getAdapter(IAnchorProvider.class);
if (anchorProvider != null) {
IAnchor anchor = anchorProvider.get(getHost());
if (anchor != null) {
return anchor;
}
}
}
return null;
}
/**
* Returns the {@link Connection} that is manipulated by this policy.
*
* @return The {@link Connection} that is manipulated by this policy.
*/
protected Connection getConnection() {
return getHost().getVisual();
}
/**
* Returns the current control points of the content.
*
* @return The current control points.
*/
protected List<BendPoint> getCurrentBendPoints() {
return getHost().getVisualBendPoints();
}
/**
* Returns the explicit anchor index for the first explicit anchor that is
* found within the connection's anchors when starting to search at the
* given connection index, and incrementing the index by the given step per
* iteration.
*
* @param startConnectionIndex
* The index at which the search starts.
* @param step
* The increment step (e.g. <code>1</code> or <code>-1</code>).
* @return The explicit anchor index for the first explicit anchor that is
* found within the connection's anchors when starting to search at
* the given index.
*/
protected int getExplicitIndex(int startConnectionIndex, int step) {
List<IAnchor> anchors = getConnection().getAnchorsUnmodifiable();
IConnectionRouter router = getConnection().getRouter();
for (int i = startConnectionIndex; i >= 0
&& i < anchors.size(); i += step) {
IAnchor anchor = anchors.get(i);
if (!router.wasInserted(anchor)) {
// found an explicit anchor => iterate explicit anchors to find
// the one with matching connection index
List<IAnchor> newAnchors = getBendOperation().getNewAnchors();
for (int j = 0; j < newAnchors.size(); j++) {
if (getBendOperation().getConnectionIndex(j) == i) {
return j;
}
}
throw new IllegalStateException(
"The explicit anchors of the connection are out of sync with the explicit anchors of the policy.");
}
}
// start and end need to be explicit, therefore, we should always be
// able to find an explicit anchor, regardless of the passed-in
// connection index
throw new IllegalStateException(
"The start and end anchor of a Connection need to be explicit.");
}
/**
* Returns an explicit anchor index for the first explicit anchor that can
* be found when iterating the connection anchors forwards, starting at the
* given connection index. If the anchor at the given index is an explicit
* anchor, an explicit anchor index for that anchor will be returned. If no
* explicit anchor is found, an exception is thrown, because the start and
* end anchor of a connection need to be explicit.
*
* @param connectionIndex
* The index that specifies the anchor of the connection at which
* the search starts.
* @return An explicit anchor index for the next explicit anchor.
*/
public int getExplicitIndexAtOrAfter(int connectionIndex) {
return getExplicitIndex(connectionIndex, 1);
}
/**
* Returns an explicit anchor index for the first explicit anchor that can
* be found when iterating the connection anchors backwards, starting at the
* given connection index. If the anchor at the given index is an explicit
* anchor, an explicit anchor index for that anchor will be returned. If no
* explicit anchor is found, an exception is thrown, because the start and
* end anchor of a connection need to be explicit.
*
* @param connectionIndex
* The index that specifies the anchor of the connection at which
* the search starts.
* @return An explicit anchor index for the previous explicit anchor.
*/
public int getExplicitIndexAtOrBefore(int connectionIndex) {
return getExplicitIndex(connectionIndex, -1);
}
@SuppressWarnings("unchecked")
@Override
public IBendableContentPart<Connection> getHost() {
return (IBendableContentPart<Connection>) super.getHost();
}
/**
* Returns the initial bend points before bending the content.
*
* @return The initial bend points.
*/
protected List<BendPoint> getInitialBendPoints() {
return initialBendPoints;
}
/**
* Computes the mouse movement delta (w.r.t. to the initial mouse position)
* in local coordinates .
*
* @param initialMousePositionInScene
* The initial mouse position in scene coordinates.
*
* @param currentMousePositionInScene
* The current mouse position in scene coordinates.
* @return The movement delta, translated into local coordinates of the
* connection
*
*/
// TODO: extract to somewhere else (this is used in several places)
protected Point getMouseDeltaInLocal(Point initialMousePositionInScene,
Point currentMousePositionInScene) {
Point mouseInLocal = FX2Geometry.toPoint(getConnection().sceneToLocal(
Geometry2FX.toFXPoint(currentMousePositionInScene)));
// compensate the movement of the local coordinate system w.r.t. the
// scene coordinate system (the scene coordinate system stays consistent
// w.r.t. mouse movement)
Point deltaInLocal = mouseInLocal.getTranslated(FX2Geometry
.toPoint(getConnection().sceneToLocal(
Geometry2FX.toFXPoint(initialMousePositionInScene)))
.getNegated());
return deltaInLocal;
}
/**
* Removes the overlay threshold, i.e. the distance between two points, so
* that they are regarded as overlaying. When the background grid is enables
* ( {@link GridModel#isShowGrid()}, then the grid cell size is used to
* determine the overlay threshold. Otherwise, the
* {@link #DEFAULT_OVERLAY_THRESHOLD} is used.
*
* @return The overlay threshold.
*/
protected double getOverlayThreshold() {
if (getConnection().getRouter() instanceof OrthogonalRouter
&& selectedExplicitAnchorIndices.size() == 2) {
// TODO: grid cell size
return DEFAULT_SEGMENT_OVERLAY_THRESHOLD;
}
// depending grid cell size
GridModel model = getHost().getRoot().getViewer()
.getAdapter(GridModel.class);
if (model != null && model.isSnapToGrid()) {
return Math.min(model.getGridCellWidth(), model.getGridCellHeight())
/ 4;
}
// fallback to default
return DEFAULT_OVERLAY_THRESHOLD;
}
private List<IContentPart<? extends Node>> getParts(
List<Node> nodesUnderMouse) {
List<IContentPart<? extends Node>> parts = new ArrayList<>();
IViewer viewer = getHost().getRoot().getViewer();
for (Node node : nodesUnderMouse) {
IVisualPart<? extends Node> part = PartUtils
.retrieveVisualPart(viewer, node);
if (part instanceof IContentPart) {
parts.add((IContentPart<? extends Node>) part);
}
}
return parts;
}
/**
* Returns the current position for the given explicit anchor index.
*
* @param explicitAnchorIndex
* @return
*/
private Point getPoint(int explicitAnchorIndex) {
return getConnection().getPoint(
getBendOperation().getConnectionIndex(explicitAnchorIndex));
}
/**
* Returns the initial positions of the selected points in the local
* coordinate system of the {@link #getConnection()}. May be
* <code>null</code> prior to the first {@link #move(Point, Point)} call.
*
* @return The initial positions of the selected points in the local
* coordinate system of the {@link #getConnection()}.
*/
public List<Point> getSelectedInitialPositions() {
return selectedInitialPositions;
}
private UpdateAnchorHintsOperation getUpdateHintsOperation() {
return (UpdateAnchorHintsOperation) ((AbstractCompositeOperation) super.getOperation())
.getOperations().get(1);
}
@Override
public void init() {
selectedExplicitAnchorIndices.clear();
selectedInitialPositions.clear();
preMoveExplicitAnchors.clear();
usePreMoveHints = true;
isNormalizationNeeded = false;
super.init();
// showAnchors("init:");
initialBendPoints = getCurrentBendPoints();
preMoveStartHint = getHost().getVisual().getStartPointHint();
preMoveEndHint = getHost().getVisual().getEndPointHint();
}
/**
* Creates a new static anchor for the given position and inserts it at the
* given index.
*
* @param insertionIndex
* The explicit anchor index at which the new anchor is inserted.
* @param mouseInScene
* The position for the new anchor in scene coordinates.
*/
protected void insertExplicitAnchor(int insertionIndex,
Point mouseInScene) {
// convert position to local coordinates
Point mouseInLocal = FX2Geometry.toPoint(getConnection()
.sceneToLocal(Geometry2FX.toFXPoint(mouseInScene)));
getBendOperation().getNewAnchors().add(insertionIndex,
createUnconnectedAnchor(mouseInLocal));
locallyExecuteOperation();
}
/**
* Returns <code>true</code> if the anchor at the given connection index is
* explicit. Otherwise returns <code>false</code>.
*
* @param connectionIndex
* The connection index that specifies the anchor to test.
* @return <code>true</code> if the specified anchor is explicit, otherwise
* <code>false</code>.
*/
public boolean isExplicit(int connectionIndex) {
IAnchor anchor = getConnection().getAnchor(connectionIndex);
return !getConnection().getRouter().wasInserted(anchor);
}
/**
* Returns true if the first specified anchor overlays the second specified
* anchor.
*
* @param overlayingExplicitAnchorIndex
* @param overlainExplicitAnchorIndex
* @return
*/
private boolean isExplicitOverlay(int overlayingExplicitAnchorIndex,
int overlainExplicitAnchorIndex) {
return getPoint(overlayingExplicitAnchorIndex).getDistance(
getPoint(overlainExplicitAnchorIndex)) <= getOverlayThreshold();
}
/**
* Returns <code>true</code> if the selected points are on a horizontal
* line. Otherwise returns <code>false</code>.
*
* @return <code>true</code> if the selected points are on a horizontal
* line, otherwise <code>false</code>.
*/
public boolean isSelectionHorizontal() {
return isSelectionHorizontal;
}
private boolean isUnpreciseEquals(double y0, double y1) {
return Math.abs(y0 - y1) < 1;
}
@Override
protected void locallyExecuteOperation() {
// locally execute bend operation
try {
getBendOperation().execute(null, null);
} catch (Exception x) {
throw new IllegalStateException(x);
}
// apply hints
if (usePreMoveHints) {
getUpdateHintsOperation().setNewHints(preMoveStartHint,
preMoveEndHint);
} else {
Point newStartHint = computeStartHint();
Point newEndHint = computeEndHint();
getUpdateHintsOperation().setNewHints(newStartHint, newEndHint);
}
// locally execute hints operation
try {
getUpdateHintsOperation().execute(null, null);
} catch (Exception x) {
throw new IllegalStateException(x);
}
}
/**
* Makes the connection anchor at the given connection index explicit and
* returns its explicit index.
*
* @param connectionIndex
* The connection index to make explicit.
* @return The (new) explicit index for the given connection index.
*/
public int makeExplicit(int connectionIndex) {
return makeExplicit(connectionIndex, connectionIndex).get(0);
}
/**
* Makes the connection anchors within the given range of connection indices
* explicit and returns their explicit indices.
*
* @param startConnectionIndex
* The first connection index to make explicit.
* @param endConnectionIndex
* The last connection index to make explicit.
* @return A list of explicit anchor indices for the given range of
* connection indices.
*/
public List<Integer> makeExplicit(int startConnectionIndex,
int endConnectionIndex) {
// new explicit point => normalization needed
isNormalizationNeeded = true;
// find the anchor handle before the start index
List<ImplicitGroup> implicitGroups = new ArrayList<>();
boolean isStartExplicit = isExplicit(startConnectionIndex);
implicitGroups.add(new ImplicitGroup(
getExplicitIndexAtOrBefore(startConnectionIndex)));
// find implicit groups within the given index range
for (int i = startConnectionIndex; i <= endConnectionIndex; i++) {
if (isExplicit(i)) {
// start a new group
int explicitAnchorHandle = getExplicitIndexAtOrBefore(i);
implicitGroups.add(new ImplicitGroup(explicitAnchorHandle));
} else {
// add point to current group
Point pointInLocal = getConnection().getPoint(i);
Point pointInScene = FX2Geometry.toPoint(getConnection()
.localToScene(Geometry2FX.toFXPoint(pointInLocal)));
implicitGroups.get(implicitGroups.size() - 1).points
.add(pointInScene);
}
}
// remove first group if empty
if (implicitGroups.get(0).points.isEmpty()) {
implicitGroups.remove(0);
}
// create explicit anchors one by one in reverse order so that the
// indices are not messed up
int addedCount = 0;
List<Integer> handles = new ArrayList<>();
for (int i = 0; i < implicitGroups.size(); i++) {
ImplicitGroup ig = implicitGroups.get(i);
int prec = ig.precedingExplicitIndex + addedCount;
if (!handles.isEmpty() || isStartExplicit) {
handles.add(prec);
}
for (Point p : ig.points) {
prec = createAfter(prec, p);
addedCount++;
handles.add(prec);
}
}
return handles;
}
/**
* Moves the currently selected point to the given mouse position in scene
* coordinates.
*
* @param initialMouseInScene
* The initial mouse position in scene coordinates.
* @param currentMouseInScene
* The current mouse position in scene coordinates.
*/
public void move(Point initialMouseInScene, Point currentMouseInScene) {
checkInitialized();
// showAnchors("Before Restore:");
// determine selection status
int numPoints = selectedExplicitAnchorIndices.size();
boolean isOrtho = numPoints == 2
&& getConnection().getRouter() instanceof OrthogonalRouter;
// save/restore explicit anchors
if (preMoveExplicitAnchors.isEmpty()) {
// first move => we need to normalize upon commit now
isNormalizationNeeded = true;
usePreMoveHints = false;
// save initial selected positions
for (int i = 0; i < selectedExplicitAnchorIndices.size(); i++) {
selectedInitialPositions.add(i,
getPoint(selectedExplicitAnchorIndices.get(i)));
}
// save initial pre-move explicit anchors
preMoveExplicitAnchors.addAll(getBendOperation().getNewAnchors());
// determine selection segment orientation
if (isOrtho) {
double y0 = selectedInitialPositions.get(0).y;
double y1 = selectedInitialPositions.get(1).y;
isSelectionHorizontal = isUnpreciseEquals(y0, y1);
}
// save initial pre-move hints
preMoveStartHint = computeStartHint();
preMoveEndHint = computeEndHint();
} else {
// restore initial pre-move explicit anchors
getBendOperation().setNewAnchors(preMoveExplicitAnchors);
// restore initial pre-move hints
getUpdateHintsOperation().setNewHints(preMoveStartHint,
preMoveEndHint);
usePreMoveHints = true;
locallyExecuteOperation();
usePreMoveHints = false;
}
// showAnchors("After Restore:");
// constrain movement in one direction for segment based connections
Point mouseDeltaInLocal = getMouseDeltaInLocal(initialMouseInScene,
currentMouseInScene);
if (isOrtho) {
if (isSelectionHorizontal) {
mouseDeltaInLocal.x = 0;
} else {
mouseDeltaInLocal.y = 0;
}
}
// update positions
for (int i = 0; i < selectedExplicitAnchorIndices.size(); i++) {
Point selectedPointCurrentPositionInLocal = selectedInitialPositions
.get(i).getTranslated(mouseDeltaInLocal);
int explicitAnchorIndex = selectedExplicitAnchorIndices.get(i);
boolean canConnect = canConnect(explicitAnchorIndex);
// update anchor
getBendOperation().getNewAnchors().set(explicitAnchorIndex,
findOrCreateAnchor(selectedPointCurrentPositionInLocal,
canConnect));
}
locallyExecuteOperation();
// showAnchors("After Move:");
// remove overlain
removeOverlain();
// showAnchors("After RemoveOverlain:");
}
/**
* For segment based connections, the control points need to be normalized,
* i.e. all control points that lie on the orthogonal connection between two
* other control points have to be removed.
*/
public void normalize() {
if (!(getConnection().getRouter() instanceof OrthogonalRouter)) {
return;
}
// enable hint computation
usePreMoveHints = false;
// execute operation so that changes are applied
locallyExecuteOperation();
// determine all connection anchors
List<IAnchor> anchors = getConnection().getAnchorsUnmodifiable();
// determine corresponding positions
List<Point> positions = getConnection().getPointsUnmodifiable();
// test each explicit static anchor for removal potential
int explicitIndex = 0; // start is explicit
boolean removed = false;
for (int i = 1; i < anchors.size() - 1; i++) {
IAnchor anchor = anchors.get(i);
if (!getConnection().getRouter().wasInserted(anchor)) {
// found an explicit anchor
explicitIndex++;
// determine surrounding positions
Point prev = positions.get(i - 1);
Point next = positions.get(i + 1);
Point current = positions.get(i);
// determine in-direction and out-direction for current
// point
Vector inDirection = new Vector(prev, current);
Vector outDirection = new Vector(current, next);
if (inDirection.isNull() || outDirection.isNull()
|| inDirection.isParallelTo(outDirection)) {
// XXX: Compute previous position in scene coordinates
// before manipulating the connection.
Point prevInScene = FX2Geometry.toPoint(
getConnection().localToScene(prev.x, prev.y));
// make previous and next explicit
if (getConnection().getRouter()
.wasInserted(anchors.get(i + 1))) {
// make next explicit
makeExplicit(i + 1);
}
if (getConnection().getRouter()
.wasInserted(anchors.get(i - 1))) {
// make previous explicit
// XXX: We need to insert a point manually here and
// cannot rely on makeExplicit() because the indices
// could have changed.
createBefore(explicitIndex, prevInScene);
explicitIndex++;
}
// remove current point as it is unnecessary
getBendOperation().getNewAnchors().remove(explicitIndex);
// start a new normalization
removed = true;
break;
}
}
}
if (removed) {
normalize();
}
}
private void removeOverlain() {
if (getConnection().getRouter() instanceof OrthogonalRouter
&& selectedExplicitAnchorIndices.size() == 2) {
// segment overlay removal for orthogonal connection
removeOverlainSegments();
} else {
// point overlay removal otherwise
removeOverlainPoints();
}
}
private void removeOverlainPoints() {
int explicitAnchorsSize = getBendOperation().getNewAnchors().size();
for (int i = selectedExplicitAnchorIndices.size() - 1; i >= 0
&& explicitAnchorsSize > 2; i--) {
int index = selectedExplicitAnchorIndices.get(i);
// XXX: If an overlay is recognized, the overlaying anchor is
// removed and practically replaced by the overlain anchor. This
// might seem unintuitive, however, it enables the user to
// cleanly remove control points by dragging them onto a neighboring
// point, without augmenting any other control points.
boolean isLeftOverlain = index > 0
&& isExplicitOverlay(index, index - 1);
boolean isRightOverlain = index < explicitAnchorsSize - 1
&& isExplicitOverlay(index, index + 1);
if (isLeftOverlain || isRightOverlain) {
int overlainIndex = isLeftOverlain ? index - 1 : index + 1;
if (selectedExplicitAnchorIndices.contains(overlainIndex)) {
// selected overlays other selected
// => skip this overlay
continue;
}
// remove from connection
getBendOperation().getNewAnchors().remove(index);
// apply changes by executing the operation
locallyExecuteOperation();
}
}
}
private void removeOverlainSegments() {
// define indices for segment overlays
int[][] possibleSegmentOverlays = new int[][] {
new int[] { -2, -1, 2, 3 }, new int[] { -2, -1, 2 },
new int[] { -1, 2, 3 }, new int[] { -1, 2 },
new int[] { -2, -1 }, new int[] { 2, 3 }, new int[] { 2 },
new int[] { -1 } };
// test for segment overlays and remove the first segment overlays that
// can be found
boolean removed = false;
for (int i = 0; i < possibleSegmentOverlays.length && !removed; i++) {
removed = testAndRemoveSegmentOverlay(possibleSegmentOverlays[i]);
}
// apply changes (if any)
if (removed) {
locallyExecuteOperation();
}
}
/**
* Provides position hints to the connection's {@link IConnectionRouter} and
* let's the router route the connection, so these position hints can be
* forwarded to the anchors.
*/
protected void route() {
}
/**
* Selects the point specified by the given segment index and parameter for
* manipulation. Captures the initial position of the selected point and the
* related initial mouse location.
*
* @param explicitAnchorIndex
* Index of the explicit anchor to select for manipulation.
*/
public void select(int explicitAnchorIndex) {
checkInitialized();
// save selected anchor handles
selectedExplicitAnchorIndices.add(explicitAnchorIndex);
}
/**
* Selects the end points of the connection segment specified by the given
* index. Makes the corresponding anchors explicit first and copies them if
* they are connected.
*
* @param segmentIndex
* The index of a connection segment.
*/
public void selectSegment(int segmentIndex) {
// determine indices of neighbor anchors
int firstSegmentIndex = segmentIndex;
int secondSegmentIndex = segmentIndex + 1;
// determine connectedness for neighbor anchors
Node firstAnchorage = getConnection().getAnchor(firstSegmentIndex)
.getAnchorage();
boolean isFirstConnected = firstAnchorage != null
&& firstAnchorage != getConnection();
Node secondAnchorage = getConnection().getAnchor(secondSegmentIndex)
.getAnchorage();
boolean isSecondConnected = secondAnchorage != null
&& secondAnchorage != getConnection();
// make explicit
List<Integer> explicit = makeExplicit(firstSegmentIndex,
secondSegmentIndex);
int firstAnchorHandle = explicit.get(0);
int secondAnchorHandle = explicit.get(1);
// create unconnected copies of the segment anchors if they are
// connected
if (isFirstConnected) {
firstAnchorHandle = createAfter(firstAnchorHandle,
FX2Geometry.toPoint(getConnection().localToScene(Geometry2FX
.toFXPoint(getPoint(firstAnchorHandle)))));
// XXX: increase index of second anchor because one anchor was
// inserted before it
secondAnchorHandle++;
}
if (isSecondConnected) {
secondAnchorHandle = createBefore(secondAnchorHandle,
FX2Geometry.toPoint(getConnection().localToScene(Geometry2FX
.toFXPoint(getPoint(secondAnchorHandle)))));
}
// select the end anchors for manipulation
select(firstAnchorHandle);
select(secondAnchorHandle);
}
// private void showAnchors(String message) {
// List<IAnchor> newAnchors = getBendOperation().getNewAnchors();
// String anchorsString = "";
// for (int i = 0, j = 0; i < getConnection().getAnchorsUnmodifiable()
// .size(); i++) {
// IAnchor anchor = getConnection().getAnchor(i);
// if (getConnection().getRouter().wasInserted(anchor)) {
// anchorsString = anchorsString + " - "
// + anchor.getClass().toString() + "["
// + getConnection().getPoint(i) + "],\n";
// } else {
// anchorsString = anchorsString
// + (selectedExplicitAnchorIndices.contains(j) ? "(*)"
// : " * ")
// + anchor.getClass().toString() + "["
// + getConnection().getPoint(i) + " :: "
// + NodeUtils.localToScene(getConnection(),
// getConnection().getPoint(i))
// + "]" + " (" + newAnchors.get(j) + "),\n";
// j++;
// }
// }
// System.out.println(message + "\n" + anchorsString);
// }
/**
* Tests if the current selection complies to the overlay specified by the
* given parameters. The <i>overlainPointIndicesRelativeToSelection</i> is
* an integer array that specifies the indices (relative to the selected
* indices) of all points that are tested to be overlain by the current
* selection.
* <p>
* The points specified by the given indices need to be aligned with the
* selection, i.e. they need to be on a vertical or horizontal line. The
* first and last indices specify the resulting segment which the selection
* snaps to in case of an overlay. If the distance between the resulting
* segment and the selected segment is smaller than the
* {@link #getOverlayThreshold()}, then all specified points and the
* selection are replaced by the result segment.
*
* @param overlainPointIndicesRelativeToSelection
* An integer array that specifies the indices (relative to the
* selected indices) of all points that are part of this overlay
* in ascending order, excluding the selected indices.
* @return <code>true</code> if the overlay was removed, otherwise
* <code>false</code>.
*/
private boolean testAndRemoveSegmentOverlay(
int[] overlainPointIndicesRelativeToSelection) {
// check that positions are present for the given indices within the
// connection. if not all are present, return without applying any
// modifications.
List<Point> points = Arrays.asList(Point.getCopy(getConnection()
.getPointsUnmodifiable().toArray(new Point[] {})));
int firstIndex = overlainPointIndicesRelativeToSelection[0];
int lastIndex = overlainPointIndicesRelativeToSelection[overlainPointIndicesRelativeToSelection.length
- 1];
int selectionStartIndexInConnection = getBendOperation()
.getConnectionIndex(selectedExplicitAnchorIndices.get(0));
if (selectionStartIndexInConnection + firstIndex < 0
|| selectionStartIndexInConnection + firstIndex >= points
.size()) {
return false;
}
if (selectionStartIndexInConnection + lastIndex < 0
|| selectionStartIndexInConnection + lastIndex >= points
.size()) {
return false;
}
// evaluate positions for the given indices
List<Point> overlainPoints = new ArrayList<>();
for (int i = 0; i < overlainPointIndicesRelativeToSelection.length; i++) {
overlainPoints.add(points.get(selectionStartIndexInConnection
+ overlainPointIndicesRelativeToSelection[i]));
}
// determine segment positions (relative to their orientations). if not
// all segments have the same position, return without applying any
// modifications.
double p = isSelectionHorizontal ? overlainPoints.get(0).y
: overlainPoints.get(0).x;
// System.out.println("same coordinate = " + p);
for (int i = 1; i < overlainPoints.size(); i++) {
Point q = overlainPoints.get(i);
if (isSelectionHorizontal && !isUnpreciseEquals(p, q.y)
|| !isSelectionHorizontal && !isUnpreciseEquals(p, q.x)) {
// wrong orientation
return false;
}
if (isSelectionHorizontal && !isUnpreciseEquals(p, q.y)
|| !isSelectionHorizontal && !isUnpreciseEquals(p, q.x)) {
// wrong position
return false;
}
}
// compute the (provisional) resulting segment from the given overlain
// indices. the first index is the start index for the result, the last
// index is the end index for the result.
Point resultStart = overlainPoints.get(0);
Point resultEnd = overlainPoints.get(overlainPoints.size() - 1);
// compute the distance between the selected segment and the overlain
// result segment. if the distance is above the removal threshold,
// return without applying any modifications.
Point selectionStart = points.get(selectionStartIndexInConnection);
Point selectionEnd = points.get(selectionStartIndexInConnection + 1);
double distance = Math
.abs(isSelectionHorizontal ? resultStart.y - selectionStart.y
: resultStart.x - selectionStart.x);
if (distance > getOverlayThreshold()) {
return false;
}
// System.out.println("=== Segment Overlay ===");
// System.out.println("selection: " + selectedExplicitAnchorIndices);
// System.out.println(
// "overlain: " + toList(overlainPointIndicesRelativeToSelection));
// System.out.println("overlain points: " + overlainPoints);
// System.out.println(
// "selection line: " + selectionStart + " -> " + selectionEnd);
// System.out.println("result line: " + resultStart + " -> " +
// resultEnd);
// System.out.println("distance: " + distance);
// at this point, the overlay is confirmed and needs to be removed.
// therefore, the overlap of selection and result needs to be removed
// and their difference needs to be saved as the final result
if (overlainPointIndicesRelativeToSelection.length <= 2) {
if (isSelectionHorizontal) {
// same y values => adjust x
if (firstIndex < 0) {
resultEnd.x = selectionEnd.x;
} else {
resultStart.x = selectionStart.x;
}
} else {
// same x values => adjust y
if (firstIndex < 0) {
resultEnd.y = selectionEnd.y;
} else {
resultStart.y = selectionStart.y;
}
}
}
// System.out.println("result: " + resultStart + " -> " + resultEnd);
// make the result segment explicit
int overlayStartIndex = Math.min(selectionStartIndexInConnection,
selectionStartIndexInConnection + firstIndex);
int overlayEndIndex = Math.max(selectionStartIndexInConnection + 1,
selectionStartIndexInConnection + lastIndex);
List<Integer> explicit = makeExplicit(overlayStartIndex,
overlayEndIndex);
// showAnchors("After makeExplicit:");
// remove the selection and the other overlain anchors
int removedCount = 0;
for (int i = explicit.size() - 2; i >= 1; i--) {
getBendOperation().getNewAnchors().remove((int) explicit.get(i));
removedCount++;
}
// overwrite the first and last explicit anchor with a new unconnected
// anchor for the adjusted result position if the respective anchor is
// currently unconnected and neither the start nor the end point
Integer resultStartIndex = explicit.get(0);
IAnchor resultStartAnchor = getBendOperation().getNewAnchors()
.get(resultStartIndex);
Connection connection = getBendOperation().getConnection();
if (resultStartIndex > 0
&& !connection.isConnected(resultStartAnchor)) {
// System.out.println(
// "Insert unconnected result start at " + resultStartIndex);
getBendOperation().getNewAnchors().set(resultStartIndex,
createUnconnectedAnchor(resultStart));
}
Integer resultEndIndex = explicit.get(explicit.size() - 1)
- removedCount;
IAnchor resultEndAnchor = getBendOperation().getNewAnchors()
.get(resultEndIndex);
if (resultEndIndex < getBendOperation().getNewAnchors().size() - 1
&& !connection.isConnected(resultEndAnchor)) {
// System.out.println(
// "Insert unconnected result end at " + resultEndIndex);
getBendOperation().getNewAnchors().set(resultEndIndex,
createUnconnectedAnchor(resultEnd));
}
return true;
}
// private List<Integer> toList(int[] array) {
// List<Integer> list = new ArrayList<>();
// for (int item : array) {
// list.add(item);
// }
// return list;
// }
@Override
public String toString() {
return "BendConnectionPolicy[host=" + getHost() + "]";
}
}