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