/** * 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($aContextDefs) { $aNode = array(); $aNode['shape'] = 'icon'; $aNode['icon_url'] = $this->GetIconURL(); $aNode['width'] = 32; $aNode['source'] = ($this->GetProperty('source') == true); $aNode['obj_class'] = get_class($this->GetProperty('object')); $aNode['obj_key'] = $this->GetProperty('object')->GetKey(); $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); $aNode['tooltip'] = $this->GetTooltip($aContextDefs); $aNode['context_icons'] = array(); $aContextRootCauses = $this->GetProperty('context_root_causes'); if (!is_null($aContextRootCauses)) { foreach($aContextRootCauses as $key => $aObjects) { $aNode['context_icons'][] = utils::GetAbsoluteUrlModulesRoot().$aContextDefs[$key]['icon']; } } return $aNode; } public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) { $Alpha = 1.0; $oPdf->SetFillColor(200, 200, 200); $oPdf->setAlpha(1); $sIconUrl = $this->GetProperty('icon_url'); $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $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')) { $sTempImageName = $this->CreateWhiteIcon($oGraph, $sIconPath); if ($sTempImageName != null) { $oPdf->Image($sTempImageName, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale, 'PNG'); } $Alpha = 0.4; $oPdf->setAlpha($Alpha); } $oPdf->Image($sIconPath, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale); $aContextRootCauses = $this->GetProperty('context_root_causes'); if (!is_null($aContextRootCauses)) { $idx = 0; foreach($aContextRootCauses as $key => $aObjects) { $sgn = 2*($idx %2) -1; $coef = floor((1+$idx)/2) * $sgn; $alpha = $coef*pi()/4 - pi()/2; $x = $this->x * $fScale + cos($alpha) * 16*1.25 * $fScale; $y = $this->y * $fScale + sin($alpha) * 16*1.25 * $fScale; $l = 32 * $fScale / 3; $sIconPath = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$aContextDefs[$key]['icon']; $oPdf->Image($sIconPath, $x - $l/2, $y - $l/2, $l, $l); $idx++; } } $oPdf->SetFont('dejavusans', '', 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')); } /** * Create a "whitened" version of the icon (retaining the transparency) to be used a background for masking the underlying lines * @param string $sIconFile The path to the file containing the icon * @return NULL|string The path to a temporary file containing the white version of the icon */ protected function CreateWhiteIcon(DisplayableGraph $oGraph, $sIconFile) { $aInfo = getimagesize($sIconFile); $im = null; switch($aInfo['mime']) { case 'image/png': if (function_exists('imagecreatefrompng')) { $im = imagecreatefrompng($sIconFile); } break; case 'image/gif': if (function_exists('imagecreatefromgif')) { $im = imagecreatefromgif($sIconFile); } break; case 'image/jpeg': case 'image/jpg': if (function_exists('imagecreatefromjpeg')) { $im = imagecreatefromjpeg($sIconFile); } break; default: return null; } if($im && imagefilter($im, IMG_FILTER_COLORIZE, 255, 255, 255)) { $sTempImageName = $oGraph->GetTempImageName(); imagesavealpha($im, true); imagepng($im, $sTempImageName); imagedestroy($im); return $sTempImageName; } else { return null; } } /** * Group together (as a special kind of nodes) all the similar neighbours of the current node * @param DisplayableGraph $oGraph * @param int $iThresholdCount * @param boolean $bDirectionUp * @param boolean $bDirectionDown */ public function GroupSimilarNeighbours(DisplayableGraph $oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true) { 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); } } else { $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); } } foreach($aNodesPerClass as $sClass => $aDefs) { foreach($aDefs as $sStatus => $aGroupProps) { if (count($aGroupProps['nodes']) >= $iThresholdCount) { $sNewId = $this->GetId().'::'.(($sStatus == 'reached') ? '_reached': ''); $oNewNode = $oGraph->GetNode($sNewId); if ($oNewNode == null) { $oNewNode = new DisplayableGroupNode($oGraph, $sNewId); $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']); } else { $oNewNode->SetProperty('count', $oNewNode->GetProperty('count')+$aGroupProps['count']); } try { $oIncomingEdge = new DisplayableEdge($oGraph, $this->GetId().'-'.$oNewNode->GetId(), $this, $oNewNode); } catch(Exception $e) { // Ignore this redundant egde } foreach($aGroupProps['nodes'] as $oNode) { foreach($oNode->GetIncomingEdges() as $oEdge) { if ($oEdge->GetSourceNode()->GetId() !== $this->GetId()) { try { $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode); } catch(Exception $e) { // ignore this edge } } } 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->AddObject($oNode->GetProperty('object')); } } $oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); } else { foreach($aGroupProps['nodes'] as $oNode) { $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); } } } } } } public function GetTooltip($aContextDefs) { $sHtml = ''; $oCurrObj = $this->GetProperty('object'); $sSubClass = get_class($oCurrObj); $sHtml .= $oCurrObj->GetHyperlink()."
"; $aContextRootCauses = $this->GetProperty('context_root_causes'); if (!is_null($aContextRootCauses)) { foreach($aContextRootCauses as $key => $aObjects) { $aContext = $aContextDefs[$key]; $aRootCauses = array(); foreach($aObjects as $oRootCause) { $aRootCauses[] = $oRootCause->GetHyperlink(); } $sHtml .= '

 '.implode(', ', $aRootCauses).'

'; } $sHtml .= '
'; } $sHtml .= ''; foreach(MetaModel::GetZListItems($sSubClass, 'list') as $sAttCode) { $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); $sHtml .= ''; } $sHtml .= '
'.$oAttDef->GetLabel().': '.$oCurrObj->GetAsHtml($sAttCode).'
'; return $sHtml; } } class DisplayableRedundancyNode extends DisplayableNode { public function GetWidth() { return 24; } public function GetForRaphael($aContextDefs) { $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); $sColor = ($this->GetProperty('is_reached_count') > $this->GetProperty('threshold')) ? '#c33' : '#999'; $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => $sColor, 'opacity' => $fDiscOpacity); $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4); $aNode['text_attr'] = array('fill' => '#fff', 'opacity' => $fTextOpacity); $aNode['tooltip'] = $this->GetTooltip($aContextDefs); return $aNode; } public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) { $oPdf->SetAlpha(1); if($this->GetProperty('is_reached_count') > $this->GetProperty('threshold')) { $oPdf->SetFillColor(200, 0, 0); } else { $oPdf->SetFillColor(144, 144, 144); } $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('dejavusans', '', 28 * $fScale, '', true); $sLabel = (string)$this->GetProperty('label'); $width = $oPdf->GetStringWidth($sLabel, 'dejavusans', 'B', 24*$fScale); $height = $oPdf->GetStringHeight(1000, $sLabel); $xPos = (float)$this->x*$fScale - $width/2; $yPos = (float)$this->y*$fScale - $height/2; $oPdf->SetXY(($this->x - 16)*$fScale, ($this->y - 16)*$fScale); $oPdf->Cell(32*$fScale, 32*$fScale, $sLabel, 0, 0, 'C', 0, '', 0, false, 'T', 'C'); } /** * @see DisplayableNode::GroupSimilarNeighbours() */ public function GroupSimilarNeighbours(DisplayableGraph $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) { 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()); } } $oGraph->_RemoveNode($oNode); $oNewNode->AddObject($oNode->GetProperty('object')); } //$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); } else { foreach($aNodes as $oNode) { //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); } } } } } } public function GetTooltip($aContextDefs) { $sHtml = ''; $sHtml .= Dict::S('UI:RelationTooltip:Redundancy')."
"; $sHtml .= ''; $sHtml .= ""; $sHtml .= ""; $sHtml .= '
".Dict::Format('UI:RelationTooltip:ImpactedItems_N_of_M' , $this->GetProperty('is_reached_count'), $this->GetProperty('min_up') + $this->GetProperty('threshold'))."
".Dict::Format('UI:RelationTooltip:CriticalThreshold_N_of_M' , $this->GetProperty('threshold'), $this->GetProperty('min_up') + $this->GetProperty('threshold'))."
'; return $sHtml; } } class DisplayableEdge extends GraphEdge { public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) { $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 { protected $aObjects; public function __construct(SimpleGraph $oGraph, $sId, $x = 0, $y = 0) { parent::__construct($oGraph, $sId, $x, $y); $this->aObjects = array(); } public function AddObject(DBObject $oObj) { $this->aObjects[$oObj->GetKey()] = $oObj; } public function GetObjects() { return $this->aObjects; } public function GetWidth() { return 50; } public function GetForRaphael($aContextDefs) { $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(); $aNode['group_index'] = $this->GetProperty('group_index'); // if supplied $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); $aNode['tooltip'] = $this->GetTooltip($aContextDefs); return $aNode; } public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) { $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-'.utils::GetCurrentEnvironment().'/', $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('dejavusans', '', 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')); } public function GetTooltip($aContextDefs) { $sHtml = ''; $iGroupIdx = $this->GetProperty('group_index'); $sHtml .= Dict::Format('UI:RelationGroupNumber_N', (1+$iGroupIdx)); return $sHtml; } } /** * A Graph that can be displayed interactively using Raphael JS or saved as a PDF document */ class DisplayableGraph extends SimpleGraph { protected $bDirectionDown; protected $aTempImages; protected $aSourceObjects; protected $aSinkObjects; public function __construct() { parent::__construct(); $this->aTempImages = array(); $this->aSourceObjects = array(); $this->aSinkObjects = array(); } public function GetTempImageName() { $sNewTempName = tempnam(APPROOT.'data', 'img-'); $this->aTempImages[] = $sNewTempName; return $sNewTempName; } public function __destruct() { foreach($this->aTempImages as $sTempFile) { @unlink($sTempFile); } } /** * Build a DisplayableGraph from a RelationGraph * @param RelationGraph $oGraph * @param number $iGroupingThreshold * @param string $bDirectionDown * @return DisplayableGraph */ public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true) { $oNewGraph = new DisplayableGraph(); $oNewGraph->bDirectionDown = $bDirectionDown; $oNodesIter = new RelationTypeIterator($oGraph, 'Node'); foreach($oNodesIter as $oNode) { switch(get_class($oNode)) { case 'RelationObjectNode': $oNewNode = new DisplayableNode($oNewGraph, $oNode->GetId(), 0, 0); $oObj = $oNode->GetProperty('object'); $sClass = get_class($oObj); if ($oNode->GetProperty('source')) { if (!array_key_exists($sClass, $oNewGraph->aSourceObjects)) { $oNewGraph->aSourceObjects[$sClass] = array(); } $oNewGraph->aSourceObjects[$sClass][] = $oObj->GetKey(); $oNewNode->SetProperty('source', true); } if ($oNode->GetProperty('sink')) { if (!array_key_exists($sClass, $oNewGraph->aSinkObjects)) { $oNewGraph->aSinkObjects[$sClass] = array(); } $oNewGraph->aSinkObjects[$sClass][] = $oObj->GetKey(); $oNewNode->SetProperty('sink', true); } $oNewNode->SetProperty('class', $sClass); $oNewNode->SetProperty('object', $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('is_reached_allowed', $oNode->GetProperty('is_reached_allowed')); $oNewNode->SetProperty('context_root_causes', $oNode->GetProperty('context_root_causes')); break; default: $oNewNode = new DisplayableRedundancyNode($oNewGraph, $oNode->GetId(), 0, 0); $iNbReached = (is_null($oNode->GetProperty('is_reached_count'))) ? 0 : $oNode->GetProperty('is_reached_count'); $oNewNode->SetProperty('label', $iNbReached."/".($oNode->GetProperty('min_up') + $oNode->GetProperty('threshold'))); $oNewNode->SetProperty('min_up', $oNode->GetProperty('min_up')); $oNewNode->SetProperty('threshold', $oNode->GetProperty('threshold')); $oNewNode->SetProperty('is_reached_count', $iNbReached); $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; } } } $oNodesIter = new RelationTypeIterator($oNewGraph, 'Node'); foreach($oNodesIter as $oNode) { if ($oNode->GetProperty('source')) { $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, true); } } // Groups numbering $oIterator = new RelationTypeIterator($oNewGraph, 'Node'); $iGroupIdx = 0; foreach($oIterator as $oNode) { if ($oNode instanceof DisplayableGroupNode) { $aGroups[] = $oNode->GetObjects(); $oNode->SetProperty('group_index', $iGroupIdx); $iGroupIdx++; } } // 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; } /** * Initializes the positions by rendering using Graphviz in xdot format * and parsing the output. * @throws Exception */ public function InitFromGraphviz() { $sDot = $this->DumpAsXDot(); if (strpos($sDot, 'digraph') === false) { throw new Exception($sDot); } $aChunks = explode(";", $sDot); foreach($aChunks as $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; } } } 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; } } public function UpdatePositions($aPositions) { foreach($aPositions as $sNodeId => $aPos) { $oNode = $this->GetNode($sNodeId); if ($oNode != null) { $oNode->x = $aPos['x']; $oNode->y = $aPos['y']; } } } /** * Renders as JSON string suitable for loading into the simple_graph widget */ function GetAsJSON($sContextKey) { $aContextDefs = $this->GetContextDefinitions($sContextKey, false); $aData = array('nodes' => array(), 'edges' => array()); $iGroupIdx = 0; $oIterator = new RelationTypeIterator($this, 'Node'); foreach($oIterator as $sId => $oNode) { if ($oNode instanceof DisplayableGroupNode) { $aGroups[] = $oNode->GetObjects(); $oNode->SetProperty('group_index', $iGroupIdx); $iGroupIdx++; } $aData['nodes'][] = $oNode->GetForRaphael($aContextDefs); } $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'); $aData['edges'][] = $aEdge; } return json_encode($aData); } /** * Renders the graph in a PDF document: centered in the current page * @param PDFPage $oPage The PDFPage representing the PDF document to draw into * @param string $sComments An optional comment to display next to the graph (HTML entities will be escaped, \n replaced by
) * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down * @param float $xMin Left coordinate of the bounding box to display the graph * @param float $xMax Right coordinate of the bounding box to display the graph * @param float $yMin Top coordinate of the bounding box to display the graph * @param float $yMax Bottom coordinate of the bounding box to display the graph */ function RenderAsPDF(PDFPage $oPage, $sComments = '', $sContextKey, $xMin = -1, $xMax = -1, $yMin = -1, $yMax = -1) { $aContextDefs = $this->GetContextDefinitions($sContextKey, false); // No need to develop the parameters $oPdf = $oPage->get_tcpdf(); $aBB = $this->GetBoundingBox(); $this->Translate(-$aBB['xmin'], -$aBB['ymin']); $aMargins = $oPdf->getMargins(); if ($xMin == -1) { $xMin = $aMargins['left']; } if ($xMax == -1) { $xMax = $oPdf->getPageWidth() - $aMargins['right']; } if ($yMin == -1) { $yMin = $aMargins['top']; } if ($yMax == -1) { $yMax = $oPdf->getPageHeight() - $aMargins['bottom']; } $fBreakMargin = $oPdf->getBreakMargin(); $oPdf->SetAutoPageBreak(false); $aRemainingArea = $this->RenderKey($oPdf, $sComments, $xMin, $yMin, $xMax, $yMax, $aContextDefs); $xMin = $aRemainingArea['xmin']; $xMax = $aRemainingArea['xmax']; $yMin = $aRemainingArea['ymin']; $yMax = $aRemainingArea['ymax']; //$oPdf->Rect($xMin, $yMin, $xMax - $xMin, $yMax - $yMin, 'D', array(), array(225, 50, 50)); $fPageW = $xMax - $xMin; $fPageH = $yMax - $yMin; $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(($xMin + $dx)/$fScale, ($yMin + $dy)/$fScale); $oIterator = new RelationTypeIterator($this, 'Edge'); $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); foreach($oIterator as $sId => $oEdge) { set_time_limit($iLoopTimeLimit); $oEdge->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs); } $oIterator = new RelationTypeIterator($this, 'Node'); foreach($oIterator as $sId => $oNode) { set_time_limit($iLoopTimeLimit); $oNode->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs); } $oIterator = new RelationTypeIterator($this, 'Node'); $oPdf->SetAutoPageBreak(true, $fBreakMargin); $oPdf->SetAlpha(1); } /** * Renders (in PDF) the key (legend) of the graphics vertically to the left of the specified zone (xmin,ymin, xmax,ymax), * and the comment (if any) at the bottom of the page. Returns the position of remaining area. * @param TCPDF $oPdf * @param string $sComments * @param float $xMin * @param float $yMin * @param float $xMax * @param float $yMax * @param hash $aContextDefs * @return hash An array ('xmin' => , 'xmax' => ,'ymin' => , 'ymax' => ) of the remaining available area to paint the graph */ protected function RenderKey(TCPDF $oPdf, $sComments, $xMin, $yMin, $xMax, $yMax, $aContextDefs) { $fFontSize = 7; // in mm $fIconSize = 6; // in mm $fPadding = 1; // in mm $oIterator = new RelationTypeIterator($this, 'Node'); $fMaxWidth = max($oPdf->GetStringWidth(Dict::S('UI:Relation:Key')) - $fIconSize, $oPdf->GetStringWidth(Dict::S('UI:Relation:Comments')) - $fIconSize); $aClasses = array(); $aIcons = array(); $aContexts = array(); $aContextIcons = array(); $oPdf->SetFont('dejavusans', '', $fFontSize, '', true); foreach($oIterator as $sId => $oNode) { if ($sClass = $oNode->GetProperty('class')) { if (!array_key_exists($sClass, $aClasses)) { $sClassLabel = MetaModel::GetName($sClass); $width = $oPdf->GetStringWidth($sClassLabel); $fMaxWidth = max($width, $fMaxWidth); $aClasses[$sClass] = $sClassLabel; $sIconUrl = $oNode->GetProperty('icon_url'); $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl); $aIcons[$sClass] = $sIconPath; } } $aContextRootCauses = $oNode->GetProperty('context_root_causes'); if (!is_null($aContextRootCauses)) { foreach($aContextRootCauses as $key => $aObjects) { $aContexts[$key] = Dict::S($aContextDefs[$key]['dict']); $aContextIcons[$key] = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$aContextDefs[$key]['icon']; } } } $oPdf->SetXY($xMin + $fPadding, $yMin + $fPadding); $yPos = $yMin + $fPadding; $oPdf->SetFillColor(225, 225, 225); $oPdf->Cell($fIconSize + $fPadding + $fMaxWidth, $fIconSize + $fPadding, Dict::S('UI:Relation:Key'), 0 /* border */, 1 /* ln */, 'C', true /* fill */); $yPos += $fIconSize + 2*$fPadding; foreach($aClasses as $sClass => $sLabel) { $oPdf->SetX($xMin + $fIconSize + $fPadding); $oPdf->Cell(0, $fIconSize + 2*$fPadding, $sLabel, 0 /* border */, 1 /* ln */); $oPdf->Image($aIcons[$sClass], $xMin+1, $yPos, $fIconSize, $fIconSize); $yPos += $fIconSize + 2*$fPadding; } foreach($aContexts as $key => $sLabel) { $oPdf->SetX($xMin + $fIconSize + $fPadding); $oPdf->Cell(0, $fIconSize + 2*$fPadding, $sLabel, 0 /* border */, 1 /* ln */); $oPdf->Image($aContextIcons[$key], $xMin+1+$fIconSize*0.125, $yPos+$fIconSize*0.125, $fIconSize*0.75, $fIconSize*0.75); $yPos += $fIconSize + 2*$fPadding; } $oPdf->Rect($xMin, $yMin, $fMaxWidth + $fIconSize + 3*$fPadding, $yMax - $yMin, 'D'); if ($sComments != '') { // Draw the comment text (surrounded by a rectangle) $xPos = $xMin + $fMaxWidth + $fIconSize + 4*$fPadding; $w = $xMax - $xPos - 2*$fPadding; $iNbLines = 1; $sText = '

'.str_replace("\n", '
', htmlentities($sComments, ENT_QUOTES, 'UTF-8'), $iNbLines).'

'; $fLineHeight = $oPdf->getStringHeight($w, $sText); $h = (1+$iNbLines) * $fLineHeight; $yPos = $yMax - 2*$fPadding - $h; $oPdf->writeHTMLCell($w, $h, $xPos + $fPadding, $yPos + $fPadding, $sText, 0 /* border */, 1 /* ln */); $oPdf->Rect($xPos, $yPos, $w + 2*$fPadding, $h + 2*$fPadding, 'D'); $yMax = $yPos - $fPadding; } return array('xmin' => $xMin + $fMaxWidth + $fIconSize + 4*$fPadding, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax); } /** * Get the context definitions from the parameters / configuration. The format of the "key" string is: * /relation_context/// * The values will be retrieved for the given class and all its parents and merged together as a single array. * Entries with an invalid query are removed from the list. * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down * @param bool $bDevelopParams Whether or not to substitute the parameters inside the queries with the supplied "context params" * @param array $aContextParams Arguments for the queries (via ToArgs()) if $bDevelopParams == true * @return multitype:multitype:string */ public function GetContextDefinitions($sContextKey, $bDevelopParams = true, $aContextParams = array()) { $aLevels = explode('/', $sContextKey); $sLeafClass = $aLevels[2]; $aRelationContext = MetaModel::GetConfig()->GetModuleSetting($aLevels[0], $aLevels[1], array()); $aContextDefs = array(); foreach(MetaModel::EnumParentClasses($sLeafClass, ENUM_PARENT_CLASSES_ALL) as $sClass) { if (isset($aRelationContext[$sClass][$aLevels[3]][$aLevels[4]]['items'])) { $aContextDefs = array_merge($aContextDefs, $aRelationContext[$sClass][$aLevels[3]][$aLevels[4]]['items']); } } // Check if the queries are valid foreach($aContextDefs as $sKey => $sDefs) { $sOQL = $aContextDefs[$sKey]['oql']; try { // Expand the parameters. If anything goes wrong, then the query is considered as invalid and removed from the list $oSearch = DBObjectSearch::FromOQL($sOQL); $aContextDefs[$sKey]['oql'] = $oSearch->ToOQL($bDevelopParams, $aContextParams); } catch(Exception $e) { unset($aContextDefs[$sKey]); } } return $aContextDefs; } /** * Display the graph inside the given page, with the "filter" drawer above it * @param WebPage $oP * @param hash $aResults * @param string $sRelation * @param ApplicationContext $oAppContext * @param array $aExcludedObjects */ function Display(WebPage $oP, $aResults, $sRelation, ApplicationContext $oAppContext, $aExcludedObjects = array(), $sObjClass = null, $iObjKey = null, $sContextKey, $aContextParams = array()) { $aContextDefs = $this->GetContextDefinitions($sContextKey, true, $aContextParams); $aExcludedByClass = array(); foreach($aExcludedObjects as $oObj) { if (!array_key_exists(get_class($oObj), $aExcludedByClass)) { $aExcludedByClass[get_class($oObj)] = array(); } $aExcludedByClass[get_class($oObj)][] = $oObj->GetKey(); } $oP->add("
\n"); $oP->add_ready_script( << $aObjects) { foreach($aObjects as $oCurrObj) { $sSubClass = get_class($oCurrObj); $aSortedElements[$sSubClass] = MetaModel::GetName($sSubClass); } } asort($aSortedElements); $idx = 0; foreach($aSortedElements as $sSubClass => $sClassName) { $oP->add(" "); $idx++; } $oP->add("

"); $oP->add("
\n"); $oP->add("
\n"); $oP->add("
".Dict::S('UI:ElementsDisplayed')."
\n"); $aAdditionalContexts = array(); foreach($aContextDefs as $sKey => $aDefinition) { $aAdditionalContexts[] = array('key' => $sKey, 'label' => Dict::S($aDefinition['dict']), 'oql' => $aDefinition['oql']); } $sDirection = utils::ReadParam('d', 'horizontal'); $iGroupingThreshold = utils::ReadParam('g', 5); $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js'); $oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css'); $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js'); $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js'); try { $this->InitFromGraphviz(); $sExportAsPdfURL = ''; $sExportAsPdfURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_pdf&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up'); $oAppcontext = new ApplicationContext(); $sContext = $oAppContext->GetForLink(); $sDrillDownURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class=%1$s&id=%2$s&'.$sContext; $sExportAsDocumentURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_attachment&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up'); $sLoadFromURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_json&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up'); $sAttachmentExportTitle = ''; if (($sObjClass != null) && ($iObjKey != null)) { $oTargetObj = MetaModel::GetObject($sObjClass, $iObjKey, false); if ($oTargetObj) { $sAttachmentExportTitle = Dict::Format('UI:Relation:AttachmentExportOptions_Name', $oTargetObj->GetName()); } } $sId = 'graph'; $oP->add('
'); $aParams = array( 'source_url' => $sLoadFromURL, 'sources' => ($this->bDirectionDown ? $this->aSourceObjects : $this->aSinkObjects), 'excluded' => $aExcludedByClass, 'grouping_threshold' => $iGroupingThreshold, 'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')), 'export_as_attachment' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsAttachment'), 'obj_class' => $sObjClass, 'obj_key' => $iObjKey), 'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')), 'labels' => array( 'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'), 'export_as_attachment_title' => $sAttachmentExportTitle, 'export' => Dict::S('UI:Button:Export'), 'cancel' => Dict::S('UI:Button:Cancel'), 'title' => Dict::S('UI:RelationOption:Title'), 'untitled' => Dict::S('UI:RelationOption:Untitled'), 'include_list' => Dict::S('UI:RelationOption:IncludeList'), 'comments' => Dict::S('UI:RelationOption:Comments'), 'grouping_threshold' => Dict::S('UI:RelationOption:GroupingThreshold'), 'refresh' => Dict::S('UI:Button:Refresh'), 'check_all' => Dict::S('UI:SearchValue:CheckAll'), 'uncheck_all' => Dict::S('UI:SearchValue:UncheckAll'), 'none_selected' => Dict::S('UI:Relation:NoneSelected'), 'nb_selected' => Dict::S('UI:SearchValue:NbSelected'), 'additional_context_info' => Dict::S('UI:Relation:AdditionalContextInfo'), 'zoom' => Dict::S('UI:Relation:Zoom'), 'loading' => Dict::S('UI:Loading'), ), 'page_format' => array( 'label' => Dict::S('UI:Relation:PDFExportPageFormat'), 'values' => array( 'A3' => Dict::S('UI:PageFormat_A3'), 'A4' => Dict::S('UI:PageFormat_A4'), 'Letter' => Dict::S('UI:PageFormat_Letter'), ), ), 'page_orientation' => array( 'label' => Dict::S('UI:Relation:PDFExportPageOrientation'), 'values' => array( 'P' => Dict::S('UI:PageOrientation_Portrait'), 'L' => Dict::S('UI:PageOrientation_Landscape'), ), ), 'additional_contexts' => $aAdditionalContexts, 'context_key' => $sContextKey, ); if (!extension_loaded('gd')) { // PDF export requires GD unset($aParams['export_as_pdf']); } if (!extension_loaded('gd') || is_null($sObjClass) || is_null($iObjKey)) { // Export as Attachment requires GD (for building the PDF) AND a valid objclass/objkey couple unset($aParams['export_as_attachment']); } $oP->add_ready_script("$('#$sId').simple_graph(".json_encode($aParams).");"); } catch(Exception $e) { $oP->add('
'.$e->getMessage().'
'); } $oP->add_script( <<