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;
+ }
+}