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.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.util.ArrayList;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

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

    /**
     * A class that shows information about the loaded graph through a popup
     * window.
     */
    class Delegate implements MouseListener {
        /**
         * Executes when the mouse button is clicked on the info icon.
         * 
         * @param e The mouse event (not used)
         */
        public void mouseClicked(final MouseEvent e) {
            // Build a frame for the popup window.
            final JFrame frame = new JFrame();
            frame.setTitle("Graph Details");
            frame.setSize(640, 480);

            // Center the popup window in the screen.
            final Toolkit toolkit = Toolkit.getDefaultToolkit();
            final Dimension size = toolkit.getScreenSize();
            final int x = (size.width - 640) / 2;
            final int y = (size.height - 480) / 2;
            frame.setLocation(x, y);

            // Create a text area for the graph details.
            final JTextArea area = new JTextArea();
            area.setLineWrap(true);
            area.setWrapStyleWord(true);
            area.setCaretPosition(0);
            frame.add(new JScrollPane(area));

            // Add the graph detail.
            final StringBuilder builder = new StringBuilder();
            if (myGraph.url() == null)
                builder.append("This graph was generated randomly.  ");
            else {
                builder.append("This graph was downloaded from:\n\n");
                builder.append(myGraph.url());
                builder.append("\n\n");
            }
            builder.append("This graph contains ");
            builder.append(myGraph.vertices());
            if (myGraph.vertices() != 1)
                builder.append(" vertices and ");
            else
                builder.append(" vertex and ");
            builder.append(myGraph.edges());
            if (myGraph.edges() != 1)
                builder.append(" edges.");
            else
                builder.append(" edge.");
            builder.append("  The vertex weights are as follows:\n\n");
            for (int i = 0; i < myGraph.vertices(); i++) {
                builder.append("[");
                builder.append(i + 1);
                builder.append("]\t");
                builder.append(myGraph.vertexAt(i));
                builder.append("\n");
            }
            if (myGraph.edges() > 0) {
                builder.append("\nThe undirected edges are as follows:\n\n");
                for (int i = 0; i < myGraph.edges(); i++) {
                    builder.append("[");
                    builder.append(i + 1);
                    builder.append("]\t");
                    builder.append(myGraph.edgeAt(i));
                    builder.append("\n");
                }
            }
            if (mySolution == null || !mySolution.hasRoot())
                builder.append("\nThis graph has not yet been solved.");
            else {
                builder.append("\nThis graph has been solved.  The root is at ");
                builder.append("index ");
                builder.append(mySolution.root() + 1);
                builder.append(".  The weight of this graph is ");
                builder.append(mySolution.weight());
                builder.append(".");
                if (myGraph.edges() > 0) {
                    builder.append("  The edges in the spanning tree that minimize ");
                    builder.append("weight are as follows:\n\n");
                    int i = 1;
                    for (final Edge edge : mySolution) {
                        builder.append("[");
                        builder.append(i++);
                        builder.append("]\t");
                        builder.append(edge);
                        builder.append("\n");
                    }
                }
            }
            area.setText(builder.toString());
            area.setCaretPosition(0);

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


        public void mouseEntered(final MouseEvent e) {
            // Do nothing.
        }


        public void mouseExited(final MouseEvent e) {
            // Do nothing.
        }


        public void mousePressed(final MouseEvent e) {
            // Do nothing.
        }


        public void mouseReleased(final MouseEvent e) {
            // Do nothing.
        }
    }

    /**
     * A collection of colors, fonts, and strokes.
     */
    private static class Theme {
        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 Color  EdgeColor          = new Color(128, 128, 128);
        final static Stroke EdgeStroke         = new BasicStroke(1,
                                                   BasicStroke.CAP_BUTT,
                                                   BasicStroke.JOIN_MITER, 10,
                                                   new float[] { 3, 3 }, 0);
        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  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(3);
        final static Color  TextColor          = new Color(32, 32, 32);
        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);
    }

    /**
     * The maximum number of edges displayed.
     */
    private static final int MAX_DISPLAYED_EDGES = 500;


    /**
     * 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();

        // Get the size of the rectangle for the text.
        final Rectangle2D rectangle = metrics.getStringBounds(text, g);
        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);
    }


    /**
     * Formats the weight value as text.
     * 
     * @param weight The weight.
     * 
     * @return A String for the weight.
     */
    private static String formatWeight(final float weight) {
        final DecimalFormat format = new DecimalFormat("0.##");
        final String text = format.format(weight);
        return text;
    }


    /**
     * 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;
    }

    private Dimension              myCachedSize;

    /** The displayed graph. */
    Graph                          myGraph;

    private final JLabel           myHelpIcon;
    private float                  myMaxWeight;
    private float                  myMinWeight;

    /** The displayed graph solution. */
    GraphSolution                  mySolution;

    private final JLabel           myStatusLabel;
    private Dimension              myVertexSize;
    private final ArrayList<Point> myXY;


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

        // Initialize some class data.
        myXY = new ArrayList<Point>();
        myCachedSize = null;

        // Load an info icon.
        final ImageIcon icon = new ImageIcon(getClass().getResource("Info.png"));

        // Do not use a layout manager.
        final int iconCX = icon.getIconWidth();
        final int iconCY = icon.getIconHeight();
        setLayout(new AnchorLayoutManager(35 + iconCX, 15 + iconCY));

        // Create a label to show the info icon.
        myHelpIcon = new JLabel(icon);
        myHelpIcon.setName("HelpIcon");
        myHelpIcon.setBounds(10, 10, iconCX, iconCY);
        myHelpIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        myHelpIcon.addMouseListener(new Delegate());
        myHelpIcon.setVisible(false);
        add(myHelpIcon, "BL");

        // Create a label to display the status.
        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");
    }


    /**
     * Computes the drawing information based on the assigned graph.
     */
    private void computeVertexLocations() {

        // Clear the vertex locations.
        myXY.clear();

        // That's all to do if there are no vertices.
        if (myGraph == null)
            return;

        // Get the number of vertices.
        final int numVertices = myGraph.vertices();
        assert numVertices > 0;

        // Get information about the size of the viewport.
        final double cx = getWidth();
        final double cy = getHeight();
        myCachedSize = getSize();

        // Compute the diameter of the vertex circle.
        final int computedDiameter = (int) (Math.min(cx, cy) * Math.PI
            / numVertices / 4);
        final int diameter = Math.min(Math.max(10, computedDiameter), 50);
        myVertexSize = new Dimension(diameter, diameter);

        // Place the vertices in an oval around the center of the viewport.
        for (int i = 0; i < myGraph.vertices(); i++) {
            final double percent = (double) i / numVertices;
            final double rangeX = 0.9 * cx / 2 - (diameter + 30);
            final double rangeY = 0.9 * cy / 2 - (diameter + 0);
            final double theta = 2.0 * Math.PI * (percent + 0.75);
            final double computedX = rangeX * Math.cos(theta);
            final double computedY = rangeY * Math.sin(theta);
            final int x = (int) (computedX + cx / 2.0);
            final int y = (int) (computedY + cy / 2.0);
            final Point point = new Point(x, y);
            myXY.add(point);
        }

        // Determine the minimum and maximum weight.
        myMinWeight = Float.MAX_VALUE;
        myMaxWeight = Float.MIN_VALUE;
        for (int i = 0; i < numVertices; i++) {
            myMinWeight = Math.min(myMinWeight, myGraph.vertexAt(i));
            myMaxWeight = Math.max(myMaxWeight, myGraph.vertexAt(i));
        }
    }


    /**
     * Returns a color based on a weight.
     * 
     * @param weight The weight.
     * 
     * @return A color.
     */
    private Color getScaledColor(final float weight) {
        final float percent = (weight - myMinWeight) / (myMaxWeight - myMinWeight);
        return getScaledColorByPercent(percent);
    }


    /**
     * Returns a color based on a percentage.
     * 
     * @param percent The percentage.
     * 
     * @return A color.
     */
    private Color getScaledColorByPercent(final float percent) {
        final double min = 198;
        final double range = 44;

        // Determine the amount of red.
        double red = min;
        if (percent > 0.50f)
            red += 2.0f * (percent - 0.50f) * range;

        // Determine the amount of green.
        double green = min;
        if (percent > 0.25f)
            if (percent < 0.5f)
                green += 4.0f * (percent - 0.25f) * range;
            else if (percent < 0.75f)
                green += 4.0f * (0.75f - percent) * range;

        // Determine the amount of blue.
        double blue = min;
        if (percent < 0.50f)
            blue += 2.0f * (0.50f - percent) * range;

        // 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) {
        // Refresh the XY locations if necessary.
        if (myCachedSize == null)
            computeVertexLocations();

        // Use antialiased graphics.
        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();

        // 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 each component.
            paintEdges(g2d);
            paintVertices(g2d);
            paintScale(g2d);

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

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


    /**
     * 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 edges.
     * 
     * @param g The graphics object.
     */
    private void paintEdges(final Graphics2D g) {
        assert null != g;

        // Draw the edges.
        if (myGraph.edges() < MAX_DISPLAYED_EDGES) {
            g.setStroke(Theme.EdgeStroke);
            g.setColor(Theme.EdgeColor);
            for (final Edge edge : myGraph) {
                final Point from = myXY.get(edge.first());
                final Point to = myXY.get(edge.second());
                g.drawLine(from.x, from.y, to.x, to.y);
            }
        }

        // Draw the edges as part of the solution.
        if (mySolution != null) {
            g.setStroke(Theme.SolutionStroke);
            g.setColor(Theme.SolutionColor);
            for (final Edge edge : mySolution) {
                final Point first = myXY.get(edge.first());
                final Point second = myXY.get(edge.second());
                g.drawLine(first.x, first.y, second.x, second.y);
            }
        }
    }


    /**
     * 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;

        // 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 = getScaledColorByPercent(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 = formatWeight(myMinWeight);
        final String max = formatWeight(myMaxWeight);
        drawCentered(g, max, left + width / 2 - 1, top - 12);
        drawCentered(g, min, left + width / 2 - 1, top + height + 10);
    }


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

        // Loop over each vertex.
        for (int i = 0; i < myXY.size(); i++) {

            // Get the coordinates and color.
            final float weight = myGraph.vertexAt(i);
            final Point point = myXY.get(i);
            final int left = point.x - myVertexSize.width / 2;
            final int top = point.y - myVertexSize.height / 2;
            final Color color = getScaledColor(weight);

            // Draw the background oval.
            g.setColor(color);
            g.fillOval(left, top, myVertexSize.width, myVertexSize.height);

            // Draw the outline in as either the root or not.
            if (mySolution != null && mySolution.hasRoot() && mySolution.root() == i) {
                g.setStroke(Theme.RootStroke1);
                g.setColor(Theme.RootColor1);
                g.drawOval(left, top, myVertexSize.width, myVertexSize.height);
                g.setStroke(Theme.RootStroke2);
                g.setColor(Theme.RootColor2);
                g.drawOval(left - 2, top - 2, myVertexSize.width + 4,
                    myVertexSize.height + 4);
            } else {
                g.setStroke(Theme.VertexBorderStroke);
                g.setColor(Theme.VertexBorderColor);
                g.drawOval(left, top, myVertexSize.width, myVertexSize.height);
            }

            // Draw the weight in the vertex if it is large enough.
            if (myVertexSize.width >= 30) {
                final String text = formatWeight(weight);
                g.setColor(Theme.TextColor);
                g.setFont(Theme.VertexFont);
                drawCentered(g, text, point.x, point.y);
            }
        }
    }


    /**
     * Resets the graph.
     */
    public void reset() {

        // Clear the graph.
        myGraph = null;
        mySolution = null;
        myXY.clear();

        // This will change the information area.
        updateInformationArea();

        // Repaint immediately.
        repaint();
    }


    /**
     * Moves and resizes this component to conform to the new bounding rectangle
     * r. This component's new position is specified by r.x and r.y, and its new
     * size is specified by r.width and r.height
     * 
     * @param r the new bounding rectangle for this component
     */
    @Override
    public void setBounds(final Rectangle r) {
        super.setBounds(r);
        computeVertexLocations();
    }


    /**
     * Sets the graph to display.
     * 
     * @param g The graph.
     */
    public void setGraph(final Graph g) {
        assert null != g;

        // Assign the new graph, and clear the cache of points.
        myGraph = g;
        myXY.clear();
        mySolution = null;

        // This will change the information area.
        updateInformationArea();

        // Repaint immediately.
        repaint();
    }


    /**
     * Sets the solution.
     * 
     * @param solution
     */
    public void setSolution(final GraphSolution solution) {

        // Assign the new solution.
        mySolution = solution;

        // This will change the information area.
        updateInformationArea();

        // Repaint immediately.
        repaint();
    }


    /**
     * Sets the information area.
     */
    private void updateInformationArea() {

        // First case: No graph is loaded.
        if (myGraph == null) {
            myStatusLabel.setText("");
            myStatusLabel.setVisible(false);
            myHelpIcon.setVisible(false);
            return;
        }

        // Build text to display in the information area.
        final StringBuilder builder = new StringBuilder();
        builder.append(myGraph.vertices());
        builder.append(myGraph.vertices() == 1 ? " vertex;  " : " vertices;  ");
        builder.append(myGraph.edges());
        builder.append(myGraph.edges() == 1 ? " edge" : " edges");

        // Display additional information if there is a solution.
        if (mySolution != null) {
            if (mySolution.hasRoot()) {
                builder.append(";  root is index ");
                builder.append(mySolution.root() + 1);
            }
            if (mySolution.hasWeight()) {
                builder.append(";  weight is ");
                builder.append(formatWeight(mySolution.weight()));
            }
        }

        myStatusLabel.setText(builder.toString());
        myStatusLabel.setVisible(true);
        myHelpIcon.setVisible(true);
    }
}
Valid HTML 4.01 Valid CSS