package net.bioclipse.spectrum.graph2d;
import java.awt.*;
import java.applet.*;
import java.util.*;
import java.lang.*;
import java.io.StreamTokenizer;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
/*
**************************************************************************
**
** Class graph.Graph2D
**
**************************************************************************
** Copyright (C) 1995, 1996 Leigh Brookshaw
**
** 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., 675 Mass Ave, Cambridge, MA 02139, USA.
**************************************************************************
**
** class Graph2D extends Canvas
**
** The main entry point and interface for the 2D graphing package.
** This class keeps track of the DataSets and the Axes.
** It has the main drawing engine that positions axis etc.
**
*************************************************************************/
/**
* This is the main plotting class. It partitions the canvas to contain the
* specified axes with the remaining space taken with the plotting region.
* Axes are packed against the walls of the canvas. The <B>paint</B> and
* <B>update</B> methods of this class handle all the drawing operations of the
* graph. This means that independent components like Axis and DataSets must be
* registered with this class to be incorporated into the plot.
*
* @version $Revision: 1.12 $, $Date: 1996/09/24 05:23:41 $
* @author Leigh Brookshaw
*/
public class Graph2D extends Canvas {
/*
** Default Background Color
*/
private Color DefaultBackground = null;
/*
*********************
**
** Protected Variables
**
*********************/
/**
* A vector list of All the axes attached
* @see Graph2d#attachAxis()
*/
protected Vector axis = new Vector(4);
/**
* A vector list of All the DataSets attached
* @see Graph2d#attachDataSet()
* @see DataSet
*/
protected Vector dataset = new Vector(10);
/**
* The markers that may have been loaded
* @see Graph2D#setMarkers()
*/
protected Markers markers = null;
/**
* The blinking "data loading" thread
* @see Graph2D#startedloading()
*/
protected LoadMessage load_thread = null;
/**
* The background color for the data window
*/
protected Color DataBackground = null;
/*
**********************
**
** Public Variables
**
*********************/
/**
* If this is greater than zero it means that
* data loading threads are active so the message "loading data"
* is flashed on the plot canvas. When it is back to zero the plot
* progresses normally
*/
public int loadingData = 0;
/**
* The width of the border at the top of the canvas. This allows
* slopover from axis labels, legends etc.
*/
public int borderTop = 20;
/**
* The width of the border at the bottom of the canvas. This allows
* slopover from axis labels, legends etc.
*/
public int borderBottom = 20;
/**
* The width of the border at the left of the canvas. This allows
* slopover from axis labels, legends etc.
*/
public int borderLeft = 20;
/**
* The width of the border at the right of the canvas. This allows
* slopover from axis labels, legends etc.
*/
public int borderRight = 20;
/**
* If set <I>true</I> a frame will be drawn around the data window.
* Any axes will overlay this frame.
*/
public boolean frame = true;
/**
* The color of the frame to be drawn
*/
public Color framecolor;
/**
* If set <I>true</I> (the default) a grid will be drawn over the data window.
* The grid will align with the major tic marks of the Innermost axes.
*/
public boolean drawgrid = true;
/**
* The color of the grid to be drawn
*/
public Color gridcolor = Color.pink;
/**
* If set <I>true</I> (the default) a grid line will be drawn
* across the data window
* at the zeros of the innermost axes.
*/
public boolean drawzero = true;
/**
* The color of the zero grid lines.
*/
public Color zerocolor = Color.orange;
/**
* The rectangle that the data will be plotted within. This is an output
* variable only.
*/
public Rectangle datarect = new Rectangle();
/**
* If set <I>true</I> (the default) the canvas will be set to the background
* color (erasing the plot) when the update method is called.
* This would only be changed for special effects.
*/
public boolean clearAll = true;
/**
* If set <I>true</I> (the default) everything associated with the plot
* will be drawn when the update method or paint method are called.
* Normally
* only modified for special effects
*/
public boolean paintAll = true;
/**
* Modify the position of the axis and the range of the axis so that
* the aspect ratio of the major tick marks are 1 and the plot is square
* on the screen
*/
public boolean square = false;
/**
* Text to be painted Last onto the Graph Canvas.
*/
public TextLine lastText = null;
/*
*******************
**
** Public Methods
**
*******************/
/**
* Load and Attach a DataSet from a File.
* The method loads the data into a DataSet class
* and attaches the class to the graph for plotting.
*
* The data is assumed to consist
* (at this stage) 2 ASCII columns of numbers x, y. As always blank lines
* are ignored and everything following # is ignored as a comment.
*
* @param file The URL of the data file to read.
* @return The DataSet constructed containing the data read.
*/
public DataSet loadFile( URL file) {
byte b[] = new byte[50];
int nbytes = 0;
int max = 100;
int inc = 100;
int n = 0;
double data[] = new double[max];
InputStream is = null;
boolean comment = false;
int c;
try {
is = file.openStream();
while( (c=is.read()) > -1 ) {
switch (c) {
case '#':
comment = true;
break;
case '\r': case '\n':
comment = false;
case ' ': case '\t':
if( nbytes > 0 ) {
String s = new String(b,0,0,nbytes);
data[n] = Double.valueOf(s).doubleValue();
n++;
if( n >= max ) {
max += inc;
double d[] = new double[max];
System.arraycopy(data, 0, d, 0, n);
data = d;
}
nbytes = 0;
}
break;
default:
if( !comment ) {
b[nbytes] = (byte)c;
nbytes++;
}
break;
}
}
if (is != null) is.close();
} catch(Exception e) {
System.out.println("Failed to load Data set from file ");
e.printStackTrace();
if (is != null) try { is.close(); } catch (Exception ev) { }
return null;
}
return loadDataSet(data,n/2);
}
/**
* Load and Attach a DataSet from an array.
* The method loads the data into a DataSet class
* and attaches the class to the graph for plotting.
*
* The data is assumed to be stored
* in the form x,y,x,y,x,y.... A local copy of the data is made.
*
* @param data The data to be loaded in the form x,y,x,y,...
* @param n The number of (x,y) data points. This means that the
* minimum length of the data array is 2*n.
* @return The DataSet constructed containing the data read.
*/
public DataSet loadDataSet( double data[], int n ) {
DataSet d;
try {
d = new DataSet(data, n);
dataset.addElement( d );
d.g2d = this;
}
catch (Exception e) {
System.out.println("Failed to load Data set ");
e.printStackTrace();
return null;
}
return d;
}
/**
* Attach a DataSet to the graph. By attaching the data set the class
* can draw the data through its paint method.
*/
public void attachDataSet( DataSet d ) {
if( d != null) {
dataset.addElement( d );
d.g2d = this;
}
}
/**
* Detach the DataSet from the class. Data associated with the DataSet
* will nolonger be plotted.
*
* @param d The DataSet to detach.
*/
public void detachDataSet( DataSet d ) {
if(d != null) {
if(d.xaxis != null) d.xaxis.detachDataSet(d);
if(d.yaxis != null) d.yaxis.detachDataSet(d);
dataset.removeElement(d);
}
}
/**
* Detach All the DataSets from the class.
*/
public void detachDataSets() {
DataSet d;
int i;
if(dataset == null | dataset.isEmpty() ) return;
for (i=0; i<dataset.size(); i++) {
d = ((DataSet)dataset.elementAt(i));
if(d.xaxis != null) d.xaxis.detachDataSet(d);
if(d.yaxis != null) d.yaxis.detachDataSet(d);
}
dataset.removeAllElements();
}
/**
* Create and attach an Axis to the graph. The position of the axis
* is one of Axis.TOP, Axis.BOTTOM, Axis.LEFT or Axis.RIGHT.
*
* @param position Position of the axis in the drawing window.
*
*/
public Axis createAxis( int position ) {
Axis a;
try {
a = new Axis(position);
a.g2d = this;
axis.addElement( a );
}
catch (Exception e) {
System.out.println("Failed to create Axis");
e.printStackTrace();
return null;
}
return a;
}
/**
* Attach a previously created Axis. Only Axes that have been attached will
* be drawn
*
* @param the Axis to attach.
*/
public void attachAxis( Axis a ) {
if(a == null) return;
try {
axis.addElement( a );
a.g2d = this;
}
catch (Exception e) {
System.out.println("Failed to attach Axis");
e.printStackTrace();
}
}
/**
* Detach a previously attached Axis.
* @param the Axis to dettach.
*/
public void detachAxis( Axis a ) {
if(a != null) {
a.detachAll();
a.g2d = null;
axis.removeElement(a);
}
}
/**
* Detach All attached Axes.
*/
public void detachAxes() {
int i;
if(axis == null | axis.isEmpty() ) return;
for (i=0; i<axis.size(); i++) {
((Axis)axis.elementAt(i)).detachAll();
((Axis)axis.elementAt(i)).g2d = null;
}
axis.removeAllElements();
}
/**
* Get the Maximum X value of all attached DataSets.
* @return The maximum value
*/
public double getXmax() {
DataSet d;
double max=0.0;
if(dataset == null | dataset.isEmpty() ) return max;
for (int i=0; i<dataset.size(); i++) {
d = ((DataSet)dataset.elementAt(i));
if(i==0) max = d.getXmax();
else max = Math.max(max,d.getXmax());
}
return max;
}
/**
* Get the Maximum Y value of all attached DataSets.
* @return The maximum value
*/
public double getYmax() {
DataSet d;
double max=0.0;
if(dataset == null | dataset.isEmpty() ) return max;
for (int i=0; i<dataset.size(); i++) {
d = ((DataSet)dataset.elementAt(i));
if(i==0) max = d.getYmax();
else max = Math.max(max,d.getYmax());
}
return max;
}
/**
* Get the Minimum X value of all attached DataSets.
* @return The minimum value
*/
public double getXmin() {
DataSet d;
double min = 0.0;
if(dataset == null | dataset.isEmpty() ) return min;
for (int i=0; i<dataset.size(); i++) {
d = ((DataSet)dataset.elementAt(i));
if(i==0) min = d.getXmin();
else min = Math.min(min,d.getXmin());
}
return min;
}
/**
* Get the Minimum Y value of all attached DataSets.
* @return The minimum value
*/
public double getYmin() {
DataSet d;
double min=0.0;
if(dataset == null | dataset.isEmpty() ) return min;
for (int i=0; i<dataset.size(); i++) {
d = ((DataSet)dataset.elementAt(i));
if(i==0) min = d.getYmin();
else min = Math.min(min,d.getYmin());
}
return min;
}
/**
* Set the markers for the plot.
* @param m Marker class containing the defined markers
* @see Markers
*/
public void setMarkers(Markers m) {
markers = m;
}
/**
* Get the markers
* @return defined Marker class
* @see Markers
*/
public Markers getMarkers() {
return markers;
}
/**
* Set the background color for the entire canvas.
* @params c The color to set the canvas
*/
public void setGraphBackground(Color c) {
if(c == null) return;
setBackground(c);
}
/**
* Set the background color for the data window.
* @params c The color to set the data window.
*/
public void setDataBackground(Color c) {
if(c == null) return;
DataBackground = c;
}
/**
* This paints the entire plot. It calls the draw methods of all the
* attached axis and data sets.
* The order of drawing is - Axis first, data legends next, data last.
* @params g Graphics state.
*/
public void paint(Graphics g) {
int i;
Graphics lg = g.create();
Rectangle r = bounds();
/* The r.x and r.y returned from bounds is relative to the
** parents space so set them equal to zero.
*/
r.x = 0;
r.y = 0;
if(DefaultBackground == null) DefaultBackground=this.getBackground();
if(DataBackground == null) DataBackground=this.getBackground();
// System.out.println("Graph2D paint method called!");
if( !paintAll ) return;
r.x += borderLeft;
r.y += borderTop;
r.width -= borderLeft+borderRight;
r.height -= borderBottom+borderTop;
paintFirst(lg,r);
if( !axis.isEmpty() ) r = drawAxis(lg, r);
else {
if(clearAll ) {
Color c = g.getColor();
g.setColor(DataBackground);
g.fillRect(r.x,r.y,r.width,r.height);
g.setColor(c);
}
drawFrame(lg,r.x,r.y,r.width,r.height);
}
paintBeforeData(lg,r);
if( !dataset.isEmpty() ) {
datarect.x = r.x;
datarect.y = r.y;
datarect.width = r.width;
datarect.height = r.height;
for (i=0; i<dataset.size(); i++) {
((DataSet)dataset.elementAt(i)).draw_data(lg,r);
}
}
paintLast(lg,r);
lg.dispose();
}
/**
* A hook into the Graph2D.paint method. This is called before
* anything is plotted. The rectangle passed is the dimension of
* the canvas minus the border dimensions.
* @params g Graphics state
* @params r Rectangle containing the graph
*/
public void paintFirst( Graphics g, Rectangle r) {
}
/**
* A hook into the Graph2D.paint method. This is called before
* the data is drawn but after the axis.
* The rectangle passed is the dimension of
* the data window.
* @params g Graphics state
* @params r Rectangle containing the data
*/
public void paintBeforeData( Graphics g, Rectangle r) {
}
/**
* A hook into the Graph2D.paint method. This is called after
* everything has been drawn.
* The rectangle passed is the dimension of
* the data window.
* @params g Graphics state
* @params r Rectangle containing the data
*/
public void paintLast( Graphics g, Rectangle r) {
if( lastText != null ) {
lastText.draw(g,r.width/2, r.height/2, TextLine.CENTER);
}
}
/**
* This method is called via the Graph2D.repaint() method.
* All it does is blank the canvas (with the background color)
* before calling paint.
*/
public void update(Graphics g) {
// System.out.println("Graph2d update method called");
if( clearAll ) {
Color c = g.getColor();
/* The r.x and r.y returned from bounds is relative to the
** parents space so set them equal to zero
*/
Rectangle r = bounds();
r.x = 0;
r.y = 0;
g.setColor(getBackground());
g.fillRect(r.x,r.y,r.width,r.height);
g.setColor(c);
}
if( paintAll ) paint(g);
}
/**
* Handle keyDown events. Only one event is handled the pressing
* of the key 'r' - this will repaint the canvas.
*/
public boolean keyDown(Event e, int key) {
if( key == 'r' ) {
repaint();
return true;
} else {
return false;
}
}
/**
* Calling this method pauses the plot and displays a flashing
* message on the screen. Mainly used when data is being loaded across the
* net. Everytime this routine is called a counter is incremented
* the method Graph2D.finishedloading() decrements the counter. When the
* counter is back to zero the plotting resumes.
* @see Graph2D#finishedloading()
* @see Graph2D#loadmessage()
* @see LoadMessage
*/
public void startedloading() {
loadingData++;
if(loadingData != 1) return;
if(load_thread == null) load_thread = new LoadMessage(this);
load_thread.setFont(new Font("Helvetica", Font.PLAIN, 25));
load_thread.begin();
}
/**
* Decrement the loading Data counter by one. When it is zero resume
* plotting.
* @see Graph2D#startedloading()
* @see Graph2D#loadmessage()
* @see LoadMessage
*/
public void finishedloading() {
loadingData--;
if(loadingData > 0) return;
if(load_thread != null) load_thread.end();
load_thread = null;
}
/**
* Change the message to be flashed on the canvas
* @param s String contining the new message.
* @see Graph2D#startedloading()
* @see Graph2D#finishedloading()
* @see LoadMessage
*/
public void loadmessage(String s) {
if(load_thread == null) load_thread = new LoadMessage(this);
load_thread.setMessage(s);
}
/*
*******************
**
** Protected Methods
**
*******************/
/**
* Force the plot to have an aspect ratio of 1 by forcing the
* axes to have the same range. If the range of the axes
* are very different some extremely odd things can occur. All axes are
* forced to have the same range, so more than 2 axis is pointless.
*/
protected Rectangle ForceSquare(Graphics g, Rectangle r) {
Axis a;
Rectangle dr;
int x = r.x;
int y = r.y;
int width = r.width;
int height = r.height;
double xmin;
double xmax;
double ymin;
double ymax;
double xrange = 0.0;
double yrange = 0.0;
double range;
double aspect;
if( dataset == null | dataset.isEmpty() ) return r;
/*
** Force all the axis to have the same range. This of course
** means that anything other than one xaxis and one yaxis
** is a bit pointless.
*/
for (int i=0; i<axis.size(); i++) {
a = (Axis)axis.elementAt(i);
range = a.maximum - a.minimum;
if(a.isVertical()) {
yrange = Math.max(range, yrange);
} else {
xrange = Math.max(range, xrange);
}
}
if(xrange <= 0 | yrange <= 0 ) return r;
if( xrange > yrange ) range = xrange;
else range = yrange;
for (int i=0; i<axis.size(); i++) {
a = (Axis)axis.elementAt(i);
a.maximum = a.minimum + range;
}
/*
** Get the new data rectangle
*/
dr = getDataRectangle(g, r);
/*
** Modify the data rectangle so that it is square.
*/
if(dr.width > dr.height) {
x += (dr.width-dr.height)/2.0;
width -= dr.width-dr.height;
} else {
y += (dr.height-dr.width)/2.0;
height -= dr.height-dr.width;
}
return new Rectangle(x,y,width,height);
}
/**
* Calculate the rectangle occupied by the data
*/
protected Rectangle getDataRectangle(Graphics g, Rectangle r) {
Axis a;
int waxis;
int x = r.x;
int y = r.y;
int width = r.width;
int height = r.height;
for (int i=0; i<axis.size(); i++) {
a = ((Axis)axis.elementAt(i));
waxis = a.getAxisWidth(g);
switch (a.getAxisPos()) {
case Axis.LEFT:
x += waxis;
width -= waxis;
break;
case Axis.RIGHT:
width -= waxis;
break;
case Axis.TOP:
y += waxis;
height -= waxis;
break;
case Axis.BOTTOM:
height -= waxis;
break;
}
}
return new Rectangle(x, y, width, height);
}
/**
*
* Draw the Axis. As each axis is drawn and aligned less of the canvas
* is avaliable to plot the data. The returned Rectangle is the canvas
* area that the data is plotted in.
*/
protected Rectangle drawAxis(Graphics g, Rectangle r) {
Axis a;
int waxis;
Rectangle dr;
int x;
int y;
int width;
int height;
if(square) r = ForceSquare(g,r);
dr = getDataRectangle(g,r);
x = dr.x;
y = dr.y;
width = dr.width;
height = dr.height;
if(clearAll ) {
Color c = g.getColor();
g.setColor(DataBackground);
g.fillRect(x,y,width,height);
g.setColor(c);
}
// Draw a frame around the data area (If requested)
if(frame) drawFrame(g,x,y,width,height);
// Now draw the axis in the order specified aligning them with the final
// data area.
for (int i=0; i<axis.size(); i++) {
a = ((Axis)axis.elementAt(i));
a.data_window = new Dimension(width,height);
switch (a.getAxisPos()) {
case Axis.LEFT:
r.x += a.width;
r.width -= a.width;
a.positionAxis(r.x,r.x,y,y+height);
if(r.x == x ) {
a.gridcolor = gridcolor;
a.drawgrid = drawgrid;
a.zerocolor = zerocolor;
a.drawzero = drawzero;
}
a.drawAxis(g);
a.drawgrid = false;
a.drawzero = false;
break;
case Axis.RIGHT:
r.width -= a.width;
a.positionAxis(r.x+r.width,r.x+r.width,y,y+height);
if(r.x+r.width == x+width ) {
a.gridcolor = gridcolor;
a.drawgrid = drawgrid;
a.zerocolor = zerocolor;
a.drawzero = drawzero;
}
a.drawAxis(g);
a.drawgrid = false;
a.drawzero = false;
break;
case Axis.TOP:
r.y += a.width;
r.height -= a.width;
a.positionAxis(x,x+width,r.y,r.y);
if(r.y == y) {
a.gridcolor = gridcolor;
a.drawgrid = drawgrid;
a.zerocolor = zerocolor;
a.drawzero = drawzero;
}
a.drawAxis(g);
a.drawgrid = false;
a.drawzero = false;
break;
case Axis.BOTTOM:
r.height -= a.width;
a.positionAxis(x,x+width,r.y+r.height,r.y+r.height);
if(r.y +r.height == y+height ) {
a.gridcolor = gridcolor;
a.drawgrid = drawgrid;
a.zerocolor = zerocolor;
a.drawzero = drawzero;
}
a.drawAxis(g);
a.drawgrid = false;
a.drawzero = false;
break;
}
}
return r;
}
/*
* Draws a frame around the data area.
*/
protected void drawFrame(Graphics g, int x, int y, int width, int height) {
Color c = g.getColor();
if( framecolor != null ) g.setColor(framecolor);
g.drawRect(x,y,width,height);
g.setColor(c);
}
}
/**
* This should be thrown if any of the packages fileloaders
* encounter a format error
*/
class FileFormatException extends Exception {
public FileFormatException(String s) {
super(s);
}
}
/**
* This is a separate thread that flashes a message
* on the Graph2D canvas that data is loading
*/
class LoadMessage extends Thread {
Graph2D g2d;
String message = "Loading Data ... Please Wait!";
String newmessage = null;
long visible = 500;
long invisible = 200;
Color foreground = Color.red;
Graphics lg = null;
Font f = null;
/**
* Instantiate the class
* @param g2d The Graph2D canvas to draw message on
*
*/
public LoadMessage(Graph2D g2d) {
this.g2d = g2d;
}
/**
* Instantiate the class
* @param g2d The Graph2D canvas to draw message on
* @param s The string to flash on the canvas
*/
public LoadMessage(Graph2D g2d, String s) {
this(g2d);
this.message = s;
}
/**
* Instantiate the class
* @param g2d The Graph2D canvas to draw message on
* @param s The string to flash on the canvas
* @param visible Number of milliseconds the message is visible
* @param invisible Number of milliseconds the message is invisible
*/
public LoadMessage(Graph2D g, String s, long visible, long invisible) {
this(g,s);
this.visible = visible;
this.invisible = invisible;
}
/**
* begin displaying the message
*/
public void begin() {
g2d.clearAll = false;
g2d.paintAll = false;
super.start();
}
/**
* end displaying message and force a graph repaint
*/
public void end() {
super.stop();
g2d.clearAll = true;
g2d.paintAll = true;
if(lg != null) lg.dispose();
g2d.repaint();
}
/**
* The method to call when the thread starts
*/
public void run() {
boolean draw = true;
FontMetrics fm;
Rectangle r;
int sw = 0;
int sa = 0;
int x = 0;
int y = 0;
setPriority(Thread.MIN_PRIORITY);
while(true) {
if( newmessage != null && draw) {
message = newmessage;
newmessage = null;
}
if(lg == null) {
lg = g2d.getGraphics();
if(lg != null) lg = lg.create();
}
if( lg != null) {
if(f != null) lg.setFont(f);
fm = lg.getFontMetrics(lg.getFont());
sw = fm.stringWidth(message);
sa = fm.getAscent();
} else {
draw = false;
}
if( draw ) {
lg.setColor(foreground);
r = g2d.bounds();
x = r.x + (r.width-sw)/2;
y = r.y + (r.height+sa)/2;
lg.drawString(message, x, y);
g2d.repaint();
try { sleep(visible); }
catch(Exception e) { }
} else {
if(lg != null) {
lg.setColor(g2d.getBackground());
lg.drawString(message, x, y);
g2d.repaint();
}
try { sleep(invisible); }
catch(Exception e) { }
}
draw = !draw;
}
}
/**
* Set the font the message will be displayed in
* @param f the font
*/
public void setFont(Font f) {
this.f = f;
}
/**
* The foreground color for the message
* @param c the foreground color
*/
public void setForeground(Color c) {
if(c == null) return;
this.foreground = c;
}
/**
* Set the message to be displayed
* @param s the message
*/
public void setMessage(String s) {
if(s==null) return;
newmessage = s;
}
}