package edu.hawaii.ics.yucheng;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlException;
import java.util.Date;
import java.util.Scanner;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
/**
* An Applet that demonstrates an algorithm to optimize the roomba's path
* through a mesh graph of obstacles, priorities, and battery consumptions.
*/
@SuppressWarnings("serial")
public class RoombaApplet extends JApplet {
/**
* A simple class that handles action performed on the cancel button.
*/
class CancelButtonDelegate implements ActionListener {
/**
* Executes when action is performed on the cancel button.
*
* @param e The action event (not used).
*/
public void actionPerformed(final ActionEvent e) {
assert null != myCancelButton;
assert null != myWorker;
myWorker.cancelSolver();
myCancelButton.setEnabled(false);
}
}
/**
* A simple class that handles action performed on the download button.
*/
class DownloadButtonDelegate implements ActionListener {
/**
* Executes when action is performed on the download button.
*
* @param event The action event (not used).
*/
public void actionPerformed(final ActionEvent event) {
assert null != myUrlField;
assert null == myWorker;
assert 0 == myPercent;
assert 0 == myStartTime;
assert 0 == myTotalTimeEstimate;
// Get the entered URL, checking for errors.
final String text = myUrlField.getText();
assert null != text;
// Check for an empty URL string.
if (text.trim().length() == 0) {
beep();
myStatusLabel.setText("Please specify a URL before downloading",
STATUS_TIMEOUT);
return;
}
// Parse the URL.
final URL url;
try {
url = new URL(text);
} catch (final MalformedURLException e) {
beep();
myStatusLabel.setText("Syntactically wrong string (not URL)",
STATUS_TIMEOUT);
return;
}
// Disable the user interface.
setLocked(true);
// Start the background worker to download and solve the graph.
myWorker = new DownloadedGraphWorker(url);
myWorker.start();
}
}
/**
* A class that solves downloaded graphs in a background thread.
*/
class DownloadedGraphWorker extends Worker {
/**
* The URL.
*/
public final URL url;
/**
* Initializes a new instance of the class. The class will download and
* solve the graph at the specified URL if it exists.
*
* @param url The URL.
*/
public DownloadedGraphWorker(final URL url) {
assert null != url;
this.url = url;
}
/**
* Reads and returns a downloaded graph.
*
* @return A graph.
*
* @throws Exception If anything goes wrong reading the graph.
*/
private Graph readGraph() throws IOException {
// Open a stream to the URL location.
final InputStream stream = url.openStream();
final Scanner scanner = new Scanner(stream);
// The scanner can be passed directly into the Graph constructor, but
// here we perform additional error handling. Because this is done only
// to display more meaningful messages to the user, this is done in this
// UI code, not in the Graph class.
final StringBuilder builder = new StringBuilder();
while (scanner.hasNextLine())
builder.append(scanner.nextLine() + "\n");
final String content = builder.toString();
for (int i = 0; i < content.length(); i++)
verifyCharacter(content.charAt(i));
// Read the graph. This should execute quickly since the graph has
// already been downloaded.
return new Graph(new Scanner(content), url);
}
/**
* Solves a graph and returns the solution.
*
* @param solver The graph solver.
*
* @return The solution.
*
* @throws Exception Thrown if there are any errors of any kind.
*/
@Override
protected GraphSolution solve(final GraphSolver solver) throws Exception {
assert null != solver;
// Read the graph, and send progress to the user interface.
sendProgress("Connecting to " + url + "...");
try {
// Read the graph, and send progress to the user interface.
final Graph graph = readGraph();
sendProgress(graph);
// Solve and return the solution.
return solver.solve(graph);
} catch (final GraphException e) {
throw e;
} catch (final FileNotFoundException e) {
throw new GraphException("File doesn't exist at the URL (or denied)", e);
} catch (final AccessControlException e) {
throw new GraphException("File doesn't exist at the URL (or denied)", e);
} catch (final Exception e) {
throw new GraphException("Unable to read graph", e);
}
}
/**
* Verifies the specified character is valid for a graph file.
*
* @param ch The character.
*/
private void verifyCharacter(final char ch) {
// Return if the character is valid.
if ("0123456789 \t\n,x".indexOf(ch) >= 0)
return;
// Check for non-ASCII.
if (ch < 32 || ch > 127)
throw new GraphException("Non-ASCII file detected");
// Check for other invalid characters.
throw new GraphException("Invalid character '" + ch + "' detected");
}
}
/**
* A class that solves random graphs in a background thread.
*/
class RandomGraphWorker extends Worker {
/**
* Solves a graph and returns the solution.
*
* @param solver The graph solver.
*
* @return The solution.
*
* @throws Exception Thrown if there are any errors of any kind.
*/
@Override
protected GraphSolution solve(final GraphSolver solver) throws Exception {
assert null != solver;
// Create the graph and send it to the user interface.
final Graph graph = GraphRandomizer.newGraph();
assert null != graph;
sendProgress(graph);
// Solve and return the solution.
return solver.solve(graph);
}
}
/**
* A simple class that handles action on the randomize button.
*/
class RandomizeButtonDelegate implements ActionListener {
/**
* Executes when the randomize button is clicked.
*
* @param e The action event (not used)
*/
public void actionPerformed(final ActionEvent e) {
assert null == myWorker;
// Disable the user interface.
setLocked(true);
// Start a new worker thread.
myWorker = new RandomGraphWorker();
myWorker.start();
}
}
/**
* A class that handles timer events occuring while the worker is busy.
*/
class TimerDelegate implements ActionListener {
/**
* Executes when the timer expires.
*
* @param e The action event (not used).
*/
public void actionPerformed(final ActionEvent e) {
final StringBuilder builder = new StringBuilder();
builder.append("Solving graph... (" + myPercent + "% complete; ");
builder.append(getElapsed() + " elapsed; ");
builder.append(getRemaining() + " remaining)");
myStatusLabel.setText(builder.toString());
return;
}
/**
* Formats a string as a number, filling it with zeros or clipping it.
*
* @param text The text to format.
*
* @param length The length of the result.
*
* @return A formatted decimal string.
*/
private String format(final String text, final int length) {
assert null != text;
if (text.length() > length)
return text.substring(text.length() - length);
String result = text;
while (result.length() < length)
result = "0" + result;
return result;
}
/**
* Formats a time value based on some number of milliseconds.
*
* @param ms The number of milliseconds.
*
* @return The formatted time.
*/
private String formatTime(final long ms) {
final StringBuilder builder = new StringBuilder();
final long days = ms / 1000 / 60 / 60 / 24;
if (days > 0)
builder.append(days + ":");
final long hours = ms / 1000 / 60 / 60 % 24;
if (days > 0 || hours > 0)
builder.append(format(Long.toString(hours), 2) + ":");
final long minutes = ms / 1000 / 60 % 60;
builder.append(format(Long.toString(minutes), 2) + ":");
final long seconds = ms / 1000 % 60;
builder.append(format(Long.toString(seconds), 2));
return builder.toString();
}
/**
* Returns the elapsed time since the solver started.
*
* @return The time as a formatted string.
*/
private String getElapsed() {
final long elapsed = new Date().getTime() - myStartTime;
return formatTime(elapsed);
}
/**
* Returns the estimated remaining time until the solver finishes.
*
* @return The estimated time as a formatted string.
*/
private String getRemaining() {
if (myPercent == 0)
return "--:--";
final long elapsed = new Date().getTime() - myStartTime;
final long remaining = Math.max(0, myTotalTimeEstimate - elapsed);
return formatTime(remaining);
}
}
/**
* A class that solves graphs in a background thread.
*/
abstract class Worker extends BackgroundWorker<GraphSolution, Object> {
/**
* A simple class that listens for progress messages from the graph solver
* and sends the progress data to the GUI thread.
*/
class Delegate implements GraphListener {
/**
* Executes when progress arrives from the solver.
*
* @param percent The percent complete.
* @param solution The best solution known.
*/
public void progress(final Integer percent, final GraphSolution solution) {
if (percent != null)
sendProgress(percent);
if (solution != null)
sendProgress(solution);
}
}
/** The graph that is being solved. */
private Graph myGraph;
/** The best known solution. */
private GraphSolution mySolution;
/**
* The graph solving object.
*/
private final GraphSolver mySolver = new BruteForceSolver();
/**
* Cancels the background worker solving the graph.
*/
public void cancelSolver() {
mySolver.setCanceled(true);
}
/**
* Processes an error that occurs in the worker thread.
*/
@Override
protected void processError(final Exception error) {
assert null != error;
assert null != myGraphBox;
assert null != myStatusLabel;
// Re-enable the user interface.
myGraphBox.setState(null, null, false, false);
setLocked(false);
// Signal the error.
beep();
myStatusLabel.setText(error.getMessage(), STATUS_TIMEOUT);
System.err.println(error);
}
/**
* Processes integer progress that arrives from the worker.
*
* @param percent The percent complete.
*/
private void processIntegerProgress(final Integer percent) {
assert null != percent;
// No estimate is available if the percent is zero.
myPercent = percent.intValue();
if (myPercent == 0) {
myTotalTimeEstimate = 0;
return;
}
// Calculate the estimated time to completion.
final long elapsed = new Date().getTime() - myStartTime;
myTotalTimeEstimate = elapsed * 100 / myPercent;
}
/**
* Executes when progress messages arrive from the background thread.
*
* @param progress A list of messages that have arrived.
*/
@Override
protected void processProgress(final Object progress) {
assert null != progress;
assert null != myGraphBox;
assert null != myStatusLabel;
// Check if the progress message is a graph.
if (progress instanceof Graph) {
myGraph = (Graph) progress;
myGraphBox.setState(myGraph, null, true, false);
return;
}
// Check if the progress message is a graph solution.
if (progress instanceof GraphSolution) {
mySolution = (GraphSolution) progress;
assert null != myGraphBox;
myGraphBox.setState(myGraph, mySolution, true, false);
return;
}
// Check if the progress message is a percent complete.
if (progress instanceof Integer) {
processIntegerProgress((Integer) progress);
return;
}
// Otherwise, this is a status message.
if (progress instanceof String) {
myStatusLabel.setText((String) progress);
return;
}
assert false;
}
/**
* Executes when the background thread finishes.
*/
@Override
protected void processResult(final GraphSolution result) {
assert null != myGraphBox;
assert null != myStatusLabel;
// Update the graph box state.
assert !(null == myGraph && null != mySolution);
myGraphBox.setState(myGraph, mySolution, false, mySolver.isCanceled());
// Update the status label text.
final String text = mySolver.isCanceled() ? "Canceled." : "Solved.";
myStatusLabel.setText(text, STATUS_TIMEOUT);
// Re-enable the user interface.F
setLocked(false);
return;
}
/**
* The entry point for the background thread.
*
* @return A solution for a graph.
*/
@Override
protected GraphSolution run() throws Exception {
// Monitor progress from the solver, solve the graph, and return the
// solution.
final Delegate delegate = new Delegate();
try {
mySolver.addGraphListener(delegate);
return solve(mySolver);
} finally {
mySolver.removeGraphListener(delegate);
}
}
/**
* Solves a graph and returns the solution.
*
* @param solver The graph solver.
*
* @return The solution.
*
* @throws Exception Thrown if there are any errors of any kind.
*/
protected abstract GraphSolution solve(GraphSolver solver) throws Exception;
}
/**
* Causes the runtime environment to beep if possible.
*/
static void beep() {
try {
Toolkit.getDefaultToolkit().beep();
} catch (final Exception e) {
// Swallow exceptions here.
System.err.println(e);
}
}
/** The cancel button. */
JButton myCancelButton;
/** The download button. */
JButton myDownloadButton;
/** The graph box. */
GraphBox myGraphBox;
/** The percent complete (when a solver is working). */
int myPercent;
/** The randomize button. */
JButton myRandomizeButton;
/** The start time in milliseconds (when a solver is running). */
long myStartTime;
/** The status label. */
StatusLabel myStatusLabel;
/** The timer that runs when a solver is running. */
Timer myTimer;
/** The total time estimate (when a solver is running). */
long myTotalTimeEstimate;
/** The URL text field. */
JTextField myUrlField;
/** The background worker. */
Worker myWorker;
/**
* The amount of time to delay when flashing status.
*/
private final int STATUS_TIMEOUT = 3000;
/**
* Determines the starting URL for the applet.
*
* @return The initial URL or null if no URL is specified.
*/
private String getInitialUrl() {
String initial = null;
String url = getDocumentBase().toString();
int q = url.indexOf("?url=");
if (q >= 0) {
url = url.substring(q + 5);
q = url.indexOf("&");
if (q >= 0)
url = url.substring(0, q);
initial = url;
}
return initial;
}
/**
* Initializes the applet.
*/
@Override
public void init() {
assert null == myTimer;
assert null == myStatusLabel;
assert null == myGraphBox;
assert null == myUrlField;
assert null == myDownloadButton;
assert null == myRandomizeButton;
assert null == myCancelButton;
// Make this applet look as native as possible.
setLookAndFeel();
Container pane = getContentPane();
pane.setBackground(new Color(0xfa, 0xfa, 0xfa));
myTimer = new Timer(250, new TimerDelegate());
setLayout(new AnchorLayoutManager(480, 360));
// Create the title message.
final JLabel titleLabel = new JLabel("Roomba Optimizer");
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 25));
titleLabel.setBounds(10, 10, 460, 40);
add(titleLabel, "TLR");
// Create the status message.
myStatusLabel = new StatusLabel("Ready to optimize graphs...");
myStatusLabel.setDefaultText("Ready to optimize more graphs...");
myStatusLabel.setBounds(10, 50, 460, 20);
add(myStatusLabel, "TLR");
// Create the graph box.
myGraphBox = new GraphBox();
myGraphBox.setBounds(10, 70, 460, 245);
add(myGraphBox, "TLBR");
// Create the URL label.
final JLabel urlField = new JLabel("URL:");
urlField.setBounds(10, 325, 30, 25);
add(urlField, "BL");
// Create the URL text field.
final String defaultUrl = "http://www2.hawaii.edu/~yucheng/coursework/ics-311/assignment-3/implementation/graphs/sample.graph";
final String initial = getInitialUrl();
final String url = initial == null ? defaultUrl : initial;
myUrlField = new JTextField(url);
myUrlField.setBounds(40, 325, 100, 25);
add(myUrlField, "BLR");
// Create the download button.
myDownloadButton = new JButton("Download");
myDownloadButton.setBounds(150, 325, 100, 25);
final DownloadButtonDelegate delegate = new DownloadButtonDelegate();
myDownloadButton.addActionListener(delegate);
add(myDownloadButton, "BR");
// Create the randomize button.
myRandomizeButton = new JButton("Randomize");
myRandomizeButton.setBounds(260, 325, 100, 25);
myRandomizeButton.addActionListener(new RandomizeButtonDelegate());
add(myRandomizeButton, "BR");
// Create the cancel button.
myCancelButton = new JButton("Cancel");
myCancelButton.setBounds(370, 325, 100, 25);
myCancelButton.addActionListener(new CancelButtonDelegate());
myCancelButton.setEnabled(false);
add(myCancelButton, "BR");
// Automatically start when the URL is specified.
if (initial != null)
delegate.actionPerformed(null);
}
/**
* Updates the components to match the specified state (locked or unlocked).
*
* @param locked True indicates the user interface is locked.
*/
void setLocked(final boolean locked) {
assert null != myCancelButton;
assert null != myRandomizeButton;
assert null != myDownloadButton;
assert null != myUrlField;
assert null != myTimer;
myCancelButton.setEnabled(locked);
myRandomizeButton.setEnabled(!locked);
myDownloadButton.setEnabled(!locked);
myUrlField.setEnabled(!locked);
if (locked) {
assert !myTimer.isRunning();
assert null == myWorker;
myTimer.start();
myStartTime = new Date().getTime();
return;
}
assert myTimer.isRunning();
assert null != myWorker;
myTimer.stop();
myStartTime = 0;
myPercent = 0;
myTotalTimeEstimate = 0;
myWorker = null;
}
/**
* Attempts to use the look and feel of the current system.
*/
private void setLookAndFeel() {
try {
final String name = UIManager.getSystemLookAndFeelClassName();
UIManager.setLookAndFeel(name);
SwingUtilities.updateComponentTreeUI(this);
} catch (final ClassNotFoundException e) {
System.err.println(e);
// Swallow exceptions here.
} catch (final InstantiationException e) {
System.err.println(e);
// Swallow exceptions here.
} catch (final IllegalAccessException e) {
System.err.println(e);
// Swallow exceptions here.
} catch (final UnsupportedLookAndFeelException e) {
System.err.println(e);
// Swallow exceptions here.
}
}
}