displayablegraph.class.inc.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  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()
  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['sink'] = ($this->GetProperty('sink') == true);
  81. $aNode['x'] = $this->x;
  82. $aNode['y']= $this->y;
  83. $aNode['label'] = $this->GetLabel();
  84. $aNode['id'] = $this->GetId();
  85. $fOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
  86. $aNode['icon_attr'] = array('opacity' => $fOpacity);
  87. $aNode['text_attr'] = array('opacity' => $fOpacity);
  88. return $aNode;
  89. }
  90. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale)
  91. {
  92. $Alpha = 1.0;
  93. $oPdf->SetFillColor(200, 200, 200);
  94. $oPdf->setAlpha(1);
  95. $sIconUrl = $this->GetProperty('icon_url');
  96. $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-production/', $sIconUrl);
  97. if ($this->GetProperty('source'))
  98. {
  99. $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => array(204, 51, 51)));
  100. $oPdf->Circle($this->x * $fScale, $this->y * $fScale, 16 * 1.25 * $fScale, 0, 360, 'D');
  101. }
  102. else if ($this->GetProperty('sink'))
  103. {
  104. $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => array(51, 51, 204)));
  105. $oPdf->Circle($this->x * $fScale, $this->y * $fScale, 16 * 1.25 * $fScale, 0, 360, 'D');
  106. }
  107. if (!$this->GetProperty('is_reached'))
  108. {
  109. $sTempImageName = $this->CreateWhiteIcon($oGraph, $sIconPath);
  110. if ($sTempImageName != null)
  111. {
  112. $oPdf->Image($sTempImageName, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale, 'PNG');
  113. }
  114. $Alpha = 0.4;
  115. $oPdf->setAlpha($Alpha);
  116. }
  117. $oPdf->Image($sIconPath, ($this->x - 16)*$fScale, ($this->y - 16)*$fScale, 32*$fScale, 32*$fScale);
  118. $oPdf->SetFont('dejavusans', '', 24 * $fScale, '', true);
  119. $width = $oPdf->GetStringWidth($this->GetProperty('label'));
  120. $height = $oPdf->GetStringHeight(1000, $this->GetProperty('label'));
  121. $oPdf->setAlpha(0.6 * $Alpha);
  122. $oPdf->SetFillColor(255, 255, 255);
  123. $oPdf->SetDrawColor(255, 255, 255);
  124. $oPdf->Rect($this->x*$fScale - $width/2, ($this->y + 18)*$fScale, $width, $height, 'DF');
  125. $oPdf->setAlpha($Alpha);
  126. $oPdf->SetTextColor(0, 0, 0);
  127. $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 18)*$fScale, $this->GetProperty('label'));
  128. }
  129. /**
  130. * Create a "whitened" version of the icon (retaining the transparency) to be used a background for masking the underlying lines
  131. * @param string $sIconFile The path to the file containing the icon
  132. * @return NULL|string The path to a temporary file containing the white version of the icon
  133. */
  134. protected function CreateWhiteIcon(DisplayableGraph $oGraph, $sIconFile)
  135. {
  136. $aInfo = getimagesize($sIconFile);
  137. $im = null;
  138. switch($aInfo['mime'])
  139. {
  140. case 'image/png':
  141. if (function_exists('imagecreatefrompng'))
  142. {
  143. $im = imagecreatefrompng($sIconFile);
  144. }
  145. break;
  146. case 'image/gif':
  147. if (function_exists('imagecreatefromgif'))
  148. {
  149. $im = imagecreatefromgif($sIconFile);
  150. }
  151. break;
  152. case 'image/jpeg':
  153. case 'image/jpg':
  154. if (function_exists('imagecreatefromjpeg'))
  155. {
  156. $im = imagecreatefromjpeg($sIconFile);
  157. }
  158. break;
  159. default:
  160. return null;
  161. }
  162. if($im && imagefilter($im, IMG_FILTER_COLORIZE, 255, 255, 255))
  163. {
  164. $sTempImageName = $oGraph->GetTempImageName();
  165. imagesavealpha($im, true);
  166. imagepng($im, $sTempImageName);
  167. imagedestroy($im);
  168. return $sTempImageName;
  169. }
  170. else
  171. {
  172. return null;
  173. }
  174. }
  175. /**
  176. * Group together (as a special kind of nodes) all the similar neighbours of the current node
  177. * @param DisplayableGraph $oGraph
  178. * @param int $iThresholdCount
  179. * @param boolean $bDirectionUp
  180. * @param boolean $bDirectionDown
  181. */
  182. public function GroupSimilarNeighbours(DisplayableGraph $oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true)
  183. {
  184. //echo "<p>".$this->GetProperty('label').":</p>";
  185. if ($this->GetProperty('grouped') === true) return;
  186. $this->SetProperty('grouped', true);
  187. if ($bDirectionDown)
  188. {
  189. $aNodesPerClass = array();
  190. foreach($this->GetOutgoingEdges() as $oEdge)
  191. {
  192. $oNode = $oEdge->GetSinkNode();
  193. if ($oNode->GetProperty('class') !== null)
  194. {
  195. $sClass = $oNode->GetProperty('class');
  196. if (($sClass!== null) && (!array_key_exists($sClass, $aNodesPerClass)))
  197. {
  198. $aNodesPerClass[$sClass] = array(
  199. 'reached' => array(
  200. 'count' => 0,
  201. 'nodes' => array(),
  202. 'icon_url' => $oNode->GetProperty('icon_url'),
  203. ),
  204. 'not_reached' => array(
  205. 'count' => 0,
  206. 'nodes' => array(),
  207. 'icon_url' => $oNode->GetProperty('icon_url'),
  208. )
  209. );
  210. }
  211. $sKey = $oNode->GetProperty('is_reached') ? 'reached' : 'not_reached';
  212. if (!array_key_exists($oNode->GetId(), $aNodesPerClass[$sClass][$sKey]['nodes']))
  213. {
  214. $aNodesPerClass[$sClass][$sKey]['nodes'][$oNode->GetId()] = $oNode;
  215. $aNodesPerClass[$sClass][$sKey]['count'] += (int)$oNode->GetProperty('count', 1);
  216. //echo "<p>New count: ".$aNodesPerClass[$sClass][$sKey]['count']."</p>";
  217. }
  218. }
  219. else
  220. {
  221. $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  222. }
  223. }
  224. foreach($aNodesPerClass as $sClass => $aDefs)
  225. {
  226. foreach($aDefs as $sStatus => $aGroupProps)
  227. {
  228. //echo "<p>$sClass/$sStatus: {$aGroupProps['count']} object(s), actually: ".count($aGroupProps['nodes'])."</p>";
  229. if (count($aGroupProps['nodes']) >= $iThresholdCount)
  230. {
  231. $oNewNode = new DisplayableGroupNode($oGraph, $this->GetId().'::'.$sClass);
  232. $oNewNode->SetProperty('label', 'x'.$aGroupProps['count']);
  233. $oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
  234. $oNewNode->SetProperty('class', $sClass);
  235. $oNewNode->SetProperty('is_reached', ($sStatus == 'reached'));
  236. $oNewNode->SetProperty('count', $aGroupProps['count']);
  237. //$oNewNode->SetProperty('grouped', true);
  238. $oIncomingEdge = new DisplayableEdge($oGraph, $this->GetId().'-'.$oNewNode->GetId(), $this, $oNewNode);
  239. foreach($aGroupProps['nodes'] as $oNode)
  240. {
  241. foreach($oNode->GetIncomingEdges() as $oEdge)
  242. {
  243. if ($oEdge->GetSourceNode()->GetId() !== $this->GetId())
  244. {
  245. $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
  246. }
  247. }
  248. foreach($oNode->GetOutgoingEdges() as $oEdge)
  249. {
  250. $aOutgoing[] = $oEdge->GetSinkNode();
  251. try
  252. {
  253. $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oNewNode, $oEdge->GetSinkNode());
  254. }
  255. catch(Exception $e)
  256. {
  257. // ignore this edge
  258. }
  259. }
  260. if ($oGraph->GetNode($oNode->GetId()))
  261. {
  262. $oGraph->_RemoveNode($oNode);
  263. }
  264. }
  265. $oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  266. }
  267. else
  268. {
  269. foreach($aGroupProps['nodes'] as $oNode)
  270. {
  271. $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  272. }
  273. }
  274. }
  275. }
  276. }
  277. }
  278. }
  279. class DisplayableRedundancyNode extends DisplayableNode
  280. {
  281. public function GetWidth()
  282. {
  283. return 24;
  284. }
  285. public function GetForRaphael()
  286. {
  287. $aNode = array();
  288. $aNode['shape'] = 'disc';
  289. $aNode['icon_url'] = $this->GetIconURL();
  290. $aNode['source'] = ($this->GetProperty('source') == true);
  291. $aNode['width'] = $this->GetWidth();
  292. $aNode['x'] = $this->x;
  293. $aNode['y']= $this->y;
  294. $aNode['label'] = $this->GetLabel();
  295. $aNode['id'] = $this->GetId();
  296. $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2);
  297. $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => '#c33', 'opacity' => $fDiscOpacity);
  298. $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
  299. $aNode['text_attr'] = array('fill' => '#fff', 'opacity' => $fTextOpacity);
  300. return $aNode;
  301. }
  302. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale)
  303. {
  304. $oPdf->SetAlpha(1);
  305. $oPdf->SetFillColor(200, 0, 0);
  306. $oPdf->SetDrawColor(0, 0, 0);
  307. $oPdf->Circle($this->x*$fScale, $this->y*$fScale, 16*$fScale, 0, 360, 'DF');
  308. $oPdf->SetTextColor(255, 255, 255);
  309. $oPdf->SetFont('dejavusans', '', 28 * $fScale, '', true);
  310. $sLabel = (string)$this->GetProperty('label');
  311. $width = $oPdf->GetStringWidth($sLabel, 'dejavusans', 'B', 24*$fScale);
  312. $height = $oPdf->GetStringHeight(1000, $sLabel);
  313. $xPos = (float)$this->x*$fScale - $width/2;
  314. $yPos = (float)$this->y*$fScale - $height/2;
  315. $oPdf->SetXY(($this->x - 16)*$fScale, ($this->y - 16)*$fScale);
  316. $oPdf->Cell(32*$fScale, 32*$fScale, $sLabel, 0, 0, 'C', 0, '', 0, false, 'T', 'C');
  317. }
  318. /**
  319. * @see DisplayableNode::GroupSimilarNeighbours()
  320. */
  321. public function GroupSimilarNeighbours(DisplayableGraph $oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true)
  322. {
  323. parent::GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  324. if ($bDirectionUp)
  325. {
  326. $aNodesPerClass = array();
  327. foreach($this->GetIncomingEdges() as $oEdge)
  328. {
  329. $oNode = $oEdge->GetSourceNode();
  330. if (($oNode->GetProperty('class') !== null) && (!$oNode->GetProperty('is_reached')))
  331. {
  332. $sClass = $oNode->GetProperty('class');
  333. if (!array_key_exists($sClass, $aNodesPerClass))
  334. {
  335. $aNodesPerClass[$sClass] = array('reached' => array(), 'not_reached' => array());
  336. }
  337. $aNodesPerClass[$sClass][$oNode->GetProperty('is_reached') ? 'reached' : 'not_reached'][] = $oNode;
  338. }
  339. else
  340. {
  341. //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  342. }
  343. }
  344. foreach($aNodesPerClass as $sClass => $aDefs)
  345. {
  346. foreach($aDefs as $sStatus => $aNodes)
  347. {
  348. //echo "<p>".$this->GetId().' has '.count($aNodes)." neighbours of class $sClass in status $sStatus\n";
  349. if (count($aNodes) >= $iThresholdCount)
  350. {
  351. $oNewNode = new DisplayableGroupNode($oGraph, '-'.$this->GetId().'::'.$sClass.'/'.$sStatus);
  352. $oNewNode->SetProperty('label', 'x'.count($aNodes));
  353. $oNewNode->SetProperty('icon_url', $aNodes[0]->GetProperty('icon_url'));
  354. $oNewNode->SetProperty('is_reached', $aNodes[0]->GetProperty('is_reached'));
  355. $oOutgoingEdge = new DisplayableEdge($oGraph, '-'.$this->GetId().'-'.$oNewNode->GetId().'/'.$sStatus, $oNewNode, $this);
  356. foreach($aNodes as $oNode)
  357. {
  358. foreach($oNode->GetIncomingEdges() as $oEdge)
  359. {
  360. $oNewEdge = new DisplayableEdge($oGraph, '-'.$oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
  361. }
  362. foreach($oNode->GetOutgoingEdges() as $oEdge)
  363. {
  364. if ($oEdge->GetSinkNode()->GetId() !== $this->GetId())
  365. {
  366. $aOutgoing[] = $oEdge->GetSinkNode();
  367. $oNewEdge = new DisplayableEdge($oGraph, '-'.$oEdge->GetId().'::'.$sClass.'/'.$sStatus, $oNewNode, $oEdge->GetSinkNode());
  368. }
  369. }
  370. //echo "<p>Replacing ".$oNode->GetId().' by '.$oNewNode->GetId()."\n";
  371. $oGraph->_RemoveNode($oNode);
  372. }
  373. //$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  374. }
  375. else
  376. {
  377. foreach($aNodes as $oNode)
  378. {
  379. //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  380. }
  381. }
  382. }
  383. }
  384. }
  385. }
  386. }
  387. class DisplayableEdge extends GraphEdge
  388. {
  389. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale)
  390. {
  391. $xStart = $this->GetSourceNode()->x * $fScale;
  392. $yStart = $this->GetSourceNode()->y * $fScale;
  393. $xEnd = $this->GetSinkNode()->x * $fScale;
  394. $yEnd = $this->GetSinkNode()->y * $fScale;
  395. $bReached = ($this->GetSourceNode()->GetProperty('is_reached') && $this->GetSinkNode()->GetProperty('is_reached'));
  396. $oPdf->setAlpha(1);
  397. if ($bReached)
  398. {
  399. $aColor = array(100, 100, 100);
  400. }
  401. else
  402. {
  403. $aColor = array(200, 200, 200);
  404. }
  405. $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aColor));
  406. $oPdf->Line($xStart, $yStart, $xEnd, $yEnd);
  407. $vx = $xEnd - $xStart;
  408. $vy = $yEnd - $yStart;
  409. $l = sqrt($vx*$vx + $vy*$vy);
  410. $vx = $vx / $l;
  411. $vy = $vy / $l;
  412. $ux = -$vy;
  413. $uy = $vx;
  414. $lPos = max($l/2, $l - 40*$fScale);
  415. $iArrowSize = 5*$fScale;
  416. $x = $xStart + $lPos * $vx;
  417. $y = $yStart + $lPos * $vy;
  418. $oPdf->Line($x, $y, $x + $iArrowSize * ($ux-$vx), $y + $iArrowSize * ($uy-$vy));
  419. $oPdf->Line($x, $y, $x - $iArrowSize * ($ux+$vx), $y - $iArrowSize * ($uy+$vy));
  420. }
  421. }
  422. class DisplayableGroupNode extends DisplayableNode
  423. {
  424. public function GetWidth()
  425. {
  426. return 50;
  427. }
  428. public function GetForRaphael()
  429. {
  430. $aNode = array();
  431. $aNode['shape'] = 'group';
  432. $aNode['icon_url'] = $this->GetIconURL();
  433. $aNode['source'] = ($this->GetProperty('source') == true);
  434. $aNode['width'] = $this->GetWidth();
  435. $aNode['x'] = $this->x;
  436. $aNode['y']= $this->y;
  437. $aNode['label'] = $this->GetLabel();
  438. $aNode['id'] = $this->GetId();
  439. $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2);
  440. $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
  441. $aNode['icon_attr'] = array('opacity' => $fTextOpacity);
  442. $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => '#fff', 'opacity' => $fDiscOpacity);
  443. $aNode['text_attr'] = array('fill' => '#000', 'opacity' => $fTextOpacity);
  444. return $aNode;
  445. }
  446. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale)
  447. {
  448. $bReached = $this->GetProperty('is_reached');
  449. $oPdf->SetFillColor(255, 255, 255);
  450. if ($bReached)
  451. {
  452. $aBorderColor = array(100, 100, 100);
  453. }
  454. else
  455. {
  456. $aBorderColor = array(200, 200, 200);
  457. }
  458. $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aBorderColor));
  459. $sIconUrl = $this->GetProperty('icon_url');
  460. $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-production/', $sIconUrl);
  461. $oPdf->SetAlpha(1);
  462. $oPdf->Circle($this->x*$fScale, $this->y*$fScale, $this->GetWidth() / 2 * $fScale, 0, 360, 'DF');
  463. if ($bReached)
  464. {
  465. $oPdf->SetAlpha(1);
  466. }
  467. else
  468. {
  469. $oPdf->SetAlpha(0.4);
  470. }
  471. $oPdf->Image($sIconPath, ($this->x - 17)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale);
  472. $oPdf->Image($sIconPath, ($this->x + 1)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale);
  473. $oPdf->Image($sIconPath, ($this->x -8)*$fScale, ($this->y +1)*$fScale, 16*$fScale, 16*$fScale);
  474. $oPdf->SetFont('dejavusans', '', 24 * $fScale, '', true);
  475. $width = $oPdf->GetStringWidth($this->GetProperty('label'));
  476. $oPdf->SetTextColor(0, 0, 0);
  477. $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 25)*$fScale, $this->GetProperty('label'));
  478. }
  479. }
  480. /**
  481. * A Graph that can be displayed interactively using Raphael JS or saved as a PDF document
  482. */
  483. class DisplayableGraph extends SimpleGraph
  484. {
  485. protected $sDirection;
  486. protected $aTempImages;
  487. public function __construct()
  488. {
  489. parent::__construct();
  490. $this->aTempImages = array();
  491. }
  492. public function GetTempImageName()
  493. {
  494. $sNewTempName = tempnam(APPROOT.'data', 'img-');
  495. $this->aTempImages[] = $sNewTempName;
  496. return $sNewTempName;
  497. }
  498. public function __destruct()
  499. {
  500. foreach($this->aTempImages as $sTempFile)
  501. {
  502. @unlink($sTempFile);
  503. }
  504. }
  505. public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true)
  506. {
  507. $oNewGraph = new DisplayableGraph();
  508. $oNodesIter = new RelationTypeIterator($oGraph, 'Node');
  509. foreach($oNodesIter as $oNode)
  510. {
  511. switch(get_class($oNode))
  512. {
  513. case 'RelationObjectNode':
  514. $oNewNode = new DisplayableNode($oNewGraph, $oNode->GetId(), 0, 0);
  515. if ($oNode->GetProperty('source'))
  516. {
  517. $oNewNode->SetProperty('source', true);
  518. }
  519. if ($oNode->GetProperty('sink'))
  520. {
  521. $oNewNode->SetProperty('sink', true);
  522. }
  523. $oObj = $oNode->GetProperty('object');
  524. $oNewNode->SetProperty('class', get_class($oObj));
  525. $oNewNode->SetProperty('icon_url', $oObj->GetIcon(false));
  526. $oNewNode->SetProperty('label', $oObj->GetRawName());
  527. $oNewNode->SetProperty('is_reached', $bDirectionDown ? $oNode->GetProperty('is_reached') : true); // When going "up" is_reached does not matter
  528. $oNewNode->SetProperty('developped', $oNode->GetProperty('developped'));
  529. break;
  530. default:
  531. $oNewNode = new DisplayableRedundancyNode($oNewGraph, $oNode->GetId(), 0, 0);
  532. $oNewNode->SetProperty('label', $oNode->GetProperty('min_up'));
  533. $oNewNode->SetProperty('is_reached', true);
  534. }
  535. }
  536. $oEdgesIter = new RelationTypeIterator($oGraph, 'Edge');
  537. foreach($oEdgesIter as $oEdge)
  538. {
  539. $oSourceNode = $oNewGraph->GetNode($oEdge->GetSourceNode()->GetId());
  540. $oSinkNode = $oNewGraph->GetNode($oEdge->GetSinkNode()->GetId());
  541. $oNewEdge = new DisplayableEdge($oNewGraph, $oEdge->GetId(), $oSourceNode, $oSinkNode);
  542. }
  543. // Remove duplicate edges between two nodes
  544. $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge');
  545. $aEdgeKeys = array();
  546. foreach($oEdgesIter as $oEdge)
  547. {
  548. $sSourceId = $oEdge->GetSourceNode()->GetId();
  549. $sSinkId = $oEdge->GetSinkNode()->GetId();
  550. if ($sSourceId == $sSinkId)
  551. {
  552. // Remove self referring edges
  553. $oNewGraph->_RemoveEdge($oEdge);
  554. }
  555. else
  556. {
  557. $sKey = $sSourceId.'//'.$sSinkId;
  558. if (array_key_exists($sKey, $aEdgeKeys))
  559. {
  560. // Remove duplicate edges
  561. $oNewGraph->_RemoveEdge($oEdge);
  562. }
  563. else
  564. {
  565. $aEdgeKeys[$sKey] = true;
  566. }
  567. }
  568. }
  569. $iNbGrouping = 1;
  570. //for($iter=0; $iter<$iNbGrouping; $iter++)
  571. {
  572. $oNodesIter = new RelationTypeIterator($oNewGraph, 'Node');
  573. foreach($oNodesIter as $oNode)
  574. {
  575. if ($oNode->GetProperty('source'))
  576. {
  577. $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, true);
  578. }
  579. }
  580. }
  581. // Remove duplicate edges between two nodes
  582. $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge');
  583. $aEdgeKeys = array();
  584. foreach($oEdgesIter as $oEdge)
  585. {
  586. $sSourceId = $oEdge->GetSourceNode()->GetId();
  587. $sSinkId = $oEdge->GetSinkNode()->GetId();
  588. if ($sSourceId == $sSinkId)
  589. {
  590. // Remove self referring edges
  591. $oNewGraph->_RemoveEdge($oEdge);
  592. }
  593. else
  594. {
  595. $sKey = $sSourceId.'//'.$sSinkId;
  596. if (array_key_exists($sKey, $aEdgeKeys))
  597. {
  598. // Remove duplicate edges
  599. $oNewGraph->_RemoveEdge($oEdge);
  600. }
  601. else
  602. {
  603. $aEdgeKeys[$sKey] = true;
  604. }
  605. }
  606. }
  607. return $oNewGraph;
  608. }
  609. public function InitFromGraphviz()
  610. {
  611. $sDot = $this->DumpAsXDot();
  612. if (strpos($sDot, 'digraph') === false)
  613. {
  614. throw new Exception($sDot);
  615. }
  616. $sDot = preg_replace('/.*label=.*,/', '', $sDot); // Get rid of label lines since they may contain weird characters than can break the split and pattern matching below
  617. $aChunks = explode(";", $sDot);
  618. foreach($aChunks as $sChunk)
  619. {
  620. //echo "<p>$sChunk</p>";
  621. if(preg_match('/"([^"]+)".+pos="([0-9\\.]+),([0-9\\.]+)"/ms', $sChunk, $aMatches))
  622. {
  623. $sId = $aMatches[1];
  624. $xPos = $aMatches[2];
  625. $yPos = $aMatches[3];
  626. $oNode = $this->GetNode($sId);
  627. $oNode->x = (float)$xPos;
  628. $oNode->y = (float)$yPos;
  629. //echo "<p>$sId at $xPos,$yPos</p>";
  630. }
  631. else
  632. {
  633. //echo "<p>No match</p>";
  634. }
  635. }
  636. }
  637. public function GetBoundingBox()
  638. {
  639. $xMin = null;
  640. $xMax = null;
  641. $yMin = null;
  642. $yMax = null;
  643. $oIterator = new RelationTypeIterator($this, 'Node');
  644. foreach($oIterator as $sId => $oNode)
  645. {
  646. if ($xMin === null) // First element in the loop
  647. {
  648. $xMin = $oNode->x - $oNode->GetWidth();
  649. $xMax = $oNode->x + $oNode->GetWidth();
  650. $yMin = $oNode->y - $oNode->GetHeight();
  651. $yMax = $oNode->y + $oNode->GetHeight();
  652. }
  653. else
  654. {
  655. $xMin = min($xMin, $oNode->x - $oNode->GetWidth() / 2);
  656. $xMax = max($xMax, $oNode->x + $oNode->GetWidth() / 2);
  657. $yMin = min($yMin, $oNode->y - $oNode->GetHeight() / 2);
  658. $yMax = max($yMax, $oNode->y + $oNode->GetHeight() / 2);
  659. }
  660. }
  661. return array('xmin' => $xMin, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax);
  662. }
  663. function Translate($dx, $dy)
  664. {
  665. $oIterator = new RelationTypeIterator($this, 'Node');
  666. foreach($oIterator as $sId => $oNode)
  667. {
  668. $oNode->x += $dx;
  669. $oNode->y += $dy;
  670. }
  671. }
  672. public function UpdatePositions($aPositions)
  673. {
  674. foreach($aPositions as $sNodeId => $aPos)
  675. {
  676. $oNode = $this->GetNode($sNodeId);
  677. if ($oNode != null)
  678. {
  679. $oNode->x = $aPos['x'];
  680. $oNode->y = $aPos['y'];
  681. }
  682. }
  683. }
  684. function RenderAsRaphael(WebPage $oP, $sId = null, $sExportAsPdfURL, $sExportAsDocumentURL, $sDrillDownURL)
  685. {
  686. if ($sId == null)
  687. {
  688. $sId = 'graph';
  689. }
  690. $aBB = $this->GetBoundingBox();
  691. $oP->add('<div id="'.$sId.'" class="simple-graph"></div>');
  692. $aParams = array(
  693. 'xmin' => $aBB['xmin'],
  694. 'xmax' => $aBB['xmax'],
  695. 'ymin' => $aBB['ymin'],
  696. 'ymax' => $aBB['ymax'],
  697. 'export_as_pdf_url' => $sExportAsPdfURL,
  698. 'export_as_document_url' => $sExportAsDocumentURL,
  699. 'drill_down_url' => $sDrillDownURL,
  700. );
  701. $oP->add_ready_script("var oGraph = $('#$sId').simple_graph(".json_encode($aParams).");");
  702. $oIterator = new RelationTypeIterator($this, 'Node');
  703. foreach($oIterator as $sId => $oNode)
  704. {
  705. $aNode = $oNode->GetForRaphael();
  706. $sJSNode = json_encode($aNode);
  707. $oP->add_ready_script("oGraph.simple_graph('add_node', $sJSNode);");
  708. }
  709. $oIterator = new RelationTypeIterator($this, 'Edge');
  710. foreach($oIterator as $sId => $oEdge)
  711. {
  712. $aEdge = array();
  713. $aEdge['id'] = $oEdge->GetId();
  714. $aEdge['source_node_id'] = $oEdge->GetSourceNode()->GetId();
  715. $aEdge['sink_node_id'] = $oEdge->GetSinkNode()->GetId();
  716. $fOpacity = ($oEdge->GetSinkNode()->GetProperty('is_reached') && $oEdge->GetSourceNode()->GetProperty('is_reached') ? 1 : 0.2);
  717. $aEdge['attr'] = array('opacity' => $fOpacity, 'stroke' => '#000');
  718. $sJSEdge = json_encode($aEdge);
  719. $oP->add_ready_script("oGraph.simple_graph('add_edge', $sJSEdge);");
  720. }
  721. $oP->add_ready_script("oGraph.simple_graph('draw');");
  722. }
  723. function RenderAsPDF(WebPage $oP, $sTitle = 'Untitled', $sPageFormat = 'A4', $sPageOrientation = 'P')
  724. {
  725. require_once(APPROOT.'lib/tcpdf/tcpdf.php');
  726. $oPdf = new TCPDF($sPageOrientation, 'mm', $sPageFormat, true, 'UTF-8', false);
  727. // set document information
  728. $oPdf->SetCreator(PDF_CREATOR);
  729. $oPdf->SetAuthor('iTop');
  730. $oPdf->SetTitle($sTitle);
  731. $oPdf->setFontSubsetting(true);
  732. // Set font
  733. // dejavusans is a UTF-8 Unicode font, if you only need to
  734. // print standard ASCII chars, you can use core fonts like
  735. // helvetica or times to reduce file size.
  736. $oPdf->SetFont('dejavusans', '', 14, '', true);
  737. // set auto page breaks
  738. $oPdf->SetAutoPageBreak(false);
  739. // Add a page
  740. // This method has several options, check the source code documentation for more information.
  741. $oPdf->AddPage();
  742. $aBB = $this->GetBoundingBox();
  743. $this->Translate(-$aBB['xmin'], -$aBB['ymin']);
  744. if ($sPageOrientation == 'P')
  745. {
  746. // Portrait mode
  747. $fHMargin = 10; // mm
  748. $fVMargin = 15; // mm
  749. }
  750. else
  751. {
  752. // Landscape mode
  753. $fHMargin = 15; // mm
  754. $fVMargin = 10; // mm
  755. }
  756. $fPageW = $oPdf->getPageWidth() - 2 * $fHMargin;
  757. $fPageH = $oPdf->getPageHeight() - 2 * $fVMargin;
  758. $w = $aBB['xmax'] - $aBB['xmin'];
  759. $h = $aBB['ymax'] - $aBB['ymin'] + 10; // Extra space for the labels which may appear "below" the icons
  760. $fScale = min($fPageW / $w, $fPageH / $h);
  761. $dx = ($fPageW - $fScale * $w) / 2;
  762. $dy = ($fPageH - $fScale * $h) / 2;
  763. $this->Translate(($fHMargin + $dx)/$fScale, ($fVMargin + $dy)/$fScale);
  764. $oIterator = new RelationTypeIterator($this, 'Edge');
  765. foreach($oIterator as $sId => $oEdge)
  766. {
  767. $oEdge->RenderAsPDF($oPdf, $this, $fScale);
  768. }
  769. $oIterator = new RelationTypeIterator($this, 'Node');
  770. foreach($oIterator as $sId => $oNode)
  771. {
  772. $oNode->RenderAsPDF($oPdf, $this, $fScale);
  773. }
  774. $oP->add($oPdf->Output('iTop.pdf', 'S'));
  775. }
  776. }