1<?php 2// Copyright (C) 2015 Combodo SARL 3// 4// This file is part of iTop. 5// 6// iTop is free software; you can redistribute it and/or modify 7// it under the terms of the GNU Affero General Public License as published by 8// the Free Software Foundation, either version 3 of the License, or 9// (at your option) any later version. 10// 11// iTop is distributed in the hope that it will be useful, 12// but WITHOUT ANY WARRANTY; without even the implied warranty of 13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14// GNU Affero General Public License for more details. 15// 16// You should have received a copy of the GNU Affero General Public License 17// along with iTop. If not, see <http://www.gnu.org/licenses/> 18 19/** 20 * Special kind of Graph for producing some nice output 21 * 22 * @copyright Copyright (C) 2015 Combodo SARL 23 * @license http://opensource.org/licenses/AGPL-3.0 24 */ 25 26class DisplayableNode extends GraphNode 27{ 28 public $x; 29 public $y; 30 31 /** 32 * Create a new node inside a graph 33 * @param SimpleGraph $oGraph 34 * @param string $sId The unique identifier of this node inside the graph 35 * @param number $x Horizontal position 36 * @param number $y Vertical position 37 */ 38 public function __construct(SimpleGraph $oGraph, $sId, $x = null, $y = null) 39 { 40 parent::__construct($oGraph, $sId); 41 $this->x = $x; 42 $this->y = $y; 43 $this->bFiltered = false; 44 } 45 46 public function GetIconURL() 47 { 48 return $this->GetProperty('icon_url', ''); 49 } 50 51 public function GetLabel() 52 { 53 return $this->GetProperty('label', $this->sId); 54 } 55 56 public function GetWidth() 57 { 58 return max(32, 5*strlen($this->GetProperty('label'))); // approximation of the text's bounding box 59 } 60 61 public function GetHeight() 62 { 63 return 32; 64 } 65 66 public function Distance2(DisplayableNode $oNode) 67 { 68 $dx = $this->x - $oNode->x; 69 $dy = $this->y - $oNode->y; 70 71 $d2 = $dx*$dx + $dy*$dy - $this->GetHeight()*$this->GetHeight(); 72 if ($d2 < 40) 73 { 74 $d2 = 40; 75 } 76 return $d2; 77 } 78 79 public function Distance(DisplayableNode $oNode) 80 { 81 return sqrt($this->Distance2($oNode)); 82 } 83 84 public function GetForRaphael($aContextDefs) 85 { 86 $aNode = array(); 87 $aNode['shape'] = 'icon'; 88 $aNode['icon_url'] = $this->GetIconURL(); 89 $aNode['width'] = 32; 90 $aNode['source'] = ($this->GetProperty('source') == true); 91 $aNode['obj_class'] = get_class($this->GetProperty('object')); 92 $aNode['obj_key'] = $this->GetProperty('object')->GetKey(); 93 $aNode['sink'] = ($this->GetProperty('sink') == true); 94 $aNode['x'] = $this->x; 95 $aNode['y']= $this->y; 96 $aNode['label'] = $this->GetLabel(); 97 $aNode['id'] = $this->GetId(); 98 $fOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4); 99 $aNode['icon_attr'] = array('opacity' => $fOpacity); 100 $aNode['text_attr'] = array('opacity' => $fOpacity); 101 $aNode['tooltip'] = $this->GetTooltip($aContextDefs); 102 $aNode['context_icons'] = array(); 103 $aContextRootCauses = $this->GetProperty('context_root_causes'); 104 if (!is_null($aContextRootCauses)) 105 { 106 foreach($aContextRootCauses as $key => $aObjects) 107 { 108 $aNode['context_icons'][] = utils::GetAbsoluteUrlModulesRoot().$aContextDefs[$key]['icon']; 109 } 110 } 111 return $aNode; 112 } 113 114 public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) 115 { 116 $Alpha = 1.0; 117 $oPdf->SetFillColor(200, 200, 200); 118 $oPdf->setAlpha(1); 119 120 $sIconUrl = $this->GetProperty('icon_url'); 121 $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl); 122 123 if ($this->GetProperty('source')) 124 { 125 $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => array(204, 51, 51))); 126 $oPdf->Circle($this->x * $fScale, $this->y * $fScale, 16 * 1.25 * $fScale, 0, 360, 'D'); 127 } 128 else if ($this->GetProperty('sink')) 129 { 130 $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => array(51, 51, 204))); 131 $oPdf->Circle($this->x * $fScale, $this->y * $fScale, 16 * 1.25 * $fScale, 0, 360, 'D'); 132 } 133 134 if (!$this->GetProperty('is_reached')) 135 { 136 $sTempImageName = $this->CreateWhiteIcon($oGraph, $sIconPath); 137 if ($sTempImageName != null) 138 { 139 $oPdf->Image($sTempImageName, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale, 'PNG'); 140 } 141 $Alpha = 0.4; 142 $oPdf->setAlpha($Alpha); 143 } 144 145 $oPdf->Image($sIconPath, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale); 146 147 $aContextRootCauses = $this->GetProperty('context_root_causes'); 148 if (!is_null($aContextRootCauses)) 149 { 150 $idx = 0; 151 foreach($aContextRootCauses as $key => $aObjects) 152 { 153 $sgn = 2*($idx %2) -1; 154 $coef = floor((1+$idx)/2) * $sgn; 155 $alpha = $coef*pi()/4 - pi()/2; 156 $x = $this->x * $fScale + cos($alpha) * 16*1.25 * $fScale; 157 $y = $this->y * $fScale + sin($alpha) * 16*1.25 * $fScale; 158 $l = 32 * $fScale / 3; 159 $sIconPath = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$aContextDefs[$key]['icon']; 160 $oPdf->Image($sIconPath, $x - $l/2, $y - $l/2, $l, $l); 161 $idx++; 162 } 163 } 164 165 $oPdf->SetFont('dejavusans', '', 24 * $fScale, '', true); 166 $width = $oPdf->GetStringWidth($this->GetProperty('label')); 167 $height = $oPdf->GetStringHeight(1000, $this->GetProperty('label')); 168 $oPdf->setAlpha(0.6 * $Alpha); 169 $oPdf->SetFillColor(255, 255, 255); 170 $oPdf->SetDrawColor(255, 255, 255); 171 $oPdf->Rect($this->x*$fScale - $width/2, ($this->y + 18)*$fScale, $width, $height, 'DF'); 172 $oPdf->setAlpha($Alpha); 173 $oPdf->SetTextColor(0, 0, 0); 174 $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 18)*$fScale, $this->GetProperty('label')); 175 } 176 177 /** 178 * Create a "whitened" version of the icon (retaining the transparency) to be used a background for masking the underlying lines 179 * @param string $sIconFile The path to the file containing the icon 180 * @return NULL|string The path to a temporary file containing the white version of the icon 181 */ 182 protected function CreateWhiteIcon(DisplayableGraph $oGraph, $sIconFile) 183 { 184 $aInfo = getimagesize($sIconFile); 185 186 $im = null; 187 switch($aInfo['mime']) 188 { 189 case 'image/png': 190 if (function_exists('imagecreatefrompng')) 191 { 192 $im = imagecreatefrompng($sIconFile); 193 } 194 break; 195 196 case 'image/gif': 197 if (function_exists('imagecreatefromgif')) 198 { 199 $im = imagecreatefromgif($sIconFile); 200 } 201 break; 202 203 case 'image/jpeg': 204 case 'image/jpg': 205 if (function_exists('imagecreatefromjpeg')) 206 { 207 $im = imagecreatefromjpeg($sIconFile); 208 } 209 break; 210 211 default: 212 return null; 213 214 } 215 if($im && imagefilter($im, IMG_FILTER_COLORIZE, 255, 255, 255)) 216 { 217 $sTempImageName = $oGraph->GetTempImageName(); 218 imagesavealpha($im, true); 219 imagepng($im, $sTempImageName); 220 imagedestroy($im); 221 return $sTempImageName; 222 } 223 else 224 { 225 return null; 226 } 227 } 228 229 public function GetObjectCount() 230 { 231 return 1; 232 } 233 234 public function GetObjectClass() 235 { 236 return is_object($this->GetProperty('object', null)) ? get_class($this->GetProperty('object', null)) : null; 237 } 238 239 protected function AddToStats($oNode, &$aNodesPerClass) 240 { 241 $sClass = $oNode->GetObjectClass(); 242 if (!array_key_exists($sClass, $aNodesPerClass)) 243 { 244 $aNodesPerClass[$sClass] = array( 245 'reached' => array( 246 'count' => 0, 247 'nodes' => array(), 248 'icon_url' => $oNode->GetProperty('icon_url'), 249 ), 250 'not_reached' => array( 251 'count' => 0, 252 'nodes' => array(), 253 'icon_url' => $oNode->GetProperty('icon_url'), 254 ) 255 ); 256 } 257 $sKey = $oNode->GetProperty('is_reached') ? 'reached' : 'not_reached'; 258 if (!array_key_exists($oNode->GetId(), $aNodesPerClass[$sClass][$sKey]['nodes'])) 259 { 260 $aNodesPerClass[$sClass][$sKey]['nodes'][$oNode->GetId()] = $oNode; 261 $aNodesPerClass[$sClass][$sKey]['count'] += $oNode->GetObjectCount(); 262 } 263 } 264 265 /** 266 * Retrieves the list of neighbour nodes, in the given direction: 'up' or 'down' 267 * @param bool $bDirectionDown 268 * @return multitype:NULL 269 */ 270 protected function GetNextNodes($bDirectionDown = true) 271 { 272 $aNextNodes = array(); 273 if ($bDirectionDown) 274 { 275 foreach($this->GetOutgoingEdges() as $oEdge) 276 { 277 $aNextNodes[] = $oEdge->GetSinkNode(); 278 } 279 } 280 else 281 { 282 foreach($this->GetIncomingEdges() as $oEdge) 283 { 284 $aNextNodes[] = $oEdge->GetSourceNode(); 285 } 286 } 287 return $aNextNodes; 288 } 289 290 /** 291 * Replaces the next neighbour node (in the given direction: 'up' or 'down') by the supplied group node 292 * preserving the connectivity of the graph 293 * @param DisplayableGraph $oGraph 294 * @param DisplayableNode $oNextNode 295 * @param DisplayableGroupNode $oNewNode 296 * @param bool $bDirectionDown 297 */ 298 protected function ReplaceNextNodeBy(DisplayableGraph $oGraph, DisplayableNode $oNextNode, DisplayableGroupNode $oNewNode, $bDirectionDown = true) 299 { 300 $sClass = $oNewNode->GetProperty('class'); 301 if ($bDirectionDown) 302 { 303 foreach($oNextNode->GetIncomingEdges() as $oEdge) 304 { 305 if ($oEdge->GetSourceNode()->GetId() !== $this->GetId()) 306 { 307 try 308 { 309 $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode); 310 } 311 catch(Exception $e) 312 { 313 // ignore this edge 314 } 315 } 316 } 317 foreach($oNextNode->GetOutgoingEdges() as $oEdge) 318 { 319 try 320 { 321 $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oNewNode, $oEdge->GetSinkNode()); 322 } 323 catch(Exception $e) 324 { 325 // ignore this edge 326 } 327 } 328 } 329 else 330 { 331 foreach($oNextNode->GetOutgoingEdges() as $oEdge) 332 { 333 if ($oEdge->GetSinkNode()->GetId() !== $this->GetId()) 334 { 335 try 336 { 337 $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oNewNode, $oEdge->GetSinkNode()); 338 } 339 catch(Exception $e) 340 { 341 // ignore this edge 342 } 343 } 344 } 345 foreach($oNextNode->GetIncomingEdges() as $oEdge) 346 { 347 try 348 { 349 $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode); 350 } 351 catch(Exception $e) 352 { 353 // ignore this edge 354 } 355 } 356 } 357 358 if ($oGraph->GetNode($oNextNode->GetId())) 359 { 360 $oGraph->_RemoveNode($oNextNode); 361 if ($oNextNode instanceof DisplayableGroupNode) 362 { 363 // Copy all the objects of the previous group into the new group 364 foreach($oNextNode->GetObjects() as $oObj) 365 { 366 $oNewNode->AddObject($oObj); 367 } 368 } 369 else 370 { 371 $oNewNode->AddObject($oNextNode->GetProperty('object')); 372 } 373 } 374 } 375 376 /** 377 * Group together (as a special kind of nodes) all the similar neighbours of the current node 378 * @param DisplayableGraph $oGraph 379 * @param int $iThresholdCount 380 * @param boolean $bDirectionUp 381 * @param boolean $bDirectionDown 382 */ 383 public function GroupSimilarNeighbours(DisplayableGraph $oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true) 384 { 385 if ($this->GetProperty('grouped') === true) return; 386 $this->SetProperty('grouped', true); 387 388 $aNodesPerClass = array(); 389 foreach($this->GetNextNodes($bDirectionDown) as $oNode) 390 { 391 $sClass = $oNode->GetObjectClass(); 392 if ($sClass !== null) 393 { 394 $this->AddToStats($oNode, $aNodesPerClass); 395 } 396 else 397 { 398 $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); 399 } 400 } 401 foreach($aNodesPerClass as $sClass => $aDefs) 402 { 403 foreach($aDefs as $sStatus => $aGroupProps) 404 { 405 if (count($aGroupProps['nodes']) >= $iThresholdCount) 406 { 407 $sNewId = $this->GetId().'::'.$sClass.'/'.(($sStatus == 'reached') ? '_reached': ''); 408 $oNewNode = $oGraph->GetNode($sNewId); 409 if ($oNewNode == null) 410 { 411 $oNewNode = new DisplayableGroupNode($oGraph, $sNewId); 412 $oNewNode->SetProperty('label', 'x'.$aGroupProps['count']); 413 $oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']); 414 $oNewNode->SetProperty('class', $sClass); 415 $oNewNode->SetProperty('is_reached', ($sStatus == 'reached')); 416 $oNewNode->SetProperty('count', $aGroupProps['count']); 417 } 418 419 try 420 { 421 if ($bDirectionDown) 422 { 423 $oIncomingEdge = new DisplayableEdge($oGraph, $this->GetId().'-'.$oNewNode->GetId(), $this, $oNewNode); 424 } 425 else 426 { 427 $oOutgoingEdge = new DisplayableEdge($oGraph, $this->GetId().'-'.$oNewNode->GetId(), $oNewNode, $this); 428 } 429 } 430 catch(Exception $e) 431 { 432 // Ignore this redundant egde 433 } 434 435 foreach($aGroupProps['nodes'] as $oNextNode) 436 { 437 $this->ReplaceNextNodeBy($oGraph, $oNextNode, $oNewNode, $bDirectionDown); 438 } 439 $oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); 440 } 441 else 442 { 443 foreach($aGroupProps['nodes'] as $oNode) 444 { 445 $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); 446 } 447 } 448 } 449 } 450 } 451 452 public function GetTooltip($aContextDefs) 453 { 454 $sHtml = ''; 455 $oCurrObj = $this->GetProperty('object'); 456 $sSubClass = get_class($oCurrObj); 457 $sHtml .= $oCurrObj->GetHyperlink()."<hr/>"; 458 $aContextRootCauses = $this->GetProperty('context_root_causes'); 459 if (!is_null($aContextRootCauses)) 460 { 461 foreach($aContextRootCauses as $key => $aObjects) 462 { 463 $aContext = $aContextDefs[$key]; 464 $aRootCauses = array(); 465 foreach($aObjects as $oRootCause) 466 { 467 $aRootCauses[] = $oRootCause->GetHyperlink(); 468 } 469 $sHtml .= '<p><img style="max-height: 24px; vertical-align:bottom;" src="'.utils::GetAbsoluteUrlModulesRoot().$aContext['icon'].'" title="'.htmlentities(Dict::S($aContext['dict'])).'"> '.implode(', ', $aRootCauses).'</p>'; 470 } 471 $sHtml .= '<hr/>'; 472 } 473 $sHtml .= '<table><tbody>'; 474 foreach(MetaModel::GetZListItems($sSubClass, 'list') as $sAttCode) 475 { 476 $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); 477 $sHtml .= '<tr><td>'.$oAttDef->GetLabel().': </td><td>'.$oCurrObj->GetAsHtml($sAttCode).'</td></tr>'; 478 } 479 $sHtml .= '</tbody></table>'; 480 return $sHtml; 481 } 482 483 /** 484 * Get the description of the node in "dot" language 485 * Used to generate the positions in the graph, but we'd better use fake label 486 * just to retain the space used by the node, without compromising the parsing 487 * of the result which may occur when using the real labels (with possible weird characters in the middle) 488 */ 489 public function GetDotAttributes($bNoLabel = false) 490 { 491 $sDot = ''; 492 if ($bNoLabel) 493 { 494 // simulate a fake label with the approximate same size as the true label 495 $sLabel = str_repeat('x',strlen($this->GetProperty('label', $this->GetId()))); 496 $sDot = 'label="'.$sLabel.'"'; 497 } 498 else 499 { 500 // actual label 501 $sLabel = addslashes($this->GetProperty('label', $this->GetId())); 502 $sDot = 'label="'.$sLabel.'"'; 503 } 504 return $sDot; 505 } 506} 507 508class DisplayableRedundancyNode extends DisplayableNode 509{ 510 public function GetWidth() 511 { 512 return 24; 513 } 514 515 public function GetForRaphael($aContextDefs) 516 { 517 $aNode = array(); 518 $aNode['shape'] = 'disc'; 519 $aNode['icon_url'] = $this->GetIconURL(); 520 $aNode['source'] = ($this->GetProperty('source') == true); 521 $aNode['width'] = $this->GetWidth(); 522 $aNode['x'] = $this->x; 523 $aNode['y']= $this->y; 524 $aNode['label'] = $this->GetLabel(); 525 $aNode['id'] = $this->GetId(); 526 $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2); 527 $sColor = ($this->GetProperty('is_reached_count') > $this->GetProperty('threshold')) ? '#c33' : '#999'; 528 $aNode['disc_attr'] = array('stroke-width' => 2, 'stroke' => '#000', 'fill' => $sColor, 'opacity' => $fDiscOpacity); 529 $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4); 530 $aNode['text_attr'] = array('fill' => '#fff', 'opacity' => $fTextOpacity); 531 $aNode['tooltip'] = $this->GetTooltip($aContextDefs); 532 return $aNode; 533 } 534 535 public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) 536 { 537 $oPdf->SetAlpha(1); 538 if($this->GetProperty('is_reached_count') > $this->GetProperty('threshold')) 539 { 540 $oPdf->SetFillColor(200, 0, 0); 541 } 542 else 543 { 544 $oPdf->SetFillColor(144, 144, 144); 545 } 546 $oPdf->SetDrawColor(0, 0, 0); 547 $oPdf->Circle($this->x*$fScale, $this->y*$fScale, 16*$fScale, 0, 360, 'DF'); 548 549 $oPdf->SetTextColor(255, 255, 255); 550 $oPdf->SetFont('dejavusans', '', 28 * $fScale, '', true); 551 $sLabel = (string)$this->GetProperty('label'); 552 $width = $oPdf->GetStringWidth($sLabel, 'dejavusans', 'B', 24*$fScale); 553 $height = $oPdf->GetStringHeight(1000, $sLabel); 554 $xPos = (float)$this->x*$fScale - $width/2; 555 $yPos = (float)$this->y*$fScale - $height/2; 556 557 $oPdf->SetXY(($this->x - 16)*$fScale, ($this->y - 16)*$fScale); 558 559 $oPdf->Cell(32*$fScale, 32*$fScale, $sLabel, 0, 0, 'C', 0, '', 0, false, 'T', 'C'); 560 } 561 562 /** 563 * @see DisplayableNode::GroupSimilarNeighbours() 564 */ 565 public function GroupSimilarNeighbours(DisplayableGraph $oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true) 566 { 567 parent::GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); 568 569 if ($bDirectionUp) 570 { 571 $aNodesPerClass = array(); 572 foreach($this->GetIncomingEdges() as $oEdge) 573 { 574 $oNode = $oEdge->GetSourceNode(); 575 576 if (($oNode->GetObjectClass() !== null) && (!$oNode->GetProperty('is_reached'))) 577 { 578 $this->AddToStats($oNode, $aNodesPerClass); 579 } 580 else 581 { 582 //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); 583 } 584 } 585 foreach($aNodesPerClass as $sClass => $aDefs) 586 { 587 foreach($aDefs as $sStatus => $aGroupProps) 588 { 589 if (count($aGroupProps['nodes']) >= $iThresholdCount) 590 { 591 $oNewNode = new DisplayableGroupNode($oGraph, '-'.$this->GetId().'::'.$sClass.'/'.$sStatus); 592 $oNewNode->SetProperty('label', 'x'.count($aGroupProps['nodes'])); 593 $oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']); 594 $oNewNode->SetProperty('is_reached', ($sStatus == 'is_reached')); 595 $oNewNode->SetProperty('class', $sClass); 596 $oNewNode->SetProperty('count', count($aGroupProps['nodes'])); 597 598 599 $sNewId = $this->GetId().'::'.$sClass.'/'.(($sStatus == 'reached') ? '_reached': ''); 600 $oNewNode = $oGraph->GetNode($sNewId); 601 if ($oNewNode == null) 602 { 603 $oNewNode = new DisplayableGroupNode($oGraph, $sNewId); 604 $oNewNode->SetProperty('label', 'x'.$aGroupProps['count']); 605 $oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']); 606 $oNewNode->SetProperty('class', $sClass); 607 $oNewNode->SetProperty('is_reached', ($sStatus == 'reached')); 608 $oNewNode->SetProperty('count', $aGroupProps['count']); 609 } 610 611 try 612 { 613 $oOutgoingEdge = new DisplayableEdge($oGraph, '-'.$this->GetId().'-'.$oNewNode->GetId().'/'.$sStatus, $oNewNode, $this); 614 } 615 catch(Exception $e) 616 { 617 // Ignore this redundant egde 618 } 619 620 foreach($aGroupProps['nodes'] as $oNextNode) 621 { 622 $this->ReplaceNextNodeBy($oGraph, $oNextNode, $oNewNode, !$bDirectionUp); 623 } 624 //$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); 625 } 626 else 627 { 628 foreach($aGroupProps['nodes'] as $oNode) 629 { 630 //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown); 631 } 632 } 633 } 634 } 635 } 636 } 637 638 public function GetTooltip($aContextDefs) 639 { 640 $sHtml = ''; 641 $sHtml .= Dict::S('UI:RelationTooltip:Redundancy')."<hr>"; 642 $sHtml .= '<table><tbody>'; 643 $sHtml .= "<tr><td>".Dict::Format('UI:RelationTooltip:ImpactedItems_N_of_M' , $this->GetProperty('is_reached_count'), $this->GetProperty('min_up') + $this->GetProperty('threshold'))."</td></tr>"; 644 $sHtml .= "<tr><td>".Dict::Format('UI:RelationTooltip:CriticalThreshold_N_of_M' , $this->GetProperty('threshold'), $this->GetProperty('min_up') + $this->GetProperty('threshold'))."</td></tr>"; 645 $sHtml .= '</tbody></table>'; 646 return $sHtml; 647 } 648 649 650 public function GetObjectCount() 651 { 652 return 0; 653 } 654 655 public function GetObjectClass() 656 { 657 return null; 658 } 659} 660 661class DisplayableEdge extends GraphEdge 662{ 663 public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) 664 { 665 $oSourceNode = $this->GetSourceNode(); 666 if (($oSourceNode->x == null) || ($oSourceNode->y == null)) 667 { 668 return; 669 } 670 $xStart = $oSourceNode->x * $fScale; 671 $yStart = $oSourceNode->y * $fScale; 672 673 $oSinkNode = $this->GetSinkNode(); 674 if (($oSinkNode->x == null) || ($oSinkNode->y == null)) 675 { 676 return; 677 } 678 $xEnd = $oSinkNode->x * $fScale; 679 $yEnd = $oSinkNode->y * $fScale; 680 681 $bReached = ($this->GetSourceNode()->GetProperty('is_reached') && $this->GetSinkNode()->GetProperty('is_reached')); 682 683 $oPdf->setAlpha(1); 684 if ($bReached) 685 { 686 $aColor = array(100, 100, 100); 687 } 688 else 689 { 690 $aColor = array(200, 200, 200); 691 } 692 $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aColor)); 693 $oPdf->Line($xStart, $yStart, $xEnd, $yEnd); 694 695 696 $vx = $xEnd - $xStart; 697 $vy = $yEnd - $yStart; 698 $l = sqrt($vx*$vx + $vy*$vy); 699 $vx = $vx / $l; 700 $vy = $vy / $l; 701 $ux = -$vy; 702 $uy = $vx; 703 $lPos = max($l/2, $l - 40*$fScale); 704 $iArrowSize = 5*$fScale; 705 706 $x = $xStart + $lPos * $vx; 707 $y = $yStart + $lPos * $vy; 708 $oPdf->Line($x, $y, $x + $iArrowSize * ($ux-$vx), $y + $iArrowSize * ($uy-$vy)); 709 $oPdf->Line($x, $y, $x - $iArrowSize * ($ux+$vx), $y - $iArrowSize * ($uy+$vy)); 710 } 711} 712 713class DisplayableGroupNode extends DisplayableNode 714{ 715 protected $aObjects; 716 717 public function __construct(SimpleGraph $oGraph, $sId, $x = 0, $y = 0) 718 { 719 parent::__construct($oGraph, $sId, $x, $y); 720 $this->aObjects = array(); 721 } 722 723 public function AddObject(DBObject $oObj = null) 724 { 725 if (is_object($oObj)) 726 { 727 $sPrevClass = $this->GetObjectClass(); 728 if (($sPrevClass !== null) && (get_class($oObj) !== $sPrevClass)) 729 { 730 throw new Exception("Error: adding an object of class '".get_class($oObj)."' to a group of '$sPrevClass' objects."); 731 } 732 $this->aObjects[$oObj->GetKey()] = $oObj; 733 } 734 } 735 736 public function GetObjects() 737 { 738 return $this->aObjects; 739 } 740 741 public function GetWidth() 742 { 743 return 50; 744 } 745 746 public function GetForRaphael($aContextDefs) 747 { 748 $aNode = array(); 749 $aNode['shape'] = 'group'; 750 $aNode['icon_url'] = $this->GetIconURL(); 751 $aNode['source'] = ($this->GetProperty('source') == true); 752 $aNode['width'] = $this->GetWidth(); 753 $aNode['x'] = $this->x; 754 $aNode['y']= $this->y; 755 $aNode['label'] = $this->GetLabel(); 756 $aNode['id'] = $this->GetId(); 757 $aNode['group_index'] = $this->GetProperty('group_index'); // if supplied 758 $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2); 759 $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4); 760 $aNode['icon_attr'] = array('opacity' => $fTextOpacity); 761 $aNode['disc_attr'] = array('stroke-width' => 2, 'stroke' => '#000', 'fill' => '#fff', 'opacity' => $fDiscOpacity); 762 $aNode['text_attr'] = array('fill' => '#000', 'opacity' => $fTextOpacity); 763 $aNode['tooltip'] = $this->GetTooltip($aContextDefs); 764 return $aNode; 765 } 766 767 public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs) 768 { 769 $bReached = $this->GetProperty('is_reached'); 770 $oPdf->SetFillColor(255, 255, 255); 771 if ($bReached) 772 { 773 $aBorderColor = array(100, 100, 100); 774 } 775 else 776 { 777 $aBorderColor = array(200, 200, 200); 778 } 779 $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aBorderColor)); 780 781 $sIconUrl = $this->GetProperty('icon_url'); 782 $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl); 783 $oPdf->SetAlpha(1); 784 $oPdf->Circle($this->x*$fScale, $this->y*$fScale, $this->GetWidth() / 2 * $fScale, 0, 360, 'DF'); 785 786 if ($bReached) 787 { 788 $oPdf->SetAlpha(1); 789 } 790 else 791 { 792 $oPdf->SetAlpha(0.4); 793 } 794 $oPdf->Image($sIconPath, ($this->x - 17)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale); 795 $oPdf->Image($sIconPath, ($this->x + 1)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale); 796 $oPdf->Image($sIconPath, ($this->x -8)*$fScale, ($this->y +1)*$fScale, 16*$fScale, 16*$fScale); 797 $oPdf->SetFont('dejavusans', '', 24 * $fScale, '', true); 798 $width = $oPdf->GetStringWidth($this->GetProperty('label')); 799 $oPdf->SetTextColor(0, 0, 0); 800 $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 25)*$fScale, $this->GetProperty('label')); 801 } 802 803 public function GetTooltip($aContextDefs) 804 { 805 $sHtml = ''; 806 $iGroupIdx = $this->GetProperty('group_index'); 807 $sHtml .= '<a href="#" onclick="$(\'.itop-simple-graph\').simple_graph(\'show_group\', \'relation_group_'.$iGroupIdx.'\');">'.Dict::Format('UI:RelationGroupNumber_N', (1+$iGroupIdx))."</a>"; 808 $sHtml .= '<hr/>'; 809 $sHtml .= '<table><tbody><tr>'; 810 $sHtml .= '<td style="vertical-align:top;padding-right: 0.5em;"><img src="'.$this->GetProperty('icon_url').'"></td><td style="vertical-align:top">'.MetaModel::GetName($this->GetObjectClass()).'<br/>'; 811 $sHtml .= Dict::Format('UI_CountOfObjectsShort', $this->GetObjectCount()).'</td>'; 812 $sHtml .= '</tr></tbody></table>'; 813 return $sHtml; 814 } 815 816 public function GetObjectCount() 817 { 818 return count($this->aObjects); 819 } 820 821 public function GetObjectClass() 822 { 823 return ($this->GetObjectCount() > 0) ? get_class(reset($this->aObjects)) : null; 824 } 825} 826 827/** 828 * A Graph that can be displayed interactively using Raphael JS or saved as a PDF document 829 */ 830class DisplayableGraph extends SimpleGraph 831{ 832 protected $bDirectionDown; 833 protected $aTempImages; 834 protected $aSourceObjects; 835 protected $aSinkObjects; 836 837 public function __construct() 838 { 839 parent::__construct(); 840 $this->aTempImages = array(); 841 $this->aSourceObjects = array(); 842 $this->aSinkObjects = array(); 843 } 844 845 public function GetTempImageName() 846 { 847 $sNewTempName = tempnam(APPROOT.'data', 'img-'); 848 $this->aTempImages[] = $sNewTempName; 849 return $sNewTempName; 850 } 851 852 public function __destruct() 853 { 854 foreach($this->aTempImages as $sTempFile) 855 { 856 @unlink($sTempFile); 857 } 858 } 859 860 /** 861 * Build a DisplayableGraph from a RelationGraph 862 * @param RelationGraph $oGraph 863 * @param number $iGroupingThreshold 864 * @param string $bDirectionDown 865 * @return DisplayableGraph 866 */ 867 public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true) 868 { 869 $oNewGraph = new DisplayableGraph(); 870 $oNewGraph->bDirectionDown = $bDirectionDown; 871 $iPreviousTimeLimit = ini_get('max_execution_time'); 872 $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); 873 874 $oNodesIter = new RelationTypeIterator($oGraph, 'Node'); 875 foreach($oNodesIter as $oNode) 876 { 877 set_time_limit($iLoopTimeLimit); 878 switch(get_class($oNode)) 879 { 880 case 'RelationObjectNode': 881 $oNewNode = new DisplayableNode($oNewGraph, $oNode->GetId(), 0, 0); 882 883 $oObj = $oNode->GetProperty('object'); 884 $sClass = get_class($oObj); 885 if ($oNode->GetProperty('source')) 886 { 887 if (!array_key_exists($sClass, $oNewGraph->aSourceObjects)) 888 { 889 $oNewGraph->aSourceObjects[$sClass] = array(); 890 } 891 $oNewGraph->aSourceObjects[$sClass][] = $oObj->GetKey(); 892 $oNewNode->SetProperty('source', true); 893 } 894 if ($oNode->GetProperty('sink')) 895 { 896 if (!array_key_exists($sClass, $oNewGraph->aSinkObjects)) 897 { 898 $oNewGraph->aSinkObjects[$sClass] = array(); 899 } 900 $oNewGraph->aSinkObjects[$sClass][] = $oObj->GetKey(); 901 $oNewNode->SetProperty('sink', true); 902 } 903 $oNewNode->SetProperty('object', $oObj); 904 $oNewNode->SetProperty('icon_url', $oObj->GetIcon(false)); 905 $oNewNode->SetProperty('label', $oObj->GetRawName()); 906 $oNewNode->SetProperty('is_reached', $bDirectionDown ? $oNode->GetProperty('is_reached') : true); // When going "up" is_reached does not matter 907 $oNewNode->SetProperty('is_reached_allowed', $oNode->GetProperty('is_reached_allowed')); 908 $oNewNode->SetProperty('context_root_causes', $oNode->GetProperty('context_root_causes')); 909 break; 910 911 default: 912 $oNewNode = new DisplayableRedundancyNode($oNewGraph, $oNode->GetId(), 0, 0); 913 $iNbReached = (is_null($oNode->GetProperty('is_reached_count'))) ? 0 : $oNode->GetProperty('is_reached_count'); 914 $oNewNode->SetProperty('label', $iNbReached."/".($oNode->GetProperty('min_up') + $oNode->GetProperty('threshold'))); 915 $oNewNode->SetProperty('min_up', $oNode->GetProperty('min_up')); 916 $oNewNode->SetProperty('threshold', $oNode->GetProperty('threshold')); 917 $oNewNode->SetProperty('is_reached_count', $iNbReached); 918 $oNewNode->SetProperty('is_reached', true); 919 } 920 } 921 $oEdgesIter = new RelationTypeIterator($oGraph, 'Edge'); 922 foreach($oEdgesIter as $oEdge) 923 { 924 set_time_limit($iLoopTimeLimit); 925 $oSourceNode = $oNewGraph->GetNode($oEdge->GetSourceNode()->GetId()); 926 $oSinkNode = $oNewGraph->GetNode($oEdge->GetSinkNode()->GetId()); 927 $oNewEdge = new DisplayableEdge($oNewGraph, $oEdge->GetId(), $oSourceNode, $oSinkNode); 928 } 929 930 // Remove duplicate edges between two nodes 931 $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge'); 932 $aEdgeKeys = array(); 933 foreach($oEdgesIter as $oEdge) 934 { 935 set_time_limit($iLoopTimeLimit); 936 $sSourceId = $oEdge->GetSourceNode()->GetId(); 937 $sSinkId = $oEdge->GetSinkNode()->GetId(); 938 if ($sSourceId == $sSinkId) 939 { 940 // Remove self referring edges 941 $oNewGraph->_RemoveEdge($oEdge); 942 } 943 else 944 { 945 $sKey = $sSourceId.'//'.$sSinkId; 946 if (array_key_exists($sKey, $aEdgeKeys)) 947 { 948 // Remove duplicate edges 949 $oNewGraph->_RemoveEdge($oEdge); 950 } 951 else 952 { 953 $aEdgeKeys[$sKey] = true; 954 } 955 } 956 } 957 958 $oNodesIter = new RelationTypeIterator($oNewGraph, 'Node'); 959 foreach($oNodesIter as $oNode) 960 { 961 set_time_limit($iLoopTimeLimit); 962 if ($bDirectionDown && $oNode->GetProperty('source')) 963 { 964 $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, $bDirectionDown); 965 } 966 else if (!$bDirectionDown && $oNode->GetProperty('sink')) 967 { 968 $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, $bDirectionDown); 969 } 970 } 971 // Groups numbering 972 $oIterator = new RelationTypeIterator($oNewGraph, 'Node'); 973 $iGroupIdx = 0; 974 foreach($oIterator as $oNode) 975 { 976 set_time_limit($iLoopTimeLimit); 977 if ($oNode instanceof DisplayableGroupNode) 978 { 979 if ($oNode->GetObjectCount() == 0) 980 { 981 // Remove empty groups 982 $oNewGraph->_RemoveNode($oNode); 983 } 984 else 985 { 986 $aGroups[] = $oNode->GetObjects(); 987 $oNode->SetProperty('group_index', $iGroupIdx); 988 $iGroupIdx++; 989 } 990 } 991 } 992 993 // Remove duplicate edges between two nodes 994 $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge'); 995 $aEdgeKeys = array(); 996 foreach($oEdgesIter as $oEdge) 997 { 998 set_time_limit($iLoopTimeLimit); 999 $sSourceId = $oEdge->GetSourceNode()->GetId(); 1000 $sSinkId = $oEdge->GetSinkNode()->GetId(); 1001 if ($sSourceId == $sSinkId) 1002 { 1003 // Remove self referring edges 1004 $oNewGraph->_RemoveEdge($oEdge); 1005 } 1006 else 1007 { 1008 $sKey = $sSourceId.'//'.$sSinkId; 1009 if (array_key_exists($sKey, $aEdgeKeys)) 1010 { 1011 // Remove duplicate edges 1012 $oNewGraph->_RemoveEdge($oEdge); 1013 } 1014 else 1015 { 1016 $aEdgeKeys[$sKey] = true; 1017 } 1018 } 1019 } 1020 set_time_limit($iPreviousTimeLimit); 1021 1022 return $oNewGraph; 1023 } 1024 1025 /** 1026 * Initializes the positions by rendering using Graphviz in xdot format 1027 * and parsing the output. 1028 * @throws Exception 1029 */ 1030 public function InitFromGraphviz() 1031 { 1032 $sDot = $this->DumpAsXDot(); 1033 if (strpos($sDot, 'digraph') === false) 1034 { 1035 throw new Exception($sDot); 1036 } 1037 1038 $aChunks = explode(";", $sDot); 1039 foreach($aChunks as $sChunk) 1040 { 1041 if(preg_match('/"([^"]+)".+pos="([0-9\\.]+),([0-9\\.]+)"/ms', $sChunk, $aMatches)) 1042 { 1043 $sId = $aMatches[1]; 1044 $xPos = $aMatches[2]; 1045 $yPos = $aMatches[3]; 1046 1047 $oNode = $this->GetNode($sId); 1048 if ($oNode !== null) 1049 { 1050 $oNode->x = (float)$xPos; 1051 $oNode->y = (float)$yPos; 1052 } 1053 else 1054 { 1055 IssueLog::Warning("??? Position of the non-existing node '$sId', x=$xPos, y=$yPos"); 1056 } 1057 } 1058 } 1059 } 1060 1061 public function GetBoundingBox() 1062 { 1063 $xMin = null; 1064 $xMax = null; 1065 $yMin = null; 1066 $yMax = null; 1067 $oIterator = new RelationTypeIterator($this, 'Node'); 1068 foreach($oIterator as $sId => $oNode) 1069 { 1070 if ($xMin === null) // First element in the loop 1071 { 1072 $xMin = $oNode->x - $oNode->GetWidth(); 1073 $xMax = $oNode->x + $oNode->GetWidth(); 1074 $yMin = $oNode->y - $oNode->GetHeight(); 1075 $yMax = $oNode->y + $oNode->GetHeight(); 1076 } 1077 else 1078 { 1079 $xMin = min($xMin, $oNode->x - $oNode->GetWidth() / 2); 1080 $xMax = max($xMax, $oNode->x + $oNode->GetWidth() / 2); 1081 $yMin = min($yMin, $oNode->y - $oNode->GetHeight() / 2); 1082 $yMax = max($yMax, $oNode->y + $oNode->GetHeight() / 2); 1083 } 1084 } 1085 1086 return array('xmin' => $xMin, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax); 1087 } 1088 1089 function Translate($dx, $dy) 1090 { 1091 $oIterator = new RelationTypeIterator($this, 'Node'); 1092 foreach($oIterator as $sId => $oNode) 1093 { 1094 $oNode->x += $dx; 1095 $oNode->y += $dy; 1096 } 1097 } 1098 1099 public function UpdatePositions($aPositions) 1100 { 1101 foreach($aPositions as $sNodeId => $aPos) 1102 { 1103 $oNode = $this->GetNode($sNodeId); 1104 if ($oNode != null) 1105 { 1106 $oNode->x = $aPos['x']; 1107 $oNode->y = $aPos['y']; 1108 } 1109 } 1110 } 1111 1112 /** 1113 * Renders as JSON string suitable for loading into the simple_graph widget 1114 */ 1115 function GetAsJSON($sContextKey) 1116 { 1117 $aContextDefs = static::GetContextDefinitions($sContextKey, false); 1118 1119 $aData = array('nodes' => array(), 'edges' => array(), 'groups' => array(), 'lists' => array()); 1120 $iGroupIdx = 0; 1121 $oIterator = new RelationTypeIterator($this, 'Node'); 1122 foreach($oIterator as $sId => $oNode) 1123 { 1124 if ($oNode instanceof DisplayableGroupNode) 1125 { 1126 // The contents of the "Groups" tab will be rendered 1127 // using a separate ajax call, since the content of 1128 // the page is made of a mix of HTML / CSS / JS which 1129 // cannot be conveyed easily in the JSON structure 1130 // So we just pass a list of groups, each being defined by a class and a list of keys 1131 // in order to avoid redoing the impact computation which is expensive 1132 $aObjects = $oNode->GetObjects(); 1133 $aKeys = array(); 1134 foreach($aObjects as $oObj) 1135 { 1136 $sClass = get_class($oObj); 1137 $aKeys[] = $oObj->GetKey(); 1138 } 1139 $aData['groups'][$iGroupIdx] = array('class' => $sClass, 'keys' => $aKeys); 1140 $oNode->SetProperty('group_index', $iGroupIdx); 1141 $iGroupIdx++; 1142 1143 if ($oNode->GetProperty('is_reached')) 1144 { 1145 // Also add the objects from this group into the 'list' tab 1146 if (!array_key_exists($sClass, $aData['lists'])) 1147 { 1148 $aData['lists'][$sClass] = $aKeys; 1149 } 1150 else 1151 { 1152 $aData['lists'][$sClass] = array_merge($aData['lists'][$sClass], $aKeys); 1153 } 1154 1155 } 1156 } 1157 if (($oNode instanceof DisplayableNode) && $oNode->GetProperty('is_reached') && is_object($oNode->GetProperty('object'))) 1158 { 1159 $sObjClass = get_class($oNode->GetProperty('object')); 1160 if (!array_key_exists($sObjClass, $aData['lists'])) 1161 { 1162 $aData['lists'][$sObjClass] = array(); 1163 } 1164 $aData['lists'][$sObjClass][] = $oNode->GetProperty('object')->GetKey(); 1165 } 1166 $aData['nodes'][] = $oNode->GetForRaphael($aContextDefs); 1167 } 1168 1169 uksort($aData['lists'], array(get_class($this), 'SortOnClassLabel')); // sort on the localized names of the classes to provide a consistent and stable order 1170 1171 $oIterator = new RelationTypeIterator($this, 'Edge'); 1172 foreach($oIterator as $sId => $oEdge) 1173 { 1174 $aEdge = array(); 1175 $aEdge['id'] = $oEdge->GetId(); 1176 $aEdge['source_node_id'] = $oEdge->GetSourceNode()->GetId(); 1177 $aEdge['sink_node_id'] = $oEdge->GetSinkNode()->GetId(); 1178 $fOpacity = ($oEdge->GetSinkNode()->GetProperty('is_reached') && $oEdge->GetSourceNode()->GetProperty('is_reached') ? 1 : 0.2); 1179 $aEdge['attr'] = array('opacity' => $fOpacity, 'stroke' => '#000'); 1180 $aData['edges'][] = $aEdge; 1181 } 1182 1183 return json_encode($aData); 1184 } 1185 1186 /** 1187 * Sort class "codes" based on their localized name 1188 * @param string $sClass1 1189 * @param string $sClass2 1190 * @return number -1, 0 or 1 1191 */ 1192 public static function SortOnClassLabel($sClass1, $sClass2) 1193 { 1194 return strcasecmp(MetaModel::GetName($sClass1), MetaModel::GetName($sClass2)); 1195 } 1196 1197 /** 1198 * Renders the graph in a PDF document: centered in the current page 1199 * @param PDFPage $oPage The PDFPage representing the PDF document to draw into 1200 * @param string $sComments An optional comment to display next to the graph (HTML entities will be escaped, \n replaced by <br/>) 1201 * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down 1202 * @param float $xMin Left coordinate of the bounding box to display the graph 1203 * @param float $xMax Right coordinate of the bounding box to display the graph 1204 * @param float $yMin Top coordinate of the bounding box to display the graph 1205 * @param float $yMax Bottom coordinate of the bounding box to display the graph 1206 */ 1207 function RenderAsPDF(PDFPage $oPage, $sComments = '', $sContextKey, $xMin = -1, $xMax = -1, $yMin = -1, $yMax = -1) 1208 { 1209 $aContextDefs = static::GetContextDefinitions($sContextKey, false); // No need to develop the parameters 1210 $oPdf = $oPage->get_tcpdf(); 1211 1212 $aBB = $this->GetBoundingBox(); 1213 $this->Translate(-$aBB['xmin'], -$aBB['ymin']); 1214 1215 $aMargins = $oPdf->getMargins(); 1216 1217 if ($xMin == -1) 1218 { 1219 $xMin = $aMargins['left']; 1220 } 1221 if ($xMax == -1) 1222 { 1223 $xMax = $oPdf->getPageWidth() - $aMargins['right']; 1224 } 1225 if ($yMin == -1) 1226 { 1227 $yMin = $aMargins['top']; 1228 } 1229 if ($yMax == -1) 1230 { 1231 $yMax = $oPdf->getPageHeight() - $aMargins['bottom']; 1232 } 1233 1234 $fBreakMargin = $oPdf->getBreakMargin(); 1235 $oPdf->SetAutoPageBreak(false); 1236 $aRemainingArea = $this->RenderKey($oPdf, $sComments, $xMin, $yMin, $xMax, $yMax, $aContextDefs); 1237 $xMin = $aRemainingArea['xmin']; 1238 $xMax = $aRemainingArea['xmax']; 1239 $yMin = $aRemainingArea['ymin']; 1240 $yMax = $aRemainingArea['ymax']; 1241 1242 //$oPdf->Rect($xMin, $yMin, $xMax - $xMin, $yMax - $yMin, 'D', array(), array(225, 50, 50)); 1243 1244 $fPageW = $xMax - $xMin; 1245 $fPageH = $yMax - $yMin; 1246 1247 $w = $aBB['xmax'] - $aBB['xmin']; 1248 $h = $aBB['ymax'] - $aBB['ymin'] + 10; // Extra space for the labels which may appear "below" the icons 1249 1250 $fScale = min($fPageW / $w, $fPageH / $h); 1251 $dx = ($fPageW - $fScale * $w) / 2; 1252 $dy = ($fPageH - $fScale * $h) / 2; 1253 1254 $this->Translate(($xMin + $dx)/$fScale, ($yMin + $dy)/$fScale); 1255 1256 $oIterator = new RelationTypeIterator($this, 'Edge'); 1257 $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); 1258 foreach($oIterator as $sId => $oEdge) 1259 { 1260 set_time_limit($iLoopTimeLimit); 1261 $oEdge->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs); 1262 } 1263 1264 $oIterator = new RelationTypeIterator($this, 'Node'); 1265 foreach($oIterator as $sId => $oNode) 1266 { 1267 set_time_limit($iLoopTimeLimit); 1268 $oNode->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs); 1269 } 1270 1271 $oPdf->SetAutoPageBreak(true, $fBreakMargin); 1272 $oPdf->SetAlpha(1); 1273 $oPdf->SetTextColor(0, 0, 0); 1274 } 1275 1276 /** 1277 * Renders (in PDF) the key (legend) of the graphics vertically to the left of the specified zone (xmin,ymin, xmax,ymax), 1278 * and the comment (if any) at the bottom of the page. Returns the position of remaining area. 1279 * @param TCPDF $oPdf 1280 * @param string $sComments 1281 * @param float $xMin 1282 * @param float $yMin 1283 * @param float $xMax 1284 * @param float $yMax 1285 * @param hash $aContextDefs 1286 * @return hash An array ('xmin' => , 'xmax' => ,'ymin' => , 'ymax' => ) of the remaining available area to paint the graph 1287 */ 1288 protected function RenderKey(TCPDF $oPdf, $sComments, $xMin, $yMin, $xMax, $yMax, $aContextDefs) 1289 { 1290 $fFontSize = 7; // in mm 1291 $fIconSize = 6; // in mm 1292 $fPadding = 1; // in mm 1293 $oIterator = new RelationTypeIterator($this, 'Node'); 1294 $fMaxWidth = max($oPdf->GetStringWidth(Dict::S('UI:Relation:Key')) - $fIconSize, $oPdf->GetStringWidth(Dict::S('UI:Relation:Comments')) - $fIconSize); 1295 $aClasses = array(); 1296 $aIcons = array(); 1297 $aContexts = array(); 1298 $aContextIcons = array(); 1299 $oPdf->SetFont('dejavusans', '', $fFontSize, '', true); 1300 foreach($oIterator as $sId => $oNode) 1301 { 1302 if ($sClass = $oNode->GetObjectClass()) 1303 { 1304 if (!array_key_exists($sClass, $aClasses)) 1305 { 1306 $sClassLabel = MetaModel::GetName($sClass); 1307 $width = $oPdf->GetStringWidth($sClassLabel); 1308 $fMaxWidth = max($width, $fMaxWidth); 1309 $aClasses[$sClass] = $sClassLabel; 1310 $sIconUrl = $oNode->GetProperty('icon_url'); 1311 $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl); 1312 $aIcons[$sClass] = $sIconPath; 1313 } 1314 } 1315 $aContextRootCauses = $oNode->GetProperty('context_root_causes'); 1316 if (!is_null($aContextRootCauses)) 1317 { 1318 foreach($aContextRootCauses as $key => $aObjects) 1319 { 1320 $aContexts[$key] = Dict::S($aContextDefs[$key]['dict']); 1321 $aContextIcons[$key] = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$aContextDefs[$key]['icon']; 1322 } 1323 } 1324 } 1325 $oPdf->SetXY($xMin + $fPadding, $yMin + $fPadding); 1326 $yPos = $yMin + $fPadding; 1327 $oPdf->SetFillColor(225, 225, 225); 1328 $oPdf->Cell($fIconSize + $fPadding + $fMaxWidth, $fIconSize + $fPadding, Dict::S('UI:Relation:Key'), 0 /* border */, 1 /* ln */, 'C', true /* fill */); 1329 $yPos += $fIconSize + 2*$fPadding; 1330 foreach($aClasses as $sClass => $sLabel) 1331 { 1332 $oPdf->SetX($xMin + $fIconSize + $fPadding); 1333 $oPdf->Cell(0, $fIconSize + 2*$fPadding, $sLabel, 0 /* border */, 1 /* ln */); 1334 $oPdf->Image($aIcons[$sClass], $xMin+1, $yPos, $fIconSize, $fIconSize); 1335 $yPos += $fIconSize + 2*$fPadding; 1336 } 1337 foreach($aContexts as $key => $sLabel) 1338 { 1339 $oPdf->SetX($xMin + $fIconSize + $fPadding); 1340 $oPdf->Cell(0, $fIconSize + 2*$fPadding, $sLabel, 0 /* border */, 1 /* ln */); 1341 $oPdf->Image($aContextIcons[$key], $xMin+1+$fIconSize*0.125, $yPos+$fIconSize*0.125, $fIconSize*0.75, $fIconSize*0.75); 1342 $yPos += $fIconSize + 2*$fPadding; 1343 } 1344 $oPdf->Rect($xMin, $yMin, $fMaxWidth + $fIconSize + 3*$fPadding, $yMax - $yMin, 'D'); 1345 1346 if ($sComments != '') 1347 { 1348 // Draw the comment text (surrounded by a rectangle) 1349 $xPos = $xMin + $fMaxWidth + $fIconSize + 4*$fPadding; 1350 $w = $xMax - $xPos - 2*$fPadding; 1351 $iNbLines = 1; 1352 $sText = '<p>'.str_replace("\n", '<br/>', htmlentities($sComments, ENT_QUOTES, 'UTF-8'), $iNbLines).'</p>'; 1353 $fLineHeight = $oPdf->getStringHeight($w, $sText); 1354 $h = (1+$iNbLines) * $fLineHeight; 1355 $yPos = $yMax - 2*$fPadding - $h; 1356 $oPdf->writeHTMLCell($w, $h, $xPos + $fPadding, $yPos + $fPadding, $sText, 0 /* border */, 1 /* ln */); 1357 $oPdf->Rect($xPos, $yPos, $w + 2*$fPadding, $h + 2*$fPadding, 'D'); 1358 $yMax = $yPos - $fPadding; 1359 } 1360 1361 return array('xmin' => $xMin + $fMaxWidth + $fIconSize + 4*$fPadding, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax); 1362 } 1363 1364 /** 1365 * Get the context definitions from the parameters / configuration. The format of the "key" string is: 1366 * <module>/relation_context/<class>/<relation>/<direction> 1367 * The values will be retrieved for the given class and all its parents and merged together as a single array. 1368 * Entries with an invalid query are removed from the list. 1369 * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down 1370 * @param bool $bDevelopParams Whether or not to substitute the parameters inside the queries with the supplied "context params" 1371 * @param array $aContextParams Arguments for the queries (via ToArgs()) if $bDevelopParams == true 1372 * @return multitype:multitype:string 1373 */ 1374 public static function GetContextDefinitions($sContextKey, $bDevelopParams = true, $aContextParams = array()) 1375 { 1376 $aContextDefs = array(); 1377 $aLevels = explode('/', $sContextKey); 1378 if (count($aLevels) < 5) 1379 { 1380 IssueLog::Warning("GetContextDefinitions: invalid 'sContextKey' = '$sContextKey'. 5 levels of / are expected !"); 1381 } 1382 else 1383 { 1384 $sLeafClass = $aLevels[2]; 1385 1386 if (!MetaModel::IsValidClass($sLeafClass)) 1387 { 1388 IssueLog::Warning("GetContextDefinitions: invalid 'sLeafClass' = '$sLeafClass'. A valid class name is expected in 3rd position inside '$sContextKey' !"); 1389 } 1390 else 1391 { 1392 $aRelationContext = MetaModel::GetConfig()->GetModuleSetting($aLevels[0], $aLevels[1], array()); 1393 foreach(MetaModel::EnumParentClasses($sLeafClass, ENUM_PARENT_CLASSES_ALL) as $sClass) 1394 { 1395 if (isset($aRelationContext[$sClass][$aLevels[3]][$aLevels[4]]['items'])) 1396 { 1397 $aContextDefs = array_merge($aContextDefs, $aRelationContext[$sClass][$aLevels[3]][$aLevels[4]]['items']); 1398 } 1399 } 1400 1401 // Check if the queries are valid 1402 foreach($aContextDefs as $sKey => $sDefs) 1403 { 1404 $sOQL = $aContextDefs[$sKey]['oql']; 1405 try 1406 { 1407 // Expand the parameters. If anything goes wrong, then the query is considered as invalid and removed from the list 1408 $oSearch = DBObjectSearch::FromOQL($sOQL); 1409 $aContextDefs[$sKey]['oql'] = $oSearch->ToOQL($bDevelopParams, $aContextParams); 1410 } 1411 catch(Exception $e) 1412 { 1413 IssueLog::Warning('Invalid OQL query: '.$sOQL.' in the parameter '.$sContextKey); 1414 unset($aContextDefs[$sKey]); 1415 } 1416 } 1417 } 1418 } 1419 return $aContextDefs; 1420 } 1421 1422 /** 1423 * Display the graph inside the given page, with the "filter" drawer above it 1424 * @param WebPage $oP 1425 * @param hash $aResults 1426 * @param string $sRelation 1427 * @param ApplicationContext $oAppContext 1428 * @param array $aExcludedObjects 1429 */ 1430 function Display(WebPage $oP, $aResults, $sRelation, ApplicationContext $oAppContext, $aExcludedObjects = array(), $sObjClass = null, $iObjKey = null, $sContextKey, $aContextParams = array()) 1431 { 1432 $aContextDefs = static::GetContextDefinitions($sContextKey, true, $aContextParams); 1433 $aExcludedByClass = array(); 1434 foreach($aExcludedObjects as $oObj) 1435 { 1436 if (!array_key_exists(get_class($oObj), $aExcludedByClass)) 1437 { 1438 $aExcludedByClass[get_class($oObj)] = array(); 1439 } 1440 $aExcludedByClass[get_class($oObj)][] = $oObj->GetKey(); 1441 } 1442 $sSftShort = Dict::S('UI:ElementsDisplayed'); 1443 $sSearchToggle = Dict::S('UI:Search:Toggle'); 1444 $oP->add("<div class=\"not-printable\">\n"); 1445 $oP->add( 1446<<<EOF 1447 <div id="ds_flash" class="search_box"> 1448 <form id="dh_flash" class="search_form_handler closed"> 1449 <h2 class="sf_title"><span class="sft_long">$sSftShort</span><span class="sft_short">$sSftShort</span><span class="sft_toggler fa fa-caret-down pull-right" title="$sSearchToggle"></span></h2> 1450 <div id="dh_flash_criterion_outer" class="sf_criterion_area"><div class="sf_criterion_row"> 1451EOF 1452 ); 1453 1454 $oP->add_ready_script( 1455<<<EOF 1456 $("#dh_flash > .sf_title").click( function() { 1457 $("#dh_flash").toggleClass('closed'); 1458 }); 1459 $('#ReloadMovieBtn').button().button('disable'); 1460EOF 1461 ); 1462 $aSortedElements = array(); 1463 foreach($aResults as $sClassIdx => $aObjects) 1464 { 1465 foreach($aObjects as $oCurrObj) 1466 { 1467 $sSubClass = get_class($oCurrObj); 1468 $aSortedElements[$sSubClass] = MetaModel::GetName($sSubClass); 1469 } 1470 } 1471 1472 asort($aSortedElements); 1473 $idx = 0; 1474 foreach($aSortedElements as $sSubClass => $sClassName) 1475 { 1476 $oP->add("<span style=\"padding-right:2em; white-space:nowrap;\"><input type=\"checkbox\" id=\"exclude_$idx\" name=\"excluded[]\" value=\"$sSubClass\" checked onChange=\"$('#ReloadMovieBtn').button('enable')\"><label for=\"exclude_$idx\"> ".MetaModel::GetClassIcon($sSubClass)." $sClassName</label></span> "); 1477 $idx++; 1478 } 1479 $oP->add("<p style=\"text-align:right\"><button type=\"button\" id=\"ReloadMovieBtn\" onClick=\"DoReload()\">".Dict::S('UI:Button:Refresh')."</button></p>"); 1480 $oP->add("</div></div></form>"); 1481 $oP->add("</div>\n"); 1482 $oP->add("</div>\n"); // class="not-printable" 1483 1484 $aAdditionalContexts = array(); 1485 foreach($aContextDefs as $sKey => $aDefinition) 1486 { 1487 $aAdditionalContexts[] = array('key' => $sKey, 'label' => Dict::S($aDefinition['dict']), 'oql' => $aDefinition['oql'], 'default' => (array_key_exists('default', $aDefinition) && ($aDefinition['default'] == 'yes'))); 1488 } 1489 1490 $sDirection = utils::ReadParam('d', 'horizontal'); 1491 $iGroupingThreshold = utils::ReadParam('g', 5); 1492 1493 $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js'); 1494 $oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css'); 1495 $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js'); 1496 $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js'); 1497 try 1498 { 1499 $this->InitFromGraphviz(); 1500 $sExportAsPdfURL = ''; 1501 $sExportAsPdfURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_pdf&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up'); 1502 $oAppcontext = new ApplicationContext(); 1503 $sContext = $oAppContext->GetForLink(); 1504 $sDrillDownURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class=%1$s&id=%2$s&'.$sContext; 1505 $sExportAsDocumentURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_attachment&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up'); 1506 $sLoadFromURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_json&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up'); 1507 $sAttachmentExportTitle = ''; 1508 if (($sObjClass != null) && ($iObjKey != null)) 1509 { 1510 $oTargetObj = MetaModel::GetObject($sObjClass, $iObjKey, false); 1511 if ($oTargetObj) 1512 { 1513 $sAttachmentExportTitle = Dict::Format('UI:Relation:AttachmentExportOptions_Name', $oTargetObj->GetName()); 1514 } 1515 } 1516 1517 $sId = 'graph'; 1518 $sStyle = ''; 1519 if ($oP->IsPrintableVersion()) 1520 { 1521 // Optimize for printing on A4/Letter vertically 1522 $sStyle = 'margin-left:auto; margin-right:auto;'; 1523 $oP->add_ready_script("$('.simple-graph').width(18/2.54*96).resizable({ stop: function() { $(window).trigger('resized'); }});"); // Default width about 18 cm, since most browsers assume 96 dpi 1524 } 1525 $oP->add('<div id="'.$sId.'" class="simple-graph" style="'.$sStyle.'"></div>'); 1526 $aParams = array( 1527 'source_url' => $sLoadFromURL, 1528 'sources' => ($this->bDirectionDown ? $this->aSourceObjects : $this->aSinkObjects), 1529 'excluded' => $aExcludedByClass, 1530 'grouping_threshold' => $iGroupingThreshold, 1531 'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')), 1532 'export_as_attachment' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsAttachment'), 'obj_class' => $sObjClass, 'obj_key' => $iObjKey), 1533 'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')), 1534 'labels' => array( 1535 'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'), 1536 'export_as_attachment_title' => $sAttachmentExportTitle, 1537 'export' => Dict::S('UI:Button:Export'), 1538 'cancel' => Dict::S('UI:Button:Cancel'), 1539 'title' => Dict::S('UI:RelationOption:Title'), 1540 'untitled' => Dict::S('UI:RelationOption:Untitled'), 1541 'include_list' => Dict::S('UI:RelationOption:IncludeList'), 1542 'comments' => Dict::S('UI:RelationOption:Comments'), 1543 'grouping_threshold' => Dict::S('UI:RelationOption:GroupingThreshold'), 1544 'refresh' => Dict::S('UI:Button:Refresh'), 1545 'check_all' => Dict::S('UI:SearchValue:CheckAll'), 1546 'uncheck_all' => Dict::S('UI:SearchValue:UncheckAll'), 1547 'none_selected' => Dict::S('UI:Relation:NoneSelected'), 1548 'nb_selected' => Dict::S('UI:SearchValue:NbSelected'), 1549 'additional_context_info' => Dict::S('UI:Relation:AdditionalContextInfo'), 1550 'zoom' => Dict::S('UI:Relation:Zoom'), 1551 'loading' => Dict::S('UI:Loading'), 1552 ), 1553 'page_format' => array( 1554 'label' => Dict::S('UI:Relation:PDFExportPageFormat'), 1555 'values' => array( 1556 'A3' => Dict::S('UI:PageFormat_A3'), 1557 'A4' => Dict::S('UI:PageFormat_A4'), 1558 'Letter' => Dict::S('UI:PageFormat_Letter'), 1559 ), 1560 ), 1561 'page_orientation' => array( 1562 'label' => Dict::S('UI:Relation:PDFExportPageOrientation'), 1563 'values' => array( 1564 'P' => Dict::S('UI:PageOrientation_Portrait'), 1565 'L' => Dict::S('UI:PageOrientation_Landscape'), 1566 ), 1567 ), 1568 'additional_contexts' => $aAdditionalContexts, 1569 'context_key' => $sContextKey, 1570 ); 1571 if (!extension_loaded('gd')) 1572 { 1573 // PDF export requires GD 1574 unset($aParams['export_as_pdf']); 1575 } 1576 if (!extension_loaded('gd') || is_null($sObjClass) || is_null($iObjKey)) 1577 { 1578 // Export as Attachment requires GD (for building the PDF) AND a valid objclass/objkey couple 1579 unset($aParams['export_as_attachment']); 1580 } 1581 $oP->add_ready_script("$('#$sId').simple_graph(".json_encode($aParams).");"); 1582 } 1583 catch(Exception $e) 1584 { 1585 $oP->add('<div>'.$e->getMessage().'</div>'); 1586 } 1587 $oP->add_script( 1588<<<EOF 1589 1590 function DoReload() 1591 { 1592 $('#ReloadMovieBtn').button('disable'); 1593 try 1594 { 1595 var aExcluded = []; 1596 $('input[name^=excluded]').each( function() { 1597 if (!$(this).prop('checked')) 1598 { 1599 aExcluded.push($(this).val()); 1600 } 1601 } ); 1602 $('#graph').simple_graph('option', {excluded_classes: aExcluded}); 1603 $('#graph').simple_graph('reload'); 1604 } 1605 catch(err) 1606 { 1607 alert(err); 1608 } 1609 } 1610EOF 1611 ); 1612 } 1613 1614}