package jsetool.gui; import java.awt.*; import java.awt.event.MouseEvent; import java.awt.font.FontRenderContext; import java.awt.geom.*; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.*; import javax.swing.JOptionPane; /** * @author Mike Lawson * * @since Oct 27, 2004 * * The ConnectivityEdge class is a basic class that draws * an directed line (i.e. one with an arrow) between * GlobalStates. * */ public class ConnectivityEdge implements Drawable, Externalizable { protected LabeledState source; protected LabeledState target; protected Point2D controlPoint; protected Rectangle2D controlSquare; protected PointToPointEdge edge; protected PointToPointEdge secondEdge; protected String edgeLabel; protected boolean isHighlighted = false; protected boolean objectIsSelected; private boolean isEditable = true; private boolean beingDragged = false; private boolean toBeDeleted; private Dimension squareDimension = new Dimension(7, 7); private static final double STRAIGHT_LINE_THRESHOLD = 5; /** * Constructs a new object. */ public ConnectivityEdge() { // Required for serialization } /** * Constructs a new edge from the given state to the target. * @param source * @param target */ public ConnectivityEdge(LabeledState source, LabeledState target) { this.source = source; this.target = target; source.addAssociatedDrawable(this); target.addAssociatedDrawable(this); edge = new PointToPointEdge(); edge.setHasArrowHead(true); } /** * Externalizable implementation. Reads a ConnectivityEdge from * * the given input stream. * @param in * @throws IOException * @throws ClassNotFoundException * */ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { edge = (PointToPointEdge)in.readObject(); secondEdge = (PointToPointEdge)in.readObject(); edgeLabel = (String)in.readObject(); source = (LabeledState)in.readObject(); target = (LabeledState)in.readObject(); isEditable = in.readBoolean(); boolean hasControlPoint = in.readBoolean(); if (hasControlPoint) { double cpX = in.readDouble(); double cpY = in.readDouble(); controlPoint = new Point2D.Double(cpX, cpY); double csX = in.readDouble(); double csY = in.readDouble(); double csW = in.readDouble(); double csH = in.readDouble(); controlSquare = new Rectangle2D.Double(csX, csY, csW, csH); } } /** * Externalizable implementation. Writes this object to * the given output stream. * * @param out * @throws IOException * */ public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(edge); out.writeObject(secondEdge); out.writeObject(edgeLabel); out.writeObject(source); out.writeObject(target); out.writeBoolean(isEditable); if (controlPoint == null) { out.writeBoolean(false); } else { out.writeBoolean(true); out.writeDouble(controlPoint.getX()); out.writeDouble(controlPoint.getY()); out.writeDouble(controlSquare.getX()); out.writeDouble(controlSquare.getY()); out.writeDouble(controlSquare.getWidth()); out.writeDouble(controlSquare.getHeight()); } } /** * Returns the target of this edg.e * @return LabeledState */ public LabeledState getTarget() { return target; } public double getHeight() { return edge.getHeight(); } public double getWidth() { return edge.getWidth(); } public double getX() { return edge.getX(); } public double getY() { return edge.getY(); } /** * Returns the label for the edge * @return String */ public String getEdgeLabel() { return edgeLabel; } /** * Sets this object as explored. * @param b */ public void setExplored(boolean b) { //does nothing for this object } /** * returns a string representation of this object. * * @return String */ public String toString() { return "Edge from: " + source.toString() + " to: " + target.toString() + " message: " + edgeLabel; } /** * Sets the label for the edge * @param edgeLabel */ public void setEdgeLabel(String edgeLabel) { this.edgeLabel = edgeLabel.trim(); } /** * Draws this object on the given graphics context. * @param g */ public void draw(Graphics g) { Graphics2D g2d = (Graphics2D)g; Point2D startPoint = getSourceToTargetStartPoint(); Point2D endPoint = getSourceToTargetendPoint(); if (controlPoint == null) { edge.setEdgePoints(startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY()); edge.draw(g); } else { edge.setEdgePoints(startPoint.getX(), startPoint.getY(), controlPoint.getX(), controlPoint.getY()); secondEdge.setEdgePoints(controlPoint.getX(), controlPoint.getY(), endPoint.getX(), endPoint.getY()); edge.draw(g); secondEdge.draw(g); g2d.fill(controlSquare); } g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Determine the size of the rendered state label. // This can only be done with a valid Graphics object, // so we must do it here each time as the graphics context // may change dynamically. if (edgeLabel == null) { return; } drawEdgeLabel(g2d); } /** * Draws the edge label on the display. * @param g2d */ protected void drawEdgeLabel(Graphics2D g2d) { Font font = new Font("Serif", Font.PLAIN, 20); g2d.setFont(font); FontRenderContext frc = g2d.getFontRenderContext(); Rectangle2D textBounds = g2d.getFont().getStringBounds(edgeLabel, frc); double startX = edge.getX(); double startY = edge.getY(); double endX = startX + edge.getWidth(); double endY = startY + edge.getHeight(); double stringX = startX + ((endX - startX) / 2) - textBounds.getWidth() / 2; double stringY = startY + ((endY - startY) / 2); // If there is a control point, put the label next to it instead if (controlPoint != null) { stringX = controlPoint.getX() + squareDimension.getWidth() + 5; stringY = controlPoint.getY(); } Paint labelPaint = isHighlighted ? Color.RED : Color.BLACK; g2d.setPaint(labelPaint); g2d.drawString(edgeLabel, (float)stringX, (float)stringY); } /** * Returns the start point of a direct edge between the source and target * @return Point2D */ private Point2D getSourceToTargetStartPoint() { Point2D answer = new Point2D.Double(); double startX; double startY; // If the source is above the target, the line goes from the bottom // of the source to the top of the target. if (source.getY() + source.getHeight() / 2 < target.getY()) { startX = source.getX() + source.getWidth() / 2; startY = source.getY() + source.getHeight(); } // If the source is below the target, the line goes from the top of the // source to the bottom of the target. else if (source.getY() - source.getHeight() / 2 > target.getY()) { startX = source.getX() + source.getWidth() / 2; startY = source.getY(); } else // they're on the same level. { // Left or right? boolean onLeft = source.getX() < target.getX(); if (onLeft) { startX = source.getX() + source.getWidth(); startY = source.getY() + source.getHeight() /2 ; } else { startX = source.getX(); startY = source.getY() + source.getHeight() /2 ; } } answer.setLocation(startX, startY); return answer; } /** * Returns the end point of a direct edge between the source and target * @return Point2D */ private Point2D getSourceToTargetendPoint() { Point2D answer = new Point2D.Double(); double endX; double endY; // If the source is above the target, the line goes from the bottom // of the source to the top of the target. if (source.getY() + source.getHeight() / 2 < target.getY()) { endX = target.getX() + target.getWidth() / 2; endY = target.getY(); } // If the source is below the target, the line goes from the top of the // source to the bottom of the target. else if (source.getY() - source.getHeight() / 2 > target.getY()) { endX = target.getX() + target.getWidth() / 2; endY = target.getY() + target.getHeight(); } else // they're on the same level. { // Left or right? boolean onLeft = source.getX() < target.getX(); if (onLeft) { endX = target.getX(); endY = target.getY() + target.getHeight() / 2; } else { endX = target.getX() + target.getWidth(); endY = target.getY() + target.getHeight() / 2; } } answer.setLocation(endX, endY); return answer; } /** * Moves this object to the given location. * @param me */ public void mouseDragged(MouseEvent me) { if (beingDragged == true) { controlPoint.setLocation(me.getX(), me.getY()); Point2D centeredPoint = centerOnPoint(me.getX(), me.getY(), squareDimension); controlSquare.setFrame(centeredPoint, squareDimension); } } /** * Returns true if the mouse event is on this object. * @param me * @return true if mouse is on this object. */ public boolean isMouseOn(MouseEvent me) { if (edge == null) return false; if (me == null) return false; boolean rc = edge.isMouseOn(me); if (secondEdge != null) { rc = rc || secondEdge.isMouseOn(me); } return rc; } /** * Toggles whether this object should be highlighted the next time it's * drawn. * @param doHighlight */ public void setHighlight(boolean doHighlight) { isHighlighted = doHighlight; edge.setHighlight(doHighlight); if (secondEdge != null) { secondEdge.setHighlight(doHighlight); } } /** * Informs this object that the mouse was pressed * on this object. * @param me */ public void mousePressed(MouseEvent me) { // Only respond if primary mouse button pressed. if (me.getButton() != MouseEvent.BUTTON1) return; // If there isn't a control point, create one // set being dragged to true. if (controlPoint == null) { controlPoint = edge.getPointOnLineCloseTo(me.getX(), me.getY()); controlSquare = new Rectangle2D.Double(0,0,0,0); controlSquare.setFrame(controlPoint, squareDimension); beingDragged = true; // Go double edged. secondEdge = new PointToPointEdge(); secondEdge.setHasArrowHead(true); edge.setHasArrowHead(false); } else if (inControlPointBoundingRectangle(me.getX(), me.getY())) { // This means the user started to drag // an existing control point. beingDragged = true; } else { beingDragged = false; } } /** * Tells this object that the mouse was released from this object. * @param me */ public void mouseReleased(MouseEvent me) { if (beingDragged == false) return; beingDragged = false; // Put the center of the control square where the mouse was released Point2D centeredPoint = centerOnPoint(me.getX(), me.getY(), squareDimension); controlSquare.setFrame(centeredPoint, squareDimension); // Create the straight-line between the two shapes, // if the control point is within the threshold of the // straight edge, go to single edge. // If not, stay as double edge. Point2D startPoint = getSourceToTargetStartPoint(); Point2D endPoint = getSourceToTargetendPoint(); Line2D tempEdge = new Line2D.Double(startPoint, endPoint); if (tempEdge.ptLineDist(centeredPoint) < STRAIGHT_LINE_THRESHOLD) { // goto single line setAsSingleEdge(); } } /** * Called by the encapsulating drawing pane whenever the mouse * is clicked on the object. This object should set itself as * "selected." * @param me the event that caused the callback. * */ public void setSelected(boolean isSelected, MouseEvent me) { objectIsSelected = isSelected; edge.setSelected(isSelected, me); if (secondEdge != null) secondEdge.setSelected(isSelected, me); } /** * Sets whether this object is editable or not, based on the value. * * Implementations should cache this value and take appropriate actions * when showEditor is called (if ever). * * An example is whether an object will let you change its label. * @param isEditable */ public void setEditable(boolean isEditable) { this.isEditable = isEditable; edge.setEditable(isEditable); } /** * Called when the user has selected to edit something about * the Drawable object, e.g. change it's label. * * @param me the event that caused the callback */ public void showEditor(MouseEvent me) { if (isEditable == true) { promptForEdgeLabel(); } } /** * Deletes this state and all associated edges. * */ public void objectIsBeingDeleted() { // Set this object to be deleted toBeDeleted = true; // Unregister edge with source and target. source.removeAssociatedDrawable(this); target.removeAssociatedDrawable(this); } /** * Returns true if objectIsBeingDeleted has been called. * * @return boolean */ public boolean toBeDeleted() { return toBeDeleted; } /** * Returns the length of the line between points one and two * @param one * @param two * @return int */ protected int getLineLength(Point one, Point two) { int dxdx = (two.x - one.x) * (two.x - one.x); int dydy = (two.y - one.y) * (two.y - one.y); return (int)Math.sqrt(dxdx + dydy); } /** * Asks the user for a label for the edge. * */ protected void promptForEdgeLabel() { boolean validLabel = false; while (!validLabel) { String temp = JOptionPane.showInputDialog(null, "Enter message (e.g. +r, -x)", edgeLabel); if (temp == null) { return; } if (temp.startsWith("+") || temp.startsWith("-")) { setEdgeLabel(temp); validLabel = true; } else { JOptionPane.showMessageDialog(null, "Error, a message must begin with + for a receive, - for a send.", "Invalid Input", JOptionPane.ERROR_MESSAGE); } } // while not valid input } /** * Returns true if the given point is on the rectangle for the * control point * @param x * @param y * @return boolean */ protected boolean inControlPointBoundingRectangle(double x, double y) { return controlSquare.contains(x,y); } /** * Returns a Point2D where the given x,y coordinates are in the * center of the given rectangular dimension * @param px * @param py * @param d * * @return Point2D */ protected Point2D centerOnPoint(double px, double py, Dimension2D d) { double x = px - d.getWidth() / 2; double y = py - d.getHeight() / 2; return new Point2D.Double(x, y); } /** * Removes the 2nd drawing edge. */ protected void setAsSingleEdge() { controlPoint = null; controlSquare = null; secondEdge = null; edge.setHasArrowHead(true); } }