GraphBox.java

package edu.hawaii.ics.yucheng;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;

/**
 * A component that displays a graph and possibly a solution.
 */
@SuppressWarnings("serial")
class GraphBox extends JPanel {

    /**
     * A simple class that handles click events for the information icon.
     */
    class DialogDelegate extends MouseAdapter {

        /**
         * Executes when the click event occurs for the information icon.
         * 
         * @param arg0 The mouse event argument (not used).
         */
        @Override
        public void mouseClicked(final MouseEvent arg0) {

            // Create and display an information frame.
            final InformationFrame frame = new InformationFrame(myGraph, mySolution,
                myIsWorking, myIsCanceled);

            // Show the dialog window.
            frame.setVisible(true);
        }
    }

    /**
     * A class that contains information about the control theme.
     */
    private static class Theme {
        final static Color  ArrivalEdgeColor    = new Color(128, 128, 128);
        final static Stroke ArrivalEdgeStroke   = new BasicStroke(1);
        final static Color  BackgroundColor1    = new Color(64, 64, 64);
        final static Color  BackgroundColor2    = new Color(32, 32, 32);
        final static Color  BorderColor         = new Color(96, 96, 96);
        final static Stroke BorderStroke        = new BasicStroke(1);
        final static Color  DepartureEdgeColor  = new Color(128, 128, 128);
        final static Stroke DepartureEdgeStroke = new BasicStroke(1);
        final static Color  EdgeColor           = new Color(96, 96, 96);
        final static Stroke EdgeStroke          = newEdgeStroke();
        final static Color  FrameColor          = new Color(96, 96, 104);
        final static Stroke FrameStroke         = new BasicStroke(3);
        final static Color  InfoColor           = new Color(192, 192, 192);
        final static Font   InfoFont            = new Font("Arial", Font.PLAIN, 11);
        final static Color  NoGraphLoadedColor  = new Color(96, 96, 96);
        final static Font   NoGraphLoadedFont   = new Font("Arial", Font.PLAIN, 11);
        final static Color  RootColor0          = new Color(96, 96, 96);
        final static Color  RootColor1          = new Color(32, 32, 32);
        final static Color  RootColor2          = new Color(242, 218, 242);
        final static Stroke RootStroke1         = new BasicStroke(4);
        final static Stroke RootStroke2         = new BasicStroke(2);
        final static Font   ScaleFont           = new Font("Arial", Font.PLAIN, 11);
        final static Color  ScaleFontColor      = new Color(192, 192, 192);
        final static Stroke ScaleStroke         = new BasicStroke(2);
        final static Color  ScaleStrokeColor    = new Color(56, 56, 56);
        final static Color  SolutionColor       = new Color(242, 218, 242);
        final static Stroke SolutionStroke      = new BasicStroke(5);
        final static Color  VertexBorderColor   = new Color(56, 56, 56);
        final static Stroke VertexBorderStroke  = new BasicStroke(2);
        final static Font   VertexFont          = new Font("Arial", Font.PLAIN, 11);


