/* * Copyright 2006, United States Government as represented by the Administrator * for the National Aeronautics and Space Administration. No copyright is * claimed in the United States under Title 17, U.S. Code. All Other Rights * Reserved. */ package gov.nasa.ial.mde.ui.graph; import gov.nasa.ial.mde.math.Bounds; import gov.nasa.ial.mde.math.PointXY; import gov.nasa.ial.mde.properties.MdeSettings; import gov.nasa.ial.mde.solver.GraphTrail; import gov.nasa.ial.mde.solver.Solution; import gov.nasa.ial.mde.solver.Solver; import gov.nasa.ial.mde.solver.symbolic.AnalyzedData; import gov.nasa.ial.mde.solver.symbolic.AnalyzedEquation; import gov.nasa.ial.mde.solver.symbolic.AnalyzedItem; import gov.nasa.ial.mde.ui.GraphNavKeys; import gov.nasa.ial.mde.util.MathUtil; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.text.NumberFormat; import javax.swing.JPanel; import org.apache.batik.dom.GenericDOMImplementation; import org.apache.batik.svggen.SVGGraphics2D; import org.apache.batik.svggen.SVGGraphics2DIOException; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; /** * The <code>CartesianGraph</code> class is used to draw the solutions * found by the <code>Solver</code>. * * @author Dan Dexter * @author Dat Truong * @version 1.0 * @since 1.0 * @see gov.nasa.ial.mde.solver.Solver */ public class CartesianGraph extends JPanel { /** * */ private static final long serialVersionUID = 1857799135537915445L; private Solver solver; // To allow for faster rendering of the graph we do our own buffering of the // drawn grid and plotted equation. private BufferedImage bi = null; private Graphics2D big2 = null; private BasicStroke userSpecifiedStroke; private BasicStroke scatterPlotStroke; // The saved path for the graph that is to be drawn. private GeneralPath cachedPath = null; private GeneralPath cachedScatterPlotPath = null; private boolean clearGraph = true; // The left right bottom and top extent of the screen in cartesian coordinates, // which is based on the bounds of the solver. private Bounds solverBounds = new Bounds(); // The previous bounds of this component. private Rectangle prevBounds = getBounds(); private int sideBorderWidth; private int bottomBorderHeight; private Rectangle bounds; private Rectangle graphBounds = getBounds(); private FontMetrics fontMetrics; private Font f; private Color bgColor; private boolean useBlackAndWhiteShade = false; private MdeSettings currentSettings = null; private NumberFormat displayNumberFormat; private boolean use1To1AspectRatio = true; private boolean resetBounds = true; private double traceXaxis = Double.NEGATIVE_INFINITY; private double traceYaxis = Double.NEGATIVE_INFINITY; private boolean simBallEnabled = false; private double simBallXaxis = Double.NEGATIVE_INFINITY; private double simBallYaxis = Double.NEGATIVE_INFINITY; private PointXY simBallNormalVector = new PointXY(0.0, 0.0); private static final int BALL_RADIUS = 4; private static final int BALL_DIAMETER = 2 * BALL_RADIUS; private static final double MAX_VALUE = Double.MAX_VALUE; // was 1000.0 private static final boolean USE_CACHED_GRAPH_DRAWING = true; @SuppressWarnings("unused") private CartesianGraph() { throw new RuntimeException("Default constructor not allowed."); } /** * Creates an instance of <code>CartesianGraph</code> using the specified * solver and settings. * * @param solver The solutions from this solver will be graphed. * @param settings The settings to use for the graph. */ public CartesianGraph(Solver solver, MdeSettings settings) { this.solver = solver; this.currentSettings = settings; this.userSpecifiedStroke = new BasicStroke(currentSettings.getLineSize()); this.scatterPlotStroke = new BasicStroke(3); // This is used to format the numbers displayed on the graph. displayNumberFormat = NumberFormat.getInstance(); displayNumberFormat.setMinimumFractionDigits(1); displayNumberFormat.setMaximumFractionDigits(3); f = new Font("SansSerif", Font.PLAIN, 9); setFont(f); fontMetrics = getFontMetrics(f); sideBorderWidth = fontMetrics.stringWidth("-999.999"); bottomBorderHeight = fontMetrics.getHeight() + 5; bgColor = Color.black; setBackground(bgColor); setPreferredSize(new Dimension(300, 300)); setFocusable(true); GraphNavKeys gnk = new GraphNavKeys(solver, this, graphBounds); addKeyListener(gnk); addMouseListener(gnk); } // end CartesianGraph public Solver getSolver() { return solver; } /** * Create a duplicate of this <code>CartesianGraph</code> instance. * * @return a duplicate of the <code>CartesianGraph</code> instance */ public CartesianGraph duplicate() { CartesianGraph g = new CartesianGraph(solver, currentSettings); g.bounds = new Rectangle(this.bounds); g.graphBounds = new Rectangle(this.graphBounds); g.use1To1AspectRatio = this.use1To1AspectRatio; g.resetBounds = true; return g; } /** * Translate the center of the graph given the specified X and Y offsets. * * @param dx the amount to shift the X-axis by * @param dy the amount to shift the Y-axis by * @see #dilate(double) */ public void translate(double dx, double dy) { // Note: Don't need to call repaint() because the updateSolution method // will fire a change event, which will result in the stateChanged() // method below being called and the graph repainted. D.Dexter 8/20/2003 solver.solve(solver.getLeft()+dx, solver.getRight()+dx, solver.getTop()+dy, solver.getBottom()+dy); } // end translate /** * Dialate (change the size of) the graph given the specified scale factor. * * @param factor the amount to scale the size of the graph by * @see #translate(double,double) */ public void dilate(double factor) { // Note: Don't need to call repaint() because the updateSolution method // will fire a change event, which will result in the stateChanged() // method below being called and the graph repainted. D.Dexter 8/20/2003 solver.solve(solver.getLeft() * factor, solver.getRight() * factor, solver.getTop() * factor, solver.getBottom() * factor); } // end dilate /** * Returns true if the graph will be drawn with a width to height aspect * ratio of one-to-one (1:1). * <p> * With a one-to-one aspect ratio a circle will appear as a perfectly round * circle and not stretched out like an egg. * <p> * The default value for this property is true. * * @return true for a one-to-one aspect ratio, false otherwise. * @see #enableOneToOneAspectRatio(boolean) */ public boolean isOneToOneAspectRatio() { return use1To1AspectRatio; } /** * If true the graph will be drawn with a width to height aspect ratio * of one-to-one (1:1). * <p> * With a one-to-one aspect ratio a circle will appear as a perfectly round * circle. Otherwise the graph will be drawn using the the current size of the * <code>CartesianGraph</code> component and a circle could appear to be stretched * out in an egg shape because the aspect ration may not be one-to-one. * <p> * The default value for this property is true. * * @param enable true to enable a one-to-one aspect ratio, false to use the full size * of the <code>CartesianGraph</code> component for drawing. * @see #isOneToOneAspectRatio() */ public void enableOneToOneAspectRatio(boolean enable) { // Determine if we need to reset the bounds. Reset the bounds the // aspect ratio flag has changed. if (enable != use1To1AspectRatio) { resetBounds = true; } use1To1AspectRatio = enable; // If the bounds need to be reset then repaint the display. if (resetBounds) { repaint(); } } /** * Returns true if the graph will be drawn using black and white with shades of gray. * Returns false if the graph will be in color. * <p> * The default value for this property is false. * * @return true if the graph will be in black and white, false otherwise for a color graph. * @see #enableBlackAndWhite(boolean) */ public boolean isBlackAndWhite() { return useBlackAndWhiteShade; } /** * If true the graph will be drawn using black and white with shades of gray. * If false the graph will be in color. * <p> * The default value for this property is false. * * @param useBlackWhiteShade true for a black and white graph, false for a color graph * @see #isBlackAndWhite() */ public void enableBlackAndWhite(boolean useBlackWhiteShade) { this.useBlackAndWhiteShade = useBlackWhiteShade; } /** * Clears the graph which will result in the sonification trace and simulation * ball indicators being cleared, the one-to-one aspect ratio is disabled, and * the AWT Thread will be notified to repaint the <code>CartesianGraph</code> component. * * @see #paintComponent(Graphics) */ public void clearGraph() { clearGraph = true; nullifyCachedPaths(); traceXaxis = Double.NEGATIVE_INFINITY; traceYaxis = Double.NEGATIVE_INFINITY; simBallXaxis = Double.NEGATIVE_INFINITY; simBallYaxis = Double.NEGATIVE_INFINITY; enableOneToOneAspectRatio(false); repaint(); } /** * Draws a graph of the solutions from the solver. The AWT Thread will be * notified to repaint the <code>CartesianGraph</code> component. */ public void drawGraph() { clearGraph = false; resetBounds = true; nullifyCachedPaths(); repaint(); } /** * Draws a graph of the solutions from the solver using the specified * settings. The settings will be updated to use those specified and then * the <code>drawGraph()</code> method is called. * * @param settings the MDE settings. * @see #drawGraph() */ public void drawGraph(MdeSettings settings) { this.currentSettings = settings; drawGraph(); } /** * Draws a graph of the solutions from the solver to the specified image. * * @param graphImage the image to draw the graph to */ public void drawGraphToImage(Image graphImage) { // Rest the flags to ensure we draw the graph from scratch. clearGraph = false; resetBounds = true; nullifyCachedPaths(); if (graphImage instanceof BufferedImage) { paintComponent(((BufferedImage)graphImage).createGraphics()); } else { paintComponent(graphImage.getGraphics()); } // Reset the flags so that the next time we draw we start from scratch. clearGraph = false; resetBounds = true; nullifyCachedPaths(); } /** * Draws a trace at the given X and Y coordinates if the trace is enabled in * the <code>MdeSettings</code>. If the solution is polar then a ball will * be drawn at the given coordinates, otherwise a vertical line will be drawn * at the given Y cordinate. * * @param x the X coordinate of the trace * @param y the Y coordinate of the trace * @see gov.nasa.ial.mde.properties.MdeSettings */ public void drawTrace(double x, double y) { if (currentSettings.showTrace()) { traceXaxis = x; traceYaxis = y; repaint(); } } /** * Returns true if the simulation ball is enabled. * <p> * The default value for this property is false. * * @return true if the simulation ball is enabled, false otherwise * @see #setSimulationBallEnabled(boolean) * @see #drawSimulationBall(double,double,double,double) */ public boolean isSimulationBallEnabled() { return simBallEnabled; } /** * If true the simulation ball will be drawn. If false the simulation ball * will not be drawn. The AWT Thread will be notified to repaint the * <code>CartesianGraph</code> component. * <p> * The default value for this property is false. * * @param enable true to enable/show the simulation ball, false to disable/hide it * @see #isSimulationBallEnabled() * @see #drawSimulationBall(double,double,double,double) */ public void setSimulationBallEnabled(boolean enable) { if (enable != simBallEnabled) { simBallEnabled = enable; // Clear the ball location if we transition to a disabled state. if (!simBallEnabled) { simBallXaxis = Double.NEGATIVE_INFINITY; simBallYaxis = Double.NEGATIVE_INFINITY; } repaint(); } } /** * Draws the simulation ball if it is enabled at the specified X and Y * coordinates and normal vector. In addition the AWT Thread will be * notified to repaint the <code>CartesianGraph</code> component. * <p> * The normal vector is used to allow the ball to roll along the given * point. * * @param x the X coordinate of the ball location * @param y the Y coordinate of the ball location * @param xNormal the X-axis component of the normal vector for the ball * @param yNormal the Y-axis component of the normal vector for the ball * @see #isSimulationBallEnabled() * @see #setSimulationBallEnabled(boolean) */ public void drawSimulationBall(double x, double y, double xNormal, double yNormal) { if (simBallEnabled) { this.simBallXaxis = x; this.simBallYaxis = y; this.simBallNormalVector.x = xNormal; this.simBallNormalVector.y = yNormal; repaint(); } } /** * Paints the <code>CartesianGraph</code> component. * * @see javax.swing.JComponent#paintComponent(java.awt.Graphics) */ public String getSVG() { setupBounds(); // Get a DOMImplementation. DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation(); // Create an instance of org.w3c.dom.Document. String svgNS = "http://www.w3.org/2000/svg"; Document document = domImpl.createDocument(svgNS, "svg", null); // Create an instance of the SVG Generator. SVGGraphics2D g2 = new SVGGraphics2D(document); if ((cachedPath == null) || !USE_CACHED_GRAPH_DRAWING) { setupGraph(g2); graphData(g2); } // Enable antialiasing g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Use the user specified line size for the stroke. if (userSpecifiedStroke.getLineWidth() != currentSettings.getLineSize()) { userSpecifiedStroke = new BasicStroke(currentSettings.getLineSize()); } g2.setStroke(userSpecifiedStroke); // Draw the sonification trace. if (currentSettings.showTrace()) { drawSonificationTrace(g2); } // Draw the simulation Ball. if (simBallEnabled) { // Use the normal vector to determine the offset of the ball. // Note: The ball radius in pixels must be converted into real numbers // of X and Y that is why we scale it by the screen and graph diminsions. drawSimulationBall(g2); } // Disable antialiasing g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); // Finally, stream out SVG to the standard output using // UTF-8 encoding. boolean useCSS = true; // we want to use CSS style attributes String result = null; Writer out; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { out = new OutputStreamWriter(baos, "UTF-8"); g2.stream(out, useCSS); result = new String(baos.toByteArray(), "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (SVGGraphics2DIOException e) { e.printStackTrace(); } return result; } // end paintComponent private void setupBounds() { // Reset the graph bounds if the solver bounds have changed. if (!solverBounds.equals(solver.getBounds())) { resetBounds = true; } // Update our cached solver bounds. solverBounds.setBounds(solver.getBounds()); // Update the graph bounds if the bounds have changed or need to be reset. bounds = getBounds(); if (resetBounds || !bounds.equals(prevBounds)) { resetBounds = false; prevBounds = bounds; graphBounds.x = bounds.x; graphBounds.y = bounds.y; graphBounds.width = bounds.width - sideBorderWidth; graphBounds.height = bounds.height - bottomBorderHeight; nullifyCachedPaths(); } // Force the graph to a 1:1 aspect ratio by adjusting the graph bounds. if (use1To1AspectRatio) { // Now adjust to the bounds of the data to be drawn. double deltaWidth = (solverBounds.right - solverBounds.left) / graphBounds.width; double deltaHeight = (solverBounds.top - solverBounds.bottom) / graphBounds.height; if (deltaWidth < deltaHeight) { int newWidth = (int)Math.round(graphBounds.width * (deltaWidth / deltaHeight)); if (graphBounds.width != newWidth) { graphBounds.width = newWidth; nullifyCachedPaths(); } } else if (deltaHeight < deltaWidth) { int newHeight = (int)Math.round(graphBounds.height * (deltaHeight / deltaWidth)); if (graphBounds.height != newHeight) { graphBounds.height = newHeight; nullifyCachedPaths(); } } } } private void drawSonificationTrace(SVGGraphics2D g2) { int traceXaxisPixel = x2pix(traceXaxis); if ((traceXaxisPixel >= 0) && (traceXaxisPixel <= graphBounds.width)) { // Use the Polar graph trace only if we have one polar graph to // sonify and no other graphs to sonify. if ((solver.getSonifyPolarCount() == 1) && (solver.getSonifyCartesianCount() == 0)) { int traceYaxisPixel = y2pix(traceYaxis); if (traceYaxisPixel == 0) { traceYaxisPixel = 1; // <== TODO: Why is this needed????? } // Draw a blue ball with a line around it. double x = (traceXaxisPixel >= 0) ? traceXaxisPixel - 3 : traceXaxisPixel + 3; double y = (traceYaxisPixel >= 0) ? traceYaxisPixel - 3 : traceYaxisPixel + 3; g2.setColor(Color.blue); g2.fill(new Ellipse2D.Double(x, y, BALL_DIAMETER, BALL_DIAMETER)); // Moving Object Simulation (circle) g2.setColor(Color.white); g2.draw(new Ellipse2D.Double(x, y, BALL_DIAMETER, BALL_DIAMETER)); } else { g2.setColor(Color.white); drawLine(traceXaxisPixel, graphBounds.height, traceXaxisPixel, 0, g2); } } } /** * Paints the <code>CartesianGraph</code> component. * * @param g the <code>Graphics</code> context to paint to * @see javax.swing.JComponent#paintComponent(java.awt.Graphics) */ public void paintComponent(Graphics g) { // Reset the graph bounds if the solver bounds have changed. setupBounds(); Graphics2D g2; if ((cachedPath == null) || !USE_CACHED_GRAPH_DRAWING) { if (USE_CACHED_GRAPH_DRAWING) { if ((bi == null) || (bi.getWidth() != bounds.width) || (bi.getHeight() != bounds.height)) { bi = (BufferedImage)this.createImage(bounds.width, bounds.height); if (bi != null) { big2 = bi.createGraphics(); if (big2 == null) { big2 = (Graphics2D)g; } } else { big2 = (Graphics2D)g; } if (big2 != null) { // Make sure we use the current font. big2.setFont(getFont()); } } g2 = big2; // Draw to the graphics 2D object of the buffered image. } else { g2 = (Graphics2D)g; } setupGraph(g2); // Don't graph the data if the clearGraph flag is set and the // path is still cached. if (!clearGraph && (cachedPath == null)) { graphData(g2); } } // Now use the 2D graphics for the component. g2 = (Graphics2D)g; if (USE_CACHED_GRAPH_DRAWING) { // Draw the cached graph image buffer. g2.drawImage(bi, 0, 0, this); // Clip any drawing outside of our graph bounds. g2.setClip(0, 0, graphBounds.width, graphBounds.height); } // Enable antialiasing g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Use the user specified line size for the stroke. if (userSpecifiedStroke.getLineWidth() != currentSettings.getLineSize()) { userSpecifiedStroke = new BasicStroke(currentSettings.getLineSize()); } g2.setStroke(userSpecifiedStroke); // Draw the sonification trace. if (currentSettings.showTrace()) { int traceXaxisPixel = x2pix(traceXaxis); if ((traceXaxisPixel >= 0) && (traceXaxisPixel <= graphBounds.width)) { // Use the Polar graph trace only if we have one polar graph to // sonify and no other graphs to sonify. if ((solver.getSonifyPolarCount() == 1) && (solver.getSonifyCartesianCount() == 0)) { int traceYaxisPixel = y2pix(traceYaxis); if (traceYaxisPixel == 0) { traceYaxisPixel = 1; // <== TODO: Why is this needed????? } // Draw a blue ball with a line around it. double x = (traceXaxisPixel >= 0) ? traceXaxisPixel - 3 : traceXaxisPixel + 3; double y = (traceYaxisPixel >= 0) ? traceYaxisPixel - 3 : traceYaxisPixel + 3; g2.setColor(Color.blue); g2.fill(new Ellipse2D.Double(x, y, BALL_DIAMETER, BALL_DIAMETER)); // Moving Object Simulation (circle) g2.setColor(Color.white); g2.draw(new Ellipse2D.Double(x, y, BALL_DIAMETER, BALL_DIAMETER)); } else { g2.setColor(Color.white); drawLine(traceXaxisPixel, graphBounds.height, traceXaxisPixel, 0, g2); } } } // Draw the simulation Ball. if (simBallEnabled) { drawSimulationBall(g2); } // Disable antialiasing g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); } // end paintComponent private void drawSimulationBall(Graphics2D g2) { // Use the normal vector to determine the offset of the ball. // Note: The ball radius in pixels must be converted into real numbers // of X and Y that is why we scale it by the screen and graph diminsions. double pointX = simBallXaxis + simBallNormalVector.x * BALL_RADIUS * (solverBounds.right - solverBounds.left) / graphBounds.width; double pointY = simBallYaxis + simBallNormalVector.y * BALL_RADIUS * (solverBounds.top - solverBounds.bottom) / graphBounds.height; // Don't use x2pix() or y2pix() because they will clip the values. // The range of the ball position is checked before it is drawn. int ballXaxisPixel = (int) (((pointX - solverBounds.left) / (solverBounds.right - solverBounds.left)) * graphBounds.width); //x2pix(x); int ballYaxisPixel = (int) (((solverBounds.top - pointY) / (solverBounds.top - solverBounds.bottom)) * graphBounds.height); //y2pix(y); // Draw the ball only if any part of it is visible. if ((ballXaxisPixel >= -BALL_RADIUS) && (ballXaxisPixel <= graphBounds.width + BALL_RADIUS) && (ballYaxisPixel >= -BALL_RADIUS) && (ballYaxisPixel <= graphBounds.height + BALL_RADIUS)) { if ((ballYaxisPixel == 0) && (solver.size() == 1)) { // NOTE: We only support one equation/data item for the simulation. Solution solution = solver.get(0); AnalyzedItem analyzedItem = solution.getAnalyzedItem(); if ((analyzedItem instanceof AnalyzedEquation) && ((AnalyzedEquation)analyzedItem).isPolar()) { ballYaxisPixel = 1; // <== Why is this needed????? } } int xBall = ballXaxisPixel - BALL_RADIUS; int yBall = ballYaxisPixel - BALL_RADIUS; // Moving Object Simulation (ball) g2.setColor(Color.red); g2.fill(new Ellipse2D.Double(xBall, yBall, BALL_DIAMETER, BALL_DIAMETER)); // Moving Object Simulation (outline of ball) g2.setColor(Color.white); g2.draw(new Ellipse2D.Double(xBall, yBall, BALL_DIAMETER, BALL_DIAMETER)); } } private void setupGraph(Graphics2D g2) { // Reset the clip region to the size of our bounds. g2.setClip(0, 0, bounds.width, bounds.height); if (useBlackAndWhiteShade) { g2.setColor(Color.white); g2.fillRect(0, 0, bounds.width, bounds.height); g2.setColor(Color.black); } else { g2.setColor(currentSettings.getBackgroundColor()); g2.fillRect(0, 0, bounds.width, bounds.height); g2.setColor(currentSettings.getAxisColor()); } // Axis lines use a basic solid line that is 1 pixel wide. g2.setStroke(new BasicStroke(1)); int xAxisPixel = -1; if ((solver.getLeft() < 0.0) && (solver.getRight() > 0.0)) { xAxisPixel = x2pix(0.0); drawLine(xAxisPixel, graphBounds.height, xAxisPixel, 0, g2); } // end if int yAxisPixel = -1; if ((solver.getTop() > 0.0) && (solver.getBottom() < 0.0)) { yAxisPixel = y2pix(0.0); drawLine(0, yAxisPixel, graphBounds.width, yAxisPixel, g2); } // end if // Grid uses dashed lines. BasicStroke dashedStroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.0f, new float[] { 1.0f }, 0.0f); g2.setStroke(dashedStroke); String axisLbl; int w,xi,yi; double delta = MathUtil.findDelta(solverBounds.right - solverBounds.left); double lastValue = solverBounds.right + 0.5 * delta; int labelYPos = graphBounds.height + bottomBorderHeight - fontMetrics.getMaxDescent(); // Draw the X axis labels. for (double x = delta * Math.ceil(solverBounds.left / delta); x < lastValue; x += delta) { xi = x2pix(x); if (Math.abs(xi - xAxisPixel) > 3) { if (useBlackAndWhiteShade) { g2.setColor(Color.lightGray); } else { g2.setColor(currentSettings.getGridColor()); } drawLine(xi, 0, xi, graphBounds.height, g2); } axisLbl = displayDouble(x); if (useBlackAndWhiteShade) { g2.setColor(Color.darkGray); } else { g2.setColor(currentSettings.getAxisColor()); } g2.drawString(axisLbl, xi, labelYPos); } // end for x delta = MathUtil.findDelta(solverBounds.top - solverBounds.bottom); lastValue = solverBounds.top + 0.5 * delta; // Draw the Y axis labels. for (double y = delta * Math.ceil(solverBounds.bottom / delta); y < lastValue; y += delta) { yi = y2pix(y); if (Math.abs(yi - yAxisPixel) > 3) { if (useBlackAndWhiteShade) { g2.setColor(Color.lightGray); } else { g2.setColor(currentSettings.getGridColor()); } drawLine(0, yi, graphBounds.width, yi, g2); } axisLbl = displayDouble(y); w = (int)(((fontMetrics.getHeight() + 5.0) * (graphBounds.height - yi)) / graphBounds.height); if (useBlackAndWhiteShade) { g2.setColor(Color.darkGray); } else { g2.setColor(currentSettings.getAxisColor()); } g2.drawString(axisLbl, graphBounds.width, yi + w - fontMetrics.getMaxDescent()); } // end for y // Clip any drawing outside of our graph bounds. g2.setClip(0, 0, graphBounds.width, graphBounds.height); } private void graphData(Graphics2D g2) { int w; cachedPath = new GeneralPath(GeneralPath.WIND_EVEN_ODD); cachedScatterPlotPath = null; Solution solution; AnalyzedData analyzedData; GraphTrail[] graphTrails; PointXY[] points; double[] xData,yData; int i,numPoints,numTrails; int xp,yp,xdrawn,ydrawn,leftIndex,rightIndex; int numSolutions = solver.size(); // Draw each of the solutions. for (int solutionIndex = 0; solutionIndex < numSolutions; solutionIndex++) { solution = solver.get(solutionIndex); graphTrails = solution.getGraphTrails(); if (solution.isShowGraph() && (graphTrails != null)) { numTrails = graphTrails.length; for (i = 0; i < numTrails; i++) { points = graphTrails[i].getPoints(); numPoints = (points != null) ? points.length : 0; if (numPoints < 2) { continue; } // Move to the first point in the trail. xp = x2pix(points[0].x); yp = y2pix(points[0].y); cachedPath.moveTo(xp,yp); // Draw the first line segment and setup the xdrawn and ydrawn values. xp = x2pix(points[1].x); yp = y2pix(points[1].y); cachedPath.lineTo(xp,yp); xdrawn = xp; ydrawn = yp; // Draw the remaining line segments. for (w = 2; w < numPoints; w++) { xp = x2pix(points[w].x); yp = y2pix(points[w].y); // Draw the line if we have not drawn it to this point before. // This will exclude duplicates. if ((xp != xdrawn) || (yp != ydrawn)) { cachedPath.lineTo(xp,yp); xdrawn = xp; ydrawn = yp; } } // Move to the last point drawn and close the path. cachedPath.moveTo(xdrawn,ydrawn); cachedPath.closePath(); } // Generate the real data scatter-plot path. if (currentSettings.isDataPointsShown() && (solution.getAnalyzedItem() instanceof AnalyzedData)) { analyzedData = (AnalyzedData)solution.getAnalyzedItem(); xData = analyzedData.getXValues(); yData = analyzedData.getYValues(); leftIndex = analyzedData.getLeftIndexBound(); rightIndex = analyzedData.getRightIndexBound(); if ((xData != null) && (yData != null) && (rightIndex >= 0) && (leftIndex <= rightIndex)) { if (cachedScatterPlotPath == null) { cachedScatterPlotPath = new GeneralPath(GeneralPath.WIND_EVEN_ODD); } // Draw the first point and setup the xdrawn and ydrawn values. xp = x2pix(xData[leftIndex]); yp = y2pix(yData[leftIndex]); cachedScatterPlotPath.moveTo(xp,yp); cachedScatterPlotPath.lineTo(xp,yp); xdrawn = xp; ydrawn = yp; // Draw the remaining points. for (i = leftIndex+1; i <= rightIndex; i++) { xp = x2pix(xData[i]); yp = y2pix(yData[i]); // Draw the point if we have not drawn it before. // This will exclude duplicates. if ((xp != xdrawn) || (yp != ydrawn)) { cachedScatterPlotPath.moveTo(xp,yp); cachedScatterPlotPath.lineTo(xp,yp); xdrawn = xp; ydrawn = yp; } } // Move to the last point drawn and close the path. cachedScatterPlotPath.moveTo(xdrawn,ydrawn); cachedScatterPlotPath.closePath(); } } } } if (userSpecifiedStroke.getLineWidth() != currentSettings.getLineSize()) { userSpecifiedStroke = new BasicStroke(currentSettings.getLineSize()); } g2.setStroke(userSpecifiedStroke); // Set the line color to use for the plotted data. if (useBlackAndWhiteShade) { g2.setColor(Color.black); } else { g2.setColor(currentSettings.getLineColor()); } // Enable antialiasing g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Draw the line path. g2.draw(cachedPath); // Disable antialiasing g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); // Draw the scatter plot path. if ((cachedScatterPlotPath != null) && currentSettings.isDataPointsShown()) { // Draw the real-data points as a dot with an odd width that is larger // than the line width used to line-plot the model data. int lineWidth = Math.max(3,(2 * currentSettings.getLineSize()) - 1); if (scatterPlotStroke.getLineWidth() != lineWidth) { scatterPlotStroke = new BasicStroke(lineWidth); } g2.setStroke(scatterPlotStroke); if (useBlackAndWhiteShade) { g2.setColor(Color.black); } else { g2.setColor(currentSettings.getDataPointColor()); } g2.draw(cachedScatterPlotPath); } } private void drawLine(int x1, int y1, int x2, int y2, Graphics2D g2) { g2.draw(new Line2D.Float(x1, y1, x2, y2)); } // end drawLine private void nullifyCachedPaths() { cachedPath = null; cachedScatterPlotPath = null; } private int x2pix(double x) { // We need to do clipping to avoid a known Java2D bug caused by drawing // lines for large coordinates with AntiAliasing turn on. DDexter 12/8/2003 if (x < solverBounds.left) { return -1; } if (x > solverBounds.right) { return bounds.width + 1; } return (int)(((x - solverBounds.left) / (solverBounds.right - solverBounds.left)) * graphBounds.width); } // end x2pix private int y2pix(double y) { // We need to do clipping to avoid a known Java2D bug caused by drawing // lines for large coordinates with AntiAliasing turn on. DDexter 12/8/2003 if (y > solverBounds.top) { return -1; } if (y < solverBounds.bottom) { return bounds.height + 1; } return (int)(((solverBounds.top - y) / (solverBounds.top - solverBounds.bottom)) * graphBounds.height); } // end y2pix private String displayDouble(double x) { if (x < -MAX_VALUE) { return "-infinity"; } if (x > MAX_VALUE) { return "infinity"; } return displayNumberFormat.format(x); } // end displayDouble } // end class CartesianGraph