/*******************************************************************************
* 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
*
*******************************************************************************/
package org.eclipse.gef.mvc.examples.logo.parts;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.gef.common.collections.SetMultimapChangeListener;
import org.eclipse.gef.fx.anchors.DynamicAnchor;
import org.eclipse.gef.fx.anchors.IAnchor;
import org.eclipse.gef.fx.anchors.OrthogonalProjectionStrategy;
import org.eclipse.gef.fx.nodes.Connection;
import org.eclipse.gef.fx.nodes.GeometryNode;
import org.eclipse.gef.fx.nodes.OrthogonalRouter;
import org.eclipse.gef.fx.nodes.PolyBezierInterpolator;
import org.eclipse.gef.fx.nodes.PolylineInterpolator;
import org.eclipse.gef.fx.nodes.StraightRouter;
import org.eclipse.gef.geometry.planar.AffineTransform;
import org.eclipse.gef.geometry.planar.IGeometry;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.mvc.examples.logo.model.AbstractGeometricElement;
import org.eclipse.gef.mvc.examples.logo.model.GeometricCurve;
import org.eclipse.gef.mvc.examples.logo.model.GeometricCurve.Decoration;
import org.eclipse.gef.mvc.examples.logo.model.GeometricCurve.RoutingStyle;
import org.eclipse.gef.mvc.fx.parts.IBendableContentPart;
import org.eclipse.gef.mvc.fx.parts.IVisualPart;
import org.eclipse.gef.mvc.fx.providers.IAnchorProvider;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeLineCap;
public class GeometricCurvePart extends AbstractGeometricElementPart<Connection>
implements IBendableContentPart<Connection> {
public static class ArrowHead extends Polygon {
public ArrowHead() {
super(0, 0, 10, 3, 10, -3);
setFill(Color.TRANSPARENT);
}
}
public static class CircleHead extends Circle {
public CircleHead() {
super(5);
setFill(Color.TRANSPARENT);
}
}
private static final String END_ROLE = "END";
private static final String START_ROLE = "START";
private final CircleHead START_CIRCLE_HEAD = new CircleHead();
private final CircleHead END_CIRCLE_HEAD = new CircleHead();
private final ArrowHead START_ARROW_HEAD = new ArrowHead();
private final ArrowHead END_ARROW_HEAD = new ArrowHead();
private GeometricCurve previousContent;
// refresh visual upon model property changes
private final ListChangeListener<Point> wayPointsChangeListener = new ListChangeListener<Point>() {
@Override
public void onChanged(javafx.collections.ListChangeListener.Change<? extends Point> c) {
refreshVisual();
}
};
private final ListChangeListener<Double> dashesChangeListener = new ListChangeListener<Double>() {
@Override
public void onChanged(javafx.collections.ListChangeListener.Change<? extends Double> c) {
refreshVisual();
}
};
private final ChangeListener<RoutingStyle> routingStyleChangeListener = new ChangeListener<RoutingStyle>() {
@Override
public void changed(ObservableValue<? extends RoutingStyle> observable, RoutingStyle oldValue,
RoutingStyle newValue) {
refreshVisual();
}
};
private final ChangeListener<Decoration> decorationChangeListener = new ChangeListener<Decoration>() {
@Override
public void changed(ObservableValue<? extends Decoration> observable, Decoration oldValue,
Decoration newValue) {
refreshVisual();
}
};
public GeometricCurvePart() {
anchoragesUnmodifiableProperty()
.addListener(new SetMultimapChangeListener<IVisualPart<? extends Node>, String>() {
@Override
public void onChanged(Change<? extends IVisualPart<? extends Node>, ? extends String> change) {
// XXX: As the selection handles show the connected
// state, their visuals need to be refreshed.
// TODO: Rather register an anchorages-observer within
// CircleSegmentHandlePart when attaching to an
// anchorage.
for (IVisualPart<? extends Node> anchored : getAnchoredsUnmodifiable()) {
anchored.refreshVisual();
}
}
});
}
@Override
protected void doAttachToAnchorageVisual(IVisualPart<? extends Node> anchorage, String role) {
IAnchor anchor = anchorage.getAdapter(IAnchorProvider.class).get(this, role);
if (role.equals(START_ROLE)) {
getVisual().setStartAnchor(anchor);
getContent().setWayPoint(0, getVisual().getStartPoint());
} else if (role.equals(END_ROLE)) {
getVisual().setEndAnchor(anchor);
getContent().setWayPoint(getContent().getWayPoints().size() - 1, getVisual().getEndPoint());
} else {
throw new IllegalStateException("Cannot attach to anchor with role <" + role + ">.");
}
}
@Override
protected void doAttachToContentAnchorage(Object contentAnchorage, String role) {
if (!(contentAnchorage instanceof AbstractGeometricElement)) {
throw new IllegalArgumentException("Inappropriate content anchorage: wrong type.");
}
AbstractGeometricElement<?> geom = (AbstractGeometricElement<?>) contentAnchorage;
if (START_ROLE.equals(role)) {
getContent().getSourceAnchorages().add(geom);
} else if (END_ROLE.equals(role)) {
getContent().getTargetAnchorages().add(geom);
}
}
@Override
protected Connection doCreateVisual() {
Connection visual = new Connection();
visual.setInterpolator(new PolyBezierInterpolator());
((GeometryNode<?>) visual.getCurve()).setStrokeLineCap(StrokeLineCap.BUTT);
return visual;
}
@Override
protected void doDetachFromAnchorageVisual(IVisualPart<? extends Node> anchorage, String role) {
if (role.equals(START_ROLE)) {
getVisual().setStartPoint(getVisual().getStartPoint());
getContent().setWayPoint(0, getVisual().getStartPoint());
} else if (role.equals(END_ROLE)) {
getVisual().setEndPoint(getVisual().getEndPoint());
getContent().setWayPoint(getContent().getWayPoints().size() - 1, getVisual().getEndPoint());
} else {
throw new IllegalStateException("Cannot detach from anchor with role <" + role + ">.");
}
}
@Override
protected void doDetachFromContentAnchorage(Object contentAnchorage, String role) {
if (START_ROLE.equals(role)) {
getContent().getSourceAnchorages().remove(contentAnchorage);
} else if (END_ROLE.equals(role)) {
getContent().getTargetAnchorages().remove(contentAnchorage);
}
}
@Override
protected SetMultimap<Object, String> doGetContentAnchorages() {
SetMultimap<Object, String> anchorages = HashMultimap.create();
Set<AbstractGeometricElement<? extends IGeometry>> sourceAnchorages = getContent().getSourceAnchorages();
for (Object src : sourceAnchorages) {
anchorages.put(src, START_ROLE);
}
Set<AbstractGeometricElement<? extends IGeometry>> targetAnchorages = getContent().getTargetAnchorages();
for (Object dst : targetAnchorages) {
anchorages.put(dst, END_ROLE);
}
return anchorages;
}
@Override
protected List<? extends Object> doGetContentChildren() {
return Collections.emptyList();
}
@Override
protected void doRefreshVisual(Connection visual) {
GeometricCurve content = getContent();
// TODO: extract router code and replace start/end/control point
// handling by calling
// setVisualBendPoints(getContentBendPoints());
List<Point> wayPoints = content.getWayPointsCopy();
// TODO: why is this needed??
AffineTransform transform = content.getTransform();
if (previousContent == null || (transform != null && !transform.equals(previousContent.getTransform())
|| transform == null && previousContent.getTransform() != null)) {
if (transform != null) {
Point[] transformedWayPoints = transform.getTransformed(wayPoints.toArray(new Point[] {}));
wayPoints = Arrays.asList(transformedWayPoints);
}
}
if (!getContentAnchoragesUnmodifiable().containsValue(START_ROLE)) {
visual.setStartPoint(wayPoints.remove(0));
} else {
visual.setStartPointHint(wayPoints.remove(0));
}
if (!getContentAnchoragesUnmodifiable().containsValue(END_ROLE)) {
visual.setEndPoint(wayPoints.remove(wayPoints.size() - 1));
} else {
visual.setEndPointHint(wayPoints.remove(wayPoints.size() - 1));
}
if (!visual.getControlPoints().equals(wayPoints)) {
visual.setControlPoints(wayPoints);
}
// decorations
switch (content.getSourceDecoration()) {
case NONE:
if (visual.getStartDecoration() != null) {
visual.setStartDecoration(null);
}
break;
case CIRCLE:
if (visual.getStartDecoration() == null || !(visual.getStartDecoration() instanceof CircleHead)) {
visual.setStartDecoration(START_CIRCLE_HEAD);
}
break;
case ARROW:
if (visual.getStartDecoration() == null || !(visual.getStartDecoration() instanceof ArrowHead)) {
visual.setStartDecoration(START_ARROW_HEAD);
}
break;
}
switch (content.getTargetDecoration()) {
case NONE:
if (visual.getEndDecoration() != null) {
visual.setEndDecoration(null);
}
break;
case CIRCLE:
if (visual.getEndDecoration() == null || !(visual.getEndDecoration() instanceof CircleHead)) {
visual.setEndDecoration(END_CIRCLE_HEAD);
}
break;
case ARROW:
if (visual.getEndDecoration() == null || !(visual.getEndDecoration() instanceof ArrowHead)) {
visual.setEndDecoration(END_ARROW_HEAD);
}
break;
}
Shape startDecorationVisual = (Shape) visual.getStartDecoration();
Shape endDecorationVisual = (Shape) visual.getEndDecoration();
// stroke paint
if (((GeometryNode<?>) visual.getCurve()).getStroke() != content.getStroke()) {
((GeometryNode<?>) visual.getCurve()).setStroke(content.getStroke());
}
if (startDecorationVisual != null && startDecorationVisual.getStroke() != content.getStroke()) {
startDecorationVisual.setStroke(content.getStroke());
}
if (endDecorationVisual != null && endDecorationVisual.getStroke() != content.getStroke()) {
endDecorationVisual.setStroke(content.getStroke());
}
// stroke width
if (((GeometryNode<?>) visual.getCurve()).getStrokeWidth() != content.getStrokeWidth()) {
((GeometryNode<?>) visual.getCurve()).setStrokeWidth(content.getStrokeWidth());
}
if (startDecorationVisual != null && startDecorationVisual.getStrokeWidth() != content.getStrokeWidth()) {
startDecorationVisual.setStrokeWidth(content.getStrokeWidth());
}
if (endDecorationVisual != null && endDecorationVisual.getStrokeWidth() != content.getStrokeWidth()) {
endDecorationVisual.setStrokeWidth(content.getStrokeWidth());
}
// dashes
List<Double> dashList = new ArrayList<>(content.getDashes().length);
for (double d : content.getDashes()) {
dashList.add(d);
}
if (!((GeometryNode<?>) visual.getCurve()).getStrokeDashArray().equals(dashList)) {
((GeometryNode<?>) visual.getCurve()).getStrokeDashArray().setAll(dashList);
}
// connection router
if (content.getRoutingStyle().equals(RoutingStyle.ORTHOGONAL)) {
// re-attach visual in case we are connected to an anchor with
// non orthogonal computation strategy
if (getVisual().getStartAnchor() != null && getVisual().getStartAnchor() instanceof DynamicAnchor
&& !(((DynamicAnchor) getVisual().getStartAnchor())
.getComputationStrategy() instanceof OrthogonalProjectionStrategy)) {
IVisualPart<? extends Node> anchorage = getViewer().getVisualPartMap()
.get(getVisual().getStartAnchor().getAnchorage());
doDetachFromAnchorageVisual(anchorage, START_ROLE);
if (anchorage != this) {
// connected to anchorage
doAttachToAnchorageVisual(anchorage, START_ROLE);
}
}
if (getVisual().getEndAnchor() != null && getVisual().getEndAnchor() instanceof DynamicAnchor
&& !(((DynamicAnchor) getVisual().getEndAnchor())
.getComputationStrategy() instanceof OrthogonalProjectionStrategy)) {
IVisualPart<? extends Node> anchorage = getViewer().getVisualPartMap()
.get(getVisual().getEndAnchor().getAnchorage());
doDetachFromAnchorageVisual(anchorage, END_ROLE);
if (anchorage != this) {
// connected to anchorage
doAttachToAnchorageVisual(anchorage, END_ROLE);
}
}
if (!(visual.getInterpolator() instanceof PolylineInterpolator)) {
visual.setInterpolator(new PolylineInterpolator());
}
if (!(visual.getRouter() instanceof OrthogonalRouter)) {
visual.setRouter(new OrthogonalRouter());
}
} else {
// re-attach visual in case we are connected to an anchor with
// orthogonal computation strategy
if (getVisual().getStartAnchor() != null && getVisual().getStartAnchor() instanceof DynamicAnchor
&& ((DynamicAnchor) getVisual().getStartAnchor())
.getComputationStrategy() instanceof OrthogonalProjectionStrategy) {
IVisualPart<? extends Node> anchorage = getViewer().getVisualPartMap()
.get(getVisual().getStartAnchor().getAnchorage());
doDetachFromAnchorageVisual(anchorage, START_ROLE);
doAttachToAnchorageVisual(anchorage, START_ROLE);
}
if (getVisual().getEndAnchor() != null && getVisual().getEndAnchor() instanceof DynamicAnchor
&& ((DynamicAnchor) getVisual().getEndAnchor())
.getComputationStrategy() instanceof OrthogonalProjectionStrategy) {
IVisualPart<? extends Node> anchorage = getViewer().getVisualPartMap()
.get(getVisual().getEndAnchor().getAnchorage());
doDetachFromAnchorageVisual(anchorage, END_ROLE);
doAttachToAnchorageVisual(anchorage, END_ROLE);
}
if (!(visual.getInterpolator() instanceof PolyBezierInterpolator)) {
visual.setInterpolator(new PolyBezierInterpolator());
}
if (!(visual.getRouter() instanceof StraightRouter)) {
visual.setRouter(new StraightRouter());
}
}
previousContent = content;
// apply effect
super.doRefreshVisual(visual);
}
@Override
public GeometricCurve getContent() {
return (GeometricCurve) super.getContent();
}
@Override
public List<BendPoint> getContentBendPoints() {
List<BendPoint> bendPoints = new ArrayList<>();
// use content way points for the positions
List<Point> wayPoints = getContent().getWayPointsCopy();
// if we have a source/target anchorage, create an attached bend point
// for it
Set<AbstractGeometricElement<? extends IGeometry>> sourceAnchorages = getContent().getSourceAnchorages();
int startIndex = 0;
if (sourceAnchorages != null && !sourceAnchorages.isEmpty()) {
bendPoints.add(new BendPoint(sourceAnchorages.iterator().next(), wayPoints.get(startIndex++)));
}
Set<AbstractGeometricElement<? extends IGeometry>> targetAnchorages = getContent().getTargetAnchorages();
int lastIndex = wayPoints.size() - 1;
if (targetAnchorages != null && !targetAnchorages.isEmpty()) {
bendPoints.add(new BendPoint(targetAnchorages.iterator().next(), wayPoints.get(lastIndex--)));
}
// add unattached bend-points for the rest of the way points
for (int i = startIndex; i <= lastIndex; i++) {
bendPoints.add(i, new BendPoint(wayPoints.get(i)));
}
return bendPoints;
}
@Override
public void setContent(Object model) {
if (model != null && !(model instanceof GeometricCurve)) {
throw new IllegalArgumentException("Only ICurve models are supported.");
}
if (getContent() != null) {
// remove property change listeners from model
getContent().wayPointsProperty().removeListener(wayPointsChangeListener);
getContent().dashesProperty().removeListener(dashesChangeListener);
getContent().routingStyleProperty().removeListener(routingStyleChangeListener);
getContent().sourceDecorationProperty().removeListener(decorationChangeListener);
getContent().targetDecorationProperty().removeListener(decorationChangeListener);
}
super.setContent(model);
if (getContent() != null) {
// add property change listeners to model
getContent().wayPointsProperty().addListener(wayPointsChangeListener);
getContent().dashesProperty().addListener(dashesChangeListener);
getContent().routingStyleProperty().addListener(routingStyleChangeListener);
getContent().sourceDecorationProperty().addListener(decorationChangeListener);
getContent().targetDecorationProperty().addListener(decorationChangeListener);
}
}
@SuppressWarnings("unchecked")
@Override
public void setContentBendPoints(List<BendPoint> bendPoints) {
getContent().getSourceAnchorages().clear();
getContent().getTargetAnchorages().clear();
List<Point> waypoints = new ArrayList<>();
for (int i = 0; i < bendPoints.size(); i++) {
BendPoint bp = bendPoints.get(i);
if (bp.isAttached()) {
if (i == 0) {
// update start anchorage
// TODO: introduce setter so this is more concise
getContent().addSourceAnchorage(
(AbstractGeometricElement<? extends IGeometry>) bp.getContentAnchorage());
// update start hint
waypoints.add(bp.getPosition());
}
if (i == bendPoints.size() - 1) {
// update end anchorage
// TODO: introduce setter so this is more concise
getContent().addTargetAnchorage(
(AbstractGeometricElement<? extends IGeometry>) bp.getContentAnchorage());
// update end point hint
waypoints.add(bp.getPosition());
}
} else {
waypoints.add(bp.getPosition());
}
}
refreshContentAnchorages();
getContent().setWayPoints(waypoints.toArray(new Point[] {}));
}
}