/*
 * -------------------------------------------------------------------------
 *      $Id: NLPanel.java,v 1.3 2004/04/07 20:08:27 estewart Exp $
 * -------------------------------------------------------------------------
 *      Copyright (c) 1999 Visual Numerics Inc. All Rights Reserved.
 *
 *      This software is confidential information which is proprietary to
 *      and a trade secret of Visual Numerics, Inc.  Use, duplication or
 *      disclosure is subject to the terms of an appropriate license
 *      agreement.
 *
 *      VISUAL NUMERICS MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE
 *      SUITABILITY OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING
 *      BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY,
 *      FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. VISUAL
 *      NUMERICS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 *      AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR
 *      ITS DERIVATIVES.
 *--------------------------------------------------------------------------
 */

/*
 * NLPanel.java
 *
 * Created on August 19, 2003, 2:15 PM
 */

package com.imsl.demo.fitting;

import com.imsl.chart.*;
import com.imsl.math.*;
import java.util.Vector;
import java.awt.event.*;
import javax.swing.JOptionPane;

public class NLPanel extends javax.swing.JPanel implements ActionListener, MouseListener {

    private Chart chart;
    private AxisXY axis;
    private Data point, line;
    private Vector dataX, dataY;
    private double[] xRange, yRange, solution, xGuess;
    private NonlinLeastSquares.Function function;
    private ChartFunction chartFunction;
    private javax.swing.JFrame parentFrame;
    private javax.swing.JRadioButtonMenuItem[] jRadioButtons;
    private final int numData = 4;
    private final String dataName[] = {"Blank", "Sunflower Growth", "Metal Response", "Plume Trajectory"};
    private int nParams;
    private StringBuffer infoSolution;
    private String[] infoParams;
    private String infoData, infoModel;
    private javax.swing.ImageIcon iconModel;
    private javax.swing.JPopupMenu popMenu;

    /** Creates new form CsPanel */
    public NLPanel(javax.swing.JFrame parent) {
        this.parentFrame = parent;
        dataX = new Vector();
        dataY = new Vector();
        
        initComponents();
        setPreferredSize(new java.awt.Dimension(parent.getSize().width, (int)(0.85*parent.getSize().height)));
        popMenu = createPopMenu();
        jButtonMore.addMouseListener(new PopupListener());
        jButtonGenerate.addActionListener(this);
        jButtonEnter.addActionListener(this);
        jButtonReset.addActionListener(this);
        radioSig.addActionListener(this);
        radioExp.addActionListener(this);
        radioPow.addActionListener(this);
        
        addDataButtons();
        setChart();
        jPanelChart.setPreferredSize(new java.awt.Dimension(parent.getSize().width, (int)(0.66*parent.getSize().height)));
        getData();
        setModel();
        update();
    }
    
    private void setChart() {
        chart = jPanelChart.getChart();
        chart.getLegend().setPaint(true);
        chart.getLegend().setViewport(0.4,0.55,0.01,0.5);
        axis = new AxisXY(chart);
        axis.setViewport(0.12,0.88,0.05,0.88);

        // Set axes ranges
        axis.getAxisX().setAutoscaleInput(AxisXY.AUTOSCALE_OFF);
        axis.getAxisY().setAutoscaleInput(AxisXY.AUTOSCALE_OFF);
        axis.getAxisX().setWindow(0.0, 50.0);
        axis.getAxisY().setWindow(0.0, 75.0);
        axis.getAxisX().setTextFormat(new java.text.DecimalFormat("###.#"));
        axis.getAxisY().setTextFormat(new java.text.DecimalFormat("###.#"));
       
        // Add MouseListeren to the chart.
        jPanelChart.addMouseListener(this);
    }
    
