//=====================================================================
// File: GraphView.java
// Class: GraphView
// Package: AFLPgui
//
// Author: James J. Benham
// Date: January 6, 1999
// Contact: james_benham@hmc.edu
//
// Genographer v1.0 - Computer assisted scoring of gels.
// Copyright (C) 1998 Montana State University
//
// 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; version 2
// of the License.
//
// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//
// The GNU General Public License is distributed in the file GPL
//=====================================================================
package AFLPgui;
import java.awt.Choice;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Label;
import java.awt.Panel;
import java.awt.PrintGraphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import AFLPcore.Bin;
import AFLPcore.Cutoff;
import AFLPcore.CutoffFunction;
import AFLPcore.DataList;
import AFLPcore.FeatureList;
import AFLPcore.Lane;
import AFLPcore.Option;
import AFLPcore.ProgOptions;
import AFLPcore.ScoringFailure;
import AFLPcore.ScoreFunction;
/**
* Displays a graph of a bin. This class manages the size of the graph
* and draws the axis and axis labels. It also provides a slider
* to manipulate the cutoffs in the bin. The actually drawing of the
* graph is done by objects of the type Graph
. Normally,
* the display is double-buffered, but this is disabled if the class is
* drawing to a printer page. When it first displays, it will adjust the
* cutoffs so that only one will appear in the bin, (the cutoff with
* the largest starting position). The size of the display and the
* graph can be manipulated by external classes.
*
* @see Graph
* @see CutoffSlider
*
* @author James J. Benham
* @version 1.1.0
* @date January, 1999
*/
public class GraphView extends Panel
implements ActionListener,
ItemListener,
MouseListener,
MouseMotionListener
{
// Info Bar component parameters
private static int LABEL_H_INSET = 5;
private static int LABEL_V_INSET = 3;
private static int LABEL_WIDTH = 600;
private static int LABEL_HEIGHT = 18;
// Error message location
private static int LABEL_HORZ = 20;
private static int LABEL_VERT = 20;
// Graph layout stuff
private static int SLIDER_WIDTH = 22;
private static int H_SPACE = 50;
private static int TICK_L_WIDTH = 30;
private static int TICK_WIDTH = 3;
private static int BOTTOM_BORDER = 15;
// Button bar constants
private static int HORZ_SPACE = 5;
private static int GRAPH_CHOICE_WIDTH = 70;
// Files for images
private static String REL_PATH = "";
private static String PREVIOUS_FILE = "l_arrow.gif";
private static String NEXT_FILE = "r_arrow.gif";
private static String ADDCT_FILE = "plus.gif";
private static String CTDIALOG_FILE = "cutoff.gif";
// Double buffering stuff
private Dimension offDimension;
private Image offImage;
private Graphics offGraphics;
// Bar stuff
private ButtonBar buttonBar;
private Bar infoBar;
private Label infoLabel;
// Button Bar components
Choice graphChoice;
ImgButton previousButton;
ImgButton nextButton;
ImgButton addCutoffButton;
ImgButton cutoffDialogButton;
CutoffDialog ctDialog;
private CutoffSlider slider;
private Bin currentBin;
private int height;
private int width;
private int h_inset;
private int v_inset;
private int graph_width;
private double maxSize;
private double minSize;
private double scale;
private double tickInc;
private double maxIntensity;
private DataList lanes;
private DataList bins;
private Graph graph[];
private int graphIndex;
private Frame topWindow;
/**
* Create a new GraphView with the specified parameters.
*
* @param lanes the lanes to include in the graph.
* @param bins a list of bins in the gel.
* @param parentWindow an owner for dialog boxes.
*/
public GraphView(DataList lanes, DataList bins, Frame parentWindow)
{
this.lanes = lanes;
this.bins = bins;
topWindow = parentWindow;
height = 320;
width = 500;
h_inset = 10;
v_inset = 10;
graph_width = 400;
currentBin = null;
graph = new Graph[2];
graph[0] = new BarGraph();
graph[1] = new ScatterGraph();
graphIndex = 0;
graphChoice = new Choice();
graphChoice.add("Bar");
graphChoice.add("Scatter");
graphChoice.select(graphIndex);
// add the slider control
slider = new CutoffSlider();
slider.addMouseListener(this);
slider.addMouseMotionListener(this);
add(slider);
slider.setBounds(0, v_inset, SLIDER_WIDTH,
height - v_inset - BOTTOM_BORDER);
setLayout(null);
setBounds(0, 0, width, height);
ctDialog = new CutoffDialog(topWindow, "Cutoffs...", true);
createButtonBar();
createInfoBar();
}
/**
* Initialize the graph view. This will do things like check to see
* that only a single cutoff applies to the specified bin in the
* lanes. It will also set the scale for the graph and score any bins
* that need scoring.
*
* @param bin the bin to show a graph for
* @param lanes the lanes to include in the graph
* @param bins a list of bins in the gel, used so the graph could switch
* to another bin.
*/
public void init(Bin bin, DataList lanes, DataList bins)
{
// make sure the size is still set.
setBounds(0, 0, width, height);
// update the lists since the gel may have changed
this.lanes = lanes;
this.bins = bins;
currentBin = bin;
if(bin != null)
{
maxSize = bin.getLocation() + bin.getRange();
minSize = bin.getLocation() - bin.getRange();
}
else
{
maxSize = -1;
minSize = 0;
}
//=================== find the max intensity=========
maxIntensity = 0;
double tempIntensity;
for(int i=0; i < lanes.size(); i++)
{
tempIntensity = ((Lane) lanes.dataAt(i)).getMaxHeight(minSize,
maxSize);
if(tempIntensity > maxIntensity)
maxIntensity = tempIntensity;
}
//=================== find the scale================
scale = (height - v_inset - BOTTOM_BORDER - 1)/maxIntensity;
//=================== set the tick increment========
if(maxIntensity >= 2000)
tickInc = 1000;
else if(maxIntensity >= 1000)
tickInc = 500;
else
tickInc = 100;
slider.setScale(scale);
slider.setSize(minSize);
slider.refresh();
// fix anything that has multiple cutoffs in the bin by taking the
// largest one and applying it to the bin.
Cutoff ct;
Lane ln;
for(int i=0; i < lanes.size(); i++)
{
ln = (Lane) lanes.dataAt(i);
ct = ln.cutoffUnder(maxSize);
// Look out for errors, like MaxSize = -1, if no bin, so
// ct will be null. In that case, don't worryabout this stuff
if( (ct != null) && (ct.getStartPos() > minSize))
{
// make this cutoff cover the whole bin.
// first delete anything between it and the edge of the bin
double pos;
Cutoff temp = ln.cutoffUnder(ct.getStartPos() - 0.00000001);
if(temp != null)
pos = temp.getStartPos();
else
pos = -1;
while(pos > minSize)
{
ln.getCutoffs().removeElement(temp);
// find the next one, subtract a little number to avoid
// equailty
temp = ln.cutoffUnder(pos - 0.00000001);
if(temp != null)
pos = temp.getStartPos();
else
pos = -1;
}
//Now adjust the cutoff.
ct.setStartPos(minSize);
}
}
// set the slider stuff
// if we don't have any lanes, then we won't be displaying anything
// anyway, so don't worry about that case.
if(lanes.size() > 0)
{
slider.setCutoff( ((Lane)lanes.dataAt(0)).cutoffUnder(maxSize));
slider.setLanes(lanes);
}
//=====score the bin initially================
// see if it is neccessary first, but check to see that it exists first
// It is possible that no bin is selected,
if(currentBin != null)
{
if(!currentBin.isScored())
try{
currentBin.score(lanes);
} catch(ScoringFailure error) {
handleScoreError(error);
}
// set the info label
infoLabel.setText(currentBin.getScoreInfo()[0]);
}
}
/**
* Gives the height of the graph display area, including borders.
*
* @return the height
*/
public int getHeight()
{
return height;
}
/**
* Sets the height of the graph display area, including borders, to
* the specified value.
*
* @param height the new height for the view
*/
public void setHeight(int height)
{
this.height = height;
setBounds(0, 0, width, height);
}
/**
* Gives the width of the graph display area, including borders.
*
* @return the width
*/
public int getWidth()
{
return width;
}
/**
* Sets the width of the graph display area, including borders, to
* the specified value.
*
* @param width the new width
*/
public void setWidth(int width)
{
this.width = width;
setBounds(0, 0, width, height);
}
/**
* Gets the width of the graph, which does not include borders. This
* is the value that will be passed to the graph drawing object.
*
* @return the width
*
* @see Graph
*/
public int getGraphWidth()
{
return graph_width;
}
/**
* Sets the width of the actual graph, which does not include the border.
* This is the value that will be passed to the graph drawing object.
*
* @param width the new width for the graph
*
* @see Graph
*/
public void setGraphWidth(int width)
{
graph_width = width;
}
/**
* Gives the bin that was used to produce the graph.
*
* @return the bin currently displayed as a graph
*/
public Bin getBin()
{
return currentBin;
}
/**
* Draws the graph. It will actually draw the axis and the tick
* marks, as well as the cutoffs, but the actual graph is
* drawn by the Graph
object selected. The display is
* normally double buffered, but double-buffering is disabled if
* the method is drawing to a printed page.
*/
public void paint(Graphics g)
{
//=======================Determine the size===============
int temp = graph[graphIndex].getPreferredWidth();
int fullWidth = width - h_inset - H_SPACE;
if(temp == -1)
graph_width = fullWidth;
else if(temp <= fullWidth)
graph_width = temp;
else
{
// decide if we should resize or not?
graph_width = fullWidth;
}
if( !(g instanceof PrintGraphics))
{
//=======================Off screen buffer setup==========
// make sure we have the offscreen buffer and that it is the right
// size.
Dimension d = getSize();
if ( (offGraphics == null)
|| (d.width != offDimension.width)
|| (d.height != offDimension.height) )
{
offDimension = d;
offImage = createImage(d.width, d.height);
offGraphics = offImage.getGraphics();
}
}
else
{
// don't need to double buffer the printer
offGraphics = g;
}
//=================== find the scale================
// can change when we go to the printer of come back from it
scale = (height - v_inset - BOTTOM_BORDER - 1)/maxIntensity;
// make sure we have a bin. If not, we don't have anything to display.
// Draw to the graphics screen and then bail. don't use the double
// buffer.
if(currentBin == null)
{
slider.setVisible(false);
g.clearRect(0, 0, width, height);
g.drawString("No Bin Selected. Graph unavailable.",
LABEL_HORZ, LABEL_VERT);
return; // bail!
}
// ============== Normal drawing ===================
// clear the screen
offGraphics.clearRect(0, 0, width, height);
slider.setVisible(true);
// set some of the stuff
int x = h_inset + H_SPACE;
int h = height - BOTTOM_BORDER;
// Draw the axis
offGraphics.setColor(Color.black);
offGraphics.drawLine(x, v_inset, x, h);
offGraphics.drawLine(x, h, x + graph_width, h);
//================== Draw Tick marks =====================
int y;
int tickLoc = (int) tickInc;
double maxHeight = (h-v_inset - 1)/scale;
while(tickLoc < maxHeight)
{
// Find the location for the tick
y = h - (int) (scale*tickLoc);
offGraphics.drawLine(x - TICK_WIDTH, y, x, y);
offGraphics.drawString("" + tickLoc, x - TICK_L_WIDTH, y + 5);
tickLoc += (int) tickInc;
}
// ==================Draw the graph=========================
// Call whichever graphing method is selected.
graph[graphIndex].drawGraph(offGraphics, x, v_inset, graph_width,
h - v_inset, BOTTOM_BORDER, scale, minSize,
maxSize, lanes);
// ================= Draw the Cutoffs =======================
// use the first lane for the whole graph, and extend it
// straight across the graph.
offGraphics.setColor(Color.lightGray);
Cutoff ct = ((Lane)lanes.dataAt(0)).cutoffUnder(maxSize);
// see if we only have one cutoff.
if(ct.getStartPos() > minSize)
System.err.println("Cutoff does not span bin!");
int numLevels = ct.getNumLevels();
int intensity;
for(int i=0; i < numLevels; i++)
{
// Just draw lines, since the graph doesn't really have a
// horizontal scale for the different places in the bin.
intensity = h - 1 - (int) (scale * ct.getCutoff(minSize, i));
offGraphics.drawLine(x + 1, intensity, x + graph_width, intensity);
}
if(!(g instanceof PrintGraphics))
g.drawImage(offImage, 0, 0, this);
}
/**
* Changes the graph to the one selected in the choice box.
*/
public void itemStateChanged(ItemEvent e)
{
if(e.getSource() == graphChoice)
{
graphIndex = graphChoice.getSelectedIndex();
refresh();
}
}
/**
* Handles the buttons in the button bar.
*/
public void actionPerformed(ActionEvent e)
{
if(e.getSource() == previousButton)
{
// find the location of this bin in the list
int location = bins.find(currentBin.getSearchKey()).location;
// now move to the previous one in the list
if(location <= 0)
infoLabel.setText("Already on first bin.");
else
{
currentBin = (Bin) bins.dataAt(location - 1);
init(currentBin, lanes, bins);
infoLabel.setText("Bin number " + (location));
refresh();
}
}
else if(e.getSource() == nextButton)
{
// find the location of this bin in the list
int location = bins.find(currentBin.getSearchKey()).location;
// now move to the previous one in the list
if(location == (bins.size() - 1))
infoLabel.setText("Already on last bin.");
else
{
currentBin = (Bin) bins.dataAt(location + 1);
init(currentBin, lanes, bins);
infoLabel.setText("Bin number " + (location + 2));
refresh();
}
}
else if(e.getSource() == addCutoffButton)
{
// Make a new function, set at half height
CutoffFunction newFunction;
newFunction = (CutoffFunction) FeatureList.getCutoffMgr().getDefault();
newFunction = (CutoffFunction) newFunction.clone();
Option opts[] = newFunction.getOptions();
opts[0].setValue(maxIntensity/2);
newFunction.setOptions(opts);
slider.addCutoffFunction(newFunction);
// rescore the bin
try{
currentBin.score(lanes);
} catch(ScoringFailure error) {
handleScoreError(error);
}
refresh();
}
else if(e.getSource() == cutoffDialogButton)
{
ctDialog.init(((Lane) lanes.dataAt(0)).cutoffUnder(maxSize),
lanes, minSize);
ctDialog.setVisible(true);
slider.setCutoff(((Lane) lanes.dataAt(0)).cutoffUnder(maxSize));
slider.refresh();
refresh();
}
}
/**
* Called when the mouse is released. It will rescore a bin if the
* mouse is released on the slider.
*/
public void mouseReleased(MouseEvent e)
{
if(e.getSource() == slider)
{
try{
currentBin.score(lanes);
} catch(ScoringFailure error) {
handleScoreError(error);
}
refresh();
infoLabel.setText(currentBin.getScoreInfo()[0]);
}
}
/**
* Called when the mouse is dragged. It will refresh the display if
* the event originated with the cutoff slider.
*/
public void mouseDragged(MouseEvent e)
{
if(e.getSource() == slider)
refresh();
}
/**
* Called to update the screen by Java. It simply calls paint.
*/
public void update(Graphics g)
{
paint(g);
}
/**
* Gives the ButtonBar associated with the graph.
*
* @return the button bar.
*/
public ButtonBar getButtonBar()
{
return buttonBar;
}
/**
* Shows an options dialog if an error occurs when scoring a bin.
* The most likely reason is that the number of cutoffs do not match
* the number expected by the scoring function. The options are used
* to adjust the scoring function.
*
* @param error the orignal failure
*/
protected void handleScoreError(ScoringFailure error)
{
ErrorDialog errorDialog = new ErrorDialog(topWindow);
errorDialog.showError(error);
// try setting the number of levels for the scoring method
ScoreFunction scoreFn = currentBin.getScoreMethod();
Option opts[] = scoreFn.getOptions();
// find out the number of levels we have, try using the first lane.
Cutoff ct = new Cutoff(minSize, 2);
if(!lanes.isEmpty())
ct = ((Lane) lanes.dataAt(0)).cutoffUnder(maxSize);
// see if the first option is a number
if(opts[0].getType() != Option.NUMBER)
throw new IllegalArgumentException("Error in scoring method " +
scoreFn.getName() + ". First " +
"option should be number of levels"
+ " expected.");
opts[0].setValue((double) ct.getNumLevels() + 1);
scoreFn.setOptions(opts);
// show the options again.
OptionDialog optD = new OptionDialog(scoreFn.getOptions(),
topWindow,
scoreFn.getName() + " Parameters");
optD.setVisible(true);
if(!optD.isCanceled())
{
opts = optD.getOptions();
scoreFn.setOptions(opts);
}
}
/**
* Lays out the components on the button bar, uses the constants declared
* in this class to control the layout.
*/
private void createButtonBar()
{
buttonBar = new ButtonBar();
buttonBar.setBounds(0, 0, 640, 32);
// Let the parent window handle the action event for the orignal
// button bar components
buttonBar.sendActionEventsTo( (ActionListener) topWindow);
// ===========Create the graph choice=================
// already created in constructor, initiallized with graph types
buttonBar.add(graphChoice);
graphChoice.addItemListener(this);
int startX = buttonBar.getFreeHorzPos();
graphChoice.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
GRAPH_CHOICE_WIDTH, ButtonBar.BUTTON_HEIGHT);
startX += GRAPH_CHOICE_WIDTH + HORZ_SPACE;
//===========Create the previous button================
previousButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath
+ REL_PATH +
PREVIOUS_FILE));
buttonBar.add(previousButton);
previousButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE;
previousButton.addActionListener(this);
//===========Create the next button====================
nextButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath +
REL_PATH +
NEXT_FILE));
buttonBar.add(nextButton);
nextButton.setBounds(startX, ButtonBar.VERT_INSET,
ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
startX += ButtonBar.BUTTON_WIDTH;
nextButton.addActionListener(this);
//===========Create the addCutoff button====================
String imgFileLoc = ProgOptions.homePath + REL_PATH + ADDCT_FILE;
addCutoffButton = new ImgButton(ButtonBar.retrieveImage(imgFileLoc));
buttonBar.add(addCutoffButton);
addCutoffButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE;
addCutoffButton.addActionListener(this);
//===========Create the cutoff dialog button===============
cutoffDialogButton =
new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath +
REL_PATH + CTDIALOG_FILE));
buttonBar.add(cutoffDialogButton);
cutoffDialogButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
ButtonBar.BUTTON_WIDTH,
ButtonBar.BUTTON_HEIGHT);
startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE;
cutoffDialogButton.addActionListener(this);
}
/**
* Returns a bar that displays information about the graph. This object
* should be displayed somewhere by the container of this object.
*
* @return the bar as described above
*/
public Bar getInfoBar()
{
return infoBar;
}
/**
* Lays out the components for the info bar. Layout is controlled with
* constants in this class.
*/
private void createInfoBar()
{
// create the label
infoLabel = new Label("Graph info...");
// create the bar, no bottom border.
infoBar = new Bar(true, false);
infoBar.setLayout(null);
infoBar.add(infoLabel);
infoLabel.setBounds(LABEL_H_INSET, LABEL_V_INSET,
LABEL_WIDTH, LABEL_HEIGHT);
infoBar.setBounds(0, 0, 600, FragmentMap.BAR_HEIGHT);
}
/**
* Updates the display so that it matches the data.
*/
public void refresh()
{
repaint();
}
//=================Unused methods to satisfy intefaces=================
/**Unused*/ public void mouseClicked(MouseEvent e) {}
/**Unused*/ public void mousePressed(MouseEvent e) {}
/**Unused*/ public void mouseMoved(MouseEvent e) {}
/**Unused*/ public void mouseEntered(MouseEvent e){}
/**Unused*/ public void mouseExited(MouseEvent e){}
}