From ef8888c679eae09d5de2e7d7b34850b37aff1c6b Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Mon, 13 Apr 2015 12:59:26 +0000 Subject: [PATCH] Rework of the relation diagrams: implemented MetaModel::GetRelatedObjectsDown (still not taking the redundancy into account) SVN:trunk[3544] --- core/dbobject.class.php | 3 + core/dbobjectset.class.php | 9 +- core/metamodel.class.php | 26 ++ core/relationgraph.class.inc.php | 502 ++++------------------------- core/simplegraph.class.inc.php | 520 +++++++++++++++++++++++++++++++ 5 files changed, 615 insertions(+), 445 deletions(-) create mode 100644 core/simplegraph.class.inc.php diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 317676ed8..069eceaa5 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -2545,6 +2545,9 @@ abstract class DBObject implements iDisplay return array(); } + /** + * Will be deprecated soon - use MetaModel::GetRelatedObjectsDown/Up instead to take redundancy into account + */ public function GetRelatedObjects($sRelCode, $iMaxDepth = 99, &$aResults = array()) { // Temporary patch: until the impact analysis GUI gets rewritten, diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index 1c1d3dfff..1fae8ff86 100644 --- a/core/dbobjectset.class.php +++ b/core/dbobjectset.class.php @@ -967,12 +967,7 @@ class DBObjectSet } /** - * Compute the "RelatedObjects" (for the given relation, as defined by MetaModel::GetRelatedObjects) for a whole set of DBObjects - * - * @param string $sRelCode The code of the relation to use for the computation - * @param int $iMaxDepth Teh maximum recursion depth - * - * @return Array An array containg all the "related" objects + * Will be deprecated soon - use MetaModel::GetRelatedObjectsDown/Up instead to take redundancy into account */ public function GetRelatedObjects($sRelCode, $iMaxDepth = 99) { @@ -996,7 +991,7 @@ class DBObjectSet } return $aRelatedObjs; } - + /** * Builds an object that contains the values that are common to all the objects * in the set. If for a given attribute, objects in the set have various values diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 44e91d9b5..56d4acf7a 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -1389,6 +1389,32 @@ abstract class MetaModel } } + /** + * Compute the "RelatedObjects" (for the given relation, as defined by MetaModel::GetRelatedObjects) for a whole set of DBObjects + * + * @param string $sRelCode The code of the relation to use for the computation + * @param array $asourceObjects The objects to start with + * @param int $iMaxDepth + * @param boolean $bEnableReduncancy + * + * @return RelationGraph The graph of all the related objects + */ + static public function GetRelatedObjectsDown($sRelCode, $aSourceObjects, $iMaxDepth = 99, $bEnableRedundancy = true) + { + $oGraph = new RelationGraph(); + foreach ($aSourceObjects as $oObject) + { + $oSourceNode = new RelationObjectNode($oGraph, $oObject); + $oSourceNode->SetProperty('source', true); + } + $aSourceNodes = $oGraph->_GetNodes(); + foreach ($aSourceNodes as $oSourceNode) + { + $oGraph->AddRelatedObjectsDown($sRelCode, $oSourceNode, $iMaxDepth, $bEnableRedundancy); + } + return $oGraph; + } + // // Object lifecycle model // diff --git a/core/relationgraph.class.inc.php b/core/relationgraph.class.inc.php index d13a202d4..1e501cb6a 100644 --- a/core/relationgraph.class.inc.php +++ b/core/relationgraph.class.inc.php @@ -16,470 +16,96 @@ // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see /** - * Data structures (i.e. PHP classes) to manage "graphs" + * Data structures (i.e. PHP classes) to build and use relation 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(); */ +require_once(APPROOT.'core/simplegraph.class.inc.php'); + /** - * Exceptions generated by the RelationGraph class + * An object Node inside a RelationGraph */ -class RelationGraphException extends Exception +class RelationObjectNode extends GraphNode { - + public function __construct($oGraph, $oObject) + { + parent::__construct($oGraph, self::MakeId($oObject)); + $this->SetProperty('object', $oObject); + $this->SetProperty('label', get_class($oObject).'::'.$oObject->GetKey().' ('.$oObject->Get('friendlyname').')'); + } + + public static function MakeId($oObject) + { + return get_class($oObject).'::'.$oObject->GetKey(); + } + } /** - * The parent class of all elements which can be part of a RelationGraph + * An redundancy Node inside a RelationGraph */ -class RelationElement +class RelationRedundancyNode extends GraphNode { - protected $sId; - protected $aProperties; - - /** - * Constructor - * @param string $sId The identifier of the object in the graph - */ - public function __construct($sId) + public function GetDotAttributes() { - $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; + $sDot = 'shape=point,label="'.$this->GetProperty('threshold').'"'; + return $sDot; +// shape=point } +} + +class RelationGraph extends SimpleGraph +{ /** - * Set the value of a named property for the object - * @param string $sPropName The name of the property to set - * @param mixed $value + * Recursively find related objects, and add them into the graph + * + * @param string $sRelCode The code of the relation to use for the computation + * @param array $oObjectNode The node from which to compute the neighbours + * @param int $iMaxDepth + * @param boolean $bEnableReduncancy + * * @return void */ - public function SetProperty($sPropName, $value) + public function AddRelatedObjectsDown($sRelCode, $oObjectNode, $iMaxDepth, $bEnableRedundancy) { - $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 ($iMaxDepth > 0) { - if (count($oNode->GetOutgoingEdges()) > 0) + $oObject = $oObjectNode->GetProperty('object'); + foreach (MetaModel::EnumRelationQueries(get_class($oObject), $sRelCode, true) as $sDummy => $aQueryInfo) { - foreach($oNode->GetOutgoingEdges() as $oEdge) + $sQuery = $aQueryInfo['sQueryDown']; + try { - $sDot .= "\t".$oNode->GetId()." -> ".$oEdge->GetSinkNode()->GetId()." [ label=\"".$oEdge->GetId()."\" ];\n"; + $oFlt = DBObjectSearch::FromOQL($sQuery); + $oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery()); + $oRelatedObj = $oObjSet->Fetch(); + } + catch (Exception $e) + { + throw new Exception("Wrong query (downstream) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage()); + } + if ($oRelatedObj) + { + do + { + $sObjectRef = RelationObjectNode::MakeId($oRelatedObj); + $oRelatedNode = $this->GetNode($sObjectRef); + if (is_null($oRelatedNode)) + { + $oRelatedNode = new RelationObjectNode($this, $oRelatedObj); + + // Recurse + $this->AddRelatedObjectsDown($sRelCode, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy); + } + $oEdge = new GraphEdge($this, $oObjectNode->GetId().' to '.$oRelatedNode->GetId(), $oObjectNode, $oRelatedNode); + } + while ($oRelatedObj = $oObjSet->Fetch()); } } - 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 diff --git a/core/simplegraph.class.inc.php b/core/simplegraph.class.inc.php new file mode 100644 index 000000000..f3bc2b630 --- /dev/null +++ b/core/simplegraph.class.inc.php @@ -0,0 +1,520 @@ + +/** + * 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/simplegraph.class.inc.php'); + * + * $oGraph = new SimpleGraph(); + * + * $oNode1 = new GraphNode($oGraph, 'Source1'); + * $oNode2 = new GraphNode($oGraph, 'Sink'); + * $oEdge1 = new GraphEdge($oGraph, 'flow1', $oNode1, $oNode2); + * $oNode3 = new GraphNode($oGraph, 'Source2'); + * $oEdge2 = new GraphEdge($oGraph, 'flow2', $oNode3, $oNode2); + * $oEdge2 = new GraphEdge($oGraph, 'flow3', $oNode2, $oNode3); + * $oEdge2 = new GraphEdge($oGraph, 'flow4', $oNode1, $oNode3); + * + * echo $oGraph->DumpAsHtmlImage(); // requires graphviz + * echo $oGraph->DumpAsHtmlText(); + */ + +/** + * Exceptions generated by the SimpleGraph class + */ +class SimpleGraphException extends Exception +{ + +} + +/** + * The parent class of all elements which can be part of a SimpleGraph + */ +class GraphElement +{ + 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 = null) + { + 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 SimpleGraph + */ +class GraphNode extends GraphElement +{ + protected $aIncomingEdges; + protected $aOutgoingEdges; + + /** + * Create a new node inside a graph + * @param SimpleGraph $oGraph + * @param string $sId The unique identifier of this node inside the graph + */ + public function __construct(SimpleGraph $oGraph, $sId) + { + parent::__construct($sId); + $this->aIncomingEdges = array(); + $this->aOutgoingEdges = array(); + $oGraph->_AddNode($this); + } + + public function GetDotAttributes() + { + $sLabel = addslashes($this->GetProperty('label', $this->GetId())); + $sDot = 'label="'.$sLabel.'"'; + return $sDot; + } + + /** + * INTERNAL USE ONLY + * @param GraphEdge $oEdge + */ + public function _AddIncomingEdge(GraphEdge $oEdge) + { + $this->aIncomingEdges[$oEdge->GetId()] = $oEdge; + } + + /** + * INTERNAL USE ONLY + * @param GraphEdge $oEdge + */ + public function _AddOutgoingEdge(GraphEdge $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 SimpleGraph + */ +class GraphEdge extends GraphElement +{ + protected $oSourceNode; + protected $oSinkNode; + + /** + * Create a new directed edge inside the given graph + * @param SimpleGraph $oGraph + * @param string $sId The unique identifier of this edge in the graph + * @param GraphNode $oSourceNode + * @param GraphNode $oSinkNode + */ + public function __construct(SimpleGraph $oGraph, $sId, GraphNode $oSourceNode, GraphNode $oSinkNode) + { + parent::__construct($sId); + $this->oSourceNode = $oSourceNode; + $this->oSinkNode = $oSinkNode; + $oGraph->_AddEdge($this); + } + + /** + * Get the "source" node for this edge + * @return GraphNode + */ + public function GetSourceNode() + { + return $this->oSourceNode; + } + + /** + * Get the "sink" node for this edge + * @return GraphNode + */ + public function GetSinkNode() + { + return $this->oSinkNode; + } + + public function GetDotAttributes() + { + $sLabel = addslashes($this->GetProperty('label', '')); + $sDot = 'label="'.$sLabel.'"'; + return $sDot; + } +} + +/** + * The main container for a graph: SimpleGraph + */ +class SimpleGraph +{ + 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(GraphNode $oNode) + { + if (array_key_exists($oNode->GetId(), $this->aNodes)) throw new SimpleGraphException('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 | GraphNode + */ + public function GetNode($sId) + { + return array_key_exists($sId, $this->aNodes) ? $this->aNodes[$sId] : null; + } + + /** + * Determine if the id already exists in amongst the existing nodes + * @param string $sId + * @return boolean + */ + public function HasNode($sId) + { + return array_key_exists($sId, $this->aNodes); + } + + /** + * INTERNAL USE ONLY + * @param GraphEdge $oEdge + * @throws SimpleGraphException + */ + public function _AddEdge(GraphEdge $oEdge) + { + if (array_key_exists($oEdge->GetId(), $this->aEdges)) throw new SimpleGraphException('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 | GraphEdge + */ + public function GetEdge($sId) + { + return array_key_exists($sId, $this->aEdges) ? $this->aEdges[$sId] : null; + } + + /** + * Determine if the id already exists in amongst the existing edges + * @param string $sId + * @return boolean + */ + public function HasEdge($sId) + { + return array_key_exists($sId, $this->aEdges); + } + + /** + * Get the description of the graph as a text string in the graphviz 'dot' language + * @return string + */ + public function GetDotDescription() + { + $sDot = +<< $oNode) + { + $sDot .= "\t\"".$oNode->GetId()."\" [ ".$oNode->GetDotAttributes()." ];\n"; + if (count($oNode->GetOutgoingEdges()) > 0) + { + foreach($oNode->GetOutgoingEdges() as $oEdge) + { + $sDot .= "\t\"".$oNode->GetId()."\" -> \"".$oEdge->GetSinkNode()->GetId()."\" [ ".$oEdge->GetDotAttributes()." ];\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); + } + else + { + throw new Exception('graphviz not found (executable path: '.$sDotExecutable.')'); + } + 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 'GraphNode': + 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 'GraphEdge': + $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 SimpleGraph $oGraph The graph to browse + * @param string $sType "Node", "Edge" or null + */ + public function __construct(SimpleGraph $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; + } +}