        /**
         * Returns the edge stroke, which is more complicated than most theme
         * objects.
         * 
         * @return The edge stroke.
         */
        private static Stroke newEdgeStroke() {
            return new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
                10, new float[] { 3, 3 }, 0);
        }
    }

    /** The graphics resolution per vertex. */
    private static final int RESOLUTION = 100;


    /**
     * Draws text centered in a component.
     * 
     * @param component The component.
     * 
     * @param g The graphics object.
     * 
     * @param text The text.
     */
    private static void drawCentered(final Component component, final Graphics g,
        final String text) {

        assert null != component;

        // Find the center and draw the point.
        final int x = component.getWidth() / 2;
        final int y = component.getHeight() / 2;
        drawCentered(g, text, x, y);
    }


    /**
     * Draws text centered at a location.
     * 
     * @param g The graphics object.
     * 
     * @param text The text.
     * 
     * @param x The horizontal location.
     * 
     * @param y The vertical location.
     */
    private static void drawCentered(final Graphics g, final String text,
        final int x, final int y) {

        assert null != g;
        assert null != text;

        // Get information about the font.
        final FontMetrics metrics = g.getFontMetrics();
        assert null != metrics;

        // Get the size of the rectangle for the text.
        final Rectangle2D rectangle = metrics.getStringBounds(text, g);
        assert null != rectangle;
        final int textHeight = (int) rectangle.getHeight();
        final int textWidth = (int) rectangle.getWidth();

        // Get the left and bottom coordinates for the text.
        final int left = 1 + x - textWidth / 2;
        final int bottom = -1 + y + textHeight / 2;

        // Draw the text.
        g.drawString(text, left, bottom);
    }


    /**
     * Returns the maximum priority in a graph.
     * 
     * @param graph The graph.
     * @return The maximum priority.
     */
    private static int getMaximumPriority(final Graph graph) {
        assert null != graph;

        int value = Integer.MIN_VALUE;
        for (int j = 0; j < graph.height; j++)
            for (int i = 0; i < graph.width; i++)
                if (!graph.base.equals(i, j) && !graph.isObstacle(i, j))
                    value = Math.max(graph.priority(i, j), value);

        return value;
    }


    /**
     * Returns the minimum priority in a graph.
     * 
     * @param graph The graph.
     * @return The minimum priority.
     */
    private static int getMinimumPriority(final Graph graph) {
        assert null != graph;

        int value = Integer.MAX_VALUE;
        for (int j = 0; j < graph.height; j++)
            for (int i = 0; i < graph.width; i++)
                if (!graph.base.equals(i, j) && !graph.isObstacle(i, j))
                    value = Math.min(graph.priority(i, j), value);

        return value;
    }


    /**
     * Returns the blue color component for scaled color.
     * 
     * @param percent The percentage.
     * @return The color component.
     */
    private static double getScaledBlue(final double percent) {
        if (percent < 0.50f)
            return 198 + 2.0f * (0.50f - percent) * 44;
        return 198;
    }


    /**
     * Returns the green color component for scaled color.
     * 
     * @param percent The percentage.
     * @return The color component.
     */
    private static double getScaledGreen(final double percent) {
        if (percent > 0.25f)
            if (percent < 0.5f)
                return 198 + 4.0f * (percent - 0.25f) * 44;
            else if (percent < 0.75f)
                return 198 + 4.0f * (0.75f - percent) * 44;
        return 198;
    }


    /**
     * Returns the red color component for scaled color.
     * 
     * @param percent The percentage.
     * @return The color component.
     */
    private static double getScaledRed(final double percent) {
        if (percent > 0.50f)
            return 198 + 2.0f * (percent - 0.50f) * 44;
        return 198;
    }


    /**
     * Updates the graphics object to use antialiasing.
     * 
     * @param g The graphics object.
     * 
     * @return The graphics object casted to a 2D graphics object.
     */
    private static Graphics2D startAntialiasing(final Graphics g) {
        assert null != g;

        Graphics2D g2d = null;

        // Cast the object safely.
        if (g instanceof Graphics2D)
            try {
                g2d = (Graphics2D) g;

                // Set antialiasing for drawing types.
                final RenderingHints.Key key = RenderingHints.KEY_ANTIALIASING;
                final Object value = RenderingHints.VALUE_ANTIALIAS_ON;
                g2d.addRenderingHints(new RenderingHints(key, value));

                // Set antialiasing for fonts.
                final RenderingHints.Key textKey = RenderingHints.KEY_TEXT_ANTIALIASING;
                final Object textValue = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
                g2d.setRenderingHint(textKey, textValue);

            } catch (final Exception e) {
                // Swallow exceptions here.
                System.err.println(e);
            }

        // Return the same graphics instance casted to a 2D graphics object.
        return g2d;
    }

    /** The graph displayed in the component. */
    Graph                myGraph;

    /** The help icon. */
    private final JLabel myHelpIcon;

    /** A flag that indicates if the solver was canceled. */
    boolean              myIsCanceled;

    /** A flag that indicates if the solver is working. */
    boolean              myIsWorking;

    /** The maximum priority of the graph. */
    private int          myMaxPriority;

    /** The minimum priority in the graph. */
    private int          myMinPriority;

    /** The graph obstacle outline. */
    private GraphOutline myOutline;

    /** The solution displayed in the component. */
    GraphSolution        mySolution;

    /** The status label. */
    private final JLabel myStatusLabel;


    /**
     * Initializes a new instance of the class.
     */
    public GraphBox() {
        super();

        // Create the help icon.
        final ImageIcon icon = new ImageIcon(getClass().getResource("Info.png"));
        final int iconCX = icon.getIconWidth();
        final int iconCY = icon.getIconHeight();

        // Set the layout manager so the components stretch with the box.
        setLayout(new AnchorLayoutManager(75 + iconCX, 15 + iconCY));

        // Create the help icon.
        myHelpIcon = new JLabel(icon);
        myHelpIcon.setName("HelpIcon");
        myHelpIcon.setBounds(10, 10, iconCX, iconCY);
        myHelpIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        myHelpIcon.addMouseListener(new DialogDelegate());
        myHelpIcon.setVisible(false);
        add(myHelpIcon, "BL");

        // Create the status label.
        myStatusLabel = new JLabel("");
        myStatusLabel.setName("StatusLabel");
        myStatusLabel.setBounds(15 + iconCX, 10, 10, iconCY);
        myStatusLabel.setForeground(Theme.InfoColor);
        myStatusLabel.setFont(Theme.InfoFont);
        myStatusLabel.setVisible(false);
        add(myStatusLabel, "BLR");
    }


    /**
     * Returns a color based on a priority.
     * 
     * @param priority The priority.
     * 
     * @return A color.
     */
    private Color getPriorityColor(final float priority) {
        if (myMaxPriority == myMinPriority)
            return getScaledColor(0.5f);

        final float range = myMaxPriority - myMinPriority;
        final float percent = (priority - myMinPriority) / range;
        return getScaledColor(percent);
    }


    /**
     * Returns a color based on a percentage.
     * 
     * @param percent The percentage.
     * 
     * @return A color.
     */
    private Color getScaledColor(final float percent) {
        // Determine the amount of red, green, and blue.
        final double red = getScaledRed(percent);
        final double green = getScaledGreen(percent);
        final double blue = getScaledBlue(percent);

        // Turn the values into a color.
        final int r = Math.min(Math.max(0, (int) red), 255);
        final int g = Math.min(Math.max(0, (int) green), 255);
        final int b = Math.min(Math.max(0, (int) blue), 255);
        return new Color(r, g, b);
    }


    /**
     * Paints the component.
     * 
     * @param g The graphics object.
     */
    @Override
    public void paint(final Graphics g) {
        final Graphics2D g2d = startAntialiasing(g);
        assert null != g2d;

        // Keep track of the old values.
        final Color oldColor = g2d.getColor();
        final Font oldFont = g2d.getFont();
        final Stroke oldStroke = g2d.getStroke();
        final AffineTransform transform = g2d.getTransform();

        // Draw the background.
        g2d.setPaint(new GradientPaint(0, 0, Theme.BackgroundColor1, 0,
            getHeight(), Theme.BackgroundColor2));
        g2d.fillRect(0, 0, getWidth(), getHeight());

        try {
            // Draw a border.
            paintBorder(g2d);

            // Check for no graph loaded.
            if (myGraph == null) {
                paintNoGraph(g2d);
                return;
            }

            // Paint the scale.
            paintScale(g2d);

            // Set the transform.
            scaleAndTranslate(g2d);

            // Paint each transformed component.
            paintFrame(g2d);
            paintEdges(g2d);
            paintVertices(g2d);

        } finally {
            // Always restore the values in the graphics object.
            g2d.setColor(oldColor);
            g2d.setFont(oldFont);
            g2d.setStroke(oldStroke);
            g2d.setTransform(transform);

            // Show the sub components.
            paintComponents(g);
        }
    }


    /**
     * Draws the arrival edges.
     * 
     * @param g The graphics object.
     */
    private void paintArrivalEdges(final Graphics2D g) {
        assert null != g;
        assert null != mySolution;

        g.setStroke(Theme.ArrivalEdgeStroke);
        g.setColor(Theme.ArrivalEdgeColor);
        for (int i = 0; i < mySolution.arrivalLength() - 1; i++) {
            final Vertex v1 = mySolution.arrivalAt(i + 0);
            final Vertex v2 = mySolution.arrivalAt(i + 1);
            paintEdge(g, v1.x, v1.y, v2.x, v2.y);
        }
    }


    /**
     * Draws a border around the component.
     * 
     * @param g The graphics object.
     */
    private void paintBorder(final Graphics2D g) {
        assert null != g;

        g.setColor(Theme.BorderColor);
        g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
    }


    /**
     * Draws the departure edges.
     * 
     * @param g The graphics object.
     */
    private void paintDepartureEdges(final Graphics2D g) {
        assert null != g;
        assert null != mySolution;

        g.setStroke(Theme.DepartureEdgeStroke);
        g.setColor(Theme.DepartureEdgeColor);
        for (int i = 0; i < mySolution.departureLength() - 1; i++) {
            final Vertex v1 = mySolution.departureAt(i + 0);
            final Vertex v2 = mySolution.departureAt(i + 1);
            paintEdge(g, v1.x, v1.y, v2.x, v2.y);
        }
    }


    /**
     * Paints an edge.
     * 
     * @param g The graphics object.
     * 
     * @param x1 The first x coordinate.
     * @param y1 The first x coordinate.
     * @param x2 The second x coordinate.
     * @param y2 The second x coordinate.
     */
    private void paintEdge(final Graphics2D g, final int x1, final int y1,
        final int x2, final int y2) {

        assert null != g;

        final int sx1 = x1 * RESOLUTION + RESOLUTION / 2;
        final int sy1 = y1 * RESOLUTION + RESOLUTION / 2;
        final int sx2 = x2 * RESOLUTION + RESOLUTION / 2;
        final int sy2 = y2 * RESOLUTION + RESOLUTION / 2;

        g.drawLine(sx1, sy1, sx2, sy2);
    }


    /**
     * Paints all edges of the graph and solution.
     * 
     * @param g The graphics object.
     */
    private void paintEdges(final Graphics2D g) {
        assert null != g;

        paintMeshEdges(g);
        if (mySolution == null)
            return;

        paintSolutionEdges(g);
        paintDepartureEdges(g);
        paintArrivalEdges(g);
    }


    /**
     * Draws a frame around the mesh graph.
     * 
     * @param g The graphics object.
     */
    private void paintFrame(final Graphics2D g) {
        assert null != g;
        assert null != myOutline;

        g.setColor(Theme.FrameColor);
        g.setStroke(Theme.FrameStroke);

        myOutline.draw(g);
    }


    /**
     * Paints a single mesh edge.
     * 
     * @param g The graphics object.
     * 
     * @param x1 The first x coordinate.
     * 
     * @param y1 The first y coordinate.
     * 
     * @param x2 The second x coordinate.
     * 
     * @param y2 The second y coordinate.
     */
    private void paintMeshEdge(final Graphics2D g, final int x1, final int y1,
        final int x2, final int y2) {

        assert null != myGraph;

        if (!myGraph.isObstacle(x1, y1) && !myGraph.isObstacle(x2, y2))
            paintEdge(g, x1, y1, x2, y2);
    }


    /**
     * Paints the mesh edges.
     * 
     * @param g The graphics object.
     */
    private void paintMeshEdges(final Graphics2D g) {
        assert null != g;
        assert null != myGraph;

        g.setStroke(Theme.EdgeStroke);
        g.setColor(Theme.EdgeColor);

        // Draw the horizontal lines.
        for (int j = 0; j < myGraph.height; j++)
            for (int i = 0; i < myGraph.width - 1; i++)
                paintMeshEdge(g, i, j, i + 1, j);

        // Draw the vertical lines.
        for (int j = 0; j < myGraph.height - 1; j++)
            for (int i = 0; i < myGraph.width; i++)
                paintMeshEdge(g, i, j, i, j + 1);
    }


    /**
     * Draws the component when no graph is loaded.
     * 
     * @param g The graphics object.
     */
    private void paintNoGraph(final Graphics2D g) {
        assert null != g;

        g.setColor(Theme.NoGraphLoadedColor);
        g.setFont(Theme.NoGraphLoadedFont);
        drawCentered(this, g, "No Graph Loaded");
    }


    /**
     * Draws the scale.
     * 
     * @param g The graphics object.
     */
    private void paintScale(final Graphics2D g) {
        assert null != g;
        assert null != myGraph;

        // If there is no need for a scale, just return.
        if (myMinPriority == Integer.MAX_VALUE)
            return;

        // Get the coordinates.
        final int left = getWidth() - 40;
        final int top = 30;
        final int width = 20;
        final int height = getHeight() - 61;
        final int numDivisions = height;
        final int divisionHeight = (int) ((float) height / numDivisions) + 1;

        // Loop over each division drawing the appropriate color.
        for (int i = 0; i < numDivisions; i++) {
            final float percent = (float) (numDivisions - i) / numDivisions;
            final Color color = getScaledColor(percent);
            final int divisionTop = (int) (top + height * (float) i / numDivisions);
            g.setColor(color);
            g.fillRect(left, divisionTop, width, divisionHeight);
        }

        // Draw a border around the color.
        g.setColor(Theme.ScaleStrokeColor);
        g.setStroke(Theme.ScaleStroke);
        g.drawRoundRect(left, top, width, height, 10, 10);
        g.drawRect(left, top, width, height);

        // Draw the min and max weights.
        g.setColor(Theme.ScaleFontColor);
        g.setFont(Theme.ScaleFont);
        final String min = Integer.toString(myMinPriority);
        final String max = Integer.toString(myMaxPriority);
        drawCentered(g, max, left + width / 2 - 1, top - 12);
        drawCentered(g, min, left + width / 2 - 1, top + height + 10);
    }


    /**
     * Draws the edges of the solution path.
     * 
     * @param g The graphics object.
     */
    private void paintSolutionEdges(final Graphics2D g) {
        assert null != g;
        assert null != mySolution;

        g.setStroke(Theme.SolutionStroke);
        g.setColor(Theme.SolutionColor);
        for (int i = 0; i < mySolution.sequenceLength() - 1; i++) {
            final Vertex v1 = mySolution.sequenceAt(i + 0);
            final Vertex v2 = mySolution.sequenceAt(i + 1);
            paintEdge(g, v1.x, v1.y, v2.x, v2.y);
        }
    }


    /**
     * Draws a single vertex.
     * 
     * @param g The graphics object.
     * 
     * @param x The x coordinate.
     * 
     * @param y The y coordinate.
     */
    private void paintVertex(final Graphics2D g, final int x, final int y) {
        assert null != g;

        // Skip obstacles.
        if (myGraph.isObstacle(x, y))
            return;

        // Determine the priority at this vertex.
        final int priority = myGraph.priority(x, y);

        // Use 20% of the space to draw the oval.
        final int sx = x * RESOLUTION + RESOLUTION * 4 / 10;
        final int sy = y * RESOLUTION + RESOLUTION * 4 / 10;
        final int cx = RESOLUTION * 2 / 10;
        final int cy = RESOLUTION * 2 / 10;

        // Get the color based on the priority or the base.
        final boolean base = myGraph.base.equals(x, y);
        final Color color = base ? Theme.RootColor0 : getPriorityColor(priority);
        g.setColor(color);
        g.fillOval(sx, sy, cx, cy);

        // Draw non-base vertices.
        if (!base) {
            g.setStroke(Theme.VertexBorderStroke);
            g.setColor(Theme.VertexBorderColor);
            g.drawOval(sx, sy, cx, cy);
            return;
        }

        // Draw an extra circle around the base vertex.
        g.setStroke(Theme.RootStroke1);
        g.setColor(Theme.RootColor1);
        g.drawOval(sx, sy, cx, cy);

        final int rx = sx - 3;
        final int ry = sy - 3;
        final int rcx = cx + 6;
        final int rcy = cy + 6;

        g.setStroke(Theme.RootStroke2);
        g.setColor(Theme.RootColor2);
        g.drawOval(rx, ry, rcx, rcy);
    }


    /**
     * Draws the vertices.
     * 
     * @param g The graphics object.
     */
    private void paintVertices(final Graphics2D g) {
        assert null != g;
        assert null != myGraph;

        // Loop over and paint each vertex.
        for (int j = 0; j < myGraph.height; j++)
            for (int i = 0; i < myGraph.width; i++)
                paintVertex(g, i, j);
    }


    /**
     * Scales and translates the graphics object so the graph fits the component.
     * 
     * @param g The graphics object.
     */
    private void scaleAndTranslate(final Graphics2D g) {
        final int resolution = 100;

        // First scale to fit.
        final double zx = Math.max(40, getWidth() - 40);
        final double sx = 1.0 * zx / (myGraph.width * resolution);
        final double sy = 1.0 * getHeight() / (myGraph.height * resolution);
        final double sf = Math.min(sx, sy) * 0.8;
        g.scale(sf, sf);

        // Then center.
        final double cx = sf * myGraph.width * resolution;
        final double cy = sf * myGraph.height * resolution;
        final double mx = 0.5 * (zx - cx);
        final double my = 0.5 * (getHeight() - cy);
        final double tx = mx / sf;
        final double ty = my / sf;
        g.translate(tx, ty);
    }


    /**
     * Sets the display state of the graph box.
     * 
     * @param graph The graph.
     * 
     * @param solution The graph solution.
     * 
     * @param isWorking True indicates the solver is still working.
     * 
     * @param isCanceled True indicates the user canceled the operation.
     */
    public void setState(final Graph graph, final GraphSolution solution,
        final boolean isWorking, final boolean isCanceled) {

        assert !(graph == null && solution != null);
        assert !(isWorking && isCanceled);

        myGraph = graph;
        mySolution = solution;
        myIsCanceled = isCanceled;
        myIsWorking = isWorking;

        if (graph == null) {

            myMinPriority = 0;
            myMaxPriority = 0;
            myOutline = null;
            myOutline = null;

            // Clear the information area when no graph is loaded.
            myStatusLabel.setText("");
            myStatusLabel.setVisible(false);
            myHelpIcon.setVisible(false);

        } else {

            myMinPriority = getMinimumPriority(graph);
            myMaxPriority = getMaximumPriority(graph);
            myOutline = new GraphOutline(graph);
            myOutline = new GraphOutline(graph);

            // Build the text for the status label.
            final StringBuilder builder = new StringBuilder();
            builder.append("Graph is " + myGraph.height + "x" + myGraph.width
                + " mesh;  battery is " + myGraph.capacity + ";  base is ("
                + myGraph.base + ")");

            // Append the solution if it is available.
            if (mySolution != null)
                builder.append(";  sum is " + mySolution.goodness);

            // Append some status text if the solution is canceled or in progress.
            if (isWorking)
                builder.append(";  solving...");
            else if (isCanceled)
                builder.append(";  canceled");

            // Show the information area.
            myStatusLabel.setText(builder.toString());
            myStatusLabel.setVisible(true);
            myHelpIcon.setVisible(true);
        }

        repaint();
    }
}
Valid HTML 4.01 Valid CSS