diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php
index da5b83b7c..e91f1febf 100644
--- a/application/displayblock.class.inc.php
+++ b/application/displayblock.class.inc.php
@@ -1412,13 +1412,20 @@ class MenuBlock extends DisplayBlock
}
}
// Relations...
- $aRelations = MetaModel::EnumRelations($sClass);
+ $aRelations = MetaModel::EnumRelationsEx($sClass);
if (count($aRelations))
{
$this->AddMenuSeparator($aActions);
- foreach($aRelations as $sRelationCode)
+ foreach($aRelations as $sRelationCode => $aRelationInfo)
{
- $aActions[$sRelationCode] = array ('label' => MetaModel::GetRelationLabel($sRelationCode), 'url' => "{$sRootUrl}pages/$sUIPage?operation=swf_navigator&relation=$sRelationCode&class=$sClass&id=$id{$sContext}");
+ if (array_key_exists('down', $aRelationInfo))
+ {
+ $aActions[$sRelationCode.'_down'] = array ('label' => $aRelationInfo['down'], 'url' => "{$sRootUrl}pages/$sUIPage?operation=swf_navigator&relation=$sRelationCode&direction=down&class=$sClass&id=$id{$sContext}");
+ }
+ if (array_key_exists('up', $aRelationInfo))
+ {
+ $aActions[$sRelationCode.'_up'] = array ('label' => $aRelationInfo['up'], 'url' => "{$sRootUrl}pages/$sUIPage?operation=swf_navigator&relation=$sRelationCode&direction=up&class=$sClass&id=$id{$sContext}");
+ }
}
}
/*
diff --git a/core/displayablegraph.class.inc.php b/core/displayablegraph.class.inc.php
new file mode 100644
index 000000000..c92fc2879
--- /dev/null
+++ b/core/displayablegraph.class.inc.php
@@ -0,0 +1,938 @@
+
+
+/**
+ * Special kind of Graph for producing some nice output
+ *
+ * @copyright Copyright (C) 2015 Combodo SARL
+ * @license http://opensource.org/licenses/AGPL-3.0
+ */
+
+class DisplayableNode extends GraphNode
+{
+ public $x;
+ public $y;
+
+ /**
+ * Create a new node inside a graph
+ * @param SimpleGraph $oGraph
+ * @param string $sId The unique identifier of this node inside the graph
+ * @param number $x Horizontal position
+ * @param number $y Vertical position
+ */
+ public function __construct(SimpleGraph $oGraph, $sId, $x = 0, $y = 0)
+ {
+ parent::__construct($oGraph, $sId);
+ $this->x = $x;
+ $this->y = $y;
+ $this->bFiltered = false;
+ }
+
+ public function GetIconURL()
+ {
+ return $this->GetProperty('icon_url', '');
+ }
+
+ public function GetLabel()
+ {
+ return $this->GetProperty('label', $this->sId);
+ }
+
+ public function GetWidth()
+ {
+ return max(32, 5*strlen($this->GetProperty('label'))); // approximation of the text's bounding box
+ }
+
+ public function GetHeight()
+ {
+ return 32;
+ }
+
+ public function Distance2(DisplayableNode $oNode)
+ {
+ $dx = $this->x - $oNode->x;
+ $dy = $this->y - $oNode->y;
+
+ $d2 = $dx*$dx + $dy*$dy - $this->GetHeight()*$this->GetHeight();
+ if ($d2 < 40)
+ {
+ $d2 = 40;
+ }
+ return $d2;
+ }
+
+ public function Distance(DisplayableNode $oNode)
+ {
+ return sqrt($this->Distance2($oNode));
+ }
+
+ public function GetForRaphael()
+ {
+ $aNode = array();
+ $aNode['shape'] = 'icon';
+ $aNode['icon_url'] = $this->GetIconURL();
+ $aNode['width'] = 32;
+ $aNode['source'] = ($this->GetProperty('source') == true);
+ $aNode['sink'] = ($this->GetProperty('sink') == true);
+ $aNode['x'] = $this->x;
+ $aNode['y']= $this->y;
+ $aNode['label'] = $this->GetLabel();
+ $aNode['id'] = $this->GetId();
+ $fOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
+ $aNode['icon_attr'] = array('opacity' => $fOpacity);
+ $aNode['text_attr'] = array('opacity' => $fOpacity);
+ return $aNode;
+ }
+
+ public function RenderAsPDF(TCPDF $oPdf, $fScale)
+ {
+ $Alpha = 1.0;
+ $oPdf->SetFillColor(200, 200, 200);
+ $oPdf->setAlpha(1);
+
+ $sIconUrl = $this->GetProperty('icon_url');
+ $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-production/', $sIconUrl);
+
+ if ($this->GetProperty('source'))
+ {
+ $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => array(204, 51, 51)));
+ $oPdf->Circle($this->x * $fScale, $this->y * $fScale, 16 * 1.25 * $fScale, 0, 360, 'D');
+ }
+ else if ($this->GetProperty('sink'))
+ {
+ $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => array(51, 51, 204)));
+ $oPdf->Circle($this->x * $fScale, $this->y * $fScale, 16 * 1.25 * $fScale, 0, 360, 'D');
+ }
+
+ if (!$this->GetProperty('is_reached'))
+ {
+ if (function_exists('imagecreatefrompng'))
+ {
+ $im = imagecreatefrompng($sIconPath);
+
+ if($im && imagefilter($im, IMG_FILTER_COLORIZE, 255, 255, 255))
+ {
+ $sTempImageName = APPROOT.'data/tmp-'.basename($sIconPath);
+ imagesavealpha($im, true);
+ imagepng($im, $sTempImageName);
+ imagedestroy($im);
+ $oPdf->Image($sTempImageName, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale);
+ }
+ }
+ $Alpha = 0.4;
+ $oPdf->setAlpha($Alpha);
+ }
+
+ $oPdf->Image($sIconPath, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale);
+ //$oPdf->Image(APPROOT.'images/blank-100x100.png', ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale, '', '', '', false, 300, '', false, $mask);
+ //Image($file, $x='', $y='', $w=0, $h=0, $type='', $link='', $align='', $resize=false, $dpi=300, $palign='', $ismask=false, $imgmask=false, $border=0, $fitbox=false, $hidden=false, $fitonpage=false, $alt=false, $altimgs=array())
+
+ $oPdf->SetFont('Helvetica', '', 24 * $fScale, '', true);
+ $width = $oPdf->GetStringWidth($this->GetProperty('label'));
+ $height = $oPdf->GetStringHeight(1000, $this->GetProperty('label'));
+ $oPdf->setAlpha(0.6 * $Alpha);
+ $oPdf->SetFillColor(255, 255, 255);
+ $oPdf->SetDrawColor(255, 255, 255);
+ $oPdf->Rect($this->x*$fScale - $width/2, ($this->y + 18)*$fScale, $width, $height, 'DF');
+ $oPdf->setAlpha($Alpha);
+ $oPdf->SetTextColor(0, 0, 0);
+ $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 18)*$fScale, $this->GetProperty('label'));
+ }
+
+ public function GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true)
+ {
+//echo "
".$this->GetProperty('label').":
";
+
+ if ($this->GetProperty('grouped') === true) return;
+ $this->SetProperty('grouped', true);
+
+ if ($bDirectionDown)
+ {
+ $aNodesPerClass = array();
+ foreach($this->GetOutgoingEdges() as $oEdge)
+ {
+ $oNode = $oEdge->GetSinkNode();
+
+ if ($oNode->GetProperty('class') !== null)
+ {
+ $sClass = $oNode->GetProperty('class');
+ if (($sClass!== null) && (!array_key_exists($sClass, $aNodesPerClass)))
+ {
+ $aNodesPerClass[$sClass] = array(
+ 'reached' => array(
+ 'count' => 0,
+ 'nodes' => array(),
+ 'icon_url' => $oNode->GetProperty('icon_url'),
+ ),
+ 'not_reached' => array(
+ 'count' => 0,
+ 'nodes' => array(),
+ 'icon_url' => $oNode->GetProperty('icon_url'),
+ )
+ );
+ }
+ $sKey = $oNode->GetProperty('is_reached') ? 'reached' : 'not_reached';
+ if (!array_key_exists($oNode->GetId(), $aNodesPerClass[$sClass][$sKey]['nodes']))
+ {
+ $aNodesPerClass[$sClass][$sKey]['nodes'][$oNode->GetId()] = $oNode;
+ $aNodesPerClass[$sClass][$sKey]['count'] += (int)$oNode->GetProperty('count', 1);
+//echo "New count: ".$aNodesPerClass[$sClass][$sKey]['count']."
";
+ }
+
+ }
+ else
+ {
+ $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
+ }
+ }
+
+ foreach($aNodesPerClass as $sClass => $aDefs)
+ {
+ foreach($aDefs as $sStatus => $aGroupProps)
+ {
+//echo "$sClass/$sStatus: {$aGroupProps['count']} object(s), actually: ".count($aGroupProps['nodes'])."
";
+ if (count($aGroupProps['nodes']) >= $iThresholdCount)
+ {
+ $oNewNode = new DisplayableGroupNode($oGraph, $this->GetId().'::'.$sClass);
+ $oNewNode->SetProperty('label', 'x'.$aGroupProps['count']);
+ $oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
+ $oNewNode->SetProperty('class', $sClass);
+ $oNewNode->SetProperty('is_reached', ($sStatus == 'reached'));
+ $oNewNode->SetProperty('count', $aGroupProps['count']);
+ //$oNewNode->SetProperty('grouped', true);
+
+ $oIncomingEdge = new DisplayableEdge($oGraph, $this->GetId().'-'.$oNewNode->GetId(), $this, $oNewNode);
+
+ foreach($aGroupProps['nodes'] as $oNode)
+ {
+ foreach($oNode->GetIncomingEdges() as $oEdge)
+ {
+ if ($oEdge->GetSourceNode()->GetId() !== $this->GetId())
+ {
+ $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
+ }
+ }
+ foreach($oNode->GetOutgoingEdges() as $oEdge)
+ {
+ $aOutgoing[] = $oEdge->GetSinkNode();
+ try
+ {
+ $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oNewNode, $oEdge->GetSinkNode());
+ }
+ catch(Exception $e)
+ {
+ // ignore this edge
+ }
+ }
+ if ($oGraph->GetNode($oNode->GetId()))
+ {
+ $oGraph->_RemoveNode($oNode);
+ }
+ }
+ $oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
+ }
+ else
+ {
+ foreach($aGroupProps['nodes'] as $oNode)
+ {
+ $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+class DisplayableRedundancyNode extends DisplayableNode
+{
+ public function GetWidth()
+ {
+ return 24;
+ }
+
+ public function GetForRaphael()
+ {
+ $aNode = array();
+ $aNode['shape'] = 'disc';
+ $aNode['icon_url'] = $this->GetIconURL();
+ $aNode['source'] = ($this->GetProperty('source') == true);
+ $aNode['width'] = $this->GetWidth();
+ $aNode['x'] = $this->x;
+ $aNode['y']= $this->y;
+ $aNode['label'] = $this->GetLabel();
+ $aNode['id'] = $this->GetId();
+ $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2);
+ $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => '#c33', 'opacity' => $fDiscOpacity);
+ $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
+ $aNode['text_attr'] = array('fill' => '#fff', 'opacity' => $fTextOpacity);
+ return $aNode;
+ }
+
+ public function RenderAsPDF(TCPDF $oPdf, $fScale)
+ {
+ $oPdf->SetAlpha(1);
+ $oPdf->SetFillColor(200, 0, 0);
+ $oPdf->SetDrawColor(0, 0, 0);
+ $oPdf->Circle($this->x*$fScale, $this->y*$fScale, 16*$fScale, 0, 360, 'DF');
+
+ $oPdf->SetTextColor(255, 255, 255);
+ $oPdf->SetFont('Helvetica', '', 28 * $fScale, '', true);
+ $sLabel = (string)$this->GetProperty('label');
+ $width = $oPdf->GetStringWidth($sLabel, 'Helvetica', 'B', 24*$fScale);
+ $height = $oPdf->GetStringHeight(1000, $sLabel);
+ $xPos = (float)$this->x*$fScale - $width/2;
+ $yPos = (float)$this->y*$fScale - $height/2;
+// $oPdf->Rect($xPos, $yPos, $width, $height, 'D');
+// $oPdf->Text($xPos, $yPos, $sLabel);
+
+ $oPdf->SetXY(($this->x - 16)*$fScale, ($this->y - 16)*$fScale);
+
+ // text on center
+ $oPdf->Cell(32*$fScale, 32*$fScale, $sLabel, 0, 0, 'C', 0, '', 0, false, 'T', 'C');
+ }
+
+ public function GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true)
+ {
+ parent::GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
+
+ if ($bDirectionUp)
+ {
+ $aNodesPerClass = array();
+ foreach($this->GetIncomingEdges() as $oEdge)
+ {
+ $oNode = $oEdge->GetSourceNode();
+
+ if (($oNode->GetProperty('class') !== null) && (!$oNode->GetProperty('is_reached')))
+ {
+ $sClass = $oNode->GetProperty('class');
+ if (!array_key_exists($sClass, $aNodesPerClass))
+ {
+ $aNodesPerClass[$sClass] = array('reached' => array(), 'not_reached' => array());
+ }
+ $aNodesPerClass[$sClass][$oNode->GetProperty('is_reached') ? 'reached' : 'not_reached'][] = $oNode;
+ }
+ else
+ {
+ //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
+ }
+ }
+
+ foreach($aNodesPerClass as $sClass => $aDefs)
+ {
+ foreach($aDefs as $sStatus => $aNodes)
+ {
+//echo "".$this->GetId().' has '.count($aNodes)." neighbours of class $sClass in status $sStatus\n";
+ if (count($aNodes) >= $iThresholdCount)
+ {
+ $oNewNode = new DisplayableGroupNode($oGraph, '-'.$this->GetId().'::'.$sClass.'/'.$sStatus);
+ $oNewNode->SetProperty('label', 'x'.count($aNodes));
+ $oNewNode->SetProperty('icon_url', $aNodes[0]->GetProperty('icon_url'));
+ $oNewNode->SetProperty('is_reached', $aNodes[0]->GetProperty('is_reached'));
+
+ $oOutgoingEdge = new DisplayableEdge($oGraph, '-'.$this->GetId().'-'.$oNewNode->GetId().'/'.$sStatus, $oNewNode, $this);
+
+ foreach($aNodes as $oNode)
+ {
+ foreach($oNode->GetIncomingEdges() as $oEdge)
+ {
+ $oNewEdge = new DisplayableEdge($oGraph, '-'.$oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
+ }
+ foreach($oNode->GetOutgoingEdges() as $oEdge)
+ {
+ if ($oEdge->GetSinkNode()->GetId() !== $this->GetId())
+ {
+ $aOutgoing[] = $oEdge->GetSinkNode();
+ $oNewEdge = new DisplayableEdge($oGraph, '-'.$oEdge->GetId().'::'.$sClass.'/'.$sStatus, $oNewNode, $oEdge->GetSinkNode());
+ }
+ }
+//echo "
Replacing ".$oNode->GetId().' by '.$oNewNode->GetId()."\n";
+ $oGraph->_RemoveNode($oNode);
+ }
+ //$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
+ }
+ else
+ {
+ foreach($aNodes as $oNode)
+ {
+ //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+class DisplayableEdge extends GraphEdge
+{
+ public function RenderAsPDF(TCPDF $oPdf, $fScale)
+ {
+ $xStart = $this->GetSourceNode()->x * $fScale;
+ $yStart = $this->GetSourceNode()->y * $fScale;
+ $xEnd = $this->GetSinkNode()->x * $fScale;
+ $yEnd = $this->GetSinkNode()->y * $fScale;
+
+ $bReached = ($this->GetSourceNode()->GetProperty('is_reached') && $this->GetSinkNode()->GetProperty('is_reached'));
+
+ $oPdf->setAlpha(1);
+ if ($bReached)
+ {
+ $aColor = array(100, 100, 100);
+ }
+ else
+ {
+ $aColor = array(200, 200, 200);
+ }
+ $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aColor));
+ $oPdf->Line($xStart, $yStart, $xEnd, $yEnd);
+
+
+ $vx = $xEnd - $xStart;
+ $vy = $yEnd - $yStart;
+ $l = sqrt($vx*$vx + $vy*$vy);
+ $vx = $vx / $l;
+ $vy = $vy / $l;
+ $ux = -$vy;
+ $uy = $vx;
+ $lPos = max($l/2, $l - 40*$fScale);
+ $iArrowSize = 5*$fScale;
+
+ $x = $xStart + $lPos * $vx;
+ $y = $yStart + $lPos * $vy;
+ $oPdf->Line($x, $y, $x + $iArrowSize * ($ux-$vx), $y + $iArrowSize * ($uy-$vy));
+ $oPdf->Line($x, $y, $x - $iArrowSize * ($ux+$vx), $y - $iArrowSize * ($uy+$vy));
+ }
+}
+
+class DisplayableGroupNode extends DisplayableNode
+{
+ public function GetWidth()
+ {
+ return 50;
+ }
+
+ public function GetForRaphael()
+ {
+ $aNode = array();
+ $aNode['shape'] = 'group';
+ $aNode['icon_url'] = $this->GetIconURL();
+ $aNode['source'] = ($this->GetProperty('source') == true);
+ $aNode['width'] = $this->GetWidth();
+ $aNode['x'] = $this->x;
+ $aNode['y']= $this->y;
+ $aNode['label'] = $this->GetLabel();
+ $aNode['id'] = $this->GetId();
+ $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2);
+ $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
+ $aNode['icon_attr'] = array('opacity' => $fTextOpacity);
+ $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => '#fff', 'opacity' => $fDiscOpacity);
+ $aNode['text_attr'] = array('fill' => '#000', 'opacity' => $fTextOpacity);
+ return $aNode;
+ }
+
+ public function RenderAsPDF(TCPDF $oPdf, $fScale)
+ {
+ $bReached = $this->GetProperty('is_reached');
+ $oPdf->SetFillColor(255, 255, 255);
+ if ($bReached)
+ {
+ $aBorderColor = array(100, 100, 100);
+ }
+ else
+ {
+ $aBorderColor = array(200, 200, 200);
+ }
+ $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aBorderColor));
+
+ $sIconUrl = $this->GetProperty('icon_url');
+ $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-production/', $sIconUrl);
+ $oPdf->SetAlpha(1);
+ $oPdf->Circle($this->x*$fScale, $this->y*$fScale, $this->GetWidth() / 2 * $fScale, 0, 360, 'DF');
+
+ if ($bReached)
+ {
+ $oPdf->SetAlpha(1);
+ }
+ else
+ {
+ $oPdf->SetAlpha(0.4);
+ }
+ $oPdf->Image($sIconPath, ($this->x - 17)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale);
+ $oPdf->Image($sIconPath, ($this->x + 1)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale);
+ $oPdf->Image($sIconPath, ($this->x -8)*$fScale, ($this->y +1)*$fScale, 16*$fScale, 16*$fScale);
+ $oPdf->SetFont('Helvetica', '', 24 * $fScale, '', true);
+ $width = $oPdf->GetStringWidth($this->GetProperty('label'));
+ $oPdf->SetTextColor(0, 0, 0);
+ $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 25)*$fScale, $this->GetProperty('label'));
+ }
+}
+
+
+class DisplayableGraph extends SimpleGraph
+{
+ protected $sDirection;
+
+ public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true)
+ {
+ $oNewGraph = new DisplayableGraph();
+
+ $oNodesIter = new RelationTypeIterator($oGraph, 'Node');
+ foreach($oNodesIter as $oNode)
+ {
+ switch(get_class($oNode))
+ {
+ case 'RelationObjectNode':
+ $oNewNode = new DisplayableNode($oNewGraph, $oNode->GetId(), 0, 0);
+
+ if ($oNode->GetProperty('source'))
+ {
+ $oNewNode->SetProperty('source', true);
+ }
+ if ($oNode->GetProperty('sink'))
+ {
+ $oNewNode->SetProperty('sink', true);
+ }
+ $oObj = $oNode->GetProperty('object');
+ $oNewNode->SetProperty('class', get_class($oObj));
+ $oNewNode->SetProperty('icon_url', $oObj->GetIcon(false));
+ $oNewNode->SetProperty('label', $oObj->GetRawName());
+ $oNewNode->SetProperty('is_reached', $bDirectionDown ? $oNode->GetProperty('is_reached') : true); // When going "up" is_reached does not matter
+ $oNewNode->SetProperty('developped', $oNode->GetProperty('developped'));
+ break;
+
+ default:
+ $oNewNode = new DisplayableRedundancyNode($oNewGraph, $oNode->GetId(), 0, 0);
+ $oNewNode->SetProperty('label', $oNode->GetProperty('min_up'));
+ $oNewNode->SetProperty('is_reached', true);
+ }
+ }
+ $oEdgesIter = new RelationTypeIterator($oGraph, 'Edge');
+ foreach($oEdgesIter as $oEdge)
+ {
+ $oSourceNode = $oNewGraph->GetNode($oEdge->GetSourceNode()->GetId());
+ $oSinkNode = $oNewGraph->GetNode($oEdge->GetSinkNode()->GetId());
+ $oNewEdge = new DisplayableEdge($oNewGraph, $oEdge->GetId(), $oSourceNode, $oSinkNode);
+ }
+
+ // Remove duplicate edges between two nodes
+ $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge');
+ $aEdgeKeys = array();
+ foreach($oEdgesIter as $oEdge)
+ {
+ $sSourceId = $oEdge->GetSourceNode()->GetId();
+ $sSinkId = $oEdge->GetSinkNode()->GetId();
+ if ($sSourceId == $sSinkId)
+ {
+ // Remove self referring edges
+ $oNewGraph->_RemoveEdge($oEdge);
+ }
+ else
+ {
+ $sKey = $sSourceId.'//'.$sSinkId;
+ if (array_key_exists($sKey, $aEdgeKeys))
+ {
+ // Remove duplicate edges
+ $oNewGraph->_RemoveEdge($oEdge);
+ }
+ else
+ {
+ $aEdgeKeys[$sKey] = true;
+ }
+ }
+ }
+
+ $iNbGrouping = 1;
+ //for($iter=0; $iter<$iNbGrouping; $iter++)
+ {
+ $oNodesIter = new RelationTypeIterator($oNewGraph, 'Node');
+ foreach($oNodesIter as $oNode)
+ {
+ if ($oNode->GetProperty('source'))
+ {
+ $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, true);
+ }
+ }
+ }
+
+ // Remove duplicate edges between two nodes
+ $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge');
+ $aEdgeKeys = array();
+ foreach($oEdgesIter as $oEdge)
+ {
+ $sSourceId = $oEdge->GetSourceNode()->GetId();
+ $sSinkId = $oEdge->GetSinkNode()->GetId();
+ if ($sSourceId == $sSinkId)
+ {
+ // Remove self referring edges
+ $oNewGraph->_RemoveEdge($oEdge);
+ }
+ else
+ {
+ $sKey = $sSourceId.'//'.$sSinkId;
+ if (array_key_exists($sKey, $aEdgeKeys))
+ {
+ // Remove duplicate edges
+ $oNewGraph->_RemoveEdge($oEdge);
+ }
+ else
+ {
+ $aEdgeKeys[$sKey] = true;
+ }
+ }
+ }
+
+ return $oNewGraph;
+ }
+
+ public function InitOnGrid()
+ {
+ $iDist = 125;
+ $aAllNodes = $this->_GetNodes();
+ $iSide = ceil(sqrt(count($aAllNodes)));
+ $xPos = 0;
+ $yPos = 0;
+ $idx = 0;
+ foreach($aAllNodes as $oNode)
+ {
+ $xPos += $iDist;
+ if (($idx % $iSide) == 0)
+ {
+ $xPos = 0;
+ $yPos += $iDist;
+ }
+
+ $oNode->x = $xPos;
+ $oNode->y = $yPos;
+
+ $idx++;
+ }
+
+ }
+
+ public function InitFromGraphviz()
+ {
+ $sDot = $this->DumpAsXDot();
+ $sDot = preg_replace('/.*label=.*,/', '', $sDot); // Get rid of label lines since they may contain weird characters than can break the split and pattern matching below
+
+ $aChunks = explode(";", $sDot);
+ foreach($aChunks as $sChunk)
+ {
+ //echo "
$sChunk
";
+ if(preg_match('/"([^"]+)".+pos="([0-9\\.]+),([0-9\\.]+)"/ms', $sChunk, $aMatches))
+ {
+ $sId = $aMatches[1];
+ $xPos = $aMatches[2];
+ $yPos = $aMatches[3];
+
+ $oNode = $this->GetNode($sId);
+ $oNode->x = (float)$xPos;
+ $oNode->y = (float)$yPos;
+
+ //echo "$sId at $xPos,$yPos
";
+ }
+ else
+ {
+ //echo "No match
";
+ }
+ }
+ }
+
+ public function BruteForceLayout($iNbTicks, $sDirection = 'horizontal')
+ {
+ $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
+ $this->sDirection = $sDirection;
+ $this->InitForces();
+ for($i=0; $i<$iNbTicks; $i++)
+ {
+ set_time_limit($iLoopTimeLimit);
+ $this->Tick();
+ }
+ }
+
+ protected function InitForces()
+ {
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ $i = 0;
+ foreach($oIterator as $sId => $oNode)
+ {
+ $oNode->SetProperty('ax', 0);
+ $oNode->SetProperty('ay', 0);
+ $oNode->SetProperty('vx', 0);
+ $oNode->SetProperty('vy', 0);
+ $i++;
+ }
+ }
+
+ protected function ComputeAcceleration()
+ {
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ foreach($oIterator as $idx => $oNode)
+ {
+ $sNodeId = $oNode->GetId();
+
+ $fx = 0;
+ $fy = 0;
+ $K = 0.6;
+ $Q = 0.3;
+
+ if ($oNode->GetProperty('source'))
+ {
+ switch($this->sDirection)
+ {
+ case 'horizontal':
+ $fx -= 30;
+ break;
+
+ case 'vertical':
+ $fy -= 30;
+ break;
+
+ default:
+ // No gravity
+ }
+ }
+ else
+ {
+ switch($this->sDirection)
+ {
+ case 'horizontal':
+ $fx += 30;
+ break;
+
+ case 'vertical':
+ $fy += 30;
+ break;
+
+ default:
+ // No gravity
+ }
+ }
+
+//echo "ComputeAcceleration - $sNodeId
\n";
+
+ $oIter2 = new RelationTypeIterator($this, 'Edge');
+ foreach($oIter2 as $sEdgeId => $oEdge)
+ {
+ $oSource = $oEdge->GetSourceNode();
+ $oSink = $oEdge->GetSinkNode();
+
+//echo "$sEdgeId ".$oSource->GetId()." -> ".$oSink->GetId()."
\n";
+
+ if ($oSource->GetId() === $sNodeId)
+ {
+ $fx += -$K * ($oSource->x - $oSink->x);
+ $fy += -$K * ($oSource->y - $oSink->y);
+//echo "$sEdgeId Sink - F($fx, $fy)
\n";
+ }
+ else if ($oSink->GetId() === $sNodeId)
+ {
+ $fx += -$K * ($oSink->x - $oSource->x);
+ $fy += -$K * ($oSink->y - $oSource->y);
+//echo "$sEdgeId Source - F($fx, $fy)
\n";
+ }
+ // Else do nothing for this node, it's not connected via this edge
+ }
+ $oIter3 = new RelationTypeIterator($this, 'Node');
+ foreach($oIter3 as $idx2 => $oOtherNode)
+ {
+ $sOtherId = $oOtherNode->GetId();
+ if ($sOtherId !== $sNodeId)
+ {
+ $d2 = $oOtherNode->Distance2($oNode) / (60*60);
+ if ($d2 < 15)
+ {
+ $dfx = -$Q * ($oOtherNode->x - $oNode->x) / $d2;
+ $dfy = -$Q * ($oOtherNode->y - $oNode->y) / $d2;
+
+ $fx += $dfx;
+ $fy += $dfy;
+ }
+
+//echo "Electrostatic: $sOtherId d2: $d2 F($dfx, $dfy)
\n";
+
+ }
+ }
+//echo "total forces: $sNodeId d2: $d2 F($fx, $fy)
\n";
+ $oNode->SetProperty('ax', $fx);
+ $oNode->SetProperty('ay', $fy);
+ }
+ }
+
+ protected function Tick()
+ {
+ $dt = 0.1;
+ $attenuation = 0.8;
+ $M = 1;
+
+ $this->ComputeAcceleration();
+
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ foreach($oIterator as $sId => $oNode)
+ {
+ $vx = $attenuation * $oNode->GetProperty('vx') + $M * $oNode->GetProperty('ax');
+ $vy = $attenuation * $oNode->GetProperty('vy') + $M * $oNode->GetProperty('ay');
+
+ $oNode->x += $dt * $vx;
+ $oNode->y += $dt * $vy;
+
+ $oNode->SetProperty('vx', $vx);
+ $oNode->SetProperty('vy', $vy);
+//echo "$sId - V($vx, $vy)
\n";
+ }
+ }
+
+ public function GetBoundingBox()
+ {
+ $xMin = null;
+ $xMax = null;
+ $yMin = null;
+ $yMax = null;
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ foreach($oIterator as $sId => $oNode)
+ {
+ if ($xMin === null) // First element in the loop
+ {
+ $xMin = $oNode->x - $oNode->GetWidth();
+ $xMax = $oNode->x + $oNode->GetWidth();
+ $yMin = $oNode->y - $oNode->GetHeight();
+ $yMax = $oNode->y + $oNode->GetHeight();
+ }
+ else
+ {
+ $xMin = min($xMin, $oNode->x - $oNode->GetWidth() / 2);
+ $xMax = max($xMax, $oNode->x + $oNode->GetWidth() / 2);
+ $yMin = min($yMin, $oNode->y - $oNode->GetHeight() / 2);
+ $yMax = max($yMax, $oNode->y + $oNode->GetHeight() / 2);
+ }
+ }
+
+ return array('xmin' => $xMin, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax);
+ }
+
+ function Translate($dx, $dy)
+ {
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ foreach($oIterator as $sId => $oNode)
+ {
+ $oNode->x += $dx;
+ $oNode->y += $dy;
+ }
+ }
+
+ function RenderAsRaphael(WebPage $oP, $sId = null, $bContinue = false)
+ {
+ if ($sId == null)
+ {
+ $sId = 'graph';
+ }
+ $aBB = $this->GetBoundingBox();
+ $oP->add('');
+ $oP->add_ready_script("var oGraph = $('#$sId').simple_graph({xmin: {$aBB['xmin']}, xmax: {$aBB['xmax']}, ymin: {$aBB['ymin']}, ymax: {$aBB['ymax']} });");
+
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ foreach($oIterator as $sId => $oNode)
+ {
+ $aNode = $oNode->GetForRaphael();
+ $sJSNode = json_encode($aNode);
+ $oP->add_ready_script("oGraph.simple_graph('add_node', $sJSNode);");
+ }
+ $oIterator = new RelationTypeIterator($this, 'Edge');
+ foreach($oIterator as $sId => $oEdge)
+ {
+ $aEdge = array();
+ $aEdge['id'] = $oEdge->GetId();
+ $aEdge['source_node_id'] = $oEdge->GetSourceNode()->GetId();
+ $aEdge['sink_node_id'] = $oEdge->GetSinkNode()->GetId();
+ $fOpacity = ($oEdge->GetSinkNode()->GetProperty('is_reached') && $oEdge->GetSourceNode()->GetProperty('is_reached') ? 1 : 0.2);
+ $aEdge['attr'] = array('opacity' => $fOpacity, 'stroke' => '#000');
+ $sJSEdge = json_encode($aEdge);
+ $oP->add_ready_script("oGraph.simple_graph('add_edge', $sJSEdge);");
+ }
+
+ $oP->add_ready_script("oGraph.simple_graph('draw');");
+ }
+
+ function RenderAsPDF(WebPage $oP, $sTitle = 'Untitled', $sPageFormat = 'A4', $sPageOrientation = 'P')
+ {
+ require_once(APPROOT.'lib/tcpdf/tcpdf.php');
+ $oPdf = new TCPDF($sPageOrientation, 'mm', $sPageFormat, true, 'UTF-8', false);
+
+ // set document information
+ $oPdf->SetCreator(PDF_CREATOR);
+ $oPdf->SetAuthor('iTop');
+ $oPdf->SetTitle($sTitle);
+
+ $oPdf->setFontSubsetting(true);
+
+ // Set font
+ // dejavusans is a UTF-8 Unicode font, if you only need to
+ // print standard ASCII chars, you can use core fonts like
+ // helvetica or times to reduce file size.
+ $oPdf->SetFont('dejavusans', '', 14, '', true);
+
+ // set auto page breaks
+ $oPdf->SetAutoPageBreak(false);
+
+ // Add a page
+ // This method has several options, check the source code documentation for more information.
+ $oPdf->AddPage();
+
+ $aBB = $this->GetBoundingBox();
+ //$this->Translate(-$aBB['xmin'], -$aBB['ymin']);
+ if ($sPageOrientation == 'P')
+ {
+ // Portrait mode
+ $fHMargin = 10; // mm
+ $fVMargin = 15; // mm
+ }
+ else
+ {
+ // Landscape mode
+ $fHMargin = 15; // mm
+ $fVMargin = 10; // mm
+ }
+
+ $fPageW = $oPdf->getPageWidth() - 2 * $fHMargin;
+ $fPageH = $oPdf->getPageHeight() - 2 * $fVMargin;
+
+ $w = $aBB['xmax'] - $aBB['xmin'];
+ $h = $aBB['ymax'] - $aBB['ymin'] + 10; // Extra space for the labels which may appear "below" the icons
+
+ $fScale = min($fPageW / $w, $fPageH / $h);
+ $dx = ($fPageW - $fScale * $w) / 2;
+ $dy = ($fPageH - $fScale * $h) / 2;
+
+ $this->Translate(($fHMargin + $dx)/$fScale, ($fVMargin + $dy)/$fScale);
+
+ $oIterator = new RelationTypeIterator($this, 'Edge');
+ foreach($oIterator as $sId => $oEdge)
+ {
+ $oEdge->RenderAsPDF($oPdf, $fScale);
+ }
+
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ foreach($oIterator as $sId => $oNode)
+ {
+ $oNode->RenderAsPDF($oPdf, $fScale);
+ }
+
+ $oP->add($oPdf->Output('iTop.pdf', 'S'));
+ }
+
+}
\ No newline at end of file
diff --git a/core/metamodel.class.php b/core/metamodel.class.php
index 85415de27..e99c9bdc5 100644
--- a/core/metamodel.class.php
+++ b/core/metamodel.class.php
@@ -231,6 +231,7 @@ abstract class MetaModel
}
private static $m_oConfig = null;
+ protected static $m_aModulesParameters = array();
private static $m_bSkipCheckToWrite = false;
private static $m_bSkipCheckExtKeys = false;
@@ -1102,6 +1103,11 @@ abstract class MetaModel
//
private static $m_aRelationInfos = array(); // array of ("relcode" => various info on the list, common to every classes)
+ /**
+ * TO BE DEPRECATED: use EnumRelationsEx instead
+ * @param string $sClass
+ * @return multitype:string unknown |Ambigous
+ */
public static function EnumRelations($sClass = '')
{
$aResult = array_keys(self::$m_aRelationInfos);
@@ -1144,23 +1150,54 @@ abstract class MetaModel
return $aResult;
}
+ public static function EnumRelationsEx($sClass)
+ {
+ $aRelationInfo = array_keys(self::$m_aRelationInfos);
+ // Return only the relations that have a meaning (i.e. for which at least one query is defined)
+ // for the specified class
+ $aClassRelations = array();
+ foreach($aRelationInfo as $sRelCode)
+ {
+ $aQueriesDown = self::EnumRelationQueries($sClass, $sRelCode, true /* Down */);
+ if (count($aQueriesDown) > 0)
+ {
+ $aClassRelations[$sRelCode]['down'] = self::GetRelationLabel($sRelCode, true);
+ }
+
+ $aQueriesUp = self::EnumRelationQueries($sClass, $sRelCode, false /* Up */);
+ if (count($aQueriesUp) > 0)
+ {
+ $aClassRelations[$sRelCode]['up'] = self::GetRelationLabel($sRelCode, false);
+ }
+ }
+
+ return $aClassRelations;
+ }
+
final static public function GetRelationDescription($sRelCode)
{
return Dict::S("Relation:$sRelCode/Description");
}
- final static public function GetRelationLabel($sRelCode)
+ final static public function GetRelationLabel($sRelCode, $bDown = true)
{
- // The legacy convention is confusing with regard to the way we have conceptualized the relations:
- // In the former representation, the main stream was named after "up"
- // Now, the relation from A to B says that something is transmitted from A to B, thus going DOWNstream as described in a petri net.
- $sKey = "Relation:$sRelCode/DownStream";
- $sLegacy = Dict::S("Relation:$sRelCode/VerbUp", $sKey);
+ if ($bDown)
+ {
+ // The legacy convention is confusing with regard to the way we have conceptualized the relations:
+ // In the former representation, the main stream was named after "up"
+ // Now, the relation from A to B says that something is transmitted from A to B, thus going DOWNstream as described in a petri net.
+ $sKey = "Relation:$sRelCode/DownStream";
+ $sLegacy = Dict::S("Relation:$sRelCode/VerbUp", $sKey);
+ }
+ else
+ {
+ $sKey = "Relation:$sRelCode/UpStream";
+ $sLegacy = Dict::S("Relation:$sRelCode/VerbDown", $sKey);
+ }
$sRet = Dict::S($sKey, $sLegacy);
return $sRet;
}
-
protected static function ComputeRelationQueries($sRelCode)
{
$bHasLegacy = false;
@@ -1339,6 +1376,10 @@ abstract class MetaModel
{
foreach ($aQueries[$sClass]['up'] as $sNeighbourId => $aNeighbourData)
{
+ if (!array_key_exists('_legacy_', $aNeighbourData))
+ {
+ continue;
+ }
if (!$aNeighbourData['_legacy_']) continue; // Skip modern definitions
$sLocalClass = $aNeighbourData['sToClass'];
@@ -1363,7 +1404,7 @@ abstract class MetaModel
$sLocalClass = $aNeighbourData['sFromClass'];
foreach (self::EnumChildClasses($aNeighbourData['sToClass'], ENUM_CHILD_CLASSES_ALL) as $sRemoteClass)
{
- $aQueries[$sRemoteClass]['up'][$sLocalClass]['sQueryDown'] = $aNeighbourData['sQueryDown'];
+ //$aQueries[$sRemoteClass]['up'][$sLocalClass]['sQueryDown'] = $aNeighbourData['sQueryDown'];
}
}
}
@@ -5227,6 +5268,20 @@ abstract class MetaModel
return self::$m_oConfig->GetModuleSetting($sModule, $sProperty, $defaultvalue);
}
+ public static function GetModuleParameter($sModule, $sProperty, $defaultvalue = null)
+ {
+ $value = $defaultvalue;
+ if (!array_key_exists($sModule, self::$m_aModulesParameters))
+ {
+
+ }
+ if (!self::$m_aModulesParameters[$sModule] == null)
+ {
+ $value = self::$m_aModulesParameters[$sModule]->Get($sProperty, $defaultvalue);
+ }
+ return $value;
+ }
+
public static function GetConfig()
{
return self::$m_oConfig;
diff --git a/core/simplegraph.class.inc.php b/core/simplegraph.class.inc.php
index f3bc2b630..e94951a0a 100644
--- a/core/simplegraph.class.inc.php
+++ b/core/simplegraph.class.inc.php
@@ -153,6 +153,24 @@ class GraphNode extends GraphElement
$this->aOutgoingEdges[$oEdge->GetId()] = $oEdge;
}
+ /**
+ * INTERNAL USE ONLY
+ * @param GraphEdge $oEdge
+ */
+ public function _RemoveIncomingEdge(GraphEdge $oEdge)
+ {
+ unset($this->aIncomingEdges[$oEdge->GetId()]);
+ }
+
+ /**
+ * INTERNAL USE ONLY
+ * @param GraphEdge $oEdge
+ */
+ public function _RemoveOutgoingEdge(GraphEdge $oEdge)
+ {
+ unset($this->aOutgoingEdges[$oEdge->GetId()]);
+ }
+
/**
* Get the list of all incoming edges on the current node
* @return Ambigous
@@ -171,6 +189,38 @@ class GraphNode extends GraphElement
return $this->aOutgoingEdges;
}
+ /**
+ * Flood fill the chart with the given value for the specified property
+ * @param string $sPropName The name of the property to set
+ * @param mixed $value Teh value to set in the property
+ * @param bool $bFloodDown Whether or not to fill in the downstream direction
+ * @param bool $bFloodUp Whether or not to fill in the upstream direction
+ */
+ public function FloodProperty($sPropName, $value, $bFloodDown, $bFloodUp)
+ {
+ if ($this->GetProperty($sPropName, null) == null)
+ {
+ // Property not already set, let's do it
+ $this->SetProperty($sPropName, $value);
+ if ($bFloodDown)
+ {
+ foreach($this->GetOutgoingEdges() as $oEdge)
+ {
+ $oEdge->SetProperty($sPropName, $value);
+ $oEdge->GetSinkNode()->FloodProperty($sPropName, $value, $bFloodDown, $bFloodUp);
+ }
+ }
+ if ($bFloodUp)
+ {
+ foreach($this->GetIncomingEdges() as $oEdge)
+ {
+ $oEdge->SetProperty($sPropName, $value);
+ $oEdge->GetSourceNode()->FloodProperty($sPropName, $value, $bFloodDown, $bFloodUp);
+ }
+ }
+ }
+ }
+
}
/**
@@ -263,11 +313,30 @@ class SimpleGraph
*/
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.');
+ 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 in the graph.');
$this->aNodes[$oNode->GetId()] = $oNode;
}
+ /**
+ * INTERNAL USE ONLY
+ * @return Ambigous
+ */
+ public function _RemoveNode(GraphNode $oNode)
+ {
+ if (!array_key_exists($oNode->GetId(), $this->aNodes)) throw new SimpleGraphException('Cannot remove the node (id='.$oNode->GetId().') from the graph. The node was not found in the graph.');
+
+ foreach($oNode->GetOutgoingEdges() as $oEdge)
+ {
+ $this->_RemoveEdge($oEdge);
+ }
+ foreach($oNode->GetIncomingEdges() as $oEdge)
+ {
+ $this->_RemoveEdge($oEdge);
+ }
+ unset($this->aNodes[$oNode->GetId()]);
+ }
+
/**
* Get the node identified by $sId or null if not found
* @param string $sId
@@ -295,13 +364,28 @@ class SimpleGraph
*/
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.');
+ 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 in the graph.');
$this->aEdges[$oEdge->GetId()] = $oEdge;
$oEdge->GetSourceNode()->_AddOutgoingEdge($oEdge);
$oEdge->GetSinkNode()->_AddIncomingEdge($oEdge);
}
+ /**
+ * INTERNAL USE ONLY
+ * @param GraphEdge $oEdge
+ * @throws SimpleGraphException
+ */
+ public function _RemoveEdge(GraphEdge $oEdge)
+ {
+ if (!array_key_exists($oEdge->GetId(), $this->aEdges)) throw new SimpleGraphException('Cannot remove edge (id='.$oEdge->GetId().') from the graph. The edge was not found.');
+
+ $oEdge->GetSourceNode()->_RemoveOutgoingEdge($oEdge);
+ $oEdge->GetSinkNode()->_RemoveIncomingEdge($oEdge);
+
+ unset($this->aEdges[$oEdge->GetId()]);
+ }
+
/**
* Get the edge indentified by $sId or null if not found
* @param string $sId
@@ -333,8 +417,9 @@ class SimpleGraph
digraph finite_state_machine {
graph [bgcolor = "transparent"];
rankdir=LR;
-size="30,30"
-node [ fontname=Verdana style=filled fillcolor="#ffffcc" ];
+size="30,30";
+fontsize=8.0;
+node [ fontname=Verdana style=filled fillcolor="#ffffcc" fontsize=8.0 ];
edge [ fontname=Verdana ];
EOF
@@ -413,6 +498,62 @@ EOF
return $sHtml;
}
+ /**
+ * 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 DumpAsXDot()
+ {
+ $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");
+ }
+ $sXdotFilePath = tempnam(APPROOT."data/tmp", 'xdot-');
+ $sDotDescription = $this->GetDotDescription();
+ $sDotFilePath = tempnam(APPROOT."data/tmp", 'dot-');
+
+ $rFile = @fopen($sDotFilePath, "w");
+ @fwrite($rFile, $sDotDescription);
+ @fclose($rFile);
+ $aOutput = array();
+ $CommandLine = "\"$sDotExecutable\" -v -Tdot < $sDotFilePath -o$sXdotFilePath 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 = ''.file_get_contents($sXdotFilePath).'
';
+ @unlink($sImageFilePath);
+ }
+ @unlink($sXdotFilePath);
+ }
+ else
+ {
+ throw new Exception('graphviz not found (executable path: '.$sDotExecutable.')');
+ }
+ return $sHtml;
+ }
+
+
/**
* Get the description of the graph as some HTML text
* @return string
@@ -456,6 +597,73 @@ EOF
}
return $sHtml;
}
+
+ /**
+ * Split the graph in a array of non connected subgraphs
+ * @return multitype:SimpleGraph unknown
+ */
+ public function GetSubgraphs()
+ {
+ $iNbColors = 0;
+ $aResult = array();
+ $oIterator = new RelationTypeIterator($this, 'Node');
+ foreach($oIterator as $oNode)
+ {
+ $iPrevColor = $oNode->GetProperty('color', null);
+
+ if ($iPrevColor == null)
+ {
+ $iNbColors++; // Start a new color
+ $oNode->FloodProperty('color', $iNbColors, true, true);
+ }
+ }
+ if ($iNbColors == 1)
+ {
+ // Everything is connected together, only one subgraph
+ $aResult[] = $this;
+ }
+ else
+ {
+ // Let's reconstruct each separate graph
+ $sClass = get_class($this);
+ for($i = 1; $i <= $iNbColors; $i++)
+ {
+ $aResult[$i] = new $sClass();
+ }
+
+ foreach($oIterator as $oNode)
+ {
+ $iNodeColor = $oNode->GetProperty('color');
+ $aResult[$iNodeColor]->_AddNode($oNode);
+ }
+
+ $oIter2 = new RelationTypeIterator($this, 'Edge');
+ foreach($oIter2 as $oEdge)
+ {
+ $iEdgeColor = $oEdge->GetProperty('color');
+ $aResult[$iEdgeColor]->_AddEdge($oEdge);
+ }
+ }
+ return $aResult;
+ }
+
+ /**
+ * Merge back to subgraphs into one
+ * @param SimpleGraph $oGraph
+ */
+ public function Merge(SimpleGraph $oGraph)
+ {
+ $oIter1 = new RelationTypeIterator($oGraph, 'Node');
+ foreach($oIter1 as $oNode)
+ {
+ $this->_AddNode($oNode);
+ }
+ $oIter2 = new RelationTypeIterator($oGraph, 'Edge');
+ foreach($oIter2 as $oEdge)
+ {
+ $this->_AddEdge($oEdge);
+ }
+ }
}
/**
diff --git a/css/light-grey.css b/css/light-grey.css
index 53ae3c08f..d4e38b0fc 100644
--- a/css/light-grey.css
+++ b/css/light-grey.css
@@ -1419,4 +1419,7 @@ div.ui-dialog-header {
.form_field_error {
border: 1px solid #933;
background: #fcc;
+}
+.simple-graph {
+ background: #fff;
}
\ No newline at end of file
diff --git a/js/fraphael.js b/js/fraphael.js
new file mode 100644
index 000000000..ed27a4eca
--- /dev/null
+++ b/js/fraphael.js
@@ -0,0 +1,574 @@
+/**
+ * FRaphael
+ * An extension for Raphael.js to make it easier to work with Filter Effects
+ *
+ * Copyright © 2013 Chris Scott
+ * Delivered with and licensed under the MIT licence
+ *
+ */
+
+// Create the global FRaphael object
+(function(scope) {
+ var version = "0.0.1",
+ license = "MIT";
+
+ var ns = "http://www.w3.org/2000/svg",
+ idCounter = 0;
+
+ var FR = {
+ // Object prototype for a filter
+ Filter: function(id) {
+ if (id == undefined) {
+ id = "filter-" + idCounter++;
+ while(FR.filters[id] != undefined) {
+ id = "filter-" + idCounter++;
+ }
+ }
+
+ if (FR.filters[id] != undefined) {
+ throw "A filter with id " + id + " already exists";
+ }
+
+ this.element = document.createElementNS(ns, "filter");
+ this.element.setAttribute("id", id);
+ this.element.setAttribute("x", "-25%");
+ this.element.setAttribute("y", "-25%");
+ this.element.setAttribute("width", "150%");
+ this.element.setAttribute("height", "150%");
+
+ this.lastFEResult = null;
+
+ FR.filters[id] = this;
+ this.id = id;
+ },
+
+ // Object prototype for an effect
+ FilterEffect: function(type, attributes) {
+ this.element = document.createElementNS(ns, type);
+ for (var key in attributes) {
+ this.element.setAttribute(key, attributes[key]);
+ }
+ },
+
+ // Return the filter applied to an element or a new filter if none are currently applied
+ getFilter: function(element) {
+ var filterId = element.data("filterId");
+ var filter = null;
+
+ if (filterId == undefined) {
+ filterId = "element-filter-" + element.id;
+ filter = element.paper.createFilter(filterId);
+ element.filter(filterId);
+ } else {
+ filter = FR.filters[filterId];
+ }
+
+ return filter;
+ },
+
+ // maintain a list of filters by id
+ filters: {}
+ };
+
+ FR.Filter.prototype = {
+ addEffect: function(type, attributes, children) {
+ var effect = new FR.FilterEffect(type, attributes);
+
+ if (children) {
+ if (children instanceof Array) {
+ for (var x in children) {
+ if (!children.hasOwnProperty(x)) continue;
+
+ effect.element.appendChild(children[x].element);
+ }
+ } else {
+ effect.element.appendChild(children.element);
+ }
+ }
+
+ this.element.appendChild(effect.element);
+
+ return this;
+ },
+
+ chainEffect: function(type, attributes, children) {
+ if (attributes == undefined) {
+ attributes = {};
+ }
+
+ var inId;
+ var outId;
+ if (attributes.in == undefined) {
+ inId = this.getLastResult();
+ } else {
+ inId = attributes.in;
+ }
+ if (attributes.result == undefined) {
+ outId = idCounter++;
+ } else {
+ outId = attributes.result;
+ }
+
+ this.lastFEResult = outId;
+
+ attributes.in = inId;
+ attributes.result = outId;
+
+ this.addEffect(type, attributes, children);
+
+ return this;
+ },
+
+ getLastResult: function() {
+ return (this.lastFEResult == undefined) ? "SourceGraphic" : this.lastFEResult;
+ },
+
+ merge: function(in1, in2, attributes) {
+ var mergeNode1 = new FR.FilterEffect("feMergeNode", {
+ in: in1
+ });
+ var mergeNode2 = new FR.FilterEffect("feMergeNode", {
+ in: in2
+ });
+
+ this.chainEffect("feMerge", attributes, [mergeNode1, mergeNode2]);
+
+ return this;
+ },
+
+ compose: function(in1, in2, operator, attributes) {
+ if (attributes == undefined) {
+ attributes = {};
+ }
+
+ if (operator == undefined) {
+ operator = "over";
+ }
+
+ attributes.in = in1;
+ attributes.in2 = in2;
+ attributes.operator = operator;
+
+ this.chainEffect("feComposite", attributes);
+
+ return this;
+ },
+
+ arithmeticCompose: function(in1, in2, k1, k2, k3, k4) {
+ if (k1 == undefined) {
+ k1 = 0;
+ }
+ if (k2 == undefined) {
+ k2 = 0;
+ }
+ if (k3 == undefined) {
+ k3 = 0;
+ }
+ if (k4 == undefined) {
+ k4 = 0;
+ }
+
+ this.compose(in1, in2, "arithmetic", {
+ k1: k1,
+ k2: k2,
+ k3: k3,
+ k4: k4
+ });
+
+ return this;
+ },
+
+ addBlur: function(stdDeviation, attributes) {
+ if (!stdDeviation) {
+ throw "Standard deviation is required to perform a blur filter";
+ }
+
+ if (attributes == undefined) {
+ attributes = {};
+ }
+ attributes.stdDeviation = stdDeviation;
+
+ this.chainEffect("feGaussianBlur", attributes);
+
+ return this;
+ },
+
+ addOffset: function(dx, dy, attributes) {
+ if (dx == undefined | dy == undefined) {
+ throw "dx and dy values are required to perform an offset FE";
+ }
+
+ if (attributes == undefined) {
+ attributes = {};
+ }
+ attributes.dx = dx;
+ attributes.dy = dy;
+
+ this.chainEffect("feOffset", attributes);
+
+ return this;
+ },
+
+ addLighting: function(x, y, z, color, type, attributes) {
+ if (x == undefined | y == undefined | z == undefined) {
+ throw "Three co-ordinates are required to create a light source";
+ }
+
+ var previousResult = this.getLastResult();
+
+ var id = idCounter++;
+
+ if (attributes == undefined) {
+ attributes = {};
+ }
+
+ attributes.result = id;
+ if (color != undefined) {
+ attributes["lighting-color"] = color;
+ }
+
+ if (type == undefined || type == "diffuse") {
+ type = "feDiffuseLighting";
+ } else if (type == "specular") {
+ type = "feSpecularLighting";
+ }
+
+ var lightSource = new FR.FilterEffect("fePointLight", {
+ x: x,
+ y: y,
+ z: z
+ });
+
+ this.chainEffect(type, attributes, lightSource).arithmeticCompose(previousResult, id, 3, 0.2, 0, 0);
+
+ return this;
+ },
+
+ addShiftToColor: function(color, moveBy, attributes) {
+ if (color == undefined) {
+ throw "A colour string is a required argument to create a colorMatrix";
+ }
+ if (moveBy == undefined) {
+ moveBy = 0.5;
+ }
+
+ var remainingColor = 1 - moveBy, x = remainingColor;
+
+ if (attributes == undefined) {
+ attributes = {};
+ }
+
+ var colorObject = Raphael.color(color);
+ var r = colorObject.r * moveBy / 255,
+ g = colorObject.g * moveBy / 255,
+ b = colorObject.b * moveBy / 255;
+
+ /**
+ * r' x 0 0 0 r r
+ * g' 0 x 0 0 g g
+ * b' = 0 0 x 0 b . b
+ * a' 0 0 0 1 0 o
+ * 1 1
+ */
+ attributes.values = x + " 0 0 0 " + r + " 0 " + x + " 0 0 " + g + " 0 0 " + x + " 0 " + b + " 0 0 0 1 0 ";
+
+ this.chainEffect("feColorMatrix", attributes);
+
+ return this;
+ },
+
+ addRecolor: function(color, opacity, attributes) {
+ if (color == undefined) {
+ throw "A colour string is a required argument to create a colorMatrix";
+ }
+ if (opacity == undefined) {
+ opacity = 1;
+ }
+
+ if (attributes == undefined) {
+ attributes = {};
+ }
+
+ var colorObject = Raphael.color(color);
+ var r = colorObject.r / 255,
+ g = colorObject.g / 255,
+ b = colorObject.b / 255;
+
+ /**
+ * r' 0 0 0 0 r r
+ * g' 0 0 0 0 g g
+ * b' = 0 0 0 0 b . b
+ * a' 0 0 0 a 0 a
+ * 1 1
+ */
+ attributes.values = "0 0 0 0 " + r + " 0 0 0 0 " + g + " 0 0 0 0 " + b + " 0 0 0 " + opacity + " 0 ";
+
+ this.chainEffect("feColorMatrix", attributes);
+
+ return this;
+ },
+
+ addDesaturate: function(saturation, attributes) {
+ if (saturation == undefined) {
+ saturnation = 0;
+ }
+
+ if (attributes == undefined) {
+ attributes = {};
+ }
+
+ attributes.values = saturation;
+ attributes.type = "saturate";
+
+ this.chainEffect("feColorMatrix", attributes);
+
+ return this;
+ },
+
+ addConvolveMatrix: function(matrix, attributes) {
+ if (matrix == undefined) {
+ throw "A matrix (usually 9 numbers) must be provided to apply a convolve matrix transform";
+ }
+
+ if (attributes == undefined) {
+ attributes = {};
+ }
+
+ attributes.kernelMatrix = matrix;
+
+ this.chainEffect("feConvolveMatrix", attributes);
+
+ return this;
+ },
+
+ createShadow: function(dx, dy, blur, opacity, color) {
+ if (dx == undefined) {
+ throw "dx is required for the shadow effect";
+ }
+ if (dy == undefined) {
+ throw "dy is required for the shadow effect";
+ }
+ if (blur == undefined) {
+ throw "blur (stdDeviation) is required for the shadow effect";
+ }
+
+ if (opacity == undefined) {
+ opacity = 0.6;
+ }
+
+ var previousResult = this.getLastResult();
+
+ if (color == undefined) {
+ color = "#000000";
+ }
+
+ this.addOffset(dx, dy, {
+ in: "SourceAlpha"
+ });
+
+ this.addRecolor(color, opacity);
+
+ this.addBlur(blur);
+
+ this.merge(this.getLastResult(), previousResult);
+
+ return this;
+ },
+
+ createEmboss: function(height, x, y, z) {
+ if (height == undefined) {
+ height = 2;
+ }
+ if (x == undefined) {
+ x = -1000;
+ }
+ if (y == undefined) {
+ y = -5000;
+ }
+ if (z == undefined) {
+ z = 300;
+ }
+
+ // Create the highlight
+
+ this.addOffset(height * x / (x + y), height * y / (x + y), {
+ in: "SourceAlpha"
+ });
+
+ this.addBlur(height * 0.5);
+
+ var whiteLightSource = new FR.FilterEffect("fePointLight", {
+ x: x,
+ y: y,
+ z: z
+ });
+
+ this.chainEffect("feSpecularLighting", {
+ surfaceScale: height,
+ specularConstant: 0.8,
+ specularExponent: 15
+ }, whiteLightSource);
+
+ this.compose(this.getLastResult(), "SourceAlpha", "in");
+ var whiteLight = this.getLastResult();
+
+ // Create the lowlight
+
+ this.addOffset(height * -1 * x / (x + y), height * -1 * y / (x + y), {
+ in: "SourceAlpha"
+ });
+
+ this.addBlur(height * 0.5);
+
+ var darkLightSource = new FR.FilterEffect("fePointLight", {
+ x: -1 * x,
+ y: -1 * y,
+ z: z
+ });
+
+ this.chainEffect("feSpecularLighting", {
+ surfaceScale: height,
+ specularConstant: 1.8,
+ specularExponent: 6
+ }, darkLightSource);
+
+ this.compose(this.getLastResult(), "SourceAlpha", "in");
+ this.chainEffect("feColorMatrix", {
+ values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"
+ });
+ var darkLight = this.getLastResult();
+
+ this.arithmeticCompose(whiteLight, darkLight, 0, 0.8, 0.5, 0);
+
+ this.merge("SourceGraphic", this.getLastResult());
+
+ return this;
+ }
+ };
+
+ scope.FRaphael = FR;
+})(this);
+
+/**
+ * add a filter to the paper by id
+ */
+Raphael.fn.createFilter = function(id) {
+ var paper = this;
+ var filter = new FRaphael.Filter(id);
+ paper.defs.appendChild(filter.element);
+
+ return filter;
+};
+
+/**
+ * Apply a filter to an element by id
+ */
+Raphael.el.filter = function(filter) {
+ var id = (filter instanceof FRaphael.Filter) ? filter.id : filter;
+
+ this.node.setAttribute("filter", "url(#" + id + ")");
+ this.data("filterId", id);
+
+ return this;
+};
+
+/**
+ * Get the current filter for an element or a new one if not
+ */
+Raphael.el.getFilter = function() {
+ return FRaphael.getFilter(this);
+};
+
+/**
+ * A shorthand method for applying blur
+ */
+Raphael.el.blur = function(stdDeviation) {
+ if (stdDeviation == undefined) {
+ stdDeviation = 3;
+ }
+
+ this.getFilter().addBlur(stdDeviation);
+
+ return this;
+};
+
+/**
+ * A shorthand method for applying a drop shadow
+ */
+Raphael.el.shadow = function(dx, dy, blur, opacity, color) {
+ if (dx == undefined) {
+ dx = 3;
+ }
+ if (dy == undefined) {
+ dy = 3;
+ }
+ if (blur == undefined) {
+ blur = 3;
+ }
+
+ this.getFilter().createShadow(dx, dy, blur, opacity, color);
+
+ return this;
+};
+
+/**
+ * A shorthand method for applying lighting
+ */
+Raphael.el.light = function(x, y, z, color, type) {
+ if (x == undefined) {
+ x = this.paper.width;
+ }
+ if (y == undefined) {
+ y = 0;
+ }
+ if (z == undefined) {
+ z = 20;
+ }
+
+ this.getFilter().addLighting(x, y, z, color, type);
+
+ return this;
+};
+
+/**
+ * A shorthand method for applying a colour shift
+ */
+Raphael.el.colorShift = function(color, shift) {
+ if (color == undefined) {
+ color = "black";
+ }
+ if (shift == undefined) {
+ shift = 0.5;
+ }
+
+ this.getFilter().addShiftToColor(color, shift);
+
+ return this;
+};
+
+/**
+ * A shorthand method for embossing
+ */
+Raphael.el.emboss = function(height) {
+ this.getFilter().createEmboss(height);
+
+ return this;
+};
+
+/**
+ * A shorthand method for desaturating
+ */
+Raphael.el.desaturate = function(saturation) {
+ this.getFilter().addDesaturate(saturation);
+
+ return this;
+};
+
+/**
+ * A shorthand method for complete desaturation
+ */
+Raphael.el.greyScale = function() {
+ this.getFilter().addDesaturate(0);
+
+ return this;
+};
diff --git a/js/simple_graph.js b/js/simple_graph.js
new file mode 100644
index 000000000..7c4e691aa
--- /dev/null
+++ b/js/simple_graph.js
@@ -0,0 +1,326 @@
+// jQuery UI style "widget" for displaying a graph
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// graph
+//
+$(function()
+{
+ // the widget definition, where "itop" is the namespace,
+ // "dashboard" the widget name
+ $.widget( "itop.simple_graph",
+ {
+ // default options
+ options:
+ {
+ xmin: 0,
+ xmax: 0,
+ ymin: 0,
+ ymax: 0,
+ align: 'center',
+ 'vertical-align': 'middle'
+ },
+
+ // the constructor
+ _create: function()
+ {
+ var me = this;
+ this.aNodes = [];
+ this.aEdges = [];
+ this.fZoom = 1.0;
+ this.xOffset = 0;
+ this.yOffset = 0;
+ this.iTextHeight = 12;
+ //this.element.height(this.element.parent().height());
+ this.oPaper = Raphael(this.element.get(0), this.element.width(), this.element.height());
+
+ this.auto_scale();
+
+ this.element
+ .addClass('itop-simple-graph');
+
+ this._create_toolkit_menu();
+ },
+
+ // called when created, and later when changing options
+ _refresh: function()
+ {
+ this.draw();
+ },
+ // events bound via _bind are removed automatically
+ // revert other modifications here
+ _destroy: function()
+ {
+ var sId = this.element.attr('id');
+ this.element
+ .removeClass('itop-simple-graph');
+
+ $('#tk_graph'+sId).remove();
+
+ },
+ // _setOptions is called with a hash of all options that are changing
+ _setOptions: function()
+ {
+ this._superApply(arguments);
+ },
+ // _setOption is called for each individual option that is changing
+ _setOption: function( key, value )
+ {
+ this._superApply(arguments);
+ },
+ draw: function()
+ {
+ this.oPaper.clear();
+ for(var k in this.aNodes)
+ {
+ this._draw_node(this.aNodes[k]);
+ }
+ for(var k in this.aEdges)
+ {
+ this._draw_edge(this.aEdges[k]);
+ }
+ },
+ _draw_node: function(oNode)
+ {
+ var iWidth = oNode.width;
+ var iHeight = 32;
+ var xPos = Math.round(oNode.x * this.fZoom + this.xOffset);
+ var yPos = Math.round(oNode.y * this.fZoom + this.yOffset);
+ oNode.tx = 0;
+ oNode.ty = 0;
+ switch(oNode.shape)
+ {
+ case 'disc':
+ oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr(oNode.disc_attr));
+ var oText = this.oPaper.text(xPos, yPos, oNode.label);
+ oText.attr(oNode.text_attr);
+ oText.transform('s'+this.fZoom);
+ oNode.aElements.push(oText);
+ break;
+
+ case 'group':
+ oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr({fill: '#fff', 'stroke-width':0}));
+ oNode.aElements.push(this.oPaper.circle(xPos, yPos, iWidth*this.fZoom / 2).attr(oNode.disc_attr));
+ var xIcon = xPos - 18 * this.fZoom;
+ var yIcon = yPos - 18 * this.fZoom;
+ oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon, yIcon, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
+ oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 18*this.fZoom, yIcon, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
+ oNode.aElements.push(this.oPaper.image(oNode.icon_url, xIcon + 9*this.fZoom, yIcon + 18*this.fZoom, 16*this.fZoom, 16*this.fZoom).attr(oNode.icon_attr));
+ var oText = this.oPaper.text(xPos, yPos +2, oNode.label);
+ oText.attr(oNode.text_attr);
+ oText.transform('s'+this.fZoom);
+ var oBB = oText.getBBox();
+ var dy = iHeight/2*this.fZoom + oBB.height/2;
+ oText.remove();
+ oText = this.oPaper.text(xPos, yPos +dy +2, oNode.label);
+ oText.attr(oNode.text_attr);
+ oText.transform('s'+this.fZoom);
+ oNode.aElements.push(oText);
+ oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}));
+ oText.toFront();
+ break;
+
+ case 'icon':
+ if(Raphael.svg)
+ {
+ // the colorShift plugin works only in SVG
+ oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).colorShift('#fff', 1));
+ }
+ oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).attr(oNode.icon_attr));
+ var oText = this.oPaper.text( xPos, yPos, oNode.label);
+ oText.attr(oNode.text_attr);
+ oText.transform('s'+this.fZoom);
+ var oBB = oText.getBBox();
+ var dy = iHeight/2*this.fZoom + oBB.height/2;
+ oText.remove();
+ oText = this.oPaper.text( xPos, yPos + dy, oNode.label);
+ oText.attr(oNode.text_attr);
+ oText.transform('s'+this.fZoom);
+ oNode.aElements.push(oText);
+ oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}).toBack());
+ break;
+ }
+ if (oNode.source)
+ {
+ oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*this.fZoom / 2).attr({stroke: '#c33', 'stroke-width': 3*this.fZoom }).toBack());
+ }
+ if (oNode.sink)
+ {
+ oNode.aElements.push(this.oPaper.circle(xPos, yPos, 1.25*iWidth*this.fZoom / 2).attr({stroke: '#33c', 'stroke-width': 3*this.fZoom }).toBack());
+ }
+
+ var me = this;
+ for(k in oNode.aElements)
+ {
+ var sNodeId = oNode.id;
+ oNode.aElements[k].drag(function(dx, dy, x, y, event) { me._move(sNodeId, dx, dy, x, y, event); }, function(x, y, event) { me._drag_start(sNodeId, x, y, event); }, function (event) { me._drag_end(sNodeId, event); });
+ }
+ },
+ _move: function(sNodeId, dx, dy, x, y, event)
+ {
+ var origDx = dx / this.fZoom;
+ var origDy = dy / this.fZoom;
+
+ var oNode = this._find_node(sNodeId);
+ oNode.x = oNode.xOrig + origDx;
+ oNode.y = oNode.yOrig + origDy;
+
+ for(k in oNode.aElements)
+ {
+ oNode.aElements[k].transform('t'+(oNode.tx + dx)+', '+(oNode.ty + dy));
+
+ for(j in this.aEdges)
+ {
+ var oEdge = this.aEdges[j];
+ if ((oEdge.source_node_id == sNodeId) || (oEdge.sink_node_id == sNodeId))
+ {
+ var sPath = this._get_edge_path(oEdge);
+ oEdge.aElements[0].attr({path: sPath});
+ }
+ }
+ }
+ },
+ _drag_start: function(sNodeId, x, y, event)
+ {
+ var oNode = this._find_node(sNodeId);
+ oNode.xOrig = oNode.x;
+ oNode.yOrig = oNode.y;
+
+ },
+ _drag_end: function(sNodeId, event)
+ {
+ var oNode = this._find_node(sNodeId);
+ oNode.tx += (oNode.x - oNode.xOrig) * this.fZoom;
+ oNode.ty += (oNode.y - oNode.yOrig) * this.fZoom;
+ oNode.xOrig = oNode.x;
+ oNode.yOrig = oNode.y;
+ },
+ _get_edge_path: function(oEdge)
+ {
+ var oStart = this._find_node(oEdge.source_node_id);
+ var oEnd = this._find_node(oEdge.sink_node_id);
+ var iArrowSize = 5;
+
+ if ((oStart == null) || (oEnd == null)) return '';
+
+ var xStart = Math.round(oStart.x * this.fZoom + this.xOffset);
+ var yStart = Math.round(oStart.y * this.fZoom + this.yOffset);
+ var xEnd = Math.round(oEnd.x * this.fZoom + this.xOffset);
+ var yEnd = Math.round(oEnd.y * this.fZoom + this.yOffset);
+
+ var sPath = Raphael.format('M{0},{1}L{2},{3}', xStart, yStart, xEnd, yEnd);
+ var vx = (xEnd - xStart);
+ var vy = (yEnd - yStart);
+ var l = Math.sqrt(vx*vx+vy*vy);
+ vx = vx / l;
+ vy = vy / l;
+ var ux = -vy;
+ var uy = vx;
+ var lPos = Math.max(l/2, l - 40*this.fZoom);
+ var xArrow = xStart + vx * lPos;
+ var yArrow = yStart + vy * lPos;
+ sPath += Raphael.format('M{0},{1}l{2},{3}M{4},{5}l{6},{7}', xArrow, yArrow, this.fZoom * iArrowSize *(-vx + ux), this.fZoom * iArrowSize *(-vy + uy), xArrow, yArrow, this.fZoom * iArrowSize *(-vx - ux), this.fZoom * iArrowSize *(-vy - uy));
+ return sPath;
+ },
+ _draw_edge: function(oEdge)
+ {
+ var fStrokeSize = Math.max(1, 2 * this.fZoom);
+ var sPath = this._get_edge_path(oEdge);
+ var oAttr = $.extend(oEdge.attr);
+ oAttr['stroke-linecap'] = 'round';
+ oAttr['stroke-width'] = fStrokeSize;
+ oEdge.aElements.push(this.oPaper.path(sPath).attr(oAttr).toBack());
+ },
+ _find_node: function(sId)
+ {
+ for(var k in this.aNodes)
+ {
+ if (this.aNodes[k].id == sId) return this.aNodes[k];
+ }
+ return null;
+ },
+ auto_scale: function()
+ {
+ var fMaxZoom = 1.5;
+ iMargin = 10;
+ xmin = this.options.xmin - iMargin;
+ xmax = this.options.xmax + iMargin;
+ ymin = this.options.ymin - iMargin;
+ ymax = this.options.ymax + iMargin;
+ var xScale = this.element.width() / (xmax - xmin);
+ var yScale = this.element.height() / (ymax - ymin + this.iTextHeight);
+
+ this.fZoom = Math.min(xScale, yScale, fMaxZoom);
+ switch(this.options.align)
+ {
+ case 'left':
+ this.xOffset = -xmin * this.fZoom;
+ break;
+
+ case 'right':
+ this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom);
+ break;
+
+ case 'center':
+ this.xOffset = (this.element.width() - (xmax - xmin) * this.fZoom) / 2;
+ break;
+ }
+ switch(this.options['vertical-align'])
+ {
+ case 'top':
+ this.yOffset = -ymin * this.fZoom;
+ break;
+
+ case 'bottom':
+ this.yOffset = this.element.height() - (ymax + this.iTextHeight) * this.fZoom;
+ break;
+
+ case 'middle':
+ this.yOffset = (this.element.height() - (ymax - ymin + this.iTextHeight) * this.fZoom) / 2;
+ break;
+ }
+
+
+ },
+ add_node: function(oNode)
+ {
+ oNode.aElements = [];
+ this.aNodes.push(oNode);
+ },
+ add_edge: function(oEdge)
+ {
+ oEdge.aElements = [];
+ this.aEdges.push(oEdge);
+ },
+ _create_toolkit_menu: function()
+ {
+ var sPopupMenuId = 'tk_graph'+this.element.attr('id');
+ var sHtml = '';
+
+ this.element.before(sHtml);
+ $('#'+sPopupMenuId).popupmenu();
+
+ var me = this;
+ $('#'+sPopupMenuId+'_pdf').click(function() { me.export_as_pdf(); });
+ $('#'+sPopupMenuId+'_document').click(function() { me.export_as_document(); });
+ $('#'+sPopupMenuId+'_reload').click(function() { me.reload(); });
+
+ },
+ export_as_pdf: function()
+ {
+ alert('Export as PDF: not yet implemented');
+ },
+ export_as_document: function()
+ {
+ alert('Export as document: not yet implemented');
+ },
+ reload: function()
+ {
+ alert('Reload: not yet implemented');
+ }
+ });
+});
diff --git a/pages/UI.php b/pages/UI.php
index 98f254976..76b9a9dbd 100644
--- a/pages/UI.php
+++ b/pages/UI.php
@@ -239,7 +239,7 @@ function DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj)
$oP->add("");
}
-function DisplayNavigatorGraphicsTab($oP, $aResults, $sClass, $id, $sRelation, $oAppContext)
+function DisplayNavigatorGraphicsTab($oP, $aResults, $oRelGraph, $sClass, $id, $sRelation, $oAppContext, $bDirectionDown)
{
$oP->SetCurrentTab(Dict::S('UI:RelationshipGraph'));
@@ -273,42 +273,23 @@ EOF
$oP->add("\n");
$oP->add("\n");
$oP->add("".Dict::S('UI:ElementsDisplayed')."
\n");
+
+ $sDirection = utils::ReadParam('d', 'horizontal');
+ $iGroupingThreshold = utils::ReadParam('g', 5);
- $width = 1000;
- $height = 700;
- $sDrillUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&'.$oAppContext->GetForLink();
- $sParams = "pWidth=$width&pHeight=$height&drillUrl=".urlencode($sDrillUrl)."&displayController=false&xmlUrl=".urlencode("./xml.navigator.php")."&obj_class=$sClass&obj_id=$id&relation=$sRelation";
-
- $oP->add("\n");
+ $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js');
+ $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js');
+ $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, $bDirectionDown);
+ $oGraph->InitFromGraphviz();
+ $oGraph->RenderAsRaphael($oP);
+ $oP->p(''.Dict::S('UI:GraphAsPDF').'');
+
$oP->add_script(
<<Get('relations_max_depth', 20);
- $oObj->GetRelatedObjects($sRelation, $iMaxRecursionDepth /* iMaxDepth */, $aResults);
+ $aSourceObjects = array($oObj);
+ if ($sRelation == 'depends on')
+ {
+ $sRelation = 'impacts';
+ $sDirection = 'up';
+ }
+ if ($sDirection == 'up')
+ {
+ $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+ }
+ else
+ {
+ $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+ }
+
+
+ $aResults = array();
+ $oIterator = new RelationTypeIterator($oRelGraph, 'Node');
+ foreach($oIterator as $oNode)
+ {
+ $oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes) do not contain an object
+ if ($oObj)
+ {
+ $sObjClass = get_class($oObj);
+ if (!array_key_exists($sClass, $aResults))
+ {
+ $aResults[$sObjClass] = array();
+ }
+ $aResults[$sObjClass][] = $oObj;
+ }
+ }
$oP->AddTabContainer('Navigator');
$oP->SetCurrentTabContainer('Navigator');
@@ -1546,11 +1558,11 @@ EOF
if ($sFirstTab == 'list')
{
DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj);
- DisplayNavigatorGraphicsTab($oP, $aResults, $sClass, $id, $sRelation, $oAppContext);
+ DisplayNavigatorGraphicsTab($oP, $aResults, $oRelGraph, $sClass, $id, $sRelation, $oAppContext, ($sDirection == 'down'));
}
else
{
- DisplayNavigatorGraphicsTab($oP, $aResults, $sClass, $id, $sRelation, $oAppContext);
+ DisplayNavigatorGraphicsTab($oP, $aResults, $oRelGraph, $sClass, $id, $sRelation, $oAppContext, ($sDirection == 'down'));
DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj);
}
diff --git a/pages/ajax.render.php b/pages/ajax.render.php
index bb1fc8035..028a5b78f 100644
--- a/pages/ajax.render.php
+++ b/pages/ajax.render.php
@@ -1724,7 +1724,42 @@ EOF
// Stop & cleanup an export...
$sToken = utils::ReadParam('token', '', false, 'raw_data');
ExcelExporter::CleanupFromToken($sToken);
- break;
+ break;
+
+ case 'relation_pdf':
+ require_once(APPROOT.'core/simplegraph.class.inc.php');
+ require_once(APPROOT.'core/relationgraph.class.inc.php');
+ require_once(APPROOT.'core/displayablegraph.class.inc.php');
+ $sClass = utils::ReadParam('class', '', false, 'class');
+ $id = utils::ReadParam('id', 0);
+ $sRelation = utils::ReadParam('relation', 'impact');
+ $sDirection = utils::ReadParam('direction', 'down');
+
+ $iGroupingThreshold = utils::ReadParam('g', 5);
+ $sPageFormat = utils::ReadParam('p', 'A4');
+ $sPageOrientation = utils::ReadParam('o', 'L');
+ $sTitle = utils::ReadParam('title', '', false, 'raw_data');
+
+ $oObj = MetaModel::GetObject($sClass, $id);
+ $iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
+ $aSourceObjects = array($oObj);
+ if ($sDirection == 'up')
+ {
+ $oRelGraph = MetaModel::GetRelatedObjectsUp($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+ }
+ else
+ {
+ $oRelGraph = MetaModel::GetRelatedObjectsDown($sRelation, $aSourceObjects, $iMaxRecursionDepth);
+ }
+
+
+ $oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
+ $oGraph->InitFromGraphviz();
+ $oGraph->RenderAsPDF($oPage, $sTitle, $sPageFormat, $sPageOrientation);
+
+ $oPage->SetContentType('application/pdf');
+ $oPage->SetContentDisposition('inline', 'iTop.pdf');
+ break;
default: