Gnuplot.java

/*
 * $Id: Gnuplot.java,v 1.38 2008/07/16 08:00:38 koga Exp $
 *
 * Copyright (C) 2004 Koga Laboratory. All rights reserved.
 *	s
 */
package org.mklab.tool.graph.gnuplot;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import org.mklab.tool.graph.PlotterException;
import org.mklab.tool.graph.gnuplot.decoration.GnuplotComponent;


/**
 * {@link Gnuplot}クラスは<a href="http://www.gnuplot.info/">gnuplot</a>でグラフを描画するためのラッパークラスです。
 * 
 * @author koga
 * @version $Revision: 1.38 $, 2004/04/30
 */
public class Gnuplot {

  /** <code>gnuplot</code> への出力ストリームに対応する {@link Writer} */
  private Writer gnuplotWriter;
  /** gnuplotのプロセス */
  private Process gnuplotProcess;

  /** メインキャンバス */
  private Canvas mainCanvas;
  /** マルチキャンバス */
  private Canvas[][] canvases;
  /** マルチキャンバスを使用中ならばtrue */
  private boolean hasMultipleCanvas = false;
  /** */
  private boolean gnuplotClosed = false;

  /** コマンド出力のバッファリングするならば場合true */
  private boolean buffering = false;
  /** gnuplotの実行環境です。 */
  private Environment environment;

  /**
   * オプションなしでGnuplotオブジェクトを生成します。
   * 
   * @throws IOException gnuplotプロセスを起動できない場合
   */
  public Gnuplot() throws IOException {
    this(Environment.getSharedEnvironment(), ""); //$NON-NLS-1$
  }

  /**
   * 新しく生成された<code>Gnuplot</code>オブジェクトを初期化します。
   * 
   * @param options ウィンドウオプション
   * @throws IOException gnuplotプロセスを起動できない場合
   */
  public Gnuplot(String options) throws IOException {
    this(Environment.getSharedEnvironment(), options);
  }

  /**
   * ウインドウオプションを指定しGnuplotオブジェクトを生成します。
   * 
   * @param env gnuplotの実行環境
   * @param options ウインドウオプション
   * @throws IOException gnuplotプロセスを起動できない場合
   */
  public Gnuplot(final Environment env, final String options) throws IOException {
    if (env == null) {
      throw new IllegalArgumentException();
    }

    this.environment = env;

    runGnuplotProcess(options);
    waitForWindowOpen();
    initializeIO();
    initializeCanvas();

    reset();
  }

  /**
   * gnuplotプロセスを起動します。
   * 
   * @param options 起動オプション
   * @throws IOException 起動に失敗した場合
   */
  private void runGnuplotProcess(final String options) throws IOException {
    final List<String> commandList;
    commandList = createCommandList(options, hashCode());

    try {
      this.gnuplotProcess = startProcess(commandList);
    } catch (IOException ex) {
      throw new IOException(Messages.getString("Gnuplot.3") + " with " + options, ex); //$NON-NLS-1$ //$NON-NLS-2$
    }
  }

  /**
   * gnuplotのウィンドウが表示するまで待ちます。
   * 
   * @throws IOException 割り込みが発生した場合
   */
  private void waitForWindowOpen() throws IOException {
    try {
      Thread.sleep(1500);
    } catch (InterruptedException e) {
      destroy();
      throw new IOException(Messages.getString("Gnuplot.0"), e); //$NON-NLS-1$
    }
  }

  /**
   * 入出力関連の初期化を行います。
   * 
   * @throws IOException gnuplotターミナルの設定に失敗した場合
   */
  private void initializeIO() throws IOException {
    //this.gnuplotWriter = new BufferedWriter(new OutputStreamWriter(this.gnuplotProcess.getOutputStream()));
    this.gnuplotWriter = new BufferedWriter(new OutputStreamWriter(this.gnuplotProcess.getOutputStream(), "UTF-8")); //$NON-NLS-1$
    try {
      setUpTerminal();
    } catch (IOException e) {
      this.gnuplotWriter.close();
      this.gnuplotWriter = null;
      this.gnuplotClosed = true;
      destroy();
      throw new IOException(Messages.getString("Gnuplot.1"), e); //$NON-NLS-1$
    }
  }

  /**
   * キャンバスの初期化を行います。
   */
  private void initializeCanvas() {
    this.mainCanvas = new Canvas(this);
    this.mainCanvas.setFrame(0, 0, 0.98, 1.0);
  }

