/*
* 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.minimize;
import gui.tree.SelectTreeDrawer;
import gui.tree.Trees;
import gui.viewer.SelectionDrawer;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import javax.swing.JOptionPane;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import automata.State;
import automata.fsa.FiniteStateAutomaton;
import automata.fsa.MinimizeTreeNode;
import automata.fsa.Minimizer;
/**
* The MinimizeController
serves as an intermediary between the
* Minimizer
object and the various gui classes that handles the
* drawing of the unminimized automaton and minimization tree. This does not
* handle the building of the minimized automata, however, which is handled by
* {@link gui.minimize.BuilderController}.
*
* @author Thomas Finley
*/
class MinimizeController {
/**
* Instantiates a new MinimizeController
.
*/
public MinimizeController(MinimizePane view, SelectionDrawer adrawer,
SelectTreeDrawer tdrawer, Minimizer minimizer) {
this.view = view;
automatonDrawer = adrawer;
treeDrawer = tdrawer;
this.minimizer = minimizer;
}
/**
* This method should be called whenever a state has the mouse down over it.
*
* @param state
* the state that was under the mouse
* @param event
* the corresponding mouse event
*/
public void stateDown(State state, MouseEvent event) {
if (state == null)
return;
TreeNode[] selected = treeDrawer.getSelected();
if (selected.length != 1)
return;
toggleState((MinimizeTreeNode) selected[0], state);
}
/**
* This method should be called whenever a node is clicked.
*
* @param node
* the node that was under the mouse
* @param event
* the corresponding mouse event
*/
public void nodeClicked(MinimizeTreeNode node, MouseEvent event) {
if (event.isPopupTrigger())
return;
// if (menu.isVisible()) return;
if (node == null) {
automatonDrawer.clearSelected();
treeDrawer.clearSelected();
view.repaint();
setEnabledness();
return;
}
setSelectedStates(node);
setEnabledness();
}
/**
* This method will set the enabledness and the tool tips of items in the
* control panel based on the currently selected tree node.
*/
void setEnabledness() {
TreeNode[] selected = treeDrawer.getSelected();
ControlPanel cp = view.controlPanel;
// We can only proceed if all has been finished.
State[] group = minimizer.getDistinguishableGroup(getAutomaton(),
getTree());
boolean done = expanding == null && group == null;
if (done) {
cp.finishAction.setEnabled(true);
selected = new TreeNode[0];
cp.finishAction.setTip("Proceed to automaton building phase.");
} else {
cp.finishAction.setEnabled(false);
cp.finishAction
.setTip("Can't proceed. Distinguishable groups still exist.");
}
// Actions that require one be selected...
if (selected.length != 1) {
// Can't do anything if not exactly one node is selected.
String s = done ? "Tree is complete. No action needed."
: "This requires one node be selected.";
cp.setTerminalAction.setEnabled(false);
cp.setTerminalAction.setTip(s);
cp.autoPartitionAction.setEnabled(false);
cp.autoPartitionAction.setTip(s);
cp.completeSubtreeAction.setEnabled(false);
cp.autoPartitionAction.setTip(s);
cp.removeAction.setEnabled(false);
cp.autoPartitionAction.setTip(s);
}
// What about checking?
if (expanding == null) {
cp.checkNodeAction.setEnabled(false);
cp.checkNodeAction.setTip("No group is being expanded.");
cp.addChildAction.setEnabled(false);
cp.addChildAction.setTip("No group is being expanded.");
} else {
// We can check!
String es = minimizer.getString(expanding.getStates());
cp.checkNodeAction.setEnabled(true);
cp.checkNodeAction.setTip("Press to check expansion of group " + es
+ ".");
cp.addChildAction.setEnabled(true);
cp.addChildAction.setTip("Add another partition to " + es + ".");
}
if (selected.length != 1)
return;
// Do items related to beginning an expansion.
MinimizeTreeNode node = (MinimizeTreeNode) selected[0];
if (expanding != null && node.getParent() == expanding) {
String es = minimizer.getString(expanding.getStates());
cp.removeAction.setEnabled(true);
cp.removeAction.setTip("Remove this partition from " + es + ".");
} else {
cp.removeAction.setEnabled(false);
cp.removeAction
.setTip("We're not expanding the parent. Cannot delete.");
}
if (expanding == null) {
String completeS = minimizer.getString(node.getStates());
cp.completeSubtreeAction
.setTip("Complete all distinguishable groups descending from group "
+ completeS + ".");
cp.completeSubtreeAction.setEnabled(true);
} else {
String es = minimizer.getString(expanding.getStates());
cp.completeSubtreeAction.setEnabled(false);
cp.completeSubtreeAction.setTip("Must finish group " + es
+ " before we do this.");
}
if (expanding == node) {
cp.setTerminalAction.setEnabled(true);
cp.setTerminalAction
.setTip("Set this group to expand on a different terminal.");
cp.autoPartitionAction.setEnabled(true);
cp.autoPartitionAction
.setTip("Complete the expansion of this group on "
+ node.getTerminal() + ".");
} else if (expanding == null) {
// We're perhaps free to expand.
if (node.getChildCount() == 0) {
cp.setTerminalAction.setEnabled(true);
cp.setTerminalAction
.setTip("Attempt to expand the group on a terminal.");
cp.autoPartitionAction.setEnabled(true);
cp.autoPartitionAction
.setTip("Complete the expansion of this group on some terminal.");
} else {
String s = "This group is already expanded.";
cp.setTerminalAction.setEnabled(false);
cp.setTerminalAction.setTip(s);
cp.autoPartitionAction.setEnabled(false);
cp.autoPartitionAction.setTip(s);
}
} else {
// We're currently expanding, but not the selected node.
String es = minimizer.getString(expanding.getStates());
cp.setTerminalAction.setEnabled(false);
cp.setTerminalAction.setTip("Cannot expand another group while "
+ es + " is in progress.");
cp.autoPartitionAction.setEnabled(false);
cp.autoPartitionAction.setTip("Cannot expand another group while "
+ es + " is in progress.");
return;
}
}
/**
* Splits the node on a terminal.
*
* @param node
* the node to split
* @return true
if the node was able to be split, false
* otherwise
*/
public boolean splitOnTerminal(MinimizeTreeNode node) {
if (!canExpand(node))
return false;
// Is this the root?
if (node.getParent() == null) {
JOptionPane.showMessageDialog(view, "You can't split the root!");
return false;
}
// Remove the children of this node.
MinimizeTreeNode[] children = killChildren(node);
// Can this even be split?
if (!minimizer
.isSplittable(node.getStates(), getAutomaton(), getTree())) {
JOptionPane.showMessageDialog(view, CANT_SPLIT);
addChildren(node, children);
return false;
}
// Get whatever terminal from the user.
String terminal = (String) JOptionPane.showInputDialog(view,
"What terminal?");
if (terminal == null) {
addChildren(node, children);
return false;
}
// Can we expand on this terminal?
if (!minimizer.isSplittableOnTerminal(node.getStates(), terminal,
getAutomaton(), getTree())) {
JOptionPane.showMessageDialog(view, "The group doesn't split "
+ "on that terminal!");
addChildren(node, children);
return false;
}
// Set the terminal.
node.setTerminal(terminal);
// Set the state so the controller knows we're in the process
// of modifying this guy.
expanding = node;
// If it's splittable, then at least two children are
// appropriate.
addChild(node);
addChild(node);
view.repaint();
return true;
}
/**
* This method should be called whenever a node has the mouse pressed on it.
*
* @param node
* the node that was under the mouse
* @param event
* the corresponding mouse event
*/
public void nodeDown(MinimizeTreeNode node, MouseEvent event) {
if (node == null)
return;
}
/**
* Possibly add or remove a state from a node.
*
* @param node
* the node in question
* @param state
* the state to add or remove
*/
public void toggleState(MinimizeTreeNode node, State state) {
if (!canModifyChild((MinimizeTreeNode) node.getParent()))
return;
try {
MinimizeTreeNode parent = (MinimizeTreeNode) node.getParent();
// Are we actually expanding this parent?
// Does the parent even have this state?
if (!Arrays.asList(parent.getStates()).contains(state)) {
JOptionPane.showMessageDialog(view,
"The group being split does not contain state "
+ state.getID() + "!");
return;
}
;
// Do any of the other children already contain it?
TreeNode[] children = Trees.children(parent);
for (int i = 0; i < children.length; i++) {
MinimizeTreeNode child = (MinimizeTreeNode) children[i];
if (child == node)
continue;
Collection c = Arrays.asList(child.getStates());
if (c.contains(state)) {
JOptionPane.showMessageDialog(view,
"Another partition already contains state "
+ state.getID() + "!");
return;
}
}
} catch (NullPointerException e) {
// That was the root.
JOptionPane.showMessageDialog(view,
"One cannot change the states in the root!");
}
// Add/remove the state to/from the list of states.
State[] states = node.getStates();
java.util.List list = new LinkedList(Arrays.asList(states));
if (list.contains(state))
list.remove(state);
else
list.add(state);
states = (State[]) list.toArray(new State[0]);
node.setUserObject(states);
setSelectedStates(node);
view.repaint();
}
/**
* Splits a node on its terminal.
*
* @param node
* the node to split, which must already have a terminal on it
* that can split this group
*/
private void split(MinimizeTreeNode node) {
expanding = node;
// Remove any children that may exist.
killChildren(node);
// Get the states for the correct splittage.
ArrayList groups = minimizer.splitOnTerminal(node.getStates(), node
.getTerminal(), getAutomaton(), getTree());
Iterator it = groups.iterator();
while (it.hasNext())
addChild(node, (State[]) it.next());
expanding = null;
}
/**
* This does the splitting of a state for you, asking for a terminal if
* necessary.
*
* @param node
* the node to split
*/
public void splitWithInput(MinimizeTreeNode node) {
if (!canExpand(node))
return;
if (node.getTerminal().equals("")) {
if (!minimizer.isSplittable(node.getStates(), getAutomaton(),
getTree())) {
JOptionPane.showMessageDialog(view, CANT_SPLIT);
return;
}
// We need to get a terminal for this node!
if (!splitOnTerminal(node))
return;
}
split(node);
}
/**
* This does the splitting of a state for you, providing on its own a
* terminal if necessary.
*
* @param node
* the node to split
*/
public void splitWithoutInput(MinimizeTreeNode node) {
if (!canExpand(node))
return;
if (node.getTerminal().equals("")) {
if (!minimizer.isSplittable(node.getStates(), getAutomaton(),
getTree())) {
JOptionPane.showMessageDialog(view, CANT_SPLIT);
return;
}
// We need to get a terminal for this node!
node.setTerminal(minimizer.getTerminalToSplit(node.getStates(),
getAutomaton(), getTree()));
}
split(node);
}
/**
* This does the splitting of all states in a tree for you.
*
* @param root
* the root of the subtree to split completely
*/
public void splitSubtree(MinimizeTreeNode root) {
if (expanding != null) {
JOptionPane.showMessageDialog(view,
"We must finish expanding group "
+ minimizer.getString(expanding.getStates())
+ "\nbefore we expand anything else.");
}
TreeNode[] children = Trees.children(root);
if (children.length == 0) {
if (!minimizer.isSplittable(root.getStates(), getAutomaton(),
getTree())) {
root.setTerminal("");
return;
}
root.setTerminal(minimizer.getTerminalToSplit(root.getStates(),
getAutomaton(), getTree()));
split(root);
children = Trees.children(root);
}
for (int i = 0; i < children.length; i++)
splitSubtree((MinimizeTreeNode) children[i]);
}
/**
* Changes the selection of states in the state drawer to be those states in
* the given node.
*
* @param node
* the node to get the states from
*/
private void setSelectedStates(MinimizeTreeNode node) {
automatonDrawer.clearSelected();
State[] states = node.getStates();
for (int i = 0; i < states.length; i++)
automatonDrawer.addSelected(states[i]);
treeDrawer.clearSelected();
treeDrawer.setSelected(node, true);
view.repaint();
}
/**
* This method destroys all children of a particular node.
*
* @param node
* the node whose children must be destroyed
* @return the array of children removed
*/
private MinimizeTreeNode[] killChildren(MinimizeTreeNode node) {
TreeNode[] children = Trees.children(node);
MinimizeTreeNode[] toReturn = new MinimizeTreeNode[children.length];
for (int i = 0; i < children.length; i++) {
toReturn[i] = (MinimizeTreeNode) children[i];
getTree().removeNodeFromParent(toReturn[i]);
}
return toReturn;
}
/**
* This method returns the tree this controller, eh, controls.
*
* @return the default tree model
*/
private DefaultTreeModel getTree() {
return (DefaultTreeModel) treeDrawer.getModel();
}
/**
* Returns the automaton that this controller is helping to minimize.
*
* @return the automaton
*/
private FiniteStateAutomaton getAutomaton() {
return (FiniteStateAutomaton) automatonDrawer.getAutomaton();
}
/**
* Adds a child to the currently expanding group.
*/
public MinimizeTreeNode addChild() {
return addChild(expanding);
}
/**
* Adds a empty child to a node.
*
* @param parent
* the node to add a child to
* @return the node that was created, or null
if the node
* could not be created
*/
public MinimizeTreeNode addChild(MinimizeTreeNode parent) {
if (parent.getStates().length <= parent.getChildCount()) {
JOptionPane.showMessageDialog(view,
"A group cannot have more partitions than elements!");
return null;
}
return addChild(parent, new State[0]);
}
/**
* Adds a child to a node with the given group.
*
* @param parent
* the node to add a child to
* @param group
* the group
* @return the node that was created, or null
if the node
* could not be created
*/
public MinimizeTreeNode addChild(MinimizeTreeNode parent, State[] group) {
if (!canModifyChild(parent))
return null;
MinimizeTreeNode node = new MinimizeTreeNode(group);
getTree().insertNodeInto(node, parent, parent.getChildCount());
view.repaint();
return node;
}
/**
* Adds an array of children to a node.
*
* @param parent
* the node to add children to
* @param children
* the child to add
*/
private void addChildren(MinimizeTreeNode parent,
MinimizeTreeNode[] children) {
// Restore the children.
for (int i = 0; i < children.length; i++)
getTree().insertNodeInto((MinimizeTreeNode) children[i], parent,
parent.getChildCount());
}
/**
* Removes a node.
*
* @param node
* the node to remove
*/
public void removeNode(MinimizeTreeNode node) {
MinimizeTreeNode parent = (MinimizeTreeNode) node.getParent();
if (parent == null) {
JOptionPane.showMessageDialog(view, "One can't remove the root!");
return;
}
if (!canModifyChild(parent))
return;
getTree().removeNodeFromParent(node);
view.repaint();
}
/**
* Checks a node, which is by default the expanding group.
*/
public boolean check() {
if (expanding == null)
return false;
return check(expanding);
}
/**
* Checks the splitting on a particular node to see if it is correct.
* Firstly, this will check the node to make sure if it is splittable; if
* not, then it is only correct if it has no children and the terminal is
* empty. If it is splittable, then it is only correct if it has a terminal,
* and if the partitions are correct for that terminal. The user will be
* presented with dialogs to indicate whatever condition was not met, or, if
* they are met, that the node is correct.
*
* @param node
* the node to check
* @return true
if it is correct, false
if it
* is incorrect
*/
public boolean check(MinimizeTreeNode node) {
MinimizeTreeNode[] children = killChildren(node);
// Take care of case where it's not splittable.
if (!minimizer
.isSplittable(node.getStates(), getAutomaton(), getTree())) {
if (node.getTerminal().equals("") && children.length == 0) {
JOptionPane.showMessageDialog(view, "This group is correct!");
addChildren(node, children);
return true;
}
addChildren(node, children);
JOptionPane.showMessageDialog(view,
"This group is unsplittable, so it must\n"
+ "have no terminal, and no partitions.");
return false;
}
// If it is splittable, make sure no subpartitions are empty.
HashSet userPartitions = new HashSet();
for (int i = 0; i < children.length; i++) {
MinimizeTreeNode child = (MinimizeTreeNode) children[i];
if (child.getStates().length == 0) {
addChildren(node, children);
JOptionPane.showMessageDialog(view,
"One of the partitions is empty!");
return false;
}
// While we're at it...
userPartitions.add(new HashSet(Arrays.asList(child.getStates())));
}
// Check to make sure that the partitions are the same.
// Remove any children that may exist.
HashSet realPartitions = new HashSet();
// Remove the children so as not to confuse the minimizer.
// killChildren(node);
ArrayList groups = minimizer.splitOnTerminal(node.getStates(), node
.getTerminal(), getAutomaton(), getTree());
addChildren(node, children);
Iterator it = groups.iterator();
while (it.hasNext())
realPartitions.add(new HashSet(Arrays.asList((State[]) it.next())));
if (!realPartitions.equals(userPartitions)) {
JOptionPane.showMessageDialog(view, "The parititons are wrong!");
return false;
}
JOptionPane.showMessageDialog(view, "The expansion is correct!");
expanding = null;
return true;
}
/**
* Check if we're able to expand some node at this time.
*
* @param node
* the node we may expand
* @return true
if we may expand the node, false
* if we may not
*/
private boolean canExpand(MinimizeTreeNode node) {
if (expanding == null && node.getChildCount() > 0) {
JOptionPane.showMessageDialog(view,
"This group has already been expanded.");
return false;
}
if (expanding == null || expanding == node)
return true;
JOptionPane.showMessageDialog(view,
"We're already expanding the group "
+ minimizer.getString(expanding.getStates()) + "!");
return false;
}
/**
* This should be called if we're seeking to modify the partitions of a
* group in some way. In the event that we cannot, and error message is
* displayed.
*
* @param parent
* the node that holds the group we're modifying partitions of,
* or null
if the child in question is the root of
* the tree
* @return true
if the parent's children can be added
* to/deleted from, or changed in the states they hold, false
* if they should not be
*/
private boolean canModifyChild(MinimizeTreeNode parent) {
if (expanding == parent && parent != null)
return true;
if (parent == null) {
JOptionPane.showMessageDialog(view, "The root cannot be changed!");
return false;
}
JOptionPane.showMessageDialog(view,
"We cannot modify the partitions of a"
+ "\ngroup we're not expanding!"
+ (expanding == null ? "" : "\nWe are expanding group "
+ minimizer.getString(expanding.getStates())));
return false;
}
/**
* Determines if we are finished with building the tree or not. All leaves
* will be examined. If the tree is not finished, a dialog box will be
* popped up saying which group can be expanded further.
*
* @return true
if the tree is done, false
if
* it is not
*/
public boolean finished() {
if (expanding != null) {
JOptionPane.showMessageDialog(view, "We are expanding group "
+ minimizer.getString(expanding.getStates())
+ "\nand so are not done.");
return false;
}
State[] group = minimizer.getDistinguishableGroup(getAutomaton(),
getTree());
if (group == null) {
/*
* JOptionPane.showMessageDialog(view, "The minimize tree is
* complete.");
*/
view.beginMinimizedAutomaton(getAutomaton(), getTree());
return true;
}
JOptionPane.showMessageDialog(view, "The tree is unfinished. Group "
+ minimizer.getString(group) + " may be partitioned.");
return false;
}
/** The component to repaint whenever something cool happens. */
private MinimizePane view;
/** The automaton selection drawer. */
private SelectionDrawer automatonDrawer;
/** The tree selection drawer. */
private SelectTreeDrawer treeDrawer;
/** The DFA minimizer. */
private Minimizer minimizer;
/**
* The current node being expanded, or null if no node is being expanded. If
* this is not null, that could indicate that the tree is quite probably in
* some sort of intermediate state.
*/
private MinimizeTreeNode expanding = null;
/** The "not splittable" message. */
private static final String CANT_SPLIT = "This group cannot be split on any terminal!";
}