フロー図を描いてみる

仕様書などで簡単なフローを描く場合ってありますよね?
本当はJUDEとかのUMLエディタを使えばいいのですが、単純なフローとか特殊なソフトとかを入れなくても描けるといいなって思い実験しました。

概要

独自のフローを記述する言語を経由してGraphvizhttp://www.graphviz.org/)用の出力ファイルを作成。そのファイルを利用して画像を作成します。

参考

http://sourceforge.jp/projects/make-flowchart/
フローチャート生成ツール

フローチャートを生成します。テキストを入力とし、出力はgraphviz向け(DOT言語)です。

次を予定しています。1:独自言語→DOT言語変換2:プログラミング言語→独自言語変換3:独自言語→プログラミング言語(コメントによる雛形) ?:簡便に使用するためのラッパ等

上記がイメージとしては近いのですが、Perlで組んであったのと独自言語の仕様があまりまとまってなかったので、PHPで自分で作ってみました。

フロー記述言語

初期化処理
#IF 個数?
	#CASE 1個
		処理1
		処理2
		処理3
	#CASE それ以外
		処理1
		処理4
終了処理

こんな記述方法にしました。

特徴としては

  • 関数や判断は#で始まる
  • 同列に記述すると上から順に実行するフローとなる
  • タブで一段下げると子どもの処理となる
  • 関数はIFしかない

実際にはswtich文しかない感じですね。ループの処理は。。。人数分上記の処理を実行するなど文字でカバー(笑)

実行結果

こんな感じのフローが出力されます。IFからの条件は本当は次のフローへの線に条件を書くべきだが条件が長い場合にあまりよいレイアウトにならないのと、1行ごとにブロックを置く設計になってしまったのでこう表示されています。

理想はこうだよね。でも条件が長いと

こんな感じになってしまいます。箱に入れても大きくなってしまうけれど、まだ見やすかったかな?

DOTへの出力

digraph G {
	graph [fontname="MS Gothic"];
	node [shape="box", fontname="MS Gothic", fixedsize="false", width=1.7, height=0.8];
	edge [fontname="MS Gothic"];
	start [label="S", shape="circle", style="filled", fillcolor="#CCCCCC", width=0.1, height=0.1];
	end [label="E", shape="circle", style="filled", fillcolor="#CCCCCC", width=0.1, height=0.1];

	start -> node1;
	node1 -> node2;
		node2 -> node3;
			node3 -> node4;
			node4 -> node5;
		node2 -> node6;
			node6 -> node7;
			node7 -> node8;
	node5 -> node9;
	node8 -> node9;
	node9 -> end;

	node1 [label="初期化処理"];
	node2 [label="個数?",shape="diamond"];
	node3 [label="1個",shape="doubleoctagon"];
	node4 [label="処理1"];
	node5 [label="処理2"];
	node6 [label="それ以外",shape="doubleoctagon"];
	node7 [label="処理1"];
	node8 [label="処理4"];
	node9 [label="終了処理"];
}

こんなDOT出力に変換しています。書式の詳細はgraphvizの仕様のままなので記述しませんが、DOTに変換後にPNGなどの画像に変換しています。

変換処理

#から始まる行が判断が必要で、タブで階層を判断します。処理の内容的にはC言語ポインターを利用したデータ構造で組んでしまったので、参照を利用しまくったPHPにはまったく見えないソースになってしまいました。。。

<?php

$tree = getFlowFile('test.flow');
$dot = outputDot( $tree );

echo "<pre>";
echo($dot);

function getFlowFile( $filename ){
	$tree = array();

	$fp = fopen( $filename, 'r' );
	if( $fp == null ){
		return $tree;
	}

	$nowLevel = 0;
	$boxIndex = 1;
	$nowTree =& $tree;
	$upTree = array();
	$upTree[] =& $tree;
	while( !feof( $fp ) ){
		$line = fgets($fp, 5120);
		$item = array();

		if( preg_match( "/(\t*)#(.*?) (.*)/", $line, $match ) ){
			// コマンド引数あり
			$level = strlen( $match[1] );
			$command = strtoupper( $match[2] );
			$text = trim( $match[3] );

			$item['level'] = $level;
			$item['command'] = $command;
			$item['text'] = $text;
		} else if( preg_match( "/(\t*)#(.*)/", $line, $match ) ){
			// コマンド引数なし
			$level = strlen( $match[1] );
			$command = strtoupper( trim($match[2]) );

			$item['level'] = $level;
			$item['command'] = $command;
		} else {
			// 文字列
			preg_match( "/(\t*)(.*)/", $line, $match );
			$level = strlen( $match[1] );
			$text = trim( $match[2] );

			if( strlen( $text ) === 0 ){
				continue;
			}

			$item = array();
			$item['level'] = $level;
			$item['text'] = $text;
		}

		$item['num'] = $boxIndex++;

		if( $nowLevel == $item['level'] ){
			$nowTree[] = $item;
		} else if( $nowLevel < $item['level'] ){
			$upTree[] =& $nowTree;
			$nowItem =& $nowTree[count($nowTree)-1];
			$nowItem['Items'] = array();
			$nowTree =& $nowItem['Items'];
			$nowTree[] = $item;
			$nowLevel = $item['level'];
		} else {
			while( $nowLevel != $item['level'] ){
				$nowTree =& $upTree[count($upTree)-1];
				unset($upTree[count($upTree)-1]);

				$nowLevel--;
			}

			if($nowTree === null){
				$nowTree =& $tree;
			}

			$nowLevel = $item['level'];
			$nowTree[] = $item;
		}
	}

	return $tree;
}

