/*******************************************************************************
* 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:
* 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.Comparator;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.gef.common.beans.property.ReadOnlyListPropertyBaseEx;
import org.eclipse.gef.common.collections.CollectionUtils;
import org.eclipse.gef.common.collections.ListListenerHelperEx;
import org.eclipse.gef.fx.anchors.AbstractAnchor;
import org.eclipse.gef.fx.anchors.AnchorKey;
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.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.convert.fx.Geometry2FX;
import org.eclipse.gef.geometry.planar.BezierCurve;
import org.eclipse.gef.geometry.planar.ICurve;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.geometry.planar.PolyBezier;
import com.google.common.collect.Iterators;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerPropertyBase;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.transform.Transform;
/**
* A (binary) {@link Connection} is a visual curveProperty, whose appearance is
* defined through a single start and end point, and a set of control points,
* which may be 'connected', i.e. be attached to an {@link IAnchor}. The exact
* curveProperty shape is determined by an {@link IConnectionRouter}, which is
* responsible of computing an {@link ICurve} geometry for a given
* {@link Connection} (which is then rendered using a {@link GeometryNode}).
* <p>
* Whether the control points are interpreted as way points (that lie on the
* curveProperty) or as 'real' control points depends on the
* {@link IConnectionInterpolator}. While {@link PolylineInterpolator} and
* {@link PolyBezierInterpolator} interpret control points to be way points,
* other routers may e.g. interpret them as the control points of a
* {@link BezierCurve}.
* <P>
* In addition to the curveProperty shape, the visual appearance of a
* {@link Connection} can be controlled via start and end decorations. They will
* be rendered 'on-top' of the curveProperty shape and the curveProperty shape
* will be properly clipped at the decorations (so it does not paint through).
*
* @author anyssen
* @author mwienand
*
*/
public class Connection extends Group {
private final class AnchorsUnmodifiableProperty
extends LazyReadOnlyListPropertyBase<IAnchor> {
@Override
public void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
@Override
public ObservableList<IAnchor> get() {
return getAnchorsUnmodifiable();
}
}
private abstract class LazyReadOnlyListPropertyBase<E>
extends ReadOnlyListPropertyBaseEx<E> {
private class EmptyProperty extends ReadOnlyBooleanPropertyBase {
@Override
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
@Override
public boolean get() {
return isEmpty();
}
@Override
public Object getBean() {
return LazyReadOnlyListPropertyBase.this;
}
@Override
public String getName() {
return "empty";
}
}
private class SizeProperty extends ReadOnlyIntegerPropertyBase {
@Override
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
@Override
public int get() {
return size();
}
@Override
public Object getBean() {
return LazyReadOnlyListPropertyBase.this;
}
@Override
public String getName() {
return "size";
}
}
private ReadOnlyBooleanProperty emptyProperty;
private ReadOnlyIntegerProperty sizeProperty;
private ObservableList<E> lazyValue = CollectionUtils
.observableArrayList();
public LazyReadOnlyListPropertyBase() {
// lazy listener will forward changes of lazy value, which will be
// updated within fireValueChangeEvent()
lazyValue.addListener(new ListChangeListener<E>() {
@Override
public void onChanged(
ListChangeListener.Change<? extends E> c) {
fireValueChangedEvent(
new ListListenerHelperEx.AtomicChange<>(get(), c));
}
});
}
@Override
public ReadOnlyBooleanProperty emptyProperty() {
if (emptyProperty == null) {
emptyProperty = new EmptyProperty();
}
return emptyProperty;
}
@Override
public void fireValueChangedEvent() {
// XXX fireValueChangedEvent() is overwritten, so it can be called
// from within refresh to notify about changes (the list change
// event will be computed from the old and current value). We thus
// overwrite getValue() to return a copy (so a change can be
// computed from oldValue and currentValue). Note that list
// change events will never be notified via
// fireValueChangedEvent(Change), as ReadOnlyListPropertyBaseEx is
// no WritableValue (so we do not have to guard this in addition).
lazyValue.setAll(get());
}
@Override
public Object getBean() {
return Connection.this;
}
@Override
public String getName() {
return "points";
}
@Override
public ReadOnlyIntegerProperty sizeProperty() {
if (sizeProperty == null) {
sizeProperty = new SizeProperty();
}
return sizeProperty;
}
}
private final class PointsUnmodifiableProperty
extends LazyReadOnlyListPropertyBase<Point> {
@Override
public ObservableList<Point> get() {
return getPointsUnmodifiable();
}
@Override
public String getName() {
return "points";
}
}
/**
* The <i>id</i> used to identify the start point of this connection at the
* start anchor.
*/
private static final String START_ROLE = "start";
/**
* The <i>id</i> used to identify the end point of this connection at the
* end anchor.
*/
private static final String END_ROLE = "end";
/**
* Prefix for the default <i>ids</i> used by this connection to identify
* specific control points at control point anchorsByKeys.
*/
private static final String CONTROL_POINT_ROLE_PREFIX = "controlpoint-";
private ObjectProperty<Node> curveProperty = new SimpleObjectProperty<>();
private ObjectProperty<Node> startDecorationProperty = null;
private ObjectProperty<Node> endDecorationProperty = null;
private ObjectProperty<IConnectionRouter> routerProperty = new SimpleObjectProperty<>(
new StraightRouter());
private ObjectProperty<IConnectionInterpolator> interpolatorProperty = new SimpleObjectProperty<>(
new PolylineInterpolator());
// XXX: Maintain anchors in a sorted map, so we can use it to determine the
// mapping between anchor keys and anchor indexes.
private TreeMap<AnchorKey, IAnchor> anchorsByKeys = new TreeMap<>(
new Comparator<AnchorKey>() {
@Override
public int compare(AnchorKey o1, AnchorKey o2) {
if (o1.getId().equals(o2.getId())) {
return 0;
} else {
if (getStartAnchorKey().getId().equals(o1.getId())) {
return -1;
} else if (getEndAnchorKey().getId()
.equals(o1.getId())) {
return 1;
} else {
if (getStartAnchorKey().getId()
.equals(o2.getId())) {
return 1;
} else if (getEndAnchorKey().getId()
.equals(o2.getId())) {
return -1;
}
return getControlAnchorIndex(o1)
- getControlAnchorIndex(o2);
}
}
}
});
private Map<AnchorKey, Point> hintsByKeys = new HashMap<>();
private ObservableList<IAnchor> anchors = CollectionUtils
.observableArrayList();
private ObservableList<Point> points = CollectionUtils
.observableArrayList();
private PointsUnmodifiableProperty pointsUnmodifiableProperty = null;
private AnchorsUnmodifiableProperty anchorsUnmodifiableProperty = null;
private Map<AnchorKey, MapChangeListener<? super AnchorKey, ? super Point>> anchorsPCL = new HashMap<>();
private ChangeListener<Node> decorationListener = new ChangeListener<Node>() {
final ChangeListener<Bounds> decorationLayoutBoundsListener = new ChangeListener<Bounds>() {
@Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
if (inRefresh) {
return;
}
// refresh decoration clip in case the layout bounds of
// the decorations have changed
refresh();
}
};
@Override
public void changed(ObservableValue<? extends Node> observable,
Node oldValue, Node newValue) {
if (inRefresh) {
return;
}
if (oldValue != null) {
oldValue.layoutBoundsProperty()
.removeListener(decorationLayoutBoundsListener);
}
if (newValue != null) {
newValue.layoutBoundsProperty()
.addListener(decorationLayoutBoundsListener);
}
refresh();
}
};
private boolean inRefresh = false;
/**
* Constructs a new {@link Connection} whose start and end point are set to
* <code>null</code>.
*/
public Connection() {
// disable resizing children which would change their layout positions
// in some cases
setAutoSizeChildren(false);
routerProperty.addListener(new ChangeListener<IConnectionRouter>() {
@Override
public void changed(
ObservableValue<? extends IConnectionRouter> observable,
IConnectionRouter oldValue, IConnectionRouter newValue) {
if (inRefresh) {
return;
}
refresh();
}
});
interpolatorProperty
.addListener(new ChangeListener<IConnectionInterpolator>() {
@Override
public void changed(
ObservableValue<? extends IConnectionInterpolator> observable,
IConnectionInterpolator oldValue,
IConnectionInterpolator newValue) {
refresh();
}
});
curveProperty.addListener(new ChangeListener<Node>() {
private ChangeListener<Transform> transformListener = new ChangeListener<Transform>() {
@Override
public void changed(
ObservableValue<? extends Transform> observable,
Transform oldValue, Transform newValue) {
refresh();
}
};
private ChangeListener<Bounds> boundsListener = new ChangeListener<Bounds>() {
@Override
public void changed(
ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
refresh();
}
};
@Override
public void changed(ObservableValue<? extends Node> observable,
Node oldValue, Node newValue) {
// TODO: unregister listeners instead of setting refresh
inRefresh = true;
if (oldValue != null) {
getChildren().remove(oldValue);
oldValue.localToParentTransformProperty()
.removeListener(transformListener);
oldValue.layoutBoundsProperty()
.removeListener(boundsListener);
}
if (newValue != null) {
newValue.layoutBoundsProperty().addListener(boundsListener);
newValue.localToParentTransformProperty()
.addListener(transformListener);
getChildren().add(newValue);
}
// XXX: Can only reattach anchor keys if the new curve is part
// of the scene graph. Otherwise, visual changes do not lead to
// anchor position recomputations, because the necessary
// visual-change-listener cannot be registered if no common
// ancestor of the anchorage and anchored nodes can be found.
reattachAnchorKeys(oldValue, newValue);
inRefresh = false;
refresh();
}
});
// set default curve
setCurve(new GeometryNode<ICurve>());
// init start and end points
setStartPoint(new Point());
setEndPoint(new Point());
}
/**
* Inserts the given {@link IAnchor} into the
* {@link #anchorsUnmodifiableProperty()} of this {@link Connection}. The
* given {@link AnchorKey} is attached to the {@link IAnchor}. Furthermore,
* a {@link #createPCL(AnchorKey) PCL} for the {@link AnchorKey} is
* registered on the position property of the {@link IAnchor} and the
* visualization is {@link #refresh() refreshed}.
*
* @param anchorKey
* The {@link AnchorKey} under which the {@link IAnchor} is to be
* registered.
* @param anchor
* The {@link IAnchor} which is inserted.
*/
protected void addAnchor(AnchorKey anchorKey, IAnchor anchor) {
if (anchorKey == null) {
throw new IllegalArgumentException("anchorKey may not be null.");
}
if (anchorKey.getAnchored() != getCurve()) {
throw new IllegalArgumentException(
"anchorKey may only be anchored to curveProperty node");
}
if (anchor == null) {
throw new IllegalArgumentException("anchor may not be null.");
}
AnchorKey startAnchorKey = getStartAnchorKey();
AnchorKey endAnchorKey = getEndAnchorKey();
List<IAnchor> controlAnchorsToMove = new ArrayList<>();
if (!anchorKey.equals(startAnchorKey)
&& !anchorKey.equals(endAnchorKey)) {
int controlAnchorIndex = getControlAnchorIndex(anchorKey);
// remove all control points at a larger index
int pointCount = anchorsByKeys.size();
for (int i = pointCount - 1; i >= 0; i--) {
// (temporarily) remove all anchorsByKeys that are to be moved
// up
AnchorKey ak = getAnchorKey(i);
if (!ak.equals(startAnchorKey) && !ak.equals(endAnchorKey)) {
if (getControlAnchorIndex(ak) >= controlAnchorIndex) {
IAnchor a = getAnchor(i);
unregisterPCL(ak, a);
controlAnchorsToMove.add(0, a);
int anchorIndex = getAnchorIndex(ak);
points.remove(anchorIndex);
anchors.remove(anchorIndex);
anchorsByKeys.remove(ak);
a.detach(ak);
}
}
}
}
// update anchor map and list
anchorsByKeys.put(anchorKey, anchor);
// attach anchor key
anchor.attach(anchorKey);
// update lists
anchors.add(getAnchorIndex(anchorKey), anchor);
points.add(getAnchorIndex(anchorKey),
FX2Geometry.toPoint(getCurve().localToParent(
Geometry2FX.toFXPoint(anchor.getPosition(anchorKey)))));
if (!anchorKey.equals(startAnchorKey)
&& !anchorKey.equals(endAnchorKey)) {
int controlIndex = getControlAnchorIndex(anchorKey);
// re-add all control points at a larger index
for (int i = 0; i < controlAnchorsToMove.size(); i++) {
AnchorKey ak = getControlAnchorKey(controlIndex + i + 1);
IAnchor a = controlAnchorsToMove.get(i);
anchorsByKeys.put(ak, a);
a.attach(ak);
int anchorIndex = getAnchorIndex(ak);
anchors.add(anchorIndex, a);
points.add(anchorIndex,
FX2Geometry.toPoint(getCurve().localToParent(
Geometry2FX.toFXPoint(a.getPosition(ak)))));
registerPCL(ak, a);
}
}
registerPCL(anchorKey, anchor);
refresh();
}
/**
* Adds the given {@link IAnchor} as a control point anchor for the given
* index into the {@link #anchorsUnmodifiableProperty()} of this
* {@link Connection}.
*
* @param index
* The position where the {@link IAnchor} is inserted within the
* control point anchorsByKeys of this {@link Connection}.
* @param anchor
* The {@link IAnchor} which determines the position of the
* corresponding control point.
*/
public void addControlAnchor(int index, IAnchor anchor) {
if (anchor == null) {
throw new IllegalArgumentException("anchor may not be null.");
}
addAnchor(getControlAnchorKey(index), anchor);
}
/**
* Adds an {@link StaticAnchor} yielding the given {@link Point} as a
* control point anchor for the given index into the
* {@link #anchorsUnmodifiableProperty()} of this {@link Connection}.
*
* @param index
* The position where the {@link IAnchor} is inserted within the
* control point anchorsByKeys of this {@link Connection}.
* @param controlPoint
* The position for the specified control point.
*/
public void addControlPoint(int index, Point controlPoint) {
if (controlPoint == null) {
throw new IllegalArgumentException("controlPoint may not be null.");
}
IAnchor anchor = new StaticAnchor(this, controlPoint);
addControlAnchor(index, anchor);
}
/**
* Returns an unmodifiable read-only list property, which contains the
* {@link IAnchor}s that determine the start point, control points, and end
* point of this {@link Connection}.
*
* @return An unmodifiable read-only list property containing this
* {@link Connection}'s anchors.
*/
public ReadOnlyListProperty<IAnchor> anchorsUnmodifiableProperty() {
// property is created lazily to save memory
if (anchorsUnmodifiableProperty == null) {
anchorsUnmodifiableProperty = new AnchorsUnmodifiableProperty();
}
return anchorsUnmodifiableProperty;
}
/**
* Creates a position change listener (PCL) which {@link #refresh()
* refreshes} this {@link Connection} upon anchor position changes
* corresponding to the given {@link AnchorKey}.
*
* @param anchorKey
* The {@link AnchorKey} for which a position change will trigger
* a {@link #refresh()} with the returned PCL.
* @return A position change listener to {@link #refresh() refresh} this
* {@link Connection} when the position for the given
* {@link AnchorKey} changes.
*/
protected MapChangeListener<? super AnchorKey, ? super Point> createPCL(
final AnchorKey anchorKey) {
return new MapChangeListener<AnchorKey, Point>() {
@Override
public void onChanged(
MapChangeListener.Change<? extends AnchorKey, ? extends Point> change) {
// if (inRefresh) {
// return;
// }
if (change.getKey().equals(anchorKey)) {
if (change.wasAdded() && change.wasRemoved()) {
Point newPoint = FX2Geometry
.toPoint(getCurve().localToParent(Geometry2FX
.toFXPoint(change.getValueAdded())));
if (!points.get(getAnchorIndex(anchorKey))
.equals(newPoint)) {
points.set(getAnchorIndex(anchorKey), newPoint);
refresh();
}
}
}
}
};
}
/**
* Returns a property wrapping the curve {@link Node}.
*
* @return The curve {@link Node} used to visualize the connection.
*/
public ObjectProperty<Node> curveProperty() {
return curveProperty;
}
/**
* Returns an {@link ObjectProperty} wrapping the end decoration
* {@link Node}.
*
* @return A property wrapping the end decoration.
*/
public ObjectProperty<Node> endDecorationProperty() {
if (endDecorationProperty == null) {
endDecorationProperty = new SimpleObjectProperty<>();
endDecorationProperty.addListener(decorationListener);
}
return endDecorationProperty;
}
/**
* Returns the anchor at the given index. The start anchor will be provided
* for <code>index == 0</code>, the end anchor for the last defined index.
* Control anchorsByKeys will be returned for all indices in between.
*
* @param index
* The index of the anchor to retrieve.
* @return The anchor at the given index.
*/
public IAnchor getAnchor(int index) {
return anchorsByKeys.get(getAnchorKey(index));
}
/**
* Returns the anchor index for the given {@link AnchorKey}.
*
* @param anchorKey
* The {@link AnchorKey} for which the anchor index is
* determined.
* @return The anchor index for the given {@link AnchorKey} or
* <code>-1</code> in case the anchor key is not contained.
*/
protected int getAnchorIndex(AnchorKey anchorKey) {
int index = 0;
Iterator<AnchorKey> iterator = anchorsByKeys.keySet().iterator();
while (iterator.hasNext()) {
if (iterator.next().equals(anchorKey)) {
return index;
}
index++;
}
return -1;
}
/**
* Returns the {@link AnchorKey} for the given anchor index, i.e. the
* reverse of {@link #getAnchorIndex(AnchorKey)}.
*
* @param anchorIndex
* The anchor index for which to determine the {@link AnchorKey}.
* @return The {@link AnchorKey} for the given anchor index.
*/
protected AnchorKey getAnchorKey(int anchorIndex) {
return Iterators.get(anchorsByKeys.keySet().iterator(), anchorIndex);
}
/**
* Returns a {@link List} containing the {@link IAnchor}s which are assigned
* to this {@link Connection} in the order: start anchor, control point
* anchorsByKeys, end anchor.
*
* @return A {@link List} containing the {@link IAnchor}s which are assigned
* to this {@link Connection}.
*/
public ObservableList<IAnchor> getAnchorsUnmodifiable() {
return FXCollections.unmodifiableObservableList(anchors);
}
/**
* Computes the 'logical' center point of the {@link Connection}, which is
* the middle control point position (in case the curveProperty consists of
* an even number of segment) or the middle point of the middle segment.
*
* @return The logical center of this {@link Connection}.
*/
public Point getCenter() {
// TODO: we would better delegate this to interpolator, as there we can
// exchange the logic
BezierCurve[] bezierCurves = null;
if (getCurve() instanceof GeometryNode && ((GeometryNode<?>) getCurve())
.getGeometry() instanceof ICurve) {
bezierCurves = ((ICurve) ((GeometryNode<?>) getCurve())
.getGeometry()).toBezier();
} else {
bezierCurves = PolyBezier
.interpolateCubic(
getPointsUnmodifiable().toArray(new Point[] {}))
.toBezier();
}
if (bezierCurves.length % 2 == 0) {
return getPoint((int) (getPointsUnmodifiable().size() - 0.5) / 2);
} else {
return bezierCurves[bezierCurves.length / 2].get(0.5);
}
}
/**
* Returns the control {@link IAnchor anchor} for the given control anchor
* index which is currently assigned, or <code>null</code> if no control
* {@link IAnchor anchor} is assigned for that index.
*
* @param index
* The control anchor index determining which control
* {@link IAnchor anchor} to return.
* @return The control {@link IAnchor anchor} for the given index, or
* <code>null</code>.
*/
public IAnchor getControlAnchor(int index) {
return anchorsByKeys.get(getControlAnchorKey(index));
}
/**
* Returns the control anchor index for the given {@link AnchorKey}, i.e.
* <code>0</code> for the first control {@link IAnchor anchor},
* <code>1</code> for the seconds, etc.
*
* @param key
* The {@link AnchorKey} whose control anchor index is returned.
* @return The control anchor index for the given {@link AnchorKey}.
* @throws IllegalArgumentException
* when there currently is no control {@link IAnchor anchor}
* assigned to this {@link Connection} for the given
* {@link AnchorKey}.
*/
protected int getControlAnchorIndex(AnchorKey key) {
if (!key.getId().startsWith(CONTROL_POINT_ROLE_PREFIX)) {
throw new IllegalArgumentException(
"Given AnchorKey " + key + " is no control anchor key.");
}
int index = Integer.parseInt(
key.getId().substring(CONTROL_POINT_ROLE_PREFIX.length()));
return index;
}
/**
* Returns the {@link AnchorKey} for the given control anchor index.
*
* @param index
* The control anchor index for which the {@link AnchorKey} is
* returned.
* @return The {@link AnchorKey} for the given control anchor index.
*/
protected AnchorKey getControlAnchorKey(int index) {
return new AnchorKey(getCurve(), CONTROL_POINT_ROLE_PREFIX + index);
}
/**
* Returns a {@link List} containing the control {@link IAnchor
* anchorsByKeys} currently assigned to this {@link Connection}.
*
* @return A {@link List} containing the control {@link IAnchor
* anchorsByKeys} currently assigned to this {@link Connection}.
*/
public List<IAnchor> getControlAnchors() {
int controlAnchorsCount = anchorsByKeys.size();
if (anchorsByKeys.containsKey(getStartAnchorKey())) {
controlAnchorsCount--;
}
if (anchorsByKeys.containsKey(getEndAnchorKey())) {
controlAnchorsCount--;
}
List<IAnchor> controlAnchors = new ArrayList<>(controlAnchorsCount);
for (int i = 0; i < controlAnchorsCount; i++) {
IAnchor controlAnchor = getControlAnchor(i);
if (controlAnchor == null) {
throw new IllegalStateException(
"control anchor may never be null.");
}
controlAnchors.add(controlAnchor);
}
return controlAnchors;
}
/**
* Returns the control {@link Point} for the given control anchor index
* within the coordinate system of this {@link Connection} which is
* determined by querying the anchor position for the corresponding
* {@link #getControlAnchor(int) control anchor}, or <code>null</code> if no
* {@link #getControlAnchor(int) control anchor} is assigned for the given
* index.
*
* @param index
* The control anchor index for which to return the anchor
* position.
* @return The start {@link Point} of this {@link Connection}, or
* <code>null</code>.
*/
public Point getControlPoint(int index) {
int anchorIndex = getAnchorIndex(getControlAnchorKey(index));
return anchorIndex < 0 ? null : points.get(anchorIndex);
}
/**
* Returns a {@link List} containing the control {@link Point}s of this
* {@link Connection}.
*
* @return A {@link List} containing the control {@link Point}s of this
* {@link Connection}.
*/
public List<Point> getControlPoints() {
int controlPointCount = getControlAnchors().size();
List<Point> controlPoints = new ArrayList<>(controlPointCount);
for (int i = 0; i < controlPointCount; i++) {
controlPoints.add(getControlPoint(i));
}
return controlPoints;
}
/**
* Returns the {@link Node} which displays the curveProperty geometry. Will
* be a {@link GeometryNode} by default.
*
* @return The {@link Node} which displays the curveProperty geometry.
*/
public Node getCurve() {
return curveProperty.get();
}
/**
* Returns the currently assigned end {@link IAnchor anchor}, or
* <code>null</code> if no end {@link IAnchor anchor} is assigned.
*
* @return The currently assigned end {@link IAnchor anchor}, or
* <code>null</code>.
*/
public IAnchor getEndAnchor() {
return anchorsByKeys.get(getEndAnchorKey());
}
/**
* Returns the end {@link AnchorKey} for this {@link Connection}. An end
* {@link AnchorKey} uses the {@link #getCurve() curveProperty node} as its
* anchored and <code>"end"</code> as its role.
*
* @return The end {@link AnchorKey} for this {@link Connection}.
*/
// TODO: AnchorKeys should not be exposed -> make protected
protected AnchorKey getEndAnchorKey() {
return new AnchorKey(getCurve(), END_ROLE);
}
/**
* Returns the end decoration {@link Node} of this {@link Connection}, or
* <code>null</code>.
*
* @return The end decoration {@link Node} of this {@link Connection}, or
* <code>null</code>.
*/
public Node getEndDecoration() {
if (endDecorationProperty == null) {
return null;
}
return endDecorationProperty.get();
}
/**
* Returns the end {@link Point} of this {@link Connection} within its
* coordinate system which is determined by querying the anchor position for
* the {@link #getEndAnchorKey() end anchor key}, or <code>null</code> when
* no {@link #getEndAnchor() end anchor} is assigned.
*
* @return The end {@link Point} of this {@link Connection}, or
* <code>null</code>.
*/
public Point getEndPoint() {
int anchorIndex = getAnchorIndex(getEndAnchorKey());
return anchorIndex < 0 ? null : points.get(anchorIndex);
}
/**
* Returns the currently set end position hint or <code>null</code> if no
* hint is present.
*
* @return The currently set end position hint or <code>null</code> if no
* hint is present.
*/
public Point getEndPointHint() {
AnchorKey endAnchorKey = getEndAnchorKey();
if (hintsByKeys.containsKey(endAnchorKey)) {
return hintsByKeys.get(endAnchorKey);
}
return null;
}
/**
* Returns the {@link IConnectionInterpolator} of this {@link Connection}.
*
* @return The {@link IConnectionInterpolator} of this {@link Connection}.
*/
public IConnectionInterpolator getInterpolator() {
return interpolatorProperty.get();
}
/**
* Returns the point at the given index. The start point will be provided
* for <code>index == 0</code>, the end point for the last defined index.
* Control points will be returned for all indices in between.
*
* @param index
* The index of the point to retrieve.
* @return The point at the given index.
*
* @see #getPointsUnmodifiable()
*/
public Point getPoint(int index) {
return points.get(index);
}
/**
* Returns the {@link Point}s constituting this {@link Connection} within
* its coordinate system in the order: start point, control points, end
* point.
*
* @return The {@link Point}s constituting this {@link Connection}.
*/
public ObservableList<Point> getPointsUnmodifiable() {
return FXCollections.unmodifiableObservableList(points);
}
/**
* Returns the {@link IConnectionRouter} of this {@link Connection}.
*
* @return The {@link IConnectionRouter} of this {@link Connection}.
*/
public IConnectionRouter getRouter() {
return routerProperty.get();
}
/**
* Returns the currently assigned start {@link IAnchor anchor}, or
* <code>null</code> if no start {@link IAnchor anchor} is assigned.
*
* @return The currently assigned start {@link IAnchor anchor}, or
* <code>null</code>.
*/
public IAnchor getStartAnchor() {
return anchorsByKeys.get(getStartAnchorKey());
}
/**
* Returns the start {@link AnchorKey} for this {@link Connection}. A start
* {@link AnchorKey} uses the {@link #getCurve() curveProperty node} as its
* anchored and <code>"start"</code> as its role.
*
* @return The start {@link AnchorKey} for this {@link Connection}.
*/
// TODO: AnchorKeys should not be exposed -> make protected
protected AnchorKey getStartAnchorKey() {
return new AnchorKey(getCurve(), START_ROLE);
}
/**
* Returns the start decoration {@link Node} of this {@link Connection}, or
* <code>null</code>.
*
* @return The start decoration {@link Node } of this {@link Connection}, or
* <code>null</code>.
*/
public Node getStartDecoration() {
if (startDecorationProperty == null) {
return null;
}
return startDecorationProperty.get();
}
/**
* Returns the start {@link Point} of this {@link Connection} within its
* coordinate system which is determined by querying the anchor position for
* the {@link #getStartAnchorKey() start anchor key}, or <code>null</code>
* when no {@link #getStartAnchor() start anchor} is assigned.
*
* @return The start {@link Point} of this {@link Connection}, or
* <code>null</code>.
*/
public Point getStartPoint() {
int anchorIndex = getAnchorIndex(getStartAnchorKey());
return anchorIndex < 0 ? null : points.get(anchorIndex);
}
/**
* Returns the currently set start position hint or <code>null</code> if no
* hint is present.
*
* @return The currently set start position hint or <code>null</code> if no
* hint is present.
*/
public Point getStartPointHint() {
AnchorKey startAnchorKey = getStartAnchorKey();
if (hintsByKeys.containsKey(startAnchorKey)) {
return hintsByKeys.get(startAnchorKey);
}
return null;
}
/**
* Returns the {@link IConnectionInterpolator} property.
*
* @return The {@link IConnectionInterpolator} property.
*/
public ObjectProperty<IConnectionInterpolator> interpolatorProperty() {
return interpolatorProperty;
}
/**
* Return <code>true</code> in case the anchor is bound to an anchorage
* unequal to this connection.
*
* @param anchor
* The anchor to test
* @return <code>true</code> if the anchor is connected, <code>false</code>
* otherwise.
*/
public boolean isConnected(IAnchor anchor) {
return anchor != null && anchor.getAnchorage() != null
&& anchor.getAnchorage() != this;
}
/**
* Returns whether the (start, end, or control) anchor at the respective
* index is connected.
*
* @param index
* The index, referring to the start, end, or a control point.
* @return <code>true</code> if the anchor at the given index is connected,
* <code>false</code> otherwise.
*/
public boolean isConnected(int index) {
if (index < 0 || index >= getAnchorsUnmodifiable().size()) {
throw new IllegalArgumentException(
"The given index is out of bounds.");
}
return isConnected(getAnchor(index));
}
/**
* Returns <code>true</code> if the currently assigned
* {@link #getControlAnchor(int) control anchor} for the given index is
* bound to an anchorage. Otherwise returns <code>false</code>.
*
* @param index
* The control anchor index of the control anchor to test for
* connectedness.
* @return <code>true</code> if the currently assigned
* {@link #getControlAnchor(int) control anchor} for the given index
* is bound to an anchorage, otherwise <code>false</code>.
*/
public boolean isControlConnected(int index) {
return isConnected(getControlAnchor(index));
}
/**
* Returns <code>true</code> if the currently assigned
* {@link #getEndAnchor() end anchor} is bound to an anchorage. Otherwise
* returns <code>false</code>.
*
* @return <code>true</code> if the currently assigned
* {@link #getEndAnchor() end anchor} is bound to an anchorage,
* otherwise <code>false</code>.
*/
public boolean isEndConnected() {
return isConnected(getEndAnchor());
}
/**
* Returns <code>true</code> if the currently assigned
* {@link #getStartAnchor() start anchor} is bound to an anchorage.
* Otherwise returns <code>false</code>.
*
* @return <code>true</code> if the currently assigned
* {@link #getStartAnchor() start anchor} is bound to an anchorage,
* otherwise <code>false</code>.
*/
public boolean isStartConnected() {
return isConnected(getStartAnchor());
}
/**
* Returns an unmodifiable read-only list property, which contains the
* points (start, control, end) that constitute this connection.
*
* @return An unmodifiable read-only list property containing this
* {@link Connection}'s points.
*/
public ReadOnlyListProperty<Point> pointsUnmodifiableProperty() {
// property is created lazily to save memory
if (pointsUnmodifiableProperty == null) {
pointsUnmodifiableProperty = new PointsUnmodifiableProperty();
}
return pointsUnmodifiableProperty;
}
/**
* Re-attaches all {@link AnchorKey}s that are managed by this
* {@link Connection}.
*
* @param oldAnchored
* The previous anchored {@link Node}.
* @param newAnchored
* The new anchored {@link Node}.
*/
protected void reattachAnchorKeys(Node oldAnchored, Node newAnchored) {
if (oldAnchored == null) {
// In case the old value was null, we should not have any anchor
// keys to re-attach.
if (!anchorsByKeys.isEmpty()) {
throw new IllegalStateException(
"Re-attach failed: no previous curve, but anchor keys present.");
}
if (!hintsByKeys.isEmpty()) {
throw new IllegalStateException(
"Re-attach failed: no previous curve, but anchor keys present.");
}
return;
} else if (newAnchored == null) {
// In case the new value was null, we should not have any anchor
// keys to re-attach.
if (!anchorsByKeys.isEmpty()) {
throw new IllegalStateException(
"Re-attach failed: no new curve, but anchor keys present.");
}
if (!hintsByKeys.isEmpty()) {
throw new IllegalStateException(
"Re-attach failed: no new curve, but anchor keys present.");
}
return;
} else {
// Re-attach all anchor keys.
for (AnchorKey oldAk : new ArrayList<>(anchorsByKeys.keySet())) {
// query anchor for oldAk
IAnchor anchor = anchorsByKeys.get(oldAk);
// unregister old anchor key
unregisterPCL(oldAk, anchor);
anchorsByKeys.remove(oldAk);
anchor.detach(oldAk);
// create anchor key (new curve, same role)
AnchorKey newAk = new AnchorKey(newAnchored, oldAk.getId());
// update position hint
if (hintsByKeys.containsKey(oldAk)) {
hintsByKeys.put(newAk, hintsByKeys.remove(oldAk));
}
// XXX: anchors and points are staying the same, no need to
// update
// register new anchor key
anchorsByKeys.put(newAk, anchor);
anchor.attach(newAk);
registerPCL(newAk, anchor);
}
}
}
/**
* Refreshes the visualization in response to anchor, position,
* transformation, etc. changes. This method is safe against reentrance,
* i.e. changes performed by {@link #refresh()} are allowed to lead to
* another {@link #refresh()} call. However, when this method is called
* reentrant, it returns immediately.
* <p>
* The process of refreshing a {@link Connection} is somewhat complicated as
* it involves transforming points according to a transformation change,
* removing volatile anchors, computing new parameters for its anchors,
* inserting volatile anchors, computing a curve geometry, and updating the
* visualization to that geometry. In addition, the position change
* listeners registered at the individual
* {@link AbstractAnchor#positionsUnmodifiableProperty()} need to be
* disabled during {@link #refresh()} to prevent
* {@link ConcurrentModificationException}. The process can be described by
* the following steps:
* <ol>
* <li>The connection unregisters all position change listeners.
* <li>The connection queries all points from its anchors and transforms
* them from curve to connection coordinates (curve-to-connection-transform,
* c2ctx).
* <li>The router removes all (previously inserted) volatile anchors.
* <li>=> The connection's points are refreshed in-place, because removal
* of anchors calls {@link #removeAnchor(AnchorKey, IAnchor)}, which updates
* the points straight away.
* <li>The router queries the (updated, user-defined) points from the
* connection and computes parameters for the anchors based on these points
* (and other criteria).
* <li>=> The router computes the {@link AnchoredReferencePoint}
* parameter value within the coordinate system of the connection.
* <li>=> The parameter is then bound to a binding that depends on the
* c2ctx, which transforms the {@link Point} from the coordinate system of
* the connection to the coordinate system of the {@link #getCurve()}.
* <li>The anchors compute new positions for the {@link AnchorKey}s for
* which new parameters were provided or parameter values changed.
* <li>The router queries the positions from the anchors and transforms them
* manually, because the connection did not yet update its points.
* <li>The router inserts volatile anchors according to the routing
* strategy.
* <li>The connection refreshes its points manually, because the position
* change listeners are disabled.
* <li>The interpolator computes a new curve geometry and applies it to the
* connection.
* <li>=> The c2ctx changes, that's why the parameters are recomputed
* from the bindings, which triggers a recomputation of the anchor
* positions.
* <li>The connection refreshed its points manually again.
* <li>The connection registers all position change listeners.
* </ol>
*/
protected void refresh() {
// guard against refreshing while refreshing
if (inRefresh) {
return;
}
inRefresh = true;
// System.out.println("+--- Refresh ---+");
// unregister PCLs
// TODO: Investigate if the fields need to be up-to-date or if the
// removal and addition of listeners can happen locally (i.e. without
// affecting anchorPCLs etc.)
for (AnchorKey ak : anchorsByKeys.keySet()) {
unregisterPCL(ak, anchorsByKeys.get(ak));
}
// clear visuals except for the curveProperty
getChildren().retainAll(getCurve());
// z-order: place decorations above curve
Node startDecoration = getStartDecoration();
if (startDecoration != null) {
getChildren().add(startDecoration);
}
Node endDecoration = getEndDecoration();
if (endDecoration != null) {
getChildren().add(endDecoration);
}
// Transform tx = getCurve().getLocalToParentTransform();
// System.out.println("| +--- Initial ---+");
// System.out.println("| | curve-t: " + tx.getTx() + "," + tx.getTy());
// System.out.println("| | points: " + points);
// System.out.println("| | anchors: " + anchors);
// update our anchorsByKeys/points
IConnectionRouter router = getRouter();
if (router != null) {
// we might need to apply a new transform to each of the points
// TODO: Do this when the transform changes!
refreshPoints();
// compute parameters and insert volatile anchors
router.route(this);
// since PCLs are disabled (to prevent CME), points need to be
// refreshed again
// XXX: The Router performs the transformation internally after
// updating the parameters and before routing.
refreshPoints();
// tx = getCurve().getLocalToParentTransform();
// System.out.println("| +--- Routed ---+");
// System.out.println("| | curve-t: " + tx.getTx() + "," +
// tx.getTy());
// System.out.println("| | points: " + points);
// System.out.println("| | anchors: " + anchors);
} else {
throw new IllegalStateException(
"An IConnectionRouter is mandatory for a Connection.");
}
IConnectionInterpolator interpolator = getInterpolator();
if (interpolator != null) {
// apply new points to the visualization
interpolator.interpolate(this);
// XXX: Changing the visualization changes the
// curve-to-connection-transform, and since the PCLs are disabled,
// the points need to be refreshed again, in order to be up-to-date.
refreshPoints();
// tx = getCurve().getLocalToParentTransform();
// System.out.println("| +--- Interpolated ---+");
// System.out.println("| | curve-t: " + tx.getTx() + "," +
// tx.getTy());
// System.out.println("| | points: " + points);
// System.out.println("| | anchors: " + anchors);
// System.out.println();
} else {
throw new IllegalStateException(
"An IConnectionInterpolator is mandatory for a Connection.");
}
// notify properties (which are lazily created)
if (anchorsUnmodifiableProperty != null) {
anchorsUnmodifiableProperty.fireValueChangedEvent();
}
if (pointsUnmodifiableProperty != null) {
pointsUnmodifiableProperty.fireValueChangedEvent();
}
// reregister PCLs
// TODO: Investigate if the fields need to be up-to-date or if the
// removal and addition of listeners can happen locally (i.e. without
// affecting anchorPCLs etc.)
for (AnchorKey ak : anchorsByKeys.keySet()) {
registerPCL(ak, anchorsByKeys.get(ak));
}
// react to events again
inRefresh = false;
}
/**
* Refreshes the points of this {@link Connection} by querying the
* individual anchor positions and transforming them from curve coordinates
* to connection coordinates.
*
* @return <code>true</code> if any points were changed, <code>false</code>
* otherwise.
*/
private boolean refreshPoints() {
// walk over all anchors to compute new points,
// transforming them using the curve's local to parent
// transform
boolean changed = false;
for (int i = 0; i < points.size(); i++) {
Point position = getAnchor(i).getPosition(getAnchorKey(i));
// XXX: Here the same computation is used that
// is also used within #createPCL().
Point newPoint = FX2Geometry.toPoint(
getCurve().localToParent(Geometry2FX.toFXPoint(position)));
if (!points.get(i).equals(newPoint)) {
points.set(i, newPoint);
changed = true;
}
}
return changed;
}
private void registerPCL(AnchorKey anchorKey, IAnchor anchor) {
if (!anchorsPCL.containsKey(anchorKey)) {
MapChangeListener<? super AnchorKey, ? super Point> pcl = createPCL(
anchorKey);
anchorsPCL.put(anchorKey, pcl);
anchor.positionsUnmodifiableProperty().addListener(pcl);
}
}
/**
* Removes all control points of this {@link Connection}.
*/
public void removeAllControlAnchors() {
removeAllControlPoints();
}
/**
* Removes all control points of this {@link Connection}.
*/
public void removeAllControlPoints() {
int controlPointsCount = anchorsByKeys.size();
if (anchorsByKeys.containsKey(getStartAnchorKey())) {
controlPointsCount--;
}
if (anchorsByKeys.containsKey(getEndAnchorKey())) {
controlPointsCount--;
}
for (int i = controlPointsCount - 1; i >= 0; i--) {
removeControlPoint(i);
}
}
/**
* Removes the given {@link AnchorKey} (and corresponding {@link IAnchor})
* from this {@link Connection}.
*
* @param anchorKey
* The {@link AnchorKey} to remove.
* @param anchor
* The corresponding {@link IAnchor}.
*/
protected void removeAnchor(AnchorKey anchorKey, IAnchor anchor) {
if (anchorKey == null) {
throw new IllegalArgumentException("anchorKey may not be null.");
}
if (anchor == null) {
throw new IllegalArgumentException("anchor may not be null.");
}
AnchorKey startAnchorKey = getStartAnchorKey();
AnchorKey endAnchorKey = getEndAnchorKey();
unregisterPCL(anchorKey, anchor);
List<IAnchor> controlAnchorsToMove = new ArrayList<>();
if (!anchorKey.equals(startAnchorKey)
&& !anchorKey.equals(endAnchorKey)) {
int controlAnchorIndex = getControlAnchorIndex(anchorKey);
// remove all control points at a larger index
int pointCount = anchorsByKeys.size();
for (int i = pointCount - 1; i >= 0; i--) {
// (temporarily) remove all anchorsByKeys that are to be moved
// up
AnchorKey ak = getAnchorKey(i);
if (!ak.equals(startAnchorKey) && !ak.equals(endAnchorKey)) {
if (getControlAnchorIndex(ak) > controlAnchorIndex) {
IAnchor a = getAnchor(i);
unregisterPCL(ak, a);
controlAnchorsToMove.add(0, a);
int anchorIndex = getAnchorIndex(ak);
points.remove(anchorIndex);
anchors.remove(anchorIndex);
anchorsByKeys.remove(ak);
a.detach(ak);
}
}
}
}
points.remove(getAnchorIndex(anchorKey));
anchors.remove(getAnchorIndex(anchorKey));
anchorsByKeys.remove(anchorKey);
anchor.detach(anchorKey);
if (!anchorKey.equals(startAnchorKey)
&& !anchorKey.equals(endAnchorKey)) {
int controlIndex = getControlAnchorIndex(anchorKey);
// re-add all control points at a larger index
for (int i = 0; i < controlAnchorsToMove.size(); i++) {
AnchorKey ak = getControlAnchorKey(controlIndex + i);
IAnchor a = controlAnchorsToMove.get(i);
anchorsByKeys.put(ak, a);
a.attach(ak);
int anchorIndex = getAnchorIndex(ak);
anchors.add(anchorIndex, a);
points.add(anchorIndex,
FX2Geometry.toPoint(getCurve().localToParent(
Geometry2FX.toFXPoint(a.getPosition(ak)))));
registerPCL(ak, a);
}
}
refresh();
}
/**
* Removes the control anchor specified by the given index from this
* {@link Connection}.
*
* @param index
* The index specifying which control anchor to remove.
*/
public void removeControlAnchor(int index) {
removeControlPoint(index);
}
/**
* Removes the control point specified by the given control anchor index
* from this {@link Connection}.
*
* @param index
* The control anchor index specifying which control point to
* remove.
*/
public void removeControlPoint(int index) {
// check index out of range
if (index < 0 || index >= getControlPoints().size()) {
throw new IllegalArgumentException("Index out of range (index: "
+ index + ", size: " + getControlPoints().size() + ").");
}
AnchorKey anchorKey = getControlAnchorKey(index);
if (!anchorsByKeys.containsKey(anchorKey)) {
throw new IllegalStateException(
"Inconsistent state: control anchor key for index " + index
+ " not registered.");
}
IAnchor oldAnchor = anchorsByKeys.get(anchorKey);
if (oldAnchor == null) {
throw new IllegalStateException(
"Inconsistent state: control anchor for index " + index
+ " is null.");
}
removeAnchor(anchorKey, oldAnchor);
}
/**
* Returns a writable property containing the {@link IConnectionRouter} of
* this connection.
*
* @return A writable property providing the {@link IConnectionRouter} used
* by this connection.
*/
public ObjectProperty<IConnectionRouter> routerProperty() {
return routerProperty;
}
/**
* Replaces the anchor currently registered for the given {@link AnchorKey}
* with the given {@link IAnchor}.
*
* @param anchorKey
* The {@link AnchorKey} under which the {@link IAnchor} is to be
* registered.
* @param anchor
* The {@link IAnchor} which is inserted.
*/
protected void setAnchor(AnchorKey anchorKey, IAnchor anchor) {
if (anchorKey == null) {
throw new IllegalArgumentException("anchorKey may not be null.");
}
if (anchorKey.getAnchored() != getCurve()) {
throw new IllegalArgumentException(
"anchorKey may only be anchored to curveProperty node");
}
if (anchor == null) {
throw new IllegalArgumentException("anchor may not be null.");
}
IAnchor oldAnchor = anchorsByKeys.put(anchorKey, anchor);
unregisterPCL(anchorKey, oldAnchor);
// detach anchor key from old anchor
oldAnchor.detach(anchorKey);
// attach anchor key to new anchor
anchor.attach(anchorKey);
// update anchor
int anchorIndex = getAnchorIndex(anchorKey);
anchors.set(anchorIndex, anchor);
// update position (if changed)
Point newPosition = FX2Geometry.toPoint(getCurve().localToParent(
Geometry2FX.toFXPoint(anchor.getPosition(anchorKey))));
if (!newPosition.equals(points.get(anchorIndex))) {
points.set(anchorIndex, newPosition);
}
registerPCL(anchorKey, anchor);
refresh();
}
/**
* Replaces all anchors of this {@link Connection} with the given
* {@link IAnchor}s, i.e. the first given {@link IAnchor} replaces the
* currently assigned start anchor, the last given {@link IAnchor} replaces
* the currently assigned end anchor, and the intermediate {@link IAnchor}s
* replace the currently assigned control anchorsByKeys.
*
* @param anchors
* The new {@link IAnchor}s for this {@link Connection}.
* @throws IllegalArgumentException
* when less than 2 {@link IAnchor}s are given.
*/
public void setAnchors(List<IAnchor> anchors) {
if (anchors.size() < 2) {
throw new IllegalArgumentException(
"start end end anchorsByKeys have to be provided.");
}
// prevent refresh before all points are properly set
boolean oldInRefresh = inRefresh;
inRefresh = true;
setStartAnchor(anchors.get(0));
if (anchors.size() > 2) {
setControlAnchors(anchors.subList(1, anchors.size() - 1));
} else {
removeAllControlPoints();
}
setEndAnchor(anchors.get(anchors.size() - 1));
inRefresh = oldInRefresh;
refresh();
}
/**
* Sets the control anchor for the given control anchor index to the given
* {@link IAnchor}.
*
* @param index
* The control anchor index of the control anchor to replace.
* @param anchor
* The new control {@link IAnchor} for that index.
*/
public void setControlAnchor(int index, IAnchor anchor) {
if (index < 0 || index >= getControlAnchors().size()) {
throw new IllegalArgumentException("index out of range.");
}
if (anchor == null) {
throw new IllegalArgumentException("anchor may not be null.");
}
AnchorKey anchorKey = getControlAnchorKey(index);
IAnchor oldAnchor = anchorsByKeys.get(anchorKey);
if (oldAnchor != anchor) {
if (oldAnchor != null) {
setAnchor(anchorKey, anchor);
} else {
addAnchor(anchorKey, anchor);
}
}
}
/**
* Replaces all control anchorsByKeys of this {@link Connection} with the
* given {@link List} of {@link IAnchor}s.
*
* @param anchors
* The new control {@link IAnchor}s for this {@link Connection}.
*/
public void setControlAnchors(List<IAnchor> anchors) {
int controlSize = getControlAnchors().size();
boolean oldInRefresh = inRefresh;
inRefresh = true;
int i = 0;
for (; i < controlSize && i < anchors.size(); i++) {
setControlAnchor(i, anchors.get(i));
}
for (; i < anchors.size(); i++) {
addControlAnchor(i, anchors.get(i));
}
int initialRemovalIndex = i;
for (; i < controlSize; i++) {
removeControlAnchor(controlSize - 1 - (i - initialRemovalIndex));
}
inRefresh = oldInRefresh;
refresh();
}
/**
* Sets the control anchor for the given control anchor index to an
* {@link StaticAnchor} which yields the given {@link Point}.
*
* @param index
* The control anchor index of the control anchor to replace.
* @param controlPoint
* The new control {@link Point} for the respective index within
* local coordinates of the {@link Connection}.
*/
public void setControlPoint(int index, Point controlPoint) {
if (controlPoint == null) {
throw new IllegalArgumentException(
"control point may not be null.");
}
IAnchor anchor = new StaticAnchor(this, controlPoint);
setControlAnchor(index, anchor);
}
/**
* Replaces all control anchorsByKeys of this {@link Connection} with
* {@link StaticAnchor}s yielding the given {@link Point}s.
*
* @param controlPoints
* The new control {@link Point}s for this {@link Connection}.
*/
public void setControlPoints(List<Point> controlPoints) {
int controlSize = getControlAnchors().size();
boolean oldInRefresh = inRefresh;
inRefresh = true;
int i = 0;
for (; i < controlSize && i < controlPoints.size(); i++) {
setControlPoint(i, controlPoints.get(i));
}
for (; i < controlPoints.size(); i++) {
addControlPoint(i, controlPoints.get(i));
}
int initialRemovalIndex = i;
for (; i < controlSize; i++) {
removeControlPoint(controlSize - 1 - (i - initialRemovalIndex));
}
inRefresh = oldInRefresh;
refresh();
}
/**
* Sets the {@link Node} that is used to render the connection.
*
* @param curve
* The new curveProperty node.
*/
public void setCurve(Node curve) {
this.curveProperty.set(curve);
}
/**
* Sets the end {@link IAnchor} of this {@link Connection} to the given
* value.
*
* @param anchor
* The new end {@link IAnchor} for this {@link Connection}.
*/
public void setEndAnchor(IAnchor anchor) {
if (anchor == null) {
throw new IllegalArgumentException("anchor may not be null.");
}
AnchorKey anchorKey = getEndAnchorKey();
IAnchor oldAnchor = anchorsByKeys.get(anchorKey);
if (oldAnchor != anchor) {
if (oldAnchor != null) {
setAnchor(anchorKey, anchor);
} else {
addAnchor(anchorKey, anchor);
}
}
}
/**
* Sets the end decoration {@link Node} of this {@link Connection} to the
* given value.
*
* @param decoration
* The new end decoration {@link Node} for this
* {@link Connection}.
*/
public void setEndDecoration(Node decoration) {
endDecorationProperty().set(decoration);
}
/**
* Sets the {@link #setEndAnchor(IAnchor) end anchor} of this
* {@link Connection} to an {@link StaticAnchor} yielding the given
* {@link Point}.
*
* @param endPoint
* The new end {@link Point} within local coordinates of the
* {@link Connection}.
*/
public void setEndPoint(Point endPoint) {
if (endPoint == null) {
throw new IllegalArgumentException("endPoint may not be null.");
}
IAnchor anchor = new StaticAnchor(this, endPoint);
setEndAnchor(anchor);
}
/**
* Sets the end position hint to the given value.
*
* @param endPositionHint
* The new end position hint.
*/
public void setEndPointHint(Point endPositionHint) {
AnchorKey endAnchorKey = getEndAnchorKey();
if (endPositionHint == null) {
if (hintsByKeys.containsKey(endAnchorKey)) {
hintsByKeys.remove(endAnchorKey);
}
} else {
hintsByKeys.put(endAnchorKey, endPositionHint);
}
refresh();
}
/**
* Sets the {@link IConnectionInterpolator} of this {@link Connection} to
* the given {@link IConnectionInterpolator}.
*
* @param interpolator
* The new {@link IConnectionInterpolator} for this
* {@link Connection}.
*/
public void setInterpolator(IConnectionInterpolator interpolator) {
interpolatorProperty.set(interpolator);
}
/**
* Replaces all anchors of this {@link Connection} with the given
* {@link IAnchor}s, i.e. the first given {@link IAnchor} replaces the
* currently assigned start anchor, the last given {@link IAnchor} replaces
* the currently assigned end anchor, and the intermediate {@link IAnchor}s
* replace the currently assigned control anchorsByKeys.
*
* @param points
* The new {@link Point}s for this {@link Connection}.
* @throws IllegalArgumentException
* when less than 2 {@link IAnchor}s are given.
*/
public void setPoints(List<Point> points) {
if (points.size() < 2) {
throw new IllegalArgumentException(
"At least two points have to be provided.");
}
// prevent refresh before all points are properly set
boolean oldInRefresh = inRefresh;
inRefresh = true;
setStartPoint(points.get(0));
if (points.size() > 2) {
setControlPoints(points.subList(1, points.size() - 1));
} else {
removeAllControlPoints();
}
setEndPoint(points.get(points.size() - 1));
inRefresh = oldInRefresh;
refresh();
}
/**
* Sets the {@link IConnectionRouter} of this {@link Connection} to the
* given value.
*
* @param router
* The new {@link IConnectionRouter} for this {@link Connection}.
*/
public void setRouter(IConnectionRouter router) {
routerProperty.set(router);
}
/**
* Sets the start {@link IAnchor} of this {@link Connection} to the given
* value.
*
* @param anchor
* The new start {@link IAnchor} for this {@link Connection}.
*/
public void setStartAnchor(IAnchor anchor) {
if (anchor == null) {
throw new IllegalArgumentException("anchor may not be null.");
}
AnchorKey anchorKey = getStartAnchorKey();
IAnchor oldAnchor = anchorsByKeys.get(anchorKey);
if (oldAnchor != anchor) {
if (oldAnchor != null) {
setAnchor(anchorKey, anchor);
} else {
addAnchor(anchorKey, anchor);
}
}
}
/**
* Sets the start decoration {@link Node} of this {@link Connection} to the
* given value.
*
* @param decoration
* The new start decoration {@link Node} for this
* {@link Connection}.
*/
public void setStartDecoration(Node decoration) {
startDecorationProperty().set(decoration);
}
/**
* Sets the {@link #setStartAnchor(IAnchor) start anchor} of this
* {@link Connection} to an {@link StaticAnchor} yielding the given
* {@link Point}.
*
* @param startPoint
* The new start {@link Point} within local coordinates of the
* {@link Connection}.
*/
public void setStartPoint(Point startPoint) {
if (startPoint == null) {
throw new IllegalArgumentException("startPoint may not be null.");
}
IAnchor anchor = new StaticAnchor(this, startPoint);
setStartAnchor(anchor);
}
/**
* Sets the start position hint to the given value.
*
* @param startPositionHint
* The new start position hint.
*/
public void setStartPointHint(Point startPositionHint) {
AnchorKey startAnchorKey = getStartAnchorKey();
if (startPositionHint == null) {
if (hintsByKeys.containsKey(startAnchorKey)) {
hintsByKeys.remove(startAnchorKey);
}
} else {
hintsByKeys.put(startAnchorKey, startPositionHint);
}
refresh();
}
/**
* Returns an {@link ObjectProperty} wrapping the start decoration
* {@link Node}.
*
* @return An Object Property wrapping the start decoration.
*/
public ObjectProperty<Node> startDecorationProperty() {
if (startDecorationProperty == null) {
startDecorationProperty = new SimpleObjectProperty<>();
startDecorationProperty.addListener(decorationListener);
}
return startDecorationProperty;
}
private void unregisterPCL(AnchorKey anchorKey, IAnchor anchor) {
if (anchorsPCL.containsKey(anchorKey)) {
anchor.positionsUnmodifiableProperty()
.removeListener(anchorsPCL.remove(anchorKey));
}
}
}