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