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. } } }