  /**
   * gnuplot実行環境を取得します。
   * 
   * @return gnuplot実行環境
   */
  public Environment getEnvironment() {
    return this.environment;
  }

  /**
   * プロセスを起動します。
   * 
   * @param commandList プロセスの起動に用いるコマンド
   * @return 生成されたgnuplotプロセス
   * @throws FileNotFoundException 作業ディレクトリが見つからない場合
   * @throws IOException プロセスの起動に失敗した場合
   */
  private Process startProcess(final List<String> commandList) throws FileNotFoundException, IOException {
    final ProcessBuilder processBuilder = new ProcessBuilder(commandList);
    final File gnuplotWorkingDirectory;
    if (this.environment.hasWorkingDirectory()) {
      gnuplotWorkingDirectory = this.environment.getWorkingDirectory();
    } else {
      gnuplotWorkingDirectory = new File("."); //$NON-NLS-1$
    }

    if (gnuplotWorkingDirectory != null) {
      if (gnuplotWorkingDirectory.exists() == false) {
        throw new FileNotFoundException(gnuplotWorkingDirectory.toString());
      }
      processBuilder.directory(gnuplotWorkingDirectory);
    }

    try {
      final Process process = processBuilder.start();
      return process;
    } catch (IOException e) {
      throw new IOException(toConnectedString(commandList), e);
    }
  }
  
  /**
   * Generates the connected string from list of string 
   * 
   * @param commandList list of string
   * @return connected string from list of string 
   */
  private String toConnectedString(List<String> commandList) {
    final StringBuffer connectedString = new StringBuffer();
    
    for (final String commnad : commandList) {
      connectedString.append(commnad);
    }
    
    return connectedString.toString();
  }

  /**
   * gnuplot起動コマンドを生成します。
   * 
   * @param options ウィンドウオプション
   * @param pid PID
   * @return コマンド
   */
  private List<String> createCommandList(final String options, final int pid) {
    final String gnuplotOptions;
    if (System.getenv("GNUPLOT_OPTIONS") != null) { //$NON-NLS-1$
      gnuplotOptions = System.getenv("GNUPLOT_OPTIONS") + " " + options; //$NON-NLS-1$ //$NON-NLS-2$
    } else {
      gnuplotOptions = options;
    }

    final List<String> commandList = new ArrayList<>();
    if (isRunningOnWindows()) {
      commandList.add("cmd.exe"); //$NON-NLS-1$
      commandList.add("/c"); //$NON-NLS-1$
//      final String pathForPgnuplot = getPathOnWindows("pgnuplot.exe"); //$NON-NLS-1$
//      if (pathForPgnuplot != null) {
//        commandList.add(pathForPgnuplot);
//        commandList.add("-"); //$NON-NLS-1$
//        
//        commandList.add("-geometry"); //$NON-NLS-1$
//        commandList.add("380x280"); //$NON-NLS-1$
//        commandList.add("-title"); //$NON-NLS-1$
//        commandList.add("plot-" + pid); //$NON-NLS-1$
//      } else {
      final String pathForGnuplot = getPathOnWindows("gnuplot.exe"); //$NON-NLS-1$
      if (pathForGnuplot != null) {
        commandList.add(pathForGnuplot);
      } else {
        commandList.add("gnuplot.exe"); //$NON-NLS-1$
      }
//      }
    } else {
      commandList.add("gnuplot"); //$NON-NLS-1$
    }

    if (gnuplotOptions.length() > 0) {
      commandList.add(gnuplotOptions);
    }
    
    return commandList;
  }

  /**
   * 実行ファイルのパスを取得します。
   * 
   * @param command  command
   * @return 実行ファイルのパス。見つからない場合はnull
   */
  private String getPathOnWindows(String command) {
    assert isRunningOnWindows();

    // %GNUPLOT_HOME%
    if (this.environment.hasHomeDirectory() == false) {
      return null;
    }
    final File home = this.environment.getHomeDirectory();
    if (home.exists() == false) {
      return null;
    }

    // %GNUPLOT_HOME%/bin or %GNUPLOT_HOME%/binary
    File binDir = new File(home, "bin"); //$NON-NLS-1$
    if (binDir.exists() == false) {
      binDir = new File(home, "binary"); //$NON-NLS-1$
    }

    // executable file
    final File gnuplotExecutable = new File(binDir,  command);
    if (gnuplotExecutable.exists() == false) {
      return null;
    }

    return gnuplotExecutable.getAbsolutePath();
  }