function outputDot( $tree ){
	$output = <<<__ECHO__
digraph G {
	graph [fontname="MS Gothic"];
	node [shape="box", fontname="MS Gothic", fixedsize="false", width=1.7, height=0.8];
	edge [fontname="MS Gothic"];
	start [label="S", shape="circle", style="filled", fillcolor="#CCCCCC", width=0.1, height=0.1];
	end [label="E", shape="circle", style="filled", fillcolor="#CCCCCC", width=0.1, height=0.1];


__ECHO__;

	$nodeInfo = array();
	$nodeRoot = array();
	foreach( $tree as $item ){
		if( count( $nodeRoot ) === 0 ){
			// Start
			$nodeNum = $item['num'];
			$output .= "\tstart -> node" . $nodeNum . ";\n";
			$nodeRoot[] = $nodeNum;
		} else {
			$nodeNum = $item['num'];
			foreach( $nodeRoot as $rootNode ){
				$output .= "\tnode" . $rootNode . " -> node" . $nodeNum . ";\n";
			}
			$nodeRoot = array();
			if( isset( $item['Items'] ) ){
				//
			} else {
				$nodeRoot[] = $nodeNum;
			}
		}
		$nodeInfo[] = $item;

		if( isset( $item['Items'] ) ){
			$nodeRoot = array();
			$nodeRoot = dispItems(&$output, &$item, &$nodeInfo, &$nodeRoot, $nodeNum, 0 );
		}
	}

	foreach($nodeRoot as $node){
		$output .= "\tnode" . $node . " -> end;\n";
	}

	$output .= "\n";
	foreach($nodeInfo as $node){
		$type = "";
		if( $node['command'] === 'IF' ){
			$type = ",shape=\"diamond\"";
		}
		if( $node['command'] === 'CASE' ){
			$type = ",shape=\"doubleoctagon\"";
		}
		if( $node['command'] === 'END' ){
			$output .= "\tnode" . $node['num'] . " [label=\"E\", shape=\"circle\", style=\"filled\", fillcolor=\"#CCCCCC\", width=0.1, height=0.1];\n";
			continue;
		}

		$output .= "\tnode" . $node['num'] . " [label=\"" . $node['text'] . "\"" . $type . "];\n";
	}

	$output .= <<<__ECHO__
}
__ECHO__;

	return $output;
}

function dispItems($output, $nodeRoot, $nodeInfo, $nodeList, $rootNode, $mode){
	foreach( $nodeRoot['Items'] as $node ){
		$nodeNum = $node['num'];

		for( $i = 0 ; $i < $node['level'] ; $i++ ){
			$output .= "\t";
		}

		$output .= "\tnode" . $rootNode . " -> node" . $nodeNum . ";\n";
		if( $mode !== 0 ) {
			$rootNode = $nodeNum;
		}
		$nodeInfo[] = $node;
		if( isset( $node['Items'] ) ){
			if( $node['command'] === 'IF' ){
				dispItems(&$output, &$node, &$nodeInfo, &$nodeList, $nodeNum, 0 );
			} else {
				dispItems(&$output, &$node, &$nodeInfo, &$nodeList, $nodeNum, 1 );
			}
		}
	}

	if( $mode !== 0 && !isset( $node['Items'] ) && $node['command'] !== 'END' ) {
		$nodeList[] = $nodeNum;
	}

	return $nodeList;
}

自分で組んだながら汚いソースです。。。ネストしているデータ構造で最後に合流するってデータはちゃんとクラスを作って処理しないとだめっすね。

応用

Wikiとかでささっと大まかなフローを書くと、そのまま図になるって利用方法を想定して実験しています。Pukiwikiとかのプラグインで組んであげると、割と簡単に図が描けると思います。

他の例

初期化処理
#IF 個数?
	#CASE 1個
		処理1
		処理2
		処理3
	#CASE それ以外
		処理1
		処理4
		#IF 状態?
			#CASE 新規
				処理5
				処理8
				処理9
				処理10
				処理11
				処理12
			#CASE その他
				処理6
				処理7
				#END
終了処理

すこし大き目のサンプルです。その場で完了する場合から#ENDを追加しています。

まとめ

この手の簡易フロー記述言語は拡張していくとそれこそプログラムになるので、あくまで簡易がいいと思います(笑)

利用想定としてはWikiを利用した仕様書作成ってのを研究していて表は書きやすいのですが、大まかな流れや条件別の計算式などを表す場合にはどうしてもフローの方がわかりやすいので検証してみました。

個人的にはこれぐらいの表現ができればあとは表でなんとかできるかなと考え中です。pukiwikiであれば子どものページを含めて1ページに表示する自作プラグインなどがあるので、コピーして張り付けるとWordに見出し付きで仕様書っぽい物ができてしまいます。

とはいえgraphvizは事前にセットアップしておく必要がありますので環境を作るのが結構面倒です。graphvizJava実装であるGrappaを利用してGAE/J上で構築した方が幸せになりそうな気がしました。。。