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