    // add the available data sets to the dataMenu
    private void addDataButtons() {
        jRadioButtons = new javax.swing.JRadioButtonMenuItem[numData];
        javax.swing.ButtonGroup group = new javax.swing.ButtonGroup();
        
        for (int i=0; i<numData; i++) {
            jRadioButtons[i] = new javax.swing.JRadioButtonMenuItem();
            jRadioButtons[i].setText(dataName[i]);
            
            jRadioButtons[i].addActionListener(new java.awt.event.ActionListener() {
                public void actionPerformed(java.awt.event.ActionEvent evt) {
                    getData();
                    update();
                }
            });
            
            group.add(jRadioButtons[i]);
            ((FittingMain)parentFrame).getDataMenu().add(jRadioButtons[i]);
        }
        jRadioButtons[1].setSelected(true);
    }        
   
    // clear and load data sets determined by the menu selection
    public void getData() {
        dataX.clear();
        dataY.clear();
        xRange = new double[2];
        yRange = new double[2];
        String xTitle = "X Value";
        String yTitle = "Y Value";
        String xFormat = "###.#";
        String yFormat = "###.#";
        int selected = -1;
        for (int i=0; i<numData; i++) {
            if (jRadioButtons[i].isSelected()) {
                selected = i;
                break;
            }
        }
        switch (selected) {
            case 0: // blank
                xRange[0] = 0.0;
                xRange[1] = 50.0;
                yRange[0] = 0.0;
                yRange[1] = 50.0;
                this.infoData = "User data.\n";
                break;
            case 1: // sunflower growth data
                double[] x1 = {7,14,21,28,35,42,49,56,63,70,77,85};
                double[] y1 = {17.93,36.36,67.67,98.10,131.00,169.50,205.50,228.30,247.10,250.50,253.80,254.50};
                this.infoData = "The growth rate of a sunflower as documented \n"+
                                "in Reed, H. S. and Holland, R. H. (1919), Growth \n"+
                                "of sunflower seeds; Proceedings of the National \n"+
                                "Academy of Sciences, volume 5, p. 140.\n\n"+
                                "As with much biological data, this growth data \n"+
                                "is well-represented by a sigmoidal model.\n";
                for (int i=0; i<x1.length; i++) {
                    dataX.add(new Double(x1[i]));
                    dataY.add(new Double(y1[i]));
                }
                xTitle = "Time, days";
                yTitle = "Height, cm";
                xFormat = "##";
                yFormat = "###";
                xRange[0] = 0.0;
                xRange[1] = 100.0;
                yRange[0] = 0.0;
                yRange[1] = 300.0;
                break;
            case 2: // NIST ultrasonic responce and metal distance.
                double[] x2 = {80,71,64,58,52,44,37,31,26,22,19,17,15};
                double[] y2 = {0.5,0.625,0.75,0.875,1.0,1.25,1.5,1.75,2.0,2.25,2.5,2.75,3.00};
                this.infoData = "This data is from the calibration of a metal\n"+
                                "alloy block subjected to ultrasound waves. It was\n"+
                                "obtained from a NIST report available at:\n"+ 
                                "http://ts.nist.gov/ts/htdocs/230/233/calibrations/mechanical/PUBS/E127-98.pdf\n\n"+
                                "This data shows exponential decay and can be\n"+
                                "represented well by an exponential model.\n\n";
                for (int i=0; i<x2.length; i++) {
                    dataX.add(new Double(x2[i]));
                    dataY.add(new Double(y2[i]));
                }
                xTitle = "Echo Amplitude, %";
                yTitle = "Metal Distance, inches";
                xFormat = "###";
                yFormat = "#.#";
                xRange[0] = 0.0;
                xRange[1] = 100.0;
                yRange[0] = 0.0;
                yRange[1] = 5.0;
                break;
            case 3: // Turbulent chimney plume trajectory
                double[] x3 = {2.90,5.7,8.69,10.63,11.58,12.76,14.48,15.19,17.37,21.27,25.52,30.38,31.90,38.28,42.54,45.58,51.05,53.17,60.77,63.81,63.81,75.96,76.57,91.15};
                double[] y3 = {1.88,2.46,2.95,3.90,3.32,4.01,3.61,4.30,3.87,5.29,5.48,6.37,6.82,6.68,7.38,7.96,8.16,8.36,9.71,9.36,9.47,11.14,10.03,12.41};
                this.infoData = "This data set is from an experimental study on \n"+
                                "the behavior of chimney plumes in a steady wind.\n"+
                                "The source is: Huq., P. and Stewart, E. J. (1996)\n"+ 
                                "A laboratory study of buoyant plumes in laminar\n"+
                                "and turbulent crossflows. Atmospheric Environment\n"+
                                "30 (7), 1125-1135.\n\n"+
                                "This trajectory data is historically known to \n"+
                                "follow a 2/3 power law.\n\n";
                for (int i=0; i<x3.length; i++) {
                    dataX.add(new Double(x3[i]));
                    dataY.add(new Double(y3[i]));
                }
                xTitle = "Distance from Source, m";
                yTitle = "Plume Elevation, m";
                xFormat = "###";
                yFormat = "##.#";
                xRange[0] = 0.0;
                xRange[1] = 100.0;
                yRange[0] = 0.0;
                yRange[1] = 15.0;
                break;
        }
        axis.getAxisX().getAxisTitle().setTitle(xTitle);
        axis.getAxisY().getAxisTitle().setTitle(yTitle);
        axis.getAxisX().setWindow(xRange);
        axis.getAxisY().setWindow(yRange);
        axis.getAxisX().setTextFormat(new java.text.DecimalFormat(xFormat));
        axis.getAxisY().setTextFormat(new java.text.DecimalFormat(yFormat));
    }        

