displayablegraph.class.inc.php 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304
  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. /**
  205. * Group together (as a special kind of nodes) all the similar neighbours of the current node
  206. * @param DisplayableGraph $oGraph
  207. * @param int $iThresholdCount
  208. * @param boolean $bDirectionUp
  209. * @param boolean $bDirectionDown
  210. */
  211. public function GroupSimilarNeighbours(DisplayableGraph $oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true)
  212. {
  213. //echo "<p>".$this->GetProperty('label').":</p>";
  214. if ($this->GetProperty('grouped') === true) return;
  215. $this->SetProperty('grouped', true);
  216. if ($bDirectionDown)
  217. {
  218. $aNodesPerClass = array();
  219. foreach($this->GetOutgoingEdges() as $oEdge)
  220. {
  221. $oNode = $oEdge->GetSinkNode();
  222. if ($oNode->GetProperty('class') !== null)
  223. {
  224. $sClass = $oNode->GetProperty('class');
  225. if (($sClass!== null) && (!array_key_exists($sClass, $aNodesPerClass)))
  226. {
  227. $aNodesPerClass[$sClass] = array(
  228. 'reached' => array(
  229. 'count' => 0,
  230. 'nodes' => array(),
  231. 'icon_url' => $oNode->GetProperty('icon_url'),
  232. ),
  233. 'not_reached' => array(
  234. 'count' => 0,
  235. 'nodes' => array(),
  236. 'icon_url' => $oNode->GetProperty('icon_url'),
  237. )
  238. );
  239. }
  240. $sKey = $oNode->GetProperty('is_reached') ? 'reached' : 'not_reached';
  241. if (!array_key_exists($oNode->GetId(), $aNodesPerClass[$sClass][$sKey]['nodes']))
  242. {
  243. $aNodesPerClass[$sClass][$sKey]['nodes'][$oNode->GetId()] = $oNode;
  244. $aNodesPerClass[$sClass][$sKey]['count'] += (int)$oNode->GetProperty('count', 1);
  245. //echo "<p>New count: ".$aNodesPerClass[$sClass][$sKey]['count']."</p>";
  246. }
  247. }
  248. else
  249. {
  250. $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  251. }
  252. }
  253. foreach($aNodesPerClass as $sClass => $aDefs)
  254. {
  255. foreach($aDefs as $sStatus => $aGroupProps)
  256. {
  257. //echo "<p>$sClass/$sStatus: {$aGroupProps['count']} object(s), actually: ".count($aGroupProps['nodes'])."</p>";
  258. if (count($aGroupProps['nodes']) >= $iThresholdCount)
  259. {
  260. $oNewNode = new DisplayableGroupNode($oGraph, $this->GetId().'::'.$sClass);
  261. $oNewNode->SetProperty('label', 'x'.$aGroupProps['count']);
  262. $oNewNode->SetProperty('icon_url', $aGroupProps['icon_url']);
  263. $oNewNode->SetProperty('class', $sClass);
  264. $oNewNode->SetProperty('is_reached', ($sStatus == 'reached'));
  265. $oNewNode->SetProperty('count', $aGroupProps['count']);
  266. //$oNewNode->SetProperty('grouped', true);
  267. $oIncomingEdge = new DisplayableEdge($oGraph, $this->GetId().'-'.$oNewNode->GetId(), $this, $oNewNode);
  268. foreach($aGroupProps['nodes'] as $oNode)
  269. {
  270. foreach($oNode->GetIncomingEdges() as $oEdge)
  271. {
  272. if ($oEdge->GetSourceNode()->GetId() !== $this->GetId())
  273. {
  274. $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
  275. }
  276. }
  277. foreach($oNode->GetOutgoingEdges() as $oEdge)
  278. {
  279. $aOutgoing[] = $oEdge->GetSinkNode();
  280. try
  281. {
  282. $oNewEdge = new DisplayableEdge($oGraph, $oEdge->GetId().'::'.$sClass, $oNewNode, $oEdge->GetSinkNode());
  283. }
  284. catch(Exception $e)
  285. {
  286. // ignore this edge
  287. }
  288. }
  289. if ($oGraph->GetNode($oNode->GetId()))
  290. {
  291. $oGraph->_RemoveNode($oNode);
  292. $oNewNode->AddObject($oNode->GetProperty('object'));
  293. }
  294. }
  295. $oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  296. }
  297. else
  298. {
  299. foreach($aGroupProps['nodes'] as $oNode)
  300. {
  301. $oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  302. }
  303. }
  304. }
  305. }
  306. }
  307. }
  308. public function GetTooltip($aContextDefs)
  309. {
  310. $sHtml = '';
  311. $oCurrObj = $this->GetProperty('object');
  312. $sSubClass = get_class($oCurrObj);
  313. $sHtml .= $oCurrObj->GetHyperlink()."<hr/>";
  314. $aContextRootCauses = $this->GetProperty('context_root_causes');
  315. if (!is_null($aContextRootCauses))
  316. {
  317. foreach($aContextRootCauses as $key => $aObjects)
  318. {
  319. //$sHtml .= print_r($aContextDefs, true);
  320. $aContext = $aContextDefs[$key];
  321. $aRootCauses = array();
  322. foreach($aObjects as $oRootCause)
  323. {
  324. $aRootCauses[] = $oRootCause->GetHyperlink();
  325. }
  326. $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>';
  327. }
  328. $sHtml .= '<hr/>';
  329. }
  330. $sHtml .= '<table><tbody>';
  331. foreach(MetaModel::GetZListItems($sSubClass, 'list') as $sAttCode)
  332. {
  333. $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
  334. $sHtml .= '<tr><td>'.$oAttDef->GetLabel().':&nbsp;</td><td>'.$oCurrObj->GetAsHtml($sAttCode).'</td></tr>';
  335. }
  336. $sHtml .= '</tbody></table>';
  337. return $sHtml;
  338. }
  339. }
  340. class DisplayableRedundancyNode extends DisplayableNode
  341. {
  342. public function GetWidth()
  343. {
  344. return 24;
  345. }
  346. public function GetForRaphael($aContextDefs)
  347. {
  348. $aNode = array();
  349. $aNode['shape'] = 'disc';
  350. $aNode['icon_url'] = $this->GetIconURL();
  351. $aNode['source'] = ($this->GetProperty('source') == true);
  352. $aNode['width'] = $this->GetWidth();
  353. $aNode['x'] = $this->x;
  354. $aNode['y']= $this->y;
  355. $aNode['label'] = $this->GetLabel();
  356. $aNode['id'] = $this->GetId();
  357. $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2);
  358. $sColor = ($this->GetProperty('is_reached_count') > $this->GetProperty('threshold')) ? '#c33' : '#999';
  359. $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => $sColor, 'opacity' => $fDiscOpacity);
  360. $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
  361. $aNode['text_attr'] = array('fill' => '#fff', 'opacity' => $fTextOpacity);
  362. $aNode['tooltip'] = $this->GetTooltip($aContextDefs);
  363. return $aNode;
  364. }
  365. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs)
  366. {
  367. $oPdf->SetAlpha(1);
  368. if($this->GetProperty('is_reached_count') > $this->GetProperty('threshold'))
  369. {
  370. $oPdf->SetFillColor(200, 0, 0);
  371. }
  372. else
  373. {
  374. $oPdf->SetFillColor(144, 144, 144);
  375. }
  376. $oPdf->SetDrawColor(0, 0, 0);
  377. $oPdf->Circle($this->x*$fScale, $this->y*$fScale, 16*$fScale, 0, 360, 'DF');
  378. $oPdf->SetTextColor(255, 255, 255);
  379. $oPdf->SetFont('dejavusans', '', 28 * $fScale, '', true);
  380. $sLabel = (string)$this->GetProperty('label');
  381. $width = $oPdf->GetStringWidth($sLabel, 'dejavusans', 'B', 24*$fScale);
  382. $height = $oPdf->GetStringHeight(1000, $sLabel);
  383. $xPos = (float)$this->x*$fScale - $width/2;
  384. $yPos = (float)$this->y*$fScale - $height/2;
  385. $oPdf->SetXY(($this->x - 16)*$fScale, ($this->y - 16)*$fScale);
  386. $oPdf->Cell(32*$fScale, 32*$fScale, $sLabel, 0, 0, 'C', 0, '', 0, false, 'T', 'C');
  387. }
  388. /**
  389. * @see DisplayableNode::GroupSimilarNeighbours()
  390. */
  391. public function GroupSimilarNeighbours(DisplayableGraph $oGraph, $iThresholdCount, $bDirectionUp = false, $bDirectionDown = true)
  392. {
  393. parent::GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  394. if ($bDirectionUp)
  395. {
  396. $aNodesPerClass = array();
  397. foreach($this->GetIncomingEdges() as $oEdge)
  398. {
  399. $oNode = $oEdge->GetSourceNode();
  400. if (($oNode->GetProperty('class') !== null) && (!$oNode->GetProperty('is_reached')))
  401. {
  402. $sClass = $oNode->GetProperty('class');
  403. if (!array_key_exists($sClass, $aNodesPerClass))
  404. {
  405. $aNodesPerClass[$sClass] = array('reached' => array(), 'not_reached' => array());
  406. }
  407. $aNodesPerClass[$sClass][$oNode->GetProperty('is_reached') ? 'reached' : 'not_reached'][] = $oNode;
  408. }
  409. else
  410. {
  411. //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  412. }
  413. }
  414. foreach($aNodesPerClass as $sClass => $aDefs)
  415. {
  416. foreach($aDefs as $sStatus => $aNodes)
  417. {
  418. //echo "<p>".$this->GetId().' has '.count($aNodes)." neighbours of class $sClass in status $sStatus\n";
  419. if (count($aNodes) >= $iThresholdCount)
  420. {
  421. $oNewNode = new DisplayableGroupNode($oGraph, '-'.$this->GetId().'::'.$sClass.'/'.$sStatus);
  422. $oNewNode->SetProperty('label', 'x'.count($aNodes));
  423. $oNewNode->SetProperty('icon_url', $aNodes[0]->GetProperty('icon_url'));
  424. $oNewNode->SetProperty('is_reached', $aNodes[0]->GetProperty('is_reached'));
  425. $oOutgoingEdge = new DisplayableEdge($oGraph, '-'.$this->GetId().'-'.$oNewNode->GetId().'/'.$sStatus, $oNewNode, $this);
  426. foreach($aNodes as $oNode)
  427. {
  428. foreach($oNode->GetIncomingEdges() as $oEdge)
  429. {
  430. $oNewEdge = new DisplayableEdge($oGraph, '-'.$oEdge->GetId().'::'.$sClass, $oEdge->GetSourceNode(), $oNewNode);
  431. }
  432. foreach($oNode->GetOutgoingEdges() as $oEdge)
  433. {
  434. if ($oEdge->GetSinkNode()->GetId() !== $this->GetId())
  435. {
  436. $aOutgoing[] = $oEdge->GetSinkNode();
  437. $oNewEdge = new DisplayableEdge($oGraph, '-'.$oEdge->GetId().'::'.$sClass.'/'.$sStatus, $oNewNode, $oEdge->GetSinkNode());
  438. }
  439. }
  440. //echo "<p>Replacing ".$oNode->GetId().' by '.$oNewNode->GetId()."\n";
  441. $oGraph->_RemoveNode($oNode);
  442. $oNewNode->AddObject($oNode->GetProperty('object'));
  443. }
  444. //$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  445. }
  446. else
  447. {
  448. foreach($aNodes as $oNode)
  449. {
  450. //$oNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
  451. }
  452. }
  453. }
  454. }
  455. }
  456. }
  457. public function GetTooltip($aContextDefs)
  458. {
  459. $sHtml = '';
  460. $sHtml .= "Redundancy<hr>";
  461. $sHtml .= '<table><tbody>';
  462. $sHtml .= "<tr><td># Items Impacted:&nbsp;</td><td>".$this->GetProperty('is_reached_count')."&nbsp;/&nbsp;".($this->GetProperty('min_up') + $this->GetProperty('threshold'))."</td></tr>";
  463. $sHtml .= "<tr><td>Critical Threshold:&nbsp;</td><td>".$this->GetProperty('threshold')."&nbsp;/&nbsp;".($this->GetProperty('min_up') + $this->GetProperty('threshold'))."</td></tr>";
  464. $sHtml .= '</tbody></table>';
  465. return $sHtml;
  466. }
  467. }
  468. class DisplayableEdge extends GraphEdge
  469. {
  470. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs)
  471. {
  472. $xStart = $this->GetSourceNode()->x * $fScale;
  473. $yStart = $this->GetSourceNode()->y * $fScale;
  474. $xEnd = $this->GetSinkNode()->x * $fScale;
  475. $yEnd = $this->GetSinkNode()->y * $fScale;
  476. $bReached = ($this->GetSourceNode()->GetProperty('is_reached') && $this->GetSinkNode()->GetProperty('is_reached'));
  477. $oPdf->setAlpha(1);
  478. if ($bReached)
  479. {
  480. $aColor = array(100, 100, 100);
  481. }
  482. else
  483. {
  484. $aColor = array(200, 200, 200);
  485. }
  486. $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aColor));
  487. $oPdf->Line($xStart, $yStart, $xEnd, $yEnd);
  488. $vx = $xEnd - $xStart;
  489. $vy = $yEnd - $yStart;
  490. $l = sqrt($vx*$vx + $vy*$vy);
  491. $vx = $vx / $l;
  492. $vy = $vy / $l;
  493. $ux = -$vy;
  494. $uy = $vx;
  495. $lPos = max($l/2, $l - 40*$fScale);
  496. $iArrowSize = 5*$fScale;
  497. $x = $xStart + $lPos * $vx;
  498. $y = $yStart + $lPos * $vy;
  499. $oPdf->Line($x, $y, $x + $iArrowSize * ($ux-$vx), $y + $iArrowSize * ($uy-$vy));
  500. $oPdf->Line($x, $y, $x - $iArrowSize * ($ux+$vx), $y - $iArrowSize * ($uy+$vy));
  501. }
  502. }
  503. class DisplayableGroupNode extends DisplayableNode
  504. {
  505. protected $aObjects;
  506. public function __construct(SimpleGraph $oGraph, $sId, $x = 0, $y = 0)
  507. {
  508. parent::__construct($oGraph, $sId, $x, $y);
  509. $this->aObjects = array();
  510. }
  511. public function AddObject(DBObject $oObj)
  512. {
  513. $this->aObjects[$oObj->GetKey()] = $oObj;
  514. }
  515. public function GetObjects()
  516. {
  517. return $this->aObjects;
  518. }
  519. public function GetWidth()
  520. {
  521. return 50;
  522. }
  523. public function GetForRaphael($aContextDefs)
  524. {
  525. $aNode = array();
  526. $aNode['shape'] = 'group';
  527. $aNode['icon_url'] = $this->GetIconURL();
  528. $aNode['source'] = ($this->GetProperty('source') == true);
  529. $aNode['width'] = $this->GetWidth();
  530. $aNode['x'] = $this->x;
  531. $aNode['y']= $this->y;
  532. $aNode['label'] = $this->GetLabel();
  533. $aNode['id'] = $this->GetId();
  534. $aNode['group_index'] = $this->GetProperty('group_index'); // if supplied
  535. $fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2);
  536. $fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
  537. $aNode['icon_attr'] = array('opacity' => $fTextOpacity);
  538. $aNode['disc_attr'] = array('stroke-width' => 3, 'stroke' => '#000', 'fill' => '#fff', 'opacity' => $fDiscOpacity);
  539. $aNode['text_attr'] = array('fill' => '#000', 'opacity' => $fTextOpacity);
  540. $aNode['tooltip'] = $this->GetTooltip($aContextDefs);
  541. return $aNode;
  542. }
  543. public function RenderAsPDF(TCPDF $oPdf, DisplayableGraph $oGraph, $fScale, $aContextDefs)
  544. {
  545. $bReached = $this->GetProperty('is_reached');
  546. $oPdf->SetFillColor(255, 255, 255);
  547. if ($bReached)
  548. {
  549. $aBorderColor = array(100, 100, 100);
  550. }
  551. else
  552. {
  553. $aBorderColor = array(200, 200, 200);
  554. }
  555. $oPdf->SetLineStyle(array('width' => 2*$fScale, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => $aBorderColor));
  556. $sIconUrl = $this->GetProperty('icon_url');
  557. $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl);
  558. $oPdf->SetAlpha(1);
  559. $oPdf->Circle($this->x*$fScale, $this->y*$fScale, $this->GetWidth() / 2 * $fScale, 0, 360, 'DF');
  560. if ($bReached)
  561. {
  562. $oPdf->SetAlpha(1);
  563. }
  564. else
  565. {
  566. $oPdf->SetAlpha(0.4);
  567. }
  568. $oPdf->Image($sIconPath, ($this->x - 17)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale);
  569. $oPdf->Image($sIconPath, ($this->x + 1)*$fScale, ($this->y - 17)*$fScale, 16*$fScale, 16*$fScale);
  570. $oPdf->Image($sIconPath, ($this->x -8)*$fScale, ($this->y +1)*$fScale, 16*$fScale, 16*$fScale);
  571. $oPdf->SetFont('dejavusans', '', 24 * $fScale, '', true);
  572. $width = $oPdf->GetStringWidth($this->GetProperty('label'));
  573. $oPdf->SetTextColor(0, 0, 0);
  574. $oPdf->Text($this->x*$fScale - $width/2, ($this->y + 25)*$fScale, $this->GetProperty('label'));
  575. }
  576. public function GetTooltip($aContextDefs)
  577. {
  578. $sHtml = '';
  579. $iGroupIdx = $this->GetProperty('group_index');
  580. $sHtml .= Dict::Format('UI:RelationGroupNumber_N', (1+$iGroupIdx));
  581. return $sHtml;
  582. }
  583. }
  584. /**
  585. * A Graph that can be displayed interactively using Raphael JS or saved as a PDF document
  586. */
  587. class DisplayableGraph extends SimpleGraph
  588. {
  589. protected $bDirectionDown;
  590. protected $aTempImages;
  591. protected $aSourceObjects;
  592. protected $aSinkObjects;
  593. public function __construct()
  594. {
  595. parent::__construct();
  596. $this->aTempImages = array();
  597. $this->aSourceObjects = array();
  598. $this->aSinkObjects = array();
  599. }
  600. public function GetTempImageName()
  601. {
  602. $sNewTempName = tempnam(APPROOT.'data', 'img-');
  603. $this->aTempImages[] = $sNewTempName;
  604. return $sNewTempName;
  605. }
  606. public function __destruct()
  607. {
  608. foreach($this->aTempImages as $sTempFile)
  609. {
  610. @unlink($sTempFile);
  611. }
  612. }
  613. /**
  614. * Build a DisplayableGraph from a RelationGraph
  615. * @param RelationGraph $oGraph
  616. * @param number $iGroupingThreshold
  617. * @param string $bDirectionDown
  618. * @return DisplayableGraph
  619. */
  620. public static function FromRelationGraph(RelationGraph $oGraph, $iGroupingThreshold = 20, $bDirectionDown = true)
  621. {
  622. $oNewGraph = new DisplayableGraph();
  623. $oNewGraph->bDirectionDown = $bDirectionDown;
  624. $oNodesIter = new RelationTypeIterator($oGraph, 'Node');
  625. foreach($oNodesIter as $oNode)
  626. {
  627. switch(get_class($oNode))
  628. {
  629. case 'RelationObjectNode':
  630. $oNewNode = new DisplayableNode($oNewGraph, $oNode->GetId(), 0, 0);
  631. $oObj = $oNode->GetProperty('object');
  632. $sClass = get_class($oObj);
  633. if ($oNode->GetProperty('source'))
  634. {
  635. if (!array_key_exists($sClass, $oNewGraph->aSourceObjects))
  636. {
  637. $oNewGraph->aSourceObjects[$sClass] = array();
  638. }
  639. $oNewGraph->aSourceObjects[$sClass][] = $oObj->GetKey();
  640. $oNewNode->SetProperty('source', true);
  641. }
  642. if ($oNode->GetProperty('sink'))
  643. {
  644. if (!array_key_exists($sClass, $oNewGraph->aSinkObjects))
  645. {
  646. $oNewGraph->aSinkObjects[$sClass] = array();
  647. }
  648. $oNewGraph->aSinkObjects[$sClass][] = $oObj->GetKey();
  649. $oNewNode->SetProperty('sink', true);
  650. }
  651. $oNewNode->SetProperty('class', $sClass);
  652. $oNewNode->SetProperty('object', $oObj);
  653. $oNewNode->SetProperty('icon_url', $oObj->GetIcon(false));
  654. $oNewNode->SetProperty('label', $oObj->GetRawName());
  655. $oNewNode->SetProperty('is_reached', $bDirectionDown ? $oNode->GetProperty('is_reached') : true); // When going "up" is_reached does not matter
  656. $oNewNode->SetProperty('is_reached_allowed', $oNode->GetProperty('is_reached_allowed'));
  657. $oNewNode->SetProperty('context_root_causes', $oNode->GetProperty('context_root_causes'));
  658. break;
  659. default:
  660. $oNewNode = new DisplayableRedundancyNode($oNewGraph, $oNode->GetId(), 0, 0);
  661. $iNbReached = (is_null($oNode->GetProperty('is_reached_count'))) ? 0 : $oNode->GetProperty('is_reached_count');
  662. $oNewNode->SetProperty('label', $iNbReached."/".($oNode->GetProperty('min_up') + $oNode->GetProperty('threshold')));
  663. $oNewNode->SetProperty('min_up', $oNode->GetProperty('min_up'));
  664. $oNewNode->SetProperty('threshold', $oNode->GetProperty('threshold'));
  665. $oNewNode->SetProperty('is_reached_count', $iNbReached);
  666. $oNewNode->SetProperty('is_reached', true);
  667. }
  668. }
  669. $oEdgesIter = new RelationTypeIterator($oGraph, 'Edge');
  670. foreach($oEdgesIter as $oEdge)
  671. {
  672. $oSourceNode = $oNewGraph->GetNode($oEdge->GetSourceNode()->GetId());
  673. $oSinkNode = $oNewGraph->GetNode($oEdge->GetSinkNode()->GetId());
  674. $oNewEdge = new DisplayableEdge($oNewGraph, $oEdge->GetId(), $oSourceNode, $oSinkNode);
  675. }
  676. // Remove duplicate edges between two nodes
  677. $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge');
  678. $aEdgeKeys = array();
  679. foreach($oEdgesIter as $oEdge)
  680. {
  681. $sSourceId = $oEdge->GetSourceNode()->GetId();
  682. $sSinkId = $oEdge->GetSinkNode()->GetId();
  683. if ($sSourceId == $sSinkId)
  684. {
  685. // Remove self referring edges
  686. $oNewGraph->_RemoveEdge($oEdge);
  687. }
  688. else
  689. {
  690. $sKey = $sSourceId.'//'.$sSinkId;
  691. if (array_key_exists($sKey, $aEdgeKeys))
  692. {
  693. // Remove duplicate edges
  694. $oNewGraph->_RemoveEdge($oEdge);
  695. }
  696. else
  697. {
  698. $aEdgeKeys[$sKey] = true;
  699. }
  700. }
  701. }
  702. $oNodesIter = new RelationTypeIterator($oNewGraph, 'Node');
  703. foreach($oNodesIter as $oNode)
  704. {
  705. if ($oNode->GetProperty('source'))
  706. {
  707. $oNode->GroupSimilarNeighbours($oNewGraph, $iGroupingThreshold, true, true);
  708. }
  709. }
  710. // Groups numbering
  711. $oIterator = new RelationTypeIterator($oNewGraph, 'Node');
  712. $iGroupIdx = 0;
  713. foreach($oIterator as $oNode)
  714. {
  715. if ($oNode instanceof DisplayableGroupNode)
  716. {
  717. $aGroups[] = $oNode->GetObjects();
  718. $oNode->SetProperty('group_index', $iGroupIdx);
  719. $iGroupIdx++;
  720. }
  721. }
  722. // Remove duplicate edges between two nodes
  723. $oEdgesIter = new RelationTypeIterator($oNewGraph, 'Edge');
  724. $aEdgeKeys = array();
  725. foreach($oEdgesIter as $oEdge)
  726. {
  727. $sSourceId = $oEdge->GetSourceNode()->GetId();
  728. $sSinkId = $oEdge->GetSinkNode()->GetId();
  729. if ($sSourceId == $sSinkId)
  730. {
  731. // Remove self referring edges
  732. $oNewGraph->_RemoveEdge($oEdge);
  733. }
  734. else
  735. {
  736. $sKey = $sSourceId.'//'.$sSinkId;
  737. if (array_key_exists($sKey, $aEdgeKeys))
  738. {
  739. // Remove duplicate edges
  740. $oNewGraph->_RemoveEdge($oEdge);
  741. }
  742. else
  743. {
  744. $aEdgeKeys[$sKey] = true;
  745. }
  746. }
  747. }
  748. return $oNewGraph;
  749. }
  750. /**
  751. * Initializes the positions by rendering using Graphviz in xdot format
  752. * and parsing the output.
  753. * @throws Exception
  754. */
  755. public function InitFromGraphviz()
  756. {
  757. $sDot = $this->DumpAsXDot();
  758. if (strpos($sDot, 'digraph') === false)
  759. {
  760. throw new Exception($sDot);
  761. }
  762. $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
  763. $aChunks = explode(";", $sDot);
  764. foreach($aChunks as $sChunk)
  765. {
  766. //echo "<p>$sChunk</p>";
  767. if(preg_match('/"([^"]+)".+pos="([0-9\\.]+),([0-9\\.]+)"/ms', $sChunk, $aMatches))
  768. {
  769. $sId = $aMatches[1];
  770. $xPos = $aMatches[2];
  771. $yPos = $aMatches[3];
  772. $oNode = $this->GetNode($sId);
  773. $oNode->x = (float)$xPos;
  774. $oNode->y = (float)$yPos;
  775. //echo "<p>$sId at $xPos,$yPos</p>";
  776. }
  777. else
  778. {
  779. //echo "<p>No match</p>";
  780. }
  781. }
  782. }
  783. public function GetBoundingBox()
  784. {
  785. $xMin = null;
  786. $xMax = null;
  787. $yMin = null;
  788. $yMax = null;
  789. $oIterator = new RelationTypeIterator($this, 'Node');
  790. foreach($oIterator as $sId => $oNode)
  791. {
  792. if ($xMin === null) // First element in the loop
  793. {
  794. $xMin = $oNode->x - $oNode->GetWidth();
  795. $xMax = $oNode->x + $oNode->GetWidth();
  796. $yMin = $oNode->y - $oNode->GetHeight();
  797. $yMax = $oNode->y + $oNode->GetHeight();
  798. }
  799. else
  800. {
  801. $xMin = min($xMin, $oNode->x - $oNode->GetWidth() / 2);
  802. $xMax = max($xMax, $oNode->x + $oNode->GetWidth() / 2);
  803. $yMin = min($yMin, $oNode->y - $oNode->GetHeight() / 2);
  804. $yMax = max($yMax, $oNode->y + $oNode->GetHeight() / 2);
  805. }
  806. }
  807. return array('xmin' => $xMin, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax);
  808. }
  809. function Translate($dx, $dy)
  810. {
  811. $oIterator = new RelationTypeIterator($this, 'Node');
  812. foreach($oIterator as $sId => $oNode)
  813. {
  814. $oNode->x += $dx;
  815. $oNode->y += $dy;
  816. }
  817. }
  818. public function UpdatePositions($aPositions)
  819. {
  820. foreach($aPositions as $sNodeId => $aPos)
  821. {
  822. $oNode = $this->GetNode($sNodeId);
  823. if ($oNode != null)
  824. {
  825. $oNode->x = $aPos['x'];
  826. $oNode->y = $aPos['y'];
  827. }
  828. }
  829. }
  830. /**
  831. * Renders as JSON string suitable for loading into the simple_graph widget
  832. */
  833. function GetAsJSON($sContextKey)
  834. {
  835. $aContextDefs = $this->GetContextDefinitions($sContextKey, false);
  836. $aData = array('nodes' => array(), 'edges' => array());
  837. $iGroupIdx = 0;
  838. $oIterator = new RelationTypeIterator($this, 'Node');
  839. foreach($oIterator as $sId => $oNode)
  840. {
  841. if ($oNode instanceof DisplayableGroupNode)
  842. {
  843. $aGroups[] = $oNode->GetObjects();
  844. $oNode->SetProperty('group_index', $iGroupIdx);
  845. $iGroupIdx++;
  846. }
  847. $aData['nodes'][] = $oNode->GetForRaphael($aContextDefs);
  848. }
  849. $oIterator = new RelationTypeIterator($this, 'Edge');
  850. foreach($oIterator as $sId => $oEdge)
  851. {
  852. $aEdge = array();
  853. $aEdge['id'] = $oEdge->GetId();
  854. $aEdge['source_node_id'] = $oEdge->GetSourceNode()->GetId();
  855. $aEdge['sink_node_id'] = $oEdge->GetSinkNode()->GetId();
  856. $fOpacity = ($oEdge->GetSinkNode()->GetProperty('is_reached') && $oEdge->GetSourceNode()->GetProperty('is_reached') ? 1 : 0.2);
  857. $aEdge['attr'] = array('opacity' => $fOpacity, 'stroke' => '#000');
  858. $aData['edges'][] = $aEdge;
  859. }
  860. return json_encode($aData);
  861. }
  862. /**
  863. * Renders the graph in a PDF document: centered in the current page
  864. * @param PDFPage $oPage The PDFPage representing the PDF document to draw into
  865. * @param string $sComments An optional comment to display next to the graph (HTML entities will be escaped, \n replaced by <br/>)
  866. * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down
  867. * @param float $xMin Left coordinate of the bounding box to display the graph
  868. * @param float $xMax Right coordinate of the bounding box to display the graph
  869. * @param float $yMin Top coordinate of the bounding box to display the graph
  870. * @param float $yMax Bottom coordinate of the bounding box to display the graph
  871. */
  872. function RenderAsPDF(PDFPage $oPage, $sComments = '', $sContextKey, $xMin = -1, $xMax = -1, $yMin = -1, $yMax = -1)
  873. {
  874. $aContextDefs = $this->GetContextDefinitions($sContextKey, false); // No need to develop the parameters
  875. $oPdf = $oPage->get_tcpdf();
  876. $aBB = $this->GetBoundingBox();
  877. $this->Translate(-$aBB['xmin'], -$aBB['ymin']);
  878. $aMargins = $oPdf->getMargins();
  879. if ($xMin == -1)
  880. {
  881. $xMin = $aMargins['left'];
  882. }
  883. if ($xMax == -1)
  884. {
  885. $xMax = $oPdf->getPageWidth() - $aMargins['right'];
  886. }
  887. if ($yMin == -1)
  888. {
  889. $yMin = $aMargins['top'];
  890. }
  891. if ($yMax == -1)
  892. {
  893. $yMax = $oPdf->getPageHeight() - $aMargins['bottom'];
  894. }
  895. $fBreakMargin = $oPdf->getBreakMargin();
  896. $oPdf->SetAutoPageBreak(false);
  897. $aRemainingArea = $this->RenderKey($oPdf, $sComments, $xMin, $yMin, $xMax, $yMax);
  898. $xMin = $aRemainingArea['xmin'];
  899. $xMax = $aRemainingArea['xmax'];
  900. $yMin = $aRemainingArea['ymin'];
  901. $yMax = $aRemainingArea['ymax'];
  902. //$oPdf->Rect($xMin, $yMin, $xMax - $xMin, $yMax - $yMin, 'D', array(), array(225, 225, 225));
  903. $fPageW = $xMax - $xMin;
  904. $fPageH = $yMax - $yMin;
  905. $w = $aBB['xmax'] - $aBB['xmin'];
  906. $h = $aBB['ymax'] - $aBB['ymin'] + 10; // Extra space for the labels which may appear "below" the icons
  907. $fScale = min($fPageW / $w, $fPageH / $h);
  908. $dx = ($fPageW - $fScale * $w) / 2;
  909. $dy = ($fPageH - $fScale * $h) / 2;
  910. $this->Translate(($xMin + $dx)/$fScale, ($yMin + $dy)/$fScale);
  911. $oIterator = new RelationTypeIterator($this, 'Edge');
  912. $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
  913. foreach($oIterator as $sId => $oEdge)
  914. {
  915. set_time_limit($iLoopTimeLimit);
  916. $oEdge->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs);
  917. }
  918. $oIterator = new RelationTypeIterator($this, 'Node');
  919. foreach($oIterator as $sId => $oNode)
  920. {
  921. set_time_limit($iLoopTimeLimit);
  922. $oNode->RenderAsPDF($oPdf, $this, $fScale, $aContextDefs);
  923. }
  924. $oIterator = new RelationTypeIterator($this, 'Node');
  925. $oPdf->SetAutoPageBreak(true, $fBreakMargin);
  926. $oPdf->SetAlpha(1);
  927. }
  928. /**
  929. * Renders (in PDF) the key (legend) of the graphics vertically to the left of the specified zone (xmin,ymin, xmax,ymax),
  930. * and the comment (if any) at the bottom of the page. Returns the position of remaining area.
  931. * @param TCPDF $oPdf
  932. * @param string $sComments
  933. * @param float $xMin
  934. * @param float $yMin
  935. * @param float $xMax
  936. * @param float $yMax
  937. * @return hash An array ('xmin' => , 'xmax' => ,'ymin' => , 'ymax' => ) of the remaining available area to paint the graph
  938. */
  939. protected function RenderKey(TCPDF $oPdf, $sComments, $xMin, $yMin, $xMax, $yMax)
  940. {
  941. $fFontSize = 7; // in mm
  942. $fIconSize = 6; // in mm
  943. $fPadding = 1; // in mm
  944. $oIterator = new RelationTypeIterator($this, 'Node');
  945. $fMaxWidth = max($oPdf->GetStringWidth(Dict::S('UI:Relation:Key')) - $fIconSize, $oPdf->GetStringWidth(Dict::S('UI:Relation:Comments')) - $fIconSize);
  946. $aClasses = array();
  947. $aIcons = array();
  948. $oPdf->SetFont('dejavusans', '', $fFontSize, '', true);
  949. foreach($oIterator as $sId => $oNode)
  950. {
  951. if ($sClass = $oNode->GetProperty('class'))
  952. {
  953. if (!array_key_exists($sClass, $aClasses))
  954. {
  955. $sClassLabel = MetaModel::GetName($sClass);
  956. $width = $oPdf->GetStringWidth($sClassLabel);
  957. $fMaxWidth = max($width, $fMaxWidth);
  958. $aClasses[$sClass] = $sClassLabel;
  959. $sIconUrl = $oNode->GetProperty('icon_url');
  960. $sIconPath = str_replace(utils::GetAbsoluteUrlModulesRoot(), APPROOT.'env-'.utils::GetCurrentEnvironment().'/', $sIconUrl);
  961. $aIcons[$sClass] = $sIconPath;
  962. }
  963. }
  964. }
  965. $oPdf->SetXY($xMin + $fPadding, $yMin + $fPadding);
  966. $yPos = $yMin + $fPadding;
  967. $oPdf->SetFillColor(225, 225, 225);
  968. $oPdf->Cell($fIconSize + $fPadding + $fMaxWidth, $fIconSize + $fPadding, Dict::S('UI:Relation:Key'), 0 /* border */, 1 /* ln */, 'C', true /* fill */);
  969. $yPos += $fIconSize + 2*$fPadding;
  970. foreach($aClasses as $sClass => $sLabel)
  971. {
  972. $oPdf->SetX($xMin + $fIconSize + $fPadding);
  973. $oPdf->Cell(0, $fIconSize + 2*$fPadding, $sLabel, 0 /* border */, 1 /* ln */);
  974. $oPdf->Image($aIcons[$sClass], $xMin+1, $yPos, $fIconSize, $fIconSize);
  975. $yPos += $fIconSize + 2*$fPadding;
  976. }
  977. $oPdf->Rect($xMin, $yMin, $fMaxWidth + $fIconSize + 3*$fPadding, $yMax - $yMin, 'D');
  978. if ($sComments != '')
  979. {
  980. // Draw the comment text (surrounded by a rectangle)
  981. $xPos = $xMin + $fMaxWidth + $fIconSize + 4*$fPadding;
  982. $w = $xMax - $xPos - 2*$fPadding;
  983. $iNbLines = 1;
  984. $sText = '<p>'.str_replace("\n", '<br/>', htmlentities($sComments, ENT_QUOTES, 'UTF-8'), $iNbLines).'</p>';
  985. $fLineHeight = $oPdf->getStringHeight($w, $sText);
  986. $h = (1+$iNbLines) * $fLineHeight;
  987. $yPos = $yMax - 2*$fPadding - $h;
  988. $oPdf->writeHTMLCell($w, $h, $xPos + $fPadding, $yPos + $fPadding, $sText, 0 /* border */, 1 /* ln */);
  989. $oPdf->Rect($xPos, $yPos, $w + 2*$fPadding, $h + 2*$fPadding, 'D');
  990. $yMax = $yPos - $fPadding;
  991. }
  992. return array('xmin' => $fMaxWidth + $fIconSize + 4*$fPadding, 'xmax' => $xMax, 'ymin' => $yMin, 'ymax' => $yMax);
  993. }
  994. //itop-tickets/relation_context/UserRequest/impacts/down
  995. /**
  996. *
  997. * @param string $sContextKey The key to fetch the queries in the configuration. Example: itop-tickets/relation_context/UserRequest/impacts/down
  998. */
  999. public function GetContextDefinitions($sContextKey, $bDevelopParams = true, $aContextParams = array())
  1000. {
  1001. $aLevels = explode('/', $sContextKey);
  1002. $aRelationContext = MetaModel::GetConfig()->GetModuleSetting($aLevels[0], $aLevels[1], array());
  1003. $aContextDefs = array();
  1004. if (isset($aRelationContext[$aLevels[2]][$aLevels[3]][$aLevels[4]]['items']))
  1005. {
  1006. $aContextDefs = $aRelationContext[$aLevels[2]][$aLevels[3]][$aLevels[4]]['items'];
  1007. }
  1008. // Check if the queries are valid
  1009. foreach($aContextDefs as $sKey => $sDefs)
  1010. {
  1011. $sOQL = $aContextDefs[$sKey]['oql'];
  1012. try
  1013. {
  1014. // Expand the parameters. If anything goes wrong, then the query is considered as invalid and removed from the list
  1015. $oSearch = DBObjectSearch::FromOQL($sOQL);
  1016. $aContextDefs[$sKey]['oql'] = $oSearch->ToOQL($bDevelopParams, $aContextParams);
  1017. }
  1018. catch(Exception $e)
  1019. {
  1020. unset($aContextDefs[$sKey]);
  1021. }
  1022. }
  1023. return $aContextDefs;
  1024. }
  1025. /**
  1026. * Display the graph inside the given page, with the "filter" drawer above it
  1027. * @param WebPage $oP
  1028. * @param hash $aResults
  1029. * @param string $sRelation
  1030. * @param ApplicationContext $oAppContext
  1031. * @param array $aExcludedObjects
  1032. */
  1033. function Display(WebPage $oP, $aResults, $sRelation, ApplicationContext $oAppContext, $aExcludedObjects = array(), $sObjClass = null, $iObjKey = null, $sContextKey, $aContextParams = array())
  1034. {
  1035. $aContextDefs = $this->GetContextDefinitions($sContextKey, true, $aContextParams);
  1036. $aExcludedByClass = array();
  1037. foreach($aExcludedObjects as $oObj)
  1038. {
  1039. if (!array_key_exists(get_class($oObj), $aExcludedByClass))
  1040. {
  1041. $aExcludedByClass[get_class($oObj)] = array();
  1042. }
  1043. $aExcludedByClass[get_class($oObj)][] = $oObj->GetKey();
  1044. }
  1045. $oP->add("<div id=\"ds_flash\" class=\"SearchDrawer\" style=\"display:none;\">\n");
  1046. $oP->add_ready_script(
  1047. <<<EOF
  1048. $( "#tabbedContent_0" ).tabs({ heightStyle: "fill" });
  1049. $("#dh_flash").click( function() {
  1050. $("#ds_flash").slideToggle('normal', function() { $("#ds_flash").parent().resize(); } );
  1051. $("#dh_flash").toggleClass('open');
  1052. });
  1053. $('#ReloadMovieBtn').button().button('disable');
  1054. EOF
  1055. );
  1056. $aSortedElements = array();
  1057. foreach($aResults as $sClassIdx => $aObjects)
  1058. {
  1059. foreach($aObjects as $oCurrObj)
  1060. {
  1061. $sSubClass = get_class($oCurrObj);
  1062. $aSortedElements[$sSubClass] = MetaModel::GetName($sSubClass);
  1063. }
  1064. }
  1065. asort($aSortedElements);
  1066. $idx = 0;
  1067. foreach($aSortedElements as $sSubClass => $sClassName)
  1068. {
  1069. $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> ");
  1070. $idx++;
  1071. }
  1072. $oP->add("<p style=\"text-align:right\"><button type=\"button\" id=\"ReloadMovieBtn\" onClick=\"DoReload()\">".Dict::S('UI:Button:Refresh')."</button></p>");
  1073. $oP->add("</div>\n");
  1074. $oP->add("<div class=\"HRDrawer\"></div>\n");
  1075. $oP->add("<div id=\"dh_flash\" class=\"DrawerHandle\">".Dict::S('UI:ElementsDisplayed')."</div>\n");
  1076. $aAdditionalContexts = array();
  1077. foreach($aContextDefs as $sKey => $aDefinition)
  1078. {
  1079. $aAdditionalContexts[] = array('key' => $sKey, 'label' => Dict::S($aDefinition['dict']), 'oql' => $aDefinition['oql']);
  1080. }
  1081. $sDirection = utils::ReadParam('d', 'horizontal');
  1082. $iGroupingThreshold = utils::ReadParam('g', 5);
  1083. $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js');
  1084. $oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css');
  1085. $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js');
  1086. $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js');
  1087. try
  1088. {
  1089. $this->InitFromGraphviz();
  1090. $sExportAsPdfURL = '';
  1091. $sExportAsPdfURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_pdf&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up');
  1092. $oAppcontext = new ApplicationContext();
  1093. $sContext = $oAppContext->GetForLink();
  1094. $sDrillDownURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class=%1$s&id=%2$s&'.$sContext;
  1095. $sExportAsDocumentURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_attachment&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up');
  1096. $sLoadFromURL = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=relation_json&relation='.$sRelation.'&direction='.($this->bDirectionDown ? 'down' : 'up');
  1097. $sAttachmentExportTitle = '';
  1098. if (($sObjClass != null) && ($iObjKey != null))
  1099. {
  1100. $oTargetObj = MetaModel::GetObject($sObjClass, $iObjKey, false);
  1101. if ($oTargetObj)
  1102. {
  1103. $sAttachmentExportTitle = Dict::Format('UI:Relation:AttachmentExportOptions_Name', $oTargetObj->GetName());
  1104. }
  1105. }
  1106. $sId = 'graph';
  1107. $oP->add('<div id="'.$sId.'" class="simple-graph"></div>');
  1108. $aParams = array(
  1109. 'source_url' => $sLoadFromURL,
  1110. 'sources' => ($this->bDirectionDown ? $this->aSourceObjects : $this->aSinkObjects),
  1111. 'excluded' => $aExcludedByClass,
  1112. 'grouping_threshold' => $iGroupingThreshold,
  1113. 'export_as_pdf' => array('url' => $sExportAsPdfURL, 'label' => Dict::S('UI:Relation:ExportAsPDF')),
  1114. 'export_as_attachment' => array('url' => $sExportAsDocumentURL, 'label' => Dict::S('UI:Relation:ExportAsAttachment'), 'obj_class' => $sObjClass, 'obj_key' => $iObjKey),
  1115. 'drill_down' => array('url' => $sDrillDownURL, 'label' => Dict::S('UI:Relation:DrillDown')),
  1116. 'labels' => array(
  1117. 'export_pdf_title' => Dict::S('UI:Relation:PDFExportOptions'),
  1118. 'export_as_attachment_title' => $sAttachmentExportTitle,
  1119. 'export' => Dict::S('UI:Button:Export'),
  1120. 'cancel' => Dict::S('UI:Button:Cancel'),
  1121. 'title' => Dict::S('UI:RelationOption:Title'),
  1122. 'untitled' => Dict::S('UI:RelationOption:Untitled'),
  1123. 'include_list' => Dict::S('UI:RelationOption:IncludeList'),
  1124. 'comments' => Dict::S('UI:RelationOption:Comments'),
  1125. 'grouping_threshold' => Dict::S('UI:RelationOption:GroupingThreshold'),
  1126. 'refresh' => Dict::S('UI:Button:Refresh'),
  1127. 'check_all' => Dict::S('UI:SearchValue:CheckAll'),
  1128. 'uncheck_all' => Dict::S('UI:SearchValue:UncheckAll'),
  1129. 'none_selected' => Dict::S('UI:Relation:NoneSelected'),
  1130. 'nb_selected' => Dict::S('UI:SearchValue:NbSelected'),
  1131. 'additional_context_info' => Dict::S('UI:Relation:AdditionalContextInfo'),
  1132. ),
  1133. 'page_format' => array(
  1134. 'label' => Dict::S('UI:Relation:PDFExportPageFormat'),
  1135. 'values' => array(
  1136. 'A3' => Dict::S('UI:PageFormat_A3'),
  1137. 'A4' => Dict::S('UI:PageFormat_A4'),
  1138. 'Letter' => Dict::S('UI:PageFormat_Letter'),
  1139. ),
  1140. ),
  1141. 'page_orientation' => array(
  1142. 'label' => Dict::S('UI:Relation:PDFExportPageOrientation'),
  1143. 'values' => array(
  1144. 'P' => Dict::S('UI:PageOrientation_Portrait'),
  1145. 'L' => Dict::S('UI:PageOrientation_Landscape'),
  1146. ),
  1147. ),
  1148. 'additional_contexts' => $aAdditionalContexts,
  1149. 'context_key' => $sContextKey,
  1150. );
  1151. if (!extension_loaded('gd'))
  1152. {
  1153. // PDF export requires GD
  1154. unset($aParams['export_as_pdf']);
  1155. }
  1156. if (!extension_loaded('gd') || is_null($sObjClass) || is_null($iObjKey))
  1157. {
  1158. // Export as Attachment requires GD (for building the PDF) AND a valid objclass/objkey couple
  1159. unset($aParams['export_as_attachment']);
  1160. }
  1161. $oP->add_ready_script("$('#$sId').simple_graph(".json_encode($aParams).");");
  1162. }
  1163. catch(Exception $e)
  1164. {
  1165. $oP->add('<div>'.$e->getMessage().'</div>');
  1166. }
  1167. $oP->add_script(
  1168. <<<EOF
  1169. function DoReload()
  1170. {
  1171. $('#ReloadMovieBtn').button('disable');
  1172. try
  1173. {
  1174. var aExcluded = [];
  1175. $('input[name^=excluded]').each( function() {
  1176. if (!$(this).prop('checked'))
  1177. {
  1178. aExcluded.push($(this).val());
  1179. }
  1180. } );
  1181. $('#graph').simple_graph('option', {excluded_classes: aExcluded});
  1182. $('#graph').simple_graph('reload');
  1183. }
  1184. catch(err)
  1185. {
  1186. alert(err);
  1187. }
  1188. }
  1189. EOF
  1190. );
  1191. }
  1192. }