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); } }