    /** load the function to solve and to plot */
    private void setModel() {
        int selected = -1;
        if (radioSig.isSelected()) selected = 0;
        if (radioExp.isSelected()) selected = 1;
        if (radioPow.isSelected()) selected = 2;
        
        switch (selected) {
            case 0: // sigmoidal model
                function =  new NonlinLeastSquares.Function() {
                    public void f(double theta[], double f[]) {
                        // theta[0] = at
                        // theta[1] = ab
                        // theta[2] = x0
                        // theta[3] = w
                        for (int i=0; i<dataX.size(); i++) {
                            f[i] = (theta[1] + (theta[0] - theta[1])/
                                (1.0 + JMath.exp(- (((Double)dataX.get(i)).doubleValue() - theta[2])/theta[3]))) - ((Double)dataY.get(i)).doubleValue();
                        }
                    }
                };
                chartFunction = new ChartFunction() {
                    public double f(double x) {
                        return (solution[1] + (solution[0] - solution[1])/
                            (1.0 + JMath.exp(- (x - solution[2])/solution[3])));
                    }
                };
                this.nParams = 4;
                this.xGuess = new double[] {300, 0, 50.0, 10.0};
                this.infoParams = new String[] {"At", "Ab", "xo", "w"};
                this.infoModel = "A sigmoidal model, with its easily identified\n"+
                                 "S shape, is used to represent many biological\n"+
                                 "processes.\n\n"+
                                 "The parameters to solve for are At, Ab, xo, and w\n\n";
                this.iconModel = new javax.swing.ImageIcon(getClass().getResource("/com/imsl/demo/fitting/eq_sig.gif"));
                break;
            case 1: // exponential over linear
                function =  new NonlinLeastSquares.Function() {
                    public void f(double theta[], double f[]) {
                        // theta[0] = b1
                        // theta[1] = b2
                        // theta[2] = b3
                        for (int i=0; i<dataX.size(); i++) {
                            double xx = ((Double)dataX.get(i)).doubleValue();
                            f[i] = JMath.exp(-theta[0]*xx)/(theta[1] + theta[2]*xx) - ((Double)dataY.get(i)).doubleValue();
                        }
                    }
                };
                chartFunction = new ChartFunction() {
                    public double f(double x) {
                        return JMath.exp(-solution[0]*x)/(solution[1] + solution[2]*x);
                    }
                };
                this.nParams = 3;
                this.xGuess = new double[] {0.1, 0.1, 0.1};
                this.infoParams = new String[] {"b1", "b2", "b3"};
                this.infoModel = "This is an exponential model with additional\n"+
                                 "inverse dependence on x, providing a better \n"+
                                 "fit than a normal exponential for some cases.\n\n"+
                                 "The parameters to solve for are b1, b2, and b3\n\n";
                this.iconModel = new javax.swing.ImageIcon(getClass().getResource("/com/imsl/demo/fitting/eq_exp.gif"));
                break;
            case 2: // two-parameter 2/3 power law
                function =  new NonlinLeastSquares.Function() {
                    public void f(double theta[], double f[]) {
                        // theta[0] = betaT
                        // theta[1] = beta
                        for (int i=0; i<dataX.size(); i++) {
                            double xx = ((Double)dataX.get(i)).doubleValue();
                            f[i] = JMath.pow((3/(theta[0]*theta[0]))*(0.05*xx*xx+xx)+JMath.pow((0.32/theta[1]),3),1./3.)-(0.32/theta[1])-((Double)dataY.get(i)).doubleValue();
                        }
                    }
                };
                chartFunction = new ChartFunction() {
                    public double f(double x) {
                        return JMath.pow((3/(solution[0]*solution[0]))*(0.05*x*x+x)+JMath.pow((0.32/solution[1]),3),1./3.)-(0.32/solution[1]);
                    }
                };
                this.nParams = 2;
                this.xGuess = new double[] {0.4, 0.6};
                this.infoParams = new String[] {"betaT", "beta"};
                this.infoModel = "A power law appears as a straight line on log\n"+
                                 "axes. For this model, we know the power is 2/3\n"+
                                 "and other adjustment variables are computed.\n\n"+
                                 "The parameters to solve for are betaT and beta\n\n";
                this.iconModel = new javax.swing.ImageIcon(getClass().getResource("/com/imsl/demo/fitting/eq_pow.gif"));
                break;
        }
    }
    
