//=====================================================================
// File:    CutoffSlider.java
// Class:   CutoffSlider
// 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.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Polygon;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import AFLPcore.DataList;
import AFLPcore.Cutoff;
import AFLPcore.CutoffFunction;
import AFLPcore.Lane;
import AFLPcore.Option;

// Use this as the default cutoff type.
import AFLPcore.LinearCutoff;

/**
 * This class provides a way to manipulate a cutoff with the mouse.
 * It should be told several important pieces of information, specifically
 * the cutoff to display, the lanes to apply the cutoff too, the scale
 * for coverting pixels to height, and the starting position of the cutoff.
 * The cutoff will be displayed as a bar with arrows at positions 
 * corresponding to the levels of the cutoff. The cutoff can be changed
 * by dragging the arrows or by clicking and having the arrows snap to
 * the location. The class also provides a message that gives some info
 * about the slider, normally what a cutoff was set to.
 *
 * @author James J. Benham
 * @version 1.0.0
 * @date August 11, 1998
 */

public class CutoffSlider extends Canvas implements MouseListener,
                                                    MouseMotionListener
{
  private static int BAR_WIDTH = 6;
  private static int BAR_H_INSET = 10;
  private static int ARROW_WIDTH = 13;
  private static int ARROW_HEIGHT = 9;
  private static int ARROW_H_INSET = BAR_H_INSET;

  private static final int MARK = 0;
  private static final int BLANK = 1;
  
  private Cutoff cutoff;
  private DataList applyToLanes;
  private String message;
  private Polygon arrow;

  private CutoffFunction currentFunction;

  private double scale;
  private double size;

  private int type;
  private int currentLevel;

  /**
   * Create a new cutoff slider. It will be initialized with a
   * default cutoff. This should be changed using the
   * <code>setCutoff</code> method.
   */
  public CutoffSlider()
  {
    cutoff = new Cutoff(0, 1);
    applyToLanes = new DataList();
    message = "";
    scale = 0;
    size = 0;
    currentFunction = new LinearCutoff();

    addMouseListener(this);
    addMouseMotionListener(this);
        
    arrow = createArrow();
  }

  /**
   * Adds the specified <code>CutoffFucntion</code> to all of the lanes
   * known to this class. If a cutoff does not already exist at the
   * position this slider applies to, then a new cutoff is added. Otherwise,
   * the cutoff is replaced.
   *
   * @param ct  the function to add.
   *
   * @see CutoffSlider#setSize
   * @see CutoffSlider#setLanes
   */
  public void addCutoffFunction(CutoffFunction ct)
  {
    Lane ln;
    Cutoff ctff = null;
    for(int i=0; i < applyToLanes.size(); i++)
      {
	ln = (Lane) applyToLanes.dataAt(i);
	ctff = ln.cutoffUnder(size);

	
	// see if we need to create a new cutoff function for this region.
 	if(ctff.getStartPos() < size)
 	  {
 	    ctff = (Cutoff) ctff.clone();
 	    ctff.setStartPos(size);
 	    ln.addCutoff(ctff);
 	  }

	ctff.addFunction(ct);
      }

    // set the cutoff that this class is using to the one in the first
    // lane, since it might have changed.
    cutoff = ((Lane) applyToLanes.dataAt(0)).cutoffUnder(size);
  }

  /**
   * Sets the cutoff that this slider is based on. The slider will use
   * this to determine the levels, etc.
   *
   * @param cutoff   the cutoff to use
   */
  public void setCutoff(Cutoff cutoff)
  {
    this.cutoff = cutoff;
  }

  /**
   * This determines where the cutoff should be applied in the lanes.
   * A cutoff applies from <code>size</code> to the next cutoff.
   * This class will use this value when it adds new cutoffs to lanes.
   *
   * @param size  the location in bp, where this cutoff should first apply.
   */
  public void setSize(double size)
  {
    this.size = size;
  }

  /**
   * Gets a message describing the current state of the slider.
   *
   * @return info about the slider
   */
  public synchronized String getMessage()
  {
    return message;
  }

  /**
   * Sets the info message for the slider.
   *
   * @param s  the message
   */
  private synchronized void setMessage(String s)
  {
    message = s;
  }

  /**
   * Draws a bar with arrows at the different cutoff levels.
   * Called every time this objects needs to be redisplayed.
   */
  public void paint(Graphics g)
  {
    // find the height;
    int height = getSize().height;

    // First, draw the bar.
    g.setColor(Color.lightGray);
    g.fillRect(BAR_H_INSET, 0, BAR_WIDTH, height);

    // ============Draw an arrow for every cutoff position======
    // make sure we have a cutoff, there might not be one, especially
    // if no bin is selected.
    if(cutoff != null)
      {
	g.setColor(Color.black);
	int yPos;
	int numLevels = cutoff.getNumLevels();
	for(int i=0; i < numLevels; i++)
	  {
	    // find the position of the cutoff
	    yPos = getSize().height - 1 - (int) (scale* cutoff.getCutoff(size,
									 i));
	    // adjust for the size of the arrow
	    yPos -= ARROW_HEIGHT/2;
	    // move the arrow
	    arrow.translate(0, yPos);
	    g.fillPolygon(arrow);
	    // move it back
	    arrow.translate(0, -yPos);
	  }
      }
  }

  /**
   * Sets the lanes known to the cutoff. These are the lanes that any
   * changes to the cutoff wil be applied to.
   *
   * @param lanes  the lanes to have their cutoffs modified by the slider.
   */
  public void setLanes(DataList lanes)
  {
    applyToLanes = lanes;
  }

  /**
   * Sets the scale for the cutoff slider. The scale is used to 
   * convert between pixels and intensity.
   *
   * @param scale  the intensity per pixel.
   */
  public void setScale(double scale)
  {
    this.scale = scale;
  }

  /**
   * Handles the mouse clicks. If the mouse is clicked over a blank
   * (non-arrow) part of the bar, then one of the cutoff levels will be 
   * set to the point the mouse clicked on. First, a level above the point
   * the mouse clicked will be searched for. If this is not found, then
   * the method looks for a level below the click point.
   */
  public void mouseClicked(MouseEvent e) 
  {
    if(type == BLANK)
      {
	int level = 0;

	// Find the level above the mouse and use that.
	// If there isn't one above, try below
	int yPos;
	int numLevels = cutoff.getNumLevels();
	for(int i=0; i < numLevels; i++)
	  {
	    // find the position of the cutoff
	    yPos = getSize().height - 1 - (int) (scale* cutoff.getCutoff(size,
									 i));
	    level = i;
	
	    // see if we are below the mouse event yet
	    if( yPos < e.getY())
	      break; // reached the point we want;
	  }

	setFunction(e.getY(), level);
	refresh();
      }
  }

  /** Unused */
  public void mousePressed(MouseEvent e)
  {
    if(type == MARK)
      {
      }
  }

  /** Unused */
  public void mouseReleased(MouseEvent e)
  {
  }

  /**
   * Determines if the mouse is over an arrow or a blank spot.
   */
  public void mouseMoved(MouseEvent e)
  {
    type = overType(e.getX(), e.getY());
  }

  /**
   * If the mouse is over an arrow, it moves the associated function
   * to the new location.
   */
  public void mouseDragged(MouseEvent e)
  {
    if(type == MARK)
      {
	setFunction(e.getY(), currentLevel);
	refresh();
      }
  }

  /**
   * Determines if the given point is in a blank area or in an arrow.
   *
   * @param x  the x position
   * @param y  the y position
   */
  public int overType(int x, int y)
  {
    int yPos;

    currentLevel = -1;


    if( x < BAR_H_INSET)
      return BLANK;

    // see if we are over a mark
    int numLevels = cutoff.getNumLevels();
    for(int i=0; i < numLevels; i++)
      {
	// find the position of the cutoff
	yPos = getSize().height - 1 - (int) (scale* cutoff.getCutoff(size, i));

	// see if the y coordinate is in the arrow range
	if( (y >= (yPos - ARROW_HEIGHT/2)) &&
	    (y <= (yPos + ARROW_HEIGHT/2)))
	  {
	    currentLevel = i;
	    return MARK;
	  }
      }

    // otherwise, we must be over a blank;
    return BLANK;
  }

  /**
   * Adjusts the cutoff function at <code>level</code> so that it's height
   * matches the height represented by <code>pos</code>. The function
   * is adjusted for all of the lanes defined. If a cutoff function does
   * not exist at the position of this slider, then a new cutoff is added.
   * Otherwise, the existing cutoff is replaced.
   *
   * @param pos    the position, in pixels, to move the cutoff function to
   * @param level  specifies the part of the cutoff to adjust.
   *
   * @see CutoffSlider#setLanes
   * @see CutoffSlider#setSize
   */
  public void setFunction(int pos, int level)
  {
    double newHeight = (getSize().height -pos - 1)/scale;
    Lane ln;
    Cutoff ctff = null;
    for(int i = 0; i < applyToLanes.size(); i++)
      {
	ln = (Lane) applyToLanes.dataAt(i);
	ctff = ln.cutoffUnder(size);

	// see if we need to create a new cutoff function for this region.
 	if(ctff.getStartPos() < size)
 	  {
 	    ctff = (Cutoff) ctff.clone();
 	    ctff.setStartPos(size);
 	    ln.addCutoff(ctff);
 	  }
	CutoffFunction fn = ctff.getCutoffFunction(level);
	Option[] opts = fn.getOptions();
	opts[0].setValue(newHeight);
	fn.setOptions(opts);
      }

    newHeight = Math.round(newHeight * 100)/100.0;
    message = "Cutoff number " + (level + 1) + " set to " + newHeight;

    // set the cutoff that this class is using to the one in the first
    // lane, since it might have changed.
    cutoff = ((Lane) applyToLanes.dataAt(0)).cutoffUnder(size);

    refresh();
  }

  /**
   * Creates a polygon that is in the shape of the arrow used to indicate
   * different levels.
   *
   * @return  the arrow
   */
  public Polygon createArrow()
  {
    Polygon poly = new Polygon();
    poly.addPoint(ARROW_H_INSET, 0);
    poly.addPoint(ARROW_H_INSET + 2, 0);
    poly.addPoint(ARROW_H_INSET + 2, 1);
    poly.addPoint(ARROW_H_INSET, 1);
    poly.addPoint(ARROW_H_INSET, 0);
    poly.addPoint(ARROW_H_INSET + ARROW_WIDTH - 1, ARROW_HEIGHT/2);
    poly.addPoint(ARROW_H_INSET + ARROW_WIDTH, ARROW_HEIGHT/2);
    poly.addPoint(ARROW_H_INSET, ARROW_HEIGHT);

    return poly;
  }

  /**
   * Causes any changes in the underlying data to be shown on the screen.
   */
  public void refresh()
  {
    repaint();
  }

  //================== unused methods from interfaces==================
  /**Unused*/public void mouseEntered(MouseEvent e) {}
  /**Unused*/public void mouseExited(MouseEvent e) {}
}
