All Downloads are FREE. Search and download functionalities are using the official Maven repository.

weka.classifiers.timeseries.gui.ForecastingPanel Maven / Gradle / Ivy

Go to download

Provides a time series forecasting environment for Weka. Includes a wrapper for Weka regression schemes that automates the process of creating lagged variables and date-derived periodic variables and provides the ability to do closed-loop forecasting. New evaluation routines are provided by a special evaluation module and graphing of predictions/forecasts are provided via the JFreeChart library. Includes both command-line and GUI user interfaces. Sample time series data can be found in ${WEKA_HOME}/packages/timeseriesForecasting/sample-data.

The newest version!
/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see .
 */

/*
 *    ForecastingPanel.java
 *    Copyright (C) 2010-2016 University of Waikato, Hamilton, New Zealand
 */

package weka.classifiers.timeseries.gui;

import weka.classifiers.timeseries.AbstractForecaster;
import weka.classifiers.timeseries.TSForecaster;
import weka.classifiers.timeseries.WekaForecaster;
import weka.classifiers.timeseries.core.OverlayForecaster;
import weka.core.SerializationHelper;
import weka.filters.supervised.attribute.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;
import weka.gui.knowledgeflow.TimeSeriesPerspective;

import javax.swing.*;
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 java.awt.*;
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;

/**
 * Main GUI panel for the forecasting environment.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision: 52593 $
 */
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 old KnowledgeFlow as a perspective */
  protected boolean m_isRunningAsPerspective = false;

  /** Holds a listener for when we are running in the new KnowledgeFlow */
  protected TimeSeriesPerspective.TimeSeriesModelListener m_forecasterListener;

  /** 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;
    }

    @Override
    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 resultList =
          (m_configAndBuild) ? new ArrayList()
            : (List) m_history.getNamedObject(name);

        if (!m_configAndBuild) {
          // go through and remove any JPanels
          List newResultList = new ArrayList();
          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 graphList = new ArrayList();
        // graph predictions for targets at specific step
        if (m_advancedConfigPanel.getGraphPredictionsAtStep() > 0) {
          int stepNum = m_advancedConfigPanel.getGraphPredictionsAtStep();
          List 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 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;

      @Override
      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() {
        @Override
        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() {
      @Override
      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() {
      @Override
      public void actionPerformed(ActionEvent e) {
        stopForecaster();
      }
    });

    m_helpBut.addActionListener(new ActionListener() {
      @Override
      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() {
      @Override
      public void actionPerformed(ActionEvent e) {
        startForecaster(m_forecaster);
      }
    });
  }

  /**
   * Set a listener that accepts WekaForecaster models. Used to pass the
   * forecaster from the contextual menu to the new Knowledge Flow perspective
   *
   * @param listener the listener to register
   */
  public void setTimeSeriesModelListener(
    TimeSeriesPerspective.TimeSeriesModelListener listener) {
    m_forecasterListener = listener;
  }

  /**
   * 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 m_framedOutputMap =
    new HashMap();

  /**
   * 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() {
        @Override
        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() {
        @Override
        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() {
        @Override
        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() {
        @Override
        public void actionPerformed(ActionEvent e) {
          m_history.removeResult(selectedName);
        }
      });
    } else {
      deleteResultBuff.setEnabled(false);
    }

    resultListMenu.add(deleteResultBuff);

    resultListMenu.addSeparator();
    List resultList = null;
    if (selectedName != null) {
      resultList = (List) 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() {
        @Override
        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() {
      @Override
      public void actionPerformed(ActionEvent e) {
        loadForecaster();
      }
    });

    if (m_isRunningAsPerspective || m_forecasterListener != null) {
      JMenuItem copyToKFClipboardMenuItem =
        new JMenuItem("Copy model to Knowledge Flow clipboard");
      resultListMenu.add(copyToKFClipboardMenuItem);
      copyToKFClipboardMenuItem.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          if (m_isRunningAsPerspective) {
            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();
            }
          } else {
            m_forecasterListener.acceptForecaster(toSave, structureToSave);
          }
        }
      });
    }

    JMenuItem reevaluateModelItem = new JMenuItem("Re-evaluate model");
    if (selectedName != null && m_runThread == null) {

      reevaluateModelItem.addActionListener(new ActionListener() {
        @Override
        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 =
          SerializationHelper.getObjectInputStream(new FileInputStream(sFile));
        // 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;

        if (wf.baseModelHasSerializer()) {
          try {
            wf.loadBaseModel(sFile.toString());
          } catch (Exception ex) {
            JOptionPane.showMessageDialog(null, ex, "Base model load failed",
              JOptionPane.ERROR_MESSAGE);
          }
        }

        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 resultList = new ArrayList();
        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 {
        forecaster.saveBaseModel(sFile.toString());
        forecaster.serializeState(sFile.toString());
        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 storedResults = (List) m_history.getNamedObject(name);
    List graphList = null;
    if (storedResults != null) {
      for (Object o : storedResults) {
        if (o instanceof List) {
          graphList = (List) 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_startBut.setEnabled(false);

    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() {
        @Override
        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
     */
    @Override
    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
     */
    @Override
    public void println(Object obj) {
      println(obj.toString());
    }

    /**
     * Log to the status area
     * 
     * @param string the string to log
     */
    @Override
    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
     */
    @Override
    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() {
        @Override
        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());
    }
  }
}