From b9b5287b37d1187dc2b770068119f5035eac16d4 Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Wed, 8 Apr 2015 14:50:01 +0000 Subject: [PATCH] Helper class to managed relation graphs. SVN:trunk[3541] --- core/relationgraph.class.inc.php | 485 +++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 core/relationgraph.class.inc.php diff --git a/core/relationgraph.class.inc.php b/core/relationgraph.class.inc.php new file mode 100644 index 000000000..d13a202d4 --- /dev/null +++ b/core/relationgraph.class.inc.php @@ -0,0 +1,485 @@ + +/** + * Data structures (i.e. PHP classes) to manage "graphs" + * + * @copyright Copyright (C) 2015 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * + * Example: + * require_once('../approot.inc.php'); + * require_once(APPROOT.'application/startup.inc.php'); + * require_once(APPROOT.'core/relationgraph.class.inc.php'); + * + * $oGraph = new RelationGraph(); + * + * $oNode1 = new RelationNode($oGraph, 'Source1'); + * $oNode2 = new RelationNode($oGraph, 'Sink'); + * $oEdge1 = new RelationEdge($oGraph, 'flow1', $oNode1, $oNode2); + * $oNode3 = new RelationNode($oGraph, 'Source2'); + * $oEdge2 = new RelationEdge($oGraph, 'flow2', $oNode3, $oNode2); + * $oEdge2 = new RelationEdge($oGraph, 'flow3', $oNode2, $oNode3); + * $oEdge2 = new RelationEdge($oGraph, 'flow4', $oNode1, $oNode3); + * + * echo $oGraph->DumpAsHtmlImage(); // requires graphviz + * echo $oGraph->DumpAsHtmlText(); + */ + +/** + * Exceptions generated by the RelationGraph class + */ +class RelationGraphException extends Exception +{ + +} + +/** + * The parent class of all elements which can be part of a RelationGraph + */ +class RelationElement +{ + protected $sId; + protected $aProperties; + + /** + * Constructor + * @param string $sId The identifier of the object in the graph + */ + public function __construct($sId) + { + $this->sId = $sId; + $this->aProperties = array(); + } + + /** + * Get the identifier of the object in the graph + * @return string + */ + public function GetId() + { + return $this->sId; + } + + /** + * Get the value of the given named property for the object + * @param string $sPropName The name of the property to get + * @param mixed $defaultValue The default value to return if the property does not exist + * @return mixed + */ + public function GetProperty($sPropName, $defaultValue) + { + return array_key_exists($sPropName, $this->aProperties) ? $this->aProperties[$sPropName] : $defaultValue; + } + + /** + * Set the value of a named property for the object + * @param string $sPropName The name of the property to set + * @param mixed $value + * @return void + */ + public function SetProperty($sPropName, $value) + { + $this->aProperties[$sPropName] = $value; + } + + /** + * Get all the known properties of the object + * @return Ambigous + */ + public function GetProperties() + { + return $this->aProperties; + } +} + +/** + * A Node inside a RelationGraph + */ +class RelationNode extends RelationElement +{ + protected $aIncomingEdges; + protected $aOutgoingEdges; + + /** + * Create a new node inside a graph + * @param RelationGraph $oGraph + * @param string $sId The unique identifier of this node inside the graph + */ + public function __construct(RelationGraph $oGraph, $sId) + { + parent::__construct($sId); + $this->aIncomingEdges = array(); + $this->aOutgoingEdges = array(); + $oGraph->_AddNode($this); + } + + /** + * INTERNAL USE ONLY + * @param RelationEdge $oEdge + */ + public function _AddIncomingEdge(RelationEdge $oEdge) + { + $this->aIncomingEdges[$oEdge->GetId()] = $oEdge; + } + + /** + * INTERNAL USE ONLY + * @param RelationEdge $oEdge + */ + public function _AddOutgoingEdge(RelationEdge $oEdge) + { + $this->aOutgoingEdges[$oEdge->GetId()] = $oEdge; + } + + /** + * Get the list of all incoming edges on the current node + * @return Ambigous + */ + public function GetIncomingEdges() + { + return $this->aIncomingEdges; + } + + /** + * Get the list of all outgoing edges from the current node + * @return Ambigous + */ + public function GetOutgoingEdges() + { + return $this->aOutgoingEdges; + } + +} + +/** + * A directed Edge inside a RelationGraph + */ +class RelationEdge extends RelationElement +{ + protected $oSourceNode; + protected $oSinkNode; + + /** + * Create a new directed edge inside the given graph + * @param RelationGraph $oGraph + * @param string $sId The unique identifier of this edge in the graph + * @param RelationNode $oSourceNode + * @param RelationNode $oSinkNode + */ + public function __construct(RelationGraph $oGraph, $sId, RelationNode $oSourceNode, RelationNode $oSinkNode) + { + parent::__construct($sId); + $this->oSourceNode = $oSourceNode; + $this->oSinkNode = $oSinkNode; + $oGraph->_AddEdge($this); + } + + /** + * Get the "source" node for this edge + * @return RelationNode + */ + public function GetSourceNode() + { + return $this->oSourceNode; + } + + /** + * Get the "sink" node for this edge + * @return RelationNode + */ + public function GetSinkNode() + { + return $this->oSinkNode; + } +} + +/** + * The main container for a graph: RelationGraph + */ +class RelationGraph +{ + protected $aNodes; + protected $aEdges; + + /** + * Creates a new empty graph + */ + public function __construct() + { + $this->aNodes = array(); + $this->aEdges = array(); + } + + /** + * INTERNAL USE ONLY + * @return Ambigous + */ + public function _GetNodes() + { + return $this->aNodes; + } + + /** + * INTERNAL USE ONLY + * @return Ambigous + */ + public function _GetEdges() + { + return $this->aEdges; + } + + /** + * INTERNAL USE ONLY + * @return Ambigous + */ + public function _AddNode(RelationNode $oNode) + { + if (array_key_exists($oNode->GetId(), $this->aNodes)) throw new RelationGraphException('Cannot add node (id='.$oNode->GetId().') to the graph. A node with the same id already exists inthe graph.'); + + $this->aNodes[$oNode->GetId()] = $oNode; + } + + /** + * Get the node identified by $sId or null if not found + * @param string $sId + * @return NULL | RelationNode + */ + public function GetNode($sId) + { + return array_key_exists($sId, $this->aNodes) ? $this->aNodes[$sId] : null; + } + + /** + * INTERNAL USE ONLY + * @param RelationEdge $oEdge + * @throws RelationGraphException + */ + public function _AddEdge(RelationEdge $oEdge) + { + if (array_key_exists($oEdge->GetId(), $this->aEdges)) throw new RelationGraphException('Cannot add edge (id='.$oEdge->GetId().') to the graph. An edge with the same id already exists inthe graph.'); + + $this->aEdges[$oEdge->GetId()] = $oEdge; + $oEdge->GetSourceNode()->_AddOutgoingEdge($oEdge); + $oEdge->GetSinkNode()->_AddIncomingEdge($oEdge); + } + + /** + * Get the edge indentified by $sId or null if not found + * @param string $sId + * @return NULL | RelationEdge + */ + public function GetEdge($sId) + { + return array_key_exists($sId, $this->aEdges) ? $this->aEdges[$sId] : null; + } + + /** + * Get the description of the graph as a text string in the graphviz 'dot' language + * @return string + */ + public function GetDotDescription() + { + $sDot = +<< $oNode) + { + if (count($oNode->GetOutgoingEdges()) > 0) + { + foreach($oNode->GetOutgoingEdges() as $oEdge) + { + $sDot .= "\t".$oNode->GetId()." -> ".$oEdge->GetSinkNode()->GetId()." [ label=\"".$oEdge->GetId()."\" ];\n"; + } + } + else + { + $sDot .= "\t".$oNode->GetId().";\n"; + } + } + + $sDot .= "}\n"; + return $sDot; + } + + /** + * Get the description of the graph as an embedded PNG image (using a data: url) as + * generated by graphviz (requires graphviz to be installed on the machine and the path to + * dot/dot.exe to be configured in the iTop configuration file) + * Note: the function creates temporary files in APPROOT/data/tmp + * @return string + */ + public function DumpAsHtmlImage() + { + $sDotExecutable = MetaModel::GetConfig()->Get('graphviz_path'); + if (file_exists($sDotExecutable)) + { + // create the file with Graphviz + if (!is_dir(APPROOT."data")) + { + @mkdir(APPROOT."data"); + } + if (!is_dir(APPROOT."data/tmp")) + { + @mkdir(APPROOT."data/tmp"); + } + $sImageFilePath = tempnam(APPROOT."data/tmp", 'png-'); + $sDotDescription = $this->GetDotDescription(); + $sDotFilePath = tempnam(APPROOT."data/tmp", 'dot-'); + + $rFile = @fopen($sDotFilePath, "w"); + @fwrite($rFile, $sDotDescription); + @fclose($rFile); + $aOutput = array(); + $CommandLine = "$sDotExecutable -v -Tpng < $sDotFilePath -o$sImageFilePath 2>&1"; + + exec($CommandLine, $aOutput, $iRetCode); + if ($iRetCode != 0) + { + $sHtml = ''; + $sHtml .= "

Error:

"; + $sHtml .= "

The command:

$CommandLine
returned $iRetCode

"; + $sHtml .= "

The output of the command is:

\n".implode("\n", $aOutput)."

"; + $sHtml .= "
"; + $sHtml .= "

Content of the '".basename($sDotFilePath)."' file:

\n$sDotDescription
"; + } + else + { + $sHtml = ''; + @unlink($sImageFilePath); + } + @unlink($sDotFilePath); + } + return $sHtml; + } + + /** + * Get the description of the graph as some HTML text + * @return string + */ + public function DumpAsHTMLText() + { + $sHtml = ''; + $oIterator = new RelationTypeIterator($this); + + foreach($oIterator as $key => $oElement) + { + $sHtml .= "

$key: ".get_class($oElement)."::".$oElement->GetId()."

"; + + switch(get_class($oElement)) + { + case 'RelationNode': + if (count($oElement->GetIncomingEdges()) > 0) + { + $sHtml .= "
    Incoming edges:\n"; + foreach($oElement->GetIncomingEdges() as $oEdge) + { + $sHtml .= "
  • From: ".$oEdge->GetSourceNode()->GetId()."
  • \n"; + } + $sHtml .= "
\n"; + } + if (count($oElement->GetOutgoingEdges()) > 0) + { + $sHtml .= "
    Outgoing edges:\n"; + foreach($oElement->GetOutgoingEdges() as $oEdge) + { + $sHtml .= "
  • To: ".$oEdge->GetSinkNode()->GetId()."
  • \n"; + } + $sHtml .= "
\n"; + } + break; + + case 'RelationEdge': + $sHtml .= "

From: ".$oElement->GetSourceNode()->GetId().", to:".$oElement->GetSinkNode()->GetId()."

\n"; + break; + } + } + return $sHtml; + } +} + +/** + * A simple iterator to "browse" the whole content of a graph, + * either for only a given type of elements (Node | Edge) or for every type. + */ +class RelationTypeIterator implements Iterator +{ + protected $iCurrentIdx; + protected $aList; + + /** + * Constructor + * @param RelationGraph $oGraph The graph to browse + * @param string $sType "Node", "Edge" or null + */ + public function __construct(RelationGraph $oGraph, $sType = null) + { + $this->iCurrentIdx = -1; + $this->aList = array(); + + switch($sType) + { + case 'Node': + foreach($oGraph->_GetNodes() as $oNode) $this->aList[] = $oNode; + break; + + case 'Edge': + foreach($oGraph->_GetEdges() as $oEdge) $this->aList[] = $oEdge; + break; + + default: + foreach($oGraph->_GetNodes() as $oNode) $this->aList[] = $oNode; + foreach($oGraph->_GetEdges() as $oEdge) $this->aList[] = $oEdge; + } + } + + public function rewind() + { + $this->iCurrentIdx = 0; + } + + public function valid() + { + return array_key_exists($this->iCurrentIdx, $this->aList); + } + + public function next() + { + $this->iCurrentIdx++; + } + + public function current() + { + return $this->aList[$this->iCurrentIdx]; + } + + public function key() + { + return $this->iCurrentIdx; + } +} \ No newline at end of file