blob: 80e6c25f23db43db0431ac6c7f2ad150ea5f43d0 [file] [log] [blame]
/*******************************************************************************
* Copyright 2005, CHISEL Group, University of Victoria, Victoria, BC, Canada.
* 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:
* The Chisel Group, University of Victoria
*******************************************************************************/
package org.eclipse.mylar.zest.core.internal.gefx;
import org.eclipse.draw2d.AbstractRouter;
import org.eclipse.draw2d.AnchorListener;
import org.eclipse.draw2d.ArrowLocator;
import org.eclipse.draw2d.Connection;
import org.eclipse.draw2d.ConnectionAnchor;
import org.eclipse.draw2d.ConnectionLocator;
import org.eclipse.draw2d.ConnectionRouter;
import org.eclipse.draw2d.DelegatingLayout;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.RotatableDecoration;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
/**
*
* @author Del Myers
*
* A connection between two anchor points. This connection is drawn as an arc,
* defined as the circular arc with the chord (ax, ay) - (bx, by) (where a and b
* are the anchors) and a depth d defined as the maximum distance from any point on
* the chord (i.e. a vector normal to the chord with magnitude d).
*
* @tag bug(114452)
*/
public class ArcConnection extends Arc implements Connection, AnchorListener {
/**
* Routes Connections directly from the source anchor to the target anchor with no
* bendpoints in between.
*/
static class NullConnectionRouter
extends AbstractRouter
{
/**
* Constructs a new NullConnectionRouter.
*/
NullConnectionRouter() { }
/**
* Routes the given Connection directly between the source and target anchors.
* @param conn the connection to be routed
*/
public void route(Connection conn) {
PointList points = conn.getPoints();
points.removeAllPoints();
Point p;
p = getStartPoint(conn);
conn.translateToRelative(p = getStartPoint(conn));
points.addPoint(p);
p = getEndPoint(conn);
conn.translateToRelative(p = getEndPoint(conn));
points.addPoint(p);
conn.setPoints(points);
}
}
/** The depth of the arc */
private int depth;
//just for reference purposes: a point on the radius of the arc, halfway
//between the endpoints.
private double px;
private double py;
//decorations for the start and end of the arc.
private RotatableDecoration endArrow;
private RotatableDecoration startArrow;
//list of points in the arc. This will include just the end points.
private PointList pointList;
//method for routing connections.
private ConnectionRouter connectionRouter;
//source and destination anchors.
private ConnectionAnchor source;
private ConnectionAnchor dest;
//default connection router.
private static ConnectionRouter NullConnectionRouter = new NullConnectionRouter();
public ArcConnection() {
super();
this.pointList = new PointList();
connectionRouter = NullConnectionRouter;
this.depth = 0;
pointList.addPoint(new Point(0,0));
pointList.addPoint(new Point(100,100));
setLayoutManager(new DelegatingLayout());
}
public ConnectionRouter getConnectionRouter() {
return connectionRouter;
}
/**
* Hooks the source and target anchors.
* @see Figure#addNotify()
*/
public void addNotify() {
super.addNotify();
hookSourceAnchor();
hookTargetAnchor();
}
/**
* Returns the list of points in this arc. Note: the points can't be changed using
* setPoints, nor by changing the PointList. The PointList is fixed within this connection.
*/
public final PointList getPoints() {
return pointList.getCopy();
}
/**
* Returns the bounds which holds all the points in this. Returns any
* previously existing bounds, else calculates by unioning all the children's
* dimensions.
* @return the bounds
*/
//@tag bug(154176-ConnectionClip(fix)) : add children to the bounds.
public Rectangle getBounds() {
if (bounds == null) {
super.getBounds();
for (int i = 0; i < getChildren().size(); i++) {
IFigure child = (IFigure)getChildren().get(i);
bounds.union(child.getBounds());
}
}
return bounds;
}
public Object getRoutingConstraint() {
if (this.connectionRouter != null)
return connectionRouter.getConstraint(this);
return null;
}
public ConnectionAnchor getSourceAnchor() {
return source;
}
public ConnectionAnchor getTargetAnchor() {
return dest;
}
public void setConnectionRouter(ConnectionRouter router) {
if (this.connectionRouter == router) return;
ConnectionRouter oldRouter = connectionRouter;
if (router == null) router = NullConnectionRouter;
this.connectionRouter = router;
firePropertyChange(Connection.PROPERTY_CONNECTION_ROUTER, oldRouter, this.connectionRouter);
}
/**
* Updates the arc for when point or depth changes occur.
*/
protected void updateArc(Point src, Point dest, int depth) {
double workingDepth = depth;
this.depth = depth;
//get cartesian coordinates: they are easier to work with.
//cartesian coordinates are easier to use for translations.
double x1 = src.x;
double y1 = -src.y;
double x2 = dest.x;
double y2 = -dest.y;
// the center of the chord
double cartChordX = (x2 + x1)/2;
double cartChordY = (y2 + y1)/2;
//special cases
if (depth == 0) {
doStraightLine(src, dest);
return;
} else if (src.equals(dest)) {
doCircle(src);
return;
}
if (x1 >= x2) {
workingDepth = -workingDepth;
}
double chordLength = Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
if (Math.abs(workingDepth) >= chordLength/2) {
workingDepth = (chordLength/3)*(workingDepth/Math.abs(workingDepth));
}
double r = (((chordLength/2)*(chordLength/2)) + ((double)workingDepth*workingDepth))/((double)2*workingDepth);
//assume that the chord is perpendicular to the origin
//to find the angle of the chord.
double cartCenterX = 0;
double cartCenterY = 0;
//Find a vector normal to the chord. This will be used for translating the
//circle back to screen coordinates.
double chordNormal = 0;
if (Math.abs(x1 - x2) <= .001) {
//slope of 0. NaN is easier to detect than 0.
chordNormal = Double.NaN;
} else if (Math.abs(y1-y2) <= 0.001) {
//infinite slope.
chordNormal = Double.POSITIVE_INFINITY;
} else {
chordNormal = -(double)(y2 - y1)/(x2 - x1);
}
double th1;
if (Double.isNaN(chordNormal)) {
cartCenterX = (y1 > y2) ? (cartChordX-r+(workingDepth)) : (cartChordX+r-(workingDepth));
cartCenterY = cartChordY;
this.px = cartChordX + ((y1 > y2) ? (workingDepth) : (-workingDepth));
this.py = (-cartChordY);
th1 = Math.PI/2;
} else if (Double.isInfinite(chordNormal)) {
cartCenterX = cartChordX;
cartCenterY = cartChordY+r-(workingDepth);
this.px = cartChordX;
this.py = (-cartChordY)+workingDepth;
th1 = 0.0;
} else {
//assume that the center of the chord is on the origin.
th1 = Math.atan(chordNormal);
cartCenterX = (r-(double)(workingDepth))*Math.sin(th1)+cartChordX;//cartChordX+r -depth;
cartCenterY = (r-(double)(workingDepth))*Math.cos(th1)+cartChordY;//cartChordY+r-depth;
this.px = (-workingDepth)*Math.sin(th1)+cartChordX;
this.py = -((-workingDepth)*Math.cos(th1)+cartChordY);
}
//update the points and depth
pointList.removeAllPoints();
pointList.addPoint(src);
pointList.addPoint(new Point(Math.round(px), Math.round(py)));
pointList.addPoint(dest);
//figure out the new angles
//translate the points to the center of the circle
double cartArcX1 = x1 - cartCenterX;
double cartArcY1 = y1 - cartCenterY;
double cartArcX2 = x2 - cartCenterX;
double cartArcY2 = y2 - cartCenterY;
double arcStart = angle360(cartArcX1, cartArcY1);
double arcEnd = angle360(cartArcX2, cartArcY2);
if (arcEnd < arcStart) {
arcEnd = arcEnd + 360;
}
double arcLength = arcEnd-arcStart;//findAngleDegrees(arcStart, arcEnd);
int padding = (chordLength > 100) ? 4 : 0;
if (depth < 0) padding = -padding;
setOffset((int)(Math.round(arcStart - padding)));
setLength((int)(Math.round(arcLength+padding*(chordLength/100))));
//update the bounds
double br = Math.abs(r);
double topx = cartCenterX - br;
double topy = cartCenterY + br;
double width = br*2;
Rectangle b = new Rectangle((int)x1, -(int)y1, (int)(px-x1), (int)(py+y1));
if (b.width < 0) {
b.width = -b.width;
b.x = b.x - b.width;
}
if (b.height < 0){
b.height = -b.height;
b.y = b.y-b.height;
}
Rectangle b2 = new Rectangle((int)x2, (int)-y2, (int)(px-x2), (int)(py+y2));
if (b2.width < 0) {
b2.width = -b2.width;
b2.x = b2.x - b2.width;
}
if (b2.height < 0){
b2.height = -b2.height;
b2.y = b2.y-b2.height;
}
int b3x = Math.min(b2.x, b.x);
int b3y = Math.min(b.y, b2.y);
arcEnd = arcEnd % 360;
int boundsTop = b3y;
int boundsLeft = b3x;
int boundsBottom = boundsTop + (b.height+b2.height);
int boundsRight = boundsLeft + (b.width+b2.width);
if (arcEnd >= 90 && (arcEnd-arcLength) <= 90) {
boundsTop = -(int)topy;
}
if (arcEnd >= 180 && (arcEnd - arcLength) <= 180) {
boundsLeft = (int)topx;
}
if ((arcEnd >= 270 && (arcEnd -arcLength) <= 270) || (arcEnd-arcLength+360 <= 270)) {
boundsBottom = (int)(width-topy);
}
if (arcEnd >=0 && (arcEnd-arcLength) <= 0){
boundsRight = (int)(topx + width);
}
Rectangle b3 = new Rectangle(boundsLeft-5, boundsTop-5, boundsRight-boundsLeft+10, boundsBottom-boundsTop+10);
setBounds(b3);
setArcBounds(new Rectangle((int)topx, -(int)topy, (int)width, (int)width));
//this.bounds = null;
}
//source and destination are the same. create a circle that passes through the point.
private void doCircle(Point src) {
setOffset(0);
setLength(360);
Rectangle circleBounds = new Rectangle(src.x - depth/2, src.y - depth, depth, depth);
setArcBounds(circleBounds);
setBounds(circleBounds);
pointList.removeAllPoints();
pointList.addPoint(src.getCopy());
pointList.addPoint(new Point(src.x, src.y-depth));
pointList.addPoint(src.getCopy());
}
private void doStraightLine(Point src, Point dest) {
int x1 = src.x;
int y1 = src.y;
int x2 = dest.x;
int y2 = dest.y;
//just a straight line.
if (x1 > x2) {
int t = x2;
x2 = x1;
x1 = t;
}
if (y1 > y2) {
int t = y2;
y2 = y1;
y1 = t;
}
Rectangle bounds = new Rectangle((int)x1-2, (int)y1-2, (int)(x2-x1)+4, (int)(y2-y1)+4);
setBounds(bounds);
setArcBounds(bounds);
pointList.removeAllPoints();
pointList.addPoint(src.getCopy());
pointList.addPoint(new Point((x1+x2)/2, (y1+y2)/2));
pointList.addPoint(dest.getCopy());
}
/**
* This implementation only pays attention to the first and the last points in the
* list because Arcs don't allow for bendpoints. The new point list will consist of
* the two end-points and a point that describes the depth of the arc.
*/
public final void setPoints(PointList list) {
Point src = list.getFirstPoint();
Point dest = list.getLastPoint();
//create the new arc.
updateArc(src, dest, getDepth());
firePropertyChange(Connection.PROPERTY_POINTS, null, pointList);
repaint();
}
public int getDepth() {
return depth;
}
/**
* Sets the depth of the arc. This is defined as the maximum distance from the
* line that connects the source and target of this connection. The sign of the
* depth (+ve or -ve) will define which side of the line the arc will appear on.
* @param depth the depth of the arc.
*/
public void setDepth(int depth) {
updateArc(getPoints().getFirstPoint(), getPoints().getLastPoint(), depth);
repaint();
}
/**
* Takes the polar coordinates x1, y1 and returns their angle between 0 and 360 degrees.
* @param x1 polar coordinate.
* @param y1 polar coordinate
* @return polar angle in 360 degrees.
*/
protected double angle360(double x1, double y1) {
double theta = Math.toDegrees(Math.atan(y1/x1));
switch (findQuadrant(x1,y1)) {
case 1: return theta;
case 2:
case 3: return theta+180;
case 4: return theta+360;
default: return theta;
}
}
/**
*
* finds the shortest angle between the two given angles. Assumes that the angles
* are between 0-360 degrees.
*
*/
protected double findAngleDegrees(double a1, double a2) {
double diff = a2-a1;
double diff2 = diff-360;
if (diff < 0) {
diff2 = 360+diff;
}
if (Math.abs(diff) < Math.abs(diff2)) {
return diff;
}
return diff2;
}
//find the quadrant, assume points are centered at 0,0
protected int findQuadrant (double x, double y) {
if (y > 0) {
if (x > 0) {
return 1;
} else {
return 2;
}
} else {
if (x > 0) {
return 4;
} else {
return 3;
}
}
}
public void setRoutingConstraint(Object cons) {
if (this.connectionRouter != null) connectionRouter.setConstraint(this, cons);
}
public void setSourceAnchor(ConnectionAnchor anchor) {
if (anchor == this.source) return;
unhookSourceAnchor();
this.source = anchor;
if (getParent()!=null) hookSourceAnchor();
revalidate();
}
public void setTargetAnchor(ConnectionAnchor anchor) {
if (anchor == this.dest) return;
unhookTargetAnchor();
this.dest = anchor;
if (getParent()!=null) hookTargetAnchor();
revalidate();
}
private void unhookSourceAnchor() {
if (getSourceAnchor() != null)
getSourceAnchor().removeAnchorListener(this);
}
private void unhookTargetAnchor() {
if (getTargetAnchor() != null)
getTargetAnchor().removeAnchorListener(this);
}
public void revalidate() {
super.revalidate();
if (connectionRouter != null)
connectionRouter.invalidate(this);
}
protected final void fillShape(Graphics graphics) {
}
protected void outlineShape(Graphics graphics) {
Color c = graphics.getForegroundColor();
graphics.setForegroundColor(c.getDevice().getSystemColor(SWT.COLOR_GRAY));
//graphics.drawOval(getBounds());
graphics.setForegroundColor(c);
Point source =getPoints().getFirstPoint();
Point target = getPoints().getLastPoint();
Rectangle bounds = getBounds();
if (depth == 0) {
graphics.drawLine(source, target);
return;
}
graphics.setClip(new Rectangle(bounds.x-1, bounds.y-1, bounds.width+2, bounds.height+2));
super.outlineShape(graphics);
}
/*public boolean containsPoint(int x, int y) {
if (depth == 0) {
//calculate for a straight line.
Point source = getPoints().getFirstPoint();
Point target = getPoints().getLastPoint();
double m = ((double)target.y - (double)source.y)/((double)target.x-(double)source.x);
double dy = m*(source.x-x) + y;
return (Math.abs(dy) <= .01);
}
if (!super.containsPoint(x, y))
return false;
Rectangle r = getArcBounds();
double ellw = r.width;
double normx = (x - r.x) / ellw - 0.5;
double ellh = r.height;
double normy = (y - r.y) / ellh - 0.5;
double distSq = (normx * normx + normy * normy);
return (Math.abs(1-distSq) <= .001);
}*/
public boolean containsPoint(int x, int y) {
return false;
}
private void hookSourceAnchor() {
if (getSourceAnchor() != null)
getSourceAnchor().addAnchorListener(this);
}
private void hookTargetAnchor() {
if (getTargetAnchor() != null)
getTargetAnchor().addAnchorListener(this);
}
public void anchorMoved(ConnectionAnchor anchor) {
revalidate();
}
/**
* Layouts this polyline. If the start and end anchors are present, the connection router
* is used to route this, after which it is laid out. It also fires a moved method.
*/
public void layout() {
if (getSourceAnchor() != null && getTargetAnchor() != null)
connectionRouter.route(this);
Rectangle oldBounds = bounds;
super.layout();
if (!getBounds().contains(oldBounds)) {
getParent().translateToParent(oldBounds);
getUpdateManager().addDirtyRegion(getParent(), oldBounds);
}
repaint();
fireFigureMoved();
}
/**
* Sets the decoration to be used at the end of the {@link Connection}.
* @param dec the new target decoration
*/
public void setTargetDecoration(RotatableDecoration dec) {
if (endArrow == dec)
return;
if (endArrow != null)
remove(endArrow);
endArrow = dec;
if (endArrow != null) {
if (depth != 0)
add(endArrow, new ArcLocator(this, ConnectionLocator.TARGET));
else
add(endArrow, new ArrowLocator(this,ConnectionLocator.TARGET));
}
}
/**
* Sets the decoration to be used at the start of the {@link Connection}.
* @param dec the new source decoration
* @since 2.0
*/
public void setSourceDecoration(RotatableDecoration dec) {
if (startArrow == dec)
return;
if (startArrow != null)
remove(startArrow);
startArrow = dec;
if (startArrow != null) {
if (depth != 0)
add(startArrow, new ArcLocator(this, ConnectionLocator.SOURCE));
else
add(startArrow, new ArrowLocator(this,ConnectionLocator.SOURCE));
}
}
}