/*******************************************************************************
* Breakout Cave Survey Visualizer
*
* Copyright (C) 2014 James Edwards
*
* jedwards8 at fastmail dot fm
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*******************************************************************************/
package com.andork.plot;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
/**
* Decides what to actually draw to a trace renderer. When the viewport is
* zoomed out so far that there are multiple data points for each column of
* pixels, special logic is required to draw the trace efficiently without
* losing peaks and valleys. Here's the trick in a nutshell: For each one pixel
* line on the screen, find all of the data points that map to within that line.
* If they're monotonically increasing or decreasing, just draw the last point.
* This preserves peaks and valleys well. Otherwise, draw a fill from the last
* point (or last fill points) to the min and max values within the one pixel
* line.
*/
public class DefaultTracePlotter implements ITracePlotter {
private final ArrayList<Point2D> linePoints = new ArrayList<Point2D>();
private final ArrayList<Point2D> fillMinPoints = new ArrayList<Point2D>();
private final ArrayList<Point2D> fillMaxPoints = new ArrayList<Point2D>();
private boolean columnIsMonotonic;
private double columnStartDomain;
private double columnStartValue;
private double columnMinDomain;
private double columnMinValue;
private double columnMaxDomain;
private double columnMaxValue;
private double columnEndDomain;
private double columnEndValue;
private double lastColumnEndDomain;
private double lastColumnEndValue;
public DefaultTracePlotter() {
reset();
}
/*
* (non-Javadoc)
*
* @see ITracePlotter#addPoint(ITraceRenderer, ViewportParams, double,
* double)
*/
@Override
public void addPoint(double domain, double value, ITraceRenderer traceRenderer, IAxisConversion domainConversion,
IAxisConversion valueConversion) {
double columnX = domainConversion.convert(columnStartDomain);
double viewX = domainConversion.convert(domain);
if (!Double.isNaN(columnStartDomain) && Math.round(viewX) > Math.round(columnX)) {
advanceColumn(traceRenderer, domainConversion, valueConversion, false);
}
if (Double.isNaN(columnStartDomain)) {
columnStartDomain = domain;
columnStartValue = value;
}
columnEndDomain = domain;
columnEndValue = value;
if (!Double.isNaN(value)) {
if (columnMinDomain < columnMaxDomain && value < columnMaxValue) {
columnIsMonotonic = false;
}
if (columnMinDomain > columnMaxDomain && value > columnMinValue) {
columnIsMonotonic = false;
}
if (Double.isNaN(columnMinValue) || value < columnMinValue) {
columnMinDomain = domain;
columnMinValue = value;
}
if (Double.isNaN(columnMaxValue) || value > columnMaxValue) {
columnMaxDomain = domain;
columnMaxValue = value;
}
}
}
private void advanceColumn(ITraceRenderer traceRenderer, IAxisConversion domainConversion,
IAxisConversion valueConversion, boolean forceFlush) {
double lastX = domainConversion.convert(lastColumnEndDomain);
double lastY = valueConversion.convert(lastColumnEndValue);
double startX = domainConversion.convert(columnStartDomain);
double startY = valueConversion.convert(columnStartValue);
double minX = domainConversion.convert(columnMinDomain);
double minY = valueConversion.convert(columnMinValue);
double maxX = domainConversion.convert(columnMaxDomain);
double maxY = valueConversion.convert(columnMaxValue);
double monoStartX = Double.NaN;
double monoStartY = Double.NaN;
double monoEndX = Double.NaN;
double monoEndY = Double.NaN;
if (columnIsMonotonic) {
if (minX < maxX) {
monoStartX = minX;
monoStartY = minY;
monoEndX = maxX;
monoEndY = maxY;
} else {
monoStartX = maxX;
monoStartY = maxY;
monoEndX = minX;
monoEndY = minY;
}
}
boolean adjacent = (int) roundNaN(startX) == (int) roundNaN(lastX) + 1;
boolean startEqualsLast = roundNaN(startX) == roundNaN(lastX) && roundNaN(startY) == roundNaN(lastY);
boolean monoStartEqualsLast = roundNaN(monoStartX) == roundNaN(lastX)
&& roundNaN(monoStartY) == roundNaN(lastY);
boolean drawStart = !Double.isNaN(startY) && !startEqualsLast;
if (!Double.isNaN(lastY)) {
if (!fillMinPoints.isEmpty()) {
if (!columnIsMonotonic && adjacent) {
fillMinPoints.add(new Point2D.Double(minX, minY));
fillMaxPoints.add(new Point2D.Double(maxX, maxY));
} else {
Point2D p = new Point2D.Double(lastX, lastY);
fillMinPoints.add(p);
fillMaxPoints.add(p);
flushFill(traceRenderer);
linePoints.add(p);
}
}
if (fillMinPoints.isEmpty()) {
if (drawStart) {
linePoints.add(new Point2D.Double(startX, startY));
}
if (columnIsMonotonic) {
if (!drawStart && !monoStartEqualsLast) {
linePoints.add(new Point2D.Double(monoStartX, monoStartY));
}
linePoints.add(new Point2D.Double(monoEndX, monoEndY));
} else {
flushLine(traceRenderer);
if (drawStart) {
Point2D p = new Point2D.Double(startX, startY);
fillMinPoints.add(p);
fillMaxPoints.add(p);
}
fillMinPoints.add(new Point2D.Double(minX, minY));
fillMaxPoints.add(new Point2D.Double(maxX, maxY));
}
}
} else {
flushLine(traceRenderer);
flushFill(traceRenderer);
if (columnIsMonotonic) {
if (!monoStartEqualsLast) {
linePoints.add(new Point2D.Double(monoStartX, monoStartY));
}
linePoints.add(new Point2D.Double(monoEndX, monoEndY));
} else {
if (drawStart) {
Point2D p = new Point2D.Double(startX, startY);
fillMinPoints.add(p);
fillMaxPoints.add(p);
}
fillMinPoints.add(new Point2D.Double(minX, minY));
fillMaxPoints.add(new Point2D.Double(maxX, maxY));
}
}
if (forceFlush) {
flushLine(traceRenderer);
flushFill(traceRenderer);
}
lastColumnEndDomain = columnEndDomain;
lastColumnEndValue = columnEndValue;
resetColumn();
}
/*
* (non-Javadoc)
*
* @see ITracePlotter#flush(ITraceRenderer, ViewportParams)
*/
@Override
public void flush(ITraceRenderer traceRenderer, IAxisConversion domainConversion, IAxisConversion valueConversion) {
advanceColumn(traceRenderer, domainConversion, valueConversion, true);
}
private void flushFill(ITraceRenderer traceRenderer) {
if (fillMinPoints.size() > 1 && fillMaxPoints.size() > 1) {
Path2D.Double minLinePath = new Path2D.Double();
Path2D.Double maxLinePath = new Path2D.Double();
Path2D.Double fillPath = new Path2D.Double();
Point2D p = fillMinPoints.get(0);
minLinePath.moveTo(p.getX(), p.getY());
fillPath.moveTo(p.getX(), p.getY());
for (int i = 1; i < fillMinPoints.size(); i++) {
p = fillMinPoints.get(i);
minLinePath.lineTo(p.getX(), p.getY());
fillPath.lineTo(p.getX(), p.getY());
}
p = fillMaxPoints.get(fillMaxPoints.size() - 1);
maxLinePath.moveTo(p.getX(), p.getY());
fillPath.lineTo(p.getX(), p.getY());
for (int i = fillMaxPoints.size() - 2; i >= 0; i--) {
p = fillMaxPoints.get(i);
maxLinePath.lineTo(p.getX(), p.getY());
fillPath.lineTo(p.getX(), p.getY());
}
fillPath.closePath();
try {
traceRenderer.drawFill(fillPath);
traceRenderer.drawLine(maxLinePath);
traceRenderer.drawLine(minLinePath);
} catch (Exception e) {
e.printStackTrace();
}
}
fillMinPoints.clear();
fillMaxPoints.clear();
}
public void flushLine(ITraceRenderer traceRenderer) {
if (linePoints.size() > 1) {
Path2D linePointsPath = new Path2D.Double();
Point2D p = linePoints.get(0);
linePointsPath.moveTo(p.getX(), p.getY());
for (int i = 1; i < linePoints.size(); i++) {
p = linePoints.get(i);
linePointsPath.lineTo(p.getX(), p.getY());
}
traceRenderer.drawLine(linePointsPath);
}
linePoints.clear();
}
/*
* (non-Javadoc)
*
* @see ITracePlotter#reset()
*/
@Override
public void reset() {
linePoints.clear();
fillMinPoints.clear();
fillMaxPoints.clear();
resetColumn();
lastColumnEndDomain = Double.NaN;
lastColumnEndValue = Double.NaN;
}
private void resetColumn() {
columnIsMonotonic = true;
columnStartDomain = Double.NaN;
columnStartValue = Double.NaN;
columnMinDomain = Double.NaN;
columnMinValue = Double.NaN;
columnMaxDomain = Double.NaN;
columnMaxValue = Double.NaN;
columnEndDomain = Double.NaN;
columnEndValue = Double.NaN;
}
private double roundNaN(double a) {
return Double.isNaN(a) ? Double.NaN : Math.round(a);
}
}