RoombaApplet.java

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.
        }
    }
}
Valid HTML 4.01 Valid CSS