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: