//=====================================================================
// File:    GelView.java
// Class:   GelView
// Package: AFLPgui
//
// Author:  James J. Benham
// Date:    August 11, 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.Button;
import java.awt.Canvas;
import java.awt.Choice;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Label;
import java.awt.Point;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;
import java.awt.image.MemoryImageSource;
import AFLPcore.Bin;
import AFLPcore.DataList;
import AFLPcore.Gel;
import AFLPcore.Lane;
import AFLPcore.NoDataError;
import AFLPcore.ProgOptions;

/**
 * Displays the gel as an image and provides the methods used to allow
 * user interaction with the gel, such as placing bins and zooming.
 * It also lets the user adjust the size range displayed and the intensity
 * of the image. The class will resize itself to contain the whole gel
 * image, so the parent should accomadate these changes.
 *
 * @author James J. Benham
 * @version 1.0.0
 * @date August 11, 1998
 */

public class GelView extends Canvas implements ActionListener,
                                               ItemListener,
                                               KeyListener,
                                               MouseListener,
                                               MouseMotionListener
{
  private static int GEL_HORZ_INSET       = 30;
  private static int GEL_VERT_INSET       = 25;
  private static int BIN_HEADER_WIDTH     = 20;
  private static int LANE_HEADER_HEIGHT   = 15;
  private static int LANE_MARK_VERT_INSET =  4;
  private static int LANE_MARK_HORZ_INSET =  2;
  private static int LANE_MARK_HEIGHT     =  7;

  private static int DEFAULT_LANE_WIDTH   =  12;
  private static int DEFAULT_BORDER_WIDTH =   4;
  private static int DEFAULT_TOP_BORDER   =  15;
  private static int DEFAULT_MIN_SIZE     =   0;
  private static int DEFAULT_MAX_SIZE     = 300;

  private static int INITIAL_LENGTH = 500;

  // Button Bar component parameters
  private static int HORZ_SPACE = 5;
  private static int ZOOM_WIDTH = 60;
  private static int ZOOM_HEIGHT_ADJUST = -5;
  private static int FIELD_WIDTH = 60;
  private static int SIZEL_WIDTH = 30;
  private static int TOL_WIDTH   = 20;
  private static int INTENSITYL_WIDTH = 55;

  private static String REL_PATH = "";
  private static String REFRESH_FILE = "refresh.gif";
  private static String BIN_FILE = "bin.gif";

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

  // Constants for the state of the mouse
  private final static int LANE_HEAD      = 0;
  private final static int BIN_HEAD       = 1;
  private final static int BIN            = 2;
  private final static int BIN_BORDER_TOP = 3;
  private final static int BIN_BORDER_BOT = 4;
  private final static int BLANK          = 5;
  private final static int OFF_GEL        = 6;
  
  // Double Buffering Variables
  private Dimension offDimension;
  private Image offImage;
  private Graphics offGraphics;

  // GUI Components
  private ButtonBar gelBar;
  private Bar infoBar;
  private Label infoLabel;
  private Choice zoomSelect;
  private TextField minSizeField;
  private TextField maxSizeField;
  private TextField intensityField;
  private ImgButton refreshButton;
  private ImgButton binButton;

  // Dialogs
  private BinDialog binDialog;
  private EntryDialog entryDialog;
  private ErrorDialog errorDialog;

  // Variables for the gel, 
  private Gel gel;
  private int gelLength;
  private int gelWidth;
  private int[] pixel;
  private Image gelImage;
  private MemoryImageSource imgSource;

  // The zoom level last selected. We don't want to recompute everything 
  // just to get to the same spot, so remember where we are
  private double zoomFactor;
  private double previousZoom;
  private int zoomIndex;

  // Mouse motion control variables
  private double sizeLoc;   // the size in bp at the current mouse position
  private int laneLoc;      // the lane at the current mouse position
  private int overType;     // the type of thing the mouse is over
  private Bin currentBin;
  private Bin selectedBin;
  private int dragMode;     // used to determine what to do when the mouse 
                            // is dragged
  private boolean dragged;  // set if the mouse dragged between press/release
  private Point dragStart;
  private Bin oldBin;

  // Different Cursors
  private Cursor standardCursor;
  private Cursor resizeCursor;
  private Cursor moveCursor;

  // Program variables
  private Frame topWindow; // this is the main program window.
  private boolean sizeSet;

  /**
   * Creates a new viewable object of the specified gel
   *
   * @param gel the gel to be displayed and manipulated
   */
  public GelView(Gel gel, Frame topWindow)
  {
    this.gel = gel;
    this.topWindow = topWindow;

    gel.setLaneWidth(DEFAULT_LANE_WIDTH);
    gel.setLaneBorder(DEFAULT_BORDER_WIDTH);
    gel.setGelTopBorder(DEFAULT_TOP_BORDER);

    gelLength = INITIAL_LENGTH;

    gel.setIntensity(gel.getGlobalMaxIntensity());

    // Create dialog box
    binDialog = new BinDialog(topWindow, "Bin Dialog", true);
    entryDialog = new EntryDialog(topWindow, "Entery Dialog", true);
    errorDialog = new ErrorDialog(topWindow);

    // construct the button bar, we only want to do this once.
    createButtonBar();

    // construct the info bar,
    createInfoBar();

    // get the mouse listeners
    addMouseListener(this);
    addMouseMotionListener(this);

    // see if we can set the current bin
    if( !(gel.getBins()).isEmpty())
      currentBin = (Bin) (gel.getBins()).dataAt(0);

    standardCursor = new Cursor(Cursor.DEFAULT_CURSOR);
    resizeCursor = new Cursor(Cursor.N_RESIZE_CURSOR);
    moveCursor = new Cursor(Cursor.MOVE_CURSOR);

    // set the size based on lane 0, if it exists. Do this here because
    // we have to wait for some of the GUI components to be created first.
    setGelSizeToMax(0);

    // rebuild the gel
    rebuild();
  }

  public void syncDisplay()
  {
    // update the display
    minSizeField.setText("" + Math.round(gel.getMinSize()*100)/100);
    maxSizeField.setText("" + Math.round(gel.getMaxSize()*100)/100);
    intensityField.setText("" + Math.round(gel.getGlobalMaxIntensity()/
					   gel.getIntensity()*100)/100.0);
  }

  public boolean isSizeSet()
  {
    return sizeSet;
  }

  public void setGelSizeToMax(int laneNum)
  {
    if(gel.getNumLanes() != 0)
      {
	Lane firstLane = gel.laneAt(laneNum);

	gel.setMinSize(firstLane.getMinSize());
	gel.setMaxSize(firstLane.getMaxSize());
	sizeSet = true;
      }
    else
      {
	// make up some defaults, this will let the program work, but
	// set sizeSet to false so we know to reassign.
	gel.setMinSize(DEFAULT_MIN_SIZE);
	gel.setMaxSize(DEFAULT_MAX_SIZE);
	sizeSet = false;
      }

    // update the display
    syncDisplay();
  }

  /**
   * Manages the display area and is called automatically when needed.
   * If the gel has been changed and needs to be redrawn, then 
   * <code>refresh</code> should be called. It will determine what action
   * to take. Paint will only display the image of the gel known to this
   * class, which may have to be rebuilt. The paint method makes use of
   * double buffering to speed the display.
   *
   * @see GelView#refresh
   */
  public void paint(Graphics g)
  {
    // 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();
      }

    offGraphics.clearRect(0, 0, d.width, d.height);

    if(gel.getNumLanes() != 0)
      {
	offGraphics.drawImage(gelImage, GEL_HORZ_INSET, GEL_VERT_INSET, this);

	drawBins(offGraphics);
	
	drawLaneHeaders(offGraphics);

	// let the parent know about any changes so it can adjust if neccessary
	//getParent().validate();
      }

    // Flip the backscreen buffer to the front
    g.drawImage(offImage, 0, 0, this);
  }

  /**
   * Draws all the bins in the gel onto the specified graphics object.
   *
   * @param g the graphics object to draw to
   */
  public void drawBins(Graphics g)
  {
    DataList bins = gel.getBins();
    int length = bins.size();
    Bin currentBin;
    double y_pos;
    double binTopSize;
    int range;
    int height;

    // Clear the bin header area
    g.clearRect(0, 0, GEL_HORZ_INSET, getSize().height);

    for(int i=0; i < length; i++)
      {
	currentBin = (Bin) bins.dataAt(i);
	g.setColor(new Color(192, 192, 192));
	
	height = (int) (2*currentBin.getRange()/gel.getSizeInc());
	// the vertical position is given by, finding the length in pixels
	// between the location and the maximum size, adjusting the starting
	// point from the center to the edge with the range, and offsetting
	// by the position of the top of the gel.
	binTopSize = (gel.getMaxSize() - currentBin.getLocation() - 
		      currentBin.getRange());
	y_pos = binTopSize/gel.getSizeInc() + GEL_VERT_INSET;
	// if we didn't hit it exactly, subtract one so that the bin
	// will include things inside it only.
	if(y_pos > (int) y_pos)
	  y_pos += 1;

	// subtract 1 from the start pos and and 2 to the height to move
	// the border outside of the bin
	g.drawRect(GEL_HORZ_INSET, (int) y_pos - 1, gelWidth, height + 2);

	// draw the bin headers
	g.setColor(Color.lightGray);
	g.fillRoundRect(GEL_HORZ_INSET - BIN_HEADER_WIDTH, (int) y_pos - 1,
			BIN_HEADER_WIDTH, height + 2, 2, 2);
      }

    // Draw the selected bin
    //   Find the positions like above
    if(selectedBin != null)
      {
	height = (int) (2*selectedBin.getRange()/gel.getSizeInc());
	binTopSize = gel.getMaxSize() - selectedBin.getLocation() - 
	  selectedBin.getRange();
	y_pos = binTopSize/gel.getSizeInc() + GEL_VERT_INSET;
	if(y_pos > (int) y_pos)
	  y_pos += 1;
	g.setColor(Color.red);
	g.fillRoundRect(GEL_HORZ_INSET - BIN_HEADER_WIDTH, (int) y_pos - 1,
			BIN_HEADER_WIDTH, height + 2, 2, 2);
      }
  }

  /**
   * Draws the headers for the lanes, (the little grey tabs at the top
   * of the gel).
   *
   * @param g  the graphics object to draw on.
   */
  public void drawLaneHeaders(Graphics g)
  {
    int x;
    int y;

    // move to the correct vertical postion
    y = GEL_VERT_INSET - LANE_HEADER_HEIGHT;

    // get the space between lanes as well as the width
    int width = gel.getLaneWidth();
    int spacing = width + gel.getLaneBorder();

    //====================Draw the headers=================
    //
    // find out how many we need to draw
    int numLanes = gel.getLanes().size();

    g.setColor(Color.lightGray);
    
    for(int i=1; i <= numLanes; i++)
      {
	// find the correct x positioin
	x = GEL_HORZ_INSET + spacing*i - width;

	g.fillRoundRect(x, y, width, LANE_HEADER_HEIGHT, 2, 2);
      }

    //====================Draw the selection marks===========
    //
    DataList lanes = gel.getLanes();
    DataList selected = gel.getSelectedLanes();

    g.setColor(Color.black);

    // adjust the width
    //width = 4;
    width = width - 2*LANE_MARK_HORZ_INSET;

    // adjust the height
    y = y + LANE_MARK_VERT_INSET;
    
    for(int i=1; i <= numLanes; i++)
      {
	// see if it should be included.
	if((selected.contains(lanes.dataAt(i - 1))))
	  {
	    // find the correct x positioin
	    x = GEL_HORZ_INSET + spacing*i - width;
	    
	    g.fillOval(x - LANE_MARK_HORZ_INSET, y, width, LANE_MARK_HEIGHT);
	  }
      }
    
  }

  /**
   * Called when the screen needs to be updated.
   */
  public void update(Graphics g)
  {
    paint(g);
  }

  /**
   * Gives the button bar that should be used with this object. The bar
   * contains the controls for the gel.
   *
   * @return a bar with the controls for the gel, to be displayed by the
   *    parent class
   */
  public ButtonBar getButtonBar()
  {
    return gelBar;
  }

  /**
   * Lays out the components on the button bar, uses the constants declared
   * in this class to control the layout.
   */
  private void createButtonBar()
  {
    gelBar = new ButtonBar();
    gelBar.setBounds(0, 0, 640, 32);

    // Let the parent window handle the action event for the orignal
    // button bar components
    gelBar.sendActionEventsTo((ActionListener) topWindow);

    int start_x = gelBar.getFreeHorzPos();

    // ===========Add Choice box====================
    zoomSelect = new Choice();
    zoomSelect.add("50%");
    zoomSelect.add("100%");
    zoomSelect.add("150%");
    zoomSelect.add("200%");
    zoomSelect.add("400%");
    zoomSelect.add("To Bin");
    zoomSelect.add("Full Gel");
    zoomSelect.add("Other...");

    zoomSelect.select("100%");

    // set the previous zoom level and the zoom index
    zoomFactor = 1.0;
    previousZoom = 1.0;
    zoomIndex = 1;

    zoomSelect.addItemListener(this);

    gelBar.add(zoomSelect);
    // The height does not seem to get set properly for some reason.
    zoomSelect.setBounds(start_x + HORZ_SPACE, ButtonBar.VERT_INSET,
			 ZOOM_WIDTH,
			 ButtonBar.BUTTON_HEIGHT + ZOOM_HEIGHT_ADJUST);

    // adjust the starting position
    start_x += HORZ_SPACE + ZOOM_WIDTH;

    // =============Add the size ranges======================
    minSizeField = new TextField("" + gel.getMinSize());
    maxSizeField = new TextField("" + gel.getMaxSize());
    
    Label sizeL = new Label("Size:", Label.RIGHT);
    Label toL = new Label("to", Label.CENTER);

    gelBar.add(minSizeField);
    gelBar.add(maxSizeField);
    gelBar.add(sizeL);
    gelBar.add(toL);

    minSizeField.addKeyListener(this);
    maxSizeField.addKeyListener(this);

    sizeL.setBounds(start_x + HORZ_SPACE, ButtonBar.VERT_INSET,
		    SIZEL_WIDTH, ButtonBar.BUTTON_HEIGHT);
    start_x += HORZ_SPACE + SIZEL_WIDTH;

    minSizeField.setBounds(start_x, ButtonBar.VERT_INSET,
			   FIELD_WIDTH, ButtonBar.BUTTON_HEIGHT);
    start_x += FIELD_WIDTH;

    toL.setBounds(start_x, ButtonBar.VERT_INSET,
			   TOL_WIDTH, ButtonBar.BUTTON_HEIGHT);
    start_x += TOL_WIDTH;

    maxSizeField.setBounds(start_x, ButtonBar.VERT_INSET,
			   FIELD_WIDTH, ButtonBar.BUTTON_HEIGHT);
    start_x += FIELD_WIDTH;

    //===================Add the intensity controls===============
    intensityField = new TextField("1.0");
    Label intensityL = new Label("Intensity: ", Label.RIGHT);

    gelBar.add(intensityL);
    gelBar.add(intensityField);

    intensityField.addKeyListener(this);

    intensityL.setBounds(start_x + HORZ_SPACE, ButtonBar.VERT_INSET,
			 INTENSITYL_WIDTH, ButtonBar.BUTTON_HEIGHT);
    start_x += INTENSITYL_WIDTH + HORZ_SPACE;

    intensityField.setBounds(start_x, ButtonBar.VERT_INSET,
			   FIELD_WIDTH, ButtonBar.BUTTON_HEIGHT);
    start_x += FIELD_WIDTH;

    //=================Add button to read in new values===========
    String imgPath = ProgOptions.homePath + REL_PATH + REFRESH_FILE;
    refreshButton = new ImgButton(ButtonBar.retrieveImage(imgPath));
    gelBar.add(refreshButton);
    refreshButton.setBounds(start_x + HORZ_SPACE, ButtonBar.VERT_INSET,
			    ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
    refreshButton.addActionListener(this);
    start_x += HORZ_SPACE + ButtonBar.BUTTON_WIDTH;

    //================Add bin button=============================
    binButton = new ImgButton(ButtonBar.retrieveImage(ProgOptions.homePath +
						      REL_PATH + BIN_FILE));
    gelBar.add(binButton);
    binButton.setBounds(start_x + HORZ_SPACE, ButtonBar.VERT_INSET, 
			ButtonBar.BUTTON_WIDTH, ButtonBar.BUTTON_HEIGHT);
    binButton.addActionListener(this);
  }

  /**
   * Returns a bar that displays information about the gel. 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 bar, no bottom border.
    infoBar = new Bar(true, false);
    infoBar.setLayout(null);
    infoLabel = new Label("Testing program info display");
    infoBar.add(infoLabel);
    infoLabel.setBounds(LABEL_H_INSET, LABEL_V_INSET,
			LABEL_WIDTH, LABEL_HEIGHT);
    infoBar.setBounds(0, 0, 600, FragmentMap.BAR_HEIGHT);
  }

  /**
   * Deal with action events. The two buttons are the bin dialog button,
   * which when clicked displays the dialog, and the refreshButton, which
   * reads in the sizes and intensity from the bar and then redisplays the
   * gel.
   */
  public void actionPerformed(ActionEvent e)
  {
    try{
      if(e.getSource() == binButton)
	{
	  binDialog.setBins(gel.getBins());
	  binDialog.setVisible(true);
	}
      else if(e.getSource() == refreshButton)
	{
	  readDisplay();

	  // redisplay
	  refresh(true);
	}
    }
    catch(Throwable error)
      {
	errorDialog.showError(error);
      }
  }

  /**
   * Handle events for the choice box, which adjusts the zoom.
   */
  public void itemStateChanged(ItemEvent e)
  {
    try{
      if(e.getSource() == zoomSelect)
	{
	  // Select the zoom level and set the scale factor;
	  switch(zoomSelect.getSelectedIndex())
	    {
	    case 0: //50%
	      zoomFactor = 0.5;
	      zoomIndex = 0;
	      break;
	    case 1: //100%
	      zoomFactor = 1;
	      zoomIndex = 1;
	      break;
	    case 2: //150%
	      zoomFactor = 1.5;
	      zoomIndex = 2;
	      break;
	    case 3: // 200
	      zoomFactor = 2;
	      zoomIndex = 3;
	      break;
	    case 4: // 400%
	      zoomFactor = 4;
	      zoomIndex = 4;
	      break;
	    case 5: // To Bin
	      if(currentBin == null)
		throw new NoDataError("No bin selected for zoom.");
	      
	      gel.setMinSize(currentBin.getLocation() - currentBin.getRange());
	      gel.setMaxSize(currentBin.getLocation() + currentBin.getRange());
	      
	      // update the display
	      minSizeField.setText("" + gel.getMinSize());
	      maxSizeField.setText("" + gel.getMaxSize());
	      
	    // adjust the zoom to force the rebuilding of the image
	      zoomFactor = previousZoom;
	      previousZoom = -1;
	      break;
	    case 6: // Full
	      // set the size, based on lane 0
	      setGelSizeToMax(0);
	      
	      // adjust the zoom to force the rebuilding of the image
	      zoomFactor = previousZoom;
	      previousZoom = -1;
	      break;
	    case 7: // Other
	      zoomFactor = entryDialog.getPercentage();
	      if(zoomFactor == -1)
		{
		  // user cancelled entry
		  zoomFactor = previousZoom;
		}
	      else
		{
		  zoomSelect.add("" + Math.round(zoomFactor*100) + "%");
		  zoomIndex = 8;
		}
	      break; 
	    case 8: // Specific other value
	      // We don't need to change anything since we should already
	      // be at this level.
	      break;
	    }

	  // Set the selction to the correct magnification
	  zoomSelect.select(zoomIndex);
	  
	  // remove the other entry if we're done with it.
	  if( (zoomSelect.getItemCount() > 8) && (zoomIndex < 8) )
	    zoomSelect.remove(8);
	  
	  //Make sure that we really changed it
	  if(zoomFactor != previousZoom)
	    zoom(zoomFactor);
	}
    }
    catch(Throwable error)
      {
	errorDialog.showError(error);
      }
  }

  /**
   * Rebuilds the gel image so that its size is increased by the
   * specified amout. Both the lane width and lane border are
   * calculated seperately, so rounding errors coul be a factor.
   * 
   * @param zoomFactor  the factor to multiply the length and width
   *   by to obtain the new image.
   *
   * @exception OutOfMemoryError  these images can get really big,
   *      so zooming in too much may cause the program to run out
   *      of memory.
   */
  private void zoom(double zoomFactor)
  {
    // adjust the values for length, lanewidth, and border width
    gelLength = (int)(INITIAL_LENGTH * zoomFactor);
    gel.setLaneWidth( (int) (DEFAULT_LANE_WIDTH * zoomFactor));
    gel.setLaneBorder( (int) (DEFAULT_BORDER_WIDTH * zoomFactor));
        
    // redisplay and rebuild the gel
    refresh(true);

    // remember where we are.
    previousZoom = zoomFactor;
  }

  /**
   * Reads in the values for the minimum size, maximum size, and 
   * intensity form the respective text fields on the button bar.
   *
   * @exception NumberFormatException  if the entries are not valid
   *       numbers.
   */
  private void readDisplay()
  {
    double minSize = 0;
    double maxSize = 0;
    double intensity = 0;
    // read in the values
    try{
      minSize = (new Double(minSizeField.getText())).doubleValue();
      maxSize = (new Double(maxSizeField.getText())).doubleValue();
      intensity = (new Double(intensityField.getText())).doubleValue();
    }
    catch(NumberFormatException e)
      {
	throw new NumberFormatException("Size and intensity must be numbers.");
      }
    
    // set the sizes
    gel.setMinSize(minSize);
    gel.setMaxSize(maxSize);
    
    // scale the intensity
    gel.setIntensity(gel.getGlobalMaxIntensity()/intensity);  
  }
  
  /**
   * Handle the click of the mouse. Includes double clicks.
   * When the mouse is clicked on the gel, a bin is added.
   * If it is double-clicked on a lane header, that lane is selected.
   */
  public void mouseClicked(MouseEvent e)
  {
    //==============Single Click actions=======================
    if(e.getClickCount() == 1)
      {
	switch(overType)
	  {
	  case BIN:
	    break;
	  case BLANK:
	    selectedBin = new Bin(sizeLoc);
	    gel.addBin(selectedBin);
	    refresh(false);
	    break;
	  case BIN_BORDER_TOP:
	  case BIN_BORDER_BOT:
	    break;
	  case BIN_HEAD:
	    selectedBin = currentBin;
	    refresh(false);
	    break;
	  }
      }
    
    // ====================Double click===================
    if(e.getClickCount() == 2)
      {
	switch(overType)
	  {
	  case LANE_HEAD:
	    // get the lane, and toggle it's selection state by
	    // adding or removing it from the selected list
	    gel.toggleSelectedLane((Lane) gel.laneAt(laneLoc - 1));

	    // invalidate the scoring on the current bin since it now
	    // has a different set of lanes
	    if(currentBin != null)
	      currentBin.setScore(false);

	    refresh(false);
	    break;
	  }
      }
  }

  /**
   * Don't care about this event.
   */
  public void mouseEntered(MouseEvent e)
  {
    //Don't care about this
  }

  /**
   * Don't care about this event.
   */
  public void mouseExited(MouseEvent e)
  {
    // Don't care about this one either
  }

  /**
   * Handle the event of the mouse button being pressed. Use this to 
   * set up for drags, however, be careful not to duplicate work done be
   * the click handler. The type of action performed depends on where the
   * mouse is pressed. Over a blank area will create a new bin, while pressing
   * down on a bin border will prepare the program to move that border.
   * The types of places that the mouse can be over are defined by constants
   * and the <code>mouseOverType</code> method. The type the mouse is 
   * currently over is stored in an instance variable and is updated whenever
   * the mouse moves.
   *
   * @see GelView#mouseOverType
   */
  public void mousePressed(MouseEvent e)
  {
    // tell drag what we're going to do
    dragMode = overType;

    // variable to say we haven't dragged yet, used to that
    // click and press/release don't both do something on one click
    dragged = false;

    dragStart = e.getPoint();

    switch(dragMode)
      {
      case BIN_BORDER_TOP:
      case BIN_BORDER_BOT:
      case BIN:
	oldBin = new Bin(currentBin.getLocation(), 
			 currentBin.getRange());
	break;
      case BLANK:
	// set up a new bin and then just pretend we're dragging the
	// bottom of it.
	oldBin = new Bin(sizeLoc, 0);
	currentBin = new Bin(sizeLoc, 0);
	gel.addBin(currentBin);
	break;
      }
  }

  /**
   * Handle the event of the mouse button being released. This will finalize
   * any dragging only if the mouse has been dragged between when this
   * is called and the press event has been called. This is since
   * press + release w/o drag = click. The action performed depends on where
   * the mouse started.
   *
   * @see GelView#mouseOverType
   */
  public void mouseReleased(MouseEvent e)
  {
    if(dragged)     // otherwise, let the click event take it
      {
	switch(dragMode)
	  {
	  case BIN_BORDER_TOP:
	  case BIN_BORDER_BOT:
	    // remove the bin so it can be sorted if need be
	    gel.removeBin(currentBin);

	    // see if the range is still positive, if it is, add it back in
	    // so the list remains sorted, if not, discard it.
	    if(currentBin.getRange() > 0)
	      {
		gel.addBin(currentBin);
		selectedBin = currentBin;
		refresh(false);
	      }
	    break;
	  case BIN:
	    // remove and then add to keep sorted
	    gel.removeBin(currentBin);
	    gel.addBin(currentBin);
	    selectedBin = currentBin;
	    refresh(false);
	    break;
	  }
      }
    else
      {
	switch(overType)
	  {
	  case BLANK:
	    // Clean up the new bin created by dragging in a blank and let
	    // click make the bin.
	    gel.removeBin(currentBin);
	    break;
	  }
      }
  }

  /**
   * Handle the dragging of the mouse. Mostly used to adjust bin borders
   * or to move bins around.
   */
  public void mouseDragged(MouseEvent e)
  {
    dragged = true;

    double adjust;
    double round;
    switch(dragMode)
      {
      case BIN_BORDER_TOP:
	// ==================adjust the border=====================
	//  Find the difference in the two points and convert to bp
	adjust = (dragStart.y - e.getY()) * gel.getSizeInc();

	// shift the center, and then increase the range
	round = Math.round((oldBin.getLocation() + adjust/2)*100)/100.0;
	currentBin.setLocation(round);
	round = Math.round((oldBin.getRange() + adjust/2)*100)/100.0;
	currentBin.setRange(round);

	refresh(false);

	infoLabel.setText("  Size: " + sizeLoc + 
			  "  Bin: " + currentBin.getLocation() +
			  " +/- " + currentBin.getRange());
	break;
      case BLANK:          //fallsthrough
      case BIN_BORDER_BOT:
	// ==================adjust the border=====================
	//  Find the difference in the two points and convert to bp
	adjust = (e.getY() - dragStart.y) * gel.getSizeInc();

	// moving down
	round = Math.round((oldBin.getLocation() - adjust/2)*100)/100.0;
	currentBin.setLocation(round);
	round = Math.round((oldBin.getRange() + adjust/2)*100)/100.0;
	currentBin.setRange(round);

	refresh(false);

	infoLabel.setText("  Size: " + sizeLoc + 
			  "  Bin: " + currentBin.getLocation() +
			  " +/- " + currentBin.getRange());
	break;	
      case BIN:
	adjust = Math.round((dragStart.y - e.getY()) * 
			    gel.getSizeInc() * 100)/100.0;

	// move the bin
	currentBin.setLocation(oldBin.getLocation() + adjust);
	
	refresh(false);

	infoLabel.setText("  Size: " + sizeLoc + 
			  "  Bin: " + currentBin.getLocation() +
			  " +/- " + currentBin.getRange());
	break;
      }
  }

  /**
   * Handle moving the mouse. It displays information about whatever the
   * mouse is pointing to in the gel. This is done by calling the 
   * <code>mouseOverType</code> method, and then displaying the appropriate
   * information.
   *
   * @see GelView#mouseOverType
   */
  public void mouseMoved(MouseEvent e)
  {
    // Find the position in bp, round to two places
    sizeLoc = Math.round(findSizeAt(e.getY())*100)/100.0;

    // Figure out which lane we're in
    laneLoc = findLaneAt(e.getX());

    // set the type we're over
    overType = mouseOverType(e.getX(), e.getY());

    // update the status
    switch(overType)
      {
      case BIN:
	setCursor(moveCursor);
	infoLabel.setText("  Size: " + sizeLoc + 
			  "  Lane: " + laneLoc +
			  "  Bin: " + currentBin.getLocation() +
			  " +/- " + currentBin.getRange());
	break;
      case BLANK:
	setCursor(standardCursor);
	infoLabel.setText("  Size: " + sizeLoc +
			  "  Lane: " + laneLoc);
	break;
      case BIN_BORDER_TOP:
      case BIN_BORDER_BOT:
	setCursor(resizeCursor);
	infoLabel.setText("  Size: " + sizeLoc + 
			  "  Drag mouse to resize bin.");
	break;
      case BIN_HEAD:
	setCursor(standardCursor);
	infoLabel.setText("Bin: " + currentBin.getLocation() +
			  " +/- " + currentBin.getRange());
	break;
      case LANE_HEAD:
	setCursor(standardCursor);
	Lane lane = (Lane) gel.getLanes().dataAt(laneLoc - 1);
	infoLabel.setText("Lane " + laneLoc + ":   " +
			  lane.getName() + " from lane " +
			  lane.getLaneNumber() + " in " +
			  lane.getGelName());
	break;
      case OFF_GEL:
	setCursor(standardCursor);
	infoLabel.setText("Gel has " + gel.getLanes().size() +
			  " lanes with " + gel.getBins().size() +
			  " bins defined.");
	break;
      }
  }

  /**
   * Determines what the mouse is pointing to. This is done be looking
   * first to see if the mouse is even on the gel. Then the position is used
   * to look for a bin at that position. If none is found, the method looks
   * for a bin up one pixel from the current position of the mouse. If it
   * finds one, then the mouse must be on the border of the bin. If not, the
   * same search is performed, except one pixel lower. This also allows the
   * method to tell which border it is on, which is important to know when
   * adjusting the borders and figuring out which direction should increase
   * the bin size and which way should decrease it. If no bin is found,
   * and the mouse is on the gel, then the mouse is assumed to be on a blank
   * portion of the gel.
   *
   * @param x  the x coordinate of the mouse, relative to this component.
   * @param y  the y coordinate of the mouse, relative to this component.
   *
   * @return the type of object the mouse is over, defined by static variables
   *         in this class.
   */
  public int mouseOverType(int x, int y)
  {
    // Do simple checks of positoin for other stuff
    if( (x < (GEL_HORZ_INSET - BIN_HEADER_WIDTH)) ||
	(x >= (GEL_HORZ_INSET + gelWidth)))
      return OFF_GEL;

    if( (y < (GEL_VERT_INSET - LANE_HEADER_HEIGHT)) ||
	(y >= (GEL_VERT_INSET + gelLength)))
      return OFF_GEL;

    // Look and see if we're on the lane headings.
    if( (y < GEL_VERT_INSET) && (y > (GEL_VERT_INSET - LANE_HEADER_HEIGHT)))
      if( x < GEL_HORZ_INSET)
	return OFF_GEL;
      else
	{
	  // skip the lane border places.
	  int remainder = (x - GEL_HORZ_INSET) % 
	    (gel.getLaneWidth() + gel.getLaneBorder());
	  if(remainder < gel.getLaneBorder())
	    return OFF_GEL;
	  else
	    return LANE_HEAD;
	}

    // See if we can find a bin at that size
    Bin tempBin = gel.binAtSize(sizeLoc);
    if(tempBin != null)
      {
	currentBin = tempBin;
	if(x >= GEL_HORZ_INSET)
	  return BIN;
	else
	  return BIN_HEAD;
      }

    // Look up and see if we land in a bin. If so, we're on it's bin
    // border.
    tempBin = gel.binAtSize(findSizeAt(y - 1));
    if(tempBin != null)
      {
	currentBin = tempBin;
	if(x >= GEL_HORZ_INSET)
	  return BIN_BORDER_BOT;
	else
	  return BIN_HEAD;
      }

    // Look down and see if we land in a bin. If so, we're on it's bin
    // border.
    tempBin = gel.binAtSize(findSizeAt(y + 1));
    if(tempBin != null)
      {
	currentBin = tempBin;
	if(x >= GEL_HORZ_INSET)
	  return BIN_BORDER_TOP;
	else
	  return BIN_HEAD;
      }

    // Do another boundry check, but since we already have the bin head
    // if it exists, we can make that check closer.
    if( (x < GEL_HORZ_INSET) ||
	(x >= (GEL_HORZ_INSET + gelWidth)))
      return OFF_GEL;

    return BLANK;
  }

  /**
   * Watches for the enter key to be pushed in a text field, and then
   * refreshes the gel when it is.
   */
  public void keyReleased(KeyEvent e)
  {
    // Assume that a release of the enter key constitutes the typing
    // of the enter key. It is very unlikely that it got pushed down
    // somewhere else and released here.

    // There are only three listeners, so enter the values if the enter
    // key is pushed
    if (e.getKeyCode() == KeyEvent.VK_ENTER)
      {
	readDisplay();
	refresh(true);
      }
  }

  /**
   * Calculates the size in bp that corresponds to the screen location
   * given.
   *
   * @param pos   the verticle position to calculate the size in bp for
   *
   * @return   the size
   */
  public double findSizeAt(int pos)
  {
    return (gel.getMaxSize() - (pos - GEL_VERT_INSET)*gel.getSizeInc());
  }

  /**
   * Gives the position in the gel of the lane at the given size. The
   * lanes are numbered 1 through n, where 1 is the rightmost lane.
   */
  public int findLaneAt(int pos)
  {
    return 1+(pos - GEL_HORZ_INSET)/(gel.getLaneWidth() + gel.getLaneBorder());
  }

  /**
   * Called when the image on screen should be redrawn. For example, when
   * a bin has been added. <b>This will not read in a new image if the
   * image has been changed.</b>
   *
   * @param rebuildGel  if this is true, then the gel image will be 
   *        recreated from the data in the gel, using the current
   *        gel settings.
   *
   * @see Gel
   */
  public void refresh(boolean rebuildGel)
  {
    if(rebuildGel)
      {
	rebuild();
	// Let the parent know about the changes.
	getParent().validate();
      }

    repaint();
  }

  /**
   * Recreates the gel image from the data. This should be used if the
   * size or intensity parameters of the gel have changed. Or if a larger
   * image is desired, etc.
   */
  public void rebuild()
  {
    infoLabel.setText("Updating gel image...");
    // remake the pixel array
    pixel = gel.getPixels(gelLength);
    
    // get the width
    gelWidth = pixel.length/gelLength;
    
    // Make the new image and store it.
    imgSource = new MemoryImageSource(gelWidth, gelLength, pixel,
				      0, gelWidth);
    gelImage = createImage(imgSource);
    
    // set the size
    setSize(gelWidth + 2*GEL_HORZ_INSET, gelLength +2*GEL_VERT_INSET);
    infoLabel.setText("Gel image update complete.");
  }

  /**
   * Gives the bin that is currently selected on the gel. This bin should
   * be the one that appears highlighted in a different color.
   *
   * @return the bin
   */
  public Bin getCurrentBin()
  {
    return selectedBin;
  }

  /**
   * Sets the selection to the specified bin. The bin must be valid,
   * but no check will be performed. As long as the bin is retrieved
   * from the list, there shouldn't be any problem. This is used
   * to keep the bin selection synchronized across bins.
   *
   * @param bin  the bin to highlight as selected.
   */
  public void setCurrentBin(Bin bin)
  {
    selectedBin = bin;
    refresh(false);
  }

  /**
   * Set the length of the gel image to the specified value. The image will
   * be recreated when this is called.
   *
   * @param length  the length in pixels.
   */
  public void setGelLength(int length)
  {
    gelLength = length;
    rebuild();
  }

  /**
   * Cleans up before this class is disposed off.
   *
   * @exception Throwable something bad happened.
   */
  public void finalize() throws Throwable
  {
    gelImage.flush();
    super.finalize();
  }

  // ==================Unused methods required by interfaces=================
  /**Unused*/public void keyPressed(KeyEvent e) {}
  /**Unused*/public void keyTyped(KeyEvent e) {}
}