  /**
   * gnuplotのターミナルを設定します。
   * 
   * @throws IOException gnuplotへメッセージを送れない場合
   */
  private void setUpTerminal() throws IOException {
    String plotTerm = System.getenv("GNUPLOT_TERM"); //$NON-NLS-1$
    if (plotTerm == null || plotTerm.equals("")) { //$NON-NLS-1$
//      if (isRunningOnWindows()) {
//        plotTerm = "windows"; //$NON-NLS-1$
//      } else {
//        plotTerm = "qt"; //$NON-NLS-1$
//      }
      
      plotTerm = "qt"; //$NON-NLS-1$
    }

    sendString("set term " + plotTerm + ";" + System.getProperty("line.separator")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
  }

  /**
   * 動作環境がWindowsOSであるか判定します。
   * 
   * @return 動作環境がWindowsOSならばtrue、そうでなければfalse
   */
  private static boolean isRunningOnWindows() {
    if (System.getProperty("os.name").startsWith("Windows")) { //$NON-NLS-1$ //$NON-NLS-2$
      return true;
    }

    return false;
  }

  /**
   * コマンドを実行します。
   * 
   * @param command コマンド
   */
  public void doCommand(final String command) {
    try {
      sendString(command + ";" + System.getProperty("line.separator")); //$NON-NLS-1$ //$NON-NLS-2$
    } catch (IOException e) {
      throw new PlotterException(command, e);
    }
  }

  /**
   * 文字列をプロッターに送ります。
   * 
   * @param message メッセージ
   * @throws IOException gnuplotへメッセージを送れない場合
   */
  private void sendString(final String message) throws IOException {
    try {
      this.gnuplotWriter.write(message);
      this.gnuplotWriter.flush();
    } catch (IOException e) {
      throw new IOException(message, e);
    }
  }

  /**
   * グラフ描画を消去します。
   */
  public void clear() {
    doCommand("clear"); //$NON-NLS-1$
  }

  /**
   * メインキャンバスを返します。
   * 
   * @return メインキャンバス
   */
  public Canvas getCanvas() {
    return this.mainCanvas;
  }

  /**
   * 複数キャンバスが使用されているか判定します。
   * 
   * @return 複数のキャンバスが使用されていればtrue、そうでなければfalse
   */
  private boolean hasMultipleCanvas() {
    return this.hasMultipleCanvas;
  }

  /**
   * リセットします。
   */
  public void reset() {
    if (hasMultipleCanvas()) {
      removeCanvases();
    } else {
      this.mainCanvas.reset();
    }
  }

  /**
   * リセットコードをプロッターに送ります。
   */
  void sendResetCode() {
    doCommand("unset logscale xyz"); //$NON-NLS-1$
    doCommand("set autoscale"); //$NON-NLS-1$
    doCommand("unset label"); //$NON-NLS-1$
    doCommand("set xlabel"); //$NON-NLS-1$
    doCommand("set ylabel"); //$NON-NLS-1$
    doCommand("set zlabel"); //$NON-NLS-1$
    doCommand("set title"); //$NON-NLS-1$
    doCommand("set zero 1e-20"); //$NON-NLS-1$
    doCommand("set style data lines"); //$NON-NLS-1$
    doCommand("set encoding utf8"); //$NON-NLS-1$
  }

  /**
   * グラフを再描画します。
   */
  public void redraw() {
    if (isBuffering()) {
      return;
    }

    try {
      if (hasMultipleCanvas()) {
        drawCanvases();
      } else if (this.mainCanvas.getLineSize() != 0) {
        drawMainCanvas();
      }
    } catch (IOException e) {
      throw new PlotterException("redraw()", e); //$NON-NLS-1$
    }
  }
  
  /**
   * 'replot'コマンドを実行します。 
   */
  public void replot() {
    if (isBuffering()) {
      return;
    }
    doCommand("replot"); //$NON-NLS-1$
  }

  /**
   * コンポーネントを更新します。
   * 
   * @param canvas コンポーネントが存在するキャンバス
   * @param component コンポーネント
   */
  void update(final Canvas canvas, final GnuplotComponent component) {
    doCommand(canvas.getFrame().getCommand());
    doCommand(component.getCommand());
    redraw();
  }

  /**
   * エキスポートのモードを設定します。
   * 
   * @param mode モード
   */
  private void setExportMode(final String mode) {
    String exportTerm;
    if (mode.equals("ps")) { //$NON-NLS-1$
      exportTerm = "postscript monochrome 'Times-Roman' 16"; //$NON-NLS-1$
    } else if (mode.equals("psplus")) { //$NON-NLS-1$
      exportTerm = "postscript plus monochrome 'Times-Roman-Mincho' 16"; //$NON-NLS-1$
    } else if (mode.equals("eps")) { //$NON-NLS-1$
      exportTerm = "postscript eps monochrome 'Times-Roman' 22"; //$NON-NLS-1$
    } else if (mode.equals("epsplus")) { //$NON-NLS-1$
      exportTerm = "postscript eps plus monochrome 'Times-Roman-Mincho' 22"; //$NON-NLS-1$
    } else if (mode.equals("fig")) { //$NON-NLS-1$
      exportTerm = "fig monochrome portrait fontsize 16 size 5 3"; //$NON-NLS-1$
    } else {
      throw new IllegalArgumentException(mode + " is " +  Messages.getString("Gnuplot.50")); //$NON-NLS-1$ //$NON-NLS-2$
    }

    doCommand("set terminal " + exportTerm); //$NON-NLS-1$
  }

  /**
   * 現在持っているグラフ情報を各種ファイルに出力します。
   * 
   * @param fileName 出力ファイル名
   * @param mode ファイルの種類{"ps", "eps", "psplus", "epsplus", "fig"}
   */
  public void export(final String fileName, final String mode) {
    export(fileName, mode, ""); //$NON-NLS-1$
  }

  /**
   * 現在持っているグラフ情報を各種ファイルに出力します。
   * 
   * @param fileName 出力ファイル名
   * @param mode ファイルの種類{"ps", "eps", "psplus", "epsplus", "fig"}
   * @param command コマンド
   */
  public void export(final String fileName, final String mode, final String command) {
    if (hasMultipleCanvas()) {
      doCommand("unset multiplot"); //$NON-NLS-1$
    }

    setExportMode(mode);
    doCommand("set output '" + fileName + "'"); //$NON-NLS-1$ //$NON-NLS-2$

    if (command.length() != 0) {
      doCommand(command);
    }

    try {
      drawCanvases();

      if (hasMultipleCanvas()) {
        doCommand("unset multiplot"); //$NON-NLS-1$
      } else {
        drawMainCanvas();
      }

      // reset the window as before
      setUpTerminal();
      doCommand("set output"); //$NON-NLS-1$

      if (hasMultipleCanvas()) {
        drawCanvases();
      } else {
        drawMainCanvas();
      }

    } catch (IOException e) {
      throw new PlotterException(e);
    }

  }

  /**
   * キャンバスを描画します。
   * 
   * @throws IOException gnuplotへメッセージを送れない場合
   */
  private void drawCanvases() throws IOException {
    doCommand("unset multiplot"); //$NON-NLS-1$

    for (int i = 0; i < this.canvases.length; i++) {
      for (int j = 0; j < this.canvases[0].length; j++) {
        final Canvas canvas = this.canvases[i][j];
        if (canvas == null) {
          continue;
        }

        final String decorationCommands = canvas.getDecorationCommands();
        if (decorationCommands.length() > 0) {
          sendString(decorationCommands);
        }
      }
    }

    doCommand("set multiplot"); //$NON-NLS-1$

    for (int i = 0; i < this.canvases.length; i++) {
      for (int j = 0; j < this.canvases[0].length; j++) {
        final Canvas canvas = this.canvases[i][j];
        if (canvas == null) {
          continue;
        }

        final String decorationCommands = canvas.getDecorationCommands();
        if (decorationCommands.length() > 0) {
          sendString(decorationCommands);
        }

        final String plotString = canvas.getPlotString();
        if (plotString.length() > 0) {
          doCommand("plot " + plotString); //$NON-NLS-1$
        }
      }
    }
  }

  /**
   * メインキャンバスを描画します。
   * 
   * @throws IOException gnuplotへメッセージを送れない場合
   */
  private void drawMainCanvas() throws IOException {
    final String decorationCommands = this.mainCanvas.getDecorationCommands();
    if (decorationCommands.length() > 0) {
      sendString(decorationCommands);
    }

    final String plotString = this.mainCanvas.getPlotString();
    if (plotString.length() > 0) {
      doCommand("plot " + plotString); //$NON-NLS-1$
    }
  }

  /**
   * @see java.lang.Object#finalize()
   */
  @Override
  protected void finalize() {
    close();
  }

  /**
   * グラフ表示を終了します。
   */
  public void close() {
    if (this.gnuplotClosed) {
      return;
    }

    reset();

    if (this.gnuplotWriter != null) {
      doCommand("quit"); //$NON-NLS-1$
    }

    try {
      this.gnuplotWriter.close();
    } catch (IOException e) {
      throw new PlotterException(e);
    } finally {
      this.gnuplotWriter = null;
      this.gnuplotClosed = true;
      destroy();
    }

  }

  /**
   * グラフを強制終了します。
   */
  public void destroy() {
    if (this.gnuplotProcess != null) {
      try {
        this.gnuplotProcess.exitValue();
      } catch (@SuppressWarnings("unused") IllegalThreadStateException e) {
        this.gnuplotProcess.destroy();
        this.gnuplotProcess = null;
      }
    }
  }

  /**
   * Gnuplotプロセスが起動中であるか判定します。
   * 
   * @return Gnuplotプロセスが起動中ならばtrue、そうでなければfalse
   */
  public boolean isRunning() {
    if (this.gnuplotProcess != null) {
      try {
        this.gnuplotProcess.exitValue();
        // 終了コードが帰ってくるので起動していない
        return false;
      } catch (@SuppressWarnings("unused") IllegalThreadStateException e) {
//        try {
//          replot();
//        } catch (@SuppressWarnings("unused") PlotterException e2) {
//          return false;
//        }

        return true;
      }
    }

    return false;
  }

  /**
   * メインキャンバスを返します。
   * 
   * @return メインキャンバス
   */
  public Canvas createCanvas() {
    if (hasMultipleCanvas()) {
      removeCanvases();

      doCommand("unset multiplot"); //$NON-NLS-1$
      clear();
      this.hasMultipleCanvas = false;
      doCommand(this.mainCanvas.getFrame().getCommand());
    }

    return this.mainCanvas;
  }

  /**
   * マルチキャンバスを削除します。
   */
  private void removeCanvases() {
    int column = 0;
    if (0 < this.canvases.length) {
      column = this.canvases[0].length;
    }

    for (int i = 0; i < this.canvases.length; i++) {
      for (int j = 0; j < column; j++) {
        this.canvases[i][j].reset();
        this.canvases[i][j] = null;
      }
      this.canvases[i] = null;
    }
    this.canvases = null;
  }

  /**
   * マルチキャンバスを生成します。
   * 
   * @param rowSize 縦方向の分割数
   * @param columnSize 横方向の分割数
   */
  public void createCanvas(final int rowSize, final int columnSize) {
    if (rowSize < 0) {
      throw new IllegalArgumentException(Messages.getString("Gnuplot.67")); //$NON-NLS-1$
    }
    if (columnSize < 0) {
      throw new IllegalArgumentException(Messages.getString("Gnuplot.68")); //$NON-NLS-1$
    }

    if (hasMultipleCanvas()) {
      removeCanvases();
    } else {
      this.mainCanvas.reset();
      doCommand("set multiplot"); //$NON-NLS-1$
      this.hasMultipleCanvas = true;
    }

    this.canvases = new Canvas[rowSize][columnSize];
    for (int i = 0; i < rowSize; i++) {
      for (int j = 0; j < columnSize; j++) {
        final double xsize = 1.0 / columnSize;
        final double ysize = 1.0 / rowSize;
        final double xorg = xsize * j;
        final double yorg = ysize * (rowSize - 1 - i);
        this.canvases[i][j] = new Canvas(this);
        this.canvases[i][j].setFrame(xorg, yorg, 0.98 * xsize, ysize);
      }
    }
  }

  /**
   * 指定されたキャンバスを返します。
   * 
   * @param row 行番号(0から始まります)
   * @param column 列番号(0から始まります)
   * @return 指定されたキャンバス
   */
  public Canvas getCanvas(final int row, final int column) {
    return this.canvases[row][column];
  }

  /**
   * 出力のバッファリングを設定します。
   * 
   * @param buffering バッファリングするならばtrue、そうでなければfalse
   */
  public void setBuffering(final boolean buffering) {
    this.buffering = buffering;
  }

  /**
   * バッファリングの設定を返します。
   * 
   * @return バッファリングするならばtrue、そうでなければfalse
   */
  public boolean isBuffering() {
    return this.buffering;
  }

}