content: /*
   * Copyright (c) 2010 Pentaho Corporation.  All rights reserved. 
   * This software was developed by Pentaho Corporation and is provided under the terms 
   * of the GNU Lesser General Public License, Version 2.1. You may not use 
   * this file except in compliance with the license. If you need a copy of the license, 
   * please go to http://www.gnu.org/licenses/lgpl-2.1.txt. The Original Code is Time Series 
   * Forecasting.  The Initial Developer is Pentaho Corporation.
   *
   * Software distributed under the GNU Lesser Public License is distributed on an "AS IS" 
   * basis, WITHOUT WARRANTY OF ANY KIND, either express or  implied. Please refer to 
   * the license for the specific language governing your rights and limitations.
   */
  
  /*
   *    ForecastingPanel.java
   *    Copyright (C) 2010 Pentaho Corporation
   */
  
  package weka.classifiers.timeseries.gui;
  
  import java.awt.BorderLayout;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.GridLayout;
  import java.awt.Point;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.InputEvent;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.awt.event.WindowAdapter;
  import java.awt.event.WindowEvent;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.ObjectInputStream;
  import java.io.ObjectOutputStream;
  import java.io.PrintStream;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.List;
  
  import javax.swing.BorderFactory;
  import javax.swing.JButton;
  import javax.swing.JCheckBox;
  import javax.swing.JComponent;
  import javax.swing.JFileChooser;
  import javax.swing.JFrame;
  import javax.swing.JMenuItem;
  import javax.swing.JOptionPane;
  import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
  import javax.swing.JScrollPane;
  import javax.swing.JSplitPane;
  import javax.swing.JTabbedPane;
  import javax.swing.JTextArea;
  import javax.swing.JViewport;
  import javax.swing.ListSelectionModel;
  import javax.swing.event.ChangeEvent;
  import javax.swing.event.ChangeListener;
  import javax.swing.event.ListSelectionEvent;
  import javax.swing.event.ListSelectionListener;
  import javax.swing.event.TableModelEvent;
  import javax.swing.event.TableModelListener;
  import javax.swing.table.TableModel;
  
  import weka.classifiers.timeseries.AbstractForecaster;
  import weka.classifiers.timeseries.TSForecaster;
  import weka.classifiers.timeseries.WekaForecaster;
  import weka.classifiers.timeseries.core.OverlayForecaster;
  import weka.classifiers.timeseries.core.TSLagMaker;
  import weka.classifiers.timeseries.core.TSLagUser;
  import weka.classifiers.timeseries.eval.TSEvaluation;
  import weka.classifiers.timeseries.eval.graph.GraphDriver;
  import weka.core.Attribute;
  import weka.core.Instance;
  import weka.core.Instances;
  import weka.core.Utils;
  import weka.gui.BrowserHelper;
  import weka.gui.LogPanel;
  import weka.gui.Logger;
  import weka.gui.ResultHistoryPanel;
  import weka.gui.TaskLogger;
  import weka.gui.WekaTaskMonitor;
  import weka.gui.beans.KnowledgeFlowApp;
  import weka.gui.beans.TimeSeriesForecasting;
  import weka.gui.beans.TimeSeriesForecastingKFPerspective;
  
  /**
   * Main GUI panel for the forecasting environment.
   * 
   * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
   * @version $Revision: 49983 $
   */
  public class ForecastingPanel extends JPanel { 
    
    /** For serialization */
    private static final long serialVersionUID = -8415151090793037265L;
  
    /** The training instances to operate on */
    protected Instances m_instances;
    
    /** Panel for logging */
    protected Logger m_log;// = new LogPanel(new WekaTaskMonitor());
    
    /** The simple configuration panel */
    protected SimpleConfigPanel m_simpleConfigPanel;
    
    /** The advanced configuration panel */
    protected AdvancedConfigPanel m_advancedConfigPanel;    
    
    /** The current forecaster */
    protected WekaForecaster m_forecaster = new WekaForecaster();
    
    /** Tabbed pane to hold the simple and advanced config panels */
    protected JTabbedPane m_configPane = new JTabbedPane();
    
    /** The main textual output area */
    protected JTextArea m_outText = new JTextArea(20, 50);
    
    /** A panel holding and controlling the results for viewing */
    protected ResultHistoryPanel m_history = new ResultHistoryPanel(m_outText);
    
    /** Button to launch the forecaster */
    protected JButton m_startBut = new JButton("Start");
    
    /** Button to stop processing */
    protected JButton m_stopBut = new JButton("Stop");
    
    /** Button to visit help docs in browser */
    protected JButton m_helpBut = new JButton("Help");
    
    /** The split panel to divided the history/results area from the configuration */
    protected JSplitPane m_splitP;
    
    protected Thread m_runThread;
    
    /** True if we are running in the KnowledgeFlow as a perspective */
    protected boolean m_isRunningAsPerspective = false;
    
    /** The file chooser for selecting model files. */
    protected JFileChooser m_fileChooser 
      = new JFileChooser(new File(System.getProperty("user.dir")));
    
    /**
     *  For each dataset, perform a check (if a timestamp is specified)
     * just once to see if it is in ascending order
     */
    protected boolean m_sortedCheck;
    
    /**
     * Tabbed pane that holds the main text output plus tabs for
     * any generated graphs
     */
    JTabbedPane m_outputPane = new JTabbedPane();
    
    /**
     * Inner class extending Thread. Executes a forecasting task
     */
    protected class ForecastingThread extends Thread {
      
      protected boolean m_configAndBuild = true;
      
      protected WekaForecaster m_threadForecaster = null;
      
      protected String m_name;
      
      public ForecastingThread(WekaForecaster forecaster, String name) {
        m_threadForecaster = forecaster;
        m_name = name;
      }
      
      public void setConfigureAndBuild(boolean configAndBuild) {
        m_configAndBuild = configAndBuild;
      }
  
      public void run() {
  
        LogPrintStream logger = new LogPrintStream();          
        logger.println("Setting up...");
  
        String name = m_name;
        StringBuffer outBuff = null;
        
        if (name == null) {
          name = (new SimpleDateFormat("HH:mm:ss - ")).format(new Date());
          outBuff = new StringBuffer();
        }      
        
        String fname = "";
        try {            
  
          if (!m_sortedCheck) {
            sortCheck();
          }
  
          // copy the current state of things
          Instances inst = new Instances(m_instances);
  
          TSEvaluation eval = new TSEvaluation(inst, 
              m_advancedConfigPanel.getHoldoutSetSize());                
          
          if (m_configAndBuild) {
            // configure the WekaForecaster with the base
            // learner
            m_threadForecaster.setBaseForecaster(m_advancedConfigPanel.getBaseClassifier());
  
            m_simpleConfigPanel.applyToForecaster(m_threadForecaster);
            m_advancedConfigPanel.applyToForecaster(m_threadForecaster);
          }
          
          m_simpleConfigPanel.applyToEvaluation(eval, m_threadForecaster);
          m_advancedConfigPanel.applyToEvaluation(eval, m_threadForecaster);
  
          eval.setForecastFuture(m_advancedConfigPanel.getOutputFuturePredictions() ||
              m_advancedConfigPanel.getGraphFuturePredictions());
          
          if (m_threadForecaster instanceof OverlayForecaster &&
              ((OverlayForecaster)m_threadForecaster).isUsingOverlayData()) {
            if (!eval.getEvaluateOnTestData() &&
                (m_advancedConfigPanel.m_outputFutureCheckBox.isSelected() ||
                    m_advancedConfigPanel.m_graphFutureCheckBox.isSelected())) {
  
              // warn the user that future forecast can't be produced for the training data
              dontShowMessageDialog("weka.classifiers.timeseries.gui.CantFutureForecastTraining",
                  "Unable to generate a future forecast beyond the end of the training\n" +
                  "data because there is no future overlay data available. Use a holdout\n" +
                  "set for evaluation in order to simulate having \"future\" overlay\n" +
                  "data available.\n\n",
              "ForecastingPanel");
            }
            
            if (eval.getEvaluateOnTestData() && 
                (m_advancedConfigPanel.m_outputFutureCheckBox.isSelected() ||
                    m_advancedConfigPanel.m_graphFutureCheckBox.isSelected())) {
              
              // warn the user that future forecast can't be produced for the test data
              dontShowMessageDialog("weka.classifiers.timeseries.gui.CantFutureForecastTesting",
                  "Unable to generate a future forecast beyond the end of the test\n" +
                  "data because there is no future overlay data available.\n\n",
              "ForecastingPanel");
            }
          }                
  
  
          fname = m_threadForecaster.getAlgorithmName();
          
          if (m_name == null) {
            String algoName = fname.substring(0, fname.indexOf(' '));
            if (algoName.startsWith("weka.classifiers.")) {
              name += algoName.substring("weka.classifiers.".length());
            } else {
              name += algoName;
            }
          }        
          
          String lagOptions = "";
          if (m_threadForecaster instanceof TSLagUser) {
            TSLagMaker lagMaker = ((TSLagUser)m_threadForecaster).getTSLagMaker();
            lagOptions = Utils.joinOptions(lagMaker.getOptions());
          }
          
          if (lagOptions.length() > 0 && m_name == null) {
            name += " [" + lagOptions + "]";
          }
          
          if (m_name == null) {
            outBuff.append("=== Run information ===\n\n");
          } else {
            outBuff = m_history.getNamedBuffer(name);
            outBuff.append("\n=== Model re-evaluation===\n\n");
          }
          
          outBuff.append("Scheme:\n\t" + fname).append("\n\n");
          
          if (lagOptions.length() > 0) {
            outBuff.append("Lagged and derived variable options:\n\t").
            append(lagOptions + "\n\n");
          }
  
          outBuff.append("Relation:     " + inst.relationName() + '\n');
          outBuff.append("Instances:    " + inst.numInstances() + '\n');
          outBuff.append("Attributes:   " + inst.numAttributes() + '\n');
          if (inst.numAttributes() < 100) {
            for (int i = 0; i < inst.numAttributes(); i++) {
              outBuff.append("              " + inst.attribute(i).name()
                  + '\n');
            }
          } else {
            outBuff.append("              [list of attributes omitted]\n");
          }
          
          if (m_configAndBuild) {
            m_history.addResult(name, outBuff);
          }
          m_history.setSingle(name);
  
          if (m_log != null) {
            m_log.logMessage("Started " + fname);
            if (m_configAndBuild) {
              logger.println("Training forecaster...");
            }
            if (m_log instanceof TaskLogger) {
              ((TaskLogger)m_log).taskStarted();
            }
          }
  
          Instances trainInst = eval.getTrainingData();
          if (m_configAndBuild) {
            m_threadForecaster.buildForecaster(trainInst, logger);
            outBuff.append("\n" + m_threadForecaster.toString());
            m_history.updateResult(name);
          }
  
          if (eval.getEvaluateOnTrainingData() || 
              eval.getEvaluateOnTestData()) {
            logger.println("Evaluating...");
          }
  
          // evaluate the forecaster
          eval.evaluateForecaster(m_threadForecaster, false, logger);
  
          // output any predictions
          if (m_advancedConfigPanel.getOutputPredictionsAtStep() > 0) {
            int step = m_advancedConfigPanel.getOutputPredictionsAtStep();
            String targetName = m_advancedConfigPanel.getOutputPredictionsTarget();
            String fieldsToForecast = m_threadForecaster.getFieldsToForecast();
            if (!fieldsToForecast.contains(targetName)) {
              throw new Exception("Cannot output predictions for \"" 
                  + targetName + "\" because that field is not being predicted.");
            }
            if (eval.getTrainingData() != null &&
                eval.getEvaluateOnTrainingData()) {
              String predString = eval.printPredictionsForTrainingData("=== Predictions " +
                  "for training data: " + targetName + " (" 
                  + step + (step > 1 ? "-steps ahead)" : "-step ahead)") + " ===", 
                  targetName, step, eval.getPrimeWindowSize());
              outBuff.append("\n").append(predString);
            }              
  
            if (eval.getTestData() != null) {
              int instanceNumOffset = 
                (eval.getTrainingData() != null && 
                    m_advancedConfigPanel.getHoldoutSetSize() > 0) 
                    ? eval.getTrainingData().numInstances()
                        : 0;
  
                    String predString = eval.printPredictionsForTestData("=== Predictions " +
                        "for test data: " + targetName + " (" 
                        + step + (step > 1 ? "-steps ahead)" : "-step ahead)") + " ===", 
                        targetName, step, instanceNumOffset);
                    outBuff.append("\n").append(predString);
            }
            m_history.updateResult(name);
          }
  
          // output any future predictions
          if (m_advancedConfigPanel.getOutputFuturePredictions()) {
            if (eval.getTrainingData() != null /*&& 
                  eval.getEvaluateOnTrainingData()*/) {
              outBuff.append("\n=== Future predictions from end of training data ===\n");
              outBuff.append(eval.printFutureTrainingForecast(m_threadForecaster));
            }
  
            if (eval.getTestData() != null && 
                eval.getEvaluateOnTestData()) {
              outBuff.append("\n=== Future predictions from end of test data ===\n");
              outBuff.append(eval.printFutureTestForecast(m_threadForecaster));
            }
            m_history.updateResult(name);
          }
  
          // evaluation summary
          if (eval.getEvaluateOnTrainingData() || eval.getEvaluateOnTestData()) {
            outBuff.append("\n" + eval.toSummaryString());
            m_history.updateResult(name);
          }
  
          // result object list
          List<Object> resultList = (m_configAndBuild) 
          ? new ArrayList<Object>() 
              : (List<Object>)m_history.getNamedObject(name);
          
          if (!m_configAndBuild) {
            // go through and remove any JPanels
            List<Object> newResultList = new ArrayList<Object>();
            for (int z = 0; z < resultList.size(); z++) {
              if (resultList.get(z) instanceof TSForecaster || 
                  resultList.get(z) instanceof Instances) {
                newResultList.add(resultList.get(z));
              }
            }
            resultList = newResultList;
          }
  
          // handle graphs
          List<JPanel> graphList = new ArrayList<JPanel>();
          // graph predictions for targets at specific step
          if (m_advancedConfigPanel.getGraphPredictionsAtStep() > 0) {
            int stepNum = m_advancedConfigPanel.getGraphPredictionsAtStep();
            List<String> targets = 
              AbstractForecaster.stringToList(m_threadForecaster.getFieldsToForecast());
            if (eval.getTrainingData() != null && eval.getEvaluateOnTrainingData()) {                
              JPanel trainTargetsAtStep = 
                eval.graphPredictionsForTargetsOnTraining(GraphDriver.getDefaultDriver(), 
                    m_threadForecaster, targets, stepNum, eval.getPrimeWindowSize());
              trainTargetsAtStep.setToolTipText("Train pred. for targets");
  
              graphList.add(trainTargetsAtStep);
            }
  
            if (eval.getTestData() != null && eval.getEvaluateOnTestData()) {
              int instanceOffset = (eval.getPrimeForTestDataWithTestData())
              ? eval.getPrimeWindowSize()
                  : 0;
              JPanel testTargetsAtStep =
                eval.graphPredictionsForTargetsOnTesting(GraphDriver.getDefaultDriver(), 
                    m_threadForecaster, targets, stepNum, instanceOffset);
              testTargetsAtStep.setToolTipText("Test pred. for targets");
  
              graphList.add(testTargetsAtStep);
            }
          }
  
          // graph predictions for specific target at selected steps
          if (m_advancedConfigPanel.getGraphTargetForSteps()) {
            String fieldsToForecast = m_threadForecaster.getFieldsToForecast();
            String selectedTarget = 
              m_advancedConfigPanel.getGraphTargetForStepsTarget();
  
            if (!fieldsToForecast.contains(selectedTarget)) {
              throw new Exception("Cannot graph predictions for \"" 
                  + selectedTarget + "\" because that field is not being predicted.");
            }
            List<Integer> stepList = 
              m_advancedConfigPanel.getGraphTargetForStepsStepList();
  
            if (eval.getTrainingData() != null && eval.getEvaluateOnTrainingData()) {
              JPanel trainStepsForTarget = 
                eval.graphPredictionsForStepsOnTraining(GraphDriver.getDefaultDriver(), 
                    m_threadForecaster, selectedTarget, stepList, eval.getPrimeWindowSize());
              trainStepsForTarget.setToolTipText("Train pred. at steps");
  
              graphList.add(trainStepsForTarget);
            }
  
            if (eval.getTestData() != null && eval.getEvaluateOnTestData()) {
              int instanceOffset = (eval.getPrimeForTestDataWithTestData())
              ? eval.getPrimeWindowSize()
                  : 0;
              JPanel testStepsForTarget =
                eval.graphPredictionsForStepsOnTesting(GraphDriver.getDefaultDriver(), 
                    m_threadForecaster, selectedTarget, stepList, instanceOffset);
              testStepsForTarget.setToolTipText("Test pred. at steps");
  
              graphList.add(testStepsForTarget);
            }
          }
  
          // graph future predictions
          if (m_advancedConfigPanel.getGraphFuturePredictions()) {
            if (eval.getTrainingData() != null /*&& eval.getEvaluateOnTrainingData()*/) {
              try {
                JPanel trainFuture = eval.
                graphFutureForecastOnTraining(GraphDriver.getDefaultDriver(), 
                    m_threadForecaster, AbstractForecaster.
                    stringToList(m_threadForecaster.getFieldsToForecast()));
                trainFuture.setToolTipText("Train future pred.");
  
                graphList.add(trainFuture);
              } catch (Exception ex) {
                if (m_threadForecaster instanceof OverlayForecaster &&
                    ((OverlayForecaster)m_threadForecaster).isUsingOverlayData()) {
                  if (m_log != null) {
                    m_log.logMessage("Unable to graph future forecast for training " +
                    		"data because no future overlay data is available");
                  }
                } else {
                  if (m_log != null) {
                    m_log.logMessage("Unable to graph future forecast for " +
                    		"training data: " + ex.getMessage());
                  }
                }
              }
            }
  
            if (eval.getTestData() != null && eval.getEvaluateOnTestData()) {
              try {
                JPanel testFuture = eval.
                graphFutureForecastOnTesting(GraphDriver.getDefaultDriver(), 
                    m_threadForecaster, AbstractForecaster.
                    stringToList(m_threadForecaster.getFieldsToForecast()));
                testFuture.setToolTipText("Test future pred.");
  
                graphList.add(testFuture);
              } catch (Exception ex) {
                if (m_threadForecaster instanceof OverlayForecaster &&
                    ((OverlayForecaster)m_threadForecaster).isUsingOverlayData()) {
                  if (m_log != null) {
                    m_log.logMessage("Unable to graph future forecast for test " +
                          "data because no future overlay data is available");
                  }
                } else {
                  if (m_log != null) {
                    m_log.logMessage("Unable to graph future forecast for test " +
                    		"data: " + ex.getMessage());
                  }
                }
              }
            }
          }
  
          try {
            if (m_configAndBuild) {
              WekaForecaster copiedForecaster = 
                (WekaForecaster)AbstractForecaster.makeCopy(m_threadForecaster);
              resultList.add(copiedForecaster);
              Instances trainHeader = new Instances(trainInst, 0);
              resultList.add(trainHeader);
            }
          } catch (Exception ex) {
            if (m_log != null) {
              logger.println("Problem copying model.");
              m_log.logMessage("Problem copying model: " + ex.getMessage());
            }
            ex.printStackTrace();
          }
  
          if (graphList.size() > 0) {
            resultList.add(graphList);              
          }
  
          m_history.addObject(name, resultList);
          if (graphList.size() > 0) {
            updateMainTabs(name);
          }
  
          if (m_log != null) {
            m_log.logMessage("Finished " + fname);
            logger.println("OK");
          }
        } catch (Exception ex) {
          ex.printStackTrace();
          if (m_log != null) {
            m_log.logMessage(ex.getMessage());
            logger.println("Problem evaluating forecaster");
          }
          JOptionPane.showMessageDialog(ForecastingPanel.this, 
              "Problem evaluating forecaster:\n" + ex.getMessage(),
              "Evaluate forecaster", JOptionPane.ERROR_MESSAGE);
  
        } finally {
  
          if (isInterrupted()) {
            if (m_log != null) {
              m_log.logMessage("Interrupted " + fname);
              logger.println("Interrupted");
            }
          }
  
          synchronized (this) {
            m_startBut.setEnabled(true);
            m_stopBut.setEnabled(false);
            m_runThread = null;
          }
  
          if (m_log instanceof TaskLogger) {
            ((TaskLogger)m_log).taskFinished();
          }
        }
      }
    }
    
    /**
     * Constructor.
     * 
     * @param log the log to use (may be null for no log)
     * @param displayWelcome true if the welcome message is to be
     * displayed as the first log entry
     */
    public ForecastingPanel(LogPanel log, boolean displayWelcome) {
      this(log, displayWelcome, true, false);
    }
    
    /**
     * Constructor
     * 
     * @param log the log to use (may be null for no log)
     * @param displayLogWelcome true if the welcome message is to be
     * displayed as the first log entry
     * @param allowSeparateTestSet true if the separate test set button is
     * to be displayed
     */
    public ForecastingPanel(LogPanel log, boolean displayLogWelcome, 
        boolean allowSeparateTestSet, boolean kfPerspective) {
      m_isRunningAsPerspective = kfPerspective;
      
      m_simpleConfigPanel = new SimpleConfigPanel(this);
      
      m_advancedConfigPanel = 
        new AdvancedConfigPanel(m_simpleConfigPanel, allowSeparateTestSet);
      
      setLayout(new BorderLayout());
      
      m_log = log;
      if (m_log != null) {
        if (displayLogWelcome) {
          String date = (new SimpleDateFormat("EEEE, d MMMM yyyy")).format(new Date());
          m_log.logMessage("Weka Forecaster");
          /*m_logPanel.logMessage("(c) " + Copyright.getFromYear() + "-" + Copyright.getToYear() 
          + " " + Copyright.getOwner() + ", " + Copyright.getAddress()); */
          // m_logPanel.logMessage("web: " + Copyright.getURL());
          m_log.logMessage("Started on " + date);
          m_log.statusMessage("Welcome to the Weka Forecaster");
        }
      }
      
      m_simpleConfigPanel.setAdvancedConfig(m_advancedConfigPanel);
      m_configPane.addTab(m_simpleConfigPanel.getTabTitle(), null, 
          m_simpleConfigPanel, m_simpleConfigPanel.getTabTitleToolTip());
      m_configPane.addTab(m_advancedConfigPanel.getTabTitle(), null, 
          m_advancedConfigPanel, m_advancedConfigPanel.getTabTitleToolTip());
      
      m_outText.setEditable(false);
      m_outText.setFont(new Font("Monospaced", Font.PLAIN, 12));
      m_outText.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
      
      final JScrollPane js = new JScrollPane(m_outText);
      js.getViewport().addChangeListener(new ChangeListener() {
        private int lastHeight;
        public void stateChanged(ChangeEvent e) {
          JViewport vp = (JViewport)e.getSource();
          int h = vp.getViewSize().height; 
          if (h != lastHeight) { // i.e. an addition not just a user scrolling
            lastHeight = h;
            int x = h - vp.getExtentSize().height;
            vp.setViewPosition(new Point(0, x));
          }
        }
      });
      
      m_outputPane.setBorder(BorderFactory.createTitledBorder("Output/Visualization"));
      
      m_outputPane.addTab("Output", null, js, "Forecaster output");
      
      m_history.setBorder(BorderFactory.createTitledBorder("Result list"));
      m_history.getList().getSelectionModel().
        addListSelectionListener(new ListSelectionListener() {
          public void valueChanged(ListSelectionEvent e) {
  
            synchronized (ForecastingPanel.this) {
              if (!e.getValueIsAdjusting()) {
                ListSelectionModel lm = (ListSelectionModel)e.getSource();
                for (int j = e.getFirstIndex(); j <= e.getLastIndex(); j++) {
                  if (lm.isSelectedIndex(j)) {
                    String name = m_history.getSelectedName();
                    updateMainTabs(name);
                  }
                }
              }
            }
          }
        });
      
      m_history.getList().addMouseListener(new MouseAdapter() {
        public void mouseClicked(MouseEvent e) {
          if (((e.getModifiers() & InputEvent.BUTTON1_MASK)
              != InputEvent.BUTTON1_MASK) || e.isAltDown()) {
           int index = m_history.getList().locationToIndex(e.getPoint());
           if (index != -1) {
             String name = m_history.getNameAtIndex(index);
             resultPopup(name, e.getX(), e.getY());
           } else {
             resultPopup(null, e.getX(), e.getY());
           }
         }
        }
      });
      
      m_history.setHandleRightClicks(false);
      
  //    add(m_configPane, BorderLayout.NORTH);
      
      JPanel lowerPanel = new JPanel();
      lowerPanel.setLayout(new BorderLayout());
      JPanel butAndHistHolder = new JPanel();
      butAndHistHolder.setLayout(new BorderLayout());
      butAndHistHolder.add(m_history, BorderLayout.CENTER);
      JPanel butHolder = new JPanel();
      butHolder.setLayout(new GridLayout(1,3));
      butHolder.add(m_startBut);
      butHolder.add(m_stopBut);
      butHolder.add(m_helpBut);
      
      m_startBut.setToolTipText("Start the forecasting process");
      m_stopBut.setToolTipText("Stop the running forecasting process");
      butAndHistHolder.add(butHolder, BorderLayout.NORTH);
      m_startBut.setEnabled(false); m_stopBut.setEnabled(false);
      
      m_stopBut.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          stopForecaster();
        }
      });
      
      m_helpBut.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          BrowserHelper.openURL(ForecastingPanel.this, 
              "http://wiki.pentaho.com/display/DATAMINING/" +
              "Time+Series+Analysis+and+Forecasting+with+Weka");
        }
      });
      m_helpBut.setToolTipText("Visit documentation for the time series " +
      		"environment in your browser");
      
      lowerPanel.add(butAndHistHolder, BorderLayout.WEST);
      lowerPanel.add(m_outputPane, BorderLayout.CENTER);
      
      m_splitP = new JSplitPane(JSplitPane.VERTICAL_SPLIT, m_configPane, lowerPanel);
      m_splitP.setOneTouchExpandable(true);
  
  //    add(lowerPanel, BorderLayout.CENTER);
      add(m_splitP, BorderLayout.CENTER);
      
      if (m_log != null && m_log instanceof JComponent) {
        add((JComponent)m_log, BorderLayout.SOUTH);
      }
      
      double width = m_history.getPreferredSize().width;
      int height = m_history.getPreferredSize().height;    
      width *= 0.75;
      m_history.setPreferredSize(new Dimension((int)width, height));
      
      m_startBut.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          startForecaster(m_forecaster);
        }
      });
    }  
    
    /**
     * Enable the start button
     * 
     * @param enable true if the start button is to be enabled
     */
    protected void enableStartButton(boolean enable) {
      m_startBut.setEnabled(enable);
    }
    
    /** Map used to store the tabs containing graph output */
    protected HashMap<String, JTabbedPane> m_framedOutputMap = 
      new HashMap<String, JTabbedPane>();
    
    /**
     * Opens the named result in a separate frame
     * 
     * @param name the name of the result from the history list to use
     */
    protected void openResultFrame(String name) {
      StringBuffer buffer = m_history.getNamedBuffer(name);
      JTabbedPane tabbedPane = m_framedOutputMap.get(name);
  
      if (buffer != null && tabbedPane == null) {
        JTextArea textA = new JTextArea(20, 50);
        textA.setEditable(false);
        textA.setFont(new Font("Monospaced", Font.PLAIN, 12));
        textA.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        textA.setText(buffer.toString());
        tabbedPane = new JTabbedPane();
        tabbedPane.addTab("Output", new JScrollPane(textA));
        updateComponentTabs(name, tabbedPane);
        m_framedOutputMap.put(name, tabbedPane);
        
        final JFrame jf = new JFrame(name);
        jf.addWindowListener(new WindowAdapter() {
          public void windowClosing(WindowEvent e) {
            m_framedOutputMap.remove(jf.getTitle());
            jf.dispose();
          }
        });
        jf.setLayout(new BorderLayout());
        jf.add(tabbedPane, BorderLayout.CENTER);
        jf.pack();
        jf.setSize(550, 400);
        jf.setVisible(true);
      }        
    }
    
    /**
     * Pops up a contextual menu in the result history list
     * 
     * @param name the selected entry in the list
     * @param x the x position for the popup
     * @param y the y position for the popup
     */
    protected void resultPopup(final String name, int x, int y) {
      final String selectedName = name;
      JPopupMenu resultListMenu = new JPopupMenu();
      
      JMenuItem showMainBuff = new JMenuItem("View in main window");
      if (selectedName != null) {
        showMainBuff.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            m_history.setSingle(selectedName);
            updateMainTabs(selectedName);
          }
        });
      } else {
        showMainBuff.setEnabled(false);
      }
      resultListMenu.add(showMainBuff);
      
      JMenuItem showSepBuff = new JMenuItem("View in separate window");
      if (selectedName != null) {
        showSepBuff.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            openResultFrame(selectedName);
          }
        });
      } else {
        showSepBuff.setEnabled(false);
      }
      resultListMenu.add(showSepBuff);    
      
      JMenuItem deleteResultBuff = new JMenuItem("Delete result");
      if (selectedName != null && m_runThread == null) {
        deleteResultBuff.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            m_history.removeResult(selectedName);
          }
        });
      } else {
        deleteResultBuff.setEnabled(false);
      }
  
      
      resultListMenu.add(deleteResultBuff);
      
      resultListMenu.addSeparator();
      List<Object> resultList = null;
      if (selectedName != null) { 
        resultList = (List<Object>)m_history.getNamedObject(name);
      }
      
      WekaForecaster saveForecaster = null;
      Instances saveForecasterStructure = null;
      if (resultList != null) {
        for (Object o : resultList) {
          if (o instanceof WekaForecaster){
            saveForecaster = (WekaForecaster)o;
          } else if (o instanceof Instances) {
            saveForecasterStructure = (Instances)o;
          }
        }
      }
      
      final WekaForecaster toSave = saveForecaster;
      final Instances structureToSave = saveForecasterStructure;
      JMenuItem saveForecasterMenuItem = new JMenuItem("Save forecasting model");
      if (saveForecaster != null) {
        saveForecasterMenuItem.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            saveForecaster(name, toSave, structureToSave); 
          }
        });
      } else {
        saveForecasterMenuItem.setEnabled(false);
      }
      resultListMenu.add(saveForecasterMenuItem);
      
      JMenuItem loadForecasterMenuItem = new JMenuItem("Load forecasting model");
      resultListMenu.add(loadForecasterMenuItem);
      loadForecasterMenuItem.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          loadForecaster();
        }
      });
      
      if (m_isRunningAsPerspective) {
        JMenuItem copyToKFClipboardMenuItem = 
          new JMenuItem("Copy model to Knowledge Flow clipboard");
        resultListMenu.add(copyToKFClipboardMenuItem);
        copyToKFClipboardMenuItem.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            try {
              KnowledgeFlowApp singleton = KnowledgeFlowApp.getSingleton();
              String encoded = 
                TimeSeriesForecasting.encodeForecasterToBase64(toSave, structureToSave);
              
              TimeSeriesForecasting component = new TimeSeriesForecasting();
              component.setEncodedForecaster(encoded);                        
  
              TimeSeriesForecastingKFPerspective.setClipboard(component);
            } catch (Exception ex) {
              ex.printStackTrace();
            }
          }
        });
      }
      
      
      JMenuItem reevaluateModelItem = new JMenuItem("Re-evaluate model");
      if (selectedName != null && m_runThread == null) {
        
        reevaluateModelItem.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            reevaluateForecaster(selectedName, toSave, structureToSave);
          }
        });
        
        reevaluateModelItem.
          setEnabled((m_advancedConfigPanel.m_trainingCheckBox.isSelected() ||
              m_advancedConfigPanel.m_holdoutCheckBox.isSelected()) &&
              m_instances != null);
      } else {
        reevaluateModelItem.setEnabled(false);
      }            
      
      resultListMenu.add(reevaluateModelItem);
      
      resultListMenu.show(m_history.getList(), x, y);
    }
    
    /**
     * Load a forecaster and add it to the history list
     */
    protected void loadForecaster() {
      File sFile = null;
      int returnVal = m_fileChooser.showOpenDialog(this);
      if (returnVal == m_fileChooser.APPROVE_OPTION) {
        sFile = m_fileChooser.getSelectedFile();
        
        if (m_log != null) {
          m_log.statusMessage("Loading forecaster...");
        }
        
        Object f = null;
        Instances header = null;
        boolean loadOK = true;
        try {
          ObjectInputStream is = new ObjectInputStream(new FileInputStream(sFile));
          f = is.readObject();
          header = (Instances)is.readObject();
          is.close();
        } catch (Exception ex) {
          JOptionPane.showMessageDialog(null, ex, "Load failed",
              JOptionPane.ERROR_MESSAGE);
          loadOK = false;
        }
        
        if (!loadOK) {
          if (m_log != null) {
            m_log.logMessage("Loading from file " + sFile.getName() + "' failed.");
            m_log.statusMessage("OK");
          }
        } else if (!(f instanceof WekaForecaster)) {
          JOptionPane.showMessageDialog(this, 
              "Loaded model is not a weka forecaster!", "Weka forecasting", 
              JOptionPane.ERROR_MESSAGE);
          loadOK = false;
        }
        
        if (loadOK) {
          String name = (new SimpleDateFormat("HH:mm:ss - ")).format(new Date());
          StringBuffer outBuff = new StringBuffer();
          WekaForecaster wf = (WekaForecaster)f;        
          
          String lagOptions = "";
          if (wf instanceof TSLagUser) {
            TSLagMaker lagMaker = ((TSLagUser)wf).getTSLagMaker();
            lagOptions = Utils.joinOptions(lagMaker.getOptions());
          }
          
          String fname = wf.getAlgorithmName();
          String algoName = fname.substring(0, fname.indexOf(' '));
          if (algoName.startsWith("weka.classifiers.")) {
            name += algoName.substring("weka.classifiers.".length());
          } else {
            name += algoName;
          }
          name += " loaded from '" + sFile.getName() + "'";
          
          outBuff.append("Scheme:\n\t" + fname).append("\n");
          outBuff.append("loaded from '" + sFile.getName() + "'\n\n");
          
          if (lagOptions.length() > 0) {
            outBuff.append("Lagged and derived variable options:\n\t").
            append(lagOptions + "\n\n");
          }
          
          outBuff.append(wf.toString());
          
          m_history.addResult(name, outBuff);
          m_history.setSingle(name);
          
          List<Object> resultList = new ArrayList<Object>();
          resultList.add(wf); resultList.add(header);
          m_history.addObject(name, resultList);
          updateMainTabs(name);        
        }      
      }
    }
    
    /**
     * Serialize a forecaster out to a file
     * 
     * @param name the name of forecaster to save
     * @param forecaster the actual forecaster to save
     * @param structure the structure of the instances used to train the forecaster
     */
    protected void saveForecaster(String name, TSForecaster forecaster, 
        Instances structure) {
      File sFile = null;
      boolean saveOK = true;
      
      int returnVal = m_fileChooser.showSaveDialog(this);
      if (returnVal == JFileChooser.APPROVE_OPTION) {
        sFile = m_fileChooser.getSelectedFile();
        if (!sFile.getName().toLowerCase().endsWith(".model")) {
          sFile = new File(sFile.getParent(), sFile.getName() 
                           + ".model");
        }
        if (m_log != null) {
          m_log.statusMessage("Saving forecaster to file...");
        }
        
        try {
          ObjectOutputStream oos =
            new ObjectOutputStream(new FileOutputStream(sFile));
          oos.writeObject(forecaster);
          if (structure != null) {
            oos.writeObject(new Instances(structure, 0));
          }
          oos.close();
        } catch (Exception ex) {
          JOptionPane.showMessageDialog(null, ex, "Save Failed",
              JOptionPane.ERROR_MESSAGE);
          saveOK = false;
        }
        
        if (saveOK) {
          if (m_log != null) {
            m_log.logMessage("Saved model (" + name
              + " ) to file '" + sFile.getName() + "'");
            m_log.statusMessage("OK");
          }        
        }
      }    
    }
    
    /**
     * Updates the display with the results from the currently selected
     * entry in the result history list
     * 
     * @param name the name of the entry from which to display results
     * @param outputPane the output pane to update
     */
    protected synchronized void updateComponentTabs(String name, 
        JTabbedPane outputPane) {
      // remove any tabs that are not the text output
      int numTabs = outputPane.getTabCount();
      if (numTabs > 1) {
        for (int i = numTabs - 1; i > 0; i--) {
          outputPane.removeTabAt(i);
        }
      }
  
      // see if there are any graphs associated with this name
      List<Object> storedResults = (List<Object>)m_history.getNamedObject(name);
      List<JPanel> graphList = null;
      if (storedResults != null) {
        for (Object o : storedResults) {
          if (o instanceof List<?>) {
            graphList = (List<JPanel>)o;
          }
        }
      }
      
      if (graphList != null && graphList.size() > 0) {                 
        // add the graphs
        for (JPanel p : graphList) {
          outputPane.addTab(p.getToolTipText(), p);
        }
      }
    }
    
    /**
     * Updates the tabs in the main display.
     * 
     * @param entryName the entry name of the currently displayed results. If
     * the selected result is the same as the current then no update is done.
     */
    protected synchronized void updateMainTabs(String entryName) {
      String name = m_history.getSelectedName();
      if (!name.equals(entryName)) {
        return;
      }
      updateComponentTabs(name, m_outputPane);
    }
    
    /**
     * Set the log to use
     * 
     * @param log the log to use
     */
    public void setLog(Logger log) {
      if (log instanceof JComponent) {
        if (m_log != null) {
          remove((JComponent)m_log);
        }
        m_log = log;
        add((JComponent)m_log, BorderLayout.SOUTH);
      }
    }
    
    /**
     * Set the training instances to use
     * 
     * @param insts the training instances to use
     * @throws Exception if a problem occurs
     */
    public void setInstances(Instances insts) throws Exception {
      
      // if this is the first set of instances seen then
      // install a listener on the simple config target panel's
      // table model so that we can enable/disable the start
      // button
      m_sortedCheck = false;
      boolean first = 
        (m_simpleConfigPanel.m_targetPanel.getTableModel() == null);
      
      m_instances = new Instances(insts);
      m_simpleConfigPanel.setInstances(insts);
      m_advancedConfigPanel.setInstances(insts);
      
      if (first) {            
        TableModel model = m_simpleConfigPanel.m_targetPanel.getTableModel();
        model.addTableModelListener(new TableModelListener() {
          public void tableChanged(TableModelEvent e) {
            int[] selected = m_simpleConfigPanel.m_targetPanel.getSelectedAttributes();
            if (selected != null && selected.length > 0) {
              m_startBut.setEnabled(true);
            } else {
              m_startBut.setEnabled(false);
            }
            m_advancedConfigPanel.updatePanel();
          }
        });
      }
    }
    
    /**
     * Check to see if the data is sorted in order of the date time 
     * stamp (if present)
     */
    protected void sortCheck() {
      if (m_instances == null) {
        return;
      }
      
      if (m_simpleConfigPanel.isUsingANativeTimeStamp()) {
        String timeStampF = m_simpleConfigPanel.getSelectedTimeStampField();
        Attribute timeStampAtt = m_instances.attribute(timeStampF); 
        if (timeStampAtt != null) {
          
          double lastNonMissing = Utils.missingValue();
          boolean ok = true;
          boolean hasMissing = false;
          for (int i = 0; i < m_instances.numInstances(); i++) {
            Instance current = m_instances.instance(i);
            
            if (Utils.isMissingValue(lastNonMissing)) {
              if (!current.isMissing(timeStampAtt)) {
                lastNonMissing = current.value(timeStampAtt);
              } else {
                hasMissing = true;
              }
            } else {
              if (!current.isMissing(timeStampAtt)) {
                if (current.value(timeStampAtt) - lastNonMissing < 0) {
                  ok = false;
                  break;
                }
                
                lastNonMissing = current.value(timeStampAtt);
              } else {
                hasMissing = true;
              }
            }
          }
          
          if (!ok && !hasMissing) {
            // ask if we should sort
            int result = JOptionPane.showConfirmDialog(ForecastingPanel.this, 
                "The data does not appear to be in sorted order of \""
                + timeStampF + "\". Do you want to sort the data?", 
                "Forecasting", JOptionPane.YES_NO_OPTION);
            
            if (result == JOptionPane.YES_OPTION) {
              if (m_log != null) {
                m_log.statusMessage("Sorting data...");
              }
              m_instances.sort(timeStampAtt);
              m_sortedCheck = true;            
            }
          }
          
          if (!ok && hasMissing) {
            // we can't really proceed in this situation. We can't sort by the timestamp
            // because instances with missing values will be put at the end of the data.
            // The only thing we can do is to remove the instances with missing time
            // stamps but this is likely to screw up the periodicity and majorly impact
            // on results.
            
            int result = JOptionPane.showConfirmDialog(ForecastingPanel.this, 
                "The data does not appear to be in sorted order of \""
                + timeStampF + "\". \nFurthermore, there are rows with\n" +
                		"missing timestamp values. We can remove these\n" +
                		"rows and then sort the data but this is likely to\n" +
                		"result in degraded performance. It is strongly\n" +
                		"recommended that you fix these issues in the data\n" +
                		"before continuing. Do you want the system to proceed\n" +
                		"anyway by removing rows with missing timestamps and\n" +
                		"then sorting the data?", 
                "Forecasting", JOptionPane.YES_NO_OPTION);
            
            if (result == JOptionPane.YES_OPTION) {
              if (m_log != null) {
                m_log.statusMessage("Removing rows with missing time stamps and sorting data...");
              }
              m_instances.deleteWithMissing(timeStampAtt);
              m_instances.sort(timeStampAtt);
              m_sortedCheck = true;            
            }
          }
        }
      }
    }
    
    /**
     * Stop the currently running thread
     */
    protected void stopForecaster() {
      if (m_runThread != null) {
        m_runThread.interrupt();
        m_runThread.stop();
      }
    }
    
    
    /**
     * Inner class defining a log based on PrintStream. This enables command line,
     * gui and central Weka logging to be unified.
     * 
     * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
     */
    class LogPrintStream extends PrintStream {
      public LogPrintStream() {
        super(System.out);
      }
      
      /**
       * Log to the status area. Logs to the log area if the string
       * begins with "WARNING" or "ERROR"
       * 
       * @param string the string to display
       */
      private void logStatusMessage(String string) {
        if (m_log != null) {
          m_log.statusMessage(string);
          if (string.contains("WARNING") || string.contains("ERROR")) {
            m_log.logMessage(string);
          }
        }
      }
      
      /**
       * Log to the status area
       * @param string the string to log
       */
      public void println(String string) {
        // make sure that the global weka log picks it up
        System.out.println(string);    
        logStatusMessage(string);
      }
      
      /**
       * Log to the status area
       * @param obj the object to log
       */
      public void println(Object obj) {
        println(obj.toString());
      }
      
      /**
       * Log to the status area
       * @param string the string to log
       */
      public void print(String string){
        // make sure that the global weka log picks it up
        System.out.print(string);
        logStatusMessage(string);      
      }
      
      /**
       * Log to the status area
       * @param obj the object to log
       */
      public void print(Object obj) {
        print(obj.toString());
      }            
    }
    
    /**
     * Reevaluate the supplied forecaster on the current data
     * 
     * @param name the name of the result from the history list associated
     * with the forecaster to be reevaluated
     * 
     * @param forecaster the forecaster to reevaluate
     * @param trainHeader the header of the data used to train the forecaster
     */
    protected void reevaluateForecaster(final String name, 
        final WekaForecaster forecaster, final Instances trainHeader) {
      
      if (!trainHeader.equalHeaders(m_instances)) {
        JOptionPane.showMessageDialog(null, "Data used to train this forecaster " +
                  "is not compatible with the currently loaded data:\n\n" 
            + trainHeader.equalHeadersMsg(m_instances), "Unable to reevaluate model",
            JOptionPane.ERROR_MESSAGE);
      } else {
        if (m_runThread == null) {
          synchronized (this) {
            m_startBut.setEnabled(false);
            m_stopBut.setEnabled(true);
          }
          
          m_runThread = new ForecastingThread(forecaster, name);
          ((ForecastingThread)m_runThread).setConfigureAndBuild(false);
          
          m_runThread.setPriority(Thread.MIN_PRIORITY);
          m_runThread.start();
        }
      }
    }
    
    /**
     * Start the forecasting process for the supplied configured forecaster
     * 
     * @param forecaster the forecaster to run
     */
    protected void startForecaster(final WekaForecaster forecaster) {
      if (m_runThread == null) {
        synchronized (this) {
          m_startBut.setEnabled(false);
          m_stopBut.setEnabled(true);
        }
        
        m_runThread = new ForecastingThread(forecaster, null);      
        
        m_runThread.setPriority(Thread.MIN_PRIORITY);
        m_runThread.start();
      }
    }
    
    private void dontShowMessageDialog(String key, String message, String dialogTitle) {
      if (!Utils.getDontShowDialog(key)) {
        JCheckBox dontShow = new JCheckBox("Do not show this message again");
        Object[] stuff = new Object[2];
        stuff[0] = message + "\n";
        stuff[1] = dontShow;
  
        JOptionPane.showMessageDialog(this, stuff, 
            dialogTitle, JOptionPane.OK_OPTION);
  
        if (dontShow.isSelected()) {
          try {
            Utils.setDontShowDialog(key);
          } catch (Exception ex) {
            // quietly ignore
          }
        }
      }
    }
        
    /**
     * Tests the Weka Forecasting panel from the command line.
     *
     * @param args must contain the name of an arff file to load.
     */
    public static void main(String[] args) {
  
      try {
        if (args.length == 0) {
          throw new Exception("supply the name of an arff file");
        }
        Instances i = new Instances(new java.io.BufferedReader(
                                    new java.io.FileReader(args[0])));
        ForecastingPanel scp = 
          new ForecastingPanel(new LogPanel(new WekaTaskMonitor()), true, false, false);
        scp.setInstances(i);
        final javax.swing.JFrame jf =
          new javax.swing.JFrame("Weka Forecasting");
        jf.getContentPane().setLayout(new BorderLayout());
        jf.getContentPane().add(scp, BorderLayout.CENTER);
        jf.addWindowListener(new java.awt.event.WindowAdapter() {
          public void windowClosing(java.awt.event.WindowEvent e) {
            jf.dispose();
            System.exit(0);
          }
        });
        jf.pack();
        jf.setVisible(true);
      } catch (Exception ex) {
        ex.printStackTrace();
        System.err.println(ex.getMessage());
      }
    }
  }
  