    private void getSolution() {
        NonlinLeastSquares zs = new NonlinLeastSquares(dataX.size(), nParams);
        zs.setGuess(xGuess);
        zs.setMaxIterations(500);
        solution = new double[nParams];
        if (dataX.size() > nParams) {
            try {
                solution = zs.solve(function);
            } catch (NonlinLeastSquares.TooManyIterationsException tmie) {
                    javax.swing.JOptionPane.showMessageDialog(this, /*tmie.getMessage()*/
                    "Too many iterations required. Select \n\n"
                    + "          More Info -> Data\n\nfor information "
                    + "about the best model\nchoice for the chosen data set.",
                    "Convergence Details", javax.swing.JOptionPane.ERROR_MESSAGE);
            }
        }
        infoSolution = new StringBuffer("Parameter Solutions\n");
        infoSolution.append("-------------------\n");
        java.text.DecimalFormat df = new java.text.DecimalFormat("####.####");
        for (int i=0; i
    private void initComponents() {//GEN-BEGIN:initComponents
        buttonGroup = new javax.swing.ButtonGroup();
        jPanel = new javax.swing.JPanel();
        jButtonEnter = new javax.swing.JButton();
        jButtonGenerate = new javax.swing.JButton();
        jLabelPoints = new javax.swing.JLabel();
        numField = new javax.swing.JTextField("6", 4);
        jButtonReset = new javax.swing.JButton();
        jPanel1 = new javax.swing.JPanel();
        jLabel1 = new javax.swing.JLabel();
        jPanelChecks = new javax.swing.JPanel();
        radioSig = new javax.swing.JRadioButton();
        radioExp = new javax.swing.JRadioButton();
        radioPow = new javax.swing.JRadioButton();
        jButtonMore = new javax.swing.JButton();
        jPanelChart = new com.imsl.chart.JPanelChart();

        setLayout(new javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS));

        setPreferredSize(new java.awt.Dimension(500, 600));
        jPanel.setMaximumSize(new java.awt.Dimension(32767, 36));
        jButtonEnter.setText("Enter Points");
        jPanel.add(jButtonEnter);

        jButtonGenerate.setText("Generate");
        jPanel.add(jButtonGenerate);

        jLabelPoints.setText("# of Points: ");
        jPanel.add(jLabelPoints);

        numField.setColumns(4);
        numField.setText("6");
        jPanel.add(numField);

        jButtonReset.setText("Reset");
        jPanel.add(jButtonReset);

        add(jPanel);

        jPanel1.setMaximumSize(new java.awt.Dimension(32767, 20));
        jPanel1.setMinimumSize(new java.awt.Dimension(89, 20));
        jPanel1.setPreferredSize(new java.awt.Dimension(89, 20));
        jLabel1.setText("Select Nonlinear Model: ");
        jPanel1.add(jLabel1);

        add(jPanel1);

        jPanelChecks.setMaximumSize(new java.awt.Dimension(32767, 36));
        jPanelChecks.setPreferredSize(new java.awt.Dimension(468, 36));
        radioSig.setSelected(true);
        radioSig.setText("Sigmoidal");
        buttonGroup.add(radioSig);
        jPanelChecks.add(radioSig);

        radioExp.setText("Exponential Over Linear");
        buttonGroup.add(radioExp);
        jPanelChecks.add(radioExp);

        radioPow.setText("2/3 Power Law  ");
        buttonGroup.add(radioPow);
        jPanelChecks.add(radioPow);

        jButtonMore.setText("More Info");
        jPanelChecks.add(jButtonMore);

        add(jPanelChecks);

        jPanelChart.setPreferredSize(new java.awt.Dimension(500, 500));
        add(jPanelChart);

    }//GEN-END:initComponents
    
    // Implement ActionListener
    public void actionPerformed(ActionEvent e) {
        if (e.getActionCommand().equals("Enter Points")) {
            // Enter points using a text area.
            javax.swing.JTextArea textArea = new javax.swing.JTextArea(10, 5);
            javax.swing.JScrollPane scroll = new javax.swing.JScrollPane(textArea);

            int option = 0;
            try {
                option = JOptionPane.showConfirmDialog(this, scroll,
                    "Enter Points", JOptionPane.OK_CANCEL_OPTION);
                if (option == JOptionPane.CANCEL_OPTION) return;
            } catch (Exception ex) {
            }

            java.util.StringTokenizer lineToken = new java.util.StringTokenizer(textArea.getText(), "\n");
            while (lineToken.hasMoreElements()) {
                java.util.StringTokenizer token = new java.util.StringTokenizer(lineToken.nextToken(), "(){}[] ,\t\n");
                // If the line does not contain 2 numbers, then ignore.
                if (token.countTokens() == 2) {
                    try {
                        double x = Double.parseDouble(token.nextToken());
                        double y = Double.parseDouble(token.nextToken());

                        // Ignore any points that are not in the range.
                        if ((x >= xRange[0]) && (x <= xRange[1]) &&
                            (y >= yRange[0]) && (y <= yRange[1])) {
                            dataX.add(new Double(x));
                            dataY.add(new Double(y));
                        }
                    } catch (Exception ex) {
                    }
                }
            }
            update();
        } else if (e.getActionCommand().equals("Generate")) {
            int i = 0;
            try {
                i = Integer.parseInt(numField.getText());
            } catch (Exception exception) {
                i = 6;
                numField.setText("6");
            } finally {
                if (i <= 0) {
                    i = 6;
                    numField.setText("6");
                }
                for (int j = 0; j < i; j++) {
                    dataX.add(new Double(xRange[1] * Math.random() + xRange[0]));
                    dataY.add(new Double(yRange[1] * Math.random() + yRange[0]));
                }
                update();
            }
        } else if (e.getActionCommand().equals("Reset")) {
            numField.setText("6");
            dataX = new Vector();
            dataY = new Vector();
            update();
            getData();
            update();
        } else if (e.getActionCommand().equals("Data")) {
            javax.swing.JOptionPane.showMessageDialog(this, infoData, 
                "Data Details", javax.swing.JOptionPane.INFORMATION_MESSAGE);
        } else if (e.getActionCommand().equals("Model")) {
            javax.swing.JOptionPane.showMessageDialog(this, infoModel, 
                "Model Details", javax.swing.JOptionPane.INFORMATION_MESSAGE, iconModel);
        } else if (e.getActionCommand().equals("Solution")) {
            // setFont() has no affect on JOptionPane
            // see: http://developer.java.sun.com/developer/bugParade/bugs/4458089.html
            //jop.setFont(new java.awt.Font("Monospaced", java.awt.Font.PLAIN, 12));
            // so we'll do it the hard way
            javax.swing.JLabel infoLabel = new javax.swing.JLabel();

            // of course, JLabel doesn't recognize \n, so it's more work here too
            infoLabel.setText("<HTML>"+infoSolution.toString().replaceAll("\n","<BR>")+"</HTML>");
            infoLabel.setFont(new java.awt.Font("Monospaced", java.awt.Font.PLAIN, 12));
            javax.swing.JOptionPane.showMessageDialog(this, infoLabel, 
                "Solution Details", javax.swing.JOptionPane.INFORMATION_MESSAGE, iconModel);
        } else {    // this means a checkbox is changed
            setModel();
            update();
        }
    }


    // Implement MouseListener
    public void mouseClicked(MouseEvent e) {
        double user[] = {0,0};
        axis.mapDeviceToUser(e.getX(), e.getY(), user);

        // Do not add the point if it is not in the range.
        if ((user[0] < xRange[0]) || (user[0] > xRange[1]) ||
            (user[1] < yRange[0]) || (user[1] > yRange[1])) return;

        // Button is to add points.  Otherwise, remove points.
        if (e.getModifiers() == MouseEvent.BUTTON1_MASK) {
            dataX.add(new Double(user[0]));
            dataY.add(new Double(user[1]));
            update();
        } else {
            int idx = -1;
            double min = 0.01;
            final double ux = (user[0]-xRange[0])/(double)(xRange[1]-xRange[0]);
            final double uy = (user[1]-yRange[0])/(double)(yRange[1]-yRange[0]);
            
            for (int i = 0; i < dataX.size(); i++) {
                double x = (((Double)dataX.get(i)).doubleValue()-xRange[0])/
                            (double)(xRange[1]-xRange[0]);
                double y = (((Double)dataY.get(i)).doubleValue()-yRange[0])/
                            (double)(yRange[1]-yRange[0]);
                double dist = Math.sqrt((ux - x)*(ux - x) + (uy - y)*(uy - y));

                if (dist < min) {
                    min = dist;
                    idx = i;
                }
            }

            if (idx != -1) {
                dataX.remove(idx);
                dataY.remove(idx);
                update();
            }
        }
    }

    public void mousePressed(MouseEvent e) {
    }

    public void mouseReleased(MouseEvent e) {
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }
    
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.ButtonGroup buttonGroup;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JLabel jLabelPoints;
    private javax.swing.JButton jButtonMore;
    private javax.swing.JButton jButtonGenerate;
    private javax.swing.JButton jButtonReset;
    private javax.swing.JTextField numField;
    private com.imsl.chart.JPanelChart jPanelChart;
    private javax.swing.JRadioButton radioPow;
    private javax.swing.JRadioButton radioExp;
    private javax.swing.JRadioButton radioSig;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JButton jButtonEnter;
    private javax.swing.JPanel jPanel;
    private javax.swing.JPanel jPanelChecks;
    // End of variables declaration//GEN-END:variables

    class PopupListener extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
            maybeShowPopup(e);
        }

        public void mouseReleased(MouseEvent e) {
            maybeShowPopup(e);
        }

        private void maybeShowPopup(MouseEvent e) {
            if (e.isPopupTrigger() || e.getModifiers() == MouseEvent.BUTTON1_MASK) {
                popMenu.show(e.getComponent(), e.getX(), e.getY());
            }
        }
    }
}