displayablegraph.class.inc.php 51 KB

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