edu.cmu.tetradapp.workbench.DisplayEdge Maven / Gradle / Ivy
///////////////////////////////////////////////////////////////////////////////
// For information as to what this class does, see the Javadoc, below. //
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, //
// 2007, 2008, 2009, 2010, 2014, 2015, 2022 by Peter Spirtes, Richard //
// Scheines, Joseph Ramsey, and Clark Glymour. //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation; either version 2 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program; if not, write to the Free Software //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //
///////////////////////////////////////////////////////////////////////////////
package edu.cmu.tetradapp.workbench;
import edu.cmu.tetrad.graph.Edge;
import edu.cmu.tetrad.graph.Endpoint;
import org.apache.commons.math3.util.FastMath;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
/**
* This component has three modes: - UNANCHORED
- NORMAL
- SELECTED
In the unanchored mode, it
* displays an edge in the the workbench, one end of which is anchored to a workbench node and the other end of which
* tracks a mouse point. The edge in this mode is useful for constructing new edges in the workbench. In the normal
* and selected modes, both ends are anchored to workbench nodes, and the edge will track these workbench nodes if they
* are moved on the workbench. The difference between the normal and selected modes is that they display the edge in
* different colors and when queried they respond differently as to whether the edge is selected. The intended use
* for this workbench edge is as follows. When an edge on the screen is first being created, an instance of this
* workbench edge is created anchored on one end to a workbench node. As the mouse is dragged, updates to its position
* are fed to the updateTrackPoint() method. When the mouse is released, the tracking edge is removed from the workbench
* and replaced with a new workbench edge which is anchored to two nodes--(1) the original node from the tracking edge
* and (2) the node which is nearest to the mouse release position.
*
* @author josephramsey
* @author Willie Wheeler
* @version $Id: $Id
*/
public class DisplayEdge extends JComponent implements IDisplayEdge {
/**
* Represents the fact that this is a directed edge, A-->B.
*/
public static final int DIRECTED = 0;
/**
* Represents the fact that this is an nondirected edge, Ao-oB
*/
public static final int NONDIRECTED = 1;
/**
* Represents the fact that this is an undirected edge, A---B
*/
public static final int UNDIRECTED = 2;
/**
* Represents the fact that this is a partially directed edge, Ao->B.
*/
public static final int PARTIALLY_ORIENTED = 3;
/**
* Represents the fact that this is a bidirected edge, A<->B.
*/
public static final int BIDIRECTED = 4;
/**
* Represents a session edge
*/
public static final int SESSION = 5;
/**
* Indicates that one end of the edge is anchored to one component, but the other half is tracking the mouse point.
*/
protected static final int HALF_ANCHORED = 0;
/**
* Indicates that both ends of the edge are anchored to components. The edge will move when those components move.
*/
protected static final int ANCHORED_UNSELECTED = 1;
/**
* Indicates that the edge is under construction, which is similar to the normal mode except that it's displayed as
* selected and will identify itself as selected upon request (for possible deletion, e.g.).
*/
protected static final int ANCHORED_SELECTED = 2;
/**
* The node that this edge is linked "from."
*/
private final DisplayNode node1;
/**
* Handler for ComponentEvent
s.
*/
private final ComponentHandler compHandler = new ComponentHandler();
/**
* Handler for PropertyChange
s.
*/
private final PropertyChangeHandler propertyChangeHandler =
new PropertyChangeHandler();
/**
* The model edge that this display is is portraying.
*/
private Edge modelEdge;
/**
* The getModel mode of the edge--HALF_ANCHORED, ANCHORED_UNSELECTED, or ANCHORED_SELECTED.
*/
private int mode;
/**
* The type of the edge, one of DIRECTED, NONDIRECTED, UNDIRECTED, PARTIALLY_ORIENTED, BIDIRECTED.
*/
private int type;
/**
* The node that this edge is linked "to."
*/
private DisplayNode node2;
/**
* For HALF_ANCHORed edges, this is the mouse point they connect to.
*/
private Point mouseTrackPoint = new Point();
/**
* This is the same mouse point, but relative to *this* edge component.
*/
private Point relativeMouseTrackPoint = new Point();
/**
* If the user clicks in this region, the edge will select.
*/
private Polygon clickRegion;
/**
* True iff only adacencies (and no endpoints) should be shown.
*/
private boolean showAdjacenciesOnly;
/**
* The offset of this edge for multiple edges between node pairs.
*/
private double offset;
/**
* The pair of points that this edge connects, from the edge of one component to the edge of the other.
*/
private PointPair connectedPoints;
/**
* The color of the line.
*/
private Color lineColor = new Color(26, 113, 169, 255);// DisplayNodeUtils.getNodeFillColor().darker().darker();
/**
* The color of the line when selected.
*/
private Color selectedColor = new Color(244, 0, 20);//DisplayNodeUtils.getNodeSelectedFillColor(); // this one
/**
* The color of the line when highlighted.
*/
private Color highlightedColor = new Color(238, 180, 34);
/**
* The width of the stroke.
*/
private float strokeWidth = 1.2f;
/**
* True iff this edge is highlighted.
*/
private boolean highlighted;
/**
* True iff this edge is solid.
*/
private boolean solid = true;
/**
* True iff this edge is thick.
*/
private boolean thick = false;
//==========================CONSTRUCTORS============================//
/**
* Constructs a new DisplayEdge connecting two components, 'node1' and 'node2', assuming that a reference to the
* model edge will not be needed.
*
* @param node1 the 'from' component.
* @param node2 the 'to' component.
* @param type the type of the edge, either UNRANDOMIZED or RANDOMIZED.
*/
protected DisplayEdge(DisplayNode node1, DisplayNode node2, int type) {
this(node1, node2, type, null);
}
/**
*
Constructor for DisplayEdge.
*
* @param node1 a {@link edu.cmu.tetradapp.workbench.DisplayNode} object
* @param node2 a {@link edu.cmu.tetradapp.workbench.DisplayNode} object
* @param type a int
* @param color a {@link java.awt.Color} object
*/
protected DisplayEdge(DisplayNode node1, DisplayNode node2, int type, Color color) {
if (node1 == null) {
throw new NullPointerException("Node1 must not be null.");
}
if (node2 == null) {
throw new NullPointerException("Node2 must not be null.");
}
if (type < 0 || type > 5) {
throw new IllegalArgumentException("Type must be one of DIRECTED, NONDIRECTED, UNDIRECTED, PARTIALLY_ORIENTED, or BIDIRECTED.");
}
this.node1 = node1;
this.node2 = node2;
this.type = type;
if (color != null) {
this.lineColor = color;
}
this.mode = DisplayEdge.ANCHORED_UNSELECTED;
node1.addComponentListener(this.compHandler);
node2.addComponentListener(this.compHandler);
node1.addPropertyChangeListener(this.propertyChangeHandler);
node2.addPropertyChangeListener(this.propertyChangeHandler);
resetBounds();
}
/**
* Constructs a new DisplayEdge connecting two components, 'node1' and 'node2', assuming that a reference to the
* model edge will be needed.
*
* @param node1 the 'from' component.
* @param node2 the 'to' component.
* @param modelEdge a {@link edu.cmu.tetrad.graph.Edge} object
*/
public DisplayEdge(Edge modelEdge, DisplayNode node1, DisplayNode node2) {
this(modelEdge, node1, node2, null);
}
/**
* Constructor for DisplayEdge.
*
* @param modelEdge a {@link edu.cmu.tetrad.graph.Edge} object
* @param node1 a {@link edu.cmu.tetradapp.workbench.DisplayNode} object
* @param node2 a {@link edu.cmu.tetradapp.workbench.DisplayNode} object
* @param color a {@link java.awt.Color} object
*/
public DisplayEdge(Edge modelEdge, DisplayNode node1, DisplayNode node2, Color color) {
if (modelEdge == null) {
throw new NullPointerException("Model edge must not be null.");
}
if (node1 == null) {
throw new NullPointerException("Node1 must not be null.");
}
if (node2 == null) {
throw new NullPointerException("Node2 must not be null.");
}
this.modelEdge = modelEdge;
this.node1 = node1;
this.node2 = node2;
if (color != null) {
this.lineColor = color;
}
this.mode = DisplayEdge.ANCHORED_UNSELECTED;
node1.addComponentListener(this.compHandler);
node2.addComponentListener(this.compHandler);
node1.addPropertyChangeListener(this.propertyChangeHandler);
node2.addPropertyChangeListener(this.propertyChangeHandler);
resetBounds();
}
/**
* Constructs a new unanchored session edge. The end of the edge at 'node1' isO anchored, but the other end tracks
* a mouse point. The mouse point should be updated by the parent component using repeated calls to
* 'updateTrackPoint'; this process is finished by finally anchoring the second end of the of edge using
* 'anchorSecondEnd'. Once this is done, the edge is considered anchored and will not be able to track a mouse
* point any longer.
*
* @param node1 the 'from' component.
* @param mouseTrackPoint the initial value of the mouse track point.
* @param type a int
* @see #updateTrackPoint
*/
public DisplayEdge(DisplayNode node1, Point mouseTrackPoint, int type) {
this(node1, mouseTrackPoint, type, null);
}
/**
* Constructor for DisplayEdge.
*
* @param node1 a {@link edu.cmu.tetradapp.workbench.DisplayNode} object
* @param mouseTrackPoint a {@link java.awt.Point} object
* @param type a int
* @param color a {@link java.awt.Color} object
*/
public DisplayEdge(DisplayNode node1, Point mouseTrackPoint, int type, Color color) {
if (node1 == null) {
throw new NullPointerException("Node1 must not be null.");
}
if (mouseTrackPoint == null) {
throw new NullPointerException(
"Mouse track point must not " + "be null.");
}
if (type < 0 || type > 5) {
throw new IllegalArgumentException("Type must be one of DIRECTED, NONDIRECTED, UNDIRECTED, PARTIALLY_ORIENTED, or BIDIRECTED.");
}
this.node1 = node1;
this.mouseTrackPoint = mouseTrackPoint;
this.type = type;
if (color != null) {
this.lineColor = color;
}
this.mode = DisplayEdge.HALF_ANCHORED;
resetBounds();
}
/**
* Calculates the distance between a pair of points.
*
* @param p1 a {@link java.awt.Point} object
* @param p2 a {@link java.awt.Point} object
* @return a double
*/
protected static double distance(Point p1, Point p2) {
double d;
d = (p1.x - p2.x) * (p1.x - p2.x);
d += (p1.y - p2.y) * (p1.y - p2.y);
d = FastMath.sqrt(d);
return d;
}
/**
* Calculates the degenerate horizontal sleeve in the case where the point pair is near horizontal.
*
* @param pp the given point pair.
* @param halfWidth the half-width of the sleeve.
* @return the sleeve as a polygon.
*/
private static Polygon getHorizSleeve(PointPair pp, int halfWidth) {
int[] xpoints = new int[4];
int[] ypoints = new int[4];
xpoints[0] = pp.getFrom().x;
xpoints[1] = pp.getFrom().x;
xpoints[2] = pp.getTo().x;
xpoints[3] = pp.getTo().x;
ypoints[0] = pp.getFrom().y + halfWidth;
ypoints[1] = pp.getFrom().y - halfWidth;
ypoints[2] = pp.getTo().y - halfWidth;
ypoints[3] = pp.getTo().y + halfWidth;
return new Polygon(xpoints, ypoints, 4);
}
//============================PUBLIC METHODS========================//
/**
* {@inheritDoc}
*
* Paints the component.
*/
public void paint(Graphics g) {
// NOTE: For this component, the resetBounds() methods should ALWAYS
// be called before repaint().
switch (this.mode) {
case DisplayEdge.HALF_ANCHORED:
g.setColor(getLineColor());
Point point = this.getRelativeMouseTrackPoint();
setConnectedPoints(calculateEdge(getNode1(), point));
if (getConnectedPoints() != null) {
drawEdge(g);
}
break;
case DisplayEdge.ANCHORED_UNSELECTED:
g.setColor(getLineColor());
setConnectedPoints(calculateEdge(getNode1(), getNode2()));
if (getConnectedPoints() != null) {
drawEdge(g);
}
break;
case DisplayEdge.ANCHORED_SELECTED:
g.setColor(getSelectedColor());
setConnectedPoints(calculateEdge(getNode1(), getNode2()));
if (getConnectedPoints() != null) {
drawEdge(g);
}
break;
default:
throw new IllegalStateException();
}
}
/**
* Draws the actual edge.
*
* @param g the graphics context.
*/
private void drawEdge(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
getConnectedPoints().getFrom().translate(-getLocation().x,
-getLocation().y);
getConnectedPoints().getTo().translate(-getLocation().x,
-getLocation().y);
setClickRegion(null);
int x1 = getConnectedPoints().getFrom().x;
int y1 = getConnectedPoints().getFrom().y;
int x2 = getConnectedPoints().getTo().x;
int y2 = getConnectedPoints().getTo().y;
// Endpoint endpointA = this.getModelEdge().getEndpoint1();
// Endpoint endpointB = this.getModelEdge().getEndpoint2();
//
// if (endpointA == Endpoint.ARROW) {
// x1 += FastMath.signum(x2 - x1) * getStrokeWidth();
// y1 += FastMath.signum(x2 - x1) * getStrokeWidth();
// }
//
// if (endpointB == Endpoint.ARROW) {
// x2 += FastMath.signum(x1 - x2) * getStrokeWidth();
// y2 += FastMath.signum(x1 - x2) * getStrokeWidth();
// }
// This silly-looking next line is required to get around an annoying
// bug that appears in Windows (but not Linux) in JDK
// 1.4.2_08 and 1.5.0_02 at least. If you display an editor, then
// select the "Save Screenshot" menu item, then do it again (or
// select the "Save Graph Image" menu item), all of the lines drawn
// by Graphics2D.drawLine in this class get atomized and distributed
// across the entire dialog, and this atomization doesn't go away
// even if you close the editor and reopen it. Must be some
// undocumented interaction between the two graphics contexts
// (the one drawing the image on screen and the other drawing it
// in the Graphics2D from BufferedImage). In any case, any stroke
// width <= 1.0 seems to cause the problem, so we pick a stroke
// width slightly greater than 1.0. jdramsey 4/16/2005
// g2d.setStroke(new BasicStroke(1.000001f));
Stroke s;
float width = thick ? 3f : 1.1f;
Stroke solid = new BasicStroke(width);
Stroke dashed = new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{9}, 0);
if (this.solid) {
s = solid;
} else {
s = dashed;
}
g2d.setStroke(s);
if (!isSelected()) {
g2d.setColor(this.lineColor);
}
g2d.drawLine(x1, y1, x2, y2);
if (!isShowAdjacenciesOnly()) {
drawEndpoints(getConnectedPoints(), g);
}
firePropertyChange("newPointPair", null, getConnectedPoints());
}
/**
* {@inheritDoc}
*
* Overrides the parent's contains() method using the click region, so that points not in the click region are
* passed through to components lying beneath this one in the z-order. (Equates the effective shape of this edge to
* its click region.)
*/
public boolean contains(int x, int y) {
Polygon clickRegion = getClickRegion();
if (clickRegion != null) {
return clickRegion.contains(new Point(x, y));
} else {
return false;
}
}
/**
* Retrieves the getModel region where mouse clicks are responded to (as opposed to passed on).
*
* @return the click region.
*/
private Polygon getClickRegion() {
if ((this.clickRegion == null) && (getConnectedPoints() != null)) {
this.clickRegion = getSleeve(getConnectedPoints());
}
return this.clickRegion;
}
/**
* Allows subclasses to set the clickable region is for this component.
*
* @param clickRegion a {@link java.awt.Polygon} object
*/
protected final void setClickRegion(Polygon clickRegion) {
this.clickRegion = clickRegion;
}
/**
* Retrieves the currents point pair which defines the line segment of the edge.
*
* @return the getModel point pair.
*/
public final PointPair getPointPair() {
switch (this.mode) {
case DisplayEdge.HALF_ANCHORED:
Point point = this.getRelativeMouseTrackPoint();
setConnectedPoints(calculateEdge(getNode1(), point));
break;
case DisplayEdge.ANCHORED_UNSELECTED:
// Falls through!
case DisplayEdge.ANCHORED_SELECTED:
setConnectedPoints(calculateEdge(getNode1(), getNode2()));
break;
default:
throw new IllegalStateException();
}
return getConnectedPoints();
}
/**
*
getComp1.
*
* @return the 'from' AbstractGraphNode to which this session edge is anchored.
*/
public final DisplayNode getComp1() {
return this.getNode1();
}
/**
* getComp2.
*
* @return the 'to' AbstractGraphNode to which this session edge is anchored.
*/
public final DisplayNode getComp2() {
return this.getNode2();
}
/**
* Getter for the field mode
.
*
* @return the getModel mode of the component.
*/
protected final int getMode() {
return this.mode;
}
/**
* getTrackPoint.
*
* @return the getModel track point for the edge. When a new edge is being created in the UI, one end is anchored
* to a AbstractGraphNode while the other tracks the mouse point. When the mouse is released, the latest mouse
* track point is needed to determine which node it's closest to so that it can be anchored to that node.
*/
public final Point getTrackPoint() {
return this.mouseTrackPoint;
}
/**
* isSelected.
*
* @return true iff the component is selected.
*/
public final boolean isSelected() {
return this.mode == DisplayEdge.ANCHORED_SELECTED;
}
/**
* {@inheritDoc}
*
* Sets whether the component is selected.
*/
public final void setSelected(boolean selected) {
if (selected == isSelected()) {
return;
}
boolean oldSelected = isSelected();
if (this.mode != DisplayEdge.HALF_ANCHORED) {
this.mode = (selected ? DisplayEdge.ANCHORED_SELECTED : DisplayEdge.ANCHORED_UNSELECTED);
firePropertyChange("selected", oldSelected, selected);
repaint();
}
}
/**
* Launches the editor for this edge, if there is one.
*/
public void launchAssociatedEditor() {
}
/**
* {@inheritDoc}
*
* Updates the position of the free end of the edge while it is in the HALF_ANCHORED mode.
*/
public final void updateTrackPoint(Point p) {
if (this.mode != DisplayEdge.HALF_ANCHORED) {
throw new IllegalStateException(
"Cannot call the updateTrackPoint " +
"method when the edge is " +
"not in HALF_ANCHORED mode.");
}
this.mouseTrackPoint = new Point(p);
resetBounds();
repaint();
}
/**
*
Getter for the field node1
.
*
* @return node 1.
*/
public final DisplayNode getNode1() {
return this.node1;
}
/**
* Getter for the field node2
.
*
* @return node 2.
*/
public final DisplayNode getNode2() {
return this.node2;
}
/**
* Getter for the field connectedPoints
.
*
* @return the two points this edge actually connects--that is, the intersections of the edge with node 1 and node
* 2.
*/
public final PointPair getConnectedPoints() {
return this.connectedPoints;
}
//==========================PROTECTED METHODS========================//
/**
* {@inheritDoc}
*
* Allows subclasses to set what the connected points are.
*/
public final void setConnectedPoints(PointPair connectedPoints) {
this.connectedPoints = connectedPoints;
}
/**
*
Getter for the field relativeMouseTrackPoint
.
*
* @return the moure track point relative to this component. (It's usually given relative to the containing
* component.)
*/
public final Point getRelativeMouseTrackPoint() {
return this.relativeMouseTrackPoint;
}
/**
* Calculates the two endpoints of the line segment connecting two given non-overlapping rectangles. (Should give
* back null for overlapping rectangles but doesn't always...)
*
* @param comp1 a {@link edu.cmu.tetradapp.workbench.DisplayNode} object
* @param comp2 a {@link edu.cmu.tetradapp.workbench.DisplayNode} object
* @return a point pair which represents the connecting line segment through the center of each rectangle touching
* the edge of each.
*/
protected final PointPair calculateEdge(DisplayNode comp1, DisplayNode comp2) {
Rectangle r1 = comp1.getBounds();
Rectangle r2 = comp2.getBounds();
Point c1 = new Point((int) (r1.x + r1.width / 2.0),
(int) (r1.y + r1.height / 2.0));
Point c2 = new Point((int) (r2.x + r2.width / 2.0),
(int) (r2.y + r2.height / 2.0));
double angle = FastMath.atan2(c1.y - c2.y, c1.x - c2.x);
angle += FastMath.PI / 2;
Point d = new Point((int) (this.offset * FastMath.cos(angle)),
(int) (this.offset * FastMath.sin(angle)));
c1.translate(d.x, d.y);
c2.translate(d.x, d.y);
Point p1 = getBoundaryIntersection(comp1, c1, c2);
Point p2 = getBoundaryIntersection(comp2, c2, c1);
if ((p1 == null) || (p2 == null)) {
c1 = new Point((int) (r1.x + r1.width / 2.0),
(int) (r1.y + r1.height / 2.0));
c2 = new Point((int) (r2.x + r2.width / 2.0),
(int) (r2.y + r2.height / 2.0));
p1 = getBoundaryIntersection(comp1, c1, c2);
p2 = getBoundaryIntersection(comp2, c2, c1);
}
if ((p1 == null) || (p2 == null)) {
return null;
}
return new PointPair(p1, p2);
}
/**
* Calculates the point pair which defines the straight line segment edge from a given point p to a given component.
* Assumes that the component contains the center point of its bounding rectangle.
*
* @param comp a {@link edu.cmu.tetradapp.workbench.DisplayNode} object
* @param p a {@link java.awt.Point} object
* @return a {@link edu.cmu.tetradapp.workbench.PointPair} object
*/
protected final PointPair calculateEdge(DisplayNode comp, Point p) {
Rectangle r = comp.getBounds();
Point p1 = new Point((int) (r.x + r.width / 2.0),
(int) (r.y + r.height / 2.0));
Point p2 = new Point(p);
p2.translate(getLocation().x, getLocation().y);
Point p3 = getBoundaryIntersection(comp, p1, p2);
return (p3 == null) ? null : new PointPair(p3, p2);
}
//============================PRIVATE METHODS========================//
/**
* Draws endpoints appropriate to the type of edge this is.
*
* @param pp the point pair which specifies where and at what angle the endpoints are to be drawn. The 'from'
* endpoint is drawn at the 'from' point angled as it it were coming from the 'to' endpoint, and
* vice-versa.
* @param g the graphics context.
*/
protected final void drawEndpoints(PointPair pp, Graphics g) {
if (this.getModelEdge() != null) {
Endpoint endpointA = this.getModelEdge().getEndpoint1();
Endpoint endpointB = this.getModelEdge().getEndpoint2();
if (endpointA == Endpoint.CIRCLE) {
drawCircleEndpoint(pp.getTo(), pp.getFrom(), g);
} else if (endpointA == Endpoint.ARROW) {
drawArrowEndpoint(pp.getTo(), pp.getFrom(), g);
}
if (endpointB == Endpoint.CIRCLE) {
drawCircleEndpoint(pp.getFrom(), pp.getTo(), g);
} else if (endpointB == Endpoint.ARROW) {
drawArrowEndpoint(pp.getFrom(), pp.getTo(), g);
}
} else {
switch (this.type) {
case DisplayEdge.SESSION:
drawSessionArrowEndpoint(pp.getFrom(), pp.getTo(), g);
break;
case DisplayEdge.DIRECTED:
drawArrowEndpoint(pp.getFrom(), pp.getTo(), g);
break;
case DisplayEdge.NONDIRECTED:
drawCircleEndpoint(pp.getTo(), pp.getFrom(), g);
drawCircleEndpoint(pp.getFrom(), pp.getTo(), g);
break;
case DisplayEdge.UNDIRECTED:
break;
case DisplayEdge.PARTIALLY_ORIENTED:
drawCircleEndpoint(pp.getTo(), pp.getFrom(), g);
drawArrowEndpoint(pp.getFrom(), pp.getTo(), g);
break;
case DisplayEdge.BIDIRECTED:
drawArrowEndpoint(pp.getFrom(), pp.getTo(), g);
drawArrowEndpoint(pp.getTo(), pp.getFrom(), g);
break;
default:
throw new IllegalArgumentException();
}
}
}
/**
* Draws an arrowhead at the 'to' end of the edge.
*/
private void drawArrowEndpoint(Point from, Point to, Graphics g) {
double a = to.x - from.x;
double b = from.y - to.y;
double theta = FastMath.atan2(b, a);
int itheta = (int) ((theta * 360.0) / (2.0 * FastMath.PI) + 180);
// g.fillArc(to.x - 18, to.y - 18, 36, 36, itheta - 15, 30);
g.fillArc(to.x - 17, to.y - 17, 34, 34,
itheta - 14 - 3 * (int) getStrokeWidth(), 29 + 6 * (int) getStrokeWidth());
}
/**
* Draws an session arrowhead at the 'to' end of the edge.
*/
private void drawSessionArrowEndpoint(Point from, Point to, Graphics g) {
double a = to.x - from.x;
double b = from.y - to.y;
double theta = FastMath.atan2(b, a);
int itheta = (int) ((theta * 360.0) / (2.0 * FastMath.PI) + 180);
// g.fillArc(to.x - 18, to.y - 18, 36, 36, itheta - 15, 30);
g.fillArc(to.x - 18, to.y - 18, 36, 36,
itheta - 33 * (int) getStrokeWidth(), 66 * (int) getStrokeWidth());
}
/**
* Draws a circle endpoint at the 'to' point angled as if coming from the 'from' point.
*/
private void drawCircleEndpoint(Point from, Point to, Graphics g) {
// final int diameter = 13;
int diameter = 12 + (int) getStrokeWidth();
double a = to.x - from.x;
double b = from.y - to.y;
double theta = FastMath.atan2(b, a);
// int itheta = (int) ((theta * 360.0) / (2.0 * FastMath.PI) + 180);
int xminus = (int) (FastMath.cos(theta) * diameter / 2);
int yplus = (int) (FastMath.sin(theta) * diameter / 2);
g.fillOval(to.x - xminus - diameter / 2, to.y + yplus - diameter / 2,
diameter, diameter);
Color c = g.getColor();
g.setColor(Color.white);
g.fillOval(to.x - xminus - diameter / 4 - 1,
to.y + yplus - diameter / 4 - 1, (int) (diameter / 1.4),
(int) (diameter / 1.4));
g.setColor(c);
}
/**
* Calculates the intersection with the boundary of the given component along a line which connects one point which
* lies inside the boundary of the component with another point which lies outside the boundary of the component. If
* the first point does not lie inside the boundary, or if the second point does not lie outside the boundary, a
* null is returned. If the connecting line intersects the boundary at more than one place, the outermost one is
* returned.
*/
private Point getBoundaryIntersection(DisplayNode comp, Point pIn,
Point pOut) {
Point loc = comp.getLocation();
if (!comp.contains(pIn.x - loc.x, pIn.y - loc.y)) {
return null;
}
if (comp.contains(pOut.x - loc.x, pOut.y - loc.y)) {
return null;
}
// Set up from, to, mid for a binary search (result = boundary
// intersection). In testing, this binary search method was
// comparable to analytic methods for finding boundary
// intersections for rectangular nodes but has the advantage
// of flexibility, since this same algorithm applies without
// modification to any convexly shaped nodes. (The 'contains'
// method for that node will of course need to be different.)
Point pFrom = new Point(pOut);
Point pTo = new Point(pIn);
Point pMid = null;
while (DisplayEdge.distance(pFrom, pTo) > 2.0) {
pMid = new Point((pFrom.x + pTo.x) / 2, (pFrom.y + pTo.y) / 2);
if (comp.contains(pMid.x - loc.x, pMid.y - loc.y)) {
pTo = pMid;
} else {
pFrom = pMid;
}
}
return pMid;
}
/**
* Calculates a sleeve around the edge which represents the region within which mouse clicks will be send to the
* edge (as opposed to ignored).
*
* @param pp the point pair representing the line segment of the edge.
* @return the Polygon representing the sleeve, or null if no such Polygon exists (because, e.g., one of the
* endpoints is null).
*/
private Polygon getSleeve(PointPair pp) {
if ((pp == null) || (pp.getFrom() == null) || (pp.getTo() == null)) {
return null;
}
// int d = 7; // halfwidth of the sleeve.
int d = (int) getStrokeWidth() + 6; // halfwidth of the sleeve.
if (FastMath.abs(pp.getFrom().y - pp.getTo().y) <= 3) {
return DisplayEdge.getHorizSleeve(pp, d);
}
int[] xpoints = new int[4];
int[] ypoints = new int[4];
double qx;
double qy;
qx = pp.getTo().x - pp.getFrom().x;
qy = pp.getTo().y - pp.getFrom().y;
double sx, sy;
sx = (double) (d * d) / (1.0 + (qx * qx) / (qy * qy));
sx = FastMath.pow(sx, 0.5);
sy = -(qx / qy) * sx;
sx += (double) pp.getFrom().x + 1.0;
sy += (double) pp.getFrom().y + 1.0;
Point t = new Point((int) (sx) - pp.getFrom().x,
(int) (sy) - pp.getFrom().y);
xpoints[0] = pp.getFrom().x + t.x;
xpoints[1] = pp.getTo().x + t.x;
xpoints[2] = pp.getTo().x - t.x;
xpoints[3] = pp.getFrom().x - t.x;
ypoints[0] = pp.getFrom().y + t.y;
ypoints[1] = pp.getTo().y + t.y;
ypoints[2] = pp.getTo().y - t.y;
ypoints[3] = pp.getFrom().y - t.y;
return new Polygon(xpoints, ypoints, 4);
}
/**
* This method resets the bounds of the edge component to the union of the bounds of the two components which the
* edge connects. It also calculates the bounds of these two components relative to this new union.
*/
private void resetBounds() {
switch (this.mode) {
case DisplayEdge.HALF_ANCHORED:
Rectangle temp = new Rectangle(this.mouseTrackPoint.x,
this.mouseTrackPoint.y, 0, 0);
setBounds(getNode1().getBounds().union(temp.getBounds()));
// node1RelativeBounds = new Rectangle(getNode1().getBounds());
this.relativeMouseTrackPoint = new Point(this.mouseTrackPoint);
// node1RelativeBounds.translate(-getLocation().x, -getLocation().y);
getRelativeMouseTrackPoint().translate(-getLocation().x,
-getLocation().y);
break;
case DisplayEdge.ANCHORED_UNSELECTED:
// Falls through!
case DisplayEdge.ANCHORED_SELECTED:
Rectangle r1 = this.node1.getBounds();
Rectangle r2 = this.node2.getBounds();
Point c1 = new Point((int) (r1.x + r1.width / 2.0),
(int) (r1.y + r1.height / 2.0));
Point c2 = new Point((int) (r2.x + r2.width / 2.0),
(int) (r2.y + r2.height / 2.0));
double angle = FastMath.atan2(c1.y - c2.y, c1.x - c2.x);
angle += FastMath.PI / 2;
Point d = new Point((int) (this.offset * FastMath.cos(angle)),
(int) (this.offset * FastMath.sin(angle)));
r1.translate(d.x, d.y);
r2.translate(d.x, d.y);
setBounds(r1.getBounds().union(r2.getBounds()));
break;
default:
throw new IllegalStateException();
}
}
private boolean isShowAdjacenciesOnly() {
return this.showAdjacenciesOnly;
}
/**
* Getter for the field modelEdge
.
*
* @return a {@link edu.cmu.tetrad.graph.Edge} object
*/
public final Edge getModelEdge() {
return this.modelEdge;
}
/**
* Getter for the field offset
.
*
* @return a double
*/
public double getOffset() {
return this.offset;
}
/**
* {@inheritDoc}
*/
public void setOffset(double offset) {
this.offset = offset;
}
/**
* Getter for the field lineColor
.
*
* @return a {@link java.awt.Color} object
*/
public Color getLineColor() {
return this.highlighted ? getHighlightedColor() : this.lineColor;
// return lineColor;
}
/**
* {@inheritDoc}
*/
public void setLineColor(Color lineColor) {
if (lineColor != null) {
this.lineColor = lineColor;
}
}
/**
* Getter for the field solid
.
*
* @return a boolean
*/
public boolean getSolid() {
return this.solid;
}
/**
* {@inheritDoc}
*/
public void setSolid(boolean solid) {
this.solid = solid;
}
/**
* {@inheritDoc}
*/
@Override
public void setThick(boolean thick) {
this.thick = thick;
}
/**
* Getter for the field selectedColor
.
*
* @return a {@link java.awt.Color} object
*/
public Color getSelectedColor() {
return this.selectedColor;
}
/**
* {@inheritDoc}
*/
public void setSelectedColor(Color selectedColor) {
this.selectedColor = selectedColor;
}
/**
* Getter for the field highlightedColor
.
*
* @return a {@link java.awt.Color} object
*/
public Color getHighlightedColor() {
return this.highlightedColor;
}
/**
* {@inheritDoc}
*/
public void setHighlightedColor(Color highlightedColor) {
this.highlightedColor = highlightedColor;
}
/**
* Getter for the field strokeWidth
.
*
* @return a float
*/
public float getStrokeWidth() {
return this.strokeWidth;
}
/**
* {@inheritDoc}
*/
public void setStrokeWidth(float strokeWidth) {
if (strokeWidth < 0f) {
throw new IllegalArgumentException("Stroke width must be at least 0.");
}
this.strokeWidth = strokeWidth;
}
/**
* {@inheritDoc}
*/
public void setHighlighted(boolean highlighted) {
this.highlighted = highlighted;
}
//======================= Event handler class========================//
/**
* Handles ComponentEvent
s.
*/
private final class ComponentHandler extends ComponentAdapter {
public void componentMoved(ComponentEvent e) {
resetBounds();
repaint();
}
public void componentResized(ComponentEvent e) {
resetBounds();
repaint();
}
}
/**
* Handles PropertyChangeEvent
s.
*/
private final class PropertyChangeHandler implements PropertyChangeListener {
/**
* This method gets called when a bound property is changed.
*
* @param evt A PropertyChangeEvent object describing the event source and the property that has changed.
*/
public void propertyChange(PropertyChangeEvent evt) {
String name = evt.getPropertyName();
if ("selected".equals(name)) {
if (Boolean.FALSE.equals(evt.getNewValue())) {
setSelected(false);
}
}
}
}
}