/*
* JFLAP - Formal Languages and Automata Package
*
*
* Susan H. Rodger
* Computer Science Department
* Duke University
* August 27, 2009
* Copyright (c) 2002-2009
* All rights reserved.
* JFLAP is open source software. Please see the LICENSE for terms.
*
*/
package gui.viewer;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import debug.EDebug;
import automata.Automaton;
import automata.Note;
import automata.State;
import automata.Transition;
import automata.event.AutomataStateEvent;
import automata.event.AutomataStateListener;
import automata.event.AutomataTransitionEvent;
import automata.event.AutomataTransitionListener;
import java.util.HashSet;
/**
* This is the very basic class of an Automaton drawer. It has facilities to
* draw the Automaton. Subclasses may be derived to have finer control over how
* things are drawn.
*
* @author Thomas Finley
* @version 1.0
*/
public class AutomatonDrawer {
/**
* Instantiates an object to draw an automaton.
*
* @param automaton
* the automaton to handle
*/
public AutomatonDrawer(Automaton automaton) {
this.automaton = automaton;
DrawerListener listener = new DrawerListener();
getAutomaton().addStateListener(listener);
getAutomaton().addTransitionListener(listener);
/*
* Use a moore state drawer if it is a Moore machine.
* This draws a little box that shows the output of the
* state.
*/
if(automaton instanceof automata.mealy.MooreMachine)
statedrawer = new MooreStateDrawer();
}
/**
* Retrieves the Automaton
handled by this drawer.
*
* @return the Automaton
handled by this drawer
*/
public Automaton getAutomaton() {
return automaton;
}
//naive optimization for drawing
ArrayList hs = new ArrayList();
HashSet lhs = new HashSet();
int specHash = Integer.MIN_VALUE;
//
/**
* Draws our automaton.
*
* @param g2
* the Graphics object to draw the automaton on
*/
public void drawAutomaton(Graphics g2) {
if (!valid)
refreshArrowMap();
Graphics2D g = (Graphics2D) g2.create();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setFont(g.getFont().deriveFont(12.0f));
// Draw transitions between states.
g.setColor(Color.black);
drawTransitions(g);
// int sh = automaton.hashCode();
// if (specHash != sh){
// specHash = sh;
// hs.clear();
// lhs.clear();
// // Draw every state...or not
// State[] states = automaton.getStates();
//
// // for (int i = 0; i < states.length; i++) {
// for (int i = states.length - 1; i >= 0; i--) {
// // drawState(g, states[i]);
// if (!lhs.contains(states[i].getPoint())){
// hs.add(states[i]);
// lhs.add(states[i].getPoint());
// }
// }
// }
// //reverse again, to get the correct ordering for non-overlapping things
// for (int i = hs.size() - 1; i >= 0; i--)
// drawState(g, hs.get(i));
State[] states = automaton.getStates();
for (int i = 0; i < states.length; i++){
drawState(g, states[i]);
}
this.drawSelectionBox(g);
g.dispose();
}
/**
* Returns the bounds for an individual state.
*
* @param state
* the state to get the bounds for
* @return the rectangle that the state needs to be in to completely enclose
* itself
*/
public Rectangle getBounds(State state) {
// int radius = (int)(statedrawer.getRadius()*scaleBy);
// int radius = (int)(statedrawer.getRadius()*curTransform.getScaleX()); //getScaleX and Y should be same
int radius = statedrawer.getRadius(); //getScaleX and Y should be same
Point p = state.getPoint();
int yAdd = state.getLabels().length * 15;
if (getAutomaton().getInitialState() == state)
return new Rectangle(p.x - radius * 2, p.y - radius, radius * 3,
radius * 2 + yAdd);
return new Rectangle(p.x - radius, p.y - radius, radius * 2, radius * 2
+ yAdd);
}
/**
* Returns the bounds for an individual transition.
*
* @param transition
* the transition to get the bounds for
* @return the rectangle that the transition needs to be in to completely
* enclose itself
*/
public Rectangle getBounds(Transition transition) {
if (!valid)
refreshArrowMap();
CurvedArrow arrow = (CurvedArrow) transitionToArrowMap.get(transition);
Rectangle2D r = arrow.getBounds();
return new Rectangle((int) r.getX(), (int) r.getY(),
(int) r.getWidth(), (int) r.getHeight());
}
/**
* Returns the bounds that the automaton is drawn in.
*
* @return the bounds that the automaton is drawn in, or null
* if there is nothing to draw, i.e., the automaton has no states
*/
public Rectangle getBounds() {
if (validBounds){
// System.out.println("Using cache");
return cachedBounds;
}
if (!valid)
refreshArrowMap();
State[] states = getAutomaton().getStates();
if (states.length == 0)
return null;
Rectangle rect = getBounds(states[0]);
for (int i = 1; i < states.length; i++)
rect.add(getBounds(states[i]));
ArrayList notes = getAutomaton().getNotes();
for(int k = 0; k < notes.size(); k++){
Note curNote = ((Note)notes.get(k));
Rectangle newBounds = new Rectangle(curNote.getAutoPoint(), new Dimension(curNote.getBounds().getSize()));
rect.add(newBounds);
}
Iterator it = arrowToTransitionMap.keySet().iterator();
while (it.hasNext()) {
CurvedArrow arrow = (CurvedArrow) it.next();
Rectangle2D arrowBounds = arrow.getBounds();
rect.add(arrowBounds);
}
validBounds = true;
// return cachedBounds = rect;
return cachedBounds = curTransform.createTransformedShape(rect).getBounds();
}
/**
* Draws a state on the automaton.
*
* @param g
* the graphics object to draw upon
* @param state
* the state to draw
*/
protected void drawState(Graphics g, State state) {
statedrawer.drawState(g, getAutomaton(), state);
if (drawLabels) {
statedrawer.drawStateLabel(g, state, state.getPoint(),
StateDrawer.STATE_COLOR);
}
}
/**
* Draws the transitions of the automaton.
*
* @param g
* the graphics object to draw upon
*/
protected void drawTransitions(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
Set arrows = arrowToTransitionMap.keySet();
Iterator it = arrows.iterator();
while (it.hasNext()) {
CurvedArrow arrow = (CurvedArrow) it.next();
if (arrow.myTransition.isSelected){
arrow.drawHighlight(g2);
arrow.drawControlPoint(g2);
}
else
arrow.draw(g2);
}
}
/**
* draws selection box
*/
protected void drawSelectionBox(Graphics g){
g.drawRect(mySelectionBounds.x, mySelectionBounds.y, mySelectionBounds.width, mySelectionBounds.height);
}
/**
* Refreshes the arrowToTransitionMap
structure.
*/
private void refreshArrowMap() {
if (automaton == null) {
//System.out.println("Automaton is null, how?");
return;
}
State[] states = automaton.getStates();
arrowToTransitionMap.clear(); // Remove old entries.
transitionToArrowMap.clear(); // Remove old entries.
for (int i = 0; i < states.length; i++) {
// This is some code that handles interstate (heh) transitions.
for (int j = i + 1; j < states.length; j++) {
// We want all transitions.
Transition[] itoj = automaton.getTransitionsFromStateToState(
states[i], states[j]);
Transition[] jtoi = automaton.getTransitionsFromStateToState(
states[j], states[i]);
float top = jtoi.length > 0 ? 0.5f : 0.0f;
float bottom = itoj.length > 0 ? 0.5f : 0.0f;
if (itoj.length + jtoi.length == 0)
continue;
// Get where points should appear to emanate from.
double angle = angle(states[i], states[j]);
Point fromI = pointOnState(states[i], angle - ANGLE);
Point fromJ = pointOnState(states[j], angle + Math.PI + ANGLE);
for (int n = 0; n < itoj.length; n++) {
if(curveTransitionMap.containsKey(itoj[n])){
top = curveTransitionMap.get(itoj[n]);
}
float curvy = top+n;
CurvedArrow arrow = n == 0 ? new CurvedArrow(fromI, fromJ,
curvy, itoj[n]) : new InvisibleCurvedArrow(fromI, fromJ,
curvy, itoj[n]);
arrow.setLabel(itoj[n].getDescription());
arrowToTransitionMap.put(arrow, itoj[n]);
transitionToArrowMap.put(itoj[n], arrow);
}
fromI = pointOnState(states[i], angle + ANGLE);
fromJ = pointOnState(states[j], angle + Math.PI - ANGLE);
for (int n = 0; n < jtoi.length; n++) {
if(curveTransitionMap.containsKey(jtoi[n])){
bottom = curveTransitionMap.get(jtoi[n]);
}
float curvy = bottom+n;
CurvedArrow arrow = n == 0 ? new CurvedArrow(fromJ, fromI,
curvy, jtoi[n]) : new InvisibleCurvedArrow(fromJ, fromI,
curvy, jtoi[n]);
String label = jtoi[n].getDescription();
arrow.setLabel(label);
arrowToTransitionMap.put(arrow, jtoi[n]);
transitionToArrowMap.put(jtoi[n], arrow);
}
}
// Now handle transitions between a single state.
Transition[] trans = automaton.getTransitionsFromStateToState(
states[i], states[i]);
if (trans.length == 0)
continue;
Point from = pointOnState(states[i], -Math.PI * 0.333);
Point to = pointOnState(states[i], -Math.PI * 0.667);
for (int n = 0; n < trans.length; n++) {
if(selfTransitionMap.containsKey(trans[n])){
//EDebug.print(selfTransitionMap);
Point storedfrom = pointOnState(states[i], (selfTransitionMap.get(trans[n])+Math.PI*.166));
Point storedto = pointOnState(states[i], (selfTransitionMap.get(trans[n])-Math.PI*.166));
CurvedArrow arrow = n == 0 ? new CurvedArrow(storedfrom, storedto, -2.0f, trans[n])
: new InvisibleCurvedArrow(storedfrom, storedto, -2.0f - n, trans[n]);
arrow.setLabel(trans[n].getDescription());
arrowToTransitionMap.put(arrow, trans[n]);
transitionToArrowMap.put(trans[n], arrow);
}else{
//EDebug.print(selfTransitionMap);
selfTransitionMap.put(trans[n], -Math.PI*.5);
CurvedArrow arrow = n == 0 ? new CurvedArrow(from, to, -2.0f, trans[n])
: new InvisibleCurvedArrow(from, to, -2.0f - n, trans[n]);
//INSERTED for TransitionGUI
arrow.myTransition = trans[n];
//END INSERTED for TransitionGUI
//MERLIN MERLIN MERLIN MERLIN MERLIN//
arrow.setLabel(trans[n].getDescription());
arrowToTransitionMap.put(arrow, trans[n]);
transitionToArrowMap.put(trans[n], arrow);
}
}
}
valid = true;
}
/**
* Given two states, if there were a line connecting the center of the two
* states, at which point would that line intersect the outside of the first
* state? In other words, the point on state1 closest to the point on
* state2.
*
* @param state1
* the first state
* @param state2
* the second state
* @return as described, the point of intersection on state1
*/
protected Point getCenterIntersection(State state1, State state2) {
return pointOnState(state1, angle(state1, state2));
}
/**
* What is the angle on state1 of the point closest to state2?
*
* @param state1
* the first state
* @param state2
* the second state
* @return the angle on state1 of the point closest to state2
*/
private double angle(State state1, State state2) {
Point p1 = state1.getPoint();
Point p2 = state2.getPoint();
double x = (double) (p2.x - p1.x);
double y = (double) (p2.y - p1.y);
return Math.atan2(y, x);
}
/**
* Given a state and an angle, if we treat the state as a circle, what point
* does that angle represent?
*
* @param state
* the state
* @param angle
* the angle on the state
* @return the point on the outside of the state with this angle
*/
public Point pointOnState(State state, double angle) {
Point point = new Point(state.getPoint());
double x = Math.cos(angle) * (double) StateDrawer.STATE_RADIUS;
double y = Math.sin(angle) * (double) StateDrawer.STATE_RADIUS;
point.translate((int) x, (int) y);
return point;
}
/**
* Informs the drawer that states in the automata have changed to the point
* where a redraw is appropriate.
*/
public void invalidate() {
valid = false;
this.invalidateBounds();
}
/**
* Informs the drawer that it should recalculate the bounds the next time
* they are requested. This method is called automatically if the automaton
* changes.
*/
public void invalidateBounds() {
validBounds = false;
}
/**
* Gets the state at a particular point.
*
* @param point
* the point to check
* @return a State
object at this particular point, or null
* if no state is at this point
*/
public State stateAtPoint(Point point) {
State[] states = getAutomaton().getStates();
// Work backwards, since we want to select the "top" state,
// and states are drawn forwards so later is on top.
for (int i = states.length - 1; i >= 0; i--)
if (point.distance(states[i].getPoint()) <= StateDrawer.STATE_RADIUS)
return states[i];
// Not found. Drat!
return null;
}
/**
* Gets the transition at a particular point.
*
* @param point
* the point to check
* @return a Transition
object at this particular point, or
* null
if no transition is at this point
*/
public Transition transitionAtPoint(Point point) {
if (!valid)
refreshArrowMap();
Set arrows = arrowToTransitionMap.keySet();
Iterator it = arrows.iterator();
while (it.hasNext()) {
CurvedArrow arrow = (CurvedArrow) it.next();
if (arrow.isNear(point, 2))
return (Transition) arrowToTransitionMap.get(arrow);
}
return null;
}
/**
* Returns the state drawer.
*
* @return the state drawer
*/
public StateDrawer getStateDrawer() {
return statedrawer;
}
/**
* Listens for changes in transitions of our automaton. This method is
* called by the internal automaton listener for this object, and while not
* called directly by the automaton, is passed along the same event.
*
* @param event
* the transition event
*/
protected void transitionChange(AutomataTransitionEvent event) {
invalidate();
}
/**
* Listens for changes in states of our automaton. This method is called by
* the internal automaton listener for this object, and while not called
* directly by the automaton, is passed along the same event.
*
* @param event
* the state event
*/
protected void stateChange(AutomataStateEvent event) {
if (event.isMove())
invalidate();
else
invalidateBounds();
}
/**
* Returns the curved arrow object that represents a particular transition.
*
* @param transition
* the transition to find the arrow for
* @return the curved arrow object that is used to draw this transition
*/
protected CurvedArrow arrowForTransition(Transition transition) {
return (CurvedArrow) transitionToArrowMap.get(transition);
}
/**
* Returns if state labels are drawn in the diagram.
*
* @return if state labels are drawn in the diagram
*/
public boolean doesDrawStateLabels() {
return drawLabels;
}
/**
* Sets if state labels should be drawn in the diagram or not.
*
* @param drawLabels
* true
if state labels should be drawn in the
* state diagram, false
if they should not be
*/
public void shouldDrawStateLabels(boolean drawLabels) {
this.drawLabels = drawLabels;
}
public void setAutomaton(Automaton newAuto) {
if (newAuto == null) {
//System.out.println("Setting automaton null");
return;
}
automaton = newAuto;
this.invalidate();
}
public void setSelectionBounds(Rectangle bounds) {
mySelectionBounds = bounds;
}
public Rectangle getSelectionBounds() {
return mySelectionBounds;
}
// public void setScale(double scale){
// scaleBy = scale;
// validBounds = false;
// }
public void setTransform(AffineTransform af){
curTransform = af;
}
private Rectangle mySelectionBounds = new Rectangle(0, 0, -1, -1);
/** The automaton we're handling. */
private Automaton automaton;
/** If we should draw state labels or not. */
private boolean drawLabels = true;
/**
* The difference in angle from the emination point of the transitions from
* the point closest to the other state.
*/
protected static final double ANGLE = Math.PI / 25.0;
/**
* Whether or not the drawing objects should be redone on the next draw.
*/
private boolean valid = false;
/**
* If any change happens at all that could effect the bounds, this is
* changed.
*/
private boolean validBounds = false;
/** The cached bounds. */
private Rectangle cachedBounds = null;
/**
* A map of self transitions mapped to their angle of appearance.
*/
public HashMap selfTransitionMap = new HashMap();
/**
* Map of curvatures for transitions
*/
public HashMap curveTransitionMap = new HashMap();
/**
* A map of curved arrows to transitions. This object is also used for
* iteration over all arrows when drawing must be done
*/
public HashMap arrowToTransitionMap = new HashMap();
/** The map from transitions to their respective arrows. */
public HashMap transitionToArrowMap = new HashMap();
/** The state drawer. */
public StateDrawer statedrawer = new StateDrawer();
// /**Amount to scale by, purely for scroll calclulation*/
// private double scaleBy = 1;
/**The transform instead*/
private AffineTransform curTransform = new AffineTransform();
/**
* This automaton listener takes care of responding to the events.
*/
private class DrawerListener implements AutomataStateListener,
AutomataTransitionListener {
/**
* Listens for changes in transitions of our automaton.
*
* @param event
* the transition event
*/
public void automataTransitionChange(AutomataTransitionEvent event) {
transitionChange(event);
}
/**
* Listens for changes in states of our automaton.
*
* @param event
* the state event
*/
public void automataStateChange(AutomataStateEvent event) {
stateChange(event);
}
}
}