
/**
 * SternVolmerPlugin.java
 * A simple plugin for analyzing stern-volmer quenching
 * Created: June 8, 2007
 *
 * @author Nathan Stevens
 * @version 0.1 6/9/2007
 */
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.table.*;
import java.io.*;
import java.text.*;
import java.util.*;

// for chatting and doing line fitting
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.DefaultXYItemRenderer;
import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.function.Function2D;
import org.jfree.data.function.LineFunction2D;
import org.jfree.data.general.DatasetUtilities;
import org.jfree.data.statistics.Regression;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

public class SternVolmerPlugin extends JFrame implements InstruViewPlugin, ActionListener, 
ListSelectionListener {
  private static final String VERSION = "0.1 (6-9-07)"; // the plugin version
  private String name = "Stern-Volmer"; // the name of the dialog
  private InstruView instruView= null; // the main instruview object
  private JTextField minTF = null;
  private JTextField maxTF = null;
  private JTextField lifetimeTF = null;
  private JLabel slopeLB = null;
  private JLabel rateLB = null;
  private JList emList = null;  // hold the emission data
  private JTextArea textArea = null; // where text data
  private TitledBorder emBorder = null; // title border for emission data
  private JComboBox unitsCB = null;
  private Vector emData = null;  // holds the emission
  private int[] sdEM = null;  // data selected in emList
  
  // array to hold the calculated results
  private double[][] results = null;
  private double[] coefficients = null;
  
  // this constructor isn't really used
  public SternVolmerPlugin() { }
  
  // the main constructor
  public SternVolmerPlugin(InstruView instruView) {
    super("SternVolmer v- " + VERSION);
    
    this.instruView = instruView;
    
    // handles clossing of this plugin
		addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent we) {
        setVisible(false);
        dispose();
      }
		});
    
    // set the layout to border layer
    getContentPane().setLayout(new BorderLayout());
    
    // some gui stuff
    JPanel panel;
    JLabel label;
    JButton button;
    TitledBorder titleBorder;
    
    // add the input parameter panel
    JPanel mp1 = new JPanel();
    mp1.setLayout(new BoxLayout(mp1, BoxLayout.Y_AXIS));
    titleBorder = BorderFactory.createTitledBorder("Input Parameters/Results ( Fo/F = 1 + kqto[Q] )");
    mp1.setBorder(titleBorder);
    
    JPanel mp1A = new JPanel();
    mp1A.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 2));
    
    label = new JLabel(" Integration Range ");
    mp1A.add(label);
    
    minTF = new JTextField("500.0", 4);
    mp1A.add(minTF);
    
    label = new JLabel(" to ");
    mp1A.add(label);
    
    maxTF = new JTextField("700.0", 4);
    mp1A.add(maxTF);
    
    label = new JLabel(" nm");
    mp1A.add(label);
    
    label = new JLabel(" || Concentration in ");
    mp1A.add(label);
    
    String[] concUnits = {"M", "mM", "uM", "nM"};
    unitsCB = new JComboBox(concUnits);
    mp1A.add(unitsCB);
    
    mp1.add(mp1A);
    
    JPanel mp1B = new JPanel();
    mp1B.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 2));
    
    label = new JLabel(" Lifetime (ns) ");
    mp1B.add(label);
    
    lifetimeTF = new JTextField("2.0", 4);
    mp1B.add(lifetimeTF);
    
    label = new JLabel(" || ");
    mp1B.add(label);
    
    label = new JLabel("<html>Slope (k<sub>q</sub>t<sub>0</sub>) =");
    mp1B.add(label);
    
    slopeLB = new JLabel(" 000.0");
    slopeLB.setForeground(Color.blue);
    mp1B.add(slopeLB);
    
    label = new JLabel(" || ");
    mp1B.add(label);
    
    label = new JLabel("<html>Rate (<i>k</i><sub>q</sub>) = ");
    mp1B.add(label);
    
    rateLB = new JLabel(" 0.0E10 ");
    rateLB.setForeground(Color.blue);
    mp1B.add(rateLB);
    
    label = new JLabel("<html>M<sup>-1</sup>s<sup>-1</sup>");
    mp1B.add(label);
    
    mp1.add(mp1B);
    
    getContentPane().add(mp1, "North");
    
    // add the panels that hold the data and results now
    JPanel mp2 = new JPanel();
    mp2.setLayout(new GridLayout(1, 2));
    
    // add the panel that allows inputing of emission data
    JPanel mp2A = new JPanel();
		mp2A.setLayout(new BoxLayout(mp2A, BoxLayout.Y_AXIS));
    emBorder = BorderFactory.createTitledBorder("Emission Data (0)");
    mp2A.setBorder(emBorder);
    
    emList = new JList();
		emList.addListSelectionListener(this);
		JScrollPane listScrollPane = new JScrollPane(emList);
		listScrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
    mp2A.add(listScrollPane);
    
    mp2A.add(Box.createRigidArea(new Dimension(0, 2)));
    
    JPanel mp2A1 = new JPanel();
    mp2A1.setLayout(new GridLayout(1, 2));
    mp2A1.setAlignmentX(Component.LEFT_ALIGNMENT);
    
    button = new JButton("Remove");
		button.addActionListener(this);
		button.setActionCommand("remove");
		mp2A1.add(button);
    
    button = new JButton("Add");
		button.addActionListener(this);
		button.setActionCommand("add");
    mp2A1.add(button);
    
    mp2A.add(mp2A1);
    mp2.add(mp2A);
    
    // add the panel that hold the absorption data list
    JPanel mp2B = new JPanel();
		mp2B.setLayout(new BoxLayout(mp2B, BoxLayout.Y_AXIS));
    titleBorder = BorderFactory.createTitledBorder("Fo/F and Fitted Data");
    mp2B.setBorder(titleBorder);
    
    textArea = new JTextArea(11, 0);
		textArea.setEditable(false);
    
    listScrollPane = new JScrollPane(textArea);
		listScrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
    mp2B.add(listScrollPane);
    
    mp2B.add(Box.createRigidArea(new Dimension(0, 2)));
    
    JPanel mp2B1 = new JPanel();
    mp2B1.setLayout(new GridLayout(1, 2));
    mp2B1.setAlignmentX(Component.LEFT_ALIGNMENT);
    
    mp2B.add(mp2B1);
    mp2.add(mp2B);
    
    getContentPane().add(mp2, "Center");
    
    // add the button to the south
    JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
    
    button = new JButton("Calculate");
    button.setMargin(new Insets(2, 5, 2, 5));
		button.setActionCommand("calculate");
		button.addActionListener(this);
    buttonPanel.add(button);
    
    button = new JButton("Plot");
    button.setMargin(new Insets(2, 5, 2, 5));
		button.setActionCommand("plot");
		button.addActionListener(this);
    buttonPanel.add(button);
    
    button = new JButton("Copy Data");
    button.setMargin(new Insets(2, 5, 2, 5));
		button.setActionCommand("copy");
		button.addActionListener(this);
    buttonPanel.add(button);
    
    button = new JButton("Clear");
    button.setMargin(new Insets(2, 5, 2, 5));
		button.setActionCommand("clear");
		button.addActionListener(this);
    buttonPanel.add(button);
    
    buttonPanel.add(Box.createHorizontalGlue());
    
    button = new JButton("Close");
    button.setMargin(new Insets(2, 5, 2, 5));
		button.setActionCommand("close");
		button.addActionListener(this);
    buttonPanel.add(button);
    
    getContentPane().add(buttonPanel, "South");
    
    // set this visible
    pack();
    setVisible(true);
    
    // now initialize the vector that holds the data
    emData = new Vector();
  }
  
  // Method to handel action events
  public void actionPerformed(ActionEvent ae) {
    String cmd = ae.getActionCommand();
    
    if(cmd.equals("calculate")) {
			calculate();
		}
    else if(cmd.equals("plot")) {
      showPlotDialog();
    }
    else if(cmd.equals("add")) {
      addEmissionData();
    }
    else if(cmd.equals("remove")) {
      removeEmissionData();
    }
    else if(cmd.equals("copy")) {
      copyData();
    }
    else if(cmd.equals("clear")) {
      clearData();
    }
    else if(cmd.equals("close")) {
      setVisible(false);
      dispose();
    }
    else {
      System.err.println("Action Command not handeled in class SternVolmerPlugin ...");
      System.err.println(cmd);
    }
  }
  
  // Method to handel list selection events
	public void valueChanged(ListSelectionEvent e) {
    if (e.getValueIsAdjusting()) {
      return;
		}
		
    JList theList = (JList)e.getSource();
    if (!theList.isSelectionEmpty()) {
      sdEM = theList.getSelectedIndices();
    }
  }
  
  // Method to calculate the rate of quenching
  private void calculate() {
    int size = emData.size();
    if(size >= 2) { // point less to do calculation on small amount of data
      results = new double[size][3];
      
      // now get the quencher concentration data from the name and check it
      SpectraData[] sda = SpectraDataUtil.getSpectraDataArray(emData);
      if(getQuencherConcentration(sda)) {
        float minWL = Float.parseFloat(minTF.getText()); // min wavelength
        float maxWL = Float.parseFloat(maxTF.getText()); // max wavelength
        float lifetime = Float.parseFloat(lifetimeTF.getText())/1.0e9f;
        
        getRelativeIntensity(sda, minWL, maxWL);
        calculateSlope();
        
        double rate = coefficients[1]/lifetime;
        
        // now printout the results
        NumberFormat nf = NumberFormat.getInstance();
        DecimalFormat df = (DecimalFormat) nf;
        df.setDecimalSeparatorAlwaysShown(true);
        
        df.applyPattern("0.00E0");
        slopeLB.setText(" " + df.format(coefficients[1]) + " ");
        
        df.applyPattern("0.00E0");
        rateLB.setText(" " + df.format(rate) + " ");
        
        String units = (String)unitsCB.getSelectedItem();
        textArea.setText("Q Conc (" + units + ")\tFo/F\tFit\n");
        
        if(units.equals("M") && (results[1][0] < 0.01)) {
          df.applyPattern("0.00E0");
        }
        else {
          df.applyPattern("0.00");
        }
        for(int i = 0; i < results.length; i++) {
          String conc = df.format(results[i][0]*getScaleFactor());
          df.applyPattern("0.00");
          String intensity = df.format(results[i][1]);
          String fit = df.format(results[i][2]);
          
          textArea.append(conc + "\t" + intensity + "\t" + fit + "\n");
        }
      }
      else {
        
      }
      
    }
    else {
      textArea.setText("Data Set To Small ...\n");
    }
  }
  
  // Method to get the concentration data
  private boolean getQuencherConcentration(SpectraData[] sda) {
    boolean good = true;
    
    for(int i = 0; i < sda.length; i++) {
      String dataName = sda[i].getName();
      int start_index = dataName.indexOf("[");
      int end_index = dataName.indexOf("]");
      
      if(start_index != -1 && end_index != -1) {
        String concString = dataName.substring(start_index + 1, end_index);
        
        // based on the units get the scale factor
        float sf = getScaleFactor();
        
        try {
          results[i][0] = Float.parseFloat(concString)/sf;
        }
        catch(NumberFormatException nfe) {
          textArea.append("(NFE)No Quencher Concentration For " + dataName + " ...\n");
          good = false;
        }
      }
      else {
        textArea.append("No Quencher Concentration For " + dataName + " ...\n");
        good = false;
      }
    }
    
    return good;
  }
  
  // Method to get the scale factor based on the units
  public float getScaleFactor() {
    float sf = 1.0f;
    String units = (String)unitsCB.getSelectedItem();
    
    if(units.equals("mM")) {
      sf = 1.0e3f;
    }
    else if(units.equals("uM")) {
      sf = 1.0e6f;
    }
    else if(units.equals("nM")) {
      sf = 1.0e9f;
    }
    else {
      sf = 1.0f;
    }
    
    return sf;
  }
  
  // Method to calculated the relative intensity using area under the curve
  private void getRelativeIntensity(SpectraData[] sda, float minWL, float maxWL) {
    float area = 0.0f;
    float max_area = 0.0f;
    
    for(int i = 0; i < sda.length; i++) {
      area = getAreaUnderCurve(sda[i].getData(), minWL, maxWL);
      results[i][1] = area;
      
      if(area > max_area) {
        max_area = area;
      }
    }
    
    // normalize the intergrated intensities to one now
    for(int i = 0; i < sda.length; i++) {
      results[i][1] = max_area/results[i][1];
      /* debug code
      System.out.println(sda[i].getName() + " " + relative_intensity[i][0] + " : " + relative_intensity[i][1]); //*/
    }
  }
  
  // Methods to calculate
  private float getAreaUnderCurve(float[][] data, float minWL, float maxWL) {
    float area = 0.0f;
    
    float dx =(data[1][0] - data[0][0]);
    
    for( int i=0; i < data.length; i++) {
      float wl = data[i][0];
      if(wl >= minWL && wl <= maxWL && (i + 1) < data.length) {
        float leftSide = data[i][1];
        float rightSide = data[i + 1][1];
        area += dx*(leftSide + rightSide) / 2.0;
      }
    }
    
    return area;
  }
  
  // Method to calculate slope
  private void calculateSlope() {
    coefficients = Regression.getOLSRegression(results);
    
    // now add this to the data array
    for(int i = 0; i < results.length; i++) {
      results[i][2] = results[i][0]*coefficients[1] + coefficients[0];
    }
  }
  
  // Method to import data
  private void addEmissionData() {
    SpectraData[] sda = instruView.emissionPanel.getSelectedSpectraData();
    if(sda != null) {
      for(int i = 0; i < sda.length; i++) {
        emData.add(sda[i]);
      }
      
      String title = "Emission Data ( " + emData.size() + " )";
      emBorder.setTitle(title);
      
      emList.setListData(emData); // add this data to the list
    }
    
    // now set the range based on the first data set
    float[][] data = sda[0].getData();
    minTF.setText("" + data[0][0]);
    maxTF.setText("" + data[data.length-1][0]);
    
    repaint();
  }
  
  // Mehod to remove any data
  private void removeEmissionData() {
    if(emData.size() > 0 && sdEM != null) {
      
      for(int i = 0; i < sdEM.length; i++) {
        int index = sdEM[i] - i;
        emData.remove(index);
      }
      
      // set the selection type to null
      sdEM = null;
      
      emList.clearSelection();
      sdEM = null;
      emList.setListData(emData); // add this data to the list
      String title = "Emission Data ( " + emData.size() + " )";
      emBorder.setTitle(title);
    }
    repaint();
  }
  
  // Method to clear all data
  private void clearData() {
    emList.clearSelection();
    emData.clear();
    
    String title = new String("Emission Data ( " + emData.size() + " )");
    emBorder.setTitle(title);
    
    emList.setListData(emData);
    results = null;
    textArea.setText("");
  }
  
  // Method to copy the information to the clipboard
  private void copyData() {
    Clipboard cb =Toolkit.getDefaultToolkit().getSystemClipboard();
    StringSelection contents = new StringSelection(textArea.getText());
    cb.setContents(contents, null);
  }
  
  // Method to plot the data
  public void showPlotDialog() {
    if(results != null) {
      SternVolmerPlotDialog dialog = new SternVolmerPlotDialog(this, results);
      dialog.pack();
      dialog.setVisible(true);
    }
  }
  
  // Method to return information about this plugin
  public String[] getPluginInformation() {
    String[] information = {"Author : Nathan Stevens",
                                            "Version : " + VERSION,
                                            "",
                                            "A simple plugin for analyzing stern-volmer quenching"};
    
    return information;
  }
  
  // Method to set any spectra data
  public void setSpectraData(SpectraData[] sda) { }
  
  // Method to add a single spectra data. not used
  public void addSpectraData(SpectraData sd) {  }
  
  // Method to set any spectra data. not used
  public void addSpectraData(SpectraData[] sda) {  }
  
  // Method to load an sdl file. not used
  public void loadSDL(ExperimentData experimentData, File file) {  }
  
  // Method to load an sdl file. not used
  public void loadSDL(ExperimentData experimentData) {  }
  
  // Method to print any messages
  public void print(String message) {
    System.out.println(message);
  }
  
  // the static class for putting it self into the class
  static class SternVolmerFactory extends PluginFactory {
    public InstruViewPlugin create(InstruView instruView) {
      return (new SternVolmerPlugin(instruView));
    }
    
    // get the experiment type
    public String getExperimentType() {
      return "sternvolmer";
    }
    
    // get information about this plugin
    public String[] getPluginInformation() {
      String[] information = {"Author : Nathan Stevens",
                                              "Version : " + VERSION,
                                              "",
                                              "A simple plugin for analyzing stern-volmer quenching"};
      
      return information;
    }
  }// -- end inner static class PlotterFactory
  
  // add this factory to the InstruviewPluginLoader factory hashtable
  static {
    InstruViewPluginLoader.pluginFactories.put("SternVolmer", new SternVolmerFactory());
  }
  
  // class the plots the data and fitted results
  public class SternVolmerPlotDialog extends JDialog {
    public SternVolmerPlotDialog() { }
    
    public SternVolmerPlotDialog(SternVolmerPlugin frame, double[][] results) {
      super(frame, "Stern-Volmer Plot", false);
      getContentPane().setLayout(new BorderLayout());
      
      NumberFormat nf = NumberFormat.getInstance();
      DecimalFormat df = (DecimalFormat) nf;
      df.setDecimalSeparatorAlwaysShown(true);
      df.applyPattern("0.0E0");
      
      // must calculate the scale factor base on smallest concentration
      double sf = getXScaleFactor(results[1][0]);
      
      XYSeries idata = new XYSeries("Experimental Data"); // The lifetime data
      XYSeries fdata = new XYSeries("Fitted Data"); // The fitted data
      
      for(int i = 0; i < results.length; i++) {
        idata.add(results[i][0]*sf, results[i][1]);
        fdata.add(results[i][0]*sf, results[i][2]);
      }
      
      XYDataset data1 = new XYSeriesCollection(idata);
      XYDataset data2 = new XYSeriesCollection(fdata);
      
      // create plot...
      String xlabel;
      if(sf != 1.0 ) {
        xlabel = "Quencher Conc. [Q] * " + df.format(sf);
      }
      else {
        xlabel = "Quencher Conc. [Q]";
      }
      
      NumberAxis xAxis = new NumberAxis(xlabel);
      xAxis.setAutoRangeIncludesZero(true);
      NumberAxis yAxis = new NumberAxis("Intensity (Fo/F)");
      yAxis.setAutoRangeIncludesZero(false);
      
      StandardXYItemRenderer renderer1 = new StandardXYItemRenderer(StandardXYItemRenderer.SHAPES);
      renderer1.setShapesFilled(true);
      
      XYPlot plot = new XYPlot(data1, xAxis, yAxis, renderer1);
      
      plot.setDataset(1, data2);
      XYItemRenderer renderer2 = new DefaultXYItemRenderer();
      renderer2.setSeriesPaint(0, Color.blue);
      plot.setRenderer(1, renderer2);
      
      // create and return the chart panel...
      JFreeChart chart = new JFreeChart(null,
      JFreeChart.DEFAULT_TITLE_FONT,
      plot, 
      true
      );
      
      ChartPanel chartPanel = new ChartPanel(chart, false);
      
      // now add to the center
      getContentPane().add(chartPanel, "Center");
    }
    
    // Method to return the scale factor
    private double getXScaleFactor(double min_Q) {
      double sf = 1.0;
      if(min_Q >= 1) {
        //do nothing
      }
      else if(min_Q >= 1.0e-3) {
        sf = 1.0e3;
      }
      else if(min_Q >= 1.0e-6) {
        sf = 1.0e6;
      }
      else if(min_Q >= 1.0e-9) {
        sf = 1.0e9;
      }
      
      return sf;
    }
  } // --end class SternVolmerlotDialog
}//--end class SternVolmerPlugin
