フロー図を描いてみる
仕様書などで簡単なフローを描く場合ってありますよね?
本当はJUDEとかのUMLエディタを使えばいいのですが、単純なフローとか特殊なソフトとかを入れなくても描けるといいなって思い実験しました。
概要
独自のフローを記述する言語を経由してGraphviz(http://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; }
自分で組んだながら汚いソースです。。。ネストしているデータ構造で最後に合流するってデータはちゃんとクラスを作って処理しないとだめっすね。
他の例
初期化処理 #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は事前にセットアップしておく必要がありますので環境を作るのが結構面倒です。graphvizのJava実装であるGrappaを利用してGAE/J上で構築した方が幸せになりそうな気がしました。。。