//=====================================================================
// File:    Thumbnail.java
// Class:   Thumbnail
// Package: AFLPgui
//
// Author:  James J. Benham
// Date:    August 12, 1998
// 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.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.ScrollPane;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
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.LinearCutoff;
import AFLPcore.Peak;
import AFLPcore.ProgOptions;
import AFLPcore.Option;
import AFLPcore.ScoreFunction;
import AFLPcore.ScoringFailure;

/**
 * This class displays thumbnails for a bin. A thumbnail is a trace of
 * a lane, but the trace is not the full trace. Instead, it is only the
 * portion of the lane that is included in the bin. Each thumbnail shows
 * the lane number and the scoring label as well as the trace. The
 * thumbnails have a set width. The width of the display area is set
 * not to exceed the the size avaiable. If all of the thumbnails do not
 * fit on one row, another row will be made. There is no limit on the 
 * number of rows. A <code>CutoffSlider</code> is provided to adjust
 * the cutoff levels. Buttons allow the user to move to the next or previous
 * bin, as well as add a cutoff level and show a dialog to manipulate the
 * cutoffs. A button also changes the scoring to the opposite of it's current
 * setting. This is useful if one has two parents, and wants to keep the
 * scoring consistant. The class can be drawn to a printer page by
 * setting the width with <code>setViewWidth</code> and <code>paint</code>.
 * The paint method will recognize the print request and  behave appropriately.
 * Normally the display is double-buffered, but this is disabled when
 * drawing to a printed page (<code>PrinteGraphics</code>). The scoring can
 * also be adjusted manually by clicking on the trace.
 *
 * @see CutoffSlider
 *
 * @author James J. Benham
 * @version 1.0.0
 * @date August 12, 1998
 */

