Javaからgnuplotへ複数プロット分のデータを送る
複数のプロット分のデータをJavaで生成しgnuplotにチャートを描かせてJava側でそれを順番に表示する。
前出のコードでは二つのプロセス間のデータ授受に標準入出力を利用している。
手軽ではあるがこれが災いして複数の画像を受け取るときに問題が起きる。
ImageIcon icon = new ImageIcon(ImageIO.read(in))
の部分でgnuplotから非同期に画像データを受けているのだが、
画像一枚分のデータを送り終わってもストリームが閉じられるまで待っているようなのだ。
しかし単数画像用のコードのようにストリームを閉じてしまうと二つ目の画像が受け取れなくなる。
対処法は色々考えられる。
例えば、受け取りに一時ファイルを使用する。場合によっては送り出しにも利用する。
これが一番簡単にできてスマートな解決策だろうと思う。
複数の画像にひとまとめにされることの意味があるのなら、
animated GIFなどの複数フレーム形式をImageMagickなどで作成してJavaで受けることもできるだろう。
特にanimated GIFであればJavaのコンポーネントでデフォルトで表示可能である。
標準入出力にこだわるのなら、ImageIO#read(InputStream)
で一括して行っている処理を分割し、
ストリームを閉じられずともImage
を返すコードを作成することもできるかもしれない。
他にもいくつか思いつくが、
ここでは画像ごとにgnuplotのプロセスを起動する強引技で書いてみる。
この方法なら前出のコードをそのまま利用できる。
import java.awt.BorderLayout; import java.io.*; import javax.imageio.ImageIO; import javax.swing.*; public class GnuplotUser3 implements Runnable { private static final String GNUPLOT = "/usr/bin/gnuplot"; public static void main(String[] args) throws IOException, InterruptedException, java.lang.reflect.InvocationTargetException { Process p = new ProcessBuilder(GNUPLOT, "-").start(); PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(p.getOutputStream()))); BufferedInputStream in = new BufferedInputStream(p.getInputStream()); out.println("set terminal png"); // output png to stdout out.println("plot '-' with linespoints"); // plot data following for (int i = 0; i <= 20; i++) { double x = 2 * Math.PI * i / 20; double y = Math.sin(x); out.println(x + " " + y); } out.println("e"); // mark end-of-data out.println("set output"); // flush gnuplot output out.close(); // flush and close stdout ImageIcon icon = new ImageIcon(ImageIO.read(in)); // create icon from stdin in.close(); // close stdin try { p.waitFor(); } catch (InterruptedException e) { } GnuplotUser3 gu = new GnuplotUser3(icon); SwingUtilities.invokeAndWait(gu); for (int k = 1; k <= 20; k++) { double phase = 2 * Math.PI * k / 20; p = new ProcessBuilder(GNUPLOT, "-").start(); out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(p.getOutputStream()))); in = new BufferedInputStream(p.getInputStream()); out.println("set terminal png"); // output png to stdout out.println("plot '-' with linespoints"); // plot data following for (int i = 0; i <= 20; i++) { double x = 2 * Math.PI * i / 20; double y = Math.sin(x + phase); out.println(x + " " + y); } out.println("e"); // mark end-of-data out.println("set output"); // flush gnuplot output out.close(); // flush and close stdout icon.setImage(ImageIO.read(in)); // create icon from stdin in.close(); // close stdin gu.label.repaint(); try { p.waitFor(); } catch (InterruptedException e) { } } } private ImageIcon icon; private JLabel label; private GnuplotUser3(ImageIcon icon) { this.icon = icon; } public void run() { JFrame f = new JFrame("sin(x)"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); label = new JLabel(icon); f.add(label, BorderLayout.CENTER); f.pack(); f.setVisible(true); } }
ここでは特にはフレーム速度を制御していない。
速すぎる場合はTimerなどで次のフレームを表示するタイミングを制御しないといけない。
遅すぎる場合はプロセス生成が遅いとかで実行する環境がこの方法に合っていないということなのだろう。
前出のコードでは標準エラーも受け取っていたが上のコードではオミットした。
誤ったplotコマンドを指示するなどで画像が送られてこない場合のように、
本当にgnuplot側のエラー出力が必要なときにそれが表示されないので、
Java側でエラー処理を追加しないとあまり意味がないことも理由の一つだ。
前出のコードが単に標準入出力を利用したプロセス間通信の例でしかなく、
元々設計も何もあったものではないから上のコードもdirtyなのはいたしかたない。
面倒なので1フレーム目の処理を2フレーム目以降用のコードにコピペしてたりしてるし。