|
|
|
require_once("System.php"); |
|
/** |
|
* @class graphviz |
|
* @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAMgmail.com) |
|
* @brief class for making Graphviz plots online |
|
*/ |
|
class graphviz { |
|
|
|
var $directed = True; |
|
|
|
var $_returnFalseOnError = true; |
|
|
|
var $graph = array( "name" => "G", |
|
"__attrs__" => array(), |
|
"__nodes__" => array(), |
|
"__edges__" => array(), |
|
"__clusters__" => array() ); |
|
|
|
var $fmt = "svg"; |
|
var $dotCommand = "/usr/bin/dot"; |
|
var $neatoCommand = "/usr/bin/neato"; |
|
|
|
/** |
|
* @brief c'tor |
|
* @param $name graph name |
|
* @param $fmt output format |
|
*/ |
|
public function __construct( $name=null, $fmt=null, $attr=null ) { |
|
|
|
if ( $attr == null ) $attr = array(); |
|
$this->graph["_attrs__"] = $attr; |
|
|
|
if ( $name != null && is_string($name) ) { |
|
$this->graph["name"] = $name; |
|
} else { |
|
$name = "G"; |
|
} |
|
if ( $fmt != null && is_string($fmt) ) { |
|
$this->fmt = $fmt; |
|
} |
|
|
|
$this->graph = array( "name" => $name, |
|
"__attrs__" => $attr, |
|
"__nodes__" => array(), |
|
"__edges__" => array(), |
|
"__clusters__" => array() ); |
|
} |
|
|
|
/** |
|
* @brief sets directed/undirected flag for the graph. |
|
* @param $directed Directed (TRUE) or undirected (FALSE) graph. |
|
*/ |
|
public function setDirected( $directed=True ) { |
|
if (is_bool($directed)) { |
|
$this->directed = $directed; |
|
} |
|
} |
|
|
|
/** |
|
* @brief cluster finder |
|
* @param $clusterName name of cluster to find |
|
* @param $where reference to array |
|
*/ |
|
public function &findCluster( $clusterName, &$where=null) { |
|
if ( $where == null ) { |
|
$where =& $this->graph; |
|
} |
|
if ( isset( $where["__clusters__"][$clusterName] ) ) { |
|
return $where["__clusters__"][$clusterName]; |
|
} else { |
|
foreach ( $where["__clusters__"] as $key => &$cluster ) { |
|
return $this->findCluster( $clusterName, &$cluster ); |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
/** |
|
* @brief add subcluster to graph |
|
* @param $id new cluster id |
|
* @parm $attr array with attributes |
|
* @param $toCluster parent subcluster |
|
*/ |
|
public function addCluster( $id, $attr=null, $toCluster=null ) { |
|
|
|
if ( $attr == null) $attr = array(); |
|
if ( $toCluster == null ) { |
|
if ( ! isset( $this->graph["__clusters__"][$id] ) ) { |
|
$this->graph["__clusters__"][$id] = array( "__attrs__" => $attr, |
|
"__nodes__" => array(), |
|
"__edges__" => array(), |
|
"__clusters__" => array() ) ; |
|
|
|
} |
|
} else { |
|
$where =& $this->findCluster( $toCluster ); |
|
if ( $where != null ) { |
|
$where["__clusters__"][$id] = array( "__attrs__" => $attr, |
|
"__nodes__" => array(), |
|
"__edges__" => array(), |
|
"__clusters__" => array() ) ; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* @brief add node to graph |
|
* @param $id node id |
|
* @param $attr array with node attributes |
|
* @param $cluster parent cluster |
|
*/ |
|
public function addNode( $id, $attr=null, $cluster=null ) { |
|
|
|
if ( $attr == null ) $attr = array(); |
|
|
|
if ( $cluster != null ) { |
|
$where =& $this->findCluster( $cluster ); |
|
if ( $where != null ) { |
|
$where["__nodes__"][$id] = $attr; |
|
} else { |
|
$this->addCluster($cluster); |
|
$this->graph["__clusters__"][$cluster]["__nodes__"][$id] = $attr; |
|
} |
|
} else { |
|
$this->graph["__nodes__"][$id] = $attr; |
|
} |
|
} |
|
|
|
/** |
|
* @brief connect to nodes |
|
* @param $from begin node |
|
* @param $to end node |
|
* @param $attr edge attributes |
|
*/ |
|
public function addEdge( $from, $to, $attr=null ) { |
|
|
|
$id = $from . "-->" . $to; |
|
if ( !isset( $this->graph["__edges__"][$id]) ) { |
|
$this->graph["__edges__"][$id] = array( "__from__" => $from, "__to__" => $to, "__attrs__" => array() ); |
|
if ( is_array( $attr ) ) { |
|
$this->graph["__edges__"][$id]["__attrs__"] = $attr; |
|
} |
|
} else { |
|
if ( is_array($attr) ) { |
|
$this->graph["__edges__"][$id]["__attrs__"] = array_merge($attr, $this->graph["__edges__"][$id]["__attrs__"] ); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* @brief transform internal subclusters representation to dot language |
|
* @param where location to parse |
|
* @return string with GraphViz markup |
|
*/ |
|
private function __parseClusters( $where = null ) { |
|
$out = ""; |
|
if ( $where == null ) { |
|
$where = $this->graph; |
|
} |
|
foreach ( $where["__clusters__"] as $clusterName => $cluster ) { |
|
$out .= "subgraph cluster_".$clusterName. " {\n"; |
|
// attributes |
|
foreach ( $cluster["__attrs__"] as $attr => $attrValue ) { |
|
$attributeList[] = $attr.'="'.$attrValue.'"'; |
|
} |
|
if (!empty($attributeList) ) { |
|
$out .= implode(';', $attributeList).";\n"; |
|
} |
|
// nodes |
|
foreach ( $cluster["__nodes__"] as $nodeName => $attr ) { |
|
foreach ( $attr as $attrName => $attrValue ) { |
|
$nodeAttr[] = $attrName . "=\"" . $attrValue . "\""; |
|
} |
|
$out .= $nodeName; |
|
if ( !empty($nodeAttr) ) { |
|
$out .= " [ " . implode(',', $nodeAttr) . " ]"; |
|
} |
|
$out .= ";\n"; |
|
} |
|
$out .= $this->__parseClusters( $cluster ); |
|
$out .= "}\n"; |
|
} |
|
return $out; |
|
} |
|
|
|
/** |
|
* @brief transform internal graph representation to dot language |
|
* @return string GraphViz markup |
|
*/ |
|
public function parse() { |
|
$parsedGraph = $this->directed ? "digraph " : "graph "; |
|
|
|
if ( isset($this->graph["name"]) && |
|
is_string($this->graph["name"]) ) { |
|
$parsedGraph .= $this->graph["name"] . " {\n"; |
|
} else { |
|
$parsedGraph .= "G {\n"; |
|
} |
|
|
|
if (isset($this->graph["__attrs__"])) { |
|
foreach ($this->graph["__attrs__"] as $key => $value) { |
|
$attributeList[] = $key . '="' . $value . '"'; |
|
} |
|
if ( !empty($attributeList) ) { |
|
$parsedGraph .= "graph [ ".implode(",", $attributeList) . " ];\n"; |
|
} |
|
} |
|
|
|
// subclusters |
|
$parsedGraph .= $this->__parseClusters(); |
|
|
|
|
|
foreach ( $this->graph["__nodes__"] as $nodeName => $attr ) { |
|
foreach ( $attr as $attrName => $attrValue ) { |
|
$nodeAttr[] = $attrName . "=\"" . $attrValue . "\""; |
|
} |
|
$parsedGraph .= $nodeName; |
|
if ( !empty($nodeAttr) ) { |
|
$parsedGraph .= " [ " . implode(',', $nodeAttr) . " ]"; |
|
} |
|
$parsedGraph .= ";\n"; |
|
} |
|
|
|
// edges |
|
foreach ( $this->graph["__edges__"] as $id => $edge ) { |
|
$from = $edge["__from__"]; |
|
$to = $edge["__to__"]; |
|
|
|
$egdeAttributes = $edge["__attrs__"]; |
|
|
|
foreach ( $edgeAttributes as $attrName => $attrValue ) { |
|
$edgeAttrList[] = $attrName."=\"".$attrValue."\""; |
|
} |
|
|
|
$parsedGraph .= $from . " -> " . $to; |
|
if ( !empty($edgeAttrList) ) { |
|
$parsedGraph .= " [ " . implode(",", $edgeAttrList) . " ]"; |
|
} |
|
$parsedGraph .= ";\n"; |
|
|
|
} |
|
// end of graph |
|
$parsedGraph .= "}\n"; |
|
|
|
return $parsedGraph; |
|
} |
|
|
|
|
|
function fetch($format = 'svg', $command = null) { |
|
$file = $this->saveParsedGraph(); |
|
if (!$file || PEAR::isError($file)) { |
|
return $file; |
|
} |
|
$outputfile = $file . '.' . $format; |
|
$rendered = $this->renderDotFile($file, $outputfile, $format, |
|
$command); |
|
if ($rendered !== true) { |
|
return $rendered; |
|
} |
|
@unlink($file); |
|
$fp = fopen($outputfile, 'rb'); |
|
if (!$fp) { |
|
if ($this->_returnFalseOnError) { |
|
return false; |
|
} |
|
$error = PEAR::raiseError('Could not read rendered file'); |
|
return $error; |
|
} |
|
$data = fread($fp, filesize($outputfile)); |
|
fclose($fp); |
|
@unlink($outputfile); |
|
return $data; |
|
} |
|
|
|
|
|
function saveParsedGraph($file = '') { |
|
$parsedGraph = $this->parse(); |
|
if (!empty($parsedGraph)) { |
|
if (empty($file)) { |
|
$file = System::mktemp('graph_'); |
|
} |
|
if ($fp = @fopen($file, 'wb')) { |
|
@fputs($fp, $parsedGraph); |
|
@fclose($fp); |
|
return $file; |
|
} |
|
} |
|
if ($this->_returnFalseOnError) { |
|
return false; |
|
} |
|
$error = PEAR::raiseError('Could not save graph'); |
|
return $error; |
|
} |
|
|
|
|
|
|
|
function renderDotFile($dotfile, $outputfile, $format = 'svg', $command = null) { |
|
if (!file_exists($dotfile)) { |
|
if ($this->_returnFalseOnError) { |
|
return false; |
|
} |
|
$error = PEAR::raiseError('Could not find dot file'); |
|
return $error; |
|
} |
|
$oldmtime = file_exists($outputfile) ? filemtime($outputfile) : 0; |
|
switch ($command) { |
|
case 'dot': |
|
case 'neato': |
|
break; |
|
default: |
|
$command = $this->directed ? 'dot' : 'neato'; |
|
} |
|
$command_orig = $command; |
|
$command = $this->binPath.(($command == 'dot') ? $this->dotCommand : $this->neatoCommand); |
|
$command .= ' -T'.escapeshellarg($format) .' -o'.escapeshellarg($outputfile) |
|
.' '.escapeshellarg($dotfile).' 2>&1'; |
|
exec($command, $msg, $return_val); |
|
clearstatcache(); |
|
if (file_exists($outputfile) && |
|
filemtime($outputfile) > $oldmtime |
|
&& $return_val == 0 ) { |
|
return true; |
|
} elseif ($this->_returnFalseOnError) { |
|
return false; |
|
} |
|
$error = PEAR::raiseError($command_orig.' command failed: '.implode("\n", $msg)); |
|
return $error; |
|
} |
|
|
|
|
|
|
|
/** |
|
* |
|
* Outputs image of the graph in a given format |
|
* This methods send HTTP headers |
|
* @param string $format Format of the output image. This may be one |
|
* of the formats supported by GraphViz. |
|
* @param string $command "dot" or "neato" |
|
* |
|
* @return boolean TRUE on success, FALSE or PEAR_Error otherwise |
|
*/ |
|
public function image( $format = 'svg', $command = null ) { |
|
|
|
$file = $this->saveParsedGraph(); |
|
if (!$file || PEAR::isError($file)) { |
|
return $file; |
|
} |
|
$outputfile = $file . '.' . $format; |
|
$rendered = $this->renderDotFile($file, $outputfile, $format, $command); |
|
|
|
if ($rendered !== true) { return $rendered; } |
|
$sendContentLengthHeader = true; |
|
switch (strtolower($format)) { |
|
case 'gif': |
|
case 'png': |
|
case 'bmp': |
|
case 'jpeg': |
|
case 'tiff': |
|
header('Content-Type: image/' . $format); |
|
break; |
|
case 'tif': |
|
header('Content-Type: image/tiff'); |
|
break; |
|
case 'jpg': |
|
header('Content-Type: image/jpeg'); |
|
break; |
|
case 'ico': |
|
header('Content-Type: image/x-icon'); |
|
break; |
|
case 'wbmp': |
|
header('Content-Type: image/vnd.wap.wbmp'); |
|
break; |
|
case 'pdf': |
|
header('Content-Type: application/pdf'); |
|
break; |
|
case 'mif': |
|
header('Content-Type: application/vnd.mif'); |
|
break; |
|
case 'vrml': |
|
header('Content-Type: application/x-vrml'); |
|
break; |
|
case 'svg': |
|
header('Content-Type: image/svg+xml'); |
|
break; |
|
case 'plain': |
|
case 'plain-ext': |
|
header('Content-Type: text/plain'); |
|
break; |
|
default: |
|
header('Content-Type: application/octet-stream'); |
|
$sendContentLengthHeader = false; |
|
} |
|
if ($sendContentLengthHeader) { |
|
header('Content-Length: ' . filesize($outputfile)); |
|
} |
|
$return = true; |
|
if (readfile($outputfile) === false) { |
|
$return = false; |
|
} |
|
@unlink($outputfile); |
|
return $return; |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class test_graphviz { |
|
|
|
public function __construct($fmt=null) { |
|
|
|
$this->gr = new graphviz( "a", "svg", array( "bgcolor" => "#ffff00") ); |
|
|
|
$this->gr->addCluster( "main", array( "label" => "main", "labelloc"=>"t", "bgcolor" => "#ff0000", "labeljust"=>"l" ), null ); |
|
$this->gr->addCluster( "sub", array( "label" => "sub", "labelloc"=>"t", "bgcolor" => "#0000ff", "labeljust"=>"l" ), "main" ); |
|
|
|
|
|
$this->gr->addNode( "node_in_graph", array("shape" => "box", "color" => "#00ff00") ); |
|
|
|
$this->gr->addNode( "node_in_main", array("shape" => "box"), "main" ); |
|
|
|
$this->gr->addNode( "node_in_sub", array("shape" => "box"), "sub" ); |
|
|
|
$this->gr->addEdge( "node_in_main", "node_in_graph" ); |
|
$this->gr->addEdge( "node_in_main", "node_in_sub" ); |
|
|
|
$this->gr->addEdge( "node_in_sub", "node_in_graph" ); |
|
|
|
if ( $fmt == null ) { |
|
$this->gr->image(); |
|
} else { |
|
echo " |
|
|
|
---dot---\n"; |
|
print_r( $this->gr->graph ); |
|
echo " |
|
|
|
|
|
"; |
|
echo " |
|
|
|
---graph---\n"; |
|
echo $this->gr->parse(); |
|
echo " |
|
|
|
|
|
"; |
|
} |
|
} |
|
} |
|
|
|
if ( isset($_GET["dot"] ) ) { |
|
$t = new test_graphviz( "dot" ); |
|
} |
|
|
|
if ( isset($_GET["test"] ) ) { |
|
$t = new test_graphviz(); |
|
} |