/*
 * -------------------------------------------------------------------------
 *      $Id: Forecasting.java,v 1.4 2005/03/14 14:55:08 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.
 *--------------------------------------------------------------------------
 */

package com.imsl.demo.forecast;
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.text.*;
import com.imsl.stat.*;
import com.imsl.chart.*;
import com.imsl.demo.gallery.Describe;


public class Forecasting extends JFrameChart implements ActionListener, ChangeListener, MouseListener, MouseMotionListener {
    private Chart chart;
    private AxisXY axis;
    private Data data, forecastData, upperLimit, lowerLimit;
    private JSlider slider;
    private JTextField predField, probField, displayField;
    private JRadioButtonMenuItem[] jRadioButtons;
    private final int numData = 3;
    private final String dataName[] = {"Oil", "Sales", "Sunspots"};
    private boolean toForecast, zoom, iszoom;
    private double[] from, to, current;
    private ARMA arma;
    private SimpleDateFormat dateFormat = new SimpleDateFormat("M/yyyy");
    private double[] y, x, yRange;
    private int[] yearRange;
    private String yTitle;
    private int dataIncrement;


    public Forecasting(boolean exitOnClose) {
        setTitle("ARMA Forecasting");
        if (!exitOnClose) {
            // remove the WindowListener,  installed by JFrameChart, that
            // exits the application when the window is closed.
            Object l[] = getListeners(java.awt.event.WindowListener.class);
            for (int k = 0;  k < l.length;  k++) {
                removeWindowListener((java.awt.event.WindowListener)l[k]);
            }
        }

        Describe des = new Describe(this, "/com/imsl/demo/forecast/Forecasting.html");
        des.show();
        Dimension ds = des.getSize();

        Dimension ss = getToolkit().getScreenSize();
        int h = Math.min(ss.width/2, ss.height-ds.height-32);
        int w = (int)(h/0.8);
        setSize(w, h);
        setLocation(ss.width-ds.width, ds.height);

        // Set default values.
        toForecast = zoom = false;
        chart = getChart();
        axis = new AxisXY(chart);
        axis.getAxisX().getAxisLabel().setTextFormat(dateFormat);
        axis.getAxisX().getAxisLabel().setTextAngle(90);
        axis.getAxisX().getAxisTitle().setTitle("Date");
        axis.getAxisY().setTextFormat(new java.text.DecimalFormat("###,###"));

        from = new double[2];
        to = new double[2];
        current = new double[2];

        // Add MouseListener and MouseMotionListener to chart panel.
        chart.getLegend().setPaint(true);
        getPanel().addMouseListener(this);
        getPanel().addMouseMotionListener(this);

        Container cp = getContentPane();

        JButton draw = new JButton("Forecast");
        draw.addActionListener(this);

        JButton reset = new JButton("Reset");
        reset.addActionListener(this);

        JLabel prob = new JLabel("Confidence", JLabel.CENTER);
        probField = new JTextField("0.50", 4);

        slider = new JSlider(1, 12, 6);
        slider.setPaintTicks(true);
        slider.setPaintLabels(true);
        slider.setMajorTickSpacing(1);
        slider.setMinorTickSpacing(1);
        slider.setSnapToTicks(true);
        slider.addChangeListener(this);

        JLabel pred = new JLabel("Predicts", JLabel.CENTER);
        predField = new JTextField("6", 3);
        predField.setEditable(false);

        JPanel buttonPanel = new JPanel();

        buttonPanel.add(draw);
        buttonPanel.add(prob);
        buttonPanel.add(probField);
        buttonPanel.add(slider);
        buttonPanel.add(pred);
        buttonPanel.add(predField);
        buttonPanel.add(reset);

        // Create the display panel
        JPanel displayPanel = new JPanel();

        displayField = new JTextField("Current position:  X = ??  Y = ??", 53);
        displayField.setBorder(null);
        displayField.setCaretPosition(0);
        displayField.setEditable(false);
        displayPanel.add(displayField);

        cp.add(buttonPanel, BorderLayout.NORTH);
        cp.add(displayPanel, BorderLayout.SOUTH);
        
        JMenuBar jMenu = this.getJMenuBar();
        JMenu menuData = new JMenu();
        menuData.setMnemonic('D');
        menuData.setText("Data");
        jMenu.add(menuData);
        
        jRadioButtons = new javax.swing.JRadioButtonMenuItem[numData];
        ButtonGroup group = new 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) {
                    toForecast = false;
                    zoom = false;
                    iszoom = false;
                    update();
                }
            });
            
            group.add(jRadioButtons[i]);
            menuData.add(jRadioButtons[i]);
        }
        jRadioButtons[1].setSelected(true);
       
        getData();
        drawGraph();
        setResizable(false);
    }


    //  Get the description line.
    private String getDescription() {
        DecimalFormat nf = new DecimalFormat("0.0000");
        StringBuffer sb = new StringBuffer();
        if (iszoom) {
            sb.append("Zooming In from:  ");
            sb.append("X = " + dateFormat.format(new Date((long) from[0])) + "  Y = " + nf.format(from[1]));
            sb.append("    To:    ");
            sb.append("X = " + dateFormat.format(new Date((long) to[0])) + "  Y = " + nf.format(to[1]));
        } else {
            sb.append("Cursor At:  X = " + dateFormat.format(new Date((long) current[0])) +
            "  Y = " + nf.format(current[1]));
        }
        return sb.toString();
    }

    // load up the selected data set
    private void getData() {
        int selected = -1;
        for (int i=0; i<numData; i++) {
            if (jRadioButtons[i].isSelected()) {
                selected = i;
                break;
            }
        }
        switch (selected) {
            case 2: // sunspot data
                y = new double[] {100.8, 81.6, 66.5, 34.8, 30.6, 7, 19.8, 92.5,
                    154.4, 125.9, 84.8, 68.1, 38.5, 22.8, 10.2, 24.1, 82.9,
                    132, 130.9, 118.1, 89.9, 66.6, 60, 46.9, 41, 21.3, 16,
                    6.4, 4.1, 6.8, 14.5, 34, 45, 43.1, 47.5, 42.2, 28.1, 10.1,
                    8.1, 2.5, 0, 1.4, 5, 12.2, 13.9, 35.4, 45.8, 41.1, 30.4,
                    23.9, 15.7, 6.6, 4, 1.8, 8.5, 16.6, 36.3, 49.7, 62.5, 67,
                    71, 47.8, 27.5, 8.5, 13.2, 56.9, 121.5, 138.3, 103.2,
                    85.8, 63.2, 36.8, 24.2, 10.7, 15, 40.1, 61.5, 98.5, 124.3,
                    95.9, 66.5, 64.5, 54.2, 39, 20.6, 6.7, 4.3, 22.8, 54.8,
                    93.8, 95.7, 77.2, 59.1, 44, 47, 30.5, 16.3, 7.3, 37.3,
                    73.9};
                yearRange = new int[] {1770, 1870, 1910};
                yRange = new double[] {-50, 200};
                dataIncrement = GregorianCalendar.YEAR;
                yTitle = "Number of Sun Spots per Year";
                break;

            case 1: // champagne sales data
                y = new double[] {2815,  2672,  2755,  2721,  2946,  3036,  2282,  2212,  2922,  4301,  5764,  7132,
                    2541,  2475,  3031,  3266,  3776,  3230,  3028,  1759,  3595,  4474,  6838,  8357,
                    3113,  3006,  4047,  3523,  3937,  3986,  3260,  1573,  3528,  5211,  7614,  9254,
                    5375,  3088,  3718,  4514,  4520,  4539,  3663,  1643,  4739,  5428,  8314, 10651,
                    3633,  4292,  4154,  4121,  4647,  4753,  3965,  1723,  5048,  6922,  9858, 11331,
                    4016,  3957,  4510,  4276,  4968,  4677,  3523,  1821,  5222,  6873, 10803, 13916,
                    2639,  2899,  3370,  3740,  2927,  3986,  4217,  1738,  5221,  6424,  9842, 13076,
                    3934,  3162,  4286,  4676,  5010,  4874,  4633,  1659,  5951,  6981,  9851, 12670};
                yearRange = new int[] {1964, 1972, 1975};
                yRange = new double[] {0, 14000};
                dataIncrement = GregorianCalendar.MONTH;
                yTitle = "Cases of Champagne Sold per Month";
                break;
                
            case 0: // west texas crude oil price
                y = new double[] {23,15.4,12.6,12.8,15.4,13.5,11.5,15,14.9,14.9,
                    15.1,16.1,18.6,17.7,18.3,18.6,19.4,20,21.3,20.2,19.5,19.8,19,
                    17.2,17.1,16.7,16.2,17.8,17.4,16.5,15.5,15.5,14.4,13.8,13.9,
                    16.2,17.9,17.8,19.4,21,20,20,19.6,18.5,19.5,20,19.8,21,22.6,
                    22.1,20.4,18.5,18.2,16.8,18.6,27.1,33.6,36,32.3,27.3,24.9,20.5,
                    19.8,20.8,21.2,20.2,21.4,21.6,21.8,23.2,22.4,19.5,18.8,19,18.9,
                    20.2,20.9,22.3,21.7,21.3,21.9,21.6,20.3,19.4,19,20,20.3,20.2,
                    19.9,19,17.8,18,17.5,18.1,16.6,14.5,15,14.7,14.6,16.3,17.8,19,
                    19.6,18.3,17.4,17.7,18.1,17.1,17.9,18.5,18.5,19.8,19.7,18.4,
                    17.3,18,18.2,17.4,17.9,19,18.8,19,21.3,23.5,21.2,20.4,21.3,21.9,
                    23.9,24.9,23.7,25.3,25.1,22.2,20.9,19.7,20.8,19.1,19.6,19.9,
                    19.7,21.2,20.1,18.3,16.7,16,15,15.4,14.8,13.6,14,13.3,14.9,14.3,
                    12.9,11.2,12.4,12,14.6,17.3,17.7,17.8,20,21.2,23.8,22.6,24.8,
                    26.1,27.2,29.3,29.8,25.7,28.7,31.8,29.7,31.2,33.8,33,34.4,28.4,
                    29.5,29.6,27.2,27.4,28.6,27.6,26.4,27.4,25.8,22.2,19.6,19.3,
                    19.6,20.7,24.4,26.2,27,25.5,26.9,28.3,29.6,28.8,26.2};
                yearRange = new int[] {1986, 2003, 2006};
                yRange = new double[] {0, 50};
                dataIncrement = GregorianCalendar.MONTH;
                yTitle = "West Texas Crude Oil, Dollars per Barrel";
                break;
        }
        
        x = new double[y.length];
        for (int i = 0; i < y.length; i++) {
            switch (dataIncrement) {
                case GregorianCalendar.YEAR:
                    x[i] = (new GregorianCalendar(yearRange[0]+i, 0, 1)).getTimeInMillis();
                    break;
                case GregorianCalendar.MONTH:
                    x[i] = (new GregorianCalendar(yearRange[0], i, 1)).getTimeInMillis();
                    break;
            }
        }
    }
    
    
    // Draw the graph.
    private void drawGraph() {
        // If in zooming mode, then zoom the graph.  Otherwise, use default ranges.
        if (zoom) {
            axis.getAxisX().setAutoscaleInput(AxisXY.AUTOSCALE_OFF);
            axis.getAxisY().setAutoscaleInput(AxisXY.AUTOSCALE_OFF);

            axis.getAxisX().setWindow(Math.min(from[0], to[0]), Math.max(from[0], to[0]));
            axis.getAxisY().setWindow(Math.min(from[1], to[1]), Math.max(from[1], to[1]));
        } else {
            axis.getAxisX().setAutoscaleInput(AxisXY.AUTOSCALE_OFF);
            axis.getAxisX().setWindow((new GregorianCalendar(yearRange[0], 0, 1)).getTimeInMillis(),
                (new GregorianCalendar(yearRange[2], 0, 1)).getTimeInMillis());
            axis.getAxisY().setAutoscaleInput(AxisXY.AUTOSCALE_OFF);
            axis.getAxisY().setWindow(yRange);
        }
        axis.getAxisY().getAxisTitle().setTitle(yTitle);

        // Draw the actual data.
        data = new Data(axis, x, y);
        data.setTitle("Actual data");
        data.setDataType(Data.DATA_TYPE_LINE | Data.DATA_TYPE_MARKER);
        data.setMarkerType(Data.MARKER_TYPE_FILLED_CIRCLE);
        data.setMarkerSize(0.7);
        data.setMarkerColor(Color.blue);
        data.setLineColor(Color.blue);

        // Use ARMA class (ARMA(2,1)) to get the forecast using backorigin = 3.
        // Number of predicts and confidence level are from the interface.
        if (toForecast) {
            try {
                int num = slider.getValue();

                // Do this if Forecast is clicked.
                if (arma == null) {
                    arma = new ARMA(2, 1, y);
                    arma.setRelativeError(0.0);
                    arma.setMaxIterations(0);
                    arma.compute();
                    arma.setConfidence(Double.parseDouble(probField.getText()));
                    arma.setBackwardOrigin(3);
                }

                double[][] forecast = arma.forecast(num);

                double[] tmp = new double[num];
                double[] x1 = new double[num];

                // Draw the forecast data.
                for (int i = 0; i < num; i++) {
                    tmp[i] = forecast[i][3];
                    switch (dataIncrement) {
                        case GregorianCalendar.YEAR: 
                            x1[i] = (new GregorianCalendar(yearRange[1]+i, 0, 1)).getTimeInMillis();
                            break;
                        case GregorianCalendar.MONTH: 
                            x1[i] = (new GregorianCalendar(yearRange[1], i, 1)).getTimeInMillis();
                            break;
                    }
                }

                // Draw upper and lower limits.
                double[] dev = arma.getDeviations();
                double[] ul = new double[dev.length];
                double[] ll = new double[dev.length];
                double min = 0.0;

                for (int i = 0; i < dev.length; i++) {
                    ul[i] = tmp[i] + dev[i];
                    ll[i] = tmp[i] - dev[i];
                    if (i == 0) min = ll[0];
                    if (min > ll[i]) min = ll[i];
                }
                min = min - 0.01;


                upperLimit = new Data(axis, x1, ul);
                upperLimit.setTitle("Upper Limit");
                upperLimit.setDataType(Data.DATA_TYPE_LINE | Data.DATA_TYPE_FILL);
                upperLimit.setReference(min);
                upperLimit.setFillColor(Color.yellow);
                upperLimit.setFillOutlineColor(Color.white);

                lowerLimit = new Data(axis, x1, ll);
                lowerLimit.setTitle("Lower Limit");
                lowerLimit.setDataType(Data.DATA_TYPE_LINE | Data.DATA_TYPE_FILL);
                lowerLimit.setReference(min);
                lowerLimit.setFillColor(Color.white);
                lowerLimit.setFillOutlineColor(Color.white);

                forecastData = new Data(axis, x1, tmp);
                forecastData.setTitle("Forecast");
                forecastData.setDataType(Data.DATA_TYPE_LINE);
                forecastData.setLineColor(Color.red);
            } catch (Exception e) {
            }
        }
    }


    // Get the information from the interface and redraw the chart.
    private void update() {
        double tmp = 0.0;

        // Check the range for the confidence level.  Use 0.50 if there is
        // an error in the input.
        try {
            tmp = Double.parseDouble(probField.getText());
        } catch (Exception exception) {
            tmp = 0.50;
            probField.setText("0.50");
        } finally {
            if (data != null) data.remove();
            if (forecastData != null) forecastData.remove();
            if (upperLimit != null) upperLimit.remove();
            if (lowerLimit != null) lowerLimit.remove();
            if ((tmp <= 0.0) || (tmp >= 1.0)) probField.setText("0.50");
            getData();
            drawGraph();
            repaint();
        }
    }


    // Implement ActionListener
    public void actionPerformed(ActionEvent e) {
        // If Reset is clicked, re-size the chart using autoscale.
        // If Draw is clicked, redraw the chart.
        if (e.getActionCommand().equals("Reset")) {
            zoom = false;
        } else {
            toForecast = true;
            arma = null;
        }
        update();
    }



    // Implement ChangeListener
    public void stateChanged(ChangeEvent e) {
        // Update text field for number of predicts.
        predField.setText(Integer.toString(slider.getValue()));
    }



    // Implement MouseListener
    public void mouseClicked(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
        // Get the starting position for zooming.
        axis.mapDeviceToUser(e.getX(), e.getY(), from);
        axis.mapDeviceToUser(e.getX(), e.getY(), to);
        zoom = true;
        iszoom = true;
    }

    public void mouseReleased(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();

        // The selection must be from 2 different points.
        int[] point = new int[2];
        axis.mapUserToDevice(from[0], from[1], point);
        if (point[0] == x) return;
        if (point[1] == y) return;

        // Get the ending position for zooming.
        axis.mapDeviceToUser(x, y, to);

        iszoom = false;
        update();
    }

    public void mouseEntered(MouseEvent e) {
        setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));
    }

    public void mouseExited(MouseEvent e) {
        setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
    }



    // Implement MouseMotionListener
    public void mouseDragged(MouseEvent e) {
        // Draw the selection square and update display information.
        paint(e, true);
        displayField.setText(getDescription());
    }

    public void mouseMoved(MouseEvent e) {
        // Update display information.
        axis.mapDeviceToUser(e.getX(), e.getY(), current);
        displayField.setText(getDescription());
    }

    // Draw the selection in different color.
    protected void paint(MouseEvent mouseEvent, boolean erase) {
        Graphics2D g = (Graphics2D) Forecasting.this.getPanel().getGraphics();
        if (erase)  paint(g);
        axis.mapDeviceToUser(mouseEvent.getX(), mouseEvent.getY(), to);
        paint(g);
        g.dispose();
    }

    protected void paint(Graphics2D g) {
        Color colorFill = new Color(255,0,255,128);
        Color colorOutline = new Color(0,255,0);

        int[] pointA = new int[2];
        int[] pointB = new int[2];

        axis.mapUserToDevice(from[0], from[1], pointA);
        axis.mapUserToDevice(to[0], to[1], pointB);

        Shape shape = new Rectangle(Math.min(pointA[0], pointB[0]),
        Math.min(pointA[1], pointB[1]), Math.abs(pointA[0]-pointB[0]),
        Math.abs(pointA[1]-pointB[1]));
        g.setXORMode(colorOutline);
        g.draw(shape);
        g.setXORMode(colorFill);
        g.fill(shape);
    }


    public static void main(String args[]) {
        boolean exitOnClose = true;
        if (args.length > 0  && args[0].equals("-noexit"))  exitOnClose = false;
        new Forecasting(exitOnClose).show();
    }
}