public class Thumbnail extends Panel implements ActionListener,
                                                MouseListener,
                                                MouseMotionListener
{
  // Error Message Location
  private static int LABEL_HORZ = 20;
  private static int LABEL_VERT = 20;

  // 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;

  // Button bar constants
  private static int HORZ_SPACE = 5;

  // Files for images
  private static String REL_PATH      = "";
  private static String INVERT_FILE   = "invert.gif";
  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";
  private static String BIN_FILE = "bin.gif";

  // State of mouse position
  public static final int BLANK = 0;
  public static final int TRACE = 1;

  // Double buffering stuff
  private Dimension offDimension;
  private Image offImage;
  private Graphics offGraphics;

  private DataList lanes;
  private DataList bins;

  private ButtonBar buttonBar;
  private Bar infoBar;
  private Label infoLabel;

  // Button Bar components
  ImgButton invertButton;
  ImgButton previousButton;
  ImgButton nextButton;
  ImgButton addCutoffButton;
  ImgButton cutoffDialogButton;

  CutoffDialog ctDialog;

  private CutoffSlider slider;

  private Bin currentBin;

  private int height;
  private int width;
  private int hspace;
  private int vspace;
  private int sliderSpace;
  private int scoreWidth;

  // variables describing the layout
  private int printViewWidth;
  private int numColumns;
  private int numRows;

  // variables to track mouse location
  private int overType;
  private double size;
  private double intensity;
  private int laneNum;

  private double minSize;
  private double maxSize;
  private double hScale;
  private double vScale;

  private double maxIntensity;

  // the main program window
  private ScrollPane parent;
  private Frame topWindow;

  /**
   * Creates thumbnails with the specified parameters.
   * 
   * @param lanes   the lanes to produce thumbnails for
   * @param bins    the bins defined in the gel.
   * @param parentWindow  a windows used as the owner of various dialog boxes.
   */
  public Thumbnail(DataList lanes, DataList bins, Frame parentWindow)
  {
    this.lanes = lanes;
    this.bins = bins;
    topWindow = parentWindow;

    height = 61;
    width = 26;
    hspace = 3;
    vspace = 13;
    sliderSpace = 22;
    scoreWidth = 8;
    setSize(500, 500);
    setLayout(null);

    // set for no thumbnails
    minSize = 0;
    maxSize = -1;
    currentBin = null;

    addMouseListener(this);
    addMouseMotionListener(this);

    // add the slider control
    slider = new CutoffSlider();
    slider.addMouseListener(this);
    slider.addMouseMotionListener(this);
    add(slider);
    slider.setBounds(0, vspace, sliderSpace, height);

    ctDialog = new CutoffDialog(topWindow, "Cutoffs...", true);

    createButtonBar();
    createInfoBar();
  }

  /**
   * Performs calculations neccessary to display the thumbnails. It
   * finds the horizontal and vertical scales used to convert between
   * pixels and base pairs, initializes the slider control with the
   * correct values, scores the bin if neccessary, and ensures that 
   * only one cutoff appears in the bin. This is accomplished by applying
   * the cutoff with the start position closest to the top of the bin
   * to the entire bin. (The top has a larger size in bp.)
   */
  public void init()
  {
    //=================== 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 scales================
    hScale = (maxSize - minSize)/(width - 3);  // -3 for border
    vScale = (height - 1)/maxIntensity;

    slider.setScale(vScale);
    slider.setSize(minSize);

    // 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);
      }

    slider.refresh();

    //=====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);
	  }
	}
  }

  /**
   * Draws the thumbnails. It will automatically adjust it's width to
   * the correct size if a <code>PrintGraphics</code> is supplied. Normally
   * the display is double-buffered, but not when drawing to a printer page.
   */
  public void paint(Graphics g)
  {
    //==================Set the size====================
    // it differs for printing and on screen stuff
    int viewWidth = 0;
    if(g instanceof PrintGraphics)
      {
	viewWidth = printViewWidth;
      }
    else
      {
	// Find out how big the parent is, and check for the scroll bar inset
	parent = (ScrollPane) getParent();
	Dimension pSize = parent.getViewportSize();
	//Dimension pSize = parent.getSize();
	viewWidth = pSize.width - parent.getVScrollbarWidth();
      }

    int numToDraw = lanes.size();

    numColumns = (viewWidth - sliderSpace)/(width + hspace);
    
    numRows = (int) Math.ceil(numToDraw/(double)numColumns);
    
    // adjust the size to the correct length
    setSize(viewWidth, (vspace + height)*numRows + vspace);

    // some of this stuff doesn't need to occur when printing, like 
    // double buffering.
    if(g instanceof PrintGraphics)
      {
	offGraphics = g;
      }
    else
      {
	//=======================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();
	  }
      }

    // check for a valid range. If there isn't one, we have a problem,
    // probbly that there is no data. Draw to the graphics since after
    // the error, we won't flip the backbuffer to the front.
    if(maxSize < minSize)
      {
	slider.setVisible(false);
	g.clearRect(0, 0, getSize().width, getSize().height);
	if(maxSize == -1)
	  {
	    g.drawString("No Bin Selected. Thumbnails unavailable.",
			 LABEL_HORZ, LABEL_VERT);
	  }
	else
	  {
	    g.drawString("Invalid size range.",
			 LABEL_HORZ, LABEL_VERT);
	  }
	return;  // get out of here
      }

    // clear the screen
    offGraphics.clearRect(0, 0, getSize().width, getSize().height);

    slider.setVisible(true);

    for(int row = 0; row < numRows; row++)
      {
	for(int column =0; column < numColumns; column++)
	  {
	    numToDraw--;
	    // check to see if we're done
	    if(numToDraw < 0)
	      break;

	    drawTrace(offGraphics, 
		      (Lane) lanes.dataAt(row*numColumns + column),
		      sliderSpace + hspace + column*(width + hspace),
		      vspace + row*(height + vspace));
	  }
      }

    if(!(g instanceof PrintGraphics))
	g.drawImage(offImage, 0, 0, this);

    // let the parent container know about the size change
    // WARNING: This seems to cause terrible flickering as the
    // image is constantly redrawn, so don't use it.
    //parent.validate();
  }

  /**
   * Draws the trace portion of the thumbnail. It uses the scales set
   * by <code>inti</code>. The trace will be drawn in the same color
   * as the signal for the lane. It also draws the border box and
   * adds the lane number and score label for the lane to the top
   * of the box, as well as the cutoff for the lane. If the cutoff is
   * a line (type <code>LinearCutoff</code> then a straight line is
   * drawn. Otherwise, the CutoffFunction is drawn by playing connect
   * the dots.
   *
   * @param g     the graphics to draw on
   * @param lane  the lane to display in the thumbnail
   * @param x     the top-left position of the thumbnail.
   * @param y     the top left position of the thumbnail.
   */
  public void drawTrace(Graphics g, Lane lane, int x, int y)
  {
    //=================== draw the trace================
    double size = minSize;
    int previousIntensity = y + height - 1 - (int)(vScale*
						   lane.getHeightAt(minSize));
    int intensity;

    // draw the box and label.
    g.setColor(Color.black);
    g.drawRect(x, y, width, height);
    g.drawString("" + lane.getLaneNumber(), x, y - 1);
    g.drawString(currentBin.getScore(lane), x + width - scoreWidth, y - 1);

    // find the color and set it
    switch(lane.getColor())
      {
      case Lane.RED:
	g.setColor(Color.red);
	break;
      case Lane.GREEN:
	g.setColor(Color.green);
	break;
      case Lane.BLUE:
	g.setColor(Color.blue);
	break;
      case Lane.YELLOW:
	g.setColor(Color.yellow);
	break;
      }
   
    // draw the actual trace
    for(int i=1; i < (width - 1); i++)
      {
	// find the height, -1 to place it above the border.
	intensity = y + height - 1 - (int) (vScale * lane.getHeightAt(size));
	g.drawLine(x + i, previousIntensity, x + i + 1, intensity);

	previousIntensity = intensity;
	size += hScale;
      }

    //================Draw the cutoff functions============
    
    g.setColor(Color.lightGray);

    Cutoff ct = lane.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();
    for(int i=0; i < numLevels; i++)
      {
	if(ct.getCutoffFunction(i) instanceof LinearCutoff)
	  {
	    intensity =  y + height - 1 - (int) (vScale * 
						 ct.getCutoff(minSize, i));
	    g.drawLine(x + 1, intensity, x + width - 1, intensity);
	  }
	else
	  {
	    size = minSize;
	    previousIntensity = (y + height - 1 - 
				 (int) (vScale * ct.getCutoff(minSize, i)));
	    for(int j=1; j < (width - 1); j++)	   
	      {
		// find the height, -1 to place it above the border.
		intensity =  y + height - 1 - (int) (vScale * 
						     ct.getCutoff(size, i));
		g.drawLine(x + j, previousIntensity, x + j + 1, intensity);
		
		previousIntensity = intensity;
		size += hScale;
	      }
	  }
      }
  }

  /**
   * Updates the display and is called by java. It simply calls paint.
   */
  public void update(Graphics g)
  {
    paint(g);
  }

  /**
   * Gives the ButtonBar associated with the thumbnails.
   *
   * @return the button bar.
   */
  public ButtonBar getButtonBar()
  {
    return buttonBar;
  }

  /**
   * Changes the scoring of the specified lane to the one that would
   * result if there was a peak at the specified size and intensity.
   * The bin's overall scoring is updated after this. The scoring that
   * would result is assumed to be the label that has an index that
   * is related to the cutoffs. This may not be true for all scoring
   * functions, but it works for all of the ones at this time.
   * If will adjust the confidence of the peaks so that peaks that should
   * be ignored have a confidence of 0, and those that should be included
   * again have a confidence of 1.
   *
   * @param size    the size in bp used to determine the label.
   * @param intensity  the height used to determine the labe.
   * @param ln      the lane whose scoring is to be changed.
   */
  private void changeScore(double size, double intensity, Lane ln)
  {
    boolean found = false; // used to tell when a label has been found
    int cutoffIndex = 0;   // tells which cutoff we're interested in

    // find the cutoff, do to init(), there should only be one under
    // maxSize that applies to the bin.
    Cutoff ct = ln.cutoffUnder(maxSize);
    String choices[];
    choices = currentBin.getScoreMethod().getChoices(ct.getNumLevels());
    
    // Go through all of the levels and find one that works
    for(int i=0; i < ct.getNumLevels(); i++)
      {
	if(intensity >= ct.getCutoff(size, i))
	   {
	     currentBin.setScore(ln, choices[i]);
	     found = true;
	     cutoffIndex = i;
	     break;
	   }
      }

    // We're not above any of the cutoffs, so go with the last choice
    if(!found)
      {
	currentBin.setScore(ln, choices[ct.getNumLevels()]);
	cutoffIndex = ct.getNumLevels();
      }

    //===================modify the peaks ===============
    // set the confidence of those above the level to 0 (marked peaks only)
    // But if the peak is in the correct region and marked, set the 
    // confidence to 1.
    // This is a signal to the scoring function to include or not
    // include them.
    DataList peaks = ln.getPeaksInRange(minSize, maxSize);
    Peak pk;

    // Look for the cutoff above this one and set the maxHeight;
    double maxHeight;
    if(cutoffIndex == 0)
      maxHeight = 9E99;       // set to a very large value
    else
      maxHeight = ct.getCutoff(size, cutoffIndex - 1);

    // see if we are on the last one, if so, the minimum is 0
    double minHeight;
    if(cutoffIndex == ct.getNumLevels())
      minHeight = 0;
    else
      minHeight = ct.getCutoff(size, cutoffIndex);
    
    for(int i=0; i < peaks.size(); i++)
      {
	pk = (Peak) peaks.dataAt(i);
	if( (pk.getHeight() >= minHeight) && pk.isMarked())
	  {
	    if( pk.getHeight() < maxHeight)
	      {
		pk.setConfidence(1);
	      }
	    else
	      pk.setConfidence(0);
	  }
      }
    
    // update the overall info
    try{
      currentBin.scoreOverall(lanes);
    } catch(ScoringFailure error) {
      handleScoreError(error);
    }

    // Show the changes on the screen
    infoLabel.setText((currentBin.getScoreInfo())[0]);
    refresh();
  }

  /**
   * 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 invert button================
    invertButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath +
							 REL_PATH + 
							 INVERT_FILE));
    buttonBar.add(invertButton);
    int startX = buttonBar.getFreeHorzPos();
    invertButton.setBounds(startX + HORZ_SPACE, ButtonBar.VERT_INSET,
			   ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
    startX += ButtonBar.BUTTON_WIDTH + HORZ_SPACE;
    invertButton.addActionListener(this);

    //===========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);
  }

  /**
   * 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("Thumbnail 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);
  }

  /**
   * Returns a bar that displays information about the thumbnails. This object
   * should be displayed somewhere by the container of this object.
   *
   * @return the bar as described above
   */
  public Bar getInfoBar()
  {
    return infoBar;
  }

  /**
   * Handles the buttons in the button bar.
   */
  public void actionPerformed(ActionEvent e)
  {
    if(e.getSource() == invertButton)
      {
	if(currentBin != null)
	  {
	    currentBin.getScoreMethod().invert();
	    try{
	      currentBin.score(lanes);
	    } catch(ScoringFailure error){
	      handleScoreError(error);
	    }

	    refresh();
	  }
      }
    else 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
	  {
	    Bin b = (Bin) bins.dataAt(location - 1);
	    setBin(b);
	    init();
	    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
	  {
	    Bin b = (Bin) bins.dataAt(location + 1);
	    setBin(b);
	    init();
	    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();
      }
  }

  /**
   * Adjusts the scoring for a lane if the mouse is over the trace.
   */
  public void mouseClicked(MouseEvent e)
  {
    if(e.getSource() == slider)
      {
	try{
	  currentBin.score(lanes);
	}catch(ScoringFailure error){
	  handleScoreError(error);
	}
	refresh();
      }
      
    if(overType == TRACE)
      {
	changeScore(size, intensity, (Lane) lanes.dataAt(laneNum));
      }
  }

  /**
   * Rescores the bin if the mouse was on the slider.
   */
  public void mouseReleased(MouseEvent e)
  {
    if(e.getSource() == slider)
      {
	try{
	  currentBin.score(lanes);
	}catch(ScoringFailure error){
	  handleScoreError(error);
	}
	   
	refresh();
      }
  }

  /**
   * Refreshes the display if the mouse is on the slider. Since the cutoffs
   * are changing, the display needs to be updated.
   */
  public void mouseDragged(MouseEvent e)
  {
    if(e.getSource() == slider)
      {
	infoLabel.setText(slider.getMessage());
	refresh();
      }
  }

  /**
   * Determines what the mouse is over and displays info in the 
   * info bar.
   */
  public void mouseMoved(MouseEvent e)
  {
    if(currentBin != null)
      {
	overType = findPositionInfo(e.getX(), e.getY());
	if(e.getSource() == slider)
	  infoLabel.setText(slider.getMessage());
	else
	  switch(overType)
	    {
	    case TRACE:
	      infoLabel.setText((currentBin.getScoreInfo())[0] +
				" Size: " + size + " Intensity: " + intensity);
	      break;
	    case BLANK:
	      infoLabel.setText((currentBin.getScoreInfo())[0]);
	      break;
	    }
      }
  }

  /**
   * Sets the lanes for which to display thumbnails to the specified
   * value.
   *
   * @param lanes a list of lanes to display.
   */
  public void setLanes(DataList lanes)
  {
    this.lanes = lanes;
  }

  /**
   * Gives the lanes that are being displayed.
   *
   * @return the lanes
   *
   * @see Thumbnail#setLanes
   */
  public DataList getLanes()
  {
    return lanes;
  }

  /**
   * Sets the width of the display area. This will only be used when
   * the thumbnails are printed, otherwise this class will look
   * up the size of the screen, which can change and use that as
   * the view width.
   *
   * @param width the width of the page.
   */
  public void setViewWidth(int width)
  {
    printViewWidth = width;
  }

  /**
   * Sets the bin for the thumbnails to the specified one. The bin is
   * required for the thumbnails to display. This method sets the range
   * of the thumbnails to that of the bin
   *
   * @param b    the bin whose thumbnails to dislay.
   */
  public void setBin(Bin b)
  {
    currentBin = b;

    if(b != null)
      {
	minSize = b.getLocation() - b.getRange();
	maxSize = b.getLocation() + b.getRange();
      }
    else
      {
	minSize = 0;
	maxSize = -1;
      }
  }

  /**
   * Gives the bin displayed. This can be changed from the one initially
   * set when the thumbnail moves to the next or previous bin.
   *
   * @return the bin.
   */
  public Bin getBin()
  {
    return currentBin;
  }

  /**
   * Gives the size of an individual thumbnail, including the border around
   * it.
   *
   * @return an container for both the widht and height
   */
  public Dimension getThumbnailSize()
  {
    return new Dimension(width + hspace, height + vspace);
  }

  /**
   * Sets the bins for the thumbnail. These are provided so that the
   * thumbnail can move to the next or previous bin.
   *
   * @param bins   the bins in the gel
   */
  public void setBinList(DataList bins)
  {
    this.bins = bins;
  }

  /**
   * Gives the size and intensity for the given point. It will also determine
   * if the point is on a trace or on a blank. If it is one a trace, it
   * will determine the corresponding size in bp and intensity of that
   * point.
   *
   * @param x    the x-coordinate of the point.
   * @param y    the y-coordinate of the point.
   *
   * @return the type the point is in either <code>BLANK</code> or
   *         <code>TRACE</code>, which are constants declared in this class.
   */
  public int findPositionInfo(int x, int y)
  {
    // Find the row and column
    int column = (x - sliderSpace)/(width + hspace);
    int row = y/(height + vspace);
    
    // see if we are between the spaces or not
    int vRemainder = y % (height + vspace);
    if(vRemainder <= vspace)
      return BLANK;

    int hRemainder = (x - sliderSpace) % (width + hspace);
    if(hRemainder <= hspace)
      return BLANK;

    // find out which lane we are one
    laneNum = row*numColumns + column;

    // make sure we don't exceed the number possible, 
    if( (column >= numColumns) || (row >= numRows) ||
	(laneNum >= lanes.size()))
      {
	laneNum = -1; // we aren't on a valid lane
	return BLANK;
      }

    size = Math.round((minSize + hScale*(hRemainder - hspace - 1))*100)/100.0;
    intensity = Math.round((height - vRemainder + 
			    vspace - 1)/vScale * 100)/100.0;

    return TRACE;
  }

  /**
   * Updates the display so that it matches the data, which may have
   * changed.
   */
  public void refresh()
  {
    repaint();
  }

  //=================Unused methods to satisfy intefaces=================
  /** Unused*/ public void mousePressed(MouseEvent e) {}
  /** Unused*/ public void mouseEntered(MouseEvent e){}
  /** Unused*/ public void mouseExited(MouseEvent e){}
}
