/*******************************************************************************
* Copyright (c) 2016 itemis AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
* Matthias Wienand (itemis AG) - contributions for Bugzilla #504480
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.parts;
import java.util.ArrayList;
import java.util.List;
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.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.planar.AffineTransform;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.mvc.fx.providers.IAnchorProvider;
import org.eclipse.gef.mvc.fx.viewer.IViewer;
import javafx.scene.Node;
import javafx.scene.transform.Affine;
import javafx.scene.transform.NonInvertibleTransformException;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
/**
* An {@link IContentPart} that supports content related bend, i.e. manipulation
* of control points.
*
* @author anyssen
* @author mwienand
*
* @param <V>
* The visual node used by this {@link IBendableContentPart}.
*
*/
public interface IBendableContentPart<V extends Node>
extends ITransformableContentPart<V>, IResizableContentPart<V> {
/**
* A representation of a bend point, which is defined either by a point or
* by a content anchorage to which the content is attached.
*/
public static class BendPoint {
/**
* Computes the size from the given list of
* {@link IBendableContentPart.BendPoint}s as the size of the bounds
* around the unattached bend points.
*
* @param bendPoints
* The list of {@link IBendableContentPart.BendPoint}s for
* which to compute the size.
* @return The size of the bounds around the unattached bend points.
*/
static Dimension computeSize(List<BendPoint> bendPoints) {
// determine min and max point to compute the bounds
Point min = null;
Point max = null;
for (BendPoint bp : bendPoints) {
if (!bp.isAttached()) {
Point pos = bp.getPosition();
if (min == null) {
// XXX: The first unattached bend-point determines the
// initial values for min and max. Copies are used so
// that
// we can safely change min and max.
min = pos.getCopy();
max = min.getCopy();
} else {
// expand min and max
if (min.x > pos.x) {
min.x = pos.x;
}
if (min.y > pos.y) {
min.y = pos.y;
}
if (max.x < pos.x) {
max.x = pos.x;
}
if (max.y < pos.y) {
max.y = pos.y;
}
}
}
}
// XXX: min == null if there are no unattached bend points
return min == null ? new Dimension()
: new Dimension(max.x - min.x, max.y - min.y);
}
/**
* Computes the translation from the given list of
* {@link IBendableContentPart.BendPoint}s as the offset of the bounds
* around the unattached bend points.
*
* @param bendPoints
* The list of {@link IBendableContentPart.BendPoint}s for
* which to compute the translation.
* @return The translation of the bounds around the unattached bend
* points.
*/
static Affine computeTranslation(List<BendPoint> bendPoints) {
// iterate over the unattached bend-points to find the minimum
Point min = null;
for (BendPoint bp : bendPoints) {
if (!bp.isAttached()) {
Point pos = bp.getPosition();
if (min == null) {
// initialize min
// XXX: copy so it can safely be changed
min = pos.getCopy();
} else {
// adjust min to the given position
if (min.x > pos.x) {
min.x = pos.x;
}
if (min.y > pos.y) {
min.y = pos.y;
}
}
}
}
// XXX: in case there are no unattached bend-points, an identity
// transformation is returned
return min == null ? new Affine()
: new Affine(new Translate(min.x, min.y));
}
/**
* Resizes the given list of {@link IBendableContentPart.BendPoint}s
* according to the bounds-change that is given by the current offset,
* current size, and final size. The unattached
* {@link IBendableContentPart.BendPoint}s will remain their relative
* positions within their bounds.
*
* @param bendPoints
* The list of {@link IBendableContentPart.BendPoint}s to
* modify.
* @param currentX
* The current x offset.
* @param currentY
* The current y offset.
* @param currentSize
* The current size.
* @param finalSize
* The final size.
* @return The resized {@link IBendableContentPart.BendPoint}s.
*/
static List<BendPoint> resize(List<BendPoint> bendPoints,
double currentX, double currentY, Dimension currentSize,
Dimension finalSize) {
// System.out.println(
// "Resize from " + currentSize + " to " + finalSize + ".");
// determine unattached bend points
List<Point> points = new ArrayList<>();
for (BendPoint bp : bendPoints) {
if (!bp.isAttached()) {
points.add(bp.getPosition());
}
}
// a) optimize for no unattached bend-points
// b) optimize for a single bend-point (size = 0, 0)
if (points.size() < 2) {
return bendPoints;
}
// determine delta size
double dw = finalSize.width - currentSize.width;
double dh = finalSize.height - currentSize.height;
// compute relative positions
double[] relX = new double[points.size()];
double[] relY = new double[relX.length];
for (int i = 0; i < relX.length; i++) {
Point p = points.get(i);
relX[i] = (p.x - currentX) / currentSize.width;
relY[i] = (p.y - currentY) / currentSize.height;
}
// resize bend points based on their relative positions
// XXX: separate index for relX and relY because they only contain
// unattached points
int pointIndex = 0;
for (BendPoint bp : bendPoints) {
if (!bp.isAttached()) {
bp.getPosition().x += relX[pointIndex] * dw;
bp.getPosition().y += relY[pointIndex] * dh;
// XXX: increase point index only after an unattached
// bend-point
// was processed
pointIndex++;
}
}
return bendPoints;
}
/**
* Transforms the given {@link List} of
* {@link IBendableContentPart.BendPoint}s according to the change
* specified by the given current and final {@link Affine}
* transformations.
*
* @param bendPoints
* The {@link List} of
* {@link IBendableContentPart.BendPoint}s to transform.
* @param currentTransform
* The current {@link Affine} transformation.
* @param totalTransform
* The final {@link Affine} transformation.
* @return The given, transformed {@link List} of
* {@link IBendableContentPart.BendPoint}s.
*/
static List<BendPoint> transform(List<BendPoint> bendPoints,
Affine currentTransform, Affine totalTransform) {
// compute delta transform
Affine inverse;
try {
inverse = currentTransform.createInverse();
} catch (NonInvertibleTransformException e) {
throw new RuntimeException(e);
}
Transform deltaTransform = new Affine(
inverse.createConcatenation(totalTransform));
// optimize for identity transform
if (deltaTransform.isIdentity()) {
return bendPoints;
}
// System.out.println("Transform by " + deltaTransform.getTx() + ",
// "
// + deltaTransform.getTy() + ".");
AffineTransform tx = FX2Geometry.toAffineTransform(deltaTransform);
// transform unattached bend points in-place
for (BendPoint bp : bendPoints) {
if (!bp.isAttached()) {
bp.getPosition().transform(tx);
}
}
return bendPoints;
}
private Object contentAnchorage;
private Point position;
/**
* Creates a new attached bend point.
*
* @param contentAnchorage
* The content anchorage, to which the point is attached.
* @param position
* A position (hint) for the attached bend point.
*/
public BendPoint(Object contentAnchorage, Point position) {
if (contentAnchorage == null) {
throw new IllegalArgumentException(
"contentAnchorage may not be null.");
}
if (position == null) {
throw new IllegalArgumentException("position may not be null");
}
this.contentAnchorage = contentAnchorage;
this.position = position.getCopy();
}
/**
* Creates a new unattached bend point.
*
* @param position
* The position of the bend point.
*/
public BendPoint(Point position) {
if (position == null) {
throw new IllegalArgumentException("position may not be null.");
}
this.position = position.getCopy();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
BendPoint other = (BendPoint) obj;
if (contentAnchorage == null) {
if (other.contentAnchorage != null) {
return false;
}
} else if (!contentAnchorage.equals(other.contentAnchorage)) {
return false;
}
if (position == null) {
if (other.position != null) {
return false;
}
} else if (!position.equals(other.position)) {
return false;
}
return true;
}
/**
* The content element to which the bend point is attached.
*
* @return The content element to which the bend point is attached.
*/
public Object getContentAnchorage() {
return contentAnchorage;
}
/**
* The position of the unattached bend point or the (optional) position
* hint for an attached bend point.
*
* @return A point representing the position if the bend point is not
* attached, or a position hint for an attached bend point.
*/
public Point getPosition() {
return position;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((contentAnchorage == null) ? 0
: contentAnchorage.hashCode());
result = prime * result
+ ((position == null) ? 0 : position.hashCode());
return result;
}
/**
* Whether this bend point is defined through an attachment of a content
* anchorage.
*
* @return <code>true</code> if the bend point is defined through an
* attachment, <code>false</code> if the bend point is defined
* through a position.
*/
public boolean isAttached() {
return contentAnchorage != null;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "BendPoint [" + (contentAnchorage != null
? "contentAnchorage=" + contentAnchorage + ", " : "")
+ (position != null ? "position=" + position : "") + "]";
}
}
/**
* Returns the visual to bend.
*
* @return The visual to bend.
*/
public default Connection getBendableVisual() {
return (Connection) getVisual();
}
/**
* Returns the current {@link BendPoint}s of this
* {@link IBendableContentPart}'s content.
*
* @return The {@link BendPoint}s of this {@link IBendableContentPart}'s
* content.
*/
public List<BendPoint> getContentBendPoints();
@Override
public default Dimension getContentSize() {
return BendPoint.computeSize(getContentBendPoints());
}
@Override
public default Affine getContentTransform() {
return BendPoint.computeTranslation(getContentBendPoints());
}
/**
* Returns the current {@link BendPoint}s of this
* {@link IBendableContentPart}'s visual.
*
* @return The {@link BendPoint}s of this {@link IBendableContentPart}'s
* visual.
*/
public default List<org.eclipse.gef.mvc.fx.parts.IBendableContentPart.BendPoint> getVisualBendPoints() {
List<BendPoint> bendPoints = new ArrayList<>();
Connection connection = getBendableVisual();
IViewer viewer = getRoot().getViewer();
List<IAnchor> anchors = connection.getAnchorsUnmodifiable();
for (int i = 0; i < anchors.size(); i++) {
IAnchor anchor = anchors.get(i);
if (!connection.getRouter().wasInserted(anchor)) {
if (connection.isConnected(i)) {
// provide a position hint for a connected bend point
Point positionHint = connection.getPoint(i);
if (i == 0 && connection.getStartPointHint() != null) {
positionHint = connection.getStartPointHint();
}
if (i == anchors.size() - 1
&& connection.getEndPointHint() != null) {
positionHint = connection.getEndPointHint();
}
// determine anchorage content
Node anchorageNode = anchor.getAnchorage();
IVisualPart<? extends Node> part = PartUtils
.retrieveVisualPart(viewer, anchorageNode);
Object anchorageContent = null;
if (part instanceof IContentPart) {
anchorageContent = ((IContentPart<? extends Node>) part)
.getContent();
}
bendPoints
.add(new BendPoint(anchorageContent, positionHint));
} else {
bendPoints.add(new BendPoint(connection.getPoint(i)));
}
}
}
return bendPoints;
}
@Override
default Dimension getVisualSize() {
return BendPoint.computeSize(getVisualBendPoints());
}
@Override
default Affine getVisualTransform() {
return BendPoint.computeTranslation(getVisualBendPoints());
}
/**
* Bends the content element as specified through the given bend points.
*
* @param bendPoints
* The bend points.
*/
public void setContentBendPoints(List<BendPoint> bendPoints);
@Override
default void setContentSize(Dimension totalSize) {
// determine visual offset
Affine visualTransform = getContentTransform();
double currentX = visualTransform.getTx();
double currentY = visualTransform.getTy();
// resize content bend points
List<BendPoint> resizedBendPoints = BendPoint.resize(
getContentBendPoints(), currentX, currentY, getContentSize(),
totalSize);
setContentBendPoints(resizedBendPoints);
}
@Override
default void setContentTransform(Affine totalTransform) {
setContentBendPoints(BendPoint.transform(getContentBendPoints(),
getContentTransform(), totalTransform));
}
/**
* Bends the visual as specified by the given bend points.
*
* @param bendPoints
* The bend points.
*/
public default void setVisualBendPoints(List<BendPoint> bendPoints) {
if (bendPoints == null || bendPoints.size() < 2) {
throw new IllegalArgumentException(
"Not enough bend points supplied!");
}
// compute anchors for the given bend points
List<IAnchor> newAnchors = new ArrayList<>();
for (int i = 0; i < bendPoints.size(); i++) {
BendPoint bp = bendPoints.get(i);
if (bp.isAttached()) {
// create anchor
IAnchorProvider anchorProvider = getRoot().getViewer()
.getContentPartMap().get(bp.getContentAnchorage())
.getAdapter(IAnchorProvider.class);
if (anchorProvider == null) {
throw new IllegalStateException(
"Anchorage does not provide anchor!");
}
IAnchor anchor = anchorProvider.get(this);
if (anchor == null) {
throw new IllegalStateException(
"AnchorProvider does not provide anchor!");
}
newAnchors.add(anchor);
// update hints
if (i == 0) {
// update start point hint
getBendableVisual()
.setStartPointHint(bendPoints.get(0).getPosition());
}
if (i == bendPoints.size() - 1) {
// update end point hint
getBendableVisual().setEndPointHint(bendPoints
.get(bendPoints.size() - 1).getPosition());
}
} else {
newAnchors.add(new StaticAnchor(getBendableVisual(),
bp.getPosition()));
}
}
// update anchors
getBendableVisual().setAnchors(newAnchors);
}
@Override
default void setVisualSize(Dimension totalSize) {
// determine visual offset
Affine visualTransform = getVisualTransform();
double currentX = visualTransform.getTx();
double currentY = visualTransform.getTy();
// resize visual bend points
List<BendPoint> resizedBendPoints = BendPoint.resize(
getVisualBendPoints(), currentX, currentY, getVisualSize(),
totalSize);
setVisualBendPoints(resizedBendPoints);
}
@Override
default void setVisualTransform(Affine totalTransform) {
setVisualBendPoints(BendPoint.transform(getVisualBendPoints(),
getVisualTransform(), totalTransform));
}
}