/******************************************************************************* * Mission Control Technologies, Copyright (c) 2009-2012, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * * The MCT platform is licensed under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * MCT includes source code licensed under additional open source licenses. See * the MCT Open Source Licenses file included with this distribution or the About * MCT Licenses dialog available at runtime from the MCT Help menu for additional * information. *******************************************************************************/ package plotter.xy; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.List; import plotter.DoubleData; import plotter.DoubleDataDouble; /** * Plots linear XY data. * @author Adam Crume */ public class ScatterXYPlotLine extends XYPlotLine implements XYDataset { private static final long serialVersionUID = 1L; private static final int DEFAULT_LINES_PER_BOUNDING_BOX = 25; /** The X data. */ private DoubleData xData = new DoubleDataDouble(); /** The Y data. */ private DoubleData yData = new DoubleDataDouble(); /** The X axis, used to retrieve the min and max. */ private XYAxis xAxis; /** The Y axis, used to retrieve the min and max. */ private XYAxis yAxis; /** The stroke used to draw the line, or null to use the default. */ private Stroke stroke; /** Number of line segments per bounding box. */ private int linesPerBoundingBox; /** Number of line segments missing from the beginning of the first bounding box. */ private int boundingBoxOffset; /** Bounding boxes for consecutive groups of line segments. */ private List<Rectangle2D> boundingBoxes = new ArrayList<Rectangle2D>(); /** * Creates a plot line. * @param xAxis the X axis * @param yAxis the Y axis */ public ScatterXYPlotLine(XYAxis xAxis, XYAxis yAxis) { this(xAxis, yAxis, DEFAULT_LINES_PER_BOUNDING_BOX); } /** * Creates a plot line. * @param xAxis the X axis * @param yAxis the Y axis * @param linesPerBoundingBox lines per bounding box, a tuning parameter for clipping */ public ScatterXYPlotLine(XYAxis xAxis, XYAxis yAxis, int linesPerBoundingBox) { this.xAxis = xAxis; this.yAxis = yAxis; this.linesPerBoundingBox = linesPerBoundingBox; } private boolean invariants() { assert 0 <= boundingBoxOffset && boundingBoxOffset < linesPerBoundingBox : "boundingBoxOffset = " + boundingBoxOffset + ", linesPerBoundingBox = " + linesPerBoundingBox; int expectedSize = (xData.getLength() + boundingBoxOffset + linesPerBoundingBox - 1) / linesPerBoundingBox; assert boundingBoxes.size() == expectedSize : "Expected boundingBoxes to be of size " + expectedSize + ", but was " + boundingBoxes.size(); if(!boundingBoxes.isEmpty()) { assert firstIndex(0) == 0 : "firstIndex(0) = " + firstIndex(0); } if(boundingBoxes.size() > 0) { assert firstIndex(1) == linesPerBoundingBox - boundingBoxOffset : "firstIndex(1) = " + firstIndex(1) + ", linesPerBoundingBox = " + linesPerBoundingBox; } return true; } @Override protected void paintComponent(Graphics g) { assert invariants(); int n = xData.getLength(); Graphics2D g2 = (Graphics2D) g; final double xstart = xAxis.getStart(); final double xend = xAxis.getEnd(); final double ystart = yAxis.getStart(); final double yend = yAxis.getEnd(); final int width = getWidth(); final int height = getHeight(); g2.setColor(getForeground()); if(stroke != null) { g2.setStroke(stroke); } // Convert the clipping rectangle to logical coordinates Rectangle clipBounds = g2.getClipBounds(); double xscale = width / (xend - xstart); double yscale = height / (yend - ystart); double xmin = xstart + (clipBounds.getMinX() - 1) / xscale; double xmax = xstart + (clipBounds.getMaxX() + 1) / xscale; double ymin = ystart + (height - clipBounds.getMinY() + 1) / yscale; double ymax = ystart + (height - clipBounds.getMaxY() - 1) / yscale; if(xmin > xmax) { double tmp = xmin; xmin = xmax; xmax = tmp; } if(ymin > ymax) { double tmp = ymin; ymin = ymax; ymax = tmp; } Rectangle2D clipLogical = new Rectangle2D.Double(xmin, ymin, xmax - xmin, ymax - ymin); // Only paint lines in bounding boxes that intersect the logical clipping rectangle int boxCount = boundingBoxes.size(); for(int k = 0; k < boxCount; k++) { Rectangle2D box = boundingBoxes.get(k); if(box != null && box.intersects(clipLogical)) { int index = firstIndex(k); int endIndex = Math.min(n, index + linesPerBoundingBox + 1); int i = index; int[] pointsx = new int[(n - i) * 2]; int[] pointsy = new int[pointsx.length]; // Loop through all the points to draw. outer: while(i < endIndex - 1) { // Find the first non-NaN point. while(Double.isNaN(xData.get(i)) || Double.isNaN(yData.get(i))) { i++; if(i == n) { break outer; } } int x = (int) ((xData.get(i) - xstart) * xscale + .5) - 1; int y = height - (int) ((yData.get(i) - ystart) * yscale + .5); int points = 0; pointsx[points] = x; pointsy[points] = y; points++; i++; // Add points until we come to the end or a NaN point. while(i < endIndex) { double xd = xData.get(i); double yd = yData.get(i); if(Double.isNaN(xd) || Double.isNaN(yd)) { i++; break; } int x2 = (int) ((xd - xstart) * xscale + .5) - 1; int y2 = height - (int) ((yd - ystart) * yscale + .5); pointsx[points] = x2; pointsy[points] = y2; points++; x = x2; y = y2; i++; } if(points > 1) { g2.drawPolyline(pointsx, pointsy, points); } } } } if(pointFill != null || pointOutline != null) { for(int k = 0; k < boxCount; k++) { Rectangle2D box = boundingBoxes.get(k); if(box == null || !box.intersects(clipLogical)) { continue; } int index = firstIndex(k); int endIndex = Math.min(n, index + linesPerBoundingBox + 1); int oldx = 0; int oldy = 0; for(int i = index; i < endIndex; i++) { double xx = xData.get(i); double yy = yData.get(i); if(Double.isNaN(xx) || Double.isNaN(yy)) { continue; } int x = (int) ((xx - xstart) * xscale + .5) - 1; int y = height - (int) ((yy - ystart) * yscale + .5); g2.translate(x - oldx, y - oldy); if(pointFill != null) { g2.fill(pointFill); } if(pointOutline != null) { g2.draw(pointOutline); } oldx = x; oldy = y; } } } assert invariants(); } /** * Returns the index of the first point in this bounding box. * @param boundingBoxIndex bounding box index * @return index of the first point in this bounding box */ private int firstIndex(int boundingBoxIndex) { return Math.max(0, boundingBoxIndex * linesPerBoundingBox - boundingBoxOffset); } private int toAxisX(int x) { // Assumption: plot line is contained in an XYPlotContents, which is contained in an XYPlot. xAxis is contained in the XYPlot. return x + getParent().getX() - xAxis.getX(); } private int toAxisY(int y) { // Assumption: plot line is contained in an XYPlotContents, which is contained in an XYPlot. yAxis is contained in the XYPlot. return y + getParent().getY() - yAxis.getY(); } /** * Repaints a data point and adjoining line segments. * @param index index of the data point */ @Override public void repaintData(int index) { // Implementation note: // We don't call repaintData(int,int) with a count of 1 // because this method gets called a lot (from SimpleXYDataset.add, for example) // so we want it to be fast. // Calculate the bounding box of the line segment(s) that need to be repainted double x = xData.get(index); double y = yData.get(index); XYAxis xAxis = getXAxis(); XYAxis yAxis = getYAxis(); int xmin = xAxis.toPhysical(x); int xmax = xmin; int ymin = yAxis.toPhysical(y); int ymax = ymin; // Take care of the previous point if(index > 0) { int x2p = xAxis.toPhysical(xData.get(index - 1)); if(x2p > xmax) { xmax = x2p; } else if(x2p < xmin) { xmin = x2p; } int y2p = yAxis.toPhysical(yData.get(index - 1)); if(y2p > ymax) { ymax = y2p; } else if(y2p < ymin) { ymin = y2p; } } // Take care of the next point if(index < xData.getLength() - 1) { int x2p = xAxis.toPhysical(xData.get(index + 1)); if(x2p > xmax) { xmax = x2p; } else if(x2p < xmin) { xmin = x2p; } int y2p = yAxis.toPhysical(yData.get(index + 1)); if(y2p > ymax) { ymax = y2p; } else if(y2p < ymin) { ymin = y2p; } } // Adjust for offsets. int xo = toAxisX(0); int yo = toAxisY(0); xmin -= xo; xmax -= xo; ymin -= yo; ymax -= yo; // Add a fudge factor, just to be sure. int fudge = 1; xmin -= fudge; xmax += fudge; ymin -= fudge; ymax += fudge; repaint(xmin, ymin, xmax - xmin, ymax - ymin); } /** * Repaints data points and adjoining line segments. * @param index index of the first data point * @param count number of data points */ @Override public void repaintData(int index, int count) { if(count == 0) { return; } // Calculate the bounding box of the line segment(s) that need to be repainted XYAxis xAxis = getXAxis(); XYAxis yAxis = getYAxis(); double xmin = Double.POSITIVE_INFINITY; double xmax = Double.NEGATIVE_INFINITY; double ymin = Double.POSITIVE_INFINITY; double ymax = Double.NEGATIVE_INFINITY; // Take care of the interior points for(int i = 0; i < count; i++) { double x = xData.get(index + i); double y = yData.get(index + i); if(x > xmax) { xmax = x; } if(x < xmin) { xmin = x; } if(y > ymax) { ymax = y; } if(y < ymin) { ymin = y; } } // Take care of the previous point if(index > 0) { double x = xData.get(index - 1); if(x > xmax) { xmax = x; } if(x < xmin) { xmin = x; } double y = yData.get(index - 1); if(y > ymax) { ymax = y; } if(y < ymin) { ymin = y; } } // Take care of the next point if(index + count < xData.getLength()) { double x = xData.get(index + count); if(x > xmax) { xmax = x; } if(x < xmin) { xmin = x; } double y = yData.get(index + count); if(y > ymax) { ymax = y; } if(y < ymin) { ymin = y; } } int xmin2 = xAxis.toPhysical(xmin); int xmax2 = xAxis.toPhysical(xmax); int ymin2 = yAxis.toPhysical(ymin); int ymax2 = yAxis.toPhysical(ymax); if(xmin2 > xmax2) { int tmp = xmin2; xmin2 = xmax2; xmax2 = tmp; } if(ymin2 > ymax2) { int tmp = ymin2; ymin2 = ymax2; ymax2 = tmp; } // Adjust for offsets. int xo = toAxisX(0); int yo = toAxisY(0); xmin2 -= xo; xmax2 -= xo; ymin2 -= yo; ymax2 -= yo; // Add a fudge factor, just to be sure. int fudge = 1; xmin2 -= fudge; xmax2 += fudge; ymin2 -= fudge; ymax2 += fudge; repaint(xmin2, ymin2, xmax2 - xmin2, ymax2 - ymin2); } /** * Returns the X data. * @return the X data */ @Override public DoubleData getXData() { return xData; } /** * Sets the X data. * @param xData the X data */ public void setXData(DoubleData xData) { this.xData = xData; } /** * Returns the Y data. * @return the Y data */ @Override public DoubleData getYData() { return yData; } /** * Sets the Y data. * @param yData the Y data */ public void setYData(DoubleData yData) { this.yData = yData; } /** * Returns the X axis. * @return the X axis */ public XYAxis getXAxis() { return xAxis; } /** * Returns the Y axis. * @return the Y axis */ public XYAxis getYAxis() { return yAxis; } /** * Returns the stroke used to draw the line. * @return the stroke used to draw the line */ public Stroke getStroke() { return stroke; } /** * Sets the stroke used to draw the line. * @param stroke the stroke used to draw the line */ public void setStroke(Stroke stroke) { this.stroke = stroke; } /** * Returns null. * @return null */ @Override public XYDimension getIndependentDimension() { return null; } @Override public void prepend(double[] x, int xoff, double[] y, int yoff, int len) { assert invariants(); for(int i = len - 1; i >= 0; i--) { double xx = x[xoff + i]; double yy = y[yoff + i]; boundingBoxOffset--; if(boundingBoxOffset == -1) { boundingBoxOffset = linesPerBoundingBox - 1; boundingBoxes.add(0, null); } if(!Double.isNaN(xx) && !Double.isNaN(yy)) { Rectangle2D box = boundingBoxes.get(0); if(box == null) { box = new Rectangle2D.Double(xx, yy, 0, 0); boundingBoxes.set(0, box); } else { box.add(xx, yy); } } if(xData.getLength() > 0) { double xxx = xData.get(0); double yyy = yData.get(0); if(!Double.isNaN(xxx) && !Double.isNaN(yyy)) { Rectangle2D box = boundingBoxes.get(0); if(box == null) { box = new Rectangle2D.Double(xxx, yyy, 0, 0); boundingBoxes.set(0, box); } else { box.add(xxx, yyy); } } } } xData.prepend(x, xoff, len); yData.prepend(y, yoff, len); repaintData(0, len); assert invariants(); } @Override public void prepend(DoubleData x, DoubleData y) { assert invariants(); double firstx = Double.NaN; double firsty = Double.NaN; if(xData.getLength() > 0) { firstx = xData.get(0); firsty = yData.get(0); } int len = x.getLength(); for(int i = len - 1; i >= 0; i--) { double xx = x.get(i); double yy = y.get(i); boundingBoxOffset--; if(boundingBoxOffset == -1) { boundingBoxOffset = linesPerBoundingBox - 1; boundingBoxes.add(0, null); } if(!Double.isNaN(xx) && !Double.isNaN(yy)) { Rectangle2D box = boundingBoxes.get(0); if(box == null) { box = new Rectangle2D.Double(xx, yy, 0, 0); boundingBoxes.set(0, box); } else { box.add(xx, yy); } } if(!Double.isNaN(firstx) && !Double.isNaN(firsty)) { Rectangle2D box = boundingBoxes.get(0); if(box == null) { box = new Rectangle2D.Double(firstx, firsty, 0, 0); boundingBoxes.set(0, box); } else { box.add(firstx, firsty); } } firstx = xx; firsty = yy; } xData.prepend(x, 0, len); yData.prepend(y, 0, len); repaintData(0, len); assert invariants(); } @Override public int getPointCount() { return xData.getLength(); } @Override public void removeAllPoints() { assert invariants(); xData.removeAll(); yData.removeAll(); boundingBoxes.clear(); boundingBoxOffset = 0; repaint(); assert invariants(); } @Override public void add(double x, double y) { assert invariants(); int nPoints = xData.getLength(); int lastBoxSize = nPoints + boundingBoxOffset; while(lastBoxSize >= linesPerBoundingBox) { lastBoxSize -= linesPerBoundingBox; } int n = boundingBoxes.size(); if(lastBoxSize == 0) { if(n > 0 && !Double.isNaN(x) && !Double.isNaN(y)) { Rectangle2D box = boundingBoxes.get(n - 1); if(box == null) { box = new Rectangle2D.Double(x, y, 0, 0); boundingBoxes.set(n - 1, box); } else { box.add(x, y); } } boundingBoxes.add(null); n++; } if(!Double.isNaN(x) && !Double.isNaN(y)) { Rectangle2D box = boundingBoxes.get(n - 1); if(box == null) { box = new Rectangle2D.Double(x, y, 0, 0); boundingBoxes.set(n - 1, box); } else { box.add(x, y); } } xData.add(x); yData.add(y); repaintData(nPoints); assert invariants(); } @Override public void removeFirst(int removeCount) { assert invariants(); repaintData(0, removeCount); boundingBoxOffset += removeCount; while(boundingBoxOffset >= linesPerBoundingBox) { boundingBoxes.remove(0); boundingBoxOffset -= linesPerBoundingBox; } xData.removeFirst(removeCount); yData.removeFirst(removeCount); assert invariants(); } @Override public void removeLast(int count) { assert invariants(); int length = xData.getLength(); repaintData(length - count, count); int boxCount = (length - count + boundingBoxOffset + linesPerBoundingBox - 1) / linesPerBoundingBox; while(boxCount < boundingBoxes.size()) { boundingBoxes.remove(boundingBoxes.size() - 1); } xData.removeLast(count); yData.removeLast(count); assert invariants(); } }