/*******************************************************************************
* 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.Stroke;
import plotter.DoubleData;
import plotter.DoubleDataDouble;
/**
* Plots linear XY data.
* Assumes that the X data is sorted.
* @author Adam Crume
*/
public class LinearXYPlotLine extends XYPlotLine implements XYDataset {
private static final long serialVersionUID = 1L;
/** 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 algorithm used to draw the line. */
private LineMode lineMode = LineMode.STRAIGHT;
/** The stroke used to draw the line, or null to use the default. */
private Stroke stroke;
/** Specifies which lines to draw around a missing/invalid point. */
private MissingPointMode missingPointMode = MissingPointMode.NONE;
/** The independent dimension stores data in increasing or decreasing order. May be null for a scatter plot or parametric plot, although this is not supported yet. */
private XYDimension independentDimension;
/**
* Creates a plot line.
* The independent dimension stores data in increasing or decreasing order. May be null for a scatter plot or parametric plot, although this is not supported yet.
* @param xAxis the X axis
* @param yAxis the Y axis
* @param independentDimension the independent dimension
*/
public LinearXYPlotLine(XYAxis xAxis, XYAxis yAxis, XYDimension independentDimension) {
this.xAxis = xAxis;
this.yAxis = yAxis;
this.independentDimension = independentDimension;
}
@Override
protected void paintComponent(Graphics g) {
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);
}
// Skip the points that are outside our clipping area
// TODO: Support a stop index, not just a start index
int index;
if(independentDimension == XYDimension.X) {
int clipx;
if(xstart < xend) {
clipx = g.getClipBounds().x;
} else {
clipx = (int) g.getClipBounds().getMaxX();
}
clipx = toAxisX(clipx) - 1;
double min = xAxis.toLogical(clipx);
index = xData.binarySearch(min);
if(index < 0) {
index = -index - 1;
}
index--;
} else {
int clipy;
if(ystart < yend) {
clipy = (int) g.getClipBounds().getMaxY();
} else {
clipy = (int) g.getClipBounds().getMinY();
}
clipy = toAxisY(clipy) - 1;
double min = yAxis.toLogical(clipy);
index = yData.binarySearch(min);
if(index < 0) {
index = -index - 1;
}
index--;
}
int i = Math.max(0, index);
double xscale = width / (xend - xstart);
double yscale = height / (yend - ystart);
int[] pointsx = new int[(n - i) * 2];
int[] pointsy = new int[pointsx.length];
DoubleData dependentData = independentDimension == XYDimension.Y ? xData : yData;
// Loop through all the points to draw.
outer: while(i < n - 1) {
// Find the first non-NaN point.
while(Double.isNaN(dependentData.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;
if(missingPointMode == MissingPointMode.RIGHT || missingPointMode == MissingPointMode.BOTH) {
if(i > 0 && i > index) {
if(independentDimension == XYDimension.X) {
pointsx[points] = (int) ((xData.get(i - 1) - xstart) * xscale + .5) - 1;
pointsy[points] = y;
points++;
} else if(independentDimension == XYDimension.Y) {
pointsy[points] = height - (int) ((yData.get(i - 1) - ystart) * yscale + .5);
pointsx[points] = x;
points++;
}
}
}
pointsx[points]=x;
pointsy[points]=y;
points++;
i++;
// Add points until we come to the end or a NaN point.
while(i < n) {
if(Double.isNaN(dependentData.get(i))) {
if(missingPointMode == MissingPointMode.BOTH || missingPointMode == MissingPointMode.LEFT) {
if(independentDimension == XYDimension.X) {
double xd = xData.get(i);
int x2 = (int) ((xd - xstart) * xscale + .5) - 1;
pointsx[points] = x2;
pointsy[points] = y;
points++;
} else if(independentDimension == XYDimension.Y) {
double yd = yData.get(i);
int y2 = height - (int) ((yd - ystart) * yscale + .5);
pointsx[points] = x;
pointsy[points] = y2;
points++;
}
}
i++;
break;
}
double xd = xData.get(i);
double yd = yData.get(i);
int x2 = (int) ((xd - xstart) * xscale + .5) - 1;
int y2 = height - (int) ((yd - ystart) * yscale + .5);
if(lineMode == LineMode.STRAIGHT) {
pointsx[points]=x2;
pointsy[points]=y2;
points++;
} else if(lineMode == LineMode.STEP_XY) {
pointsx[points]=x2;
pointsy[points]=y;
points++;
pointsx[points]=x2;
pointsy[points]=y2;
points++;
} else {
pointsx[points]=x;
pointsy[points]=y2;
points++;
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 || pointIcon != null) {
int oldx = 0;
int oldy = 0;
for(i = Math.max(0, index); i < n; i++) {
double xx = xData.get(i);
double yy = yData.get(i);
if(!Double.isNaN(xx) && !Double.isNaN(yy)) {
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);
}
if(pointIcon != null) {
pointIcon.paintIcon(this, g2, 0, 0);
}
oldx = x;
oldy = y;
}
}
}
}
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 line mode used for connecting points.
* @return the line mode used for connecting points
*/
public LineMode getLineMode() {
return lineMode;
}
/**
* Sets the line mode used for connecting points.
* @param lineMode the line mode used for connecting points
*/
public void setLineMode(LineMode lineMode) {
this.lineMode = lineMode;
}
/**
* 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 the missing point mode.
* @return the missing point mode
*/
public MissingPointMode getMissingPointMode() {
return missingPointMode;
}
/**
* Sets the missing point mode.
* @param missingPointMode the missing point mode
*/
public void setMissingPointMode(MissingPointMode missingPointMode) {
this.missingPointMode = missingPointMode;
}
/**
* Returns the independent dimension. May be null.
* @return the independent dimension
*/
@Override
public XYDimension getIndependentDimension() {
return independentDimension;
}
@Override
public void prepend(double[] x, int xoff, double[] y, int yoff, int len) {
xData.prepend(x, xoff, len);
yData.prepend(y, yoff, len);
repaintData(0, len);
}
@Override
public void prepend(DoubleData x, DoubleData y) {
int len = x.getLength();
if(len != y.getLength()) {
throw new IllegalArgumentException("x and y must be the same length. x.length = " + x.getLength() + ", y.length = " + y.getLength());
}
xData.prepend(x, 0, len);
yData.prepend(y, 0, len);
repaintData(0, len);
}
@Override
public int getPointCount() {
return xData.getLength();
}
@Override
public void removeAllPoints() {
xData.removeAll();
yData.removeAll();
repaint();
}
@Override
public void add(double x, double y) {
xData.add(x);
yData.add(y);
repaintData(xData.getLength() - 1);
}
@Override
public void removeFirst(int removeCount) {
repaintData(0, removeCount);
xData.removeFirst(removeCount);
yData.removeFirst(removeCount);
}
@Override
public void removeLast(int count) {
repaintData(xData.getLength() - count, count);
xData.removeLast(count);
yData.removeLast(count);
}
/**
* A line mode specifies how lines are drawn connecting points.
*/
public enum LineMode {
/**
* Straight lines are drawn between points.
*/
STRAIGHT,
/**
* A horizontal line is drawn to the new point's X coordinate, then a vertical line is drawn.
*/
STEP_XY,
/**
* A vertical line is drawn the the new point's Y coordinate, then a horizontal line is drawn.
*/
STEP_YX
}
}