displayablegraph.class.inc.php 49 KB

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