package jas.hist;
import jas.plot.CoordinateTransformation;
import jas.plot.DateCoordinateTransformation;
import jas.plot.DoubleCoordinateTransformation;
import jas.plot.OverlayContainer;
import jas.plot.PlotGraphics;
import java.awt.Color;
import java.awt.Image;
import java.awt.image.ColorModel;
import java.awt.image.ImageConsumer;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
/**
* Horrendously complicated class for painting scatterplots. It is designed
* to deal with scatter plots containing up to about a million points, and
* uses Java's Image classes to deal with drawing the image reasonably
* efficiently. The class is designed to allow the plot to be redrawn efficiently
* when more points are added to the scatter plot (a common occurence).
*
* The class is also designed to deal reasonably with the scatter plot being
* resized.
*/
class ScatterOverlay extends TwoDOverlay implements ImageObserver
{
private Image imageCache = null;
private Image newImage = null;
private JASHist2DScatterData parent;
private boolean async = false;
ScatterOverlay(JASHist2DScatterData parent)
{
super(parent);
this.parent = parent;
}
public void containerNotify(OverlayContainer c)
{
if (c == null)
{
Image image = imageCache;
if (image != null)
{
((ScatterImage) image.getSource()).abort();
}
image = newImage;
if (image != null)
{
((ScatterImage) image.getSource()).abort();
}
}
super.containerNotify(c);
}
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height)
{
//Attention, probably run in ImageProducer thread
//System.out.println("image="+img+" infoflags="+Integer.toHexString(infoflags));
if (img == imageCache)
{
if ((infoflags & ABORT) != 0)
{
imageCache = null;
}
else if ((infoflags & FRAMEBITS) != 0)
{
container.repaint();
//System.out.println("REPAINT");
}
}
else if (img == newImage)
{
if ((infoflags & ABORT) != 0)
{
newImage = null;
}
}
return true;
}
public void paint(PlotGraphics g, boolean isPrinting)
{
async = !isPrinting;
JASHistScatterPlotStyle style = (JASHistScatterPlotStyle) parent.style;
if (style.getDisplayAsScatterPlot())
{
CoordinateTransformation xp = container.getXTransformation();
final CoordinateTransformation yp = container.getYTransformation();
if (xp instanceof DateCoordinateTransformation)
{
xp = new DateTransformationConverter((DateCoordinateTransformation) xp);
}
if (xp instanceof DoubleCoordinateTransformation && yp instanceof DoubleCoordinateTransformation)
{
final DoubleCoordinateTransformation xt = (DoubleCoordinateTransformation) xp;
final DoubleCoordinateTransformation yt = (DoubleCoordinateTransformation) yp;
final double x1 = xt.convert(xt.getPlotMin());
final double x2 = xt.convert(xt.getPlotMax());
final double y1 = yt.convert(yt.getPlotMin());
final double y2 = yt.convert(yt.getPlotMax());
final int height = (int) (y1 - y2);
final int width = (int) (x2 - x1);
//Image cache is the "current" image, if it doesnt exist
//create it.
Image current = imageCache;
if (current == null)
{
current = container.createImage(new ScatterImage(width, height, xt, yt));
imageCache = current;
g.drawImage(current, x1, y2, this);
//System.out.println("Imagecache created"+imageCache);
}
else
{
int oldWidth = current.getWidth(null);
int oldHeight = current.getHeight(null);
if ((oldWidth == width) && (oldHeight == height))
{
// Size unchanged, simple paint
//System.out.println("Draw "+imageCache+" unscaled");
g.drawImage(current, x1, y2, this);
}
// height, width will be negative if the ImageProducer has
// not filled them in yet.
else if ((oldWidth > 0) && (oldHeight > 0))
{
// Size changed, if we do not have an new image
// already in preparation create one now
Image next = newImage;
if (next == null)
{
next = container.createImage(new ScatterImage(width, height, xt, yt));
newImage = next;
container.prepareImage(next, this);
//System.out.println("newImage created"+newImage);
}
else
{
int newWidth = next.getWidth(null);
int newHeight = next.getHeight(null);
if ((newWidth > 0) && (newHeight > 0) && ((newWidth != width) || (newHeight != height)))
{
// the new image is already the wrong size
// TODO: Signal the image to start over with the new size
//next.setSize(width,height);
}
}
// Until the new image is ready we will draw a
// scaled version of the current image
//System.out.println("Draw "+imageCache+" scaled to "+width+","+height);
g.drawImage(current, x1, y2, width, height, this); // approximation for now
}
}
}
}
else
{
super.paint(g,isPrinting);
}
}
public void paintIcon(final PlotGraphics g, final int width, final int height)
{
JASHistScatterPlotStyle style = (JASHistScatterPlotStyle) parent.style;
if (style.getDisplayAsScatterPlot())
{
g.setColor(style.getDataPointColor());
g.fillRect(1, 1, width - 2, height - 2);
}
else
{
super.paintIcon(g, width, height);
}
}
/**
* Called if more points have been added to image
*/
void continueImage()
{
//System.out.println("continueImage");
Image image = imageCache;
if (image != null)
{
((ScatterImage) image.getSource()).continueDrawing();
}
image = newImage;
if (image != null)
{
((ScatterImage) image.getSource()).continueDrawing();
}
}
void restartImage(boolean newEnumNeeded)
{
//System.out.println("restartImage"+newEnumNeeded);
Image image = imageCache;
if (image != null)
{
((ScatterImage) image.getSource()).restart(newEnumNeeded);
}
image = newImage;
if (image != null)
{
((ScatterImage) image.getSource()).restart(newEnumNeeded);
}
}
private void imageReallyComplete(ImageProducer producer)
{
// Attention, executed in thread of ImageProducer
Image next = newImage;
if ((next != null) && (next.getSource() == producer))
{
Image oldImage = imageCache;
imageCache = next;
oldImage.flush();
newImage = null;
container.repaint();
//System.out.println("Image Replace");
}
}
final class ScatterImage implements ImageProducer, Runnable
{
final private DoubleCoordinateTransformation xt;
final private DoubleCoordinateTransformation yt;
private ImageConsumer consumer;
private ScatterEnumeration enumer;
private Thread thread;
private boolean abort = false;
private boolean pastPointOfNoContinue = false;
final private int height;
final private int width;
ScatterImage(int width, int height, DoubleCoordinateTransformation xt, DoubleCoordinateTransformation yt)
{
this.width = width;
this.height = height;
// TODO: Better coordinate transforms
// These are live references to the coordinate transformation, so they
// can change if the size of the plot changes while the image is
// being produced :-(
this.xt = xt;
this.yt = yt;
}
public boolean isConsumer(ImageConsumer c)
{
return consumer.equals(c);
}
public void addConsumer(ImageConsumer c)
{
//System.out.println("addConsumer "+c);
if (consumer != null)
{
throw new RuntimeException("Only single consumer supported");
}
consumer = c;
}
public void removeConsumer(ImageConsumer c)
{
consumer = null;
}
public void requestTopDownLeftRightResend(ImageConsumer c)
{
// forget it!
}
public void run()
{
ImageConsumer consumer = this.consumer;
if (consumer != null)
{
deliverImage(consumer);
}
thread = null;
//System.out.println("Thread stopped");
}
public void startProduction(ImageConsumer c)
{
//System.out.println("start");
consumer = c;
if (async)
{
thread = new Thread(this);
thread.start();
}
else
{
run();
}
}
void abort()
{
try
{
Thread t = thread;
if (t != null)
{
//System.out.println("-----------Interrupt");
abort = true;
t.join();
//System.out.println("-----------join");
}
}
catch (InterruptedException x)
{
}
finally
{
abort = false;
}
}
/**
* A continue message means more data points have been
* added to the enumeration.
*/
void continueDrawing()
{
Thread t = thread;
if (t != null)
{
// Check if we are past the point of no continue
// If not we don't need to do anything, because we
// will naturally continue.
synchronized (this)
{
if (!pastPointOfNoContinue)
{
return;
}
}
// If we are then we must wait for this thread to
// complete before starting a new one
try
{
t.join();
}
catch (InterruptedException x)
{
}
}
if (async)
{
thread = new Thread(this);
thread.start();
}
else
{
run();
}
}
/**
* Restart means we need to start drawing the image again,
* from the beginning.
*/
void restart(final boolean newEnumNeeded)
{
abort(); // Stop the current thread
if (newEnumNeeded)
{
enumer = null;
}
else if (enumer != null)
{
enumer.restart();
}
if (async)
{
thread = new Thread(this);
thread.start();
}
else
{
run();
}
}
private void deliverImage(ImageConsumer consumer)
{
JASHistScatterPlotStyle style = (JASHistScatterPlotStyle) parent.style;
final Color c = style.getDataPointColor();
final byte[] r = { 0, (byte) c.getRed() };
final byte[] g = { 0, (byte) c.getGreen() };
final byte[] b = { 0, (byte) c.getBlue() };
//Fix to JAS-94 and JAS-232
//TO-DO Figure out why setting the alpha value to (byte)255
//the problems in the bugs above reappear.
final byte[] a = { 0, (byte) 254 };
final ColorModel model = new IndexColorModel(1, 2, r, g, b, a);
byte[] pixels = new byte[width * height];
consumer.setDimensions(width, height);
consumer.setColorModel(model);
final double[] d = new double[2];
// for one image the following five variables are constant
// (if any of them changes we have to start a new thread)
final int point = style.getDataPointStyle();
final int size = style.getDataPointSize();
final int size2 = size / 2; // just to keep things fast
final double x1 = xt.convert(xt.getPlotMin());
final double y2 = yt.convert(yt.getPlotMax());
if (enumer == null)
{
final double data_xMin = parent.dataSource.getXMin();
final double data_xMax = parent.dataSource.getXMax();
final double data_yMin = parent.dataSource.getYMin();
final double data_yMax = parent.dataSource.getYMax();
final double plot_xMin = xt.getPlotMin();
final double plot_xMax = xt.getPlotMax();
final double plot_yMin = yt.getPlotMin();
final double plot_yMax = yt.getPlotMax();
if ((data_xMin < plot_xMin) || (data_xMax > plot_xMax) || (data_yMin < plot_yMin) || (data_yMax > plot_yMax))
{
final double xMin = Math.max(data_xMin, plot_xMin);
final double xMax = Math.min(data_xMax, plot_xMax);
final double yMin = Math.max(data_yMin, plot_yMin);
final double yMax = Math.min(data_yMax, plot_yMax);
// if a point is beyond our regular x bounds by amount "xExtra"
// or beyond our regular y bounds by amount "yExtra"
// we want to include it anyway because at least part of
// its dot will appear within the bounds
final double xExtra = ((double) size2 * (plot_xMax - plot_xMin)) / (double) (xt.convert(plot_xMax) - x1);
final double yExtra = ((double) size2 * (plot_yMax - plot_yMin)) / (double) (yt.convert(plot_yMin) - y2);
if (((data_xMin + xExtra) < plot_xMin) || ((data_xMax - xExtra) > plot_xMax) || ((data_yMin + yExtra) < plot_yMin) || ((data_yMax - yExtra) > plot_yMax))
{
enumer = parent.dataSource.startEnumeration(xMin - xExtra, xMax + xExtra, yMin - yExtra, yMax + yExtra);
}
else
{
enumer = parent.dataSource.startEnumeration();
}
}
else
{
enumer = parent.dataSource.startEnumeration();
}
}
long nextUpdate = System.currentTimeMillis() + 200L;
for (int n = 0; !abort; n++)
{
boolean ok = enumer.getNextPoint(d);
if (!ok)
{
synchronized (this)
{
// We need to double check, just in case
// more data was added since we checked a millisecond ago
// (arent threads fun)
ok = enumer.getNextPoint(d);
if (!ok)
{
pastPointOfNoContinue = true;
break;
}
}
}
final int col = Math.round((float) (xt.convert(d[0]) - x1));
final int row = Math.round((float) (yt.convert(d[1]) - y2));
int i;
int col_start;
int col_end;
int row_start;
int row_end;
if (size < 2)
{
if ((col < 0) || (col >= width))
{
continue;
}
try
{
pixels[(row * width) + col] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
else
{
switch (point)
{
case JASHistScatterPlotStyle.SYMBOL_BOX:
col_end = Math.min(width, col + size2);
row_start = Math.max(0, row - size2);
row_end = Math.min(height, row + size2);
for (i = row_start * width; row_start < row_end;
i += width, row_start++)
for (col_start = Math.max(0, col - size2);
col_start < col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
break;
case JASHistScatterPlotStyle.SYMBOL_TRIANGLE:
row_start = row - ((size * 64) / 100); // approximates the top of the triangle
i = Math.max(0, row_start) * width;
for (int j = Math.max(0, -row_start); j < size;
j++, i += width)
{
final int extra = j / 2;
col_end = Math.min(width - 1, col + extra);
for (col_start = Math.max(0, col - extra);
col_start <= col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
break;
case JASHistScatterPlotStyle.SYMBOL_DIAMOND:
row_start = Math.max(0, row - size2);
row_end = Math.min(height, row + size2);
i = row_start * width;
for (int j = Math.max(size2 - row, 0); j < size2;
j++, i += width)
{
col_end = Math.min(width - 1, col + j);
for (col_start = Math.max(0, col - j);
col_start <= col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
for (int j = size2; j >= 0; j--, i += width)
{
col_end = Math.min(width - 1, col + j);
for (col_start = Math.max(0, col - j);
col_start <= col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
break;
case JASHistScatterPlotStyle.SYMBOL_STAR:
i = row * width;
col_end = Math.min(width - 1, col + size2);
for (col_start = Math.max(0, col - size2);
col_start <= col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
i -= (size2 * width);
for (int j = -size2; j <= size2; j++, i += width)
{
if ((col >= 0) && (col < width))
{
try
{
pixels[i + col] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
if (((col + j) >= 0) && ((col + j) < width))
{
try
{
pixels[i + col + j] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
if (((col - j) >= 0) && ((col - j) < width))
{
try
{
pixels[(i + col) - j] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
}
break;
case JASHistScatterPlotStyle.SYMBOL_VERT_LINE:
if ((col >= 0) && (col < width))
{
row_start = Math.max(0, row - size2);
row_end = Math.min(height, row + size2);
i = (row_start * width) + col;
for (; row_start < row_end; row_start++, i += width)
try
{
pixels[i] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
break;
case JASHistScatterPlotStyle.SYMBOL_HORIZ_LINE:
if ((row >= 0) && (row < height))
{
i = row * width;
col_end = Math.min(width, col + size2);
for (col_start = Math.max(0, col - size2);
col_start < col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
break;
case JASHistScatterPlotStyle.SYMBOL_CROSS:
i = row * width;
col_end = Math.min(width - 1, col + size2);
for (col_start = Math.max(0, col - size2);
col_start <= col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
row_start = Math.max(0, row - size2);
row_end = Math.min(height - 1, row + size2);
i -= (((row - row_start) * width) - col);
for (; row_start <= row_end; row_start++, i += width)
try
{
pixels[i] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
break;
case JASHistScatterPlotStyle.SYMBOL_SQUARE:
row_start = Math.max(0, row - size2);
row_end = Math.min(height, row + size2);
col_end = Math.min(width, col + size2);
i = row_start * width;
col_start = Math.max(0, col - size2);
if (row_start > 0)
{
for (; col_start < col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
final boolean l_Line = (col - size2) >= 0;
final boolean r_Line = (col + size2) < width;
i += col;
for (; row_start < row_end; row_start++, i += width)
{
if (l_Line)
{
try
{
pixels[i - size2] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
if (r_Line)
{
try
{
pixels[i + size2] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
}
}
i -= col;
for (col_start = Math.max(0, col - size2);
col_start <= col_end; col_start++)
try
{
pixels[i + col_start] = 1;
}
catch (final ArrayIndexOutOfBoundsException e)
{
}
break;
}
}
final long now = System.currentTimeMillis();
if (now > nextUpdate)
{
consumer.setPixels(0, 0, width, height, model, pixels, 0, width);
consumer.imageComplete(consumer.SINGLEFRAMEDONE);
nextUpdate = now + 200;
//System.out.println("Thread running n="+n);
}
}
if (abort)
{
consumer.imageComplete(consumer.IMAGEABORTED);
}
else
{
// Note, we may still have more frames, for example if the style changes,
// or if more points need to be displayed
consumer.setPixels(0, 0, width, height, model, pixels, 0, width);
consumer.imageComplete(consumer.SINGLEFRAMEDONE);
imageReallyComplete(this);
}
}
}
}